Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- README.md +14 -10
- app.py +44 -31
- graph.sqlite +2 -2
- web/app.js +79 -65
- web/index.html +34 -33
- web/styles.css +15 -7
README.md
CHANGED
|
@@ -11,7 +11,7 @@ license: mit
|
|
| 11 |
|
| 12 |
# SetVL Node Graph Viewer
|
| 13 |
|
| 14 |
-
只读浏览 SetVL-480K 全库的 node graph:**12,250 个场景、119,996 个节点、
|
| 15 |
|
| 16 |
界面与本地的标注复核台一致——三栏布局、J/K 键盘导航、卫星图/电子地图切换、
|
| 17 |
邻接矩阵、街景放大。区别是**去掉了全部写入**:没有判定按钮,后端也没有对应接口。
|
|
@@ -19,17 +19,21 @@ license: mit
|
|
| 19 |
## 怎么看
|
| 20 |
|
| 21 |
- **左栏**:全库场景浏览。可按城市、scene_id、最少节点数筛选,
|
| 22 |
-
也可只看含孤儿 / 含
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
<span style="color:#
|
|
|
|
|
|
|
|
|
|
| 27 |
`B` 切换电子地图,`E` 切换沿路走线/直线。
|
| 28 |
- **右栏**:选中节点或边的完整属性与自动判定信号。
|
| 29 |
-
- **街景
|
| 30 |
-
(`NE 40°`、`SE 130°`…
|
| 31 |
-
|
|
|
|
| 32 |
|
| 33 |
图片存在配套的私有 Dataset 里,由 CDN 按需分发。
|
| 34 |
|
| 35 |
-
建图规则见 `
|
|
|
|
| 11 |
|
| 12 |
# SetVL Node Graph Viewer
|
| 13 |
|
| 14 |
+
只读浏览 SetVL-480K 全库的 node graph:**12,250 个场景、119,996 个节点、120,363 条边**(策略 `merged_policy_0915`)。
|
| 15 |
|
| 16 |
界面与本地的标注复核台一致——三栏布局、J/K 键盘导航、卫星图/电子地图切换、
|
| 17 |
邻接矩阵、街景放大。区别是**去掉了全部写入**:没有判定按钮,后端也没有对应接口。
|
|
|
|
| 19 |
## 怎么看
|
| 20 |
|
| 21 |
- **左栏**:全库场景浏览。可按城市、scene_id、最少节点数筛选,
|
| 22 |
+
也可只看含孤儿节点 / 含带标记的边的场景,或按 §1-§4 的边类筛。
|
| 23 |
+
`J`/`K` 上下,`[`/`]` 翻页。
|
| 24 |
+
- **中栏**:卫星底图上叠出图结构。节点按所属走廊着色,红圈是孤儿(§6);
|
| 25 |
+
边按 §1-§4 的四类着色——
|
| 26 |
+
<span style="color:#6ba8e8">蓝 = §1 同走廊相邻</span>、
|
| 27 |
+
<span style="color:#68c8a0">绿 = §2 路网可达</span>、
|
| 28 |
+
<span style="color:#f1a43c">橙 = §3 短直连</span>、
|
| 29 |
+
<span style="color:#e8646b">红 = §4 分量合并</span>。
|
| 30 |
`B` 切换电子地图,`E` 切换沿路走线/直线。
|
| 31 |
- **右栏**:选中节点或边的完整属性与自动判定信号。
|
| 32 |
+
- **街景**:每个 pano 的 4 个朝向**同时显示**在一个 2×2 方格里,各自标出
|
| 33 |
+
方位(`NE 40°`、`SE 130°`…,按正北起顺时针排)。不用点任何按钮就能看全一个
|
| 34 |
+
路口。鼠标悬在某张图上,中栏地图里对应的那支箭头会同步放大;反过来悬在
|
| 35 |
+
箭头上也会点亮那张图。点图放大,`H` 键依次放大看。
|
| 36 |
|
| 37 |
图片存在配套的私有 Dataset 里,由 CDN 按需分发。
|
| 38 |
|
| 39 |
+
建图规则见 `merged_policy_0915.md`(在源仓库里,**不随本 Space 一起上传**)。
|
app.py
CHANGED
|
@@ -119,21 +119,18 @@ def effective(nodes: List[Dict], edges: List[Dict]) -> None:
|
|
| 119 |
o = n["is_orphan_human"]
|
| 120 |
n["is_orphan"] = int(o) if o is not None else (n["is_orphan_auto"] or 0)
|
| 121 |
n["corridor_overridden"] = n["corridor_id_human"] is not None
|
| 122 |
-
n["
|
| 123 |
for e in edges:
|
| 124 |
-
e["origin"] = e["origin_human"] or e["origin_auto"] or "
|
| 125 |
rc = e["road_connected_human"]
|
| 126 |
e["road_connected"] = int(rc) if rc is not None else (e["road_connected_auto"] or 0)
|
| 127 |
-
hc = e["high_cost_human"]
|
| 128 |
-
e["high_cost"] = int(hc) if hc is not None else (e["high_cost_auto"] or 0)
|
| 129 |
-
e["cost_class"] = e["cost_class_auto"]
|
| 130 |
e["verdict"] = e["edge_verdict_human"]
|
| 131 |
e["in_graph"] = e["edge_verdict_human"] != "reject"
|
| 132 |
e["is_manual"] = e["origin_human"] == "manual" or e["origin_auto"] is None
|
| 133 |
e["origin_overridden"] = e["origin_human"] is not None
|
| 134 |
e["road_path"] = _loads(e["road_path_auto"])
|
| 135 |
e["evidence"] = _loads(e["evidence_auto"])
|
| 136 |
-
e["
|
| 137 |
e["review_status"] = e["review_status_human"] or "unreviewed"
|
| 138 |
|
| 139 |
|
|
@@ -178,9 +175,11 @@ def stats_payload() -> Dict[str, Any]:
|
|
| 178 |
"edges": q("SELECT COUNT(*) FROM edge"),
|
| 179 |
"total_items": q("SELECT COUNT(*) FROM node") + q("SELECT COUNT(*) FROM edge"),
|
| 180 |
"origins": origins,
|
| 181 |
-
"
|
|
|
|
| 182 |
"orphans": q("SELECT COUNT(*) FROM node WHERE is_orphan_auto=1"),
|
| 183 |
-
"
|
|
|
|
| 184 |
}
|
| 185 |
|
| 186 |
|
|
@@ -191,7 +190,7 @@ def stats() -> JSONResponse:
|
|
| 191 |
|
| 192 |
@api.get("/api/scenes")
|
| 193 |
def scenes(city: str = "", min_nodes: Optional[int] = None,
|
| 194 |
-
orphan: str = "",
|
| 195 |
q: str = "", order: str = "scene_id",
|
| 196 |
limit: int = 60, offset: int = 0) -> JSONResponse:
|
| 197 |
where, args = [], []
|
|
@@ -207,11 +206,13 @@ def scenes(city: str = "", min_nodes: Optional[int] = None,
|
|
| 207 |
if orphan in ("1", "true"):
|
| 208 |
where.append("EXISTS (SELECT 1 FROM node n WHERE n.scene_id=s.scene_id "
|
| 209 |
"AND n.is_orphan_auto=1)")
|
| 210 |
-
if
|
| 211 |
where.append("EXISTS (SELECT 1 FROM edge e WHERE e.scene_id=s.scene_id "
|
| 212 |
-
"AND e.
|
| 213 |
-
if
|
| 214 |
-
where.append("
|
|
|
|
|
|
|
| 215 |
clause = (" WHERE " + " AND ".join(where)) if where else ""
|
| 216 |
order_sql = {"scene_id": "s.scene_id",
|
| 217 |
"nodes_desc": "s.node_count DESC, s.scene_id",
|
|
@@ -219,12 +220,13 @@ def scenes(city: str = "", min_nodes: Optional[int] = None,
|
|
| 219 |
limit = max(1, min(int(limit), 200))
|
| 220 |
rows = con().execute(
|
| 221 |
"SELECT s.scene_id, s.city, s.node_count, s.satellite_path, "
|
| 222 |
-
"s.
|
| 223 |
"(SELECT COUNT(*) FROM edge e WHERE e.scene_id=s.scene_id) AS edge_count, "
|
| 224 |
"(SELECT COUNT(*) FROM node n WHERE n.scene_id=s.scene_id AND n.is_orphan_auto=1) "
|
| 225 |
" AS orphan_count, "
|
| 226 |
-
"(SELECT COUNT(*) FROM edge e WHERE e.scene_id=s.scene_id
|
| 227 |
-
"
|
|
|
|
| 228 |
f"FROM scene s{clause} ORDER BY {order_sql} LIMIT ? OFFSET ?",
|
| 229 |
args + [limit, max(0, int(offset))])
|
| 230 |
total = con().execute(f"SELECT COUNT(*) FROM scene s{clause}", args).fetchone()[0]
|
|
@@ -233,43 +235,54 @@ def scenes(city: str = "", min_nodes: Optional[int] = None,
|
|
| 233 |
|
| 234 |
|
| 235 |
@api.get("/api/queue")
|
| 236 |
-
def queue(city: str = "",
|
| 237 |
-
tier: str = "", include_reviewed: str = "",
|
| 238 |
limit: int = 60, offset: int = 0) -> JSONResponse:
|
| 239 |
-
"""「带
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
parts, args = [], []
|
| 241 |
limit = max(1, min(int(limit), 200))
|
| 242 |
for tbl in ("node", "edge"):
|
| 243 |
if kind and kind != tbl:
|
| 244 |
continue
|
| 245 |
key = "t.label" if tbl == "node" else "t.source_label || '-' || t.target_label"
|
| 246 |
-
cond = ["t.triage_signals_auto IS NOT NULL", "t.triage_signals_auto != '[]'"]
|
| 247 |
sub: List[Any] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
if city:
|
| 249 |
cond.append("s.city = ?")
|
| 250 |
sub.append(city)
|
| 251 |
-
if signal:
|
| 252 |
-
cond.append("EXISTS (SELECT 1 FROM json_each(t.triage_signals_auto) je "
|
| 253 |
-
"WHERE json_extract(je.value,'$.code') = ?)")
|
| 254 |
-
sub.append(signal)
|
| 255 |
parts.append(
|
| 256 |
f"SELECT '{tbl}' AS kind, t.scene_id AS scene_id, s.city AS city, "
|
| 257 |
-
f"{key} AS item_key,
|
| 258 |
-
"
|
| 259 |
-
"(t.confidence_auto IS NULL) AS conf_null, "
|
| 260 |
-
"t.triage_signals_auto AS signals, 'unreviewed' AS review_status "
|
| 261 |
f"FROM {tbl} t JOIN scene s ON s.scene_id=t.scene_id "
|
| 262 |
"WHERE " + " AND ".join(cond))
|
| 263 |
args += sub
|
| 264 |
if not parts:
|
| 265 |
return JSONResponse({"items": [], "limit": limit, "offset": offset})
|
| 266 |
sql = (" UNION ALL ".join(parts) +
|
| 267 |
-
" ORDER BY
|
| 268 |
items = []
|
| 269 |
for r in con().execute(sql, args + [limit, max(0, int(offset))]):
|
| 270 |
d = dict(r)
|
| 271 |
-
d.pop("
|
| 272 |
-
d["signals"] = _loads(d.pop("signals")) or []
|
| 273 |
items.append(d)
|
| 274 |
return JSONResponse({"items": items, "limit": limit, "offset": offset})
|
| 275 |
|
|
|
|
| 119 |
o = n["is_orphan_human"]
|
| 120 |
n["is_orphan"] = int(o) if o is not None else (n["is_orphan_auto"] or 0)
|
| 121 |
n["corridor_overridden"] = n["corridor_id_human"] is not None
|
| 122 |
+
n["corridor_ids"] = _loads(n["corridor_ids_auto"]) or []
|
| 123 |
for e in edges:
|
| 124 |
+
e["origin"] = e["origin_human"] or e["origin_auto"] or "manual"
|
| 125 |
rc = e["road_connected_human"]
|
| 126 |
e["road_connected"] = int(rc) if rc is not None else (e["road_connected_auto"] or 0)
|
|
|
|
|
|
|
|
|
|
| 127 |
e["verdict"] = e["edge_verdict_human"]
|
| 128 |
e["in_graph"] = e["edge_verdict_human"] != "reject"
|
| 129 |
e["is_manual"] = e["origin_human"] == "manual" or e["origin_auto"] is None
|
| 130 |
e["origin_overridden"] = e["origin_human"] is not None
|
| 131 |
e["road_path"] = _loads(e["road_path_auto"])
|
| 132 |
e["evidence"] = _loads(e["evidence_auto"])
|
| 133 |
+
e["notes"] = _loads(e["notes_auto"]) or []
|
| 134 |
e["review_status"] = e["review_status_human"] or "unreviewed"
|
| 135 |
|
| 136 |
|
|
|
|
| 175 |
"edges": q("SELECT COUNT(*) FROM edge"),
|
| 176 |
"total_items": q("SELECT COUNT(*) FROM node") + q("SELECT COUNT(*) FROM edge"),
|
| 177 |
"origins": origins,
|
| 178 |
+
"noted_edges": q("SELECT COUNT(*) FROM edge WHERE notes_auto IS NOT NULL "
|
| 179 |
+
"AND notes_auto <> '[]'"),
|
| 180 |
"orphans": q("SELECT COUNT(*) FROM node WHERE is_orphan_auto=1"),
|
| 181 |
+
"policy": q("SELECT graph_version FROM scene WHERE graph_version IS NOT NULL "
|
| 182 |
+
"LIMIT 1") or "",
|
| 183 |
}
|
| 184 |
|
| 185 |
|
|
|
|
| 190 |
|
| 191 |
@api.get("/api/scenes")
|
| 192 |
def scenes(city: str = "", min_nodes: Optional[int] = None,
|
| 193 |
+
orphan: str = "", noted: str = "", origin: str = "",
|
| 194 |
q: str = "", order: str = "scene_id",
|
| 195 |
limit: int = 60, offset: int = 0) -> JSONResponse:
|
| 196 |
where, args = [], []
|
|
|
|
| 206 |
if orphan in ("1", "true"):
|
| 207 |
where.append("EXISTS (SELECT 1 FROM node n WHERE n.scene_id=s.scene_id "
|
| 208 |
"AND n.is_orphan_auto=1)")
|
| 209 |
+
if noted in ("1", "true"):
|
| 210 |
where.append("EXISTS (SELECT 1 FROM edge e WHERE e.scene_id=s.scene_id "
|
| 211 |
+
"AND e.notes_auto IS NOT NULL AND e.notes_auto <> '[]')")
|
| 212 |
+
if origin:
|
| 213 |
+
where.append("EXISTS (SELECT 1 FROM edge e WHERE e.scene_id=s.scene_id "
|
| 214 |
+
"AND e.origin_auto = ?)")
|
| 215 |
+
args.append(origin)
|
| 216 |
clause = (" WHERE " + " AND ".join(where)) if where else ""
|
| 217 |
order_sql = {"scene_id": "s.scene_id",
|
| 218 |
"nodes_desc": "s.node_count DESC, s.scene_id",
|
|
|
|
| 220 |
limit = max(1, min(int(limit), 200))
|
| 221 |
rows = con().execute(
|
| 222 |
"SELECT s.scene_id, s.city, s.node_count, s.satellite_path, "
|
| 223 |
+
"s.graph_version, s.utm_zone_auto, "
|
| 224 |
"(SELECT COUNT(*) FROM edge e WHERE e.scene_id=s.scene_id) AS edge_count, "
|
| 225 |
"(SELECT COUNT(*) FROM node n WHERE n.scene_id=s.scene_id AND n.is_orphan_auto=1) "
|
| 226 |
" AS orphan_count, "
|
| 227 |
+
"(SELECT COUNT(*) FROM edge e WHERE e.scene_id=s.scene_id "
|
| 228 |
+
" AND e.notes_auto IS NOT NULL AND e.notes_auto <> '[]') AS noted_count, "
|
| 229 |
+
"0 AS revision_count "
|
| 230 |
f"FROM scene s{clause} ORDER BY {order_sql} LIMIT ? OFFSET ?",
|
| 231 |
args + [limit, max(0, int(offset))])
|
| 232 |
total = con().execute(f"SELECT COUNT(*) FROM scene s{clause}", args).fetchone()[0]
|
|
|
|
| 235 |
|
| 236 |
|
| 237 |
@api.get("/api/queue")
|
| 238 |
+
def queue(city: str = "", note: str = "", kind: str = "",
|
|
|
|
| 239 |
limit: int = 60, offset: int = 0) -> JSONResponse:
|
| 240 |
+
"""「带标记的」列表。
|
| 241 |
+
|
| 242 |
+
merged_policy_0915 没有分诊分层和置信度分数,所以这里不再按分数排序。
|
| 243 |
+
列出的是策略本身认为值得看一眼的:§6 的孤儿节点,和带非阻断标记的边
|
| 244 |
+
(§0 跨水/穿建筑、§4 绕远、无道路通路)。
|
| 245 |
+
"""
|
| 246 |
parts, args = [], []
|
| 247 |
limit = max(1, min(int(limit), 200))
|
| 248 |
for tbl in ("node", "edge"):
|
| 249 |
if kind and kind != tbl:
|
| 250 |
continue
|
| 251 |
key = "t.label" if tbl == "node" else "t.source_label || '-' || t.target_label"
|
|
|
|
| 252 |
sub: List[Any] = []
|
| 253 |
+
if tbl == "node":
|
| 254 |
+
if note and note != "orphan":
|
| 255 |
+
continue
|
| 256 |
+
cond = ["t.is_orphan_auto = 1"]
|
| 257 |
+
reason = ("json_array(json_object('code','orphan','snap_m',"
|
| 258 |
+
"t.snap_distance_m_auto))")
|
| 259 |
+
else:
|
| 260 |
+
if note == "orphan":
|
| 261 |
+
continue
|
| 262 |
+
cond = ["t.notes_auto IS NOT NULL", "t.notes_auto <> '[]'"]
|
| 263 |
+
reason = "t.notes_auto"
|
| 264 |
+
if note:
|
| 265 |
+
cond.append("EXISTS (SELECT 1 FROM json_each(t.notes_auto) je "
|
| 266 |
+
"WHERE json_extract(je.value,'$.code') = ?)")
|
| 267 |
+
sub.append(note)
|
| 268 |
if city:
|
| 269 |
cond.append("s.city = ?")
|
| 270 |
sub.append(city)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
parts.append(
|
| 272 |
f"SELECT '{tbl}' AS kind, t.scene_id AS scene_id, s.city AS city, "
|
| 273 |
+
f"{key} AS item_key, {reason} AS notes_json, "
|
| 274 |
+
"'unreviewed' AS review_status "
|
|
|
|
|
|
|
| 275 |
f"FROM {tbl} t JOIN scene s ON s.scene_id=t.scene_id "
|
| 276 |
"WHERE " + " AND ".join(cond))
|
| 277 |
args += sub
|
| 278 |
if not parts:
|
| 279 |
return JSONResponse({"items": [], "limit": limit, "offset": offset})
|
| 280 |
sql = (" UNION ALL ".join(parts) +
|
| 281 |
+
" ORDER BY scene_id, kind, item_key LIMIT ? OFFSET ?")
|
| 282 |
items = []
|
| 283 |
for r in con().execute(sql, args + [limit, max(0, int(offset))]):
|
| 284 |
d = dict(r)
|
| 285 |
+
d["notes"] = _loads(d.pop("notes_json")) or []
|
|
|
|
| 286 |
items.append(d)
|
| 287 |
return JSONResponse({"items": items, "limit": limit, "offset": offset})
|
| 288 |
|
graph.sqlite
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fcdb5ac705bc00a356350061d77c881fd4ee3436710aaeaa2d2989afc2377460
|
| 3 |
+
size 166027264
|
web/app.js
CHANGED
|
@@ -23,6 +23,22 @@
|
|
| 23 |
*/
|
| 24 |
var EDIT = null;
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
var S = {
|
| 27 |
mode: 'scenes', // 'scenes' 浏览全库 | 'queue' 复核队列
|
| 28 |
queue: [], filtered: [], selected: null, // {kind, scene_id, key:[...]}
|
|
@@ -92,9 +108,8 @@ function corridorColor(cid) {
|
|
| 92 |
function queueQuery() {
|
| 93 |
var q = new URLSearchParams();
|
| 94 |
if ($('cityFilter').value) q.set('city', $('cityFilter').value);
|
| 95 |
-
if ($('tierFilter').value) q.set('tier', $('tierFilter').value);
|
| 96 |
if ($('kindFilter').value) q.set('kind', $('kindFilter').value);
|
| 97 |
-
if ($('signalFilter').value) q.set('
|
| 98 |
if ($('showReviewedSel').value) q.set('include_reviewed', '1');
|
| 99 |
q.set('limit', String(S.limit));
|
| 100 |
q.set('offset', String(S.offset));
|
|
@@ -108,8 +123,8 @@ function sceneQuery() {
|
|
| 108 |
if ((v = $('minNodes').value)) q.set('min_nodes', v);
|
| 109 |
if ((v = $('editedFilter').value)) q.set('edited', v);
|
| 110 |
if ($('fOrphan').checked) q.set('orphan', '1');
|
| 111 |
-
if ($('
|
| 112 |
-
if ($('
|
| 113 |
if ((v = $('searchInput').value.trim())) q.set('q', v);
|
| 114 |
q.set('limit', String(S.limit));
|
| 115 |
q.set('offset', String(S.offset));
|
|
@@ -139,8 +154,7 @@ function renderScenes() {
|
|
| 139 |
var badges = '';
|
| 140 |
if (sc.revision_count) badges += '<span class="badge edited">改过 ' + sc.revision_count + '</span>';
|
| 141 |
if (sc.orphan_count) badges += '<span class="badge orphan">孤儿 ' + sc.orphan_count + '</span>';
|
| 142 |
-
if (sc.
|
| 143 |
-
if (sc.net_fragmented_auto) badges += '<span class="badge frag">S1</span>';
|
| 144 |
// 三行压成两行:城市并进第一行(原来单独占一行只为显示一个词),
|
| 145 |
// 没有徽章时整条 meta 行不渲染,而不是留一个占位的破折号。
|
| 146 |
var name = sc.scene_id.split(':')[1] || sc.scene_id;
|
|
@@ -191,11 +205,10 @@ var FILTERS = [
|
|
| 191 |
{ id: 'minNodes', label: '最少节点', mode: 'scenes', type: 'value' },
|
| 192 |
{ id: 'editedFilter', label: '状态', mode: 'scenes', type: 'select' },
|
| 193 |
{ id: 'fOrphan', label: '含孤儿', mode: 'scenes', type: 'check' },
|
| 194 |
-
{ id: '
|
| 195 |
-
{ id: '
|
| 196 |
{ id: 'kindFilter', label: 'Kind', mode: 'queue', type: 'select' },
|
| 197 |
-
{ id: '
|
| 198 |
-
{ id: 'signalFilter', label: 'Signal', mode: 'queue', type: 'select' },
|
| 199 |
{ id: 'showReviewedSel', label: '已复核', mode: 'queue', type: 'select' },
|
| 200 |
];
|
| 201 |
|
|
@@ -260,16 +273,17 @@ function applySearch() {
|
|
| 260 |
});
|
| 261 |
renderList();
|
| 262 |
}
|
| 263 |
-
function
|
| 264 |
-
|
| 265 |
-
return
|
| 266 |
-
var extra = Object.keys(
|
| 267 |
.map(function (k) {
|
| 268 |
-
var v =
|
| 269 |
-
return
|
| 270 |
}).join(' ');
|
| 271 |
-
return '<span class="sig-code">' + esc(
|
| 272 |
-
|
|
|
|
| 273 |
}
|
| 274 |
function itemId(i) { return i.kind + '|' + i.scene_id + '|' + i.item_key; }
|
| 275 |
/* 左栏渲染的统一入口。
|
|
@@ -313,10 +327,9 @@ function renderQueue() {
|
|
| 313 |
+ (i.review_status === 'needs_adjudication' ? ' adjudication' : '');
|
| 314 |
b.innerHTML =
|
| 315 |
'<div class="queue-top"><strong>' + (i.kind === 'node' ? '●' : '─') + ' ' +
|
| 316 |
-
esc(i.item_key) + '</strong><span>' +
|
| 317 |
-
|
| 318 |
-
'<div class="queue-meta">
|
| 319 |
-
signalHtml(i.signals) + '</div>' +
|
| 320 |
'<div class="queue-scene">' + esc(i.scene_id) + '</div>';
|
| 321 |
b.onclick = function () { selectItem(i); };
|
| 322 |
list.appendChild(b);
|
|
@@ -568,10 +581,9 @@ function drawGraph() {
|
|
| 568 |
var geo = S.scene.edges.map(function (e) {
|
| 569 |
var a = pos[e.source_label], b = pos[e.target_label];
|
| 570 |
if (!a || !b) return null;
|
| 571 |
-
//
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
if (!costly && S.edgeMode === 'road' && e.road_path && e.road_path.length > 1) {
|
| 575 |
// 折线两端落在吸附点上,不在节点上——补两段短接头,否则边看着是断的
|
| 576 |
return { e: e, costly: costly, road: true,
|
| 577 |
pts: [a].concat(e.road_path.map(function (q) { return proj(q[0], q[1]); })).concat([b]) };
|
|
@@ -583,8 +595,7 @@ function drawGraph() {
|
|
| 583 |
geo.forEach(function (g, ei) {
|
| 584 |
if (!g) return;
|
| 585 |
var e = g.e;
|
| 586 |
-
var colour =
|
| 587 |
-
|| 'var(--bridge)';
|
| 588 |
var isSel = sel && sel.kind === 'edge'
|
| 589 |
&& sel.key[1] === e.source_label && sel.key[2] === e.target_label;
|
| 590 |
var dead = !e.in_graph;
|
|
@@ -744,15 +755,16 @@ function drawAdjacency() {
|
|
| 744 |
var idx = {};
|
| 745 |
labels.forEach(function (l, i) { idx[l] = i; });
|
| 746 |
var cell = {};
|
| 747 |
-
var counts = {
|
| 748 |
S.scene.edges.forEach(function (e) {
|
| 749 |
cell[e.source_label + '|' + e.target_label] = e;
|
| 750 |
cell[e.target_label + '|' + e.source_label] = e;
|
| 751 |
if (counts[e.origin_auto] != null) counts[e.origin_auto]++;
|
| 752 |
});
|
| 753 |
-
$('
|
| 754 |
-
$('
|
| 755 |
-
$('
|
|
|
|
| 756 |
|
| 757 |
var sel = S.selected;
|
| 758 |
var html = '<tr><th></th>' + labels.map(function (l) { return '<th>' + l + '</th>'; }).join('') + '</tr>';
|
|
@@ -768,7 +780,7 @@ function drawAdjacency() {
|
|
| 768 |
}
|
| 769 |
var isSel = sel && sel.kind === 'edge'
|
| 770 |
&& sel.key[1] === e.source_label && sel.key[2] === e.target_label;
|
| 771 |
-
var cls = 'edge e-' + (e.origin || '
|
| 772 |
(e.in_graph ? '' : ' rejected');
|
| 773 |
html += '<td class="' + cls + '" data-e="' + e.source_label + '-' +
|
| 774 |
e.target_label + '">' + (e.in_graph ? '·' : '×') + '</td>';
|
|
@@ -799,15 +811,14 @@ function drawAdjacency() {
|
|
| 799 |
var isSel = sel && sel.kind === 'edge'
|
| 800 |
&& sel.key[1] === e.source_label && sel.key[2] === e.target_label;
|
| 801 |
d.className = 'edge-row' + (isSel ? ' sel' : '');
|
| 802 |
-
var colour =
|
| 803 |
-
|| 'var(--bridge)';
|
| 804 |
if (!e.in_graph) d.classList.add('rejected');
|
| 805 |
d.innerHTML = '<i style="background:' + colour + '"></i>' +
|
| 806 |
esc(e.source_label) + '–' + esc(e.target_label) +
|
| 807 |
(e.is_manual ? ' <span style="color:var(--cyan)">M</span>' : '') +
|
| 808 |
(e.in_graph ? '' : ' <span style="color:var(--no)">已拒</span>') +
|
| 809 |
-
(e.
|
| 810 |
-
'">
|
| 811 |
'<span class="len">' + (e.euclid_distance_m == null ? '—' : (+e.euclid_distance_m).toFixed(0) + ' m') + '</span>';
|
| 812 |
d.onclick = function () { pickEdge(e); };
|
| 813 |
listEl.appendChild(d);
|
|
@@ -965,43 +976,44 @@ function renderInspector() {
|
|
| 965 |
var isNode = S.selected.kind === 'node';
|
| 966 |
$('decisionTitle').textContent = isNode ? '节点属���' : '边属性';
|
| 967 |
$('itemId').textContent = isNode ? row.label : (row.source_label + '→' + row.target_label);
|
| 968 |
-
var conf = row.confidence_auto;
|
| 969 |
var html = '';
|
| 970 |
|
|
|
|
|
|
|
| 971 |
html += '<section class="signal-block"><div class="block-title">' +
|
| 972 |
-
'<span>
|
| 973 |
-
var
|
| 974 |
-
|
|
|
|
|
|
|
| 975 |
var extra = Object.keys(sg).filter(function (k) { return k !== 'code'; })
|
| 976 |
.map(function (k) {
|
| 977 |
var v = sg[k];
|
| 978 |
return k + '=' + (typeof v === 'number' ? (+v).toFixed(3) : JSON.stringify(v));
|
| 979 |
}).join(' ');
|
| 980 |
return '<div class="signal-row"><b>' + esc(sg.code) + '</b><em>' + esc(extra) + '</em></div>';
|
| 981 |
-
}).join('') : '<div class="signal-row"><em>无
|
| 982 |
-
html += '<
|
| 983 |
-
Math.round((conf == null ? 0 : conf) * 100) + '%"></span></div>';
|
| 984 |
-
html += '<div style="margin-top:5px;color:var(--faint);font:600 9px var(--mono)">CONFIDENCE ' +
|
| 985 |
-
(conf == null ? '—' : (+conf).toFixed(4)) + '</div></section>';
|
| 986 |
|
| 987 |
if (isNode) {
|
| 988 |
html += kv([
|
| 989 |
['panoid', row.panoid],
|
| 990 |
['lat / lon', row.lat.toFixed(6) + ', ' + row.lon.toFixed(6)],
|
| 991 |
-
['走廊', row.corridor_name || '—'],
|
| 992 |
-
['走廊
|
|
|
|
| 993 |
['吸附距离 m', row.snap_distance_m_auto],
|
| 994 |
-
['孤儿', row.is_orphan ? '是' : '否'],
|
| 995 |
['人工覆盖', row.corridor_overridden ? '走廊已改' : '—'],
|
| 996 |
]).replace('<dl', '<dl style="padding:11px 14px"');
|
| 997 |
} else {
|
| 998 |
html += kv([
|
| 999 |
-
['
|
| 1000 |
-
['
|
| 1001 |
-
['路
|
|
|
|
| 1002 |
['绕路比', row.detour_ratio_auto],
|
| 1003 |
['有道路可通', row.road_connected ? '是' : '否'],
|
| 1004 |
-
['高花费边', row.high_cost ? ('是 · ' + (row.cost_class || '')) : '否'],
|
| 1005 |
['走线点数', row.road_path ? row.road_path.length : '—'],
|
| 1006 |
['证据', row.evidence ? JSON.stringify(row.evidence) : '—'],
|
| 1007 |
['人工裁定', row.verdict || '—'],
|
|
@@ -1087,16 +1099,18 @@ function refreshStats() {
|
|
| 1087 |
$('statItems').textContent = st.nodes.toLocaleString() + ' 节点';
|
| 1088 |
$('statQueue').textContent = st.edges.toLocaleString() + ' 边';
|
| 1089 |
var o = st.origins || {};
|
| 1090 |
-
var
|
| 1091 |
-
var
|
| 1092 |
-
|
| 1093 |
-
$('
|
| 1094 |
-
$('
|
| 1095 |
-
$('
|
| 1096 |
-
$('
|
| 1097 |
-
|
| 1098 |
-
//
|
| 1099 |
-
|
|
|
|
|
|
|
| 1100 |
}).catch(function () {});
|
| 1101 |
}
|
| 1102 |
|
|
@@ -1113,8 +1127,8 @@ function boot() {
|
|
| 1113 |
}).then(function () { setMode('scenes'); });
|
| 1114 |
|
| 1115 |
['cityFilter', 'orderFilter', 'minNodes', 'editedFilter',
|
| 1116 |
-
'fOrphan', '
|
| 1117 |
-
'
|
| 1118 |
var el = $(id);
|
| 1119 |
if (el) el.onchange = function () { S.offset = 0; syncFilterChrome(); loadList(); };
|
| 1120 |
});
|
|
|
|
| 23 |
*/
|
| 24 |
var EDIT = null;
|
| 25 |
|
| 26 |
+
/* merged_policy_0915 §1-§4 的四类边。策略正文用蓝/绿/橙/红称呼它们,
|
| 27 |
+
这里的键就是 origin_auto 的取值。 */
|
| 28 |
+
var EDGE_COLOR = {
|
| 29 |
+
corridor: 'var(--e-corridor)', // 蓝 §1 同走廊相邻
|
| 30 |
+
road_route: 'var(--e-road)', // 绿 §2 路网可达
|
| 31 |
+
short_link: 'var(--e-short)', // 橙 §3 短直连
|
| 32 |
+
component_merge: 'var(--e-merge)', // 红 §4 分量合并
|
| 33 |
+
};
|
| 34 |
+
var EDGE_LABEL = {
|
| 35 |
+
corridor: '§1 同走廊相邻',
|
| 36 |
+
road_route: '§2 路网可达',
|
| 37 |
+
short_link: '§3 短直连',
|
| 38 |
+
component_merge: '§4 分量合并',
|
| 39 |
+
manual: '人工新增',
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
var S = {
|
| 43 |
mode: 'scenes', // 'scenes' 浏览全库 | 'queue' 复核队列
|
| 44 |
queue: [], filtered: [], selected: null, // {kind, scene_id, key:[...]}
|
|
|
|
| 108 |
function queueQuery() {
|
| 109 |
var q = new URLSearchParams();
|
| 110 |
if ($('cityFilter').value) q.set('city', $('cityFilter').value);
|
|
|
|
| 111 |
if ($('kindFilter').value) q.set('kind', $('kindFilter').value);
|
| 112 |
+
if ($('signalFilter').value) q.set('note', $('signalFilter').value);
|
| 113 |
if ($('showReviewedSel').value) q.set('include_reviewed', '1');
|
| 114 |
q.set('limit', String(S.limit));
|
| 115 |
q.set('offset', String(S.offset));
|
|
|
|
| 123 |
if ((v = $('minNodes').value)) q.set('min_nodes', v);
|
| 124 |
if ((v = $('editedFilter').value)) q.set('edited', v);
|
| 125 |
if ($('fOrphan').checked) q.set('orphan', '1');
|
| 126 |
+
if ($('fNoted').checked) q.set('noted', '1');
|
| 127 |
+
if ($('originFilter').value) q.set('origin', $('originFilter').value);
|
| 128 |
if ((v = $('searchInput').value.trim())) q.set('q', v);
|
| 129 |
q.set('limit', String(S.limit));
|
| 130 |
q.set('offset', String(S.offset));
|
|
|
|
| 154 |
var badges = '';
|
| 155 |
if (sc.revision_count) badges += '<span class="badge edited">改过 ' + sc.revision_count + '</span>';
|
| 156 |
if (sc.orphan_count) badges += '<span class="badge orphan">孤儿 ' + sc.orphan_count + '</span>';
|
| 157 |
+
if (sc.noted_count) badges += '<span class="badge cost">标记 ' + sc.noted_count + '</span>';
|
|
|
|
| 158 |
// 三行压成两行:城市并进第一行(原来单独占一行只为显示一个词),
|
| 159 |
// 没有徽章时整条 meta 行不渲染,而不是留一个占位的破折号。
|
| 160 |
var name = sc.scene_id.split(':')[1] || sc.scene_id;
|
|
|
|
| 205 |
{ id: 'minNodes', label: '最少节点', mode: 'scenes', type: 'value' },
|
| 206 |
{ id: 'editedFilter', label: '状态', mode: 'scenes', type: 'select' },
|
| 207 |
{ id: 'fOrphan', label: '含孤儿', mode: 'scenes', type: 'check' },
|
| 208 |
+
{ id: 'fNoted', label: '含带标记的边', mode: 'scenes', type: 'check' },
|
| 209 |
+
{ id: 'originFilter', label: '含某类边', mode: 'scenes', type: 'select' },
|
| 210 |
{ id: 'kindFilter', label: 'Kind', mode: 'queue', type: 'select' },
|
| 211 |
+
{ id: 'signalFilter', label: '标记', mode: 'queue', type: 'select' },
|
|
|
|
| 212 |
{ id: 'showReviewedSel', label: '已复核', mode: 'queue', type: 'select' },
|
| 213 |
];
|
| 214 |
|
|
|
|
| 273 |
});
|
| 274 |
renderList();
|
| 275 |
}
|
| 276 |
+
function noteHtml(notes) {
|
| 277 |
+
/* 队列项必须自带「为什么被列出来」。策略里没有分数,所以直接列标记码。 */
|
| 278 |
+
return (notes || []).map(function (n) {
|
| 279 |
+
var extra = Object.keys(n).filter(function (k) { return k !== 'code'; })
|
| 280 |
.map(function (k) {
|
| 281 |
+
var v = n[k];
|
| 282 |
+
return typeof v === 'number' ? (+v).toFixed(1) : String(v);
|
| 283 |
}).join(' ');
|
| 284 |
+
return '<span class="sig-code">' + esc(n.code) + '</span>' +
|
| 285 |
+
(extra ? '<span>' + esc(extra) + '</span>' : '');
|
| 286 |
+
}).join('') || '<span>—</span>';
|
| 287 |
}
|
| 288 |
function itemId(i) { return i.kind + '|' + i.scene_id + '|' + i.item_key; }
|
| 289 |
/* 左栏渲染的统一入口。
|
|
|
|
| 327 |
+ (i.review_status === 'needs_adjudication' ? ' adjudication' : '');
|
| 328 |
b.innerHTML =
|
| 329 |
'<div class="queue-top"><strong>' + (i.kind === 'node' ? '●' : '─') + ' ' +
|
| 330 |
+
esc(i.item_key) + '</strong><span>' + (i.kind === 'node' ? '节点' : '边') +
|
| 331 |
+
'</span></div>' +
|
| 332 |
+
'<div class="queue-meta">' + noteHtml(i.notes) + '</div>' +
|
|
|
|
| 333 |
'<div class="queue-scene">' + esc(i.scene_id) + '</div>';
|
| 334 |
b.onclick = function () { selectItem(i); };
|
| 335 |
list.appendChild(b);
|
|
|
|
| 581 |
var geo = S.scene.edges.map(function (e) {
|
| 582 |
var a = pos[e.source_label], b = pos[e.target_label];
|
| 583 |
if (!a || !b) return null;
|
| 584 |
+
// §5:橙边(短直连)和红边(分量合并)按定义就是直线,没有 road_path。
|
| 585 |
+
var costly = e.origin === 'component_merge';
|
| 586 |
+
if (S.edgeMode === 'road' && e.road_path && e.road_path.length > 1) {
|
|
|
|
| 587 |
// 折线两端落在吸附点上,不在节点上——补两段短接头,否则边看着是断的
|
| 588 |
return { e: e, costly: costly, road: true,
|
| 589 |
pts: [a].concat(e.road_path.map(function (q) { return proj(q[0], q[1]); })).concat([b]) };
|
|
|
|
| 595 |
geo.forEach(function (g, ei) {
|
| 596 |
if (!g) return;
|
| 597 |
var e = g.e;
|
| 598 |
+
var colour = EDGE_COLOR[e.origin] || 'var(--muted)';
|
|
|
|
| 599 |
var isSel = sel && sel.kind === 'edge'
|
| 600 |
&& sel.key[1] === e.source_label && sel.key[2] === e.target_label;
|
| 601 |
var dead = !e.in_graph;
|
|
|
|
| 755 |
var idx = {};
|
| 756 |
labels.forEach(function (l, i) { idx[l] = i; });
|
| 757 |
var cell = {};
|
| 758 |
+
var counts = { corridor: 0, road_route: 0, short_link: 0, component_merge: 0 };
|
| 759 |
S.scene.edges.forEach(function (e) {
|
| 760 |
cell[e.source_label + '|' + e.target_label] = e;
|
| 761 |
cell[e.target_label + '|' + e.source_label] = e;
|
| 762 |
if (counts[e.origin_auto] != null) counts[e.origin_auto]++;
|
| 763 |
});
|
| 764 |
+
$('gCorridor').textContent = counts.corridor;
|
| 765 |
+
$('gRoad').textContent = counts.road_route;
|
| 766 |
+
$('gShort').textContent = counts.short_link;
|
| 767 |
+
$('gMerge').textContent = counts.component_merge;
|
| 768 |
|
| 769 |
var sel = S.selected;
|
| 770 |
var html = '<tr><th></th>' + labels.map(function (l) { return '<th>' + l + '</th>'; }).join('') + '</tr>';
|
|
|
|
| 780 |
}
|
| 781 |
var isSel = sel && sel.kind === 'edge'
|
| 782 |
&& sel.key[1] === e.source_label && sel.key[2] === e.target_label;
|
| 783 |
+
var cls = 'edge e-' + (e.origin || 'manual') + (isSel ? ' sel' : '') +
|
| 784 |
(e.in_graph ? '' : ' rejected');
|
| 785 |
html += '<td class="' + cls + '" data-e="' + e.source_label + '-' +
|
| 786 |
e.target_label + '">' + (e.in_graph ? '·' : '×') + '</td>';
|
|
|
|
| 811 |
var isSel = sel && sel.kind === 'edge'
|
| 812 |
&& sel.key[1] === e.source_label && sel.key[2] === e.target_label;
|
| 813 |
d.className = 'edge-row' + (isSel ? ' sel' : '');
|
| 814 |
+
var colour = EDGE_COLOR[e.origin] || 'var(--muted)';
|
|
|
|
| 815 |
if (!e.in_graph) d.classList.add('rejected');
|
| 816 |
d.innerHTML = '<i style="background:' + colour + '"></i>' +
|
| 817 |
esc(e.source_label) + '–' + esc(e.target_label) +
|
| 818 |
(e.is_manual ? ' <span style="color:var(--cyan)">M</span>' : '') +
|
| 819 |
(e.in_graph ? '' : ' <span style="color:var(--no)">已拒</span>') +
|
| 820 |
+
((e.notes && e.notes.length) ? ' <span class="cost-tag" title="' +
|
| 821 |
+
esc(e.notes.map(function (n) { return n.code; }).join(', ')) + '">!</span>' : '') +
|
| 822 |
'<span class="len">' + (e.euclid_distance_m == null ? '—' : (+e.euclid_distance_m).toFixed(0) + ' m') + '</span>';
|
| 823 |
d.onclick = function () { pickEdge(e); };
|
| 824 |
listEl.appendChild(d);
|
|
|
|
| 976 |
var isNode = S.selected.kind === 'node';
|
| 977 |
$('decisionTitle').textContent = isNode ? '节点属���' : '边属性';
|
| 978 |
$('itemId').textContent = isNode ? row.label : (row.source_label + '→' + row.target_label);
|
|
|
|
| 979 |
var html = '';
|
| 980 |
|
| 981 |
+
// merged_policy_0915 没有置信度分数,也没有分诊分层。
|
| 982 |
+
// 这里展示的是策略真正产出的东西:非阻断标记。
|
| 983 |
html += '<section class="signal-block"><div class="block-title">' +
|
| 984 |
+
'<span>' + (isNode ? '节点标记' : '边的标记(非阻断)') + '</span></div>';
|
| 985 |
+
var marks = isNode
|
| 986 |
+
? (row.is_orphan ? [{ code: 'orphan', snap_m: row.snap_distance_m_auto }] : [])
|
| 987 |
+
: (row.notes || []);
|
| 988 |
+
html += marks.length ? marks.map(function (sg) {
|
| 989 |
var extra = Object.keys(sg).filter(function (k) { return k !== 'code'; })
|
| 990 |
.map(function (k) {
|
| 991 |
var v = sg[k];
|
| 992 |
return k + '=' + (typeof v === 'number' ? (+v).toFixed(3) : JSON.stringify(v));
|
| 993 |
}).join(' ');
|
| 994 |
return '<div class="signal-row"><b>' + esc(sg.code) + '</b><em>' + esc(extra) + '</em></div>';
|
| 995 |
+
}).join('') : '<div class="signal-row"><em>无标记</em></div>';
|
| 996 |
+
html += '</section>';
|
|
|
|
|
|
|
|
|
|
| 997 |
|
| 998 |
if (isNode) {
|
| 999 |
html += kv([
|
| 1000 |
['panoid', row.panoid],
|
| 1001 |
['lat / lon', row.lat.toFixed(6) + ', ' + row.lon.toFixed(6)],
|
| 1002 |
+
['走廊(最近)', row.corridor_name || '—'],
|
| 1003 |
+
['所属走廊数', (row.corridor_ids || []).length],
|
| 1004 |
+
['到走廊垂距 m', row.corridor_offset_m_auto],
|
| 1005 |
['吸附距离 m', row.snap_distance_m_auto],
|
| 1006 |
+
['孤儿 §6', row.is_orphan ? '是' : '否'],
|
| 1007 |
['人工覆盖', row.corridor_overridden ? '走廊已改' : '—'],
|
| 1008 |
]).replace('<dl', '<dl style="padding:11px 14px"');
|
| 1009 |
} else {
|
| 1010 |
html += kv([
|
| 1011 |
+
['类别', EDGE_LABEL[row.origin] || row.origin],
|
| 1012 |
+
['直线距离 m', row.euclid_distance_m_auto],
|
| 1013 |
+
['沿路距离 m', row.road_route_m_auto],
|
| 1014 |
+
['走线长度 m', row.path_length_m_auto],
|
| 1015 |
['绕路比', row.detour_ratio_auto],
|
| 1016 |
['有道路可通', row.road_connected ? '是' : '否'],
|
|
|
|
| 1017 |
['走线点数', row.road_path ? row.road_path.length : '—'],
|
| 1018 |
['证据', row.evidence ? JSON.stringify(row.evidence) : '—'],
|
| 1019 |
['人工裁定', row.verdict || '—'],
|
|
|
|
| 1099 |
$('statItems').textContent = st.nodes.toLocaleString() + ' 节点';
|
| 1100 |
$('statQueue').textContent = st.edges.toLocaleString() + ' 边';
|
| 1101 |
var o = st.origins || {};
|
| 1102 |
+
var cor = o.corridor || 0, road = o.road_route || 0;
|
| 1103 |
+
var short_ = o.short_link || 0, merge = o.component_merge || 0;
|
| 1104 |
+
var tot = cor + road + short_ + merge || 1;
|
| 1105 |
+
$('countCorridor').textContent = cor.toLocaleString();
|
| 1106 |
+
$('countRoad').textContent = road.toLocaleString();
|
| 1107 |
+
$('countShort').textContent = short_.toLocaleString();
|
| 1108 |
+
$('countMerge').textContent = merge.toLocaleString();
|
| 1109 |
+
// 进度条表示「边有多少来自道路本身」(§1 蓝 + §2 绿),
|
| 1110 |
+
// 剩下的是路网表达不了、靠 §3/§4 补上的。
|
| 1111 |
+
var fromRoad = (cor + road) / tot * 100;
|
| 1112 |
+
$('progressLabel').textContent = '沿路 ' + Math.round(fromRoad) + '%';
|
| 1113 |
+
$('progressBar').style.width = fromRoad + '%';
|
| 1114 |
}).catch(function () {});
|
| 1115 |
}
|
| 1116 |
|
|
|
|
| 1127 |
}).then(function () { setMode('scenes'); });
|
| 1128 |
|
| 1129 |
['cityFilter', 'orderFilter', 'minNodes', 'editedFilter',
|
| 1130 |
+
'fOrphan', 'fNoted', 'originFilter',
|
| 1131 |
+
'kindFilter', 'signalFilter', 'showReviewedSel'].forEach(function (id) {
|
| 1132 |
var el = $(id);
|
| 1133 |
if (el) el.onchange = function () { S.offset = 0; syncFilterChrome(); loadList(); };
|
| 1134 |
});
|
web/index.html
CHANGED
|
@@ -23,11 +23,11 @@
|
|
| 23 |
<i aria-hidden="true"></i>
|
| 24 |
<b id="statQueue">—</b><span>边</span>
|
| 25 |
<i aria-hidden="true"></i>
|
| 26 |
-
<span class="readout-origins" id="originBreakdown" title="边按
|
| 27 |
-
<em class="o-
|
| 28 |
-
<em class="o-
|
| 29 |
-
<em class="o-
|
| 30 |
-
<em class="o-
|
| 31 |
</span>
|
| 32 |
</div>
|
| 33 |
|
|
@@ -96,9 +96,19 @@
|
|
| 96 |
</label>
|
| 97 |
</div>
|
| 98 |
<div class="chip-row" id="sceneChips">
|
| 99 |
-
<label class="chip"><input id="fOrphan" type="checkbox"><span>含孤儿</span></label>
|
| 100 |
-
<label class="chip"><input id="
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
</div>
|
| 103 |
</div>
|
| 104 |
|
|
@@ -112,27 +122,17 @@
|
|
| 112 |
<option value="edge">只看边</option>
|
| 113 |
</select>
|
| 114 |
</label>
|
| 115 |
-
|
| 116 |
-
<select id="tierFilter">
|
| 117 |
-
<option value="">全部</option>
|
| 118 |
-
<option value="1">Tier 1 必须</option>
|
| 119 |
-
<option value="2">Tier 2 优先</option>
|
| 120 |
-
<option value="3">Tier 3 抽查</option>
|
| 121 |
-
</select>
|
| 122 |
-
</label>
|
| 123 |
</div>
|
| 124 |
<div class="filter-row">
|
| 125 |
-
<label class="ctl"><span>
|
| 126 |
<select id="signalFilter">
|
| 127 |
-
<option value="">全部
|
| 128 |
-
<option value="
|
| 129 |
-
<option value="
|
| 130 |
-
<option value="
|
| 131 |
-
<option value="
|
| 132 |
-
<option value="
|
| 133 |
-
<option value="E2">E2 绕路超限</option>
|
| 134 |
-
<option value="E3">E3 绕路偏大</option>
|
| 135 |
-
<option value="E4">E4 选边并列</option>
|
| 136 |
</select>
|
| 137 |
</label>
|
| 138 |
<label class="ctl"><span>已复核</span>
|
|
@@ -202,10 +202,10 @@
|
|
| 202 |
</div>
|
| 203 |
<div class="image-caption">
|
| 204 |
<span class="legend">
|
| 205 |
-
<span><i class="lg-
|
| 206 |
-
<span><i class="lg-
|
| 207 |
-
<span><i class="lg-
|
| 208 |
-
<span><i class="lg-
|
| 209 |
<span><i class="lg-orphan"></i>孤儿</span>
|
| 210 |
</span>
|
| 211 |
</div>
|
|
@@ -247,9 +247,10 @@
|
|
| 247 |
<h3 id="graphTitle">节点邻接矩阵</h3>
|
| 248 |
</div>
|
| 249 |
<div class="graph-counts">
|
| 250 |
-
<span><b id="
|
| 251 |
-
<span><b id="
|
| 252 |
-
<span><b id="
|
|
|
|
| 253 |
</div>
|
| 254 |
</div>
|
| 255 |
<p class="graph-rule">每一格是一条无向边,颜色对应它的来源。点击格子或右侧列表可直接检查该条边。</p>
|
|
|
|
| 23 |
<i aria-hidden="true"></i>
|
| 24 |
<b id="statQueue">—</b><span>边</span>
|
| 25 |
<i aria-hidden="true"></i>
|
| 26 |
+
<span class="readout-origins" id="originBreakdown" title="边按 §1-§4 分类">
|
| 27 |
+
<em class="o-corridor"><b id="countCorridor">—</b>同走廊</em>
|
| 28 |
+
<em class="o-road"><b id="countRoad">—</b>路网可达</em>
|
| 29 |
+
<em class="o-short"><b id="countShort">—</b>短直连</em>
|
| 30 |
+
<em class="o-merge"><b id="countMerge">—</b>分量合并</em>
|
| 31 |
</span>
|
| 32 |
</div>
|
| 33 |
|
|
|
|
| 96 |
</label>
|
| 97 |
</div>
|
| 98 |
<div class="chip-row" id="sceneChips">
|
| 99 |
+
<label class="chip"><input id="fOrphan" type="checkbox"><span>含孤儿节点</span></label>
|
| 100 |
+
<label class="chip"><input id="fNoted" type="checkbox"><span>含带标记的边</span></label>
|
| 101 |
+
</div>
|
| 102 |
+
<div class="filter-row">
|
| 103 |
+
<label class="ctl"><span>含某类边</span>
|
| 104 |
+
<select id="originFilter">
|
| 105 |
+
<option value="">全部</option>
|
| 106 |
+
<option value="corridor">§1 同走廊</option>
|
| 107 |
+
<option value="road_route">§2 路网可达</option>
|
| 108 |
+
<option value="short_link">§3 短直连</option>
|
| 109 |
+
<option value="component_merge">§4 分量合并</option>
|
| 110 |
+
</select>
|
| 111 |
+
</label>
|
| 112 |
</div>
|
| 113 |
</div>
|
| 114 |
|
|
|
|
| 122 |
<option value="edge">只看边</option>
|
| 123 |
</select>
|
| 124 |
</label>
|
| 125 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
</div>
|
| 127 |
<div class="filter-row">
|
| 128 |
+
<label class="ctl"><span>标记</span>
|
| 129 |
<select id="signalFilter">
|
| 130 |
+
<option value="">全部标记</option>
|
| 131 |
+
<option value="orphan">§6 孤儿节点</option>
|
| 132 |
+
<option value="water_crossed">§0 跨水</option>
|
| 133 |
+
<option value="building_crossed">§0 穿建筑</option>
|
| 134 |
+
<option value="no_road_path">无道路通路</option>
|
| 135 |
+
<option value="long_detour">§4 红边绕远</option>
|
|
|
|
|
|
|
|
|
|
| 136 |
</select>
|
| 137 |
</label>
|
| 138 |
<label class="ctl"><span>已复核</span>
|
|
|
|
| 202 |
</div>
|
| 203 |
<div class="image-caption">
|
| 204 |
<span class="legend">
|
| 205 |
+
<span><i class="lg-corridor"></i>§1 同走廊</span>
|
| 206 |
+
<span><i class="lg-road"></i>§2 路网可达</span>
|
| 207 |
+
<span><i class="lg-short"></i>§3 短直连</span>
|
| 208 |
+
<span><i class="lg-merge"></i>§4 分量合并</span>
|
| 209 |
<span><i class="lg-orphan"></i>孤儿</span>
|
| 210 |
</span>
|
| 211 |
</div>
|
|
|
|
| 247 |
<h3 id="graphTitle">节点邻接矩阵</h3>
|
| 248 |
</div>
|
| 249 |
<div class="graph-counts">
|
| 250 |
+
<span><b id="gCorridor">—</b> 同走廊</span>
|
| 251 |
+
<span><b id="gRoad">—</b> 路网</span>
|
| 252 |
+
<span><b id="gShort">—</b> 直连</span>
|
| 253 |
+
<span><b id="gMerge">—</b> 合并</span>
|
| 254 |
</div>
|
| 255 |
</div>
|
| 256 |
<p class="graph-rule">每一格是一条无向边,颜色对应它的来源。点击格子或右侧列表可直接检查该条边。</p>
|
web/styles.css
CHANGED
|
@@ -29,6 +29,12 @@
|
|
| 29 |
--chain: #68c8c0;
|
| 30 |
--intersection: #f1a43c;
|
| 31 |
--bridge: #b8a2e0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
--road: #70c79b;
|
| 33 |
--radius: 4px;
|
| 34 |
--shadow: 0 24px 70px rgba(0, 0, 0, .28);
|
|
@@ -97,8 +103,8 @@ button { color: inherit; }
|
|
| 97 |
.readout-origins { display: flex; gap: 10px; min-width: 0; overflow: hidden; }
|
| 98 |
.readout-origins em { font-style: normal; display: flex; align-items: baseline; gap: 3px; color: var(--faint); letter-spacing: .06em; }
|
| 99 |
.readout-origins b { font: 650 11px var(--mono); }
|
| 100 |
-
.o-
|
| 101 |
-
.o-
|
| 102 |
|
| 103 |
.origin-meter { flex: 1 1 auto; min-width: 50px; height: 2px; background: var(--line-soft); }
|
| 104 |
.origin-meter span { display: block; height: 100%; width: 0; background: linear-gradient(90deg, var(--signal), #ffd38b); transition: width .45s ease; }
|
|
@@ -307,8 +313,8 @@ kbd {
|
|
| 307 |
.legend { display: flex; gap: 9px; }
|
| 308 |
.legend i { display: inline-block; width: 12px; height: 2px; margin-right: 3px; vertical-align: middle; }
|
| 309 |
.legend span { color: var(--faint); font: 600 8px var(--mono); white-space: nowrap; }
|
| 310 |
-
.lg-
|
| 311 |
-
.lg-
|
| 312 |
.lg-orphan { width: 7px !important; height: 7px !important; border-radius: 50%; background: var(--anchor); }
|
| 313 |
|
| 314 |
.image-stage { display: block; width: 100%; padding: 0; border: 0; background: #0b0e0d; position: relative; overflow: hidden; }
|
|
@@ -415,9 +421,11 @@ kbd {
|
|
| 415 |
width: 21px; height: 21px; text-align: center; border: 1px solid var(--line-soft); color: var(--faint);
|
| 416 |
}
|
| 417 |
.adjacency-table td.edge { cursor: pointer; }
|
| 418 |
-
|
| 419 |
-
.adjacency-table td.e-
|
| 420 |
-
.adjacency-table td.e-
|
|
|
|
|
|
|
| 421 |
.adjacency-table td.diag { background: var(--line-soft); }
|
| 422 |
.adjacency-table td.addable { cursor: cell; color: #3d4a45; }
|
| 423 |
.adjacency-table td.addable:hover { background: rgba(104,200,192,.18); color: var(--cyan); }
|
|
|
|
| 29 |
--chain: #68c8c0;
|
| 30 |
--intersection: #f1a43c;
|
| 31 |
--bridge: #b8a2e0;
|
| 32 |
+
/* merged_policy_0915 §1-§4 的四色。策略正文就是用蓝/绿/橙/红称呼它们的,
|
| 33 |
+
配色按那个来,不再沿用旧策略的「走廊链/路口/桥接」三色。 */
|
| 34 |
+
--e-corridor: #6ba8e8; /* 蓝 §1 同走廊相邻 */
|
| 35 |
+
--e-road: #68c8a0; /* 绿 §2 路网可达 */
|
| 36 |
+
--e-short: #f1a43c; /* 橙 §3 短直连 */
|
| 37 |
+
--e-merge: #e8646b; /* 红 §4 分量合并 */
|
| 38 |
--road: #70c79b;
|
| 39 |
--radius: 4px;
|
| 40 |
--shadow: 0 24px 70px rgba(0, 0, 0, .28);
|
|
|
|
| 103 |
.readout-origins { display: flex; gap: 10px; min-width: 0; overflow: hidden; }
|
| 104 |
.readout-origins em { font-style: normal; display: flex; align-items: baseline; gap: 3px; color: var(--faint); letter-spacing: .06em; }
|
| 105 |
.readout-origins b { font: 650 11px var(--mono); }
|
| 106 |
+
.o-corridor b { color: var(--e-corridor); } .o-road b { color: var(--e-road); }
|
| 107 |
+
.o-short b { color: var(--e-short); } .o-merge b { color: var(--e-merge); }
|
| 108 |
|
| 109 |
.origin-meter { flex: 1 1 auto; min-width: 50px; height: 2px; background: var(--line-soft); }
|
| 110 |
.origin-meter span { display: block; height: 100%; width: 0; background: linear-gradient(90deg, var(--signal), #ffd38b); transition: width .45s ease; }
|
|
|
|
| 313 |
.legend { display: flex; gap: 9px; }
|
| 314 |
.legend i { display: inline-block; width: 12px; height: 2px; margin-right: 3px; vertical-align: middle; }
|
| 315 |
.legend span { color: var(--faint); font: 600 8px var(--mono); white-space: nowrap; }
|
| 316 |
+
.lg-corridor { background: var(--e-corridor); } .lg-road { background: var(--e-road); }
|
| 317 |
+
.lg-short { background: var(--e-short); } .lg-merge { background: var(--e-merge); }
|
| 318 |
.lg-orphan { width: 7px !important; height: 7px !important; border-radius: 50%; background: var(--anchor); }
|
| 319 |
|
| 320 |
.image-stage { display: block; width: 100%; padding: 0; border: 0; background: #0b0e0d; position: relative; overflow: hidden; }
|
|
|
|
| 421 |
width: 21px; height: 21px; text-align: center; border: 1px solid var(--line-soft); color: var(--faint);
|
| 422 |
}
|
| 423 |
.adjacency-table td.edge { cursor: pointer; }
|
| 424 |
+
/* 旧策略的 e-chain 已随规则作废 */
|
| 425 |
+
.adjacency-table td.e-corridor { background: rgba(107,168,232,.32); color: var(--paper); }
|
| 426 |
+
.adjacency-table td.e-road_route { background: rgba(104,200,160,.32); color: var(--paper); }
|
| 427 |
+
.adjacency-table td.e-short_link { background: rgba(241,164,60,.32); color: var(--paper); }
|
| 428 |
+
.adjacency-table td.e-component_merge { background: rgba(232,100,107,.32); color: var(--paper); }
|
| 429 |
.adjacency-table td.diag { background: var(--line-soft); }
|
| 430 |
.adjacency-table td.addable { cursor: cell; color: #3d4a45; }
|
| 431 |
.adjacency-table td.addable:hover { background: rgba(104,200,192,.18); color: var(--cyan); }
|