Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- web/app.js +137 -103
web/app.js
CHANGED
|
@@ -453,20 +453,27 @@ function drawScene() {
|
|
| 453 |
}
|
| 454 |
|
| 455 |
/* ---------- 重叠边的分离 ----------------------------------------------
|
| 456 |
-
* 同一条街上常常叠着好几条边
|
| 457 |
-
*
|
| 458 |
-
* 后只有最后画的那条可见,颜色就"丢"了一种。
|
| 459 |
*
|
| 460 |
-
*
|
| 461 |
-
*
|
| 462 |
-
* (早先的版本按「中点+角度」分桶,结果在共享节点处首尾相接的相邻边被
|
| 463 |
-
* 误判成重叠,全库 83% 的边都被挪开——那不是重叠,是正常的链式连接。)
|
| 464 |
*
|
| 465 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
*/
|
| 467 |
-
var
|
| 468 |
-
var
|
| 469 |
-
var
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 487 |
-
|
| 488 |
-
var
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
var
|
|
|
|
|
|
|
|
|
|
|
|
|
| 495 |
for (var k = 0; k < n; k++) {
|
| 496 |
-
var want =
|
| 497 |
while (j < segs.length && acc + segs[j] < want) { acc += segs[j]; j++; }
|
|
|
|
| 498 |
var t = segs[j] ? (want - acc) / segs[j] : 0;
|
| 499 |
-
|
| 500 |
-
|
|
|
|
|
|
|
|
|
|
| 501 |
}
|
| 502 |
return out;
|
| 503 |
}
|
| 504 |
-
function
|
| 505 |
-
var
|
| 506 |
-
|
| 507 |
-
pts.forEach(function (p) { if (distToPolyline(p, b) <= COINCIDE_TOL) hit++; });
|
| 508 |
-
return hit / pts.length;
|
| 509 |
}
|
| 510 |
|
| 511 |
-
/* 返回
|
| 512 |
-
function
|
| 513 |
var n = polys.length;
|
| 514 |
-
var
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 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
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
});
|
| 563 |
}
|
| 564 |
|
|
@@ -590,7 +633,8 @@ function drawGraph() {
|
|
| 590 |
}
|
| 591 |
return { e: e, costly: costly, road: false, pts: [a, b] };
|
| 592 |
});
|
| 593 |
-
|
|
|
|
| 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
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
}
|
| 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 + ' · ' +
|
| 630 |
-
|
|
|
|
| 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 |
});
|