Files
CM95/web/kyushu_compare.html
2026-09-19 07:18:40 +08:00

306 lines
14 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>九州 S-52 PBF 左右对比</title>
<link rel="stylesheet" href="/assets/maplibre-gl.css" />
<style>
*{box-sizing:border-box}
html,body{height:100%;margin:0;font-family:system-ui,-apple-system,"Segoe UI",sans-serif;background:#111;color:#eee}
body{overflow:hidden}
#toolbar{height:46px;display:flex;align-items:center;gap:14px;padding:0 12px;background:#17191d;border-bottom:1px solid #3a3d43;font-size:13px}
#toolbar strong{font-size:14px;white-space:nowrap}
#toolbar .meta{color:#abb2bd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#toolbar button{border:1px solid #555;background:#272b31;color:#fff;border-radius:5px;padding:5px 9px;cursor:pointer}
#toolbar label{display:flex;gap:5px;align-items:center;white-space:nowrap}
#maps{height:calc(100% - 46px);display:grid;grid-template-columns:1fr 1fr;gap:2px;background:#555}
.pane{position:relative;min-width:0;overflow:hidden;background:#d8edf5}
.map{position:absolute;inset:0}
.pane-title{position:absolute;z-index:5;left:10px;top:10px;max-width:calc(100% - 62px);padding:7px 9px;border-radius:6px;background:rgba(18,20,24,.88);box-shadow:0 1px 6px rgba(0,0,0,.3);font-size:12px;line-height:1.4}
.pane-title b{display:block;font-size:13px}
.state{font-size:11px;color:#cbd2dc}
.state.ok{color:#91e39b}.state.err{color:#ff8c8c}
.inspector{position:absolute;z-index:5;left:10px;right:10px;bottom:10px;max-height:26%;overflow:auto;padding:7px 9px;border-radius:6px;background:rgba(18,20,24,.88);font:11px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap;pointer-events:auto}
.inspector.empty{color:#9ca5b1}
#center-readout{margin-left:auto;color:#cbd2dc;white-space:nowrap;font-variant-numeric:tabular-nums}
.divider-label{position:absolute;z-index:6;top:50%;transform:translateY(-50%);background:rgba(0,0,0,.6);padding:4px 5px;font-size:10px;color:#ddd;pointer-events:none}
#left-pane .divider-label{right:3px;border-radius:4px 0 0 4px}
#right-pane .divider-label{left:3px;border-radius:0 4px 4px 0}
#debug-panel{position:absolute;z-index:20;top:56px;right:10px;width:270px;padding:10px 11px;border:1px solid #555;border-radius:7px;background:rgba(20,22,26,.94);box-shadow:0 3px 12px rgba(0,0,0,.35);font-size:12px}
#debug-panel.collapsed{display:none}
#debug-panel .debug-title{display:flex;align-items:center;justify-content:space-between;margin-bottom:7px;font-weight:600}
#debug-panel label{display:flex;align-items:center;gap:7px;margin:7px 0;line-height:1.3}
#debug-panel .debug-note{margin-top:8px;color:#aeb7c3;font-size:11px;line-height:1.4}
#debug-panel button{border:1px solid #555;background:#272b31;color:#fff;border-radius:4px;padding:3px 7px;cursor:pointer}
@media(max-width:760px){
#toolbar .meta{display:none}
#maps{grid-template-columns:1fr;grid-template-rows:1fr 1fr}
.inspector{max-height:32%}
}
</style>
</head>
<body>
<div id="toolbar">
<strong>九州 S-52 PBF 对比</strong>
<span class="meta">左:Production v2 本地 PBF 右:extentfix 原始数据源</span>
<label><input id="sync-toggle" type="checkbox" checked />同步视图</label>
<button id="reset-btn" type="button">重置九州</button>
<button id="debug-toggle-btn" type="button">调试</button>
<span id="center-readout">--</span>
</div>
<div id="debug-panel" class="collapsed">
<div class="debug-title"><span>图层调试</span><button id="debug-collapse-btn" type="button">收起</button></div>
<label><input id="debug-ref-bathy" type="checkbox" checked />右侧显示 bathymetry_line</label>
<label><input id="debug-highlight-depcnt" type="checkbox" />左右突出 DEPCNT</label>
<label><input id="debug-inspector" type="checkbox" checked />点击 Feature 检查</label>
<div class="debug-note">DEPCNT = depth_contour;bathymetry_line 是参考 PBF 的额外高密度海底地形线。</div>
</div>
<div id="maps">
<section class="pane" id="left-pane">
<div id="left-map" class="map"></div>
<div class="pane-title"><b>左:九州 Production v2</b><span id="left-state" class="state">等待参考 style…</span></div>
<div id="left-inspector" class="inspector empty">点击任一侧海图,对比同一位置的 Feature。</div>
<div class="divider-label">LOCAL</div>
</section>
<section class="pane" id="right-pane">
<div id="right-map" class="map"></div>
<div class="pane-title"><b>右:NavSea extentfix</b><span id="right-state" class="state">加载远端 style…</span></div>
<div id="right-inspector" class="inspector empty">点击任一侧海图,对比同一位置的 Feature。</div>
<div class="divider-label">REFERENCE</div>
</section>
</div>
<script src="/assets/maplibre-gl.js"></script>
<script>
const REFERENCE_STYLE =
"https://pbf.jpcn.ccwu.cc/style/style.navsea-delivery-full-semantic-extentfix.json";
const LOCAL_TILES = location.origin + "/pbf/{z}/{x}/{y}.pbf";
const KYUSHU_BOUNDS = [[128.0,29.5],[132.5,34.8]];
const START = {center:[130.25,32.15],zoom:6.6,bearing:0,pitch:0};
const leftState = document.getElementById("left-state");
const rightState = document.getElementById("right-state");
const centerReadout = document.getElementById("center-readout");
const syncToggle = document.getElementById("sync-toggle");
const debugPanel = document.getElementById("debug-panel");
const debugRefBathy = document.getElementById("debug-ref-bathy");
const debugHighlightDepcnt = document.getElementById("debug-highlight-depcnt");
const debugInspector = document.getElementById("debug-inspector");
function setState(el,text,kind){
el.textContent=text;
el.className="state" + (kind ? " " + kind : "");
}
function emptyStyle(){
return {version:8,sources:{},layers:[{id:"background",type:"background",paint:{"background-color":"#d8edf5"}}]};
}
function setLayerVisibility(map,id,visible){
if(map.getLayer(id))map.setLayoutProperty(id,"visibility",visible ? "visible" : "none");
}
function applyDebugStyles(){
setLayerVisibility(rightMap,"bathymetry-support",debugRefBathy.checked);
setLayerVisibility(rightMap,"bathymetry-depth-labels",debugRefBathy.checked);
for(const map of [leftMap,rightMap]){
if(map.getLayer("depth-contours")){
map.setPaintProperty("depth-contours","line-color",
debugHighlightDepcnt.checked ? "#ff00ff" : "rgba(136,152,139,1)");
map.setPaintProperty("depth-contours","line-width",
debugHighlightDepcnt.checked ? 3 : 1);
}
}
const display=debugInspector.checked ? "" : "none";
document.getElementById("left-inspector").style.display=display;
document.getElementById("right-inspector").style.display=display;
}
const leftMap = new maplibregl.Map({
container:"left-map",style:emptyStyle(),center:START.center,zoom:START.zoom,bearing:0,pitch:0,attributionControl:false
});
const rightMap = new maplibregl.Map({
container:"right-map",style:emptyStyle(),center:START.center,zoom:START.zoom,bearing:0,pitch:0,attributionControl:false
});
leftMap.addControl(new maplibregl.NavigationControl(),"top-right");
rightMap.addControl(new maplibregl.NavigationControl(),"top-right");
let syncing=false;
function sanitizeReferenceStyle(raw){
const style=JSON.parse(JSON.stringify(raw));
const sources=style.sources || {};
const removed=[];
style.layers=(style.layers || []).filter(function(layer){
if(!layer.source || sources[layer.source])return true;
removed.push(layer.id + " -> " + layer.source);
return false;
});
return {style:style,removed:removed};
}
function cloneReferenceForLocal(referenceStyle){
const style=JSON.parse(JSON.stringify(referenceStyle));
const source=style.sources && style.sources.navsea_delivery;
if(!source)throw new Error("参考 style 中找不到 sources.navsea_delivery");
delete source.url;
source.type="vector";
source.tiles=[LOCAL_TILES];
source.minzoom=5;
source.maxzoom=12;
source.bounds=[128.0,29.5,132.5,34.8];
source.attribution="CM95 九州 S-52 Production Pipeline v2";
style.name=(style.name || "NavSea") + " / Kyushu Production v2";
return style;
}
async function loadComparisonStyles(){
try{
setState(rightState,"下载并检查参考 style…","");
const response=await fetch(REFERENCE_STYLE,{cache:"no-store"});
if(!response.ok)throw new Error("HTTP " + response.status);
const raw=await response.json();
const checked=sanitizeReferenceStyle(raw);
const removedText=checked.removed.length
? ";忽略无效 layer: " + checked.removed.join(", ")
: "";
rightMap.setStyle(checked.style);
leftMap.setStyle(cloneReferenceForLocal(checked.style));
setState(rightState,"参考 style 已加载" + removedText,"ok");
}catch(err){
console.error(err);
setState(rightState,"参考 style 加载失败:" + (err.message || err),"err");
setState(leftState,"等待有效参考 style","err");
}
}
leftMap.on("style.load",function(){
const sources=Object.keys((leftMap.getStyle() || {}).sources || {});
if(sources.includes("navsea_delivery"))setState(leftState,"Production v2 PBF 已接入","ok");
applyDebugStyles();
});
rightMap.on("style.load",applyDebugStyles);
loadComparisonStyles();
function cameraOf(map){
const c=map.getCenter();
return {center:[c.lng,c.lat],zoom:map.getZoom(),bearing:map.getBearing(),pitch:map.getPitch()};
}
function syncCamera(from,to){
if(syncing || !syncToggle.checked)return;
syncing=true;
try{to.jumpTo(cameraOf(from));}finally{syncing=false;}
}
leftMap.on("move",function(){syncCamera(leftMap,rightMap);});
rightMap.on("move",function(){syncCamera(rightMap,leftMap);});
function updateReadout(map){
const c=map.getCenter();
centerReadout.textContent="z=" + map.getZoom().toFixed(2) + " " +
c.lng.toFixed(5) + ", " + c.lat.toFixed(5) + " b=" + map.getBearing().toFixed(1) + "°";
}
leftMap.on("move",function(){updateReadout(leftMap);});
rightMap.on("move",function(){updateReadout(rightMap);});
updateReadout(leftMap);
document.getElementById("reset-btn").addEventListener("click",function(){
leftMap.fitBounds(KYUSHU_BOUNDS,{padding:36,duration:0});
syncCamera(leftMap,rightMap);
});
document.getElementById("debug-toggle-btn").addEventListener("click",function(){
debugPanel.classList.toggle("collapsed");
});
document.getElementById("debug-collapse-btn").addEventListener("click",function(){
debugPanel.classList.add("collapsed");
});
debugRefBathy.addEventListener("change",applyDebugStyles);
debugHighlightDepcnt.addEventListener("change",applyDebugStyles);
debugInspector.addEventListener("change",function(){
applyDebugStyles();
if(!debugInspector.checked){
if(leftMarker){leftMarker.remove();leftMarker=null;}
if(rightMarker){rightMarker.remove();rightMarker=null;}
}
});
function featureKey(f){
const p=f.properties || {};
return [
f.layer && f.layer.id,
f.sourceLayer || "",
f.id == null ? "" : f.id,
p.canonical_object_type || "",
p.source_object_class || ""
].join("|");
}
function summarize(f){
const p=f.properties || {};
const bits=[
"layer=" + ((f.layer && f.layer.id) || "-"),
"source-layer=" + (f.sourceLayer || "-"),
"id=" + (f.id == null ? "-" : f.id),
p.canonical_object_type ? "type=" + p.canonical_object_type : "",
p.source_object_class ? "class=" + p.source_object_class : "",
p.chart_label_text ? "label=" + p.chart_label_text : "",
p.s52_runtime_csp ? "csp=" + p.s52_runtime_csp : "",
p.s52_csp ? "s52=" + p.s52_csp : "",
p.depth_min_m !== undefined ? "dmin=" + p.depth_min_m : "",
p.depth_max_m !== undefined ? "dmax=" + p.depth_max_m : "",
p.contour_depth_m !== undefined ? "contour=" + p.contour_depth_m : "",
p.s52_hazard_depth_m !== undefined ? "hazard=" + p.s52_hazard_depth_m : ""
].filter(Boolean);
return bits.join(" ");
}
function inspect(map,lngLat,element){
if(!map.loaded()){
element.textContent="地图仍在加载。";
return;
}
const point=map.project(lngLat);
const box=[[point.x-5,point.y-5],[point.x+5,point.y+5]];
let features=[];
try{features=map.queryRenderedFeatures(box);}catch(err){console.warn(err);}
const unique=new Map();
for(const f of features){
const key=featureKey(f);
if(!unique.has(key))unique.set(key,f);
}
const rows=Array.from(unique.values()).slice(0,24);
element.classList.toggle("empty",rows.length===0);
if(rows.length){
element.textContent=lngLat.lng.toFixed(6) + ", " + lngLat.lat.toFixed(6) +
"\n命中 " + unique.size + " 项(显示前 " + rows.length + " 项)\n" +
rows.map(function(f,i){return String(i+1).padStart(2,"0") + ". " + summarize(f);}).join("\n");
}else{
element.textContent=lngLat.lng.toFixed(6) + ", " + lngLat.lat.toFixed(6) +
"\n此位置没有命中已渲染 Feature。";
}
}
let leftMarker=null;
let rightMarker=null;
function compareAt(lngLat){
if(!debugInspector.checked)return;
if(leftMarker)leftMarker.remove();
if(rightMarker)rightMarker.remove();
leftMarker=new maplibregl.Marker().setLngLat(lngLat).addTo(leftMap);
rightMarker=new maplibregl.Marker().setLngLat(lngLat).addTo(rightMap);
inspect(leftMap,lngLat,document.getElementById("left-inspector"));
inspect(rightMap,lngLat,document.getElementById("right-inspector"));
}
leftMap.on("click",function(e){compareAt(e.lngLat);});
rightMap.on("click",function(e){compareAt(e.lngLat);});
function mapError(side,el,event){
const err=event && event.error ? event.error : event;
const msg=err && err.message ? err.message : String(err || "unknown");
console.error(side,msg);
if(msg.indexOf("404")<0)setState(el,side + "错误:" + msg,"err");
}
leftMap.on("error",function(e){mapError("左侧",leftState,e);});
rightMap.on("error",function(e){mapError("右侧",rightState,e);});
window.addEventListener("resize",function(){
leftMap.resize();
rightMap.resize();
});
</script>
</body>
</html>