feat: finalize Kyushu S-52 production release
This commit is contained in:
@@ -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(){
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user