chore: 全量快照提交以防磁盘风险
This commit is contained in:
7
src/Domain/__init__.py
Normal file
7
src/Domain/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""NavSea Chart Domain prototype modules.
|
||||
|
||||
This package is intentionally isolated from the current production builder.
|
||||
It exists to prototype the next-generation Chart Domain output structure
|
||||
without changing the current engineering/delivery PBF generation pipeline.
|
||||
"""
|
||||
|
||||
336
src/Domain/build_chart_domain_tiles.py
Normal file
336
src/Domain/build_chart_domain_tiles.py
Normal file
@@ -0,0 +1,336 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate isolated NavSea Chart Domain prototype PBF packages.
|
||||
|
||||
This builder is intentionally separate from the current production builders.
|
||||
It reads an existing engineering-style PBF root, re-groups layers by the new
|
||||
Chart Domain model, and writes prototype packages without touching the current
|
||||
engineering/delivery product line.
|
||||
|
||||
Current output model:
|
||||
|
||||
- safety package: safety_core + safety_extended
|
||||
- detail package: detail
|
||||
|
||||
Layers marked as domain_pending are not emitted to final packages. They are
|
||||
instead recorded in the audit report so we can refine semantics before any
|
||||
production switch-over.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from collections import Counter, defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import mapbox_vector_tile
|
||||
|
||||
from chart_domain_model import (
|
||||
DETAIL,
|
||||
DOMAIN_PENDING,
|
||||
PHYSICAL_PACK_BY_DOMAIN,
|
||||
SAFETY_CORE,
|
||||
SAFETY_EXTENDED,
|
||||
get_layer_domain_spec,
|
||||
normalize_layer_std,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TileResult:
|
||||
safety_written: bool
|
||||
detail_written: bool
|
||||
feature_count: int
|
||||
pending_count: int
|
||||
|
||||
|
||||
def text_or_none(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
text = str(value).strip()
|
||||
return text or None
|
||||
|
||||
|
||||
class ChartDomainTileBuilder:
|
||||
def __init__(
|
||||
self,
|
||||
input_root: Path,
|
||||
output_root: Path,
|
||||
*,
|
||||
include_pending_in_safety: bool = False,
|
||||
) -> None:
|
||||
self.input_root = input_root
|
||||
self.output_root = output_root
|
||||
self.include_pending_in_safety = include_pending_in_safety
|
||||
self.safety_root = output_root / "safety"
|
||||
self.detail_root = output_root / "detail"
|
||||
self.audit_json_path = output_root / "chart_domain_build_audit.json"
|
||||
self.audit_md_path = output_root / "chart_domain_build_audit.md"
|
||||
|
||||
self.feature_count = 0
|
||||
self.renderable_count = 0
|
||||
self.pending_count = 0
|
||||
self.unmapped_source_count = 0
|
||||
self.domain_counter: Counter[str] = Counter()
|
||||
self.layer_counter: Counter[str] = Counter()
|
||||
self.pending_layer_counter: Counter[str] = Counter()
|
||||
self.pending_examples: list[dict[str, Any]] = []
|
||||
|
||||
def build(self) -> None:
|
||||
self.output_root.mkdir(parents=True, exist_ok=True)
|
||||
self.clear_existing_tiles()
|
||||
tile_paths = sorted(self.input_root.glob("*/*/*.pbf"))
|
||||
|
||||
safety_written = 0
|
||||
detail_written = 0
|
||||
for tile_path in tile_paths:
|
||||
result = self.process_tile(tile_path)
|
||||
self.feature_count += result.feature_count
|
||||
self.pending_count += result.pending_count
|
||||
if result.safety_written:
|
||||
safety_written += 1
|
||||
if result.detail_written:
|
||||
detail_written += 1
|
||||
|
||||
self.write_audit_report(
|
||||
tile_count=len(tile_paths),
|
||||
safety_written=safety_written,
|
||||
detail_written=detail_written,
|
||||
)
|
||||
|
||||
def clear_existing_tiles(self) -> None:
|
||||
for pack_root in (self.safety_root, self.detail_root):
|
||||
for path in pack_root.glob("*/*/*.pbf"):
|
||||
path.unlink()
|
||||
|
||||
def process_tile(self, tile_path: Path) -> TileResult:
|
||||
rel = tile_path.relative_to(self.input_root)
|
||||
z = int(rel.parts[0])
|
||||
x = int(rel.parts[1])
|
||||
y = int(tile_path.stem)
|
||||
|
||||
decoded = mapbox_vector_tile.decode(tile_path.read_bytes())
|
||||
pack_layers: dict[str, dict[str, list[dict[str, Any]]]] = {
|
||||
"safety": defaultdict(list),
|
||||
"detail": defaultdict(list),
|
||||
}
|
||||
pack_extents: dict[str, dict[str, int]] = {
|
||||
"safety": {},
|
||||
"detail": {},
|
||||
}
|
||||
|
||||
tile_feature_count = 0
|
||||
tile_pending_count = 0
|
||||
|
||||
for input_layer_name, payload in decoded.items():
|
||||
extent = int(payload.get("extent") or 4096)
|
||||
for feature in payload.get("features", []):
|
||||
tile_feature_count += 1
|
||||
properties = dict(feature.get("properties") or {})
|
||||
|
||||
source_layer_jp = text_or_none(properties.get("source_layer_jp")) or input_layer_name
|
||||
raw_source_layer_std = text_or_none(properties.get("source_layer_std")) or input_layer_name
|
||||
source_layer_std = normalize_layer_std(raw_source_layer_std, source_layer_jp)
|
||||
spec = get_layer_domain_spec(source_layer_std)
|
||||
|
||||
self.domain_counter[spec.chart_domain] += 1
|
||||
self.layer_counter[source_layer_std] += 1
|
||||
|
||||
output_properties = dict(properties)
|
||||
output_properties["source_layer_jp"] = source_layer_jp
|
||||
output_properties["source_layer_std"] = source_layer_std
|
||||
output_properties["chart_domain"] = spec.chart_domain
|
||||
output_properties["offline_pack"] = spec.offline_pack
|
||||
output_properties["resolver_priority"] = spec.resolver_priority
|
||||
output_properties["domain_status"] = spec.domain_status
|
||||
output_properties["chart_domain_semantic_zh"] = spec.chinese_semantic
|
||||
|
||||
pack_name = PHYSICAL_PACK_BY_DOMAIN.get(spec.chart_domain)
|
||||
if spec.chart_domain == DOMAIN_PENDING and self.include_pending_in_safety:
|
||||
pack_name = "safety"
|
||||
|
||||
if pack_name is None:
|
||||
tile_pending_count += 1
|
||||
self.pending_layer_counter[source_layer_std] += 1
|
||||
if len(self.pending_examples) < 30:
|
||||
self.pending_examples.append(
|
||||
{
|
||||
"tile": f"{z}/{x}/{y}",
|
||||
"input_layer": input_layer_name,
|
||||
"source_layer_jp": source_layer_jp,
|
||||
"source_layer_std": source_layer_std,
|
||||
"canonical_object_type": output_properties.get("canonical_object_type"),
|
||||
"fid": output_properties.get("fid"),
|
||||
"reason": spec.notes or "domain_pending layer is excluded from final packages",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
output_feature = {
|
||||
"geometry": feature["geometry"],
|
||||
"properties": output_properties,
|
||||
}
|
||||
if feature.get("id") is not None:
|
||||
output_feature["id"] = feature["id"]
|
||||
|
||||
pack_layers[pack_name][source_layer_std].append(output_feature)
|
||||
pack_extents[pack_name].setdefault(source_layer_std, extent)
|
||||
self.renderable_count += 1
|
||||
|
||||
safety_written = self.write_pack_tile(
|
||||
pack_name="safety",
|
||||
rel=rel,
|
||||
layers=pack_layers["safety"],
|
||||
extents=pack_extents["safety"],
|
||||
)
|
||||
detail_written = self.write_pack_tile(
|
||||
pack_name="detail",
|
||||
rel=rel,
|
||||
layers=pack_layers["detail"],
|
||||
extents=pack_extents["detail"],
|
||||
)
|
||||
|
||||
return TileResult(
|
||||
safety_written=safety_written,
|
||||
detail_written=detail_written,
|
||||
feature_count=tile_feature_count,
|
||||
pending_count=tile_pending_count,
|
||||
)
|
||||
|
||||
def write_pack_tile(
|
||||
self,
|
||||
*,
|
||||
pack_name: str,
|
||||
rel: Path,
|
||||
layers: dict[str, list[dict[str, Any]]],
|
||||
extents: dict[str, int],
|
||||
) -> bool:
|
||||
if not layers:
|
||||
return False
|
||||
|
||||
encoded_layers = []
|
||||
per_layer_options: dict[str, dict[str, int]] = {}
|
||||
for layer_name, features in sorted(layers.items()):
|
||||
if not features:
|
||||
continue
|
||||
encoded_layers.append({"name": layer_name, "features": features})
|
||||
per_layer_options[layer_name] = {"extents": extents[layer_name]}
|
||||
|
||||
if not encoded_layers:
|
||||
return False
|
||||
|
||||
pack_root = self.safety_root if pack_name == "safety" else self.detail_root
|
||||
output_path = pack_root / rel
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_bytes(
|
||||
mapbox_vector_tile.encode(encoded_layers, per_layer_options=per_layer_options)
|
||||
)
|
||||
return True
|
||||
|
||||
def write_audit_report(self, *, tile_count: int, safety_written: int, detail_written: int) -> None:
|
||||
payload = {
|
||||
"input_root": str(self.input_root),
|
||||
"output_root": str(self.output_root),
|
||||
"include_pending_in_safety": self.include_pending_in_safety,
|
||||
"tile_count": tile_count,
|
||||
"safety_written_tiles": safety_written,
|
||||
"detail_written_tiles": detail_written,
|
||||
"feature_count": self.feature_count,
|
||||
"renderable_feature_count": self.renderable_count,
|
||||
"pending_feature_count": self.pending_count,
|
||||
"domain_counts": dict(self.domain_counter),
|
||||
"layer_counts": dict(self.layer_counter.most_common()),
|
||||
"pending_layer_counts": dict(self.pending_layer_counter.most_common()),
|
||||
"pending_examples": self.pending_examples,
|
||||
}
|
||||
self.audit_json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
lines = [
|
||||
"# NavSea Chart Domain Build Audit",
|
||||
"",
|
||||
f"- input_root: `{self.input_root}`",
|
||||
f"- output_root: `{self.output_root}`",
|
||||
f"- include_pending_in_safety: `{self.include_pending_in_safety}`",
|
||||
f"- tile_count: `{tile_count}`",
|
||||
f"- safety_written_tiles: `{safety_written}`",
|
||||
f"- detail_written_tiles: `{detail_written}`",
|
||||
f"- feature_count: `{self.feature_count}`",
|
||||
f"- renderable_feature_count: `{self.renderable_count}`",
|
||||
f"- pending_feature_count: `{self.pending_count}`",
|
||||
"",
|
||||
"## Domain Counts",
|
||||
"",
|
||||
]
|
||||
for name, count in self.domain_counter.most_common():
|
||||
lines.append(f"- `{name}`: `{count}`")
|
||||
|
||||
lines.extend(["", "## Pending Layers", ""])
|
||||
if not self.pending_layer_counter:
|
||||
lines.append("- none")
|
||||
else:
|
||||
for layer_name, count in self.pending_layer_counter.most_common():
|
||||
lines.append(f"- `{layer_name}`: `{count}`")
|
||||
|
||||
lines.extend(["", "## Pending Examples", ""])
|
||||
if not self.pending_examples:
|
||||
lines.append("- none")
|
||||
else:
|
||||
for item in self.pending_examples[:20]:
|
||||
lines.append(
|
||||
f"- tile=`{item['tile']}` input=`{item['input_layer']}` std=`{item['source_layer_std']}` "
|
||||
f"object=`{item.get('canonical_object_type') or 'n/a'}` fid=`{item.get('fid')}`"
|
||||
)
|
||||
lines.append(f" reason: {item['reason']}")
|
||||
|
||||
self.audit_md_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Build isolated NavSea Chart Domain PBF packages.")
|
||||
parser.add_argument(
|
||||
"--input-root",
|
||||
type=Path,
|
||||
default=Path("/home/wwwroot/pbf-engineering-karatsu-10nm"),
|
||||
help="Input engineering PBF root to transform.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-root",
|
||||
type=Path,
|
||||
default=Path("/home/wwwroot/pbf-domain-karatsu-10nm"),
|
||||
help="Output root for chart domain prototype packages.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-pending-in-safety",
|
||||
action="store_true",
|
||||
help="Compatibility mode: emit domain_pending layers into the safety package instead of excluding them.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
builder = ChartDomainTileBuilder(
|
||||
input_root=args.input_root,
|
||||
output_root=args.output_root,
|
||||
include_pending_in_safety=args.include_pending_in_safety,
|
||||
)
|
||||
builder.build()
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"input_root": str(args.input_root),
|
||||
"output_root": str(args.output_root),
|
||||
"include_pending_in_safety": args.include_pending_in_safety,
|
||||
"audit_json": str(builder.audit_json_path),
|
||||
"audit_md": str(builder.audit_md_path),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
156
src/Domain/build_legacy_compatible_domain_style.py
Normal file
156
src/Domain/build_legacy_compatible_domain_style.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""Generate a legacy-compatible style that consumes the Domain PBF packages.
|
||||
|
||||
This script preserves the current production rendering rules as much as
|
||||
possible by transforming the original legacy style:
|
||||
|
||||
- keep the original layer ordering, paint, layout, filters, sprite, glyphs
|
||||
- remap each legacy Japanese `source-layer` to the standardized Domain layer
|
||||
- route each style layer to the physical Domain package source:
|
||||
- `domain_safety`
|
||||
- `domain_detail`
|
||||
|
||||
The goal of this script is *not* to redesign the map. It exists to validate
|
||||
that the redefined Domain PBF structure can still reproduce the legacy visual
|
||||
appearance with minimal rendering loss.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from chart_domain_model import (
|
||||
DETAIL,
|
||||
SAFETY_CORE,
|
||||
SAFETY_EXTENDED,
|
||||
get_layer_domain_spec,
|
||||
)
|
||||
from style_layer_naming import make_style_layer_id, parse_source_layer_rules, remap_style_layer
|
||||
|
||||
|
||||
def domain_source_name(chart_domain: str) -> str:
|
||||
if chart_domain in (SAFETY_CORE, SAFETY_EXTENDED):
|
||||
return "domain_safety"
|
||||
if chart_domain == DETAIL:
|
||||
return "domain_detail"
|
||||
# Pending layers are intentionally pointed to safety. If the target layer
|
||||
# does not exist in the tile, MapLibre will simply render nothing.
|
||||
return "domain_safety"
|
||||
|
||||
|
||||
def transform_style(
|
||||
legacy_style: dict,
|
||||
jp_to_std: dict[str, str],
|
||||
safety_tiles_url: str,
|
||||
detail_tiles_url: str,
|
||||
) -> tuple[dict, dict[str, int]]:
|
||||
style = copy.deepcopy(legacy_style)
|
||||
style["name"] = "NavSea Domain Legacy Compatible Karatsu 10nm"
|
||||
|
||||
legacy_sources = style.get("sources", {})
|
||||
style["sources"] = {
|
||||
"domain_safety": {
|
||||
"type": "vector",
|
||||
"minzoom": 0,
|
||||
"maxzoom": 12,
|
||||
"tiles": [safety_tiles_url],
|
||||
"attribution": "© NavSea Domain",
|
||||
},
|
||||
"domain_detail": {
|
||||
"type": "vector",
|
||||
"minzoom": 0,
|
||||
"maxzoom": 12,
|
||||
"tiles": [detail_tiles_url],
|
||||
"attribution": "© NavSea Domain",
|
||||
},
|
||||
"mapple": legacy_sources.get("mapple", {}),
|
||||
"shipfinder": legacy_sources.get("shipfinder", {}),
|
||||
}
|
||||
|
||||
counts = {
|
||||
"remapped_layers": 0,
|
||||
"pending_layers": 0,
|
||||
"unchanged_layers": 0,
|
||||
}
|
||||
|
||||
used_ids: set[str] = set()
|
||||
source_role_counts: dict[tuple[str, str], int] = {}
|
||||
generic_role_counts: dict[tuple[str, str], int] = {}
|
||||
|
||||
for layer in style.get("layers", []):
|
||||
source_layer_std, remapped = remap_style_layer(layer, jp_to_std)
|
||||
if not remapped:
|
||||
layer["id"] = make_style_layer_id(
|
||||
layer=layer,
|
||||
source_layer_std=None,
|
||||
used_ids=used_ids,
|
||||
source_role_counts=source_role_counts,
|
||||
generic_role_counts=generic_role_counts,
|
||||
)
|
||||
counts["unchanged_layers"] += 1
|
||||
continue
|
||||
|
||||
spec = get_layer_domain_spec(source_layer_std)
|
||||
layer["source"] = domain_source_name(spec.chart_domain)
|
||||
layer["id"] = make_style_layer_id(
|
||||
layer=layer,
|
||||
source_layer_std=source_layer_std,
|
||||
used_ids=used_ids,
|
||||
source_role_counts=source_role_counts,
|
||||
generic_role_counts=generic_role_counts,
|
||||
)
|
||||
counts["remapped_layers"] += 1
|
||||
if spec.domain_status != "active":
|
||||
counts["pending_layers"] += 1
|
||||
|
||||
return style, counts
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Build a legacy-compatible style for Domain PBFs.")
|
||||
parser.add_argument(
|
||||
"--legacy-style",
|
||||
default="/mnt/sda1/www/newpec/style.patched.local.json",
|
||||
help="Path to the original legacy style JSON.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--source-layer-rules",
|
||||
default="/root/sourceserver/pbf/tasks/pbf/mappings/navsea_source_layer_rules_v1.yaml",
|
||||
help="Path to source-layer mapping rules.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
default="/root/sourceserver/pbf/src/Domain/style.domain-legacy-compatible-karatsu-10nm.json",
|
||||
help="Output style path.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--safety-tiles-url",
|
||||
default="http://192.168.200.184/pbf-domain-karatsu-10nm/safety/{z}/{x}/{y}.pbf",
|
||||
help="Domain safety package tiles URL template.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--detail-tiles-url",
|
||||
default="http://192.168.200.184/pbf-domain-karatsu-10nm/detail/{z}/{x}/{y}.pbf",
|
||||
help="Domain detail package tiles URL template.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
legacy_style = json.loads(Path(args.legacy_style).read_text(encoding="utf-8"))
|
||||
jp_to_std = parse_source_layer_rules(Path(args.source_layer_rules))
|
||||
style, counts = transform_style(
|
||||
legacy_style=legacy_style,
|
||||
jp_to_std=jp_to_std,
|
||||
safety_tiles_url=args.safety_tiles_url,
|
||||
detail_tiles_url=args.detail_tiles_url,
|
||||
)
|
||||
|
||||
output_path = Path(args.output)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(json.dumps(style, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"output": str(output_path), **counts}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
249
src/Domain/chart_domain_model.py
Normal file
249
src/Domain/chart_domain_model.py
Normal file
@@ -0,0 +1,249 @@
|
||||
"""Chart Domain model definitions for the isolated NavSea domain prototype.
|
||||
|
||||
The model in this module is based on NavSea_Chart_Domain_Model_v1.md.
|
||||
It groups standardized layers into:
|
||||
|
||||
- safety_core
|
||||
- safety_extended
|
||||
- detail
|
||||
- domain_pending
|
||||
|
||||
This file deliberately does not mutate the current production mapping system.
|
||||
It provides a standalone semantic model for the new experimental PBF flow.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LayerDomainSpec:
|
||||
layer_std: str
|
||||
chart_domain: str
|
||||
offline_pack: str
|
||||
resolver_priority: int
|
||||
chinese_semantic: str
|
||||
domain_status: str = "active"
|
||||
notes: str = ""
|
||||
|
||||
|
||||
DOMAIN_PENDING = "domain_pending"
|
||||
SAFETY_CORE = "safety_core"
|
||||
SAFETY_EXTENDED = "safety_extended"
|
||||
DETAIL = "detail"
|
||||
|
||||
PHYSICAL_PACK_BY_DOMAIN = {
|
||||
SAFETY_CORE: "safety",
|
||||
SAFETY_EXTENDED: "safety",
|
||||
DETAIL: "detail",
|
||||
DOMAIN_PENDING: None,
|
||||
}
|
||||
|
||||
# Prototype-only aliases for source layers that still appear in current
|
||||
# engineering PBFs without a populated source_layer_std property.
|
||||
# This keeps the new Domain prototype isolated from the production mapping
|
||||
# registry while allowing the v1 model to be exercised end-to-end.
|
||||
SOURCE_LAYER_STD_ALIASES = {
|
||||
"P投錨注意障害物": "anchor_caution_hazard_area",
|
||||
"P航行危険障害物": "navigation_hazard_area",
|
||||
"P錨泊地等": "anchorage_area",
|
||||
"p施設・境界線等": "facility_boundary_point",
|
||||
}
|
||||
|
||||
|
||||
LAYER_DOMAIN_SPECS: dict[str, LayerDomainSpec] = {
|
||||
"navigation_hazard_area": LayerDomainSpec(
|
||||
"navigation_hazard_area", SAFETY_CORE, "safety", 10, "航行危险障害物面"
|
||||
),
|
||||
"navigation_hazard_outline": LayerDomainSpec(
|
||||
"navigation_hazard_outline", SAFETY_CORE, "safety", 10, "航行危险障害物边界"
|
||||
),
|
||||
"navigation_hazard_point": LayerDomainSpec(
|
||||
"navigation_hazard_point", SAFETY_CORE, "safety", 10, "航行危险障害物点"
|
||||
),
|
||||
"anchor_caution_hazard_area": LayerDomainSpec(
|
||||
"anchor_caution_hazard_area", SAFETY_EXTENDED, "safety", 20, "抛锚注意障害物面"
|
||||
),
|
||||
"anchor_caution_hazard_outline": LayerDomainSpec(
|
||||
"anchor_caution_hazard_outline", SAFETY_EXTENDED, "safety", 20, "抛锚注意障害物边界"
|
||||
),
|
||||
"anchor_caution_hazard_point": LayerDomainSpec(
|
||||
"anchor_caution_hazard_point", SAFETY_EXTENDED, "safety", 20, "抛锚注意障害物点"
|
||||
),
|
||||
"depth_contour": LayerDomainSpec(
|
||||
"depth_contour", SAFETY_CORE, "base", 10, "等深线"
|
||||
),
|
||||
"depth_contour_overview": LayerDomainSpec(
|
||||
"depth_contour_overview", SAFETY_CORE, "base", 10, "概略等深线"
|
||||
),
|
||||
"navigation_marks": LayerDomainSpec(
|
||||
"navigation_marks", SAFETY_CORE, "base", 10, "航标/灯标/浮标点"
|
||||
),
|
||||
"land_area": LayerDomainSpec(
|
||||
"land_area", SAFETY_CORE, "base", 10, "陆地区域", notes="Primary land/sea discriminator."
|
||||
),
|
||||
"submerged_reef_area": LayerDomainSpec(
|
||||
"submerged_reef_area", SAFETY_CORE, "safety", 10, "潜堤/潜礁区域"
|
||||
),
|
||||
"hazard_boundary_line": LayerDomainSpec(
|
||||
"hazard_boundary_line", SAFETY_CORE, "safety", 10, "危险界线"
|
||||
),
|
||||
"hazard_boundary_outline": LayerDomainSpec(
|
||||
"hazard_boundary_outline", SAFETY_CORE, "safety", 10, "危险界边界"
|
||||
),
|
||||
"anchorage_area": LayerDomainSpec(
|
||||
"anchorage_area", SAFETY_EXTENDED, "safety", 20, "锚地/锚泊地区域"
|
||||
),
|
||||
"anchorage_outline": LayerDomainSpec(
|
||||
"anchorage_outline", SAFETY_EXTENDED, "safety", 20, "锚地边界"
|
||||
),
|
||||
"anchorage_point": LayerDomainSpec(
|
||||
"anchorage_point", SAFETY_EXTENDED, "safety", 20, "锚地符号点"
|
||||
),
|
||||
"clearance_limit_line": LayerDomainSpec(
|
||||
"clearance_limit_line", SAFETY_EXTENDED, "safety", 20, "高度/净空限制线"
|
||||
),
|
||||
"clearance_limit_point": LayerDomainSpec(
|
||||
"clearance_limit_point", SAFETY_EXTENDED, "safety", 20, "高度限制点标注"
|
||||
),
|
||||
"bridge_structure": LayerDomainSpec(
|
||||
"bridge_structure", SAFETY_EXTENDED, "safety", 20, "桥梁等结构物面"
|
||||
),
|
||||
"onshore_structure_area": LayerDomainSpec(
|
||||
"onshore_structure_area", SAFETY_EXTENDED, "safety", 20, "陆上结构物面"
|
||||
),
|
||||
"onshore_structure_line": LayerDomainSpec(
|
||||
"onshore_structure_line", SAFETY_EXTENDED, "safety", 20, "陆上结构物线"
|
||||
),
|
||||
"onshore_structure_point": LayerDomainSpec(
|
||||
"onshore_structure_point", SAFETY_EXTENDED, "safety", 20, "陆上结构物点"
|
||||
),
|
||||
"baseline_area": LayerDomainSpec(
|
||||
"baseline_area", SAFETY_EXTENDED, "safety", 20, "基本线相关面", notes="Container-like semantic; may need refinement."
|
||||
),
|
||||
"baseline_line": LayerDomainSpec(
|
||||
"baseline_line", SAFETY_EXTENDED, "safety", 20, "基本线线层", notes="Container-like semantic; may need refinement."
|
||||
),
|
||||
"baseline_outline": LayerDomainSpec(
|
||||
"baseline_outline", SAFETY_EXTENDED, "safety", 20, "基本线边界", notes="Container-like semantic; may need refinement."
|
||||
),
|
||||
"route_area": LayerDomainSpec(
|
||||
"route_area", SAFETY_EXTENDED, "safety", 20, "航路区域"
|
||||
),
|
||||
"route_outline": LayerDomainSpec(
|
||||
"route_outline", SAFETY_EXTENDED, "safety", 20, "航路边界"
|
||||
),
|
||||
"route_axis_line": LayerDomainSpec(
|
||||
"route_axis_line", SAFETY_EXTENDED, "safety", 20, "航路线/航路轴线"
|
||||
),
|
||||
"route_boundary_point": LayerDomainSpec(
|
||||
"route_boundary_point", SAFETY_EXTENDED, "safety", 20, "航路边界标注点"
|
||||
),
|
||||
"leading_line_outline": LayerDomainSpec(
|
||||
"leading_line_outline", SAFETY_EXTENDED, "safety", 20, "导标/引导线边界"
|
||||
),
|
||||
"pilot_station_point": LayerDomainSpec(
|
||||
"pilot_station_point", SAFETY_EXTENDED, "safety", 20, "引航站点"
|
||||
),
|
||||
"fixed_fishing_gear_area": LayerDomainSpec(
|
||||
"fixed_fishing_gear_area", SAFETY_CORE, "safety", 10, "定置渔具区域"
|
||||
),
|
||||
"place_label_sea": LayerDomainSpec(
|
||||
"place_label_sea", DETAIL, "detail", 30, "海上地名标注"
|
||||
),
|
||||
"place_label_land": LayerDomainSpec(
|
||||
"place_label_land", DETAIL, "detail", 30, "陆上地名标注"
|
||||
),
|
||||
"seabed_text_point": LayerDomainSpec(
|
||||
"seabed_text_point", DETAIL, "detail", 30, "底质标注点"
|
||||
),
|
||||
"facility_boundary_area": LayerDomainSpec(
|
||||
"facility_boundary_area", DETAIL, "detail", 30, "设施/边界相关区域", notes="Broad semantic; may need refinement."
|
||||
),
|
||||
"facility_boundary_outline": LayerDomainSpec(
|
||||
"facility_boundary_outline", DETAIL, "detail", 30, "设施/边界边线", notes="Broad semantic; may need refinement."
|
||||
),
|
||||
"facility_boundary_area_transparent": LayerDomainSpec(
|
||||
"facility_boundary_area_transparent", DETAIL, "detail", 30, "透明设施/边界区域"
|
||||
),
|
||||
"facility_boundary_point": LayerDomainSpec(
|
||||
"facility_boundary_point", DETAIL, "detail", 30, "设施/边界符号点"
|
||||
),
|
||||
"hole_area": LayerDomainSpec(
|
||||
"hole_area", DETAIL, "detail", 30, "穴/凹地区域"
|
||||
),
|
||||
"bathymetry_line": LayerDomainSpec(
|
||||
"bathymetry_line", DETAIL, "detail", 30, "海底地形支撑线", notes="Support line, not a final business semantic."
|
||||
),
|
||||
"seabed_line": LayerDomainSpec(
|
||||
"seabed_line", DETAIL, "detail", 30, "海底线"
|
||||
),
|
||||
"depth_zone_700": LayerDomainSpec(
|
||||
"depth_zone_700", DOMAIN_PENDING, "pending", 99, "深度分带编码层 700", domain_status="pending"
|
||||
),
|
||||
"depth_zone_702": LayerDomainSpec(
|
||||
"depth_zone_702", DOMAIN_PENDING, "pending", 99, "深度分带编码层 702", domain_status="pending"
|
||||
),
|
||||
"depth_zone_725": LayerDomainSpec(
|
||||
"depth_zone_725", DOMAIN_PENDING, "pending", 99, "深度分带编码层 725", domain_status="pending"
|
||||
),
|
||||
"depth_zone_739": LayerDomainSpec(
|
||||
"depth_zone_739", DOMAIN_PENDING, "pending", 99, "深度分带编码层 739", domain_status="pending"
|
||||
),
|
||||
"depth_zone_740": LayerDomainSpec(
|
||||
"depth_zone_740", DOMAIN_PENDING, "pending", 99, "深度分带编码层 740", domain_status="pending"
|
||||
),
|
||||
"depth_zone_741": LayerDomainSpec(
|
||||
"depth_zone_741", DOMAIN_PENDING, "pending", 99, "深度分带编码层 741", domain_status="pending"
|
||||
),
|
||||
"depth_zone_748": LayerDomainSpec(
|
||||
"depth_zone_748", DOMAIN_PENDING, "pending", 99, "深度分带编码层 748", domain_status="pending"
|
||||
),
|
||||
"depth_zone_749": LayerDomainSpec(
|
||||
"depth_zone_749", DOMAIN_PENDING, "pending", 99, "深度分带编码层 749", domain_status="pending"
|
||||
),
|
||||
"clip_outline_721": LayerDomainSpec(
|
||||
"clip_outline_721", DOMAIN_PENDING, "pending", 99, "编码型边界层 721", domain_status="pending"
|
||||
),
|
||||
"clip_outline_730": LayerDomainSpec(
|
||||
"clip_outline_730", DOMAIN_PENDING, "pending", 99, "编码型边界层 730", domain_status="pending"
|
||||
),
|
||||
"clip_outline_754": LayerDomainSpec(
|
||||
"clip_outline_754", DOMAIN_PENDING, "pending", 99, "编码型边界层 754", domain_status="pending"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_layer_domain_spec(layer_std: str) -> LayerDomainSpec:
|
||||
"""Return the declared spec for a standardized layer.
|
||||
|
||||
Unrecognized layers are intentionally treated as pending so the prototype
|
||||
never silently assigns them to a production domain.
|
||||
"""
|
||||
|
||||
spec = LAYER_DOMAIN_SPECS.get(layer_std)
|
||||
if spec is not None:
|
||||
return spec
|
||||
return LayerDomainSpec(
|
||||
layer_std=layer_std,
|
||||
chart_domain=DOMAIN_PENDING,
|
||||
offline_pack="pending",
|
||||
resolver_priority=99,
|
||||
chinese_semantic=f"未注册标准层 {layer_std}",
|
||||
domain_status="pending",
|
||||
notes="No explicit Chart Domain mapping exists yet.",
|
||||
)
|
||||
|
||||
|
||||
def normalize_layer_std(source_layer_std: str, source_layer_jp: str) -> str:
|
||||
"""Return the prototype-standardized layer name.
|
||||
|
||||
In the current engineering PBF line, a few legacy layers still do not
|
||||
carry source_layer_std. The new Domain prototype must normalize them
|
||||
locally without mutating the production mapping chain.
|
||||
"""
|
||||
|
||||
if source_layer_std and source_layer_std != source_layer_jp:
|
||||
return source_layer_std
|
||||
return SOURCE_LAYER_STD_ALIASES.get(source_layer_jp, source_layer_std or source_layer_jp)
|
||||
602
src/Domain/navsea-compare-legacy-delivery-karatsu-10nm.html
Normal file
602
src/Domain/navsea-compare-legacy-delivery-karatsu-10nm.html
Normal file
@@ -0,0 +1,602 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>NavSea Original vs New Build Karatsu 10nm</title>
|
||||
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--bg: #eff3ef;
|
||||
--panel: rgba(248, 246, 241, 0.95);
|
||||
--ink: #1b262d;
|
||||
--muted: #617077;
|
||||
--line: rgba(27, 38, 45, 0.14);
|
||||
--accent: #1f6f5f;
|
||||
--accent-dark: #144b40;
|
||||
--shadow: 0 18px 36px rgba(27, 38, 45, 0.14);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(31, 111, 95, 0.12), transparent 30%),
|
||||
radial-gradient(circle at bottom right, rgba(196, 124, 55, 0.09), transparent 26%),
|
||||
var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: "Avenir Next", "Segoe UI", "PingFang SC", "Noto Sans SC", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 14px 18px 12px;
|
||||
background: linear-gradient(180deg, rgba(248, 246, 241, 0.98), rgba(248, 246, 241, 0.9));
|
||||
border-bottom: 1px solid var(--line);
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 10;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
max-width: 980px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-actions label {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
select,
|
||||
button {
|
||||
height: 42px;
|
||||
padding: 0 16px;
|
||||
border-radius: 12px;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
select {
|
||||
min-width: 146px;
|
||||
border: 1px solid rgba(27, 38, 45, 0.14);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: var(--ink);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid rgba(20, 75, 64, 0.18);
|
||||
background: linear-gradient(180deg, var(--accent), var(--accent-dark));
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 22px rgba(20, 75, 64, 0.18);
|
||||
}
|
||||
|
||||
.layers-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 18px;
|
||||
background: rgba(248, 246, 241, 0.86);
|
||||
border-bottom: 1px solid var(--line);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.layers-toolbar .label {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.layers-toolbar button {
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.layer-strip {
|
||||
display: none;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
overflow: auto;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.layer-strip.open {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.layer-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(27, 38, 45, 0.1);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.pane {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
border-right: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.pane:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.map {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.pane-tag {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 5;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: var(--panel);
|
||||
border: 1px solid rgba(27, 38, 45, 0.08);
|
||||
box-shadow: 0 12px 24px rgba(27, 38, 45, 0.12);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.pane-tag strong {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.pane-tag span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.inspect-panel {
|
||||
position: fixed;
|
||||
top: 92px;
|
||||
right: 14px;
|
||||
z-index: 35;
|
||||
width: min(320px, calc(100vw - 28px));
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow: auto;
|
||||
padding: 10px 11px;
|
||||
border-radius: 14px;
|
||||
background: rgba(248, 246, 241, 0.94);
|
||||
border: 1px solid rgba(27, 38, 45, 0.12);
|
||||
box-shadow: 0 14px 28px rgba(27, 38, 45, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.inspect-panel h3 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.inspect-hint {
|
||||
margin: 0 0 8px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.inspect-meta {
|
||||
display: grid;
|
||||
grid-template-columns: 88px 1fr;
|
||||
gap: 4px 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.inspect-meta dt {
|
||||
color: var(--muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.inspect-meta dd {
|
||||
margin: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.inspect-json {
|
||||
margin: 0;
|
||||
padding: 9px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(27, 38, 45, 0.92);
|
||||
color: #eef3f6;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.status {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 14px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 30;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(27, 38, 45, 0.84);
|
||||
color: #f7f9fb;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.03em;
|
||||
max-width: calc(100% - 28px);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
body {
|
||||
grid-template-rows: auto auto auto 1fr;
|
||||
}
|
||||
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
|
||||
.pane {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.pane:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.inspect-panel {
|
||||
top: auto;
|
||||
right: 12px;
|
||||
bottom: 54px;
|
||||
width: min(320px, calc(100vw - 24px));
|
||||
max-height: 32vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<div class="title">NavSea 原始版 vs 新版构建 · 唐津 10 海里</div>
|
||||
<div class="subtitle">左边固定加载原始样式 <code>style.patched.local.json</code>。右边可在 <code>delivery</code> 和 <code>engineering</code> 之间切换,当前使用的是兼容样式,以便直接对比现有瓦片输出。</div>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<label for="mode-select">右侧模式</label>
|
||||
<select id="mode-select">
|
||||
<option value="delivery">delivery</option>
|
||||
<option value="engineering">engineering</option>
|
||||
</select>
|
||||
<button id="toggle-layers-btn" type="button">图层开关</button>
|
||||
<button id="reload-btn" type="button">重新加载</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layers-toolbar">
|
||||
<span class="label">右侧图层</span>
|
||||
<button id="layers-all-on" type="button">全开</button>
|
||||
<button id="layers-all-off" type="button">全关</button>
|
||||
<div id="layer-strip" class="layer-strip"></div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<section class="pane">
|
||||
<div class="pane-tag">
|
||||
<strong>左侧</strong>
|
||||
<span>原始样式 · style.patched.local.json</span>
|
||||
</div>
|
||||
<div id="map-left" class="map"></div>
|
||||
</section>
|
||||
<section class="pane">
|
||||
<div class="pane-tag">
|
||||
<strong>右侧</strong>
|
||||
<span id="right-pane-label">delivery 兼容样式</span>
|
||||
</div>
|
||||
<div id="map-right" class="map"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside class="inspect-panel">
|
||||
<h3>点击查询</h3>
|
||||
<p id="inspect-hint" class="inspect-hint">点击左图或右图任意对象,查看 source、source-layer、geometry 和 properties。</p>
|
||||
<dl class="inspect-meta">
|
||||
<dt>面板</dt>
|
||||
<dd id="inspect-side">尚未选择对象</dd>
|
||||
<dt>source</dt>
|
||||
<dd id="inspect-source">-</dd>
|
||||
<dt>source-layer</dt>
|
||||
<dd id="inspect-layer">-</dd>
|
||||
<dt>geometry</dt>
|
||||
<dd id="inspect-geometry">-</dd>
|
||||
<dt>render layer</dt>
|
||||
<dd id="inspect-render-layer">-</dd>
|
||||
</dl>
|
||||
<pre id="inspect-json" class="inspect-json">{
|
||||
"message": "等待点击对象"
|
||||
}</pre>
|
||||
</aside>
|
||||
|
||||
<div id="status" class="status">等待加载样式…</div>
|
||||
|
||||
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
|
||||
<script>
|
||||
const MODE_STYLES = {
|
||||
delivery: "./style.navsea-delivery-karatsu-10nm.json",
|
||||
engineering: "./style.compare-engineering-karatsu-10nm.json"
|
||||
};
|
||||
|
||||
const initialView = {
|
||||
center: [129.9697, 33.4425],
|
||||
zoom: 11,
|
||||
pitch: 0,
|
||||
bearing: 0
|
||||
};
|
||||
|
||||
const statusEl = document.getElementById("status");
|
||||
const reloadBtn = document.getElementById("reload-btn");
|
||||
const toggleLayersBtn = document.getElementById("toggle-layers-btn");
|
||||
const modeSelect = document.getElementById("mode-select");
|
||||
const layerStripEl = document.getElementById("layer-strip");
|
||||
const layersAllOnBtn = document.getElementById("layers-all-on");
|
||||
const layersAllOffBtn = document.getElementById("layers-all-off");
|
||||
const rightPaneLabel = document.getElementById("right-pane-label");
|
||||
const inspectHintEl = document.getElementById("inspect-hint");
|
||||
const inspectSideEl = document.getElementById("inspect-side");
|
||||
const inspectSourceEl = document.getElementById("inspect-source");
|
||||
const inspectLayerEl = document.getElementById("inspect-layer");
|
||||
const inspectGeometryEl = document.getElementById("inspect-geometry");
|
||||
const inspectRenderLayerEl = document.getElementById("inspect-render-layer");
|
||||
const inspectJsonEl = document.getElementById("inspect-json");
|
||||
const maps = { left: null, right: null };
|
||||
let syncing = false;
|
||||
let styleLoadVersion = Date.now().toString();
|
||||
|
||||
function setStatus(text) {
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
|
||||
function isToggleableLayer(layer) {
|
||||
return layer && layer.id && layer.type !== "background";
|
||||
}
|
||||
|
||||
function getVisibility(map, layerId) {
|
||||
return map.getLayoutProperty(layerId, "visibility") !== "none";
|
||||
}
|
||||
|
||||
function setVisibility(map, layerId, visible) {
|
||||
map.setLayoutProperty(layerId, "visibility", visible ? "visible" : "none");
|
||||
}
|
||||
|
||||
function resetInspectPanel(message) {
|
||||
inspectSideEl.textContent = "尚未选择对象";
|
||||
inspectSourceEl.textContent = "-";
|
||||
inspectLayerEl.textContent = "-";
|
||||
inspectGeometryEl.textContent = "-";
|
||||
inspectRenderLayerEl.textContent = "-";
|
||||
inspectHintEl.textContent = message;
|
||||
inspectJsonEl.textContent = JSON.stringify({ message }, null, 2);
|
||||
}
|
||||
|
||||
function updateInspectPanel(side, feature) {
|
||||
const sourceLayer = feature.sourceLayer || feature.layer?.["source-layer"] || "-";
|
||||
const renderLayer = feature.layer?.id || "-";
|
||||
inspectHintEl.textContent = "已选中对象。再次点击其他对象可继续比对。";
|
||||
inspectSideEl.textContent = side === "left" ? "左侧原始版" : `右侧 ${modeSelect.value}`;
|
||||
inspectSourceEl.textContent = feature.source || "-";
|
||||
inspectLayerEl.textContent = sourceLayer;
|
||||
inspectGeometryEl.textContent = feature.geometry?.type || "-";
|
||||
inspectRenderLayerEl.textContent = renderLayer;
|
||||
inspectJsonEl.textContent = JSON.stringify(feature.properties || {}, null, 2);
|
||||
}
|
||||
|
||||
function handleMapClick(side, event) {
|
||||
const map = maps[side];
|
||||
const features = map.queryRenderedFeatures(event.point);
|
||||
if (!features.length) {
|
||||
resetInspectPanel(`点击位置没有命中对象。当前面板来自${side === "left" ? "左侧原始版" : `右侧 ${modeSelect.value}`}。`);
|
||||
return;
|
||||
}
|
||||
const [feature] = features;
|
||||
console.log("clicked feature:", feature);
|
||||
updateInspectPanel(side, feature);
|
||||
}
|
||||
|
||||
function renderLayerStrip() {
|
||||
const map = maps.right;
|
||||
if (!map || !map.isStyleLoaded()) {
|
||||
layerStripEl.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const style = map.getStyle();
|
||||
const toggleableLayers = (style.layers || []).filter(isToggleableLayer);
|
||||
layerStripEl.innerHTML = "";
|
||||
|
||||
toggleableLayers.forEach((layer) => {
|
||||
const item = document.createElement("label");
|
||||
item.className = "layer-chip";
|
||||
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = getVisibility(map, layer.id);
|
||||
checkbox.addEventListener("change", () => {
|
||||
setVisibility(map, layer.id, checkbox.checked);
|
||||
});
|
||||
|
||||
const text = document.createElement("span");
|
||||
text.textContent = layer.id;
|
||||
|
||||
item.appendChild(checkbox);
|
||||
item.appendChild(text);
|
||||
layerStripEl.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function setAllRightLayers(visible) {
|
||||
const map = maps.right;
|
||||
if (!map || !map.isStyleLoaded()) return;
|
||||
const style = map.getStyle();
|
||||
(style.layers || []).filter(isToggleableLayer).forEach((layer) => {
|
||||
setVisibility(map, layer.id, visible);
|
||||
});
|
||||
renderLayerStrip();
|
||||
}
|
||||
|
||||
async function fetchStyle(url) {
|
||||
const response = await fetch(url, { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`加载样式失败: ${url}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function withCacheBuster(url, version) {
|
||||
const separator = url.includes("?") ? "&" : "?";
|
||||
return `${url}${separator}v=${version}`;
|
||||
}
|
||||
|
||||
function applySourceCacheBuster(style, version) {
|
||||
if (style.sprite) {
|
||||
style.sprite = withCacheBuster(style.sprite, version);
|
||||
}
|
||||
if (style.glyphs) {
|
||||
style.glyphs = withCacheBuster(style.glyphs, version);
|
||||
}
|
||||
const sources = style.sources || {};
|
||||
Object.values(sources).forEach((source) => {
|
||||
if (Array.isArray(source.tiles)) {
|
||||
source.tiles = source.tiles.map((tileUrl) => withCacheBuster(tileUrl, version));
|
||||
}
|
||||
if (typeof source.url === "string") {
|
||||
source.url = withCacheBuster(source.url, version);
|
||||
}
|
||||
});
|
||||
return style;
|
||||
}
|
||||
|
||||
function syncMap(sourceMap, targetMap) {
|
||||
if (syncing || !targetMap) return;
|
||||
syncing = true;
|
||||
targetMap.jumpTo({
|
||||
center: sourceMap.getCenter(),
|
||||
zoom: sourceMap.getZoom(),
|
||||
bearing: sourceMap.getBearing(),
|
||||
pitch: sourceMap.getPitch()
|
||||
});
|
||||
syncing = false;
|
||||
}
|
||||
|
||||
function attachSync(map, otherKey) {
|
||||
["move", "zoom", "rotate", "pitch"].forEach((eventName) => {
|
||||
map.on(eventName, () => syncMap(map, maps[otherKey]));
|
||||
});
|
||||
}
|
||||
|
||||
async function applyStyle(side) {
|
||||
const url = side === "left"
|
||||
? "../style.patched.local.json"
|
||||
: MODE_STYLES[modeSelect.value];
|
||||
const style = applySourceCacheBuster(await fetchStyle(url), styleLoadVersion);
|
||||
const map = maps[side];
|
||||
const camera = map ? {
|
||||
center: map.getCenter(),
|
||||
zoom: map.getZoom(),
|
||||
bearing: map.getBearing(),
|
||||
pitch: map.getPitch()
|
||||
} : initialView;
|
||||
|
||||
if (!map) {
|
||||
maps[side] = new maplibregl.Map({
|
||||
container: side === "left" ? "map-left" : "map-right",
|
||||
style,
|
||||
center: camera.center,
|
||||
zoom: camera.zoom,
|
||||
bearing: camera.bearing,
|
||||
pitch: camera.pitch
|
||||
});
|
||||
maps[side].addControl(new maplibregl.NavigationControl(), "top-right");
|
||||
attachSync(maps[side], side === "left" ? "right" : "left");
|
||||
maps[side].on("click", (event) => handleMapClick(side, event));
|
||||
if (side === "right") {
|
||||
maps[side].on("style.load", renderLayerStrip);
|
||||
}
|
||||
} else {
|
||||
map.setStyle(style, { diff: false });
|
||||
map.once("style.load", () => {
|
||||
map.jumpTo(camera);
|
||||
if (side === "right") renderLayerStrip();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadStyles() {
|
||||
styleLoadVersion = Date.now().toString();
|
||||
rightPaneLabel.textContent = `${modeSelect.value} 兼容样式`;
|
||||
setStatus("正在加载左右样式…");
|
||||
reloadBtn.disabled = true;
|
||||
try {
|
||||
await Promise.all([applyStyle("left"), applyStyle("right")]);
|
||||
setStatus("样式已加载。拖动任一侧,另一侧会跟随。");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setStatus(error.message || "样式加载失败");
|
||||
} finally {
|
||||
reloadBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
reloadBtn.addEventListener("click", reloadStyles);
|
||||
modeSelect.addEventListener("change", reloadStyles);
|
||||
toggleLayersBtn.addEventListener("click", () => {
|
||||
layerStripEl.classList.toggle("open");
|
||||
});
|
||||
layersAllOnBtn.addEventListener("click", () => setAllRightLayers(true));
|
||||
layersAllOffBtn.addEventListener("click", () => setAllRightLayers(false));
|
||||
resetInspectPanel("点击左图或右图任意对象,查看 source、source-layer、geometry 和 properties。");
|
||||
reloadStyles();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
291
src/Domain/navsea-compare-legacy-domain.html
Normal file
291
src/Domain/navsea-compare-legacy-domain.html
Normal file
@@ -0,0 +1,291 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>NavSea Legacy vs Domain</title>
|
||||
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--bg: #eef3f0;
|
||||
--panel: rgba(248, 246, 241, 0.94);
|
||||
--ink: #18262a;
|
||||
--muted: #607076;
|
||||
--line: rgba(24, 38, 42, 0.14);
|
||||
--accent: #0d6f73;
|
||||
--accent-dark: #0a5357;
|
||||
--shadow: 0 18px 36px rgba(24, 38, 42, 0.14);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(13, 111, 115, 0.12), transparent 30%),
|
||||
radial-gradient(circle at bottom right, rgba(198, 112, 46, 0.08), transparent 28%),
|
||||
var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: "Avenir Next", "Segoe UI", "PingFang SC", "Noto Sans SC", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 14px 18px 12px;
|
||||
background: linear-gradient(180deg, rgba(248, 246, 241, 0.98), rgba(248, 246, 241, 0.88));
|
||||
border-bottom: 1px solid var(--line);
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 10;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
max-width: 920px;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 42px;
|
||||
padding: 0 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(10, 83, 87, 0.18);
|
||||
background: linear-gradient(180deg, var(--accent), var(--accent-dark));
|
||||
color: #fff;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 22px rgba(10, 83, 87, 0.18);
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.pane {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
border-right: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.pane:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.map {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.pane-tag {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 5;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: var(--panel);
|
||||
border: 1px solid rgba(24, 38, 42, 0.08);
|
||||
box-shadow: 0 12px 24px rgba(24, 38, 42, 0.12);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.pane-tag strong {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.pane-tag span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.status {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 14px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 5;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(24, 38, 42, 0.84);
|
||||
color: #f7f9fb;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.03em;
|
||||
max-width: calc(100% - 28px);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
|
||||
.pane {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.pane:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<div class="title">NavSea Legacy vs Domain</div>
|
||||
<div class="subtitle">左边固定加载原始样式 <code>style.patched.local.json</code>。右边固定加载 Domain 试验样式 <code>style.domain-karatsu-10nm.json</code>,它消费新的 <code>safety/detail</code> 双包 `pbf`,用于观察语义分层后的表达。</div>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<button id="reload-btn" type="button">重新加载</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<section class="pane">
|
||||
<div class="pane-tag">
|
||||
<strong>左侧</strong>
|
||||
<span>原始样式 · style.patched.local.json</span>
|
||||
</div>
|
||||
<div id="map-left" class="map"></div>
|
||||
</section>
|
||||
<section class="pane">
|
||||
<div class="pane-tag">
|
||||
<strong>右侧</strong>
|
||||
<span>Domain 样式 · safety/detail 双包</span>
|
||||
</div>
|
||||
<div id="map-right" class="map"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="status" class="status">等待加载样式…</div>
|
||||
|
||||
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
|
||||
<script>
|
||||
const STYLE_URLS = {
|
||||
left: '../style.patched.local.json',
|
||||
right: './style.domain-karatsu-10nm.json'
|
||||
};
|
||||
|
||||
const initialView = {
|
||||
center: [129.9697, 33.4425],
|
||||
zoom: 11,
|
||||
pitch: 0,
|
||||
bearing: 0
|
||||
};
|
||||
|
||||
const statusEl = document.getElementById('status');
|
||||
const reloadBtn = document.getElementById('reload-btn');
|
||||
const maps = { left: null, right: null };
|
||||
let syncing = false;
|
||||
|
||||
function setStatus(text) {
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
|
||||
async function fetchStyle(url) {
|
||||
const response = await fetch(url, { cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
throw new Error(`加载样式失败: ${url}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function syncMap(sourceMap, targetMap) {
|
||||
if (syncing || !targetMap) return;
|
||||
syncing = true;
|
||||
targetMap.jumpTo({
|
||||
center: sourceMap.getCenter(),
|
||||
zoom: sourceMap.getZoom(),
|
||||
bearing: sourceMap.getBearing(),
|
||||
pitch: sourceMap.getPitch()
|
||||
});
|
||||
syncing = false;
|
||||
}
|
||||
|
||||
function attachSync(map, otherKey) {
|
||||
['move', 'zoom', 'rotate', 'pitch'].forEach((eventName) => {
|
||||
map.on(eventName, () => syncMap(map, maps[otherKey]));
|
||||
});
|
||||
}
|
||||
|
||||
async function applyStyle(side) {
|
||||
const style = await fetchStyle(STYLE_URLS[side]);
|
||||
const map = maps[side];
|
||||
const camera = map
|
||||
? {
|
||||
center: map.getCenter(),
|
||||
zoom: map.getZoom(),
|
||||
bearing: map.getBearing(),
|
||||
pitch: map.getPitch()
|
||||
}
|
||||
: initialView;
|
||||
|
||||
if (!map) {
|
||||
maps[side] = new maplibregl.Map({
|
||||
container: side === 'left' ? 'map-left' : 'map-right',
|
||||
style,
|
||||
center: camera.center,
|
||||
zoom: camera.zoom,
|
||||
bearing: camera.bearing,
|
||||
pitch: camera.pitch
|
||||
});
|
||||
maps[side].addControl(new maplibregl.NavigationControl(), 'top-right');
|
||||
attachSync(maps[side], side === 'left' ? 'right' : 'left');
|
||||
} else {
|
||||
map.setStyle(style, { diff: false });
|
||||
map.once('style.load', () => {
|
||||
map.jumpTo(camera);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadStyles() {
|
||||
setStatus('正在加载左右样式…');
|
||||
reloadBtn.disabled = true;
|
||||
try {
|
||||
await Promise.all([applyStyle('left'), applyStyle('right')]);
|
||||
setStatus('样式已加载。拖动任一侧,另一侧会跟随。');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setStatus(error.message || '样式加载失败');
|
||||
} finally {
|
||||
reloadBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
reloadBtn.addEventListener('click', reloadStyles);
|
||||
reloadStyles();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
266
src/Domain/navsea-domain-compatible.html
Normal file
266
src/Domain/navsea-domain-compatible.html
Normal file
@@ -0,0 +1,266 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>NavSea Domain Compatible</title>
|
||||
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--bg: #eef3ef;
|
||||
--panel: rgba(248, 246, 241, 0.95);
|
||||
--ink: #19252c;
|
||||
--muted: #5f6d74;
|
||||
--line: rgba(25, 37, 44, 0.14);
|
||||
--accent: #85501d;
|
||||
--accent-dark: #5f3812;
|
||||
--shadow: 0 18px 36px rgba(25, 37, 44, 0.14);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(133, 80, 29, 0.12), transparent 30%),
|
||||
radial-gradient(circle at bottom right, rgba(11, 108, 143, 0.10), transparent 28%),
|
||||
var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: "Avenir Next", "Segoe UI", "PingFang SC", "Noto Sans SC", sans-serif;
|
||||
}
|
||||
body { display: grid; grid-template-rows: auto 1fr; }
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 14px 18px 12px;
|
||||
background: linear-gradient(180deg, rgba(248, 246, 241, 0.98), rgba(248, 246, 241, 0.88));
|
||||
border-bottom: 1px solid var(--line);
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 10;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.title { font-size: 18px; font-weight: 700; letter-spacing: 0.02em; }
|
||||
.subtitle { margin-top: 4px; font-size: 13px; color: var(--muted); max-width: 980px; }
|
||||
.toolbar-actions { display: flex; gap: 10px; align-items: center; }
|
||||
button {
|
||||
height: 42px;
|
||||
padding: 0 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(95, 56, 18, 0.18);
|
||||
background: linear-gradient(180deg, var(--accent), var(--accent-dark));
|
||||
color: #fff;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 22px rgba(95, 56, 18, 0.18);
|
||||
}
|
||||
.map-shell { position: relative; min-height: 0; }
|
||||
.map { position: absolute; inset: 0; }
|
||||
.pane-tag {
|
||||
position: absolute; top: 12px; left: 12px; z-index: 5; padding: 10px 12px;
|
||||
border-radius: 12px; background: var(--panel);
|
||||
border: 1px solid rgba(25, 37, 44, 0.08);
|
||||
box-shadow: 0 12px 24px rgba(25, 37, 44, 0.12); backdrop-filter: blur(6px);
|
||||
}
|
||||
.pane-tag strong { display: block; font-size: 13px; margin-bottom: 3px; }
|
||||
.pane-tag span { display: block; font-size: 12px; color: var(--muted); }
|
||||
.status {
|
||||
position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
|
||||
z-index: 5; padding: 8px 12px; border-radius: 999px;
|
||||
background: rgba(25, 37, 44, 0.84); color: #f7f9fb; font-size: 12px;
|
||||
letter-spacing: 0.03em; max-width: calc(100% - 28px);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.layer-panel {
|
||||
position: absolute;
|
||||
top: 92px;
|
||||
right: 12px;
|
||||
z-index: 6;
|
||||
width: 340px;
|
||||
max-height: calc(100% - 128px);
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
background: rgba(248, 246, 241, 0.96);
|
||||
border: 1px solid rgba(25, 37, 44, 0.1);
|
||||
box-shadow: 0 12px 24px rgba(25, 37, 44, 0.14);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.layer-panel h3 { margin: 0 0 8px; font-size: 14px; }
|
||||
.layer-panel p { margin: 0 0 10px; font-size: 12px; color: var(--muted); line-height: 1.45; }
|
||||
.layer-actions { display: flex; gap: 8px; margin-bottom: 10px; }
|
||||
.layer-actions button { height: 34px; padding: 0 10px; border-radius: 10px; font-size: 12px; }
|
||||
.layer-list { display: grid; gap: 6px; }
|
||||
.layer-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 7px 8px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.layer-item input { margin-top: 2px; }
|
||||
.layer-id {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
word-break: break-word;
|
||||
}
|
||||
.layer-desc {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
line-height: 1.35;
|
||||
word-break: break-word;
|
||||
}
|
||||
@media (max-width: 920px) {
|
||||
.layer-panel {
|
||||
width: calc(100% - 24px);
|
||||
max-height: 42%;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<div class="title">NavSea Domain Compatible</div>
|
||||
<div class="subtitle">单页体验唐津 10 海里的 Chart Domain 兼容样式。当前样式目标是使用新的 Domain `pbf` 结构,尽量复现旧视觉表达。</div>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<button id="reload-btn" type="button">重新加载</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="map-shell">
|
||||
<div class="pane-tag">
|
||||
<strong>Domain 单图</strong>
|
||||
<span>style.domain-legacy-compatible-karatsu-10nm.json</span>
|
||||
</div>
|
||||
<div id="map" class="map"></div>
|
||||
<aside class="layer-panel">
|
||||
<h3>图层开关</h3>
|
||||
<p>这里列出当前 Domain 兼容样式的全部 `layer id`。你可以逐个开关,查看具体图层的叠加效果。</p>
|
||||
<div class="layer-actions">
|
||||
<button id="layers-all-on" type="button">全开</button>
|
||||
<button id="layers-all-off" type="button">全关</button>
|
||||
</div>
|
||||
<div id="layer-list" class="layer-list">样式加载后显示…</div>
|
||||
</aside>
|
||||
<div id="status" class="status">等待加载样式…</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
|
||||
<script>
|
||||
const STYLE_URL = './style.domain-legacy-compatible-karatsu-10nm.json';
|
||||
const initialView = {
|
||||
center: [129.9697, 33.4425],
|
||||
zoom: 11,
|
||||
pitch: 0,
|
||||
bearing: 0
|
||||
};
|
||||
|
||||
const statusEl = document.getElementById('status');
|
||||
const reloadBtn = document.getElementById('reload-btn');
|
||||
const layerListEl = document.getElementById('layer-list');
|
||||
const layersAllOnBtn = document.getElementById('layers-all-on');
|
||||
const layersAllOffBtn = document.getElementById('layers-all-off');
|
||||
let map = null;
|
||||
|
||||
function setStatus(text) {
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
|
||||
function isToggleableLayer(layer) {
|
||||
return layer && layer.id && layer.type !== 'background';
|
||||
}
|
||||
|
||||
function layerDesc(layer) {
|
||||
const parts = [layer.type || 'unknown'];
|
||||
if (layer.source) parts.push(layer.source);
|
||||
if (layer['source-layer']) parts.push(layer['source-layer']);
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
function buildLayerPanel(mapInstance) {
|
||||
const style = mapInstance.getStyle();
|
||||
const layers = (style.layers || []).filter(isToggleableLayer);
|
||||
layerListEl.innerHTML = '';
|
||||
for (const layer of layers) {
|
||||
const row = document.createElement('label');
|
||||
row.className = 'layer-item';
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.checked = mapInstance.getLayoutProperty(layer.id, 'visibility') !== 'none';
|
||||
checkbox.addEventListener('change', () => {
|
||||
mapInstance.setLayoutProperty(layer.id, 'visibility', checkbox.checked ? 'visible' : 'none');
|
||||
});
|
||||
|
||||
const meta = document.createElement('div');
|
||||
|
||||
const idEl = document.createElement('span');
|
||||
idEl.className = 'layer-id';
|
||||
idEl.textContent = layer.id;
|
||||
|
||||
const descEl = document.createElement('span');
|
||||
descEl.className = 'layer-desc';
|
||||
descEl.textContent = layerDesc(layer);
|
||||
|
||||
meta.appendChild(idEl);
|
||||
meta.appendChild(descEl);
|
||||
row.appendChild(checkbox);
|
||||
row.appendChild(meta);
|
||||
layerListEl.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
function setAllVisibility(visible) {
|
||||
if (!map) return;
|
||||
const layers = (map.getStyle().layers || []).filter(isToggleableLayer);
|
||||
for (const layer of layers) {
|
||||
map.setLayoutProperty(layer.id, 'visibility', visible ? 'visible' : 'none');
|
||||
}
|
||||
buildLayerPanel(map);
|
||||
}
|
||||
|
||||
function createMap() {
|
||||
if (map) map.remove();
|
||||
layerListEl.textContent = '样式加载后显示…';
|
||||
setStatus('正在加载 Domain 兼容样式…');
|
||||
|
||||
map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
style: STYLE_URL,
|
||||
center: initialView.center,
|
||||
zoom: initialView.zoom,
|
||||
pitch: initialView.pitch,
|
||||
bearing: initialView.bearing,
|
||||
hash: false
|
||||
});
|
||||
|
||||
map.addControl(new maplibregl.NavigationControl(), 'top-right');
|
||||
|
||||
map.on('load', () => {
|
||||
buildLayerPanel(map);
|
||||
setStatus('Domain 样式已加载,可以直接体验唐津 10 海里 Chart Domain pbf。');
|
||||
});
|
||||
|
||||
map.on('error', (event) => {
|
||||
const detail = event && event.error ? event.error.message : '未知错误';
|
||||
setStatus(`加载异常:${detail}`);
|
||||
});
|
||||
}
|
||||
|
||||
reloadBtn.addEventListener('click', createMap);
|
||||
layersAllOnBtn.addEventListener('click', () => setAllVisibility(true));
|
||||
layersAllOffBtn.addEventListener('click', () => setAllVisibility(false));
|
||||
|
||||
createMap();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
136
src/Domain/navsea-domain-land-sea.html
Normal file
136
src/Domain/navsea-domain-land-sea.html
Normal file
@@ -0,0 +1,136 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>NavSea Domain Land Sea</title>
|
||||
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--bg: #edf4f3;
|
||||
--panel: rgba(248, 246, 241, 0.95);
|
||||
--ink: #19252c;
|
||||
--muted: #5f6d74;
|
||||
--line: rgba(25, 37, 44, 0.14);
|
||||
--accent: #85501d;
|
||||
--accent-dark: #5f3812;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink); font-family: "Avenir Next", "Segoe UI", "PingFang SC", "Noto Sans SC", sans-serif; }
|
||||
body { display: grid; grid-template-rows: auto 1fr; }
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 14px 18px 12px;
|
||||
background: linear-gradient(180deg, rgba(248, 246, 241, 0.98), rgba(248, 246, 241, 0.9));
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.title { font-size: 18px; font-weight: 700; }
|
||||
.subtitle { margin-top: 4px; font-size: 13px; color: var(--muted); max-width: 980px; }
|
||||
button {
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(95, 56, 18, 0.18);
|
||||
background: linear-gradient(180deg, var(--accent), var(--accent-dark));
|
||||
color: #fff;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
.map-shell { position: relative; min-height: 0; }
|
||||
.map { position: absolute; inset: 0; }
|
||||
.pane-tag, .legend {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: var(--panel);
|
||||
border: 1px solid rgba(25, 37, 44, 0.08);
|
||||
box-shadow: 0 12px 24px rgba(25, 37, 44, 0.12);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.pane-tag { top: 12px; left: 12px; }
|
||||
.legend { top: 12px; right: 12px; width: 290px; }
|
||||
.pane-tag strong, .legend strong { display: block; font-size: 13px; margin-bottom: 4px; }
|
||||
.pane-tag span, .legend p { display: block; font-size: 12px; color: var(--muted); line-height: 1.45; margin: 0; }
|
||||
.legend ul { margin: 8px 0 0; padding-left: 16px; font-size: 12px; color: var(--ink); }
|
||||
.legend li { margin: 4px 0; }
|
||||
.status {
|
||||
position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
|
||||
z-index: 5; padding: 8px 12px; border-radius: 999px;
|
||||
background: rgba(25, 37, 44, 0.84); color: #f7f9fb; font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<div class="title">NavSea Domain Land / Sea View</div>
|
||||
<div class="subtitle">唐津 10 海里 Chart Domain 实验页。这里把 `land_area` 作为陆海二分主判定层:`land_area` 内是陆地,外部背景默认视为海域。</div>
|
||||
</div>
|
||||
<button id="reload-btn" type="button">重新加载</button>
|
||||
</div>
|
||||
|
||||
<div class="map-shell">
|
||||
<div class="pane-tag">
|
||||
<strong>陆海分离单图</strong>
|
||||
<span>使用 `land_area` + 海域背景快速区分陆地与海洋</span>
|
||||
</div>
|
||||
<aside class="legend">
|
||||
<strong>当前表达规则</strong>
|
||||
<p>这是为了快速、稳定地区分陆地和海洋,不是完整海图样式。</p>
|
||||
<ul>
|
||||
<li>海域:浅蓝色背景</li>
|
||||
<li>陆地:`land_area` 米色填充</li>
|
||||
<li>港岸/防波堤:`onshore_structure_*` 与 `baseline_*` 叠线</li>
|
||||
<li>水深:`depth_contour*` 保留作参考</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<div id="map" class="map"></div>
|
||||
<div id="status" class="status">等待加载样式…</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
|
||||
<script>
|
||||
const STYLE_URL = './style.domain-land-sea-karatsu-10nm.json';
|
||||
const initialView = {
|
||||
center: [129.9697, 33.4425],
|
||||
zoom: 11,
|
||||
pitch: 0,
|
||||
bearing: 0
|
||||
};
|
||||
const statusEl = document.getElementById('status');
|
||||
const reloadBtn = document.getElementById('reload-btn');
|
||||
let map = null;
|
||||
|
||||
function setStatus(text) {
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
|
||||
function createMap() {
|
||||
if (map) map.remove();
|
||||
setStatus('正在加载陆海分离样式…');
|
||||
map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
style: STYLE_URL,
|
||||
center: initialView.center,
|
||||
zoom: initialView.zoom,
|
||||
pitch: initialView.pitch,
|
||||
bearing: initialView.bearing
|
||||
});
|
||||
map.addControl(new maplibregl.NavigationControl(), 'top-right');
|
||||
map.on('load', () => setStatus('陆海分离视图已加载。'));
|
||||
map.on('error', (event) => {
|
||||
const detail = event && event.error ? event.error.message : '未知错误';
|
||||
setStatus(`加载异常:${detail}`);
|
||||
});
|
||||
}
|
||||
|
||||
reloadBtn.addEventListener('click', createMap);
|
||||
createMap();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
249
src/Domain/navsea_domain_render_audit.py
Normal file
249
src/Domain/navsea_domain_render_audit.py
Normal file
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run a Domain-aware render audit for legacy-vs-domain-compatible styles.
|
||||
|
||||
This script intentionally does not modify or replace the existing
|
||||
`navsea_render_audit.py` flow. It exists as a separate audit line for the
|
||||
new Domain PBF experiment.
|
||||
|
||||
Why a separate audit is required:
|
||||
- the legacy audit uses tile-instance identity that includes the raw
|
||||
`source-layer` name
|
||||
- the Domain PBF line intentionally renames layers into standardized
|
||||
semantic names
|
||||
- therefore the legacy audit will report false positives such as
|
||||
`missing_in_engineering` + `extra_in_engineering` even when the rendered
|
||||
result is visually identical
|
||||
|
||||
This Domain-aware audit keeps the original render-observation comparison
|
||||
logic, but changes the tile-instance identity to use the legacy/original
|
||||
layer identity (`source_layer_jp` when present). This allows:
|
||||
|
||||
- original source layer `P基本線ククリ`
|
||||
- Domain standardized layer `baseline_outline`
|
||||
|
||||
to be matched as the same rendered object instance when they share the same
|
||||
legacy lineage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
BASE_AUDIT_PATH = Path("/root/sourceserver/pbf/navsea_render_audit.py")
|
||||
DEFAULT_ORIGINAL_STYLE_PATH = Path("/mnt/sda1/www/newpec/style.patched.local.json")
|
||||
DEFAULT_DOMAIN_STYLE_PATH = Path("/mnt/sda1/www/newpec/domain/style.domain-legacy-compatible-karatsu-10nm.json")
|
||||
DEFAULT_ORIGINAL_TILE_ROOT = Path(
|
||||
"/home/wwwroot/newpec/exported_auto/"
|
||||
"tile.mapple-on.jp__newpec-mvt-20260106__z___x___y_.pbf/tiles"
|
||||
)
|
||||
DEFAULT_DOMAIN_TILE_ROOT = Path("/tmp/pbf-domain-karatsu-10nm-merged")
|
||||
DEFAULT_REPORT_MD_PATH = Path("/root/sourceserver/pbf/NavSea_Original_vs_Domain_Compatible_Render_Audit_Karatsu_10nm.md")
|
||||
DEFAULT_REPORT_JSON_PATH = Path("/root/sourceserver/pbf/NavSea_Original_vs_Domain_Compatible_Render_Audit_Karatsu_10nm.json")
|
||||
DEFAULT_AUDIT_NAME = "navsea_original_vs_domain_compatible_karatsu_10nm"
|
||||
MAX_EXAMPLES = 30
|
||||
|
||||
|
||||
def load_base_module():
|
||||
spec = importlib.util.spec_from_file_location("navsea_render_audit_base", BASE_AUDIT_PATH)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Unable to load base audit module: {BASE_AUDIT_PATH}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Audit original vs Domain-compatible render outputs.")
|
||||
parser.add_argument("--audit-name", default=DEFAULT_AUDIT_NAME)
|
||||
parser.add_argument("--original-style", type=Path, default=DEFAULT_ORIGINAL_STYLE_PATH)
|
||||
parser.add_argument("--domain-style", type=Path, default=DEFAULT_DOMAIN_STYLE_PATH)
|
||||
parser.add_argument("--original-tile-root", type=Path, default=DEFAULT_ORIGINAL_TILE_ROOT)
|
||||
parser.add_argument("--domain-tile-root", type=Path, default=DEFAULT_DOMAIN_TILE_ROOT)
|
||||
parser.add_argument("--report-md", type=Path, default=DEFAULT_REPORT_MD_PATH)
|
||||
parser.add_argument("--report-json", type=Path, default=DEFAULT_REPORT_JSON_PATH)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def domain_aware_instance_id(feature: Any) -> str:
|
||||
legacy_layer = feature.properties.get("source_layer_jp") or feature.layer_name
|
||||
return (
|
||||
f"{feature.object_id}|z:{feature.tile_z}|x:{feature.tile_x}|"
|
||||
f"y:{feature.tile_y}|layer:{legacy_layer}"
|
||||
)
|
||||
|
||||
|
||||
def format_component_map(component_map: dict[str, tuple[str, ...]] | dict[str, list[str]]) -> str:
|
||||
if not component_map:
|
||||
return "none"
|
||||
parts = []
|
||||
for key in sorted(component_map):
|
||||
values = component_map[key]
|
||||
parts.append(f"{key}={list(values)}")
|
||||
return "; ".join(parts)
|
||||
|
||||
|
||||
def write_reports(
|
||||
*,
|
||||
original_style_path: Path,
|
||||
domain_style_path: Path,
|
||||
original_tile_root: Path,
|
||||
domain_tile_root: Path,
|
||||
report_md_path: Path,
|
||||
report_json_path: Path,
|
||||
original_count: int,
|
||||
domain_count: int,
|
||||
result_count: int,
|
||||
status_counter: Counter[str],
|
||||
source_layer_counter: Counter[tuple[str, str]],
|
||||
mismatch_examples: list[dict[str, Any]],
|
||||
) -> None:
|
||||
payload = {
|
||||
"original_feature_instances": original_count,
|
||||
"domain_feature_instances": domain_count,
|
||||
"result_count": result_count,
|
||||
"status_counts": dict(status_counter),
|
||||
"source_layer_issue_counts": [
|
||||
{"status": status, "source_layer": source_layer, "count": count}
|
||||
for (status, source_layer), count in source_layer_counter.most_common(30)
|
||||
],
|
||||
"mismatch_examples": mismatch_examples,
|
||||
}
|
||||
report_json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
lines = [
|
||||
"# NavSea 原始版 vs Domain 兼容版渲染审计报告",
|
||||
"",
|
||||
"## 范围",
|
||||
"",
|
||||
f"- 原始样式: `{original_style_path}`",
|
||||
f"- Domain 样式: `{domain_style_path}`",
|
||||
f"- 原始瓦片根目录: `{original_tile_root}`",
|
||||
f"- Domain 瓦片根目录: `{domain_tile_root}`",
|
||||
f"- 原始 feature 实例数: `{original_count}`",
|
||||
f"- Domain feature 实例数: `{domain_count}`",
|
||||
f"- 审计结果数: `{result_count}`",
|
||||
"",
|
||||
"## Domain 口径说明",
|
||||
"",
|
||||
"- 主键优先使用 legacy `fid`。",
|
||||
"- 没有 `fid` 的对象,回退到 `geometry + 稳定旧属性`。",
|
||||
"- 审计粒度保留 tile 实例,因为渲染具有 zoom 敏感性。",
|
||||
"- 与旧审计不同,本脚本按 `source_layer_jp` 对齐实例,不按 Domain 标准层名对齐。",
|
||||
"",
|
||||
"## 结果统计",
|
||||
"",
|
||||
]
|
||||
for status, count in status_counter.most_common():
|
||||
lines.append(f"- `{status}`: `{count}`")
|
||||
|
||||
lines.extend(["", "## 主要问题层", ""])
|
||||
if not source_layer_counter:
|
||||
lines.append("- 没有发现差异。")
|
||||
else:
|
||||
for (status, source_layer), count in source_layer_counter.most_common(20):
|
||||
lines.append(f"- `{status}` | `{source_layer}` | `{count}`")
|
||||
|
||||
lines.extend(["", "## 差异样例", ""])
|
||||
if not mismatch_examples:
|
||||
lines.append("- 没有差异样例。")
|
||||
else:
|
||||
for item in mismatch_examples:
|
||||
lines.append(
|
||||
f"- `{item['status']}` | tile=`{item['tile']}` | source_layer=`{item['source_layer']}` | "
|
||||
f"fid=`{item['fid_legacy']}` | object=`{item['canonical_object_type'] or 'n/a'}`"
|
||||
)
|
||||
lines.append(f" original: {format_component_map(item['original_components'])}")
|
||||
lines.append(f" domain: {format_component_map(item['domain_components'])}")
|
||||
|
||||
report_md_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
base = load_base_module()
|
||||
|
||||
base.feature_instance_id = domain_aware_instance_id
|
||||
|
||||
original_style = base.load_style(args.original_style)
|
||||
domain_style = base.load_style(args.domain_style)
|
||||
domain_tiles = base.iter_tile_paths(args.domain_tile_root)
|
||||
|
||||
original_count = 0
|
||||
domain_count = 0
|
||||
result_count = 0
|
||||
status_counter: Counter[str] = Counter()
|
||||
source_layer_counter: Counter[tuple[str, str]] = Counter()
|
||||
mismatch_examples: list[dict[str, Any]] = []
|
||||
|
||||
for domain_tile in domain_tiles:
|
||||
rel = domain_tile.relative_to(args.domain_tile_root)
|
||||
original_tile = args.original_tile_root / rel
|
||||
if not original_tile.exists():
|
||||
continue
|
||||
|
||||
original_instances = base.decode_tile_instances("original", args.original_tile_root, original_style, original_tile)
|
||||
domain_instances = base.decode_tile_instances("domain", args.domain_tile_root, domain_style, domain_tile)
|
||||
original_count += len(original_instances)
|
||||
domain_count += len(domain_instances)
|
||||
|
||||
all_instance_ids = sorted(set(original_instances) | set(domain_instances))
|
||||
for instance_id in all_instance_ids:
|
||||
item = base.compare_instance(original_instances.get(instance_id), domain_instances.get(instance_id))
|
||||
result_count += 1
|
||||
status_counter[item["status"]] += 1
|
||||
if item["status"] != "exact_match":
|
||||
source_layer_counter[(item["status"], item["source_layer"] or "unknown")] += 1
|
||||
if len(mismatch_examples) < MAX_EXAMPLES:
|
||||
mismatch_examples.append(
|
||||
{
|
||||
"status": item["status"],
|
||||
"tile": item["tile"],
|
||||
"source_layer": item["source_layer"],
|
||||
"fid_legacy": item["fid_legacy"],
|
||||
"canonical_object_type": item["canonical_object_type"],
|
||||
"original_components": item["original_components"],
|
||||
"domain_components": item["engineering_components"],
|
||||
}
|
||||
)
|
||||
|
||||
write_reports(
|
||||
original_style_path=args.original_style,
|
||||
domain_style_path=args.domain_style,
|
||||
original_tile_root=args.original_tile_root,
|
||||
domain_tile_root=args.domain_tile_root,
|
||||
report_md_path=args.report_md,
|
||||
report_json_path=args.report_json,
|
||||
original_count=original_count,
|
||||
domain_count=domain_count,
|
||||
result_count=result_count,
|
||||
status_counter=status_counter,
|
||||
source_layer_counter=source_layer_counter,
|
||||
mismatch_examples=mismatch_examples,
|
||||
)
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"original_feature_instances": original_count,
|
||||
"domain_feature_instances": domain_count,
|
||||
"results": result_count,
|
||||
"report_md": str(args.report_md),
|
||||
"report_json": str(args.report_json),
|
||||
"audit_name": args.audit_name,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
269
src/Domain/navsea_mbtiles_validator.py
Normal file
269
src/Domain/navsea_mbtiles_validator.py
Normal file
@@ -0,0 +1,269 @@
|
||||
"""Validate NavSea MBTiles files against Packaging Spec v1.
|
||||
|
||||
This validator is intentionally standalone and does not affect existing
|
||||
production tile generation or import flows. It can be used from CLI and from
|
||||
other Python modules.
|
||||
|
||||
Current scope follows:
|
||||
- tasks/pbf/MbTiles/navsea_mbtiles_validator_task_zh.md
|
||||
- tasks/pbf/MbTiles/navsea_mbtiles_packaging_spec_task_zh.md
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sqlite3
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ALLOWED_FORMATS = {"pbf", "mvt", "png", "jpg", "jpeg", "webp"}
|
||||
ALLOWED_PACKAGE_TYPES = {"base", "cache", "update"}
|
||||
REQUIRED_METADATA = [
|
||||
"format",
|
||||
"minzoom",
|
||||
"maxzoom",
|
||||
"bounds",
|
||||
"navsea_package_type",
|
||||
"navsea_package_id",
|
||||
"navsea_source_family",
|
||||
"navsea_schema_version",
|
||||
]
|
||||
RECOMMENDED_METADATA = [
|
||||
"name",
|
||||
"description",
|
||||
"navsea_generated_at",
|
||||
"navsea_tile_count",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationIssue:
|
||||
level: str
|
||||
code: str
|
||||
message: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
valid: bool
|
||||
issues: list[ValidationIssue]
|
||||
path: str
|
||||
|
||||
|
||||
def load_metadata(conn: sqlite3.Connection) -> dict[str, str]:
|
||||
cur = conn.execute("SELECT name, value FROM metadata")
|
||||
return {str(name): str(value) for name, value in cur.fetchall()}
|
||||
|
||||
|
||||
def table_exists(conn: sqlite3.Connection, table: str) -> bool:
|
||||
cur = conn.execute(
|
||||
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1",
|
||||
(table,),
|
||||
)
|
||||
return cur.fetchone() is not None
|
||||
|
||||
|
||||
def column_names(conn: sqlite3.Connection, table: str) -> set[str]:
|
||||
cur = conn.execute(f"PRAGMA table_info({table})")
|
||||
return {str(row[1]) for row in cur.fetchall()}
|
||||
|
||||
|
||||
def validate_mbtiles(path: str) -> ValidationResult:
|
||||
issues: list[ValidationIssue] = []
|
||||
db_path = Path(path)
|
||||
|
||||
if not db_path.exists():
|
||||
return ValidationResult(
|
||||
valid=False,
|
||||
issues=[ValidationIssue("error", "file_missing", f"file not found: {db_path}")],
|
||||
path=str(db_path),
|
||||
)
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
if not table_exists(conn, "metadata"):
|
||||
issues.append(ValidationIssue("error", "missing_table", "missing table: metadata"))
|
||||
if not table_exists(conn, "tiles"):
|
||||
issues.append(ValidationIssue("error", "missing_table", "missing table: tiles"))
|
||||
|
||||
if table_exists(conn, "tiles"):
|
||||
cols = column_names(conn, "tiles")
|
||||
for col in ("zoom_level", "tile_column", "tile_row", "tile_data"):
|
||||
if col not in cols:
|
||||
issues.append(ValidationIssue("error", "missing_column", f"tiles missing column: {col}"))
|
||||
|
||||
metadata: dict[str, str] = {}
|
||||
if table_exists(conn, "metadata"):
|
||||
metadata = load_metadata(conn)
|
||||
for key in REQUIRED_METADATA:
|
||||
if key not in metadata or metadata[key] == "":
|
||||
issues.append(ValidationIssue("error", "missing_metadata", f"missing metadata: {key}"))
|
||||
|
||||
fmt = metadata.get("format")
|
||||
if fmt and fmt not in ALLOWED_FORMATS:
|
||||
issues.append(ValidationIssue("error", "invalid_format", f"invalid format: {fmt}"))
|
||||
|
||||
ptype = metadata.get("navsea_package_type")
|
||||
if ptype and ptype not in ALLOWED_PACKAGE_TYPES:
|
||||
issues.append(
|
||||
ValidationIssue("error", "invalid_package_type", f"invalid navsea_package_type: {ptype}")
|
||||
)
|
||||
|
||||
minzoom = metadata.get("minzoom")
|
||||
maxzoom = metadata.get("maxzoom")
|
||||
try:
|
||||
if minzoom is not None and maxzoom is not None and int(minzoom) > int(maxzoom):
|
||||
issues.append(ValidationIssue("error", "invalid_zoom_range", "minzoom > maxzoom"))
|
||||
except ValueError:
|
||||
issues.append(ValidationIssue("error", "invalid_zoom_value", "minzoom/maxzoom must be integers"))
|
||||
|
||||
bounds = metadata.get("bounds")
|
||||
if bounds:
|
||||
try:
|
||||
parts = [float(x) for x in bounds.split(",")]
|
||||
if len(parts) != 4:
|
||||
raise ValueError("bounds must contain 4 numbers")
|
||||
except Exception:
|
||||
issues.append(ValidationIssue("error", "invalid_bounds", f"invalid bounds: {bounds}"))
|
||||
|
||||
for key in RECOMMENDED_METADATA:
|
||||
if key not in metadata or metadata[key] == "":
|
||||
issues.append(ValidationIssue("warning", "missing_metadata", f"missing metadata: {key}"))
|
||||
|
||||
if table_exists(conn, "tiles"):
|
||||
cur = conn.execute("SELECT COUNT(*) FROM tiles WHERE tile_data IS NULL OR length(tile_data)=0")
|
||||
empty_tiles = int(cur.fetchone()[0])
|
||||
if empty_tiles > 0:
|
||||
issues.append(ValidationIssue("error", "empty_tile_data", f"empty tile_data rows: {empty_tiles}"))
|
||||
|
||||
cur = conn.execute(
|
||||
"""
|
||||
SELECT COUNT(*) FROM (
|
||||
SELECT zoom_level, tile_column, tile_row, COUNT(*) AS c
|
||||
FROM tiles
|
||||
GROUP BY zoom_level, tile_column, tile_row
|
||||
HAVING c > 1
|
||||
)
|
||||
"""
|
||||
)
|
||||
duplicates = int(cur.fetchone()[0])
|
||||
if duplicates > 0:
|
||||
issues.append(ValidationIssue("error", "duplicate_tile_key", f"duplicate tile keys: {duplicates}"))
|
||||
|
||||
cur = conn.execute("SELECT COUNT(*) FROM tiles WHERE tile_row < 0")
|
||||
bad_rows = int(cur.fetchone()[0])
|
||||
if bad_rows > 0:
|
||||
issues.append(ValidationIssue("error", "invalid_tile_row", f"negative tile_row rows: {bad_rows}"))
|
||||
|
||||
cur = conn.execute("SELECT COUNT(*) FROM tiles")
|
||||
actual_tile_count = int(cur.fetchone()[0])
|
||||
|
||||
ptype = metadata.get("navsea_package_type")
|
||||
if ptype == "base":
|
||||
if metadata.get("navsea_cache_mutable", "").lower() == "true":
|
||||
issues.append(
|
||||
ValidationIssue("error", "base_mutable_forbidden", "base package must not be marked mutable")
|
||||
)
|
||||
if not metadata.get("navsea_source_family"):
|
||||
issues.append(
|
||||
ValidationIssue("error", "missing_metadata", "base package requires navsea_source_family")
|
||||
)
|
||||
elif ptype == "cache":
|
||||
if metadata.get("navsea_cache_mutable", "").lower() != "true":
|
||||
issues.append(
|
||||
ValidationIssue("error", "cache_mutable_required", "cache package requires navsea_cache_mutable=true")
|
||||
)
|
||||
elif ptype == "update":
|
||||
if not metadata.get("navsea_update_version"):
|
||||
issues.append(
|
||||
ValidationIssue("error", "update_version_required", "update package requires navsea_update_version")
|
||||
)
|
||||
|
||||
expected_tile_count = metadata.get("navsea_tile_count")
|
||||
if expected_tile_count:
|
||||
try:
|
||||
if int(expected_tile_count) != actual_tile_count:
|
||||
issues.append(
|
||||
ValidationIssue(
|
||||
"warning",
|
||||
"tile_count_mismatch",
|
||||
f"navsea_tile_count={expected_tile_count}, actual={actual_tile_count}",
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
issues.append(
|
||||
ValidationIssue("warning", "invalid_tile_count_metadata", f"invalid navsea_tile_count: {expected_tile_count}")
|
||||
)
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
valid = not any(issue.level == "error" for issue in issues)
|
||||
return ValidationResult(valid=valid, issues=issues, path=str(db_path))
|
||||
|
||||
|
||||
def format_cli(result: ValidationResult) -> str:
|
||||
lines = [f"VALID={str(result.valid).lower()} path={result.path}"]
|
||||
for issue in result.issues:
|
||||
level = "ERROR" if issue.level == "error" else "WARN "
|
||||
lines.append(f"[{level}] {issue.code}: {issue.message}")
|
||||
if len(result.issues) == 0:
|
||||
lines.append("[OK ] no issues")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Validate NavSea MBTiles against Packaging Spec v1.")
|
||||
parser.add_argument("path", help="Path to .mbtiles file")
|
||||
parser.add_argument("--report-json", help="Optional JSON report output path")
|
||||
parser.add_argument("--report-md", help="Optional Markdown report output path")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def write_reports(result: ValidationResult, report_json: str | None, report_md: str | None) -> None:
|
||||
if report_json:
|
||||
p = Path(report_json)
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"valid": result.valid,
|
||||
"path": result.path,
|
||||
"issues": [asdict(issue) for issue in result.issues],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
if report_md:
|
||||
p = Path(report_md)
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
lines = [
|
||||
"# NavSea MBTiles Validation Report",
|
||||
"",
|
||||
f"- path: `{result.path}`",
|
||||
f"- valid: `{str(result.valid).lower()}`",
|
||||
f"- issue_count: `{len(result.issues)}`",
|
||||
"",
|
||||
]
|
||||
if not result.issues:
|
||||
lines.append("- No issues found.")
|
||||
else:
|
||||
for issue in result.issues:
|
||||
lines.append(f"- `{issue.level}` `{issue.code}`: {issue.message}")
|
||||
p.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
result = validate_mbtiles(args.path)
|
||||
write_reports(result, args.report_json, args.report_md)
|
||||
print(format_cli(result))
|
||||
raise SystemExit(0 if result.valid else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
275
src/Domain/package_domain_mbtiles.py
Normal file
275
src/Domain/package_domain_mbtiles.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""Package Domain tiles into an MBTiles container without touching production flows.
|
||||
|
||||
This script is intentionally isolated under src/Domain. It is used to:
|
||||
|
||||
1. Read an existing Domain pack tile tree in XYZ layout.
|
||||
2. Optionally filter layers by Chart Domain semantic group.
|
||||
3. Write a standards-compliant MBTiles file following
|
||||
tasks/pbf/MbTiles/navsea_mbtiles_packaging_spec_task_zh.md.
|
||||
|
||||
Current primary use case:
|
||||
- Package Karatsu 10nm `safety_core` content from the Domain `safety` pack
|
||||
into a standalone `base` MBTiles file.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import sqlite3
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
import mapbox_vector_tile
|
||||
|
||||
from chart_domain_model import LAYER_DOMAIN_SPECS, SAFETY_CORE
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TileRecord:
|
||||
z: int
|
||||
x: int
|
||||
y_xyz: int
|
||||
data: bytes
|
||||
|
||||
|
||||
def xyz_to_tms_row(z: int, y_xyz: int) -> int:
|
||||
return (1 << z) - 1 - y_xyz
|
||||
|
||||
|
||||
def tile_lon(x: int, z: int) -> float:
|
||||
return x / (2**z) * 360.0 - 180.0
|
||||
|
||||
|
||||
def tile_lat(y: int, z: int) -> float:
|
||||
n = math.pi - (2.0 * math.pi * y) / (2**z)
|
||||
return math.degrees(math.atan(math.sinh(n)))
|
||||
|
||||
|
||||
def tile_bounds_xyz(z: int, x: int, y: int) -> tuple[float, float, float, float]:
|
||||
min_lon = tile_lon(x, z)
|
||||
max_lon = tile_lon(x + 1, z)
|
||||
max_lat = tile_lat(y, z)
|
||||
min_lat = tile_lat(y + 1, z)
|
||||
return min_lon, min_lat, max_lon, max_lat
|
||||
|
||||
|
||||
def filter_tile_to_safety_core(tile_bytes: bytes) -> bytes | None:
|
||||
decoded = mapbox_vector_tile.decode(tile_bytes)
|
||||
encoded_layers = []
|
||||
per_layer_options: dict[str, dict[str, int]] = {}
|
||||
|
||||
for layer_name, layer_payload in sorted(decoded.items()):
|
||||
spec = LAYER_DOMAIN_SPECS.get(layer_name)
|
||||
if spec is None or spec.chart_domain != SAFETY_CORE:
|
||||
continue
|
||||
features = layer_payload.get("features", [])
|
||||
if not features:
|
||||
continue
|
||||
encoded_layers.append({"name": layer_name, "features": features})
|
||||
extent = layer_payload.get("extent") or layer_payload.get("extents") or 4096
|
||||
per_layer_options[layer_name] = {"extents": int(extent)}
|
||||
|
||||
if not encoded_layers:
|
||||
return None
|
||||
|
||||
return mapbox_vector_tile.encode(encoded_layers, per_layer_options=per_layer_options)
|
||||
|
||||
|
||||
def iter_filtered_tiles(input_root: Path) -> Iterable[TileRecord]:
|
||||
for path in sorted(input_root.rglob("*.pbf")):
|
||||
try:
|
||||
z = int(path.parts[-3])
|
||||
x = int(path.parts[-2])
|
||||
y_xyz = int(path.stem)
|
||||
except ValueError:
|
||||
continue
|
||||
filtered = filter_tile_to_safety_core(path.read_bytes())
|
||||
if filtered is None:
|
||||
continue
|
||||
yield TileRecord(z=z, x=x, y_xyz=y_xyz, data=filtered)
|
||||
|
||||
|
||||
def ensure_schema(conn: sqlite3.Connection) -> None:
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS metadata (name TEXT, value TEXT);
|
||||
CREATE TABLE IF NOT EXISTS tiles (
|
||||
zoom_level INTEGER,
|
||||
tile_column INTEGER,
|
||||
tile_row INTEGER,
|
||||
tile_data BLOB
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS tile_index
|
||||
ON tiles (zoom_level, tile_column, tile_row);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def insert_metadata(conn: sqlite3.Connection, metadata: dict[str, str]) -> None:
|
||||
conn.executemany(
|
||||
"INSERT INTO metadata(name, value) VALUES(?, ?)",
|
||||
list(metadata.items()),
|
||||
)
|
||||
|
||||
|
||||
def build_mbtiles(*, input_root: Path, output_mbtiles: Path, region_id: str) -> dict[str, object]:
|
||||
if output_mbtiles.exists():
|
||||
output_mbtiles.unlink()
|
||||
output_mbtiles.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
conn = sqlite3.connect(output_mbtiles)
|
||||
ensure_schema(conn)
|
||||
|
||||
minzoom: int | None = None
|
||||
maxzoom: int | None = None
|
||||
bounds: tuple[float, float, float, float] | None = None
|
||||
high_zoom_bounds: tuple[float, float, float, float] | None = None
|
||||
tile_count = 0
|
||||
hasher = hashlib.sha256()
|
||||
|
||||
for rec in iter_filtered_tiles(input_root):
|
||||
conn.execute(
|
||||
"INSERT INTO tiles(zoom_level, tile_column, tile_row, tile_data) VALUES(?, ?, ?, ?)",
|
||||
(rec.z, rec.x, xyz_to_tms_row(rec.z, rec.y_xyz), sqlite3.Binary(rec.data)),
|
||||
)
|
||||
tile_count += 1
|
||||
minzoom = rec.z if minzoom is None else min(minzoom, rec.z)
|
||||
prev_maxzoom = maxzoom
|
||||
maxzoom = rec.z if maxzoom is None else max(maxzoom, rec.z)
|
||||
|
||||
b = tile_bounds_xyz(rec.z, rec.x, rec.y_xyz)
|
||||
if bounds is None:
|
||||
bounds = b
|
||||
else:
|
||||
bounds = (
|
||||
min(bounds[0], b[0]),
|
||||
min(bounds[1], b[1]),
|
||||
max(bounds[2], b[2]),
|
||||
max(bounds[3], b[3]),
|
||||
)
|
||||
|
||||
if prev_maxzoom is None or rec.z > prev_maxzoom:
|
||||
high_zoom_bounds = b
|
||||
elif rec.z == prev_maxzoom:
|
||||
if high_zoom_bounds is None:
|
||||
high_zoom_bounds = b
|
||||
else:
|
||||
high_zoom_bounds = (
|
||||
min(high_zoom_bounds[0], b[0]),
|
||||
min(high_zoom_bounds[1], b[1]),
|
||||
max(high_zoom_bounds[2], b[2]),
|
||||
max(high_zoom_bounds[3], b[3]),
|
||||
)
|
||||
|
||||
hasher.update(f"{rec.z}/{rec.x}/{rec.y_xyz}\n".encode("utf-8"))
|
||||
hasher.update(rec.data)
|
||||
|
||||
if tile_count == 0 or minzoom is None or maxzoom is None or bounds is None:
|
||||
conn.close()
|
||||
raise RuntimeError(f"No safety_core tiles found under {input_root}")
|
||||
|
||||
generated_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
||||
package_id = f"navsea.base.{region_id}.safety-core.{timestamp}.{hasher.hexdigest()[:8]}"
|
||||
effective_bounds = high_zoom_bounds or bounds
|
||||
center = f"{(effective_bounds[0]+effective_bounds[2])/2:.6f},{(effective_bounds[1]+effective_bounds[3])/2:.6f},{maxzoom}"
|
||||
metadata = {
|
||||
"format": "pbf",
|
||||
"minzoom": str(minzoom),
|
||||
"maxzoom": str(maxzoom),
|
||||
"bounds": ",".join(f"{v:.6f}" for v in effective_bounds),
|
||||
"center": center,
|
||||
"scheme": "tms",
|
||||
"navsea_package_type": "base",
|
||||
"navsea_package_id": package_id,
|
||||
"navsea_source_family": "navsea.chart.domain.safety-core",
|
||||
"navsea_schema_version": "1",
|
||||
"name": f"NavSea {region_id} safety_core base",
|
||||
"description": f"NavSea Domain safety_core MBTiles package for {region_id}",
|
||||
"type": "overlay",
|
||||
"version": "domain-v1",
|
||||
"navsea_region_id": region_id,
|
||||
"navsea_generated_at": generated_at,
|
||||
"navsea_priority_hint": "10",
|
||||
"navsea_tile_count": str(tile_count),
|
||||
"navsea_data_hash": hasher.hexdigest(),
|
||||
"navsea_projection": "webmercator",
|
||||
"navsea_base_edition": "domain-v1",
|
||||
"navsea_coverage_tier": "harbor-detail",
|
||||
"navsea_is_fallback": "true",
|
||||
}
|
||||
insert_metadata(conn, metadata)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
"input_root": str(input_root),
|
||||
"output_mbtiles": str(output_mbtiles),
|
||||
"tile_count": tile_count,
|
||||
"minzoom": minzoom,
|
||||
"maxzoom": maxzoom,
|
||||
"bounds": metadata["bounds"],
|
||||
"package_id": package_id,
|
||||
"data_hash": metadata["navsea_data_hash"],
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Package Domain safety_core tiles into MBTiles.")
|
||||
parser.add_argument("--input-root", required=True, help="Domain safety pack root in XYZ tile layout.")
|
||||
parser.add_argument("--output-mbtiles", required=True, help="Output MBTiles file path.")
|
||||
parser.add_argument("--region-id", required=True, help="Region identifier, e.g. karatsu-10nm.")
|
||||
parser.add_argument("--report-json", help="Optional output JSON report path.")
|
||||
parser.add_argument("--report-md", help="Optional output Markdown report path.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def write_reports(result: dict[str, object], report_json: Path | None, report_md: Path | None) -> None:
|
||||
if report_json is not None:
|
||||
report_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
report_json.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
if report_md is not None:
|
||||
report_md.parent.mkdir(parents=True, exist_ok=True)
|
||||
lines = [
|
||||
"# NavSea Domain MBTiles Packaging Report",
|
||||
"",
|
||||
f"- input_root: `{result['input_root']}`",
|
||||
f"- output_mbtiles: `{result['output_mbtiles']}`",
|
||||
f"- tile_count: `{result['tile_count']}`",
|
||||
f"- minzoom: `{result['minzoom']}`",
|
||||
f"- maxzoom: `{result['maxzoom']}`",
|
||||
f"- bounds: `{result['bounds']}`",
|
||||
f"- package_id: `{result['package_id']}`",
|
||||
f"- data_hash: `{result['data_hash']}`",
|
||||
"",
|
||||
"## Metadata",
|
||||
"",
|
||||
]
|
||||
for key, value in result["metadata"].items():
|
||||
lines.append(f"- `{key}` = `{value}`")
|
||||
report_md.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
result = build_mbtiles(
|
||||
input_root=Path(args.input_root),
|
||||
output_mbtiles=Path(args.output_mbtiles),
|
||||
region_id=args.region_id,
|
||||
)
|
||||
write_reports(
|
||||
result,
|
||||
Path(args.report_json) if args.report_json else None,
|
||||
Path(args.report_md) if args.report_md else None,
|
||||
)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
54
src/Domain/recode_style_layer_names.py
Normal file
54
src/Domain/recode_style_layer_names.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from style_layer_naming import make_style_layer_id, parse_source_layer_rules, remap_style_layer
|
||||
|
||||
|
||||
def recode_style(path: Path, jp_to_std: dict[str, str]) -> dict[str, int]:
|
||||
style = json.loads(path.read_text(encoding="utf-8"))
|
||||
counts = {"rewritten_ids": 0, "rewritten_source_layers": 0}
|
||||
used_ids: set[str] = set()
|
||||
source_role_counts: dict[tuple[str, str], int] = {}
|
||||
generic_role_counts: dict[tuple[str, str], int] = {}
|
||||
|
||||
for layer in style.get("layers", []):
|
||||
source_layer_std, remapped = remap_style_layer(layer, jp_to_std)
|
||||
if remapped:
|
||||
counts["rewritten_source_layers"] += 1
|
||||
new_id = make_style_layer_id(
|
||||
layer=layer,
|
||||
source_layer_std=source_layer_std,
|
||||
used_ids=used_ids,
|
||||
source_role_counts=source_role_counts,
|
||||
generic_role_counts=generic_role_counts,
|
||||
)
|
||||
if layer.get("id") != new_id:
|
||||
layer["id"] = new_id
|
||||
counts["rewritten_ids"] += 1
|
||||
|
||||
path.write_text(json.dumps(style, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return counts
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Recode style layer ids and source-layers into NavSea standard names.")
|
||||
parser.add_argument("styles", nargs="+", help="Style JSON files to rewrite.")
|
||||
parser.add_argument(
|
||||
"--source-layer-rules",
|
||||
default="/root/sourceserver/pbf/tasks/pbf/mappings/navsea_source_layer_rules_v1.yaml",
|
||||
help="Path to source-layer mapping rules.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
jp_to_std = parse_source_layer_rules(Path(args.source_layer_rules))
|
||||
for style_path in args.styles:
|
||||
path = Path(style_path)
|
||||
counts = recode_style(path, jp_to_std)
|
||||
print(json.dumps({"style": str(path), **counts}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
5652
src/Domain/style.compare-engineering-karatsu-10nm.json
Normal file
5652
src/Domain/style.compare-engineering-karatsu-10nm.json
Normal file
File diff suppressed because it is too large
Load Diff
521
src/Domain/style.domain-karatsu-10nm.json
Normal file
521
src/Domain/style.domain-karatsu-10nm.json
Normal file
@@ -0,0 +1,521 @@
|
||||
{
|
||||
"version": 8,
|
||||
"name": "NavSea Chart Domain Karatsu 10nm",
|
||||
"sprite": "https://tile.mapple-on.jp/newpec-symbols-20251001/sprite",
|
||||
"glyphs": "https://tile.mapple-on.jp/glyphs/{fontstack}/{range}.pbf",
|
||||
"sources": {
|
||||
"mapple": {
|
||||
"type": "raster",
|
||||
"minzoom": 0,
|
||||
"maxzoom": 16,
|
||||
"tileSize": 256,
|
||||
"tiles": [
|
||||
"https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png"
|
||||
],
|
||||
"attribution": "© 昭文社"
|
||||
},
|
||||
"domain_safety": {
|
||||
"type": "vector",
|
||||
"minzoom": 0,
|
||||
"maxzoom": 12,
|
||||
"tiles": [
|
||||
"http://192.168.200.184/pbf-domain-karatsu-10nm/safety/{z}/{x}/{y}.pbf"
|
||||
],
|
||||
"attribution": "© NavSea Domain"
|
||||
},
|
||||
"domain_detail": {
|
||||
"type": "vector",
|
||||
"minzoom": 0,
|
||||
"maxzoom": 12,
|
||||
"tiles": [
|
||||
"http://192.168.200.184/pbf-domain-karatsu-10nm/detail/{z}/{x}/{y}.pbf"
|
||||
],
|
||||
"attribution": "© NavSea Domain"
|
||||
}
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"paint": {
|
||||
"background-color": "#cfe4ea"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "base-raster",
|
||||
"type": "raster",
|
||||
"source": "mapple",
|
||||
"minzoom": 0,
|
||||
"maxzoom": 16,
|
||||
"paint": {
|
||||
"raster-opacity": 0.55,
|
||||
"raster-saturation": -0.2,
|
||||
"raster-contrast": -0.08
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "land-area",
|
||||
"type": "fill",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "land_area",
|
||||
"paint": {
|
||||
"fill-color": "#d7c39e",
|
||||
"fill-opacity": 0.88
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "onshore-structure-area",
|
||||
"type": "fill",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "onshore_structure_area",
|
||||
"paint": {
|
||||
"fill-color": "#8ba38a",
|
||||
"fill-opacity": 0.32
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fixed-fishing-gear-area",
|
||||
"type": "fill",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "fixed_fishing_gear_area",
|
||||
"paint": {
|
||||
"fill-color": "#e36ad6",
|
||||
"fill-opacity": 0.12,
|
||||
"fill-outline-color": "#cf48c4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "anchor-caution-area",
|
||||
"type": "fill",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "anchor_caution_hazard_area",
|
||||
"paint": {
|
||||
"fill-color": "#d86cc8",
|
||||
"fill-opacity": 0.08,
|
||||
"fill-outline-color": "#c94ebb"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bathymetry-line",
|
||||
"type": "line",
|
||||
"source": "domain_detail",
|
||||
"source-layer": "bathymetry_line",
|
||||
"paint": {
|
||||
"line-color": "#b6ccd1",
|
||||
"line-width": 1,
|
||||
"line-opacity": 0.8
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "seabed-line",
|
||||
"type": "line",
|
||||
"source": "domain_detail",
|
||||
"source-layer": "seabed_line",
|
||||
"paint": {
|
||||
"line-color": "#6b6b6b",
|
||||
"line-width": 1,
|
||||
"line-dasharray": [
|
||||
4,
|
||||
2
|
||||
],
|
||||
"line-opacity": 0.75
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "baseline-line",
|
||||
"type": "line",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "baseline_line",
|
||||
"paint": {
|
||||
"line-color": "#7f5531",
|
||||
"line-width": 1.6,
|
||||
"line-opacity": 0.88
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "baseline-outline",
|
||||
"type": "line",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "baseline_outline",
|
||||
"paint": {
|
||||
"line-color": "#7f5531",
|
||||
"line-width": 1,
|
||||
"line-dasharray": [
|
||||
3,
|
||||
2
|
||||
],
|
||||
"line-opacity": 0.9
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "onshore-structure-line",
|
||||
"type": "line",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "onshore_structure_line",
|
||||
"paint": {
|
||||
"line-color": "#617e60",
|
||||
"line-width": 1.4
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bridge-structure-line",
|
||||
"type": "line",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "bridge_structure",
|
||||
"paint": {
|
||||
"line-color": "#616161",
|
||||
"line-width": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "clearance-limit-line",
|
||||
"type": "line",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "clearance_limit_line",
|
||||
"paint": {
|
||||
"line-color": "#c33d3d",
|
||||
"line-width": 1.4,
|
||||
"line-dasharray": [
|
||||
2,
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "hazard-boundary-outline",
|
||||
"type": "line",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "hazard_boundary_outline",
|
||||
"paint": {
|
||||
"line-color": "#a35dbf",
|
||||
"line-width": 1.3,
|
||||
"line-dasharray": [
|
||||
2,
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "anchor-caution-outline",
|
||||
"type": "line",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "anchor_caution_hazard_outline",
|
||||
"paint": {
|
||||
"line-color": "#d04cc1",
|
||||
"line-width": 1.4,
|
||||
"line-dasharray": [
|
||||
3,
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "depth-contour",
|
||||
"type": "line",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "depth_contour",
|
||||
"paint": {
|
||||
"line-color": "#7ed8ae",
|
||||
"line-width": [
|
||||
"interpolate",
|
||||
[
|
||||
"linear"
|
||||
],
|
||||
[
|
||||
"zoom"
|
||||
],
|
||||
7,
|
||||
0.8,
|
||||
12,
|
||||
1.3
|
||||
],
|
||||
"line-opacity": 0.95
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "depth-contour-labels",
|
||||
"type": "symbol",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "depth_contour",
|
||||
"minzoom": 10,
|
||||
"layout": {
|
||||
"symbol-placement": "line",
|
||||
"symbol-spacing": 140,
|
||||
"text-field": [
|
||||
"coalesce",
|
||||
[
|
||||
"get",
|
||||
"chart_label_text"
|
||||
],
|
||||
[
|
||||
"to-string",
|
||||
[
|
||||
"get",
|
||||
"least_depth_m"
|
||||
]
|
||||
]
|
||||
],
|
||||
"text-font": [
|
||||
"Noto Sans Regular"
|
||||
],
|
||||
"text-size": 11
|
||||
},
|
||||
"paint": {
|
||||
"text-color": "#0f171a",
|
||||
"text-halo-color": "#f0f8fa",
|
||||
"text-halo-width": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "navigation-marks-symbol",
|
||||
"type": "symbol",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "navigation_marks",
|
||||
"minzoom": 7,
|
||||
"layout": {
|
||||
"icon-image": [
|
||||
"coalesce",
|
||||
[
|
||||
"get",
|
||||
"chart_icon_image"
|
||||
],
|
||||
"symbol-daytime-301"
|
||||
],
|
||||
"icon-size": 0.95,
|
||||
"icon-allow-overlap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "navigation-marks-label",
|
||||
"type": "symbol",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "navigation_marks",
|
||||
"minzoom": 10,
|
||||
"layout": {
|
||||
"text-field": [
|
||||
"get",
|
||||
"chart_label_text"
|
||||
],
|
||||
"text-font": [
|
||||
"Noto Sans Regular"
|
||||
],
|
||||
"text-size": 13,
|
||||
"text-offset": [
|
||||
0.9,
|
||||
0.1
|
||||
],
|
||||
"text-anchor": "left"
|
||||
},
|
||||
"paint": {
|
||||
"text-color": "#111111",
|
||||
"text-halo-color": "#ffffff",
|
||||
"text-halo-width": 1.4
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "navigation-marks-subtext",
|
||||
"type": "symbol",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "navigation_marks",
|
||||
"minzoom": 11,
|
||||
"layout": {
|
||||
"text-field": [
|
||||
"get",
|
||||
"chart_label_subtext"
|
||||
],
|
||||
"text-font": [
|
||||
"Noto Sans Regular"
|
||||
],
|
||||
"text-size": 11,
|
||||
"text-offset": [
|
||||
1,
|
||||
1.25
|
||||
],
|
||||
"text-anchor": "left"
|
||||
},
|
||||
"paint": {
|
||||
"text-color": [
|
||||
"coalesce",
|
||||
[
|
||||
"get",
|
||||
"chart_text_color"
|
||||
],
|
||||
"#2a2a2a"
|
||||
],
|
||||
"text-halo-color": "#ffffff",
|
||||
"text-halo-width": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "anchor-caution-symbol",
|
||||
"type": "symbol",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "anchor_caution_hazard_point",
|
||||
"minzoom": 9,
|
||||
"layout": {
|
||||
"icon-image": [
|
||||
"coalesce",
|
||||
[
|
||||
"get",
|
||||
"chart_icon_image"
|
||||
],
|
||||
"symbol-daytime-428"
|
||||
],
|
||||
"icon-size": 0.92,
|
||||
"icon-allow-overlap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "navigation-hazard-symbol",
|
||||
"type": "symbol",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "navigation_hazard_point",
|
||||
"minzoom": 9,
|
||||
"layout": {
|
||||
"icon-image": [
|
||||
"coalesce",
|
||||
[
|
||||
"get",
|
||||
"chart_icon_image"
|
||||
],
|
||||
"symbol-daytime-421"
|
||||
],
|
||||
"icon-size": 0.92,
|
||||
"icon-allow-overlap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "facility-boundary-symbol",
|
||||
"type": "symbol",
|
||||
"source": "domain_detail",
|
||||
"source-layer": "facility_boundary_point",
|
||||
"minzoom": 11,
|
||||
"layout": {
|
||||
"icon-image": [
|
||||
"coalesce",
|
||||
[
|
||||
"get",
|
||||
"chart_icon_image"
|
||||
],
|
||||
"symbol-daytime-428"
|
||||
],
|
||||
"icon-size": 0.82,
|
||||
"icon-allow-overlap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "onshore-structure-symbol",
|
||||
"type": "symbol",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "onshore_structure_point",
|
||||
"minzoom": 11,
|
||||
"layout": {
|
||||
"icon-image": [
|
||||
"coalesce",
|
||||
[
|
||||
"get",
|
||||
"chart_icon_image"
|
||||
],
|
||||
"symbol-daytime-428"
|
||||
],
|
||||
"icon-size": 0.82,
|
||||
"icon-allow-overlap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "clearance-limit-label",
|
||||
"type": "symbol",
|
||||
"source": "domain_safety",
|
||||
"source-layer": "clearance_limit_point",
|
||||
"minzoom": 11,
|
||||
"layout": {
|
||||
"text-field": [
|
||||
"coalesce",
|
||||
[
|
||||
"get",
|
||||
"chart_label_text"
|
||||
],
|
||||
[
|
||||
"get",
|
||||
"名称"
|
||||
]
|
||||
],
|
||||
"text-font": [
|
||||
"Noto Sans Regular"
|
||||
],
|
||||
"text-size": 12
|
||||
},
|
||||
"paint": {
|
||||
"text-color": "#c33d3d",
|
||||
"text-halo-color": "#ffffff",
|
||||
"text-halo-width": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "seabed-text",
|
||||
"type": "symbol",
|
||||
"source": "domain_detail",
|
||||
"source-layer": "seabed_text_point",
|
||||
"minzoom": 11,
|
||||
"layout": {
|
||||
"text-field": [
|
||||
"get",
|
||||
"chart_label_text"
|
||||
],
|
||||
"text-font": [
|
||||
"Noto Sans Regular"
|
||||
],
|
||||
"text-size": 12
|
||||
},
|
||||
"paint": {
|
||||
"text-color": "#000000",
|
||||
"text-halo-color": "#ffffff",
|
||||
"text-halo-width": 1.1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "place-labels-sea",
|
||||
"type": "symbol",
|
||||
"source": "domain_detail",
|
||||
"source-layer": "place_label_sea",
|
||||
"minzoom": 10,
|
||||
"layout": {
|
||||
"text-field": [
|
||||
"get",
|
||||
"chart_label_text"
|
||||
],
|
||||
"text-font": [
|
||||
"Noto Sans Regular"
|
||||
],
|
||||
"text-size": 13
|
||||
},
|
||||
"paint": {
|
||||
"text-color": "#111111",
|
||||
"text-halo-color": "#ffffff",
|
||||
"text-halo-width": 1.4
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "place-labels-land",
|
||||
"type": "symbol",
|
||||
"source": "domain_detail",
|
||||
"source-layer": "place_label_land",
|
||||
"minzoom": 10,
|
||||
"layout": {
|
||||
"text-field": [
|
||||
"get",
|
||||
"chart_label_text"
|
||||
],
|
||||
"text-font": [
|
||||
"Noto Sans Regular"
|
||||
],
|
||||
"text-size": 13
|
||||
},
|
||||
"paint": {
|
||||
"text-color": "#3d2b18",
|
||||
"text-halo-color": "#ffffff",
|
||||
"text-halo-width": 1.4
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
5598
src/Domain/style.domain-legacy-compatible-karatsu-10nm.json
Normal file
5598
src/Domain/style.domain-legacy-compatible-karatsu-10nm.json
Normal file
File diff suppressed because it is too large
Load Diff
119
src/Domain/style_layer_naming.py
Normal file
119
src/Domain/style_layer_naming.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from chart_domain_model import get_layer_domain_spec, normalize_layer_std
|
||||
|
||||
|
||||
MANUAL_JP_ALIASES = {
|
||||
"P投錨注意障害物透明": "anchor_caution_hazard_area",
|
||||
"P投錨注意障害物": "anchor_caution_hazard_area",
|
||||
"P航行危険障害物": "navigation_hazard_area",
|
||||
"P錨泊地等": "anchorage_area",
|
||||
"P橋りょう等構造物": "bridge_structure",
|
||||
"L701": "depth_zone_700",
|
||||
"L738": "depth_zone_739",
|
||||
}
|
||||
|
||||
|
||||
def parse_source_layer_rules(path: Path) -> dict[str, str]:
|
||||
mapping: dict[str, str] = {}
|
||||
current_jp: str | None = None
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if line.startswith("- source_layer_jp:"):
|
||||
current_jp = line.split(":", 1)[1].strip()
|
||||
elif line.startswith("source_layer_std:") and current_jp:
|
||||
mapping[current_jp] = line.split(":", 1)[1].strip()
|
||||
current_jp = None
|
||||
return mapping
|
||||
|
||||
|
||||
def resolve_source_layer_std(source_layer: str, jp_to_std: dict[str, str]) -> str:
|
||||
mapped = jp_to_std.get(source_layer, MANUAL_JP_ALIASES.get(source_layer, source_layer))
|
||||
return normalize_layer_std(mapped, source_layer)
|
||||
|
||||
|
||||
def infer_layer_role(layer: dict) -> str:
|
||||
layer_type = str(layer.get("type") or "layer")
|
||||
legacy_id = str(layer.get("id") or "")
|
||||
layout = layer.get("layout") or {}
|
||||
|
||||
if layer_type == "background":
|
||||
return "background"
|
||||
if layer_type == "raster":
|
||||
return "raster"
|
||||
|
||||
if "arc" in legacy_id:
|
||||
return "arc"
|
||||
if "flare" in legacy_id or "フレア" in legacy_id:
|
||||
return "flare"
|
||||
|
||||
has_icon = "icon-image" in layout
|
||||
text_field = layout.get("text-field")
|
||||
text_field_repr = str(text_field)
|
||||
if text_field is not None:
|
||||
if "chart_label_subtext" in text_field_repr or "灯略記" in legacy_id or "abbr" in legacy_id:
|
||||
return "label_subtext"
|
||||
if "chart_label_text" in text_field_repr or "名称" in legacy_id or "地名" in legacy_id:
|
||||
return "label"
|
||||
return "text"
|
||||
if has_icon:
|
||||
return "symbol"
|
||||
return layer_type
|
||||
|
||||
|
||||
def make_style_layer_id(
|
||||
*,
|
||||
layer: dict,
|
||||
source_layer_std: str | None,
|
||||
used_ids: set[str],
|
||||
source_role_counts: dict[tuple[str, str], int],
|
||||
generic_role_counts: dict[tuple[str, str], int],
|
||||
) -> str:
|
||||
role = infer_layer_role(layer)
|
||||
if source_layer_std:
|
||||
key = (source_layer_std, role)
|
||||
source_role_counts[key] = source_role_counts.get(key, 0) + 1
|
||||
seq = source_role_counts[key]
|
||||
candidate = f"{source_layer_std}__{role}"
|
||||
else:
|
||||
source_name = str(layer.get("source") or layer.get("type") or "layer")
|
||||
source_name = re.sub(r"[^a-z0-9]+", "_", source_name.lower()).strip("_") or "layer"
|
||||
key = (source_name, role)
|
||||
generic_role_counts[key] = generic_role_counts.get(key, 0) + 1
|
||||
seq = generic_role_counts[key]
|
||||
candidate = f"{source_name}__{role}"
|
||||
|
||||
if seq > 1:
|
||||
candidate = f"{candidate}__{seq:02d}"
|
||||
|
||||
while candidate in used_ids:
|
||||
seq += 1
|
||||
candidate = f"{candidate}__{seq:02d}"
|
||||
|
||||
used_ids.add(candidate)
|
||||
return candidate
|
||||
|
||||
|
||||
def remap_style_layer(layer: dict, jp_to_std: dict[str, str]) -> tuple[str | None, bool]:
|
||||
source_layer = layer.get("source-layer")
|
||||
if not source_layer:
|
||||
return None, False
|
||||
|
||||
source_name = str(layer.get("source") or "")
|
||||
if source_name not in {
|
||||
"newpec",
|
||||
"navsea",
|
||||
"navsea_delivery",
|
||||
"navsea_engineering",
|
||||
"domain_safety",
|
||||
"domain_detail",
|
||||
}:
|
||||
return None, False
|
||||
|
||||
source_layer_std = resolve_source_layer_std(str(source_layer), jp_to_std)
|
||||
get_layer_domain_spec(source_layer_std)
|
||||
layer["source-layer"] = source_layer_std
|
||||
return source_layer_std, True
|
||||
313
src/pbf/layer-groups.navsea-delivery-kyushu.json
Normal file
313
src/pbf/layer-groups.navsea-delivery-kyushu.json
Normal file
@@ -0,0 +1,313 @@
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"target_style": "style.navsea-delivery-kyushu.json",
|
||||
"target_profile": "kyushu",
|
||||
"notes": [
|
||||
"本文件用于前端图层分组消费,不直接替代 style。",
|
||||
"前端应优先通过 setLayoutProperty(layerId, 'visibility', ...) 控制显示隐藏。",
|
||||
"base_groups 默认不建议暴露给普通用户关闭。"
|
||||
],
|
||||
"base_groups": [
|
||||
{
|
||||
"id": "base_map",
|
||||
"label_key": "pbf.group.base_map",
|
||||
"icon_key": "pbf.icon.base_map",
|
||||
"feature_flag": "pbf.layer_group.base_map",
|
||||
"sort_order": 0,
|
||||
"user_toggleable": false,
|
||||
"default_visible": true,
|
||||
"layer_ids": [
|
||||
"sea-area-fill",
|
||||
"tidal-flat",
|
||||
"river-water",
|
||||
"land-area",
|
||||
"land-hole",
|
||||
"coast-structures-area",
|
||||
"coast-structures-line",
|
||||
"clip-outline"
|
||||
]
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": "safety_hazards",
|
||||
"label_key": "pbf.group.safety_hazards",
|
||||
"icon_key": "pbf.icon.safety_hazards",
|
||||
"feature_flag": "pbf.layer_group.safety_hazards",
|
||||
"sort_order": 10,
|
||||
"user_toggleable": true,
|
||||
"default_visible": true,
|
||||
"layer_ids": [
|
||||
"navigation-hazard-polygons",
|
||||
"navigation-hazard-outline",
|
||||
"hazard-points",
|
||||
"hazard-polygons",
|
||||
"anchor-danger-outline",
|
||||
"anchor-hazard-points",
|
||||
"anchor-hazard-points-428",
|
||||
"submerged-structures",
|
||||
"shoal-danger-area",
|
||||
"unsurveyed-area",
|
||||
"danger-outline"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "navigation_marks",
|
||||
"label_key": "pbf.group.navigation_marks",
|
||||
"icon_key": "pbf.icon.navigation_marks",
|
||||
"feature_flag": "pbf.layer_group.navigation_marks",
|
||||
"sort_order": 20,
|
||||
"user_toggleable": true,
|
||||
"default_visible": true,
|
||||
"layer_ids": [
|
||||
"nav-light-flare",
|
||||
"nav-light-arc",
|
||||
"nav-marks-harbor-lighthouses",
|
||||
"nav-marks-breakwater-lighthouses",
|
||||
"nav-marks-small-lights",
|
||||
"nav-marks-light-beacons",
|
||||
"nav-marks",
|
||||
"nav-mark-labels",
|
||||
"nav-light-abbr-labels"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "anchorage_services",
|
||||
"label_key": "pbf.group.anchorage_services",
|
||||
"icon_key": "pbf.icon.anchorage_services",
|
||||
"feature_flag": "pbf.layer_group.anchorage_services",
|
||||
"sort_order": 30,
|
||||
"user_toggleable": true,
|
||||
"default_visible": true,
|
||||
"layer_ids": [
|
||||
"anchorage-areas",
|
||||
"anchorage-outline",
|
||||
"anchorage-symbols",
|
||||
"pilot-station-points"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "port_facilities",
|
||||
"label_key": "pbf.group.port_facilities",
|
||||
"icon_key": "pbf.icon.port_facilities",
|
||||
"feature_flag": "pbf.layer_group.port_facilities",
|
||||
"sort_order": 40,
|
||||
"user_toggleable": true,
|
||||
"default_visible": true,
|
||||
"layer_ids": [
|
||||
"facility-zones",
|
||||
"facility-zone-outline",
|
||||
"facility-points",
|
||||
"land-structures-area",
|
||||
"land-structures-line",
|
||||
"landmark-points",
|
||||
"landmark-point-fallback"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bridge_clearance",
|
||||
"label_key": "pbf.group.bridge_clearance",
|
||||
"icon_key": "pbf.icon.bridge_clearance",
|
||||
"feature_flag": "pbf.layer_group.bridge_clearance",
|
||||
"sort_order": 50,
|
||||
"user_toggleable": true,
|
||||
"default_visible": true,
|
||||
"layer_ids": [
|
||||
"bridge-area",
|
||||
"bridge-outline",
|
||||
"height-limit-line",
|
||||
"height-limit-points",
|
||||
"height-limit-name-labels",
|
||||
"height-limit-value-labels"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "depth_contours",
|
||||
"label_key": "pbf.group.depth_contours",
|
||||
"icon_key": "pbf.icon.depth_contours",
|
||||
"feature_flag": "pbf.layer_group.depth_contours",
|
||||
"sort_order": 60,
|
||||
"user_toggleable": true,
|
||||
"default_visible": true,
|
||||
"layer_ids": [
|
||||
"depth-contours",
|
||||
"depth-contours-major",
|
||||
"depth-contour-labels",
|
||||
"depth-contours-major-labels"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bathymetry_detail",
|
||||
"label_key": "pbf.group.bathymetry_detail",
|
||||
"icon_key": "pbf.icon.bathymetry_detail",
|
||||
"feature_flag": "pbf.layer_group.bathymetry_detail",
|
||||
"sort_order": 70,
|
||||
"user_toggleable": true,
|
||||
"default_visible": false,
|
||||
"layer_ids": [
|
||||
"bathymetry-support",
|
||||
"bathymetry-depth-labels",
|
||||
"water-boundary",
|
||||
"regulatory-boundary",
|
||||
"special-pattern-line-754"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "fishery_gear",
|
||||
"label_key": "pbf.group.fishery_gear",
|
||||
"icon_key": "pbf.icon.fishery_gear",
|
||||
"feature_flag": "pbf.layer_group.fishery_gear",
|
||||
"sort_order": 80,
|
||||
"user_toggleable": true,
|
||||
"default_visible": true,
|
||||
"layer_ids": [
|
||||
"fishery-areas",
|
||||
"fishery-areas-outline"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "seabed_material",
|
||||
"label_key": "pbf.group.seabed_material",
|
||||
"icon_key": "pbf.icon.seabed_material",
|
||||
"feature_flag": "pbf.layer_group.seabed_material",
|
||||
"sort_order": 90,
|
||||
"user_toggleable": true,
|
||||
"default_visible": false,
|
||||
"layer_ids": [
|
||||
"bottom-material-labels",
|
||||
"subsea-cables"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "place_labels",
|
||||
"label_key": "pbf.group.place_labels",
|
||||
"icon_key": "pbf.icon.place_labels",
|
||||
"feature_flag": "pbf.layer_group.place_labels",
|
||||
"sort_order": 100,
|
||||
"user_toggleable": true,
|
||||
"default_visible": true,
|
||||
"layer_ids": [
|
||||
"place-labels-sea-en",
|
||||
"place-labels-sea-ja",
|
||||
"place-labels-land-en",
|
||||
"place-labels-land-ja"
|
||||
]
|
||||
}
|
||||
],
|
||||
"presets": [
|
||||
{
|
||||
"id": "navigation",
|
||||
"label_key": "pbf.preset.navigation",
|
||||
"description_key": "pbf.preset.navigation.description",
|
||||
"icon_key": "pbf.icon.preset.navigation",
|
||||
"feature_flag": "pbf.preset.navigation",
|
||||
"sort_order": 10,
|
||||
"visible_group_ids": [
|
||||
"base_map",
|
||||
"safety_hazards",
|
||||
"navigation_marks",
|
||||
"anchorage_services",
|
||||
"port_facilities",
|
||||
"bridge_clearance",
|
||||
"depth_contours",
|
||||
"fishery_gear",
|
||||
"place_labels"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "fishing",
|
||||
"label_key": "pbf.preset.fishing",
|
||||
"description_key": "pbf.preset.fishing.description",
|
||||
"icon_key": "pbf.icon.preset.fishing",
|
||||
"feature_flag": "pbf.preset.fishing",
|
||||
"sort_order": 20,
|
||||
"visible_group_ids": [
|
||||
"base_map",
|
||||
"safety_hazards",
|
||||
"navigation_marks",
|
||||
"depth_contours",
|
||||
"bathymetry_detail",
|
||||
"fishery_gear",
|
||||
"seabed_material",
|
||||
"place_labels"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "custom",
|
||||
"label_key": "pbf.preset.custom",
|
||||
"description_key": "pbf.preset.custom.description",
|
||||
"icon_key": "pbf.icon.preset.custom",
|
||||
"feature_flag": "pbf.preset.custom",
|
||||
"sort_order": 30,
|
||||
"visible_group_ids": []
|
||||
}
|
||||
],
|
||||
"i18n": {
|
||||
"zh-CN": {
|
||||
"pbf.group.base_map": "基础底图",
|
||||
"pbf.group.safety_hazards": "鱼礁/海底危险物",
|
||||
"pbf.group.navigation_marks": "海上标识",
|
||||
"pbf.group.anchorage_services": "锚地/引航",
|
||||
"pbf.group.port_facilities": "港口与设施",
|
||||
"pbf.group.bridge_clearance": "桥梁/净空",
|
||||
"pbf.group.depth_contours": "等深线",
|
||||
"pbf.group.bathymetry_detail": "海底地形",
|
||||
"pbf.group.fishery_gear": "渔网/渔具",
|
||||
"pbf.group.seabed_material": "海底质",
|
||||
"pbf.group.place_labels": "地名",
|
||||
"pbf.icon.base_map": "map",
|
||||
"pbf.icon.safety_hazards": "hazard",
|
||||
"pbf.icon.navigation_marks": "buoy",
|
||||
"pbf.icon.anchorage_services": "anchor",
|
||||
"pbf.icon.port_facilities": "harbor",
|
||||
"pbf.icon.bridge_clearance": "bridge",
|
||||
"pbf.icon.depth_contours": "contour",
|
||||
"pbf.icon.bathymetry_detail": "bathymetry",
|
||||
"pbf.icon.fishery_gear": "net",
|
||||
"pbf.icon.seabed_material": "seabed",
|
||||
"pbf.icon.place_labels": "label",
|
||||
"pbf.preset.navigation": "航海模式",
|
||||
"pbf.preset.navigation.description": "危险物、海上标识、锚地、净空、设施优先,海底细节降权。",
|
||||
"pbf.icon.preset.navigation": "compass",
|
||||
"pbf.preset.fishing": "钓鱼模式",
|
||||
"pbf.preset.fishing.description": "等深线、海底地形、底质、鱼礁优先,保留必要安全信息。",
|
||||
"pbf.icon.preset.fishing": "fish",
|
||||
"pbf.preset.custom": "自定义",
|
||||
"pbf.preset.custom.description": "以前端当前用户选择为准,不强制覆盖。",
|
||||
"pbf.icon.preset.custom": "sliders"
|
||||
},
|
||||
"en-US": {
|
||||
"pbf.group.base_map": "Base Map",
|
||||
"pbf.group.safety_hazards": "Reefs / Seabed Hazards",
|
||||
"pbf.group.navigation_marks": "Marine Marks",
|
||||
"pbf.group.anchorage_services": "Anchorage / Pilot",
|
||||
"pbf.group.port_facilities": "Ports / Facilities",
|
||||
"pbf.group.bridge_clearance": "Bridges / Clearance",
|
||||
"pbf.group.depth_contours": "Depth Contours",
|
||||
"pbf.group.bathymetry_detail": "Bathymetry",
|
||||
"pbf.group.fishery_gear": "Fishing Gear",
|
||||
"pbf.group.seabed_material": "Seabed Material",
|
||||
"pbf.group.place_labels": "Place Labels",
|
||||
"pbf.icon.base_map": "map",
|
||||
"pbf.icon.safety_hazards": "hazard",
|
||||
"pbf.icon.navigation_marks": "buoy",
|
||||
"pbf.icon.anchorage_services": "anchor",
|
||||
"pbf.icon.port_facilities": "harbor",
|
||||
"pbf.icon.bridge_clearance": "bridge",
|
||||
"pbf.icon.depth_contours": "contour",
|
||||
"pbf.icon.bathymetry_detail": "bathymetry",
|
||||
"pbf.icon.fishery_gear": "net",
|
||||
"pbf.icon.seabed_material": "seabed",
|
||||
"pbf.icon.place_labels": "label",
|
||||
"pbf.preset.navigation": "Navigation Mode",
|
||||
"pbf.preset.navigation.description": "Prioritize hazards, marine marks, anchorage, clearance, and facilities while de-emphasizing fine seabed detail.",
|
||||
"pbf.icon.preset.navigation": "compass",
|
||||
"pbf.preset.fishing": "Fishing Mode",
|
||||
"pbf.preset.fishing.description": "Prioritize contours, bathymetry, seabed material, and reefs while retaining essential safety information.",
|
||||
"pbf.icon.preset.fishing": "fish",
|
||||
"pbf.preset.custom": "Custom",
|
||||
"pbf.preset.custom.description": "Use the current user-selected visibility without forcing a preset.",
|
||||
"pbf.icon.preset.custom": "sliders"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,7 +323,7 @@
|
||||
|
||||
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
|
||||
<script>
|
||||
const HTML_VERSION = "compare-r19-20260402-1203";
|
||||
const HTML_VERSION = "compare-r23-20260405-1533";
|
||||
const ORIGINAL_STYLE_URL = "./style.json";
|
||||
const ORIGINAL_TILE_URL = "http://192.168.200.184/newpec/exported_auto/tile.mapple-on.jp__newpec-mvt-20260106__z___x___y_.pbf/tiles/{z}/{x}/{y}.pbf";
|
||||
const PROFILES = {
|
||||
@@ -353,7 +353,7 @@
|
||||
deliveryPaneLabel: "正式 delivery style + 九州 PBF",
|
||||
deliveryStyleUrl: "./domain/style.navsea-delivery-kyushu.json",
|
||||
deliveryTileUrl: "http://192.168.200.184/pbf-delivery-kyushu-reencoded/{z}/{x}/{y}.pbf",
|
||||
deliveryStyleVersion: "kyushu-style-r1-20260331-2205",
|
||||
deliveryStyleVersion: "kyushu-style-r5-20260405-1533",
|
||||
deliveryPbfVersion: "kyushu-pbf-reencoded-20260325-1105",
|
||||
deliveryStyleFile: "style.navsea-delivery-kyushu.json",
|
||||
deliveryPbfName: "pbf-delivery-kyushu-reencoded",
|
||||
|
||||
269
src/pbf/navsea-final-delivery-full.html
Normal file
269
src/pbf/navsea-final-delivery-full.html
Normal file
@@ -0,0 +1,269 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>NavSea Final Delivery Full</title>
|
||||
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--bg: #edf2ef;
|
||||
--panel: rgba(248, 246, 241, 0.95);
|
||||
--ink: #18232b;
|
||||
--muted: #617078;
|
||||
--line: rgba(24, 35, 43, 0.12);
|
||||
--accent: #0b5f79;
|
||||
--accent-dark: #084454;
|
||||
--shadow: 0 16px 32px rgba(24, 35, 43, 0.15);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(11, 95, 121, 0.12), transparent 30%),
|
||||
radial-gradient(circle at bottom right, rgba(185, 133, 68, 0.10), transparent 26%),
|
||||
var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: "Avenir Next", "Segoe UI", "PingFang SC", "Noto Sans SC", sans-serif;
|
||||
}
|
||||
|
||||
body { display: grid; grid-template-rows: auto 1fr; }
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 18px;
|
||||
padding: 14px 18px 12px;
|
||||
background: linear-gradient(180deg, rgba(248, 246, 241, 0.98), rgba(248, 246, 241, 0.9));
|
||||
border-bottom: 1px solid var(--line);
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 10;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.title { font-size: 18px; font-weight: 700; letter-spacing: 0.02em; }
|
||||
.subtitle { margin-top: 4px; font-size: 13px; color: var(--muted); max-width: 920px; line-height: 1.5; }
|
||||
.toolbar-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
button {
|
||||
height: 40px;
|
||||
padding: 0 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(8, 68, 84, 0.18);
|
||||
background: linear-gradient(180deg, var(--accent), var(--accent-dark));
|
||||
color: #fff;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 22px rgba(8, 68, 84, 0.18);
|
||||
}
|
||||
.layout { position: relative; min-height: 0; }
|
||||
.map { position: absolute; inset: 0; }
|
||||
.map-tag {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 5;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: var(--panel);
|
||||
border: 1px solid rgba(24, 35, 43, 0.08);
|
||||
box-shadow: 0 12px 24px rgba(24, 35, 43, 0.12);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.map-tag strong { display: block; font-size: 13px; margin-bottom: 3px; }
|
||||
.map-tag span { display: block; font-size: 12px; color: var(--muted); }
|
||||
.inspect-panel {
|
||||
position: fixed;
|
||||
top: 86px;
|
||||
right: 14px;
|
||||
z-index: 30;
|
||||
width: min(340px, calc(100vw - 28px));
|
||||
max-height: calc(100vh - 150px);
|
||||
overflow: auto;
|
||||
padding: 10px 11px;
|
||||
border-radius: 14px;
|
||||
background: rgba(248, 246, 241, 0.94);
|
||||
border: 1px solid rgba(24, 35, 43, 0.12);
|
||||
box-shadow: 0 14px 28px rgba(24, 35, 43, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.inspect-panel h3 { margin: 0 0 4px; font-size: 13px; }
|
||||
.inspect-hint { margin: 0 0 8px; color: var(--muted); font-size: 11px; line-height: 1.4; }
|
||||
.inspect-meta {
|
||||
display: grid;
|
||||
grid-template-columns: 92px 1fr;
|
||||
gap: 4px 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.inspect-meta dt { color: var(--muted); font-weight: 700; }
|
||||
.inspect-meta dd { margin: 0; word-break: break-word; }
|
||||
.inspect-json {
|
||||
margin: 0;
|
||||
padding: 9px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(24, 35, 43, 0.92);
|
||||
color: #eef3f6;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.status {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 14px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 30;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(24, 35, 43, 0.84);
|
||||
color: #f7f9fb;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.03em;
|
||||
max-width: calc(100% - 28px);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@media (max-width: 920px) {
|
||||
.inspect-panel {
|
||||
top: auto;
|
||||
right: 12px;
|
||||
bottom: 54px;
|
||||
width: min(320px, calc(100vw - 24px));
|
||||
max-height: 32vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<div class="title">NavSea 最终版 Delivery · 全国候选</div>
|
||||
<div class="subtitle">当前页面固定加载全国候选 delivery 样式 <code>style.navsea-delivery-full.json</code>。点击任意对象可查看对象属性和点击经纬度。</div>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<button id="reload-btn" type="button">重新加载</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<div class="map-tag">
|
||||
<strong>全国候选</strong>
|
||||
<span>Delivery PBF · Full 20260408</span>
|
||||
</div>
|
||||
<div id="map" class="map"></div>
|
||||
</div>
|
||||
|
||||
<aside class="inspect-panel">
|
||||
<h3>点击查询</h3>
|
||||
<p id="inspect-hint" class="inspect-hint">点击地图对象,查看点击坐标、source、source-layer、geometry 和 properties。</p>
|
||||
<dl class="inspect-meta">
|
||||
<dt>点击坐标</dt>
|
||||
<dd id="inspect-lnglat">-</dd>
|
||||
<dt>source</dt>
|
||||
<dd id="inspect-source">-</dd>
|
||||
<dt>source-layer</dt>
|
||||
<dd id="inspect-layer">-</dd>
|
||||
<dt>geometry</dt>
|
||||
<dd id="inspect-geometry">-</dd>
|
||||
<dt>render layer</dt>
|
||||
<dd id="inspect-render-layer">-</dd>
|
||||
</dl>
|
||||
<pre id="inspect-json" class="inspect-json">{
|
||||
"message": "等待点击对象"
|
||||
}</pre>
|
||||
</aside>
|
||||
|
||||
<div id="status" class="status">等待加载样式…</div>
|
||||
|
||||
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
|
||||
<script>
|
||||
const STYLE_URL = "./domain/style.navsea-delivery-full.json";
|
||||
const INITIAL_VIEW = {
|
||||
center: [137.0, 35.8],
|
||||
zoom: 5,
|
||||
pitch: 0,
|
||||
bearing: 0
|
||||
};
|
||||
|
||||
const statusEl = document.getElementById("status");
|
||||
const reloadBtn = document.getElementById("reload-btn");
|
||||
const inspectHintEl = document.getElementById("inspect-hint");
|
||||
const inspectLngLatEl = document.getElementById("inspect-lnglat");
|
||||
const inspectSourceEl = document.getElementById("inspect-source");
|
||||
const inspectLayerEl = document.getElementById("inspect-layer");
|
||||
const inspectGeometryEl = document.getElementById("inspect-geometry");
|
||||
const inspectRenderLayerEl = document.getElementById("inspect-render-layer");
|
||||
const inspectJsonEl = document.getElementById("inspect-json");
|
||||
|
||||
function setStatus(text) {
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
|
||||
function updateInspect({ lngLat, feature, layerId }) {
|
||||
inspectHintEl.textContent = "已命中对象。继续点击可查看其他对象。";
|
||||
inspectLngLatEl.textContent = `${lngLat.lng.toFixed(6)}, ${lngLat.lat.toFixed(6)}`;
|
||||
inspectSourceEl.textContent = feature.source || "-";
|
||||
inspectLayerEl.textContent = feature.sourceLayer || "-";
|
||||
inspectGeometryEl.textContent = feature.geometry?.type || "-";
|
||||
inspectRenderLayerEl.textContent = layerId || "-";
|
||||
inspectJsonEl.textContent = JSON.stringify(feature.properties || {}, null, 2);
|
||||
}
|
||||
|
||||
function loadMap() {
|
||||
setStatus("正在加载全国候选 delivery 样式…");
|
||||
const map = new maplibregl.Map({
|
||||
container: "map",
|
||||
style: `${STYLE_URL}?v=${Date.now()}`,
|
||||
center: INITIAL_VIEW.center,
|
||||
zoom: INITIAL_VIEW.zoom,
|
||||
pitch: INITIAL_VIEW.pitch,
|
||||
bearing: INITIAL_VIEW.bearing,
|
||||
hash: true
|
||||
});
|
||||
|
||||
map.addControl(new maplibregl.NavigationControl({ visualizePitch: true }), "top-left");
|
||||
|
||||
map.on("load", () => {
|
||||
setStatus("全国候选 delivery 已加载,可直接缩放、拖动、点击查询。");
|
||||
});
|
||||
|
||||
map.on("error", (event) => {
|
||||
const message = event?.error?.message || "样式或瓦片加载失败";
|
||||
setStatus(`加载失败:${message}`);
|
||||
});
|
||||
|
||||
map.on("click", (event) => {
|
||||
const features = map.queryRenderedFeatures(event.point);
|
||||
if (!features.length) {
|
||||
inspectHintEl.textContent = "当前点击位置没有命中对象。";
|
||||
inspectLngLatEl.textContent = `${event.lngLat.lng.toFixed(6)}, ${event.lngLat.lat.toFixed(6)}`;
|
||||
inspectSourceEl.textContent = "-";
|
||||
inspectLayerEl.textContent = "-";
|
||||
inspectGeometryEl.textContent = "-";
|
||||
inspectRenderLayerEl.textContent = "-";
|
||||
inspectJsonEl.textContent = JSON.stringify({ message: "当前点击位置没有命中对象" }, null, 2);
|
||||
return;
|
||||
}
|
||||
updateInspect({
|
||||
lngLat: event.lngLat,
|
||||
feature: features[0],
|
||||
layerId: features[0].layer?.id || "-"
|
||||
});
|
||||
});
|
||||
|
||||
reloadBtn.addEventListener("click", () => {
|
||||
setStatus("正在重新加载全国候选 delivery 样式…");
|
||||
map.setStyle(`${STYLE_URL}?v=${Date.now()}`);
|
||||
});
|
||||
}
|
||||
|
||||
loadMap();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
384
src/pbf/navsea-final-delivery-karatsu-10nm.html
Normal file
384
src/pbf/navsea-final-delivery-karatsu-10nm.html
Normal file
@@ -0,0 +1,384 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>NavSea Final Delivery Karatsu 10nm</title>
|
||||
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--bg: #edf2ef;
|
||||
--panel: rgba(248, 246, 241, 0.95);
|
||||
--ink: #18232b;
|
||||
--muted: #617078;
|
||||
--line: rgba(24, 35, 43, 0.12);
|
||||
--accent: #0b5f79;
|
||||
--accent-dark: #084454;
|
||||
--shadow: 0 16px 32px rgba(24, 35, 43, 0.15);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(11, 95, 121, 0.12), transparent 30%),
|
||||
radial-gradient(circle at bottom right, rgba(185, 133, 68, 0.10), transparent 26%),
|
||||
var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: "Avenir Next", "Segoe UI", "PingFang SC", "Noto Sans SC", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 18px;
|
||||
padding: 14px 18px 12px;
|
||||
background: linear-gradient(180deg, rgba(248, 246, 241, 0.98), rgba(248, 246, 241, 0.9));
|
||||
border-bottom: 1px solid var(--line);
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 10;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
max-width: 920px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 40px;
|
||||
padding: 0 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(8, 68, 84, 0.18);
|
||||
background: linear-gradient(180deg, var(--accent), var(--accent-dark));
|
||||
color: #fff;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 22px rgba(8, 68, 84, 0.18);
|
||||
}
|
||||
|
||||
.layout {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.map {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.map-tag {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 5;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: var(--panel);
|
||||
border: 1px solid rgba(24, 35, 43, 0.08);
|
||||
box-shadow: 0 12px 24px rgba(24, 35, 43, 0.12);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.map-tag strong {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.map-tag span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.inspect-panel {
|
||||
position: fixed;
|
||||
top: 86px;
|
||||
right: 14px;
|
||||
z-index: 30;
|
||||
width: min(340px, calc(100vw - 28px));
|
||||
max-height: calc(100vh - 150px);
|
||||
overflow: auto;
|
||||
padding: 10px 11px;
|
||||
border-radius: 14px;
|
||||
background: rgba(248, 246, 241, 0.94);
|
||||
border: 1px solid rgba(24, 35, 43, 0.12);
|
||||
box-shadow: 0 14px 28px rgba(24, 35, 43, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.inspect-panel h3 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.inspect-hint {
|
||||
margin: 0 0 8px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.inspect-meta {
|
||||
display: grid;
|
||||
grid-template-columns: 92px 1fr;
|
||||
gap: 4px 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.inspect-meta dt {
|
||||
color: var(--muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.inspect-meta dd {
|
||||
margin: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.inspect-json {
|
||||
margin: 0;
|
||||
padding: 9px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(24, 35, 43, 0.92);
|
||||
color: #eef3f6;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.status {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 14px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 30;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(24, 35, 43, 0.84);
|
||||
color: #f7f9fb;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.03em;
|
||||
max-width: calc(100% - 28px);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.inspect-panel {
|
||||
top: auto;
|
||||
right: 12px;
|
||||
bottom: 54px;
|
||||
width: min(320px, calc(100vw - 24px));
|
||||
max-height: 32vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<div class="title">NavSea 最终版 Delivery · 唐津 10 海里</div>
|
||||
<div class="subtitle">当前页面固定加载最终交付样式 <code>style.navsea-delivery-karatsu-10nm.json</code>。点击任意对象可查看对象属性和点击经纬度,适合最终验收和问题定位。</div>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<button id="reload-btn" type="button">重新加载</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<div class="map-tag">
|
||||
<strong>最终版</strong>
|
||||
<span>Delivery PBF · Reencoded FID</span>
|
||||
</div>
|
||||
<div id="map" class="map"></div>
|
||||
</div>
|
||||
|
||||
<aside class="inspect-panel">
|
||||
<h3>点击查询</h3>
|
||||
<p id="inspect-hint" class="inspect-hint">点击地图对象,查看点击坐标、source、source-layer、geometry 和 properties。</p>
|
||||
<dl class="inspect-meta">
|
||||
<dt>点击坐标</dt>
|
||||
<dd id="inspect-lnglat">-</dd>
|
||||
<dt>source</dt>
|
||||
<dd id="inspect-source">-</dd>
|
||||
<dt>source-layer</dt>
|
||||
<dd id="inspect-layer">-</dd>
|
||||
<dt>geometry</dt>
|
||||
<dd id="inspect-geometry">-</dd>
|
||||
<dt>render layer</dt>
|
||||
<dd id="inspect-render-layer">-</dd>
|
||||
</dl>
|
||||
<pre id="inspect-json" class="inspect-json">{
|
||||
"message": "等待点击对象"
|
||||
}</pre>
|
||||
</aside>
|
||||
|
||||
<div id="status" class="status">等待加载样式…</div>
|
||||
|
||||
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
|
||||
<script>
|
||||
const STYLE_URL = "./domain/style.navsea-delivery-karatsu-10nm.json";
|
||||
const INITIAL_VIEW = {
|
||||
center: [129.9697, 33.4425],
|
||||
zoom: 11,
|
||||
pitch: 0,
|
||||
bearing: 0
|
||||
};
|
||||
|
||||
const statusEl = document.getElementById("status");
|
||||
const reloadBtn = document.getElementById("reload-btn");
|
||||
const inspectHintEl = document.getElementById("inspect-hint");
|
||||
const inspectLngLatEl = document.getElementById("inspect-lnglat");
|
||||
const inspectSourceEl = document.getElementById("inspect-source");
|
||||
const inspectLayerEl = document.getElementById("inspect-layer");
|
||||
const inspectGeometryEl = document.getElementById("inspect-geometry");
|
||||
const inspectRenderLayerEl = document.getElementById("inspect-render-layer");
|
||||
const inspectJsonEl = document.getElementById("inspect-json");
|
||||
let map;
|
||||
let styleLoadVersion = Date.now().toString();
|
||||
|
||||
function setStatus(text) {
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
|
||||
function withCacheBuster(url, version) {
|
||||
const separator = url.includes("?") ? "&" : "?";
|
||||
return `${url}${separator}v=${version}`;
|
||||
}
|
||||
|
||||
async function fetchStyle(url) {
|
||||
const response = await fetch(withCacheBuster(url, styleLoadVersion), { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`加载样式失败: ${url}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function applySourceCacheBuster(style, version) {
|
||||
if (style.sprite) {
|
||||
style.sprite = withCacheBuster(style.sprite, version);
|
||||
}
|
||||
if (style.glyphs) {
|
||||
style.glyphs = withCacheBuster(style.glyphs, version);
|
||||
}
|
||||
const sources = style.sources || {};
|
||||
Object.values(sources).forEach((source) => {
|
||||
if (Array.isArray(source.tiles)) {
|
||||
source.tiles = source.tiles.map((tileUrl) => withCacheBuster(tileUrl, version));
|
||||
}
|
||||
if (typeof source.url === "string") {
|
||||
source.url = withCacheBuster(source.url, version);
|
||||
}
|
||||
});
|
||||
return style;
|
||||
}
|
||||
|
||||
function resetInspectPanel(message) {
|
||||
inspectHintEl.textContent = message;
|
||||
inspectLngLatEl.textContent = "-";
|
||||
inspectSourceEl.textContent = "-";
|
||||
inspectLayerEl.textContent = "-";
|
||||
inspectGeometryEl.textContent = "-";
|
||||
inspectRenderLayerEl.textContent = "-";
|
||||
inspectJsonEl.textContent = JSON.stringify({ message }, null, 2);
|
||||
}
|
||||
|
||||
function formatLngLat(lngLat) {
|
||||
return `${lngLat.lng.toFixed(6)}, ${lngLat.lat.toFixed(6)}`;
|
||||
}
|
||||
|
||||
function updateInspectPanel(event, feature) {
|
||||
inspectHintEl.textContent = "已选中对象。再次点击其他对象可继续查看。";
|
||||
inspectLngLatEl.textContent = formatLngLat(event.lngLat);
|
||||
inspectSourceEl.textContent = feature.source || "-";
|
||||
inspectLayerEl.textContent = feature.sourceLayer || feature.layer?.["source-layer"] || "-";
|
||||
inspectGeometryEl.textContent = feature.geometry?.type || "-";
|
||||
inspectRenderLayerEl.textContent = feature.layer?.id || "-";
|
||||
inspectJsonEl.textContent = JSON.stringify(feature.properties || {}, null, 2);
|
||||
}
|
||||
|
||||
async function loadMap() {
|
||||
styleLoadVersion = Date.now().toString();
|
||||
setStatus("正在加载最终版样式…");
|
||||
reloadBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const style = applySourceCacheBuster(await fetchStyle(STYLE_URL), styleLoadVersion);
|
||||
|
||||
if (!map) {
|
||||
map = new maplibregl.Map({
|
||||
container: "map",
|
||||
style,
|
||||
center: INITIAL_VIEW.center,
|
||||
zoom: INITIAL_VIEW.zoom,
|
||||
bearing: INITIAL_VIEW.bearing,
|
||||
pitch: INITIAL_VIEW.pitch
|
||||
});
|
||||
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
||||
map.on("click", (event) => {
|
||||
const features = map.queryRenderedFeatures(event.point);
|
||||
if (!features.length) {
|
||||
resetInspectPanel(`点击位置没有命中对象。点击坐标: ${formatLngLat(event.lngLat)}`);
|
||||
return;
|
||||
}
|
||||
const [feature] = features;
|
||||
console.log("clicked feature:", feature);
|
||||
updateInspectPanel(event, feature);
|
||||
});
|
||||
map.on("load", () => setStatus("最终版样式已加载,可以直接点击对象查看属性和坐标。"));
|
||||
} else {
|
||||
const camera = {
|
||||
center: map.getCenter(),
|
||||
zoom: map.getZoom(),
|
||||
bearing: map.getBearing(),
|
||||
pitch: map.getPitch()
|
||||
};
|
||||
map.setStyle(style, { diff: false });
|
||||
map.once("style.load", () => {
|
||||
map.jumpTo(camera);
|
||||
setStatus("最终版样式已重新加载。");
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setStatus(error.message || "样式加载失败");
|
||||
} finally {
|
||||
reloadBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
reloadBtn.addEventListener("click", loadMap);
|
||||
resetInspectPanel("点击地图对象,查看点击坐标、source、source-layer、geometry 和 properties。");
|
||||
loadMap();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
313
src/pbf/navsea-final-delivery-kyushu.html
Normal file
313
src/pbf/navsea-final-delivery-kyushu.html
Normal file
@@ -0,0 +1,313 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>NavSea Final Delivery Kyushu</title>
|
||||
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--bg: #edf2ef;
|
||||
--panel: rgba(248, 246, 241, 0.95);
|
||||
--ink: #18232b;
|
||||
--muted: #617078;
|
||||
--line: rgba(24, 35, 43, 0.12);
|
||||
--accent: #0b5f79;
|
||||
--accent-dark: #084454;
|
||||
--shadow: 0 16px 32px rgba(24, 35, 43, 0.15);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(11, 95, 121, 0.12), transparent 30%),
|
||||
radial-gradient(circle at bottom right, rgba(185, 133, 68, 0.10), transparent 26%),
|
||||
var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: "Avenir Next", "Segoe UI", "PingFang SC", "Noto Sans SC", sans-serif;
|
||||
}
|
||||
|
||||
body { display: grid; grid-template-rows: auto 1fr; }
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 18px;
|
||||
padding: 14px 18px 12px;
|
||||
background: linear-gradient(180deg, rgba(248, 246, 241, 0.98), rgba(248, 246, 241, 0.9));
|
||||
border-bottom: 1px solid var(--line);
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 10;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.title { font-size: 18px; font-weight: 700; letter-spacing: 0.02em; }
|
||||
.subtitle { margin-top: 4px; font-size: 13px; color: var(--muted); max-width: 920px; line-height: 1.5; }
|
||||
.toolbar-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
button {
|
||||
height: 40px;
|
||||
padding: 0 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(8, 68, 84, 0.18);
|
||||
background: linear-gradient(180deg, var(--accent), var(--accent-dark));
|
||||
color: #fff;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 22px rgba(8, 68, 84, 0.18);
|
||||
}
|
||||
.layout { position: relative; min-height: 0; }
|
||||
.map { position: absolute; inset: 0; }
|
||||
.map-tag {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 5;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: var(--panel);
|
||||
border: 1px solid rgba(24, 35, 43, 0.08);
|
||||
box-shadow: 0 12px 24px rgba(24, 35, 43, 0.12);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.map-tag strong { display: block; font-size: 13px; margin-bottom: 3px; }
|
||||
.map-tag span { display: block; font-size: 12px; color: var(--muted); }
|
||||
.inspect-panel {
|
||||
position: fixed;
|
||||
top: 86px;
|
||||
right: 14px;
|
||||
z-index: 30;
|
||||
width: min(340px, calc(100vw - 28px));
|
||||
max-height: calc(100vh - 150px);
|
||||
overflow: auto;
|
||||
padding: 10px 11px;
|
||||
border-radius: 14px;
|
||||
background: rgba(248, 246, 241, 0.94);
|
||||
border: 1px solid rgba(24, 35, 43, 0.12);
|
||||
box-shadow: 0 14px 28px rgba(24, 35, 43, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.inspect-panel h3 { margin: 0 0 4px; font-size: 13px; }
|
||||
.inspect-hint { margin: 0 0 8px; color: var(--muted); font-size: 11px; line-height: 1.4; }
|
||||
.inspect-meta {
|
||||
display: grid;
|
||||
grid-template-columns: 92px 1fr;
|
||||
gap: 4px 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.inspect-meta dt { color: var(--muted); font-weight: 700; }
|
||||
.inspect-meta dd { margin: 0; word-break: break-word; }
|
||||
.inspect-json {
|
||||
margin: 0;
|
||||
padding: 9px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(24, 35, 43, 0.92);
|
||||
color: #eef3f6;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.status {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 14px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 30;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(24, 35, 43, 0.84);
|
||||
color: #f7f9fb;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.03em;
|
||||
max-width: calc(100% - 28px);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@media (max-width: 920px) {
|
||||
.inspect-panel {
|
||||
top: auto;
|
||||
right: 12px;
|
||||
bottom: 54px;
|
||||
width: min(320px, calc(100vw - 24px));
|
||||
max-height: 32vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<div class="title">NavSea 最终版 Delivery · 九州</div>
|
||||
<div class="subtitle">当前页面固定加载九州 delivery 样式 <code>style.navsea-delivery-kyushu.json</code>。点击任意对象可查看对象属性和点击经纬度。</div>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<button id="reload-btn" type="button">重新加载</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<div class="map-tag">
|
||||
<strong>九州最终版</strong>
|
||||
<span>Delivery PBF · Reencoded FID</span>
|
||||
</div>
|
||||
<div id="map" class="map"></div>
|
||||
</div>
|
||||
|
||||
<aside class="inspect-panel">
|
||||
<h3>点击查询</h3>
|
||||
<p id="inspect-hint" class="inspect-hint">点击地图对象,查看点击坐标、source、source-layer、geometry 和 properties。</p>
|
||||
<dl class="inspect-meta">
|
||||
<dt>点击坐标</dt>
|
||||
<dd id="inspect-lnglat">-</dd>
|
||||
<dt>source</dt>
|
||||
<dd id="inspect-source">-</dd>
|
||||
<dt>source-layer</dt>
|
||||
<dd id="inspect-layer">-</dd>
|
||||
<dt>geometry</dt>
|
||||
<dd id="inspect-geometry">-</dd>
|
||||
<dt>render layer</dt>
|
||||
<dd id="inspect-render-layer">-</dd>
|
||||
</dl>
|
||||
<pre id="inspect-json" class="inspect-json">{
|
||||
"message": "等待点击对象"
|
||||
}</pre>
|
||||
</aside>
|
||||
|
||||
<div id="status" class="status">等待加载样式…</div>
|
||||
|
||||
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
|
||||
<script>
|
||||
const STYLE_URL = "./style.navsea-delivery-kyushu.json";
|
||||
const INITIAL_VIEW = {
|
||||
center: [130.7, 32.2],
|
||||
zoom: 10,
|
||||
pitch: 0,
|
||||
bearing: 0
|
||||
};
|
||||
|
||||
const statusEl = document.getElementById("status");
|
||||
const reloadBtn = document.getElementById("reload-btn");
|
||||
const inspectHintEl = document.getElementById("inspect-hint");
|
||||
const inspectLngLatEl = document.getElementById("inspect-lnglat");
|
||||
const inspectSourceEl = document.getElementById("inspect-source");
|
||||
const inspectLayerEl = document.getElementById("inspect-layer");
|
||||
const inspectGeometryEl = document.getElementById("inspect-geometry");
|
||||
const inspectRenderLayerEl = document.getElementById("inspect-render-layer");
|
||||
const inspectJsonEl = document.getElementById("inspect-json");
|
||||
let map;
|
||||
let styleLoadVersion = Date.now().toString();
|
||||
|
||||
function setStatus(text) {
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
|
||||
function withCacheBuster(url, version) {
|
||||
const separator = url.includes("?") ? "&" : "?";
|
||||
return `${url}${separator}v=${version}`;
|
||||
}
|
||||
|
||||
async function fetchStyle(url) {
|
||||
const response = await fetch(withCacheBuster(url, styleLoadVersion), { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`加载样式失败: ${url}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function applySourceCacheBuster(style, version) {
|
||||
if (style.sprite) style.sprite = withCacheBuster(style.sprite, version);
|
||||
if (style.glyphs) style.glyphs = withCacheBuster(style.glyphs, version);
|
||||
const sources = style.sources || {};
|
||||
Object.values(sources).forEach((source) => {
|
||||
if (Array.isArray(source.tiles)) {
|
||||
source.tiles = source.tiles.map((tileUrl) => withCacheBuster(tileUrl, version));
|
||||
}
|
||||
if (typeof source.url === "string") {
|
||||
source.url = withCacheBuster(source.url, version);
|
||||
}
|
||||
});
|
||||
return style;
|
||||
}
|
||||
|
||||
function resetInspectPanel(message) {
|
||||
inspectHintEl.textContent = message;
|
||||
inspectLngLatEl.textContent = "-";
|
||||
inspectSourceEl.textContent = "-";
|
||||
inspectLayerEl.textContent = "-";
|
||||
inspectGeometryEl.textContent = "-";
|
||||
inspectRenderLayerEl.textContent = "-";
|
||||
inspectJsonEl.textContent = JSON.stringify({ message }, null, 2);
|
||||
}
|
||||
|
||||
function formatLngLat(lngLat) {
|
||||
return `${lngLat.lng.toFixed(6)}, ${lngLat.lat.toFixed(6)}`;
|
||||
}
|
||||
|
||||
function updateInspectPanel(event, feature) {
|
||||
inspectHintEl.textContent = "已选中对象。再次点击其他对象可继续查看。";
|
||||
inspectLngLatEl.textContent = formatLngLat(event.lngLat);
|
||||
inspectSourceEl.textContent = feature.source || "-";
|
||||
inspectLayerEl.textContent = feature.sourceLayer || feature.layer?.["source-layer"] || "-";
|
||||
inspectGeometryEl.textContent = feature.geometry?.type || "-";
|
||||
inspectRenderLayerEl.textContent = feature.layer?.id || "-";
|
||||
inspectJsonEl.textContent = JSON.stringify(feature.properties || {}, null, 2);
|
||||
}
|
||||
|
||||
async function loadMap() {
|
||||
styleLoadVersion = Date.now().toString();
|
||||
setStatus("正在加载九州最终版样式…");
|
||||
reloadBtn.disabled = true;
|
||||
try {
|
||||
const style = applySourceCacheBuster(await fetchStyle(STYLE_URL), styleLoadVersion);
|
||||
if (!map) {
|
||||
map = new maplibregl.Map({
|
||||
container: "map",
|
||||
style,
|
||||
center: INITIAL_VIEW.center,
|
||||
zoom: INITIAL_VIEW.zoom,
|
||||
bearing: INITIAL_VIEW.bearing,
|
||||
pitch: INITIAL_VIEW.pitch
|
||||
});
|
||||
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
||||
map.on("click", (event) => {
|
||||
const features = map.queryRenderedFeatures(event.point);
|
||||
if (!features.length) {
|
||||
resetInspectPanel(`点击位置没有命中对象。点击坐标: ${formatLngLat(event.lngLat)}`);
|
||||
return;
|
||||
}
|
||||
const [feature] = features;
|
||||
console.log("clicked feature:", feature);
|
||||
updateInspectPanel(event, feature);
|
||||
});
|
||||
map.on("load", () => setStatus("九州最终版样式已加载,可以直接点击对象查看属性和坐标。"));
|
||||
} else {
|
||||
const camera = {
|
||||
center: map.getCenter(),
|
||||
zoom: map.getZoom(),
|
||||
bearing: map.getBearing(),
|
||||
pitch: map.getPitch()
|
||||
};
|
||||
map.setStyle(style, { diff: false });
|
||||
map.once("style.load", () => {
|
||||
map.jumpTo(camera);
|
||||
setStatus("九州最终版样式已重新加载。");
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setStatus(error.message || "样式加载失败");
|
||||
} finally {
|
||||
reloadBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
reloadBtn.addEventListener("click", loadMap);
|
||||
resetInspectPanel("点击地图对象,查看点击坐标、source、source-layer、geometry 和 properties。");
|
||||
loadMap();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -21,7 +21,7 @@
|
||||
"tiles": [
|
||||
"https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png"
|
||||
],
|
||||
"attribution": "© 昭文社"
|
||||
"attribution": "国土院"
|
||||
},
|
||||
"shipfinder": {
|
||||
"type": "geojson",
|
||||
|
||||
2967
src/pbf/style.navsea-delivery-full.json
Normal file
2967
src/pbf/style.navsea-delivery-full.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 8,
|
||||
"name": "NavSea Delivery Karatsu 10nm",
|
||||
"sprite": "https://tile.mapple-on.jp/newpec-symbols-20251001/sprite",
|
||||
"sprite": "http://192.168.200.184/newpec/sprite/sprite?v=111",
|
||||
"glyphs": "http://192.168.200.184/newpec/fonts/{fontstack}/{range}.pbf",
|
||||
"sources": {
|
||||
"mapple": {
|
||||
@@ -79,7 +79,7 @@
|
||||
"depth_zone_6000-7000m",
|
||||
"depth_zone_7000-8000m",
|
||||
"depth_zone_8000-9000m",
|
||||
"depth_zone_9000m以深",
|
||||
"depth_zone_over_9000m",
|
||||
"water_area"
|
||||
],
|
||||
true,
|
||||
@@ -115,7 +115,7 @@
|
||||
"6000-7000m",
|
||||
"7000-8000m",
|
||||
"8000-9000m",
|
||||
"9000m以深"
|
||||
"over_9000m"
|
||||
],
|
||||
true,
|
||||
false
|
||||
@@ -174,15 +174,15 @@
|
||||
"rgba(139,118,255,1)",
|
||||
"8000-9000m",
|
||||
"rgba(150,100,255,1)",
|
||||
"9000m以深",
|
||||
"over_9000m",
|
||||
"rgba(160,100,255,1)",
|
||||
"河川域",
|
||||
"river_area",
|
||||
"rgba(129,195,226,1)",
|
||||
"湖沼域",
|
||||
"lake_area",
|
||||
"rgba(129,195,226,1)",
|
||||
"陸上水域",
|
||||
"inland_water_area",
|
||||
"rgba(129,195,226,1)",
|
||||
"干潮帯",
|
||||
"tidal_flat",
|
||||
"rgba(143,191,147,1)",
|
||||
[
|
||||
"match",
|
||||
@@ -236,7 +236,7 @@
|
||||
"rgba(139,118,255,1)",
|
||||
"depth_zone_8000-9000m",
|
||||
"rgba(150,100,255,1)",
|
||||
"depth_zone_9000m以深",
|
||||
"depth_zone_over_9000m",
|
||||
"rgba(160,100,255,1)",
|
||||
"water_area",
|
||||
"rgba(129,195,226,1)",
|
||||
@@ -266,7 +266,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"干潮帯"
|
||||
"tidal_flat"
|
||||
]
|
||||
],
|
||||
"paint": {
|
||||
@@ -295,9 +295,9 @@
|
||||
"canonical_object_type"
|
||||
],
|
||||
[
|
||||
"河川域",
|
||||
"湖沼域",
|
||||
"陸上水域"
|
||||
"river_area",
|
||||
"lake_area",
|
||||
"inland_water_area"
|
||||
],
|
||||
true,
|
||||
false
|
||||
@@ -328,7 +328,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"未測海域"
|
||||
"unsurveyed_area"
|
||||
]
|
||||
],
|
||||
"paint": {
|
||||
@@ -357,7 +357,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"浅所危険界"
|
||||
"shoal_danger_area"
|
||||
]
|
||||
],
|
||||
"paint": {
|
||||
@@ -387,9 +387,9 @@
|
||||
"canonical_object_type"
|
||||
],
|
||||
[
|
||||
"防波堤",
|
||||
"浮施設・桟橋",
|
||||
"撤去跡"
|
||||
"breakwater",
|
||||
"floating_facility_pier",
|
||||
"removed_structure_remains"
|
||||
],
|
||||
true,
|
||||
false
|
||||
@@ -1362,7 +1362,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"沿岸灯台 (15M over)"
|
||||
"coastal_lighthouse_over_15m"
|
||||
],
|
||||
[
|
||||
"!=",
|
||||
@@ -1394,7 +1394,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"沿岸灯台 (15M over)"
|
||||
"coastal_lighthouse_over_15m"
|
||||
],
|
||||
[
|
||||
"==",
|
||||
@@ -1453,7 +1453,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"港湾灯台"
|
||||
"harbor_lighthouse"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1473,7 +1473,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"防波堤灯台"
|
||||
"breakwater_lighthouse"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1493,7 +1493,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"灯 (Lt)"
|
||||
"minor_light"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1513,7 +1513,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"灯標"
|
||||
"light_beacon"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1533,7 +1533,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"沿岸灯台 (15M over)"
|
||||
"coastal_lighthouse_over_15m"
|
||||
],
|
||||
"symbol-daytime-300",
|
||||
[
|
||||
@@ -1542,7 +1542,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"港湾灯台"
|
||||
"harbor_lighthouse"
|
||||
],
|
||||
"symbol-daytime-301",
|
||||
[
|
||||
@@ -1551,7 +1551,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"防波堤灯台"
|
||||
"breakwater_lighthouse"
|
||||
],
|
||||
"symbol-daytime-302",
|
||||
[
|
||||
@@ -1560,7 +1560,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"灯 (Lt)"
|
||||
"minor_light"
|
||||
],
|
||||
"symbol-daytime-303",
|
||||
[
|
||||
@@ -1581,7 +1581,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"港湾灯台"
|
||||
"harbor_lighthouse"
|
||||
],
|
||||
[
|
||||
"!=",
|
||||
@@ -1589,7 +1589,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"防波堤灯台"
|
||||
"breakwater_lighthouse"
|
||||
],
|
||||
[
|
||||
"!=",
|
||||
@@ -1597,7 +1597,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"灯 (Lt)"
|
||||
"minor_light"
|
||||
],
|
||||
[
|
||||
"!=",
|
||||
@@ -1605,7 +1605,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"灯標"
|
||||
"light_beacon"
|
||||
]
|
||||
]
|
||||
},
|
||||
@@ -1632,17 +1632,17 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"煙突",
|
||||
"chimney",
|
||||
"symbol-daytime-640",
|
||||
"塔、やぐら、風車",
|
||||
"tower_yagura_windmill",
|
||||
"symbol-daytime-650",
|
||||
"海事関係署",
|
||||
"maritime_office",
|
||||
"symbol-daytime-660",
|
||||
"漁業協同組合",
|
||||
"fishing_cooperative",
|
||||
"symbol-daytime-661",
|
||||
"山頂",
|
||||
"mountain_top",
|
||||
"symbol-daytime-681",
|
||||
"その他 (記念碑等)",
|
||||
"other_landmark_monument",
|
||||
"symbol-daytime-698",
|
||||
""
|
||||
]
|
||||
@@ -1653,17 +1653,17 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"煙突",
|
||||
"chimney",
|
||||
true,
|
||||
"塔、やぐら、風車",
|
||||
"tower_yagura_windmill",
|
||||
true,
|
||||
"海事関係署",
|
||||
"maritime_office",
|
||||
true,
|
||||
"漁業協同組合",
|
||||
"fishing_cooperative",
|
||||
true,
|
||||
"山頂",
|
||||
"mountain_top",
|
||||
true,
|
||||
"その他 (記念碑等)",
|
||||
"other_landmark_monument",
|
||||
true,
|
||||
false
|
||||
]
|
||||
@@ -1679,17 +1679,17 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"煙突",
|
||||
"chimney",
|
||||
false,
|
||||
"塔、やぐら、風車",
|
||||
"tower_yagura_windmill",
|
||||
false,
|
||||
"海事関係署",
|
||||
"maritime_office",
|
||||
false,
|
||||
"漁業協同組合",
|
||||
"fishing_cooperative",
|
||||
false,
|
||||
"山頂",
|
||||
"mountain_top",
|
||||
false,
|
||||
"その他 (記念碑等)",
|
||||
"other_landmark_monument",
|
||||
false,
|
||||
true
|
||||
],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 8,
|
||||
"name": "NavSea Delivery Karatsu 20nm Final",
|
||||
"sprite": "https://tile.mapple-on.jp/newpec-symbols-20251001/sprite",
|
||||
"sprite": "http://192.168.200.184/newpec/sprite/sprite?v=111",
|
||||
"glyphs": "http://192.168.200.184/newpec/fonts/{fontstack}/{range}.pbf",
|
||||
"sources": {
|
||||
"mapple": {
|
||||
@@ -82,7 +82,7 @@
|
||||
"depth_zone_6000-7000m",
|
||||
"depth_zone_7000-8000m",
|
||||
"depth_zone_8000-9000m",
|
||||
"depth_zone_9000m以深",
|
||||
"depth_zone_over_9000m",
|
||||
"water_area"
|
||||
],
|
||||
true,
|
||||
@@ -118,7 +118,7 @@
|
||||
"6000-7000m",
|
||||
"7000-8000m",
|
||||
"8000-9000m",
|
||||
"9000m以深"
|
||||
"over_9000m"
|
||||
],
|
||||
true,
|
||||
false
|
||||
@@ -177,15 +177,15 @@
|
||||
"rgba(139,118,255,1)",
|
||||
"8000-9000m",
|
||||
"rgba(150,100,255,1)",
|
||||
"9000m以深",
|
||||
"over_9000m",
|
||||
"rgba(160,100,255,1)",
|
||||
"河川域",
|
||||
"river_area",
|
||||
"rgba(129,195,226,1)",
|
||||
"湖沼域",
|
||||
"lake_area",
|
||||
"rgba(129,195,226,1)",
|
||||
"陸上水域",
|
||||
"inland_water_area",
|
||||
"rgba(129,195,226,1)",
|
||||
"干潮帯",
|
||||
"tidal_flat",
|
||||
"rgba(143,191,147,1)",
|
||||
[
|
||||
"match",
|
||||
@@ -239,7 +239,7 @@
|
||||
"rgba(139,118,255,1)",
|
||||
"depth_zone_8000-9000m",
|
||||
"rgba(150,100,255,1)",
|
||||
"depth_zone_9000m以深",
|
||||
"depth_zone_over_9000m",
|
||||
"rgba(160,100,255,1)",
|
||||
"water_area",
|
||||
"rgba(129,195,226,1)",
|
||||
@@ -269,7 +269,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"干潮帯"
|
||||
"tidal_flat"
|
||||
]
|
||||
],
|
||||
"paint": {
|
||||
@@ -298,9 +298,9 @@
|
||||
"canonical_object_type"
|
||||
],
|
||||
[
|
||||
"河川域",
|
||||
"湖沼域",
|
||||
"陸上水域"
|
||||
"river_area",
|
||||
"lake_area",
|
||||
"inland_water_area"
|
||||
],
|
||||
true,
|
||||
false
|
||||
@@ -331,7 +331,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"未測海域"
|
||||
"unsurveyed_area"
|
||||
]
|
||||
],
|
||||
"paint": {
|
||||
@@ -360,7 +360,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"浅所危険界"
|
||||
"shoal_danger_area"
|
||||
]
|
||||
],
|
||||
"paint": {
|
||||
@@ -390,9 +390,9 @@
|
||||
"canonical_object_type"
|
||||
],
|
||||
[
|
||||
"防波堤",
|
||||
"浮施設・桟橋",
|
||||
"撤去跡"
|
||||
"breakwater",
|
||||
"floating_facility_pier",
|
||||
"removed_structure_remains"
|
||||
],
|
||||
true,
|
||||
false
|
||||
@@ -1561,7 +1561,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"灯 (Lt)"
|
||||
"minor_light"
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -1585,7 +1585,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"沿岸灯台 (15M over)"
|
||||
"coastal_lighthouse_over_15m"
|
||||
],
|
||||
[
|
||||
"==",
|
||||
@@ -1643,7 +1643,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"港湾灯台"
|
||||
"harbor_lighthouse"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1662,7 +1662,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"防波堤灯台"
|
||||
"breakwater_lighthouse"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1681,7 +1681,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"灯 (Lt)"
|
||||
"minor_light"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1747,7 +1747,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"灯標"
|
||||
"light_beacon"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1865,7 +1865,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"沿岸灯台 (15M over)"
|
||||
"coastal_lighthouse_over_15m"
|
||||
],
|
||||
"symbol-daytime-300",
|
||||
[
|
||||
@@ -1874,7 +1874,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"港湾灯台"
|
||||
"harbor_lighthouse"
|
||||
],
|
||||
"symbol-daytime-301",
|
||||
[
|
||||
@@ -1883,7 +1883,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"防波堤灯台"
|
||||
"breakwater_lighthouse"
|
||||
],
|
||||
"symbol-daytime-302",
|
||||
[
|
||||
@@ -1892,7 +1892,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"灯 (Lt)"
|
||||
"minor_light"
|
||||
],
|
||||
"symbol-daytime-303",
|
||||
[
|
||||
@@ -1901,7 +1901,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"浮標 (やぐら型)"
|
||||
"lattice_buoy"
|
||||
],
|
||||
"symbol-daytime-327",
|
||||
[
|
||||
@@ -1910,7 +1910,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"円柱型浮標"
|
||||
"pillar_buoy"
|
||||
],
|
||||
"symbol-daytime-323",
|
||||
[
|
||||
@@ -1919,7 +1919,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"円筒型浮標"
|
||||
"can_buoy"
|
||||
],
|
||||
"symbol-daytime-325",
|
||||
[
|
||||
@@ -1940,7 +1940,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"港湾灯台"
|
||||
"harbor_lighthouse"
|
||||
],
|
||||
[
|
||||
"!=",
|
||||
@@ -1948,7 +1948,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"防波堤灯台"
|
||||
"breakwater_lighthouse"
|
||||
],
|
||||
[
|
||||
"!=",
|
||||
@@ -1956,7 +1956,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"灯 (Lt)"
|
||||
"minor_light"
|
||||
],
|
||||
[
|
||||
"!=",
|
||||
@@ -1964,7 +1964,7 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"灯標"
|
||||
"light_beacon"
|
||||
]
|
||||
]
|
||||
},
|
||||
@@ -2028,17 +2028,17 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"煙突",
|
||||
"chimney",
|
||||
"symbol-daytime-640",
|
||||
"塔、やぐら、風車",
|
||||
"tower_yagura_windmill",
|
||||
"symbol-daytime-650",
|
||||
"海事関係署",
|
||||
"maritime_office",
|
||||
"symbol-daytime-660",
|
||||
"漁業協同組合",
|
||||
"fishing_cooperative",
|
||||
"symbol-daytime-661",
|
||||
"山頂",
|
||||
"mountain_top",
|
||||
"symbol-daytime-681",
|
||||
"その他 (記念碑等)",
|
||||
"other_landmark_monument",
|
||||
"symbol-daytime-698",
|
||||
""
|
||||
]
|
||||
@@ -2049,17 +2049,17 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"煙突",
|
||||
"chimney",
|
||||
true,
|
||||
"塔、やぐら、風車",
|
||||
"tower_yagura_windmill",
|
||||
true,
|
||||
"海事関係署",
|
||||
"maritime_office",
|
||||
true,
|
||||
"漁業協同組合",
|
||||
"fishing_cooperative",
|
||||
true,
|
||||
"山頂",
|
||||
"mountain_top",
|
||||
true,
|
||||
"その他 (記念碑等)",
|
||||
"other_landmark_monument",
|
||||
true,
|
||||
false
|
||||
]
|
||||
@@ -2075,17 +2075,17 @@
|
||||
"get",
|
||||
"canonical_object_type"
|
||||
],
|
||||
"煙突",
|
||||
"chimney",
|
||||
false,
|
||||
"塔、やぐら、風車",
|
||||
"tower_yagura_windmill",
|
||||
false,
|
||||
"海事関係署",
|
||||
"maritime_office",
|
||||
false,
|
||||
"漁業協同組合",
|
||||
"fishing_cooperative",
|
||||
false,
|
||||
"山頂",
|
||||
"mountain_top",
|
||||
false,
|
||||
"その他 (記念碑等)",
|
||||
"other_landmark_monument",
|
||||
false,
|
||||
true
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user