Add Catenary DC electrification layer, CommunityMapLocation fixes, and CI/CD
Deploy & Release / build-and-deploy (push) Successful in 1m3s

This commit is contained in:
m4xx1m committed 2026-10-21 04:55:00 +03:00
1 parent 8252fd6ba9
commit 68d5058106
11 files changed
+660 -20

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
name: Deploy & Release
on:
push:
branches: [ main ]
tags: [ 'v*' ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- name: Validate Datasets & Syntax
run: |
node -e "JSON.parse(require('fs').readFileSync('catenary_dv.json')); JSON.parse(require('fs').readFileSync('trackdata_dv.json')); JSON.parse(require('fs').readFileSync('poi_dv.json')); console.log('Datasets valid!');"
node --check index.js
node --check legend.js
node --check config.js
node --check js/mapdata.js
echo "All JS syntax checks passed!"
- name: Upload Web Map Artifact
uses: actions/upload-artifact@v3
with:
name: dv-community-map-catenary
path: |
index.html
style.css
config.js
legend.js
index.js
mapSprites.svg
favicon.png
opengraph.png
catenary_dv.json
trackdata_dv.json
poi_dv.json
shop_items.json
js/
terrains/
- name: Create Gitea Release
if: startsWith(github.ref, 'refs/tags/')
uses: akkuman/gitea-release-action@v1
with:
files: |-
catenary_dv.json
trackdata_dv.json
+46
View File
@@ -0,0 +1,46 @@
name: Deploy to GitHub Pages
on:
push:
branches: [ main ]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Validate Datasets & Syntax
run: |
node -e "JSON.parse(require('fs').readFileSync('catenary_dv.json')); JSON.parse(require('fs').readFileSync('trackdata_dv.json')); JSON.parse(require('fs').readFileSync('poi_dv.json')); console.log('Datasets valid!');"
node --check index.js
node --check legend.js
node --check config.js
node --check js/mapdata.js
echo "Syntax check OK"
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: '.'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
@@ -1,4 +1,4 @@
using DV.Customization;
using DV.Customization;
using DV.HUD;
using DV.Simulation.Cars;
using DV.Utils;
@@ -59,7 +59,7 @@ namespace CommunityMapLocation.Api.Location
}
}
return new LocationResponse(PlayerManager.PlayerTransform, reverse)
return new LocationResponse(PlayerManager.PlayerTransform, rotationTransform, reverse)
{
Cars = relevantCars.Values.ToArray() ?? Array.Empty<CarLocationResponse>()
};
@@ -1,4 +1,4 @@
using DV.OriginShift;
using DV.OriginShift;
using System.Collections.Generic;
using UnityEngine;
@@ -7,6 +7,11 @@ namespace CommunityMapLocation.Api.Location
public class LocationResponse
{
public LocationResponse(Transform playerTransform, bool reverse)
: this(playerTransform, playerTransform, reverse)
{
}
public LocationResponse(Transform playerTransform, Transform rotationTransform, bool reverse)
{
var position = playerTransform.AbsolutePosition();
@@ -14,7 +19,8 @@ namespace CommunityMapLocation.Api.Location
Y = position.y;
Z = position.z;
var angle = playerTransform.rotation.eulerAngles.y;
var targetTransform = rotationTransform != null ? rotationTransform : playerTransform;
var angle = targetTransform.rotation.eulerAngles.y;
if (reverse)
{
+49 -6
View File
@@ -1,8 +1,51 @@
# [Unofficial Community Map for Derail Valley](https://pyronicampt.github.io/DV-Community-Map/)
A map to let you see the valley in more detail than the in-game map, featuring speed signage, grade indication, curve radius, etc.
# Derail Valley Community Map with Catenary DC Layer
## Current Plans
- [ ] Generally improve the look and UX of the map
Interactive vector web map of Derail Valley railway network with DC electrification infrastructure overlay (Catenary-DC / WE6SIM mod).
## Contribution Notes
Please avoid using AI to generate code for pull requests. It makes it much more difficult to maintain and slows maintainence of features to a crawl. This project was built by hand with a specific architecture, AI may make functional code, but not sensible code.
## Architecture & Stack
- **Frontend**: Vanilla ES6 modules, HTML5 Canvas 2D (double-buffered with dynamic overlay).
- **Coordinate System**: Game engine coordinates (meters), origin at bottom-left, mapped via `Matrix` and `View` transforms.
- **Datasets**:
- `catenary_dv.json`: 164 chained polylines (optimized from 3,751 segments), 9 traction substations, 5 neutral sections, 34 interruption clusters, 129 warning signs.
- `trackdata_dv.json`: Rail tracks (cubic Bézier curves) and junction points.
- `poi_dv.json`: Stations, offices, service tracks, turntables, landmarks.
- `shop_items.json`: Shop inventories and prices.
## Catenary Layer Features
- **Wire Suspension Rendering**:
- `Single` (6.0m, 1500V/750V): standard solid vector line.
- `Dual` (6.0m, 1500V): high-capacity twin catenary stroke.
- `Quad` (6.0m, 1500V): heavy 4-wire mountain pass stroke with dark border.
- `Trolley` (6.0m, 750V): fine yard wire.
- `Side Rail` (4.5m, 1000V): dashed 3rd contact rail at IME and SM mine tracks.
- `Neutral Sections` (0V): contrasting red-and-white dashed lines with `0V` danger badges.
- **Color Modes**:
- `Voltage`: 1500V DC (`#00d2ff`), 1000V DC (`#ff9f1c`), 750V DC (`#2ec4b6`), 0V Neutral (`#e63946`).
- `Substation`: Distinct color per feeding substation (CME/IME1500, CP750, FF1500, HB1500, IME1000, IMW1500, OR750, SM1500, SM1000).
- `Wire Type`: Color-coded by physical suspension category.
- **Markers & Tooltips**:
- Traction Substations: `⚡` badge with nominal voltage, supply voltage, max load (kW), and regenerative inverter presence.
- Neutral Sections: `⛔` badge with section ID, length, connecting substations, and disconnect traction warning.
- Interruption Signs: `⤓` diamond badge for pantograph drop/raise warnings.
- Contact Wires: Nearest-point hovering displaying voltage, substation feed, category, and segment length.
## CommunityMapLocation Mod Integration
### Bug Fixes
1. **Rotation Desync**: `LocationResponse` accepts `rotationTransform` from locomotive transform or camera heading (fixing player orientation desync).
2. **URL Formatting**: Fixed string immutability bug in `address.replaceAll` inside `connectPlayerLocation`.
### CORS Configuration
In `<GameDirectory>/Mods/CommunityMapLocation/Settings.xml`:
```xml
<Settings>
<Port>9090</Port>
<RequestSourceType>Any</RequestSourceType>
<RequestSource>http://localhost:8080</RequestSource>
</Settings>
```
*Note: If `RequestSourceType` is `Any`, the game mod allows cross-origin requests from any web host (localhost, GitHub Pages, Gitea).*
## CI/CD Workflows
- **Gitea Actions** (`.gitea/workflows/deploy.yaml`): JSON dataset validation, JS syntax verification, artifact packaging with `actions/upload-artifact@v3`, releases with `akkuman/gitea-release-action@v1`.
- **GitHub Pages** (`.github/workflows/deploy-pages.yml`): Automated GitHub Pages deployment on push to `main`.
+1
View File
@@ -0,0 +1 @@
{"metadata":{"source":"Derail Valley Catenary-DC / WE6SIM (1500 V DC catenary)","total_polylines":164,"total_wires":3751,"total_substations":9,"total_neutral_sections":5,"total_interruption_clusters":34,"total_signs":129,"voltage_levels":[1500,1000,750,0]},"substations":[{"id":"CME/IME1500","x":14880.059,"y":245.569641,"z":14040.495,"supply_voltage":1650.0,"nominal_voltage":1500,"maximum_load":5500.0,"has_inverter":true,"file":"scenery_CME-IME.json"},{"id":"CP750","x":2194.443,"y":145.997864,"z":8921.762,"supply_voltage":825.0,"nominal_voltage":750,"maximum_load":4000.0,"has_inverter":false,"file":"scenery_CP.json"},{"id":"FF1500","x":9335.659,"y":120.178413,"z":13298.051,"supply_voltage":1650.0,"nominal_voltage":1500,"maximum_load":4000.0,"has_inverter":true,"file":"scenery_FF.json"},{"id":"HB1500","x":12360.086,"y":113.61673,"z":3496.618,"supply_voltage":1650.0,"nominal_voltage":1500,"maximum_load":4500.0,"has_inverter":true,"file":"scenery_HB.json"},{"id":"IME1000","x":14971.501,"y":249.062012,"z":15327.436,"supply_voltage":1100.0,"nominal_voltage":1000,"maximum_load":2000.0,"has_inverter":false,"file":"scenery_IME.json"},{"id":"IMW1500","x":2333.273,"y":134.553085,"z":13127.537,"supply_voltage":1650.0,"nominal_voltage":1500,"maximum_load":4500.0,"has_inverter":true,"file":"scenery_IMW.json"},{"id":"OR750","x":6502.203,"y":144.807587,"z":11587.063,"supply_voltage":750.0,"nominal_voltage":750,"maximum_load":2000.0,"has_inverter":false,"file":"scenery_OR.json"},{"id":"SM1500","x":8430.701,"y":130.830338,"z":7929.717,"supply_voltage":1500.0,"nominal_voltage":1500,"maximum_load":4000.0,"has_inverter":true,"file":"scenery_SM.json"},{"id":"SM1000","x":8375.657,"y":130.941376,"z":7877.38,"supply_voltage":1100.0,"nominal_voltage":1000,"maximum_load":2000.0,"has_inverter":false,"file":"scenery_SM.json"}],"neutral_sections":[{"id":"[CP]![IMW]","file":"scenery_[CP]![IMW].json","center":{"x":1576.93,"z":10300.64},"bounds":[1526.39,10066.57,1655.5,10529.48],"total_length":485.42,"wire_count":12,"signs":[{"template":"Signs/NeutralBegin","type":"NeutralBegin","x":1527.21,"y":144.06,"z":10106.96,"file":"scenery_[CP]![IMW].json"},{"template":"Signs/NeutralEndLowVoltageOtherSide","type":"NeutralEndLowVoltageOtherSide","x":1527.23,"y":144.04,"z":10108.47,"file":"scenery_[CP]![IMW].json"},{"template":"Signs/NeutralWarning","type":"NeutralWarning","x":1558.15,"y":144.19,"z":9906.9,"file":"scenery_[CP]![IMW].json"},{"template":"Signs/NeutralEnd","type":"NeutralEnd","x":1645.83,"y":139.95,"z":10488.09,"file":"scenery_[CP]![IMW].json"},{"template":"Signs/NeutralBeginOtherSide","type":"NeutralBeginOtherSide","x":1646.46,"y":139.91,"z":10489.96,"file":"scenery_[CP]![IMW].json"},{"template":"Signs/NeutralWarningOtherSide","type":"NeutralWarningOtherSide","x":1662.85,"y":137.37,"z":10674.58,"file":"scenery_[CP]![IMW].json"}],"wires":[{"start":{"x":1645.83,"y":139.95,"z":10488.09},"end":{"x":1655.5,"y":139.95,"z":10529.48},"length":42.5,"wire_type":"end_anchor_single","category":"single","substation":"NEUTRAL","voltage":0,"file":"scenery_[CP]![IMW].json"},{"start":{"x":1631.98,"y":140.52,"z":10450.17},"end":{"x":1645.83,"y":140.52,"z":10488.1},"length":40.38,"wire_type":"plain_single","category":"single","substation":"NEUTRAL","voltage":0,"file":"scenery_[CP]![IMW].json"},{"start":{"x":1616.8,"y":141.09,"z":10414.4},"end":{"x":1631.98,"y":141.09,"z":10450.18},"length":38.87,"wire_type":"plain_single","category":"single","substation":"NEUTRAL","voltage":0,"file":"scenery_[CP]![IMW].json"},{"start":{"x":1600.29,"y":141.55,"z":10377.27},"end":{"x":1616.8,"y":141.55,"z":10414.4},"length":40.63,"wire_type":"plain_single","category":"single","substation":"NEUTRAL","voltage":0,"file":"scenery_[CP]![IMW].json"},{"start":{"x":1584.3,"y":142.11,"z":10340.1},"end":{"x":1600.29,"y":142.11,"z":10377.28},"length":40.47,"wire_type":"plain_single","category":"single","substation":"NEUTRAL","voltage":0,"file":"scenery_[CP]![IMW].json"},{"start":{"x":1567.91,"y":142.65,"z":10303.16},"end":{"x":1584.3,"y":142.65,"z":10340.1},"length":40.42,"wire_type":"plain_single","category":"single","substation":"NEUTRAL","voltage":0,"file":"scenery_[CP]![IMW].json"},{"start":{"x":1553.65,"y":143.1,"z":10265.41},"end":{"x":1567.91,"y":143.1,"z":10303.16},"length":40.35,"wire_type":"middle_anchor_single","category":"single","substation":"NEUTRAL","voltage":0,"file":"scenery_[CP]![IMW].json"},{"start":{"x":1541.91,"y":143.52,"z":10226.95},"end":{"x":1553.66,"y":143.52,"z":10265.41},"length":40.21,"wire_type":"plain_single","category":"single","substation":"NEUTRAL","voltage":0,"file":"scenery_[CP]![IMW].json"},{"start":{"x":1533.38,"y":143.76,"z":10187.23},"end":{"x":1541.91,"y":143.76,"z":10226.95},"length":40.63,"wire_type":"plain_single","category":"single","substation":"NEUTRAL","voltage":0,"file":"scenery_[CP]![IMW].json"},{"start":{"x":1528.89,"y":143.96,"z":10148.36},"end":{"x":1533.38,"y":143.96,"z":10187.23},"length":39.13,"wire_type":"plain_single","categoLine truncated
+29
View File
@@ -110,3 +110,32 @@ export const spriteBounds = {
service_charger: new SpriteBounds(60,120,20,20),
player: new SpriteBounds(20,0,20,20),
}
export const catenaryVoltageColors = {
'1500': '#00d2ff',
'1000': '#ff9f1c',
'750': '#2ec4b6',
'0': '#e63946',
};
export const catenarySubstationColors = {
'CME/IME1500': '#3a86ff',
'CP750': '#06d6a0',
'FF1500': '#8338ec',
'HB1500': '#ff006e',
'IME1000': '#fb5607',
'IMW1500': '#00b4d8',
'OR750': '#52b788',
'SM1500': '#ffd166',
'SM1000': '#f77f00',
'NEUTRAL': '#e63946',
};
export const catenaryWireTypeColors = {
'single': '#48cae4',
'dual': '#0077b6',
'quad': '#7209b7',
'trolley': '#43aa8b',
'side_rail': '#f3722c',
'neutral': '#e63946',
};
+4
View File
@@ -90,6 +90,10 @@
<hr>
Track Colors:
<div id="legendKeyColors"></div>
<hr>
Catenary Network:
<div id="legendCatenaryKey"></div>
<div id="legendCatenaryColors"></div>
</div>
<div class="tabButton" title="Legend">
<svg class="inlineSvg"><use href="#Key"></use></svg>
+190
View File
@@ -28,6 +28,7 @@ document.addEventListener('DOMContentLoaded', async () => {
await loadShopData('shop_items.json');
await loadTrackData('trackdata_dv.json');
await loadPoiData('poi_dv.json');
await loadCatenaryData('catenary_dv.json');
MapData.sortMarkers();
Utils.generateGradeArrows();
@@ -275,6 +276,15 @@ async function loadShopData(file) {
MapData.sortShops();
}
async function loadCatenaryData(file) {
try {
let catData = await (await fetch(new Request(file))).json();
MapData.addCatenaryData(catData);
} catch(e) {
console.error('Failed to load catenary data:', e);
}
}
let railTrack = {};
let curve = {};
@@ -313,6 +323,9 @@ function redrawMap(){
mapctx.stroke();
}
}
drawCatenaryLayer(mapctx, trackWidth);
let curSprite;
let spriteSize;
let textMeasure;
@@ -619,3 +632,180 @@ function redrawDynamics(){
}
MapData.view.dynDirty = false;
}
function drawCatenaryLayer(ctx, trackWidth) {
if(!MapData.layers.catenary) return;
const pixelRatio = MapData.view.pixelRatio;
const scale = MapData.view.scale;
// 1. Contact Wires
if(MapData.layers.catenaryWires && MapData.catenaryPolylines){
let minX = MapData.view.unconvertX(0) - Config.viewCullMargin / scale;
let maxX = MapData.view.unconvertX(mapCanvas.width) + Config.viewCullMargin / scale;
let minZ = MapData.view.unconvertY(mapCanvas.height) - Config.viewCullMargin / scale;
let maxZ = MapData.view.unconvertY(0) + Config.viewCullMargin / scale;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
for(const poly of MapData.catenaryPolylines){
let b = poly.bounds;
if(b[2] < minX || b[0] > maxX || b[3] < minZ || b[1] > maxZ) continue;
let pts = poly.points;
if(!pts || pts.length < 2) continue;
ctx.beginPath();
ctx.moveTo(MapData.view.convertX(pts[0][0]), MapData.view.convertY(pts[0][1]));
for(let i = 1; i < pts.length; i++){
ctx.lineTo(MapData.view.convertX(pts[i][0]), MapData.view.convertY(pts[i][1]));
}
if(poly.is_neutral){
let w = Math.max(3.5 * pixelRatio, trackWidth * 0.7);
ctx.lineWidth = w;
ctx.setLineDash([]);
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.setLineDash([12 * pixelRatio, 12 * pixelRatio]);
ctx.strokeStyle = '#e63946';
ctx.stroke();
ctx.setLineDash([]);
} else if(poly.category === 'side_rail'){
ctx.lineWidth = Math.max(2.5 * pixelRatio, trackWidth * 0.45);
ctx.setLineDash([8 * pixelRatio, 4 * pixelRatio]);
ctx.strokeStyle = poly.color;
ctx.stroke();
ctx.setLineDash([]);
} else if(poly.category === 'quad'){
ctx.lineWidth = Math.max(4 * pixelRatio, trackWidth * 0.85);
ctx.strokeStyle = '#111111';
ctx.stroke();
ctx.lineWidth = Math.max(2.2 * pixelRatio, trackWidth * 0.55);
ctx.strokeStyle = poly.color;
ctx.stroke();
} else if(poly.category === 'dual'){
ctx.lineWidth = Math.max(3.2 * pixelRatio, trackWidth * 0.7);
ctx.strokeStyle = poly.color;
ctx.stroke();
} else if(poly.category === 'trolley'){
ctx.lineWidth = Math.max(1.8 * pixelRatio, trackWidth * 0.4);
ctx.strokeStyle = poly.color;
ctx.stroke();
} else {
ctx.lineWidth = Math.max(2.4 * pixelRatio, trackWidth * 0.55);
ctx.strokeStyle = poly.color;
ctx.stroke();
}
}
}
// 2. Neutral Section Markers
if(MapData.layers.catenaryNeutrals && MapData.catenaryNeutrals){
for(const n of MapData.catenaryNeutrals){
let nx = MapData.view.convertX(n.center.x);
let ny = MapData.view.convertY(n.center.z);
if(nx < -50 || nx > mapCanvas.width + 50 || ny < -50 || ny > mapCanvas.height + 50) continue;
let badgeSize = 13 * pixelRatio;
ctx.beginPath();
ctx.arc(nx, ny, badgeSize, 0, Math.PI * 2);
ctx.fillStyle = '#e63946';
ctx.fill();
ctx.lineWidth = 2 * pixelRatio;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.fillStyle = '#ffffff';
ctx.font = `bold ${10 * pixelRatio}px "Noto Sans"`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('0V', nx, ny);
if(scale >= 0.04){
ctx.font = `bold ${11 * pixelRatio}px "Noto Sans"`;
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2.5 * pixelRatio;
ctx.strokeText(`⛔ ${n.id}`, nx, ny + 18 * pixelRatio);
ctx.fillStyle = '#ffcccc';
ctx.fillText(`⛔ ${n.id}`, nx, ny + 18 * pixelRatio);
}
}
}
// 3. Interruption Markers
if(MapData.layers.catenaryInterruptions && MapData.catenaryInterruptions){
for(const inter of MapData.catenaryInterruptions){
let ix = MapData.view.convertX(inter.center.x);
let iy = MapData.view.convertY(inter.center.z);
if(ix < -50 || ix > mapCanvas.width + 50 || iy < -50 || iy > mapCanvas.height + 50) continue;
let s = 10 * pixelRatio;
ctx.beginPath();
ctx.moveTo(ix, iy - s);
ctx.lineTo(ix + s, iy);
ctx.lineTo(ix, iy + s);
ctx.lineTo(ix - s, iy);
ctx.closePath();
ctx.fillStyle = '#ff9f1c';
ctx.fill();
ctx.lineWidth = 1.5 * pixelRatio;
ctx.strokeStyle = '#000000';
ctx.stroke();
ctx.fillStyle = '#000000';
ctx.font = `bold ${11 * pixelRatio}px "Noto Sans"`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('⤓', ix, iy);
}
}
// 4. Traction Substations
if(MapData.layers.catenarySubstations && MapData.catenarySubstations){
for(const sub of MapData.catenarySubstations){
let sx = MapData.view.convertX(sub.x);
let sy = MapData.view.convertY(sub.z);
if(sx < -80 || sx > mapCanvas.width + 80 || sy < -80 || sy > mapCanvas.height + 80) continue;
let radius = 14 * pixelRatio;
let subColor = sub.nominal_voltage === 1500 ? '#0077b6' : (sub.nominal_voltage === 1000 ? '#e85d04' : '#2b9348');
ctx.beginPath();
ctx.arc(sx, sy, radius + 2 * pixelRatio, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fill();
ctx.beginPath();
ctx.arc(sx, sy, radius, 0, Math.PI * 2);
ctx.fillStyle = subColor;
ctx.fill();
ctx.lineWidth = 2 * pixelRatio;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.fillStyle = '#ffe600';
ctx.font = `bold ${15 * pixelRatio}px "Noto Sans"`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('⚡', sx, sy - 1 * pixelRatio);
if(scale >= 0.03){
let labelText = `${sub.id} (${sub.nominal_voltage}V)`;
ctx.font = `bold ${11 * pixelRatio}px "Noto Sans"`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 3 * pixelRatio;
ctx.strokeText(labelText, sx, sy + radius + 3 * pixelRatio);
ctx.fillStyle = '#ffffff';
ctx.fillText(labelText, sx, sy + radius + 3 * pixelRatio);
}
}
}
ctx.textAlign = 'start';
ctx.textBaseline = 'alphabetic';
}
+197 -5
View File
@@ -88,11 +88,30 @@ export let view = {
}
}
export let layers = {};
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(){
@@ -572,6 +591,56 @@ export function testTooltip(cursorX, cursorY){
}
if(finalTooltip != '') return finalTooltip;
if(layers.catenary && layers.catenarySubstations){
for(const sub of catenarySubstations){
let radius = 24 / view.scale;
if(Math.pow(sub.x - cursorX, 2) + Math.pow(sub.z - cursorY, 2) <= radius * radius){
return sub.tooltip;
}
}
}
if(layers.catenary && layers.catenaryNeutrals){
for(const n of catenaryNeutrals){
let radius = 35 / view.scale;
if(Math.pow(n.center.x - cursorX, 2) + Math.pow(n.center.z - cursorY, 2) <= radius * radius){
return n.tooltip;
}
}
}
if(layers.catenary && layers.catenaryInterruptions){
for(const inter of catenaryInterruptions){
let radius = 24 / view.scale;
if(Math.pow(inter.center.x - cursorX, 2) + Math.pow(inter.center.z - cursorY, 2) <= radius * radius){
return inter.tooltip;
}
}
}
if(layers.catenary && layers.catenaryWires){
let bestWireDistSq = Infinity;
let wireTooltip = '';
let wireBoundsPadding = 25 / view.scale;
for(const poly of catenaryPolylines){
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;
}
}
}
}
let maxWireDist = Math.min(Math.max(1.2, 12 / view.scale), 80);
if(bestWireDistSq <= maxWireDist * maxWireDist){
return wireTooltip;
}
}
let bestDistance = Infinity;
let test = {};
let boundsPadding = 40/view.scale;
@@ -684,16 +753,139 @@ export function setTrackColorMode(mode){
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){
inter.tooltip = `<h1>⤓ Прерывание контактной сети</h1>` +
`Знаки: ${(inter.signs || []).join(', ')}\n` +
`Внимание: Опустить токоприемники (Drop Pantographs)!`;
}
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} звеньев)`;
}
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){
if(!address.startsWith('http://')){
address.replaceAll(/.*:\/\//g, '');
address = 'http://' + address;
let cleanAddress = (address || '').trim().replace(/^https?:\/\//i, '').replace(/\/+$/, '');
cleanAddress = 'http://' + cleanAddress;
if(!cleanAddress.endsWith('/location')){
cleanAddress += '/location';
}
address += '/location';
address = cleanAddress;
console.log(`Connecting to game at ${address}`);
let started = false;
const endConnect = () => {
+79 -5
View File
@@ -5,7 +5,7 @@ import * as Config from './config.js';
import { Color } from './js/colorlib.js';
import * as MapData from './js/mapdata.js';
const settingsVersion = 1;
const settingsVersion = 2;
let settingEntries = [
{
@@ -17,6 +17,72 @@ let settingEntries = [
}
}
},
{
label: 'Catenary Network',
id: 'toggle_catenary',
saveState: true,
state: true,
func: state =>{
MapData.layers.catenary = state;
MapData.view.dirty = true;
},
children: [
{
label: 'Catenary Color Mode',
id: 'dropdown_catenaryColor',
saveState: true,
options: [
'Voltage',
'Substation',
'Wire Type',
],
state: 'Voltage',
func: state =>{
MapData.setCatenaryColorMode(state);
}
},
{
label: 'Contact Wires',
id: 'toggle_catenaryWires',
saveState: true,
state: true,
func: state =>{
MapData.layers.catenaryWires = state;
MapData.view.dirty = true;
}
},
{
label: 'Traction Substations',
id: 'toggle_catenarySubstations',
saveState: true,
state: true,
func: state =>{
MapData.layers.catenarySubstations = state;
MapData.view.dirty = true;
}
},
{
label: 'Neutral Sections (Токоразделы)',
id: 'toggle_catenaryNeutrals',
saveState: true,
state: true,
func: state =>{
MapData.layers.catenaryNeutrals = state;
MapData.view.dirty = true;
}
},
{
label: 'Interruption Signs',
id: 'toggle_catenaryInterruptions',
saveState: true,
state: true,
func: state =>{
MapData.layers.catenaryInterruptions = state;
MapData.view.dirty = true;
}
}
]
},
{
label: 'Color Mode',
id: 'dropdown_trackColor',
@@ -602,10 +668,18 @@ function populateKey(){
addKeyEntry('garage', 'Garage');
addKeyEntry('demonstrator', 'Demonstrator Spawn');
addKeyEntry('player', 'Player');
//Add empty entries to make entries align right in the columns.
/*for(let i=0; i<1; i++){
addKeyEntry();
}*/
populateCatenaryKey();
}
function populateCatenaryKey(){
const catKey = document.getElementById('legendCatenaryKey');
if(!catKey) return;
catKey.innerHTML = `
<div style="margin: 3px 0;"><span style="display:inline-block;width:1.2em;height:1.2em;background:#0077b6;border-radius:50%;text-align:center;line-height:1.2em;font-size:11px;color:#fff;border:1px solid #fff;">⚡</span> Traction Substation</div>
<div style="margin: 3px 0;"><span style="display:inline-block;width:1.2em;height:1.2em;background:#e63946;border-radius:3px;text-align:center;line-height:1.2em;font-size:10px;color:#fff;border:1px dashed #fff;">⛔</span> Neutral Section (0V Токораздел)</div>
<div style="margin: 3px 0;"><span style="display:inline-block;width:1.2em;height:1.2em;background:#ff9f1c;border-radius:3px;text-align:center;line-height:1.2em;font-size:11px;color:#000;border:1px solid #000;">⤓</span> Lower Pantographs Warning</div>
`;
MapData.updateCatenaryLegend();
}
/**