feat: finalize Kyushu S-52 production release

This commit is contained in:
OpenAI Codex
2026-09-19 15:09:11 +08:00
parent 9dd6282817
commit 472acd6938
4 changed files with 498 additions and 15 deletions

View File

@@ -51,10 +51,12 @@
</div>
<div id="debug-panel" class="collapsed">
<div class="debug-title"><span>图层调试</span><button id="debug-collapse-btn" type="button">收起</button></div>
<label><input id="debug-left-s52" type="checkbox" checked />左侧使用 S-52 style</label>
<label><input id="debug-runtime-preview" type="checkbox" checked />左侧 runtime 默认阈值预览</label>
<label><input id="debug-ref-bathy" type="checkbox" checked />右侧显示 bathymetry_line</label>
<label><input id="debug-highlight-depcnt" type="checkbox" />左右突出 DEPCNT</label>
<label><input id="debug-inspector" type="checkbox" checked />点击 Feature 检查</label>
<div class="debug-note">DEPCNT = depth_contour;bathymetry_line 是参考 PBF 的额外高密度海底地形线。</div>
<div class="debug-note">runtime 预览:shallow=2m / safety contour=3m / safety depth=3m / deep=6m;只消费 PBF facts,不改 PBF。DEPCNT safety contour 从当前已加载等深值中选择下一条实际线。Hazard exact CSP 仍由正式 Runtime Adapter 负责。bathymetry_line 是参考 PBF 的额外高密度海底地形线。</div>
</div>
<div id="maps">
<section class="pane" id="left-pane">
@@ -84,6 +86,8 @@ const rightState = document.getElementById("right-state");
const centerReadout = document.getElementById("center-readout");
const syncToggle = document.getElementById("sync-toggle");
const debugPanel = document.getElementById("debug-panel");
const debugLeftS52 = document.getElementById("debug-left-s52");
const debugRuntimePreview = document.getElementById("debug-runtime-preview");
const debugRefBathy = document.getElementById("debug-ref-bathy");
const debugHighlightDepcnt = document.getElementById("debug-highlight-depcnt");
const debugInspector = document.getElementById("debug-inspector");
@@ -98,15 +102,170 @@ function emptyStyle(){
function setLayerVisibility(map,id,visible){
if(map.getLayer(id))map.setLayoutProperty(id,"visibility",visible ? "visible" : "none");
}
function templateLayer(template,id){
return template && (template.layers || []).find(function(layer){return layer.id===id;});
}
const RUNTIME_DEFAULTS=Object.freeze({
shallowContourM:2.0,
safetyContourM:3.0,
safetyDepthM:3.0,
deepContourM:6.0,
twoShades:false
});
const RUNTIME_LAYER_IDS=[
"runtime-preview-depare",
"runtime-preview-depcnt",
"runtime-preview-soundg"
];
let runtimeEffectiveSafetyContourM=null;
function removeRuntimePreviewLayers(map){
for(const id of RUNTIME_LAYER_IDS){
if(map.getLayer(id))map.removeLayer(id);
}
runtimeEffectiveSafetyContourM=null;
}
function runtimeDepthColorExpression(){
const dmin=["to-number",["get","depth_min_m"],-1];
const dmax=["to-number",["get","depth_max_m"],["+",dmin,0.01]];
return ["case",
["!",["has","depth_min_m"]],"#bad5e1",
["all",[">=",dmin,RUNTIME_DEFAULTS.deepContourM],[">",dmax,RUNTIME_DEFAULTS.deepContourM]],"#d4eaee",
["all",[">=",dmin,RUNTIME_DEFAULTS.safetyContourM],[">",dmax,RUNTIME_DEFAULTS.safetyContourM]],"#bad5e1",
["all",[">=",dmin,RUNTIME_DEFAULTS.shallowContourM],[">",dmax,RUNTIME_DEFAULTS.shallowContourM]],"#98c5f2",
["all",[">=",dmin,0],[">",dmax,0]],"#73b6ef",
"#83b295"
];
}
function runtimeDepcntColorExpression(){
if(runtimeEffectiveSafetyContourM===null)return "#7d898c";
return ["case",
["==",["to-number",["get","contour_depth_m"],-9999],runtimeEffectiveSafetyContourM],"#525a5c",
"#7d898c"
];
}
function runtimeDepcntWidthExpression(){
if(runtimeEffectiveSafetyContourM===null)return 1;
return ["case",
["==",["to-number",["get","contour_depth_m"],-9999],runtimeEffectiveSafetyContourM],2,
1
];
}
function updateRuntimeSafetyContour(){
if(!debugRuntimePreview.checked || !leftMap.getSource("cm95") || !leftMap.isSourceLoaded("cm95"))return;
const values=[];
for(const feature of leftMap.querySourceFeatures("cm95",{sourceLayer:"depth_contour"})){
const value=Number(feature.properties && feature.properties.contour_depth_m);
if(Number.isFinite(value) && value>=RUNTIME_DEFAULTS.safetyContourM)values.push(value);
}
const next=values.length ? Math.min.apply(null,values) : null;
if(next===runtimeEffectiveSafetyContourM)return;
runtimeEffectiveSafetyContourM=next;
if(leftMap.getLayer("runtime-preview-depcnt")){
leftMap.setPaintProperty("runtime-preview-depcnt","line-color",runtimeDepcntColorExpression());
leftMap.setPaintProperty("runtime-preview-depcnt","line-width",runtimeDepcntWidthExpression());
}
}
function addRuntimeDepthAreaLayer(map){
const beforeId=map.getLayer("s52-p01-ac-land-area") ? "s52-p01-ac-land-area" : undefined;
map.addLayer({
id:"runtime-preview-depare",
type:"fill",
source:"cm95",
"source-layer":"baseline_area",
filter:["all",
["==",["get","s52_mariner_dependent"],1],
["match",["get","s52_runtime_csp"],["DEPARE01","DEPARE02"],true,false]
],
paint:{
"fill-color":runtimeDepthColorExpression(),
"fill-opacity":0.92
}
},beforeId);
}
function addRuntimeContourLayer(map){
map.addLayer({
id:"runtime-preview-depcnt",
type:"line",
source:"cm95",
"source-layer":"depth_contour",
filter:["all",
["==",["get","s52_mariner_dependent"],1],
["==",["get","s52_runtime_csp"],"DEPCNT02"]
],
paint:{
"line-color":runtimeDepcntColorExpression(),
"line-width":runtimeDepcntWidthExpression()
}
});
}
function addRuntimeSoundingLayer(map){
map.addLayer({
id:"runtime-preview-soundg",
type:"symbol",
source:"cm95",
"source-layer":"seabed_text_point",
filter:["all",
["==",["get","s52_mariner_dependent"],1],
["==",["get","s52_runtime_csp"],"SOUNDG02"],
["has","s52_sounding_depth_m"]
],
layout:{
"text-field":["coalesce",["get","chart_label_text"],["to-string",["get","s52_sounding_depth_m"]]],
"text-size":10,
"text-allow-overlap":false
},
paint:{
"text-color":["case",
["<=",["to-number",["get","s52_sounding_depth_m"],99999],RUNTIME_DEFAULTS.safetyDepthM],"#070707",
"#7d898c"
],
"text-halo-color":"#d4eaee",
"text-halo-width":0.5
}
});
}
function installRuntimePreviewLayers(map){
removeRuntimePreviewLayers(map);
if(!debugRuntimePreview.checked || !map.getSource("cm95"))return;
addRuntimeDepthAreaLayer(map);
addRuntimeContourLayer(map);
addRuntimeSoundingLayer(map);
setTimeout(updateRuntimeSafetyContour,0);
}
function applyDebugStyles(){
setLayerVisibility(rightMap,"bathymetry-support",debugRefBathy.checked);
setLayerVisibility(rightMap,"bathymetry-depth-labels",debugRefBathy.checked);
for(const map of [leftMap,rightMap]){
if(map.getLayer("depth-contours")){
map.setPaintProperty("depth-contours","line-color",
debugHighlightDepcnt.checked ? "#ff00ff" : "rgba(136,152,139,1)");
map.setPaintProperty("depth-contours","line-width",
debugHighlightDepcnt.checked ? 3 : 1);
for(const pair of [[leftMap,activeLeftStyleTemplate],[rightMap,activeRightStyleTemplate]]){
const map=pair[0], template=pair[1];
const depthLayers=((map.getStyle() || {}).layers || []).filter(function(layer){
return layer.type==="line" && layer["source-layer"]==="depth_contour";
});
for(const layer of depthLayers){
if(debugHighlightDepcnt.checked){
map.setPaintProperty(layer.id,"line-color","#ff00ff");
map.setPaintProperty(layer.id,"line-width",3);
}else{
if(layer.id==="runtime-preview-depcnt"){
map.setPaintProperty(layer.id,"line-color",runtimeDepcntColorExpression());
map.setPaintProperty(layer.id,"line-width",runtimeDepcntWidthExpression());
continue;
}
const original=templateLayer(template,layer.id);
if(original && original.paint && original.paint["line-color"]!==undefined){
map.setPaintProperty(layer.id,"line-color",original.paint["line-color"]);
}
if(original && original.paint && original.paint["line-width"]!==undefined){
map.setPaintProperty(layer.id,"line-width",original.paint["line-width"]);
}
}
}
}
const display=debugInspector.checked ? "" : "none";
@@ -123,6 +282,10 @@ const rightMap = new maplibregl.Map({
leftMap.addControl(new maplibregl.NavigationControl(),"top-right");
rightMap.addControl(new maplibregl.NavigationControl(),"top-right");
let syncing=false;
let referenceStyleTemplate=null;
let s52StyleTemplate=null;
let activeLeftStyleTemplate=null;
let activeRightStyleTemplate=null;
function sanitizeReferenceStyle(raw){
const style=JSON.parse(JSON.stringify(raw));
@@ -151,6 +314,47 @@ function cloneReferenceForLocal(referenceStyle){
return style;
}
function prepareS52Style(raw){
const style=JSON.parse(JSON.stringify(raw));
const source=style.sources && style.sources.cm95;
if(!source)throw new Error("完整 S-52 style 中找不到 sources.cm95");
source.type="vector";
source.tiles=[LOCAL_TILES];
source.minzoom=5;
source.maxzoom=12;
source.bounds=[128.0,29.5,132.5,34.8];
source.attribution="CM95 九州 S-52 Production Pipeline v2";
style.sprite=location.origin + "/s52/sprite";
if(referenceStyleTemplate && referenceStyleTemplate.glyphs){
style.glyphs=referenceStyleTemplate.glyphs;
}
style.name=(style.name || "CM95 S-52") + " / Full production debug";
return style;
}
async function ensureS52StyleTemplate(){
if(s52StyleTemplate)return s52StyleTemplate;
const response=await fetch("/s52/full-style.json",{cache:"no-store"});
if(!response.ok)throw new Error("S-52 full style HTTP " + response.status);
s52StyleTemplate=await response.json();
return s52StyleTemplate;
}
async function applyLeftStyleMode(){
if(debugLeftS52.checked){
setState(leftState,"加载完整 S-52 style…","");
const raw=await ensureS52StyleTemplate();
const style=prepareS52Style(raw);
activeLeftStyleTemplate=style;
leftMap.setStyle(JSON.parse(JSON.stringify(style)));
}else{
if(!referenceStyleTemplate)return;
const style=cloneReferenceForLocal(referenceStyleTemplate);
activeLeftStyleTemplate=style;
leftMap.setStyle(JSON.parse(JSON.stringify(style)));
}
}
async function loadComparisonStyles(){
try{
setState(rightState,"下载并检查参考 style…","");
@@ -161,8 +365,10 @@ async function loadComparisonStyles(){
const removedText=checked.removed.length
? ";忽略无效 layer: " + checked.removed.join(", ")
: "";
rightMap.setStyle(checked.style);
leftMap.setStyle(cloneReferenceForLocal(checked.style));
referenceStyleTemplate=checked.style;
activeRightStyleTemplate=checked.style;
rightMap.setStyle(JSON.parse(JSON.stringify(checked.style)));
await applyLeftStyleMode();
setState(rightState,"参考 style 已加载" + removedText,"ok");
}catch(err){
console.error(err);
@@ -173,10 +379,20 @@ async function loadComparisonStyles(){
leftMap.on("style.load",function(){
const sources=Object.keys((leftMap.getStyle() || {}).sources || {});
if(sources.includes("navsea_delivery"))setState(leftState,"Production v2 PBF 已接入","ok");
if(sources.includes("cm95")){
installRuntimePreviewLayers(leftMap);
setState(leftState,"Production v2 / S-52 style" + (debugRuntimePreview.checked ? " + runtime preview" : ""),"ok");
}else if(sources.includes("navsea_delivery")){
setState(leftState,"Production v2 / extentfix style","ok");
}
applyDebugStyles();
});
rightMap.on("style.load",applyDebugStyles);
leftMap.on("sourcedata",function(e){
if(e && e.sourceId==="cm95" && debugRuntimePreview.checked){
updateRuntimeSafetyContour();
}
});
loadComparisonStyles();
function cameraOf(map){
@@ -209,6 +425,19 @@ document.getElementById("debug-toggle-btn").addEventListener("click",function(){
document.getElementById("debug-collapse-btn").addEventListener("click",function(){
debugPanel.classList.add("collapsed");
});
debugLeftS52.addEventListener("change",function(){
applyLeftStyleMode().catch(function(err){
console.error(err);
setState(leftState,"左侧 S-52 style 切换失败:" + (err.message || err),"err");
});
});
debugRuntimePreview.addEventListener("change",function(){
if(leftMap.getSource("cm95")){
installRuntimePreviewLayers(leftMap);
setState(leftState,"Production v2 / S-52 style" + (debugRuntimePreview.checked ? " + runtime preview" : ""),"ok");
applyDebugStyles();
}
});
debugRefBathy.addEventListener("change",applyDebugStyles);
debugHighlightDepcnt.addEventListener("change",applyDebugStyles);
debugInspector.addEventListener("change",function(){

View File

@@ -7,7 +7,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlparse
RELEASE_ID = "cm95-d1-62a85bd40d3a-bbaaeafee1b4"
RELEASE_ID = "cm95-d1-62a85bd40d3a-2b2e7d976a43"
def parse_args():
@@ -24,11 +24,25 @@ def main():
project_root = web_root.parent
release_dir = project_root / "build" / "releases-s52-v2" / args.release_id
pbf_root = release_dir / "pbf" / "delivery"
report_root = release_dir / "reports" / "delivery"
manifest_path = release_dir / "release_manifest_v1.json"
html_path = web_root / "kyushu_compare.html"
assets_root = project_root / "build" / "fukuoka_s52_web" / "site" / "assets"
s52_style_path = project_root / "build" / "s52_style_compiler_release_2b2e7d976a43" / "style.s52.json"
s52_sprite_root = project_root / "build" / "s52_resources_v2" / "final"
required = [pbf_root, manifest_path, html_path, assets_root / "maplibre-gl.js", assets_root / "maplibre-gl.css"]
required = [
pbf_root,
manifest_path,
html_path,
assets_root / "maplibre-gl.js",
assets_root / "maplibre-gl.css",
s52_style_path,
s52_sprite_root / "s52-sprite-full.json",
s52_sprite_root / "s52-sprite-full.png",
s52_sprite_root / "s52-sprite-full@2x.json",
s52_sprite_root / "s52-sprite-full@2x.png",
]
missing = [str(path) for path in required if not path.exists()]
if missing:
raise SystemExit("Missing required path(s):\n" + "\n".join(missing))
@@ -66,6 +80,24 @@ def main():
if path == "/release-manifest.json":
self.send_bytes(manifest_path.read_bytes(), "application/json; charset=utf-8")
return
if path == "/s52/full-style.json":
self.send_bytes(s52_style_path.read_bytes(), "application/json; charset=utf-8")
return
if path in (
"/s52/sprite.json",
"/s52/sprite.png",
"/s52/sprite@2x.json",
"/s52/sprite@2x.png",
):
suffix = ".json" if path.endswith(".json") else ".png"
scale = "@2x" if "@2x" in path else ""
sprite_path = s52_sprite_root / f"s52-sprite-full{scale}{suffix}"
content_type = "application/json; charset=utf-8" if suffix == ".json" else "image/png"
self.send_bytes(sprite_path.read_bytes(), content_type)
return
if path in ("/assets/maplibre-gl.js", "/assets/maplibre-gl.css"):
filename = path.rsplit("/", 1)[-1]
file_path = assets_root / filename
@@ -85,6 +117,20 @@ def main():
return
tile = pbf_root / z / x / yfile
if not tile.is_file():
report = report_root / z / x / f"{y}.json"
if report.is_file():
try:
report_obj = json.loads(report.read_text(encoding="utf-8"))
except Exception:
report_obj = {}
if (
int(report_obj.get("planned_output_feature_count") or 0) == 0
and int(report_obj.get("output_feature_count") or 0) == 0
and not (report_obj.get("hard_gate_unmapped") or [])
and not (report_obj.get("encoder_dropped_features") or [])
):
self.send_bytes(b"", "application/vnd.mapbox-vector-tile", status=204)
return
self.send_error(404, "PBF tile not found")
return
self.send_bytes(tile.read_bytes(), "application/vnd.mapbox-vector-tile")