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

@@ -0,0 +1,171 @@
# CM95 S-52 Production Pipeline v2 与九州运行报告 v3
日期:2026-09-19
## 1. 本阶段结论
本阶段已完成九州工程 AOI 的 S-52 资源契约补全、MySQL 8 空间查询修复、全量 PBF 重新生产、Release finalize、release-scoped S-52 style 编译和 Web 对比页切换。
最终 Release 状态为 **PASS**。本次 PASS 表示 Production Pipeline、AOI footprint、S-52 resource/runtime contract、PBF/report/decoded feature 对账和当前 release gate 均通过;不表示所有 ENC 语义待审项目已经清零。
## 2. 权威输入与版本
- OpenCPN S-52 参考 commit:`f7211b4e1874c453e37a7f190aa191c55a25c32e`
- Builder contract:`cm95pbf-builder/0.4.10-s52-resource-contract`
- Production pipeline:`cm95pbf-production/2.2-s52-resource-contract`
- Lookup IR SHA-256:`f63cbc0d3c9317b1873c0b0c470a5e7371d1b6d8d58393da660f70b837a52eb7`
- Resource contract SHA-256:`ebdce70cf8f67dff3ecdeecc8e8f161d9626153a860c69972d41612ed803ef1e`
## 3. S-52 资源契约
按当前 OpenCPN 定义统计,完整资源宇宙为:
- SY:1018
- AP:30
- LC:59
- Defined total:1107
- Active reachable:SY 902 / AP 27 / LC 48 / total 977
`build/s52_resources_v2/resource_contract.json` 最终为 **PASS**,defined missing=0,active missing=0。
资源编译同时处理了 OpenCPN 重名 symbol 的 bitmap/vector 选择、真正 vector-only symbol、全部 vector AP、全部 LC,以及 OpenCPN 自身未定义资源引用的官方 fallback 语义;未人为伪造上游不存在的资源。
## 4. AOI 与生产计划
工程 AOI:`kyushu-main-nearby-v1`
bbox:`128.0,29.5,132.5,34.8`
Zoom 5–12 的完整计划数:
- z5=2,z6=4,z7=12,z8=24
- z9=70,z10=247,z11=962,z12=3744
- total=5065
Plan ID:`2b2e7d976a43df64da3d031c3863ab596469c7331fc990a4025238e6b9b0d0c9`
Release ID:`cm95-d1-62a85bd40d3a-2b2e7d976a43`
旧 incomplete release `cm95-d1-62a85bd40d3a-7cb5890160d2` 已在本次生产前整目录删除。新 release 从零建立,不做 migrate / hardlink / copy,也不复用旧 PBF。历史已 finalize 的 sparse release `cm95-d1-62a85bd40d3a-bbaaeafee1b4` 仅作为历史参考保留。
## 5. MySQL 8 空间索引问题与修复
实际生产数据库位于 NAS `192.168.200.230` 的 MySQL 8.0.45 Docker 实例,数据库为 `cm95_japan`;X220 本地 MySQL 不是本次生产数据源。
原 geometry 列虽然数据实际为 SRID 4326,但列定义没有声明固定 SRID。MySQL 8 因此不把现有 SPATIAL index 作为查询候选,即使 SQL 使用 `FORCE INDEX(spx_*)`,执行计划仍为 `type=ALL / possible_keys=None`,每个 tile 会反复扫描几十万 geometry。
隔离实验确认:将列声明改为 `... NOT NULL SRID 4326` 后,执行计划变为 `type=range` 并实际使用 `spx_*`。
已修复:
- `chart_area_geometry` + `spx_area_geom`
- `chart_line_geometry` + `spx_line_geom`
- `chart_point_geometry` + `spx_point_geom`
- `chart_sounding_point` + `spx_sounding_geom`
代表性查询修复后实测:AREA 约 0.31s、LINE 热缓存约 0.09s、POINT 约 0.02s、SOUNDING 约 0.03–0.07s。生产恢复后吞吐显著提升。
仓库仍保留 MySQL 5.7 历史兼容 schema,本次没有把 `sql/001_schema_mysql57.sql` 强行改成 MySQL 8 专用列语法。后续若新建 MySQL 8 production 数据库,必须在投入 PBF 生产前同样确认四张空间表的列级 `SRID 4326` 与 `spx_*` 实际可用,避免回退到全表扫描。
## 6. 九州 Production 最终结果
生产任务最终为:
- PASS:5065 / 5065
- FAILED:0
- Report:5065
- 实体 PBF:5064
- 合法显式空 tile:1
- planned output features:476121
- report output features:476121
- decoded PBF features:476121
唯一没有实体 PBF 的 tile 是 `12/3530/1648`。其 report 明确记录 `planned_output_feature_count=0`、`output_feature_count=0`,且无 hard gate、无 encoder drop,因此属于合法空瓦片,不应伪造空 PBF。
Release gate 已调整为:AOI complete 要求每个计划 tile 都有 PASS report;非空 tile 必须有 PBF;经过明确验证的空 tile 可以不产生实体 PBF。
最终 `tile_footprint_contract.status=PASS`,`materialized_or_empty_tile_count=5065`,missing non-empty PBF=[],unexpected PBF=[]。
## 7. S-52 Runtime Contract
本 release 使用:
- point table:Paper
- boundary table:Plain
- mariner mode:runtime
Final manifest:
- `s52_resource_contract.status=PASS`
- `s52_runtime_contract.status=PASS`
- mariner-dependent features:286975
- dynamic final leaks:`{}`
- runtime failure count:0
Runtime CSP 数量:
- DEPARE01:47516
- DEPCNT02:46189
- OBSTRN04:45633
- SOUNDG02:146728
- WRECKS02:909
因此 PBF 保存水深、危险物、sounding 等事实字段,而不会永久写死当前用户的 safety contour / safety depth / isolated danger 等动态最终结果。
## 8. Release-scoped S-52 Style
对新 release 的 5064 个实体 PBF 做 aggregate usage 编译后生成:
`build/s52_style_compiler_release_2b2e7d976a43/style.s52.json`
结果:
- compiled layers:142
- 使用范围:5064 PBF / 28 source layers / 476121 features
- resource contract:PASS
- hard gate failures:[]
关键 FSHFAC、FSHHAV、SOUNDG 和 LOWACC/fallback 资源均可在生成 style 中找到。
完整 sprite 使用 `build/s52_resources_v2/final/s52-sprite-full`,并补齐浏览器需要的 1x / 2x sprite JSON/PNG。
## 9. Web 对比与实际 WebGL 验证
比较服务已切换到新 Release,端口 `18888`。
`/health` 当前返回新 release、release status PASS、PBF=5064、decoded features=476121、S-52 runtime contract PASS。
浏览器实际加载验证已完成,而不是只检查 DOM:
- release-scoped style:HTTP 200
- sprite 1x/2x JSON/PNG:HTTP 200
- 普通 PBF:HTTP 200
- 合法空 tile:HTTP 204
- 左右 MapLibre style 状态均为 OK
- 左侧实际 `querySourceFeatures` / `queryRenderedFeatures` 有数据
- 静态 LANDA fill 在截图中实际出现;其 0.92 opacity 混合色 `#CABD83` 有大量像素
为验证 runtime PBF,本地比较页增加了可关闭的 runtime 默认阈值预览。默认值来自现有 `S52MarinerSettings`:shallow=2m、safety contour=3m、safety depth=3m、deep=6m。
z11 近岸样本实测:
- effective safety contour:5m
- runtime DEPARE rendered:24
- runtime DEPCNT rendered:35
- runtime SOUNDG rendered:61
- source SOUNDG facts:202
实际截图已生成并检查。runtime preview 只读取 PBF facts,不修改 PBF;Hazard exact isolated-danger / symbol composition 仍明确留给正式 NavSea Runtime Adapter,不在 Web 调试页中伪造。
## 10. 仍然保留的语义待审项
Final manifest 的 `semantic_completeness` 仍为 **PENDING_REVIEW**:
- observed gap variants:141
- pending semantic review:112
- pending semantic review feature rows:52828
这部分是既有语义映射 backlog,与本次资源契约、PBF 技术生产和 runtime contract PASS 是不同维度。
因此本阶段可以确认“技术生产链与 S-52 资源/runtime 契约闭环”,但不能描述为“全部 ENC/S-52 业务语义已经 100% 完成”。
## 11. 最终结论
九州完整 AOI 的新 S-52 runtime Release 已完成从零生产并 finalize PASS。MySQL 8 空间查询根因已确认并在当前 NAS 数据库修复,生产查询已真正使用 SPATIAL index。
新 Release、完整 S-52 资源、release-scoped style、Web 对比服务和 runtime facts 预览已连通。后续工作可以基于该 Release 继续做语义 backlog 审查和正式 NavSea Runtime Adapter,而不需要重新设计本次 Production/Resource Contract 基线。

View File

@@ -482,17 +482,54 @@ def finalize_release(
}
tile_enumeration_mode = str(plan.get("tile_enumeration_mode") or "coverage")
pbf_rel_paths = {
path.relative_to(pbf_root).as_posix()
for path in pbf_files
}
explicit_empty_tiles: list[str] = []
missing_nonempty_pbf_tiles: list[str] = []
report_tile_paths: set[str] = set()
for path in report_files:
obj = json.loads(path.read_text(encoding="utf-8"))
rel_pbf = path.relative_to(report_root).with_suffix(".pbf").as_posix()
report_tile_paths.add(rel_pbf)
if rel_pbf in pbf_rel_paths:
continue
planned_count = int(obj.get("planned_output_feature_count") or 0)
output_count = int(obj.get("output_feature_count") or 0)
hard_gate_count = len(obj.get("hard_gate_unmapped") or [])
encoder_drop_count = len(obj.get("encoder_dropped_features") or [])
if (
planned_count == 0
and output_count == 0
and hard_gate_count == 0
and encoder_drop_count == 0
):
explicit_empty_tiles.append(rel_pbf)
else:
missing_nonempty_pbf_tiles.append(rel_pbf)
unexpected_pbf_tiles = sorted(pbf_rel_paths - report_tile_paths)
tile_footprint_contract = {
"status": "NOT_APPLICABLE",
"mode": tile_enumeration_mode,
"planned_tile_jobs": int(plan["job_count"]),
"pbf_file_count": len(pbf_files),
"report_file_count": len(report_files),
"explicit_empty_tile_count": len(explicit_empty_tiles),
"materialized_or_empty_tile_count": len(pbf_files) + len(explicit_empty_tiles),
"missing_nonempty_pbf_tiles": missing_nonempty_pbf_tiles,
"unexpected_pbf_tiles": unexpected_pbf_tiles,
}
if tile_enumeration_mode == "aoi-complete":
tile_footprint_contract["status"] = (
"PASS"
if len(pbf_files) == len(report_files) == int(plan["job_count"])
if (
len(report_files) == int(plan["job_count"])
and len(pbf_files) + len(explicit_empty_tiles) == int(plan["job_count"])
and not missing_nonempty_pbf_tiles
and not unexpected_pbf_tiles
)
else "FAIL"
)
@@ -551,8 +588,8 @@ def finalize_release(
if tile_enumeration_mode == "aoi-complete":
if len(report_files) != int(plan["job_count"]):
failures.append("aoi_complete_report_count_mismatch")
if len(pbf_files) != int(plan["job_count"]):
failures.append("aoi_complete_pbf_count_mismatch")
if tile_footprint_contract["status"] != "PASS":
failures.append("aoi_complete_tile_footprint_mismatch")
if hard_gate:
failures.append("hard_gate_unmapped")
if encoder_dropped:

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")