diff --git a/docs/报告/CM95_S52_ProductionPipelineV2与九州运行报告_v3.md b/docs/报告/CM95_S52_ProductionPipelineV2与九州运行报告_v3.md new file mode 100644 index 0000000..a0b9397 --- /dev/null +++ b/docs/报告/CM95_S52_ProductionPipelineV2与九州运行报告_v3.md @@ -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 基线。 diff --git a/src/cm95pbf/release.py b/src/cm95pbf/release.py index fb43f56..3aa436d 100644 --- a/src/cm95pbf/release.py +++ b/src/cm95pbf/release.py @@ -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: diff --git a/web/kyushu_compare.html b/web/kyushu_compare.html index 9c4b740..e34e0ed 100644 --- a/web/kyushu_compare.html +++ b/web/kyushu_compare.html @@ -51,10 +51,12 @@
@@ -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(){ diff --git a/web/kyushu_compare_server.py b/web/kyushu_compare_server.py index 978c32f..0c75c33 100644 --- a/web/kyushu_compare_server.py +++ b/web/kyushu_compare_server.py @@ -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")