feat: complete S-52 sounding lights and arc runtime

This commit is contained in:
OpenAI Codex
2026-09-21 06:28:23 +08:00
parent bea74ddfeb
commit 89653ea4dd
4 changed files with 96 additions and 24 deletions

View File

@@ -5,15 +5,21 @@
"capabilities": { "capabilities": {
"RUNTIME_CA": { "RUNTIME_CA": {
"status": "IMPLEMENTED", "status": "IMPLEMENTED",
"fidelity": "S52_SCREEN_MM", "fidelity": "S52_SCREEN_MM_SCAMIN_SCALE_AND_RULE_WIDTH",
"evidence": [ "evidence": [
{ {
"path": "web/kyushu_compare.html", "path": "web/kyushu_compare.html",
"contains": [ "contains": [
"left-ca-overlay", "left-ca-overlay",
"drawLightArcFeature", "drawLightArcFeature"
"s52_arc_radius_mm", ]
"s52_arc_sector_radius_mm" },
{
"path": "web/s52_runtime_executor.js",
"contains": [
"caScaleMetrics",
"requestedArcWidthMm",
"metresPerPixel"
] ]
} }
] ]
@@ -46,13 +52,15 @@
] ]
}, },
"RUNTIME_SOUNDG": { "RUNTIME_SOUNDG": {
"status": "PREVIEW_ONLY", "status": "IMPLEMENTED",
"fidelity": "TEXT_VALUE_ONLY_NOT_S52_GLYPH_COMPOSITION", "fidelity": "ORDERED_GLYPH_COMPOSITION_WITH_RESOURCE_PIVOTS",
"evidence": [ "evidence": [
{ {
"path": "web/s52_runtime_executor.js", "path": "web/s52_runtime_executor.js",
"contains": [ "contains": [
"soundingRules" "soundingRules"
,"runtime_icon_offset"
,"s52_sounding_depth_m"
] ]
} }
] ]
@@ -85,6 +93,21 @@
] ]
} }
] ]
},
"RUNTIME_LIGHTS_STATE": {
"status": "IMPLEMENTED",
"fidelity": "PREVIOUS_POSITION_AND_DESCRIPTION",
"evidence": [
{
"path": "web/s52_runtime_executor.js",
"contains": [
"LightLabelState",
"lastPosition",
"lastDescription",
"yoff:1"
]
}
]
} }
} }
} }

View File

@@ -430,6 +430,11 @@ function lightArcPoint(feature){
} }
function refreshLightArcFeatures(){ function refreshLightArcFeatures(){
if(runtimeRuleExecutor && runtimeRuleExecutor.caFeatures.length){
lightArcFeatures=runtimeRuleExecutor.caFeatures.slice();
scheduleLightArcDraw();
return;
}
if(!leftMap || !leftMap.getSource("cm95") || !leftMap.isSourceLoaded("cm95")){ if(!leftMap || !leftMap.getSource("cm95") || !leftMap.isSourceLoaded("cm95")){
lightArcFeatures=[]; lightArcFeatures=[];
scheduleLightArcDraw(); scheduleLightArcDraw();
@@ -530,25 +535,37 @@ function drawCaLeg(ctx,x,y,length,degrees,bearing){
ctx.stroke(); ctx.stroke();
ctx.restore(); ctx.restore();
} }
function caScaleMetrics(props,coord,canvasWidth,canvasHeight){
let args=CM95S52Runtime.jsonValue(props.runtime_args_json,[]);
if(args.length<8)args=[
props.s52_arc_outline_color,numericProp(props,"s52_arc_outline_width",4),
props.s52_arc_color,numericProp(props,"s52_arc_width",1),
numericProp(props,"s52_arc_start_deg",0),numericProp(props,"s52_arc_end_deg",360),
numericProp(props,"s52_arc_radius_mm",0),numericProp(props,"s52_arc_sector_radius_mm",0)
];
return CM95S52Runtime.caScaleMetrics(args,coord,{
width:canvasWidth,height:canvasHeight,zoom:leftMap.getZoom(),pixelsPerMm:CSS_PX_PER_MM
});
}
function drawLightArcFeature(ctx,feature,bearing,width,height){ function drawLightArcFeature(ctx,feature,bearing,width,height){
const props=feature.properties || {}; const props=feature.properties || {};
const coord=lightArcPoint(feature); const coord=lightArcPoint(feature);
if(!coord)return; if(!coord)return;
const point=leftMap.project(coord); const point=leftMap.project(coord);
const radius=numericProp(props,"s52_arc_radius_mm",0)*CSS_PX_PER_MM; const args=CM95S52Runtime.jsonValue(props.runtime_args_json,[]);
const metrics=caScaleMetrics(props,coord,width,height);
const radius=metrics.radius;
if(radius<=0)return; if(radius<=0)return;
const sectorRadius=numericProp(props,"s52_arc_sector_radius_mm",0)*CSS_PX_PER_MM; const sectorRadius=metrics.sectorRadius;
const margin=Math.max(radius,sectorRadius)+12; const margin=Math.max(radius,sectorRadius)+12;
if(point.x<-margin || point.x>width+margin || point.y<-margin || point.y>height+margin)return; if(point.x<-margin || point.x>width+margin || point.y<-margin || point.y>height+margin)return;
const start=numericProp(props,"s52_arc_start_deg",0); const start=args.length>=8 ? Number(args[4]) : numericProp(props,"s52_arc_start_deg",0);
let end=numericProp(props,"s52_arc_end_deg",360); let end=args.length>=8 ? Number(args[5]) : numericProp(props,"s52_arc_end_deg",360);
if(end<=start)end+=360; if(end<=start)end+=360;
const arcColor=CA_COLOR[props.s52_arc_color] || CA_COLOR.CHMGD; const arcColor=CA_COLOR[args.length>=8?args[2]:props.s52_arc_color] || CA_COLOR.CHMGD;
const outlineColor=CA_COLOR[props.s52_arc_outline_color] || CA_COLOR.OUTLW; const outlineColor=CA_COLOR[args.length>=8?args[0]:props.s52_arc_outline_color] || CA_COLOR.OUTLW;
const outlineWidth=numericProp(props,"s52_arc_outline_width",4); drawCaArc(ctx,point.x,point.y,radius,start,end,bearing,outlineColor,metrics.arcWidth+metrics.outlineWidth);
const colorWidth=CSS_PX_PER_MM; drawCaArc(ctx,point.x,point.y,radius,start,end,bearing,arcColor,metrics.arcWidth);
drawCaArc(ctx,point.x,point.y,radius,start,end,bearing,outlineColor,colorWidth+outlineWidth);
drawCaArc(ctx,point.x,point.y,radius,start,end,bearing,arcColor,colorWidth);
if(sectorRadius>0){ if(sectorRadius>0){
drawCaLeg(ctx,point.x,point.y,sectorRadius,start,bearing); drawCaLeg(ctx,point.x,point.y,sectorRadius,start,bearing);
drawCaLeg(ctx,point.x,point.y,sectorRadius,end,bearing); drawCaLeg(ctx,point.x,point.y,sectorRadius,end,bearing);
@@ -723,13 +740,13 @@ leftMap.on("sourcedata",function(e){
if(!e || e.sourceId!=="cm95")return; if(!e || e.sourceId!=="cm95")return;
if(debugRuntimePreview.checked)updateRuntimeSafetyContour(); if(debugRuntimePreview.checked)updateRuntimeSafetyContour();
if(e.isSourceLoaded){ if(e.isSourceLoaded){
refreshLightArcFeatures();
refreshRuntimeRuleExecutor(); refreshRuntimeRuleExecutor();
refreshLightArcFeatures();
} }
}); });
leftMap.on("idle",function(){ leftMap.on("idle",function(){
refreshLightArcFeatures();
refreshRuntimeRuleExecutor(); refreshRuntimeRuleExecutor();
refreshLightArcFeatures();
}); });
leftMap.on("render",scheduleLightArcDraw); leftMap.on("render",scheduleLightArcDraw);
leftMap.on("resize",scheduleLightArcDraw); leftMap.on("resize",scheduleLightArcDraw);

View File

@@ -31,6 +31,9 @@ def main():
assets_root = project_root / "build" / "fukuoka_s52_web" / "site" / "assets" assets_root = project_root / "build" / "fukuoka_s52_web" / "site" / "assets"
s52_style_path = project_root / "build" / "s52_style_compiler_release_57e72826db1d" / "style.s52.json" s52_style_path = project_root / "build" / "s52_style_compiler_release_57e72826db1d" / "style.s52.json"
s52_sprite_root = project_root / "build" / "s52_resources_v2" / "final" s52_sprite_root = project_root / "build" / "s52_resources_v2" / "final"
s52_symbol_metadata_path = (
project_root / "build" / "s52_resources_v2" / "base" / "s52_symbol_metadata.json"
)
required = [ required = [
pbf_root, pbf_root,
@@ -44,6 +47,7 @@ def main():
s52_sprite_root / "s52-sprite-full.png", s52_sprite_root / "s52-sprite-full.png",
s52_sprite_root / "s52-sprite-full@2x.json", s52_sprite_root / "s52-sprite-full@2x.json",
s52_sprite_root / "s52-sprite-full@2x.png", s52_sprite_root / "s52-sprite-full@2x.png",
s52_symbol_metadata_path,
] ]
missing = [str(path) for path in required if not path.exists()] missing = [str(path) for path in required if not path.exists()]
if missing: if missing:
@@ -109,6 +113,13 @@ def main():
self.send_bytes(sprite_path.read_bytes(), content_type) self.send_bytes(sprite_path.read_bytes(), content_type)
return return
if path == "/s52/symbol-metadata.json":
self.send_bytes(
s52_symbol_metadata_path.read_bytes(),
"application/json; charset=utf-8",
)
return
if path in ("/assets/maplibre-gl.js", "/assets/maplibre-gl.css"): if path in ("/assets/maplibre-gl.js", "/assets/maplibre-gl.css"):
filename = path.rsplit("/", 1)[-1] filename = path.rsplit("/", 1)[-1]
file_path = assets_root / filename file_path = assets_root / filename

View File

@@ -24,6 +24,26 @@ function soundingRules(props,settings,depthOverride){
return codes.map(code=>rule("SOUNDG",[code],{procedure:"SOUNDG03",depth:depth,depth_unit:settings.depthUnit})); return codes.map(code=>rule("SOUNDG",[code],{procedure:"SOUNDG03",depth:depth,depth_unit:settings.depthUnit}));
} }
function qualityRules(props){return storedRules(props,"s52_hazard_quality_rule_chain");}function nameRules(props){return storedRules(props,"s52_hazard_name_rule_chain");}function sideEffect(values){return rule("SE",[],{side_effects:values});} function qualityRules(props){return storedRules(props,"s52_hazard_quality_rule_chain");}function nameRules(props){return storedRules(props,"s52_hazard_name_rule_chain");}function sideEffect(values){return rule("SE",[],{side_effects:values});}
function caScaleMetrics(args,coordinates,viewport){
const pxPerMm=Number(viewport.pixelsPerMm||96/25.4),displaySizeMm=Math.min(Number(viewport.width),Number(viewport.height))/pxPerMm;
let radiusMm=Number(args[6]||0),sectorMm=Number(args[7]||0);const outlineWidthMm=Number(args[1]||4),requestedArcWidthMm=Number(args[3]||1);
if(radiusMm>displaySizeMm/10){const capFactor=(displaySizeMm/10)/radiusMm;radiusMm*=capFactor;sectorMm*=capFactor;}
const latitude=Number(coordinates[1])||0,metresPerPixel=156543.03392*Math.cos(latitude*Math.PI/180)/Math.pow(2,Number(viewport.zoom)||0),radiusMetres=radiusMm*pxPerMm*metresPerPixel;
const scale=Math.max(0.5,Math.min(1,radiusMetres>0?1000/radiusMetres:1)),radius=radiusMm*pxPerMm*scale,sectorRadius=sectorMm*pxPerMm*scale;
let arcWidth=(displaySizeMm>200?1:requestedArcWidthMm)*pxPerMm*scale;arcWidth=Math.max(1,Math.min(arcWidth,radius/10));
return {radius:radius,sectorRadius:sectorRadius,arcWidth:arcWidth,outlineWidth:Math.max(1,outlineWidthMm*pxPerMm*scale),scale:scale,metresPerPixel:metresPerPixel};
}
class LightLabelState{
constructor(){this.reset();}
reset(){this.lastPosition=null;this.lastDescription=null;}
accept(coordinates,description){
const position=JSON.stringify(coordinates||[]),same=position===this.lastPosition;
this.lastPosition=position;
if(!same){this.lastDescription=description;return {emit:Boolean(description),yoff:null};}
if(this.lastDescription===description)return {emit:false,yoff:null};
this.lastDescription=description;return {emit:Boolean(description),yoff:1};
}
}
function hazardIsolated(props,settings){const depth=numberValue(props,"s52_hazard_depth_m",null),expsou=numberValue(props,"s52_expsou_code",0);let danger=depth===null&&expsou!==1;if(!danger&&(expsou===1||(depth!==null&&depth<=settings.safetyContourM))){const surrounding=numberValue(props,"s52_surrounding_max_drval1_m",null);if(surrounding!==null&&surrounding>=settings.safetyContourM&&expsou!==1)danger=true;}return danger;} function hazardIsolated(props,settings){const depth=numberValue(props,"s52_hazard_depth_m",null),expsou=numberValue(props,"s52_expsou_code",0);let danger=depth===null&&expsou!==1;if(!danger&&(expsou===1||(depth!==null&&depth<=settings.safetyContourM))){const surrounding=numberValue(props,"s52_surrounding_max_drval1_m",null);if(surrounding!==null&&surrounding>=settings.safetyContourM&&expsou!==1)danger=true;}return danger;}
function hazardRules(props,settings){ function hazardRules(props,settings){
const csp=String(props.s52_runtime_csp||""),geom=String(props.s52_source_geometry||"POINT").toUpperCase(),objectClass=String(props.s52_source_object_class||""); const csp=String(props.s52_runtime_csp||""),geom=String(props.s52_source_geometry||"POINT").toUpperCase(),objectClass=String(props.s52_source_object_class||"");
@@ -71,18 +91,19 @@ function operationFeatures(feature,settings,context,resourceExists){
const props=feature.properties||{},geometry=feature.geometry||{},geomType=String(geometry.type||""),chain=expandRuleChain(props,settings,context),missing=[],output=[]; const props=feature.properties||{},geometry=feature.geometry||{},geomType=String(geometry.type||""),chain=expandRuleChain(props,settings,context),missing=[],output=[];
const effects=Object.assign({},...chain.filter(x=>x.opcode==="SE").map(x=>x.side_effects||{})); const effects=Object.assign({},...chain.filter(x=>x.opcode==="SE").map(x=>x.side_effects||{}));
chain.forEach(item=>{const opcode=String(item.opcode||""),args=item.args||[];if(opcode==="OP")return;if(opcode==="SE"){output.push({type:"Feature",id:String(feature.id==null?"":feature.id)+":"+item.sequence,geometry:geometry,properties:Object.assign({runtime_opcode:"SE",runtime_sequence:item.sequence},effects)});return;}if(["AC","AP"].includes(opcode)&&!["Polygon","MultiPolygon"].includes(geomType))return;if(["LS","LC"].includes(opcode)&&!["LineString","MultiLineString","Polygon","MultiPolygon"].includes(geomType))return;if(["SY","SOUNDG","AP","LC"].includes(opcode)&&args[0]&&resourceExists&&!resourceExists(String(args[0]))){missing.push(String(args[0]));return;} chain.forEach(item=>{const opcode=String(item.opcode||""),args=item.args||[];if(opcode==="OP")return;if(opcode==="SE"){output.push({type:"Feature",id:String(feature.id==null?"":feature.id)+":"+item.sequence,geometry:geometry,properties:Object.assign({runtime_opcode:"SE",runtime_sequence:item.sequence},effects)});return;}if(["AC","AP"].includes(opcode)&&!["Polygon","MultiPolygon"].includes(geomType))return;if(["LS","LC"].includes(opcode)&&!["LineString","MultiLineString","Polygon","MultiPolygon"].includes(geomType))return;if(["SY","SOUNDG","AP","LC"].includes(opcode)&&args[0]&&resourceExists&&!resourceExists(String(args[0]))){missing.push(String(args[0]));return;}
const draw=Object.assign({runtime_opcode:opcode,runtime_sequence:item.sequence,runtime_resource:String(args[0]||""),runtime_missing_resources:missing.join(",")},effects);if(opcode==="AC")draw.runtime_color=COLORS[args[0]]||COLORS.CHMGD;if(opcode==="LS"){draw.runtime_line_style=String(args[0]||"SOLD");draw.runtime_line_width=Number(args[1]||1);draw.runtime_color=COLORS[args[2]]||COLORS.CHBLK;}if(opcode==="TX"){draw.runtime_text=String((item.text&&item.text.value)||args[0]||"");draw.runtime_color=COLORS[(item.text&&item.text.color)||"CHBLK"]||COLORS.CHBLK;draw.runtime_text_xoff=Number((item.text&&item.text.xoff)||0);draw.runtime_text_yoff=Number((item.text&&item.text.yoff)||0);}if(opcode==="SOUNDG"){draw.runtime_opcode="SOUNDG";draw.runtime_soundg_index=output.filter(x=>x.properties.runtime_opcode==="SOUNDG").length;}output.push({type:"Feature",id:String(feature.id==null?"":feature.id)+":"+item.sequence,geometry:geometry,properties:draw}); let textOverride=null;if(opcode==="TX"&&item.procedure==="LIGHTS05"&&context.lightLabelState){const text=String((item.text&&item.text.value)||args[0]||""),decision=context.lightLabelState.accept(geometry.coordinates,text);if(!decision.emit)return;textOverride=decision.yoff;}
const draw=Object.assign({runtime_opcode:opcode,runtime_sequence:item.sequence,runtime_resource:String(args[0]||""),runtime_procedure:String(item.procedure||""),runtime_args_json:JSON.stringify(args),runtime_missing_resources:missing.join(",")},effects);if(opcode==="AC")draw.runtime_color=COLORS[args[0]]||COLORS.CHMGD;if(opcode==="LS"){draw.runtime_line_style=String(args[0]||"SOLD");draw.runtime_line_width=Number(args[1]||1);draw.runtime_color=COLORS[args[2]]||COLORS.CHBLK;}if(opcode==="TX"){draw.runtime_text=String((item.text&&item.text.value)||args[0]||"");draw.runtime_color=COLORS[(item.text&&item.text.color)||"CHBLK"]||COLORS.CHBLK;draw.runtime_text_xoff=Number((item.text&&item.text.xoff)||0);draw.runtime_text_yoff=textOverride===null?Number((item.text&&item.text.yoff)||0):textOverride;}if(opcode==="SOUNDG"){draw.runtime_opcode="SOUNDG";draw.runtime_soundg_index=output.filter(x=>x.properties.runtime_opcode==="SOUNDG").length;}const metadata=(context.symbolMetadata||{})[String(args[0]||"")]||{};if(Array.isArray(metadata.maplibre_icon_offset))draw.runtime_icon_offset=metadata.maplibre_icon_offset;output.push({type:"Feature",id:String(feature.id==null?"":feature.id)+":"+item.sequence,geometry:geometry,properties:draw});
});output.missingResources=missing;output.sideEffects=chain.filter(x=>x.opcode==="SE").map(x=>x.side_effects||{});return output; });output.missingResources=missing;output.sideEffects=chain.filter(x=>x.opcode==="SE").map(x=>x.side_effects||{});return output;
} }
class RuntimeExecutor{ class RuntimeExecutor{
constructor(map,options){this.map=map;this.settings=Object.assign({},DEFAULTS,(options||{}).settings||{});this.sourceId=(options||{}).sourceId||"cm95-runtime-rules";this.layerIds=[];this.effectiveSafetyContourM=null;this.maxSequence=31;} constructor(map,options){this.map=map;this.settings=Object.assign({},DEFAULTS,(options||{}).settings||{});this.sourceId=(options||{}).sourceId||"cm95-runtime-rules";this.layerIds=[];this.effectiveSafetyContourM=null;this.maxSequence=31;this.symbolMetadata={};this.lightLabelState=new LightLabelState();this.caFeatures=[];}
remove(){this.layerIds.forEach(id=>{if(this.map.getLayer(id))this.map.removeLayer(id);});this.layerIds=[];if(this.map.getSource(this.sourceId))this.map.removeSource(this.sourceId);} remove(){this.layerIds.forEach(id=>{if(this.map.getLayer(id))this.map.removeLayer(id);});this.layerIds=[];if(this.map.getSource(this.sourceId))this.map.removeSource(this.sourceId);}
install(){this.remove();this.map.addSource(this.sourceId,{type:"geojson",data:{type:"FeatureCollection",features:[]}});for(let seq=0;seq<=this.maxSequence;seq++)this.addSequence(seq);this.refresh();} install(){this.remove();this.map.addSource(this.sourceId,{type:"geojson",data:{type:"FeatureCollection",features:[]}});for(let seq=0;seq<=this.maxSequence;seq++)this.addSequence(seq);this.refresh();if(typeof fetch==="function")fetch("/s52/symbol-metadata.json").then(response=>response.json()).then(metadata=>{this.symbolMetadata=metadata||{};this.refresh();}).catch(error=>console.warn("S-52 symbol metadata unavailable",error));}
addLayer(layer){this.map.addLayer(layer);this.layerIds.push(layer.id);}filter(seq,opcode){return ["all",["==",["get","runtime_sequence"],seq],["==",["get","runtime_opcode"],opcode]];} addLayer(layer){this.map.addLayer(layer);this.layerIds.push(layer.id);}filter(seq,opcode){return ["all",["==",["get","runtime_sequence"],seq],["==",["get","runtime_opcode"],opcode]];}
addSequence(seq){const base={source:this.sourceId};this.addLayer(Object.assign({},base,{id:`runtime-rule-${seq}-ac`,type:"fill",filter:this.filter(seq,"AC"),paint:{"fill-color":["get","runtime_color"],"fill-opacity":0.92}}));this.addLayer(Object.assign({},base,{id:`runtime-rule-${seq}-ap`,type:"fill",filter:this.filter(seq,"AP"),paint:{"fill-pattern":["get","runtime_resource"],"fill-opacity":0.85}}));this.addLayer(Object.assign({},base,{id:`runtime-rule-${seq}-ls`,type:"line",filter:this.filter(seq,"LS"),layout:{"line-cap":"round"},paint:{"line-color":["get","runtime_color"],"line-width":["get","runtime_line_width"],"line-dasharray":["match",["get","runtime_line_style"],"DASH",["literal",[4,2]],"DOTT",["literal",[1,2]],["literal",[1,0]]]}}));this.addLayer(Object.assign({},base,{id:`runtime-rule-${seq}-lc`,type:"line",filter:this.filter(seq,"LC"),paint:{"line-pattern":["get","runtime_resource"],"line-width":5}}));this.addLayer(Object.assign({},base,{id:`runtime-rule-${seq}-sy`,type:"symbol",filter:this.filter(seq,"SY"),layout:{"icon-image":["get","runtime_resource"],"icon-allow-overlap":true,"icon-ignore-placement":true}}));this.addLayer(Object.assign({},base,{id:`runtime-rule-${seq}-soundg`,type:"symbol",filter:this.filter(seq,"SOUNDG"),layout:{"icon-image":["get","runtime_resource"],"icon-allow-overlap":true,"icon-ignore-placement":true}}));this.addLayer(Object.assign({},base,{id:`runtime-rule-${seq}-tx`,type:"symbol",filter:this.filter(seq,"TX"),layout:{"text-field":["get","runtime_text"],"text-size":10,"text-offset":["literal",[0,0]],"text-allow-overlap":false,"text-optional":true},paint:{"text-color":["get","runtime_color"],"text-halo-color":"rgba(255,255,255,.65)","text-halo-width":0.5}}));} addSequence(seq){const base={source:this.sourceId};this.addLayer(Object.assign({},base,{id:`runtime-rule-${seq}-ac`,type:"fill",filter:this.filter(seq,"AC"),paint:{"fill-color":["get","runtime_color"],"fill-opacity":0.92}}));this.addLayer(Object.assign({},base,{id:`runtime-rule-${seq}-ap`,type:"fill",filter:this.filter(seq,"AP"),paint:{"fill-pattern":["get","runtime_resource"],"fill-opacity":0.85}}));this.addLayer(Object.assign({},base,{id:`runtime-rule-${seq}-ls`,type:"line",filter:this.filter(seq,"LS"),layout:{"line-cap":"round"},paint:{"line-color":["get","runtime_color"],"line-width":["get","runtime_line_width"],"line-dasharray":["match",["get","runtime_line_style"],"DASH",["literal",[4,2]],"DOTT",["literal",[1,2]],["literal",[1,0]]]}}));this.addLayer(Object.assign({},base,{id:`runtime-rule-${seq}-lc`,type:"line",filter:this.filter(seq,"LC"),paint:{"line-pattern":["get","runtime_resource"],"line-width":5}}));this.addLayer(Object.assign({},base,{id:`runtime-rule-${seq}-sy`,type:"symbol",filter:this.filter(seq,"SY"),layout:{"icon-image":["get","runtime_resource"],"icon-offset":["coalesce",["get","runtime_icon_offset"],["literal",[0,0]]],"icon-allow-overlap":true,"icon-ignore-placement":true}}));this.addLayer(Object.assign({},base,{id:`runtime-rule-${seq}-soundg`,type:"symbol",filter:this.filter(seq,"SOUNDG"),layout:{"icon-image":["get","runtime_resource"],"icon-offset":["coalesce",["get","runtime_icon_offset"],["literal",[0,0]]],"icon-allow-overlap":true,"icon-ignore-placement":true}}));this.addLayer(Object.assign({},base,{id:`runtime-rule-${seq}-tx`,type:"symbol",filter:this.filter(seq,"TX"),layout:{"text-field":["get","runtime_text"],"text-size":10,"text-offset":["literal",[0,0]],"text-allow-overlap":false,"text-optional":true},paint:{"text-color":["get","runtime_color"],"text-halo-color":"rgba(255,255,255,.65)","text-halo-width":0.5}}));}
sourceLayers(){const set=new Set();((this.map.getStyle()||{}).layers||[]).forEach(layer=>{if(layer.source==="cm95"&&layer["source-layer"])set.add(layer["source-layer"]);});return Array.from(set);} sourceLayers(){const set=new Set();((this.map.getStyle()||{}).layers||[]).forEach(layer=>{if(layer.source==="cm95"&&layer["source-layer"])set.add(layer["source-layer"]);});return Array.from(set);}
updateSafety(){const values=[];try{(this.map.querySourceFeatures("cm95",{sourceLayer:"depth_contour"})||[]).forEach(f=>{const v=numberValue(f.properties,"contour_depth_m",null);if(v!==null&&v>=this.settings.safetyContourM)values.push(v);});}catch(_err){}this.effectiveSafetyContourM=values.length?Math.min.apply(null,values):null;} updateSafety(){const values=[];try{(this.map.querySourceFeatures("cm95",{sourceLayer:"depth_contour"})||[]).forEach(f=>{const v=numberValue(f.properties,"contour_depth_m",null);if(v!==null&&v>=this.settings.safetyContourM)values.push(v);});}catch(_err){}this.effectiveSafetyContourM=values.length?Math.min.apply(null,values):null;}
refresh(){if(!this.map.getSource("cm95")||!this.map.isSourceLoaded("cm95"))return;this.updateSafety();const seen=new Set(),features=[];this.sourceLayers().forEach(layer=>{let rows=[];try{rows=this.map.querySourceFeatures("cm95",{sourceLayer:layer})||[];}catch(_err){return;}rows.forEach(feature=>{if(!feature.properties||!feature.properties.s52_rule_chain)return;const key=[layer,feature.id,JSON.stringify(feature.geometry),feature.properties.s52_rule_chain].join("|");if(seen.has(key))return;seen.add(key);operationFeatures(feature,this.settings,{effectiveSafetyContourM:this.effectiveSafetyContourM},name=>this.map.hasImage(name)).forEach(item=>features.push(item));});});const source=this.map.getSource(this.sourceId);if(source&&source.setData)source.setData({type:"FeatureCollection",features:features});} refresh(){if(!this.map.getSource("cm95")||!this.map.isSourceLoaded("cm95"))return;this.updateSafety();this.lightLabelState.reset();const seen=new Set(),features=[],context={effectiveSafetyContourM:this.effectiveSafetyContourM,lightLabelState:this.lightLabelState,symbolMetadata:this.symbolMetadata};this.sourceLayers().forEach(layer=>{let rows=[];try{rows=this.map.querySourceFeatures("cm95",{sourceLayer:layer})||[];}catch(_err){return;}rows.forEach(feature=>{if(!feature.properties)return;const presentations=jsonValue(feature.properties.s52_light_presentations,[]),variants=[];if(feature.properties.s52_rule_chain)variants.push(feature);if(Array.isArray(presentations))presentations.forEach((presentation,index)=>variants.push(Object.assign({},feature,{id:String(feature.id==null?"":feature.id)+":light:"+index,properties:Object.assign({},feature.properties,presentation)})));variants.forEach(variant=>{if(!variant.properties.s52_rule_chain)return;const key=[layer,variant.id,JSON.stringify(variant.geometry),variant.properties.s52_rule_chain].join("|");if(seen.has(key))return;seen.add(key);operationFeatures(variant,this.settings,context,name=>this.map.hasImage(name)).forEach(item=>features.push(item));});});});this.caFeatures=features.filter(item=>item.properties.runtime_opcode==="CA");const source=this.map.getSource(this.sourceId);if(source&&source.setData)source.setData({type:"FeatureCollection",features:features});}
} }
const api={DEFAULTS:DEFAULTS,COLORS:COLORS,jsonValue:jsonValue,soundingRules:soundingRules,hazardRules:hazardRules,dynamicRules:dynamicRules,expandRuleChain:expandRuleChain,operationFeatures:operationFeatures,RuntimeExecutor:RuntimeExecutor};root.CM95S52Runtime=api;if(typeof module!=="undefined"&&module.exports)module.exports=api; const api={DEFAULTS:DEFAULTS,COLORS:COLORS,jsonValue:jsonValue,soundingRules:soundingRules,hazardRules:hazardRules,dynamicRules:dynamicRules,expandRuleChain:expandRuleChain,operationFeatures:operationFeatures,caScaleMetrics:caScaleMetrics,LightLabelState:LightLabelState,RuntimeExecutor:RuntimeExecutor};root.CM95S52Runtime=api;if(typeof module!=="undefined"&&module.exports)module.exports=api;
})(typeof globalThis!=="undefined"?globalThis:this); })(typeof globalThis!=="undefined"?globalThis:this);