Base Site Upload
This commit is contained in:
1 parent
414e123a85
commit
96a4aef921
7 files changed
+21503
No files matched your search
+177
@@ -0,0 +1,177 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link href="style.css" rel="stylesheet" type="text/css" media="all">
|
||||||
|
<title>Derail Valley Community Map</title>
|
||||||
|
<script type="module" src="index.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="mapContainer">
|
||||||
|
<svg id="map" xmlns="http://www.w3.org/2000/svg" width="1000" height="1000" viewBox="0 0 10000 10000">
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--grade_flat: #ffff;
|
||||||
|
--grade_0: #fdffde;
|
||||||
|
--grade_1: #f8ff99;
|
||||||
|
--grade_2: #edfc1b;
|
||||||
|
--grade_3: #fcb520;
|
||||||
|
--grade_4: #f07837;
|
||||||
|
--grade_5: #e66441;
|
||||||
|
--grade_6: #ce3f57;
|
||||||
|
--grade_7: #c33577;
|
||||||
|
--grade_8: #a3008d;
|
||||||
|
--grade_9: #690097;
|
||||||
|
}
|
||||||
|
.sign, .rail, .signText{
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.signText{
|
||||||
|
fill:#000;
|
||||||
|
font: 60px Arial;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.signText.gradeSign{
|
||||||
|
font-size: 30px;
|
||||||
|
stroke:#fff;
|
||||||
|
stroke-width: 6px;
|
||||||
|
paint-order: stroke fill;
|
||||||
|
}
|
||||||
|
.rail{
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
.rail.grade_flat{
|
||||||
|
stroke: var(--grade_flat);
|
||||||
|
}
|
||||||
|
.rail.grade_0{
|
||||||
|
stroke: var(--grade_0);
|
||||||
|
}
|
||||||
|
.rail.grade_1{
|
||||||
|
stroke: var(--grade_1);
|
||||||
|
}
|
||||||
|
.rail.grade_2{
|
||||||
|
stroke: var(--grade_2);
|
||||||
|
}
|
||||||
|
.rail.grade_3{
|
||||||
|
stroke: var(--grade_3);
|
||||||
|
}
|
||||||
|
.rail.grade_4{
|
||||||
|
stroke: var(--grade_4);
|
||||||
|
}
|
||||||
|
.rail.grade_5{
|
||||||
|
stroke: var(--grade_5);
|
||||||
|
}
|
||||||
|
.rail.grade_6{
|
||||||
|
stroke: var(--grade_6);
|
||||||
|
}
|
||||||
|
.rail.grade_7{
|
||||||
|
stroke: var(--grade_7);
|
||||||
|
}
|
||||||
|
.rail.grade_8{
|
||||||
|
stroke: var(--grade_8);
|
||||||
|
}
|
||||||
|
.rail.grade_9{
|
||||||
|
stroke: var(--grade_9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradeSign.grade_0{
|
||||||
|
fill: var(--grade_0);
|
||||||
|
}
|
||||||
|
.gradeSign.grade_1{
|
||||||
|
fill: var(--grade_1);
|
||||||
|
}
|
||||||
|
.gradeSign.grade_2{
|
||||||
|
fill: var(--grade_2);
|
||||||
|
}
|
||||||
|
.gradeSign.grade_3{
|
||||||
|
fill: var(--grade_3);
|
||||||
|
}
|
||||||
|
.gradeSign.grade_4{
|
||||||
|
fill: var(--grade_4);
|
||||||
|
}
|
||||||
|
.gradeSign.grade_5{
|
||||||
|
fill: var(--grade_5);
|
||||||
|
}
|
||||||
|
.gradeSign.grade_6{
|
||||||
|
fill: var(--grade_6);
|
||||||
|
}
|
||||||
|
.gradeSign.grade_7{
|
||||||
|
fill: var(--grade_7);
|
||||||
|
}
|
||||||
|
.gradeSign.grade_8{
|
||||||
|
fill: var(--grade_8);
|
||||||
|
}
|
||||||
|
.gradeSign.grade_9{
|
||||||
|
fill: var(--grade_9);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style id="zoomStyle"></style>
|
||||||
|
<defs>
|
||||||
|
<g id="gradeArrow" transform="scale(10)" viewBox="0 0 10 10" refX="5" refY="5">
|
||||||
|
<path d="M 0 -5 L 3 4 L -3 4 z"/>
|
||||||
|
</g>
|
||||||
|
<g id="speedSign" transform="scale(15)" viewBox="0 0 10 10" refX="5" refY="5" style="fill:#fff; stroke:#e73819">
|
||||||
|
<circle cx="0" cy="0" r="3"/>
|
||||||
|
</g>
|
||||||
|
<g id="speedSignWarning" transform="scale(15)" viewBox="0 0 10 10" refX="5" refY="5" style="fill:#702012; stroke:#e73819">
|
||||||
|
<circle cx="0" cy="0" r="3"/>
|
||||||
|
</g>
|
||||||
|
<g id="speedSign_1" transform="scale(1)" viewBox="0 0 10 10" refX="5" refY="5">
|
||||||
|
<use href="#speedSignWarning"></use>
|
||||||
|
<text class="signText" style="fill:#fff;transform:translate(-15px, 20px)">1</text>
|
||||||
|
</g>
|
||||||
|
<g id="speedSign_2" transform="scale(1)" viewBox="0 0 10 10" refX="5" refY="5">
|
||||||
|
<use href="#speedSignWarning"></use>
|
||||||
|
<text class="signText" style="fill:#fff;transform:translate(-15px, 20px)">2</text>
|
||||||
|
</g>
|
||||||
|
<g id="speedSign_3" transform="scale(1)" viewBox="0 0 10 10" refX="5" refY="5">
|
||||||
|
<use href="#speedSignWarning"></use>
|
||||||
|
<text class="signText" style="fill:#fff;transform:translate(-15px, 20px)">3</text>
|
||||||
|
</g>
|
||||||
|
<g id="speedSign_4" transform="scale(1)" viewBox="0 0 10 10" refX="5" refY="5">
|
||||||
|
<use href="#speedSign"></use>
|
||||||
|
<text class="signText" style="transform:translate(-15px, 20px)">4</text>
|
||||||
|
</g>
|
||||||
|
<g id="speedSign_5" transform="scale(1)" viewBox="0 0 10 10" refX="5" refY="5">
|
||||||
|
<use href="#speedSign"></use>
|
||||||
|
<text class="signText" style="transform:translate(-16px, 20px)">5</text>
|
||||||
|
</g>
|
||||||
|
<g id="speedSign_6" transform="scale(1)" viewBox="0 0 10 10" refX="5" refY="5">
|
||||||
|
<use href="#speedSign"></use>
|
||||||
|
<text class="signText" style="transform:translate(-15px, 20px)">6</text>
|
||||||
|
</g>
|
||||||
|
<g id="speedSign_7" transform="scale(1)" viewBox="0 0 10 10" refX="5" refY="5">
|
||||||
|
<use href="#speedSign"></use>
|
||||||
|
<text class="signText" style="transform:translate(-15px, 20px)">7</text>
|
||||||
|
</g>
|
||||||
|
<g id="speedSign_8" transform="scale(1)" viewBox="0 0 10 10" refX="5" refY="5">
|
||||||
|
<use href="#speedSign"></use>
|
||||||
|
<text class="signText" style="transform:translate(-16px, 20px)">8</text>
|
||||||
|
</g>
|
||||||
|
<g id="speedSign_9" transform="scale(1)" viewBox="0 0 10 10" refX="5" refY="5">
|
||||||
|
<use href="#speedSign"></use>
|
||||||
|
<text class="signText" style="transform:translate(-15px, 20px)">9</text>
|
||||||
|
</g>
|
||||||
|
<g id="speedSign_10" transform="scale(1)" viewBox="0 0 10 10" refX="5" refY="5">
|
||||||
|
<use href="#speedSign"></use>
|
||||||
|
<text class="signText" style="transform:translate(-35px, 20px)">10</text>
|
||||||
|
</g>
|
||||||
|
<g id="speedSign_12" transform="scale(1)" viewBox="0 0 10 10" refX="5" refY="5">
|
||||||
|
<use href="#speedSign"></use>
|
||||||
|
<text class="signText" style="transform:translate(-35px, 20px)">12</text>
|
||||||
|
</g>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="headerbar">
|
||||||
|
<div>
|
||||||
|
DV Community Map
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="footer">
|
||||||
|
Map v0.1 alpha
|
||||||
|
<br>
|
||||||
|
Updated for Game Build 99.3
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import * as Vector from './js/vectorlib.js';
|
||||||
|
import * as Bezier from './js/bezierlib.js';
|
||||||
|
import * as Utils from './js/utillib.js';
|
||||||
|
|
||||||
|
// CONFIG
|
||||||
|
const bezierGradeResolution = 80;
|
||||||
|
const bezierLengthResolution = 80;
|
||||||
|
const bezierCurvatureResolution = 80;
|
||||||
|
const padding = 500;
|
||||||
|
const zoomFactor = 1.2;
|
||||||
|
const trackWidth = {
|
||||||
|
base: 90,
|
||||||
|
min: 3,
|
||||||
|
max: 90,
|
||||||
|
}
|
||||||
|
const signSize = {
|
||||||
|
base: 4,
|
||||||
|
min: 0.05,
|
||||||
|
max: 2.4
|
||||||
|
}
|
||||||
|
const zoomThresholds = [
|
||||||
|
{zoom:1, distance:500},
|
||||||
|
{zoom:1.25, distance:200},
|
||||||
|
{zoom:1.5, distance:130},
|
||||||
|
{zoom:2.5, distance:80},
|
||||||
|
{zoom:5, distance:40},
|
||||||
|
{zoom:20, distance:20},
|
||||||
|
{zoom:60, distance:10},
|
||||||
|
];
|
||||||
|
const scaleCullThresholds = {
|
||||||
|
[1.0]: 500,
|
||||||
|
[1.25]: 200,
|
||||||
|
[1.5]: 130,
|
||||||
|
[2.5]: 80,
|
||||||
|
[5]: 40,
|
||||||
|
[20]: 20,
|
||||||
|
[60]: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// CODE
|
||||||
|
const map = document.getElementById('map');
|
||||||
|
const map_container = document.getElementById('mapContainer');
|
||||||
|
const svgns = map.getAttribute('xmlns');
|
||||||
|
|
||||||
|
const map_rails = document.createElementNS(svgns, 'g');
|
||||||
|
map_rails.setAttribute('id', 'map_rails');
|
||||||
|
map.appendChild(map_rails);
|
||||||
|
|
||||||
|
const map_markers = document.createElementNS(svgns, 'g');
|
||||||
|
map_markers.setAttribute('id', 'map_markers');
|
||||||
|
map.appendChild(map_markers);
|
||||||
|
|
||||||
|
let mapData = [];
|
||||||
|
let poiData = [];
|
||||||
|
let tracks = [];
|
||||||
|
let markers = [];
|
||||||
|
|
||||||
|
let previousMapScale = -1;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadTrackData('trackdata_dv.json');
|
||||||
|
mapScrollSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadTrackData(file){
|
||||||
|
mapData = await (await fetch(new Request(file))).json();
|
||||||
|
establishDimensions();
|
||||||
|
fillSvg();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handling for the map scrolling and zooming */
|
||||||
|
function mapScrollSetup(){
|
||||||
|
let mapNav = {x:0, y:0, scale:1};
|
||||||
|
let prevMouse = {x:0, y:0, scale:1};
|
||||||
|
let scrollDat = {ticking:false};
|
||||||
|
const mouseDownHandler = e => {
|
||||||
|
map_container.style.cursor = 'grabbing';
|
||||||
|
prevMouse.x = e.clientX;
|
||||||
|
prevMouse.y = e.clientY;
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', mouseMoveHandler);
|
||||||
|
document.addEventListener('mouseup', mouseUpHandler);
|
||||||
|
};
|
||||||
|
const mouseMoveHandler = e => {
|
||||||
|
mapNav.x += prevMouse.x-e.clientX;
|
||||||
|
mapNav.y += prevMouse.y-e.clientY;
|
||||||
|
|
||||||
|
updateMapview(mapNav);
|
||||||
|
|
||||||
|
prevMouse.x = e.clientX;
|
||||||
|
prevMouse.y = e.clientY;
|
||||||
|
};
|
||||||
|
const mouseUpHandler = () => {
|
||||||
|
map_container.style.removeProperty('cursor');
|
||||||
|
document.removeEventListener('mousemove', mouseMoveHandler);
|
||||||
|
document.removeEventListener('mouseup', mouseUpHandler);
|
||||||
|
};
|
||||||
|
const zoomHandler = e => {
|
||||||
|
if(!scrollDat.ticking){
|
||||||
|
scrollDat.ticking = true;
|
||||||
|
let rect = map_container.getBoundingClientRect();
|
||||||
|
let cursorLocal = {
|
||||||
|
x: -e.clientX - rect.left,
|
||||||
|
y: -e.clientY - rect.top
|
||||||
|
};
|
||||||
|
let scaleMult = 1;
|
||||||
|
if(e.deltaY < 0){
|
||||||
|
scaleMult = zoomFactor;
|
||||||
|
}else if(e.deltaY > 0){
|
||||||
|
scaleMult = 1/zoomFactor;
|
||||||
|
}
|
||||||
|
mapNav.x = cursorLocal.x - scaleMult * (cursorLocal.x-mapNav.x);
|
||||||
|
mapNav.y = cursorLocal.y - scaleMult * (cursorLocal.y-mapNav.y);
|
||||||
|
mapNav.scale *= scaleMult;
|
||||||
|
updateMapview(mapNav);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollDat.ticking = false;
|
||||||
|
}, 20);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
map_container.addEventListener('mousedown', mouseDownHandler);
|
||||||
|
map_container.addEventListener('wheel', zoomHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Readjust map view to focus on position */
|
||||||
|
function updateMapview(navData = {x:0, y:0, scale:1}){
|
||||||
|
map.style.transform = `translate(${-navData.x}px, ${-navData.y}px) scale(${navData.scale})`;
|
||||||
|
if(navData.scale !== previousMapScale){
|
||||||
|
let dirtyScale = previousMapScale < 0;
|
||||||
|
for(const cullThreshold in scaleCullThresholds){
|
||||||
|
if(dirtyScale || (previousMapScale < cullThreshold && navData.scale > cullThreshold) || (previousMapScale > cullThreshold && navData.scale < cullThreshold)){
|
||||||
|
dirtyScale = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map_rails.setAttribute('stroke-width', Utils.clamp(trackWidth.base/navData.scale, trackWidth.min, trackWidth.max));
|
||||||
|
if(dirtyScale){
|
||||||
|
let zoomThresholdStyles = '';
|
||||||
|
for(let i=0; i<zoomThresholds.length; i++){
|
||||||
|
zoomThresholdStyles += `\n.zoomThreshold_${i}{display:${navData.scale > zoomThresholds[i].zoom ? 'unset' : 'none'}}`;
|
||||||
|
}
|
||||||
|
map.getElementById('zoomStyle').innerHTML = `
|
||||||
|
.sign{
|
||||||
|
transform: scale(${Utils.clamp(signSize.base/navData.scale, signSize.min, signSize.max)});
|
||||||
|
}${zoomThresholdStyles}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
previousMapScale = navData.scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the bounding box of all the tracks, and set the map bounds accordingly. Also flips the Y axis.
|
||||||
|
* **Note:** Does NOT evaluate beziers, uses the control points only.
|
||||||
|
*/
|
||||||
|
function establishDimensions(){
|
||||||
|
let minX = Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let minAlt = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
let maxAlt = -Infinity;
|
||||||
|
for(const obj of mapData){
|
||||||
|
for(const point of obj.points){
|
||||||
|
minX = Math.min(minX, point[0].x, point[1].x, point[2].x);
|
||||||
|
minY = Math.min(minY, point[0].z, point[1].z, point[2].z);
|
||||||
|
minAlt = Math.min(minAlt, point[0].y, point[1].y, point[2].y);
|
||||||
|
maxX = Math.max(maxX, point[0].x, point[1].x, point[2].x);
|
||||||
|
maxY = Math.max(maxY, point[0].z, point[1].z, point[2].z);
|
||||||
|
maxAlt = Math.max(maxAlt, point[0].y, point[1].y, point[2].y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let width = maxX-minX;
|
||||||
|
let height = maxY-minY;
|
||||||
|
map.setAttribute('viewBox', `${minX-padding} ${minY-padding} ${width+2*padding} ${height+2*padding}`);
|
||||||
|
|
||||||
|
/*updateMapview({
|
||||||
|
x: map_container.width/2-map.getAttribute('width')/2,
|
||||||
|
y: map_container.height/2-map.getAttribute('height')/2,
|
||||||
|
scale: 1
|
||||||
|
});*/
|
||||||
|
updateMapview();
|
||||||
|
console.log(`(${minX}, ${minY}) to (${maxX}, ${maxY}). Altitude from ${minAlt} to ${maxAlt}`);
|
||||||
|
|
||||||
|
// Flip the z coordinate so the map doesn't display upside-down
|
||||||
|
for(const obj of mapData){
|
||||||
|
for(const point of obj.points){
|
||||||
|
point[0].z = minY+maxY-point[0].z;
|
||||||
|
point[1].z = minY+maxY-point[1].z;
|
||||||
|
point[2].z = minY+maxY-point[2].z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Populate the SVG data */
|
||||||
|
function fillSvg(){
|
||||||
|
for(const obj of mapData){
|
||||||
|
switch(obj.type){
|
||||||
|
case 'bezier':
|
||||||
|
drawTracks(obj);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sortTracks();
|
||||||
|
cleanMarkers();
|
||||||
|
drawMarkers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Insert individual rail paths, as well as creating signage data for each path*/
|
||||||
|
function drawTracks(bezierData){
|
||||||
|
const trackCol = Utils.rgba2hex(Math.random(),Math.random(),Math.random(),1.0);
|
||||||
|
for(let i=0; i+1<bezierData.points.length; i++){
|
||||||
|
const bezStart = bezierData.points[i][0];
|
||||||
|
const bezHandle1 = bezierData.points[i][2];
|
||||||
|
const bezHandle2 = bezierData.points[i+1][1];
|
||||||
|
const bezEnd = bezierData.points[i+1][0];
|
||||||
|
let newBezier = document.createElementNS(svgns, 'path');
|
||||||
|
let genPath = 'M ';
|
||||||
|
genPath += `${bezStart.x} ${bezStart.z} `;
|
||||||
|
genPath += `C ${bezHandle1.x} ${bezHandle1.z} ${bezHandle2.x} ${bezHandle2.z} ${bezEnd.x} ${bezEnd.z} `;
|
||||||
|
let grade = Bezier.estimateMaxGrade(bezStart, bezHandle1, bezHandle2, bezEnd, bezierGradeResolution);
|
||||||
|
let gradeClass = Utils.gradeToClass(grade);
|
||||||
|
let length = Bezier.estimateLength(bezStart, bezHandle1, bezHandle2, bezEnd, bezierLengthResolution);
|
||||||
|
let curvature = Bezier.estimateMaxCurvature(bezStart, bezHandle1, bezHandle2, bezEnd, bezierCurvatureResolution);
|
||||||
|
newBezier.setAttribute('d', genPath);
|
||||||
|
|
||||||
|
newBezier.classList.add('rail');
|
||||||
|
newBezier.classList.add(gradeClass ?? 'grade_flat');
|
||||||
|
|
||||||
|
//newBezier.setAttribute('stroke', Utils.rgba2hex(grade*50*0,(bezStart.y-113)/(252-113),length*0.01*0,1.0));
|
||||||
|
//newBezier.setAttribute('stroke', Utils.rgba2hex((1/curvature) * 100,0,0,1.0));
|
||||||
|
//newBezier.setAttribute('stroke', trackCol);
|
||||||
|
//newBezier.setAttribute('data-grade', grade);
|
||||||
|
//newBezier.setAttribute('data-length', length);
|
||||||
|
//newBezier.setAttribute('data-maxradius', 1/curvature);
|
||||||
|
tracks.push([(bezStart.y+bezEnd.y)*0.5, newBezier]);
|
||||||
|
|
||||||
|
// Curve tooltip
|
||||||
|
let title = document.createElementNS(svgns, 'title');
|
||||||
|
title.innerHTML = [
|
||||||
|
`"${bezierData.name}"`,
|
||||||
|
`Max Grade: ${Math.round(grade*1000)/10}%`,
|
||||||
|
`Min Radius: ${Math.round(1/curvature)} meters`,
|
||||||
|
`Top Speed: ${Utils.radiusToSpeed(1/curvature)} km/h`,
|
||||||
|
`Length: ${Math.round(length*10)/10} meters`,
|
||||||
|
`Altitude: ${Math.round((bezStart.y+bezEnd.y)*0.5)} meters`,
|
||||||
|
].join('\n');
|
||||||
|
newBezier.appendChild(title);
|
||||||
|
|
||||||
|
// Add Signage
|
||||||
|
if(grade > 0.001){
|
||||||
|
markers.push({
|
||||||
|
type: 'grade',
|
||||||
|
value: grade,
|
||||||
|
class: gradeClass,
|
||||||
|
position: Bezier.evaluatePoint(bezStart, bezHandle1, bezHandle2, bezEnd, 0.5),
|
||||||
|
tangent: grade > 0
|
||||||
|
?
|
||||||
|
Bezier.evaluateVelocity(bezStart, bezHandle1, bezHandle2, bezEnd, 0.5)
|
||||||
|
:
|
||||||
|
Bezier.evaluateVelocity(bezEnd, bezHandle2, bezHandle1,bezStart, 0.5)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
markers.push({
|
||||||
|
type: 'speed',
|
||||||
|
value: Utils.radiusToSpeed(1/curvature),
|
||||||
|
position: Bezier.evaluatePoint(bezStart, bezHandle1, bezHandle2, bezEnd, 0.5),
|
||||||
|
tangent: Bezier.evaluateVelocity(bezStart, bezHandle1, bezHandle2, bezEnd, 0.5)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sort the tracks and put them into the svg */
|
||||||
|
function sortTracks(){
|
||||||
|
tracks.sort((a,b) => {
|
||||||
|
return a[0]-b[0];
|
||||||
|
});
|
||||||
|
for(const track of tracks){
|
||||||
|
map_rails.appendChild(track[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hide extraneous signage, reducing clutter. */
|
||||||
|
function cleanMarkers(){
|
||||||
|
let roughDist = 0;
|
||||||
|
for(let i=0; i<markers.length; i++){
|
||||||
|
markers[i].crowdingScore = 0;
|
||||||
|
markers[i].nearestDist = 100000;
|
||||||
|
for(let j=0; j<markers.length; j++){
|
||||||
|
if(i != j && markers[i].type == markers[j].type){
|
||||||
|
roughDist = Vector.roughDistance(markers[i].position, markers[j].position);
|
||||||
|
markers[i].crowdingScore += 1/roughDist;
|
||||||
|
if(roughDist < markers[i].nearestDist){
|
||||||
|
markers[i].nearestDist = roughDist;
|
||||||
|
markers[i].nearest = markers[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//let randomZoomThreshold = Utils.randomRange(1, 5);
|
||||||
|
//if(randomZoomThreshold < 4) markers[i].skip = true;
|
||||||
|
/*
|
||||||
|
for(let j=0; j<markers.length; j++){
|
||||||
|
if(i != j && !markers[j].skip && markers[i].type == markers[j].type){
|
||||||
|
|
||||||
|
if(Vector.isWithinAARange(markers[i].position, markers[j].position, 120)){
|
||||||
|
if(markers[i].type == 'grade'){
|
||||||
|
if(Math.abs(markers[j].value) > Math.abs(markers[i].value)) markers[i].skip = true;
|
||||||
|
else markers[j].skip = true;
|
||||||
|
}else if(markers[i].type == 'speed'){
|
||||||
|
if(markers[j].value > markers[i].value) markers[j].skip = true;
|
||||||
|
else markers[i].skip = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create the signage */
|
||||||
|
function drawMarkers(){
|
||||||
|
const gradeSigns = document.createElementNS(svgns, 'g');
|
||||||
|
gradeSigns.setAttribute('id', 'grade_Signs');
|
||||||
|
map_markers.appendChild(gradeSigns);
|
||||||
|
const speedSigns = document.createElementNS(svgns, 'g');
|
||||||
|
speedSigns.setAttribute('id', 'speed_signs');
|
||||||
|
map_markers.appendChild(speedSigns);
|
||||||
|
for(const markerData of markers){
|
||||||
|
//if(markerData.skip) continue;
|
||||||
|
let marker = document.createElementNS(svgns, 'g');
|
||||||
|
let markerImg = document.createElementNS(svgns, 'use');
|
||||||
|
let title = document.createElementNS(svgns, 'title');
|
||||||
|
let text = document.createElementNS(svgns, 'text');
|
||||||
|
marker.appendChild(markerImg);
|
||||||
|
marker.appendChild(text);
|
||||||
|
marker.appendChild(title);
|
||||||
|
marker.setAttribute('transform', `translate(${markerData.position.x} ${markerData.position.z})`);
|
||||||
|
// Grade signs
|
||||||
|
if(markerData.type == 'grade'){
|
||||||
|
markerImg.setAttribute('href', '#gradeArrow');
|
||||||
|
markerImg.classList.add('sign', 'gradeSign');
|
||||||
|
text.classList.add('signText', 'gradeSign');
|
||||||
|
if(markerData.class) markerImg.classList.add(markerData.class);
|
||||||
|
const tanLen = Math.sqrt(markerData.tangent.x*markerData.tangent.x + markerData.tangent.z*markerData.tangent.z) * Math.sign(markerData.tangent.y);
|
||||||
|
const rot = Math.atan2(markerData.tangent.x/tanLen, -markerData.tangent.z/tanLen) * (180/Math.PI);
|
||||||
|
marker.setAttribute('transform', `translate(${markerData.position.x} ${markerData.position.z}) rotate(${rot})`);
|
||||||
|
text.setAttribute('transform', `rotate(${-rot})`);
|
||||||
|
title.innerHTML = `${Math.round(markerData.value*1000)/10}%`;
|
||||||
|
text.innerHTML = `${Math.round(markerData.value*1000)/10}`;
|
||||||
|
gradeSigns.appendChild(marker);
|
||||||
|
// Speed Signs
|
||||||
|
}else if(markerData.type == 'speed'){
|
||||||
|
markerImg.setAttribute('href', `#speedSign_${Math.round(markerData.value/10)}`);
|
||||||
|
markerImg.classList.add('sign', 'speedSign');
|
||||||
|
text.classList.add('signText', 'speedSign');
|
||||||
|
title.innerHTML = `${Math.round(markerData.value)} km/h`;
|
||||||
|
/*let offset = Vector.normalize({x:markerData.tangent.z, y:0, z:-markerData.tangent.x});
|
||||||
|
marker.setAttribute('transform', `translate(${markerData.position.x + offset.x*100} ${markerData.position.z + offset.z*50})`);*/
|
||||||
|
marker.setAttribute('transform', `translate(${markerData.position.x} ${markerData.position.z})`);
|
||||||
|
speedSigns.appendChild(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let i=zoomThresholds.length-1; i>= 0; i--){
|
||||||
|
if(markerData.nearestDist < zoomThresholds[i].distance || i==0){
|
||||||
|
title.innerHTML += `\n${markerData.nearestDist}`;
|
||||||
|
marker.classList.add(`zoomThreshold_${i}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
'use strict';
|
||||||
|
import * as Vector from './vectorlib.js';
|
||||||
|
/** A 3D vector
|
||||||
|
* @typedef {Object} Vector
|
||||||
|
* @property {number} x
|
||||||
|
* @property {number} y
|
||||||
|
* @property {number} z
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Estimate the maximum grade of the bezier.
|
||||||
|
* @param {Vector} p1 - Start
|
||||||
|
* @param {Vector} h1 - Handle 1
|
||||||
|
* @param {Vector} h2 - Handle 2
|
||||||
|
* @param {Vector} p2 - End
|
||||||
|
* @param {number} iterationCount - How many steps along the bezier to check
|
||||||
|
* @returns {number} Grade as a decimal
|
||||||
|
*/
|
||||||
|
export function estimateMaxGrade(p1, h1, h2, p2, iterationCount = 50){
|
||||||
|
let grade = 0;
|
||||||
|
let totalGrade = 0;
|
||||||
|
for(let i=0; i<=iterationCount; i++){
|
||||||
|
let tan = evaluateVelocity(p1, h1, h2, p2, i/iterationCount);
|
||||||
|
let newGrade = Math.abs(tan.y)/Math.sqrt(tan.x*tan.x + tan.z*tan.z);
|
||||||
|
grade = Math.max(grade, newGrade);
|
||||||
|
totalGrade += newGrade;
|
||||||
|
}
|
||||||
|
return grade;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Estimate the length of the bezier.
|
||||||
|
* @param {Vector} p1 - Start
|
||||||
|
* @param {Vector} h1 - Handle 1
|
||||||
|
* @param {Vector} h2 - Handle 2
|
||||||
|
* @param {Vector} p2 - End
|
||||||
|
* @param {number} iterationCount - How many steps along the bezier to check
|
||||||
|
* @returns {number} Length
|
||||||
|
*/
|
||||||
|
export function estimateLength(p1, h1, h2, p2, iterationCount = 50){
|
||||||
|
let length = 0;
|
||||||
|
let prevPos = p1;
|
||||||
|
let curPos = {};
|
||||||
|
for(let i=1; i<=iterationCount; i++){
|
||||||
|
curPos = evaluatePoint(p1, h1, h2, p2, i/iterationCount);
|
||||||
|
length += Vector.distance(prevPos, curPos);
|
||||||
|
prevPos = curPos;
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Estimate the maximum 3D curvature of the bezier.
|
||||||
|
* @param {Vector} p1 - Start
|
||||||
|
* @param {Vector} h1 - Handle 1
|
||||||
|
* @param {Vector} h2 - Handle 2
|
||||||
|
* @param {Vector} p2 - End
|
||||||
|
* @param {number} iterationCount - How many steps along the bezier to check
|
||||||
|
* @returns {number} Curvature, take `1/curvature` to get radius.
|
||||||
|
*/
|
||||||
|
export function estimateMaxCurvature(p1, h1, h2, p2, iterationCount = 50){
|
||||||
|
let curvature = 0;
|
||||||
|
let t = 0;
|
||||||
|
let vel = {};
|
||||||
|
for(let i=0; i<iterationCount; i++){
|
||||||
|
t = i/iterationCount;
|
||||||
|
vel = evaluateVelocity(p1, h1, h2, p2, t);
|
||||||
|
curvature = Math.max(curvature,
|
||||||
|
Vector.length(Vector.cross(vel, evaluateAcceleration(p1, h1, h2, p2, t)))
|
||||||
|
/Math.pow(Vector.length(vel),3));
|
||||||
|
}
|
||||||
|
return curvature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Evaluate the bezier at `t`, getting position.
|
||||||
|
* @param {Vector} p1 - Start
|
||||||
|
* @param {Vector} h1 - Handle 1
|
||||||
|
* @param {Vector} h2 - Handle 2
|
||||||
|
* @param {Vector} p2 - End
|
||||||
|
* @param {number} t - 0 to 1 of how far into the curve to check.
|
||||||
|
* @returns {Vector}
|
||||||
|
*/
|
||||||
|
export function evaluatePoint(p1, h1, h2, p2, t){
|
||||||
|
let result = {};
|
||||||
|
let t3 = t*t*t;
|
||||||
|
let t2 = t*t;
|
||||||
|
let p1v = -t3 + 3*t2 - 3*t + 1;
|
||||||
|
let h1v = 3*t3 - 6*t2 + 3*t;
|
||||||
|
let h2v = -3*t3 + 3*t2;
|
||||||
|
for(const axis of ['x', 'y', 'z']){
|
||||||
|
result[axis] = p1[axis]*p1v + h1[axis]*h1v + h2[axis]*h2v + p2[axis]*t3;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Evaluate the bezier's first derivative at `t`, getting velocity.
|
||||||
|
* @param {Vector} p1 - Start
|
||||||
|
* @param {Vector} h1 - Handle 1
|
||||||
|
* @param {Vector} h2 - Handle 2
|
||||||
|
* @param {Vector} p2 - End
|
||||||
|
* @param {number} t - 0 to 1 of how far into the curve to check.
|
||||||
|
* @returns {Vector}
|
||||||
|
*/
|
||||||
|
export function evaluateVelocity(p1, h1, h2, p2, t){
|
||||||
|
let result = {};
|
||||||
|
let t2 = t*t;
|
||||||
|
let p1v = -3*t2 + 6*t -3;
|
||||||
|
let h1v = 9*t2 - 12*t +3;
|
||||||
|
let h2v = -9*t2 + 6*t;
|
||||||
|
let p2v = 3*t2;
|
||||||
|
for(const axis of ['x', 'y', 'z']){
|
||||||
|
result[axis] = p1[axis]*p1v + h1[axis]*h1v + h2[axis]*h2v + p2[axis]*p2v;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Evaluate the bezier's second derivative at `t`, getting acceleration.
|
||||||
|
* @param {Vector} p1 - Start
|
||||||
|
* @param {Vector} h1 - Handle 1
|
||||||
|
* @param {Vector} h2 - Handle 2
|
||||||
|
* @param {Vector} p2 - End
|
||||||
|
* @param {number} t - 0 to 1 of how far into the curve to check.
|
||||||
|
* @returns {Vector}
|
||||||
|
*/
|
||||||
|
export function evaluateAcceleration(p1, h1, h2, p2, t){
|
||||||
|
let result = {};
|
||||||
|
let p1v = -6*t + 6;
|
||||||
|
let h1v = 18*t -12;
|
||||||
|
let h2v = -18*t + 6;
|
||||||
|
let p2v = 6*t;
|
||||||
|
for(const axis of ['x', 'y', 'z']){
|
||||||
|
result[axis] = p1[axis]*p1v + h1[axis]*h1v + h2[axis]*h2v + p2[axis]*p2v;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const gradeIncrements = [
|
||||||
|
0.0498,
|
||||||
|
0.04,
|
||||||
|
0.035,
|
||||||
|
0.03,
|
||||||
|
0.025,
|
||||||
|
0.02,
|
||||||
|
0.015,
|
||||||
|
0.01,
|
||||||
|
0.005,
|
||||||
|
0.001
|
||||||
|
]
|
||||||
|
|
||||||
|
export function randomRange(min=0.0, max=1.0){
|
||||||
|
return Math.random()*(max-min) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rgba2hex(r,g,b,a=1.0){
|
||||||
|
r = Math.min(Math.max(r,0),1);
|
||||||
|
g = Math.min(Math.max(g,0),1);
|
||||||
|
b = Math.min(Math.max(b,0),1);
|
||||||
|
a = Math.min(Math.max(a,0),1);
|
||||||
|
return `#${
|
||||||
|
Math.round(r*255).toString(16).padStart(2, '0').substring(0,2)
|
||||||
|
}${
|
||||||
|
Math.round(g*255).toString(16).padStart(2, '0').substring(0,2)
|
||||||
|
}${
|
||||||
|
Math.round(b*255).toString(16).padStart(2, '0').substring(0,2)
|
||||||
|
}${
|
||||||
|
Math.round(a*255).toString(16).padStart(2, '0').substring(0,2)
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gradeToClass(grade){
|
||||||
|
let result = '';
|
||||||
|
for(let j=0; j<gradeIncrements.length; j++){
|
||||||
|
if(grade > gradeIncrements[j]){
|
||||||
|
result = `grade_${gradeIncrements.length-(j+1)}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(result) return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function radiusToSpeed(radius){
|
||||||
|
if(radius < 50) return 10;
|
||||||
|
else if(radius < 70) return 20;
|
||||||
|
else if(radius < 95) return 30;
|
||||||
|
else if(radius < 130) return 40;
|
||||||
|
else if(radius < 170) return 50;
|
||||||
|
else if(radius < 230) return 60;
|
||||||
|
else if(radius < 360) return 70;
|
||||||
|
else if(radius < 700) return 80;
|
||||||
|
else if(radius < 900) return 90;
|
||||||
|
else if(radius < 1200) return 100;
|
||||||
|
return 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clamp(v, min = 0, max = 1){
|
||||||
|
return Math.max(Math.min(v, max), min);
|
||||||
|
}
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** A 3D vector
|
||||||
|
* @typedef {Object} Vector
|
||||||
|
* @property {number} x
|
||||||
|
* @property {number} y
|
||||||
|
* @property {number} z
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Get Euclidean Distance between v1 and v2.
|
||||||
|
* @param {Vector} v1
|
||||||
|
* @param {Vector} v2
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function distance(v1, v2){
|
||||||
|
return Math.sqrt(Math.pow(v1.x-v2.x, 2)+Math.pow(v1.y-v2.y, 2)+Math.pow(v1.z-v2.z, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get Manhattan Distance between v1 and v2.
|
||||||
|
* @param {Vector} v1
|
||||||
|
* @param {Vector} v2
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function manhattanDistance(v1, v2){
|
||||||
|
return Math.abs(v1.x-v2.x) + Math.abs(v1.y-v2.y) + Math.abs(v1.z-v2.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get Distance between v1 and v2 with decreasing accuracy over distance.
|
||||||
|
* @param {Vector} v1
|
||||||
|
* @param {Vector} v2
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function roughDistance(v1, v2, farThreshold = 1600){
|
||||||
|
const halfFar = farThreshold/2;
|
||||||
|
let subvec = absSubtract(v1, v2);
|
||||||
|
if(subvec.x > farThreshold) return subvec.x;
|
||||||
|
else if(subvec.y > farThreshold) return subvec.y;
|
||||||
|
else if(subvec.z > farThreshold) return subvec.z;
|
||||||
|
else if(subvec.x > halfFar || subvec.y > halfFar || subvec.z > halfFar) return subvec.x+subvec.y+subvec.z;
|
||||||
|
return distance(v1, v2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get length of a vector.
|
||||||
|
* @param {Vector} v1
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function length(v1){
|
||||||
|
return Math.sqrt(v1.x*v1.x + v1.y*v1.y + v1.z*v1.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return the same vector with a length of 1.
|
||||||
|
* @param {Vector} v1
|
||||||
|
* @returns {Vector}
|
||||||
|
*/
|
||||||
|
export function normalize(v1){
|
||||||
|
const len = length(v1);
|
||||||
|
if(len === 0) return v1;
|
||||||
|
return {x:v1.x/len, y:v1.y/len, z:v1.z/len};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the cross-product of v1 and v2
|
||||||
|
* @param {Vector} v1
|
||||||
|
* @param {Vector} v2
|
||||||
|
* @param {boolean} normalize - normalize the input vectors
|
||||||
|
* @returns {Vector}
|
||||||
|
*/
|
||||||
|
export function cross(v1, v2, normalize = false){
|
||||||
|
if(normalize){
|
||||||
|
v1 = normalize(v1);
|
||||||
|
v2 = normalize(v2);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: v1.y*v2.z - v1.z*v2.y,
|
||||||
|
y: v1.z*v2.x - v1.x*v2.z,
|
||||||
|
z: v1.x*v2.y - v1.y*v2.x
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the dot-product of v1 and v2
|
||||||
|
* @param {Vector} v1
|
||||||
|
* @param {Vector} v2
|
||||||
|
* @param {boolean} normalize - normalize the input vectors
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function dot(v1, v2, normalize = false){
|
||||||
|
let len = 1;
|
||||||
|
if(normalize) len = 1/(length(v1) * length(v2));
|
||||||
|
return v1.x*v2.x*len + v1.y*v2.y*len + v1.z*v2.z*len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Are v1 and v2 within range of each other.
|
||||||
|
* @param {Vector} v1
|
||||||
|
* @param {Vector} v2
|
||||||
|
* @param {number} range
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isWithinAARange(v1, v2, range){
|
||||||
|
return !(Math.abs(v2.x-v1.x) > range || Math.abs(v2.y-v1.y) > range || Math.abs(v2.z-v1.z) > range);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subtract(v1, v2){
|
||||||
|
return {x:v1.x-v2.x, y:v1.y-v2.y, z:v1.y-v2.y};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function absSubtract(v1, v2){
|
||||||
|
return {x:Math.abs(v1.x-v2.x), y:Math.abs(v1.y-v2.y), z:Math.abs(v1.y-v2.y)};
|
||||||
|
}
|
||||||
|
|
||||||
|
class vector {
|
||||||
|
constructor(x,y,z){
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wdth,wght@0,62.5..100,100..900;1,62.5..100,100..900&family=Protest+Strike&display=swap');
|
||||||
|
|
||||||
|
html, body, #map, #mapContainer, h1, h2, h3, h4, h5{
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body{
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
font-family: "Noto Sans";
|
||||||
|
font-variation-settings: "wght" 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
#headerbar{
|
||||||
|
position:fixed;
|
||||||
|
width: fit-content;
|
||||||
|
font-size: 30px;
|
||||||
|
background: linear-gradient(120deg, #3d5779, #121522);
|
||||||
|
border: 2px solid #b3b3b396;
|
||||||
|
border-left: 0px;
|
||||||
|
color: #e79830;
|
||||||
|
font-variation-settings: "wght" 500;
|
||||||
|
padding: 10px 30px 10px 30px;
|
||||||
|
margin: 10px 0px;
|
||||||
|
border-radius: 0px 10px 10px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer{
|
||||||
|
position:fixed;
|
||||||
|
width: fit-content;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 5px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #e79830;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map{
|
||||||
|
transition: transform 100ms ease-out;
|
||||||
|
transform-origin: 0% 0%;
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mapContainer{
|
||||||
|
position:fixed;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
background: linear-gradient(#444, #333);
|
||||||
|
}
|
||||||
+20589
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user