feat: implement S-52 hazard presentation
This commit is contained in:
65
docs/报告/CM95_S52_危险物阶段报告_v1.md
Normal file
65
docs/报告/CM95_S52_危险物阶段报告_v1.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# CM95 S-52 危险物阶段报告 v1
|
||||
|
||||
## 范围
|
||||
|
||||
本阶段沿 OpenCPN S-52 流程实现危险物条件符号化:
|
||||
|
||||
- `OBSTRN04`
|
||||
- `UDWHAZ03`
|
||||
- `WRECKS02`
|
||||
- Point / Line / Area 三种几何分支
|
||||
- DEPARE / DRGARE 空间关联上下文
|
||||
|
||||
基线仍为 Linux 项目 `/root/sourceserver/cm95`。
|
||||
正式 production release 未修改。
|
||||
|
||||
## 实现原则
|
||||
|
||||
危险物不能只按对象自身属性选图标。
|
||||
Builder 先收集当前 scale 的 `DEPARE / DRGARE` 几何与 `DRVAL1/DRVAL2`,
|
||||
只对 `OBSTRN / UWTROC / WRECKS` 做空间 `intersects`,再把关联水深上下文交给 CSP。
|
||||
|
||||
`UDWHAZ03` 根据 safety contour、对象推导深度、`EXPSOU`、`WATLEV` 和关联水深区判断 isolated danger。
|
||||
## 福冈湾 z12 实测
|
||||
|
||||
Tile:`12/3531/1641`
|
||||
|
||||
- 危险物相关 Feature:40
|
||||
- `WRECKS`:21
|
||||
- `UWTROC`:16
|
||||
- `OBSTRN`:3
|
||||
- `OBSTRN04` 已解析:19 / 19
|
||||
- 未解析 `OBSTRN04`:0
|
||||
- 本 tile isolated danger:0
|
||||
- `HAZARD_TILE_CONTRACT=PASS`
|
||||
|
||||
本 tile 实际产生的主要符号包括:
|
||||
|
||||
- `FOULGND1`
|
||||
- `DANGER51`
|
||||
- `DANGER52`
|
||||
- `UWTROC03`
|
||||
- `UWTROC04`
|
||||
- `LNDARE01`
|
||||
|
||||
危险物自带的 sounding 继续复用 `SOUNDG03 / SNDFRM02` 符号序列。
|
||||
## 浏览器验收
|
||||
|
||||
测试页:`presentation_v7.html`
|
||||
|
||||
Chrome DevTools Protocol 实测状态:
|
||||
|
||||
- 页面标题进入 `READY CM95 福冈湾 S52`
|
||||
- 危险物 S-52:44
|
||||
- `OBSTRN04 已解析`:19
|
||||
- `isolated danger`:0
|
||||
- LIGHTS05、SOUNDG、深度色、等深线仍保持正常
|
||||
|
||||
折叠 HUD 后截图确认危险物 S-52 symbol 已实际绘制,不是仅写入 PBF 属性。
|
||||
|
||||
## 已知资源闭合缺口
|
||||
|
||||
OpenCPN 当前 `s52cnsy.cpp` 的 `OBSTRN04` 会引用 `DANGER53`,
|
||||
但当前 `chartsymbols.xml` 中没有同名 Symbol 定义。
|
||||
福冈湾本 tile 未触发该分支,因此不阻塞本阶段。
|
||||
CM95 对该缺口保持 fail-closed,不自行创造替代图标。
|
||||
@@ -1708,10 +1708,18 @@ class Cm95TileBuilder:
|
||||
floating_relations: set[Any] = set()
|
||||
floating_points: set[Any] = set()
|
||||
contour_values: list[float] = []
|
||||
depth_areas: list[dict[str, Any]] = []
|
||||
safety_contour = (
|
||||
self.s52.settings.safety_contour_m if self.s52 is not None else None
|
||||
)
|
||||
for row in rows:
|
||||
if row.object_class in {"DEPARE", "DRGARE"} and row.geometry_type == "AREA":
|
||||
depth_areas.append({
|
||||
"geom": row.geom_wgs84,
|
||||
"geometry_type": row.geometry_type,
|
||||
"drval1": row.attrs.get("DRVAL1"),
|
||||
"drval2": row.attrs.get("DRVAL2"),
|
||||
})
|
||||
if row.object_class == "DEPCNT" and safety_contour is not None:
|
||||
try:
|
||||
value = float(row.attrs.get("VALDCO"))
|
||||
@@ -1733,6 +1741,7 @@ class Cm95TileBuilder:
|
||||
"floating_relations": floating_relations,
|
||||
"floating_points": floating_points,
|
||||
"next_safe_contour_m": min(contour_values) if contour_values else None,
|
||||
"depth_areas": depth_areas,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -1745,6 +1754,25 @@ class Cm95TileBuilder:
|
||||
return (float(geom.x), float(geom.y)) in context["floating_points"]
|
||||
return False
|
||||
|
||||
def _associated_depth_ranges(self, row: SourceFeature) -> list[dict[str, Any]]:
|
||||
if row.object_class not in {"OBSTRN", "UWTROC", "WRECKS"}:
|
||||
return []
|
||||
result: list[dict[str, Any]] = []
|
||||
for item in self._s52_context.get("depth_areas", []):
|
||||
geom = item.get("geom")
|
||||
try:
|
||||
intersects = geom is not None and geom.intersects(row.geom_wgs84)
|
||||
except Exception:
|
||||
intersects = False
|
||||
if not intersects:
|
||||
continue
|
||||
result.append({
|
||||
"geometry_type": item.get("geometry_type"),
|
||||
"drval1": item.get("drval1"),
|
||||
"drval2": item.get("drval2"),
|
||||
})
|
||||
return result
|
||||
|
||||
def _s52_properties(self, row: SourceFeature) -> dict[str, Any]:
|
||||
if self.s52 is None:
|
||||
return {}
|
||||
@@ -1753,6 +1781,7 @@ class Cm95TileBuilder:
|
||||
row.geometry_type,
|
||||
row.attrs,
|
||||
next_safe_contour_m=self._s52_context.get("next_safe_contour_m"),
|
||||
associated_depth_ranges=self._associated_depth_ranges(row),
|
||||
)
|
||||
|
||||
def _append_s52_fallback_output(
|
||||
|
||||
@@ -372,14 +372,276 @@ def resolve_soundg03_instruction(
|
||||
}
|
||||
return instruction, extra
|
||||
|
||||
|
||||
def _int_attr(attrs: dict[str, Any], key: str, default: int = -9) -> int:
|
||||
try:
|
||||
return int(attrs.get(key))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def _hazard_depth_context(
|
||||
associated_depth_ranges: list[dict[str, Any]] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
return list(associated_depth_ranges or [])
|
||||
|
||||
|
||||
def resolve_udwhaz03_instruction(
|
||||
attrs: dict[str, Any],
|
||||
depth_value: float | None,
|
||||
settings: S52MarinerSettings,
|
||||
*,
|
||||
associated_depth_ranges: list[dict[str, Any]] | None = None,
|
||||
) -> tuple[str, dict[str, Any]]:
|
||||
danger = False
|
||||
expsou = _int_attr(attrs, "EXPSOU", 0)
|
||||
if depth_value is None and expsou != 1:
|
||||
danger = True
|
||||
if not danger and (
|
||||
expsou == 1
|
||||
or (depth_value is not None and depth_value <= settings.safety_contour_m)
|
||||
):
|
||||
for item in _hazard_depth_context(associated_depth_ranges):
|
||||
primitive = str(item.get("geometry_type") or "AREA").upper()
|
||||
try:
|
||||
drval1 = float(item.get("drval1") or 0.0)
|
||||
except (TypeError, ValueError):
|
||||
drval1 = 0.0
|
||||
try:
|
||||
drval2 = float(item.get("drval2") or 0.0)
|
||||
except (TypeError, ValueError):
|
||||
drval2 = 0.0
|
||||
if primitive == "LINE":
|
||||
if drval2 < settings.safety_contour_m:
|
||||
danger = True
|
||||
break
|
||||
elif drval1 >= settings.safety_contour_m and expsou != 1:
|
||||
danger = True
|
||||
break
|
||||
|
||||
extra = {
|
||||
"s52_isolated_danger": int(danger),
|
||||
"s52_display_base_promoted": int(danger),
|
||||
}
|
||||
if depth_value is not None:
|
||||
extra["s52_hazard_depth_m"] = float(depth_value)
|
||||
if not danger:
|
||||
return "", extra
|
||||
watlev = _int_attr(attrs, "WATLEV", 0)
|
||||
if watlev in {1, 2}:
|
||||
return "", extra
|
||||
return "SY(ISODGR51)", extra
|
||||
|
||||
|
||||
def _area_least_depth(
|
||||
associated_depth_ranges: list[dict[str, Any]] | None,
|
||||
) -> float | None:
|
||||
values: list[float] = []
|
||||
for item in _hazard_depth_context(associated_depth_ranges):
|
||||
try:
|
||||
values.append(float(item.get("drval1")))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return min(values) if values else None
|
||||
|
||||
|
||||
def resolve_obstrn04_instruction(
|
||||
object_class: str,
|
||||
geometry_type: str,
|
||||
attrs: dict[str, Any],
|
||||
settings: S52MarinerSettings,
|
||||
*,
|
||||
associated_depth_ranges: list[dict[str, Any]] | None = None,
|
||||
) -> tuple[str, dict[str, Any]]:
|
||||
valsou = _optional_float(attrs.get("VALSOU"))
|
||||
depth_value = valsou
|
||||
if depth_value is None and str(geometry_type).upper() == "AREA":
|
||||
depth_value = _area_least_depth(associated_depth_ranges)
|
||||
if depth_value is None:
|
||||
catobs = _int_attr(attrs, "CATOBS", 0)
|
||||
watlev = _int_attr(attrs, "WATLEV", 0)
|
||||
expsou = _int_attr(attrs, "EXPSOU", 0)
|
||||
if expsou != 1:
|
||||
if catobs == 6:
|
||||
depth_value = 0.01
|
||||
elif watlev == 5:
|
||||
depth_value = 0.0
|
||||
elif watlev == 3:
|
||||
depth_value = 0.01
|
||||
else:
|
||||
depth_value = -15.0
|
||||
|
||||
udw, extra = resolve_udwhaz03_instruction(
|
||||
attrs,
|
||||
depth_value,
|
||||
settings,
|
||||
associated_depth_ranges=associated_depth_ranges,
|
||||
)
|
||||
extra["s52_hazard_procedure"] = "OBSTRN04"
|
||||
geom = str(geometry_type).upper()
|
||||
if geom == "POINT":
|
||||
if udw:
|
||||
return udw, extra
|
||||
sounding = False
|
||||
watlev = _int_attr(attrs, "WATLEV", -9)
|
||||
if valsou is not None:
|
||||
if valsou <= 20.0:
|
||||
if object_class == "UWTROC":
|
||||
if watlev in {4, 5}:
|
||||
instruction = "SY(UWTROC04)"
|
||||
else:
|
||||
instruction = "SY(DANGER51)"
|
||||
sounding = True
|
||||
else:
|
||||
if watlev in {1, 2}:
|
||||
instruction = "SY(LNDARE01)"
|
||||
elif watlev == 3:
|
||||
instruction = "SY(DANGER52)"
|
||||
sounding = True
|
||||
elif watlev in {4, 5}:
|
||||
instruction = "SY(DANGER53)"
|
||||
sounding = True
|
||||
else:
|
||||
instruction = "SY(DANGER51)"
|
||||
sounding = True
|
||||
else:
|
||||
instruction = "SY(DANGER52)"
|
||||
sounding = True
|
||||
else:
|
||||
if object_class == "UWTROC":
|
||||
instruction = (
|
||||
"SY(LNDARE01)" if watlev == 2 else
|
||||
"SY(UWTROC03)" if watlev == 3 else
|
||||
"SY(UWTROC04)"
|
||||
)
|
||||
else:
|
||||
instruction = (
|
||||
"SY(OBSTRN11)" if watlev in {1, 2} else
|
||||
"SY(OBSTRN03)" if watlev in {4, 5} else
|
||||
"SY(OBSTRN01)"
|
||||
)
|
||||
if sounding and valsou is not None:
|
||||
snd, snd_extra = resolve_soundg03_instruction(attrs, valsou, settings)
|
||||
instruction += ";" + snd if snd else ""
|
||||
extra.update(snd_extra)
|
||||
return instruction, extra
|
||||
|
||||
if geom == "LINE":
|
||||
quapos = _int_attr(attrs, "QUAPOS", 0)
|
||||
if 2 <= quapos < 10:
|
||||
return ("LC(LOWACC41)" if udw else "LC(LOWACC31)"), extra
|
||||
if udw:
|
||||
return "LS(DOTT,2,CHBLK)", extra
|
||||
if valsou is None or valsou <= 20.0:
|
||||
return "LS(DOTT,2,CHBLK)", extra
|
||||
return "LS(DASH,2,CHBLK)", extra
|
||||
|
||||
# AREA
|
||||
if udw:
|
||||
return "AC(DEPVS);AP(FOULAR01);LS(DOTT,2,CHBLK);" + udw, extra
|
||||
watlev = _int_attr(attrs, "WATLEV", -9)
|
||||
if valsou is not None:
|
||||
line = "LS(DOTT,2,CHBLK)" if valsou <= 20.0 else "LS(DASH,2,CHBLK)"
|
||||
return line, extra
|
||||
if watlev in {1, 2}:
|
||||
return "AC(CHBRN);LS(SOLD,2,CSTLN)", extra
|
||||
if watlev == 4:
|
||||
return "AC(DEPIT);LS(DASH,2,CSTLN)", extra
|
||||
if _int_attr(attrs, "CATOBS", -9) == 6 and watlev in {3, 5}:
|
||||
return "AC(DEPVS);AP(FOULAR01);LS(DOTT,2,CHBLK)", extra
|
||||
return "AC(DEPVS);LS(DOTT,2,CHBLK)", extra
|
||||
|
||||
|
||||
def resolve_wrecks02_instruction(
|
||||
geometry_type: str,
|
||||
attrs: dict[str, Any],
|
||||
settings: S52MarinerSettings,
|
||||
*,
|
||||
associated_depth_ranges: list[dict[str, Any]] | None = None,
|
||||
) -> tuple[str, dict[str, Any]]:
|
||||
valsou = _optional_float(attrs.get("VALSOU"))
|
||||
watlev = _int_attr(attrs, "WATLEV", -9)
|
||||
catwrk = _int_attr(attrs, "CATWRK", -9)
|
||||
depth_value = valsou
|
||||
if depth_value is None and str(geometry_type).upper() == "AREA":
|
||||
depth_value = _area_least_depth(associated_depth_ranges)
|
||||
if depth_value is None:
|
||||
if catwrk == 1:
|
||||
depth_value = 20.0
|
||||
elif catwrk == 2:
|
||||
depth_value = 0.0
|
||||
elif catwrk in {4, 5}:
|
||||
depth_value = -15.0
|
||||
elif watlev == 3:
|
||||
depth_value = 0.01
|
||||
elif watlev == 5:
|
||||
depth_value = 0.0
|
||||
else:
|
||||
depth_value = -15.0
|
||||
|
||||
quasou7 = 7 in _int_list(attrs.get("QUASOU"))
|
||||
if quasou7:
|
||||
udw, udw_extra = "", {"s52_isolated_danger": 0, "s52_display_base_promoted": 0}
|
||||
else:
|
||||
udw, udw_extra = resolve_udwhaz03_instruction(
|
||||
attrs, depth_value, settings,
|
||||
associated_depth_ranges=associated_depth_ranges,
|
||||
)
|
||||
extra = dict(udw_extra)
|
||||
extra.update({"s52_hazard_procedure": "WRECKS02", "s52_hazard_depth_m": float(depth_value)})
|
||||
geom = str(geometry_type).upper()
|
||||
if geom == "POINT":
|
||||
if udw:
|
||||
return udw, extra
|
||||
if valsou is not None:
|
||||
symbol = "DANGER51" if valsou < settings.safety_contour_m else "DANGER52"
|
||||
instruction = f"SY({symbol});TX('Wk',3,1,2,'15110',2,0,CHBLK,21)"
|
||||
if quasou7:
|
||||
instruction += ";SY(WRECKS07)"
|
||||
snd, snd_extra = resolve_soundg03_instruction(attrs, valsou, settings)
|
||||
if snd:
|
||||
instruction += ";" + snd
|
||||
extra.update(snd_extra)
|
||||
return instruction, extra
|
||||
if catwrk == 1 and watlev == 3:
|
||||
return "SY(WRECKS04)", extra
|
||||
if catwrk == 2 and watlev == 3:
|
||||
return "SY(WRECKS05)", extra
|
||||
if catwrk in {4, 5} or watlev in {1, 2, 4, 5}:
|
||||
return "SY(WRECKS01)", extra
|
||||
return "SY(WRECKS05)", extra
|
||||
|
||||
quapos = _int_attr(attrs, "QUAPOS", 0)
|
||||
if 2 <= quapos < 10:
|
||||
line = "LC(LOWACC41)"
|
||||
elif udw or valsou is None or valsou <= 20.0:
|
||||
line = "LS(DOTT,2,CHBLK)" if udw or valsou is not None else "LS(DOTT,2,CSTLN)"
|
||||
else:
|
||||
line = "LS(DASH,2,CHBLK)"
|
||||
instruction = line
|
||||
if valsou is not None and valsou <= 20.0:
|
||||
instruction += ";" + udw if udw else ""
|
||||
snd, snd_extra = resolve_soundg03_instruction(attrs, valsou, settings)
|
||||
if snd:
|
||||
instruction += ";" + snd
|
||||
extra.update(snd_extra)
|
||||
elif valsou is None:
|
||||
color = "CHBRN" if watlev in {1, 2} else "DEPIT" if watlev == 4 else "DEPVS"
|
||||
instruction += f";AC({color})"
|
||||
if udw:
|
||||
instruction += ";" + udw
|
||||
return instruction, extra
|
||||
|
||||
def resolve_csp_hints(
|
||||
csp_name: str,
|
||||
object_class: str,
|
||||
geometry_type: str,
|
||||
attrs: dict[str, Any],
|
||||
settings: S52MarinerSettings,
|
||||
*,
|
||||
next_safe_contour_m: float | None = None,
|
||||
sounding_depth_m: float | None = None,
|
||||
associated_depth_ranges: list[dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
name = str(csp_name or "")[:8]
|
||||
instruction: str | None = None
|
||||
@@ -405,6 +667,18 @@ def resolve_csp_hints(
|
||||
attrs, sounding_depth_m, settings
|
||||
)
|
||||
extra.update(sounding_extra)
|
||||
elif name == "OBSTRN04":
|
||||
instruction, hazard_extra = resolve_obstrn04_instruction(
|
||||
object_class, geometry_type=geometry_type,
|
||||
attrs=attrs, settings=settings,
|
||||
associated_depth_ranges=associated_depth_ranges,
|
||||
)
|
||||
extra.update(hazard_extra)
|
||||
elif name == "WRECKS02":
|
||||
instruction, hazard_extra = resolve_wrecks02_instruction(
|
||||
geometry_type, attrs, settings, associated_depth_ranges=associated_depth_ranges
|
||||
)
|
||||
extra.update(hazard_extra)
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ DELIVERY_PROPERTY_ALLOWLIST = frozenset(
|
||||
"s52_arc_start_deg",
|
||||
"s52_arc_width",
|
||||
"s52_has_text",
|
||||
"s52_display_base_promoted",
|
||||
"s52_hazard_depth_m",
|
||||
"s52_hazard_procedure",
|
||||
"s52_isolated_danger",
|
||||
"s52_line_color",
|
||||
"s52_line_complex_code",
|
||||
"s52_line_style",
|
||||
|
||||
@@ -341,6 +341,7 @@ class S52LookupRegistry:
|
||||
*,
|
||||
next_safe_contour_m: float | None = None,
|
||||
sounding_depth_m: float | None = None,
|
||||
associated_depth_ranges: list[dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
row = self.resolve(object_class, geometry_type, attrs)
|
||||
if row is None:
|
||||
@@ -364,10 +365,12 @@ class S52LookupRegistry:
|
||||
resolved = resolve_csp_hints(
|
||||
procedure,
|
||||
str(object_class),
|
||||
str(geometry_type),
|
||||
normalized_attrs,
|
||||
self.settings,
|
||||
next_safe_contour_m=next_safe_contour_m,
|
||||
sounding_depth_m=sounding_depth_m,
|
||||
associated_depth_ranges=associated_depth_ranges,
|
||||
)
|
||||
if resolved:
|
||||
out.update(resolved)
|
||||
|
||||
Reference in New Issue
Block a user