feat: compile S-52 presentation into MapLibre style

This commit is contained in:
OpenAI Codex
2026-09-17 21:14:41 +08:00
parent 6de980d47f
commit eb9203e062
3 changed files with 1117 additions and 2 deletions

View File

@@ -0,0 +1,59 @@
{
"schema": "navsea.cm95.s52-style-compiler-config/v1",
"name": "CM95 S-52 Fukuoka compiled style v1",
"palette": "DAY_BRIGHT",
"sprite": "http://192.168.200.184:8877/s52-sprite-lc-ap-v3",
"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:8877/tiles-presentation-v10/{z}/{x}/{y}.pbf"],
"bounds": [130.341796875, 33.57801474614399, 130.4296875, 33.65120829920498],
"minzoom": 12,
"maxzoom": 12
},
"resources": {
"colors": "build/s52_poc_patterns_v1/s52_colors.json",
"symbol_metadata": "build/s52_poc_patterns_v1/s52_symbol_metadata.json",
"bitmap_pattern_metadata": "build/s52_poc_patterns_v1/s52_pattern_metadata.json",
"vector_pattern_metadata": "build/s52_ap_v3/s52-sprite-lc-ap-pattern-metadata.json",
"line_metadata": "build/s52_lc_v1/s52-sprite-lc-line-metadata.json",
"sprite_index": "build/s52_ap_v3/s52-sprite-lc-ap.json",
"lookup_ir": "build/s52_presentation_ir/lookup_rule_ir.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": "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": "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"]}
]
}

1010
tools/cm95_s52_style_compiler.py Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -94,7 +94,20 @@ def _svg_for_pattern(node: ET.Element, palette: dict[str, str], dpi: float) -> t
if pts: if pts:
current = pts[-1] current = pts[-1]
elif cmd == "PD": elif cmd == "PD":
for target in _points(arg): targets = _points(arg)
if not targets and current is not None:
# OpenCPN RenderFromHPGL draws an argument-less PD as a
# one-screen-pixel horizontal segment, then advances the
# current point to that segment end. DRGARE01 relies on it.
x1, y1 = cv(current)
color = pen_colors.get(pen, next(iter(pen_colors.values())))
stroke = max(1.0, pen_width * nominal)
elements.append(
f'<line x1="{x1:.3f}" y1="{y1:.3f}" x2="{x1 + 1.0:.3f}" y2="{y1:.3f}" '
f'stroke="{color}" stroke-width="{stroke:.3f}" stroke-linecap="round" />'
)
current = (current[0] + (1.0 / unit), current[1])
for target in targets:
if current is None: if current is None:
current = target current = target
continue continue
@@ -143,6 +156,11 @@ def main() -> int:
ap.add_argument("--base-sprite-png", required=True, type=Path) ap.add_argument("--base-sprite-png", required=True, type=Path)
ap.add_argument("--base-sprite-json", required=True, type=Path) ap.add_argument("--base-sprite-json", required=True, type=Path)
ap.add_argument("--usage-audit", required=True, type=Path) ap.add_argument("--usage-audit", required=True, type=Path)
ap.add_argument(
"--usage-pbf",
type=Path,
help="Optionally union materialized s52_area_pattern_code values from a PBF tile.",
)
ap.add_argument("--output-prefix", required=True, type=Path) ap.add_argument("--output-prefix", required=True, type=Path)
ap.add_argument("--palette", default="DAY_BRIGHT") ap.add_argument("--palette", default="DAY_BRIGHT")
ap.add_argument("--dpi", default=96.0, type=float) ap.add_argument("--dpi", default=96.0, type=float)
@@ -157,7 +175,22 @@ def main() -> int:
raise SystemExit("missing patterns") raise SystemExit("missing patterns")
by_name = {_text(n, "name"): n for n in patterns.findall("pattern")} by_name = {_text(n, "name"): n for n in patterns.findall("pattern")}
usage = json.loads(args.usage_audit.read_text(encoding="utf-8")) usage = json.loads(args.usage_audit.read_text(encoding="utf-8"))
used = [str(row[0]) for row in usage.get("pattern_resource_counts", [])] lookup_used = {str(row[0]) for row in usage.get("pattern_resource_counts", [])}
pbf_used: set[str] = set()
if args.usage_pbf is not None:
try:
import mapbox_vector_tile
except ImportError as exc:
raise SystemExit(
"--usage-pbf requires mapbox_vector_tile; run with the project .venv Python"
) from exc
decoded = mapbox_vector_tile.decode(args.usage_pbf.read_bytes())
for layer in decoded.values():
for feature in layer.get("features") or []:
value = (feature.get("properties") or {}).get("s52_area_pattern_code")
if value not in (None, ""):
pbf_used.add(str(value))
used = sorted(lookup_used | pbf_used)
names = [ names = [
name for name in used name for name in used
if name in by_name if name in by_name
@@ -183,6 +216,15 @@ def main() -> int:
pp = tmp / f"{idx:03d}-{name}.png" pp = tmp / f"{idx:03d}-{name}.png"
sp.write_text(svg, encoding="utf-8") sp.write_text(svg, encoding="utf-8")
subprocess.run(["convert", "-background", "none", str(sp), str(pp)], check=True) subprocess.run(["convert", "-background", "none", str(sp), str(pp)], check=True)
alpha_probe = subprocess.run(
[
"convert", str(pp), "-alpha", "extract",
"-format", "%[fx:maxima]", "info:",
],
check=True, capture_output=True, text=True,
).stdout.strip()
if not alpha_probe or float(alpha_probe) <= 0.0:
raise RuntimeError(f"pattern {name} rendered fully transparent")
w, h = _png_size(pp) w, h = _png_size(pp)
key = f"ap-{name}" key = f"ap-{name}"
sprite[key] = {"x": 0, "y": y, "width": w, "height": h, "pixelRatio": 1} sprite[key] = {"x": 0, "y": y, "width": w, "height": h, "pixelRatio": 1}
@@ -206,6 +248,10 @@ def main() -> int:
print(json.dumps({ print(json.dumps({
"schema": "navsea.cm95.s52-vector-pattern-sprite/v1", "schema": "navsea.cm95.s52-vector-pattern-sprite/v1",
"palette": args.palette, "palette": args.palette,
"lookup_usage_pattern_count": len(lookup_used),
"pbf_usage_pattern_count": len(pbf_used),
"pbf_usage_patterns": sorted(pbf_used),
"effective_usage_pattern_count": len(used),
"vector_pattern_count": len(names), "vector_pattern_count": len(names),
"vector_patterns": names, "vector_patterns": names,
"base_sprite": {"width": base_w, "height": base_h}, "base_sprite": {"width": base_w, "height": base_h},