MarcoXL commited on
Commit
74ffc79
·
1 Parent(s): 18cdf99

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. web/app.js +117 -126
  2. web/index.html +2 -2
web/app.js CHANGED
@@ -44,7 +44,7 @@ var S = {
44
  queue: [], filtered: [], selected: null, // {kind, scene_id, key:[...]}
45
  scenes: [], sceneSel: null,
46
  scene: null, offset: 0, limit: 100,
47
- basemap: 'satellite', mapZoom: 17, edgeMode: 'road',
48
  boot: null,
49
  };
50
 
@@ -452,28 +452,29 @@ function drawScene() {
452
  if (S.basemap === 'map') renderMapTiles();
453
  }
454
 
455
- /* ---------- 重叠边的分离 ----------------------------------------------
456
- * 同一条街上常常叠着好几条边。新策略(merged_policy_0915)之后这更普遍了:
457
- * §1 蓝边和 §2 绿边都沿路画,一条绿边往往**只有一段**和某条蓝边重合。
458
  *
459
- * 所以不能整条边一起挪。之前那版按「整条边 75% 重合」判定,实测只挪动 6%
460
- * 的边,而真实被压住的采样点有 30% ——差的全是部分重合。
 
 
461
  *
462
- * 现在改成**逐点侧移**:
463
- * 1. 每条折线按固定间距重采样
464
- * 2. 每个采样点去找「附近且走向相近」的其它边的点,凑成一组
465
- * 3. 组内按边的下标定序,各自沿法线偏移 (rank - (n-1)/2) * SEP
466
- * 4. 沿边做滑动平均,避免分组变化处抖动
467
  *
468
- * 法线方向按**归一到 [0,180) 的走向**算:同一条路上反向行进的两条边,
469
- * 法线必须指向同一侧,否则 rank 0 和 rank 1 会被推到同一边。
470
  */
471
- var SEP = 4.2; // 相邻两条重合边的间距(raster 单位,400 = 全宽)
472
- var NEAR_TOL = 3.2; // 多近算"压在一起"
473
- var ANG_TOL = 30.0; // 走向差多少度以内才算同向(度)
474
- var STEP = 3.0; // 重采样间距
475
- var DILATE = 3; // 把偏移量向两侧扩张几个采样点(见下)
476
- var TAPER = 9.0; // 端点附近多少距离内收敛回 0(raster 单位)
 
 
477
 
478
  function distToSegment(p, a, b) {
479
  var dx = b[0] - a[0], dy = b[1] - a[1];
@@ -492,116 +493,96 @@ function distToPolyline(p, pts) {
492
  }
493
  function polyLength(pts) {
494
  var L = 0;
495
- for (var i = 0; i + 1 < pts.length; i++) L += Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]);
 
496
  return L;
497
  }
498
- /* 沿折线等距取点,同时给出每点的走向(归一到 [0,180))。 */
499
- function resample(pts, step) {
500
- var out = [];
501
  var total = polyLength(pts);
502
- var n = Math.max(2, Math.ceil(total / step) + 1);
503
  var segs = [];
504
  for (var i = 0; i + 1 < pts.length; i++)
505
  segs.push(Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]));
 
506
  for (var k = 0; k < n; k++) {
507
  var want = total * k / (n - 1), acc = 0, j = 0;
508
  while (j < segs.length && acc + segs[j] < want) { acc += segs[j]; j++; }
509
  if (j >= segs.length) j = segs.length - 1;
510
  var t = segs[j] ? (want - acc) / segs[j] : 0;
511
- var a = pts[j], b = pts[j + 1];
512
- var ang = Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI;
513
- ang = ((ang % 180) + 180) % 180; // 归一:反向行进算同一走向
514
- out.push({ x: a[0] + (b[0] - a[0]) * t, y: a[1] + (b[1] - a[1]) * t,
515
- ang: ang, s: want });
516
  }
517
  return out;
518
  }
519
- function angDiff(a, b) {
520
- var d = Math.abs(a - b) % 180;
521
- return d > 90 ? 180 - d : d;
 
 
 
 
522
  }
523
 
524
- /* 返回每条边的新折线。不重合的边原样返回。 */
525
- function separateEdges(polys) {
526
  var n = polys.length;
527
- var samples = polys.map(function (p) {
528
- return (p && p.length > 1) ? resample(p, STEP) : [];
529
- });
530
- // 网格索引:只跟邻近格子里的点比,不做 O(总点数^2)
531
- var CELL = NEAR_TOL * 2;
532
- var grid = {};
533
- samples.forEach(function (pts, ei) {
534
- pts.forEach(function (q, pi) {
535
- var key = Math.floor(q.x / CELL) + ':' + Math.floor(q.y / CELL);
536
- (grid[key] || (grid[key] = [])).push([ei, pi]);
537
- });
538
- });
539
-
540
- var offsets = samples.map(function (pts) { return pts.map(function () { return 0; }); });
541
- samples.forEach(function (pts, ei) {
542
- pts.forEach(function (q, pi) {
543
- // 收集与本点重合的其它边
544
- var group = [ei];
545
- var cx = Math.floor(q.x / CELL), cy = Math.floor(q.y / CELL);
546
- for (var dx = -1; dx <= 1; dx++) for (var dy = -1; dy <= 1; dy++) {
547
- var cell = grid[(cx + dx) + ':' + (cy + dy)];
548
- if (!cell) continue;
549
- for (var t = 0; t < cell.length; t++) {
550
- var ej = cell[t][0], pj = cell[t][1];
551
- if (ej === ei || group.indexOf(ej) >= 0) continue;
552
- var r = samples[ej][pj];
553
- if (Math.hypot(r.x - q.x, r.y - q.y) > NEAR_TOL) continue;
554
- if (angDiff(r.ang, q.ang) > ANG_TOL) continue;
555
- group.push(ej);
556
- }
557
- }
558
- if (group.length < 2) return;
559
- group.sort(function (a, b) { return a - b; }); // 确定性:按边的下标
560
- var rank = group.indexOf(ei);
561
- offsets[ei][pi] = (rank - (group.length - 1) / 2) * SEP;
562
  });
563
  });
 
 
564
 
565
- return polys.map(function (poly, ei) {
566
- var pts = samples[ei];
567
- if (!pts.length) return poly;
568
- var off = offsets[ei];
569
- if (!off.some(function (v) { return v !== 0; })) return poly; // 没被挪
570
-
571
- // 1) 扩张:把每个点的偏移取邻域内绝对值最大的那个。
572
- // 直接做滑动平均会把短重合段的偏移摊薄到不起作用——重合只有三四个点时
573
- // 平均下来只剩三分之一,等于没挪。扩张先把量保住,过渡放到重合段之外。
574
- var dil = off.map(function (_, i) {
575
- var best = 0;
576
- for (var k = -DILATE; k <= DILATE; k++) {
577
- var j = i + k;
578
- if (j < 0 || j >= off.length) continue;
579
- if (Math.abs(off[j]) > Math.abs(best)) best = off[j];
580
- }
581
- return best;
582
- });
583
- // 2) 三点平均,磨圆分组变化处的折角。窗口小到不会再摊薄量级。
584
- var sm = dil.map(function (_, i) {
585
- var a = 0, c = 0;
586
- for (var k = -1; k <= 1; k++) {
587
- var j = i + k;
588
- if (j >= 0 && j < dil.length) { a += dil[j]; c++; }
589
- }
590
- return a / c;
591
- });
592
- // 3) 端点收敛,**必须放在平滑之后**:先收敛再平滑的话,三点平均会把
593
- // 邻点的偏移带回端点,边就从节点上脱开了(实测 425 条里脱开 251 条)。
594
- // §1/§2 的边本来就都汇聚到节点,那里不可能也不需要分开。
595
- var total = pts[pts.length - 1].s || 1;
596
- sm = sm.map(function (v, i) {
597
- var d = Math.min(pts[i].s, total - pts[i].s);
598
- return d >= TAPER ? v : v * (d / TAPER);
599
- });
600
- return pts.map(function (q, i) {
601
- var nx = -Math.sin(q.ang * Math.PI / 180);
602
- var ny = Math.cos(q.ang * Math.PI / 180);
603
- return [q.x + nx * sm[i], q.y + ny * sm[i]];
604
- });
605
  });
606
  }
607
 
@@ -633,8 +614,8 @@ function drawGraph() {
633
  }
634
  return { e: e, costly: costly, road: false, pts: [a, b] };
635
  });
636
- // 逐点侧移:重合的部分被推开,不重合的部分原样
637
- var drawn = separateEdges(geo.map(function (g) { return g ? g.pts : []; }));
638
 
639
  geo.forEach(function (g, ei) {
640
  if (!g) return;
@@ -643,25 +624,35 @@ function drawGraph() {
643
  var isSel = sel && sel.kind === 'edge'
644
  && sel.key[1] === e.source_label && sel.key[2] === e.target_label;
645
  var dead = !e.in_graph;
646
- var pts = drawn[ei] || g.pts;
647
- var moved = pts !== g.pts;
648
-
649
- // 一律用 polyline:侧移之后连"直线"边也可能变成折线
650
- // (两端不重合、中段被推开时,直线会被弯成一个浅弧)
651
- var el = mk('polyline', {
652
- points: pts.map(function (q) { return q[0].toFixed(2) + ',' + q[1].toFixed(2); }).join(' '),
653
- fill: 'none', stroke: isSel ? '#fff' : colour,
654
- 'stroke-width': isSel ? 3 : (g.costly ? 2.2 : 1.7),
655
- opacity: dead ? .22 : 1,
656
- 'stroke-dasharray': g.costly ? '6 4' : '',
657
- 'stroke-linejoin': 'round', 'stroke-linecap': 'round',
658
- style: 'cursor:pointer',
659
- });
 
 
 
 
 
 
 
 
 
 
660
  el.onclick = function () { pickEdge(e); };
661
  var title = mk('title', {});
662
  title.textContent = e.source_label + '–' + e.target_label + ' · ' +
663
  (EDGE_LABEL[e.origin] || e.origin || '') +
664
- (moved ? '(与其它边重合,已错开显示)' : '');
665
  el.appendChild(title);
666
  svg.appendChild(el);
667
  });
 
44
  queue: [], filtered: [], selected: null, // {kind, scene_id, key:[...]}
45
  scenes: [], sceneSel: null,
46
  scene: null, offset: 0, limit: 100,
47
+ basemap: 'satellite', mapZoom: 17, edgeMode: 'straight', // 直线更醒目,默认走直线
48
  boot: null,
49
  };
50
 
 
452
  if (S.basemap === 'map') renderMapTiles();
453
  }
454
 
455
+ /* ---------- 重合边的分离 ----------------------------------------------
456
+ * 直线更醒目,所以默认就画直线(`E` 可切到沿路走线)。
 
457
  *
458
+ * 直线模式下**真正重合的很少**:实测 464 条边里只有 6 对、涉及 8 条边
459
+ * (1.7%)。因为直线只在「共线且投影重叠」时才压在一起,而策略的 §2.5 /
460
+ * §3.3 本来就禁止「第三个节点落在路径/直线上」,大部分共线情形已被排除。
461
+ * 剩下的是两条边恰好首尾错开地共线。
462
  *
463
+ * 所以分离用**整条边平移**,不是逐点侧移:
464
+ * - 直线平移之后还是直线,不会被弯成弧
465
+ * - 端点跟着挪,但挪动量被压在节点圆点半径(5)以内,看上去仍然连着节点
 
 
466
  *
467
+ * 逐点侧移那一版是为沿路走线写的(那种模式下大量边共用同一条道路中心线,
468
+ * 遮挡 17%),它会把直线弯掉,与「直线更醒目」正好相反。
469
  */
470
+ var SEP = 4.2; // 同组相邻两条边的间距(raster 单位,画布 400)
471
+ var MAX_OFF = 6.0; // 单条边的最大平移量。
472
+ // 节点圆点半径 5 且画在边之上,所以平移 5 以内完全被盖住;
473
+ // 取 6 是给大组留余地(5 条边一组时挤到 2.25 反而低于容差),
474
+ // 超出的那 1 单位比边宽(2)还窄,看上去仍然连着节点。
475
+ var NEAR_TOL = 2.5; // 多近算"压在一起"
476
+ var OVERLAP_FRAC = 0.6; // 采样点落在容差内的比例门槛
477
+ var NSAMP = 9; // 每条边取几个采样点做重合判定
478
 
479
  function distToSegment(p, a, b) {
480
  var dx = b[0] - a[0], dy = b[1] - a[1];
 
493
  }
494
  function polyLength(pts) {
495
  var L = 0;
496
+ for (var i = 0; i + 1 < pts.length; i++)
497
+ L += Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]);
498
  return L;
499
  }
500
+ /* 沿折线等距取 n 个点(含两端)。 */
501
+ function sampleAlong(pts, n) {
 
502
  var total = polyLength(pts);
503
+ if (!total) return [pts[0]];
504
  var segs = [];
505
  for (var i = 0; i + 1 < pts.length; i++)
506
  segs.push(Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]));
507
+ var out = [];
508
  for (var k = 0; k < n; k++) {
509
  var want = total * k / (n - 1), acc = 0, j = 0;
510
  while (j < segs.length && acc + segs[j] < want) { acc += segs[j]; j++; }
511
  if (j >= segs.length) j = segs.length - 1;
512
  var t = segs[j] ? (want - acc) / segs[j] : 0;
513
+ out.push([pts[j][0] + (pts[j + 1][0] - pts[j][0]) * t,
514
+ pts[j][1] + (pts[j + 1][1] - pts[j][1]) * t]);
 
 
 
515
  }
516
  return out;
517
  }
518
+ /* 两条边压在一起的程度。取两个方向的较大者——一条短边整个趴在长边上时,
519
+ 短→长的比例高而长→短的低,只看一个方向会漏掉。 */
520
+ function overlapFraction(a, b) {
521
+ var hit = 0;
522
+ var pts = sampleAlong(a, NSAMP);
523
+ pts.forEach(function (p) { if (distToPolyline(p, b) <= NEAR_TOL) hit++; });
524
+ return hit / pts.length;
525
  }
526
 
527
+ /* 返回每条边的平移量(沿自身法线,单位 raster)。不重合的为 0。 */
528
+ function planOffsets(polys) {
529
  var n = polys.length;
530
+ var parent = [];
531
+ for (var i = 0; i < n; i++) parent.push(i);
532
+ function find(x) { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; }
533
+ function join(x, y) { var a = find(x), b = find(y); if (a !== b) parent[Math.max(a, b)] = Math.min(a, b); }
534
+
535
+ for (var i = 0; i < n; i++) {
536
+ if (!polys[i] || polys[i].length < 2) continue;
537
+ for (var j = i + 1; j < n; j++) {
538
+ if (!polys[j] || polys[j].length < 2) continue;
539
+ var f = Math.max(overlapFraction(polys[i], polys[j]),
540
+ overlapFraction(polys[j], polys[i]));
541
+ if (f >= OVERLAP_FRAC) join(i, j);
542
+ }
543
+ }
544
+ var groups = {};
545
+ for (var k = 0; k < n; k++) {
546
+ if (!polys[k] || polys[k].length < 2) continue;
547
+ var r = find(k);
548
+ (groups[r] || (groups[r] = [])).push(k);
549
+ }
550
+ var off = {};
551
+ Object.keys(groups).forEach(function (g) {
552
+ var list = groups[g];
553
+ if (list.length < 2) return;
554
+ list.sort(function (a, b) { return a - b; }); // 确定性:按边的绘制序
555
+ // 组大时收窄间距,保证最外侧那条仍在节点圆点内(否则边看着从节点脱开)
556
+ var sep = Math.min(SEP, 2 * MAX_OFF / (list.length - 1));
557
+ list.forEach(function (ei, idx) {
558
+ off[ei] = (idx - (list.length - 1) / 2) * sep;
 
 
 
 
 
 
559
  });
560
  });
561
+ return off;
562
+ }
563
 
564
+ /* 整条折线沿法线平移 dist。直线平移后仍是直线。
565
+ 拐角处取相邻两段法线的平均,折线不会在拐点裂开。 */
566
+ function offsetPolyline(pts, dist) {
567
+ if (!dist || pts.length < 2) return pts;
568
+ // 法线方向必须与「边的朝向」无关。边的两端按 source<target 的字母序存,
569
+ // 与几何走向无关——同一条线上首尾相反的两条边,各按自己的方向算法线会
570
+ // 指向相反侧,于是 +d 和 -d 把它们推到了**同一边**,等于没分开。
571
+ // 把整体走向归一到 [0,180),方向落在下半圆时翻转平移量。
572
+ var deg = (Math.atan2(pts[pts.length - 1][1] - pts[0][1],
573
+ pts[pts.length - 1][0] - pts[0][0]) * 180 / Math.PI + 360) % 360;
574
+ if (deg >= 180) dist = -dist;
575
+ var norms = [];
576
+ for (var i = 0; i + 1 < pts.length; i++) {
577
+ var dx = pts[i + 1][0] - pts[i][0], dy = pts[i + 1][1] - pts[i][1];
578
+ var L = Math.hypot(dx, dy) || 1;
579
+ norms.push([-dy / L, dx / L]);
580
+ }
581
+ return pts.map(function (p, i) {
582
+ var a = norms[Math.max(0, i - 1)], b = norms[Math.min(norms.length - 1, i)];
583
+ var nx = (a[0] + b[0]) / 2, ny = (a[1] + b[1]) / 2;
584
+ var L = Math.hypot(nx, ny) || 1;
585
+ return [p[0] + nx / L * dist, p[1] + ny / L * dist];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
586
  });
587
  }
588
 
 
614
  }
615
  return { e: e, costly: costly, road: false, pts: [a, b] };
616
  });
617
+ // 整条边平移:直线平移后仍是直线
618
+ var off = planOffsets(geo.map(function (g) { return g ? g.pts : []; }));
619
 
620
  geo.forEach(function (g, ei) {
621
  if (!g) return;
 
624
  var isSel = sel && sel.kind === 'edge'
625
  && sel.key[1] === e.source_label && sel.key[2] === e.target_label;
626
  var dead = !e.in_graph;
627
+ var shift = off[ei] || 0;
628
+ var pts = offsetPolyline(g.pts, shift);
629
+
630
+ var el;
631
+ if (pts.length === 2) {
632
+ el = mk('line', {
633
+ x1: pts[0][0].toFixed(2), y1: pts[0][1].toFixed(2),
634
+ x2: pts[1][0].toFixed(2), y2: pts[1][1].toFixed(2),
635
+ stroke: isSel ? '#fff' : colour,
636
+ 'stroke-width': isSel ? 3 : 2.0,
637
+ opacity: dead ? .22 : 1,
638
+ 'stroke-linecap': 'round',
639
+ style: 'cursor:pointer',
640
+ });
641
+ } else {
642
+ el = mk('polyline', {
643
+ points: pts.map(function (q) { return q[0].toFixed(2) + ',' + q[1].toFixed(2); }).join(' '),
644
+ fill: 'none', stroke: isSel ? '#fff' : colour,
645
+ 'stroke-width': isSel ? 3 : 1.8,
646
+ opacity: dead ? .22 : 1,
647
+ 'stroke-linejoin': 'round', 'stroke-linecap': 'round',
648
+ style: 'cursor:pointer',
649
+ });
650
+ }
651
  el.onclick = function () { pickEdge(e); };
652
  var title = mk('title', {});
653
  title.textContent = e.source_label + '–' + e.target_label + ' · ' +
654
  (EDGE_LABEL[e.origin] || e.origin || '') +
655
+ (shift ? '(与其它边重合,已错开显示)' : '');
656
  el.appendChild(title);
657
  svg.appendChild(el);
658
  });
web/index.html CHANGED
@@ -180,8 +180,8 @@
180
  <span class="image-kind">Scene Graph</span>
181
  <div class="map-card-tools">
182
  <div class="basemap-switch" role="group" aria-label="边的画法">
183
- <button type="button" id="edgeRoad" class="active" title="沿道路走线">ROAD</button>
184
- <button type="button" id="edgeStraight" title="直线连接">LINE</button>
185
  </div>
186
  <div class="basemap-switch" role="group" aria-label="底图切换">
187
  <button type="button" id="baseSat" class="active">SAT</button>
 
180
  <span class="image-kind">Scene Graph</span>
181
  <div class="map-card-tools">
182
  <div class="basemap-switch" role="group" aria-label="边的画法">
183
+ <button type="button" id="edgeStraight" class="active" title="直线连接(默认,更醒目)">LINE</button>
184
+ <button type="button" id="edgeRoad" title="沿道路走线">ROAD</button>
185
  </div>
186
  <div class="basemap-switch" role="group" aria-label="底图切换">
187
  <button type="button" id="baseSat" class="active">SAT</button>