feat: finalize Kyushu S-52 0.4.12 release

This commit is contained in:
OpenAI Codex
2026-09-20 14:39:00 +08:00
parent 99fdbdc694
commit aad1b403de
5 changed files with 283 additions and 12 deletions

View File

@@ -44,6 +44,7 @@
{"name": "facility_boundary_area", "capabilities": ["fill", "line", "symbol", "text"]}, {"name": "facility_boundary_area", "capabilities": ["fill", "line", "symbol", "text"]},
{"name": "facility_boundary_outline", "capabilities": ["line", "symbol", "text"]}, {"name": "facility_boundary_outline", "capabilities": ["line", "symbol", "text"]},
{"name": "facility_boundary_point", "capabilities": ["symbol", "text"]}, {"name": "facility_boundary_point", "capabilities": ["symbol", "text"]},
{"name": "fixed_fishing_gear_area", "capabilities": ["fill", "line", "symbol", "text"]},
{"name": "land_area", "capabilities": ["fill", "line", "symbol", "text"]}, {"name": "land_area", "capabilities": ["fill", "line", "symbol", "text"]},
{"name": "navigation_hazard_area", "capabilities": ["fill", "line", "symbol", "text"]}, {"name": "navigation_hazard_area", "capabilities": ["fill", "line", "symbol", "text"]},
{"name": "navigation_hazard_outline", "capabilities": ["line", "symbol", "text"]}, {"name": "navigation_hazard_outline", "capabilities": ["line", "symbol", "text"]},

View File

@@ -0,0 +1,270 @@
# CM95 S-52 0.4.12 OpenCPN 覆盖与危险物修复报告 v1
日期:2026-09-20
## 1. 结论
CM95 九州 S-52 0.4.12 已完成新的全量 PBF 生产、Source→PBF presentation conservation 审计、release finalize、release-scoped S-52 style 编译、runtime capability 审计以及实际浏览器回归。
正式 Release:
`cm95-d1-62a85bd40d3a-57e72826db1d`
Builder Contract:
`cm95pbf-builder/0.4.12-opencpn-cm93-coverage`
Production Pipeline:
`cm95pbf-production/2.3-opencpn-cm93-coverage`
Plan ID:
`57e72826db1d3e3e69f0b3b084bef4fb9469b5edc47dc0030fc2ec3e46adecf9`
## 2. 本阶段修复
### 2.1 M_COVR 语义
Builder 的 coverage 选择改为与 OpenCPN CM93 quilt 语义一致:
- 只有显式 `M_COVR` / `chart_coverage` 几何可以定义有效 chart coverage;
- 不再在 cell 缺少 M_COVR 时,把 `chart_cell bbox` 当成 coverage;
- 避免空或不完整的高细节 cell 用整个索引 bbox 错误遮挡较粗尺度内容;
- 独立 Source→PBF 守恒审计器同步采用同一 coverage 语义,避免 audit scope 与 Builder 分叉。
### 2.2 FSHHAV → FSHFAC
按 OpenCPN CM93 `CreateS57Obj()` 的类替换规则:
`FSHHAV → FSHFAC`
修正 Delivery mapping:
- FSHHAV AREA → `fixed_fishing_gear_area`
- FSHHAV POINT → `s52_points`
- 保留 S-52 `FSHHAV02` area pattern / dashed line;
- 保留 S-52 `FSHHAV01` point symbol;
- 不再把 FSHHAV 错误表达成 `fish_reef`。
全新 Release 扫描结果:
- `fixed_fishing_gear_area` 中 FSHFAC occurrence:769
- `s52_points` 中 FSHFAC occurrence:4022
- 新 FSHFAC layer 中 `fish_reef` occurrence:0
### 2.3 OBSTRN04 / WRECKS02 点危险物 Runtime
Web compare 增加 point runtime renderer,读取 PBF 中 mariner-neutral runtime facts,在当前 mariner defaults 下动态选择 S-52 symbol。
当前支持的 point symbol 包括:
- `ISODGR51`
- `DANGER01 / DANGER51 / DANGER52`
- `UWTROC03 / UWTROC04`
- `OBSTRN01 / OBSTRN03 / OBSTRN11`
- `WRECKS01 / WRECKS04 / WRECKS05`
- `LNDARE01`
OpenCPN `OBSTRN04` 可引用但 chartsymbols 中不存在的 `DANGER53` 继续 fail-closed,不自行创造替代资源。
当前 OBSTRN/WRECKS runtime capability 定义为 `PREVIEW_ONLY`,因为 point symbol 已实现,但 AREA/LINE 最终 runtime portrayal 仍未完成。
## 3. Production 结果
- planned jobs:5065
- PASS:5065
- FAILED:0
- PBF files:5064
- reports:5065
- explicit empty tile:1
- planned output features:476120
- report output features:476120
- decoded PBF features:476120
- tile footprint contract:PASS
- S-52 resource contract:PASS
- resource defined:1107
- resource active:977
- S-52 runtime data contract:PASS
- release failures:[]
Runtime CSP counts:
- DEPARE01:47516
- DEPCNT02:46189
- OBSTRN04:45633
- SOUNDG02:146728
- WRECKS02:909
## 4. Multi-LIGHTS 与 Presentation Conservation
Release finalize 的 multi-light contract:
- required:true
- status:PASS
- report multi-light groups:357
- PBF multi-light groups:357
- report light presentation records:745
- PBF light presentation records:745
- failure count:0
0.4.12 最初 finalize 时因版本判断仍只匹配 `0.4.11-s52-multi-light-presentations` 字符串,曾把该 contract 错误标为 NOT_APPLICABLE。
已修正为使用 `_builder_contract_at_least(..., (0,4,11))`,重新 finalize 后 0.4.12 正确执行 multi-light 硬门槛并 PASS。
独立 Source→PBF presentation conservation:
- expected multi-light groups:187
- actual multi-light groups:187
- expected presentation records:390
- actual presentation records:390
- missing:0
- unexpected:0
- signature mismatch:0
- tile occurrence presentation drift:0
- tile occurrence scope mismatch:0
- source resolution failure:0
- PBF parse failure:0
- failures:[]
Artifact:
`build/audits/cm95_s52_kyushu_v4_presentation_conservation.json`
Manifest 中该 contract 状态:PASS。
## 5. Release-scoped S-52 Style
输出:
`build/s52_style_compiler_release_57e72826db1d/style.s52.json`
第一次编译正确发现新 source layer 尚未进入 compiler contract:
- `SOURCE_LAYER_CONTRACT_GAP: fixed_fishing_gear_area`
- AP materialized coverage gap:769
- LS materialized coverage gap:769
这证明 PBF 已有 FSHFAC area presentation facts,但完整 style 尚未消费该新 layer。
修复:
`config/s52/cm95_s52_style_compiler_v1.json`
加入:
`fixed_fishing_gear_area: fill / line / symbol / text`
重新编译结果:
- hard_gate_failures:[]
- resource contract:PASS
- compiled layers:140
- PBF count:5064
- feature count:476120
- AP gap:0
- LS gap:0
- style bytes:1549579
因此 FSHFAC area 不再只是“PBF 有数据”,完整 S-52 style 也已实际消费 AP/LS presentation。
## 6. Runtime Capability Audit
CA 单项:
- RUNTIME_CA feature count:1276
- status:IMPLEMENTED
- fidelity:S52_SCREEN_MM
- audit:PASS
Artifact:
`build/audits/cm95_s52_kyushu_v4_runtime_ca_capability.json`
完整 Web target strict audit:
- RUNTIME_CA:IMPLEMENTED
- RUNTIME_DEPARE:PREVIEW_ONLY
- RUNTIME_DEPCNT:PREVIEW_ONLY
- RUNTIME_SOUNDG:PREVIEW_ONLY
- RUNTIME_OBSTRN:PREVIEW_ONLY
- RUNTIME_WRECKS:PREVIEW_ONLY
因此 full runtime capability audit 仍为:
`FAIL — runtime_capability_not_implemented`
这是预期且正确的严格结果,不影响 Data Release PASS,也不得据此声称当前 Web compare 已实现完整 S-52 runtime parity。
Artifact:
`build/audits/cm95_s52_kyushu_v4_web_runtime_capability.json`
## 7. 实际浏览器回归
### 7.1 OBSTRN / WRECKS
测试位置约:
`130.277626E, 33.692775N`
真实 Chrome + MapLibre:
- Production v4 / 0.4.12 页面已加载
- `runtime-preview-hazard-symbols` layer 存在
- cm95 source loaded
- queryRenderedFeatures:38
- 实际出现的 runtime symbols 包括:
- DANGER51
- UWTROC03
- UWTROC04
- `leftMap.isStyleLoaded() = true`
截图:
`/tmp/cm95_v4_hazard_runtime.png`
### 7.2 Multi-LIGHTS / CA
测试位置:
`130.78248061577605E, 34.09885081506925N`
实际 runtime 仍得到两条 sector:
- source record 3077:LITYW,66° → 106°
- source record 3078:LITYW,116° → 320°
Canvas non-transparent pixels:4427。
因此 0.4.12 coverage / FSHFAC 修正没有破坏 0.4.11 已通过的 multi-LIGHTS / CA 闭环。
## 8. 18888
比较服务器已经切换到:
`cm95-d1-62a85bd40d3a-57e72826db1d`
Release-scoped style:
`build/s52_style_compiler_release_57e72826db1d/style.s52.json`
页面标识:
`Production v4 / 0.4.12`
当前 `/health`:
- release_status:PASS
- pbf_file_count:5064
- decoded_pbf_features:476120
- s52_runtime_contract:PASS
- s52_light_presentation_contract:PASS
- s52_presentation_conservation_contract:PASS
Style / sprite 资源均返回 HTTP 200。
## 9. 剩余边界
0.4.12 Data Release 已可正式使用。
仍未完成的是完整 runtime portrayal,而不是 PBF 数据 release:
- DEPARE / DEPCNT:仍为默认 mariner settings preview
- SOUNDG:仍未实现完整 S-52 glyph composition
- OBSTRN / WRECKS:当前只完成 Point symbol runtime,AREA/LINE pending
因此下一阶段如继续追求与 OpenCPN 的完整视觉一致性,应优先完成上述 runtime CSP,而不是再次重建当前 0.4.12 PBF。

View File

@@ -440,8 +440,8 @@ def finalize_release(
s52_runtime_leaks = Counter() s52_runtime_leaks = Counter()
s52_runtime_fact_counts = Counter() s52_runtime_fact_counts = Counter()
s52_runtime_unknown_fact_exceptions = Counter() s52_runtime_unknown_fact_exceptions = Counter()
light_presentation_required = str(plan.get("builder_contract_version") or "").startswith( light_presentation_required = _builder_contract_at_least(
"cm95pbf-builder/0.4.11-s52-multi-light-presentations" plan.get("builder_contract_version"), (0, 4, 11)
) )
pbf_multi_light_groups = 0 pbf_multi_light_groups = 0
pbf_light_presentation_records = 0 pbf_light_presentation_records = 0

View File

@@ -44,7 +44,7 @@
<body> <body>
<div id="toolbar"> <div id="toolbar">
<strong>九州 S-52 PBF 对比</strong> <strong>九州 S-52 PBF 对比</strong>
<span class="meta">左:Production v3 / 0.4.11 本地 PBF 右:extentfix 原始数据源</span> <span class="meta">左:Production v4 / 0.4.12 本地 PBF 右:extentfix 原始数据源</span>
<label><input id="sync-toggle" type="checkbox" checked />同步视图</label> <label><input id="sync-toggle" type="checkbox" checked />同步视图</label>
<button id="reset-btn" type="button">重置九州</button> <button id="reset-btn" type="button">重置九州</button>
<button id="debug-toggle-btn" type="button">调试</button> <button id="debug-toggle-btn" type="button">调试</button>
@@ -63,7 +63,7 @@
<section class="pane" id="left-pane"> <section class="pane" id="left-pane">
<div id="left-map" class="map"></div> <div id="left-map" class="map"></div>
<canvas id="left-ca-overlay" class="ca-overlay" aria-hidden="true"></canvas> <canvas id="left-ca-overlay" class="ca-overlay" aria-hidden="true"></canvas>
<div class="pane-title"><b>左:九州 Production v3 / 0.4.11</b><span id="left-state" class="state">等待参考 style…</span></div> <div class="pane-title"><b>左:九州 Production v4 / 0.4.12</b><span id="left-state" class="state">等待参考 style…</span></div>
<div id="left-inspector" class="inspector empty">点击任一侧海图,对比同一位置的 Feature。</div> <div id="left-inspector" class="inspector empty">点击任一侧海图,对比同一位置的 Feature。</div>
<div class="divider-label">LOCAL</div> <div class="divider-label">LOCAL</div>
</section> </section>
@@ -598,8 +598,8 @@ function cloneReferenceForLocal(referenceStyle){
source.minzoom=5; source.minzoom=5;
source.maxzoom=12; source.maxzoom=12;
source.bounds=[128.0,29.5,132.5,34.8]; source.bounds=[128.0,29.5,132.5,34.8];
source.attribution="CM95 九州 S-52 Production 0.4.11 multi-light"; source.attribution="CM95 九州 S-52 Production 0.4.12 OpenCPN coverage";
style.name=(style.name || "NavSea") + " / Kyushu Production v3 / 0.4.11"; style.name=(style.name || "NavSea") + " / Kyushu Production v4 / 0.4.12";
return style; return style;
} }
@@ -612,7 +612,7 @@ function prepareS52Style(raw){
source.minzoom=5; source.minzoom=5;
source.maxzoom=12; source.maxzoom=12;
source.bounds=[128.0,29.5,132.5,34.8]; source.bounds=[128.0,29.5,132.5,34.8];
source.attribution="CM95 九州 S-52 Production 0.4.11 multi-light"; source.attribution="CM95 九州 S-52 Production 0.4.12 OpenCPN coverage";
style.sprite=location.origin + "/s52/sprite"; style.sprite=location.origin + "/s52/sprite";
if(referenceStyleTemplate && referenceStyleTemplate.glyphs){ if(referenceStyleTemplate && referenceStyleTemplate.glyphs){
style.glyphs=referenceStyleTemplate.glyphs; style.glyphs=referenceStyleTemplate.glyphs;
@@ -670,9 +670,9 @@ leftMap.on("style.load",function(){
const sources=Object.keys((leftMap.getStyle() || {}).sources || {}); const sources=Object.keys((leftMap.getStyle() || {}).sources || {});
if(sources.includes("cm95")){ if(sources.includes("cm95")){
installRuntimePreviewLayers(leftMap); installRuntimePreviewLayers(leftMap);
setState(leftState,"Production v3 / 0.4.11 / S-52 style" + (debugRuntimePreview.checked ? " + runtime preview" : ""),"ok"); setState(leftState,"Production v4 / 0.4.12 / S-52 style" + (debugRuntimePreview.checked ? " + runtime preview" : ""),"ok");
}else if(sources.includes("navsea_delivery")){ }else if(sources.includes("navsea_delivery")){
setState(leftState,"Production v3 / 0.4.11 / extentfix style","ok"); setState(leftState,"Production v4 / 0.4.12 / extentfix style","ok");
} }
applyDebugStyles(); applyDebugStyles();
refreshLightArcFeatures(); refreshLightArcFeatures();
@@ -733,7 +733,7 @@ debugLeftS52.addEventListener("change",function(){
debugRuntimePreview.addEventListener("change",function(){ debugRuntimePreview.addEventListener("change",function(){
if(leftMap.getSource("cm95")){ if(leftMap.getSource("cm95")){
installRuntimePreviewLayers(leftMap); installRuntimePreviewLayers(leftMap);
setState(leftState,"Production v3 / 0.4.11 / S-52 style" + (debugRuntimePreview.checked ? " + runtime preview" : ""),"ok"); setState(leftState,"Production v4 / 0.4.12 / S-52 style" + (debugRuntimePreview.checked ? " + runtime preview" : ""),"ok");
applyDebugStyles(); applyDebugStyles();
} }
}); });

View File

@@ -7,7 +7,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse
RELEASE_ID = "cm95-d1-62a85bd40d3a-1c27f0a66f6b" RELEASE_ID = "cm95-d1-62a85bd40d3a-57e72826db1d"
def parse_args(): def parse_args():
@@ -28,7 +28,7 @@ def main():
manifest_path = release_dir / "release_manifest_v1.json" manifest_path = release_dir / "release_manifest_v1.json"
html_path = web_root / "kyushu_compare.html" html_path = web_root / "kyushu_compare.html"
assets_root = project_root / "build" / "fukuoka_s52_web" / "site" / "assets" assets_root = project_root / "build" / "fukuoka_s52_web" / "site" / "assets"
s52_style_path = project_root / "build" / "s52_style_compiler_release_1c27f0a66f6b" / "style.s52.json" s52_style_path = project_root / "build" / "s52_style_compiler_release_57e72826db1d" / "style.s52.json"
s52_sprite_root = project_root / "build" / "s52_resources_v2" / "final" s52_sprite_root = project_root / "build" / "s52_resources_v2" / "final"
required = [ required = [