Fix catenary layer toggles, tooltip collision, side rail rendering, and typed interruption signs
Deploy & Release / build-and-deploy (push) Failing after 14s

This commit is contained in:
m4xx1m committed 2026-10-21 05:12:42 +03:00
1 parent 68d5058106
commit 0301fde1a9
5 files changed
+186 -75

No files matched your search

+5
View File
@@ -46,10 +46,15 @@ jobs:
js/ js/
terrains/ terrains/
- name: Package Web Map Zip
run: |
zip -r dv-community-map-web.zip 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 - name: Create Gitea Release
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
uses: akkuman/gitea-release-action@v1 uses: akkuman/gitea-release-action@v1
with: with:
files: |- files: |-
dv-community-map-web.zip
catenary_dv.json catenary_dv.json
trackdata_dv.json trackdata_dv.json
+3 -3
View File
@@ -25,9 +25,9 @@ Interactive vector web map of Derail Valley railway network with DC electrificat
- `Wire Type`: Color-coded by physical suspension category. - `Wire Type`: Color-coded by physical suspension category.
- **Markers & Tooltips**: - **Markers & Tooltips**:
- Traction Substations: `⚡` badge with nominal voltage, supply voltage, max load (kW), and regenerative inverter presence. - 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. - Neutral Sections: `⛔ 0V` center badge with section ID, length, connecting substations, disconnect traction warning, and entrance/exit boundary signs (`NeutralBegin`, `NeutralEnd`, `NeutralWarning`) on high zoom.
- Interruption Signs: `⤓` diamond badge for pantograph drop/raise warnings. - Interruption Signs: Typed diamond badges distinguishing Drop Pantographs (`⤓`), Advance Warning (`⚠`), and Raise Pantographs (`⤒`).
- Contact Wires: Nearest-point hovering displaying voltage, substation feed, category, and segment length. - Integrated Tooltips: Hovering over electrified track curves provides unified telemetry displaying both track geometry (grade, curve radius, speed limit, altitude) and catenary network specs (voltage, suspension type, feeding substation, section length).
## CommunityMapLocation Mod Integration ## CommunityMapLocation Mod Integration
+81 -19
View File
@@ -639,8 +639,8 @@ function drawCatenaryLayer(ctx, trackWidth) {
const pixelRatio = MapData.view.pixelRatio; const pixelRatio = MapData.view.pixelRatio;
const scale = MapData.view.scale; const scale = MapData.view.scale;
// 1. Contact Wires // 1. Contact Wires & Neutral Lines (decoupled toggles)
if(MapData.layers.catenaryWires && MapData.catenaryPolylines){ if(MapData.catenaryPolylines && (MapData.layers.catenaryWires || MapData.layers.catenaryNeutrals)){
let minX = MapData.view.unconvertX(0) - Config.viewCullMargin / scale; let minX = MapData.view.unconvertX(0) - Config.viewCullMargin / scale;
let maxX = MapData.view.unconvertX(mapCanvas.width) + Config.viewCullMargin / scale; let maxX = MapData.view.unconvertX(mapCanvas.width) + Config.viewCullMargin / scale;
let minZ = MapData.view.unconvertY(mapCanvas.height) - Config.viewCullMargin / scale; let minZ = MapData.view.unconvertY(mapCanvas.height) - Config.viewCullMargin / scale;
@@ -650,6 +650,12 @@ function drawCatenaryLayer(ctx, trackWidth) {
ctx.lineJoin = 'round'; ctx.lineJoin = 'round';
for(const poly of MapData.catenaryPolylines){ for(const poly of MapData.catenaryPolylines){
if(poly.is_neutral){
if(!MapData.layers.catenaryNeutrals) continue;
} else {
if(!MapData.layers.catenaryWires) continue;
}
let b = poly.bounds; let b = poly.bounds;
if(b[2] < minX || b[0] > maxX || b[3] < minZ || b[1] > maxZ) continue; if(b[2] < minX || b[0] > maxX || b[3] < minZ || b[1] > maxZ) continue;
@@ -663,7 +669,7 @@ function drawCatenaryLayer(ctx, trackWidth) {
} }
if(poly.is_neutral){ if(poly.is_neutral){
let w = Math.max(3.5 * pixelRatio, trackWidth * 0.7); let w = Math.max(3.8 * pixelRatio, trackWidth * 0.75);
ctx.lineWidth = w; ctx.lineWidth = w;
ctx.setLineDash([]); ctx.setLineDash([]);
ctx.strokeStyle = '#ffffff'; ctx.strokeStyle = '#ffffff';
@@ -674,20 +680,23 @@ function drawCatenaryLayer(ctx, trackWidth) {
ctx.stroke(); ctx.stroke();
ctx.setLineDash([]); ctx.setLineDash([]);
} else if(poly.category === 'side_rail'){ } else if(poly.category === 'side_rail'){
ctx.lineWidth = Math.max(2.5 * pixelRatio, trackWidth * 0.45); ctx.lineWidth = Math.max(3.0 * pixelRatio, trackWidth * 0.55);
ctx.setLineDash([8 * pixelRatio, 4 * pixelRatio]); ctx.setLineDash([10 * pixelRatio, 5 * pixelRatio]);
ctx.strokeStyle = '#111111';
ctx.stroke();
ctx.lineWidth = Math.max(1.8 * pixelRatio, trackWidth * 0.35);
ctx.strokeStyle = poly.color; ctx.strokeStyle = poly.color;
ctx.stroke(); ctx.stroke();
ctx.setLineDash([]); ctx.setLineDash([]);
} else if(poly.category === 'quad'){ } else if(poly.category === 'quad'){
ctx.lineWidth = Math.max(4 * pixelRatio, trackWidth * 0.85); ctx.lineWidth = Math.max(4.2 * pixelRatio, trackWidth * 0.85);
ctx.strokeStyle = '#111111'; ctx.strokeStyle = '#111111';
ctx.stroke(); ctx.stroke();
ctx.lineWidth = Math.max(2.2 * pixelRatio, trackWidth * 0.55); ctx.lineWidth = Math.max(2.4 * pixelRatio, trackWidth * 0.55);
ctx.strokeStyle = poly.color; ctx.strokeStyle = poly.color;
ctx.stroke(); ctx.stroke();
} else if(poly.category === 'dual'){ } else if(poly.category === 'dual'){
ctx.lineWidth = Math.max(3.2 * pixelRatio, trackWidth * 0.7); ctx.lineWidth = Math.max(3.4 * pixelRatio, trackWidth * 0.7);
ctx.strokeStyle = poly.color; ctx.strokeStyle = poly.color;
ctx.stroke(); ctx.stroke();
} else if(poly.category === 'trolley'){ } else if(poly.category === 'trolley'){
@@ -702,13 +711,12 @@ function drawCatenaryLayer(ctx, trackWidth) {
} }
} }
// 2. Neutral Section Markers // 2. Neutral Section Markers & Detail Signs
if(MapData.layers.catenaryNeutrals && MapData.catenaryNeutrals){ if(MapData.layers.catenaryNeutrals && MapData.catenaryNeutrals){
for(const n of MapData.catenaryNeutrals){ for(const n of MapData.catenaryNeutrals){
let nx = MapData.view.convertX(n.center.x); let nx = MapData.view.convertX(n.center.x);
let ny = MapData.view.convertY(n.center.z); let ny = MapData.view.convertY(n.center.z);
if(nx < -50 || nx > mapCanvas.width + 50 || ny < -50 || ny > mapCanvas.height + 50) continue; if(nx >= -50 && nx <= mapCanvas.width + 50 && ny >= -50 && ny <= mapCanvas.height + 50){
let badgeSize = 13 * pixelRatio; let badgeSize = 13 * pixelRatio;
ctx.beginPath(); ctx.beginPath();
ctx.arc(nx, ny, badgeSize, 0, Math.PI * 2); ctx.arc(nx, ny, badgeSize, 0, Math.PI * 2);
@@ -724,7 +732,7 @@ function drawCatenaryLayer(ctx, trackWidth) {
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.fillText('0V', nx, ny); ctx.fillText('0V', nx, ny);
if(scale >= 0.04){ if(scale >= 0.035){
ctx.font = `bold ${11 * pixelRatio}px "Noto Sans"`; ctx.font = `bold ${11 * pixelRatio}px "Noto Sans"`;
ctx.strokeStyle = '#000000'; ctx.strokeStyle = '#000000';
ctx.lineWidth = 2.5 * pixelRatio; ctx.lineWidth = 2.5 * pixelRatio;
@@ -733,33 +741,87 @@ function drawCatenaryLayer(ctx, trackWidth) {
ctx.fillText(`⛔ ${n.id}`, nx, ny + 18 * pixelRatio); ctx.fillText(`⛔ ${n.id}`, nx, ny + 18 * pixelRatio);
} }
} }
// High zoom boundary signs (NeutralWarning, NeutralBegin, NeutralEnd)
if(scale >= 0.04 && n.signs){
for(const sign of n.signs){
let sx = MapData.view.convertX(sign.x);
let sy = MapData.view.convertY(sign.z);
if(sx < -20 || sx > mapCanvas.width + 20 || sy < -20 || sy > mapCanvas.height + 20) continue;
let isWarn = sign.type.includes('Warning');
let isBegin = sign.type.includes('Begin');
let signColor = isWarn ? '#f59e0b' : (isBegin ? '#e63946' : '#10b981');
let signSymbol = isWarn ? '⚠' : (isBegin ? '⛔' : '✓');
let r = 8 * pixelRatio;
ctx.beginPath();
ctx.arc(sx, sy, r, 0, Math.PI * 2);
ctx.fillStyle = signColor;
ctx.fill();
ctx.lineWidth = 1.2 * pixelRatio;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.fillStyle = isWarn ? '#000000' : '#ffffff';
ctx.font = `bold ${8 * pixelRatio}px "Noto Sans"`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(signSymbol, sx, sy);
if(scale >= 0.08){
ctx.font = `${9 * pixelRatio}px "Noto Sans"`;
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2 * pixelRatio;
ctx.strokeText(sign.type, sx, sy - 12 * pixelRatio);
ctx.fillStyle = '#ffffff';
ctx.fillText(sign.type, sx, sy - 12 * pixelRatio);
}
}
}
}
} }
// 3. Interruption Markers // 3. Interruption Markers
if(MapData.layers.catenaryInterruptions && MapData.catenaryInterruptions){ if(MapData.layers.catenaryInterruptions && MapData.catenaryInterruptions && scale >= 0.015){
for(const inter of MapData.catenaryInterruptions){ for(const inter of MapData.catenaryInterruptions){
let ix = MapData.view.convertX(inter.center.x); let ix = MapData.view.convertX(inter.center.x);
let iy = MapData.view.convertY(inter.center.z); let iy = MapData.view.convertY(inter.center.z);
if(ix < -50 || ix > mapCanvas.width + 50 || iy < -50 || iy > mapCanvas.height + 50) continue; if(ix < -50 || ix > mapCanvas.width + 50 || iy < -50 || iy > mapCanvas.height + 50) continue;
let s = 10 * pixelRatio; let kind = inter.kind || 'drop';
let fillColor = kind === 'warning' ? '#f59e0b' : (kind === 'raise' ? '#10b981' : '#f97316');
let textColor = kind === 'warning' ? '#000000' : '#ffffff';
let icon = kind === 'warning' ? '⚠' : (kind === 'raise' ? '⤒' : '⤓');
let s = Math.max(7 * pixelRatio, Math.min(11 * pixelRatio, 220 * scale * pixelRatio));
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(ix, iy - s); ctx.moveTo(ix, iy - s);
ctx.lineTo(ix + s, iy); ctx.lineTo(ix + s, iy);
ctx.lineTo(ix, iy + s); ctx.lineTo(ix, iy + s);
ctx.lineTo(ix - s, iy); ctx.lineTo(ix - s, iy);
ctx.closePath(); ctx.closePath();
ctx.fillStyle = '#ff9f1c'; ctx.fillStyle = fillColor;
ctx.fill(); ctx.fill();
ctx.lineWidth = 1.5 * pixelRatio; ctx.lineWidth = 1.5 * pixelRatio;
ctx.strokeStyle = '#000000'; ctx.strokeStyle = '#000000';
ctx.stroke(); ctx.stroke();
ctx.fillStyle = '#000000'; ctx.fillStyle = textColor;
ctx.font = `bold ${11 * pixelRatio}px "Noto Sans"`; ctx.font = `bold ${Math.round(s * 1.05)}px "Noto Sans"`;
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.fillText('⤓', ix, iy); ctx.fillText(icon, ix, iy);
if(scale >= 0.045){
let desc = kind === 'warning' ? '⚠ Разрыв КС' : (kind === 'raise' ? '⤒ Подъем пантографа' : '⤓ Опустить пантограф');
ctx.font = `bold ${10 * pixelRatio}px "Noto Sans"`;
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2.5 * pixelRatio;
ctx.strokeText(desc, ix, iy + s + 7 * pixelRatio);
ctx.fillStyle = fillColor;
ctx.fillText(desc, ix, iy + s + 7 * pixelRatio);
}
} }
} }
@@ -792,7 +854,7 @@ function drawCatenaryLayer(ctx, trackWidth) {
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.fillText('⚡', sx, sy - 1 * pixelRatio); ctx.fillText('⚡', sx, sy - 1 * pixelRatio);
if(scale >= 0.03){ if(scale >= 0.025){
let labelText = `${sub.id} (${sub.nominal_voltage}V)`; let labelText = `${sub.id} (${sub.nominal_voltage}V)`;
ctx.font = `bold ${11 * pixelRatio}px "Noto Sans"`; ctx.font = `bold ${11 * pixelRatio}px "Noto Sans"`;
ctx.textAlign = 'center'; ctx.textAlign = 'center';
+92 -50
View File
@@ -565,6 +565,35 @@ export function testTooltip(cursorX, cursorY){
} }
if(finalTooltip != '') return 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){ for(const marker of markers){
if(!marker.visible) continue; if(!marker.visible) continue;
if(marker.boundsCheck != null){ if(marker.boundsCheck != null){
@@ -591,38 +620,35 @@ export function testTooltip(cursorX, cursorY){
} }
if(finalTooltip != '') return finalTooltip; if(finalTooltip != '') return finalTooltip;
if(layers.catenary && layers.catenarySubstations){ // 2. Rail tracks curve check
for(const sub of catenarySubstations){ let bestDistance = Infinity;
let radius = 24 / view.scale; let trackTooltip = '';
if(Math.pow(sub.x - cursorX, 2) + Math.pow(sub.z - cursorY, 2) <= radius * radius){ let boundsPadding = 40 / view.scale;
return sub.tooltip; 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);
if(layers.catenary && layers.catenaryNeutrals){ // 3. Catenary wire check
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 wireTooltip = '';
let wireTooltipCompact = '';
if(layers.catenary && (layers.catenaryWires || layers.catenaryNeutrals)){
let bestWireDistSq = Infinity;
let wireBoundsPadding = 25 / view.scale; let wireBoundsPadding = 25 / view.scale;
for(const poly of catenaryPolylines){ for(const poly of catenaryPolylines){
if(poly.is_neutral && !layers.catenaryNeutrals) continue;
if(!poly.is_neutral && !layers.catenaryWires) continue;
let b = poly.bounds; let b = poly.bounds;
if(cursorX >= b[0] - wireBoundsPadding && cursorX <= b[2] + wireBoundsPadding && cursorY >= b[1] - wireBoundsPadding && cursorY <= b[3] + wireBoundsPadding){ if(cursorX >= b[0] - wireBoundsPadding && cursorX <= b[2] + wireBoundsPadding && cursorY >= b[1] - wireBoundsPadding && cursorY <= b[3] + wireBoundsPadding){
let pts = poly.points; let pts = poly.points;
@@ -631,35 +657,30 @@ export function testTooltip(cursorX, cursorY){
if(d2 < bestWireDistSq){ if(d2 < bestWireDistSq){
bestWireDistSq = d2; bestWireDistSq = d2;
wireTooltip = poly.tooltip; wireTooltip = poly.tooltip;
wireTooltipCompact = poly.tooltipCompact || poly.tooltip;
} }
} }
} }
} }
let maxWireDist = Math.min(Math.max(1.2, 12 / view.scale), 80); let maxWireDist = Math.min(Math.max(1.2, 12 / view.scale), 50);
if(bestWireDistSq <= maxWireDist * maxWireDist){ 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 wireTooltip;
} }
}
let bestDistance = Infinity; return '';
let test = {};
let boundsPadding = 40/view.scale;
for(const railTrack of railTracks){
if(cursorX >= railTrack.bounds.min.x - boundsPadding && cursorX <= railTrack.bounds.max.x + boundsPadding && cursorY >= railTrack.bounds.min.z - boundsPadding && cursorY <= railTrack.bounds.max.z + boundsPadding){
for(const curve of railTrack.curves){
if(cursorX >= curve.bounds.min.x - boundsPadding && cursorX <= curve.bounds.max.x + boundsPadding && cursorY >= curve.bounds.min.z - boundsPadding && cursorY <= curve.bounds.max.z + boundsPadding){
test = Bezier.getNearestPoint2D(curve.start, curve.h1, curve.h2, curve.end, {x:cursorX, y:cursorY}, Math.floor(curve.length*2));
if(test.distance < bestDistance){
bestDistance = test.distance;
finalTooltip = curve.tooltip;
testPoint = test.point;
}
}
}
}
}
return bestDistance <= Math.min(Math.max(0.9, 12/view.scale), 120) ? finalTooltip : '';
} }
export function setTrackColorMode(mode){ export function setTrackColorMode(mode){
@@ -797,9 +818,27 @@ export function addCatenaryData(data){
} }
for(const inter of catenaryInterruptions){ for(const inter of catenaryInterruptions){
inter.tooltip = `<h1>⤓ Прерывание контактной сети</h1>` + let hasDrop = (inter.signs || []).some(s => s.includes('Drop') && !s.includes('Warning'));
`Знаки: ${(inter.signs || []).join(', ')}\n` + let hasRaise = (inter.signs || []).some(s => s.includes('Raise'));
`Внимание: Опустить токоприемники (Drop Pantographs)!`; 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){ for(const p of catenaryPolylines){
@@ -814,6 +853,9 @@ export function addCatenaryData(data){
`Тип подвески: ${catDesc}\n` + `Тип подвески: ${catDesc}\n` +
`Тяговая подстанция: ${p.substation}\n` + `Тяговая подстанция: ${p.substation}\n` +
`Длина участка: ${p.length} м (${p.wire_count} звеньев)`; `Длина участка: ${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); setCatenaryColorMode(catenaryColorMode);
+5 -3
View File
@@ -675,9 +675,11 @@ function populateCatenaryKey(){
const catKey = document.getElementById('legendCatenaryKey'); const catKey = document.getElementById('legendCatenaryKey');
if(!catKey) return; if(!catKey) return;
catKey.innerHTML = ` 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.3em;height:1.3em;background:#0077b6;border-radius:50%;text-align:center;line-height:1.3em;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.3em;height:1.3em;background:#e63946;border-radius:3px;text-align:center;line-height:1.3em;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> <div style="margin: 3px 0;"><span style="display:inline-block;width:1.3em;height:1.3em;background:#f97316;border-radius:3px;text-align:center;line-height:1.3em;font-size:11px;color:#ffffff;border:1px solid #000;">⤓</span> Lower Pantographs (Опустить пантограф)</div>
<div style="margin: 3px 0;"><span style="display:inline-block;width:1.3em;height:1.3em;background:#f59e0b;border-radius:3px;text-align:center;line-height:1.3em;font-size:11px;color:#000;border:1px solid #000;">⚠</span> Interruption Warning (Предупреждение)</div>
<div style="margin: 3px 0;"><span style="display:inline-block;width:1.3em;height:1.3em;background:#10b981;border-radius:3px;text-align:center;line-height:1.3em;font-size:11px;color:#fff;border:1px solid #fff;">⤒</span> Raise Pantographs (Поднять пантограф)</div>
`; `;
MapData.updateCatenaryLegend(); MapData.updateCatenaryLegend();
} }