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

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