mapshaper 0.7.27 → 0.7.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mapshaper.js +2327 -642
- package/package.json +3 -3
- package/www/mapshaper.js +2327 -642
package/mapshaper.js
CHANGED
|
@@ -31748,10 +31748,6 @@ ${svg}
|
|
|
31748
31748
|
.option('tolerance', {
|
|
31749
31749
|
describe: 'acceptable error when buffering lines and polygons (default is 1%)'
|
|
31750
31750
|
})
|
|
31751
|
-
// .option('circle-quality', {
|
|
31752
|
-
// // segments per circle in joins and caps
|
|
31753
|
-
// type: 'integer'
|
|
31754
|
-
// })
|
|
31755
31751
|
.option('cap-style', {
|
|
31756
31752
|
describe: 'flat or round (default is round)'
|
|
31757
31753
|
})
|
|
@@ -31764,6 +31760,14 @@ ${svg}
|
|
|
31764
31760
|
describe: '[polygons] buffer unshared boundaries without covering source polygon areas',
|
|
31765
31761
|
type: 'flag'
|
|
31766
31762
|
})
|
|
31763
|
+
.option('fill-gaps', {
|
|
31764
|
+
describe: '[polygons] fill enclosed holes and inlets whose opening is narrower than the buffer distance, without growing the outer boundary',
|
|
31765
|
+
type: 'flag'
|
|
31766
|
+
})
|
|
31767
|
+
.option('max-widening', {
|
|
31768
|
+
describe: '[with fill-gaps] fill interior gaps up to this multiple of the buffer distance wide (default 5); wider gaps are kept open',
|
|
31769
|
+
type: 'number'
|
|
31770
|
+
})
|
|
31767
31771
|
.option('geodesic', {
|
|
31768
31772
|
describe: '[projected data] buffer using geodesic distances',
|
|
31769
31773
|
type: 'flag'
|
|
@@ -31777,7 +31781,6 @@ ${svg}
|
|
|
31777
31781
|
type: 'integer'
|
|
31778
31782
|
})
|
|
31779
31783
|
.option('quad-segs', {
|
|
31780
|
-
// .option('arc-quality', {
|
|
31781
31784
|
describe: 'segments per quarter-circle in joins and caps (default is 8)',
|
|
31782
31785
|
type: 'integer'
|
|
31783
31786
|
})
|
|
@@ -31785,10 +31788,18 @@ ${svg}
|
|
|
31785
31788
|
// generate initial buffer shapes but don't dissolve them
|
|
31786
31789
|
type: 'flag'
|
|
31787
31790
|
})
|
|
31788
|
-
.option('debug-
|
|
31791
|
+
.option('debug-mosaic', {
|
|
31789
31792
|
type: 'flag'
|
|
31790
31793
|
})
|
|
31791
|
-
.option('debug-
|
|
31794
|
+
.option('debug-voronoi', {
|
|
31795
|
+
// output the inter-feature medial-axis (Voronoi) cut-lines used to
|
|
31796
|
+
// partition contested space in a topological polygon buffer
|
|
31797
|
+
type: 'flag'
|
|
31798
|
+
})
|
|
31799
|
+
.option('debug-delaunay', {
|
|
31800
|
+
// output the medial-construction triangles (Delaunay triangles bridging
|
|
31801
|
+
// two features within buffer reach) used to build the medial axis in a
|
|
31802
|
+
// topological polygon buffer
|
|
31792
31803
|
type: 'flag'
|
|
31793
31804
|
})
|
|
31794
31805
|
.option('left', {
|
|
@@ -31816,19 +31827,25 @@ ${svg}
|
|
|
31816
31827
|
})
|
|
31817
31828
|
.option('no-loop-removal', {
|
|
31818
31829
|
// Loop removal (collapsing self-overlap loops from two-sided line buffers
|
|
31819
|
-
// before the dissolve) is on by default; this opts out.
|
|
31820
|
-
|
|
31821
|
-
|
|
31830
|
+
// before the dissolve) is on by default; this opts out.
|
|
31831
|
+
type: 'flag'
|
|
31832
|
+
})
|
|
31833
|
+
.option('loop-removal-turn-gate', {
|
|
31834
|
+
// Undocumented: for two-sided open-path buffers, use the source-turn-gate
|
|
31835
|
+
// loop-removal method instead of the default crossing-direction method.
|
|
31836
|
+
// Kept as an alternative for A/B comparison and as a conservative fallback.
|
|
31822
31837
|
type: 'flag'
|
|
31823
31838
|
})
|
|
31824
|
-
.option('
|
|
31825
|
-
// Undocumented escape hatch: build buffers with the older sector-
|
|
31826
|
-
// construction
|
|
31827
|
-
// band-coverage audit, unioned by a boundary flood
|
|
31828
|
-
// default
|
|
31829
|
-
//
|
|
31830
|
-
//
|
|
31831
|
-
//
|
|
31839
|
+
.option('band-method', {
|
|
31840
|
+
// Undocumented escape hatch: build buffers with the older band (sector-
|
|
31841
|
+
// band) construction -- per-segment offset bands + join-sector rings + a
|
|
31842
|
+
// band-coverage audit, unioned by a boundary flood -- instead of the
|
|
31843
|
+
// default construction (the clean-outline construction for polygon grow,
|
|
31844
|
+
// the winding-fill construction otherwise). Applies to line buffers (one-
|
|
31845
|
+
// and two-sided) and all polygon buffers (positive, negative,
|
|
31846
|
+
// topological); where a path's default already is the band construction,
|
|
31847
|
+
// this is a no-op. Kept as a slower-but-conservative fallback and a
|
|
31848
|
+
// debugging aid in case the default construction mishandles some input.
|
|
31832
31849
|
type: 'flag'
|
|
31833
31850
|
})
|
|
31834
31851
|
.option('no-cleanup', {
|
|
@@ -40670,7 +40687,7 @@ ${svg}
|
|
|
40670
40687
|
i2y = y;
|
|
40671
40688
|
}
|
|
40672
40689
|
|
|
40673
|
-
const center = circumcenter(i0x, i0y, i1x, i1y, i2x, i2y);
|
|
40690
|
+
const center = circumcenter$1(i0x, i0y, i1x, i1y, i2x, i2y);
|
|
40674
40691
|
this._cx = center.x;
|
|
40675
40692
|
this._cy = center.y;
|
|
40676
40693
|
|
|
@@ -40940,7 +40957,7 @@ ${svg}
|
|
|
40940
40957
|
return x * x + y * y;
|
|
40941
40958
|
}
|
|
40942
40959
|
|
|
40943
|
-
function circumcenter(ax, ay, bx, by, cx, cy) {
|
|
40960
|
+
function circumcenter$1(ax, ay, bx, by, cx, cy) {
|
|
40944
40961
|
const dx = bx - ax;
|
|
40945
40962
|
const dy = by - ay;
|
|
40946
40963
|
const ex = cx - ax;
|
|
@@ -41123,19 +41140,6 @@ ${svg}
|
|
|
41123
41140
|
{flat: false, no_holes: false, per_part_holes: !!opts.per_part_holes});
|
|
41124
41141
|
|
|
41125
41142
|
// rewindPolygonParts(lyr, nodes);
|
|
41126
|
-
if (opts.debug_winding) {
|
|
41127
|
-
lyr.shapes = lyr.shapes.map(function(shp, i) {
|
|
41128
|
-
var tiles = mosaicIndex.getTilesByShapeIds([i]);
|
|
41129
|
-
if (!tiles.length) return null;
|
|
41130
|
-
var parts = [];
|
|
41131
|
-
tiles.forEach(function(tile) {
|
|
41132
|
-
parts.push.apply(parts, tile);
|
|
41133
|
-
});
|
|
41134
|
-
return parts;
|
|
41135
|
-
});
|
|
41136
|
-
return;
|
|
41137
|
-
}
|
|
41138
|
-
|
|
41139
41143
|
if (opts.debug_mosaic) {
|
|
41140
41144
|
tmp = composeMosaicLayer(lyr, mosaicIndex.mosaic);
|
|
41141
41145
|
lyr.shapes = tmp.shapes;
|
|
@@ -41346,10 +41350,17 @@ ${svg}
|
|
|
41346
41350
|
|
|
41347
41351
|
// Returns {ring, srcPos}: ring is the closed coordinate ring (first point
|
|
41348
41352
|
// repeated as last); srcPos is the parallel source-position array.
|
|
41349
|
-
|
|
41353
|
+
// @allowDegenerate: return null instead of erroring when the ring collapsed
|
|
41354
|
+
// to fewer than 3 points (an offset loop whose source ring shrank away, e.g.
|
|
41355
|
+
// a hole smaller than the buffer radius) -- a normal outcome, not a bug.
|
|
41356
|
+
self.done = function(allowDegenerate) {
|
|
41350
41357
|
var ring = path.slice().reverse().concat(buffer);
|
|
41351
41358
|
var srcPos = pathPos.slice().reverse().concat(bufferPos);
|
|
41352
41359
|
if (ring.length < 3) {
|
|
41360
|
+
if (allowDegenerate) {
|
|
41361
|
+
init();
|
|
41362
|
+
return null;
|
|
41363
|
+
}
|
|
41353
41364
|
error('Defective buffer ring:', ring);
|
|
41354
41365
|
}
|
|
41355
41366
|
ring.push(ring[0].concat());
|
|
@@ -41401,9 +41412,7 @@ ${svg}
|
|
|
41401
41412
|
// Max source-path turn (degrees) a collapsible loop may span. Below this the
|
|
41402
41413
|
// path is too straight to have enclosed an uncovered region, so the crossing is
|
|
41403
41414
|
// a covered overshoot; above it the loop may be a real buffer hole and is left
|
|
41404
|
-
// for the dissolve.
|
|
41405
|
-
// 150 degrees while the tightest observed real hole spans ~210, so this sits in
|
|
41406
|
-
// the gap with margin on both sides. A fully enclosing loop turns ~360.
|
|
41415
|
+
// for the dissolve.
|
|
41407
41416
|
var BUFFER_LOOP_MAX_TURN = 150;
|
|
41408
41417
|
|
|
41409
41418
|
// ring: closed ring (first point repeated as last) of [x, y] points.
|
|
@@ -41419,75 +41428,173 @@ ${svg}
|
|
|
41419
41428
|
var gated = !!(srcPos && turnPrefix);
|
|
41420
41429
|
var n = ring.length - 1; // distinct points (the last repeats the first)
|
|
41421
41430
|
// Compact retained points into `out` instead of splicing collapsed spans out
|
|
41422
|
-
// of one array
|
|
41423
|
-
//
|
|
41424
|
-
// and looks a bounded window forward, so it can append kept points and drop a
|
|
41425
|
-
// collapsed span by advancing the read cursor -- O(1) per collapse.
|
|
41426
|
-
//
|
|
41427
|
-
// `a` is the current anchor (the last committed point, out[last]); `b` is the
|
|
41428
|
-
// point after it (a ring vertex, or a synthetic crossing after a collapse);
|
|
41429
|
-
// `r` indexes the ring vertex following `b`. This mirrors the in-place scan's
|
|
41430
|
-
// anchor i with a = ring[i], b = ring[i+1], r = i+2.
|
|
41431
|
+
// of one array. A collapse drops a span by advancing `nextRingIndex`, so the
|
|
41432
|
+
// ring tail is never shifted.
|
|
41431
41433
|
var out = [ring[0]];
|
|
41432
41434
|
var outPos = gated ? [srcPos[0]] : null;
|
|
41433
|
-
var
|
|
41434
|
-
var
|
|
41435
|
-
var
|
|
41435
|
+
var segmentEnd = ring[1];
|
|
41436
|
+
var segmentEndPos = gated ? srcPos[1] : 0;
|
|
41437
|
+
var nextRingIndex = 2; // first ring vertex after segmentEnd
|
|
41438
|
+
|
|
41436
41439
|
while (true) {
|
|
41437
|
-
var
|
|
41438
|
-
var
|
|
41439
|
-
var
|
|
41440
|
-
|
|
41441
|
-
var
|
|
41442
|
-
var
|
|
41443
|
-
|
|
41444
|
-
|
|
41440
|
+
var anchor = out[out.length - 1];
|
|
41441
|
+
var anchorPos = gated ? outPos[outPos.length - 1] : 0;
|
|
41442
|
+
var ax = anchor[0], ay = anchor[1];
|
|
41443
|
+
var bx = segmentEnd[0], by = segmentEnd[1];
|
|
41444
|
+
var lastScanIndex = Math.min(nextRingIndex + maxGap - 2, n - 2);
|
|
41445
|
+
var crossing = null;
|
|
41446
|
+
var crossingEndIndex = 0;
|
|
41447
|
+
|
|
41448
|
+
for (var scanIndex = nextRingIndex; scanIndex <= lastScanIndex; scanIndex++) {
|
|
41449
|
+
var c = ring[scanIndex], d = ring[scanIndex + 1];
|
|
41445
41450
|
var hit = segHit(ax, ay, bx, by, c[0], c[1], d[0], d[1]);
|
|
41446
41451
|
if (!hit) continue;
|
|
41447
41452
|
// Collapse only covered overshoots (small source turn). A larger source
|
|
41448
41453
|
// turn means the span may enclose a real buffer hole, which must be left
|
|
41449
41454
|
// for the dissolve -- whatever the pocket's winding orientation.
|
|
41450
|
-
|
|
41451
|
-
|
|
41455
|
+
var spanTurn = gated ? getSpanTurn(anchorPos, segmentEndPos, srcPos,
|
|
41456
|
+
nextRingIndex, scanIndex + 1, turnPrefix) : null;
|
|
41457
|
+
if (gated && spanTurn >= maxTurn) {
|
|
41452
41458
|
continue;
|
|
41453
41459
|
}
|
|
41454
|
-
|
|
41455
|
-
|
|
41456
|
-
// and advance the cursor past the collapsed vertices.
|
|
41457
|
-
b = hit;
|
|
41458
|
-
if (gated) bpos = apos; // collapsed point inherits the anchor's source pos
|
|
41459
|
-
r = r + t + 1;
|
|
41460
|
-
collapsed = true;
|
|
41460
|
+
crossing = hit;
|
|
41461
|
+
crossingEndIndex = scanIndex + 1;
|
|
41461
41462
|
break;
|
|
41462
41463
|
}
|
|
41463
|
-
|
|
41464
|
-
|
|
41465
|
-
|
|
41466
|
-
|
|
41467
|
-
|
|
41468
|
-
|
|
41469
|
-
|
|
41464
|
+
|
|
41465
|
+
if (crossing) {
|
|
41466
|
+
// Replace the collapsed span with the crossing. Keep the same anchor and
|
|
41467
|
+
// rescan because the new segment may cross another nearby segment.
|
|
41468
|
+
segmentEnd = crossing;
|
|
41469
|
+
if (gated) segmentEndPos = anchorPos;
|
|
41470
|
+
nextRingIndex = crossingEndIndex;
|
|
41471
|
+
continue;
|
|
41472
|
+
}
|
|
41473
|
+
|
|
41474
|
+
out.push(segmentEnd); // safe: anchor -> segmentEnd found no collapsible loop
|
|
41475
|
+
if (gated) outPos.push(segmentEndPos);
|
|
41476
|
+
if (nextRingIndex > n - 1) break; // no vertex left to become segmentEnd
|
|
41477
|
+
segmentEnd = ring[nextRingIndex];
|
|
41478
|
+
if (gated) segmentEndPos = srcPos[nextRingIndex];
|
|
41479
|
+
nextRingIndex++;
|
|
41470
41480
|
}
|
|
41471
41481
|
if (out.length < 4) return ring; // collapsed away; keep original
|
|
41472
41482
|
out.push(out[0].concat());
|
|
41473
41483
|
return out;
|
|
41474
41484
|
}
|
|
41475
41485
|
|
|
41476
|
-
//
|
|
41477
|
-
|
|
41478
|
-
|
|
41479
|
-
|
|
41480
|
-
|
|
41481
|
-
|
|
41486
|
+
// Signed area (x2) of the sub-loop X -> segmentEnd -> ring[lo..hi] -> X.
|
|
41487
|
+
function loopAreaSign(xx, xy, segEndX, segEndY, ring, lo, hi) {
|
|
41488
|
+
var s = (xx * segEndY - segEndX * xy);
|
|
41489
|
+
var px = segEndX, py = segEndY;
|
|
41490
|
+
for (var k = lo; k <= hi; k++) {
|
|
41491
|
+
var q = ring[k];
|
|
41492
|
+
s += px * q[1] - q[0] * py;
|
|
41493
|
+
px = q[0]; py = q[1];
|
|
41494
|
+
}
|
|
41495
|
+
s += px * xy - xx * py;
|
|
41496
|
+
return s; // >0 CCW, <0 CW
|
|
41497
|
+
}
|
|
41498
|
+
function ringSignedArea(ring) {
|
|
41499
|
+
var s = 0;
|
|
41500
|
+
for (var i = 0; i < ring.length - 1; i++) {
|
|
41501
|
+
s += ring[i][0] * ring[i + 1][1] - ring[i + 1][0] * ring[i][1];
|
|
41502
|
+
}
|
|
41503
|
+
return s;
|
|
41504
|
+
}
|
|
41505
|
+
|
|
41506
|
+
// Collapse self-overlap loops using the crossing-direction signal instead of the
|
|
41507
|
+
// source-turn gate (removeBufferRingLoops). Where a constructed offset ring has a
|
|
41508
|
+
// consistent +/-1 base winding -- the two-sided outline of an OPEN path -- the
|
|
41509
|
+
// winding sense of each minimal self-crossing loop classifies it exactly: a
|
|
41510
|
+
// sub-loop wound the SAME way as its parent ring is a covered fold-back overlap
|
|
41511
|
+
// (collapse it; the dissolve would only fill it), while a sub-loop wound the
|
|
41512
|
+
// OPPOSITE way bounds a winding-0 pocket the dissolve must keep as a hole.
|
|
41513
|
+
//
|
|
41514
|
+
// This is more precise than the turn-gate's source-turn heuristic and needs no
|
|
41515
|
+
// source-path provenance (srcPos/turnPrefix), only the ring geometry. It does
|
|
41516
|
+
// NOT apply to the winding-fill construction used for closed rings / polygons,
|
|
41517
|
+
// where the base winding is not a constant +/-1, so a local loop's winding sense
|
|
41518
|
+
// does not determine the absolute (hole vs covered) winding of its interior.
|
|
41519
|
+
//
|
|
41520
|
+
// Pass 1 marks every vertex inside an opposite-wound (hole) loop so the collapse
|
|
41521
|
+
// pass never eats a hole, including an overlap loop that happens to wrap one.
|
|
41522
|
+
function removeBufferRingLoopsByDirection(ring, maxGap) {
|
|
41523
|
+
if (!ring || ring.length < 6) return ring;
|
|
41524
|
+
var n = ring.length - 1;
|
|
41525
|
+
var parentCCW = (ringSignedArea(ring) >= 0);
|
|
41526
|
+
// Pass 1: a sub-loop wound OPPOSITE to its parent ring encloses a winding-0
|
|
41527
|
+
// hole the dissolve must keep; mark its vertices so the collapse pass never
|
|
41528
|
+
// eats it (covers an overlap loop that happens to wrap a hole).
|
|
41529
|
+
var holeVertex = new Uint8Array(n);
|
|
41530
|
+
for (var i = 0; i < n - 1; i++) {
|
|
41531
|
+
var a = ring[i], b = ring[i + 1];
|
|
41532
|
+
var jMax = Math.min(i + maxGap, n - 1);
|
|
41533
|
+
for (var j = i + 2; j <= jMax; j++) {
|
|
41534
|
+
if (i === 0 && j === n - 1) continue;
|
|
41535
|
+
var c = ring[j], d = ring[j + 1];
|
|
41536
|
+
var hit = segHit(a[0], a[1], b[0], b[1], c[0], c[1], d[0], d[1]);
|
|
41537
|
+
if (!hit) continue;
|
|
41538
|
+
var loopCCW = loopAreaSign(hit[0], hit[1], b[0], b[1], ring, i + 2, j) >= 0;
|
|
41539
|
+
if (loopCCW !== parentCCW) {
|
|
41540
|
+
for (var k = i + 1; k <= j; k++) holeVertex[k] = 1;
|
|
41541
|
+
}
|
|
41542
|
+
}
|
|
41543
|
+
}
|
|
41544
|
+
// Pass 2: collapse sub-loops wound the SAME way as the parent (covered
|
|
41545
|
+
// fold-back overlaps) unless they would eat a hole vertex.
|
|
41546
|
+
var out = [ring[0]];
|
|
41547
|
+
var segmentEnd = ring[1];
|
|
41548
|
+
var nextRingIndex = 2;
|
|
41549
|
+
while (true) {
|
|
41550
|
+
var anchor = out[out.length - 1];
|
|
41551
|
+
var ax = anchor[0], ay = anchor[1], bx = segmentEnd[0], by = segmentEnd[1];
|
|
41552
|
+
var lastScanIndex = Math.min(nextRingIndex + maxGap - 2, n - 2);
|
|
41553
|
+
var crossing = null, crossingEndIndex = 0;
|
|
41554
|
+
for (var s = nextRingIndex; s <= lastScanIndex; s++) {
|
|
41555
|
+
var cc = ring[s], dd = ring[s + 1];
|
|
41556
|
+
var hit2 = segHit(ax, ay, bx, by, cc[0], cc[1], dd[0], dd[1]);
|
|
41557
|
+
if (!hit2) continue;
|
|
41558
|
+
var ovCCW = loopAreaSign(hit2[0], hit2[1], bx, by, ring, nextRingIndex, s) >= 0;
|
|
41559
|
+
if (ovCCW !== parentCCW) continue; // opposite-wound loop is a hole: keep
|
|
41560
|
+
var wrapsHole = false;
|
|
41561
|
+
for (var k2 = nextRingIndex; k2 <= s; k2++) {
|
|
41562
|
+
if (holeVertex[k2]) { wrapsHole = true; break; }
|
|
41563
|
+
}
|
|
41564
|
+
if (wrapsHole) continue;
|
|
41565
|
+
crossing = hit2;
|
|
41566
|
+
crossingEndIndex = s + 1;
|
|
41567
|
+
break;
|
|
41568
|
+
}
|
|
41569
|
+
if (crossing) {
|
|
41570
|
+
segmentEnd = crossing;
|
|
41571
|
+
nextRingIndex = crossingEndIndex;
|
|
41572
|
+
continue;
|
|
41573
|
+
}
|
|
41574
|
+
out.push(segmentEnd);
|
|
41575
|
+
if (nextRingIndex > n - 1) break;
|
|
41576
|
+
segmentEnd = ring[nextRingIndex];
|
|
41577
|
+
nextRingIndex++;
|
|
41578
|
+
}
|
|
41579
|
+
if (out.length < 4) return ring;
|
|
41580
|
+
out.push(out[0].concat());
|
|
41581
|
+
return out;
|
|
41582
|
+
}
|
|
41583
|
+
|
|
41584
|
+
// Absolute source turn spanned by a crossing candidate. The span covers the
|
|
41585
|
+
// anchor position `apos`, its successor `bpos`, and ring positions srcPos[lo..hi].
|
|
41586
|
+
// A pocket touching a cap (NaN position) is never treated as a covered overshoot.
|
|
41587
|
+
function getSpanTurn(apos, bpos, srcPos, lo, hi, turnPrefix) {
|
|
41588
|
+
if (apos !== apos || bpos !== bpos) return Infinity; // NaN cap
|
|
41482
41589
|
var posLo = apos < bpos ? apos : bpos;
|
|
41483
41590
|
var posHi = apos > bpos ? apos : bpos;
|
|
41484
41591
|
for (var k = lo; k <= hi; k++) {
|
|
41485
41592
|
var p = srcPos[k];
|
|
41486
|
-
if (p !== p) return
|
|
41593
|
+
if (p !== p) return Infinity; // NaN
|
|
41487
41594
|
if (p < posLo) posLo = p;
|
|
41488
41595
|
if (p > posHi) posHi = p;
|
|
41489
41596
|
}
|
|
41490
|
-
return
|
|
41597
|
+
return turnPrefix[posHi] - turnPrefix[posLo];
|
|
41491
41598
|
}
|
|
41492
41599
|
|
|
41493
41600
|
// Fast strict-interior segment crossing; returns [x, y] or null. Buffer join
|
|
@@ -42333,6 +42440,18 @@ ${svg}
|
|
|
42333
42440
|
|
|
42334
42441
|
// each path may be converted into multiple buffer rings, which later
|
|
42335
42442
|
// need to be dissolved
|
|
42443
|
+
// Re-anchor a closed ring [v0, v1, ..., v0] to the midpoint of its first edge:
|
|
42444
|
+
// returns [m, v1, ..., v0, m] where m = midpoint(v0, v1). The offset then
|
|
42445
|
+
// starts/ends mid-edge (a collinear seam) and v0 becomes an interior join.
|
|
42446
|
+
function startRingAtEdgeMidpoint(verts) {
|
|
42447
|
+
var a = verts[0], b = verts[1];
|
|
42448
|
+
var m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
42449
|
+
var out = [m];
|
|
42450
|
+
for (var i = 1; i < verts.length; i++) out.push(verts[i]);
|
|
42451
|
+
out.push(m.concat());
|
|
42452
|
+
return out;
|
|
42453
|
+
}
|
|
42454
|
+
|
|
42336
42455
|
function makeSinglePathRings(pathArcs, dist) {
|
|
42337
42456
|
var rings = [];
|
|
42338
42457
|
var pathSideVerts = collectPathVertices(pathArcs);
|
|
@@ -42340,18 +42459,23 @@ ${svg}
|
|
|
42340
42459
|
if (simplifyIntervalFn) {
|
|
42341
42460
|
verts = presimplifyPathVerts(verts, simplifyIntervalFn(dist), dist);
|
|
42342
42461
|
}
|
|
42343
|
-
if (!oneSidedBuffer && !opts.
|
|
42462
|
+
if (!oneSidedBuffer && !opts.band_method && pathIsOpen(verts)) {
|
|
42344
42463
|
// Fast path for ordinary two-sided line buffers: emit one closed
|
|
42345
42464
|
// outline instead of many per-segment bands that must be dissolved.
|
|
42346
|
-
// The
|
|
42465
|
+
// The band-method escape hatch skips it to fall through to the
|
|
42347
42466
|
// per-segment band construction (makeLeftBufferRings, no winding fill).
|
|
42348
42467
|
var built = makeTwoSidedOutlineRing(verts, dist);
|
|
42349
42468
|
if (opts.no_loop_removal) return [built.ring];
|
|
42350
|
-
//
|
|
42351
|
-
//
|
|
42352
|
-
//
|
|
42353
|
-
|
|
42354
|
-
|
|
42469
|
+
// Strip self-overlap loops (the dissolve would fill them anyway) so it has
|
|
42470
|
+
// fewer segments and self-intersections to resolve, while keeping real
|
|
42471
|
+
// holes. The two-sided outline of an open path has a consistent +/-1 base
|
|
42472
|
+
// winding, so the crossing-direction method classifies loops exactly from
|
|
42473
|
+
// the ring geometry alone; the source-turn gate is kept as an alternative.
|
|
42474
|
+
if (opts.loop_removal_turn_gate) {
|
|
42475
|
+
return [removeBufferRingLoops(built.ring, BUFFER_LOOP_WINDOW,
|
|
42476
|
+
built.srcPos, getSourceTurnPrefix(verts))];
|
|
42477
|
+
}
|
|
42478
|
+
return [removeBufferRingLoopsByDirection(built.ring, BUFFER_LOOP_WINDOW)];
|
|
42355
42479
|
}
|
|
42356
42480
|
if (!opts.right || opts.left) {
|
|
42357
42481
|
rings = rings.concat(buildOneSidedRings(verts));
|
|
@@ -42369,19 +42493,43 @@ ${svg}
|
|
|
42369
42493
|
// (no winding_fill) and the source-path edge get no provenance and pass
|
|
42370
42494
|
// through unchanged.
|
|
42371
42495
|
//
|
|
42372
|
-
//
|
|
42373
|
-
//
|
|
42374
|
-
//
|
|
42375
|
-
//
|
|
42376
|
-
//
|
|
42377
|
-
//
|
|
42378
|
-
//
|
|
42496
|
+
// The source-turn gate is applied to both closed source rings AND open
|
|
42497
|
+
// one-sided arcs (e.g. a topological polygon's unbuffered-boundary remnant,
|
|
42498
|
+
// buffered with caps). An open arc can have a concave-join dent that is its
|
|
42499
|
+
// region's only coverage, which a purely geometric remover would cut away --
|
|
42500
|
+
// but the source-turn gate keeps such coverage via per-ring source-position
|
|
42501
|
+
// provenance, so it is safe here and lets the topological pipeline's
|
|
42502
|
+
// per-feature dissolve start from far cleaner rings (its dominant cost).
|
|
42503
|
+
// Callers whose winding construction is not safe to collapse this way (the
|
|
42504
|
+
// one-sided line buffer) opt out by passing no_loop_removal.
|
|
42379
42505
|
function buildOneSidedRings(sideVerts) {
|
|
42506
|
+
// Outline mode offsets a closed source ring to a single self-contained
|
|
42507
|
+
// loop with no source-path edge to close it, so the loop must close on
|
|
42508
|
+
// itself at the start vertex. Starting at a corner leaves that seam
|
|
42509
|
+
// unjoined (the raw first/last offset endpoints sit off the boundary,
|
|
42510
|
+
// breaking inward elbow closures and the loop remover's first-vertex
|
|
42511
|
+
// assumption). Restart at the midpoint of the first edge: the seam then
|
|
42512
|
+
// falls mid-edge (collinear -- no join needed) and the original start
|
|
42513
|
+
// corner becomes an ordinary interior join.
|
|
42514
|
+
if (opts.outline && sideVerts.length > 2 && !pathIsOpen(sideVerts)) {
|
|
42515
|
+
sideVerts = startRingAtEdgeMidpoint(sideVerts);
|
|
42516
|
+
}
|
|
42380
42517
|
var built = makeLeftBufferRings(sideVerts, dist,
|
|
42381
42518
|
oneSidedBuffer ? pathSideVerts : null);
|
|
42382
|
-
if (opts.no_loop_removal || !opts.winding_fill
|
|
42519
|
+
if (opts.no_loop_removal || !opts.winding_fill) {
|
|
42383
42520
|
return built.rings;
|
|
42384
42521
|
}
|
|
42522
|
+
if (opts.outline) {
|
|
42523
|
+
// Outline mode rings are clean offset-only loops (no source-path edge),
|
|
42524
|
+
// so each has a consistent +/-1 base winding and the crossing-direction
|
|
42525
|
+
// remover classifies overshoot loops exactly from the ring geometry --
|
|
42526
|
+
// the same condition that makes it safe for the open-path two-sided
|
|
42527
|
+
// outline. (The band-ribbon rings of the default construction do not,
|
|
42528
|
+
// which is why they use the source-turn gate below.)
|
|
42529
|
+
return built.rings.map(function(ring) {
|
|
42530
|
+
return removeBufferRingLoopsByDirection(ring, BUFFER_LOOP_WINDOW);
|
|
42531
|
+
});
|
|
42532
|
+
}
|
|
42385
42533
|
var turnPrefix = getSourceTurnPrefix(sideVerts);
|
|
42386
42534
|
return built.rings.map(function(ring, i) {
|
|
42387
42535
|
var srcPos = built.srcPositions[i];
|
|
@@ -42395,8 +42543,7 @@ ${svg}
|
|
|
42395
42543
|
var rings = [];
|
|
42396
42544
|
// Parallel to rings[]: each entry is the source-position array for the
|
|
42397
42545
|
// corresponding ring (from builder.done()), or null for rings with no
|
|
42398
|
-
// single-path provenance (join sectors, band patches).
|
|
42399
|
-
// winding-fill caller to gate loop removal.
|
|
42546
|
+
// single-path provenance (join sectors, band patches).
|
|
42400
42547
|
var ringsSrcPos = [];
|
|
42401
42548
|
var openPath = pathIsOpen(verts);
|
|
42402
42549
|
var x0, y0, x1, y1, x2, y2; // path traversal coords
|
|
@@ -42409,7 +42556,8 @@ ${svg}
|
|
|
42409
42556
|
var segId;
|
|
42410
42557
|
|
|
42411
42558
|
function flushRing() {
|
|
42412
|
-
var d = builder.done();
|
|
42559
|
+
var d = builder.done(!!opts.outline);
|
|
42560
|
+
if (!d) return; // outline loop collapsed (e.g. hole smaller than radius)
|
|
42413
42561
|
rings.push(d.ring);
|
|
42414
42562
|
ringsSrcPos.push(d.srcPos);
|
|
42415
42563
|
}
|
|
@@ -42421,7 +42569,9 @@ ${svg}
|
|
|
42421
42569
|
if (verts.length > 0) {
|
|
42422
42570
|
x0 = x2 = verts[0][0];
|
|
42423
42571
|
y0 = y2 = verts[0][1];
|
|
42424
|
-
|
|
42572
|
+
// Outline mode emits the offset polyline only (no source-path band edge),
|
|
42573
|
+
// so the ring is a single self-contained offset loop (see buildOneSidedRings).
|
|
42574
|
+
if (!opts.outline) addPathStart(verts[0]);
|
|
42425
42575
|
}
|
|
42426
42576
|
|
|
42427
42577
|
for (segId = 0; segId < verts.length - 1; segId++) {
|
|
@@ -42444,9 +42594,8 @@ ${svg}
|
|
|
42444
42594
|
|
|
42445
42595
|
// various connections between current offset segment and prev segment.
|
|
42446
42596
|
// Offset ("buffer") vertices are tagged with the source segment id
|
|
42447
|
-
// (segId)
|
|
42448
|
-
// the
|
|
42449
|
-
// with the current segId is harmless for the smooth turn gate.
|
|
42597
|
+
// (segId); the slight imprecision of tagging a previous-segment endpoint
|
|
42598
|
+
// (p2Prev) with the current segId is harmless for provenance tracking.
|
|
42450
42599
|
if (segId === 0) {
|
|
42451
42600
|
// first extruded segment - no previous segment to join to - add
|
|
42452
42601
|
// first endpoint to the buffer
|
|
@@ -42508,11 +42657,11 @@ ${svg}
|
|
|
42508
42657
|
// the two adjacent buffer sections; without it, the dissolved buffer
|
|
42509
42658
|
// is pinched at the bend vertex.
|
|
42510
42659
|
pushAuxRing(makeJoinSectorRing(x1, y1, bearing - 90, -joinAngle, dist, p1, p2Prev));
|
|
42511
|
-
addPathStart(verts[segId]);
|
|
42660
|
+
if (!opts.outline) addPathStart(verts[segId]);
|
|
42512
42661
|
builder.addBufferVertex(p1, segId);
|
|
42513
42662
|
}
|
|
42514
42663
|
|
|
42515
|
-
addPathSegment(verts[segId], verts[segId + 1], pathSideVerts);
|
|
42664
|
+
if (!opts.outline) addPathSegment(verts[segId], verts[segId + 1], pathSideVerts);
|
|
42516
42665
|
// in v4, offset direction (bearing) is the same for both segment
|
|
42517
42666
|
// endpoints, because we are projecting lat/lon coords to Mercator and
|
|
42518
42667
|
// using planar geometry for all datasets
|
|
@@ -42521,14 +42670,22 @@ ${svg}
|
|
|
42521
42670
|
p2Prev = p2;
|
|
42522
42671
|
}
|
|
42523
42672
|
|
|
42673
|
+
var closedPath = (x2 == x0 && y2 == y0);
|
|
42674
|
+
|
|
42524
42675
|
// TODO: add this to cap and join code below
|
|
42525
|
-
if (p2Prev) {
|
|
42676
|
+
if (p2Prev && !(opts.outline && closedPath)) {
|
|
42526
42677
|
// add final offset segment endpoint (the last path segment is never
|
|
42527
|
-
// suppressed, so the buffer section in progress ends normally)
|
|
42678
|
+
// suppressed, so the buffer section in progress ends normally).
|
|
42679
|
+
// Outline closed rings skip it: the midpoint-restart seam is collinear,
|
|
42680
|
+
// so p2Prev (the last segment's recomputed endpoint offset) is a ~1 ULP
|
|
42681
|
+
// duplicate of the first offset vertex p1First. The ring must close by
|
|
42682
|
+
// duplicating p1First exactly (done() below), not on a recalculated
|
|
42683
|
+
// final point -- the sub-ULP gap leaves a sliver the winding dissolve
|
|
42684
|
+
// cannot resolve, collapsing the whole buffer in some JS engines.
|
|
42528
42685
|
builder.addBufferVertex(p2Prev, segId - 1);
|
|
42529
42686
|
}
|
|
42530
42687
|
|
|
42531
|
-
if (
|
|
42688
|
+
if (opts.outline && closedPath) ; else if (closedPath) { // closed path
|
|
42532
42689
|
// add join to finish closed path
|
|
42533
42690
|
// TODO - figure out which bearing to use
|
|
42534
42691
|
joinAngle = getJoinAngle(bearing, firstBearing);
|
|
@@ -43389,7 +43546,7 @@ ${svg}
|
|
|
43389
43546
|
time('buffer');
|
|
43390
43547
|
var spherical = isLatLngCRS(getDatasetCRS(dataset));
|
|
43391
43548
|
var oneSided = !!opts.left !== !!opts.right;
|
|
43392
|
-
var debug = opts.debug_offset || opts.
|
|
43549
|
+
var debug = opts.debug_offset || opts.debug_mosaic;
|
|
43393
43550
|
// One-sided buffers use a winding-number fill, run per-feature so each source
|
|
43394
43551
|
// path's mosaic stays small. The construction is one-sided by design (offset
|
|
43395
43552
|
// curve + end caps + the source path as the inner edge), so the winding fill
|
|
@@ -43404,12 +43561,12 @@ ${svg}
|
|
|
43404
43561
|
// outline + boundary-flood dissolve + artifact-hole filter, which is faster
|
|
43405
43562
|
// and has no wrong side to clean.
|
|
43406
43563
|
//
|
|
43407
|
-
// The undocumented '
|
|
43564
|
+
// The undocumented 'band-method' option forces the older non-winding
|
|
43408
43565
|
// construction here too (the per-feature pipeline below handles one-sided
|
|
43409
43566
|
// buffers via its winding_fill:false coverage test), as a conservative
|
|
43410
43567
|
// fallback.
|
|
43411
43568
|
var useWinding = oneSided && !debug && opts.winding_fill !== false &&
|
|
43412
|
-
!opts.
|
|
43569
|
+
!opts.band_method;
|
|
43413
43570
|
if (useWinding) {
|
|
43414
43571
|
// no_loop_removal: the one-sided construction's overshoot loops can be a
|
|
43415
43572
|
// concave bend's only band coverage, so collapsing them would cut holes in
|
|
@@ -43424,19 +43581,19 @@ ${svg}
|
|
|
43424
43581
|
}
|
|
43425
43582
|
var dataset2;
|
|
43426
43583
|
if (debug) {
|
|
43427
|
-
// Debug visualizations (raw offset rings,
|
|
43428
|
-
//
|
|
43429
|
-
//
|
|
43430
|
-
//
|
|
43431
|
-
//
|
|
43432
|
-
//
|
|
43433
|
-
//
|
|
43584
|
+
// Debug visualizations (raw offset rings, mosaic) want the whole layer's
|
|
43585
|
+
// geometry/topology in one dataset; keep the original global dissolve for
|
|
43586
|
+
// them (no artifact-hole filter runs in debug mode anyway). Mirror the real
|
|
43587
|
+
// pipeline's construction so the debug view reflects what the buffer
|
|
43588
|
+
// actually builds: an all-closed-ring two-sided layer uses the winding-fill
|
|
43589
|
+
// + loop-removal construction (and a winding-number dissolve), so
|
|
43590
|
+
// debug-offset shows the loop-removed offset rings and the no-loop-removal
|
|
43434
43591
|
// flag has a visible effect. (See makePolylineBufferTwoSidedPerFeature.)
|
|
43435
|
-
var
|
|
43436
|
-
var debugMakerOpts =
|
|
43592
|
+
var useWindingConstruction = !oneSided && layerIsAllClosed(lyr, dataset.arcs);
|
|
43593
|
+
var debugMakerOpts = useWindingConstruction ?
|
|
43437
43594
|
Object.assign({}, opts, {winding_fill: true}) : opts;
|
|
43438
43595
|
var debugDissolveOpts = Object.assign({}, opts, {per_part_holes: true},
|
|
43439
|
-
|
|
43596
|
+
useWindingConstruction ? {winding_fill: true} : null);
|
|
43440
43597
|
dataset2 = importGeoJSON(makeShapeBufferGeoJSON(lyr, dataset, debugMakerOpts), {});
|
|
43441
43598
|
dissolveBufferDataset2(dataset2, debugDissolveOpts);
|
|
43442
43599
|
} else {
|
|
@@ -43843,7 +44000,7 @@ ${svg}
|
|
|
43843
44000
|
}
|
|
43844
44001
|
|
|
43845
44002
|
function useArtifactHoleFilter(opts) {
|
|
43846
|
-
return !opts.debug_offset && !opts.
|
|
44003
|
+
return !opts.debug_offset && !opts.debug_mosaic;
|
|
43847
44004
|
}
|
|
43848
44005
|
|
|
43849
44006
|
// Remove artifact rings left by dissolving the self-intersecting outline rings
|
|
@@ -44402,136 +44559,1198 @@ ${svg}
|
|
|
44402
44559
|
return lng;
|
|
44403
44560
|
}
|
|
44404
44561
|
|
|
44405
|
-
|
|
44406
|
-
|
|
44407
|
-
|
|
44408
|
-
|
|
44562
|
+
function MaxHeap() {
|
|
44563
|
+
return new Heap('max');
|
|
44564
|
+
}
|
|
44565
|
+
|
|
44566
|
+
// A heap data structure used for computing Visvalingam simplification data.
|
|
44567
|
+
// type: 'max' or 'min' (min is default)
|
|
44409
44568
|
//
|
|
44410
|
-
|
|
44411
|
-
|
|
44412
|
-
|
|
44413
|
-
|
|
44414
|
-
|
|
44415
|
-
|
|
44569
|
+
function Heap(type) {
|
|
44570
|
+
var heapBuf = utils.expandoBuffer(Int32Array),
|
|
44571
|
+
indexBuf = utils.expandoBuffer(Int32Array),
|
|
44572
|
+
heavierThan = type == 'max' ? lessThan : greaterThan,
|
|
44573
|
+
itemsInHeap = 0,
|
|
44574
|
+
dataArr,
|
|
44575
|
+
heapArr,
|
|
44576
|
+
indexArr;
|
|
44416
44577
|
|
|
44417
|
-
|
|
44418
|
-
|
|
44419
|
-
|
|
44420
|
-
|
|
44421
|
-
|
|
44422
|
-
|
|
44423
|
-
|
|
44424
|
-
|
|
44425
|
-
|
|
44578
|
+
this.init = function(values) {
|
|
44579
|
+
var i;
|
|
44580
|
+
dataArr = values;
|
|
44581
|
+
itemsInHeap = values.length;
|
|
44582
|
+
heapArr = heapBuf(itemsInHeap);
|
|
44583
|
+
indexArr = indexBuf(itemsInHeap);
|
|
44584
|
+
for (i=0; i<itemsInHeap; i++) {
|
|
44585
|
+
insertValue(i, i);
|
|
44586
|
+
}
|
|
44587
|
+
// place non-leaf items
|
|
44588
|
+
for (i=(itemsInHeap-2) >> 1; i >= 0; i--) {
|
|
44589
|
+
downHeap(i);
|
|
44426
44590
|
}
|
|
44427
44591
|
};
|
|
44428
44592
|
|
|
44429
|
-
|
|
44430
|
-
|
|
44431
|
-
|
|
44432
|
-
message(utils.format("Removed %'d sliver%s using %s", removed, utils.pluralSuffix(removed), filterData.label));
|
|
44433
|
-
|
|
44434
|
-
// Remove null shapes (likely removed by clipping/erasing, although possibly already present)
|
|
44435
|
-
if (opts.remove_empty) {
|
|
44436
|
-
cmd.filterFeatures(lyr, dataset.arcs, {remove_empty: true, verbose: false});
|
|
44437
|
-
}
|
|
44438
|
-
return removed;
|
|
44439
|
-
}
|
|
44593
|
+
this.size = function() {
|
|
44594
|
+
return itemsInHeap;
|
|
44595
|
+
};
|
|
44440
44596
|
|
|
44441
|
-
|
|
44442
|
-
|
|
44443
|
-
|
|
44444
|
-
|
|
44445
|
-
|
|
44446
|
-
|
|
44447
|
-
var pathFilter = function(path) {
|
|
44448
|
-
var prevArcs = 0,
|
|
44449
|
-
newArcs = 0;
|
|
44450
|
-
for (var i=0, n=path && path.length || 0; i<n; i++) {
|
|
44451
|
-
if (flags[absArcId(path[i])] > 0) {
|
|
44452
|
-
newArcs++;
|
|
44453
|
-
} else {
|
|
44454
|
-
prevArcs++;
|
|
44455
|
-
}
|
|
44456
|
-
}
|
|
44457
|
-
// filter paths that contain arcs from both original and clip/erase layers
|
|
44458
|
-
// and are small
|
|
44459
|
-
if (newArcs > 0 && prevArcs > 0 && ringTest(path)) {
|
|
44460
|
-
removed++;
|
|
44461
|
-
return null;
|
|
44597
|
+
// Update a single value and re-heap
|
|
44598
|
+
this.updateValue = function(valIdx, val) {
|
|
44599
|
+
var heapIdx = indexArr[valIdx];
|
|
44600
|
+
dataArr[valIdx] = val;
|
|
44601
|
+
if (!(heapIdx >= 0 && heapIdx < itemsInHeap)) {
|
|
44602
|
+
error("Out-of-range heap index.");
|
|
44462
44603
|
}
|
|
44604
|
+
downHeap(upHeap(heapIdx));
|
|
44463
44605
|
};
|
|
44464
44606
|
|
|
44465
|
-
|
|
44466
|
-
|
|
44467
|
-
|
|
44468
|
-
markLayerChanged(lyr, {operation: 'filter-clip-slivers', unit: 'shapes'});
|
|
44469
|
-
return removed;
|
|
44470
|
-
}
|
|
44607
|
+
this.popValue = function() {
|
|
44608
|
+
return dataArr[this.pop()];
|
|
44609
|
+
};
|
|
44471
44610
|
|
|
44472
|
-
|
|
44473
|
-
|
|
44474
|
-
|
|
44475
|
-
var index = new PathIndex(clipShapes, nodes.arcs);
|
|
44611
|
+
this.getValue = function(idx) {
|
|
44612
|
+
return dataArr[idx];
|
|
44613
|
+
};
|
|
44476
44614
|
|
|
44477
|
-
|
|
44478
|
-
return
|
|
44479
|
-
}
|
|
44615
|
+
this.peek = function() {
|
|
44616
|
+
return heapArr[0];
|
|
44617
|
+
};
|
|
44480
44618
|
|
|
44481
|
-
function
|
|
44482
|
-
|
|
44483
|
-
|
|
44484
|
-
return clipped && clipped.length > 0 ? clipped : null;
|
|
44485
|
-
}
|
|
44619
|
+
this.peekValue = function() {
|
|
44620
|
+
return dataArr[heapArr[0]];
|
|
44621
|
+
};
|
|
44486
44622
|
|
|
44487
|
-
|
|
44488
|
-
|
|
44489
|
-
|
|
44490
|
-
|
|
44491
|
-
|
|
44492
|
-
|
|
44493
|
-
|
|
44494
|
-
|
|
44495
|
-
|
|
44496
|
-
|
|
44497
|
-
|
|
44498
|
-
|
|
44499
|
-
|
|
44623
|
+
// Return the idx of the lowest-value item in the heap
|
|
44624
|
+
this.pop = function() {
|
|
44625
|
+
var popIdx;
|
|
44626
|
+
if (itemsInHeap <= 0) {
|
|
44627
|
+
error("Tried to pop from an empty heap.");
|
|
44628
|
+
}
|
|
44629
|
+
popIdx = heapArr[0];
|
|
44630
|
+
insertValue(0, heapArr[--itemsInHeap]); // move last item in heap into root position
|
|
44631
|
+
downHeap(0);
|
|
44632
|
+
return popIdx;
|
|
44633
|
+
};
|
|
44634
|
+
|
|
44635
|
+
function upHeap(idx) {
|
|
44636
|
+
var parentIdx;
|
|
44637
|
+
// Move item up in the heap until it's at the top or is not lighter than its parent
|
|
44638
|
+
while (idx > 0) {
|
|
44639
|
+
parentIdx = (idx - 1) >> 1;
|
|
44640
|
+
if (heavierThan(idx, parentIdx)) {
|
|
44641
|
+
break;
|
|
44500
44642
|
}
|
|
44643
|
+
swapItems(idx, parentIdx);
|
|
44644
|
+
idx = parentIdx;
|
|
44501
44645
|
}
|
|
44502
|
-
return
|
|
44646
|
+
return idx;
|
|
44503
44647
|
}
|
|
44504
|
-
}
|
|
44505
44648
|
|
|
44506
|
-
|
|
44507
|
-
|
|
44508
|
-
|
|
44509
|
-
});
|
|
44649
|
+
// Swap item at @idx with any lighter children
|
|
44650
|
+
function downHeap(idx) {
|
|
44651
|
+
var minIdx = compareDown(idx);
|
|
44510
44652
|
|
|
44511
|
-
|
|
44512
|
-
|
|
44513
|
-
|
|
44514
|
-
|
|
44515
|
-
|
|
44653
|
+
while (minIdx > idx) {
|
|
44654
|
+
swapItems(idx, minIdx);
|
|
44655
|
+
idx = minIdx; // descend in the heap
|
|
44656
|
+
minIdx = compareDown(idx);
|
|
44657
|
+
}
|
|
44516
44658
|
}
|
|
44517
|
-
return cw;
|
|
44518
|
-
}
|
|
44519
44659
|
|
|
44520
|
-
|
|
44521
|
-
|
|
44522
|
-
|
|
44523
|
-
|
|
44524
|
-
|
|
44660
|
+
function swapItems(a, b) {
|
|
44661
|
+
var i = heapArr[a];
|
|
44662
|
+
insertValue(a, heapArr[b]);
|
|
44663
|
+
insertValue(b, i);
|
|
44664
|
+
}
|
|
44525
44665
|
|
|
44526
|
-
|
|
44527
|
-
|
|
44528
|
-
|
|
44529
|
-
|
|
44666
|
+
// Associate a heap idx with the index of a value in data arr
|
|
44667
|
+
function insertValue(heapIdx, valId) {
|
|
44668
|
+
indexArr[valId] = heapIdx;
|
|
44669
|
+
heapArr[heapIdx] = valId;
|
|
44670
|
+
}
|
|
44530
44671
|
|
|
44531
|
-
|
|
44532
|
-
|
|
44533
|
-
|
|
44534
|
-
|
|
44672
|
+
// comparator for Visvalingam min heap
|
|
44673
|
+
// @a, @b: Indexes in @heapArr
|
|
44674
|
+
function greaterThan(a, b) {
|
|
44675
|
+
var idx1 = heapArr[a],
|
|
44676
|
+
idx2 = heapArr[b],
|
|
44677
|
+
val1 = dataArr[idx1],
|
|
44678
|
+
val2 = dataArr[idx2];
|
|
44679
|
+
// If values are equal, compare array indexes.
|
|
44680
|
+
// This is not a requirement of the Visvalingam algorithm,
|
|
44681
|
+
// but it generates output that matches Mahes Visvalingam's
|
|
44682
|
+
// reference implementation.
|
|
44683
|
+
// See https://hydra.hull.ac.uk/assets/hull:10874/content
|
|
44684
|
+
return (val1 > val2 || val1 === val2 && idx1 > idx2);
|
|
44685
|
+
}
|
|
44686
|
+
|
|
44687
|
+
// comparator for max heap
|
|
44688
|
+
function lessThan(a, b) {
|
|
44689
|
+
var idx1 = heapArr[a],
|
|
44690
|
+
idx2 = heapArr[b];
|
|
44691
|
+
return dataArr[idx1] < dataArr[idx2];
|
|
44692
|
+
}
|
|
44693
|
+
|
|
44694
|
+
function compareDown(idx) {
|
|
44695
|
+
var a = 2 * idx + 1,
|
|
44696
|
+
b = a + 1,
|
|
44697
|
+
n = itemsInHeap;
|
|
44698
|
+
if (a < n && heavierThan(idx, a)) {
|
|
44699
|
+
idx = a;
|
|
44700
|
+
}
|
|
44701
|
+
if (b < n && heavierThan(idx, b)) {
|
|
44702
|
+
idx = b;
|
|
44703
|
+
}
|
|
44704
|
+
return idx;
|
|
44705
|
+
}
|
|
44706
|
+
}
|
|
44707
|
+
|
|
44708
|
+
var Visvalingam = {};
|
|
44709
|
+
|
|
44710
|
+
Visvalingam.getArcCalculator = function(metric, is3D) {
|
|
44711
|
+
var heap = new Heap(),
|
|
44712
|
+
prevBuf = utils.expandoBuffer(Int32Array),
|
|
44713
|
+
nextBuf = utils.expandoBuffer(Int32Array),
|
|
44714
|
+
calc = is3D ?
|
|
44715
|
+
function(b, c, d, xx, yy, zz) {
|
|
44716
|
+
return metric(xx[b], yy[b], zz[b], xx[c], yy[c], zz[c], xx[d], yy[d], zz[d]);
|
|
44717
|
+
} :
|
|
44718
|
+
function(b, c, d, xx, yy) {
|
|
44719
|
+
return metric(xx[b], yy[b], xx[c], yy[c], xx[d], yy[d]);
|
|
44720
|
+
};
|
|
44721
|
+
|
|
44722
|
+
// Calculate Visvalingam simplification data for an arc
|
|
44723
|
+
// @kk (Float64Array|Array) Receives calculated simplification thresholds
|
|
44724
|
+
// @xx, @yy, (@zz) Buffers containing vertex coordinates
|
|
44725
|
+
return function calcVisvalingam(kk, xx, yy, zz) {
|
|
44726
|
+
var arcLen = kk.length,
|
|
44727
|
+
prevArr = prevBuf(arcLen),
|
|
44728
|
+
nextArr = nextBuf(arcLen),
|
|
44729
|
+
val, maxVal = -Infinity,
|
|
44730
|
+
b, c, d; // indexes of points along arc
|
|
44731
|
+
|
|
44732
|
+
if (zz && !is3D) {
|
|
44733
|
+
error("[visvalingam] Received z-axis data for 2D simplification");
|
|
44734
|
+
} else if (!zz && is3D) {
|
|
44735
|
+
error("[visvalingam] Missing z-axis data for 3D simplification");
|
|
44736
|
+
} else if (kk.length > xx.length) {
|
|
44737
|
+
error("[visvalingam] Incompatible data arrays:", kk.length, xx.length);
|
|
44738
|
+
}
|
|
44739
|
+
|
|
44740
|
+
// Initialize Visvalingam "effective area" values and references to
|
|
44741
|
+
// prev/next points for each point in arc.
|
|
44742
|
+
for (c=0; c<arcLen; c++) {
|
|
44743
|
+
b = c-1;
|
|
44744
|
+
d = c+1;
|
|
44745
|
+
if (b < 0 || d >= arcLen) {
|
|
44746
|
+
val = Infinity; // endpoint maxVals
|
|
44747
|
+
} else {
|
|
44748
|
+
val = calc(b, c, d, xx, yy, zz);
|
|
44749
|
+
}
|
|
44750
|
+
kk[c] = val;
|
|
44751
|
+
nextArr[c] = d;
|
|
44752
|
+
prevArr[c] = b;
|
|
44753
|
+
}
|
|
44754
|
+
heap.init(kk);
|
|
44755
|
+
|
|
44756
|
+
// Calculate removal thresholds for each internal point in the arc
|
|
44757
|
+
//
|
|
44758
|
+
while (heap.size() > 0) {
|
|
44759
|
+
c = heap.pop(); // Remove the point with the least effective area.
|
|
44760
|
+
val = kk[c];
|
|
44761
|
+
if (val === Infinity) {
|
|
44762
|
+
break;
|
|
44763
|
+
}
|
|
44764
|
+
if (val < maxVal) {
|
|
44765
|
+
// don't assign current point a lesser value than the last removed vertex
|
|
44766
|
+
kk[c] = maxVal;
|
|
44767
|
+
} else {
|
|
44768
|
+
maxVal = val;
|
|
44769
|
+
}
|
|
44770
|
+
|
|
44771
|
+
// Recompute effective area of neighbors of the removed point.
|
|
44772
|
+
b = prevArr[c];
|
|
44773
|
+
d = nextArr[c];
|
|
44774
|
+
if (b > 0) {
|
|
44775
|
+
val = calc(prevArr[b], b, d, xx, yy, zz);
|
|
44776
|
+
heap.updateValue(b, val);
|
|
44777
|
+
}
|
|
44778
|
+
if (d < arcLen-1) {
|
|
44779
|
+
val = calc(b, d, nextArr[d], xx, yy, zz);
|
|
44780
|
+
heap.updateValue(d, val);
|
|
44781
|
+
}
|
|
44782
|
+
nextArr[b] = d;
|
|
44783
|
+
prevArr[d] = b;
|
|
44784
|
+
}
|
|
44785
|
+
};
|
|
44786
|
+
};
|
|
44787
|
+
|
|
44788
|
+
Visvalingam.standardMetric = geom.triangleArea;
|
|
44789
|
+
Visvalingam.standardMetric3D = geom.triangleArea3D;
|
|
44790
|
+
|
|
44791
|
+
Visvalingam.getWeightedMetric = function(opts) {
|
|
44792
|
+
var weight = Visvalingam.getWeightFunction(opts);
|
|
44793
|
+
return function(ax, ay, bx, by, cx, cy) {
|
|
44794
|
+
var area = geom.triangleArea(ax, ay, bx, by, cx, cy),
|
|
44795
|
+
cos = geom.cosine(ax, ay, bx, by, cx, cy);
|
|
44796
|
+
return weight(cos) * area;
|
|
44797
|
+
};
|
|
44798
|
+
};
|
|
44799
|
+
|
|
44800
|
+
Visvalingam.getWeightedMetric3D = function(opts) {
|
|
44801
|
+
var weight = Visvalingam.getWeightFunction(opts);
|
|
44802
|
+
return function(ax, ay, az, bx, by, bz, cx, cy, cz) {
|
|
44803
|
+
var area = geom.triangleArea3D(ax, ay, az, bx, by, bz, cx, cy, cz),
|
|
44804
|
+
cos = geom.cosine3D(ax, ay, az, bx, by, bz, cx, cy, cz);
|
|
44805
|
+
return weight(cos) * area;
|
|
44806
|
+
};
|
|
44807
|
+
};
|
|
44808
|
+
|
|
44809
|
+
Visvalingam.getWeightCoefficient = function(opts) {
|
|
44810
|
+
return opts && utils.isNumber(opts && opts.weighting) ? opts.weighting : 0.7;
|
|
44811
|
+
};
|
|
44812
|
+
|
|
44813
|
+
// Get a parameterized version of Visvalingam.weight()
|
|
44814
|
+
Visvalingam.getWeightFunction = function(opts) {
|
|
44815
|
+
var k = Visvalingam.getWeightCoefficient(opts);
|
|
44816
|
+
return function(cos) {
|
|
44817
|
+
return -cos * k + 1;
|
|
44818
|
+
};
|
|
44819
|
+
};
|
|
44820
|
+
|
|
44821
|
+
// Weight triangle area by inverse cosine
|
|
44822
|
+
// Standard weighting favors 90-deg angles; this curve peaks at 120 deg.
|
|
44823
|
+
Visvalingam.weight = function(cos) {
|
|
44824
|
+
var k = 0.7;
|
|
44825
|
+
return -cos * k + 1;
|
|
44826
|
+
};
|
|
44827
|
+
|
|
44828
|
+
Visvalingam.getEffectiveAreaSimplifier = function(use3D) {
|
|
44829
|
+
var metric = use3D ? Visvalingam.standardMetric3D : Visvalingam.standardMetric;
|
|
44830
|
+
return Visvalingam.getPathSimplifier(metric, use3D);
|
|
44831
|
+
};
|
|
44832
|
+
|
|
44833
|
+
Visvalingam.getWeightedSimplifier = function(opts, use3D) {
|
|
44834
|
+
var metric = use3D ? Visvalingam.getWeightedMetric3D(opts) : Visvalingam.getWeightedMetric(opts);
|
|
44835
|
+
return Visvalingam.getPathSimplifier(metric, use3D);
|
|
44836
|
+
};
|
|
44837
|
+
|
|
44838
|
+
Visvalingam.getPathSimplifier = function(metric, use3D) {
|
|
44839
|
+
return Visvalingam.scaledSimplify(Visvalingam.getArcCalculator(metric, use3D));
|
|
44840
|
+
};
|
|
44841
|
+
|
|
44842
|
+
|
|
44843
|
+
Visvalingam.scaledSimplify = function(f) {
|
|
44844
|
+
return function(kk, xx, yy, zz) {
|
|
44845
|
+
f(kk, xx, yy, zz);
|
|
44846
|
+
for (var i=1, n=kk.length - 1; i<n; i++) {
|
|
44847
|
+
// convert area metric to a linear equivalent
|
|
44848
|
+
kk[i] = Math.sqrt(kk[i]) * 0.65;
|
|
44849
|
+
}
|
|
44850
|
+
};
|
|
44851
|
+
};
|
|
44852
|
+
|
|
44853
|
+
// Build approximate inter-feature Voronoi (medial-axis) cut lines for the
|
|
44854
|
+
// topological polygon buffer. Where two features' buffers overlap, the
|
|
44855
|
+
// contested space should be partitioned by proximity to the source polygons;
|
|
44856
|
+
// the locus of points equidistant from two sources is a generalized Voronoi
|
|
44857
|
+
// boundary. We approximate it by sampling points along the source rings (one
|
|
44858
|
+
// label per feature) and emitting the dual Voronoi edges that separate sites of
|
|
44859
|
+
// different features.
|
|
44860
|
+
//
|
|
44861
|
+
// The returned lines are injected into the buffer mosaic as cut-lines: they
|
|
44862
|
+
// subdivide each contested tile along the equidistant boundary, and any portion
|
|
44863
|
+
// lying outside the buffers is pruned by the mosaic builder (detachAcyclicArcs).
|
|
44864
|
+
// Only the boundary between two features' final regions survives the per-feature
|
|
44865
|
+
// tile dissolve.
|
|
44866
|
+
//
|
|
44867
|
+
// @coordDistances: per-feature buffer distance in source-coordinate units (the
|
|
44868
|
+
// caller converts from meters via getCoordinateDistance), used both as the
|
|
44869
|
+
// densification scale and as the proximity prune (two sites can only be jointly
|
|
44870
|
+
// contested if they are within the sum of their features' radii).
|
|
44871
|
+
|
|
44872
|
+
// Baseline cap used to derive the spacing floor totalLen/MAX_SITES. On small
|
|
44873
|
+
// inputs this floor sits well below the buffer distance and adaptive sampling
|
|
44874
|
+
// works; it is the floor that keeps simple mosaics stable.
|
|
44875
|
+
var MAX_SITES = 60000;
|
|
44876
|
+
|
|
44877
|
+
// The spacing floor is also capped at FLOOR_DISTANCE_FRACTION of the buffer
|
|
44878
|
+
// distance. On a large mosaic totalLen/MAX_SITES grows until it approaches (or
|
|
44879
|
+
// exceeds) the buffer distance, which flattens the floor onto maxSpacing and
|
|
44880
|
+
// disables adaptive densification -- so the narrowest channels (a river between
|
|
44881
|
+
// two states) zig-zag. Capping the floor at a fraction of the buffer distance
|
|
44882
|
+
// guarantees adaptive headroom regardless of input size. The value matches the
|
|
44883
|
+
// floor/maxSpacing ratio at which small mosaics already sample cleanly.
|
|
44884
|
+
var FLOOR_DISTANCE_FRACTION = 0.1;
|
|
44885
|
+
|
|
44886
|
+
// Soft target total site count. coarsen scales the gap-proportional spacing up
|
|
44887
|
+
// until the predicted total falls under this, keeping the Delaunay bounded on
|
|
44888
|
+
// dense shared-border mosaics while leaving sparse inputs at coarsen=1 (fully
|
|
44889
|
+
// adaptive). Set above the site counts of typical large inputs so those stay
|
|
44890
|
+
// fully resolved.
|
|
44891
|
+
var SITE_BUDGET = 800000;
|
|
44892
|
+
|
|
44893
|
+
function buildInterFeatureMedialLines(shapes, coordDistances, arcs, opts) {
|
|
44894
|
+
opts = opts || {};
|
|
44895
|
+
profileStart('medial:collectSites');
|
|
44896
|
+
var sites = collectSites(shapes, coordDistances, arcs);
|
|
44897
|
+
profileEnd('medial:collectSites');
|
|
44898
|
+
if (!sites || sites.coords.length < 3) return null;
|
|
44899
|
+
if (profileEnabled()) {
|
|
44900
|
+
message('[medial] sample sites: ' + sites.coords.length);
|
|
44901
|
+
}
|
|
44902
|
+
profileStart('medial:computeSegments');
|
|
44903
|
+
var medial = computeMedialSegments(sites, coordDistances, sites.grid);
|
|
44904
|
+
profileEnd('medial:computeSegments');
|
|
44905
|
+
if (medial.segments.length === 0) return null;
|
|
44906
|
+
// Stitch the individual Voronoi edges (2-point segments that meet at shared
|
|
44907
|
+
// circumcenters) into maximal polylines so the medial axis can be simplified
|
|
44908
|
+
// and injected as connected paths rather than a swarm of tiny stubs.
|
|
44909
|
+
profileStart('medial:assembleChains');
|
|
44910
|
+
var chains = assembleChains(medial.segments, medial.coords);
|
|
44911
|
+
profileEnd('medial:assembleChains');
|
|
44912
|
+
if (opts.simplifyInterval > 0) {
|
|
44913
|
+
profileStart('medial:simplify');
|
|
44914
|
+
chains = chains.map(function(chain) {
|
|
44915
|
+
return simplifyChain(chain, opts.simplifyInterval);
|
|
44916
|
+
});
|
|
44917
|
+
profileEnd('medial:simplify');
|
|
44918
|
+
}
|
|
44919
|
+
return {
|
|
44920
|
+
type: 'MultiLineString',
|
|
44921
|
+
coordinates: chains
|
|
44922
|
+
};
|
|
44923
|
+
}
|
|
44924
|
+
|
|
44925
|
+
// Build the medial-construction triangles for the -buffer debug-delaunay option
|
|
44926
|
+
// as a GeometryCollection of triangle polygons. collectSites returns only the
|
|
44927
|
+
// contested sites, so the Delaunay is already the per-region mesh from which the
|
|
44928
|
+
// medial axis is derived; this keeps the triangles whose circumcenter is an
|
|
44929
|
+
// actual medial vertex. A triangle qualifies when it has a cross-feature edge
|
|
44930
|
+
// within buffer reach AND its circumcenter lies inside the overlap
|
|
44931
|
+
// (circumradius <= reach). The second test drops the long, thin triangles that
|
|
44932
|
+
// span a ribbon's concave bends: their circumcenters are wild and the medial
|
|
44933
|
+
// computation discards their segments, so showing them would just add spurious
|
|
44934
|
+
// spans. Returns null when nothing bridges two features.
|
|
44935
|
+
function buildInterFeatureDelaunay(shapes, coordDistances, arcs) {
|
|
44936
|
+
var sites = collectSites(shapes, coordDistances, arcs);
|
|
44937
|
+
if (!sites || sites.coords.length < 3) return null;
|
|
44938
|
+
var coords = sites.coords;
|
|
44939
|
+
var owner = sites.owner;
|
|
44940
|
+
var triangles = Delaunator.from(coords).triangles;
|
|
44941
|
+
var geometries = [];
|
|
44942
|
+
for (var i = 0; i < triangles.length; i += 3) {
|
|
44943
|
+
var ia = triangles[i], ib = triangles[i + 1], ic = triangles[i + 2];
|
|
44944
|
+
var reach = Math.max(
|
|
44945
|
+
bridgingReach(ia, ib, coords, owner, coordDistances),
|
|
44946
|
+
bridgingReach(ib, ic, coords, owner, coordDistances),
|
|
44947
|
+
bridgingReach(ic, ia, coords, owner, coordDistances));
|
|
44948
|
+
if (reach <= 0) continue; // no contested edge
|
|
44949
|
+
var a = coords[ia], b = coords[ib], c = coords[ic];
|
|
44950
|
+
var cc = circumcenter(a, b, c);
|
|
44951
|
+
if (!cc) continue; // degenerate (near-collinear)
|
|
44952
|
+
var rx = cc[0] - a[0], ry = cc[1] - a[1];
|
|
44953
|
+
if (rx * rx + ry * ry > reach * reach) continue; // wild circumcenter
|
|
44954
|
+
geometries.push({
|
|
44955
|
+
type: 'Polygon',
|
|
44956
|
+
coordinates: [[
|
|
44957
|
+
[a[0], a[1]], [b[0], b[1]], [c[0], c[1]], [a[0], a[1]]
|
|
44958
|
+
]]
|
|
44959
|
+
});
|
|
44960
|
+
}
|
|
44961
|
+
if (geometries.length === 0) return null;
|
|
44962
|
+
return {type: 'GeometryCollection', geometries: geometries};
|
|
44963
|
+
}
|
|
44964
|
+
|
|
44965
|
+
// Buffer reach (sum of the two source radii) of edge (i, j) if its endpoints are
|
|
44966
|
+
// different features and close enough for their buffers to overlap -- the same
|
|
44967
|
+
// test computeMedialSegments uses to decide whether an edge's bisector is a
|
|
44968
|
+
// contested medial edge. Returns 0 when the edge does not bridge features.
|
|
44969
|
+
function bridgingReach(i, j, coords, owner, coordDistances) {
|
|
44970
|
+
if (owner[i] === owner[j]) return 0;
|
|
44971
|
+
var dx = coords[i][0] - coords[j][0];
|
|
44972
|
+
var dy = coords[i][1] - coords[j][1];
|
|
44973
|
+
var reach = coordDistances[owner[i]] + coordDistances[owner[j]];
|
|
44974
|
+
return Math.sqrt(dx * dx + dy * dy) <= reach ? reach : 0;
|
|
44975
|
+
}
|
|
44976
|
+
|
|
44977
|
+
// Stitch 2-point medial segments into maximal polylines. The medial network is
|
|
44978
|
+
// a graph whose vertices are the Delaunay triangles' circumcenters: every
|
|
44979
|
+
// segment endpoint is a vertex id indexing @coords, so adjacent edges that meet
|
|
44980
|
+
// at a shared circumcenter share an id directly -- no coordinate hashing needed.
|
|
44981
|
+
// Degree-2 vertices lie mid-path; degree-1 (hull-ray ends) and degree-3+ (where
|
|
44982
|
+
// 3+ features meet) vertices are junctions. Each returned chain runs between two
|
|
44983
|
+
// junctions (or around an isolated loop).
|
|
44984
|
+
function assembleChains(segments, coords) {
|
|
44985
|
+
var nodes = new Array(coords.length);
|
|
44986
|
+
function getNode(id) {
|
|
44987
|
+
var n = nodes[id];
|
|
44988
|
+
if (!n) { n = nodes[id] = {coord: coords[id], edges: []}; }
|
|
44989
|
+
return n;
|
|
44990
|
+
}
|
|
44991
|
+
var edges = segments.map(function(seg) {
|
|
44992
|
+
var a = getNode(seg[0]);
|
|
44993
|
+
var b = getNode(seg[1]);
|
|
44994
|
+
var edge = {a: a, b: b, used: false};
|
|
44995
|
+
a.edges.push(edge);
|
|
44996
|
+
b.edges.push(edge);
|
|
44997
|
+
return edge;
|
|
44998
|
+
});
|
|
44999
|
+
function other(edge, node) {
|
|
45000
|
+
return edge.a === node ? edge.b : edge.a;
|
|
45001
|
+
}
|
|
45002
|
+
function walk(start, firstEdge) {
|
|
45003
|
+
var chain = [start.coord];
|
|
45004
|
+
var node = start;
|
|
45005
|
+
var edge = firstEdge;
|
|
45006
|
+
while (true) {
|
|
45007
|
+
edge.used = true;
|
|
45008
|
+
node = other(edge, node);
|
|
45009
|
+
chain.push(node.coord);
|
|
45010
|
+
if (node.edges.length !== 2) break; // junction or dangling end
|
|
45011
|
+
var next = node.edges[0] === edge ? node.edges[1] : node.edges[0];
|
|
45012
|
+
if (next.used) break; // closed loop back to start
|
|
45013
|
+
edge = next;
|
|
45014
|
+
}
|
|
45015
|
+
return chain;
|
|
45016
|
+
}
|
|
45017
|
+
var chains = [];
|
|
45018
|
+
// Chains anchored at junctions / dangling ends first...
|
|
45019
|
+
for (var id = 0; id < nodes.length; id++) {
|
|
45020
|
+
var node = nodes[id];
|
|
45021
|
+
if (!node || node.edges.length === 2) continue;
|
|
45022
|
+
node.edges.forEach(function(edge) {
|
|
45023
|
+
if (!edge.used) chains.push(walk(node, edge));
|
|
45024
|
+
});
|
|
45025
|
+
}
|
|
45026
|
+
// ...then any remaining all-degree-2 loops.
|
|
45027
|
+
edges.forEach(function(edge) {
|
|
45028
|
+
if (!edge.used) chains.push(walk(edge.a, edge));
|
|
45029
|
+
});
|
|
45030
|
+
return chains;
|
|
45031
|
+
}
|
|
45032
|
+
|
|
45033
|
+
// Weighted-Visvalingam simplifier shared across chains. Its internal heap and
|
|
45034
|
+
// scratch buffers are reused per call, so a single instance is safe for the
|
|
45035
|
+
// sequential per-chain calls below.
|
|
45036
|
+
var medialSimplifier = Visvalingam.getWeightedSimplifier({}, false);
|
|
45037
|
+
|
|
45038
|
+
// Smooth a medial polyline with weighted Visvalingam, dropping vertices whose
|
|
45039
|
+
// effective area (expressed as a linear-equivalent by scaledSimplify) falls
|
|
45040
|
+
// below @interval. Endpoints carry an Infinity threshold and are always kept.
|
|
45041
|
+
function simplifyChain(points, interval) {
|
|
45042
|
+
var n = points.length;
|
|
45043
|
+
if (n < 3) return points;
|
|
45044
|
+
var xx = new Float64Array(n);
|
|
45045
|
+
var yy = new Float64Array(n);
|
|
45046
|
+
var kk = new Float64Array(n);
|
|
45047
|
+
for (var i = 0; i < n; i++) {
|
|
45048
|
+
xx[i] = points[i][0];
|
|
45049
|
+
yy[i] = points[i][1];
|
|
45050
|
+
}
|
|
45051
|
+
medialSimplifier(kk, xx, yy);
|
|
45052
|
+
var out = [];
|
|
45053
|
+
for (i = 0; i < n; i++) {
|
|
45054
|
+
if (kk[i] >= interval) out.push(points[i]);
|
|
45055
|
+
}
|
|
45056
|
+
return out.length >= 2 ? out : [points[0], points[n - 1]];
|
|
45057
|
+
}
|
|
45058
|
+
|
|
45059
|
+
// Boundary sample spacing as a fraction of the local gap width: smaller gives a
|
|
45060
|
+
// smoother medial axis (more sites) in narrow channels. 0.5 keeps the spacing
|
|
45061
|
+
// at most half the gap, so a channel of width w has at least two samples per
|
|
45062
|
+
// bank across it.
|
|
45063
|
+
var GAP_FACTOR = 0.5;
|
|
45064
|
+
|
|
45065
|
+
// Gaps narrower than this fraction of the buffer distance are treated as
|
|
45066
|
+
// "touching" (at or below the buffer's positional tolerance, ~1%): no medial is
|
|
45067
|
+
// densified there, since the shared source boundary already partitions the
|
|
45068
|
+
// overlap. This keeps coincident mosaic borders from flooding the triangulation
|
|
45069
|
+
// with collinear sites while leaving real channels (the Columbia is ~3% of the
|
|
45070
|
+
// buffer distance) fully sampled.
|
|
45071
|
+
var TOUCHING_GAP_FRACTION = 0.002;
|
|
45072
|
+
|
|
45073
|
+
// Boundary arcs that could bound a gap, found from the layer topology: in a
|
|
45074
|
+
// shared-arc polygon mosaic an interior border between two features is one arc
|
|
45075
|
+
// used once forward and once reversed, so the source boundary already
|
|
45076
|
+
// partitions any buffer overlap there and it needs no medial. An arc used in
|
|
45077
|
+
// only one direction is an external boundary, an inlet edge, or a hole edge --
|
|
45078
|
+
// the only places a gap can be. Pruning the shared borders here drops the bulk
|
|
45079
|
+
// of a dense mosaic (most county/state borders are shared) before any distance
|
|
45080
|
+
// work; keptSites' distance test still separates the real gaps from the open
|
|
45081
|
+
// external boundary, so inputs whose coincident borders are NOT shared arcs
|
|
45082
|
+
// (each polygon carries its own copy) still come out correct, just less pruned.
|
|
45083
|
+
// Returns one open path per candidate arc, tagged with its owner feature.
|
|
45084
|
+
function collectCandidateArcPaths(shapes, coordDistances, arcs) {
|
|
45085
|
+
var n = arcs.size();
|
|
45086
|
+
var fwd = new Int32Array(n).fill(-1);
|
|
45087
|
+
var rev = new Int32Array(n).fill(-1);
|
|
45088
|
+
for (var s = 0; s < shapes.length; s++) {
|
|
45089
|
+
var shape = shapes[s];
|
|
45090
|
+
if (!shape || !(coordDistances[s] > 0)) continue;
|
|
45091
|
+
for (var p = 0; p < shape.length; p++) {
|
|
45092
|
+
var ids = shape[p];
|
|
45093
|
+
for (var k = 0; k < ids.length; k++) {
|
|
45094
|
+
var id = ids[k];
|
|
45095
|
+
if (id < 0) { if (rev[~id] === -1) rev[~id] = s; }
|
|
45096
|
+
else if (fwd[id] === -1) fwd[id] = s;
|
|
45097
|
+
}
|
|
45098
|
+
}
|
|
45099
|
+
}
|
|
45100
|
+
var paths = [];
|
|
45101
|
+
for (var i = 0; i < n; i++) {
|
|
45102
|
+
var f = fwd[i], r = rev[i];
|
|
45103
|
+
if (f === -1 && r === -1) continue; // arc not used by a buffered feature
|
|
45104
|
+
if (f !== -1 && r !== -1) continue; // shared interior border -- not a gap
|
|
45105
|
+
var pts = arcCoords(arcs, i);
|
|
45106
|
+
if (pts.length >= 2) paths.push({owner: f !== -1 ? f : r, points: pts});
|
|
45107
|
+
}
|
|
45108
|
+
return paths;
|
|
45109
|
+
}
|
|
45110
|
+
|
|
45111
|
+
function arcCoords(arcs, arcId) {
|
|
45112
|
+
var iter = arcs.getArcIter(arcId);
|
|
45113
|
+
var pts = [];
|
|
45114
|
+
while (iter.hasNext()) pts.push([iter.x, iter.y]);
|
|
45115
|
+
return pts;
|
|
45116
|
+
}
|
|
45117
|
+
|
|
45118
|
+
// Gather labeled Voronoi sites from the gap-candidate boundary arcs (see
|
|
45119
|
+
// collectCandidateArcPaths) of the buffered features.
|
|
45120
|
+
//
|
|
45121
|
+
// Those arcs are sampled adaptively: where two features run close together
|
|
45122
|
+
// (e.g. the opposite banks of a narrow river) the boundary is sampled finely so
|
|
45123
|
+
// the medial axis tracks the channel centerline instead of zigzagging between
|
|
45124
|
+
// the banks; where features are far apart the spacing relaxes to the buffer
|
|
45125
|
+
// distance. The local gap width is measured directly to the candidate boundary
|
|
45126
|
+
// segments (computeVertexGaps), driving the densification in a single pass.
|
|
45127
|
+
function collectSites(shapes, coordDistances, arcs) {
|
|
45128
|
+
if (!arcs) return null;
|
|
45129
|
+
var paths = collectCandidateArcPaths(shapes, coordDistances, arcs);
|
|
45130
|
+
if (paths.length < 2) return null;
|
|
45131
|
+
|
|
45132
|
+
// Assign every candidate vertex a stable id (vid) so a per-vertex gap can be
|
|
45133
|
+
// looked up while densifying its segments.
|
|
45134
|
+
var verts = buildVertexLayout(paths);
|
|
45135
|
+
if (verts.count < 2) return null;
|
|
45136
|
+
var totalLen = ringsLength(paths.map(function(p) { return p.points; }));
|
|
45137
|
+
// Spacing floor: totalLen/MAX_SITES is the simple-mosaic floor (keeps small
|
|
45138
|
+
// inputs stable and near-coincident borders from over-sampling), but it is
|
|
45139
|
+
// capped at a fraction of the buffer distance so a large mosaic keeps adaptive
|
|
45140
|
+
// headroom instead of flattening onto maxSpacing. When the cap binds, fitCoarsen
|
|
45141
|
+
// is what bounds the actual site total.
|
|
45142
|
+
var maxDistance = 0;
|
|
45143
|
+
for (var ci = 0; ci < coordDistances.length; ci++) {
|
|
45144
|
+
if (coordDistances[ci] > maxDistance) maxDistance = coordDistances[ci];
|
|
45145
|
+
}
|
|
45146
|
+
var spacingFloor = Math.min(totalLen / MAX_SITES,
|
|
45147
|
+
maxDistance * FLOOR_DISTANCE_FRACTION);
|
|
45148
|
+
|
|
45149
|
+
// Sample spacing is proportional to the local gap (gap * GAP_FACTOR * coarsen),
|
|
45150
|
+
// floored at spacingFloor and capped at the buffer distance, so the narrowest
|
|
45151
|
+
// channels (a river between two states) get the finest sampling and wide
|
|
45152
|
+
// overlaps the coarsest. coarsen scales the whole distribution up just enough
|
|
45153
|
+
// to fit the site budget on dense mosaics (counties), keeping narrow channels
|
|
45154
|
+
// proportionally finer than wide ones; on sparse inputs it stays 1 (fully
|
|
45155
|
+
// adaptive). The per-vertex gap is computed directly from the boundary geometry
|
|
45156
|
+
// in a single pass, then we densify once.
|
|
45157
|
+
var grid = buildSegmentGrid(verts, coordDistances);
|
|
45158
|
+
var gaps = computeVertexGaps(grid, verts, coordDistances);
|
|
45159
|
+
var coarsen = fitCoarsen(verts, gaps, coordDistances, spacingFloor);
|
|
45160
|
+
var sites = densifyVertices(verts, gaps, coordDistances, spacingFloor, coarsen);
|
|
45161
|
+
// Triangulate only the sites bordering a real gap. Its medial segments come
|
|
45162
|
+
// exclusively from cross-feature edges, and the well-shaped triangles that
|
|
45163
|
+
// bridge a gap have their apex within reach too (a far apex makes a thin
|
|
45164
|
+
// triangle whose wild circumcenter is filtered out, or a hull edge that is
|
|
45165
|
+
// extrapolated as an outward ray). Dropping the touching interior borders and
|
|
45166
|
+
// the no-feature coastline shrinks the one remaining Delaunay and avoids
|
|
45167
|
+
// building a redundant medial where the source boundary already partitions.
|
|
45168
|
+
var kept = keptSites(sites, grid, coordDistances);
|
|
45169
|
+
// Keep the segment grid with the sites so computeMedialSegments can re-measure
|
|
45170
|
+
// the true source gap when the sample-pair proximity test is too coarse.
|
|
45171
|
+
kept.grid = grid;
|
|
45172
|
+
return kept;
|
|
45173
|
+
}
|
|
45174
|
+
|
|
45175
|
+
// Bucket every boundary segment into a uniform grid so the nearest cross-feature
|
|
45176
|
+
// segment to an arbitrary point can be found by probing its 3x3 cell
|
|
45177
|
+
// neighborhood. The cell equals the maximum reach (sum of the two largest buffer
|
|
45178
|
+
// distances), so any in-reach segment is guaranteed to fall in that 3x3 window.
|
|
45179
|
+
// Returns null when there is no positive reach. Reused for both the per-vertex
|
|
45180
|
+
// gap (drives adaptive sampling) and the per-site keep test (gapAtPoint).
|
|
45181
|
+
function buildSegmentGrid(verts, coordDistances) {
|
|
45182
|
+
var paths = verts.paths;
|
|
45183
|
+
var maxDist = 0;
|
|
45184
|
+
for (var d = 0; d < coordDistances.length; d++) {
|
|
45185
|
+
if (coordDistances[d] > maxDist) maxDist = coordDistances[d];
|
|
45186
|
+
}
|
|
45187
|
+
var cell = 2 * maxDist; // upper bound on any pair's reach
|
|
45188
|
+
if (!(cell > 0)) return null;
|
|
45189
|
+
var xmin = Infinity, ymin = Infinity, ymax = -Infinity;
|
|
45190
|
+
paths.forEach(function(path) {
|
|
45191
|
+
var pts = path.points;
|
|
45192
|
+
for (var i = 0; i < pts.length; i++) {
|
|
45193
|
+
if (pts[i][0] < xmin) xmin = pts[i][0];
|
|
45194
|
+
if (pts[i][1] < ymin) ymin = pts[i][1];
|
|
45195
|
+
if (pts[i][1] > ymax) ymax = pts[i][1];
|
|
45196
|
+
}
|
|
45197
|
+
});
|
|
45198
|
+
// +1 cell index shift keeps probed -1 neighbors non-negative; rowSpan packs
|
|
45199
|
+
// (col, row) into a collision-free integer key.
|
|
45200
|
+
var rowSpan = Math.floor((ymax - ymin) / cell) + 3;
|
|
45201
|
+
function cellKey(cx, cy) { return (cx + 1) * rowSpan + (cy + 1); }
|
|
45202
|
+
function colOf(x) { return Math.floor((x - xmin) / cell); }
|
|
45203
|
+
function rowOf(y) { return Math.floor((y - ymin) / cell); }
|
|
45204
|
+
var seg = {x0: [], y0: [], x1: [], y1: [], feat: [], reach: []};
|
|
45205
|
+
var grid = new Map();
|
|
45206
|
+
paths.forEach(function(path) {
|
|
45207
|
+
var pts = path.points;
|
|
45208
|
+
var feat = path.owner;
|
|
45209
|
+
var reach = coordDistances[feat];
|
|
45210
|
+
for (var k = 0; k + 1 < pts.length; k++) {
|
|
45211
|
+
var ax = pts[k][0], ay = pts[k][1], bx = pts[k + 1][0], by = pts[k + 1][1];
|
|
45212
|
+
var idx = seg.feat.length;
|
|
45213
|
+
seg.x0.push(ax); seg.y0.push(ay); seg.x1.push(bx); seg.y1.push(by);
|
|
45214
|
+
seg.feat.push(feat); seg.reach.push(reach);
|
|
45215
|
+
var cxa = colOf(Math.min(ax, bx)), cxb = colOf(Math.max(ax, bx));
|
|
45216
|
+
var cya = rowOf(Math.min(ay, by)), cyb = rowOf(Math.max(ay, by));
|
|
45217
|
+
for (var gx = cxa; gx <= cxb; gx++) {
|
|
45218
|
+
for (var gy = cya; gy <= cyb; gy++) {
|
|
45219
|
+
var key = cellKey(gx, gy);
|
|
45220
|
+
var bucket = grid.get(key);
|
|
45221
|
+
if (bucket) bucket.push(idx); else grid.set(key, [idx]);
|
|
45222
|
+
}
|
|
45223
|
+
}
|
|
45224
|
+
}
|
|
45225
|
+
});
|
|
45226
|
+
return {seg: seg, grid: grid, cellKey: cellKey, colOf: colOf, rowOf: rowOf};
|
|
45227
|
+
}
|
|
45228
|
+
|
|
45229
|
+
// The channel width at (x, y): distance to the nearest different-feature segment
|
|
45230
|
+
// within their combined reach, or Infinity if none. Works for any point, not
|
|
45231
|
+
// just original vertices, so a long edge whose endpoints are out of reach but
|
|
45232
|
+
// whose middle crosses a gap is still measured correctly at the interior sites.
|
|
45233
|
+
function gapAtPoint(ctx, x, y, feat, reachF) {
|
|
45234
|
+
return nearestCrossFeatureSegmentDist(x, y, feat, reachF, ctx.seg, ctx.grid,
|
|
45235
|
+
ctx.cellKey, ctx.colOf, ctx.rowOf, Infinity);
|
|
45236
|
+
}
|
|
45237
|
+
|
|
45238
|
+
// Local gap at each original vertex (drives adaptive sampling, see
|
|
45239
|
+
// segmentSpacing). Measuring straight to the boundary segments yields the true
|
|
45240
|
+
// gap in a single pass, replacing the old triangulate -> estimate-gap ->
|
|
45241
|
+
// re-densify refinement loop that existed only because a coarse sampling can't
|
|
45242
|
+
// see a narrow gap (its nearest cross-feature SAMPLE is far).
|
|
45243
|
+
function computeVertexGaps(ctx, verts, coordDistances) {
|
|
45244
|
+
profileStart('medial:segmentGaps');
|
|
45245
|
+
var gaps = filledArray(verts.count, Infinity);
|
|
45246
|
+
if (ctx) {
|
|
45247
|
+
verts.paths.forEach(function(path) {
|
|
45248
|
+
var pts = path.points;
|
|
45249
|
+
var vids = path.vids;
|
|
45250
|
+
var feat = path.owner;
|
|
45251
|
+
var reachF = coordDistances[feat];
|
|
45252
|
+
for (var k = 0; k < vids.length; k++) {
|
|
45253
|
+
var g = gapAtPoint(ctx, pts[k][0], pts[k][1], feat, reachF);
|
|
45254
|
+
if (g < gaps[vids[k]]) gaps[vids[k]] = g;
|
|
45255
|
+
}
|
|
45256
|
+
});
|
|
45257
|
+
}
|
|
45258
|
+
profileEnd('medial:segmentGaps');
|
|
45259
|
+
return gaps;
|
|
45260
|
+
}
|
|
45261
|
+
|
|
45262
|
+
// Distance from (vx, vy) (owned by @feat, reach @reachF) to the nearest
|
|
45263
|
+
// different-feature segment within their combined reach, or @best if none is
|
|
45264
|
+
// closer. Probes the 3x3 grid-cell neighborhood (cell == max reach).
|
|
45265
|
+
function nearestCrossFeatureSegmentDist(vx, vy, feat, reachF, seg, grid, cellKey,
|
|
45266
|
+
colOf, rowOf, best) {
|
|
45267
|
+
var cx = colOf(vx), cy = rowOf(vy);
|
|
45268
|
+
for (var gx = cx - 1; gx <= cx + 1; gx++) {
|
|
45269
|
+
for (var gy = cy - 1; gy <= cy + 1; gy++) {
|
|
45270
|
+
var bucket = grid.get(cellKey(gx, gy));
|
|
45271
|
+
if (!bucket) continue;
|
|
45272
|
+
for (var b = 0; b < bucket.length; b++) {
|
|
45273
|
+
var s = bucket[b];
|
|
45274
|
+
if (seg.feat[s] === feat) continue;
|
|
45275
|
+
var reach = reachF + seg.reach[s];
|
|
45276
|
+
var dsq = pointSegDistSq2(vx, vy, seg.x0[s], seg.y0[s], seg.x1[s], seg.y1[s]);
|
|
45277
|
+
if (dsq <= reach * reach) {
|
|
45278
|
+
var dist = Math.sqrt(dsq);
|
|
45279
|
+
if (dist < best) best = dist;
|
|
45280
|
+
}
|
|
45281
|
+
}
|
|
45282
|
+
}
|
|
45283
|
+
}
|
|
45284
|
+
return best;
|
|
45285
|
+
}
|
|
45286
|
+
|
|
45287
|
+
// True when medial vertex c lies in the buffer overlap of features fp and fq:
|
|
45288
|
+
// within fp's radius of an fp-owned source segment AND within fq's radius of an
|
|
45289
|
+
// fq-owned source segment. Measured against the actual source segments via the
|
|
45290
|
+
// grid, so it is correct regardless of how coarsely the banks were sampled --
|
|
45291
|
+
// unlike the sample-pair distance, which overestimates the gap when the nearest
|
|
45292
|
+
// samples on opposite banks are staggered or far from the true closest approach.
|
|
45293
|
+
// Slack on each reach when rescuing a cross-feature edge whose sample endpoints
|
|
45294
|
+
// fell outside the cheap proximity test. It absorbs the discretization of the
|
|
45295
|
+
// medial graph near a pinch point: the connecting Voronoi edge is bounded by the
|
|
45296
|
+
// site spacing (capped at the buffer distance), so a genuinely contested edge can
|
|
45297
|
+
// run up to ~1.5x reach and its medial vertices can land a similar fraction
|
|
45298
|
+
// outside the overlap. 1.3 covers the worst real case observed (~1.18) with
|
|
45299
|
+
// headroom, while spurious edges between sites contested with *other* features
|
|
45300
|
+
// miss by far more (>=1.5 or have no nearby source segment) and stay pruned.
|
|
45301
|
+
var MEDIAL_OVERLAP_SLACK = 1.3;
|
|
45302
|
+
|
|
45303
|
+
function medialVertexInOverlap(ctx, c, fp, fq, rp, rq) {
|
|
45304
|
+
var sp = rp * MEDIAL_OVERLAP_SLACK, sq = rq * MEDIAL_OVERLAP_SLACK;
|
|
45305
|
+
return pointFeatureDistSq(ctx, c[0], c[1], fp) <= sp * sp &&
|
|
45306
|
+
pointFeatureDistSq(ctx, c[0], c[1], fq) <= sq * sq;
|
|
45307
|
+
}
|
|
45308
|
+
|
|
45309
|
+
// Squared distance from (x, y) to the nearest segment owned by feature @feat,
|
|
45310
|
+
// probing the 3x3 grid-cell neighborhood (cell == max reach, so any segment
|
|
45311
|
+
// within a single feature's radius is in the window). Infinity if none.
|
|
45312
|
+
function pointFeatureDistSq(ctx, x, y, feat) {
|
|
45313
|
+
var seg = ctx.seg, grid = ctx.grid;
|
|
45314
|
+
var cx = ctx.colOf(x), cy = ctx.rowOf(y);
|
|
45315
|
+
var best = Infinity;
|
|
45316
|
+
for (var gx = cx - 1; gx <= cx + 1; gx++) {
|
|
45317
|
+
for (var gy = cy - 1; gy <= cy + 1; gy++) {
|
|
45318
|
+
var bucket = grid.get(ctx.cellKey(gx, gy));
|
|
45319
|
+
if (!bucket) continue;
|
|
45320
|
+
for (var b = 0; b < bucket.length; b++) {
|
|
45321
|
+
var s = bucket[b];
|
|
45322
|
+
if (seg.feat[s] !== feat) continue;
|
|
45323
|
+
var d2 = pointSegDistSq2(x, y, seg.x0[s], seg.y0[s], seg.x1[s], seg.y1[s]);
|
|
45324
|
+
if (d2 < best) best = d2;
|
|
45325
|
+
}
|
|
45326
|
+
}
|
|
45327
|
+
}
|
|
45328
|
+
return best;
|
|
45329
|
+
}
|
|
45330
|
+
|
|
45331
|
+
// Keep only the sites that border a real gap: a different feature within reach
|
|
45332
|
+
// (finite gap) but farther than the touching threshold. These are the only sites
|
|
45333
|
+
// that can shape the medial axis. Touching/coincident interior borders (gap ~ 0,
|
|
45334
|
+
// the shared source boundary already partitions them) and the no-feature
|
|
45335
|
+
// coastline (gap = Infinity) are dropped, so the Delaunay covers just the
|
|
45336
|
+
// genuine gaps -- no triangulation is wasted on borders that need no medial.
|
|
45337
|
+
// The gap is measured per site (not per vertex) so a long edge whose endpoints
|
|
45338
|
+
// fall out of reach but whose middle crosses a gap keeps its interior points.
|
|
45339
|
+
function keptSites(sites, ctx, coordDistances) {
|
|
45340
|
+
profileStart('medial:contested');
|
|
45341
|
+
var result = {coords: [], owner: [], origin: []};
|
|
45342
|
+
if (!ctx) {
|
|
45343
|
+
profileEnd('medial:contested');
|
|
45344
|
+
return result;
|
|
45345
|
+
}
|
|
45346
|
+
var coords = sites.coords, owner = sites.owner, origin = sites.origin;
|
|
45347
|
+
for (var i = 0; i < coords.length; i++) {
|
|
45348
|
+
var feat = owner[i];
|
|
45349
|
+
var reach = coordDistances[feat];
|
|
45350
|
+
var g = gapAtPoint(ctx, coords[i][0], coords[i][1], feat, reach);
|
|
45351
|
+
if (isFinite(g) && g > reach * TOUCHING_GAP_FRACTION) {
|
|
45352
|
+
result.coords.push(coords[i]);
|
|
45353
|
+
result.owner.push(feat);
|
|
45354
|
+
result.origin.push(origin[i]);
|
|
45355
|
+
}
|
|
45356
|
+
}
|
|
45357
|
+
profileEnd('medial:contested');
|
|
45358
|
+
return result;
|
|
45359
|
+
}
|
|
45360
|
+
|
|
45361
|
+
function ringsLength(rings) {
|
|
45362
|
+
var sum = 0;
|
|
45363
|
+
rings.forEach(function(points) {
|
|
45364
|
+
for (var i = 1; i < points.length; i++) {
|
|
45365
|
+
var dx = points[i][0] - points[i - 1][0];
|
|
45366
|
+
var dy = points[i][1] - points[i - 1][1];
|
|
45367
|
+
sum += Math.sqrt(dx * dx + dy * dy);
|
|
45368
|
+
}
|
|
45369
|
+
});
|
|
45370
|
+
return sum;
|
|
45371
|
+
}
|
|
45372
|
+
|
|
45373
|
+
function filledArray(n, v) {
|
|
45374
|
+
var a = new Float64Array(n);
|
|
45375
|
+
a.fill(v);
|
|
45376
|
+
return a;
|
|
45377
|
+
}
|
|
45378
|
+
|
|
45379
|
+
// Flatten the candidate arc paths into a vertex layout: one entry per path
|
|
45380
|
+
// carrying its owner feature, its points, and a stable id (vid) for each vertex,
|
|
45381
|
+
// so densifyVertices can re-sample using a per-vid gap estimate. Each candidate
|
|
45382
|
+
// arc is an open polyline (every coordinate is a vertex, no wrap-around); a
|
|
45383
|
+
// closed ring made of a single arc arrives with its first point repeated at the
|
|
45384
|
+
// end, so treating it as open still covers the full loop.
|
|
45385
|
+
function buildVertexLayout(paths) {
|
|
45386
|
+
var layout = [];
|
|
45387
|
+
var count = 0;
|
|
45388
|
+
paths.forEach(function(path) {
|
|
45389
|
+
var points = path.points;
|
|
45390
|
+
var m = points.length;
|
|
45391
|
+
if (m < 2) return;
|
|
45392
|
+
var vids = [];
|
|
45393
|
+
for (var i = 0; i < m; i++) vids.push(count++);
|
|
45394
|
+
layout.push({owner: path.owner, points: points, vids: vids});
|
|
45395
|
+
});
|
|
45396
|
+
return {paths: layout, count: count};
|
|
45397
|
+
}
|
|
45398
|
+
|
|
45399
|
+
// The spacing for a path segment: the tighter of its two endpoints' gap-derived
|
|
45400
|
+
// spacings (so a segment straddling a narrowing gap samples at the finer rate).
|
|
45401
|
+
function segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen) {
|
|
45402
|
+
var sA = spacingFromGap(gaps[path.vids[k]], maxSpacing, spacingFloor, coarsen);
|
|
45403
|
+
var sB = spacingFromGap(gaps[path.vids[k + 1]], maxSpacing, spacingFloor, coarsen);
|
|
45404
|
+
return Math.min(sA, sB);
|
|
45405
|
+
}
|
|
45406
|
+
|
|
45407
|
+
// Re-sample every candidate path: emit each original vertex (tagged with its
|
|
45408
|
+
// vid) plus interior points spaced by the local gap-derived spacing (see
|
|
45409
|
+
// segmentSpacing). Paths are open, so the last vertex has no following segment.
|
|
45410
|
+
// Long edges are densified even where their endpoints are out of reach, so a
|
|
45411
|
+
// contested middle is sampled; keptSites later prunes the points that turn out
|
|
45412
|
+
// not to border a real gap.
|
|
45413
|
+
function densifyVertices(verts, gaps, coordDistances, spacingFloor, coarsen) {
|
|
45414
|
+
var coords = [];
|
|
45415
|
+
var owner = [];
|
|
45416
|
+
var origin = []; // vid for original vertices, -1 for interpolated points
|
|
45417
|
+
verts.paths.forEach(function(path) {
|
|
45418
|
+
var maxSpacing = coordDistances[path.owner];
|
|
45419
|
+
var m = path.vids.length;
|
|
45420
|
+
for (var k = 0; k < m; k++) {
|
|
45421
|
+
var a = path.points[k];
|
|
45422
|
+
coords.push([a[0], a[1]]);
|
|
45423
|
+
owner.push(path.owner);
|
|
45424
|
+
origin.push(path.vids[k]);
|
|
45425
|
+
if (k + 1 >= m) continue; // open path: no segment past the last vertex
|
|
45426
|
+
var b = path.points[k + 1];
|
|
45427
|
+
var s = segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen);
|
|
45428
|
+
var dx = b[0] - a[0], dy = b[1] - a[1];
|
|
45429
|
+
var len = Math.sqrt(dx * dx + dy * dy);
|
|
45430
|
+
if (s > 0 && len > s) {
|
|
45431
|
+
var steps = Math.floor(len / s);
|
|
45432
|
+
for (var t = 1; t <= steps; t++) {
|
|
45433
|
+
var f = t / (steps + 1);
|
|
45434
|
+
coords.push([a[0] + dx * f, a[1] + dy * f]);
|
|
45435
|
+
owner.push(path.owner);
|
|
45436
|
+
origin.push(-1);
|
|
45437
|
+
}
|
|
45438
|
+
}
|
|
45439
|
+
}
|
|
45440
|
+
});
|
|
45441
|
+
return {coords: coords, owner: owner, origin: origin};
|
|
45442
|
+
}
|
|
45443
|
+
|
|
45444
|
+
function spacingFromGap(gap, maxSpacing, spacingFloor, coarsen) {
|
|
45445
|
+
if (!isFinite(gap)) return maxSpacing;
|
|
45446
|
+
// A gap at or below the buffer's positional tolerance means the two features
|
|
45447
|
+
// effectively touch: there is no contested channel to run a medial down, and
|
|
45448
|
+
// the shared source boundary already partitions the overlap. Densifying it
|
|
45449
|
+
// would only flood a coincident border with collinear sites (millions of them
|
|
45450
|
+
// on a clean topological mosaic), so leave it at the coarse spacing.
|
|
45451
|
+
if (gap < maxSpacing * TOUCHING_GAP_FRACTION) return maxSpacing;
|
|
45452
|
+
var s = gap * GAP_FACTOR * coarsen;
|
|
45453
|
+
if (s > maxSpacing) s = maxSpacing;
|
|
45454
|
+
if (s < spacingFloor) s = spacingFloor;
|
|
45455
|
+
return s;
|
|
45456
|
+
}
|
|
45457
|
+
|
|
45458
|
+
// Predicted total site count for a given coarsen, matching densifyVertices'
|
|
45459
|
+
// emission rule exactly (one site per original vertex plus floor(len/spacing)
|
|
45460
|
+
// interior points per segment). Pure counting, no Delaunay -- cheap enough to
|
|
45461
|
+
// binary-search coarsen against. Counts pre-keep sites (the densification work),
|
|
45462
|
+
// which is what coarsen actually bounds.
|
|
45463
|
+
function predictSiteCount(verts, gaps, coordDistances, spacingFloor, coarsen) {
|
|
45464
|
+
var total = verts.count; // every original vertex is emitted
|
|
45465
|
+
verts.paths.forEach(function(path) {
|
|
45466
|
+
var maxSpacing = coordDistances[path.owner];
|
|
45467
|
+
var m = path.vids.length;
|
|
45468
|
+
for (var k = 0; k + 1 < m; k++) {
|
|
45469
|
+
var s = segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen);
|
|
45470
|
+
var a = path.points[k];
|
|
45471
|
+
var b = path.points[k + 1];
|
|
45472
|
+
var dx = b[0] - a[0], dy = b[1] - a[1];
|
|
45473
|
+
var len = Math.sqrt(dx * dx + dy * dy);
|
|
45474
|
+
if (s > 0 && len > s) total += Math.floor(len / s);
|
|
45475
|
+
}
|
|
45476
|
+
});
|
|
45477
|
+
return total;
|
|
45478
|
+
}
|
|
45479
|
+
|
|
45480
|
+
// Smallest coarsen (>= 1) whose predicted site count fits SITE_BUDGET. Site
|
|
45481
|
+
// count decreases monotonically as coarsen grows (spacing widens), so binary
|
|
45482
|
+
// search converges; capped because near-coincident gaps (gap ~ 0) can't be
|
|
45483
|
+
// thinned by coarsen and are bounded by spacingFloor instead.
|
|
45484
|
+
function fitCoarsen(verts, gaps, coordDistances, spacingFloor) {
|
|
45485
|
+
if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, 1) <= SITE_BUDGET) {
|
|
45486
|
+
return 1;
|
|
45487
|
+
}
|
|
45488
|
+
var lo = 1, hi = 1024;
|
|
45489
|
+
if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, hi) > SITE_BUDGET) {
|
|
45490
|
+
return hi; // even fully coarsened we can't fit; accept the floor-bounded count
|
|
45491
|
+
}
|
|
45492
|
+
for (var i = 0; i < 20; i++) {
|
|
45493
|
+
var mid = (lo + hi) / 2;
|
|
45494
|
+
if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, mid) > SITE_BUDGET) {
|
|
45495
|
+
lo = mid;
|
|
45496
|
+
} else {
|
|
45497
|
+
hi = mid;
|
|
45498
|
+
}
|
|
45499
|
+
}
|
|
45500
|
+
return hi;
|
|
45501
|
+
}
|
|
45502
|
+
|
|
45503
|
+
function nextHalfedge(e) {
|
|
45504
|
+
return e % 3 === 2 ? e - 2 : e + 1;
|
|
45505
|
+
}
|
|
45506
|
+
|
|
45507
|
+
function triangleOfEdge(e) {
|
|
45508
|
+
return Math.floor(e / 3);
|
|
45509
|
+
}
|
|
45510
|
+
|
|
45511
|
+
function computeMedialSegments(sites, coordDistances, ctx) {
|
|
45512
|
+
var coords = sites.coords;
|
|
45513
|
+
var owner = sites.owner;
|
|
45514
|
+
profileStart('medial:delaunay');
|
|
45515
|
+
var del = Delaunator.from(coords);
|
|
45516
|
+
profileEnd('medial:delaunay');
|
|
45517
|
+
var triangles = del.triangles;
|
|
45518
|
+
var halfedges = del.halfedges;
|
|
45519
|
+
var ntri = triangles.length / 3;
|
|
45520
|
+
// Medial-graph vertex coords, indexed by id. Triangle t's circumcenter is
|
|
45521
|
+
// vertex id t (so the three medial edges meeting at it share that id without
|
|
45522
|
+
// coordinate hashing); hull-ray ends are appended with fresh ids.
|
|
45523
|
+
var verts = new Array(ntri);
|
|
45524
|
+
var i;
|
|
45525
|
+
for (i = 0; i < ntri; i++) {
|
|
45526
|
+
verts[i] = circumcenter(
|
|
45527
|
+
coords[triangles[3 * i]],
|
|
45528
|
+
coords[triangles[3 * i + 1]],
|
|
45529
|
+
coords[triangles[3 * i + 2]]);
|
|
45530
|
+
}
|
|
45531
|
+
var segments = [];
|
|
45532
|
+
for (var e = 0; e < triangles.length; e++) {
|
|
45533
|
+
var opp = halfedges[e];
|
|
45534
|
+
var p = triangles[e];
|
|
45535
|
+
var q = triangles[nextHalfedge(e)];
|
|
45536
|
+
var fp = owner[p], fq = owner[q];
|
|
45537
|
+
if (fp === fq) continue;
|
|
45538
|
+
var dx = coords[p][0] - coords[q][0];
|
|
45539
|
+
var dy = coords[p][1] - coords[q][1];
|
|
45540
|
+
var siteDist = Math.sqrt(dx * dx + dy * dy);
|
|
45541
|
+
var rp = coordDistances[fp], rq = coordDistances[fq];
|
|
45542
|
+
var reach = rp + rq;
|
|
45543
|
+
var t1 = triangleOfEdge(e);
|
|
45544
|
+
var c1 = verts[t1];
|
|
45545
|
+
if (!c1) continue; // degenerate (near-collinear) triangle
|
|
45546
|
+
// Sites within the sum of their radii are accepted directly; this is the
|
|
45547
|
+
// common, cheap case. When they are farther apart, the bisector might still
|
|
45548
|
+
// be contested -- the nearest sample pair overestimates the true source gap
|
|
45549
|
+
// where banks are sampled coarsely or staggered. Re-measure the actual gap
|
|
45550
|
+
// at the medial vertex against the source segments (the grid) and rescue the
|
|
45551
|
+
// edge if it really lies in the buffer overlap. Without the rescue the medial
|
|
45552
|
+
// axis fragments at such spots, leaving the equidistant cut wall open so the
|
|
45553
|
+
// overlap face is never subdivided and a whole contested corridor is assigned
|
|
45554
|
+
// to one feature (a feature wrapping a neighbor's enclosed island).
|
|
45555
|
+
var near = siteDist <= reach;
|
|
45556
|
+
if (opp === -1) {
|
|
45557
|
+
if (!near && !(ctx && medialVertexInOverlap(ctx, c1, fp, fq, rp, rq))) continue;
|
|
45558
|
+
// Hull edge: the Voronoi edge here is an unbounded ray (the bisector of
|
|
45559
|
+
// two sites on the convex hull). Emit it as an outward ray from the
|
|
45560
|
+
// circumcenter so the medial line reaches and crosses the buffer
|
|
45561
|
+
// boundary -- otherwise an interior medial segment that ends at this
|
|
45562
|
+
// circumcenter would dangle inside a tile and be pruned, leaving no cut.
|
|
45563
|
+
// The excess outside the buffers is trimmed by detachAcyclicArcs.
|
|
45564
|
+
var third = coords[triangles[nextHalfedge(nextHalfedge(e))]];
|
|
45565
|
+
var end = outwardRayEnd(c1, coords[p], coords[q], third, reach);
|
|
45566
|
+
if (end) {
|
|
45567
|
+
var rayId = verts.length;
|
|
45568
|
+
verts.push(end);
|
|
45569
|
+
segments.push([t1, rayId]);
|
|
45570
|
+
}
|
|
45571
|
+
continue;
|
|
45572
|
+
}
|
|
45573
|
+
// interior edge: emit once (at the lower halfedge index)
|
|
45574
|
+
if (opp < e) continue;
|
|
45575
|
+
var t2 = triangleOfEdge(opp);
|
|
45576
|
+
var c2 = verts[t2];
|
|
45577
|
+
if (!c2) continue;
|
|
45578
|
+
if (!near && !(ctx &&
|
|
45579
|
+
(medialVertexInOverlap(ctx, c1, fp, fq, rp, rq) ||
|
|
45580
|
+
medialVertexInOverlap(ctx, c2, fp, fq, rp, rq)))) continue;
|
|
45581
|
+
var sx = c1[0] - c2[0], sy = c1[1] - c2[1];
|
|
45582
|
+
var segLen = Math.sqrt(sx * sx + sy * sy);
|
|
45583
|
+
// a real medial edge inside the overlap is short (on the order of the site
|
|
45584
|
+
// spacing plus the gap); a very long segment comes from a near-degenerate
|
|
45585
|
+
// triangle whose circumcenter is wild, so drop it
|
|
45586
|
+
if (segLen > 3 * (reach + siteDist)) continue;
|
|
45587
|
+
segments.push([t1, t2]);
|
|
45588
|
+
}
|
|
45589
|
+
return {segments: segments, coords: verts};
|
|
45590
|
+
}
|
|
45591
|
+
|
|
45592
|
+
// Endpoint of the outward Voronoi ray for a hull edge (p, q) whose triangle's
|
|
45593
|
+
// third vertex is @third: starts at the circumcenter @c, runs along the edge's
|
|
45594
|
+
// perpendicular bisector, away from @third (outward), a length proportional to
|
|
45595
|
+
// the buffer reach so it clears the buffer boundary.
|
|
45596
|
+
function outwardRayEnd(c, p, q, third, reach) {
|
|
45597
|
+
var ex = q[0] - p[0], ey = q[1] - p[1];
|
|
45598
|
+
var nx = -ey, ny = ex; // a normal to the edge
|
|
45599
|
+
var mx = (p[0] + q[0]) / 2, my = (p[1] + q[1]) / 2;
|
|
45600
|
+
// orient the normal away from the third vertex (outward from the hull)
|
|
45601
|
+
if (nx * (mx - third[0]) + ny * (my - third[1]) < 0) {
|
|
45602
|
+
nx = -nx;
|
|
45603
|
+
ny = -ny;
|
|
45604
|
+
}
|
|
45605
|
+
var len = Math.sqrt(nx * nx + ny * ny);
|
|
45606
|
+
if (len === 0 || !isFinite(len)) return null;
|
|
45607
|
+
var L = 3 * reach;
|
|
45608
|
+
return [c[0] + nx / len * L, c[1] + ny / len * L];
|
|
45609
|
+
}
|
|
45610
|
+
|
|
45611
|
+
function circumcenter(a, b, c) {
|
|
45612
|
+
var ax = a[0], ay = a[1], bx = b[0], by = b[1], cx = c[0], cy = c[1];
|
|
45613
|
+
var d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
|
|
45614
|
+
if (d === 0 || !isFinite(d)) return null;
|
|
45615
|
+
var a2 = ax * ax + ay * ay;
|
|
45616
|
+
var b2 = bx * bx + by * by;
|
|
45617
|
+
var c2 = cx * cx + cy * cy;
|
|
45618
|
+
var ux = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d;
|
|
45619
|
+
var uy = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d;
|
|
45620
|
+
if (!isFinite(ux) || !isFinite(uy)) return null;
|
|
45621
|
+
return [ux, uy];
|
|
45622
|
+
}
|
|
45623
|
+
|
|
45624
|
+
// Remove small-area polygon rings (very simple implementation of sliver removal)
|
|
45625
|
+
// TODO: more sophisticated sliver detection (e.g. could consider ratio of area to perimeter)
|
|
45626
|
+
// TODO: consider merging slivers into adjacent polygons to prevent gaps from forming
|
|
45627
|
+
// TODO: consider separate gap removal function as an alternative to merging slivers
|
|
45628
|
+
//
|
|
45629
|
+
cmd.filterSlivers = function(lyr, dataset, opts) {
|
|
45630
|
+
if (lyr.geometry_type != 'polygon') {
|
|
45631
|
+
return 0;
|
|
45632
|
+
}
|
|
45633
|
+
return filterSlivers(lyr, dataset, opts);
|
|
45634
|
+
};
|
|
45635
|
+
|
|
45636
|
+
function filterSlivers(lyr, dataset, optsArg) {
|
|
45637
|
+
var opts = utils.extend({sliver_control: 1}, optsArg);
|
|
45638
|
+
var filterData = getSliverFilter(lyr, dataset, opts);
|
|
45639
|
+
var ringTest = filterData.filter;
|
|
45640
|
+
var removed = 0;
|
|
45641
|
+
var pathFilter = function(path, i, paths) {
|
|
45642
|
+
if (ringTest(path, i, paths)) {
|
|
45643
|
+
removed++;
|
|
45644
|
+
return null;
|
|
45645
|
+
}
|
|
45646
|
+
};
|
|
45647
|
+
|
|
45648
|
+
noteLayerWillChange(lyr, {operation: 'filter-slivers', unit: 'shapes'});
|
|
45649
|
+
editShapes(lyr.shapes, pathFilter);
|
|
45650
|
+
markLayerChanged(lyr, {operation: 'filter-slivers', unit: 'shapes'});
|
|
45651
|
+
message(utils.format("Removed %'d sliver%s using %s", removed, utils.pluralSuffix(removed), filterData.label));
|
|
45652
|
+
|
|
45653
|
+
// Remove null shapes (likely removed by clipping/erasing, although possibly already present)
|
|
45654
|
+
if (opts.remove_empty) {
|
|
45655
|
+
cmd.filterFeatures(lyr, dataset.arcs, {remove_empty: true, verbose: false});
|
|
45656
|
+
}
|
|
45657
|
+
return removed;
|
|
45658
|
+
}
|
|
45659
|
+
|
|
45660
|
+
function filterClipSlivers(lyr, clipLyr, arcs) {
|
|
45661
|
+
var threshold = getDefaultSliverThreshold(lyr, arcs);
|
|
45662
|
+
// message('Using variable sliver threshold (based on ' + (threshold / 1e6) + ' sqkm)');
|
|
45663
|
+
var ringTest = getSliverTest(arcs, threshold, 1);
|
|
45664
|
+
var flags = new Uint8Array(arcs.size());
|
|
45665
|
+
var removed = 0;
|
|
45666
|
+
var pathFilter = function(path) {
|
|
45667
|
+
var prevArcs = 0,
|
|
45668
|
+
newArcs = 0;
|
|
45669
|
+
for (var i=0, n=path && path.length || 0; i<n; i++) {
|
|
45670
|
+
if (flags[absArcId(path[i])] > 0) {
|
|
45671
|
+
newArcs++;
|
|
45672
|
+
} else {
|
|
45673
|
+
prevArcs++;
|
|
45674
|
+
}
|
|
45675
|
+
}
|
|
45676
|
+
// filter paths that contain arcs from both original and clip/erase layers
|
|
45677
|
+
// and are small
|
|
45678
|
+
if (newArcs > 0 && prevArcs > 0 && ringTest(path)) {
|
|
45679
|
+
removed++;
|
|
45680
|
+
return null;
|
|
45681
|
+
}
|
|
45682
|
+
};
|
|
45683
|
+
|
|
45684
|
+
countArcsInShapes(clipLyr.shapes, flags);
|
|
45685
|
+
noteLayerWillChange(lyr, {operation: 'filter-clip-slivers', unit: 'shapes'});
|
|
45686
|
+
editShapes(lyr.shapes, pathFilter);
|
|
45687
|
+
markLayerChanged(lyr, {operation: 'filter-clip-slivers', unit: 'shapes'});
|
|
45688
|
+
return removed;
|
|
45689
|
+
}
|
|
45690
|
+
|
|
45691
|
+
// Assumes: Arcs have been divided
|
|
45692
|
+
//
|
|
45693
|
+
function clipPolylines(targetShapes, clipShapes, nodes, type) {
|
|
45694
|
+
var index = new PathIndex(clipShapes, nodes.arcs);
|
|
45695
|
+
|
|
45696
|
+
return targetShapes.map(function(shp) {
|
|
45697
|
+
return clipPolyline(shp);
|
|
45698
|
+
});
|
|
45699
|
+
|
|
45700
|
+
function clipPolyline(shp) {
|
|
45701
|
+
var clipped = null;
|
|
45702
|
+
if (shp) clipped = shp.reduce(clipPath, []);
|
|
45703
|
+
return clipped && clipped.length > 0 ? clipped : null;
|
|
45704
|
+
}
|
|
45705
|
+
|
|
45706
|
+
function clipPath(memo, path) {
|
|
45707
|
+
var clippedPath = null,
|
|
45708
|
+
arcId, enclosed;
|
|
45709
|
+
for (var i=0; i<path.length; i++) {
|
|
45710
|
+
arcId = path[i];
|
|
45711
|
+
enclosed = index.arcIsEnclosed(arcId);
|
|
45712
|
+
if (enclosed && type == 'clip' || !enclosed && type == 'erase') {
|
|
45713
|
+
if (!clippedPath) {
|
|
45714
|
+
memo.push(clippedPath = []);
|
|
45715
|
+
}
|
|
45716
|
+
clippedPath.push(arcId);
|
|
45717
|
+
} else {
|
|
45718
|
+
clippedPath = null;
|
|
45719
|
+
}
|
|
45720
|
+
}
|
|
45721
|
+
return memo;
|
|
45722
|
+
}
|
|
45723
|
+
}
|
|
45724
|
+
|
|
45725
|
+
var PolylineClipping = /*#__PURE__*/Object.freeze({
|
|
45726
|
+
__proto__: null,
|
|
45727
|
+
clipPolylines: clipPolylines
|
|
45728
|
+
});
|
|
45729
|
+
|
|
45730
|
+
// TODO: to prevent invalid holes,
|
|
45731
|
+
// could erase the holes from the space-enclosing rings.
|
|
45732
|
+
function appendHolesToRings(cw, ccw) {
|
|
45733
|
+
for (var i=0, n=ccw.length; i<n; i++) {
|
|
45734
|
+
cw.push(ccw[i]);
|
|
45735
|
+
}
|
|
45736
|
+
return cw;
|
|
45737
|
+
}
|
|
45738
|
+
|
|
45739
|
+
function getPolygonDissolver(nodes, spherical) {
|
|
45740
|
+
spherical = spherical && !nodes.arcs.isPlanar();
|
|
45741
|
+
var flags = new Uint8Array(nodes.arcs.size());
|
|
45742
|
+
var divide = getHoleDivider(nodes, spherical);
|
|
45743
|
+
var pathfind = getRingIntersector(nodes, flags);
|
|
45744
|
+
|
|
45745
|
+
return function(shp) {
|
|
45746
|
+
if (!shp) return null;
|
|
45747
|
+
var cw = [],
|
|
45748
|
+
ccw = [];
|
|
45749
|
+
|
|
45750
|
+
divide(shp, cw, ccw);
|
|
45751
|
+
cw = pathfind(cw, 'flatten');
|
|
45752
|
+
ccw.forEach(reversePath);
|
|
45753
|
+
ccw = pathfind(ccw, 'flatten');
|
|
44535
45754
|
ccw.forEach(reversePath);
|
|
44536
45755
|
var shp2 = appendHolesToRings(cw, ccw);
|
|
44537
45756
|
var dissolved = pathfind(shp2, 'dissolve');
|
|
@@ -45382,40 +46601,530 @@ ${svg}
|
|
|
45382
46601
|
};
|
|
45383
46602
|
}
|
|
45384
46603
|
|
|
45385
|
-
var ArcClassifier = /*#__PURE__*/Object.freeze({
|
|
45386
|
-
__proto__: null,
|
|
45387
|
-
getArcClassifier: getArcClassifier
|
|
45388
|
-
});
|
|
45389
|
-
|
|
45390
|
-
function makePolygonBuffer(lyr, dataset, opts) {
|
|
45391
|
-
var spherical = isLatLngCRS(getDatasetCRS(dataset));
|
|
45392
|
-
//
|
|
45393
|
-
//
|
|
45394
|
-
//
|
|
45395
|
-
|
|
45396
|
-
|
|
45397
|
-
|
|
45398
|
-
|
|
45399
|
-
|
|
45400
|
-
|
|
45401
|
-
|
|
45402
|
-
|
|
45403
|
-
|
|
45404
|
-
|
|
45405
|
-
//
|
|
45406
|
-
|
|
45407
|
-
//
|
|
45408
|
-
|
|
45409
|
-
|
|
45410
|
-
|
|
45411
|
-
|
|
45412
|
-
|
|
45413
|
-
|
|
45414
|
-
|
|
45415
|
-
|
|
46604
|
+
var ArcClassifier = /*#__PURE__*/Object.freeze({
|
|
46605
|
+
__proto__: null,
|
|
46606
|
+
getArcClassifier: getArcClassifier
|
|
46607
|
+
});
|
|
46608
|
+
|
|
46609
|
+
function makePolygonBuffer(lyr, dataset, opts) {
|
|
46610
|
+
var spherical = isLatLngCRS(getDatasetCRS(dataset));
|
|
46611
|
+
// debug-mosaic is implemented only for line buffers; for polygons it has no
|
|
46612
|
+
// handling and would leak into the per-shape dissolve and corrupt output, so
|
|
46613
|
+
// drop it and warn rather than silently mislead.
|
|
46614
|
+
if (opts.debug_mosaic) {
|
|
46615
|
+
warn('debug-mosaic is not implemented for polygon buffers; ignoring');
|
|
46616
|
+
opts = Object.assign({}, opts, {debug_mosaic: false});
|
|
46617
|
+
}
|
|
46618
|
+
if (opts.fill_gaps) {
|
|
46619
|
+
// Fill enclosed holes and narrow-mouthed inlets without growing the outer
|
|
46620
|
+
// boundary -- a topology-aware morphological closing (see
|
|
46621
|
+
// makeGapFillPolygonBuffer). Taken ahead of the debug and polar/normal
|
|
46622
|
+
// branches: fill-gaps is inherently topological and builds its medial at the
|
|
46623
|
+
// fill radius R, so it owns the debug views too (it re-enters this function
|
|
46624
|
+
// at R to produce them) and takes precedence over an explicit topological.
|
|
46625
|
+
var fillResult = makeGapFillPolygonBuffer(lyr, dataset, opts);
|
|
46626
|
+
// fill-gaps may itself return a debug dataset (it owns the medial debug
|
|
46627
|
+
// views); never cull those.
|
|
46628
|
+
if (!bufferOutputIsDebug(opts)) {
|
|
46629
|
+
cullSubTolerancePolygonArtifacts(fillResult, lyr, dataset, opts);
|
|
46630
|
+
}
|
|
46631
|
+
return fillResult;
|
|
46632
|
+
}
|
|
46633
|
+
if (opts.debug_delaunay) {
|
|
46634
|
+
// Undocumented: emit the medial-construction triangles (the Delaunay
|
|
46635
|
+
// triangles that bridge two features within buffer reach), whose
|
|
46636
|
+
// circumcenters are the medial vertices -- the contested ribbon where the
|
|
46637
|
+
// medial axis is built, not the full hull triangulation.
|
|
46638
|
+
return makeDelaunayDebugDataset(lyr, dataset, opts);
|
|
46639
|
+
}
|
|
46640
|
+
if (opts.debug_voronoi) {
|
|
46641
|
+
// Undocumented: emit the inter-feature medial-axis (Voronoi) cut-lines that
|
|
46642
|
+
// the topological pipeline injects to partition contested space, before they
|
|
46643
|
+
// are cut into the mosaic and dissolved. Useful for inspecting the medial
|
|
46644
|
+
// construction (sampling density, centerline tracking, simplification).
|
|
46645
|
+
return makeVoronoiDebugDataset(lyr, dataset, opts);
|
|
46646
|
+
}
|
|
46647
|
+
if (opts.debug_offset) {
|
|
46648
|
+
// Raw offset rings, undissolved (as with the line debug-offset): show the
|
|
46649
|
+
// construction's offset loops before the winding/boundary dissolve. Honors
|
|
46650
|
+
// no-loop-removal (loop removal runs inside the maker) and band-method, and
|
|
46651
|
+
// is taken ahead of the polar branch so it shows the raw clamped offsets
|
|
46652
|
+
// rather than the clipped/dissolved polar result.
|
|
46653
|
+
var debugDataset = importGeoJSON(
|
|
46654
|
+
makePolygonDebugOffsetGeoJSON(lyr, dataset, opts), {type: 'polygon'});
|
|
46655
|
+
if (spherical && debugDataset.arcs) {
|
|
46656
|
+
splitAntimeridianBufferDataset(debugDataset);
|
|
46657
|
+
}
|
|
46658
|
+
return debugDataset;
|
|
46659
|
+
}
|
|
46660
|
+
if (spherical && opts.polar) {
|
|
46661
|
+
// Pole/antimeridian-sliced polygons (grow only): keep the seam edges at the
|
|
46662
|
+
// extent and clip to the world rectangle instead of wrapping at the
|
|
46663
|
+
// antimeridian (see makePolarPolygonBuffer).
|
|
46664
|
+
var polar = makePolarPolygonBuffer(lyr, dataset, opts);
|
|
46665
|
+
cullSubTolerancePolygonArtifacts(polar, lyr, dataset, opts);
|
|
46666
|
+
return polar;
|
|
46667
|
+
}
|
|
46668
|
+
var output = buildPolygonBufferOutput(lyr, dataset, opts);
|
|
46669
|
+
var dataset2 = importGeoJSON(output.geojson, {type: 'polygon'});
|
|
46670
|
+
if (spherical) {
|
|
46671
|
+
splitAntimeridianBufferDataset(dataset2);
|
|
46672
|
+
if (output.dissolveAfterSplit) {
|
|
46673
|
+
dissolveBufferDataset2(dataset2, opts);
|
|
46674
|
+
}
|
|
46675
|
+
}
|
|
46676
|
+
cullSubTolerancePolygonArtifacts(dataset2, lyr, dataset, opts);
|
|
46677
|
+
return dataset2;
|
|
46678
|
+
}
|
|
46679
|
+
|
|
46680
|
+
// True when the buffer is producing a debug view (raw offsets or medial
|
|
46681
|
+
// construction) rather than a real buffer; those must skip the artifact cull.
|
|
46682
|
+
function bufferOutputIsDebug(opts) {
|
|
46683
|
+
return !!(opts.debug_delaunay || opts.debug_voronoi || opts.debug_offset ||
|
|
46684
|
+
opts.debug_mosaic);
|
|
46685
|
+
}
|
|
46686
|
+
|
|
46687
|
+
// Sub-tolerance artifact cull. The discrete medial sampling and the fill-gaps
|
|
46688
|
+
// mask boolean leave a scatter of degenerate, near-zero-area positive sliver
|
|
46689
|
+
// parts (most egregiously in fill-gaps: e.g. ~160 spurious parts on six
|
|
46690
|
+
// counties, most of them literally zero area). A part smaller than a tol x tol
|
|
46691
|
+
// square -- the buffer's own positional accuracy -- is below the noise floor and
|
|
46692
|
+
// is dropped. The smallest legitimate buffer part (the grow of a point-like
|
|
46693
|
+
// feature, ~pi*d^2) is ~4 orders of magnitude larger, so the threshold never
|
|
46694
|
+
// touches real geometry, and a genuinely thin-but-long fill (area =
|
|
46695
|
+
// tol*length >> tol^2) is kept. Holes are intentionally left alone: a small hole
|
|
46696
|
+
// can be a neighbor's preserved territory (see removePositiveBufferArtifactHoles,
|
|
46697
|
+
// which is territory-aware) and must not be filled here. Disabled when tolerance
|
|
46698
|
+
// is turned off (tolerance=0), matching the medial-simplify contract.
|
|
46699
|
+
function cullSubTolerancePolygonArtifacts(outDataset, srcLyr, srcDataset, opts) {
|
|
46700
|
+
if (opts.tolerance === 0 || opts.tolerance == '0' || opts.tolerance == '0%') return;
|
|
46701
|
+
if (!outDataset || !outDataset.arcs) return;
|
|
46702
|
+
var lyr = outDataset.layers.filter(function(l) {
|
|
46703
|
+
return l.geometry_type == 'polygon';
|
|
46704
|
+
})[0];
|
|
46705
|
+
if (!lyr || !lyr.shapes) return;
|
|
46706
|
+
var arcs = outDataset.arcs;
|
|
46707
|
+
var distanceFn = getBufferDistanceFunction(srcLyr, srcDataset, opts);
|
|
46708
|
+
var tolFn = getBufferToleranceFunction(srcDataset, opts);
|
|
46709
|
+
var crsArcs = srcDataset.arcs || arcs; // CRS-bearing arcs for the unit conversion
|
|
46710
|
+
lyr.shapes = lyr.shapes.map(function(shp, i) {
|
|
46711
|
+
if (!shp || shp.length === 0) return shp;
|
|
46712
|
+
var dist = distanceFn(i);
|
|
46713
|
+
if (!(dist > 0)) return shp;
|
|
46714
|
+
var tolCoord = getCoordinateDistance(tolFn(dist), crsArcs);
|
|
46715
|
+
var minArea = tolCoord * tolCoord;
|
|
46716
|
+
if (!(minArea > 0)) return shp;
|
|
46717
|
+
var kept = shp.filter(function(ring) {
|
|
46718
|
+
var area = getPlanarPathArea(ring, arcs);
|
|
46719
|
+
return !(area > 0 && area < minArea); // drop sub-tolerance positive parts
|
|
46720
|
+
});
|
|
46721
|
+
return kept.length > 0 ? kept : null;
|
|
46722
|
+
});
|
|
46723
|
+
}
|
|
46724
|
+
|
|
46725
|
+
// Build the per-shape GeoJSON offset output for a polygon buffer, choosing the
|
|
46726
|
+
// construction:
|
|
46727
|
+
// - The clean-outline construction is the default for ordinary polygon grow
|
|
46728
|
+
// (outer rings offset to a single self-contained loop; far fewer rings and
|
|
46729
|
+
// self-intersections into the winding dissolve than the band ribbon).
|
|
46730
|
+
// - The topological pipeline (which pre-dissolves per feature into a shared
|
|
46731
|
+
// mosaic) and the band-method escape hatch keep the band-ribbon construction.
|
|
46732
|
+
// - Negative buffers and hole shrink fall back to the band erode inside the
|
|
46733
|
+
// outline path itself.
|
|
46734
|
+
// Shared by makePolygonBuffer and makePolarPolygonBuffer so the polar option is
|
|
46735
|
+
// a true no-op on non-polar shapes (same construction as the plain buffer).
|
|
46736
|
+
function buildPolygonBufferOutput(lyr, dataset, opts) {
|
|
46737
|
+
var useOutline = !opts.band_method && !opts.topological;
|
|
46738
|
+
return useOutline ?
|
|
46739
|
+
makeOutlinePolygonBufferGeoJSON(lyr, dataset, opts) :
|
|
46740
|
+
makePolygonBufferGeoJSON(lyr, dataset, opts);
|
|
46741
|
+
}
|
|
46742
|
+
|
|
46743
|
+
// Short unit suffixes for re-emitting a scaled distance string (see
|
|
46744
|
+
// fillGapsRadiusStr); parseMeasure2 normalizes input units to these canonical
|
|
46745
|
+
// names. A null (unitless) value is re-emitted bare and interpreted like an
|
|
46746
|
+
// unsuffixed radius (meters for lat-long, CRS units for projected).
|
|
46747
|
+
var GAP_FILL_UNIT_SUFFIX = {
|
|
46748
|
+
meters: 'm',
|
|
46749
|
+
kilometers: 'km',
|
|
46750
|
+
feet: 'ft',
|
|
46751
|
+
miles: 'mi'
|
|
46752
|
+
};
|
|
46753
|
+
|
|
46754
|
+
// Default max-widening factor: an interior gap is kept open only if it is at
|
|
46755
|
+
// least this many times wider than the mouth size; narrower gaps are filled. Set
|
|
46756
|
+
// well above 1 so the closing does not leave a string of small holes along a
|
|
46757
|
+
// channel whose width fluctuates around the mouth size (e.g. the Columbia).
|
|
46758
|
+
var GAP_MAX_WIDENING_DEFAULT = 5;
|
|
46759
|
+
|
|
46760
|
+
// The mouth size for the gap fill is the buffer distance (the standard radius
|
|
46761
|
+
// option), which must be a positive constant.
|
|
46762
|
+
function parseFillGaps(opts) {
|
|
46763
|
+
var parsed = parseMeasure2(opts.radius);
|
|
46764
|
+
if (!(parsed.value > 0)) {
|
|
46765
|
+
stop$1('The fill-gaps option requires a positive buffer distance');
|
|
46766
|
+
}
|
|
46767
|
+
return parsed;
|
|
46768
|
+
}
|
|
46769
|
+
|
|
46770
|
+
// Build a radius string of (mouthSize/2 * factor) preserving the input units, so
|
|
46771
|
+
// the sub-buffers' own unit/CRS conversion applies (matching how -buffer radius
|
|
46772
|
+
// is parsed). factor 1 gives the mouth radius r = mouthSize/2 (two banks of a
|
|
46773
|
+
// channel narrower than the mouth size, 2r, meet under a grow of r); factor k
|
|
46774
|
+
// (the max-widening multiple) gives the larger fill radius R = k*mouthSize/2.
|
|
46775
|
+
function fillGapsRadiusStr(parsed, factor) {
|
|
46776
|
+
return String(parsed.value * factor / 2) +
|
|
46777
|
+
(GAP_FILL_UNIT_SUFFIX[parsed.units] || '');
|
|
46778
|
+
}
|
|
46779
|
+
|
|
46780
|
+
// max-widening: keep an interior gap open only if it is wider than this multiple
|
|
46781
|
+
// of the fill-gaps mouth size (default GAP_MAX_WIDENING_DEFAULT). Must be >= 1
|
|
46782
|
+
// (the threshold cannot be narrower than the mouth itself).
|
|
46783
|
+
function getGapMaxWideningFactor(opts) {
|
|
46784
|
+
if (opts.max_widening == null) return GAP_MAX_WIDENING_DEFAULT;
|
|
46785
|
+
var k = Number(opts.max_widening);
|
|
46786
|
+
if (!(k >= 1)) {
|
|
46787
|
+
stop$1('The max-widening option must be a number >= 1');
|
|
46788
|
+
}
|
|
46789
|
+
return k;
|
|
46790
|
+
}
|
|
46791
|
+
|
|
46792
|
+
// Fill enclosed holes and narrow-mouthed inlets of a polygon mosaic (e.g. a
|
|
46793
|
+
// river up to its mouth) without growing the outer boundary. This is a
|
|
46794
|
+
// topology-aware morphological closing of the mosaic with two thresholds:
|
|
46795
|
+
// - the mouth radius r = mouthSize/2 gates which gaps are sealed off from the
|
|
46796
|
+
// open coast (only openings narrower than the mouth size are closed);
|
|
46797
|
+
// - the fill radius R = k*mouthSize/2 (k = max-widening factor) controls how
|
|
46798
|
+
// far into a sealed inlet the fill reaches and how wide an interior gap must
|
|
46799
|
+
// be to stay open.
|
|
46800
|
+
// Steps:
|
|
46801
|
+
// - the topological buffer grows every feature by R and partitions the
|
|
46802
|
+
// contested space by nearest source (the per-feature dilation, T_R); this
|
|
46803
|
+
// covers every interior gap narrower than k*mouthSize and splits it down the
|
|
46804
|
+
// medial axis;
|
|
46805
|
+
// - the mask is the closing of the land by the mouth radius r: dilate the land
|
|
46806
|
+
// by r, union it (D_r), erode by r. The mask is the original land plus any
|
|
46807
|
+
// gap narrower than the mouth size, with the outward collar pulled back to
|
|
46808
|
+
// the source outline; its interior holes are the gaps wider than the mouth
|
|
46809
|
+
// that lie behind a narrow mouth (rivers' wide reaches, enclosed lakes);
|
|
46810
|
+
// - holes of the mask narrower than k*mouthSize are filled (their max
|
|
46811
|
+
// inscribed circle is smaller than R), so only genuinely large open bodies
|
|
46812
|
+
// (e.g. the Great Lakes) stay open;
|
|
46813
|
+
// - clipping T_R to the filled mask drops the R-wide collar and the kept-open
|
|
46814
|
+
// bodies but keeps each feature's original area plus its medial share of the
|
|
46815
|
+
// narrow-gap fill.
|
|
46816
|
+
// Feature order is preserved so source attributes stay aligned.
|
|
46817
|
+
function makeGapFillPolygonBuffer(lyr, dataset, opts) {
|
|
46818
|
+
var parsed = parseFillGaps(opts);
|
|
46819
|
+
var k = getGapMaxWideningFactor(opts);
|
|
46820
|
+
var mouthRadius = fillGapsRadiusStr(parsed, 1); // r = mouthSize/2
|
|
46821
|
+
var fillRadius = fillGapsRadiusStr(parsed, k); // R = k*mouthSize/2
|
|
46822
|
+
var baseOpts = Object.assign({}, opts, {fill_gaps: false, max_widening: null});
|
|
46823
|
+
// fill-gaps is inherently topological and builds its partition at the larger
|
|
46824
|
+
// fill radius R, so every sub-buffer overrides the radius (and turns on the
|
|
46825
|
+
// topological pipeline for the fill dilation) regardless of the flags the user
|
|
46826
|
+
// passed -- topological need not be given explicitly.
|
|
46827
|
+
var fillOpts = Object.assign({}, baseOpts, {radius: fillRadius, topological: true});
|
|
46828
|
+
// Fill + medial partition at R so interior pockets up to k*mouthSize wide are
|
|
46829
|
+
// covered and split by nearest source.
|
|
46830
|
+
var dilated = makePolygonBuffer(lyr, dataset, fillOpts);
|
|
46831
|
+
// The medial debug views (debug-delaunay/-voronoi) and debug-offset short-
|
|
46832
|
+
// circuit makePolygonBuffer and return their construction directly; since
|
|
46833
|
+
// fill-gaps builds at R, that early return IS the debug output we want, so
|
|
46834
|
+
// pass it straight through instead of trying to mask/clip a debug dataset.
|
|
46835
|
+
if (opts.debug_delaunay || opts.debug_voronoi || opts.debug_offset) {
|
|
46836
|
+
return dilated;
|
|
46837
|
+
}
|
|
46838
|
+
var dilatedLyr = dilated.layers[0];
|
|
46839
|
+
if (!dilatedLyr || !dilated.arcs) return dilated;
|
|
46840
|
+
// Mouth-gating mask: closing of the land by the mouth radius r (dilate by r,
|
|
46841
|
+
// union, erode by r). Built separately from the fill dilation because the
|
|
46842
|
+
// mouth threshold stays at the mouth size while the fill reaches further.
|
|
46843
|
+
var dilatedAtR = makePolygonBuffer(lyr, dataset,
|
|
46844
|
+
Object.assign({}, baseOpts, {radius: mouthRadius, topological: false}));
|
|
46845
|
+
var union = unionBufferDataset(dilatedAtR, baseOpts);
|
|
46846
|
+
if (!union || !union.arcs) return dilated;
|
|
46847
|
+
var closing = makePolygonBuffer(union.layers[0], union,
|
|
46848
|
+
Object.assign({}, baseOpts, {radius: '-' + mouthRadius, topological: false}));
|
|
46849
|
+
if (!closing || !closing.arcs) return dilated;
|
|
46850
|
+
// Fill the mask's interior gaps that are narrower than k*mouthSize (keep wider
|
|
46851
|
+
// ones open) so the clip below fills them too.
|
|
46852
|
+
var keepRadius = getCoordinateDistance(
|
|
46853
|
+
parseConstantBufferDistance(fillRadius, getDatasetCRS(dataset)) || 0,
|
|
46854
|
+
closing.arcs);
|
|
46855
|
+
fillNarrowMaskHoles(closing, keepRadius);
|
|
46856
|
+
// Carry the source attributes through the clip: clipping drops fully-empty
|
|
46857
|
+
// features, so attach a per-feature data table (aligned 1:1 with the dilation)
|
|
46858
|
+
// before clipping rather than relying on a post-hoc count-match copy.
|
|
46859
|
+
if (lyr.data && dilatedLyr.shapes &&
|
|
46860
|
+
dilatedLyr.shapes.length == lyr.data.size()) {
|
|
46861
|
+
dilatedLyr.data = lyr.data.clone();
|
|
46862
|
+
}
|
|
46863
|
+
// remove_slivers: the dilation-vs-mask clip carves thin slivers along its
|
|
46864
|
+
// boundary (a feature can pick up dozens of degenerate edge slivers); clip's
|
|
46865
|
+
// own sliver filter drops the rings that mix dilation and mask arcs and fail a
|
|
46866
|
+
// compactness-weighted area test, leaving the substantial fill regions intact.
|
|
46867
|
+
clipLayersInPlace(dilated.layers, closing, dilated, 'clip',
|
|
46868
|
+
{no_cleanup: true, no_warn: true, remove_slivers: true});
|
|
46869
|
+
return dilated;
|
|
46870
|
+
}
|
|
46871
|
+
|
|
46872
|
+
// Remove interior rings (holes) of the closing mask whose largest inscribed
|
|
46873
|
+
// circle has radius smaller than keepRadius, i.e. gaps narrower than 2*keepRadius
|
|
46874
|
+
// (= k*mouthSize). Dropping the hole ring makes the mask solid there, so the
|
|
46875
|
+
// clip fills the gap; wide gaps (large lakes) keep their ring and stay open. The
|
|
46876
|
+
// inscribed radius is estimated from the gap's pole of inaccessibility (the
|
|
46877
|
+
// anchor point), the same way label points are placed. Modifies the mask in
|
|
46878
|
+
// place; the clip rebuilds topology afterward, so dropped rings need no cleanup.
|
|
46879
|
+
function fillNarrowMaskHoles(maskDataset, keepRadius) {
|
|
46880
|
+
var arcs = maskDataset.arcs;
|
|
46881
|
+
var lyr = maskDataset.layers[0];
|
|
46882
|
+
if (!lyr || !arcs || !(keepRadius > 0)) return;
|
|
46883
|
+
lyr.shapes = (lyr.shapes || []).map(function(shape) {
|
|
46884
|
+
if (!shape) return shape;
|
|
46885
|
+
var kept = [];
|
|
46886
|
+
exportPathData(shape, arcs, 'polygon').pathData.forEach(function(path) {
|
|
46887
|
+
if (path.area >= 0) {
|
|
46888
|
+
kept.push(path.ids.concat()); // outer ring: always keep
|
|
46889
|
+
return;
|
|
46890
|
+
}
|
|
46891
|
+
// hole: keep only if wide enough to hold a disk of radius keepRadius
|
|
46892
|
+
var holeShape = [reversePath(path.ids.concat())];
|
|
46893
|
+
var anchor = findAnchorPoint(holeShape, arcs);
|
|
46894
|
+
var radius = anchor ?
|
|
46895
|
+
getPointToShapeDistance(anchor.x, anchor.y, holeShape, arcs) : 0;
|
|
46896
|
+
if (radius >= keepRadius) {
|
|
46897
|
+
kept.push(path.ids.concat());
|
|
46898
|
+
}
|
|
46899
|
+
});
|
|
46900
|
+
return kept.length > 0 ? kept : null;
|
|
46901
|
+
});
|
|
46902
|
+
}
|
|
46903
|
+
|
|
46904
|
+
// Dissolve every feature of a buffered dataset into a single union polygon (the
|
|
46905
|
+
// morphological dilation). The per-feature dilation tiles the union exactly, so
|
|
46906
|
+
// merging all rings into one shape and boundary-dissolving yields the union
|
|
46907
|
+
// outline (with any wide enclosed holes preserved). Returns null if empty.
|
|
46908
|
+
function unionBufferDataset(dataset, opts) {
|
|
46909
|
+
var lyr = dataset.layers[0];
|
|
46910
|
+
if (!lyr || !dataset.arcs) return null;
|
|
46911
|
+
var coords = [];
|
|
46912
|
+
(lyr.shapes || []).forEach(function(shp) {
|
|
46913
|
+
if (shp) coords = coords.concat(getPolygonMultiPolygonCoords(shp, dataset.arcs));
|
|
46914
|
+
});
|
|
46915
|
+
if (coords.length === 0) return null;
|
|
46916
|
+
var unionDataset = getBufferDataset(coords);
|
|
46917
|
+
if (!unionDataset.arcs) return null;
|
|
46918
|
+
dissolveBufferDataset2(unionDataset, Object.assign({}, opts, {winding_fill: false}));
|
|
46919
|
+
return unionDataset;
|
|
46920
|
+
}
|
|
46921
|
+
|
|
46922
|
+
// Raw (undissolved) offset rings for the polygon buffer's debug-offset view.
|
|
46923
|
+
// Drives the same per-ring makers the real construction uses, so the view shows
|
|
46924
|
+
// exactly what is built and 'no-loop-removal' has a visible effect (loop removal
|
|
46925
|
+
// runs inside the maker -- see buildOneSidedRings). Positive grow uses the
|
|
46926
|
+
// clean-outline maker by default (the band maker under band-method/topological);
|
|
46927
|
+
// holes are eroded with the band maker reversed to outer orientation (matching
|
|
46928
|
+
// makeOutlineBufferGeometry); negative (erode) buffers offset every ring inward
|
|
46929
|
+
// with the band maker.
|
|
46930
|
+
function makePolygonDebugOffsetGeoJSON(lyr, dataset, opts) {
|
|
46931
|
+
var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
|
|
46932
|
+
var useOutline = !opts.band_method && !opts.topological;
|
|
46933
|
+
var leftOpts = useOutline ? Object.assign({}, opts, {outline: true}) : opts;
|
|
46934
|
+
var leftMaker = getPolygonRingBufferMaker(dataset, leftOpts, 'left');
|
|
46935
|
+
var rightMaker = getPolygonRingBufferMaker(dataset,
|
|
46936
|
+
Object.assign({}, opts, {outline: false}), 'right');
|
|
46937
|
+
var geometries = lyr.shapes.map(function(shape, i) {
|
|
46938
|
+
var distance = distanceFn(i);
|
|
46939
|
+
if (!distance || !shape) return null;
|
|
46940
|
+
var coords;
|
|
46941
|
+
if (distance > 0) {
|
|
46942
|
+
var rings = splitShapeRingsByArea(shape, dataset.arcs);
|
|
46943
|
+
coords = getBufferMultiPolygonCoords(rings.outer, distance, leftMaker);
|
|
46944
|
+
if (rings.holes.length > 0) {
|
|
46945
|
+
coords = coords.concat(getBufferMultiPolygonCoords(
|
|
46946
|
+
rings.holes.map(reversePath), distance, rightMaker));
|
|
46947
|
+
}
|
|
46948
|
+
} else {
|
|
46949
|
+
coords = getBufferMultiPolygonCoords(shape, -distance, rightMaker);
|
|
46950
|
+
}
|
|
46951
|
+
return coords.length > 0 ? {type: 'MultiPolygon', coordinates: coords} : null;
|
|
46952
|
+
});
|
|
46953
|
+
return {type: 'GeometryCollection', geometries: geometries};
|
|
46954
|
+
}
|
|
46955
|
+
|
|
46956
|
+
// Clean-outline polygon buffer (the default polygon-grow construction): offset
|
|
46957
|
+
// each source ring to a single self-contained closed loop (no source-path band
|
|
46958
|
+
// edge), strip self-overlaps with the crossing-direction loop remover (safe
|
|
46959
|
+
// because a single offset loop has a consistent +/-1 base winding), then union
|
|
46960
|
+
// the loops by winding number. The loops carry far fewer rings and self-
|
|
46961
|
+
// intersections into the dissolve than the band-ribbon construction, and the
|
|
46962
|
+
// direction remover collapses more overshoot loops than the source-turn gate.
|
|
46963
|
+
//
|
|
46964
|
+
// Used for the positive (grow) buffer. The outer source rings are offset
|
|
46965
|
+
// outward with the fast clean-outline construction (a single self-contained loop
|
|
46966
|
+
// per ring, crossing-direction loop removal, winding union) -- this is where the
|
|
46967
|
+
// method pays off, since a large coastline's outer boundary dominates the
|
|
46968
|
+
// dissolve cost. Holes shrink, which is an INWARD offset; an inward outline
|
|
46969
|
+
// offset is fragile (its elbow insets self-cross on concave holes and invert on
|
|
46970
|
+
// over-shrink, and mapshaper's winding flood can't tell a collapsed contour from
|
|
46971
|
+
// a valid one because it normalizes orientation), so holes are shrunk with the
|
|
46972
|
+
// proven band-ribbon erode and then carved out of the grown outer (see
|
|
46973
|
+
// makeOutlineBufferGeometry). Negative (erode) buffers fall back to the band
|
|
46974
|
+
// construction entirely.
|
|
46975
|
+
function makeOutlinePolygonBufferGeoJSON(lyr, dataset, opts) {
|
|
46976
|
+
var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
|
|
46977
|
+
var hasPositiveDistance = false;
|
|
46978
|
+
var hasNegativeDistance = false;
|
|
46979
|
+
// Force the clean-outline construction on the outer maker (this is the
|
|
46980
|
+
// default polygon-grow path, so it must not depend on a command-line flag).
|
|
46981
|
+
var outlineOpts = Object.assign({}, opts, {outline: true});
|
|
46982
|
+
var outerMaker = getPolygonRingBufferMaker(dataset, outlineOpts, 'left');
|
|
46983
|
+
// Band maker for negative buffers and for shrinking holes.
|
|
46984
|
+
var bandOpts = Object.assign({}, opts, {outline: false});
|
|
46985
|
+
var bandRightMaker = getPolygonRingBufferMaker(dataset, bandOpts, 'right');
|
|
46986
|
+
// Left winding-fill maker for the nested-fill fallback (see below).
|
|
46987
|
+
var bandLeftMaker = getPolygonRingBufferMaker(dataset, bandOpts, 'left');
|
|
46988
|
+
var holeEroder = function(holeShape, dist) {
|
|
46989
|
+
return makeNegativePolygonBufferGeometry(holeShape, dist, dataset, bandOpts,
|
|
46990
|
+
bandRightMaker);
|
|
46991
|
+
};
|
|
46992
|
+
var geometries = lyr.shapes.map(function(shape, i) {
|
|
46993
|
+
var distance = distanceFn(i);
|
|
46994
|
+
if (!distance || !shape) return null;
|
|
46995
|
+
if (distance < 0) {
|
|
46996
|
+
hasNegativeDistance = true;
|
|
46997
|
+
return makeNegativePolygonBufferGeometry(shape, -distance, dataset,
|
|
46998
|
+
bandOpts, bandRightMaker);
|
|
46999
|
+
}
|
|
47000
|
+
hasPositiveDistance = true;
|
|
47001
|
+
var geom;
|
|
47002
|
+
if (shapeHasFillInsideHole(shape, dataset.arcs)) {
|
|
47003
|
+
// The clean-outline construction grows ALL of a shape's outer rings into
|
|
47004
|
+
// one solid fill before carving holes (makeOutlineBufferGeometry), which
|
|
47005
|
+
// absorbs a fill nested inside a hole: the nested fill's offset loop just
|
|
47006
|
+
// adds winding to the surrounding solid, and the later hole carve removes
|
|
47007
|
+
// the region where it lived. The winding-fill construction instead groups
|
|
47008
|
+
// rings by containment and buffers each group independently, so the nested
|
|
47009
|
+
// fill is grown on its own and survives. It is slower, so use it only for
|
|
47010
|
+
// the rare shapes that actually nest a fill inside a hole.
|
|
47011
|
+
geom = makePositivePolygonBufferGeometry(shape, distance, dataset, opts,
|
|
47012
|
+
bandLeftMaker);
|
|
47013
|
+
} else {
|
|
47014
|
+
geom = makeOutlineBufferGeometry(shape, dataset.arcs, distance, opts,
|
|
47015
|
+
outerMaker, holeEroder);
|
|
47016
|
+
}
|
|
47017
|
+
if (opts.polar && shapeReachesPole(shape, dataset.arcs)) {
|
|
47018
|
+
// A pole-touching ring collapses onto the pole line during Mercator
|
|
47019
|
+
// offset construction, so the outline keeps only the coastline; append
|
|
47020
|
+
// the source rings and let the dataset-level dissolve union them into the
|
|
47021
|
+
// full grown shape (same handling as the band construction).
|
|
47022
|
+
geom = appendSourceRings(geom, shape, dataset.arcs);
|
|
47023
|
+
}
|
|
47024
|
+
return geom;
|
|
47025
|
+
});
|
|
47026
|
+
return {
|
|
47027
|
+
geojson: {
|
|
47028
|
+
type: 'GeometryCollection',
|
|
47029
|
+
geometries: geometries
|
|
47030
|
+
},
|
|
47031
|
+
dissolveAfterSplit: hasPositiveDistance && !hasNegativeDistance
|
|
47032
|
+
};
|
|
47033
|
+
}
|
|
47034
|
+
|
|
47035
|
+
// True if the shape has a fill (positive-area ring) nested directly inside a
|
|
47036
|
+
// hole (negative-area ring) -- e.g. an island sitting in a lake. The clean-
|
|
47037
|
+
// outline grow can't represent such a fill (it grows all outer rings into one
|
|
47038
|
+
// solid before carving holes, absorbing the nested fill), so callers route
|
|
47039
|
+
// these shapes to the winding-fill construction instead.
|
|
47040
|
+
function shapeHasFillInsideHole(shape, arcs) {
|
|
47041
|
+
var paths = exportPathData(shape, arcs, 'polygon').pathData;
|
|
47042
|
+
var fills = 0, holes = 0;
|
|
47043
|
+
paths.forEach(function(p) {
|
|
47044
|
+
if (p.area > 0) fills++;
|
|
47045
|
+
else if (p.area < 0) holes++;
|
|
47046
|
+
});
|
|
47047
|
+
// Nesting a fill inside a hole needs at least two fills (a container and the
|
|
47048
|
+
// nested one) and at least one hole; bail cheaply otherwise (the common case)
|
|
47049
|
+
// before building the spatial index.
|
|
47050
|
+
if (fills < 2 || holes < 1) return false;
|
|
47051
|
+
var ringShapes = paths.map(function(p) { return [p.ids]; });
|
|
47052
|
+
var index = new PathIndex(ringShapes, arcs);
|
|
47053
|
+
return paths.some(function(p) {
|
|
47054
|
+
if (p.area <= 0) return false; // only fills can be nested inside a hole
|
|
47055
|
+
var containerId = index.findSmallestEnclosingPolygon(p.ids);
|
|
47056
|
+
return containerId > -1 && paths[containerId].area < 0;
|
|
47057
|
+
});
|
|
47058
|
+
}
|
|
47059
|
+
|
|
47060
|
+
// Split a shape's rings into outer rings (positive signed area) and hole rings
|
|
47061
|
+
// (negative signed area), keeping each ring's arc ids.
|
|
47062
|
+
function splitShapeRingsByArea(shape, arcs) {
|
|
47063
|
+
var outer = [];
|
|
47064
|
+
var holes = [];
|
|
47065
|
+
exportPathData(shape, arcs, 'polygon').pathData.forEach(function(path) {
|
|
47066
|
+
(path.area < 0 ? holes : outer).push(path.ids.concat());
|
|
47067
|
+
});
|
|
47068
|
+
return {outer: outer, holes: holes};
|
|
47069
|
+
}
|
|
47070
|
+
|
|
47071
|
+
function makeOutlineBufferGeometry(shape, arcs, distance, opts, outerMaker,
|
|
47072
|
+
holeEroder) {
|
|
47073
|
+
var rings = splitShapeRingsByArea(shape, arcs);
|
|
47074
|
+
var outerLoops = rings.outer.length > 0 ?
|
|
47075
|
+
getBufferMultiPolygonCoords(rings.outer, distance, outerMaker) : [];
|
|
47076
|
+
if (outerLoops.length === 0) return null;
|
|
47077
|
+
// Resolve the outer offset loops' self-overlaps into clean grown polygons.
|
|
47078
|
+
var coords = dissolveOffsetRingsToCoords(outerLoops, opts);
|
|
47079
|
+
if (coords.length === 0) return null;
|
|
47080
|
+
// Strip artifact holes left by the outer grow (the offset loops dissolve into
|
|
47081
|
+
// a clean fill, so any interior ring is a self-overlap artifact) BEFORE
|
|
47082
|
+
// carving the real holes. The artifact filter's heuristic can otherwise
|
|
47083
|
+
// delete a legitimately-shrunk hole whose eroded boundary happens to run near
|
|
47084
|
+
// the source outline; the carved holes below are explicit and known-good, so
|
|
47085
|
+
// they must not pass through it.
|
|
47086
|
+
var grown = removePositiveBufferArtifactHoles(
|
|
47087
|
+
{type: 'MultiPolygon', coordinates: coords}, shape, arcs, distance);
|
|
47088
|
+
coords = grown ? grown.coordinates : [];
|
|
47089
|
+
if (coords.length === 0) return null;
|
|
47090
|
+
if (rings.holes.length > 0) {
|
|
47091
|
+
// Shrink the holes (an inward offset) with the band erode: treat each hole
|
|
47092
|
+
// as a polygon to erode by reversing it to outer (CCW) orientation, then
|
|
47093
|
+
// carve the eroded regions out of the grown outer.
|
|
47094
|
+
var holeShape = rings.holes.map(reversePath);
|
|
47095
|
+
var holeGeom = holeEroder(holeShape, distance);
|
|
47096
|
+
if (holeGeom && holeGeom.coordinates.length > 0) {
|
|
47097
|
+
coords = subtractHolesFromOuter(coords, holeGeom.coordinates);
|
|
45416
47098
|
}
|
|
45417
47099
|
}
|
|
45418
|
-
return
|
|
47100
|
+
if (coords.length === 0) return null;
|
|
47101
|
+
return {type: 'MultiPolygon', coordinates: coords};
|
|
47102
|
+
}
|
|
47103
|
+
|
|
47104
|
+
// Carve clean shrunk-hole regions out of clean grown-outer polygons. Both arrive
|
|
47105
|
+
// as positive (CCW) rings. The winding union can't subtract one nested loop from
|
|
47106
|
+
// another -- GeoJSON import rewinds every outer ring to CCW, so two separately
|
|
47107
|
+
// imported nested loops both read as fill -- so instead we make each hole a
|
|
47108
|
+
// negative-area inner ring of the same shape: reverse the hole rings at the arc
|
|
47109
|
+
// level (reversing GeoJSON coordinates wouldn't survive import's rewind) and let
|
|
47110
|
+
// groupPolygonRings (which classifies a ring as a hole by negative area) nest
|
|
47111
|
+
// each hole into its containing outer. The outer and hole rings are disjoint
|
|
47112
|
+
// (holes lie strictly inside outers), so no intersection cuts are needed.
|
|
47113
|
+
function subtractHolesFromOuter(outerCoords, holeCoords) {
|
|
47114
|
+
if (!holeCoords || holeCoords.length === 0) return outerCoords;
|
|
47115
|
+
var dataset = importGeoJSON({
|
|
47116
|
+
type: 'GeometryCollection',
|
|
47117
|
+
geometries: [
|
|
47118
|
+
{type: 'MultiPolygon', coordinates: outerCoords},
|
|
47119
|
+
{type: 'MultiPolygon', coordinates: holeCoords}
|
|
47120
|
+
]
|
|
47121
|
+
}, {type: 'polygon'});
|
|
47122
|
+
if (!dataset.arcs) return outerCoords;
|
|
47123
|
+
var shapes = dataset.layers[0].shapes;
|
|
47124
|
+
var outerShape = shapes[0] || [];
|
|
47125
|
+
var holeShape = shapes[1] || [];
|
|
47126
|
+
var merged = outerShape.concat(holeShape.map(reversePath));
|
|
47127
|
+
return getPolygonMultiPolygonCoords(merged, dataset.arcs);
|
|
45419
47128
|
}
|
|
45420
47129
|
|
|
45421
47130
|
// World rectangle (lng/lat) the polar buffer is clipped to.
|
|
@@ -45436,7 +47145,7 @@ ${svg}
|
|
|
45436
47145
|
if (polarBufferHasNegativeDistance(lyr, dataset, opts)) {
|
|
45437
47146
|
stop$1('The polar option does not support negative (erode) buffers yet.');
|
|
45438
47147
|
}
|
|
45439
|
-
var output =
|
|
47148
|
+
var output = buildPolygonBufferOutput(lyr, dataset, opts);
|
|
45440
47149
|
var dataset2 = importGeoJSON(output.geojson, {type: 'polygon'});
|
|
45441
47150
|
if (dataset2.arcs) {
|
|
45442
47151
|
if (output.dissolveAfterSplit) {
|
|
@@ -45536,9 +47245,9 @@ ${svg}
|
|
|
45536
47245
|
}
|
|
45537
47246
|
|
|
45538
47247
|
function getPolygonRingBufferMaker(dataset, opts, side, winding) {
|
|
45539
|
-
// The
|
|
45540
|
-
// for callers that request winding-fill (see the '
|
|
45541
|
-
var useWinding = !opts.
|
|
47248
|
+
// The band-method escape hatch forces the older non-winding construction even
|
|
47249
|
+
// for callers that request winding-fill (see the 'band-method' option).
|
|
47250
|
+
var useWinding = !opts.band_method;
|
|
45542
47251
|
var makerOpts = Object.assign({}, opts, {
|
|
45543
47252
|
left: side == 'left',
|
|
45544
47253
|
right: side == 'right',
|
|
@@ -45581,6 +47290,7 @@ ${svg}
|
|
|
45581
47290
|
tmpGeometries.push(getPolygonGeometry(shape, dataset.arcs));
|
|
45582
47291
|
});
|
|
45583
47292
|
|
|
47293
|
+
profileStart('topo:offsets');
|
|
45584
47294
|
shapes.forEach(function(shape, i) {
|
|
45585
47295
|
var distance = distances[i];
|
|
45586
47296
|
var pathData, bufferCoords;
|
|
@@ -45590,10 +47300,10 @@ ${svg}
|
|
|
45590
47300
|
bufferCoords = getBufferMultiPolygonCoords(pathData.paths, distance, bufferMaker);
|
|
45591
47301
|
// Resolve the winding-fill rings' self-overlaps into a clean polygon (the
|
|
45592
47302
|
// mosaic's boundary-flood membership cannot), so this feature enters the
|
|
45593
|
-
// shared mosaic as an ordinary polygon. The
|
|
47303
|
+
// shared mosaic as an ordinary polygon. The band-method fallback emits
|
|
45594
47304
|
// boundary-flood-resolvable bands, so it feeds the mosaic directly (as the
|
|
45595
47305
|
// topological pipeline did before the winding-fill construction).
|
|
45596
|
-
if (!opts.
|
|
47306
|
+
if (!opts.band_method) {
|
|
45597
47307
|
bufferCoords = dissolveOffsetRingsToCoords(bufferCoords, opts);
|
|
45598
47308
|
}
|
|
45599
47309
|
if (bufferCoords.length > 0) {
|
|
@@ -45604,6 +47314,7 @@ ${svg}
|
|
|
45604
47314
|
});
|
|
45605
47315
|
}
|
|
45606
47316
|
});
|
|
47317
|
+
profileEnd('topo:offsets');
|
|
45607
47318
|
|
|
45608
47319
|
if (!hasPositiveDistance || tmpGeometries.length === 0) {
|
|
45609
47320
|
geometries = shapes.map(function() { return null; });
|
|
@@ -45613,7 +47324,8 @@ ${svg}
|
|
|
45613
47324
|
geometries: tmpGeometries
|
|
45614
47325
|
}, {type: 'polygon'});
|
|
45615
47326
|
geometries = makeTopologicalPolygonBufferGeometries(shapes, distances,
|
|
45616
|
-
sourceIds, bufferIds, tmpDataset, dataset.arcs
|
|
47327
|
+
sourceIds, bufferIds, tmpDataset, dataset.arcs,
|
|
47328
|
+
getMedialSimplifyInterval(dataset, opts, distances));
|
|
45617
47329
|
}
|
|
45618
47330
|
return {
|
|
45619
47331
|
geojson: {
|
|
@@ -45625,39 +47337,168 @@ ${svg}
|
|
|
45625
47337
|
}
|
|
45626
47338
|
|
|
45627
47339
|
function makeTopologicalPolygonBufferGeometries(shapes, distances, sourceIds,
|
|
45628
|
-
bufferIds, tmpDataset, sourceArcs) {
|
|
45629
|
-
|
|
45630
|
-
|
|
47340
|
+
bufferIds, tmpDataset, sourceArcs, medialSimplifyInterval) {
|
|
47341
|
+
// Inject inter-feature Voronoi (medial-axis) cut lines so the buffer mosaic's
|
|
47342
|
+
// contested tiles are subdivided along the equidistant boundary before the
|
|
47343
|
+
// tiles are assigned (see assignment by nearest source below).
|
|
47344
|
+
profileStart('topo:medial');
|
|
47345
|
+
var dataset = injectMedialCutLines(tmpDataset, shapes, distances, sourceArcs,
|
|
47346
|
+
medialSimplifyInterval);
|
|
47347
|
+
profileEnd('topo:medial');
|
|
47348
|
+
var tmpLyr = dataset.layers.filter(function(l) {
|
|
47349
|
+
return l.geometry_type == 'polygon';
|
|
47350
|
+
})[0];
|
|
47351
|
+
profileStart('topo:intersectionCuts');
|
|
47352
|
+
var nodes = addIntersectionCuts(dataset, {rebuild_topology: true});
|
|
47353
|
+
profileEnd('topo:intersectionCuts');
|
|
47354
|
+
profileStart('topo:mosaicIndex');
|
|
45631
47355
|
var mosaicIndex = new MosaicIndex(tmpLyr, nodes, {flat: false, no_holes: false});
|
|
47356
|
+
profileEnd('topo:mosaicIndex');
|
|
45632
47357
|
var pathfind = getRingIntersector(mosaicIndex.nodes);
|
|
45633
47358
|
var sourceIdIndex = getIdLookup(sourceIds);
|
|
45634
47359
|
var bufferIdIndex = getIdToFeatureIdLookup(bufferIds);
|
|
45635
47360
|
var sourceAreas = getSourceShapeAreas(shapes, sourceArcs);
|
|
45636
|
-
|
|
47361
|
+
var sourceInteriorPoints = getSourceInteriorPoints(shapes, sourceArcs);
|
|
47362
|
+
var ownerCtx = createTileOwnerContext(shapes, sourceArcs, sourceAreas,
|
|
47363
|
+
mosaicIndex.nodes.arcs);
|
|
47364
|
+
profileStart('topo:assignTiles');
|
|
47365
|
+
var result = shapes.map(function(shape, i) {
|
|
45637
47366
|
var distance = distances[i];
|
|
45638
47367
|
var tileIds, geom;
|
|
45639
47368
|
if (!distance || !shape) return null;
|
|
45640
47369
|
tileIds = getTopologicalBufferTileIds(sourceIds[i], bufferIds[i],
|
|
45641
|
-
i, mosaicIndex, sourceIdIndex, bufferIdIndex,
|
|
47370
|
+
i, mosaicIndex, sourceIdIndex, bufferIdIndex, ownerCtx);
|
|
45642
47371
|
geom = getTileIdsGeometry(tileIds, mosaicIndex, pathfind);
|
|
45643
|
-
|
|
47372
|
+
// Preserve holes that coincide with another feature's source territory (a
|
|
47373
|
+
// tile excluded to prevent buffer overlap, see getTopologicalBufferTileIds);
|
|
47374
|
+
// the single-shape artifact/sliver heuristics would otherwise fill them.
|
|
47375
|
+
return removePositiveBufferArtifactHoles(geom, shape, sourceArcs, distance,
|
|
47376
|
+
{points: sourceInteriorPoints, featureId: i});
|
|
47377
|
+
});
|
|
47378
|
+
profileEnd('topo:assignTiles');
|
|
47379
|
+
return result;
|
|
47380
|
+
}
|
|
47381
|
+
|
|
47382
|
+
// Build the working dataset for the topological mosaic: the source/buffer
|
|
47383
|
+
// polygons plus a polyline layer of inter-feature Voronoi cut-lines (so the
|
|
47384
|
+
// contested tiles split along the equidistant boundary). Returns @tmpDataset
|
|
47385
|
+
// unchanged when there are no contested edges (no overlap between features).
|
|
47386
|
+
function injectMedialCutLines(tmpDataset, shapes, distances, arcs, simplifyInterval) {
|
|
47387
|
+
var coordDistances = distances.map(function(d) {
|
|
47388
|
+
return d > 0 ? getCoordinateDistance(d, arcs) : 0;
|
|
47389
|
+
});
|
|
47390
|
+
var medial = buildInterFeatureMedialLines(shapes, coordDistances, arcs,
|
|
47391
|
+
{simplifyInterval: simplifyInterval || 0});
|
|
47392
|
+
if (!medial) return tmpDataset;
|
|
47393
|
+
var lineDataset = importGeoJSON({
|
|
47394
|
+
type: 'GeometryCollection',
|
|
47395
|
+
geometries: [medial]
|
|
47396
|
+
}, {});
|
|
47397
|
+
if (!lineDataset.arcs) return tmpDataset;
|
|
47398
|
+
return mergeDatasets([tmpDataset, lineDataset]);
|
|
47399
|
+
}
|
|
47400
|
+
|
|
47401
|
+
// Smoothing scale for the constructed medial lines, as a multiple of the
|
|
47402
|
+
// buffer's own positional tolerance. Weighted Visvalingam removes sub-tolerance
|
|
47403
|
+
// wiggles (the residual zigzag of the discrete medial sampling) and thins the
|
|
47404
|
+
// uneven vertex density without pulling the partition off the centerline:
|
|
47405
|
+
// keeping the interval near the buffer's accuracy budget bounds any deviation to
|
|
47406
|
+
// what the buffer geometry already tolerates.
|
|
47407
|
+
var MEDIAL_SIMPLIFY_FACTOR = 3; // 2
|
|
47408
|
+
|
|
47409
|
+
// Simplification interval (in source-coordinate units) for the medial cut-lines.
|
|
47410
|
+
// Tied to the buffer tolerance at the largest buffer distance present, so the
|
|
47411
|
+
// medial axis is smoothed at the same scale the buffer outline is approximated.
|
|
47412
|
+
// Returns 0 (no simplification) when tolerance is disabled or all distances are
|
|
47413
|
+
// non-positive.
|
|
47414
|
+
function getMedialSimplifyInterval(dataset, opts, distances) {
|
|
47415
|
+
if (opts.tolerance === 0 || opts.tolerance == '0' || opts.tolerance == '0%') {
|
|
47416
|
+
return 0;
|
|
47417
|
+
}
|
|
47418
|
+
var repDist = 0;
|
|
47419
|
+
for (var i = 0; i < distances.length; i++) {
|
|
47420
|
+
if (distances[i] > repDist) repDist = distances[i];
|
|
47421
|
+
}
|
|
47422
|
+
if (repDist <= 0) return 0;
|
|
47423
|
+
var tolMeters = getBufferToleranceFunction(dataset, opts)(repDist);
|
|
47424
|
+
return getCoordinateDistance(tolMeters, dataset.arcs) * MEDIAL_SIMPLIFY_FACTOR;
|
|
47425
|
+
}
|
|
47426
|
+
|
|
47427
|
+
// Per-feature buffer distances for the debug builders below, in meters and in
|
|
47428
|
+
// source-coordinate units (matching what the topological pipeline computes).
|
|
47429
|
+
function getMedialDebugDistances(lyr, dataset, opts) {
|
|
47430
|
+
var shapes = lyr.shapes || [];
|
|
47431
|
+
var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
|
|
47432
|
+
var distances = shapes.map(function(shape, i) {
|
|
47433
|
+
var d = shape ? distanceFn(i) : 0;
|
|
47434
|
+
return d > 0 ? d : 0;
|
|
47435
|
+
});
|
|
47436
|
+
var coordDistances = distances.map(function(d) {
|
|
47437
|
+
return d > 0 ? getCoordinateDistance(d, dataset.arcs) : 0;
|
|
45644
47438
|
});
|
|
47439
|
+
return {shapes: shapes, distances: distances, coordDistances: coordDistances};
|
|
47440
|
+
}
|
|
47441
|
+
|
|
47442
|
+
// Build a polyline dataset of the inter-feature medial-axis (Voronoi) cut-lines
|
|
47443
|
+
// for the -buffer debug-voronoi option (topological only). These are the same
|
|
47444
|
+
// lines injected into the mosaic to partition contested space, after the
|
|
47445
|
+
// post-construction smoothing simplification.
|
|
47446
|
+
function makeVoronoiDebugDataset(lyr, dataset, opts) {
|
|
47447
|
+
if (!opts.topological) {
|
|
47448
|
+
warn('debug-voronoi has no effect without the topological option; ignoring');
|
|
47449
|
+
return importGeoJSON({type: 'GeometryCollection', geometries: []}, {});
|
|
47450
|
+
}
|
|
47451
|
+
var d = getMedialDebugDistances(lyr, dataset, opts);
|
|
47452
|
+
var medial = buildInterFeatureMedialLines(d.shapes, d.coordDistances, dataset.arcs,
|
|
47453
|
+
{simplifyInterval: getMedialSimplifyInterval(dataset, opts, d.distances)});
|
|
47454
|
+
var geometries = medial ? [medial] : [];
|
|
47455
|
+
return importGeoJSON({type: 'GeometryCollection', geometries: geometries}, {});
|
|
47456
|
+
}
|
|
47457
|
+
|
|
47458
|
+
// Build a polygon dataset of the Delaunay triangulation of the adaptive sample
|
|
47459
|
+
// sites for the -buffer debug-delaunay option (topological only) -- the mesh the
|
|
47460
|
+
// medial axis is derived from.
|
|
47461
|
+
function makeDelaunayDebugDataset(lyr, dataset, opts) {
|
|
47462
|
+
if (!opts.topological) {
|
|
47463
|
+
warn('debug-delaunay has no effect without the topological option; ignoring');
|
|
47464
|
+
return importGeoJSON({type: 'GeometryCollection', geometries: []}, {type: 'polygon'});
|
|
47465
|
+
}
|
|
47466
|
+
var d = getMedialDebugDistances(lyr, dataset, opts);
|
|
47467
|
+
var tris = buildInterFeatureDelaunay(d.shapes, d.coordDistances, dataset.arcs);
|
|
47468
|
+
// tris is a GeometryCollection of one Polygon per triangle; import it directly
|
|
47469
|
+
// so each triangle is its own feature (selectable/inspectable).
|
|
47470
|
+
return importGeoJSON(tris || {type: 'GeometryCollection', geometries: []},
|
|
47471
|
+
{type: 'polygon'});
|
|
45645
47472
|
}
|
|
45646
47473
|
|
|
45647
47474
|
function getTopologicalBufferTileIds(sourceId, bufferId, featureId, mosaicIndex,
|
|
45648
|
-
sourceIdIndex, bufferIdIndex,
|
|
47475
|
+
sourceIdIndex, bufferIdIndex, ownerCtx) {
|
|
45649
47476
|
var ids = [];
|
|
45650
47477
|
var index = [];
|
|
45651
47478
|
addTileIds(ids, index, mosaicIndex.getTileIdsByShapeId(sourceId));
|
|
45652
47479
|
if (bufferId >= 0) {
|
|
45653
47480
|
addTileIds(ids, index, mosaicIndex.getTileIdsByShapeId(bufferId).filter(function(tileId) {
|
|
45654
47481
|
return !tileHasSourcePolygon(tileId, mosaicIndex, sourceIdIndex) &&
|
|
45655
|
-
getBufferTileOwnerId(tileId, mosaicIndex, bufferIdIndex,
|
|
47482
|
+
getBufferTileOwnerId(tileId, mosaicIndex, bufferIdIndex, ownerCtx) == featureId;
|
|
45656
47483
|
}));
|
|
45657
47484
|
}
|
|
45658
47485
|
return ids;
|
|
45659
47486
|
}
|
|
45660
47487
|
|
|
47488
|
+
// Per-mosaic context for nearest-source tile ownership. Source-shape segment
|
|
47489
|
+
// indexes and tile representative points are built lazily and cached, since
|
|
47490
|
+
// only a fraction of tiles are contested.
|
|
47491
|
+
function createTileOwnerContext(shapes, sourceArcs, sourceAreas, mosaicArcs) {
|
|
47492
|
+
return {
|
|
47493
|
+
shapes: shapes,
|
|
47494
|
+
sourceArcs: sourceArcs,
|
|
47495
|
+
sourceAreas: sourceAreas,
|
|
47496
|
+
mosaicArcs: mosaicArcs,
|
|
47497
|
+
sourceIndexCache: [],
|
|
47498
|
+
anchorCache: []
|
|
47499
|
+
};
|
|
47500
|
+
}
|
|
47501
|
+
|
|
45661
47502
|
function addTileIds(memo, index, ids) {
|
|
45662
47503
|
ids.forEach(function(id) {
|
|
45663
47504
|
if (index[id]) return;
|
|
@@ -45672,29 +47513,110 @@ ${svg}
|
|
|
45672
47513
|
});
|
|
45673
47514
|
}
|
|
45674
47515
|
|
|
45675
|
-
|
|
45676
|
-
|
|
45677
|
-
|
|
47516
|
+
// Tiny relative tolerance for treating two source distances as equal (a tile
|
|
47517
|
+
// sitting on the equidistant boundary), falling back to the largest-area then
|
|
47518
|
+
// lowest-id rule for a deterministic result.
|
|
47519
|
+
var OWNER_DIST_EPS = 1e-6;
|
|
47520
|
+
|
|
47521
|
+
// Owner of a contested buffer tile: among the features whose buffer covers the
|
|
47522
|
+
// tile, the one whose source polygon is nearest to the tile's representative
|
|
47523
|
+
// point. Choosing only among covering features means reassignment never creates
|
|
47524
|
+
// a coverage gap (the tile is inside every candidate's buffer); the Voronoi
|
|
47525
|
+
// cut-lines split tiles along the equidistant boundary so each sub-tile lies on
|
|
47526
|
+
// one source's near side. Ties (a tile straddling the boundary, or one the
|
|
47527
|
+
// medial axis did not reach) fall back to largest area then lowest id.
|
|
47528
|
+
function getBufferTileOwnerId(tileId, mosaicIndex, bufferIdIndex, ownerCtx) {
|
|
47529
|
+
var candidates = [];
|
|
45678
47530
|
mosaicIndex.getSourceIdsByTileId(tileId).forEach(function(shapeId) {
|
|
45679
47531
|
var featureId = bufferIdIndex[shapeId];
|
|
45680
|
-
|
|
45681
|
-
|
|
45682
|
-
|
|
45683
|
-
|
|
45684
|
-
|
|
45685
|
-
|
|
45686
|
-
|
|
47532
|
+
if (featureId >= 0) candidates.push(featureId);
|
|
47533
|
+
});
|
|
47534
|
+
if (candidates.length === 0) return -1;
|
|
47535
|
+
if (candidates.length === 1) return candidates[0];
|
|
47536
|
+
var p = getTileAnchorPoint(tileId, mosaicIndex, ownerCtx);
|
|
47537
|
+
if (!p) return pickLargestAreaFeature(candidates, ownerCtx.sourceAreas);
|
|
47538
|
+
var ownerId = -1;
|
|
47539
|
+
var bestDist = Infinity;
|
|
47540
|
+
var bestArea = -Infinity;
|
|
47541
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
47542
|
+
var featureId = candidates[i];
|
|
47543
|
+
var dist = getPointToSourceDistance(p.x, p.y, featureId, ownerCtx);
|
|
47544
|
+
var area = ownerCtx.sourceAreas[featureId];
|
|
47545
|
+
var tol = bestDist === Infinity ? 0 :
|
|
47546
|
+
OWNER_DIST_EPS * Math.max(1, Math.abs(bestDist), Math.abs(dist));
|
|
47547
|
+
if (dist < bestDist - tol ||
|
|
47548
|
+
(Math.abs(dist - bestDist) <= tol &&
|
|
47549
|
+
(area > bestArea || area == bestArea && featureId < ownerId))) {
|
|
47550
|
+
ownerId = featureId;
|
|
47551
|
+
bestDist = dist;
|
|
47552
|
+
bestArea = area;
|
|
47553
|
+
}
|
|
47554
|
+
}
|
|
47555
|
+
return ownerId;
|
|
47556
|
+
}
|
|
47557
|
+
|
|
47558
|
+
function pickLargestAreaFeature(candidates, sourceAreas) {
|
|
47559
|
+
var ownerId = -1;
|
|
47560
|
+
var ownerArea = -Infinity;
|
|
47561
|
+
candidates.forEach(function(featureId) {
|
|
47562
|
+
var area = sourceAreas[featureId];
|
|
47563
|
+
if (area > ownerArea || area == ownerArea && featureId < ownerId) {
|
|
47564
|
+
ownerId = featureId;
|
|
47565
|
+
ownerArea = area;
|
|
45687
47566
|
}
|
|
45688
47567
|
});
|
|
45689
47568
|
return ownerId;
|
|
45690
47569
|
}
|
|
45691
47570
|
|
|
47571
|
+
// Representative interior point of a mosaic tile, cached by tile id. Uses the
|
|
47572
|
+
// pole of inaccessibility (findAnchorPoint), which is guaranteed to lie inside
|
|
47573
|
+
// the tile -- unlike the centroid, which for a thin or curved contested strip
|
|
47574
|
+
// (the sub-tiles the medial-axis cuts create along a narrow channel) can fall
|
|
47575
|
+
// outside the tile, on the wrong side of the equidistant boundary, and so
|
|
47576
|
+
// misclassify the strip's nearest source. The centroid is used only as a
|
|
47577
|
+
// fallback if the anchor probe fails.
|
|
47578
|
+
function getTileAnchorPoint(tileId, mosaicIndex, ownerCtx) {
|
|
47579
|
+
if (tileId in ownerCtx.anchorCache) return ownerCtx.anchorCache[tileId];
|
|
47580
|
+
var tile = mosaicIndex.mosaic[tileId];
|
|
47581
|
+
var p = findAnchorPoint(tile, ownerCtx.mosaicArcs) ||
|
|
47582
|
+
getPathCentroid(tile[0], ownerCtx.mosaicArcs) || null;
|
|
47583
|
+
ownerCtx.anchorCache[tileId] = p;
|
|
47584
|
+
return p;
|
|
47585
|
+
}
|
|
47586
|
+
|
|
47587
|
+
// Distance from a point to a feature's source polygon, using a lazily-built and
|
|
47588
|
+
// cached chunk-bounds segment index (see buildShapeSegmentIndex).
|
|
47589
|
+
function getPointToSourceDistance(x, y, featureId, ownerCtx) {
|
|
47590
|
+
var index = ownerCtx.sourceIndexCache[featureId];
|
|
47591
|
+
if (!index) {
|
|
47592
|
+
index = ownerCtx.sourceIndexCache[featureId] =
|
|
47593
|
+
buildShapeSegmentIndex(ownerCtx.shapes[featureId], ownerCtx.sourceArcs);
|
|
47594
|
+
}
|
|
47595
|
+
return getPointToIndexedShapeDistance(x, y, index);
|
|
47596
|
+
}
|
|
47597
|
+
|
|
45692
47598
|
function getSourceShapeAreas(shapes, arcs) {
|
|
45693
47599
|
return shapes.map(function(shape) {
|
|
45694
47600
|
return Math.abs(getShapeArea(shape, arcs));
|
|
45695
47601
|
});
|
|
45696
47602
|
}
|
|
45697
47603
|
|
|
47604
|
+
// One interior point per positive ring-group (part) of every source shape,
|
|
47605
|
+
// tagged with its feature id. A multipart source contributes a point for each
|
|
47606
|
+
// detached part (e.g. a small island ring), so the topological hole filter can
|
|
47607
|
+
// recognize a neighbor's territory even when it is one part of a multipolygon.
|
|
47608
|
+
function getSourceInteriorPoints(shapes, arcs) {
|
|
47609
|
+
var points = [];
|
|
47610
|
+
shapes.forEach(function(shape, featureId) {
|
|
47611
|
+
if (!shape) return;
|
|
47612
|
+
getPolygonRingGroupShapes(shape, arcs).forEach(function(group) {
|
|
47613
|
+
var p = findAnchorPoint(group, arcs);
|
|
47614
|
+
if (p) points.push({x: p.x, y: p.y, featureId: featureId});
|
|
47615
|
+
});
|
|
47616
|
+
});
|
|
47617
|
+
return points;
|
|
47618
|
+
}
|
|
47619
|
+
|
|
45698
47620
|
function getIdLookup(ids) {
|
|
45699
47621
|
var index = [];
|
|
45700
47622
|
ids.forEach(function(id) {
|
|
@@ -45862,10 +47784,10 @@ ${svg}
|
|
|
45862
47784
|
if (!bufferDataset.arcs) return null;
|
|
45863
47785
|
// The default offset rings come from the winding-fill maker (one self-
|
|
45864
47786
|
// overlapping ring per source ring) and must be unioned by winding number;
|
|
45865
|
-
// the
|
|
47787
|
+
// the band-method fallback emits overlapping bands that a boundary flood
|
|
45866
47788
|
// resolves instead (its maker leaves winding_fill off to match).
|
|
45867
47789
|
dissolveBufferDataset2(bufferDataset,
|
|
45868
|
-
Object.assign({}, opts, {winding_fill: !opts.
|
|
47790
|
+
Object.assign({}, opts, {winding_fill: !opts.band_method}));
|
|
45869
47791
|
bufferShape = bufferLyr.shapes && bufferLyr.shapes[0];
|
|
45870
47792
|
if (!bufferShape) return null;
|
|
45871
47793
|
bufferData = exportPathData(bufferShape, bufferDataset.arcs, 'polygon');
|
|
@@ -45894,8 +47816,28 @@ ${svg}
|
|
|
45894
47816
|
});
|
|
45895
47817
|
}
|
|
45896
47818
|
|
|
45897
|
-
|
|
47819
|
+
// True if the buffer geometry has any interior ring (a candidate artifact
|
|
47820
|
+
// hole). A clean positive grow -- the common case, e.g. a hole-free coastline
|
|
47821
|
+
// dissolved by the winding-number fill -- has none, so the (potentially large)
|
|
47822
|
+
// source-shape spatial indexes below need never be built.
|
|
47823
|
+
function bufferGeomHasCandidateHole(geom) {
|
|
47824
|
+
if (geom.type == 'Polygon') return geom.coordinates.length > 1;
|
|
47825
|
+
if (geom.type == 'MultiPolygon') {
|
|
47826
|
+
return geom.coordinates.some(function(polygon) { return polygon.length > 1; });
|
|
47827
|
+
}
|
|
47828
|
+
return false;
|
|
47829
|
+
}
|
|
47830
|
+
|
|
47831
|
+
// @territory (optional, topological pipeline only): {points, featureId} where
|
|
47832
|
+
// points are source-part interior points tagged by feature id; a candidate hole
|
|
47833
|
+
// enclosing another feature's point is that neighbor's territory (excluded to
|
|
47834
|
+
// prevent overlap) and is always kept.
|
|
47835
|
+
function removePositiveBufferArtifactHoles(geom, shape, arcs, distance, territory) {
|
|
45898
47836
|
if (!geom) return null;
|
|
47837
|
+
// Nothing to filter unless the result actually has interior rings. Skip the
|
|
47838
|
+
// index build (filterArtifactHoles is itself a no-op on hole-free polygons,
|
|
47839
|
+
// so this only avoids needless work, not any classification).
|
|
47840
|
+
if (!bufferGeomHasCandidateHole(geom)) return geom;
|
|
45899
47841
|
var threshold = getPositiveHoleArtifactThreshold(distance, arcs);
|
|
45900
47842
|
var minHoleArea = getPositiveHoleArtifactAreaThreshold(distance, arcs);
|
|
45901
47843
|
var sourceHoles = getSourceHoleShapes(shape, arcs);
|
|
@@ -45914,7 +47856,9 @@ ${svg}
|
|
|
45914
47856
|
shapeIndex: buildShapeSegmentIndex(shape, arcs),
|
|
45915
47857
|
pathIndex: shape && shape.length > 0 ? new PathIndex([shape], arcs) : null,
|
|
45916
47858
|
holeIndex: sourceHoles.length > 0 ?
|
|
45917
|
-
buildShapeSegmentIndex(sourceHoles.map(function(h) {return h[0];}), arcs) : null
|
|
47859
|
+
buildShapeSegmentIndex(sourceHoles.map(function(h) {return h[0];}), arcs) : null,
|
|
47860
|
+
territoryPoints: territory ? territory.points : null,
|
|
47861
|
+
featureId: territory ? territory.featureId : -1
|
|
45918
47862
|
};
|
|
45919
47863
|
if (geom.type == 'Polygon') {
|
|
45920
47864
|
geom.coordinates = filterArtifactHoles(geom.coordinates, minHoleArea, ctx);
|
|
@@ -46024,11 +47968,40 @@ ${svg}
|
|
|
46024
47968
|
function filterArtifactHoles(polygon, minHoleArea, ctx) {
|
|
46025
47969
|
if (polygon.length < 2) return polygon;
|
|
46026
47970
|
return [polygon[0]].concat(polygon.slice(1).filter(function(ring) {
|
|
47971
|
+
// A hole over another feature's source is real territory (kept regardless of
|
|
47972
|
+
// size or how deep it sits in this feature's buffer); see getSourceInteriorPoints.
|
|
47973
|
+
if (ringEnclosesOtherTerritory(ring, ctx)) return true;
|
|
46027
47974
|
return Math.abs(getGeoJSONRingArea(ring)) > minHoleArea &&
|
|
46028
47975
|
!positiveBufferHoleIsArtifact(ring, ctx);
|
|
46029
47976
|
}));
|
|
46030
47977
|
}
|
|
46031
47978
|
|
|
47979
|
+
function ringEnclosesOtherTerritory(ring, ctx) {
|
|
47980
|
+
var points = ctx.territoryPoints;
|
|
47981
|
+
if (!points) return false;
|
|
47982
|
+
for (var i = 0; i < points.length; i++) {
|
|
47983
|
+
if (points[i].featureId === ctx.featureId) continue;
|
|
47984
|
+
if (pointInGeoJSONRing(points[i].x, points[i].y, ring)) return true;
|
|
47985
|
+
}
|
|
47986
|
+
return false;
|
|
47987
|
+
}
|
|
47988
|
+
|
|
47989
|
+
// Ray-casting point-in-ring test for a closed GeoJSON ring (array of [x, y],
|
|
47990
|
+
// first == last). Boundary cases are irrelevant here: territory probe points are
|
|
47991
|
+
// well inside their source part, far from any hole-ring edge.
|
|
47992
|
+
function pointInGeoJSONRing(x, y, ring) {
|
|
47993
|
+
var inside = false;
|
|
47994
|
+
for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
47995
|
+
var xi = ring[i][0], yi = ring[i][1];
|
|
47996
|
+
var xj = ring[j][0], yj = ring[j][1];
|
|
47997
|
+
if ((yi > y) !== (yj > y) &&
|
|
47998
|
+
x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
|
|
47999
|
+
inside = !inside;
|
|
48000
|
+
}
|
|
48001
|
+
}
|
|
48002
|
+
return inside;
|
|
48003
|
+
}
|
|
48004
|
+
|
|
46032
48005
|
function positiveBufferHoleIsArtifact(ring, ctx) {
|
|
46033
48006
|
var n = ring.length - 1; // skip duplicate endpoint
|
|
46034
48007
|
var step = Math.max(1, Math.floor(n / 20));
|
|
@@ -48528,6 +50501,9 @@ ${svg}
|
|
|
48528
50501
|
if (opts.topological && lyr.geometry_type != 'polygon') {
|
|
48529
50502
|
stop$1('The topological buffer option requires a polygon layer');
|
|
48530
50503
|
}
|
|
50504
|
+
if (opts.fill_gaps && lyr.geometry_type != 'polygon') {
|
|
50505
|
+
stop$1('The fill-gaps option requires a polygon layer');
|
|
50506
|
+
}
|
|
48531
50507
|
if (opts.geodesic && !isLatLngCRS(getDatasetCRS(dataset))) {
|
|
48532
50508
|
// Geodesic buffer of projected data: reproject the source paths through
|
|
48533
50509
|
// WGS84 lng/lat, run the ordinary (spherical) buffer pipeline, then
|
|
@@ -53900,219 +55876,73 @@ ${svg}
|
|
|
53900
55876
|
var rec = records[i] || {};
|
|
53901
55877
|
if (!shp) {
|
|
53902
55878
|
// case: record with no geometry -- retain in the output layer
|
|
53903
|
-
shapes2.push(null);
|
|
53904
|
-
records2.push(rec);
|
|
53905
|
-
return;
|
|
53906
|
-
}
|
|
53907
|
-
outputLines = [];
|
|
53908
|
-
outputKeys = [];
|
|
53909
|
-
outputMatches = [];
|
|
53910
|
-
forEachShapePart(shp, onPart);
|
|
53911
|
-
outputLines.forEach(function(shape2, i) {
|
|
53912
|
-
shapes2.push(shape2);
|
|
53913
|
-
records2.push(utils.extend({}, rec));
|
|
53914
|
-
index2.push(outputMatches[i]);
|
|
53915
|
-
});
|
|
53916
|
-
});
|
|
53917
|
-
polylineLyr.shapes = shapes2;
|
|
53918
|
-
polylineLyr.data = new DataTable(records2);
|
|
53919
|
-
markLayerChanged(polylineLyr, {operation: 'divide', unit: 'shapes-data'});
|
|
53920
|
-
joinTables(polylineLyr.data, polygonLyr.data, function(i) {
|
|
53921
|
-
return index2[i] || [];
|
|
53922
|
-
}, opts);
|
|
53923
|
-
|
|
53924
|
-
function addDividedParts(parts, keys, matches) {
|
|
53925
|
-
var keyId, key;
|
|
53926
|
-
for (var i=0; i<parts.length; i++) {
|
|
53927
|
-
key = keys[i];
|
|
53928
|
-
keyId = outputKeys.indexOf(key);
|
|
53929
|
-
if (keyId == -1) {
|
|
53930
|
-
outputKeys.push(key);
|
|
53931
|
-
outputLines.push([parts[i]]);
|
|
53932
|
-
outputMatches.push(matches[i]);
|
|
53933
|
-
} else {
|
|
53934
|
-
outputLines[keyId].push(parts[i]);
|
|
53935
|
-
}
|
|
53936
|
-
}
|
|
53937
|
-
}
|
|
53938
|
-
|
|
53939
|
-
function getKey(shapeIds) {
|
|
53940
|
-
return shapeIds.sort().join(',');
|
|
53941
|
-
// multiple matches: treat like no match
|
|
53942
|
-
// return shapeIds.length == 1 ? String(shapeIds[0]) : '-1';
|
|
53943
|
-
}
|
|
53944
|
-
|
|
53945
|
-
// Partition each part
|
|
53946
|
-
function onPart(ids) {
|
|
53947
|
-
var parts2 = [];
|
|
53948
|
-
var keys2 = [];
|
|
53949
|
-
var matches2 = [];
|
|
53950
|
-
var prevKey = null;
|
|
53951
|
-
var containingIds, key, part2, arcId;
|
|
53952
|
-
// assign each arc to a divided shape
|
|
53953
|
-
for (var i=0, n=ids.length; i<n; i++) {
|
|
53954
|
-
arcId = ids[i];
|
|
53955
|
-
containingIds = index.findShapesEnclosingArc(absArcId(arcId));
|
|
53956
|
-
key = getKey(containingIds);
|
|
53957
|
-
if (key === prevKey) {
|
|
53958
|
-
// case: continuation of a part
|
|
53959
|
-
part2.push(arcId);
|
|
53960
|
-
} else {
|
|
53961
|
-
// case: start of a new part
|
|
53962
|
-
part2 = [arcId];
|
|
53963
|
-
parts2.push(part2);
|
|
53964
|
-
keys2.push(key);
|
|
53965
|
-
matches2.push(containingIds);
|
|
53966
|
-
}
|
|
53967
|
-
prevKey = key;
|
|
53968
|
-
}
|
|
53969
|
-
addDividedParts(parts2, keys2, matches2);
|
|
53970
|
-
}
|
|
53971
|
-
}
|
|
53972
|
-
|
|
53973
|
-
function MaxHeap() {
|
|
53974
|
-
return new Heap('max');
|
|
53975
|
-
}
|
|
53976
|
-
|
|
53977
|
-
// A heap data structure used for computing Visvalingam simplification data.
|
|
53978
|
-
// type: 'max' or 'min' (min is default)
|
|
53979
|
-
//
|
|
53980
|
-
function Heap(type) {
|
|
53981
|
-
var heapBuf = utils.expandoBuffer(Int32Array),
|
|
53982
|
-
indexBuf = utils.expandoBuffer(Int32Array),
|
|
53983
|
-
heavierThan = type == 'max' ? lessThan : greaterThan,
|
|
53984
|
-
itemsInHeap = 0,
|
|
53985
|
-
dataArr,
|
|
53986
|
-
heapArr,
|
|
53987
|
-
indexArr;
|
|
53988
|
-
|
|
53989
|
-
this.init = function(values) {
|
|
53990
|
-
var i;
|
|
53991
|
-
dataArr = values;
|
|
53992
|
-
itemsInHeap = values.length;
|
|
53993
|
-
heapArr = heapBuf(itemsInHeap);
|
|
53994
|
-
indexArr = indexBuf(itemsInHeap);
|
|
53995
|
-
for (i=0; i<itemsInHeap; i++) {
|
|
53996
|
-
insertValue(i, i);
|
|
53997
|
-
}
|
|
53998
|
-
// place non-leaf items
|
|
53999
|
-
for (i=(itemsInHeap-2) >> 1; i >= 0; i--) {
|
|
54000
|
-
downHeap(i);
|
|
54001
|
-
}
|
|
54002
|
-
};
|
|
54003
|
-
|
|
54004
|
-
this.size = function() {
|
|
54005
|
-
return itemsInHeap;
|
|
54006
|
-
};
|
|
54007
|
-
|
|
54008
|
-
// Update a single value and re-heap
|
|
54009
|
-
this.updateValue = function(valIdx, val) {
|
|
54010
|
-
var heapIdx = indexArr[valIdx];
|
|
54011
|
-
dataArr[valIdx] = val;
|
|
54012
|
-
if (!(heapIdx >= 0 && heapIdx < itemsInHeap)) {
|
|
54013
|
-
error("Out-of-range heap index.");
|
|
54014
|
-
}
|
|
54015
|
-
downHeap(upHeap(heapIdx));
|
|
54016
|
-
};
|
|
54017
|
-
|
|
54018
|
-
this.popValue = function() {
|
|
54019
|
-
return dataArr[this.pop()];
|
|
54020
|
-
};
|
|
54021
|
-
|
|
54022
|
-
this.getValue = function(idx) {
|
|
54023
|
-
return dataArr[idx];
|
|
54024
|
-
};
|
|
54025
|
-
|
|
54026
|
-
this.peek = function() {
|
|
54027
|
-
return heapArr[0];
|
|
54028
|
-
};
|
|
54029
|
-
|
|
54030
|
-
this.peekValue = function() {
|
|
54031
|
-
return dataArr[heapArr[0]];
|
|
54032
|
-
};
|
|
54033
|
-
|
|
54034
|
-
// Return the idx of the lowest-value item in the heap
|
|
54035
|
-
this.pop = function() {
|
|
54036
|
-
var popIdx;
|
|
54037
|
-
if (itemsInHeap <= 0) {
|
|
54038
|
-
error("Tried to pop from an empty heap.");
|
|
54039
|
-
}
|
|
54040
|
-
popIdx = heapArr[0];
|
|
54041
|
-
insertValue(0, heapArr[--itemsInHeap]); // move last item in heap into root position
|
|
54042
|
-
downHeap(0);
|
|
54043
|
-
return popIdx;
|
|
54044
|
-
};
|
|
54045
|
-
|
|
54046
|
-
function upHeap(idx) {
|
|
54047
|
-
var parentIdx;
|
|
54048
|
-
// Move item up in the heap until it's at the top or is not lighter than its parent
|
|
54049
|
-
while (idx > 0) {
|
|
54050
|
-
parentIdx = (idx - 1) >> 1;
|
|
54051
|
-
if (heavierThan(idx, parentIdx)) {
|
|
54052
|
-
break;
|
|
54053
|
-
}
|
|
54054
|
-
swapItems(idx, parentIdx);
|
|
54055
|
-
idx = parentIdx;
|
|
55879
|
+
shapes2.push(null);
|
|
55880
|
+
records2.push(rec);
|
|
55881
|
+
return;
|
|
54056
55882
|
}
|
|
54057
|
-
|
|
54058
|
-
|
|
54059
|
-
|
|
54060
|
-
|
|
54061
|
-
|
|
54062
|
-
|
|
55883
|
+
outputLines = [];
|
|
55884
|
+
outputKeys = [];
|
|
55885
|
+
outputMatches = [];
|
|
55886
|
+
forEachShapePart(shp, onPart);
|
|
55887
|
+
outputLines.forEach(function(shape2, i) {
|
|
55888
|
+
shapes2.push(shape2);
|
|
55889
|
+
records2.push(utils.extend({}, rec));
|
|
55890
|
+
index2.push(outputMatches[i]);
|
|
55891
|
+
});
|
|
55892
|
+
});
|
|
55893
|
+
polylineLyr.shapes = shapes2;
|
|
55894
|
+
polylineLyr.data = new DataTable(records2);
|
|
55895
|
+
markLayerChanged(polylineLyr, {operation: 'divide', unit: 'shapes-data'});
|
|
55896
|
+
joinTables(polylineLyr.data, polygonLyr.data, function(i) {
|
|
55897
|
+
return index2[i] || [];
|
|
55898
|
+
}, opts);
|
|
54063
55899
|
|
|
54064
|
-
|
|
54065
|
-
|
|
54066
|
-
|
|
54067
|
-
|
|
55900
|
+
function addDividedParts(parts, keys, matches) {
|
|
55901
|
+
var keyId, key;
|
|
55902
|
+
for (var i=0; i<parts.length; i++) {
|
|
55903
|
+
key = keys[i];
|
|
55904
|
+
keyId = outputKeys.indexOf(key);
|
|
55905
|
+
if (keyId == -1) {
|
|
55906
|
+
outputKeys.push(key);
|
|
55907
|
+
outputLines.push([parts[i]]);
|
|
55908
|
+
outputMatches.push(matches[i]);
|
|
55909
|
+
} else {
|
|
55910
|
+
outputLines[keyId].push(parts[i]);
|
|
55911
|
+
}
|
|
54068
55912
|
}
|
|
54069
55913
|
}
|
|
54070
55914
|
|
|
54071
|
-
function
|
|
54072
|
-
|
|
54073
|
-
|
|
54074
|
-
|
|
54075
|
-
}
|
|
54076
|
-
|
|
54077
|
-
// Associate a heap idx with the index of a value in data arr
|
|
54078
|
-
function insertValue(heapIdx, valId) {
|
|
54079
|
-
indexArr[valId] = heapIdx;
|
|
54080
|
-
heapArr[heapIdx] = valId;
|
|
54081
|
-
}
|
|
54082
|
-
|
|
54083
|
-
// comparator for Visvalingam min heap
|
|
54084
|
-
// @a, @b: Indexes in @heapArr
|
|
54085
|
-
function greaterThan(a, b) {
|
|
54086
|
-
var idx1 = heapArr[a],
|
|
54087
|
-
idx2 = heapArr[b],
|
|
54088
|
-
val1 = dataArr[idx1],
|
|
54089
|
-
val2 = dataArr[idx2];
|
|
54090
|
-
// If values are equal, compare array indexes.
|
|
54091
|
-
// This is not a requirement of the Visvalingam algorithm,
|
|
54092
|
-
// but it generates output that matches Mahes Visvalingam's
|
|
54093
|
-
// reference implementation.
|
|
54094
|
-
// See https://hydra.hull.ac.uk/assets/hull:10874/content
|
|
54095
|
-
return (val1 > val2 || val1 === val2 && idx1 > idx2);
|
|
54096
|
-
}
|
|
54097
|
-
|
|
54098
|
-
// comparator for max heap
|
|
54099
|
-
function lessThan(a, b) {
|
|
54100
|
-
var idx1 = heapArr[a],
|
|
54101
|
-
idx2 = heapArr[b];
|
|
54102
|
-
return dataArr[idx1] < dataArr[idx2];
|
|
55915
|
+
function getKey(shapeIds) {
|
|
55916
|
+
return shapeIds.sort().join(',');
|
|
55917
|
+
// multiple matches: treat like no match
|
|
55918
|
+
// return shapeIds.length == 1 ? String(shapeIds[0]) : '-1';
|
|
54103
55919
|
}
|
|
54104
55920
|
|
|
54105
|
-
|
|
54106
|
-
|
|
54107
|
-
|
|
54108
|
-
|
|
54109
|
-
|
|
54110
|
-
|
|
54111
|
-
|
|
54112
|
-
|
|
54113
|
-
|
|
55921
|
+
// Partition each part
|
|
55922
|
+
function onPart(ids) {
|
|
55923
|
+
var parts2 = [];
|
|
55924
|
+
var keys2 = [];
|
|
55925
|
+
var matches2 = [];
|
|
55926
|
+
var prevKey = null;
|
|
55927
|
+
var containingIds, key, part2, arcId;
|
|
55928
|
+
// assign each arc to a divided shape
|
|
55929
|
+
for (var i=0, n=ids.length; i<n; i++) {
|
|
55930
|
+
arcId = ids[i];
|
|
55931
|
+
containingIds = index.findShapesEnclosingArc(absArcId(arcId));
|
|
55932
|
+
key = getKey(containingIds);
|
|
55933
|
+
if (key === prevKey) {
|
|
55934
|
+
// case: continuation of a part
|
|
55935
|
+
part2.push(arcId);
|
|
55936
|
+
} else {
|
|
55937
|
+
// case: start of a new part
|
|
55938
|
+
part2 = [arcId];
|
|
55939
|
+
parts2.push(part2);
|
|
55940
|
+
keys2.push(key);
|
|
55941
|
+
matches2.push(containingIds);
|
|
55942
|
+
}
|
|
55943
|
+
prevKey = key;
|
|
54114
55944
|
}
|
|
54115
|
-
|
|
55945
|
+
addDividedParts(parts2, keys2, matches2);
|
|
54116
55946
|
}
|
|
54117
55947
|
}
|
|
54118
55948
|
|
|
@@ -59573,151 +61403,6 @@ ${svg}
|
|
|
59573
61403
|
return m;
|
|
59574
61404
|
}
|
|
59575
61405
|
|
|
59576
|
-
var Visvalingam = {};
|
|
59577
|
-
|
|
59578
|
-
Visvalingam.getArcCalculator = function(metric, is3D) {
|
|
59579
|
-
var heap = new Heap(),
|
|
59580
|
-
prevBuf = utils.expandoBuffer(Int32Array),
|
|
59581
|
-
nextBuf = utils.expandoBuffer(Int32Array),
|
|
59582
|
-
calc = is3D ?
|
|
59583
|
-
function(b, c, d, xx, yy, zz) {
|
|
59584
|
-
return metric(xx[b], yy[b], zz[b], xx[c], yy[c], zz[c], xx[d], yy[d], zz[d]);
|
|
59585
|
-
} :
|
|
59586
|
-
function(b, c, d, xx, yy) {
|
|
59587
|
-
return metric(xx[b], yy[b], xx[c], yy[c], xx[d], yy[d]);
|
|
59588
|
-
};
|
|
59589
|
-
|
|
59590
|
-
// Calculate Visvalingam simplification data for an arc
|
|
59591
|
-
// @kk (Float64Array|Array) Receives calculated simplification thresholds
|
|
59592
|
-
// @xx, @yy, (@zz) Buffers containing vertex coordinates
|
|
59593
|
-
return function calcVisvalingam(kk, xx, yy, zz) {
|
|
59594
|
-
var arcLen = kk.length,
|
|
59595
|
-
prevArr = prevBuf(arcLen),
|
|
59596
|
-
nextArr = nextBuf(arcLen),
|
|
59597
|
-
val, maxVal = -Infinity,
|
|
59598
|
-
b, c, d; // indexes of points along arc
|
|
59599
|
-
|
|
59600
|
-
if (zz && !is3D) {
|
|
59601
|
-
error("[visvalingam] Received z-axis data for 2D simplification");
|
|
59602
|
-
} else if (!zz && is3D) {
|
|
59603
|
-
error("[visvalingam] Missing z-axis data for 3D simplification");
|
|
59604
|
-
} else if (kk.length > xx.length) {
|
|
59605
|
-
error("[visvalingam] Incompatible data arrays:", kk.length, xx.length);
|
|
59606
|
-
}
|
|
59607
|
-
|
|
59608
|
-
// Initialize Visvalingam "effective area" values and references to
|
|
59609
|
-
// prev/next points for each point in arc.
|
|
59610
|
-
for (c=0; c<arcLen; c++) {
|
|
59611
|
-
b = c-1;
|
|
59612
|
-
d = c+1;
|
|
59613
|
-
if (b < 0 || d >= arcLen) {
|
|
59614
|
-
val = Infinity; // endpoint maxVals
|
|
59615
|
-
} else {
|
|
59616
|
-
val = calc(b, c, d, xx, yy, zz);
|
|
59617
|
-
}
|
|
59618
|
-
kk[c] = val;
|
|
59619
|
-
nextArr[c] = d;
|
|
59620
|
-
prevArr[c] = b;
|
|
59621
|
-
}
|
|
59622
|
-
heap.init(kk);
|
|
59623
|
-
|
|
59624
|
-
// Calculate removal thresholds for each internal point in the arc
|
|
59625
|
-
//
|
|
59626
|
-
while (heap.size() > 0) {
|
|
59627
|
-
c = heap.pop(); // Remove the point with the least effective area.
|
|
59628
|
-
val = kk[c];
|
|
59629
|
-
if (val === Infinity) {
|
|
59630
|
-
break;
|
|
59631
|
-
}
|
|
59632
|
-
if (val < maxVal) {
|
|
59633
|
-
// don't assign current point a lesser value than the last removed vertex
|
|
59634
|
-
kk[c] = maxVal;
|
|
59635
|
-
} else {
|
|
59636
|
-
maxVal = val;
|
|
59637
|
-
}
|
|
59638
|
-
|
|
59639
|
-
// Recompute effective area of neighbors of the removed point.
|
|
59640
|
-
b = prevArr[c];
|
|
59641
|
-
d = nextArr[c];
|
|
59642
|
-
if (b > 0) {
|
|
59643
|
-
val = calc(prevArr[b], b, d, xx, yy, zz);
|
|
59644
|
-
heap.updateValue(b, val);
|
|
59645
|
-
}
|
|
59646
|
-
if (d < arcLen-1) {
|
|
59647
|
-
val = calc(b, d, nextArr[d], xx, yy, zz);
|
|
59648
|
-
heap.updateValue(d, val);
|
|
59649
|
-
}
|
|
59650
|
-
nextArr[b] = d;
|
|
59651
|
-
prevArr[d] = b;
|
|
59652
|
-
}
|
|
59653
|
-
};
|
|
59654
|
-
};
|
|
59655
|
-
|
|
59656
|
-
Visvalingam.standardMetric = geom.triangleArea;
|
|
59657
|
-
Visvalingam.standardMetric3D = geom.triangleArea3D;
|
|
59658
|
-
|
|
59659
|
-
Visvalingam.getWeightedMetric = function(opts) {
|
|
59660
|
-
var weight = Visvalingam.getWeightFunction(opts);
|
|
59661
|
-
return function(ax, ay, bx, by, cx, cy) {
|
|
59662
|
-
var area = geom.triangleArea(ax, ay, bx, by, cx, cy),
|
|
59663
|
-
cos = geom.cosine(ax, ay, bx, by, cx, cy);
|
|
59664
|
-
return weight(cos) * area;
|
|
59665
|
-
};
|
|
59666
|
-
};
|
|
59667
|
-
|
|
59668
|
-
Visvalingam.getWeightedMetric3D = function(opts) {
|
|
59669
|
-
var weight = Visvalingam.getWeightFunction(opts);
|
|
59670
|
-
return function(ax, ay, az, bx, by, bz, cx, cy, cz) {
|
|
59671
|
-
var area = geom.triangleArea3D(ax, ay, az, bx, by, bz, cx, cy, cz),
|
|
59672
|
-
cos = geom.cosine3D(ax, ay, az, bx, by, bz, cx, cy, cz);
|
|
59673
|
-
return weight(cos) * area;
|
|
59674
|
-
};
|
|
59675
|
-
};
|
|
59676
|
-
|
|
59677
|
-
Visvalingam.getWeightCoefficient = function(opts) {
|
|
59678
|
-
return opts && utils.isNumber(opts && opts.weighting) ? opts.weighting : 0.7;
|
|
59679
|
-
};
|
|
59680
|
-
|
|
59681
|
-
// Get a parameterized version of Visvalingam.weight()
|
|
59682
|
-
Visvalingam.getWeightFunction = function(opts) {
|
|
59683
|
-
var k = Visvalingam.getWeightCoefficient(opts);
|
|
59684
|
-
return function(cos) {
|
|
59685
|
-
return -cos * k + 1;
|
|
59686
|
-
};
|
|
59687
|
-
};
|
|
59688
|
-
|
|
59689
|
-
// Weight triangle area by inverse cosine
|
|
59690
|
-
// Standard weighting favors 90-deg angles; this curve peaks at 120 deg.
|
|
59691
|
-
Visvalingam.weight = function(cos) {
|
|
59692
|
-
var k = 0.7;
|
|
59693
|
-
return -cos * k + 1;
|
|
59694
|
-
};
|
|
59695
|
-
|
|
59696
|
-
Visvalingam.getEffectiveAreaSimplifier = function(use3D) {
|
|
59697
|
-
var metric = use3D ? Visvalingam.standardMetric3D : Visvalingam.standardMetric;
|
|
59698
|
-
return Visvalingam.getPathSimplifier(metric, use3D);
|
|
59699
|
-
};
|
|
59700
|
-
|
|
59701
|
-
Visvalingam.getWeightedSimplifier = function(opts, use3D) {
|
|
59702
|
-
var metric = use3D ? Visvalingam.getWeightedMetric3D(opts) : Visvalingam.getWeightedMetric(opts);
|
|
59703
|
-
return Visvalingam.getPathSimplifier(metric, use3D);
|
|
59704
|
-
};
|
|
59705
|
-
|
|
59706
|
-
Visvalingam.getPathSimplifier = function(metric, use3D) {
|
|
59707
|
-
return Visvalingam.scaledSimplify(Visvalingam.getArcCalculator(metric, use3D));
|
|
59708
|
-
};
|
|
59709
|
-
|
|
59710
|
-
|
|
59711
|
-
Visvalingam.scaledSimplify = function(f) {
|
|
59712
|
-
return function(kk, xx, yy, zz) {
|
|
59713
|
-
f(kk, xx, yy, zz);
|
|
59714
|
-
for (var i=1, n=kk.length - 1; i<n; i++) {
|
|
59715
|
-
// convert area metric to a linear equivalent
|
|
59716
|
-
kk[i] = Math.sqrt(kk[i]) * 0.65;
|
|
59717
|
-
}
|
|
59718
|
-
};
|
|
59719
|
-
};
|
|
59720
|
-
|
|
59721
61406
|
function getSimplifyMethodLabel(slug) {
|
|
59722
61407
|
return {
|
|
59723
61408
|
dp: "Ramer-Douglas-Peucker",
|
|
@@ -62073,7 +63758,7 @@ ${svg}
|
|
|
62073
63758
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
62074
63759
|
}
|
|
62075
63760
|
|
|
62076
|
-
var version = "0.7.
|
|
63761
|
+
var version = "0.7.29";
|
|
62077
63762
|
|
|
62078
63763
|
// Parse command line args into commands and run them
|
|
62079
63764
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|