feat: consume manifest-driven native roads
This commit is contained in:
95
blender/osmassets/native_road_manifest.py
Normal file
95
blender/osmassets/native_road_manifest.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Pure-Python validation for native-road package manifests."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
CONTRACT = "native-road-package/v1.1"
|
||||
RENDERABLE_ROLES = ("surface", "marking")
|
||||
VALID_ROLES = RENDERABLE_ROLES + ("semantic",)
|
||||
|
||||
|
||||
def _error(message):
|
||||
raise RuntimeError("Invalid native road manifest: " + message)
|
||||
|
||||
|
||||
def _source_name(value):
|
||||
return (isinstance(value, str) and value
|
||||
and value == os.path.basename(value)
|
||||
and value not in (".", ".."))
|
||||
|
||||
|
||||
def _validate_split(source, split):
|
||||
if not isinstance(split, dict):
|
||||
_error("layer '%s' splitBy must be an object" % source)
|
||||
prop = split.get("prop")
|
||||
cases = split.get("cases")
|
||||
if not isinstance(prop, str) or not prop or not isinstance(cases, list) or not cases:
|
||||
_error("layer '%s' has an invalid splitBy" % source)
|
||||
|
||||
matches = set()
|
||||
defaults = 0
|
||||
for case in cases:
|
||||
if not isinstance(case, dict) or not isinstance(case.get("material"), str):
|
||||
_error("layer '%s' has an invalid splitBy case" % source)
|
||||
is_default = case.get("default") is True
|
||||
has_match = "match" in case
|
||||
if is_default == has_match:
|
||||
_error("layer '%s' splitBy cases need exactly one of match/default" % source)
|
||||
if is_default:
|
||||
defaults += 1
|
||||
else:
|
||||
match = case["match"]
|
||||
if match in matches:
|
||||
_error("layer '%s' has a duplicate splitBy match" % source)
|
||||
matches.add(match)
|
||||
if defaults != 1:
|
||||
_error("layer '%s' splitBy needs exactly one default case" % source)
|
||||
|
||||
|
||||
def load(native_road_dir):
|
||||
"""Load a v1.1 manifest and ensure it declares exactly its GeoJSON files."""
|
||||
manifest_path = os.path.join(native_road_dir, "manifest.json")
|
||||
if not os.path.isfile(manifest_path):
|
||||
raise RuntimeError("Native road manifest is missing: " + manifest_path)
|
||||
try:
|
||||
with open(manifest_path, "r", encoding="utf-8") as handle:
|
||||
manifest = json.load(handle)
|
||||
except (OSError, ValueError) as error:
|
||||
raise RuntimeError("Could not read native road manifest: " + str(error)) from error
|
||||
|
||||
if not isinstance(manifest, dict) or manifest.get("contract") != CONTRACT:
|
||||
_error("unsupported contract")
|
||||
layers = manifest.get("layers")
|
||||
if not isinstance(layers, list) or not layers:
|
||||
_error("layers must be a non-empty array")
|
||||
|
||||
declared = set()
|
||||
for spec in layers:
|
||||
if not isinstance(spec, dict):
|
||||
_error("layer entries must be objects")
|
||||
source = spec.get("source")
|
||||
role = spec.get("role")
|
||||
if not _source_name(source) or source in declared or role not in VALID_ROLES:
|
||||
_error("layer source or role is invalid")
|
||||
declared.add(source)
|
||||
if role in RENDERABLE_ROLES:
|
||||
if not isinstance(spec.get("materialLayer"), str) or not spec["materialLayer"]:
|
||||
_error("renderable layer '%s' needs materialLayer" % source)
|
||||
if "splitBy" in spec:
|
||||
_validate_split(source, spec["splitBy"])
|
||||
elif "materialLayer" in spec or "splitBy" in spec:
|
||||
_error("semantic layer '%s' must not declare rendering" % source)
|
||||
|
||||
layers_dir = os.path.join(native_road_dir, "layers")
|
||||
if not os.path.isdir(layers_dir):
|
||||
raise RuntimeError("Native road layers directory is missing: " + layers_dir)
|
||||
published = {
|
||||
name[:-8] for name in os.listdir(layers_dir)
|
||||
if name.endswith(".geojson") and os.path.isfile(os.path.join(layers_dir, name))
|
||||
}
|
||||
if published != declared:
|
||||
missing = sorted(declared - published)
|
||||
extra = sorted(published - declared)
|
||||
_error("declared GeoJSON mismatch (missing=%s, extra=%s)" % (missing, extra))
|
||||
return manifest
|
||||
Reference in New Issue
Block a user