From 0301fde1a91d6656c1583745d80713830754aa3b Mon Sep 17 00:00:00 2001 From: m4xx1m Date: Wed, 21 Oct 2026 05:12:42 +0300 Subject: [PATCH] Fix catenary layer toggles, tooltip collision, side rail rendering, and typed interruption signs --- .gitea/workflows/deploy.yaml | 5 ++ README.md | 6 +- index.js | 136 ++++++++++++++++++++++++---------- js/mapdata.js | 138 +++++++++++++++++++++++------------ legend.js | 8 +- 5 files changed, 202 insertions(+), 91 deletions(-) diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 121af9c..0119a6f 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -46,10 +46,15 @@ jobs: js/ 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 if: startsWith(github.ref, 'refs/tags/') uses: akkuman/gitea-release-action@v1 with: files: |- + dv-community-map-web.zip catenary_dv.json trackdata_dv.json diff --git a/README.md b/README.md index 474752b..a340bb0 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,9 @@ Interactive vector web map of Derail Valley railway network with DC electrificat - `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. + - 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: Typed diamond badges distinguishing Drop Pantographs (`⤓`), Advance Warning (`⚠`), and Raise Pantographs (`⤒`). + - 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 diff --git a/index.js b/index.js index bcdf7ea..89ba6ec 100644 --- a/index.js +++ b/index.js @@ -639,8 +639,8 @@ function drawCatenaryLayer(ctx, trackWidth) { const pixelRatio = MapData.view.pixelRatio; const scale = MapData.view.scale; - // 1. Contact Wires - if(MapData.layers.catenaryWires && MapData.catenaryPolylines){ + // 1. Contact Wires & Neutral Lines (decoupled toggles) + if(MapData.catenaryPolylines && (MapData.layers.catenaryWires || MapData.layers.catenaryNeutrals)){ 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; @@ -650,6 +650,12 @@ function drawCatenaryLayer(ctx, trackWidth) { ctx.lineJoin = 'round'; 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; 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){ - let w = Math.max(3.5 * pixelRatio, trackWidth * 0.7); + let w = Math.max(3.8 * pixelRatio, trackWidth * 0.75); ctx.lineWidth = w; ctx.setLineDash([]); ctx.strokeStyle = '#ffffff'; @@ -674,20 +680,23 @@ function drawCatenaryLayer(ctx, trackWidth) { 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.lineWidth = Math.max(3.0 * pixelRatio, trackWidth * 0.55); + 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.stroke(); ctx.setLineDash([]); } 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.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.stroke(); } 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.stroke(); } else if(poly.category === 'trolley'){ @@ -702,64 +711,117 @@ function drawCatenaryLayer(ctx, trackWidth) { } } - // 2. Neutral Section Markers + // 2. Neutral Section Markers & Detail Signs 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; + if(nx >= -50 && nx <= mapCanvas.width + 50 && ny >= -50 && ny <= mapCanvas.height + 50){ + 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(); - 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); - 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.035){ + 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); + } + } - 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); + // 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 - if(MapData.layers.catenaryInterruptions && MapData.catenaryInterruptions){ + if(MapData.layers.catenaryInterruptions && MapData.catenaryInterruptions && scale >= 0.015){ 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; + 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.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.fillStyle = fillColor; ctx.fill(); ctx.lineWidth = 1.5 * pixelRatio; ctx.strokeStyle = '#000000'; ctx.stroke(); - ctx.fillStyle = '#000000'; - ctx.font = `bold ${11 * pixelRatio}px "Noto Sans"`; + ctx.fillStyle = textColor; + ctx.font = `bold ${Math.round(s * 1.05)}px "Noto Sans"`; ctx.textAlign = 'center'; 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.fillText('⚡', sx, sy - 1 * pixelRatio); - if(scale >= 0.03){ + if(scale >= 0.025){ let labelText = `${sub.id} (${sub.nominal_voltage}V)`; ctx.font = `bold ${11 * pixelRatio}px "Noto Sans"`; ctx.textAlign = 'center'; diff --git a/js/mapdata.js b/js/mapdata.js index e866358..408776d 100644 --- a/js/mapdata.js +++ b/js/mapdata.js @@ -565,6 +565,35 @@ export function testTooltip(cursorX, cursorY){ } 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){ @@ -591,38 +620,35 @@ 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; + // 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); - 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){ + // 3. Catenary wire check + let wireTooltip = ''; + let wireTooltipCompact = ''; + if(layers.catenary && (layers.catenaryWires || layers.catenaryNeutrals)){ let bestWireDistSq = Infinity; - let wireTooltip = ''; 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; @@ -631,35 +657,30 @@ export function testTooltip(cursorX, cursorY){ if(d2 < bestWireDistSq){ bestWireDistSq = d2; wireTooltip = poly.tooltip; + wireTooltipCompact = poly.tooltipCompact || poly.tooltip; } } } } - let maxWireDist = Math.min(Math.max(1.2, 12 / view.scale), 80); - if(bestWireDistSq <= maxWireDist * maxWireDist){ - return wireTooltip; + let maxWireDist = Math.min(Math.max(1.2, 12 / view.scale), 50); + if(bestWireDistSq > maxWireDist * maxWireDist){ + wireTooltip = ''; + wireTooltipCompact = ''; } } - let bestDistance = Infinity; - let test = {}; - let boundsPadding = 40/view.scale; - for(const railTrack of railTracks){ - if(cursorX >= railTrack.bounds.min.x - boundsPadding && cursorX <= railTrack.bounds.max.x + boundsPadding && cursorY >= railTrack.bounds.min.z - boundsPadding && cursorY <= railTrack.bounds.max.z + boundsPadding){ - for(const curve of railTrack.curves){ - if(cursorX >= curve.bounds.min.x - boundsPadding && cursorX <= curve.bounds.max.x + boundsPadding && cursorY >= curve.bounds.min.z - boundsPadding && cursorY <= curve.bounds.max.z + boundsPadding){ - test = Bezier.getNearestPoint2D(curve.start, curve.h1, curve.h2, curve.end, {x:cursorX, y:cursorY}, Math.floor(curve.length*2)); - if(test.distance < bestDistance){ - bestDistance = test.distance; - finalTooltip = curve.tooltip; - testPoint = test.point; - } - } - } - } + // 4. Combined or individual resolution + if(trackHit && wireTooltipCompact){ + return `${trackTooltip}\n
\n${wireTooltipCompact}`; + } + if(trackHit){ + return trackTooltip; + } + if(wireTooltip){ + return wireTooltip; } - return bestDistance <= Math.min(Math.max(0.9, 12/view.scale), 120) ? finalTooltip : ''; + return ''; } export function setTrackColorMode(mode){ @@ -797,9 +818,27 @@ export function addCatenaryData(data){ } for(const inter of catenaryInterruptions){ - inter.tooltip = `

⤓ Прерывание контактной сети

` + - `Знаки: ${(inter.signs || []).join(', ')}\n` + - `Внимание: Опустить токоприемники (Drop Pantographs)!`; + 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 = `

${title}

` + + `Действие: ${action}\n` + + `Знаки: ${(inter.signs || []).join(', ')}`; } for(const p of catenaryPolylines){ @@ -814,6 +853,9 @@ export function addCatenaryData(data){ `Тип подвески: ${catDesc}\n` + `Тяговая подстанция: ${p.substation}\n` + `Длина участка: ${p.length} м (${p.wire_count} звеньев)`; + + p.tooltipCompact = `
⚡ Контактная сеть: ${p.is_neutral ? 'Токораздел (0 В)' : p.voltage + ' В DC'}
` + + `Подвеска: ${catDesc}\nПодстанция: ${p.substation} | Длина: ${p.length} м`; } setCatenaryColorMode(catenaryColorMode); diff --git a/legend.js b/legend.js index 6a34acc..7d881fe 100644 --- a/legend.js +++ b/legend.js @@ -675,9 +675,11 @@ function populateCatenaryKey(){ const catKey = document.getElementById('legendCatenaryKey'); if(!catKey) return; catKey.innerHTML = ` -
⚡ Traction Substation
-
⛔ Neutral Section (0V Токораздел)
-
⤓ Lower Pantographs Warning
+
⚡ Traction Substation (Тяговая подстанция)
+
⛔ Neutral Section (0V Токораздел)
+
⤓ Lower Pantographs (Опустить пантограф)
+
⚠ Interruption Warning (Предупреждение)
+
⤒ Raise Pantographs (Поднять пантограф)
`; MapData.updateCatenaryLegend(); }