#!/usr/bin/env python3 """生成 CM95 / NavSea Visual Object Reviewer 使用的中文名称补充表。 注意: - 该文件只用于 UI 显示和人工理解; - 不参与 Visual Object 匹配、聚类、Layer 判定; - CM95 英文说明来自当前 CM95 仓库中的 S-52 metadata; - 中文名称为工程显示译名,不冒充 IHO 官方中文译名。 """ from __future__ import annotations import json import re from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parent OUTPUT = ROOT / "output" CM95_ROOT = Path("/root/sourceserver/cm95") LAYER_ZH = { "anchor_caution_hazard_outline": "锚泊注意/危险区轮廓", "anchor_caution_hazard_point": "锚泊注意/危险点", "anchorage_area": "锚地面", "anchorage_outline": "锚地轮廓", "anchorage_point": "锚地点", "baseline_area": "基础区域", "baseline_line": "基础线", "baseline_outline": "基础区域轮廓", "bathymetry_line": "海底地形线", "bridge_structure": "桥梁结构", "clearance_limit_line": "净空限制线", "clearance_limit_point": "净空限制点", "clip_outline_754": "特殊边界线", "depth_contour": "等深线", "depth_zone_739": "水深分区 739", "depth_zone_741": "水深分区 741", "facility_boundary_outline": "设施边界轮廓", "facility_boundary_point": "港口/设施点", "fixed_fishing_gear_area": "定置渔具区域", "hazard_boundary_outline": "危险边界轮廓", "hole_area": "孔洞/挖空区域", "land_area": "陆地区域", "navigation_hazard_outline": "航行危险轮廓", "navigation_hazard_point": "航行危险点", "navigation_marks": "航路标识", "onshore_structure_area": "陆上结构物面", "onshore_structure_line": "陆上结构物线", "onshore_structure_point": "陆上结构物点", "pilot_station_point": "引航站", "place_label_land": "陆地地名", "place_label_sea": "海域地名", "seabed_line": "海底对象线", "seabed_text_point": "海底文字点", "submerged_reef_area": "水下礁区域", "tide": "潮汐", } STD_KEY_ZH = { "tidespot-daytime": "潮汐点", "pattern_fill_fishery_or_fish_reef_area": "定置渔业/鱼礁填充", "special_pattern_line_754": "特殊边界线", "bollard_pile_stake": "系缆柱、桩、杭", "cleared_danger_object": "扫海済危险物", "dolphin_structure": "系泊墩", "eddy_whirlpool": "涡流", "fish_reef": "鱼礁", "fish_reef_dangerous": "危险鱼礁", "foul_ground": "碍锚海底", "isolated_danger": "孤立危险物", "obstruction": "障碍物", "rock_exposed_drying_awash_group": "露出岩/干出岩/洗岩", "sand_wave": "沙波", "seaweed": "海草", "subsea_installation_outfall_intake": "海底设施/放水口/取水口", "sunken_rock": "暗岩", "tide_rips_overfalls": "急潮/波纹", "tower_yagura_observation_platform": "塔/架台/测台", "wreck_fully_submerged_dangerous": "危险全沉船", "wreck_hull_exposed": "船体或桅杆露出沉船", "wreck_surveyed": "测定済沉船", "anchorage_designated": "指定锚地", "anchorage_quarantine": "检疫锚地", "anchoring_prohibited": "禁止锚泊区", "restriction_area_group": "限制区域", "light_flare_green": "绿色灯光", "light_flare_red": "红色灯光", "light_flare_yellow_white": "黄色/白色灯光", "harbor_lighthouse": "港湾灯塔", "breakwater_lighthouse": "防波堤灯塔", "coastal_lighthouse": "沿岸灯塔", "light_minor": "小型灯标", "light_beacon": "灯标", "light_beacon_east_cardinal_variantt": "东方位灯标", "light_beacon_isolated_danger_variantt_311359": "孤立危险标识灯标", "light_beacon_north_cardinal_variantt": "北方位灯标", "light_beacon_north_cardinal_variantt_311356": "北方位灯标", "light_beacon_safe_water_variantt_311360": "安全水域标识灯标", "light_beacon_south_cardinal_variantt": "南方位灯标", "light_beacon_west_cardinal_variantt_311358": "西方位灯标", "buoy_can": "罐形浮标", "buoy_east_cardinal": "东方位浮标", "buoy_isolated_danger": "孤立危险浮标", "buoy_lattice": "架形浮标", "buoy_north_cardinal": "北方位浮标", "buoy_pillar": "柱形浮标", "buoy_port_lateral_variantt_312350": "左舷浮标", "buoy_safe_water": "安全水域浮标", "buoy_south_cardinal": "南方位浮标", "buoy_special_mark": "特殊标识浮标", "buoy_starboard_lateral_variantt_312351": "右舷浮标", "buoy_west_cardinal": "西方位浮标", "mooring_buoy": "系泊浮标", "fisherina": "渔港休闲设施", "fishing_port": "渔港", "marina": "游艇码头", "port_general_small_harbor_group": "一般港湾/小港湾", "sea_station_umi_no_eki": "海之驿", "pilot_station": "引航站", "symbol-daytime-401": "露出岩/干出岩/洗岩", "symbol-daytime-404": "暗岩", "symbol-daytime-405": "扫海済危险物", "symbol-daytime-409": "孤立危险物", "symbol-daytime-410": "船体或桅杆露出沉船", "symbol-daytime-412": "危险全沉船", "symbol-daytime-413": "测定済沉船", "symbol-daytime-420": "碍锚海底", "symbol-daytime-421": "沙波", "symbol-daytime-422": "急潮/波纹", "symbol-daytime-424": "涡流", "symbol-daytime-425": "海草", "symbol-daytime-427": "障碍物", "symbol-daytime-428": "鱼礁", "symbol-daytime-429": "危险鱼礁", "symbol-daytime-431": "塔/架台/测台", "symbol-daytime-432": "系缆柱、桩、杭", "symbol-daytime-433": "系泊墩", "symbol-daytime-434": "海底设施/放水口/取水口", "symbol-daytime-640": "烟囱", "symbol-daytime-650": "陆上显著结构物", "symbol-daytime-660": "陆上显著结构物", "symbol-daytime-661": "陆上显著结构物", "symbol-daytime-681": "陆上显著结构物", "symbol-daytime-698": "其他陆上结构物", } def color_zh(text: str) -> str: replacements = [ ("black/yellow/black", "黑/黄/黑"), ("yellow/black/yellow", "黄/黑/黄"), ("black/red/black", "黑/红/黑"), ("green/white", "绿/白"), ("red/white", "红/白"), ("black/yellow", "黑/黄"), ("yellow/black", "黄/黑"), ("white/black", "白/黑"), ("black/white", "黑/白"), ("white/red", "白/红"), ("red/green", "红/绿"), ("white or yellow", "白色或黄色"), ("yellow", "黄色"), ("green", "绿色"), ("red", "红色"), ("white", "白色"), ("black", "黑色"), ] out = text for en, zh in replacements: out = out.replace(en, zh) return out def s52_zh(key: str, desc: str) -> tuple[str, str]: """返回 (中文显示名, 来源说明)。""" d = desc.strip().lower() if not d: fallback = { "STARPT01": "星形点标志", "TOPMA115": "顶标", "TOPMAR01": "浮标顶标", }.get(key, "未提供说明") return fallback, "工程显示译名" exact = { "anchorage area": "锚地", "designated anchor berth for a single vessel": "指定单船锚泊位", "symbol for opening bridge": "开启桥", "berth number symbol": "泊位编号", "built-up area": "建成区", "coastguard station": "海岸警卫站", "chimney": "烟囱", "conspicuous chimney": "显著烟囱", "ho information note": "海图机构信息注记", "flare stack": "火炬塔", "flagstaff, flagpole": "旗杆", "fog signal": "雾号", "fortified structure": "防御工事", "fish haven": "鱼礁/鱼类栖息设施", "fishing harbour": "渔港", "light flare": "灯光标志", "floodlight": "泛光灯", "fish farm": "养殖场", "mooring dolphin": "系泊墩", "mast": "桅杆", "offshore platform": "海上平台", "pilot boarding place": "引航员登轮点", "pile or bollard": "桩/系缆柱", "traffic precautionary area": "通航注意区", "radar reflector": "雷达反射器", "radio station": "无线电台", "radar transponder beacon": "雷达应答标", "yacht harbour, marina": "游艇码头", "sand waves": "沙波", "traffic crossing area": "交通交叉区", "traffic roundabout": "交通环岛", "weed, kelp": "海草/海带", "windmotor": "风车", "submarine cable": "海底电缆", "ferry route": "渡轮航线", "fishing stakes": "定置渔桩", "area with minor restrictions or information notices": "轻微限制/信息提示区域", "beacon in general, black/yellow/back, full-chart": "一般立标(黑/黄/黑,完整显示)", "beacon in general, yellow/back/yellow, full-chart": "一般立标(黄/黑/黄,完整显示)", "beacon in general, black/red/back, full-chart": "一般立标(黑/红/黑,完整显示)", "conical buoy,lateral-green,full-chart": "锥形侧面浮标(绿色,完整显示)", "mooring buoy, can shape, paper-chart": "罐形系泊浮标(海图符号)", "pillar buoy,cardianl-yby,full-chart": "柱形方位浮标(黄/黑/黄,完整显示)", "topmark for buoys, cone point up, paper-chart": "浮标顶标:单锥尖向上", "topmark for buoys, 2 cones point upward, paper-chart": "浮标顶标:双锥尖向上", "topmark for buoys, 2 cones point downward, paper-chart": "浮标顶标:双锥尖向下", "topmark for buoys, 2 cones base to base, paper-chart": "浮标顶标:双锥底相对", "topmark for buoys, 2 cones point to point, paper-chart": "浮标顶标:双锥尖相对", "topmark for buoys, sphere, paper-chart": "浮标顶标:球形", "topmark for buoys, 2 spheres, paper-chart": "浮标顶标:双球", "topmark for buoys, cylinder, paper-chart": "浮标顶标:圆柱形", "beacon top mark, red boarded square board, vertical": "立标顶标:红边方形板(竖向)", } if d in exact: return exact[d], "S-52 英文说明的中文显示译名" if d.startswith("anchorage area as a point"): return "锚地点", "S-52 英文说明的中文显示译名" if d.startswith("beacon in general"): suffix = d.split(",", 1)[1].strip() if "," in d else "" return "一般立标" + (f"({color_zh(suffix.replace('full-chart','完整显示').replace('paper-chart','海图符号'))})" if suffix else ""), "S-52 英文说明的中文显示译名" if "spar beacon" in d: return color_zh(d.replace("spar beacon", "杆形立标").replace("full-chart", "完整显示")), "S-52 英文说明的中文显示译名" if "stake or pole beacon" in d: return color_zh(d.replace("minor, ", "小型").replace("stake or pole beacon", "桩/杆形立标").replace("full-chart", "完整显示")), "S-52 英文说明的中文显示译名" if d.startswith("beacon tower"): return color_zh(d.replace("beacon tower", "塔形立标").replace("full-chart", "完整显示").replace("brb", "黑/红/黑")), "S-52 英文说明的中文显示译名" if "buoy" in d: out = d out = out.replace("can buoy", "罐形浮标").replace("conical buoy", "锥形浮标") out = out.replace("mooring buoy", "系泊浮标").replace("pillar buoy", "柱形浮标") out = out.replace("spherical buoy", "球形浮标").replace("spar buoy", "杆形浮标") out = out.replace("super-buoy", "特大型浮标").replace("super buoy", "特大型浮标") out = out.replace("default symbol for buoy", "一般浮标") out = out.replace("paper-chart", "海图符号").replace("full-chart", "完整显示") out = out.replace("lateral-r", "侧面标识-红").replace("lateral-g", "侧面标识-绿") out = out.replace("special purpose-y", "特殊用途-黄") out = out.replace("cardinal-byb", "方位标-黑/黄/黑").replace("cardinal-yby", "方位标-黄/黑/黄") out = out.replace("cardinal-by", "方位标-黑/黄").replace("cardinal-yb", "方位标-黄/黑") out = out.replace("isol.danger-brb", "孤立危险标-黑/红/黑").replace("safe water-rw", "安全水域标-红/白") out = color_zh(out) return out, "S-52 英文说明的中文显示译名" if d.startswith("caution area"): return "注意区域", "S-52 英文说明的中文显示译名" if d.startswith("cautionary area"): return "谨慎航行区域", "S-52 英文说明的中文显示译名" if d.startswith("foul area of seabed"): return "碍锚海底(航行安全但不宜锚泊)", "S-52 英文说明的中文显示译名" if d.startswith("fish trap"): return "渔具/鱼栅/金枪鱼网", "S-52 英文说明的中文显示译名" if d.startswith("navigable lock gate"): return "可通航闸门", "S-52 英文说明的中文显示译名" if d.startswith("non-navigable lock gate"): return "不可通航闸门", "S-52 英文说明的中文显示译名" if d.startswith("light flare"): return color_zh(d.replace("light flare", "灯光标志")), "S-52 英文说明的中文显示译名" if d.startswith("light float"): return "灯浮体", "S-52 英文说明的中文显示译名" if d.startswith("light vessel"): return color_zh(d.replace("light vessel", "灯船").replace("full-chart", "完整显示")), "S-52 英文说明的中文显示译名" if d.startswith("cursor pick site for a magnetic anomaly"): return "磁异常位置", "S-52 英文说明的中文显示译名" if d.startswith("point feature or area of low accuracy"): return "低位置精度对象/区域", "S-52 英文说明的中文显示译名" if d.startswith("position of an elevation or control point"): return "高程点/控制点", "S-52 英文说明的中文显示译名" if d.startswith("object which is not sufficiently described"): return "未充分描述/无对应符号的对象", "S-52 英文说明的中文显示译名" if d.startswith("symbol indicating this object is radar conspicuous"): return "雷达显著物标志", "S-52 英文说明的中文显示译名" if d.startswith("recommended route"): return "推荐航路", "S-52 英文说明的中文显示译名" if d.startswith("topmark for buoys"): tail = d.split(",", 1)[1].strip() if "," in d else "" trans = tail.replace("2 cones point upward", "双锥尖向上").replace("2 cones point downward", "双锥尖向下") trans = trans.replace("2 cones base to base", "双锥底相对").replace("2 cones point to point", "双锥尖相对") trans = trans.replace("cone point up", "单锥尖向上").replace("2 spheres", "双球").replace("sphere", "球形").replace("cylinder", "圆柱形") trans = trans.replace("paper-chart", "海图符号") return f"浮标顶标:{trans}", "S-52 英文说明的中文显示译名" if d.startswith("topmark for beacons"): tail = d.split(",", 1)[1].strip() if "," in d else "" trans = tail.replace("2 cones point upward", "双锥尖向上").replace("2 cones point downward", "双锥尖向下") trans = trans.replace("2 cones base to base", "双锥底相对").replace("2 cones point to point", "双锥尖相对") trans = trans.replace("cone point up", "单锥尖向上").replace("2 spheres", "双球").replace("sphere", "球形").replace("cylinder", "圆柱形") trans = trans.replace("paper-chart", "海图符号") return f"立标顶标:{trans}", "S-52 英文说明的中文显示译名" if d.startswith("beacon top mark"): return color_zh(d.replace("beacon top mark", "立标顶标").replace("square board", "方形板").replace("diagonal", "对角").replace("vertical", "竖向")), "S-52 英文说明的中文显示译名" if d.startswith("tower"): return color_zh(d.replace("tower", "塔").replace("full-chart and simplified", "完整/简化显示")), "S-52 英文说明的中文显示译名" if d == "water tower": return "水塔", "S-52 英文说明的中文显示译名" if d == "conspicuous tower": return "显著塔", "S-52 英文说明的中文显示译名" if d == "radio, television tower": return "无线电/电视塔", "S-52 英文说明的中文显示译名" if d == "conspicuous radio, television tower": return "显著无线电/电视塔", "S-52 英文说明的中文显示译名" if d.startswith("traffic direction"): return "分道通航单向航道交通方向", "S-52 英文说明的中文显示译名" if d.startswith("overfalls, eddies and breakers"): return "急流、涡流和碎波", "S-52 英文说明的中文显示译名" if d.startswith("pattern of symbols for a fish haven"): return "鱼礁填充图案", "S-52 英文说明的中文显示译名" if d.startswith("pattern of symbols for a marine farm"): return "养殖场填充图案", "S-52 英文说明的中文显示译名" if d.startswith("pattern of symbols for a marsh"): return "沼泽填充图案", "S-52 英文说明的中文显示译名" if d.startswith("incompletely surveyed area"): return "未完全测量区域", "S-52 英文说明的中文显示译名" if d.startswith("pattern of symbols for an area which is not sufficiently described"): return "未充分描述区域填充图案", "S-52 英文说明的中文显示译名" if d.startswith("precautionary area or a traffic separation scheme"): return "通航注意区/分道通航交叉或环岛", "S-52 英文说明的中文显示译名" if d.startswith("pattern of symbols for wooded areas"): return "林地区域填充图案", "S-52 英文说明的中文显示译名" if d == "essa or pssa boundary": return "特别敏感海域/环境敏感海域边界", "S-52 英文说明的中文显示译名" if d.startswith("safety contour of low accuracy"): return "低位置精度安全等深线", "S-52 英文说明的中文显示译名" if d.startswith("coastline or shoreline construction of low accuracy"): return "低位置精度海岸线/岸线构筑物", "S-52 英文说明的中文显示译名" if d.startswith("oil, gas pipeline"): return "油气管线(海底或陆上)", "S-52 英文说明的中文显示译名" if d.startswith("water pipeline"): return "水管/下水管等", "S-52 英文说明的中文显示译名" if d.startswith("non-regulated recommended two-way track"): return "非管制推荐双向航迹", "S-52 英文说明的中文显示译名" if d.startswith("non-regulated recommended one-way track"): return "非管制推荐单向航迹", "S-52 英文说明的中文显示译名" # 兜底:避免伪造精确术语。仍展示英文原说明。 return "待人工确认中文名", "尚未建立可靠中文显示译名" def build_cm95_names() -> dict[str, Any]: cm = json.loads((OUTPUT / "cm95-anonymous" / "anonymous-catalog.json").read_text(encoding="utf-8")) keys = sorted({o["resource_key"] for o in cm["visual_objects"] if o.get("resource_key")}) base = json.loads((CM95_ROOT / "build/s52_resources_v2/base/s52_symbol_metadata.json").read_text(encoding="utf-8")) ap_raw = json.loads((CM95_ROOT / "build/s52_resources_v2/ap/s52-sprite-ap-pattern-metadata.json").read_text(encoding="utf-8")) lc_raw = json.loads((CM95_ROOT / "build/s52_resources_v2/final/s52-sprite-full-line-metadata.json").read_text(encoding="utf-8")) ap = {v.get("sprite_key"): v for v in ap_raw.values()} lc = {v.get("sprite_key"): v for v in lc_raw.values()} out: dict[str, Any] = {} for key in keys: rec = base.get(key) or ap.get(key) or lc.get(key) source_type = "symbol" if key in base else ("fill-pattern" if key in ap else ("line-pattern" if key in lc else "unknown")) desc = str((rec or {}).get("description") or "") if not desc: # 两个基础图标可从其同名 pattern metadata 得到安全的对象级概念。 if key == "FSHHAV02": desc = "fish haven" elif key == "MARSHES1": desc = "marsh" elif key == "CROSSX01": desc = "" zh, zh_source = s52_zh(key, desc) if key == "MARSHES1": zh, zh_source = "沼泽", "由同名 S-52 填充图案说明补充" elif key == "FSHHAV02": zh, zh_source = "鱼礁/鱼类栖息设施", "由同名 S-52 填充图案说明补充" elif key == "CROSSX01": zh, zh_source = "交叉标志(待确认)", "S-52 metadata 未找到说明,名称仅作 UI 提示" out[key] = { "code": key, "resource_type": source_type, "official_description_en": desc or None, "display_name_zh": zh, "display_name_source": zh_source, } return out def std_name(key: str, layer: str) -> dict[str, Any]: zh = STD_KEY_ZH.get(key) source = "NavSea 标准资源语义键中文显示译名" if not zh: if key.startswith("leading_light"): zh = "导灯" elif key.startswith("leading_mark"): zh = "导标" elif key.startswith("arc-daytime"): zh = "灯弧" elif key.startswith("symbol-daytime"): zh = LAYER_ZH.get(layer, "标准视觉对象") source = "按标准 Layer 显示,具体对象名待补" else: zh = LAYER_ZH.get(layer, "标准视觉对象") source = "按标准 Layer 显示,具体对象名待补" return { "resource_key": key, "standard_layer": layer, "standard_layer_zh": LAYER_ZH.get(layer, layer), "display_name_zh": zh, "display_name_source": source, } def build_std_names() -> dict[str, Any]: std = json.loads((OUTPUT / "navsea-standard" / "standard-catalog.json").read_text(encoding="utf-8")) out: dict[str, Any] = {} for obj in std["visual_objects"]: key = obj.get("resource_key") layer = obj.get("standard_layer_name") if not key or not layer: continue out[key] = std_name(key, layer) return out def main() -> None: result = { "schema": "visual-object-reference-names/v1", "policy": { "ui_only": True, "used_for_matching": False, "used_for_layer_assignment": False, "chinese_names_are_display_translations": True, }, "cm95": build_cm95_names(), "navsea_standard": build_std_names(), "standard_layer_zh": LAYER_ZH, } path = OUTPUT / "reference-name-enrichment-v1.json" path.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(path) print("CM95", len(result["cm95"])) print("NavSea Standard", len(result["navsea_standard"])) print("CM95 待人工确认中文名", sum(x["display_name_zh"] == "待人工确认中文名" for x in result["cm95"].values())) if __name__ == "__main__": main()