docs(pbf): archive Full production provenance and engineering history
This commit is contained in:
164
navsea_build_unified_semantic_assets.py
Normal file
164
navsea_build_unified_semantic_assets.py
Normal file
@@ -0,0 +1,164 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
MAP_JSON = ROOT / "src/pbf/semantic_sprite_key_map_2026-08-08.json"
|
||||
SOURCE_SPRITE_JSON = Path("/mnt/sda1/www/newpec/sprite/sprite@2x.json")
|
||||
SOURCE_SPRITE_PNG = Path("/mnt/sda1/www/newpec/sprite/sprite@2x.png")
|
||||
OUTPUT_SPRITE_DIR = Path("/mnt/sda1/www/newpec/sprite-semantic-unified-20260808")
|
||||
SOURCE_STYLE = ROOT / "src/pbf/style.navsea-delivery-full-semantic-extentfix.json"
|
||||
OUTPUT_STYLE = ROOT / "src/pbf/style.navsea-delivery-full-semantic-unified-20260808.json"
|
||||
DEPLOYED_STYLE = Path("/mnt/sda1/www/newpec/domain/style.navsea-delivery-full-semantic-unified-20260808.json")
|
||||
SPRITE_URL = "http://192.168.200.184/newpec/sprite-semantic-unified-20260808/sprite"
|
||||
PBF_URL = "http://192.168.200.184/pbf-delivery-full-semantic-unified-20260808/{z}/{x}/{y}.pbf"
|
||||
|
||||
|
||||
def pack_sprite(mapping: dict[str, str], source_meta: dict[str, dict], scale: float, max_width: int):
|
||||
source = Image.open(SOURCE_SPRITE_PNG).convert("RGBA")
|
||||
positions: dict[str, dict] = {}
|
||||
packed: list[tuple[str, tuple[int, int, int, int]]] = []
|
||||
cursor_x = cursor_y = row_height = 0
|
||||
for old_key, new_key in mapping.items():
|
||||
meta = source_meta[old_key]
|
||||
width = max(1, round(meta["width"] * scale))
|
||||
height = max(1, round(meta["height"] * scale))
|
||||
if cursor_x and cursor_x + width > max_width:
|
||||
cursor_x = 0
|
||||
cursor_y += row_height
|
||||
row_height = 0
|
||||
packed.append((old_key, (cursor_x, cursor_y, width, height)))
|
||||
positions[new_key] = {
|
||||
"x": cursor_x,
|
||||
"y": cursor_y,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"pixelRatio": 1 if scale == 0.5 else 2,
|
||||
}
|
||||
cursor_x += width
|
||||
row_height = max(row_height, height)
|
||||
|
||||
image = Image.new("RGBA", (max_width, cursor_y + row_height), (0, 0, 0, 0))
|
||||
for old_key, (x, y, width, height) in packed:
|
||||
meta = source_meta[old_key]
|
||||
crop = source.crop((meta["x"], meta["y"], meta["x"] + meta["width"], meta["y"] + meta["height"]))
|
||||
if scale != 1.0:
|
||||
crop = crop.resize((width, height), Image.Resampling.LANCZOS)
|
||||
image.paste(crop, (x, y))
|
||||
return image, positions
|
||||
|
||||
|
||||
def replace_literals(value, mapping: dict[str, str]):
|
||||
if isinstance(value, dict):
|
||||
return {key: replace_literals(item, mapping) for key, item in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [replace_literals(item, mapping) for item in value]
|
||||
if isinstance(value, str):
|
||||
return mapping.get(value, value)
|
||||
return value
|
||||
|
||||
|
||||
def build_style(mapping: dict[str, str]) -> dict:
|
||||
style = json.loads(SOURCE_STYLE.read_text(encoding="utf-8"))
|
||||
style = replace_literals(style, mapping)
|
||||
|
||||
# The source style contains a second, older semantic vocabulary such as
|
||||
# ``buoy_can`` and ``coastal_lighthouse``. Those names are not guaranteed
|
||||
# to exist in the actual PNG atlas. The delivery PBF already carries the
|
||||
# resolved, auditable image key in chart_icon_image/chart_fill_pattern, so
|
||||
# use those properties as the single style/sprite contract. This also
|
||||
# makes an unavailable source image render as empty instead of silently
|
||||
# pointing at a non-existent sprite key.
|
||||
direct_icon = ["coalesce", ["get", "chart_icon_image"], ""]
|
||||
direct_fill_pattern = ["coalesce", ["get", "chart_fill_pattern"], ""]
|
||||
direct_line_pattern = ["coalesce", ["get", "chart_line_pattern"], ""]
|
||||
for layer in style.get("layers", []):
|
||||
layer_id = layer.get("id")
|
||||
if layer_id in {
|
||||
"hazard-points",
|
||||
"anchorage-symbols",
|
||||
"anchor-hazard-points",
|
||||
"anchor-hazard-points-428",
|
||||
"nav-marks-harbor-lighthouses",
|
||||
"nav-marks-breakwater-lighthouses",
|
||||
"nav-marks-small-lights",
|
||||
"nav-marks-light-beacons",
|
||||
"nav-marks",
|
||||
"facility-points",
|
||||
"pilot-station-points",
|
||||
"landmark-points",
|
||||
}:
|
||||
layer.setdefault("layout", {})["icon-image"] = direct_icon
|
||||
if layer.get("id") == "nav-light-arc":
|
||||
layer.setdefault("layout", {})["icon-image"] = [
|
||||
"case",
|
||||
["has", "arc_id"],
|
||||
["get", "arc_id"],
|
||||
[
|
||||
"match",
|
||||
["get", "light_color_code"],
|
||||
"green",
|
||||
mapping["arc-daytime-m1"],
|
||||
"red",
|
||||
mapping["arc-daytime-m2"],
|
||||
mapping["arc-daytime-m3"],
|
||||
],
|
||||
]
|
||||
if layer_id == "nav-light-flare":
|
||||
layer.setdefault("layout", {})["icon-image"] = [
|
||||
"match",
|
||||
["get", "light_color_code"],
|
||||
"green",
|
||||
mapping["light-daytime-1"],
|
||||
"red",
|
||||
mapping["light-daytime-2"],
|
||||
"yellow",
|
||||
mapping["light-daytime-3"],
|
||||
"white",
|
||||
mapping["light-daytime-3"],
|
||||
"amber",
|
||||
mapping["light-daytime-3"],
|
||||
"orange",
|
||||
mapping["light-daytime-3"],
|
||||
mapping["light-daytime-3"],
|
||||
]
|
||||
if layer_id == "fishery-areas":
|
||||
layer.setdefault("paint", {})["fill-pattern"] = direct_fill_pattern
|
||||
if layer_id == "special-pattern-line-754":
|
||||
layer.setdefault("paint", {})["line-pattern"] = direct_line_pattern
|
||||
style["name"] = "NavSea Delivery Full Semantic Unified 20260808"
|
||||
style["sprite"] = SPRITE_URL
|
||||
source = style.get("sources", {}).get("navsea_delivery")
|
||||
if source:
|
||||
source["tiles"] = [PBF_URL]
|
||||
return style
|
||||
|
||||
|
||||
def main() -> None:
|
||||
mapping = json.loads(MAP_JSON.read_text(encoding="utf-8"))
|
||||
source_meta = json.loads(SOURCE_SPRITE_JSON.read_text(encoding="utf-8"))
|
||||
if OUTPUT_SPRITE_DIR.exists():
|
||||
shutil.rmtree(OUTPUT_SPRITE_DIR)
|
||||
OUTPUT_SPRITE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
for scale, json_name, png_name, width in (
|
||||
(1.0, "sprite@2x.json", "sprite@2x.png", 2048),
|
||||
(0.5, "sprite.json", "sprite.png", 1024),
|
||||
):
|
||||
image, metadata = pack_sprite(mapping, source_meta, scale, width)
|
||||
image.save(OUTPUT_SPRITE_DIR / png_name)
|
||||
(OUTPUT_SPRITE_DIR / json_name).write_text(json.dumps(metadata, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
style = build_style(mapping)
|
||||
OUTPUT_STYLE.write_text(json.dumps(style, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
DEPLOYED_STYLE.parent.mkdir(parents=True, exist_ok=True)
|
||||
DEPLOYED_STYLE.write_text(json.dumps(style, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"sprite_dir": str(OUTPUT_SPRITE_DIR), "sprite_keys": len(mapping), "style": str(OUTPUT_STYLE), "deployed_style": str(DEPLOYED_STYLE)}, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user