Add Catenary DC electrification layer, CommunityMapLocation fixes, and CI/CD
Deploy & Release / build-and-deploy (push) Successful in 1m3s
Deploy & Release / build-and-deploy (push) Successful in 1m3s
This commit is contained in:
11 files changed
+660
-20
No files matched your search
@@ -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
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 = () => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user