chore: 全量快照提交以防磁盘风险

This commit is contained in:
OpenAI Codex
2026-04-08 19:32:25 +08:00
parent 86726e3247
commit 4f96d7be91
243 changed files with 49757 additions and 343 deletions

7
src/Domain/__init__.py Normal file
View 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.
"""

View 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()

View 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": "&copy; NavSea Domain",
},
"domain_detail": {
"type": "vector",
"minzoom": 0,
"maxzoom": 12,
"tiles": [detail_tiles_url],
"attribution": "&copy; 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()

View 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)

View 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>

View 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>

View 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>

View 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>

View 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()

View 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()

View 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()

View 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()

File diff suppressed because it is too large Load Diff

View 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": "&copy; 昭文社"
},
"domain_safety": {
"type": "vector",
"minzoom": 0,
"maxzoom": 12,
"tiles": [
"http://192.168.200.184/pbf-domain-karatsu-10nm/safety/{z}/{x}/{y}.pbf"
],
"attribution": "&copy; 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": "&copy; 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
}
}
]
}

File diff suppressed because it is too large Load Diff

View 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

View 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"
}
}
}

View File

@@ -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",

View 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>

View 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>

View 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>

View File

@@ -21,7 +21,7 @@
"tiles": [
"https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png"
],
"attribution": "&copy; 昭文社"
"attribution": "国土院"
},
"shipfinder": {
"type": "geojson",

File diff suppressed because it is too large Load Diff

View File

@@ -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
],

View File

@@ -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