From ed53864598f3059f524841fbe25dcd488fc1301a Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Sun, 27 Sep 2026 10:20:23 +0800 Subject: [PATCH] feat: enable Japan nationwide CM95 S-52 comparison page --- ...5_s52_style_compiler_japan_compare_v1.json | 283 +++++ docs/CM95_S52_日本全国HTML对比使用说明_v1.md | 31 + .../CM95_S52_日本全国HTML对比启用报告_v1.md | 34 + tools/cm95_s52_japan_compare_build_style.py | 108 ++ web/japan_compare.html | 994 ++++++++++++++++++ web/japan_compare_server.py | 199 ++++ 6 files changed, 1649 insertions(+) create mode 100644 config/s52/cm95_s52_style_compiler_japan_compare_v1.json create mode 100644 docs/CM95_S52_日本全国HTML对比使用说明_v1.md create mode 100644 docs/报告/CM95_S52_日本全国HTML对比启用报告_v1.md create mode 100644 tools/cm95_s52_japan_compare_build_style.py create mode 100644 web/japan_compare.html create mode 100644 web/japan_compare_server.py diff --git a/config/s52/cm95_s52_style_compiler_japan_compare_v1.json b/config/s52/cm95_s52_style_compiler_japan_compare_v1.json new file mode 100644 index 0000000..a149c19 --- /dev/null +++ b/config/s52/cm95_s52_style_compiler_japan_compare_v1.json @@ -0,0 +1,283 @@ +{ + "schema": "navsea.cm95.s52-style-compiler-config/v1", + "name": "CM95 S-52 Japan nationwide comparison style v1", + "palette": "DAY_BRIGHT", + "sprite": "http://192.168.200.184:18889/s52/sprite", + "background_color": "DEPDW", + "fallback_color": "NODTA", + "area_fill_opacity": 0.92, + "area_pattern_opacity": 0.85, + "line_width_fallback": 1.0, + "complex_line_width_px": 5.0, + "display_categories": [ + "Displaybase", + "Standard", + "Other" + ], + "source": { + "id": "cm95", + "type": "vector", + "tiles": [ + "http://192.168.200.184:18889/pbf/{z}/{x}/{y}.pbf" + ], + "bounds": [ + 122.0, + 20.0, + 154.5, + 46.0 + ], + "minzoom": 0, + "maxzoom": 12 + }, + "resources": { + "colors": "build/s52_resources_v2/base/s52_colors.json", + "symbol_metadata": "build/s52_resources_v2/base/s52_symbol_metadata.json", + "bitmap_pattern_metadata": "build/s52_resources_v2/base/s52_pattern_metadata.json", + "vector_pattern_metadata": "build/s52_resources_v2/ap/s52-sprite-ap-pattern-metadata.json", + "line_metadata": "build/s52_resources_v2/final/s52-sprite-full-line-metadata.json", + "sprite_index": "build/s52_resources_v2/final/s52-sprite-full.json", + "lookup_ir": "build/s52_presentation_ir_v2/lookup_rule_ir.json", + "resource_contract": "build/s52_resources_v2/resource_contract.json" + }, + "source_layers": [ + { + "name": "anchor_caution_hazard_area", + "capabilities": [ + "fill", + "line", + "symbol", + "text" + ] + }, + { + "name": "anchor_caution_hazard_point", + "capabilities": [ + "symbol", + "text" + ] + }, + { + "name": "anchorage_area", + "capabilities": [ + "fill", + "line", + "symbol", + "text" + ] + }, + { + "name": "anchorage_outline", + "capabilities": [ + "line", + "symbol", + "text" + ] + }, + { + "name": "anchorage_point", + "capabilities": [ + "symbol", + "text" + ] + }, + { + "name": "baseline_area", + "capabilities": [ + "fill", + "line", + "symbol", + "text" + ] + }, + { + "name": "baseline_line", + "capabilities": [ + "line", + "symbol", + "text" + ] + }, + { + "name": "bridge_structure", + "capabilities": [ + "fill", + "line", + "symbol", + "text" + ] + }, + { + "name": "clearance_limit_line", + "capabilities": [ + "line", + "symbol", + "text" + ] + }, + { + "name": "clearance_limit_point", + "capabilities": [ + "symbol", + "text" + ] + }, + { + "name": "cm95_unmapped", + "capabilities": [ + "fill", + "line", + "symbol", + "text" + ] + }, + { + "name": "depth_contour", + "capabilities": [ + "line", + "text" + ] + }, + { + "name": "facility_boundary_area", + "capabilities": [ + "fill", + "line", + "symbol", + "text" + ] + }, + { + "name": "facility_boundary_outline", + "capabilities": [ + "line", + "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": "navigation_hazard_area", + "capabilities": [ + "fill", + "line", + "symbol", + "text" + ] + }, + { + "name": "navigation_hazard_outline", + "capabilities": [ + "line", + "symbol", + "text" + ] + }, + { + "name": "navigation_hazard_point", + "capabilities": [ + "symbol", + "text" + ] + }, + { + "name": "navigation_marks", + "capabilities": [ + "symbol", + "text" + ] + }, + { + "name": "onshore_structure_area", + "capabilities": [ + "fill", + "line", + "symbol", + "text" + ] + }, + { + "name": "onshore_structure_point", + "capabilities": [ + "symbol", + "text" + ] + }, + { + "name": "pilot_station_point", + "capabilities": [ + "symbol", + "text" + ] + }, + { + "name": "place_label_land", + "capabilities": [ + "symbol", + "text" + ] + }, + { + "name": "s52_areas", + "capabilities": [ + "fill", + "line", + "symbol", + "text" + ] + }, + { + "name": "s52_lines", + "capabilities": [ + "line", + "symbol", + "text" + ] + }, + { + "name": "s52_points", + "capabilities": [ + "symbol", + "text" + ] + }, + { + "name": "seabed_line", + "capabilities": [ + "line", + "symbol", + "text" + ] + }, + { + "name": "seabed_text_point", + "capabilities": [ + "symbol", + "text" + ] + } + ] +} diff --git a/docs/CM95_S52_日本全国HTML对比使用说明_v1.md b/docs/CM95_S52_日本全国HTML对比使用说明_v1.md new file mode 100644 index 0000000..3688499 --- /dev/null +++ b/docs/CM95_S52_日本全国HTML对比使用说明_v1.md @@ -0,0 +1,31 @@ +# CM95 S-52 日本全国 HTML 对比使用说明 v1 + +日期:2026-09-27。项目:Linux X220 的 `/root/sourceserver/cm95`。 + +## 访问入口 + +- 全国对比:`http://192.168.200.184:18889/`,局域网访问。 +- 旧九州对比:`http://192.168.200.184:18888/`,保持不变。 +- 全国健康检查:`http://192.168.200.184:18889/health`。 +- 全国生产摘要:`http://192.168.200.184:18889/release-summary.json`。 +- 全国 PBF:`/pbf/{z}/{x}/{y}.pbf`,对应现有 Release 的 `pbf/delivery` 原始产物。 +- 左侧:全国 CM95 现有 S-52 Release;右侧:原 NavSea extentfix 旧版参考(不是同一版,也非官方基准)。 +- 保留旧页面的同步视图、完整 S-52/参考 Style 开关、runtime 预览、灯弧、等深线高亮、左右点选对象检查;新增全国总览与地区快捷定位。 + +## 版本和限制 + +- Release:`cm95-d1-62a85bd40d3a-ae61b8866e80`,Dataset 1,z0~z12。 +- 生产 177389 Job 中 177384 PASS,5 个低 Zoom FAILED;用户已接受遗留。z0~z3 总览可能缺图,尤其 z0/0/0。 +- 全国 PBF 原样全量提供;浏览器 S-52 Style 使用分层确定性抽样扫描实际 PBF 的使用情况以控制图层数量。此测试 Style **不代表全国全部符号已经逐条覆盖/验收**。 +- 右侧是不同数据/不同 Style 版本的视觉参考,差异需要再区分数据、Style、缩放及运行时原因。 +- 不存在已通过的全国 Finalize manifest;健康检查以原始生产摘要报告真实状态 `TEST_READY_WITH_KNOWN_ISSUES`。 +- 此对比入口用于辅助导航工程阶段 B 的调查与视觉验证,不是 B 阶段 PASS、官方 ENC 或 ECDIS 认证。 + +## 文件与运行 + +- 页面:`web/japan_compare.html`;服务:`web/japan_compare_server.py`;原九州页面/服务不改动。 +- 样式配置:`config/s52/cm95_s52_style_compiler_japan_compare_v1.json`。 +- 样式生成器:`tools/cm95_s52_japan_compare_build_style.py`;产物:`build/s52_style_compiler_japan_compare_v1/`。 +- 样式需要重建时,在项目目录执行 `./.venv/bin/python tools/cm95_s52_japan_compare_build_style.py`。 +- 服务重启命令:`./.venv/bin/python -u web/japan_compare_server.py --host 0.0.0.0 --port 18889`。 +- 不触碰 MySQL、Builder、生产 SQLite、原 PBF 或旧九州端口。 \ No newline at end of file diff --git a/docs/报告/CM95_S52_日本全国HTML对比启用报告_v1.md b/docs/报告/CM95_S52_日本全国HTML对比启用报告_v1.md new file mode 100644 index 0000000..779e473 --- /dev/null +++ b/docs/报告/CM95_S52_日本全国HTML对比启用报告_v1.md @@ -0,0 +1,34 @@ +# CM95 S-52 日本全国 HTML 对比启用报告 v1 + +日期:2026-09-27;设备:Linux X220;项目:`/root/sourceserver/cm95`。 + +## 1. 入口和实际数据 + +- 全国入口:`http://192.168.200.184:18889/`;旧九州入口:`http://192.168.200.184:18888/`(未修改)。 +- 全国 Release:`cm95-d1-62a85bd40d3a-ae61b8866e80`;左侧直接读取现有 `pbf/delivery`。 +- 右侧沿用旧 `style.navsea-delivery-full-semantic-extentfix.json`,与全国 S-52 不是同版本对等数据集,供视觉排查参考。 +- 全国生产状态仍是 177389 Job、177384 PASS、5 FAILED、177383 PBF;不伪造 Finalize manifest。 +- 页面使用独立 `web/japan_compare.html` 和 `web/japan_compare_server.py`,不覆盖九州页面及服务。 + +## 2. 浏览器样式与功能 + +- 样式按真实全国 PBF 的确定性分层/港口抽样编译:2418 / 177383 文件;产出 170 图层、1735989 字节。 +- 样式资源契约 PASS、hard_gate_failures=[];抽样仅控制 Style 用到的规则/符号集合,所有原 PBF 按请求直接服务。 +- 继续支持左右同步、S-52/extentfix 样式开关、runtime/灯弧预览、等深线高亮、点选对象检查。 +- 新增全国总览和 9 个地区快捷入口;z0~z3 的 5 个失败 Tile 有 UI 警告。 +- 全国 B 阶段尚未 PASS;本页面提供调查入口,不替代原始语义审计或实机航行验证。 + +## 3. 联通和烟雾检查 + +- 从另一台 Windows 设备访问全国根路径和健康检查:HTTP 200,Release ID 正确。 +- 全国 12 地区 × z10/z11/z12:36/36 PBF HTTP 200,MVT MIME 正确。 +- Style、Sprite、MapLibre JS、本地 Fukuoka z12 PBF:HTTP 200;旧九州根路径继续 HTTP 200。 +- headless Chromium 加载全国双地图:左右状态为已加载;本地 source loaded 且可查询已渲染 Feature;东京湾快捷定位后左右中心一致,可检索两侧 Feature。 +- 当前页面含 runtime 动态覆盖;headless 的 `leftMap.isStyleLoaded()` 未稳定为 true,但 `isSourceLoaded('cm95')` 和 `areTilesLoaded()` 为 true,显示/交互测试正常。B 运行时验收仍需人工复核。 +- JS 语法、Python 编译、样式 JSON/资源契约均通过。 + +## 4. 保留问题 + +- 5 个失败低 Zoom Tile 原样保留,特别是全球 z0/0/0 缺图。 +- Style 编译采用 2418 样本,不应被解释为全量符号或完整 S-52 呈现通过。 +- 浏览器比对仅为不同版本视觉参考;差异需进一步定位数据、Style、运行时及缩放来源。 diff --git a/tools/cm95_s52_japan_compare_build_style.py b/tools/cm95_s52_japan_compare_build_style.py new file mode 100644 index 0000000..ea809a2 --- /dev/null +++ b/tools/cm95_s52_japan_compare_build_style.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""只读汇总全国实际 PBF 使用情况,复用原 S-52 compiler 编译轻量对比样式。""" +from __future__ import annotations +import importlib.util +import argparse +import math +import os +import json +from pathlib import Path +import sys +import time + +ROOT = Path(__file__).resolve().parents[1] +RELEASE = "cm95-d1-62a85bd40d3a-ae61b8866e80" +PBF_DIR = ROOT / "build" / "releases-s52-v2" / RELEASE / "pbf" / "delivery" +OUT = ROOT / "build" / "s52_style_compiler_japan_compare_v1" +SPEC = importlib.util.spec_from_file_location("cm95_s52_style_compiler", ROOT / "tools" / "cm95_s52_style_compiler.py") +assert SPEC and SPEC.loader +compiler = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(compiler) + +def main() -> int: + parser = argparse.ArgumentParser(description="全国 Style 按真实 PBF 的代表性样本编译") + parser.add_argument("--all", action="store_true", help="扫描全部 PBF(资源密集,默认采用分层样本)") + args = parser.parse_args() + # 用 scandir 逐级枚举,避免为 177383 个文件创建和排序 Path 对象。 + strides = {9: 4, 10: 20, 11: 100, 12: 400} + files = [] + by_zoom = {} + all_count = 0 + with os.scandir(PBF_DIR) as zoom_items: + zoom_dirs = sorted((d for d in zoom_items if d.is_dir() and d.name.isdigit()), key=lambda d: int(d.name)) + for zoom_dir in zoom_dirs: + z = int(zoom_dir.name) + with os.scandir(zoom_dir.path) as x_items: + x_dirs = sorted((d for d in x_items if d.is_dir() and d.name.isdigit()), key=lambda d: int(d.name)) + for x_dir in x_dirs: + with os.scandir(x_dir.path) as y_items: + entries = sorted((d for d in y_items if d.is_file() and d.name.endswith('.pbf')), key=lambda d: int(d.name[:-4])) + for entry in entries: + i = by_zoom.get(z, 0) + by_zoom[z] = i + 1 + all_count += 1 + if args.all or z <= 8 or i % strides.get(z, 1) == 0: + files.append(Path(entry.path)) + if not all_count: + raise SystemExit('全国 PBF 目录为空') + ports = [(140.7288,41.7688),(140.8719,38.2682),(139.77,35.58), + (136.75,34.73),(135.32,34.63),(132.44,34.30),(130.38,33.59), + (129.97,33.44),(129.87,32.74),(130.56,31.56),(127.68,26.21), + (124.16,24.34)] + seen = set(files) + for lon,lat in ports: + for z in (10,11,12): + n = 1 << z + x = int((lon+180)/360*n) + y = int((1-math.asinh(math.tan(math.radians(lat)))/math.pi)/2*n) + path = PBF_DIR / str(z) / str(x) / (str(y)+".pbf") + if path.is_file() and path not in seen: + files.append(path) + seen.add(path) + files.sort() + print("SAMPLE", len(files), "of", all_count, "PBF; all production tiles remain served", flush=True) + OUT.mkdir(parents=True, exist_ok=True) + total = 0 + agg = None + chunk = [] + begun = time.monotonic() + for path in files: + chunk.append(compiler._scan_pbf_usage(path)) + total += 1 + if len(chunk) == 200 or total == len(files): + one = compiler._merge_pbf_usages(chunk, "national chunk") + agg = one if agg is None else compiler._merge_pbf_usages([agg, one], "national aggregate") + chunk.clear() + if total % 10000 == 0 or total == len(files): + print(f"USAGE {total}/{len(files)} elapsed={int(time.monotonic()-begun)}s", flush=True) + assert agg is not None + agg["pbf_count"] = total + agg["pbf"] = str(PBF_DIR) + (OUT / "usage_scope.json").write_text(json.dumps({ + "release_id": RELEASE, "mode": "all" if args.all else "deterministic_stratified_preview", + "all_pbf_count": all_count, "scanned_pbf_count": total, "strides": strides, + "ports": ports, "note": "仅控制浏览器 Style 的显示规则集合,不改变或删减全国 PBF 数据" + }, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + usage_path = OUT / "national_pbf_usage.json" + usage_path.write_text(json.dumps(agg, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + # 不修改原编译器;将完整全国聚合 usage 交给原有编译入口。 + compiler._scan_pbf_directory = lambda path: agg + sys.argv = [ + str(ROOT / "tools" / "cm95_s52_style_compiler.py"), + "--config", str(ROOT / "config" / "s52" / "cm95_s52_style_compiler_japan_compare_v1.json"), + "--usage-pbf-dir", str(PBF_DIR), + "--output", str(OUT / "style.s52.json"), + "--audit-output", str(OUT / "style.s52.audit.json"), + ] + result = int(compiler.main()) + style = json.loads((OUT / "style.s52.json").read_text(encoding="utf-8")) + audit = json.loads((OUT / "style.s52.audit.json").read_text(encoding="utf-8")) + print("COMPLETE", json.dumps({"pbf_count": total, "layer_count": len(style["layers"]), + "style_bytes": (OUT / "style.s52.json").stat().st_size, + "hard_gate_failures": audit.get("hard_gate_failures"), + "resource_contract_status": audit.get("resource_contract_status")}, ensure_ascii=False), flush=True) + return result + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/web/japan_compare.html b/web/japan_compare.html new file mode 100644 index 0000000..1856a21 --- /dev/null +++ b/web/japan_compare.html @@ -0,0 +1,994 @@ + + + + + + 日本全国 CM95 S-52 PBF 对比 + + + + +
+ 日本全国 S-52 PBF 对比 + 左:X220 全国 S-52 Release 右:旧版 extentfix 参考(不同版本) + + + + + 测试版 · 样式分层抽样 · z0~z3 缺图 5 Tile + -- +
+ +
+
+
+ +
左:日本全国 S-52(样式分层抽样)等待参考 style…
+
点击任一侧海图,对比同一位置的 Feature。
+
LOCAL
+
+
+
+
右:NavSea extentfix 旧版参考加载远端 style…
+
点击任一侧海图,对比同一位置的 Feature。
+
REFERENCE
+
+
+ + + + + + diff --git a/web/japan_compare_server.py b/web/japan_compare_server.py new file mode 100644 index 0000000..2e7b67c --- /dev/null +++ b/web/japan_compare_server.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from urllib.parse import urlparse + +RELEASE_ID = "cm95-d1-62a85bd40d3a-ae61b8866e80" + + +def parse_args(): + parser = argparse.ArgumentParser(description="Japan nationwide S-52 PBF side-by-side comparison test server") + parser.add_argument("--host", default="0.0.0.0") + parser.add_argument("--port", type=int, default=18889) + parser.add_argument("--release-id", default=RELEASE_ID) + return parser.parse_args() + + +def main(): + args = parse_args() + web_root = Path(__file__).resolve().parent + 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" + summary_path = release_dir / "production_run_summary_v1.json" + html_path = web_root / "japan_compare.html" + runtime_executor_path = web_root / "s52_runtime_executor.js" + assets_root = project_root / "build" / "fukuoka_s52_web" / "site" / "assets" + # Comparison style is compiled from the same S-52 resources for nationwide coverage. + s52_style_path = ( + project_root / "build" / "s52_style_compiler_japan_compare_v1" / "style.s52.json" + ) + s52_sprite_root = project_root / "build" / "s52_resources_v2" / "final" + s52_symbol_metadata_path = ( + project_root / "build" / "s52_resources_v2" / "base" / "s52_symbol_metadata.json" + ) + + required = [ + pbf_root, + summary_path, + html_path, + runtime_executor_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", + s52_symbol_metadata_path, + ] + missing = [str(path) for path in required if not path.exists()] + if missing: + raise SystemExit("Missing required path(s):\n" + "\n".join(missing)) + + summary = json.loads(summary_path.read_text(encoding="utf-8")) + if summary.get("release_id") != args.release_id: + raise SystemExit("Production summary release_id mismatch") + class Handler(BaseHTTPRequestHandler): + def send_bytes( + self, data: bytes, content_type: str, status: int = 200, *, cache_control: str = "no-cache" + ): + self.send_response(status) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(data))) + self.send_header("Cache-Control", cache_control) + self.send_header("Access-Control-Allow-Origin", "*") + self.end_headers() + self.wfile.write(data) + + def do_GET(self): + path = urlparse(self.path).path + + if path in ("/", "/index.html", "/compare"): + self.send_bytes( + html_path.read_bytes(), "text/html; charset=utf-8", cache_control="no-store" + ) + return + + if path == "/s52_runtime_executor.js": + self.send_bytes( + runtime_executor_path.read_bytes(), + "application/javascript; charset=utf-8", + cache_control="no-store", + ) + return + + if path == "/health": + payload = { + "status": "ok", + "release_id": args.release_id, + "release_status": "TEST_READY_WITH_KNOWN_ISSUES", + "source": "production_run_summary_v1.json (not a finalized manifest)", + "jobs": summary.get("jobs"), + "states": summary.get("states"), + "pbf_file_count": summary.get("pbf_files"), + "output_features": summary.get("output_features"), + "known_failed_tiles": ["0/0/0", "1/1/0", "2/3/1", "3/7/2", "3/7/3"], + "s52_runtime_contract": "NOT_VALIDATED_FOR_NATIONAL_RELEASE", + } + self.send_bytes(json.dumps(payload, ensure_ascii=False).encode("utf-8"), "application/json; charset=utf-8") + return + + if path == "/release-manifest.json": + self.send_error(404, "Nationwide release has no finalized manifest") + return + + if path == "/release-summary.json": + self.send_bytes(summary_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", + cache_control="no-store", + ) + 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 == "/s52/symbol-metadata.json": + self.send_bytes( + s52_symbol_metadata_path.read_bytes(), + "application/json; charset=utf-8", + ) + return + + if path in ("/assets/maplibre-gl.js", "/assets/maplibre-gl.css"): + filename = path.rsplit("/", 1)[-1] + file_path = assets_root / filename + content_type = "application/javascript; charset=utf-8" if filename.endswith(".js") else "text/css; charset=utf-8" + self.send_bytes(file_path.read_bytes(), content_type) + return + + if path.startswith("/pbf/") and path.endswith(".pbf"): + parts = path[len("/pbf/"):].split("/") + if len(parts) != 3: + self.send_error(404) + return + z, x, yfile = parts + y = yfile[:-4] + if not (z.isdigit() and x.isdigit() and y.isdigit()): + self.send_error(404) + 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") + return + + self.send_error(404) + + def log_message(self, fmt, *args): + print(f"{self.address_string()} - {fmt % args}") + + server = ThreadingHTTPServer((args.host, args.port), Handler) + print(f"Japan nationwide compare: http://{args.host}:{args.port}/") + print(f"Release: {release_dir}") + print(f"PBF: {pbf_root}") + try: + server.serve_forever() + except KeyboardInterrupt: + pass + finally: + server.server_close() + + +if __name__ == "__main__": + main()