165 lines
6.9 KiB
Python
165 lines
6.9 KiB
Python
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()
|