MarcoXL commited on
Commit
eeeb56e
·
1 Parent(s): 825672c

Upload folder using huggingface_hub

Browse files
Files changed (6) hide show
  1. README.md +14 -10
  2. app.py +44 -31
  3. graph.sqlite +2 -2
  4. web/app.js +79 -65
  5. web/index.html +34 -33
  6. 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 个节点、108,374 条边**。
15
 
16
  界面与本地的标注复核台一致——三栏布局、J/K 键盘导航、卫星图/电子地图切换、
17
  邻接矩阵、街景放大。区别是**去掉了全部写入**:没有判定按钮,后端也没有对应接口。
@@ -19,17 +19,21 @@ license: mit
19
  ## 怎么看
20
 
21
  - **左栏**:全库场景浏览。可按城市、scene_id、最少节点数筛选,
22
- 也可只看含孤儿 / 含高花费边 / 路网碎片化的场景。`J`/`K` 上下,`[`/`]` 翻页。
23
- - **中栏**:卫星底图上叠出图结构。节点按所属走廊着色,红圈是孤儿;
24
- 边按来源着色——<span style="color:#68c8c0">青=走廊链</span>、
25
- <span style="color:#f1a43c">橙=路口边</span>、
26
- <span style="color:#b8a2e0">紫=桥接边</span>,白色加粗虚线=高花费边。
 
 
 
27
  `B` 切换电子地图,`E` 切换沿路走线/直线。
28
  - **右栏**:选中节点或边的完整属性与自动判定信号。
29
- - **街景朝向**:每个 pano 有 4 张街景,图下方一排按钮按罗盘顺时针列出
30
- (`NE 40°`、`SE 130°`…),点一下换朝向。`H` 键下一个、`Shift`+`H` 上一个;
31
- 选中一条边时两侧会同步翻,方便对同朝向比较。
 
32
 
33
  图片存在配套的私有 Dataset 里,由 CDN 按需分发。
34
 
35
- 建图规则见 `GRAPH_RULES.md`(在源仓库里,**不随本 Space 一起上传**)。
 
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["triage_signals"] = _loads(n["triage_signals_auto"]) or []
123
  for e in edges:
124
- e["origin"] = e["origin_human"] or e["origin_auto"] or "bridge"
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["triage_signals"] = _loads(e["triage_signals_auto"]) or []
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
- "high_cost": q("SELECT COUNT(*) FROM edge WHERE high_cost_auto=1"),
 
182
  "orphans": q("SELECT COUNT(*) FROM node WHERE is_orphan_auto=1"),
183
- "fragmented": q("SELECT COUNT(*) FROM scene WHERE net_fragmented_auto=1"),
 
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 = "", high_cost: str = "", fragmented: 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 high_cost in ("1", "true"):
211
  where.append("EXISTS (SELECT 1 FROM edge e WHERE e.scene_id=s.scene_id "
212
- "AND e.high_cost_auto=1)")
213
- if fragmented in ("1", "true"):
214
- where.append("s.net_fragmented_auto = 1")
 
 
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.net_fragmented_auto, "
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 AND e.high_cost_auto=1) "
227
- " AS high_cost_count, 0 AS revision_count "
 
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 = "", signal: str = "", kind: 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, COALESCE(t.triage_tier_auto,0) AS tier, "
258
- "t.confidence_auto AS confidence, "
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 conf_null ASC, confidence ASC, scene_id, item_key LIMIT ? OFFSET ?")
268
  items = []
269
  for r in con().execute(sql, args + [limit, max(0, int(offset))]):
270
  d = dict(r)
271
- d.pop("conf_null", None)
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:58a07b075f692f69317cc1956f4eff308e6ca3c112d711c38cbb32f6b195f721
3
- size 153534464
 
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('signal', $('signalFilter').value);
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 ($('fHighCost').checked) q.set('high_cost', '1');
112
- if ($('fFragmented').checked) q.set('fragmented', '1');
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.high_cost_count) badges += '<span class="badge cost">$$ ' + sc.high_cost_count + '</span>';
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: 'fHighCost', label: '含高花费边', mode: 'scenes', type: 'check' },
195
- { id: 'fFragmented', label: '路网碎片化', mode: 'scenes', type: 'check' },
196
  { id: 'kindFilter', label: 'Kind', mode: 'queue', type: 'select' },
197
- { id: 'tierFilter', label: 'Tier', mode: 'queue', type: 'select' },
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 signalHtml(sigs) {
264
- if (!sigs || !sigs.length) return '<b>信号缺失</b>';
265
- return sigs.map(function (s) {
266
- var extra = Object.keys(s).filter(function (k) { return k !== 'code' && k !== 'queued'; })
267
  .map(function (k) {
268
- var v = s[k];
269
- return k + '=' + (typeof v === 'number' ? (+v).toFixed(2) : JSON.stringify(v));
270
  }).join(' ');
271
- return '<span class="sig-code">' + esc(s.code) + '</span>' + (extra ? ' ' + esc(extra) : '');
272
- }).join(' · ');
 
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
- (i.confidence == null ? '—' : (+i.confidence).toFixed(3)) + '</span></div>' +
318
- '<div class="queue-meta"><span class="tier-pill tier-' + i.tier + '">T' + i.tier + '</span>' +
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
- // 高花费边一律画直线:它的 road_path 已在建图阶段丢弃(绕出几十倍的
572
- // 折线画在图上只会误导),真实路网距离仍在 road_distance_m 里。
573
- var costly = !!e.high_cost;
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 = { corridor_chain: 'var(--chain)', intersection: 'var(--intersection)' }[e.origin]
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 = { corridor_chain: 0, intersection: 0, bridge: 0 };
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
- $('gChain').textContent = counts.corridor_chain;
754
- $('gInter').textContent = counts.intersection;
755
- $('gBridge').textContent = counts.bridge;
 
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 || 'bridge') + (isSel ? ' sel' : '') +
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 = { corridor_chain: 'var(--chain)', intersection: 'var(--intersection)' }[e.origin]
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.high_cost ? ' <span class="cost-tag" title="' + esc(e.cost_class || '') +
810
- '">$$</span>' : '') +
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>自动判定信号</span></div>';
973
- var sigs = row.triage_signals || [];
974
- html += sigs.length ? sigs.map(function (sg) {
 
 
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>无信号(高置信)</em></div>';
982
- html += '<div class="conf-meter"><span style="width:' +
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
- ['走廊来源', row.corridor_source_auto],
 
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
- ['来源', row.origin],
1000
- ['欧氏距离 m', row.euclid_distance_m_auto],
1001
- ['路网距离 m', row.road_distance_m_auto],
 
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 chain = o.corridor_chain || 0, inter = o.intersection || 0, bridge = o.bridge || 0;
1091
- var tot = chain + inter + bridge || 1;
1092
- $('countChain').textContent = chain.toLocaleString();
1093
- $('countInter').textContent = inter.toLocaleString();
1094
- $('countBridge').textContent = bridge.toLocaleString();
1095
- $('countCost').textContent = (st.high_cost || 0).toLocaleString();
1096
- $('progressLabel').textContent =
1097
- '走廊链 ' + Math.round(chain / tot * 100) + '%';
1098
- // 进度条在这里表示「边有多少来自真实道路走廊」,而不是复核进度
1099
- $('progressBar').style.width = (chain / tot * 100) + '%';
 
 
1100
  }).catch(function () {});
1101
  }
1102
 
@@ -1113,8 +1127,8 @@ function boot() {
1113
  }).then(function () { setMode('scenes'); });
1114
 
1115
  ['cityFilter', 'orderFilter', 'minNodes', 'editedFilter',
1116
- 'fOrphan', 'fHighCost', 'fFragmented',
1117
- 'tierFilter', 'kindFilter', 'signalFilter', 'showReviewedSel'].forEach(function (id) {
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-chain"><b id="countChain">—</b>走廊链</em>
28
- <em class="o-inter"><b id="countInter">—</b>路口</em>
29
- <em class="o-bridge"><b id="countBridge">—</b>桥接</em>
30
- <em class="o-cost"><b id="countCost">—</b>高花费</em>
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="fHighCost" type="checkbox"><span>含高花费边</span></label>
101
- <label class="chip"><input id="fFragmented" type="checkbox"><span>路网碎片化</span></label>
 
 
 
 
 
 
 
 
 
 
102
  </div>
103
  </div>
104
 
@@ -112,27 +122,17 @@
112
  <option value="edge">只看边</option>
113
  </select>
114
  </label>
115
- <label class="ctl"><span>Tier</span>
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>Signal</span>
126
  <select id="signalFilter">
127
- <option value="">全部信号</option>
128
- <option value="A1">A1 孤儿</option>
129
- <option value="A2">A2 吸附偏远</option>
130
- <option value="A3">A3 无路名</option>
131
- <option value="A4">A4 归属歧义</option>
132
- <option value="E1">E1 无道路通路</option>
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-chain"></i>走廊链</span>
206
- <span><i class="lg-inter"></i>路口边</span>
207
- <span><i class="lg-bridge"></i>桥接边</span>
208
- <span><i class="lg-road" style="border-top:3px dashed var(--paper);background:none"></i>高花费边</span>
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="gChain">—</b> chain</span>
251
- <span><b id="gInter">—</b> inter</span>
252
- <span><b id="gBridge">—</b> bridge</span>
 
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-chain b { color: var(--chain); } .o-inter b { color: var(--intersection); }
101
- .o-bridge b { color: var(--bridge); } .o-cost b { color: var(--paper); }
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-chain { background: var(--chain); } .lg-inter { background: var(--intersection); }
311
- .lg-bridge { background: var(--bridge); } .lg-road { background: var(--road); }
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
- .adjacency-table td.e-chain { background: rgba(104,200,192,.3); color: var(--paper); }
419
- .adjacency-table td.e-intersection { background: rgba(241,164,60,.3); color: var(--paper); }
420
- .adjacency-table td.e-bridge { background: rgba(184,162,224,.3); color: var(--paper); }
 
 
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); }