feat: preserve ordered S-52 runtime rule chains
This commit is contained in:
@@ -1,2 +1,2 @@
|
|||||||
__version__ = "0.4.12-opencpn-cm93-coverage"
|
__version__ = "0.4.13-s52-ordered-rule-ir"
|
||||||
BUILDER_CONTRACT_VERSION = "cm95pbf-builder/0.4.12-opencpn-cm93-coverage"
|
BUILDER_CONTRACT_VERSION = "cm95pbf-builder/0.4.13-s52-ordered-rule-ir"
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import math
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .presentation import (
|
from .presentation import (
|
||||||
|
materialize_rule_ir,
|
||||||
materialize_symbol_hints,
|
materialize_symbol_hints,
|
||||||
|
materialize_text_hints,
|
||||||
parse_instruction_string,
|
parse_instruction_string,
|
||||||
static_rule_hints,
|
static_rule_hints,
|
||||||
)
|
)
|
||||||
@@ -26,7 +28,7 @@ class S52MarinerSettings:
|
|||||||
# profile into tile properties. The fixed S52MarinerSettings path remains
|
# profile into tile properties. The fixed S52MarinerSettings path remains
|
||||||
# available for OpenCPN/reference rendering.
|
# available for OpenCPN/reference rendering.
|
||||||
MARINER_DEPENDENT_CSPS = frozenset(
|
MARINER_DEPENDENT_CSPS = frozenset(
|
||||||
{"DEPARE01", "DEPARE02", "DEPCNT02", "SOUNDG02", "OBSTRN04", "WRECKS02"}
|
{"DEPARE01", "DEPARE02", "DEPCNT02", "SOUNDG02", "OBSTRN04", "WRECKS02", "RESARE02"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -597,6 +599,16 @@ def resolve_obstrn04_instruction(
|
|||||||
*,
|
*,
|
||||||
associated_depth_ranges: list[dict[str, Any]] | None = None,
|
associated_depth_ranges: list[dict[str, Any]] | None = None,
|
||||||
) -> tuple[str, dict[str, Any]]:
|
) -> tuple[str, dict[str, Any]]:
|
||||||
|
quality = resolve_quapnt01_instruction(attrs)
|
||||||
|
name_text = (
|
||||||
|
"TX(OBJNAM,1,2,3,'15118',-1,-1,CHBLK,26)"
|
||||||
|
if attrs.get("OBJNAM") not in {None, ""}
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
def finish(*parts: str) -> str:
|
||||||
|
return ";".join(part.strip(";") for part in parts if part)
|
||||||
|
|
||||||
valsou = _optional_float(attrs.get("VALSOU"))
|
valsou = _optional_float(attrs.get("VALSOU"))
|
||||||
depth_value = valsou
|
depth_value = valsou
|
||||||
if depth_value is None and str(geometry_type).upper() == "AREA":
|
if depth_value is None and str(geometry_type).upper() == "AREA":
|
||||||
@@ -625,7 +637,7 @@ def resolve_obstrn04_instruction(
|
|||||||
geom = str(geometry_type).upper()
|
geom = str(geometry_type).upper()
|
||||||
if geom == "POINT":
|
if geom == "POINT":
|
||||||
if udw:
|
if udw:
|
||||||
return udw, extra
|
return finish(udw, quality, name_text), extra
|
||||||
sounding = False
|
sounding = False
|
||||||
watlev = _int_attr(attrs, "WATLEV", -9)
|
watlev = _int_attr(attrs, "WATLEV", -9)
|
||||||
if valsou is not None:
|
if valsou is not None:
|
||||||
@@ -671,32 +683,45 @@ def resolve_obstrn04_instruction(
|
|||||||
snd, snd_extra = resolve_soundg03_instruction(attrs, valsou, settings)
|
snd, snd_extra = resolve_soundg03_instruction(attrs, valsou, settings)
|
||||||
instruction += ";" + snd if snd else ""
|
instruction += ";" + snd if snd else ""
|
||||||
extra.update(snd_extra)
|
extra.update(snd_extra)
|
||||||
return instruction, extra
|
return finish(instruction, quality, name_text), extra
|
||||||
|
|
||||||
if geom == "LINE":
|
if geom == "LINE":
|
||||||
quapos = _int_attr(attrs, "QUAPOS", 0)
|
quapos = _int_attr(attrs, "QUAPOS", 0)
|
||||||
if 2 <= quapos < 10:
|
if 2 <= quapos < 10:
|
||||||
return ("LC(LOWACC41)" if udw else "LC(LOWACC31)"), extra
|
return finish(
|
||||||
|
"LC(LOWACC41)" if udw else "LC(LOWACC31)", name_text
|
||||||
|
), extra
|
||||||
if udw:
|
if udw:
|
||||||
return "LS(DOTT,2,CHBLK)", extra
|
return finish("LS(DOTT,2,CHBLK)", name_text), extra
|
||||||
if valsou is None or valsou <= 20.0:
|
if valsou is None or valsou <= 20.0:
|
||||||
return "LS(DOTT,2,CHBLK)", extra
|
line = "LS(DOTT,2,CHBLK)"
|
||||||
return "LS(DASH,2,CHBLK)", extra
|
if valsou is not None:
|
||||||
|
snd, snd_extra = resolve_soundg03_instruction(attrs, valsou, settings)
|
||||||
|
extra.update(snd_extra)
|
||||||
|
line = finish(line, snd)
|
||||||
|
return finish(line, name_text), extra
|
||||||
|
return finish("LS(DASH,2,CHBLK)", name_text), extra
|
||||||
|
|
||||||
# AREA
|
# AREA
|
||||||
if udw:
|
if udw:
|
||||||
return "AC(DEPVS);AP(FOULAR01);LS(DOTT,2,CHBLK);" + udw, extra
|
return finish(
|
||||||
|
"AC(DEPVS);AP(FOULAR01);LS(DOTT,2,CHBLK)", udw, quality, name_text
|
||||||
|
), extra
|
||||||
watlev = _int_attr(attrs, "WATLEV", -9)
|
watlev = _int_attr(attrs, "WATLEV", -9)
|
||||||
if valsou is not None:
|
if valsou is not None:
|
||||||
line = "LS(DOTT,2,CHBLK)" if valsou <= 20.0 else "LS(DASH,2,CHBLK)"
|
line = "LS(DOTT,2,CHBLK)" if valsou <= 20.0 else "LS(DASH,2,CHBLK)"
|
||||||
return line, extra
|
snd, snd_extra = resolve_soundg03_instruction(attrs, valsou, settings)
|
||||||
|
extra.update(snd_extra)
|
||||||
|
return finish(line, snd, quality, name_text), extra
|
||||||
if watlev in {1, 2}:
|
if watlev in {1, 2}:
|
||||||
return "AC(CHBRN);LS(SOLD,2,CSTLN)", extra
|
return finish("AC(CHBRN);LS(SOLD,2,CSTLN)", quality, name_text), extra
|
||||||
if watlev == 4:
|
if watlev == 4:
|
||||||
return "AC(DEPIT);LS(DASH,2,CSTLN)", extra
|
return finish("AC(DEPIT);LS(DASH,2,CSTLN)", quality, name_text), extra
|
||||||
if _int_attr(attrs, "CATOBS", -9) == 6 and watlev in {3, 5}:
|
if _int_attr(attrs, "CATOBS", -9) == 6 and watlev in {3, 5}:
|
||||||
return "AC(DEPVS);AP(FOULAR01);LS(DOTT,2,CHBLK)", extra
|
return finish(
|
||||||
return "AC(DEPVS);LS(DOTT,2,CHBLK)", extra
|
"AC(DEPVS);AP(FOULAR01);LS(DOTT,2,CHBLK)", quality, name_text
|
||||||
|
), extra
|
||||||
|
return finish("AC(DEPVS);LS(DOTT,2,CHBLK)", quality, name_text), extra
|
||||||
|
|
||||||
|
|
||||||
def resolve_wrecks02_instruction(
|
def resolve_wrecks02_instruction(
|
||||||
@@ -737,9 +762,14 @@ def resolve_wrecks02_instruction(
|
|||||||
extra = dict(udw_extra)
|
extra = dict(udw_extra)
|
||||||
extra.update({"s52_hazard_procedure": "WRECKS02", "s52_hazard_depth_m": float(depth_value)})
|
extra.update({"s52_hazard_procedure": "WRECKS02", "s52_hazard_depth_m": float(depth_value)})
|
||||||
geom = str(geometry_type).upper()
|
geom = str(geometry_type).upper()
|
||||||
|
quality = resolve_quapnt01_instruction(attrs)
|
||||||
|
|
||||||
|
def finish(*parts: str) -> str:
|
||||||
|
return ";".join(part.strip(";") for part in parts if part)
|
||||||
|
|
||||||
if geom == "POINT":
|
if geom == "POINT":
|
||||||
if udw:
|
if udw:
|
||||||
return udw, extra
|
return finish(udw, quality), extra
|
||||||
if valsou is not None:
|
if valsou is not None:
|
||||||
symbol = "DANGER51" if valsou < settings.safety_contour_m else "DANGER52"
|
symbol = "DANGER51" if valsou < settings.safety_contour_m else "DANGER52"
|
||||||
instruction = f"SY({symbol});TX('Wk',3,1,2,'15110',2,0,CHBLK,21)"
|
instruction = f"SY({symbol});TX('Wk',3,1,2,'15110',2,0,CHBLK,21)"
|
||||||
@@ -749,14 +779,16 @@ def resolve_wrecks02_instruction(
|
|||||||
if snd:
|
if snd:
|
||||||
instruction += ";" + snd
|
instruction += ";" + snd
|
||||||
extra.update(snd_extra)
|
extra.update(snd_extra)
|
||||||
return instruction, extra
|
return finish(instruction, quality), extra
|
||||||
if catwrk == 1 and watlev == 3:
|
if catwrk == 1 and watlev == 3:
|
||||||
return "SY(WRECKS04)", extra
|
return finish("SY(WRECKS04)", quality), extra
|
||||||
if catwrk == 2 and watlev == 3:
|
if catwrk == 2 and watlev == 3:
|
||||||
return "SY(WRECKS05)", extra
|
return finish("SY(WRECKS05)", quality), extra
|
||||||
if catwrk in {4, 5} or watlev in {1, 2, 4, 5}:
|
if catwrk in {4, 5} or watlev in {1, 2, 4, 5}:
|
||||||
return "SY(WRECKS01)", extra
|
return finish("SY(WRECKS01)", quality), extra
|
||||||
return "SY(WRECKS05)", extra
|
if "CATWRK" not in attrs or "WATLEV" not in attrs:
|
||||||
|
return quality, extra
|
||||||
|
return finish("SY(WRECKS05)", quality), extra
|
||||||
|
|
||||||
quapos = _int_attr(attrs, "QUAPOS", 0)
|
quapos = _int_attr(attrs, "QUAPOS", 0)
|
||||||
if 2 <= quapos < 10:
|
if 2 <= quapos < 10:
|
||||||
@@ -774,6 +806,7 @@ def resolve_wrecks02_instruction(
|
|||||||
instruction = line
|
instruction = line
|
||||||
if valsou is not None and valsou <= 20.0:
|
if valsou is not None and valsou <= 20.0:
|
||||||
instruction += ";" + udw if udw else ""
|
instruction += ";" + udw if udw else ""
|
||||||
|
instruction = finish(instruction, quality)
|
||||||
snd, snd_extra = resolve_soundg03_instruction(attrs, valsou, settings)
|
snd, snd_extra = resolve_soundg03_instruction(attrs, valsou, settings)
|
||||||
if snd:
|
if snd:
|
||||||
instruction += ";" + snd
|
instruction += ";" + snd
|
||||||
@@ -783,6 +816,9 @@ def resolve_wrecks02_instruction(
|
|||||||
instruction += f";AC({color})"
|
instruction += f";AC({color})"
|
||||||
if udw:
|
if udw:
|
||||||
instruction += ";" + udw
|
instruction += ";" + udw
|
||||||
|
instruction = finish(instruction, quality)
|
||||||
|
else:
|
||||||
|
instruction = finish(instruction, udw, quality)
|
||||||
return instruction, extra
|
return instruction, extra
|
||||||
|
|
||||||
|
|
||||||
@@ -969,6 +1005,7 @@ def resolve_csp_hints(
|
|||||||
|
|
||||||
hints = static_rule_hints(instruction)
|
hints = static_rule_hints(instruction)
|
||||||
hints.update(materialize_symbol_hints(instruction, attrs))
|
hints.update(materialize_symbol_hints(instruction, attrs))
|
||||||
|
hints.update(materialize_text_hints(instruction, attrs))
|
||||||
if name == "LIGHTS05" and hints.get("s52_symbol_code"):
|
if name == "LIGHTS05" and hints.get("s52_symbol_code"):
|
||||||
extra["s52_light_symbol_code"] = hints["s52_symbol_code"]
|
extra["s52_light_symbol_code"] = hints["s52_symbol_code"]
|
||||||
hints.update(extra)
|
hints.update(extra)
|
||||||
@@ -976,4 +1013,7 @@ def resolve_csp_hints(
|
|||||||
hints["s52_csp_rule_opcodes"] = ",".join(
|
hints["s52_csp_rule_opcodes"] = ",".join(
|
||||||
rule.opcode for rule in parse_instruction_string(instruction)
|
rule.opcode for rule in parse_instruction_string(instruction)
|
||||||
)
|
)
|
||||||
|
hints["s52_rule_chain"] = materialize_rule_ir(
|
||||||
|
instruction, attrs, source="csp", procedure=name
|
||||||
|
)
|
||||||
return hints
|
return hints
|
||||||
|
|||||||
@@ -73,7 +73,18 @@ DELIVERY_PROPERTY_ALLOWLIST = frozenset(
|
|||||||
"s52_line_sequence",
|
"s52_line_sequence",
|
||||||
"s52_line_style",
|
"s52_line_style",
|
||||||
"s52_line_width",
|
"s52_line_width",
|
||||||
|
"s52_line1_style",
|
||||||
|
"s52_line1_width",
|
||||||
|
"s52_line1_color",
|
||||||
|
"s52_line2_style",
|
||||||
|
"s52_line2_width",
|
||||||
|
"s52_line2_color",
|
||||||
"s52_rule_opcodes",
|
"s52_rule_opcodes",
|
||||||
|
"s52_rule_chain",
|
||||||
|
"s52_rule_chain_version",
|
||||||
|
"s52_source_object_class",
|
||||||
|
"s52_source_geometry",
|
||||||
|
"s52_scamin_override",
|
||||||
"s52_light_arc_color",
|
"s52_light_arc_color",
|
||||||
"s52_light_arc_radius_mm",
|
"s52_light_arc_radius_mm",
|
||||||
"s52_light_mode",
|
"s52_light_mode",
|
||||||
|
|||||||
@@ -167,6 +167,32 @@ def materialize_symbol_hints(instruction: str, attrs: dict[str, Any]) -> dict[st
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def materialize_rule_ir(
|
||||||
|
instruction: str,
|
||||||
|
attrs: dict[str, Any],
|
||||||
|
*,
|
||||||
|
source: str,
|
||||||
|
procedure: str | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Return an ordered, executable representation of an S-52 rule string."""
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
for rule in parse_instruction_string(instruction):
|
||||||
|
item = rule.to_dict()
|
||||||
|
item.update({"source": source, "procedure": procedure})
|
||||||
|
if rule.opcode in {"TX", "TE"}:
|
||||||
|
text = materialize_text_hints(rule.raw, attrs)
|
||||||
|
if text.get("s52_text1_value") not in (None, ""):
|
||||||
|
item["text"] = {
|
||||||
|
key.removeprefix("s52_text1_"): value
|
||||||
|
for key, value in text.items()
|
||||||
|
if key.startswith("s52_text1_")
|
||||||
|
}
|
||||||
|
if rule.opcode == "OP":
|
||||||
|
item["side_effect"] = "upstream_parser_noop"
|
||||||
|
out.append(item)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
class PresentationParseError(ValueError):
|
class PresentationParseError(ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -359,6 +385,11 @@ def static_rule_hints(instruction: str) -> dict[str, Any]:
|
|||||||
line_rules = [
|
line_rules = [
|
||||||
rule for rule in rules if rule.opcode == "LS" and len(rule.args) >= 3
|
rule for rule in rules if rule.opcode == "LS" and len(rule.args) >= 3
|
||||||
]
|
]
|
||||||
|
for index, rule in enumerate(line_rules, 1):
|
||||||
|
prefix = f"s52_line{index}_"
|
||||||
|
out[prefix + "style"] = rule.args[0].strip().strip("'\"")
|
||||||
|
out[prefix + "width"] = rule.args[1].strip().strip("'\"")
|
||||||
|
out[prefix + "color"] = rule.args[2].strip().strip("'\"")
|
||||||
if len(line_rules) > 1:
|
if len(line_rules) > 1:
|
||||||
out["s52_line_sequence"] = "|".join(
|
out["s52_line_sequence"] = "|".join(
|
||||||
",".join(arg.strip().strip("'\"") for arg in rule.args[:3])
|
",".join(arg.strip().strip("'\"") for arg in rule.args[:3])
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ from __future__ import annotations
|
|||||||
from typing import Any, Iterable
|
from typing import Any, Iterable
|
||||||
|
|
||||||
from .presentation import (
|
from .presentation import (
|
||||||
|
materialize_rule_ir,
|
||||||
materialize_symbol_hints,
|
materialize_symbol_hints,
|
||||||
materialize_text_hints,
|
materialize_text_hints,
|
||||||
|
parse_instruction_string,
|
||||||
static_rule_hints,
|
static_rule_hints,
|
||||||
)
|
)
|
||||||
from .csp import (
|
from .csp import (
|
||||||
@@ -30,12 +32,8 @@ S52_DISPLAY_PRIORITY_BY_CODE = {
|
|||||||
|
|
||||||
|
|
||||||
def _apply_presentation_side_effects(out: dict[str, Any]) -> None:
|
def _apply_presentation_side_effects(out: dict[str, Any]) -> None:
|
||||||
override = str(out.get("s52_priority_override") or "")
|
|
||||||
if override and override[0] in S52_DISPLAY_PRIORITY_BY_CODE:
|
|
||||||
out["s52_display_priority"] = S52_DISPLAY_PRIORITY_BY_CODE[override[0]]
|
|
||||||
if int(out.get("s52_display_base_promoted") or 0) == 1:
|
if int(out.get("s52_display_base_promoted") or 0) == 1:
|
||||||
out["s52_display_category"] = "Displaybase"
|
out["s52_display_category"] = "Displaybase"
|
||||||
out["s52_display_priority"] = "Hazards"
|
|
||||||
|
|
||||||
|
|
||||||
OPENCPN_CM93_CLASS_SUBSTITUTIONS: dict[str, str] = {
|
OPENCPN_CM93_CLASS_SUBSTITUTIONS: dict[str, str] = {
|
||||||
@@ -485,6 +483,7 @@ class S52LookupRegistry:
|
|||||||
if token.strip()
|
if token.strip()
|
||||||
]
|
]
|
||||||
unresolved: list[str] = []
|
unresolved: list[str] = []
|
||||||
|
resolved_by_procedure: dict[str, dict[str, Any]] = {}
|
||||||
for procedure in procedures:
|
for procedure in procedures:
|
||||||
if self.mariner_mode == "runtime" and is_mariner_dependent_csp(procedure):
|
if self.mariner_mode == "runtime" and is_mariner_dependent_csp(procedure):
|
||||||
out.update(
|
out.update(
|
||||||
@@ -511,8 +510,32 @@ class S52LookupRegistry:
|
|||||||
)
|
)
|
||||||
if resolved:
|
if resolved:
|
||||||
out.update(resolved)
|
out.update(resolved)
|
||||||
|
resolved_by_procedure[procedure] = resolved
|
||||||
else:
|
else:
|
||||||
unresolved.append(procedure)
|
unresolved.append(procedure)
|
||||||
|
ordered_chain: list[dict[str, Any]] = []
|
||||||
|
for rule in parse_instruction_string(instruction):
|
||||||
|
if rule.opcode != "CS" or not rule.args:
|
||||||
|
ordered_chain.extend(
|
||||||
|
materialize_rule_ir(rule.raw, normalized_attrs, source="lookup")
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
procedure = rule.args[0].strip().strip("'\"")[:8]
|
||||||
|
if procedure in unresolved:
|
||||||
|
dynamic = materialize_rule_ir(
|
||||||
|
rule.raw, normalized_attrs, source="lookup"
|
||||||
|
)[0]
|
||||||
|
dynamic["dynamic"] = True
|
||||||
|
dynamic["procedure"] = procedure
|
||||||
|
ordered_chain.append(dynamic)
|
||||||
|
else:
|
||||||
|
ordered_chain.extend(
|
||||||
|
list(resolved_by_procedure.get(procedure, {}).get("s52_rule_chain") or [])
|
||||||
|
)
|
||||||
|
out["s52_rule_chain_version"] = 1
|
||||||
|
out["s52_rule_chain"] = ordered_chain
|
||||||
|
out["s52_source_object_class"] = lookup_class
|
||||||
|
out["s52_source_geometry"] = str(geometry_type).upper()
|
||||||
if unresolved:
|
if unresolved:
|
||||||
out["s52_csp"] = ",".join(unresolved)
|
out["s52_csp"] = ",".join(unresolved)
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user