* _mergeLineStrings * Merge LineString features that share a prophash and have near-coincident * endpoints along a tile edge. This is the LineString equivalent of * `_mergePolygons` — it stitches road segments split by MVT tile clipping. * * @param {Object} cache * @param {stri
(cache, prophash, featureIDs, lowTile, highTile)
| 654 | * @param {Tile} highTile |
| 655 | */ |
| 656 | _mergeLineStrings(cache, prophash, featureIDs, lowTile, highTile) { |
| 657 | const features = Array.from(featureIDs) |
| 658 | .map(id => cache.features.get(id)) |
| 659 | .filter(f => f && f.geojson?.geometry?.type === 'LineString'); |
| 660 | |
| 661 | if (features.length < 2) return; |
| 662 | |
| 663 | const SNAP_TOL = 5e-5; // ~5.5 m at equator |
| 664 | |
| 665 | // Build a working list of coordinate arrays (+ a reference to source properties) |
| 666 | let lines = features.map(f => ({ |
| 667 | coords: f.geojson.geometry.coordinates.slice(), // shallow copy of array |
| 668 | properties: f.geojson.properties |
| 669 | })); |
| 670 | |
| 671 | // Iteratively merge pairs of lines whose endpoints are close together |
| 672 | // AND whose approach directions are compatible (prevents merging perpendicular roads) |
| 673 | const MAX_MERGE_ANGLE = 30; // degrees — roads must approach within 30° of each other |
| 674 | let didMerge = true; |
| 675 | while (didMerge) { |
| 676 | didMerge = false; |
| 677 | for (let i = 0; i < lines.length && !didMerge; i++) { |
| 678 | for (let j = i + 1; j < lines.length && !didMerge; j++) { |
| 679 | const a = lines[i].coords; |
| 680 | const b = lines[j].coords; |
| 681 | const aFirst = a[0]; |
| 682 | const aLast = a[a.length - 1]; |
| 683 | const bFirst = b[0]; |
| 684 | const bLast = b[b.length - 1]; |
| 685 | |
| 686 | let merged = null; |
| 687 | |
| 688 | if (this._pointsClose(aLast, bFirst, SNAP_TOL) && this._directionsCompatible(a, b, MAX_MERGE_ANGLE)) { |
| 689 | merged = this._joinLineCoords(a, b, SNAP_TOL); // A ──→ B |
| 690 | } else if (this._pointsClose(aFirst, bLast, SNAP_TOL) && this._directionsCompatible(b, a, MAX_MERGE_ANGLE)) { |
| 691 | merged = this._joinLineCoords(b, a, SNAP_TOL); // B ──→ A |
| 692 | } else if (this._pointsClose(aLast, bLast, SNAP_TOL)) { |
| 693 | const bRev = b.slice().reverse(); |
| 694 | if (this._directionsCompatible(a, bRev, MAX_MERGE_ANGLE)) { |
| 695 | merged = this._joinLineCoords(a, bRev, SNAP_TOL); // A ──→ rev(B) |
| 696 | } |
| 697 | } else if (this._pointsClose(aFirst, bFirst, SNAP_TOL)) { |
| 698 | const aRev = a.slice().reverse(); |
| 699 | if (this._directionsCompatible(aRev, b, MAX_MERGE_ANGLE)) { |
| 700 | merged = this._joinLineCoords(aRev, b, SNAP_TOL); // rev(A) ──→ B |
| 701 | } |
| 702 | } |
| 703 | |
| 704 | if (merged) { |
| 705 | lines[i] = { coords: merged, properties: lines[i].properties }; |
| 706 | lines.splice(j, 1); |
| 707 | didMerge = true; |
| 708 | } |
| 709 | } |
| 710 | } |
| 711 | } |
| 712 | |
| 713 | // ── Re-cache results ─────────────────────────────────────────────────── |
no test coverage detected