feat: implement S-52 shoreline construction presentation
This commit is contained in:
62
docs/报告/CM95_S52_岸线构造与位置精度阶段报告_v1.md
Normal file
62
docs/报告/CM95_S52_岸线构造与位置精度阶段报告_v1.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# CM95 S-52 岸线构造与位置精度阶段报告 v1
|
||||
|
||||
## 范围
|
||||
|
||||
本阶段实现 OpenCPN 对应的:
|
||||
|
||||
- `SLCONS03`
|
||||
- `QUAPOS01`
|
||||
- `QUALIN01`
|
||||
- `QUAPNT01`
|
||||
- S-52 bitmap area pattern → MapLibre Sprite 编译
|
||||
|
||||
基线:Linux `/root/sourceserver/cm95`。
|
||||
正式 production release 未修改。
|
||||
|
||||
## 规则实现
|
||||
|
||||
`SLCONS03` 已覆盖 Point / Line / Area。
|
||||
岸线构造根据 `QUAPOS / CONDTN / CATSLC / WATLEV` 生成 `SY / LC / LS / AP` Presentation Rule IR。
|
||||
|
||||
`QUAPOS01` 对线对象走 `QUALIN01`,点/面走 `QUAPNT01`;低精度位置转换为 `LOWACCxx / QUAPOSxx`。
|
||||
## 福冈湾 z12 实测
|
||||
|
||||
Tile:`12/3531/1641`
|
||||
|
||||
- `SLCONS03`:169 / 169 已解析
|
||||
- `QUAPOS01`:23 / 23 已解析
|
||||
- 本阶段后剩余未解析 CSP:`RESTRN01=2`、`RESARE02=1`
|
||||
- `SLCONS_QUAPOS_TILE_CONTRACT=PASS`
|
||||
|
||||
SLCONS 结果分布:
|
||||
|
||||
- LINE `SOLD,2,CSTLN`:135
|
||||
- AREA `SOLD,2,CSTLN`:15
|
||||
- AREA `DASH,2,CSTLN`:8
|
||||
- AREA `SOLD,4,CSTLN`:5
|
||||
- AREA `DASH,1,CSTLN`:3
|
||||
- LINE `DASH,1,CSTLN`:2
|
||||
- LINE `DASH,2,CSTLN`:1
|
||||
- `AP(CROSSX01)`:31 个 AREA
|
||||
## Sprite Pattern
|
||||
|
||||
S-52 Sprite 编译器现在同时读取 `<symbols>` 与 `<patterns>`。
|
||||
当前 DAY_BRIGHT atlas 自动输出:
|
||||
|
||||
- bitmap symbol:1013
|
||||
- bitmap pattern:8
|
||||
- sprite 总 entry:1021
|
||||
|
||||
`CROSSX01`:16×16,atlas 坐标 `(400,1040)`,pivot `(8,8)`。
|
||||
`PATTERN_SPRITE_CONTRACT=PASS`。
|
||||
|
||||
## 浏览器验收
|
||||
|
||||
`presentation_v8.html` 使用独立 `s52-sprite-v8` 避免旧 Sprite 缓存。
|
||||
CDP 等待页面进入 READY 后确认:
|
||||
|
||||
- SLCONS03=169
|
||||
- QUAPOS01=23
|
||||
- `CROSSX01` 实际显示
|
||||
- 非 favicon 浏览器错误:0
|
||||
- LIGHTS05、SOUNDG、危险物、深度色与等深线保持正常
|
||||
@@ -14,6 +14,7 @@ class S52MarinerSettings:
|
||||
safety_depth_m: float = 3.0
|
||||
deep_contour_m: float = 6.0
|
||||
two_shades: bool = False
|
||||
symbolized_boundaries: bool = False
|
||||
|
||||
|
||||
def _float(attrs: dict[str, Any], key: str, default: float) -> float:
|
||||
@@ -632,6 +633,129 @@ def resolve_wrecks02_instruction(
|
||||
instruction += ";" + udw
|
||||
return instruction, extra
|
||||
|
||||
|
||||
def resolve_quapnt01_instruction(attrs: dict[str, Any]) -> str:
|
||||
quapos = _int_attr(attrs, "QUAPOS", 10)
|
||||
if not (2 <= quapos < 10):
|
||||
return ""
|
||||
if quapos == 4:
|
||||
return "SY(QUAPOS01)"
|
||||
if quapos == 5:
|
||||
return "SY(QUAPOS02)"
|
||||
if quapos in {7, 8}:
|
||||
return "SY(QUAPOS03)"
|
||||
return "SY(LOWACC03)"
|
||||
|
||||
|
||||
def resolve_qualin01_instruction(
|
||||
object_class: str,
|
||||
attrs: dict[str, Any],
|
||||
) -> str:
|
||||
quapos_present = attrs.get("QUAPOS") is not None
|
||||
quapos = _int_attr(attrs, "QUAPOS", 0)
|
||||
if quapos_present:
|
||||
if 2 <= quapos < 10:
|
||||
return "LC(LOWACC21)"
|
||||
return ""
|
||||
if object_class == "COALNE":
|
||||
conrad = _int_attr(attrs, "CONRAD", 0)
|
||||
if conrad == 1:
|
||||
return "LS(SOLD,3,CHMGF);LS(SOLD,1,CSTLN)"
|
||||
return "LS(SOLD,1,CSTLN)"
|
||||
|
||||
|
||||
def resolve_quapos01_instruction(
|
||||
object_class: str,
|
||||
geometry_type: str,
|
||||
attrs: dict[str, Any],
|
||||
) -> str:
|
||||
if str(geometry_type).upper() == "LINE":
|
||||
return resolve_qualin01_instruction(object_class, attrs)
|
||||
return resolve_quapnt01_instruction(attrs)
|
||||
|
||||
|
||||
def resolve_slcons03_instruction(
|
||||
geometry_type: str,
|
||||
attrs: dict[str, Any],
|
||||
) -> str:
|
||||
geom = str(geometry_type).upper()
|
||||
quapos_present = attrs.get("QUAPOS") is not None
|
||||
quapos = _int_attr(attrs, "QUAPOS", 0)
|
||||
|
||||
if geom == "POINT":
|
||||
if quapos_present and 2 <= quapos < 10:
|
||||
return "SY(LOWACC01)"
|
||||
return ""
|
||||
|
||||
instructions: list[str] = []
|
||||
if geom == "AREA":
|
||||
instructions.append("AP(CROSSX01)")
|
||||
|
||||
if quapos_present and 2 <= quapos < 10:
|
||||
instructions.append("LC(LOWACC01)")
|
||||
return ";".join(instructions)
|
||||
|
||||
condtn = _int_attr(attrs, "CONDTN", 0)
|
||||
catslc = _int_attr(attrs, "CATSLC", 0)
|
||||
watlev = _int_attr(attrs, "WATLEV", 0)
|
||||
if condtn in {1, 2}:
|
||||
instructions.append("LS(DASH,1,CSTLN)")
|
||||
elif catslc in {6, 15, 16}:
|
||||
instructions.append("LS(SOLD,4,CSTLN)")
|
||||
elif watlev == 2:
|
||||
instructions.append("LS(SOLD,2,CSTLN)")
|
||||
elif watlev in {3, 4}:
|
||||
instructions.append("LS(DASH,2,CSTLN)")
|
||||
else:
|
||||
instructions.append("LS(SOLD,2,CSTLN)")
|
||||
return ";".join(instructions)
|
||||
|
||||
def resolve_restrn01_instruction(attrs: dict[str, Any]) -> str:
|
||||
restrn = set(_int_list(attrs.get("RESTRN")))
|
||||
if not restrn:
|
||||
return ""
|
||||
if restrn & {7, 8, 14}:
|
||||
symbol = "ENTRES61" if restrn & {1, 2, 3, 4, 5, 6} else "ENTRES71" if restrn & {9, 10, 11, 12, 13, 14, 15} else "ENTRES51"
|
||||
elif restrn & {1, 2}:
|
||||
symbol = "ACHRES61" if restrn & {3, 4, 5, 6} else "ACHRES71" if restrn & {9, 10, 11, 12, 13, 14, 15} else "ACHRES51"
|
||||
elif restrn & {3, 4, 5, 6}:
|
||||
symbol = "FSHRES71" if restrn & {9, 10, 11, 12, 13, 14, 15} else "FSHRES51"
|
||||
else:
|
||||
symbol = "INFARE51" if restrn & {9, 10, 11, 12, 13, 14, 15} else "RSRDEF51"
|
||||
return f"SY({symbol})"
|
||||
|
||||
|
||||
def resolve_resare02_instruction(
|
||||
attrs: dict[str, Any], settings: S52MarinerSettings
|
||||
) -> str:
|
||||
restrn = set(_int_list(attrs.get("RESTRN")))
|
||||
catrea = set(_int_list(attrs.get("CATREA")))
|
||||
group_a = {1, 8, 9, 12, 14, 19, 21, 25}
|
||||
group_b = {4, 5, 6, 7, 10, 18, 20, 22, 23, 24}
|
||||
priority = False
|
||||
if restrn:
|
||||
if restrn & {7, 8, 14}:
|
||||
symbol = "ENTRES61" if (restrn & {1,2,3,4,5,6} or catrea & group_a) else "ENTRES71" if (restrn & {9,10,11,12,13,14,15} or catrea & group_b) else "ENTRES51"
|
||||
line_code = "RESARE51"; priority = True
|
||||
elif restrn & {1, 2}:
|
||||
symbol = "ACHRES61" if (restrn & {3,4,5,6} or catrea & group_a) else "ACHRES71" if (restrn & {9,10,11,12,13,14,15} or catrea & group_b) else "RESTRN51"
|
||||
line_code = "RESARE51"; priority = True
|
||||
elif restrn & {3,4,5,6}:
|
||||
symbol = "FSHRES51" if catrea & group_a else "FSHRES71" if (restrn & {9,10,11,12,13,14,15} or catrea & group_b) else "FSHRES51"
|
||||
line_code = "FSHRES51"; priority = True
|
||||
else:
|
||||
symbol = "INFARE51" if restrn & {9,10,11,12,13,14,15} else "RSRDEF51"
|
||||
line_code = "CTYARE51"
|
||||
else:
|
||||
symbol = "CTYARE71" if (catrea & group_a and catrea & group_b) else "CTYARE51" if catrea & group_a else "INFARE51" if catrea & group_b else "RSRDEF51"
|
||||
line_code = "CTYARE51"
|
||||
line = f"LC({line_code})" if settings.symbolized_boundaries else "LS(DASH,2,CHMGD)"
|
||||
parts = [line, f"SY({symbol})"]
|
||||
if priority:
|
||||
parts.insert(0, "OP(6---)")
|
||||
return ";".join(parts)
|
||||
|
||||
|
||||
def resolve_csp_hints(
|
||||
csp_name: str,
|
||||
object_class: str,
|
||||
@@ -679,6 +803,14 @@ def resolve_csp_hints(
|
||||
geometry_type, attrs, settings, associated_depth_ranges=associated_depth_ranges
|
||||
)
|
||||
extra.update(hazard_extra)
|
||||
elif name == "QUAPOS01":
|
||||
instruction = resolve_quapos01_instruction(object_class, geometry_type, attrs)
|
||||
elif name == "SLCONS03":
|
||||
instruction = resolve_slcons03_instruction(geometry_type, attrs)
|
||||
elif name == "RESTRN01":
|
||||
instruction = resolve_restrn01_instruction(attrs)
|
||||
elif name == "RESARE02":
|
||||
instruction = resolve_resare02_instruction(attrs, settings)
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
@@ -73,6 +73,27 @@ def _parse_symbols(root: ET.Element) -> dict[str, dict[str, Any]]:
|
||||
return out
|
||||
|
||||
|
||||
def _parse_patterns(root: ET.Element) -> dict[str, dict[str, Any]]:
|
||||
out: dict[str, dict[str, Any]] = {}
|
||||
parent = root.find("patterns")
|
||||
if parent is None:
|
||||
return out
|
||||
for node in parent.findall("pattern"):
|
||||
name = _text(node, "name")
|
||||
if not name:
|
||||
continue
|
||||
out[name] = {
|
||||
"name": name,
|
||||
"description": _text(node, "description"),
|
||||
"definition": _text(node, "definition") or None,
|
||||
"filltype": _text(node, "filltype") or None,
|
||||
"spacing": _text(node, "spacing") or None,
|
||||
"color_ref": _text(node, "color-ref") or None,
|
||||
"bitmap": _parse_bitmap(node),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def _parse_lookups(root: ET.Element, targets: set[str]) -> list[dict[str, Any]]:
|
||||
out: list[dict[str, Any]] = []
|
||||
parent = root.find("lookups")
|
||||
@@ -231,6 +252,7 @@ def main() -> int:
|
||||
raise SystemExit(f"missing chartsymbols.xml: {xml_path}")
|
||||
root = ET.parse(xml_path).getroot()
|
||||
symbols = _parse_symbols(root)
|
||||
patterns = _parse_patterns(root)
|
||||
lookups = _parse_lookups(root, targets)
|
||||
color_tables = _parse_color_tables(root)
|
||||
|
||||
@@ -245,13 +267,20 @@ def main() -> int:
|
||||
|
||||
atlas_width, atlas_height = _png_size(atlas_path)
|
||||
direct_symbols = sorted({name for row in lookups for name in row["sy"]})
|
||||
direct_patterns = sorted({name for row in lookups for name in row["ap"]})
|
||||
csp_names = sorted({name for row in lookups for name in row["cs"]})
|
||||
sprite_symbols = (
|
||||
sorted(name for name, symbol in symbols.items() if symbol.get("bitmap") is not None)
|
||||
if sprite_scope == "all"
|
||||
else direct_symbols
|
||||
)
|
||||
sprite_patterns = (
|
||||
sorted(name for name, pattern in patterns.items() if pattern.get("bitmap") is not None)
|
||||
if sprite_scope == "all"
|
||||
else direct_patterns
|
||||
)
|
||||
metadata: dict[str, dict[str, Any]] = {}
|
||||
pattern_metadata: dict[str, dict[str, Any]] = {}
|
||||
missing_symbols: list[str] = []
|
||||
vector_only_symbols: list[str] = []
|
||||
out_of_bounds_symbols: list[str] = []
|
||||
@@ -278,6 +307,26 @@ def main() -> int:
|
||||
"maplibre_icon_offset": _icon_offset(bitmap),
|
||||
}
|
||||
|
||||
for name in sprite_patterns:
|
||||
pattern = patterns.get(name)
|
||||
if pattern is None:
|
||||
continue
|
||||
bitmap = pattern.get("bitmap")
|
||||
if bitmap is None:
|
||||
continue
|
||||
if (
|
||||
bitmap["x"] < 0
|
||||
or bitmap["y"] < 0
|
||||
or bitmap["x"] + bitmap["width"] > atlas_width
|
||||
or bitmap["y"] + bitmap["height"] > atlas_height
|
||||
):
|
||||
continue
|
||||
pattern_metadata[name] = dict(pattern)
|
||||
|
||||
collisions = sorted(set(metadata) & set(pattern_metadata))
|
||||
if collisions:
|
||||
raise SystemExit(f"sprite symbol/pattern name collision: {collisions}")
|
||||
|
||||
output = args.output
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(atlas_path, output / "sprite.png")
|
||||
@@ -285,12 +334,19 @@ def main() -> int:
|
||||
name: _sprite_entry(metadata[name]["bitmap"])
|
||||
for name in sorted(metadata)
|
||||
}
|
||||
sprite_json.update({
|
||||
name: _sprite_entry(pattern_metadata[name]["bitmap"])
|
||||
for name in sorted(pattern_metadata)
|
||||
})
|
||||
(output / "sprite.json").write_text(
|
||||
json.dumps(sprite_json, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
(output / "s52_symbol_metadata.json").write_text(
|
||||
json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
(output / "s52_pattern_metadata.json").write_text(
|
||||
json.dumps(pattern_metadata, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
(output / "s52_target_lookups.json").write_text(
|
||||
json.dumps(lookups, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
@@ -323,7 +379,10 @@ def main() -> int:
|
||||
"conditional_lookup_count": len(conditional_rows),
|
||||
"conditional_procedures": csp_names,
|
||||
"direct_symbol_reference_count": len(direct_symbols),
|
||||
"direct_pattern_reference_count": len(direct_patterns),
|
||||
"sprite_scope": sprite_scope,
|
||||
"sprite_symbol_entry_count": len(metadata),
|
||||
"sprite_pattern_entry_count": len(pattern_metadata),
|
||||
"sprite_entry_count": len(sprite_json),
|
||||
"missing_symbol_definitions": direct_missing,
|
||||
"vector_only_symbol_definitions": vector_only_symbols,
|
||||
|
||||
Reference in New Issue
Block a user