306 lines
14 KiB
HTML
306 lines
14 KiB
HTML
<!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>
|