Canvas rewrite
-Full rewrite to use Canvas instead of SVG -Restructured backend
This commit is contained in:
1 parent
856320c976
commit
45d9e8d577
13 files changed
+1612
-1056
No files matched your search
@@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
import * as Vector from './vectorlib.js';
|
||||
import * as Utils from './utillib.js';
|
||||
/** A 3D vector
|
||||
* @typedef {Object} Vector
|
||||
* @property {number} x
|
||||
@@ -138,4 +139,53 @@ export function evaluateAcceleration(p1, h1, h2, p2, t){
|
||||
result[axis] = p1[axis]*p1v + h1[axis]*h1v + h2[axis]*h2v + p2[axis]*p2v;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Get the min and max bounds of a bezier.
|
||||
* @param {Vector} p1
|
||||
* @param {Vector} h1
|
||||
* @param {Vector} h2
|
||||
* @param {Vector} p2
|
||||
*/
|
||||
export function getBounds(p1, h1, h2, p2){
|
||||
// TODO: replace this with a not brute-forced version.
|
||||
let pos;
|
||||
let min = {x:Infinity, y:Infinity, z:Infinity};
|
||||
let max = {x:-Infinity, y:-Infinity, z:-Infinity};
|
||||
for(let i=0; i<=40; i++){
|
||||
pos = evaluatePoint(p1, h1, h2, p2, i/40);
|
||||
min.x = Math.min(pos.x, min.x);
|
||||
max.x = Math.max(pos.x, max.x);
|
||||
min.y = Math.min(pos.y, min.y);
|
||||
max.y = Math.max(pos.y, max.y);
|
||||
min.z = Math.min(pos.z, min.z);
|
||||
max.z = Math.max(pos.z, max.z);
|
||||
}
|
||||
return {min:min, max:max};
|
||||
}
|
||||
|
||||
/** Get the closest point on the curve to `testPoint` in 2D space, as well as the distance.
|
||||
* (This means using the (x,z) coordinates of the bezier as (x,y))
|
||||
* @param {Vector} p1
|
||||
* @param {Vector} h1
|
||||
* @param {Vector} h2
|
||||
* @param {Vector} p2
|
||||
* @param {*} testPoint
|
||||
*/
|
||||
export function getNearestPoint2D(p1, h1, h2, p2, testPoint, iterationCount = 200){
|
||||
// TODO: replace this with a more accurate version
|
||||
let minDistance = Infinity;
|
||||
let bezEval;
|
||||
let evalDist;
|
||||
let closestPoint = {x:0, y:0};
|
||||
for(let i=0; i<=iterationCount; i++){
|
||||
bezEval = evaluatePoint(p1, h1, h2, p2, i/iterationCount);
|
||||
evalDist = Math.pow(testPoint.x-bezEval.x, 2) + Math.pow(testPoint.y-bezEval.z, 2);
|
||||
if(evalDist < minDistance){
|
||||
minDistance = evalDist;
|
||||
closestPoint.x = bezEval.x;
|
||||
closestPoint.y = bezEval.y;
|
||||
}
|
||||
}
|
||||
return {point:closestPoint, distance:Math.sqrt(minDistance)};
|
||||
}
|
||||
+573
@@ -0,0 +1,573 @@
|
||||
'use strict';
|
||||
|
||||
import * as Vector from './vectorlib.js';
|
||||
import * as Bezier from './bezierlib.js';
|
||||
import * as Utils from './utillib.js';
|
||||
import {Color} from './colorlib.js';
|
||||
import * as Config from './../config.js';
|
||||
|
||||
export let railTracks = [];
|
||||
export let markers = [];
|
||||
|
||||
let visibleMarkers = [];
|
||||
|
||||
let uniqueTrackNames = {};
|
||||
|
||||
export let testPoint = {x:0,y:0};
|
||||
|
||||
export let matrix = {
|
||||
initialize: () => {
|
||||
matrix.minX = Infinity;
|
||||
matrix.minY = Infinity;
|
||||
matrix.minAlt = Infinity;
|
||||
matrix.maxX = -Infinity;
|
||||
matrix.maxY = -Infinity;
|
||||
matrix.maxAlt = -Infinity;
|
||||
|
||||
for(const railTrack of railTracks){
|
||||
for(const curve of railTrack.curves){
|
||||
matrix.minX = Math.min(matrix.minX, curve.start.x, curve.h1.x, curve.h2.x, curve.end.x);
|
||||
matrix.minY = Math.min(matrix.minY, curve.start.z, curve.h1.z, curve.h2.z, curve.end.z);
|
||||
matrix.minAlt = Math.min(matrix.minAlt, curve.start.y, curve.h1.y, curve.h2.y, curve.end.y);
|
||||
matrix.maxX = Math.max(matrix.maxX, curve.start.x, curve.h1.x, curve.h2.x, curve.end.x);
|
||||
matrix.maxY = Math.max(matrix.maxY, curve.start.z, curve.h1.z, curve.h2.z, curve.end.z);
|
||||
matrix.maxAlt = Math.max(matrix.maxAlt, curve.start.y, curve.h1.y, curve.h2.y, curve.end.y);
|
||||
}
|
||||
}
|
||||
matrix.width = matrix.maxX-matrix.minX;
|
||||
matrix.height = matrix.maxY-matrix.minY;
|
||||
|
||||
console.log(`(${matrix.minX}, ${matrix.minY}) to (${matrix.maxX}, ${matrix.maxY}). Altitude from ${matrix.minAlt} to ${matrix.maxAlt}`);
|
||||
}
|
||||
};
|
||||
|
||||
export let view = {
|
||||
initialize: () => {
|
||||
const mapContainer = document.getElementById('mapContainer');
|
||||
let isLandscape = mapContainer.clientWidth > mapContainer.clientHeight;
|
||||
view.scale = Math.min(mapContainer.clientWidth/(matrix.width + Config.defaultMapPadding*2), mapContainer.clientHeight/(matrix.height + Config.defaultMapPadding*2));
|
||||
if(isLandscape){
|
||||
view.x = mapContainer.clientWidth*0.6 - (matrix.width * 0.5 + matrix.minX)*view.scale;
|
||||
view.y = (matrix.minY - Config.defaultMapPadding) * view.scale;
|
||||
}else{
|
||||
view.x = -(matrix.minX - Config.defaultMapPadding) * view.scale;
|
||||
view.y = -(mapContainer.clientHeight*0.5 - (matrix.height * 0.5 + matrix.minY)*view.scale);
|
||||
}
|
||||
view.dirty = false;
|
||||
|
||||
view.convertX = x => {
|
||||
return x * view.scale + view.x;
|
||||
}
|
||||
view.convertY = y => {
|
||||
return (matrix.minY+matrix.maxY-y) * view.scale - view.y;
|
||||
}
|
||||
|
||||
view.unconvertX = x => {
|
||||
return (x - view.x) / view.scale;
|
||||
}
|
||||
view.unconvertY = y => {
|
||||
return -((y + view.y) / view.scale - (matrix.minY + matrix.maxY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export let layers = {};
|
||||
|
||||
export function reset(){
|
||||
railTracks = [];
|
||||
markers = [];
|
||||
}
|
||||
|
||||
export function sortTracks(){
|
||||
railTracks.sort((a,b) => {
|
||||
if(a.curves && b.curves)
|
||||
return a.curves[0].start.y-b.curves[b.curves.length-1].end.y;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function sortMarkers(){
|
||||
markers.sort((a,b) => {
|
||||
if(a.type == b.type){
|
||||
switch(a.type){
|
||||
case 'speed':
|
||||
return b.value-a.value;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if(a.type == 'station') return 1;
|
||||
if(b.type == 'station') return -1;
|
||||
if(a.type == 'speed') return 1;
|
||||
if(b.type == 'speed') return -1;
|
||||
if(a.type == 'junction') return 1;
|
||||
if(b.type == 'junction') return -1;
|
||||
if(a.type == 'service') return 1;
|
||||
if(b.type == 'service') return -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function filterToTrack(trackName, trackNum = 0){
|
||||
if(typeof(trackName) == 'string'){
|
||||
for(let railTrack of railTracks){
|
||||
if(railTrack.name == trackName && railTrack.number == trackNum){
|
||||
railTracks = [railTrack];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}else if(typeof(trackName) == 'object'){
|
||||
let newRailTracks = [];
|
||||
for(let railTrack of railTracks){
|
||||
for(let filter of trackName){
|
||||
if(railTrack.name == filter.name && railTrack.number == (filter.number ?? 0)){
|
||||
newRailTracks.push(railTrack);
|
||||
}
|
||||
}
|
||||
}
|
||||
railTracks = newRailTracks;
|
||||
}
|
||||
}
|
||||
|
||||
export function addTrack(trackData){
|
||||
let newTrack = {
|
||||
name: trackData.name,
|
||||
curves: [],
|
||||
bounds: {min:{x:Infinity,y:Infinity,z:Infinity},max:{x:-Infinity,y:-Infinity,z:-Infinity}},
|
||||
};
|
||||
if(uniqueTrackNames[trackData.name] == null){
|
||||
uniqueTrackNames[trackData.name] = 0;
|
||||
}else{
|
||||
uniqueTrackNames[trackData.name]++;
|
||||
}
|
||||
newTrack.number = uniqueTrackNames[trackData.name];
|
||||
|
||||
let yardData = /\[(#)\]|\[(Y)\][_-]\[(.*?)\][_-]\[((.*?)[_-](.*?)[_-](.*?)|.*)\]/.exec(trackData.name);
|
||||
if(yardData){
|
||||
if(yardData[1]){ // track is [#], thus in yard limits, but not a specific designated yard.
|
||||
newTrack.isYard = true;
|
||||
}else if(yardData[2]){ // track is [Y], and is a specific yard.
|
||||
newTrack.isYard = true;
|
||||
newTrack.yardStation = yardData[3]; // Which station the track belongs to
|
||||
newTrack.yardDesignation = yardData[4]; // Full yard designation, used on signs
|
||||
newTrack.yardName = yardData[5]; // A yard, B yard, C yard, etc.
|
||||
newTrack.yardSidingNumber = yardData[6]; // Which siding within the yard
|
||||
newTrack.yardUsage = yardData[7]; // Outbound? Parking? Inbound? Loading? That's this.
|
||||
}
|
||||
}else if(/Turntable Track/.test(newTrack.name)){
|
||||
newTrack.isTurntable = true;
|
||||
}else if(/\[track (diverging|through)\]/.test(newTrack.name)){
|
||||
newTrack.isJunction = true;
|
||||
}
|
||||
newTrack.randomColor = Color.random();
|
||||
|
||||
for(let i=0; i+1<trackData.points.length; i++){
|
||||
const curveStart = trackData.points[i].position;
|
||||
const curveHandle1 = trackData.points[i].h2;
|
||||
const curveHandle2 = trackData.points[i+1].h1;
|
||||
const curveEnd = trackData.points[i+1].position;
|
||||
|
||||
newTrack.curves[i] = {
|
||||
start: curveStart,
|
||||
h1: curveHandle1,
|
||||
h2: curveHandle2,
|
||||
end: curveEnd,
|
||||
grade: Bezier.estimateGrade(curveStart, curveHandle1, curveHandle2, curveEnd, Config.bezierGradeResolution),
|
||||
length: Bezier.estimateLength(curveStart, curveHandle1, curveHandle2, curveEnd, Config.bezierLengthResolution),
|
||||
curvature: Bezier.estimateCurvature(curveStart, curveHandle1, curveHandle2, curveEnd, Config.bezierCurvatureResolution),
|
||||
randomColor: Color.random(),
|
||||
color: '#fff',
|
||||
averageHeight: (curveStart.y+curveEnd.y) * 0.5,
|
||||
bounds: Bezier.getBounds(curveStart, curveHandle1, curveHandle2, curveEnd),
|
||||
}
|
||||
newTrack.curves[i].gradeClass = Utils.gradeToClass(newTrack.curves[i].grade);
|
||||
newTrack.curves[i].postedSpeed = Utils.radiusToSpeed(1/newTrack.curves[i].curvature);
|
||||
if(newTrack.isYard || newTrack.isTurntable) newTrack.curves[i].postedSpeed = Math.min(newTrack.curves[i].postedSpeed, 50);
|
||||
|
||||
newTrack.curves[i].tooltip = [
|
||||
`<h2>Rail</h2>"${trackData.name}"${uniqueTrackNames[trackData.name] > 0 ? ' #'+uniqueTrackNames[trackData.name] : ''} Sec${i}`,
|
||||
`Max Grade: ${Math.round(newTrack.curves[i].grade*1000)/10}%`,
|
||||
`Min Radius: ${Math.round(1/newTrack.curves[i].curvature)} meters`,
|
||||
`Top Speed: ${Utils.radiusToSpeed(1/newTrack.curves[i].curvature)} km/h`,
|
||||
`Length: ${Math.round(newTrack.curves[i].length*10)/10} meters`,
|
||||
`Altitude (ASL): ${Math.round(newTrack.curves[i].averageHeight)-110} meters`,
|
||||
].join('\n');
|
||||
|
||||
newTrack.bounds.min.x = Math.min(newTrack.bounds.min.x, newTrack.curves[i].bounds.min.x);
|
||||
newTrack.bounds.max.x = Math.max(newTrack.bounds.max.x, newTrack.curves[i].bounds.max.x);
|
||||
newTrack.bounds.min.y = Math.min(newTrack.bounds.min.y, newTrack.curves[i].bounds.min.y);
|
||||
newTrack.bounds.max.y = Math.max(newTrack.bounds.max.y, newTrack.curves[i].bounds.max.y);
|
||||
newTrack.bounds.min.z = Math.min(newTrack.bounds.min.z, newTrack.curves[i].bounds.min.z);
|
||||
newTrack.bounds.max.z = Math.max(newTrack.bounds.max.z, newTrack.curves[i].bounds.max.z);
|
||||
}
|
||||
|
||||
railTracks.push(newTrack);
|
||||
}
|
||||
|
||||
export function addJunction(junctionData){
|
||||
if(junctionData.name.startsWith('S-') || junctionData.excludeFromJunctionMap)
|
||||
junctionData.minZoom = 1;
|
||||
else if(junctionData.name.startsWith('W-'))
|
||||
junctionData.minZoom = 0.05;
|
||||
junctionData.tooltip = `<h2>Junction</h2>"${junctionData.name}" - #${junctionData.index}\n${junctionData.excludeFromJunctionMap ? 'Inv' : 'V'}isible On Map\nLinked Junctions:\n- ${junctionData.linkedJunctions.join('\n- ')}`;
|
||||
junctionData.tooltipHitzone = Config.tooltipHitzone.junction;
|
||||
markers.push(junctionData);
|
||||
}
|
||||
|
||||
export function addPoi(poiData, level = 0){
|
||||
let newPoi = {
|
||||
type: poiData.type,
|
||||
name: poiData.name,
|
||||
color: poiData.color,
|
||||
position: poiData.position,
|
||||
};
|
||||
if(level > 0) newPoi.minZoom = 0.3;
|
||||
|
||||
switch(poiData.type){
|
||||
case 'service':
|
||||
newPoi.serviceTypes = [];
|
||||
if(poiData.repair) newPoi.serviceTypes.push('Repair');
|
||||
if(poiData.diesel) newPoi.serviceTypes.push('Diesel');
|
||||
if(poiData.charger) newPoi.serviceTypes.push('Charger');
|
||||
if(newPoi.serviceTypes.length == 0){
|
||||
newPoi.tooltip = '<h1>Service</h1><hr>?';
|
||||
}else{
|
||||
newPoi.tooltip = '<h1>Service</h1><hr>- '+newPoi.serviceTypes.join('\n- ');
|
||||
}
|
||||
break;
|
||||
case 'station':
|
||||
newPoi.maxZoom = 1;
|
||||
newPoi.nickname = poiData.nickname;
|
||||
newPoi.shorthand = poiData.shorthand;
|
||||
newPoi.color = poiData.color;
|
||||
newPoi.tooltip = `<h1>${poiData.name}</h1>`;
|
||||
break;
|
||||
case 'shop':
|
||||
newPoi.tooltip = `<h1>Shop</h1>${poiData.stock ? '<hr>'+poiData.stock.sort().join('\n') : ''}`;
|
||||
break;
|
||||
case 'water':
|
||||
newPoi.tooltip = '<h1>Water Tower</h1>';
|
||||
break;
|
||||
case 'coal':
|
||||
newPoi.tooltip = '<h1>Coal Tower</h1>';
|
||||
break;
|
||||
case 'office':
|
||||
if(poiData.museum) newPoi.tooltip = '<h1>Museum Office</h1>';
|
||||
else if(poiData.military) newPoi.tooltip = '<h1>Military Office</h1>';
|
||||
else newPoi.tooltip = '<h1>Office</h1>';
|
||||
break;
|
||||
default:
|
||||
newPoi.tooltip = `<h1>${newPoi.name}</h1>${poiData.description ? '<hr>'+poiData.description : ''}`;
|
||||
newPoi.minZoom = 0.07;
|
||||
break;
|
||||
}
|
||||
|
||||
markers.push(newPoi);
|
||||
if(poiData.children){
|
||||
for(let child of poiData.children){
|
||||
addPoi(child, level+1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function generateTrackSignage(){
|
||||
let yardMarkers = {};
|
||||
for(const railTrack of railTracks){
|
||||
let trackZoneData = {};
|
||||
let lastCurve = false;
|
||||
|
||||
let resetTzdGrade = (i=0) => {
|
||||
trackZoneData.gradeClass = railTrack.curves[i].gradeClass;
|
||||
trackZoneData.gradeDirection = Math.sign(railTrack.curves[i].grade);
|
||||
trackZoneData.gradeZoneStart = i;
|
||||
trackZoneData.gradeZoneLength = 0;
|
||||
};
|
||||
let resetTzdSpeed = (i=0) => {
|
||||
trackZoneData.postedSpeed = railTrack.curves[i].postedSpeed;
|
||||
trackZoneData.speedZoneStart = i;
|
||||
trackZoneData.speedZoneLength = 0;
|
||||
};
|
||||
|
||||
resetTzdGrade();
|
||||
resetTzdSpeed();
|
||||
|
||||
for(let i=0; i<railTrack.curves.length; i++){
|
||||
let nextIndex = i+1;
|
||||
if(nextIndex == railTrack.curves.length){
|
||||
lastCurve = true;
|
||||
nextIndex = i;
|
||||
}
|
||||
//Grade Markers
|
||||
trackZoneData.gradeZoneLength += railTrack.curves[i].length;
|
||||
if(railTrack.curves[nextIndex].gradeClass != trackZoneData.gradeClass
|
||||
|| Math.sign(railTrack.curves[nextIndex].grade) != trackZoneData.gradeDirection
|
||||
|| trackZoneData.gradeZoneLength > Config.maxZoneLength
|
||||
|| lastCurve){
|
||||
if(railTrack.curves[i].gradeClass != 'flat'){
|
||||
let markerIndex = Math.ceil((trackZoneData.gradeZoneStart + i)/2);
|
||||
let markerOffset = (trackZoneData.gradeZoneStart + i) % 2 ? 0.1 : 0.6;
|
||||
let b1 = railTrack.curves[markerIndex].start;
|
||||
let b2 = railTrack.curves[markerIndex].h1;
|
||||
let b3 = railTrack.curves[markerIndex].h2;
|
||||
let b4 = railTrack.curves[markerIndex].end;
|
||||
|
||||
let newMarker = {
|
||||
type: 'grade',
|
||||
value: railTrack.curves[markerIndex].grade,
|
||||
position: Bezier.evaluatePoint(b1, b2, b3, b4, markerOffset),
|
||||
gradeClass: railTrack.curves[markerIndex].gradeClass,
|
||||
minZoom: Math.max(0.02, Math.min(1.0, 20/trackZoneData.gradeZoneLength)),
|
||||
zoneLength: trackZoneData.gradeZoneLength,
|
||||
zoneCount: i - trackZoneData.gradeZoneStart + 1,
|
||||
tooltipHitzone: {radius:16},
|
||||
}
|
||||
let tangent = newMarker.value > 0
|
||||
? Bezier.evaluateVelocity(b1, b2, b3, b4, markerOffset)
|
||||
: Bezier.evaluateVelocity(b4, b3, b2, b1, markerOffset);
|
||||
let tangentLength = Math.sqrt(tangent.x * tangent.x + tangent.z * tangent.z) * Math.sign(tangent.y);
|
||||
newMarker.rotation = Math.atan2(tangent.x/tangentLength, tangent.z/tangentLength);
|
||||
newMarker.tooltip = `<h2>Grade</h2>${Math.round(newMarker.value*1000)/10}% Grade\nUphill bearing ${Utils.mod(newMarker.rotation/(Math.PI*2)*360, 360).toFixed(0)}, ${Utils.angleToCardinalDirection(newMarker.rotation)}\nSection Length: ${newMarker.zoneLength.toFixed(1)} meters\nCovers ${newMarker.zoneCount} track sections`;
|
||||
markers.push(newMarker);
|
||||
}
|
||||
resetTzdGrade(nextIndex);
|
||||
}
|
||||
//Speed Markers
|
||||
trackZoneData.speedZoneLength += railTrack.curves[i].length;
|
||||
if(railTrack.curves[nextIndex].postedSpeed != trackZoneData.postedSpeed
|
||||
|| trackZoneData.speedZoneLength > Config.maxZoneLength
|
||||
|| lastCurve){
|
||||
let markerIndex = Math.ceil((trackZoneData.speedZoneStart + i)/2);
|
||||
let markerOffset = (trackZoneData.speedZoneStart + i) % 2 ? 0.2 : 0.7;
|
||||
let b1 = railTrack.curves[markerIndex].start;
|
||||
let b2 = railTrack.curves[markerIndex].h1;
|
||||
let b3 = railTrack.curves[markerIndex].h2;
|
||||
let b4 = railTrack.curves[markerIndex].end;
|
||||
|
||||
let newMarker = {
|
||||
type: 'speed',
|
||||
value: railTrack.curves[markerIndex].postedSpeed,
|
||||
position: Bezier.evaluatePoint(b1, b2, b3, b4, markerOffset),
|
||||
zoneLength: trackZoneData.speedZoneLength,
|
||||
zoneCount: i - trackZoneData.speedZoneStart + 1,
|
||||
minZoom: 0.05,
|
||||
}
|
||||
if((railTrack.isYard || railTrack.isJunction) && newMarker.value > 30){
|
||||
if(newMarker.value > 40) newMarker.minZoom = 5;
|
||||
else newMarker.minZoom = 0.5;
|
||||
}
|
||||
if(newMarker.value > 80) newMarker.minZoom *= 6.5;
|
||||
else if(newMarker.value > 60) newMarker.minZoom *= 5;
|
||||
else if(newMarker.value > 40) newMarker.minZoom *= 3;
|
||||
|
||||
if(newMarker.zoneLength > 500) newMarker.minZoom *= 0.15;
|
||||
|
||||
if(newMarker.zoneLength < 35 && newMarker.value > 40) newMarker.minZoom *= 6;
|
||||
|
||||
newMarker.tooltip = `<h2>Speed</h2>${Math.round(newMarker.value)} km/h\nSection Length: ${newMarker.zoneLength.toFixed(1)} meters\nCovers ${newMarker.zoneCount} track sections`;
|
||||
markers.push(newMarker);
|
||||
resetTzdSpeed(nextIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if(railTrack.yardDesignation && railTrack.yardDesignation != '--'){
|
||||
if(!yardMarkers[railTrack.yardStation]) yardMarkers[railTrack.yardStation] = {};
|
||||
let centerIndex = Math.floor(railTrack.curves.length/2);
|
||||
let yardSignPos = railTrack.curves[centerIndex].start;
|
||||
if(railTrack.curves.length % 2 == 1){
|
||||
let curve = railTrack.curves[centerIndex];
|
||||
yardSignPos = Bezier.evaluatePoint(curve.start, curve.h1, curve.h2, curve.end, 0.5);
|
||||
}
|
||||
if(railTrack.yardName){
|
||||
if(!yardMarkers[railTrack.yardStation][railTrack.yardName])
|
||||
yardMarkers[railTrack.yardStation][railTrack.yardName] = {accCenter:{x:0,y:0,z:0},accCount:0};
|
||||
yardMarkers[railTrack.yardStation][railTrack.yardName].accCenter.x += yardSignPos.x;
|
||||
yardMarkers[railTrack.yardStation][railTrack.yardName].accCenter.y += yardSignPos.y;
|
||||
yardMarkers[railTrack.yardStation][railTrack.yardName].accCenter.z += yardSignPos.z;
|
||||
yardMarkers[railTrack.yardStation][railTrack.yardName].accCount++;
|
||||
}
|
||||
let newMarker = {
|
||||
type: 'yardSiding',
|
||||
name: railTrack.yardDesignation,
|
||||
position: yardSignPos,
|
||||
minZoom: 2.5,
|
||||
};
|
||||
if(railTrack.yardName && railTrack.yardSidingNumber && railTrack.yardUsage){
|
||||
if(!Config.sidingUsageMeanings[railTrack.yardUsage]) console.log(`Unknown siding usage for ${railTrack.yardDesignation} at ${railTrack.yardStation}`);
|
||||
newMarker.tooltip = `<h2>Siding</h2>${railTrack.yardDesignation}\n${Config.sidingUsageMeanings[railTrack.yardUsage]}`;
|
||||
}else{
|
||||
newMarker.tooltip = `<h2>Siding</h2>${railTrack.yardDesignation}`;
|
||||
}
|
||||
markers.push(newMarker);
|
||||
}
|
||||
}
|
||||
|
||||
for(let station in yardMarkers){
|
||||
for(let yard in yardMarkers[station]){
|
||||
markers.push({
|
||||
type: 'yard',
|
||||
name: `${yard} Yard`,
|
||||
position: {
|
||||
x: yardMarkers[station][yard].accCenter.x/yardMarkers[station][yard].accCount,
|
||||
y: yardMarkers[station][yard].accCenter.y/yardMarkers[station][yard].accCount,
|
||||
z: yardMarkers[station][yard].accCenter.z/yardMarkers[station][yard].accCount,
|
||||
},
|
||||
tooltip: `<h1>${yard} Yard</h1>`,
|
||||
maxZoom: 2.5,
|
||||
minZoom: 0.25,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the relevant tooltip for where the cursor is (if possible)
|
||||
* Returns "" if there is none.
|
||||
* @param {Number} cursorX
|
||||
* @param {Number} cursorY
|
||||
* @returns {String}
|
||||
*/
|
||||
export function testTooltip(cursorX, cursorY){
|
||||
cursorX = view.unconvertX(cursorX);
|
||||
cursorY = view.unconvertY(cursorY);
|
||||
|
||||
let finalTooltip = '';
|
||||
let radius = 0;
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
for(const marker of markers){
|
||||
if(!marker.visible) continue;
|
||||
if(marker.boundsCheck != null){
|
||||
if(marker.boundsCheck(cursorX, cursorY))
|
||||
finalTooltip = marker.tooltip ?? finalTooltip;
|
||||
}else if(marker.tooltipHitzone && marker.tooltipHitzone.width && marker.tooltipHitzone.height){
|
||||
width = marker.tooltipHitzone.width * 0.5/view.scale;
|
||||
height = marker.tooltipHitzone.height * 0.5/view.scale;
|
||||
if( marker.position.x - cursorX + (marker.tooltipHitzone.offsetX ?? 0)/view.scale <= width
|
||||
&& marker.position.x - cursorX + (marker.tooltipHitzone.offsetX ?? 0)/view.scale >= -width
|
||||
&& marker.position.z - cursorY + (marker.tooltipHitzone.offsetY ?? 0)/view.scale <= height
|
||||
&& marker.position.z - cursorY + (marker.tooltipHitzone.offsetY ?? 0)/view.scale >= -height){
|
||||
finalTooltip = marker.tooltip ?? finalTooltip;
|
||||
}
|
||||
}else{
|
||||
if(marker.tooltipHitzone)
|
||||
radius = (marker.tooltipHitzone.radius ?? Config.tooltipHitzone.default.radius) / view.scale;
|
||||
else
|
||||
radius = Config.tooltipHitzone.default.radius / view.scale;
|
||||
if(Math.pow(marker.position.x - cursorX, 2) + Math.pow(marker.position.z - cursorY, 2) <= radius * radius){
|
||||
finalTooltip = marker.tooltip ?? finalTooltip;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(finalTooltip != '') return finalTooltip;
|
||||
|
||||
let bestDistance = Infinity;
|
||||
let test = {};
|
||||
let boundsPadding = 40/view.scale;
|
||||
for(const railTrack of railTracks){
|
||||
if(cursorX >= railTrack.bounds.min.x - boundsPadding && cursorX <= railTrack.bounds.max.x + boundsPadding && cursorY >= railTrack.bounds.min.z - boundsPadding && cursorY <= railTrack.bounds.max.z + boundsPadding){
|
||||
for(const curve of railTrack.curves){
|
||||
if(cursorX >= curve.bounds.min.x - boundsPadding && cursorX <= curve.bounds.max.x + boundsPadding && cursorY >= curve.bounds.min.z - boundsPadding && cursorY <= curve.bounds.max.z + boundsPadding){
|
||||
test = Bezier.getNearestPoint2D(curve.start, curve.h1, curve.h2, curve.end, {x:cursorX, y:cursorY}, Math.floor(curve.length*2));
|
||||
if(test.distance < bestDistance){
|
||||
bestDistance = test.distance;
|
||||
finalTooltip = curve.tooltip;
|
||||
testPoint = test.point;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestDistance <= Math.min(Math.max(0.9, 12/view.scale), 120) ? finalTooltip : '';
|
||||
}
|
||||
|
||||
export function setTrackColorMode(mode){
|
||||
let modeFunction;
|
||||
const keyElement = document.getElementById('legendKeyColors');
|
||||
let gradientString = '';
|
||||
keyElement.innerHTML = '';
|
||||
switch(mode){
|
||||
case 'Track Type':
|
||||
modeFunction = (curve, railTrack) => {
|
||||
let finalColor = '#aaa';
|
||||
if(railTrack.isYard && railTrack.yardDesignation) finalColor = '#4bf';
|
||||
else if(railTrack.isYard) finalColor = '#4fb';
|
||||
else if(railTrack.isJunction) finalColor = '#fb4';
|
||||
else if(railTrack.isTurntable) finalColor = '#b4f';
|
||||
|
||||
curve.color = finalColor;
|
||||
};
|
||||
keyElement.innerHTML = `
|
||||
<span class="colorKey"><span style="background:#4bf"></span> Designated Yard</span>
|
||||
<span class="colorKey"><span style="background:#4fb"></span> Other Yard</span>
|
||||
<span class="colorKey"><span style="background:#fb4"></span> Point</span>
|
||||
<span class="colorKey"><span style="background:#b4f"></span> Turntable</span>
|
||||
<span class="colorKey"><span style="background:#aaa"></span> Other Tracks</span>`;
|
||||
break;
|
||||
case 'Grade':
|
||||
modeFunction = curve => {
|
||||
curve.color = Config.gradeColors['grade_'+curve.gradeClass];
|
||||
};
|
||||
for(let grade in Config.gradeColors){
|
||||
keyElement.innerHTML += `<span class="colorKey" style="${grade.replace('grade_','') < 6 || grade == 'grade_flat' ? 'color:#000;' : ''}padding:0px 3px;background:${Config.gradeColors[grade]}">${(Utils.classToGrade(grade.replace('grade_',''))*100).toFixed(1)}%</span>`;
|
||||
}
|
||||
break;
|
||||
case 'Altitude':
|
||||
modeFunction = (curve, railTrack) => {
|
||||
curve.color = Color.blendGradient(Config.altitudeGradient, (curve.averageHeight-matrix.minAlt)/(matrix.maxAlt-matrix.minAlt)).hex;
|
||||
}
|
||||
gradientString = 'linear-gradient(to right';
|
||||
for(let col of Config.altitudeGradient) gradientString += ', '+col.hex;
|
||||
gradientString += ')';
|
||||
|
||||
keyElement.innerHTML = `
|
||||
<div style="display:flex">
|
||||
<div>${Math.floor(matrix.minAlt)}m</div>
|
||||
<div class="gradientKey" style="flex-grow:1;background:${gradientString}"></div>
|
||||
<div>${Math.ceil(matrix.maxAlt)}m</div>
|
||||
</div>
|
||||
`;
|
||||
break;
|
||||
case 'Speed':
|
||||
modeFunction = curve => {
|
||||
curve.color = Color.blendGradient(Config.speedGradient, (curve.postedSpeed+10)/110).hex;
|
||||
};
|
||||
|
||||
gradientString = 'linear-gradient(to right';
|
||||
for(let col of Config.speedGradient) gradientString += ', '+col.hex;
|
||||
gradientString += ')';
|
||||
keyElement.innerHTML = `
|
||||
<div style="display:flex">
|
||||
<div>10 kmh</div>
|
||||
<div class="gradientKey" style="flex-grow:1;background:${gradientString}"></div>
|
||||
<div>120 kmh</div>
|
||||
</div>
|
||||
`;
|
||||
break;
|
||||
case 'Track Random':
|
||||
modeFunction = (curve, railTrack) => {
|
||||
curve.color = railTrack.randomColor.hex;
|
||||
};
|
||||
keyElement.innerHTML = 'Random per continuous track section';
|
||||
break;
|
||||
case 'Curve Random':
|
||||
modeFunction = curve => {
|
||||
curve.color = curve.randomColor.hex;
|
||||
};
|
||||
keyElement.innerHTML = 'Random per individual track bezier curve';
|
||||
break;
|
||||
default:
|
||||
modeFunction = curve => {
|
||||
curve.color = '#fff';
|
||||
};
|
||||
break;
|
||||
}
|
||||
if(modeFunction != null){
|
||||
for(let railTrack of railTracks){
|
||||
for(let curve of railTrack.curves){
|
||||
modeFunction(curve, railTrack);
|
||||
}
|
||||
}
|
||||
}
|
||||
view.dirty = true;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
export class SpriteBounds{
|
||||
constructor(x, y, width, height){
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
}
|
||||
+98
-1
@@ -1,5 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
import * as Config from './../config.js';
|
||||
import {SpriteBounds} from './spritelib.js';
|
||||
|
||||
export const gradeIncrements = [
|
||||
0.0498,
|
||||
0.04,
|
||||
@@ -11,7 +14,60 @@ export const gradeIncrements = [
|
||||
0.01,
|
||||
0.005,
|
||||
0.001
|
||||
]
|
||||
];
|
||||
|
||||
const gradeArrowDimensions = 64;
|
||||
export const gradeArrows = new OffscreenCanvas(11*gradeArrowDimensions, 2*gradeArrowDimensions);
|
||||
|
||||
export function generateGradeArrows(){
|
||||
const ctx = gradeArrows.getContext('2d');
|
||||
const mapSprites = document.getElementById('mapSprites');
|
||||
for(let i=0; i<11; i++){
|
||||
ctx.drawImage(
|
||||
mapSprites,
|
||||
Config.spriteBounds.gradeArrow.x,
|
||||
Config.spriteBounds.gradeArrow.y,
|
||||
Config.spriteBounds.gradeArrow.width,
|
||||
Config.spriteBounds.gradeArrow.height,
|
||||
i*gradeArrowDimensions,
|
||||
0,
|
||||
gradeArrowDimensions,
|
||||
gradeArrowDimensions
|
||||
);
|
||||
ctx.drawImage(
|
||||
mapSprites,
|
||||
Config.spriteBounds.gradeArrowDown.x,
|
||||
Config.spriteBounds.gradeArrowDown.y,
|
||||
Config.spriteBounds.gradeArrowDown.width,
|
||||
Config.spriteBounds.gradeArrowDown.height,
|
||||
i*gradeArrowDimensions,
|
||||
gradeArrowDimensions,
|
||||
gradeArrowDimensions,
|
||||
gradeArrowDimensions
|
||||
);
|
||||
|
||||
if(i == 0){
|
||||
Config.spriteBounds['gradeArrow_flat'] = new SpriteBounds(0, 0, gradeArrowDimensions, gradeArrowDimensions);
|
||||
Config.spriteBounds['gradeArrowDown_flat'] = new SpriteBounds(0, gradeArrowDimensions, gradeArrowDimensions, gradeArrowDimensions);
|
||||
}else{
|
||||
Config.spriteBounds['gradeArrow_'+(i-1)] = new SpriteBounds(i*gradeArrowDimensions, 0, gradeArrowDimensions, gradeArrowDimensions);
|
||||
Config.spriteBounds['gradeArrowDown_'+(i-1)] = new SpriteBounds(i*gradeArrowDimensions, gradeArrowDimensions, gradeArrowDimensions, gradeArrowDimensions);
|
||||
}
|
||||
}
|
||||
let arrowLayer = gradeArrows.transferToImageBitmap();
|
||||
for(let i=0; i<11; i++){
|
||||
if(i == 0)
|
||||
ctx.fillStyle = Config.gradeColors.grade_flat;
|
||||
else
|
||||
ctx.fillStyle = Config.gradeColors['grade_'+(i-1)];
|
||||
ctx.fillRect(i*gradeArrowDimensions, 0, gradeArrowDimensions, gradeArrowDimensions * 2);
|
||||
}
|
||||
ctx.globalCompositeOperation = 'multiply';
|
||||
ctx.drawImage(arrowLayer, 0, 0);
|
||||
ctx.globalCompositeOperation = 'destination-in';
|
||||
ctx.drawImage(arrowLayer, 0, 0);
|
||||
arrowLayer.close();
|
||||
}
|
||||
|
||||
export function randomRange(min=0.0, max=1.0){
|
||||
return Math.random()*(max-min) + min;
|
||||
@@ -74,4 +130,45 @@ export function radiusToSpeed(radius){
|
||||
|
||||
export function clamp(v, min = 0, max = 1){
|
||||
return Math.max(Math.min(v, max), min);
|
||||
}
|
||||
|
||||
export function zoomToCullLevel(zoom){
|
||||
if(zoom < 1) return 0;
|
||||
if(zoom < 2) return 1;
|
||||
if(zoom < 3) return 2;
|
||||
if(zoom < 6) return 3;
|
||||
if(zoom < 12) return 4;
|
||||
if(zoom < 24) return 5;
|
||||
if(zoom < 60) return 6;
|
||||
return 7;
|
||||
}
|
||||
|
||||
export function lerp(a, b, t){
|
||||
return (a * (1-t)) + (b*t);
|
||||
}
|
||||
|
||||
export function mod(a, b){
|
||||
return (a % b + b) % b;
|
||||
}
|
||||
|
||||
export function titleCase(text){
|
||||
text = text.toString();
|
||||
if(text){
|
||||
return `${text[0].toUpperCase()}${text.substring(1)}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const cardinalDirections = [
|
||||
'North',
|
||||
'Northeast',
|
||||
'East',
|
||||
'Southeast',
|
||||
'South',
|
||||
'Southwest',
|
||||
'West',
|
||||
'Northwest',
|
||||
]
|
||||
export function angleToCardinalDirection(angle){
|
||||
return cardinalDirections[Math.floor(mod(angle/(2*Math.PI) + (0.5/cardinalDirections.length), 1)*cardinalDirections.length)];
|
||||
}
|
||||
Reference in new issue
Block a user