feat: compile S-52 presentation into MapLibre style
This commit is contained in:
59
config/s52/cm95_s52_style_compiler_v1.json
Normal file
59
config/s52/cm95_s52_style_compiler_v1.json
Normal 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
1010
tools/cm95_s52_style_compiler.py
Executable file
File diff suppressed because it is too large
Load Diff
@@ -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},
|
||||||
|
|||||||
Reference in New Issue
Block a user