1002 lines
43 KiB
JavaScript
1002 lines
43 KiB
JavaScript
'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 = [];
|
||
export let dynamicMarkers = [];
|
||
|
||
let visibleMarkers = [];
|
||
|
||
let uniqueTrackNames = {};
|
||
|
||
export let testPoint = {x:0,y:0};
|
||
|
||
let locationData = null;
|
||
export let playerMarker = {
|
||
type: 'player',
|
||
position: {x:8192, y:200, z:8192},
|
||
rotation: 0.0,
|
||
minZoom: 0.0,
|
||
hidden: true,
|
||
};
|
||
let locationUpdateRate = 500;
|
||
|
||
export let Shops = {
|
||
Common:[]
|
||
}
|
||
|
||
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));
|
||
view.pixelRatio = window.devicePixelRatio;
|
||
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.dynDirty = false;
|
||
|
||
view.convertX = x => {
|
||
return x * view.scale * view.pixelRatio + view.x * view.pixelRatio;
|
||
}
|
||
view.convertY = y => {
|
||
return (matrix.minY+matrix.maxY-y) * view.scale * view.pixelRatio - view.y * view.pixelRatio;
|
||
}
|
||
|
||
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 = {
|
||
catenary: true,
|
||
catenaryWires: true,
|
||
catenarySubstations: true,
|
||
catenaryNeutrals: true,
|
||
catenaryInterruptions: true,
|
||
};
|
||
|
||
export let catenaryData = null;
|
||
export let catenaryPolylines = [];
|
||
export let catenarySubstations = [];
|
||
export let catenaryNeutrals = [];
|
||
export let catenaryInterruptions = [];
|
||
export let catenarySigns = [];
|
||
export let catenaryColorMode = 'Voltage';
|
||
|
||
export function reset(){
|
||
railTracks = [];
|
||
markers = [];
|
||
catenaryPolylines = [];
|
||
catenarySubstations = [];
|
||
catenaryNeutrals = [];
|
||
catenaryInterruptions = [];
|
||
catenarySigns = [];
|
||
}
|
||
|
||
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(markerSortFunction);
|
||
}
|
||
|
||
function markerSortFunction(a, b){
|
||
if(a.type == 'player') return 1;
|
||
if(b.type == 'player') return -1;
|
||
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 == 'dummy_polyline') return 1;
|
||
if(b.type == 'dummy_polyline') 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;
|
||
|
||
if(a.type == 'demonstratorSpawnHint') return -1;
|
||
if(b.type == 'demonstratorSpawnHint') return 1;
|
||
return 0;
|
||
}
|
||
|
||
export function sortShops(){
|
||
for(let shop in Shops){
|
||
Shops[shop].sort((a,b) => {
|
||
return a.name.localeCompare(b.name);
|
||
});
|
||
}
|
||
}
|
||
|
||
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><hr><div class="smol" style="columns:2">';
|
||
if(poiData.shopId){
|
||
for(let item of Shops[poiData.shopId]){
|
||
newPoi.tooltip += `<div>${item.name}${item.count > 1 ? ` (x${item.count})` : ''} - $${item.price}</div>`;
|
||
}
|
||
for(let item of Shops.Common){
|
||
newPoi.tooltip += `<div class="quiet">${item.name}${item.count > 1 ? ` (x${item.count})` : ''} - $${item.price}</div>`;
|
||
}
|
||
}
|
||
newPoi.tooltip += '</div>';
|
||
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;
|
||
case 'demonstratorSpawn':
|
||
newPoi.name = newPoi.name.replace('spawn anchor ', '');
|
||
newPoi.tooltip = `<h2>Demonstrator Spawnpoint</h2>${newPoi.name ?? ''}`;
|
||
break;
|
||
case 'demonstratorSpawnHint':
|
||
newPoi.tooltip = `<h2>Demonstrator Spawn Hint</h2>${newPoi.name ?? ''}`;
|
||
newPoi.radius = poiData.radius;
|
||
break;
|
||
default:
|
||
if(poiData.type.startsWith('dummy_')){
|
||
newPoi.ignoreCulling = true;
|
||
break;
|
||
}
|
||
newPoi.tooltip = `<h1>${newPoi.name}</h1>${poiData.description ? '<hr>'+poiData.description : ''}`;
|
||
newPoi.minZoom = 0.07;
|
||
break;
|
||
}
|
||
|
||
if(poiData.type != 'dummy')
|
||
markers.push(newPoi);
|
||
|
||
if(poiData.children){
|
||
for(let child of poiData.children){
|
||
addPoi(child, level+(poiData.type == 'dummy' ? 0 : 1));
|
||
}
|
||
}
|
||
}
|
||
|
||
export function addShopItem(itemData){
|
||
let newItem = {
|
||
name: itemData.name,
|
||
price: itemData.price,
|
||
count: itemData.count,
|
||
};
|
||
if(itemData.soldOnlyAt.length == 0){
|
||
for(let item of Shops.Common){
|
||
if(item.name == newItem.name && item.price == newItem.price){
|
||
item.count += newItem.count;
|
||
return;
|
||
}
|
||
}
|
||
Shops.Common.push(newItem);
|
||
}else{
|
||
for(let shop of itemData.soldOnlyAt){
|
||
if(!Shops[shop]) Shops[shop] = [];
|
||
|
||
for(let item of Shops[shop]){
|
||
if(item.name == newItem.name && item.price == newItem.price){
|
||
item.count += newItem.count;
|
||
return;
|
||
}
|
||
}
|
||
Shops[shop].push(newItem);
|
||
}
|
||
}
|
||
}
|
||
|
||
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 dynamicMarkers){
|
||
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;
|
||
|
||
// 1. Catenary Point badges (substations, neutrals, interruptions) with clamped screen radius
|
||
if(layers.catenary && layers.catenarySubstations && view.scale >= 0.012){
|
||
let subRadius = Math.min(22 / view.scale, 40);
|
||
for(const sub of catenarySubstations){
|
||
if(Math.pow(sub.x - cursorX, 2) + Math.pow(sub.z - cursorY, 2) <= subRadius * subRadius){
|
||
return sub.tooltip;
|
||
}
|
||
}
|
||
}
|
||
|
||
if(layers.catenary && layers.catenaryNeutrals && view.scale >= 0.012){
|
||
let neutralRadius = Math.min(30 / view.scale, 45);
|
||
for(const n of catenaryNeutrals){
|
||
if(Math.pow(n.center.x - cursorX, 2) + Math.pow(n.center.z - cursorY, 2) <= neutralRadius * neutralRadius){
|
||
return n.tooltip;
|
||
}
|
||
}
|
||
}
|
||
|
||
if(layers.catenary && layers.catenaryInterruptions && view.scale >= 0.018){
|
||
let interRadius = Math.min(18 / view.scale, 35);
|
||
for(const inter of catenaryInterruptions){
|
||
if(Math.pow(inter.center.x - cursorX, 2) + Math.pow(inter.center.z - cursorY, 2) <= interRadius * interRadius){
|
||
return inter.tooltip;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. Static Markers (stations, POIs, speed signs, grade arrows, yards)
|
||
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;
|
||
|
||
// 2. Rail tracks curve check
|
||
let bestDistance = Infinity;
|
||
let trackTooltip = '';
|
||
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){
|
||
let 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;
|
||
trackTooltip = curve.tooltip;
|
||
testPoint = test.point;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let trackHit = bestDistance <= Math.min(Math.max(0.9, 12 / view.scale), 120);
|
||
|
||
// 3. Catenary wire check
|
||
let wireTooltip = '';
|
||
let wireTooltipCompact = '';
|
||
if(layers.catenary && (layers.catenaryWires || layers.catenaryNeutrals)){
|
||
let bestWireDistSq = Infinity;
|
||
let wireBoundsPadding = 25 / view.scale;
|
||
for(const poly of catenaryPolylines){
|
||
if(poly.is_neutral && !layers.catenaryNeutrals) continue;
|
||
if(!poly.is_neutral && !layers.catenaryWires) continue;
|
||
let b = poly.bounds;
|
||
if(cursorX >= b[0] - wireBoundsPadding && cursorX <= b[2] + wireBoundsPadding && cursorY >= b[1] - wireBoundsPadding && cursorY <= b[3] + wireBoundsPadding){
|
||
let pts = poly.points;
|
||
for(let i = 0; i < pts.length - 1; i++){
|
||
let d2 = distToSegmentSquared(cursorX, cursorY, pts[i][0], pts[i][1], pts[i+1][0], pts[i+1][1]);
|
||
if(d2 < bestWireDistSq){
|
||
bestWireDistSq = d2;
|
||
wireTooltip = poly.tooltip;
|
||
wireTooltipCompact = poly.tooltipCompact || poly.tooltip;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let maxWireDist = Math.min(Math.max(1.2, 12 / view.scale), 50);
|
||
if(bestWireDistSq > maxWireDist * maxWireDist){
|
||
wireTooltip = '';
|
||
wireTooltipCompact = '';
|
||
}
|
||
}
|
||
|
||
// 4. Combined or individual resolution
|
||
if(trackHit && wireTooltipCompact){
|
||
return `${trackTooltip}\n<hr style="margin: 4px 0;">\n${wireTooltipCompact}`;
|
||
}
|
||
if(trackHit){
|
||
return trackTooltip;
|
||
}
|
||
if(wireTooltip){
|
||
return wireTooltip;
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
export function distToSegmentSquared(px, pz, x1, z1, x2, z2) {
|
||
let dx = x2 - x1;
|
||
let dz = z2 - z1;
|
||
let l2 = dx * dx + dz * dz;
|
||
if (l2 === 0) {
|
||
let ex = px - x1;
|
||
let ez = pz - z1;
|
||
return ex * ex + ez * ez;
|
||
}
|
||
let t = ((px - x1) * dx + (pz - z1) * dz) / l2;
|
||
t = Math.max(0, Math.min(1, t));
|
||
let nx = x1 + t * dx;
|
||
let nz = z1 + t * dz;
|
||
let rx = px - nx;
|
||
let rz = pz - nz;
|
||
return rx * rx + rz * rz;
|
||
}
|
||
|
||
export function addCatenaryData(data){
|
||
catenaryData = data;
|
||
catenaryPolylines = data.polylines || [];
|
||
catenarySubstations = data.substations || [];
|
||
catenaryNeutrals = data.neutral_sections || [];
|
||
catenaryInterruptions = data.interruptions || [];
|
||
catenarySigns = data.signs || [];
|
||
|
||
for(const sub of catenarySubstations){
|
||
sub.tooltip = `<h1>⚡ Тяговая подстанция ${sub.id}</h1>` +
|
||
`Номинальное напряжение: ${sub.nominal_voltage} В DC\n` +
|
||
`Питающее напряжение: ${sub.supply_voltage} В DC\n` +
|
||
`Максимальная мощность: ${sub.maximum_load} кВт\n` +
|
||
`Инвертор рекуперации: ${sub.has_inverter ? 'Есть (возврат энергии в сеть)' : 'Нет'}`;
|
||
}
|
||
|
||
for(const n of catenaryNeutrals){
|
||
n.tooltip = `<h1>⛔ Токораздел (Нейтральная вставка): ${n.id}</h1>` +
|
||
`Напряжение: 0 В (Обесточенный участок)\n` +
|
||
`Длина вставки: ${n.total_length} м\n` +
|
||
`Проводов: ${n.wire_count}\n` +
|
||
`Знаки: ${(n.signs || []).map(s => s.type).join(', ')}\n` +
|
||
`Внимание: Отключить тягу перед прохождением!`;
|
||
}
|
||
|
||
for(const inter of catenaryInterruptions){
|
||
let hasDrop = (inter.signs || []).some(s => s.includes('Drop') && !s.includes('Warning'));
|
||
let hasRaise = (inter.signs || []).some(s => s.includes('Raise'));
|
||
let hasWarn = (inter.signs || []).some(s => s.includes('Warning'));
|
||
|
||
let title = '⤓ Прерывание контактной сети';
|
||
let action = 'Опустить токоприемники (Drop Pantographs)!';
|
||
if(hasWarn && !hasDrop && !hasRaise){
|
||
title = '⚠ Предупреждение: Разрыв контактной сети';
|
||
action = 'Внимание: Подготовиться к опусканию токоприемников (Drop Warning ahead)!';
|
||
inter.kind = 'warning';
|
||
} else if(hasRaise && !hasDrop){
|
||
title = '⤒ Конец прерывания контактной сети';
|
||
action = 'Конец обесточенного участка: Поднять токоприемники (Raise Pantographs)!';
|
||
inter.kind = 'raise';
|
||
} else {
|
||
inter.kind = 'drop';
|
||
}
|
||
|
||
inter.tooltip = `<h1>${title}</h1>` +
|
||
`Действие: ${action}\n` +
|
||
`Знаки: ${(inter.signs || []).join(', ')}`;
|
||
}
|
||
|
||
for(const p of catenaryPolylines){
|
||
let catDesc = 'Одинарная подвеска (Single 6.0м)';
|
||
if(p.category === 'dual') catDesc = 'Двойная подвеска (Dual 6.0м, повышенная токоотдача)';
|
||
else if(p.category === 'quad') catDesc = 'Усиленная 4-проводная (Quad 6.0м, горный перевал)';
|
||
else if(p.category === 'trolley') catDesc = 'Трамвайная/простая подвеска (Trolley 6.0м, маневровые)';
|
||
else if(p.category === 'side_rail') catDesc = 'Боковой 3-й контактный рельс (Side Rail 4.5м, шахты)';
|
||
|
||
p.tooltip = `<h1>⚡ Контактная сеть: ${p.is_neutral ? 'Токораздел (0 В)' : p.voltage + ' В DC'}</h1>` +
|
||
`Напряжение: ${p.voltage} В DC\n` +
|
||
`Тип подвески: ${catDesc}\n` +
|
||
`Тяговая подстанция: ${p.substation}\n` +
|
||
`Длина участка: ${p.length} м (${p.wire_count} звеньев)`;
|
||
|
||
p.tooltipCompact = `<div style="font-weight:bold;color:${p.is_neutral ? '#e63946' : '#00d2ff'}">⚡ Контактная сеть: ${p.is_neutral ? 'Токораздел (0 В)' : p.voltage + ' В DC'}</div>` +
|
||
`Подвеска: ${catDesc}\nПодстанция: ${p.substation} | Длина: ${p.length} м`;
|
||
}
|
||
|
||
setCatenaryColorMode(catenaryColorMode);
|
||
}
|
||
|
||
export function setCatenaryColorMode(mode){
|
||
catenaryColorMode = mode;
|
||
for(const p of catenaryPolylines){
|
||
if(p.is_neutral){
|
||
p.color = '#e63946';
|
||
continue;
|
||
}
|
||
switch(mode){
|
||
case 'Substation':
|
||
p.color = Config.catenarySubstationColors[p.substation] || '#3a86ff';
|
||
break;
|
||
case 'Wire Type':
|
||
p.color = Config.catenaryWireTypeColors[p.category] || '#48cae4';
|
||
break;
|
||
case 'Voltage':
|
||
default:
|
||
p.color = Config.catenaryVoltageColors[String(p.voltage)] || '#00d2ff';
|
||
break;
|
||
}
|
||
}
|
||
updateCatenaryLegend();
|
||
view.dirty = true;
|
||
}
|
||
|
||
export function updateCatenaryLegend(){
|
||
const elem = document.getElementById('legendCatenaryColors');
|
||
if(!elem) return;
|
||
elem.innerHTML = '';
|
||
switch(catenaryColorMode){
|
||
case 'Voltage':
|
||
elem.innerHTML = `
|
||
<span class="colorKey"><span style="background:${Config.catenaryVoltageColors['1500']}"></span> 1500V DC (Магистраль)</span>
|
||
<span class="colorKey"><span style="background:${Config.catenaryVoltageColors['1000']}"></span> 1000V DC (Боковой рельс)</span>
|
||
<span class="colorKey"><span style="background:${Config.catenaryVoltageColors['750']}"></span> 750V DC (Троллей)</span>
|
||
<span class="colorKey"><span style="background:${Config.catenaryVoltageColors['0']}"></span> 0V (Токораздел)</span>
|
||
`;
|
||
break;
|
||
case 'Substation':
|
||
let subHtml = '';
|
||
for(const [subId, col] of Object.entries(Config.catenarySubstationColors)){
|
||
subHtml += `<span class="colorKey"><span style="background:${col}"></span> ${subId}</span> `;
|
||
}
|
||
elem.innerHTML = subHtml;
|
||
break;
|
||
case 'Wire Type':
|
||
elem.innerHTML = `
|
||
<span class="colorKey"><span style="background:${Config.catenaryWireTypeColors['single']}"></span> Single (6.0м)</span>
|
||
<span class="colorKey"><span style="background:${Config.catenaryWireTypeColors['dual']}"></span> Dual (6.0м)</span>
|
||
<span class="colorKey"><span style="background:${Config.catenaryWireTypeColors['quad']}"></span> Quad (Перевал 6.0м)</span>
|
||
<span class="colorKey"><span style="background:${Config.catenaryWireTypeColors['trolley']}"></span> Trolley (6.0м)</span>
|
||
<span class="colorKey"><span style="background:${Config.catenaryWireTypeColors['side_rail']}"></span> 3-й рельс (4.5м)</span>
|
||
`;
|
||
break;
|
||
}
|
||
}
|
||
|
||
export function setLocationUpdateRate(rate){
|
||
locationUpdateRate = rate;
|
||
}
|
||
|
||
export function connectPlayerLocation(address, status){
|
||
let cleanAddress = (address || '').trim().replace(/^https?:\/\//i, '').replace(/\/+$/, '');
|
||
cleanAddress = 'http://' + cleanAddress;
|
||
if(!cleanAddress.endsWith('/location')){
|
||
cleanAddress += '/location';
|
||
}
|
||
address = cleanAddress;
|
||
console.log(`Connecting to game at ${address}`);
|
||
let started = false;
|
||
const endConnect = () => {
|
||
locationData = null;
|
||
dynamicMarkers = [];
|
||
status.value = 'Connect';
|
||
playerMarker.hidden = true;
|
||
view.dynDirty = true;
|
||
}
|
||
const pingLocation = async () => {
|
||
if(locationData == null && started){
|
||
endConnect();
|
||
return;
|
||
}
|
||
status.value = 'Disconnect...';
|
||
try {
|
||
const fetchRequest = await fetch(new Request(address), {signal: AbortSignal.timeout(5000)});
|
||
if(!fetchRequest.ok || (locationData == null && started)){
|
||
endConnect();
|
||
return;
|
||
}
|
||
status.value = 'Disconnect ';
|
||
locationData = await (fetchRequest).json();
|
||
playerMarker.position = {
|
||
x: locationData.x,
|
||
y: locationData.y,
|
||
z: locationData.z,
|
||
};
|
||
playerMarker.rotation = locationData.rotation;
|
||
playerMarker.tooltip = `<h1>Player</h1>X: ${playerMarker.position.x.toFixed(2)}\nY: ${playerMarker.position.y.toFixed(2)}\nZ: ${playerMarker.position.z.toFixed(2)}\nBearing ${(playerMarker.rotation * (180/Math.PI)).toFixed(1)} (${Utils.angleToCardinalDirection(playerMarker.rotation)})`;
|
||
playerMarker.hidden = false;
|
||
|
||
dynamicMarkers = [];
|
||
if(!locationData.cars) locationData.cars = [];
|
||
for(let car of locationData.cars){
|
||
car.name = car.name.replaceAll('(Clone)', '').replaceAll('_',' ').replaceAll(/(Car|Loco)/g, '');
|
||
dynamicMarkers.push({
|
||
type: 'car',
|
||
name: 'car.id',
|
||
position: {
|
||
x: car.x,
|
||
y: car.y,
|
||
z: car.z
|
||
},
|
||
length: car.length - 0.5,
|
||
width: 2.0,
|
||
rotation: car.rotation,
|
||
guid: car.carGuid,
|
||
isLoco: car.isLoco,
|
||
tooltip: `<h1>${car.id}</h1>Type: ${car.name}\nLength: ${car.length.toFixed(1)}m${car.speed >= 0.1 ? `\nSpeed: ${(car.speed * 3.6).toFixed(1)} km/h` : '' }${car.derailed ? '\nDerailed!' : ''}`,
|
||
derailed: car.derailed,
|
||
isPlayer: car.isPlayer,
|
||
isActive: car.isActive,
|
||
});
|
||
}
|
||
dynamicMarkers.push(playerMarker);
|
||
|
||
started = true;
|
||
view.dynDirty = true;
|
||
setTimeout(pingLocation, locationUpdateRate);
|
||
}catch(e){
|
||
endConnect();
|
||
console.log(e);
|
||
}
|
||
}
|
||
pingLocation();
|
||
}
|
||
|
||
export function disconnectPlayerLocation(){
|
||
console.log('Disconnecting');
|
||
locationData = null;
|
||
} |