From 99fdbdc694e98473af1071f596febbf5654eb0ce Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Sun, 20 Sep 2026 09:28:34 +0800 Subject: [PATCH] fix: align CM93 coverage and hazard runtime with OpenCPN --- config/mappings/cm95_to_navsea_v1.yaml | 52 ++----- ...2_runtime_capabilities_web_compare_v1.json | 31 +++- src/cm95pbf/__init__.py | 4 +- src/cm95pbf/builder.py | 27 ++-- src/cm95pbf/production.py | 2 +- ...m95_s52_presentation_conservation_audit.py | 24 ++-- web/kyushu_compare.html | 132 +++++++++++++++++- 7 files changed, 192 insertions(+), 80 deletions(-) diff --git a/config/mappings/cm95_to_navsea_v1.yaml b/config/mappings/cm95_to_navsea_v1.yaml index 890a1b6..f51ecb4 100644 --- a/config/mappings/cm95_to_navsea_v1.yaml +++ b/config/mappings/cm95_to_navsea_v1.yaml @@ -477,24 +477,20 @@ rules: const: structure_pile notes: '' - rule_id: CM95-023 - mapping_version: v1 + mapping_version: v1.4.12-opencpn-cm93 cm95_object_class_id: 60 cm95_object_class: FSHHAV cm95_description: Fish haven geometry_type: POINT aoi_feature_count: 18 - target_source_layer: anchor_caution_hazard_point - canonical_family: hazard - canonical_object_type: fish_reef + target_source_layer: s52_points + canonical_family: fishing_facility + canonical_object_type: fishing_facility chart_render_type: symbol - safety_class: hard_gate + safety_class: important mapping_status: MAPPED - style_fields: - chart_symbol_code: - const: fish_reef - chart_icon_image: - const: reef_fish - notes: '' + style_fields: {} + notes: 'OpenCPN CM93 CreateS57Obj() substitutes FSHHAV -> FSHFAC. Preserve the S-52 FSHFAC/FSHHAV01 presentation instead of inventing fish_reef semantics.' - rule_id: CM95-024 mapping_version: v1 cm95_object_class_id: 11 @@ -1473,38 +1469,20 @@ rules: style_fields: chart_line_width: 1.0 - rule_id: CM95-PROD-001 - mapping_version: v1.4.3 + mapping_version: v1.4.12-opencpn-cm93 cm95_object_class_id: 60 cm95_object_class: FSHHAV cm95_description: Fish haven geometry_type: AREA aoi_feature_count: 0 - target_source_layer: anchor_caution_hazard_area - canonical_family: hazard - canonical_object_type: fish_reef + target_source_layer: fixed_fishing_gear_area + canonical_family: fishing_facility + canonical_object_type: fishing_facility chart_render_type: fill - safety_class: hard_gate - mapping_status: MAPPED_DERIVED_POINTS - style_fields: - chart_fill_pattern: - const: pattern_fill_fish_reef - least_depth_m: - from: VALSOU - derived_outputs: - - source_layer: anchor_caution_hazard_outline - geometry: boundary - style_fields: - chart_line_width: 1.0 - chart_line_color: '#88988b' - - source_layer: anchor_caution_hazard_point - geometry: point_on_surface - style_fields: - least_depth_m: - from: VALSOU - chart_symbol_code: fish_reef - chart_icon_image: reef_fish - notes: 'Production closure: Dataset-1 contains FSHHAV AREA. Preserve area + outline + point-on-surface; never downgrade - to unmapped.' + safety_class: important + mapping_status: MAPPED + style_fields: {} + notes: 'OpenCPN CM93 CreateS57Obj() substitutes FSHHAV -> FSHFAC. Preserve the area as a fishing-facility presentation using the S-52 FSHFAC lookup (FSHHAV02 AP + dashed LS); do not downgrade it to fish_reef.' relation20_validation: aoi: 10/914/385 nav_point_rows: 233 diff --git a/config/s52/cm95_s52_runtime_capabilities_web_compare_v1.json b/config/s52/cm95_s52_runtime_capabilities_web_compare_v1.json index 720ae86..53caea8 100644 --- a/config/s52/cm95_s52_runtime_capabilities_web_compare_v1.json +++ b/config/s52/cm95_s52_runtime_capabilities_web_compare_v1.json @@ -57,14 +57,33 @@ ] }, "RUNTIME_OBSTRN": { - "status": "NOT_IMPLEMENTED", - "fidelity": "NONE", - "evidence": [] + "status": "PREVIEW_ONLY", + "fidelity": "POINT_SYMBOL_RUNTIME_ONLY_AREA_LINE_PENDING", + "evidence": [ + { + "path": "web/kyushu_compare.html", + "contains": [ + "runtime-preview-hazard-symbols", + "runtimeHazardSymbol", + "OBSTRN04", + "ISODGR51" + ] + } + ] }, "RUNTIME_WRECKS": { - "status": "NOT_IMPLEMENTED", - "fidelity": "NONE", - "evidence": [] + "status": "PREVIEW_ONLY", + "fidelity": "POINT_SYMBOL_RUNTIME_ONLY_AREA_LINE_PENDING", + "evidence": [ + { + "path": "web/kyushu_compare.html", + "contains": [ + "runtime-preview-hazard-symbols", + "runtimeHazardSymbol", + "WRECKS02" + ] + } + ] } } } diff --git a/src/cm95pbf/__init__.py b/src/cm95pbf/__init__.py index 65dc25f..28df7fe 100644 --- a/src/cm95pbf/__init__.py +++ b/src/cm95pbf/__init__.py @@ -1,2 +1,2 @@ -__version__ = "0.4.11-s52-multi-light-presentations" -BUILDER_CONTRACT_VERSION = "cm95pbf-builder/0.4.11-s52-multi-light-presentations" +__version__ = "0.4.12-opencpn-cm93-coverage" +BUILDER_CONTRACT_VERSION = "cm95pbf-builder/0.4.12-opencpn-cm93-coverage" diff --git a/src/cm95pbf/builder.py b/src/cm95pbf/builder.py index 72c8f15..576db77 100644 --- a/src/cm95pbf/builder.py +++ b/src/cm95pbf/builder.py @@ -1168,21 +1168,18 @@ class Cm95TileBuilder: if geom is not None and not geom.is_empty: by_cell[int(row["cell_id"])].append(_to_mercator_geom(geom)) - pieces = [] - for cell in cells: - cid = int(cell["id"]) - if by_cell.get(cid): - pieces.append(unary_union(by_cell[cid])) - continue - minx, miny = lonlat_to_mercator( - float(cell["bbox_min_lon"]), - float(cell["bbox_min_lat"]), - ) - maxx, maxy = lonlat_to_mercator( - float(cell["bbox_max_lon"]), - float(cell["bbox_max_lat"]), - ) - pieces.append(box(minx, miny, maxx, maxy)) + # Match OpenCPN CM93 quilt semantics: only explicit M_COVR geometry + # participates in valid-region selection. A chart_cell bbox is only an + # index envelope and must never be promoted to coverage when M_COVR is + # absent, otherwise an effectively empty higher-detail cell can mask + # valid coarser-scale chart content. + pieces = [ + unary_union(by_cell[cid]) + for cid in sorted(by_cell) + if by_cell[cid] + ] + if not pieces: + return None return unary_union(pieces).intersection(expanded_box_m) diff --git a/src/cm95pbf/production.py b/src/cm95pbf/production.py index 3475435..cf093fb 100644 --- a/src/cm95pbf/production.py +++ b/src/cm95pbf/production.py @@ -35,7 +35,7 @@ from .policy import ( PRODUCTION_PLAN_SCHEMA_V1 = "navsea.cm95.production-plan/v1" PRODUCTION_PLAN_SCHEMA = "navsea.cm95.production-plan/v2" SUPPORTED_PRODUCTION_PLAN_SCHEMAS = {PRODUCTION_PLAN_SCHEMA_V1, PRODUCTION_PLAN_SCHEMA} -PRODUCTION_PIPELINE_VERSION = "cm95pbf-production/2.2-s52-resource-contract" +PRODUCTION_PIPELINE_VERSION = "cm95pbf-production/2.3-opencpn-cm93-coverage" PRODUCTION_RUN_SCHEMA = "navsea.cm95.production-run/v1" TILE_ENUMERATION_MODES = {"auto", "coverage", "aoi-complete"} diff --git a/tools/cm95_s52_presentation_conservation_audit.py b/tools/cm95_s52_presentation_conservation_audit.py index 6371ef2..11cec7f 100644 --- a/tools/cm95_s52_presentation_conservation_audit.py +++ b/tools/cm95_s52_presentation_conservation_audit.py @@ -383,22 +383,14 @@ def _load_coverage_unions( _to_mercator_geom(geom) ) - pieces = [] - for cell in cells: - cid = int(cell["id"]) - if by_cell.get(cid): - pieces.append(unary_union(by_cell[cid])) - continue - minx, miny = lonlat_to_mercator( - float(cell["bbox_min_lon"]), - float(cell["bbox_min_lat"]), - ) - maxx, maxy = lonlat_to_mercator( - float(cell["bbox_max_lon"]), - float(cell["bbox_max_lat"]), - ) - pieces.append(box(minx, miny, maxx, maxy)) - + # Keep the independent audit scope identical to OpenCPN/Builder + # coverage semantics: only explicit M_COVR rows define valid coverage. + # Never substitute the chart_cell index bbox for a missing M_COVR. + pieces = [ + unary_union(by_cell[cid]) + for cid in sorted(by_cell) + if by_cell[cid] + ] if pieces: result[scale] = unary_union(pieces).intersection( broad_box_m diff --git a/web/kyushu_compare.html b/web/kyushu_compare.html index b856d6d..a8d0b7e 100644 --- a/web/kyushu_compare.html +++ b/web/kyushu_compare.html @@ -119,7 +119,14 @@ const RUNTIME_DEFAULTS=Object.freeze({ const RUNTIME_LAYER_IDS=[ "runtime-preview-depare", "runtime-preview-depcnt", - "runtime-preview-soundg" + "runtime-preview-soundg", + "runtime-preview-hazard-symbols" +]; +const RUNTIME_HAZARD_SOURCE_ID="runtime-preview-hazards"; +const RUNTIME_HAZARD_SOURCE_LAYERS=[ + "navigation_hazard_point", + "anchor_caution_hazard_point", + "s52_points" ]; let runtimeEffectiveSafetyContourM=null; @@ -127,6 +134,7 @@ function removeRuntimePreviewLayers(map){ for(const id of RUNTIME_LAYER_IDS){ if(map.getLayer(id))map.removeLayer(id); } + if(map.getSource(RUNTIME_HAZARD_SOURCE_ID))map.removeSource(RUNTIME_HAZARD_SOURCE_ID); runtimeEffectiveSafetyContourM=null; } @@ -234,12 +242,124 @@ function addRuntimeSoundingLayer(map){ }); } +function runtimeNumber(props,key,fallback=null){ + const value=Number(props && props[key]); + return Number.isFinite(value) ? value : fallback; +} +function runtimeHazardIsolated(props){ + const depth=runtimeNumber(props,"s52_hazard_depth_m",null); + const expsou=runtimeNumber(props,"s52_expsou_code",0); + if(depth===null && expsou!==1)return true; + if(expsou===1 || (depth!==null && depth<=RUNTIME_DEFAULTS.safetyContourM)){ + const surrounding=runtimeNumber(props,"s52_surrounding_max_drval1_m",null); + if(surrounding!==null && surrounding>=RUNTIME_DEFAULTS.safetyContourM && expsou!==1){ + return true; + } + } + return false; +} +function runtimeHazardSymbol(props){ + const csp=String((props && props.s52_runtime_csp) || ""); + const canonical=String((props && props.canonical_object_type) || ""); + const watlev=runtimeNumber(props,"s52_watlev_code",-9); + const hasVal=props && props.least_depth_m!==undefined && props.least_depth_m!==null && props.least_depth_m!==""; + const valsou=hasVal ? runtimeNumber(props,"least_depth_m",null) : null; + const isolated=runtimeHazardIsolated(props); + + if(isolated && ![1,2].includes(watlev))return "ISODGR51"; + + if(csp==="WRECKS02"){ + if(valsou!==null)return valsou20)return "DANGER52"; + if(watlev===2)return "LNDARE01"; + if(watlev===3)return "UWTROC03"; + return "UWTROC04"; + } + + if(valsou!==null){ + if(valsou>20)return "DANGER52"; + if(watlev===-9)return "DANGER01"; + if([1,2].includes(watlev))return "LNDARE01"; + if(watlev===3)return "DANGER52"; + if([4,5].includes(watlev))return null; // OpenCPN references undefined DANGER53; fail closed. + return "DANGER51"; + } + if([1,2].includes(watlev))return "OBSTRN11"; + if([4,5].includes(watlev))return "OBSTRN03"; + return "OBSTRN01"; +} +function refreshRuntimeHazards(){ + if(!debugRuntimePreview.checked || !leftMap.getSource("cm95") || !leftMap.isSourceLoaded("cm95"))return; + const source=leftMap.getSource(RUNTIME_HAZARD_SOURCE_ID); + if(!source || typeof source.setData!=="function")return; + const seen=new Set(); + const features=[]; + for(const layer of RUNTIME_HAZARD_SOURCE_LAYERS){ + let rows=[]; + try{rows=leftMap.querySourceFeatures("cm95",{sourceLayer:layer}) || [];}catch(_err){continue;} + for(const feature of rows){ + const props=feature.properties || {}; + const csp=String(props.s52_runtime_csp || ""); + if(csp!=="OBSTRN04" && csp!=="WRECKS02")continue; + if(!feature.geometry || feature.geometry.type!=="Point")continue; + const symbol=runtimeHazardSymbol(props); + if(!symbol)continue; + const key=[feature.id==null?"":feature.id,feature.geometry.coordinates[0],feature.geometry.coordinates[1],symbol].join("|"); + if(seen.has(key))continue; + seen.add(key); + features.push({ + type:"Feature", + id:String(feature.id==null?key:feature.id), + geometry:feature.geometry, + properties:{ + runtime_symbol:symbol, + runtime_csp:csp, + source_feature_id:feature.id==null?"":String(feature.id) + } + }); + } + } + source.setData({type:"FeatureCollection",features:features}); +} +function addRuntimeHazardLayer(map){ + map.addSource(RUNTIME_HAZARD_SOURCE_ID,{ + type:"geojson", + data:{type:"FeatureCollection",features:[]} + }); + map.addLayer({ + id:"runtime-preview-hazard-symbols", + type:"symbol", + source:RUNTIME_HAZARD_SOURCE_ID, + layout:{ + "icon-image":["get","runtime_symbol"], + "icon-size":1, + "icon-allow-overlap":true, + "icon-ignore-placement":true + } + }); + setTimeout(refreshRuntimeHazards,0); +} + function installRuntimePreviewLayers(map){ removeRuntimePreviewLayers(map); if(!debugRuntimePreview.checked || !map.getSource("cm95"))return; addRuntimeDepthAreaLayer(map); addRuntimeContourLayer(map); addRuntimeSoundingLayer(map); + addRuntimeHazardLayer(map); setTimeout(updateRuntimeSafetyContour,0); } @@ -561,9 +681,15 @@ rightMap.on("style.load",applyDebugStyles); leftMap.on("sourcedata",function(e){ if(!e || e.sourceId!=="cm95")return; if(debugRuntimePreview.checked)updateRuntimeSafetyContour(); - if(e.isSourceLoaded)refreshLightArcFeatures(); + if(e.isSourceLoaded){ + refreshLightArcFeatures(); + refreshRuntimeHazards(); + } +}); +leftMap.on("idle",function(){ + refreshLightArcFeatures(); + refreshRuntimeHazards(); }); -leftMap.on("idle",refreshLightArcFeatures); leftMap.on("render",scheduleLightArcDraw); leftMap.on("resize",scheduleLightArcDraw); loadComparisonStyles();