fix: align CM93 coverage and hazard runtime with OpenCPN

This commit is contained in:
OpenAI Codex
2026-09-20 09:28:34 +08:00
parent 3092ff20a3
commit 99fdbdc694
7 changed files with 192 additions and 80 deletions

View File

@@ -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

View File

@@ -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"
]
}
]
}
}
}

View File

@@ -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"

View File

@@ -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)

View File

@@ -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"}

View File

@@ -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

View File

@@ -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 valsou<RUNTIME_DEFAULTS.safetyContourM ? "DANGER51" : "DANGER52";
const catwrk=runtimeNumber(props,"s52_catwrk_code",-9);
if(catwrk===1 && watlev===3)return "WRECKS04";
if(catwrk===2 && watlev===3)return "WRECKS05";
if([4,5].includes(catwrk) || [1,2,4,5].includes(watlev))return "WRECKS01";
return "WRECKS05";
}
if(csp!=="OBSTRN04")return null;
if(canonical==="underwater_rock"){
if(valsou!==null && valsou<=20){
if([4,5].includes(watlev))return "UWTROC04";
return "DANGER51";
}
if(valsou!==null && valsou>20)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();