200 lines
8.1 KiB
Python
200 lines
8.1 KiB
Python
#!/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()
|