feat: finalize Kyushu S-52 production release
This commit is contained in:
171
docs/报告/CM95_S52_ProductionPipelineV2与九州运行报告_v3.md
Normal file
171
docs/报告/CM95_S52_ProductionPipelineV2与九州运行报告_v3.md
Normal 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 基线。
|
||||
@@ -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:
|
||||
|
||||
@@ -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