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

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. web/app.js +137 -103
web/app.js CHANGED
@@ -453,20 +453,27 @@ function drawScene() {
453
  }
454
 
455
  /* ---------- 重叠边的分离 ----------------------------------------------
456
- * 同一条街上常常叠着好几条边:桥接边从 A 直连到 B,而 A-B 之间的走廊链
457
- * 正好也沿着同一条路;不同走廊被合并成同一几何时也会完全重合。叠在一起
458
- * 后只有最后画的那条可见,颜色就"丢"了一种。
459
  *
460
- * 判定用真实几何而不是哈希:沿边 A 采样若干点,量它们到边 B 折线的距离,
461
- * 若绝大部分点都落在 COINCIDE_TOL 之内,就认为两条边视觉上压在一起。
462
- * (早先的版本按「中点+角度」分桶,结果在共享节点处首尾相接的相邻边被
463
- * 误判成重叠,全库 83% 的边都被挪开——那不是重叠,是正常的链式连接。)
464
  *
465
- * 同组的边各自沿法线偏移 (i - (n-1)/2) * SEG_SEP,并排显示。
 
 
 
 
 
 
 
466
  */
467
- var SEG_SEP = 3.2; // 相邻两条重合边的间距(raster 单位,400 = 全宽)
468
- var COINCIDE_TOL = 2.5; // 视为"压在一起"的垂直距离
469
- var COINCIDE_FRAC = 0.75; // 采样点落在容差内的比例门槛
 
 
 
470
 
471
  function distToSegment(p, a, b) {
472
  var dx = b[0] - a[0], dy = b[1] - a[1];
@@ -483,82 +490,118 @@ function distToPolyline(p, pts) {
483
  }
484
  return best;
485
  }
486
- /* 沿折线等距取 n 个点(含两端) */
487
- function sampleAlong(pts, n) {
488
- var segs = [], total = 0;
489
- for (var i = 0; i + 1 < pts.length; i++) {
490
- var L = Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]);
491
- segs.push(L); total += L;
492
- }
493
- if (!total) return [pts[0]];
494
- var out = [], step = total / (n - 1);
 
 
 
 
495
  for (var k = 0; k < n; k++) {
496
- var want = Math.min(k * step, total), acc = 0, j = 0;
497
  while (j < segs.length && acc + segs[j] < want) { acc += segs[j]; j++; }
 
498
  var t = segs[j] ? (want - acc) / segs[j] : 0;
499
- out.push([pts[j][0] + (pts[j + 1][0] - pts[j][0]) * t,
500
- pts[j][1] + (pts[j + 1][1] - pts[j][1]) * t]);
 
 
 
501
  }
502
  return out;
503
  }
504
- function coincideFraction(a, b) {
505
- var pts = sampleAlong(a, 12);
506
- var hit = 0;
507
- pts.forEach(function (p) { if (distToPolyline(p, b) <= COINCIDE_TOL) hit++; });
508
- return hit / pts.length;
509
  }
510
 
511
- /* 返回 edgeIndex -> 偏移量。只有真正重合的边才会拿到非零值。 */
512
- function planOffsets(polys) {
513
  var n = polys.length;
514
- var parent = [];
515
- for (var i = 0; i < n; i++) parent.push(i);
516
- function find(x) { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; }
517
- function join(x, y) { var a = find(x), b = find(y); if (a !== b) parent[b] = a; }
518
-
519
- for (var i = 0; i < n; i++) {
520
- if (!polys[i] || polys[i].length < 2) continue;
521
- for (var j = i + 1; j < n; j++) {
522
- if (!polys[j] || polys[j].length < 2) continue;
523
- // 一条短边完全压在长边上时,只有短→长这个方向比例高,取 max
524
- var f = Math.max(coincideFraction(polys[i], polys[j]),
525
- coincideFraction(polys[j], polys[i]));
526
- if (f >= COINCIDE_FRAC) join(i, j);
527
- }
528
- }
529
- var groups = {};
530
- for (var k = 0; k < n; k++) {
531
- if (!polys[k] || polys[k].length < 2) continue;
532
- var r = find(k);
533
- (groups[r] || (groups[r] = [])).push(k);
534
- }
535
- var shift = {};
536
- Object.keys(groups).forEach(function (g) {
537
- var list = groups[g];
538
- if (list.length < 2) return; // 不重合的一条都不动
539
- list.sort(function (a, b) { return a - b; }); // 稳定:按边的绘制序
540
- list.forEach(function (ei, idx) {
541
- shift[ei] = (idx - (list.length - 1) / 2) * SEG_SEP;
542
  });
543
  });
544
- return shift;
545
- }
546
 
547
- /* 按法线方向平移一条折线。每段单独算法线,拐角处取相邻两段的平均,
548
- 这样折线偏移后不会在拐点裂开。 */
549
- function offsetPolyline(pts, dist) {
550
- if (!dist || pts.length < 2) return pts;
551
- var norms = [];
552
- for (var i = 0; i + 1 < pts.length; i++) {
553
- var dx = pts[i + 1][0] - pts[i][0], dy = pts[i + 1][1] - pts[i][1];
554
- var L = Math.hypot(dx, dy) || 1;
555
- norms.push([-dy / L, dx / L]);
556
- }
557
- return pts.map(function (p, i) {
558
- var a = norms[Math.max(0, i - 1)], b = norms[Math.min(norms.length - 1, i)];
559
- var nx = (a[0] + b[0]) / 2, ny = (a[1] + b[1]) / 2;
560
- var L = Math.hypot(nx, ny) || 1;
561
- return [p[0] + nx / L * dist, p[1] + ny / L * dist];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
562
  });
563
  }
564
 
@@ -590,7 +633,8 @@ function drawGraph() {
590
  }
591
  return { e: e, costly: costly, road: false, pts: [a, b] };
592
  });
593
- var rank = planOffsets(geo.map(function (g) { return g ? g.pts : []; }));
 
594
 
595
  geo.forEach(function (g, ei) {
596
  if (!g) return;
@@ -599,35 +643,25 @@ function drawGraph() {
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;
602
-
603
- var shift = rank[ei] || 0;
604
- var pts = offsetPolyline(g.pts, shift);
605
-
606
- var el;
607
- if (g.road) {
608
- el = mk('polyline', {
609
- points: pts.map(function (q) { return q[0].toFixed(2) + ',' + q[1].toFixed(2); }).join(' '),
610
- fill: 'none', stroke: isSel ? '#fff' : colour,
611
- 'stroke-width': isSel ? 3 : 1.7, opacity: dead ? .22 : 1,
612
- 'stroke-linejoin': 'round', 'stroke-linecap': 'round',
613
- style: 'cursor:pointer',
614
- });
615
- } else {
616
- el = mk('line', {
617
- x1: pts[0][0], y1: pts[0][1], x2: pts[1][0], y2: pts[1][1],
618
- stroke: isSel ? '#fff' : colour,
619
- 'stroke-width': isSel ? 3 : (g.costly ? 2.2 : 1.7),
620
- opacity: dead ? .22 : 1,
621
- 'stroke-dasharray': g.costly ? '6 4' : '',
622
- style: 'cursor:pointer',
623
- });
624
- }
625
- // 被挪开的边加一层深色描边,和邻线之间留出视觉缝隙
626
- if (shift) el.setAttribute('paint-order', 'stroke');
627
  el.onclick = function () { pickEdge(e); };
628
  var title = mk('title', {});
629
- title.textContent = e.source_label + '–' + e.target_label + ' · ' + (e.origin || '')
630
- + (shift ? '(与其它边重合,已错开显示)' : '');
 
631
  el.appendChild(title);
632
  svg.appendChild(el);
633
  });
 
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];
 
490
  }
491
  return best;
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
  }
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
  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
  });