diff --git a/backend/services/chart_manager.py b/backend/services/chart_manager.py index 60b81aa..671830f 100644 --- a/backend/services/chart_manager.py +++ b/backend/services/chart_manager.py @@ -805,18 +805,27 @@ def _build_cache(cell_id: str, enc_path: Path): log.info("ENC %s → %d depth features", cell_id, len(depths)) land = _parse_land(enc_path) - with open(cell_dir / "land.geojson", "w") as f: - json.dump({"type": "FeatureCollection", "features": land}, f) - log.info("ENC %s → %d land features", cell_id, len(land)) + land_path = cell_dir / "land.geojson" + # Preserve existing land cache when the .000 has no LNDARE layer + # (e.g. custom charts built from CSV that only contain nav aids) + if land or not land_path.exists(): + with open(land_path, "w") as f: + json.dump({"type": "FeatureCollection", "features": land}, f) + log.info("ENC %s → %d land features%s", cell_id, len(land), + " (preserved existing)" if not land and land_path.exists() else "") hazards = _parse_hazards(enc_path) - with open(cell_dir / "hazards.geojson", "w") as f: - json.dump({"type": "FeatureCollection", "features": hazards}, f) + hazards_path = cell_dir / "hazards.geojson" + if hazards or not hazards_path.exists(): + with open(hazards_path, "w") as f: + json.dump({"type": "FeatureCollection", "features": hazards}, f) log.info("ENC %s → %d hazard features", cell_id, len(hazards)) zones = _parse_zones(enc_path) - with open(cell_dir / "zones.geojson", "w") as f: - json.dump({"type": "FeatureCollection", "features": zones}, f) + zones_path = cell_dir / "zones.geojson" + if zones or not zones_path.exists(): + with open(zones_path, "w") as f: + json.dump({"type": "FeatureCollection", "features": zones}, f) log.info("ENC %s → %d zone features", cell_id, len(zones)) # Cache count and bbox in meta.json so list_cells() doesn't need to read features.geojson diff --git a/frontend/js/map.js b/frontend/js/map.js index 27bc37f..81d5218 100644 --- a/frontend/js/map.js +++ b/frontend/js/map.js @@ -1215,105 +1215,54 @@ function _encBeaconCanvas(colours, catlam, region, sz = 44) { return c; } -// ── Range / Leading-mark dayboard — dos triángulos concéntricos 3-D ───────── -// Outer triangle: WHITE (visible against any background) -// Inner triangle: BLACK (center — classic range-mark dayboard pattern) -// Both point UP. Mounted on a 3-D post with tripod base and optional light flare. -// This symbol is used for ANY LIGHTS/LNDMRK feature that carries an ORIENT -// attribute, meaning it defines a leading-line bearing. -function _encRangeDaymarkCanvas(colours, hasLight, sz = 52) { +// ── Range / Leading-mark dayboard — dos triángulos concéntricos ────────────── +// Símbolo estándar IALA: triángulo exterior BLANCO + triángulo interior NEGRO. +// Ambos apuntan hacia arriba. Sin patas, sin mástil — solo la marca diurna. +// Flare de luz en el ápice si la feature tiene LITCHR. +function _encRangeDaymarkCanvas(colours, hasLight, sz = 48) { const c = _mkC(sz); const ctx = c.getContext('2d'); const cx = sz / 2; - // ── Layout ────────────────────────────────────────────────────────────── - const groundY = sz * 0.92; - const legHub = sz * 0.72; - const poleTop = sz * 0.42; // dayboard starts here (base of board) - const boardBot = poleTop; - const boardTop = sz * 0.08; // apex of outer triangle - const boardW = sz * 0.80; // max width of outer triangle at its base - - // Light colour (top dot) from feature colour code + // Light colour for optional flare const lightCss = colours[0] ? _s57css(colours[0]) : '#ffffff'; - // ── Ground shadow ──────────────────────────────────────────────────────── - ctx.beginPath(); ctx.ellipse(cx, groundY, sz * 0.20, sz * 0.04, 0, 0, Math.PI * 2); - ctx.fillStyle = 'rgba(0,0,0,0.22)'; ctx.fill(); + // ── Layout ─────────────────────────────────────────────────────────────── + const triTop = sz * 0.06; // apex of outer triangle + const triBot = sz * 0.92; // base of outer triangle + const halfW = sz * 0.44; // half-width at base - // ── Tripod base ────────────────────────────────────────────────────────── - // Back stub - ctx.beginPath(); ctx.moveTo(cx, legHub); ctx.lineTo(cx + sz*0.06, groundY * 0.97); - ctx.strokeStyle = '#555'; ctx.lineWidth = 1.0; ctx.stroke(); - // Left leg - ctx.beginPath(); ctx.moveTo(cx, legHub); ctx.lineTo(cx - sz*0.26, groundY); - ctx.strokeStyle = '#666'; ctx.lineWidth = 1.3; ctx.stroke(); - ctx.strokeStyle = '#aaa'; ctx.lineWidth = 0.4; ctx.stroke(); - // Right leg - ctx.beginPath(); ctx.moveTo(cx, legHub); ctx.lineTo(cx + sz*0.26, groundY); - ctx.strokeStyle = '#555'; ctx.lineWidth = 1.3; ctx.stroke(); - ctx.strokeStyle = '#999'; ctx.lineWidth = 0.4; ctx.stroke(); - - // ── Hub ring ───────────────────────────────────────────────────────────── - ctx.beginPath(); ctx.ellipse(cx, legHub, sz*0.09, sz*0.030, 0, 0, Math.PI*2); - const hubG = ctx.createRadialGradient(cx - sz*0.04, legHub, 1, cx, legHub, sz*0.09); - hubG.addColorStop(0, '#ccc'); hubG.addColorStop(1, '#555'); - ctx.fillStyle = hubG; ctx.fill(); - ctx.strokeStyle = '#333'; ctx.lineWidth = 0.4; ctx.stroke(); - - // ── Mast / pole ─────────────────────────────────────────────────────────── - const pw = sz * 0.07; - const mastG = ctx.createLinearGradient(cx - pw/2, 0, cx + pw/2, 0); - mastG.addColorStop(0, '#999'); mastG.addColorStop(0.3, '#eee'); mastG.addColorStop(1, '#666'); - ctx.fillStyle = mastG; - ctx.fillRect(cx - pw/2, poleTop, pw, legHub - poleTop); - ctx.strokeStyle = '#333'; ctx.lineWidth = 0.4; - ctx.strokeRect(cx - pw/2, poleTop, pw, legHub - poleTop); - - // ── OUTER triangle (WHITE) — 3-D face with gradient ────────────────────── - // The triangle board catches sunlight from upper-left → left face lighter, - // right face darker, giving it a 3-D planar look. - const outerGrad = ctx.createLinearGradient(cx - boardW/2, 0, cx + boardW/2, 0); - outerGrad.addColorStop(0, 'rgba(255,255,255,1.00)'); // lit left edge - outerGrad.addColorStop(0.40,'rgba(240,240,240,1.00)'); // face - outerGrad.addColorStop(1, 'rgba(190,190,190,1.00)'); // shadow right + // ── OUTER triangle — WHITE ─────────────────────────────────────────────── ctx.beginPath(); - ctx.moveTo(cx, boardTop); - ctx.lineTo(cx + boardW / 2, boardBot); - ctx.lineTo(cx - boardW / 2, boardBot); + ctx.moveTo(cx, triTop); + ctx.lineTo(cx + halfW, triBot); + ctx.lineTo(cx - halfW, triBot); ctx.closePath(); - ctx.fillStyle = outerGrad; ctx.fill(); - // Glow outline - ctx.shadowBlur = 6; ctx.shadowColor = 'rgba(255,255,255,0.8)'; - ctx.strokeStyle = 'rgba(255,255,255,0.9)'; ctx.lineWidth = 1.2; ctx.stroke(); - ctx.shadowBlur = 0; ctx.shadowColor = 'transparent'; - ctx.strokeStyle = '#555'; ctx.lineWidth = 0.7; ctx.stroke(); + ctx.fillStyle = '#ffffff'; + ctx.fill(); + // thin dark border so it reads against light backgrounds + ctx.strokeStyle = '#444'; + ctx.lineWidth = 1.0; + ctx.stroke(); - // ── INNER triangle (BLACK) — 3-D face ──────────────────────────────────── - // Inner tri is 45% the size of the outer, centred on the board face. - // Its apex is at (cx, boardTop + inset) and base is near boardBot. - const iW = boardW * 0.45; - const iTop = boardTop + (boardBot - boardTop) * 0.18; // inset from apex - const iBot = boardBot - (boardBot - boardTop) * 0.08; // inset from base - const innerGrad = ctx.createLinearGradient(cx - iW/2, 0, cx + iW/2, 0); - innerGrad.addColorStop(0, '#3a3a3a'); // lit left face (dark but not pure black) - innerGrad.addColorStop(0.45,'#111111'); // center - innerGrad.addColorStop(1, '#000000'); // shadow right + // ── INNER triangle — BLACK (45 % of outer) ─────────────────────────────── + const iS = 0.45; + const triH = triBot - triTop; + const iTop = triTop + triH * (1 - iS) * 0.50; + const iBot = triBot - triH * (1 - iS) * 0.15; + const iHalf = halfW * iS * ((iBot - triTop) / triH); ctx.beginPath(); ctx.moveTo(cx, iTop); - ctx.lineTo(cx + iW / 2, iBot); - ctx.lineTo(cx - iW / 2, iBot); + ctx.lineTo(cx + iHalf, iBot); + ctx.lineTo(cx - iHalf, iBot); ctx.closePath(); - ctx.fillStyle = innerGrad; ctx.fill(); - // Left-edge highlight streak (3-D illusion) - ctx.beginPath(); - ctx.moveTo(cx - iW * 0.04, iTop + (iBot - iTop) * 0.06); - ctx.lineTo(cx - iW * 0.30, iBot - (iBot - iTop) * 0.05); - ctx.strokeStyle = 'rgba(120,120,120,0.55)'; ctx.lineWidth = iW * 0.06; ctx.stroke(); - ctx.strokeStyle = '#000'; ctx.lineWidth = 0.5; - ctx.beginPath(); ctx.moveTo(cx, iTop); ctx.lineTo(cx+iW/2, iBot); ctx.lineTo(cx-iW/2, iBot); ctx.closePath(); ctx.stroke(); + ctx.fillStyle = '#111111'; + ctx.fill(); + ctx.strokeStyle = '#000'; + ctx.lineWidth = 0.5; + ctx.stroke(); - // ── Light flare at apex (if feature has a light) ─────────────────────────── + // ── Light flare at apex ─────────────────────────────────────────────────── if (hasLight) { - _drawLightFlare(ctx, cx, boardTop - sz * 0.02, lightCss); + _drawLightFlare(ctx, cx, triTop - sz * 0.02, lightCss); } return c;