From 293b0c45efd835c10775a1c2602acc038b65be69 Mon Sep 17 00:00:00 2001 From: aerom Date: Thu, 30 Apr 2026 13:55:08 -0400 Subject: [PATCH] fix(data): light info (LITCHR/range/height) en boyas y balizas de orilla frontend/js/map.js: - Reemplaza merge exacto por merge de proximidad (~50m) en loadChartFeatures para capturar pares LIGHTS/BOYLAT con coordenadas no exactamente iguales - Guard null-canvas en encStyle con fallback visible + console.warn - Mejora JS de debug: log layer/aidType cuando usa fallback backend/services/chart_manager.py: - Expande extraccion de light_desc a category buoy+beacon+landmark (antes solo BOYLAT/BOYCAR; BCNLAT/BCNWTW/LNDMRK perdian LITCHR silenciosamente) --- backend/services/chart_manager.py | 5 ++- frontend/js/map.js | 69 +++++++++++++++++++++++-------- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/backend/services/chart_manager.py b/backend/services/chart_manager.py index 92d1899..60b81aa 100644 --- a/backend/services/chart_manager.py +++ b/backend/services/chart_manager.py @@ -271,7 +271,10 @@ def _parse_cell(enc_path: Path) -> list[dict]: "colours": colours, # list of S-57 colour codes "colour_code": colours[0] if colours else None, # primary colour } - if category == "light" or layer_name in ("BOYLAT","BOYCAR","BOYISD","BOYSPP","BOYSAW"): + # Extract light description for ALL navigational aids and lights. + # Previously only BOY* layers were included, so BCNLAT/BCNWTW/LNDMRK + # silently lost their LITCHR/SIGPER/VALNMR — fixed here. + if category in ("light", "buoy", "beacon", "landmark"): props["light_desc"] = _light_desc(row) if layer_name.startswith("BOY") or layer_name.startswith("BCN"): shape = _safe(row.get("BOYSHP")) diff --git a/frontend/js/map.js b/frontend/js/map.js index eb1738d..b2e9df0 100644 --- a/frontend/js/map.js +++ b/frontend/js/map.js @@ -1645,6 +1645,18 @@ function encStyle(feature, resolution) { } } + // Safety net: if any draw function threw, canvas may be null. Fall back to + // a small colored circle so the feature is still visible and clickable. + if (!canvas) { + const fc = document.createElement('canvas'); fc.width = fc.height = 16; + const fx = fc.getContext('2d'); + fx.beginPath(); fx.arc(8, 8, 7, 0, Math.PI * 2); + fx.fillStyle = colours[0] ? _s57css(colours[0]) : '#607d8b'; fx.fill(); + fx.strokeStyle = '#fff'; fx.lineWidth = 1.5; fx.stroke(); + canvas = fc; + console.warn('[ENC] canvas fallback for', layer, aidType, colours); + } + // Compute zoom from resolution (deterministic, no map object access needed). // OL WebMercator: resolution ≈ 156543 / 2^zoom → zoom = log2(156543/res) const _zr = resolution ? Math.log2(156543.03392 / Math.max(resolution, 0.001)) : 14; @@ -2005,46 +2017,69 @@ window.loadChartFeatures = async function() { const fc = await r.json(); if (!fc.features?.length) return; - // Merge LIGHTS into co-located structures. NOAA codifies a buoy and its - // light as two features at the same coords. The structure often lacks - // COLOUR / CATLAM while the light carries it — we backfill the structure - // with the light's colour, then drop the (now redundant) LIGHT_POINT. - const structByKey = new Map(); // coordKey → structure feature + // ── Merge LIGHTS into nearby structure features ──────────────────────── + // S-57 encodes each buoy/beacon and its light as TWO separate objects. + // They may be at exactly the same coordinates (NOAA pattern) or up to + // ~50 m apart (≈0.00045°). We try exact match first, then proximity. + // After merging: light attrs copied into host, LIGHT_POINT dropped. + const MERGE_DEG = 0.00045; // ~50 m — matches backend proximity radius + const structList = []; // [{ f, x, y }] — all structure features for (const f of fc.features) { const lyr = (f.properties?.layer || '').toUpperCase(); if (lyr.startsWith('BOY') || lyr.startsWith('BCN') || lyr === 'LNDMRK') { - const c = f.geometry.coordinates; - structByKey.set(`${c[0].toFixed(6)}_${c[1].toFixed(6)}`, f); + const [x, y] = f.geometry.coordinates; + structList.push({ f, x, y }); } } + // Fast exact-coord lookup (grid key rounded to 6 dp ≈ 0.11 m) + const structByKey = new Map(); + for (const s of structList) { + structByKey.set(`${s.x.toFixed(6)}_${s.y.toFixed(6)}`, s.f); + } + const before = fc.features.length; fc.features = fc.features.filter(f => { if (f.properties?.aid_type !== 'LIGHT_POINT') return true; - const c = f.geometry.coordinates; - const host = structByKey.get(`${c[0].toFixed(6)}_${c[1].toFixed(6)}`); - if (!host) return true; // standalone light — keep - // Backfill host colour from light if missing + const [lx, ly] = f.geometry.coordinates; + + // 1) Try exact coordinate match + let host = structByKey.get(`${lx.toFixed(6)}_${ly.toFixed(6)}`); + + // 2) Proximity fallback (~50 m radius) + if (!host) { + let bestDist = MERGE_DEG; + for (const s of structList) { + const d = Math.sqrt((lx - s.x) ** 2 + (ly - s.y) ** 2); + if (d < bestDist) { bestDist = d; host = s.f; } + } + } + + if (!host) return true; // standalone light with no nearby structure — keep + const hp = host.properties; const lp = f.properties; + + // Backfill colour from light if host has none if (!hp.colours?.length && lp.colours?.length) { hp.colours = lp.colours.slice(); - // Re-classify host now that we have colour info → infer CATLAM + // Infer CATLAM from colour when missing const region = hp.cell_region || 'B'; const ialaB = region === 'B'; const c0 = lp.colours[0]; if (!hp.catlam) { - if (c0 === 4) hp.catlam = ialaB ? 1 : 2; // green → port in B, stbd in A - else if (c0 === 3) hp.catlam = ialaB ? 2 : 1; // red → stbd in B, port in A + if (c0 === 4) hp.catlam = ialaB ? 1 : 2; + else if (c0 === 3) hp.catlam = ialaB ? 2 : 1; } - // Update aid_type to reflect inferred lateral significance if (hp.catlam === 1) hp.aid_type = 'LATERAL_PORT'; else if (hp.catlam === 2) hp.aid_type = 'LATERAL_STBD'; } - // Carry light attributes (character, range, height) into the host for the popup + // Copy light character, range, height → these appear in the tooltip if (lp.light_desc && !hp.light_desc) hp.light_desc = lp.light_desc; if (lp.range_nm && !hp.range_nm) hp.range_nm = lp.range_nm; if (lp.height_m && !hp.height_m) hp.height_m = lp.height_m; - return false; // drop the light, host now represents it + // Copy orient so the buoy/beacon can anchor its leading line correctly + if (lp.orient != null && hp.orient == null) hp.orient = lp.orient; + return false; // drop the now-redundant LIGHT_POINT }); const dropped = before - fc.features.length;