audit full semantic extent and AOI visuals

This commit is contained in:
OpenAI Codex
2026-04-18 09:43:42 +08:00
parent 4f96d7be91
commit d50b127eae
9 changed files with 5113 additions and 1 deletions

View File

@@ -0,0 +1,639 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>NavSea Full Audit Compare</title>
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
<style>
:root {
--bg: #eef2ed;
--panel: rgba(250, 248, 242, 0.94);
--ink: #17232b;
--muted: #667780;
--line: rgba(23, 35, 43, 0.12);
--shadow: 0 14px 28px rgba(23, 35, 43, 0.14);
--left: #305f72;
--right: #8b5e34;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
height: 100%;
background:
radial-gradient(circle at top left, rgba(48, 95, 114, 0.12), transparent 28%),
radial-gradient(circle at bottom right, rgba(139, 94, 52, 0.1), transparent 24%),
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;
gap: 18px;
align-items: flex-start;
padding: 14px 18px 12px;
background: linear-gradient(180deg, rgba(250, 248, 242, 0.98), rgba(250, 248, 242, 0.9));
border-bottom: 1px solid var(--line);
box-shadow: var(--shadow);
z-index: 20;
flex-wrap: wrap;
}
.title {
font-size: 18px;
font-weight: 700;
letter-spacing: 0.02em;
}
.subtitle {
margin-top: 4px;
max-width: 920px;
font-size: 13px;
line-height: 1.5;
color: var(--muted);
}
.toolbar-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.version-chip {
display: inline-flex;
align-items: center;
min-height: 40px;
padding: 0 14px;
border-radius: 999px;
border: 1px solid rgba(23, 35, 43, 0.16);
background: rgba(250, 248, 242, 0.9);
color: var(--ink);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.01em;
white-space: nowrap;
box-shadow: 0 10px 22px rgba(23, 35, 43, 0.08);
}
button {
height: 40px;
padding: 0 14px;
border-radius: 12px;
border: 1px solid rgba(23, 35, 43, 0.16);
background: linear-gradient(180deg, #234a59, #17333f);
color: #fff;
font: inherit;
font-weight: 700;
cursor: pointer;
box-shadow: 0 10px 22px rgba(23, 35, 43, 0.18);
}
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
min-height: 0;
position: relative;
}
.pane {
position: relative;
min-width: 0;
border-right: 1px solid var(--line);
}
.pane:last-child {
border-right: none;
}
.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(23, 35, 43, 0.08);
box-shadow: 0 12px 24px rgba(23, 35, 43, 0.12);
backdrop-filter: blur(6px);
}
.pane-tag strong {
display: block;
margin-bottom: 3px;
font-size: 13px;
}
.pane-tag span {
display: block;
font-size: 12px;
color: var(--muted);
}
.inspect-panel {
position: fixed;
right: 14px;
bottom: 14px;
z-index: 30;
width: min(380px, calc(100vw - 28px));
max-height: 42vh;
overflow: auto;
padding: 11px 12px;
border-radius: 14px;
background: rgba(250, 248, 242, 0.95);
border: 1px solid rgba(23, 35, 43, 0.12);
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}
.inspect-panel h3 {
margin: 0 0 6px;
font-size: 13px;
}
.inspect-hint {
margin: 0 0 8px;
color: var(--muted);
font-size: 11px;
line-height: 1.45;
}
.inspect-meta {
display: grid;
grid-template-columns: 96px 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(23, 35, 43, 0.93);
color: #eef4f7;
font-size: 11px;
line-height: 1.4;
white-space: pre-wrap;
word-break: break-word;
overflow: auto;
}
.status {
position: fixed;
left: 50%;
bottom: 14px;
transform: translateX(-50%);
z-index: 30;
max-width: calc(100% - 420px);
padding: 8px 12px;
border-radius: 999px;
background: rgba(23, 35, 43, 0.84);
color: #f7f9fb;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
body.audit-mode .pane-tag,
body.audit-mode .inspect-panel,
body.audit-mode .status,
body.audit-mode .maplibregl-ctrl-top-right,
body.audit-mode .maplibregl-ctrl-bottom-right,
body.audit-mode .maplibregl-ctrl-bottom-left {
display: none !important;
}
@media (max-width: 980px) {
.layout {
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
.pane {
border-right: none;
border-bottom: 1px solid var(--line);
}
.pane:last-child {
border-bottom: none;
}
.inspect-panel {
width: min(360px, calc(100vw - 24px));
max-height: 30vh;
right: 12px;
bottom: 54px;
}
.status {
max-width: calc(100% - 24px);
}
}
</style>
</head>
<body>
<div class="toolbar">
<div>
<div id="page-title" class="title">全国人工审计对比页</div>
<div id="page-subtitle" class="subtitle">左侧固定加载原始 <code>style.json</code> + 原始 newpec 瓦片;右侧固定加载全国 delivery 的 <code>style.navsea-delivery-full.json</code> + 全国 delivery PBF + 当前 sprite。两边联动同步适合直接审 style / pbf / sprite 的整体视觉差异。</div>
</div>
<div class="toolbar-actions">
<div id="html-chip" class="version-chip">HTML: 加载中</div>
<div id="style-chip" class="version-chip">Style: 加载中</div>
<div id="pbf-chip" class="version-chip">PBF: 加载中</div>
<div id="sprite-chip" class="version-chip">Sprite: 加载中</div>
<button id="reload-btn" type="button">重新加载</button>
<button id="home-btn" type="button">回到全国</button>
</div>
</div>
<div class="layout">
<section class="pane">
<div class="pane-tag">
<strong style="color: var(--left);">左侧 · 原始版</strong>
<span>style.json + 原始 newpec 瓦片</span>
</div>
<div id="map-original" class="map"></div>
</section>
<section class="pane">
<div class="pane-tag">
<strong style="color: var(--right);">右侧 · Delivery</strong>
<span id="delivery-pane-label">全国 delivery style + 全国 delivery PBF + 当前 sprite</span>
</div>
<div id="map-delivery" class="map"></div>
</section>
</div>
<aside class="inspect-panel">
<h3>点击对比</h3>
<p id="inspect-hint" class="inspect-hint">点击左右任一地图对象,查看当前命中的 source、source-layer、render layer 和属性。</p>
<dl class="inspect-meta">
<dt>面板</dt>
<dd id="inspect-side">-</dd>
<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 HTML_VERSION = "full-audit-r2-20260416-1504";
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 searchParams = new URLSearchParams(window.location.search);
const auditMode = searchParams.get("audit") === "1";
const variantParam = (searchParams.get("variant") || "semantic").trim().toLowerCase();
const PROFILES = {
full: {
label: "全国",
pageTitle: "全国人工审计对比页",
subtitle: "左侧固定加载原始 <code>style.json</code> + 原始 newpec 瓦片;右侧固定加载全国 delivery 版 <code>style.navsea-delivery-full.json</code> + 全国 delivery PBF。两边联动同步适合直接做原始版与全国 delivery 的截图对比审计。",
deliveryPaneLabel: "全国 delivery style + 全国 delivery PBF",
deliveryStyleUrl: "./domain/style.navsea-delivery-full.json",
deliveryTileUrl: "http://192.168.200.184/pbf-delivery-full-20260415/{z}/{x}/{y}.pbf",
deliveryStyleVersion: "full-delivery-style-current",
deliveryPbfVersion: "full-delivery-pbf-20260415",
deliverySpriteVersion: "newpec-sprite-current",
deliveryStyleFile: "style.navsea-delivery-full.json",
deliveryPbfName: "pbf-delivery-full-20260415",
deliverySpriteName: "newpec/sprite/sprite",
initialView: {
center: [136.5, 35.8],
zoom: 4.8,
bearing: 0,
pitch: 0
},
homeLabel: "回到全国"
},
semantic: {
label: "全国",
pageTitle: "全国人工审计对比页",
subtitle: "左侧固定加载原始 <code>style.json</code> + 原始 newpec 瓦片;右侧固定加载全国 delivery 的语义版 <code>style.navsea-delivery-full-semantic.json</code> + 语义版全国 delivery PBF + 语义版 sprite atlas。两边联动同步适合直接做原始版与全国语义版的截图对比审计。",
deliveryPaneLabel: "全国语义版 delivery style + 语义版全国 delivery PBF + 语义版 sprite",
deliveryStyleUrl: "./domain/style.navsea-delivery-full-semantic.json",
deliveryTileUrl: "http://192.168.200.184/pbf-delivery-full-semantic-20260416/{z}/{x}/{y}.pbf",
deliveryStyleVersion: "full-semantic-style-r3-20260416-2210",
deliveryPbfVersion: "full-semantic-pbf-r2-20260416-2248-hazardfix",
deliverySpriteVersion: "full-semantic-sprite-r1-20260416-1628",
deliveryStyleFile: "style.navsea-delivery-full-semantic.json",
deliveryPbfName: "pbf-delivery-full-semantic-20260416",
deliverySpriteName: "newpec/sprite-semantic/sprite",
initialView: {
center: [136.5, 35.8],
zoom: 4.8,
bearing: 0,
pitch: 0
},
homeLabel: "回到全国"
}
};
const PROFILE = PROFILES[variantParam] || PROFILES.semantic;
const reloadBtn = document.getElementById("reload-btn");
const homeBtn = document.getElementById("home-btn");
const pageTitleEl = document.getElementById("page-title");
const pageSubtitleEl = document.getElementById("page-subtitle");
const deliveryPaneLabelEl = document.getElementById("delivery-pane-label");
const htmlChip = document.getElementById("html-chip");
const styleChip = document.getElementById("style-chip");
const pbfChip = document.getElementById("pbf-chip");
const spriteChip = document.getElementById("sprite-chip");
const statusEl = document.getElementById("status");
const inspectHintEl = document.getElementById("inspect-hint");
const inspectSideEl = document.getElementById("inspect-side");
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 originalMap;
let deliveryMap;
let syncLock = false;
let styleVersion = `${HTML_VERSION}-${Date.now()}`;
htmlChip.textContent = `HTML: ${HTML_VERSION}`;
if (auditMode) document.body.classList.add("audit-mode");
function parseNumberParam(value) {
if (value === null || value === undefined || value === "") return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function getRequestedCamera() {
const centerValue = searchParams.get("center");
const zoomValue = parseNumberParam(searchParams.get("zoom"));
const bearingValue = parseNumberParam(searchParams.get("bearing"));
const pitchValue = parseNumberParam(searchParams.get("pitch"));
if (!centerValue && zoomValue === null && bearingValue === null && pitchValue === null) {
return {
center: PROFILE.initialView.center,
zoom: PROFILE.initialView.zoom,
bearing: PROFILE.initialView.bearing,
pitch: PROFILE.initialView.pitch
};
}
let center = PROFILE.initialView.center;
if (centerValue) {
const parts = centerValue.split(",").map((item) => Number(item.trim()));
if (parts.length === 2 && parts.every(Number.isFinite)) {
center = [parts[0], parts[1]];
}
}
return {
center,
zoom: zoomValue ?? PROFILE.initialView.zoom,
bearing: bearingValue ?? PROFILE.initialView.bearing,
pitch: pitchValue ?? PROFILE.initialView.pitch
};
}
function applyProfileUi() {
pageTitleEl.textContent = PROFILE.pageTitle;
pageSubtitleEl.innerHTML = PROFILE.subtitle;
deliveryPaneLabelEl.textContent = PROFILE.deliveryPaneLabel;
homeBtn.textContent = PROFILE.homeLabel;
styleChip.textContent = `Style: ${PROFILE.deliveryStyleVersion} (${PROFILE.deliveryStyleFile})`;
pbfChip.textContent = `PBF: ${PROFILE.deliveryPbfVersion} (${PROFILE.deliveryPbfName})`;
spriteChip.textContent = `Sprite: ${PROFILE.deliverySpriteVersion} (${PROFILE.deliverySpriteName})`;
document.title = `NavSea Compare · ${PROFILE.label}`;
}
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, styleVersion), { cache: "no-store" });
if (!response.ok) {
throw new Error(`加载样式失败: ${url}`);
}
return response.json();
}
function touchStyleAssets(style) {
if (style.sprite) style.sprite = withCacheBuster(style.sprite, styleVersion);
if (style.glyphs) style.glyphs = withCacheBuster(style.glyphs, styleVersion);
Object.values(style.sources || {}).forEach((source) => {
if (Array.isArray(source.tiles)) {
source.tiles = source.tiles.map((tileUrl) => withCacheBuster(tileUrl, styleVersion));
}
if (typeof source.url === "string") {
source.url = withCacheBuster(source.url, styleVersion);
}
});
return style;
}
function buildOriginalStyle(style) {
const next = structuredClone(style);
if (next.sources?.newpec) {
next.sources.newpec.tiles = [withCacheBuster(ORIGINAL_TILE_URL, styleVersion)];
}
return touchStyleAssets(next);
}
function buildDeliveryStyle(style) {
const next = structuredClone(style);
if (next.sources?.navsea_delivery) {
next.sources.navsea_delivery.tiles = [withCacheBuster(PROFILE.deliveryTileUrl, styleVersion)];
}
return touchStyleAssets(next);
}
function resetInspectPanel(message) {
inspectHintEl.textContent = message;
inspectSideEl.textContent = "-";
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(side, event, feature) {
inspectHintEl.textContent = "已选中对象。继续点击可对照两侧差异。";
inspectSideEl.textContent = side;
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);
}
function bindInspect(map, sideLabel) {
map.on("click", (event) => {
const features = map.queryRenderedFeatures(event.point);
if (!features.length) {
resetInspectPanel(`${sideLabel}点击位置没有命中对象。点击坐标: ${formatLngLat(event.lngLat)}`);
return;
}
updateInspectPanel(sideLabel, event, features[0]);
});
}
function syncMaps(primary, secondary) {
primary.on("move", () => {
if (syncLock) return;
syncLock = true;
secondary.jumpTo({
center: primary.getCenter(),
zoom: primary.getZoom(),
bearing: primary.getBearing(),
pitch: primary.getPitch()
});
syncLock = false;
});
}
async function loadMaps() {
const requestedCamera = getRequestedCamera();
styleVersion = `${HTML_VERSION}-${Date.now()}`;
reloadBtn.disabled = true;
homeBtn.disabled = true;
setStatus(`正在加载全国原始版和 delivery 版样式… HTML ${HTML_VERSION}`);
try {
const [originalStyleRaw, deliveryStyleRaw] = await Promise.all([
fetchStyle(ORIGINAL_STYLE_URL),
fetchStyle(PROFILE.deliveryStyleUrl)
]);
const originalStyle = buildOriginalStyle(originalStyleRaw);
const deliveryStyle = buildDeliveryStyle(deliveryStyleRaw);
if (!originalMap && !deliveryMap) {
originalMap = new maplibregl.Map({
container: "map-original",
style: originalStyle,
center: requestedCamera.center,
zoom: requestedCamera.zoom,
bearing: requestedCamera.bearing,
pitch: requestedCamera.pitch
});
deliveryMap = new maplibregl.Map({
container: "map-delivery",
style: deliveryStyle,
center: requestedCamera.center,
zoom: requestedCamera.zoom,
bearing: requestedCamera.bearing,
pitch: requestedCamera.pitch
});
originalMap.addControl(new maplibregl.NavigationControl(), "top-right");
deliveryMap.addControl(new maplibregl.NavigationControl(), "top-right");
bindInspect(originalMap, "左侧原始版");
bindInspect(deliveryMap, "右侧Delivery");
syncMaps(originalMap, deliveryMap);
syncMaps(deliveryMap, originalMap);
let loaded = 0;
function markLoaded() {
loaded += 1;
if (loaded >= 2) {
setStatus(`全国双屏已加载可直接拖动、缩放、点击对比。HTML ${HTML_VERSION}`);
}
}
originalMap.on("load", markLoaded);
deliveryMap.on("load", markLoaded);
} else {
const camera = requestedCamera;
let loaded = 0;
function markLoaded() {
loaded += 1;
if (loaded >= 2) {
originalMap.jumpTo(camera);
deliveryMap.jumpTo(camera);
setStatus(`全国双屏样式已重新加载。HTML ${HTML_VERSION}`);
}
}
originalMap.once("style.load", markLoaded);
deliveryMap.once("style.load", markLoaded);
originalMap.setStyle(originalStyle, { diff: false });
deliveryMap.setStyle(deliveryStyle, { diff: false });
}
} catch (error) {
console.error(error);
setStatus(error.message || "加载失败");
} finally {
reloadBtn.disabled = false;
homeBtn.disabled = false;
}
}
reloadBtn.addEventListener("click", loadMaps);
homeBtn.addEventListener("click", () => {
if (!originalMap || !deliveryMap) return;
const camera = {
center: PROFILE.initialView.center,
zoom: PROFILE.initialView.zoom,
bearing: PROFILE.initialView.bearing,
pitch: PROFILE.initialView.pitch
};
window.history.replaceState({}, "", window.location.pathname);
originalMap.jumpTo(camera);
deliveryMap.jumpTo(camera);
setStatus("已回到全国初始视角。");
});
applyProfileUi();
resetInspectPanel("点击左右任一地图对象,查看当前命中的 source、source-layer、render layer 和属性。");
loadMaps();
</script>
</body>
</html>