fix: align CM93 coverage and hazard runtime with OpenCPN
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user