mapshaper 0.7.27 → 0.7.28

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.
Files changed (3) hide show
  1. package/mapshaper.js +2266 -642
  2. package/package.json +3 -3
  3. package/www/mapshaper.js +2266 -642
package/www/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-winding', {
31791
+ .option('debug-mosaic', {
31789
31792
  type: 'flag'
31790
31793
  })
31791
- .option('debug-mosaic', {
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. Undocumented: it
31820
- // is an internal construction optimization whose output matches the
31821
- // un-optimized buffer within the error tolerance.
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('sector-band', {
31825
- // Undocumented escape hatch: build buffers with the older sector-band
31826
- // construction (per-segment offset bands + join-sector rings + a
31827
- // band-coverage audit, unioned by a boundary flood) instead of the
31828
- // default winding-fill construction. Applies to line buffers (one- and
31829
- // two-sided) and all polygon buffers (positive, negative, topological).
31830
- // Kept as a slower-but-conservative fallback and a debugging aid in case
31831
- // the winding-fill construction is found to mishandle some input.
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
- self.done = function() {
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. Empirically, shallow-concavity artifacts span well under
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: every splice shifts the whole tail (O(tail) per collapse,
41423
- // O(n^2) over the ring), whereas the scan only ever commits a growing prefix
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 b = ring[1];
41434
- var bpos = gated ? srcPos[1] : 0;
41435
- var r = 2;
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 a = out[out.length - 1];
41438
- var ax = a[0], ay = a[1], bx = b[0], by = b[1];
41439
- var apos = gated ? outPos[outPos.length - 1] : 0;
41440
- // forward segment t maps to the in-place scan's j = (anchor index) + 2 + t
41441
- var maxT = Math.min(maxGap - 2, n - 2 - r);
41442
- var collapsed = false;
41443
- for (var t = 0; t <= maxT; t++) {
41444
- var c = ring[r + t], d = ring[r + t + 1];
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
- if (gated &&
41451
- !spanIsCovered(apos, bpos, srcPos, r, r + t + 1, turnPrefix, maxTurn)) {
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
- // Replace the span a..ring[r+t] with the crossing: the anchor stays and is
41455
- // re-scanned (its new segment may cross again), so keep `a`, set b = hit,
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
- if (collapsed) continue;
41464
- out.push(b); // the anchor's successor can no longer collapse; commit it
41465
- if (gated) outPos.push(bpos);
41466
- if (r > n - 1) break; // no ring vertex left to become the next b
41467
- b = ring[r];
41468
- if (gated) bpos = srcPos[r];
41469
- r++;
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
- // True when the source-path span feeding a collapsed pocket turns by less than
41477
- // maxTurn (a covered overshoot, safe to collapse). The span covers the anchor
41478
- // position `apos`, its successor `bpos`, and ring positions srcPos[lo..hi]. A
41479
- // pocket touching a cap (NaN position) is never treated as a covered overshoot.
41480
- function spanIsCovered(apos, bpos, srcPos, lo, hi, turnPrefix, maxTurn) {
41481
- if (apos !== apos || bpos !== bpos) return false; // NaN cap
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 false; // NaN
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 (turnPrefix[posHi] - turnPrefix[posLo]) < maxTurn;
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.sector_band && pathIsOpen(verts)) {
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 sector-band escape hatch skips it to fall through to 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
- // Default: strip self-overlap loops (the dissolve would fill them anyway)
42351
- // so it has fewer segments and self-intersections to resolve; only loops
42352
- // spanning a small source-path turn are removed, so real holes are kept
42353
- return [removeBufferRingLoops(built.ring, BUFFER_LOOP_WINDOW,
42354
- built.srcPos, getSourceTurnPrefix(verts))];
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
- // Restricted to the winding-fill construction on closed source rings: a
42373
- // closed ring's offset overshoots are doubly-covered (safe to collapse), but
42374
- // an OPEN one-sided arc (e.g. a topological polygon's unbuffered-boundary
42375
- // remnant, buffered with caps) can have a concave-join dent that is its
42376
- // region's only coverage, which collapsing would cut away. Callers whose
42377
- // winding construction is not safe to collapse this way (the one-sided line
42378
- // buffer) opt out by passing no_loop_removal.
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 || pathIsOpen(sideVerts)) {
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). Used by the
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
- addPathStart(verts[0]);
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) so loop removal can gate self-crossings by source-path turn;
42448
- // the slight imprecision of tagging a previous-segment endpoint (p2Prev)
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 (x2 == x0 && y2 == y0) { // closed path
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.debug_winding || opts.debug_mosaic;
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 'sector-band' option forces the older non-winding
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.sector_band;
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, winding tiles, mosaic) want the
43428
- // whole layer's geometry/topology in one dataset; keep the original global
43429
- // dissolve for them (no artifact-hole filter runs in debug mode anyway).
43430
- // Mirror the real pipeline's construction so the debug view reflects what
43431
- // the buffer actually builds: an all-closed-ring two-sided layer uses the
43432
- // winding-fill + loop-removal construction (and a winding-number dissolve),
43433
- // so debug-offset shows the loop-removed offset rings and the no-loop-removal
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 debugWinding = !oneSided && layerIsAllClosed(lyr, dataset.arcs);
43436
- var debugMakerOpts = debugWinding ?
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
- debugWinding ? {winding_fill: true} : null);
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.debug_winding && !opts.debug_mosaic;
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,1137 @@ ${svg}
44402
44559
  return lng;
44403
44560
  }
44404
44561
 
44405
- // Remove small-area polygon rings (very simple implementation of sliver removal)
44406
- // TODO: more sophisticated sliver detection (e.g. could consider ratio of area to perimeter)
44407
- // TODO: consider merging slivers into adjacent polygons to prevent gaps from forming
44408
- // TODO: consider separate gap removal function as an alternative to merging slivers
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
- cmd.filterSlivers = function(lyr, dataset, opts) {
44411
- if (lyr.geometry_type != 'polygon') {
44412
- return 0;
44413
- }
44414
- return filterSlivers(lyr, dataset, opts);
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
- function filterSlivers(lyr, dataset, optsArg) {
44418
- var opts = utils.extend({sliver_control: 1}, optsArg);
44419
- var filterData = getSliverFilter(lyr, dataset, opts);
44420
- var ringTest = filterData.filter;
44421
- var removed = 0;
44422
- var pathFilter = function(path, i, paths) {
44423
- if (ringTest(path, i, paths)) {
44424
- removed++;
44425
- return null;
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
- noteLayerWillChange(lyr, {operation: 'filter-slivers', unit: 'shapes'});
44430
- editShapes(lyr.shapes, pathFilter);
44431
- markLayerChanged(lyr, {operation: 'filter-slivers', unit: 'shapes'});
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
- function filterClipSlivers(lyr, clipLyr, arcs) {
44442
- var threshold = getDefaultSliverThreshold(lyr, arcs);
44443
- // message('Using variable sliver threshold (based on ' + (threshold / 1e6) + ' sqkm)');
44444
- var ringTest = getSliverTest(arcs, threshold, 1);
44445
- var flags = new Uint8Array(arcs.size());
44446
- var removed = 0;
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
- countArcsInShapes(clipLyr.shapes, flags);
44466
- noteLayerWillChange(lyr, {operation: 'filter-clip-slivers', unit: 'shapes'});
44467
- editShapes(lyr.shapes, pathFilter);
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
- // Assumes: Arcs have been divided
44473
- //
44474
- function clipPolylines(targetShapes, clipShapes, nodes, type) {
44475
- var index = new PathIndex(clipShapes, nodes.arcs);
44611
+ this.getValue = function(idx) {
44612
+ return dataArr[idx];
44613
+ };
44476
44614
 
44477
- return targetShapes.map(function(shp) {
44478
- return clipPolyline(shp);
44479
- });
44615
+ this.peek = function() {
44616
+ return heapArr[0];
44617
+ };
44480
44618
 
44481
- function clipPolyline(shp) {
44482
- var clipped = null;
44483
- if (shp) clipped = shp.reduce(clipPath, []);
44484
- return clipped && clipped.length > 0 ? clipped : null;
44485
- }
44619
+ this.peekValue = function() {
44620
+ return dataArr[heapArr[0]];
44621
+ };
44486
44622
 
44487
- function clipPath(memo, path) {
44488
- var clippedPath = null,
44489
- arcId, enclosed;
44490
- for (var i=0; i<path.length; i++) {
44491
- arcId = path[i];
44492
- enclosed = index.arcIsEnclosed(arcId);
44493
- if (enclosed && type == 'clip' || !enclosed && type == 'erase') {
44494
- if (!clippedPath) {
44495
- memo.push(clippedPath = []);
44496
- }
44497
- clippedPath.push(arcId);
44498
- } else {
44499
- clippedPath = null;
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 memo;
44646
+ return idx;
44503
44647
  }
44504
- }
44505
44648
 
44506
- var PolylineClipping = /*#__PURE__*/Object.freeze({
44507
- __proto__: null,
44508
- clipPolylines: clipPolylines
44509
- });
44649
+ // Swap item at @idx with any lighter children
44650
+ function downHeap(idx) {
44651
+ var minIdx = compareDown(idx);
44510
44652
 
44511
- // TODO: to prevent invalid holes,
44512
- // could erase the holes from the space-enclosing rings.
44513
- function appendHolesToRings(cw, ccw) {
44514
- for (var i=0, n=ccw.length; i<n; i++) {
44515
- cw.push(ccw[i]);
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
- function getPolygonDissolver(nodes, spherical) {
44521
- spherical = spherical && !nodes.arcs.isPlanar();
44522
- var flags = new Uint8Array(nodes.arcs.size());
44523
- var divide = getHoleDivider(nodes, spherical);
44524
- var pathfind = getRingIntersector(nodes, flags);
44660
+ function swapItems(a, b) {
44661
+ var i = heapArr[a];
44662
+ insertValue(a, heapArr[b]);
44663
+ insertValue(b, i);
44664
+ }
44525
44665
 
44526
- return function(shp) {
44527
- if (!shp) return null;
44528
- var cw = [],
44529
- ccw = [];
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
- divide(shp, cw, ccw);
44532
- cw = pathfind(cw, 'flatten');
44533
- ccw.forEach(reversePath);
44534
- ccw = pathfind(ccw, 'flatten');
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);
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
+ return keptSites(sites, grid, coordDistances);
45169
+ }
45170
+
45171
+ // Bucket every boundary segment into a uniform grid so the nearest cross-feature
45172
+ // segment to an arbitrary point can be found by probing its 3x3 cell
45173
+ // neighborhood. The cell equals the maximum reach (sum of the two largest buffer
45174
+ // distances), so any in-reach segment is guaranteed to fall in that 3x3 window.
45175
+ // Returns null when there is no positive reach. Reused for both the per-vertex
45176
+ // gap (drives adaptive sampling) and the per-site keep test (gapAtPoint).
45177
+ function buildSegmentGrid(verts, coordDistances) {
45178
+ var paths = verts.paths;
45179
+ var maxDist = 0;
45180
+ for (var d = 0; d < coordDistances.length; d++) {
45181
+ if (coordDistances[d] > maxDist) maxDist = coordDistances[d];
45182
+ }
45183
+ var cell = 2 * maxDist; // upper bound on any pair's reach
45184
+ if (!(cell > 0)) return null;
45185
+ var xmin = Infinity, ymin = Infinity, ymax = -Infinity;
45186
+ paths.forEach(function(path) {
45187
+ var pts = path.points;
45188
+ for (var i = 0; i < pts.length; i++) {
45189
+ if (pts[i][0] < xmin) xmin = pts[i][0];
45190
+ if (pts[i][1] < ymin) ymin = pts[i][1];
45191
+ if (pts[i][1] > ymax) ymax = pts[i][1];
45192
+ }
45193
+ });
45194
+ // +1 cell index shift keeps probed -1 neighbors non-negative; rowSpan packs
45195
+ // (col, row) into a collision-free integer key.
45196
+ var rowSpan = Math.floor((ymax - ymin) / cell) + 3;
45197
+ function cellKey(cx, cy) { return (cx + 1) * rowSpan + (cy + 1); }
45198
+ function colOf(x) { return Math.floor((x - xmin) / cell); }
45199
+ function rowOf(y) { return Math.floor((y - ymin) / cell); }
45200
+ var seg = {x0: [], y0: [], x1: [], y1: [], feat: [], reach: []};
45201
+ var grid = new Map();
45202
+ paths.forEach(function(path) {
45203
+ var pts = path.points;
45204
+ var feat = path.owner;
45205
+ var reach = coordDistances[feat];
45206
+ for (var k = 0; k + 1 < pts.length; k++) {
45207
+ var ax = pts[k][0], ay = pts[k][1], bx = pts[k + 1][0], by = pts[k + 1][1];
45208
+ var idx = seg.feat.length;
45209
+ seg.x0.push(ax); seg.y0.push(ay); seg.x1.push(bx); seg.y1.push(by);
45210
+ seg.feat.push(feat); seg.reach.push(reach);
45211
+ var cxa = colOf(Math.min(ax, bx)), cxb = colOf(Math.max(ax, bx));
45212
+ var cya = rowOf(Math.min(ay, by)), cyb = rowOf(Math.max(ay, by));
45213
+ for (var gx = cxa; gx <= cxb; gx++) {
45214
+ for (var gy = cya; gy <= cyb; gy++) {
45215
+ var key = cellKey(gx, gy);
45216
+ var bucket = grid.get(key);
45217
+ if (bucket) bucket.push(idx); else grid.set(key, [idx]);
45218
+ }
45219
+ }
45220
+ }
45221
+ });
45222
+ return {seg: seg, grid: grid, cellKey: cellKey, colOf: colOf, rowOf: rowOf};
45223
+ }
45224
+
45225
+ // The channel width at (x, y): distance to the nearest different-feature segment
45226
+ // within their combined reach, or Infinity if none. Works for any point, not
45227
+ // just original vertices, so a long edge whose endpoints are out of reach but
45228
+ // whose middle crosses a gap is still measured correctly at the interior sites.
45229
+ function gapAtPoint(ctx, x, y, feat, reachF) {
45230
+ return nearestCrossFeatureSegmentDist(x, y, feat, reachF, ctx.seg, ctx.grid,
45231
+ ctx.cellKey, ctx.colOf, ctx.rowOf, Infinity);
45232
+ }
45233
+
45234
+ // Local gap at each original vertex (drives adaptive sampling, see
45235
+ // segmentSpacing). Measuring straight to the boundary segments yields the true
45236
+ // gap in a single pass, replacing the old triangulate -> estimate-gap ->
45237
+ // re-densify refinement loop that existed only because a coarse sampling can't
45238
+ // see a narrow gap (its nearest cross-feature SAMPLE is far).
45239
+ function computeVertexGaps(ctx, verts, coordDistances) {
45240
+ profileStart('medial:segmentGaps');
45241
+ var gaps = filledArray(verts.count, Infinity);
45242
+ if (ctx) {
45243
+ verts.paths.forEach(function(path) {
45244
+ var pts = path.points;
45245
+ var vids = path.vids;
45246
+ var feat = path.owner;
45247
+ var reachF = coordDistances[feat];
45248
+ for (var k = 0; k < vids.length; k++) {
45249
+ var g = gapAtPoint(ctx, pts[k][0], pts[k][1], feat, reachF);
45250
+ if (g < gaps[vids[k]]) gaps[vids[k]] = g;
45251
+ }
45252
+ });
45253
+ }
45254
+ profileEnd('medial:segmentGaps');
45255
+ return gaps;
45256
+ }
45257
+
45258
+ // Distance from (vx, vy) (owned by @feat, reach @reachF) to the nearest
45259
+ // different-feature segment within their combined reach, or @best if none is
45260
+ // closer. Probes the 3x3 grid-cell neighborhood (cell == max reach).
45261
+ function nearestCrossFeatureSegmentDist(vx, vy, feat, reachF, seg, grid, cellKey,
45262
+ colOf, rowOf, best) {
45263
+ var cx = colOf(vx), cy = rowOf(vy);
45264
+ for (var gx = cx - 1; gx <= cx + 1; gx++) {
45265
+ for (var gy = cy - 1; gy <= cy + 1; gy++) {
45266
+ var bucket = grid.get(cellKey(gx, gy));
45267
+ if (!bucket) continue;
45268
+ for (var b = 0; b < bucket.length; b++) {
45269
+ var s = bucket[b];
45270
+ if (seg.feat[s] === feat) continue;
45271
+ var reach = reachF + seg.reach[s];
45272
+ var dsq = pointSegDistSq2(vx, vy, seg.x0[s], seg.y0[s], seg.x1[s], seg.y1[s]);
45273
+ if (dsq <= reach * reach) {
45274
+ var dist = Math.sqrt(dsq);
45275
+ if (dist < best) best = dist;
45276
+ }
45277
+ }
45278
+ }
45279
+ }
45280
+ return best;
45281
+ }
45282
+
45283
+ // Keep only the sites that border a real gap: a different feature within reach
45284
+ // (finite gap) but farther than the touching threshold. These are the only sites
45285
+ // that can shape the medial axis. Touching/coincident interior borders (gap ~ 0,
45286
+ // the shared source boundary already partitions them) and the no-feature
45287
+ // coastline (gap = Infinity) are dropped, so the Delaunay covers just the
45288
+ // genuine gaps -- no triangulation is wasted on borders that need no medial.
45289
+ // The gap is measured per site (not per vertex) so a long edge whose endpoints
45290
+ // fall out of reach but whose middle crosses a gap keeps its interior points.
45291
+ function keptSites(sites, ctx, coordDistances) {
45292
+ profileStart('medial:contested');
45293
+ var result = {coords: [], owner: [], origin: []};
45294
+ if (!ctx) {
45295
+ profileEnd('medial:contested');
45296
+ return result;
45297
+ }
45298
+ var coords = sites.coords, owner = sites.owner, origin = sites.origin;
45299
+ for (var i = 0; i < coords.length; i++) {
45300
+ var feat = owner[i];
45301
+ var reach = coordDistances[feat];
45302
+ var g = gapAtPoint(ctx, coords[i][0], coords[i][1], feat, reach);
45303
+ if (isFinite(g) && g > reach * TOUCHING_GAP_FRACTION) {
45304
+ result.coords.push(coords[i]);
45305
+ result.owner.push(feat);
45306
+ result.origin.push(origin[i]);
45307
+ }
45308
+ }
45309
+ profileEnd('medial:contested');
45310
+ return result;
45311
+ }
45312
+
45313
+ function ringsLength(rings) {
45314
+ var sum = 0;
45315
+ rings.forEach(function(points) {
45316
+ for (var i = 1; i < points.length; i++) {
45317
+ var dx = points[i][0] - points[i - 1][0];
45318
+ var dy = points[i][1] - points[i - 1][1];
45319
+ sum += Math.sqrt(dx * dx + dy * dy);
45320
+ }
45321
+ });
45322
+ return sum;
45323
+ }
45324
+
45325
+ function filledArray(n, v) {
45326
+ var a = new Float64Array(n);
45327
+ a.fill(v);
45328
+ return a;
45329
+ }
45330
+
45331
+ // Flatten the candidate arc paths into a vertex layout: one entry per path
45332
+ // carrying its owner feature, its points, and a stable id (vid) for each vertex,
45333
+ // so densifyVertices can re-sample using a per-vid gap estimate. Each candidate
45334
+ // arc is an open polyline (every coordinate is a vertex, no wrap-around); a
45335
+ // closed ring made of a single arc arrives with its first point repeated at the
45336
+ // end, so treating it as open still covers the full loop.
45337
+ function buildVertexLayout(paths) {
45338
+ var layout = [];
45339
+ var count = 0;
45340
+ paths.forEach(function(path) {
45341
+ var points = path.points;
45342
+ var m = points.length;
45343
+ if (m < 2) return;
45344
+ var vids = [];
45345
+ for (var i = 0; i < m; i++) vids.push(count++);
45346
+ layout.push({owner: path.owner, points: points, vids: vids});
45347
+ });
45348
+ return {paths: layout, count: count};
45349
+ }
45350
+
45351
+ // The spacing for a path segment: the tighter of its two endpoints' gap-derived
45352
+ // spacings (so a segment straddling a narrowing gap samples at the finer rate).
45353
+ function segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen) {
45354
+ var sA = spacingFromGap(gaps[path.vids[k]], maxSpacing, spacingFloor, coarsen);
45355
+ var sB = spacingFromGap(gaps[path.vids[k + 1]], maxSpacing, spacingFloor, coarsen);
45356
+ return Math.min(sA, sB);
45357
+ }
45358
+
45359
+ // Re-sample every candidate path: emit each original vertex (tagged with its
45360
+ // vid) plus interior points spaced by the local gap-derived spacing (see
45361
+ // segmentSpacing). Paths are open, so the last vertex has no following segment.
45362
+ // Long edges are densified even where their endpoints are out of reach, so a
45363
+ // contested middle is sampled; keptSites later prunes the points that turn out
45364
+ // not to border a real gap.
45365
+ function densifyVertices(verts, gaps, coordDistances, spacingFloor, coarsen) {
45366
+ var coords = [];
45367
+ var owner = [];
45368
+ var origin = []; // vid for original vertices, -1 for interpolated points
45369
+ verts.paths.forEach(function(path) {
45370
+ var maxSpacing = coordDistances[path.owner];
45371
+ var m = path.vids.length;
45372
+ for (var k = 0; k < m; k++) {
45373
+ var a = path.points[k];
45374
+ coords.push([a[0], a[1]]);
45375
+ owner.push(path.owner);
45376
+ origin.push(path.vids[k]);
45377
+ if (k + 1 >= m) continue; // open path: no segment past the last vertex
45378
+ var b = path.points[k + 1];
45379
+ var s = segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen);
45380
+ var dx = b[0] - a[0], dy = b[1] - a[1];
45381
+ var len = Math.sqrt(dx * dx + dy * dy);
45382
+ if (s > 0 && len > s) {
45383
+ var steps = Math.floor(len / s);
45384
+ for (var t = 1; t <= steps; t++) {
45385
+ var f = t / (steps + 1);
45386
+ coords.push([a[0] + dx * f, a[1] + dy * f]);
45387
+ owner.push(path.owner);
45388
+ origin.push(-1);
45389
+ }
45390
+ }
45391
+ }
45392
+ });
45393
+ return {coords: coords, owner: owner, origin: origin};
45394
+ }
45395
+
45396
+ function spacingFromGap(gap, maxSpacing, spacingFloor, coarsen) {
45397
+ if (!isFinite(gap)) return maxSpacing;
45398
+ // A gap at or below the buffer's positional tolerance means the two features
45399
+ // effectively touch: there is no contested channel to run a medial down, and
45400
+ // the shared source boundary already partitions the overlap. Densifying it
45401
+ // would only flood a coincident border with collinear sites (millions of them
45402
+ // on a clean topological mosaic), so leave it at the coarse spacing.
45403
+ if (gap < maxSpacing * TOUCHING_GAP_FRACTION) return maxSpacing;
45404
+ var s = gap * GAP_FACTOR * coarsen;
45405
+ if (s > maxSpacing) s = maxSpacing;
45406
+ if (s < spacingFloor) s = spacingFloor;
45407
+ return s;
45408
+ }
45409
+
45410
+ // Predicted total site count for a given coarsen, matching densifyVertices'
45411
+ // emission rule exactly (one site per original vertex plus floor(len/spacing)
45412
+ // interior points per segment). Pure counting, no Delaunay -- cheap enough to
45413
+ // binary-search coarsen against. Counts pre-keep sites (the densification work),
45414
+ // which is what coarsen actually bounds.
45415
+ function predictSiteCount(verts, gaps, coordDistances, spacingFloor, coarsen) {
45416
+ var total = verts.count; // every original vertex is emitted
45417
+ verts.paths.forEach(function(path) {
45418
+ var maxSpacing = coordDistances[path.owner];
45419
+ var m = path.vids.length;
45420
+ for (var k = 0; k + 1 < m; k++) {
45421
+ var s = segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen);
45422
+ var a = path.points[k];
45423
+ var b = path.points[k + 1];
45424
+ var dx = b[0] - a[0], dy = b[1] - a[1];
45425
+ var len = Math.sqrt(dx * dx + dy * dy);
45426
+ if (s > 0 && len > s) total += Math.floor(len / s);
45427
+ }
45428
+ });
45429
+ return total;
45430
+ }
45431
+
45432
+ // Smallest coarsen (>= 1) whose predicted site count fits SITE_BUDGET. Site
45433
+ // count decreases monotonically as coarsen grows (spacing widens), so binary
45434
+ // search converges; capped because near-coincident gaps (gap ~ 0) can't be
45435
+ // thinned by coarsen and are bounded by spacingFloor instead.
45436
+ function fitCoarsen(verts, gaps, coordDistances, spacingFloor) {
45437
+ if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, 1) <= SITE_BUDGET) {
45438
+ return 1;
45439
+ }
45440
+ var lo = 1, hi = 1024;
45441
+ if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, hi) > SITE_BUDGET) {
45442
+ return hi; // even fully coarsened we can't fit; accept the floor-bounded count
45443
+ }
45444
+ for (var i = 0; i < 20; i++) {
45445
+ var mid = (lo + hi) / 2;
45446
+ if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, mid) > SITE_BUDGET) {
45447
+ lo = mid;
45448
+ } else {
45449
+ hi = mid;
45450
+ }
45451
+ }
45452
+ return hi;
45453
+ }
45454
+
45455
+ function nextHalfedge(e) {
45456
+ return e % 3 === 2 ? e - 2 : e + 1;
45457
+ }
45458
+
45459
+ function triangleOfEdge(e) {
45460
+ return Math.floor(e / 3);
45461
+ }
45462
+
45463
+ function computeMedialSegments(sites, coordDistances) {
45464
+ var coords = sites.coords;
45465
+ var owner = sites.owner;
45466
+ profileStart('medial:delaunay');
45467
+ var del = Delaunator.from(coords);
45468
+ profileEnd('medial:delaunay');
45469
+ var triangles = del.triangles;
45470
+ var halfedges = del.halfedges;
45471
+ var ntri = triangles.length / 3;
45472
+ // Medial-graph vertex coords, indexed by id. Triangle t's circumcenter is
45473
+ // vertex id t (so the three medial edges meeting at it share that id without
45474
+ // coordinate hashing); hull-ray ends are appended with fresh ids.
45475
+ var verts = new Array(ntri);
45476
+ var i;
45477
+ for (i = 0; i < ntri; i++) {
45478
+ verts[i] = circumcenter(
45479
+ coords[triangles[3 * i]],
45480
+ coords[triangles[3 * i + 1]],
45481
+ coords[triangles[3 * i + 2]]);
45482
+ }
45483
+ var segments = [];
45484
+ for (var e = 0; e < triangles.length; e++) {
45485
+ var opp = halfedges[e];
45486
+ var p = triangles[e];
45487
+ var q = triangles[nextHalfedge(e)];
45488
+ if (owner[p] === owner[q]) continue;
45489
+ var dx = coords[p][0] - coords[q][0];
45490
+ var dy = coords[p][1] - coords[q][1];
45491
+ var siteDist = Math.sqrt(dx * dx + dy * dy);
45492
+ var reach = coordDistances[owner[p]] + coordDistances[owner[q]];
45493
+ // sites whose sources are farther apart than the sum of their radii can
45494
+ // never have overlapping buffers, so their bisector is not a contested edge
45495
+ if (siteDist > reach) continue;
45496
+ var t1 = triangleOfEdge(e);
45497
+ var c1 = verts[t1];
45498
+ if (!c1) continue; // degenerate (near-collinear) triangle
45499
+ if (opp === -1) {
45500
+ // Hull edge: the Voronoi edge here is an unbounded ray (the bisector of
45501
+ // two sites on the convex hull). Emit it as an outward ray from the
45502
+ // circumcenter so the medial line reaches and crosses the buffer
45503
+ // boundary -- otherwise an interior medial segment that ends at this
45504
+ // circumcenter would dangle inside a tile and be pruned, leaving no cut.
45505
+ // The excess outside the buffers is trimmed by detachAcyclicArcs.
45506
+ var third = coords[triangles[nextHalfedge(nextHalfedge(e))]];
45507
+ var end = outwardRayEnd(c1, coords[p], coords[q], third, reach);
45508
+ if (end) {
45509
+ var rayId = verts.length;
45510
+ verts.push(end);
45511
+ segments.push([t1, rayId]);
45512
+ }
45513
+ continue;
45514
+ }
45515
+ // interior edge: emit once (at the lower halfedge index)
45516
+ if (opp < e) continue;
45517
+ var t2 = triangleOfEdge(opp);
45518
+ var c2 = verts[t2];
45519
+ if (!c2) continue;
45520
+ var sx = c1[0] - c2[0], sy = c1[1] - c2[1];
45521
+ var segLen = Math.sqrt(sx * sx + sy * sy);
45522
+ // a real medial edge inside the overlap is short (on the order of the site
45523
+ // spacing plus the gap); a very long segment comes from a near-degenerate
45524
+ // triangle whose circumcenter is wild, so drop it
45525
+ if (segLen > 3 * (reach + siteDist)) continue;
45526
+ segments.push([t1, t2]);
45527
+ }
45528
+ return {segments: segments, coords: verts};
45529
+ }
45530
+
45531
+ // Endpoint of the outward Voronoi ray for a hull edge (p, q) whose triangle's
45532
+ // third vertex is @third: starts at the circumcenter @c, runs along the edge's
45533
+ // perpendicular bisector, away from @third (outward), a length proportional to
45534
+ // the buffer reach so it clears the buffer boundary.
45535
+ function outwardRayEnd(c, p, q, third, reach) {
45536
+ var ex = q[0] - p[0], ey = q[1] - p[1];
45537
+ var nx = -ey, ny = ex; // a normal to the edge
45538
+ var mx = (p[0] + q[0]) / 2, my = (p[1] + q[1]) / 2;
45539
+ // orient the normal away from the third vertex (outward from the hull)
45540
+ if (nx * (mx - third[0]) + ny * (my - third[1]) < 0) {
45541
+ nx = -nx;
45542
+ ny = -ny;
45543
+ }
45544
+ var len = Math.sqrt(nx * nx + ny * ny);
45545
+ if (len === 0 || !isFinite(len)) return null;
45546
+ var L = 3 * reach;
45547
+ return [c[0] + nx / len * L, c[1] + ny / len * L];
45548
+ }
45549
+
45550
+ function circumcenter(a, b, c) {
45551
+ var ax = a[0], ay = a[1], bx = b[0], by = b[1], cx = c[0], cy = c[1];
45552
+ var d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
45553
+ if (d === 0 || !isFinite(d)) return null;
45554
+ var a2 = ax * ax + ay * ay;
45555
+ var b2 = bx * bx + by * by;
45556
+ var c2 = cx * cx + cy * cy;
45557
+ var ux = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d;
45558
+ var uy = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d;
45559
+ if (!isFinite(ux) || !isFinite(uy)) return null;
45560
+ return [ux, uy];
45561
+ }
45562
+
45563
+ // Remove small-area polygon rings (very simple implementation of sliver removal)
45564
+ // TODO: more sophisticated sliver detection (e.g. could consider ratio of area to perimeter)
45565
+ // TODO: consider merging slivers into adjacent polygons to prevent gaps from forming
45566
+ // TODO: consider separate gap removal function as an alternative to merging slivers
45567
+ //
45568
+ cmd.filterSlivers = function(lyr, dataset, opts) {
45569
+ if (lyr.geometry_type != 'polygon') {
45570
+ return 0;
45571
+ }
45572
+ return filterSlivers(lyr, dataset, opts);
45573
+ };
45574
+
45575
+ function filterSlivers(lyr, dataset, optsArg) {
45576
+ var opts = utils.extend({sliver_control: 1}, optsArg);
45577
+ var filterData = getSliverFilter(lyr, dataset, opts);
45578
+ var ringTest = filterData.filter;
45579
+ var removed = 0;
45580
+ var pathFilter = function(path, i, paths) {
45581
+ if (ringTest(path, i, paths)) {
45582
+ removed++;
45583
+ return null;
45584
+ }
45585
+ };
45586
+
45587
+ noteLayerWillChange(lyr, {operation: 'filter-slivers', unit: 'shapes'});
45588
+ editShapes(lyr.shapes, pathFilter);
45589
+ markLayerChanged(lyr, {operation: 'filter-slivers', unit: 'shapes'});
45590
+ message(utils.format("Removed %'d sliver%s using %s", removed, utils.pluralSuffix(removed), filterData.label));
45591
+
45592
+ // Remove null shapes (likely removed by clipping/erasing, although possibly already present)
45593
+ if (opts.remove_empty) {
45594
+ cmd.filterFeatures(lyr, dataset.arcs, {remove_empty: true, verbose: false});
45595
+ }
45596
+ return removed;
45597
+ }
45598
+
45599
+ function filterClipSlivers(lyr, clipLyr, arcs) {
45600
+ var threshold = getDefaultSliverThreshold(lyr, arcs);
45601
+ // message('Using variable sliver threshold (based on ' + (threshold / 1e6) + ' sqkm)');
45602
+ var ringTest = getSliverTest(arcs, threshold, 1);
45603
+ var flags = new Uint8Array(arcs.size());
45604
+ var removed = 0;
45605
+ var pathFilter = function(path) {
45606
+ var prevArcs = 0,
45607
+ newArcs = 0;
45608
+ for (var i=0, n=path && path.length || 0; i<n; i++) {
45609
+ if (flags[absArcId(path[i])] > 0) {
45610
+ newArcs++;
45611
+ } else {
45612
+ prevArcs++;
45613
+ }
45614
+ }
45615
+ // filter paths that contain arcs from both original and clip/erase layers
45616
+ // and are small
45617
+ if (newArcs > 0 && prevArcs > 0 && ringTest(path)) {
45618
+ removed++;
45619
+ return null;
45620
+ }
45621
+ };
45622
+
45623
+ countArcsInShapes(clipLyr.shapes, flags);
45624
+ noteLayerWillChange(lyr, {operation: 'filter-clip-slivers', unit: 'shapes'});
45625
+ editShapes(lyr.shapes, pathFilter);
45626
+ markLayerChanged(lyr, {operation: 'filter-clip-slivers', unit: 'shapes'});
45627
+ return removed;
45628
+ }
45629
+
45630
+ // Assumes: Arcs have been divided
45631
+ //
45632
+ function clipPolylines(targetShapes, clipShapes, nodes, type) {
45633
+ var index = new PathIndex(clipShapes, nodes.arcs);
45634
+
45635
+ return targetShapes.map(function(shp) {
45636
+ return clipPolyline(shp);
45637
+ });
45638
+
45639
+ function clipPolyline(shp) {
45640
+ var clipped = null;
45641
+ if (shp) clipped = shp.reduce(clipPath, []);
45642
+ return clipped && clipped.length > 0 ? clipped : null;
45643
+ }
45644
+
45645
+ function clipPath(memo, path) {
45646
+ var clippedPath = null,
45647
+ arcId, enclosed;
45648
+ for (var i=0; i<path.length; i++) {
45649
+ arcId = path[i];
45650
+ enclosed = index.arcIsEnclosed(arcId);
45651
+ if (enclosed && type == 'clip' || !enclosed && type == 'erase') {
45652
+ if (!clippedPath) {
45653
+ memo.push(clippedPath = []);
45654
+ }
45655
+ clippedPath.push(arcId);
45656
+ } else {
45657
+ clippedPath = null;
45658
+ }
45659
+ }
45660
+ return memo;
45661
+ }
45662
+ }
45663
+
45664
+ var PolylineClipping = /*#__PURE__*/Object.freeze({
45665
+ __proto__: null,
45666
+ clipPolylines: clipPolylines
45667
+ });
45668
+
45669
+ // TODO: to prevent invalid holes,
45670
+ // could erase the holes from the space-enclosing rings.
45671
+ function appendHolesToRings(cw, ccw) {
45672
+ for (var i=0, n=ccw.length; i<n; i++) {
45673
+ cw.push(ccw[i]);
45674
+ }
45675
+ return cw;
45676
+ }
45677
+
45678
+ function getPolygonDissolver(nodes, spherical) {
45679
+ spherical = spherical && !nodes.arcs.isPlanar();
45680
+ var flags = new Uint8Array(nodes.arcs.size());
45681
+ var divide = getHoleDivider(nodes, spherical);
45682
+ var pathfind = getRingIntersector(nodes, flags);
45683
+
45684
+ return function(shp) {
45685
+ if (!shp) return null;
45686
+ var cw = [],
45687
+ ccw = [];
45688
+
45689
+ divide(shp, cw, ccw);
45690
+ cw = pathfind(cw, 'flatten');
45691
+ ccw.forEach(reversePath);
45692
+ ccw = pathfind(ccw, 'flatten');
44535
45693
  ccw.forEach(reversePath);
44536
45694
  var shp2 = appendHolesToRings(cw, ccw);
44537
45695
  var dissolved = pathfind(shp2, 'dissolve');
@@ -45382,40 +46540,530 @@ ${svg}
45382
46540
  };
45383
46541
  }
45384
46542
 
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
- // The debug-offset/debug-winding/debug-mosaic visualizations are implemented
45393
- // only for line buffers. They have no handling in the polygon pipeline; left
45394
- // unstripped they leak into the per-shape dissolve and produce malformed
45395
- // output, so drop them and warn rather than silently mislead.
45396
- if (opts.debug_offset || opts.debug_winding || opts.debug_mosaic) {
45397
- warn('debug-offset/debug-winding/debug-mosaic are not implemented for polygon buffers; ignoring');
45398
- opts = Object.assign({}, opts, {
45399
- debug_offset: false,
45400
- debug_winding: false,
45401
- debug_mosaic: false
45402
- });
45403
- }
45404
- if (spherical && opts.polar) {
45405
- // Pole/antimeridian-sliced polygons (grow only): keep the seam edges at the
45406
- // extent and clip to the world rectangle instead of wrapping at the
45407
- // antimeridian (see makePolarPolygonBuffer).
45408
- return makePolarPolygonBuffer(lyr, dataset, opts);
45409
- }
45410
- var output = makePolygonBufferGeoJSON(lyr, dataset, opts);
45411
- var dataset2 = importGeoJSON(output.geojson, {type: 'polygon'});
45412
- if (spherical) {
45413
- splitAntimeridianBufferDataset(dataset2);
45414
- if (output.dissolveAfterSplit) {
45415
- dissolveBufferDataset2(dataset2, opts);
46543
+ var ArcClassifier = /*#__PURE__*/Object.freeze({
46544
+ __proto__: null,
46545
+ getArcClassifier: getArcClassifier
46546
+ });
46547
+
46548
+ function makePolygonBuffer(lyr, dataset, opts) {
46549
+ var spherical = isLatLngCRS(getDatasetCRS(dataset));
46550
+ // debug-mosaic is implemented only for line buffers; for polygons it has no
46551
+ // handling and would leak into the per-shape dissolve and corrupt output, so
46552
+ // drop it and warn rather than silently mislead.
46553
+ if (opts.debug_mosaic) {
46554
+ warn('debug-mosaic is not implemented for polygon buffers; ignoring');
46555
+ opts = Object.assign({}, opts, {debug_mosaic: false});
46556
+ }
46557
+ if (opts.fill_gaps) {
46558
+ // Fill enclosed holes and narrow-mouthed inlets without growing the outer
46559
+ // boundary -- a topology-aware morphological closing (see
46560
+ // makeGapFillPolygonBuffer). Taken ahead of the debug and polar/normal
46561
+ // branches: fill-gaps is inherently topological and builds its medial at the
46562
+ // fill radius R, so it owns the debug views too (it re-enters this function
46563
+ // at R to produce them) and takes precedence over an explicit topological.
46564
+ var fillResult = makeGapFillPolygonBuffer(lyr, dataset, opts);
46565
+ // fill-gaps may itself return a debug dataset (it owns the medial debug
46566
+ // views); never cull those.
46567
+ if (!bufferOutputIsDebug(opts)) {
46568
+ cullSubTolerancePolygonArtifacts(fillResult, lyr, dataset, opts);
46569
+ }
46570
+ return fillResult;
46571
+ }
46572
+ if (opts.debug_delaunay) {
46573
+ // Undocumented: emit the medial-construction triangles (the Delaunay
46574
+ // triangles that bridge two features within buffer reach), whose
46575
+ // circumcenters are the medial vertices -- the contested ribbon where the
46576
+ // medial axis is built, not the full hull triangulation.
46577
+ return makeDelaunayDebugDataset(lyr, dataset, opts);
46578
+ }
46579
+ if (opts.debug_voronoi) {
46580
+ // Undocumented: emit the inter-feature medial-axis (Voronoi) cut-lines that
46581
+ // the topological pipeline injects to partition contested space, before they
46582
+ // are cut into the mosaic and dissolved. Useful for inspecting the medial
46583
+ // construction (sampling density, centerline tracking, simplification).
46584
+ return makeVoronoiDebugDataset(lyr, dataset, opts);
46585
+ }
46586
+ if (opts.debug_offset) {
46587
+ // Raw offset rings, undissolved (as with the line debug-offset): show the
46588
+ // construction's offset loops before the winding/boundary dissolve. Honors
46589
+ // no-loop-removal (loop removal runs inside the maker) and band-method, and
46590
+ // is taken ahead of the polar branch so it shows the raw clamped offsets
46591
+ // rather than the clipped/dissolved polar result.
46592
+ var debugDataset = importGeoJSON(
46593
+ makePolygonDebugOffsetGeoJSON(lyr, dataset, opts), {type: 'polygon'});
46594
+ if (spherical && debugDataset.arcs) {
46595
+ splitAntimeridianBufferDataset(debugDataset);
46596
+ }
46597
+ return debugDataset;
46598
+ }
46599
+ if (spherical && opts.polar) {
46600
+ // Pole/antimeridian-sliced polygons (grow only): keep the seam edges at the
46601
+ // extent and clip to the world rectangle instead of wrapping at the
46602
+ // antimeridian (see makePolarPolygonBuffer).
46603
+ var polar = makePolarPolygonBuffer(lyr, dataset, opts);
46604
+ cullSubTolerancePolygonArtifacts(polar, lyr, dataset, opts);
46605
+ return polar;
46606
+ }
46607
+ var output = buildPolygonBufferOutput(lyr, dataset, opts);
46608
+ var dataset2 = importGeoJSON(output.geojson, {type: 'polygon'});
46609
+ if (spherical) {
46610
+ splitAntimeridianBufferDataset(dataset2);
46611
+ if (output.dissolveAfterSplit) {
46612
+ dissolveBufferDataset2(dataset2, opts);
46613
+ }
46614
+ }
46615
+ cullSubTolerancePolygonArtifacts(dataset2, lyr, dataset, opts);
46616
+ return dataset2;
46617
+ }
46618
+
46619
+ // True when the buffer is producing a debug view (raw offsets or medial
46620
+ // construction) rather than a real buffer; those must skip the artifact cull.
46621
+ function bufferOutputIsDebug(opts) {
46622
+ return !!(opts.debug_delaunay || opts.debug_voronoi || opts.debug_offset ||
46623
+ opts.debug_mosaic);
46624
+ }
46625
+
46626
+ // Sub-tolerance artifact cull. The discrete medial sampling and the fill-gaps
46627
+ // mask boolean leave a scatter of degenerate, near-zero-area positive sliver
46628
+ // parts (most egregiously in fill-gaps: e.g. ~160 spurious parts on six
46629
+ // counties, most of them literally zero area). A part smaller than a tol x tol
46630
+ // square -- the buffer's own positional accuracy -- is below the noise floor and
46631
+ // is dropped. The smallest legitimate buffer part (the grow of a point-like
46632
+ // feature, ~pi*d^2) is ~4 orders of magnitude larger, so the threshold never
46633
+ // touches real geometry, and a genuinely thin-but-long fill (area =
46634
+ // tol*length >> tol^2) is kept. Holes are intentionally left alone: a small hole
46635
+ // can be a neighbor's preserved territory (see removePositiveBufferArtifactHoles,
46636
+ // which is territory-aware) and must not be filled here. Disabled when tolerance
46637
+ // is turned off (tolerance=0), matching the medial-simplify contract.
46638
+ function cullSubTolerancePolygonArtifacts(outDataset, srcLyr, srcDataset, opts) {
46639
+ if (opts.tolerance === 0 || opts.tolerance == '0' || opts.tolerance == '0%') return;
46640
+ if (!outDataset || !outDataset.arcs) return;
46641
+ var lyr = outDataset.layers.filter(function(l) {
46642
+ return l.geometry_type == 'polygon';
46643
+ })[0];
46644
+ if (!lyr || !lyr.shapes) return;
46645
+ var arcs = outDataset.arcs;
46646
+ var distanceFn = getBufferDistanceFunction(srcLyr, srcDataset, opts);
46647
+ var tolFn = getBufferToleranceFunction(srcDataset, opts);
46648
+ var crsArcs = srcDataset.arcs || arcs; // CRS-bearing arcs for the unit conversion
46649
+ lyr.shapes = lyr.shapes.map(function(shp, i) {
46650
+ if (!shp || shp.length === 0) return shp;
46651
+ var dist = distanceFn(i);
46652
+ if (!(dist > 0)) return shp;
46653
+ var tolCoord = getCoordinateDistance(tolFn(dist), crsArcs);
46654
+ var minArea = tolCoord * tolCoord;
46655
+ if (!(minArea > 0)) return shp;
46656
+ var kept = shp.filter(function(ring) {
46657
+ var area = getPlanarPathArea(ring, arcs);
46658
+ return !(area > 0 && area < minArea); // drop sub-tolerance positive parts
46659
+ });
46660
+ return kept.length > 0 ? kept : null;
46661
+ });
46662
+ }
46663
+
46664
+ // Build the per-shape GeoJSON offset output for a polygon buffer, choosing the
46665
+ // construction:
46666
+ // - The clean-outline construction is the default for ordinary polygon grow
46667
+ // (outer rings offset to a single self-contained loop; far fewer rings and
46668
+ // self-intersections into the winding dissolve than the band ribbon).
46669
+ // - The topological pipeline (which pre-dissolves per feature into a shared
46670
+ // mosaic) and the band-method escape hatch keep the band-ribbon construction.
46671
+ // - Negative buffers and hole shrink fall back to the band erode inside the
46672
+ // outline path itself.
46673
+ // Shared by makePolygonBuffer and makePolarPolygonBuffer so the polar option is
46674
+ // a true no-op on non-polar shapes (same construction as the plain buffer).
46675
+ function buildPolygonBufferOutput(lyr, dataset, opts) {
46676
+ var useOutline = !opts.band_method && !opts.topological;
46677
+ return useOutline ?
46678
+ makeOutlinePolygonBufferGeoJSON(lyr, dataset, opts) :
46679
+ makePolygonBufferGeoJSON(lyr, dataset, opts);
46680
+ }
46681
+
46682
+ // Short unit suffixes for re-emitting a scaled distance string (see
46683
+ // fillGapsRadiusStr); parseMeasure2 normalizes input units to these canonical
46684
+ // names. A null (unitless) value is re-emitted bare and interpreted like an
46685
+ // unsuffixed radius (meters for lat-long, CRS units for projected).
46686
+ var GAP_FILL_UNIT_SUFFIX = {
46687
+ meters: 'm',
46688
+ kilometers: 'km',
46689
+ feet: 'ft',
46690
+ miles: 'mi'
46691
+ };
46692
+
46693
+ // Default max-widening factor: an interior gap is kept open only if it is at
46694
+ // least this many times wider than the mouth size; narrower gaps are filled. Set
46695
+ // well above 1 so the closing does not leave a string of small holes along a
46696
+ // channel whose width fluctuates around the mouth size (e.g. the Columbia).
46697
+ var GAP_MAX_WIDENING_DEFAULT = 5;
46698
+
46699
+ // The mouth size for the gap fill is the buffer distance (the standard radius
46700
+ // option), which must be a positive constant.
46701
+ function parseFillGaps(opts) {
46702
+ var parsed = parseMeasure2(opts.radius);
46703
+ if (!(parsed.value > 0)) {
46704
+ stop$1('The fill-gaps option requires a positive buffer distance');
46705
+ }
46706
+ return parsed;
46707
+ }
46708
+
46709
+ // Build a radius string of (mouthSize/2 * factor) preserving the input units, so
46710
+ // the sub-buffers' own unit/CRS conversion applies (matching how -buffer radius
46711
+ // is parsed). factor 1 gives the mouth radius r = mouthSize/2 (two banks of a
46712
+ // channel narrower than the mouth size, 2r, meet under a grow of r); factor k
46713
+ // (the max-widening multiple) gives the larger fill radius R = k*mouthSize/2.
46714
+ function fillGapsRadiusStr(parsed, factor) {
46715
+ return String(parsed.value * factor / 2) +
46716
+ (GAP_FILL_UNIT_SUFFIX[parsed.units] || '');
46717
+ }
46718
+
46719
+ // max-widening: keep an interior gap open only if it is wider than this multiple
46720
+ // of the fill-gaps mouth size (default GAP_MAX_WIDENING_DEFAULT). Must be >= 1
46721
+ // (the threshold cannot be narrower than the mouth itself).
46722
+ function getGapMaxWideningFactor(opts) {
46723
+ if (opts.max_widening == null) return GAP_MAX_WIDENING_DEFAULT;
46724
+ var k = Number(opts.max_widening);
46725
+ if (!(k >= 1)) {
46726
+ stop$1('The max-widening option must be a number >= 1');
46727
+ }
46728
+ return k;
46729
+ }
46730
+
46731
+ // Fill enclosed holes and narrow-mouthed inlets of a polygon mosaic (e.g. a
46732
+ // river up to its mouth) without growing the outer boundary. This is a
46733
+ // topology-aware morphological closing of the mosaic with two thresholds:
46734
+ // - the mouth radius r = mouthSize/2 gates which gaps are sealed off from the
46735
+ // open coast (only openings narrower than the mouth size are closed);
46736
+ // - the fill radius R = k*mouthSize/2 (k = max-widening factor) controls how
46737
+ // far into a sealed inlet the fill reaches and how wide an interior gap must
46738
+ // be to stay open.
46739
+ // Steps:
46740
+ // - the topological buffer grows every feature by R and partitions the
46741
+ // contested space by nearest source (the per-feature dilation, T_R); this
46742
+ // covers every interior gap narrower than k*mouthSize and splits it down the
46743
+ // medial axis;
46744
+ // - the mask is the closing of the land by the mouth radius r: dilate the land
46745
+ // by r, union it (D_r), erode by r. The mask is the original land plus any
46746
+ // gap narrower than the mouth size, with the outward collar pulled back to
46747
+ // the source outline; its interior holes are the gaps wider than the mouth
46748
+ // that lie behind a narrow mouth (rivers' wide reaches, enclosed lakes);
46749
+ // - holes of the mask narrower than k*mouthSize are filled (their max
46750
+ // inscribed circle is smaller than R), so only genuinely large open bodies
46751
+ // (e.g. the Great Lakes) stay open;
46752
+ // - clipping T_R to the filled mask drops the R-wide collar and the kept-open
46753
+ // bodies but keeps each feature's original area plus its medial share of the
46754
+ // narrow-gap fill.
46755
+ // Feature order is preserved so source attributes stay aligned.
46756
+ function makeGapFillPolygonBuffer(lyr, dataset, opts) {
46757
+ var parsed = parseFillGaps(opts);
46758
+ var k = getGapMaxWideningFactor(opts);
46759
+ var mouthRadius = fillGapsRadiusStr(parsed, 1); // r = mouthSize/2
46760
+ var fillRadius = fillGapsRadiusStr(parsed, k); // R = k*mouthSize/2
46761
+ var baseOpts = Object.assign({}, opts, {fill_gaps: false, max_widening: null});
46762
+ // fill-gaps is inherently topological and builds its partition at the larger
46763
+ // fill radius R, so every sub-buffer overrides the radius (and turns on the
46764
+ // topological pipeline for the fill dilation) regardless of the flags the user
46765
+ // passed -- topological need not be given explicitly.
46766
+ var fillOpts = Object.assign({}, baseOpts, {radius: fillRadius, topological: true});
46767
+ // Fill + medial partition at R so interior pockets up to k*mouthSize wide are
46768
+ // covered and split by nearest source.
46769
+ var dilated = makePolygonBuffer(lyr, dataset, fillOpts);
46770
+ // The medial debug views (debug-delaunay/-voronoi) and debug-offset short-
46771
+ // circuit makePolygonBuffer and return their construction directly; since
46772
+ // fill-gaps builds at R, that early return IS the debug output we want, so
46773
+ // pass it straight through instead of trying to mask/clip a debug dataset.
46774
+ if (opts.debug_delaunay || opts.debug_voronoi || opts.debug_offset) {
46775
+ return dilated;
46776
+ }
46777
+ var dilatedLyr = dilated.layers[0];
46778
+ if (!dilatedLyr || !dilated.arcs) return dilated;
46779
+ // Mouth-gating mask: closing of the land by the mouth radius r (dilate by r,
46780
+ // union, erode by r). Built separately from the fill dilation because the
46781
+ // mouth threshold stays at the mouth size while the fill reaches further.
46782
+ var dilatedAtR = makePolygonBuffer(lyr, dataset,
46783
+ Object.assign({}, baseOpts, {radius: mouthRadius, topological: false}));
46784
+ var union = unionBufferDataset(dilatedAtR, baseOpts);
46785
+ if (!union || !union.arcs) return dilated;
46786
+ var closing = makePolygonBuffer(union.layers[0], union,
46787
+ Object.assign({}, baseOpts, {radius: '-' + mouthRadius, topological: false}));
46788
+ if (!closing || !closing.arcs) return dilated;
46789
+ // Fill the mask's interior gaps that are narrower than k*mouthSize (keep wider
46790
+ // ones open) so the clip below fills them too.
46791
+ var keepRadius = getCoordinateDistance(
46792
+ parseConstantBufferDistance(fillRadius, getDatasetCRS(dataset)) || 0,
46793
+ closing.arcs);
46794
+ fillNarrowMaskHoles(closing, keepRadius);
46795
+ // Carry the source attributes through the clip: clipping drops fully-empty
46796
+ // features, so attach a per-feature data table (aligned 1:1 with the dilation)
46797
+ // before clipping rather than relying on a post-hoc count-match copy.
46798
+ if (lyr.data && dilatedLyr.shapes &&
46799
+ dilatedLyr.shapes.length == lyr.data.size()) {
46800
+ dilatedLyr.data = lyr.data.clone();
46801
+ }
46802
+ // remove_slivers: the dilation-vs-mask clip carves thin slivers along its
46803
+ // boundary (a feature can pick up dozens of degenerate edge slivers); clip's
46804
+ // own sliver filter drops the rings that mix dilation and mask arcs and fail a
46805
+ // compactness-weighted area test, leaving the substantial fill regions intact.
46806
+ clipLayersInPlace(dilated.layers, closing, dilated, 'clip',
46807
+ {no_cleanup: true, no_warn: true, remove_slivers: true});
46808
+ return dilated;
46809
+ }
46810
+
46811
+ // Remove interior rings (holes) of the closing mask whose largest inscribed
46812
+ // circle has radius smaller than keepRadius, i.e. gaps narrower than 2*keepRadius
46813
+ // (= k*mouthSize). Dropping the hole ring makes the mask solid there, so the
46814
+ // clip fills the gap; wide gaps (large lakes) keep their ring and stay open. The
46815
+ // inscribed radius is estimated from the gap's pole of inaccessibility (the
46816
+ // anchor point), the same way label points are placed. Modifies the mask in
46817
+ // place; the clip rebuilds topology afterward, so dropped rings need no cleanup.
46818
+ function fillNarrowMaskHoles(maskDataset, keepRadius) {
46819
+ var arcs = maskDataset.arcs;
46820
+ var lyr = maskDataset.layers[0];
46821
+ if (!lyr || !arcs || !(keepRadius > 0)) return;
46822
+ lyr.shapes = (lyr.shapes || []).map(function(shape) {
46823
+ if (!shape) return shape;
46824
+ var kept = [];
46825
+ exportPathData(shape, arcs, 'polygon').pathData.forEach(function(path) {
46826
+ if (path.area >= 0) {
46827
+ kept.push(path.ids.concat()); // outer ring: always keep
46828
+ return;
46829
+ }
46830
+ // hole: keep only if wide enough to hold a disk of radius keepRadius
46831
+ var holeShape = [reversePath(path.ids.concat())];
46832
+ var anchor = findAnchorPoint(holeShape, arcs);
46833
+ var radius = anchor ?
46834
+ getPointToShapeDistance(anchor.x, anchor.y, holeShape, arcs) : 0;
46835
+ if (radius >= keepRadius) {
46836
+ kept.push(path.ids.concat());
46837
+ }
46838
+ });
46839
+ return kept.length > 0 ? kept : null;
46840
+ });
46841
+ }
46842
+
46843
+ // Dissolve every feature of a buffered dataset into a single union polygon (the
46844
+ // morphological dilation). The per-feature dilation tiles the union exactly, so
46845
+ // merging all rings into one shape and boundary-dissolving yields the union
46846
+ // outline (with any wide enclosed holes preserved). Returns null if empty.
46847
+ function unionBufferDataset(dataset, opts) {
46848
+ var lyr = dataset.layers[0];
46849
+ if (!lyr || !dataset.arcs) return null;
46850
+ var coords = [];
46851
+ (lyr.shapes || []).forEach(function(shp) {
46852
+ if (shp) coords = coords.concat(getPolygonMultiPolygonCoords(shp, dataset.arcs));
46853
+ });
46854
+ if (coords.length === 0) return null;
46855
+ var unionDataset = getBufferDataset(coords);
46856
+ if (!unionDataset.arcs) return null;
46857
+ dissolveBufferDataset2(unionDataset, Object.assign({}, opts, {winding_fill: false}));
46858
+ return unionDataset;
46859
+ }
46860
+
46861
+ // Raw (undissolved) offset rings for the polygon buffer's debug-offset view.
46862
+ // Drives the same per-ring makers the real construction uses, so the view shows
46863
+ // exactly what is built and 'no-loop-removal' has a visible effect (loop removal
46864
+ // runs inside the maker -- see buildOneSidedRings). Positive grow uses the
46865
+ // clean-outline maker by default (the band maker under band-method/topological);
46866
+ // holes are eroded with the band maker reversed to outer orientation (matching
46867
+ // makeOutlineBufferGeometry); negative (erode) buffers offset every ring inward
46868
+ // with the band maker.
46869
+ function makePolygonDebugOffsetGeoJSON(lyr, dataset, opts) {
46870
+ var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
46871
+ var useOutline = !opts.band_method && !opts.topological;
46872
+ var leftOpts = useOutline ? Object.assign({}, opts, {outline: true}) : opts;
46873
+ var leftMaker = getPolygonRingBufferMaker(dataset, leftOpts, 'left');
46874
+ var rightMaker = getPolygonRingBufferMaker(dataset,
46875
+ Object.assign({}, opts, {outline: false}), 'right');
46876
+ var geometries = lyr.shapes.map(function(shape, i) {
46877
+ var distance = distanceFn(i);
46878
+ if (!distance || !shape) return null;
46879
+ var coords;
46880
+ if (distance > 0) {
46881
+ var rings = splitShapeRingsByArea(shape, dataset.arcs);
46882
+ coords = getBufferMultiPolygonCoords(rings.outer, distance, leftMaker);
46883
+ if (rings.holes.length > 0) {
46884
+ coords = coords.concat(getBufferMultiPolygonCoords(
46885
+ rings.holes.map(reversePath), distance, rightMaker));
46886
+ }
46887
+ } else {
46888
+ coords = getBufferMultiPolygonCoords(shape, -distance, rightMaker);
46889
+ }
46890
+ return coords.length > 0 ? {type: 'MultiPolygon', coordinates: coords} : null;
46891
+ });
46892
+ return {type: 'GeometryCollection', geometries: geometries};
46893
+ }
46894
+
46895
+ // Clean-outline polygon buffer (the default polygon-grow construction): offset
46896
+ // each source ring to a single self-contained closed loop (no source-path band
46897
+ // edge), strip self-overlaps with the crossing-direction loop remover (safe
46898
+ // because a single offset loop has a consistent +/-1 base winding), then union
46899
+ // the loops by winding number. The loops carry far fewer rings and self-
46900
+ // intersections into the dissolve than the band-ribbon construction, and the
46901
+ // direction remover collapses more overshoot loops than the source-turn gate.
46902
+ //
46903
+ // Used for the positive (grow) buffer. The outer source rings are offset
46904
+ // outward with the fast clean-outline construction (a single self-contained loop
46905
+ // per ring, crossing-direction loop removal, winding union) -- this is where the
46906
+ // method pays off, since a large coastline's outer boundary dominates the
46907
+ // dissolve cost. Holes shrink, which is an INWARD offset; an inward outline
46908
+ // offset is fragile (its elbow insets self-cross on concave holes and invert on
46909
+ // over-shrink, and mapshaper's winding flood can't tell a collapsed contour from
46910
+ // a valid one because it normalizes orientation), so holes are shrunk with the
46911
+ // proven band-ribbon erode and then carved out of the grown outer (see
46912
+ // makeOutlineBufferGeometry). Negative (erode) buffers fall back to the band
46913
+ // construction entirely.
46914
+ function makeOutlinePolygonBufferGeoJSON(lyr, dataset, opts) {
46915
+ var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
46916
+ var hasPositiveDistance = false;
46917
+ var hasNegativeDistance = false;
46918
+ // Force the clean-outline construction on the outer maker (this is the
46919
+ // default polygon-grow path, so it must not depend on a command-line flag).
46920
+ var outlineOpts = Object.assign({}, opts, {outline: true});
46921
+ var outerMaker = getPolygonRingBufferMaker(dataset, outlineOpts, 'left');
46922
+ // Band maker for negative buffers and for shrinking holes.
46923
+ var bandOpts = Object.assign({}, opts, {outline: false});
46924
+ var bandRightMaker = getPolygonRingBufferMaker(dataset, bandOpts, 'right');
46925
+ // Left winding-fill maker for the nested-fill fallback (see below).
46926
+ var bandLeftMaker = getPolygonRingBufferMaker(dataset, bandOpts, 'left');
46927
+ var holeEroder = function(holeShape, dist) {
46928
+ return makeNegativePolygonBufferGeometry(holeShape, dist, dataset, bandOpts,
46929
+ bandRightMaker);
46930
+ };
46931
+ var geometries = lyr.shapes.map(function(shape, i) {
46932
+ var distance = distanceFn(i);
46933
+ if (!distance || !shape) return null;
46934
+ if (distance < 0) {
46935
+ hasNegativeDistance = true;
46936
+ return makeNegativePolygonBufferGeometry(shape, -distance, dataset,
46937
+ bandOpts, bandRightMaker);
46938
+ }
46939
+ hasPositiveDistance = true;
46940
+ var geom;
46941
+ if (shapeHasFillInsideHole(shape, dataset.arcs)) {
46942
+ // The clean-outline construction grows ALL of a shape's outer rings into
46943
+ // one solid fill before carving holes (makeOutlineBufferGeometry), which
46944
+ // absorbs a fill nested inside a hole: the nested fill's offset loop just
46945
+ // adds winding to the surrounding solid, and the later hole carve removes
46946
+ // the region where it lived. The winding-fill construction instead groups
46947
+ // rings by containment and buffers each group independently, so the nested
46948
+ // fill is grown on its own and survives. It is slower, so use it only for
46949
+ // the rare shapes that actually nest a fill inside a hole.
46950
+ geom = makePositivePolygonBufferGeometry(shape, distance, dataset, opts,
46951
+ bandLeftMaker);
46952
+ } else {
46953
+ geom = makeOutlineBufferGeometry(shape, dataset.arcs, distance, opts,
46954
+ outerMaker, holeEroder);
46955
+ }
46956
+ if (opts.polar && shapeReachesPole(shape, dataset.arcs)) {
46957
+ // A pole-touching ring collapses onto the pole line during Mercator
46958
+ // offset construction, so the outline keeps only the coastline; append
46959
+ // the source rings and let the dataset-level dissolve union them into the
46960
+ // full grown shape (same handling as the band construction).
46961
+ geom = appendSourceRings(geom, shape, dataset.arcs);
46962
+ }
46963
+ return geom;
46964
+ });
46965
+ return {
46966
+ geojson: {
46967
+ type: 'GeometryCollection',
46968
+ geometries: geometries
46969
+ },
46970
+ dissolveAfterSplit: hasPositiveDistance && !hasNegativeDistance
46971
+ };
46972
+ }
46973
+
46974
+ // True if the shape has a fill (positive-area ring) nested directly inside a
46975
+ // hole (negative-area ring) -- e.g. an island sitting in a lake. The clean-
46976
+ // outline grow can't represent such a fill (it grows all outer rings into one
46977
+ // solid before carving holes, absorbing the nested fill), so callers route
46978
+ // these shapes to the winding-fill construction instead.
46979
+ function shapeHasFillInsideHole(shape, arcs) {
46980
+ var paths = exportPathData(shape, arcs, 'polygon').pathData;
46981
+ var fills = 0, holes = 0;
46982
+ paths.forEach(function(p) {
46983
+ if (p.area > 0) fills++;
46984
+ else if (p.area < 0) holes++;
46985
+ });
46986
+ // Nesting a fill inside a hole needs at least two fills (a container and the
46987
+ // nested one) and at least one hole; bail cheaply otherwise (the common case)
46988
+ // before building the spatial index.
46989
+ if (fills < 2 || holes < 1) return false;
46990
+ var ringShapes = paths.map(function(p) { return [p.ids]; });
46991
+ var index = new PathIndex(ringShapes, arcs);
46992
+ return paths.some(function(p) {
46993
+ if (p.area <= 0) return false; // only fills can be nested inside a hole
46994
+ var containerId = index.findSmallestEnclosingPolygon(p.ids);
46995
+ return containerId > -1 && paths[containerId].area < 0;
46996
+ });
46997
+ }
46998
+
46999
+ // Split a shape's rings into outer rings (positive signed area) and hole rings
47000
+ // (negative signed area), keeping each ring's arc ids.
47001
+ function splitShapeRingsByArea(shape, arcs) {
47002
+ var outer = [];
47003
+ var holes = [];
47004
+ exportPathData(shape, arcs, 'polygon').pathData.forEach(function(path) {
47005
+ (path.area < 0 ? holes : outer).push(path.ids.concat());
47006
+ });
47007
+ return {outer: outer, holes: holes};
47008
+ }
47009
+
47010
+ function makeOutlineBufferGeometry(shape, arcs, distance, opts, outerMaker,
47011
+ holeEroder) {
47012
+ var rings = splitShapeRingsByArea(shape, arcs);
47013
+ var outerLoops = rings.outer.length > 0 ?
47014
+ getBufferMultiPolygonCoords(rings.outer, distance, outerMaker) : [];
47015
+ if (outerLoops.length === 0) return null;
47016
+ // Resolve the outer offset loops' self-overlaps into clean grown polygons.
47017
+ var coords = dissolveOffsetRingsToCoords(outerLoops, opts);
47018
+ if (coords.length === 0) return null;
47019
+ // Strip artifact holes left by the outer grow (the offset loops dissolve into
47020
+ // a clean fill, so any interior ring is a self-overlap artifact) BEFORE
47021
+ // carving the real holes. The artifact filter's heuristic can otherwise
47022
+ // delete a legitimately-shrunk hole whose eroded boundary happens to run near
47023
+ // the source outline; the carved holes below are explicit and known-good, so
47024
+ // they must not pass through it.
47025
+ var grown = removePositiveBufferArtifactHoles(
47026
+ {type: 'MultiPolygon', coordinates: coords}, shape, arcs, distance);
47027
+ coords = grown ? grown.coordinates : [];
47028
+ if (coords.length === 0) return null;
47029
+ if (rings.holes.length > 0) {
47030
+ // Shrink the holes (an inward offset) with the band erode: treat each hole
47031
+ // as a polygon to erode by reversing it to outer (CCW) orientation, then
47032
+ // carve the eroded regions out of the grown outer.
47033
+ var holeShape = rings.holes.map(reversePath);
47034
+ var holeGeom = holeEroder(holeShape, distance);
47035
+ if (holeGeom && holeGeom.coordinates.length > 0) {
47036
+ coords = subtractHolesFromOuter(coords, holeGeom.coordinates);
45416
47037
  }
45417
47038
  }
45418
- return dataset2;
47039
+ if (coords.length === 0) return null;
47040
+ return {type: 'MultiPolygon', coordinates: coords};
47041
+ }
47042
+
47043
+ // Carve clean shrunk-hole regions out of clean grown-outer polygons. Both arrive
47044
+ // as positive (CCW) rings. The winding union can't subtract one nested loop from
47045
+ // another -- GeoJSON import rewinds every outer ring to CCW, so two separately
47046
+ // imported nested loops both read as fill -- so instead we make each hole a
47047
+ // negative-area inner ring of the same shape: reverse the hole rings at the arc
47048
+ // level (reversing GeoJSON coordinates wouldn't survive import's rewind) and let
47049
+ // groupPolygonRings (which classifies a ring as a hole by negative area) nest
47050
+ // each hole into its containing outer. The outer and hole rings are disjoint
47051
+ // (holes lie strictly inside outers), so no intersection cuts are needed.
47052
+ function subtractHolesFromOuter(outerCoords, holeCoords) {
47053
+ if (!holeCoords || holeCoords.length === 0) return outerCoords;
47054
+ var dataset = importGeoJSON({
47055
+ type: 'GeometryCollection',
47056
+ geometries: [
47057
+ {type: 'MultiPolygon', coordinates: outerCoords},
47058
+ {type: 'MultiPolygon', coordinates: holeCoords}
47059
+ ]
47060
+ }, {type: 'polygon'});
47061
+ if (!dataset.arcs) return outerCoords;
47062
+ var shapes = dataset.layers[0].shapes;
47063
+ var outerShape = shapes[0] || [];
47064
+ var holeShape = shapes[1] || [];
47065
+ var merged = outerShape.concat(holeShape.map(reversePath));
47066
+ return getPolygonMultiPolygonCoords(merged, dataset.arcs);
45419
47067
  }
45420
47068
 
45421
47069
  // World rectangle (lng/lat) the polar buffer is clipped to.
@@ -45436,7 +47084,7 @@ ${svg}
45436
47084
  if (polarBufferHasNegativeDistance(lyr, dataset, opts)) {
45437
47085
  stop$1('The polar option does not support negative (erode) buffers yet.');
45438
47086
  }
45439
- var output = makePolygonBufferGeoJSON(lyr, dataset, opts);
47087
+ var output = buildPolygonBufferOutput(lyr, dataset, opts);
45440
47088
  var dataset2 = importGeoJSON(output.geojson, {type: 'polygon'});
45441
47089
  if (dataset2.arcs) {
45442
47090
  if (output.dissolveAfterSplit) {
@@ -45536,9 +47184,9 @@ ${svg}
45536
47184
  }
45537
47185
 
45538
47186
  function getPolygonRingBufferMaker(dataset, opts, side, winding) {
45539
- // The sector-band escape hatch forces the older non-winding construction even
45540
- // for callers that request winding-fill (see the 'sector-band' option).
45541
- var useWinding = !opts.sector_band;
47187
+ // The band-method escape hatch forces the older non-winding construction even
47188
+ // for callers that request winding-fill (see the 'band-method' option).
47189
+ var useWinding = !opts.band_method;
45542
47190
  var makerOpts = Object.assign({}, opts, {
45543
47191
  left: side == 'left',
45544
47192
  right: side == 'right',
@@ -45581,6 +47229,7 @@ ${svg}
45581
47229
  tmpGeometries.push(getPolygonGeometry(shape, dataset.arcs));
45582
47230
  });
45583
47231
 
47232
+ profileStart('topo:offsets');
45584
47233
  shapes.forEach(function(shape, i) {
45585
47234
  var distance = distances[i];
45586
47235
  var pathData, bufferCoords;
@@ -45590,10 +47239,10 @@ ${svg}
45590
47239
  bufferCoords = getBufferMultiPolygonCoords(pathData.paths, distance, bufferMaker);
45591
47240
  // Resolve the winding-fill rings' self-overlaps into a clean polygon (the
45592
47241
  // mosaic's boundary-flood membership cannot), so this feature enters the
45593
- // shared mosaic as an ordinary polygon. The sector-band fallback emits
47242
+ // shared mosaic as an ordinary polygon. The band-method fallback emits
45594
47243
  // boundary-flood-resolvable bands, so it feeds the mosaic directly (as the
45595
47244
  // topological pipeline did before the winding-fill construction).
45596
- if (!opts.sector_band) {
47245
+ if (!opts.band_method) {
45597
47246
  bufferCoords = dissolveOffsetRingsToCoords(bufferCoords, opts);
45598
47247
  }
45599
47248
  if (bufferCoords.length > 0) {
@@ -45604,6 +47253,7 @@ ${svg}
45604
47253
  });
45605
47254
  }
45606
47255
  });
47256
+ profileEnd('topo:offsets');
45607
47257
 
45608
47258
  if (!hasPositiveDistance || tmpGeometries.length === 0) {
45609
47259
  geometries = shapes.map(function() { return null; });
@@ -45613,7 +47263,8 @@ ${svg}
45613
47263
  geometries: tmpGeometries
45614
47264
  }, {type: 'polygon'});
45615
47265
  geometries = makeTopologicalPolygonBufferGeometries(shapes, distances,
45616
- sourceIds, bufferIds, tmpDataset, dataset.arcs);
47266
+ sourceIds, bufferIds, tmpDataset, dataset.arcs,
47267
+ getMedialSimplifyInterval(dataset, opts, distances));
45617
47268
  }
45618
47269
  return {
45619
47270
  geojson: {
@@ -45625,39 +47276,168 @@ ${svg}
45625
47276
  }
45626
47277
 
45627
47278
  function makeTopologicalPolygonBufferGeometries(shapes, distances, sourceIds,
45628
- bufferIds, tmpDataset, sourceArcs) {
45629
- var tmpLyr = tmpDataset.layers[0];
45630
- var nodes = addIntersectionCuts(tmpDataset, {rebuild_topology: true});
47279
+ bufferIds, tmpDataset, sourceArcs, medialSimplifyInterval) {
47280
+ // Inject inter-feature Voronoi (medial-axis) cut lines so the buffer mosaic's
47281
+ // contested tiles are subdivided along the equidistant boundary before the
47282
+ // tiles are assigned (see assignment by nearest source below).
47283
+ profileStart('topo:medial');
47284
+ var dataset = injectMedialCutLines(tmpDataset, shapes, distances, sourceArcs,
47285
+ medialSimplifyInterval);
47286
+ profileEnd('topo:medial');
47287
+ var tmpLyr = dataset.layers.filter(function(l) {
47288
+ return l.geometry_type == 'polygon';
47289
+ })[0];
47290
+ profileStart('topo:intersectionCuts');
47291
+ var nodes = addIntersectionCuts(dataset, {rebuild_topology: true});
47292
+ profileEnd('topo:intersectionCuts');
47293
+ profileStart('topo:mosaicIndex');
45631
47294
  var mosaicIndex = new MosaicIndex(tmpLyr, nodes, {flat: false, no_holes: false});
47295
+ profileEnd('topo:mosaicIndex');
45632
47296
  var pathfind = getRingIntersector(mosaicIndex.nodes);
45633
47297
  var sourceIdIndex = getIdLookup(sourceIds);
45634
47298
  var bufferIdIndex = getIdToFeatureIdLookup(bufferIds);
45635
47299
  var sourceAreas = getSourceShapeAreas(shapes, sourceArcs);
45636
- return shapes.map(function(shape, i) {
47300
+ var sourceInteriorPoints = getSourceInteriorPoints(shapes, sourceArcs);
47301
+ var ownerCtx = createTileOwnerContext(shapes, sourceArcs, sourceAreas,
47302
+ mosaicIndex.nodes.arcs);
47303
+ profileStart('topo:assignTiles');
47304
+ var result = shapes.map(function(shape, i) {
45637
47305
  var distance = distances[i];
45638
47306
  var tileIds, geom;
45639
47307
  if (!distance || !shape) return null;
45640
47308
  tileIds = getTopologicalBufferTileIds(sourceIds[i], bufferIds[i],
45641
- i, mosaicIndex, sourceIdIndex, bufferIdIndex, sourceAreas);
47309
+ i, mosaicIndex, sourceIdIndex, bufferIdIndex, ownerCtx);
45642
47310
  geom = getTileIdsGeometry(tileIds, mosaicIndex, pathfind);
45643
- return removePositiveBufferArtifactHoles(geom, shape, sourceArcs, distance);
47311
+ // Preserve holes that coincide with another feature's source territory (a
47312
+ // tile excluded to prevent buffer overlap, see getTopologicalBufferTileIds);
47313
+ // the single-shape artifact/sliver heuristics would otherwise fill them.
47314
+ return removePositiveBufferArtifactHoles(geom, shape, sourceArcs, distance,
47315
+ {points: sourceInteriorPoints, featureId: i});
47316
+ });
47317
+ profileEnd('topo:assignTiles');
47318
+ return result;
47319
+ }
47320
+
47321
+ // Build the working dataset for the topological mosaic: the source/buffer
47322
+ // polygons plus a polyline layer of inter-feature Voronoi cut-lines (so the
47323
+ // contested tiles split along the equidistant boundary). Returns @tmpDataset
47324
+ // unchanged when there are no contested edges (no overlap between features).
47325
+ function injectMedialCutLines(tmpDataset, shapes, distances, arcs, simplifyInterval) {
47326
+ var coordDistances = distances.map(function(d) {
47327
+ return d > 0 ? getCoordinateDistance(d, arcs) : 0;
47328
+ });
47329
+ var medial = buildInterFeatureMedialLines(shapes, coordDistances, arcs,
47330
+ {simplifyInterval: simplifyInterval || 0});
47331
+ if (!medial) return tmpDataset;
47332
+ var lineDataset = importGeoJSON({
47333
+ type: 'GeometryCollection',
47334
+ geometries: [medial]
47335
+ }, {});
47336
+ if (!lineDataset.arcs) return tmpDataset;
47337
+ return mergeDatasets([tmpDataset, lineDataset]);
47338
+ }
47339
+
47340
+ // Smoothing scale for the constructed medial lines, as a multiple of the
47341
+ // buffer's own positional tolerance. Weighted Visvalingam removes sub-tolerance
47342
+ // wiggles (the residual zigzag of the discrete medial sampling) and thins the
47343
+ // uneven vertex density without pulling the partition off the centerline:
47344
+ // keeping the interval near the buffer's accuracy budget bounds any deviation to
47345
+ // what the buffer geometry already tolerates.
47346
+ var MEDIAL_SIMPLIFY_FACTOR = 3; // 2
47347
+
47348
+ // Simplification interval (in source-coordinate units) for the medial cut-lines.
47349
+ // Tied to the buffer tolerance at the largest buffer distance present, so the
47350
+ // medial axis is smoothed at the same scale the buffer outline is approximated.
47351
+ // Returns 0 (no simplification) when tolerance is disabled or all distances are
47352
+ // non-positive.
47353
+ function getMedialSimplifyInterval(dataset, opts, distances) {
47354
+ if (opts.tolerance === 0 || opts.tolerance == '0' || opts.tolerance == '0%') {
47355
+ return 0;
47356
+ }
47357
+ var repDist = 0;
47358
+ for (var i = 0; i < distances.length; i++) {
47359
+ if (distances[i] > repDist) repDist = distances[i];
47360
+ }
47361
+ if (repDist <= 0) return 0;
47362
+ var tolMeters = getBufferToleranceFunction(dataset, opts)(repDist);
47363
+ return getCoordinateDistance(tolMeters, dataset.arcs) * MEDIAL_SIMPLIFY_FACTOR;
47364
+ }
47365
+
47366
+ // Per-feature buffer distances for the debug builders below, in meters and in
47367
+ // source-coordinate units (matching what the topological pipeline computes).
47368
+ function getMedialDebugDistances(lyr, dataset, opts) {
47369
+ var shapes = lyr.shapes || [];
47370
+ var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
47371
+ var distances = shapes.map(function(shape, i) {
47372
+ var d = shape ? distanceFn(i) : 0;
47373
+ return d > 0 ? d : 0;
47374
+ });
47375
+ var coordDistances = distances.map(function(d) {
47376
+ return d > 0 ? getCoordinateDistance(d, dataset.arcs) : 0;
45644
47377
  });
47378
+ return {shapes: shapes, distances: distances, coordDistances: coordDistances};
47379
+ }
47380
+
47381
+ // Build a polyline dataset of the inter-feature medial-axis (Voronoi) cut-lines
47382
+ // for the -buffer debug-voronoi option (topological only). These are the same
47383
+ // lines injected into the mosaic to partition contested space, after the
47384
+ // post-construction smoothing simplification.
47385
+ function makeVoronoiDebugDataset(lyr, dataset, opts) {
47386
+ if (!opts.topological) {
47387
+ warn('debug-voronoi has no effect without the topological option; ignoring');
47388
+ return importGeoJSON({type: 'GeometryCollection', geometries: []}, {});
47389
+ }
47390
+ var d = getMedialDebugDistances(lyr, dataset, opts);
47391
+ var medial = buildInterFeatureMedialLines(d.shapes, d.coordDistances, dataset.arcs,
47392
+ {simplifyInterval: getMedialSimplifyInterval(dataset, opts, d.distances)});
47393
+ var geometries = medial ? [medial] : [];
47394
+ return importGeoJSON({type: 'GeometryCollection', geometries: geometries}, {});
47395
+ }
47396
+
47397
+ // Build a polygon dataset of the Delaunay triangulation of the adaptive sample
47398
+ // sites for the -buffer debug-delaunay option (topological only) -- the mesh the
47399
+ // medial axis is derived from.
47400
+ function makeDelaunayDebugDataset(lyr, dataset, opts) {
47401
+ if (!opts.topological) {
47402
+ warn('debug-delaunay has no effect without the topological option; ignoring');
47403
+ return importGeoJSON({type: 'GeometryCollection', geometries: []}, {type: 'polygon'});
47404
+ }
47405
+ var d = getMedialDebugDistances(lyr, dataset, opts);
47406
+ var tris = buildInterFeatureDelaunay(d.shapes, d.coordDistances, dataset.arcs);
47407
+ // tris is a GeometryCollection of one Polygon per triangle; import it directly
47408
+ // so each triangle is its own feature (selectable/inspectable).
47409
+ return importGeoJSON(tris || {type: 'GeometryCollection', geometries: []},
47410
+ {type: 'polygon'});
45645
47411
  }
45646
47412
 
45647
47413
  function getTopologicalBufferTileIds(sourceId, bufferId, featureId, mosaicIndex,
45648
- sourceIdIndex, bufferIdIndex, sourceAreas) {
47414
+ sourceIdIndex, bufferIdIndex, ownerCtx) {
45649
47415
  var ids = [];
45650
47416
  var index = [];
45651
47417
  addTileIds(ids, index, mosaicIndex.getTileIdsByShapeId(sourceId));
45652
47418
  if (bufferId >= 0) {
45653
47419
  addTileIds(ids, index, mosaicIndex.getTileIdsByShapeId(bufferId).filter(function(tileId) {
45654
47420
  return !tileHasSourcePolygon(tileId, mosaicIndex, sourceIdIndex) &&
45655
- getBufferTileOwnerId(tileId, mosaicIndex, bufferIdIndex, sourceAreas) == featureId;
47421
+ getBufferTileOwnerId(tileId, mosaicIndex, bufferIdIndex, ownerCtx) == featureId;
45656
47422
  }));
45657
47423
  }
45658
47424
  return ids;
45659
47425
  }
45660
47426
 
47427
+ // Per-mosaic context for nearest-source tile ownership. Source-shape segment
47428
+ // indexes and tile representative points are built lazily and cached, since
47429
+ // only a fraction of tiles are contested.
47430
+ function createTileOwnerContext(shapes, sourceArcs, sourceAreas, mosaicArcs) {
47431
+ return {
47432
+ shapes: shapes,
47433
+ sourceArcs: sourceArcs,
47434
+ sourceAreas: sourceAreas,
47435
+ mosaicArcs: mosaicArcs,
47436
+ sourceIndexCache: [],
47437
+ anchorCache: []
47438
+ };
47439
+ }
47440
+
45661
47441
  function addTileIds(memo, index, ids) {
45662
47442
  ids.forEach(function(id) {
45663
47443
  if (index[id]) return;
@@ -45672,29 +47452,110 @@ ${svg}
45672
47452
  });
45673
47453
  }
45674
47454
 
45675
- function getBufferTileOwnerId(tileId, mosaicIndex, bufferIdIndex, sourceAreas) {
45676
- var ownerId = -1;
45677
- var ownerArea = -Infinity;
47455
+ // Tiny relative tolerance for treating two source distances as equal (a tile
47456
+ // sitting on the equidistant boundary), falling back to the largest-area then
47457
+ // lowest-id rule for a deterministic result.
47458
+ var OWNER_DIST_EPS = 1e-6;
47459
+
47460
+ // Owner of a contested buffer tile: among the features whose buffer covers the
47461
+ // tile, the one whose source polygon is nearest to the tile's representative
47462
+ // point. Choosing only among covering features means reassignment never creates
47463
+ // a coverage gap (the tile is inside every candidate's buffer); the Voronoi
47464
+ // cut-lines split tiles along the equidistant boundary so each sub-tile lies on
47465
+ // one source's near side. Ties (a tile straddling the boundary, or one the
47466
+ // medial axis did not reach) fall back to largest area then lowest id.
47467
+ function getBufferTileOwnerId(tileId, mosaicIndex, bufferIdIndex, ownerCtx) {
47468
+ var candidates = [];
45678
47469
  mosaicIndex.getSourceIdsByTileId(tileId).forEach(function(shapeId) {
45679
47470
  var featureId = bufferIdIndex[shapeId];
45680
- var area;
45681
- if (featureId >= 0) {
45682
- area = sourceAreas[featureId];
45683
- if (area > ownerArea || area == ownerArea && featureId < ownerId) {
45684
- ownerId = featureId;
45685
- ownerArea = area;
45686
- }
47471
+ if (featureId >= 0) candidates.push(featureId);
47472
+ });
47473
+ if (candidates.length === 0) return -1;
47474
+ if (candidates.length === 1) return candidates[0];
47475
+ var p = getTileAnchorPoint(tileId, mosaicIndex, ownerCtx);
47476
+ if (!p) return pickLargestAreaFeature(candidates, ownerCtx.sourceAreas);
47477
+ var ownerId = -1;
47478
+ var bestDist = Infinity;
47479
+ var bestArea = -Infinity;
47480
+ for (var i = 0; i < candidates.length; i++) {
47481
+ var featureId = candidates[i];
47482
+ var dist = getPointToSourceDistance(p.x, p.y, featureId, ownerCtx);
47483
+ var area = ownerCtx.sourceAreas[featureId];
47484
+ var tol = bestDist === Infinity ? 0 :
47485
+ OWNER_DIST_EPS * Math.max(1, Math.abs(bestDist), Math.abs(dist));
47486
+ if (dist < bestDist - tol ||
47487
+ (Math.abs(dist - bestDist) <= tol &&
47488
+ (area > bestArea || area == bestArea && featureId < ownerId))) {
47489
+ ownerId = featureId;
47490
+ bestDist = dist;
47491
+ bestArea = area;
47492
+ }
47493
+ }
47494
+ return ownerId;
47495
+ }
47496
+
47497
+ function pickLargestAreaFeature(candidates, sourceAreas) {
47498
+ var ownerId = -1;
47499
+ var ownerArea = -Infinity;
47500
+ candidates.forEach(function(featureId) {
47501
+ var area = sourceAreas[featureId];
47502
+ if (area > ownerArea || area == ownerArea && featureId < ownerId) {
47503
+ ownerId = featureId;
47504
+ ownerArea = area;
45687
47505
  }
45688
47506
  });
45689
47507
  return ownerId;
45690
47508
  }
45691
47509
 
47510
+ // Representative interior point of a mosaic tile, cached by tile id. Uses the
47511
+ // pole of inaccessibility (findAnchorPoint), which is guaranteed to lie inside
47512
+ // the tile -- unlike the centroid, which for a thin or curved contested strip
47513
+ // (the sub-tiles the medial-axis cuts create along a narrow channel) can fall
47514
+ // outside the tile, on the wrong side of the equidistant boundary, and so
47515
+ // misclassify the strip's nearest source. The centroid is used only as a
47516
+ // fallback if the anchor probe fails.
47517
+ function getTileAnchorPoint(tileId, mosaicIndex, ownerCtx) {
47518
+ if (tileId in ownerCtx.anchorCache) return ownerCtx.anchorCache[tileId];
47519
+ var tile = mosaicIndex.mosaic[tileId];
47520
+ var p = findAnchorPoint(tile, ownerCtx.mosaicArcs) ||
47521
+ getPathCentroid(tile[0], ownerCtx.mosaicArcs) || null;
47522
+ ownerCtx.anchorCache[tileId] = p;
47523
+ return p;
47524
+ }
47525
+
47526
+ // Distance from a point to a feature's source polygon, using a lazily-built and
47527
+ // cached chunk-bounds segment index (see buildShapeSegmentIndex).
47528
+ function getPointToSourceDistance(x, y, featureId, ownerCtx) {
47529
+ var index = ownerCtx.sourceIndexCache[featureId];
47530
+ if (!index) {
47531
+ index = ownerCtx.sourceIndexCache[featureId] =
47532
+ buildShapeSegmentIndex(ownerCtx.shapes[featureId], ownerCtx.sourceArcs);
47533
+ }
47534
+ return getPointToIndexedShapeDistance(x, y, index);
47535
+ }
47536
+
45692
47537
  function getSourceShapeAreas(shapes, arcs) {
45693
47538
  return shapes.map(function(shape) {
45694
47539
  return Math.abs(getShapeArea(shape, arcs));
45695
47540
  });
45696
47541
  }
45697
47542
 
47543
+ // One interior point per positive ring-group (part) of every source shape,
47544
+ // tagged with its feature id. A multipart source contributes a point for each
47545
+ // detached part (e.g. a small island ring), so the topological hole filter can
47546
+ // recognize a neighbor's territory even when it is one part of a multipolygon.
47547
+ function getSourceInteriorPoints(shapes, arcs) {
47548
+ var points = [];
47549
+ shapes.forEach(function(shape, featureId) {
47550
+ if (!shape) return;
47551
+ getPolygonRingGroupShapes(shape, arcs).forEach(function(group) {
47552
+ var p = findAnchorPoint(group, arcs);
47553
+ if (p) points.push({x: p.x, y: p.y, featureId: featureId});
47554
+ });
47555
+ });
47556
+ return points;
47557
+ }
47558
+
45698
47559
  function getIdLookup(ids) {
45699
47560
  var index = [];
45700
47561
  ids.forEach(function(id) {
@@ -45862,10 +47723,10 @@ ${svg}
45862
47723
  if (!bufferDataset.arcs) return null;
45863
47724
  // The default offset rings come from the winding-fill maker (one self-
45864
47725
  // overlapping ring per source ring) and must be unioned by winding number;
45865
- // the sector-band fallback emits overlapping bands that a boundary flood
47726
+ // the band-method fallback emits overlapping bands that a boundary flood
45866
47727
  // resolves instead (its maker leaves winding_fill off to match).
45867
47728
  dissolveBufferDataset2(bufferDataset,
45868
- Object.assign({}, opts, {winding_fill: !opts.sector_band}));
47729
+ Object.assign({}, opts, {winding_fill: !opts.band_method}));
45869
47730
  bufferShape = bufferLyr.shapes && bufferLyr.shapes[0];
45870
47731
  if (!bufferShape) return null;
45871
47732
  bufferData = exportPathData(bufferShape, bufferDataset.arcs, 'polygon');
@@ -45894,8 +47755,28 @@ ${svg}
45894
47755
  });
45895
47756
  }
45896
47757
 
45897
- function removePositiveBufferArtifactHoles(geom, shape, arcs, distance) {
47758
+ // True if the buffer geometry has any interior ring (a candidate artifact
47759
+ // hole). A clean positive grow -- the common case, e.g. a hole-free coastline
47760
+ // dissolved by the winding-number fill -- has none, so the (potentially large)
47761
+ // source-shape spatial indexes below need never be built.
47762
+ function bufferGeomHasCandidateHole(geom) {
47763
+ if (geom.type == 'Polygon') return geom.coordinates.length > 1;
47764
+ if (geom.type == 'MultiPolygon') {
47765
+ return geom.coordinates.some(function(polygon) { return polygon.length > 1; });
47766
+ }
47767
+ return false;
47768
+ }
47769
+
47770
+ // @territory (optional, topological pipeline only): {points, featureId} where
47771
+ // points are source-part interior points tagged by feature id; a candidate hole
47772
+ // enclosing another feature's point is that neighbor's territory (excluded to
47773
+ // prevent overlap) and is always kept.
47774
+ function removePositiveBufferArtifactHoles(geom, shape, arcs, distance, territory) {
45898
47775
  if (!geom) return null;
47776
+ // Nothing to filter unless the result actually has interior rings. Skip the
47777
+ // index build (filterArtifactHoles is itself a no-op on hole-free polygons,
47778
+ // so this only avoids needless work, not any classification).
47779
+ if (!bufferGeomHasCandidateHole(geom)) return geom;
45899
47780
  var threshold = getPositiveHoleArtifactThreshold(distance, arcs);
45900
47781
  var minHoleArea = getPositiveHoleArtifactAreaThreshold(distance, arcs);
45901
47782
  var sourceHoles = getSourceHoleShapes(shape, arcs);
@@ -45914,7 +47795,9 @@ ${svg}
45914
47795
  shapeIndex: buildShapeSegmentIndex(shape, arcs),
45915
47796
  pathIndex: shape && shape.length > 0 ? new PathIndex([shape], arcs) : null,
45916
47797
  holeIndex: sourceHoles.length > 0 ?
45917
- buildShapeSegmentIndex(sourceHoles.map(function(h) {return h[0];}), arcs) : null
47798
+ buildShapeSegmentIndex(sourceHoles.map(function(h) {return h[0];}), arcs) : null,
47799
+ territoryPoints: territory ? territory.points : null,
47800
+ featureId: territory ? territory.featureId : -1
45918
47801
  };
45919
47802
  if (geom.type == 'Polygon') {
45920
47803
  geom.coordinates = filterArtifactHoles(geom.coordinates, minHoleArea, ctx);
@@ -46024,11 +47907,40 @@ ${svg}
46024
47907
  function filterArtifactHoles(polygon, minHoleArea, ctx) {
46025
47908
  if (polygon.length < 2) return polygon;
46026
47909
  return [polygon[0]].concat(polygon.slice(1).filter(function(ring) {
47910
+ // A hole over another feature's source is real territory (kept regardless of
47911
+ // size or how deep it sits in this feature's buffer); see getSourceInteriorPoints.
47912
+ if (ringEnclosesOtherTerritory(ring, ctx)) return true;
46027
47913
  return Math.abs(getGeoJSONRingArea(ring)) > minHoleArea &&
46028
47914
  !positiveBufferHoleIsArtifact(ring, ctx);
46029
47915
  }));
46030
47916
  }
46031
47917
 
47918
+ function ringEnclosesOtherTerritory(ring, ctx) {
47919
+ var points = ctx.territoryPoints;
47920
+ if (!points) return false;
47921
+ for (var i = 0; i < points.length; i++) {
47922
+ if (points[i].featureId === ctx.featureId) continue;
47923
+ if (pointInGeoJSONRing(points[i].x, points[i].y, ring)) return true;
47924
+ }
47925
+ return false;
47926
+ }
47927
+
47928
+ // Ray-casting point-in-ring test for a closed GeoJSON ring (array of [x, y],
47929
+ // first == last). Boundary cases are irrelevant here: territory probe points are
47930
+ // well inside their source part, far from any hole-ring edge.
47931
+ function pointInGeoJSONRing(x, y, ring) {
47932
+ var inside = false;
47933
+ for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) {
47934
+ var xi = ring[i][0], yi = ring[i][1];
47935
+ var xj = ring[j][0], yj = ring[j][1];
47936
+ if ((yi > y) !== (yj > y) &&
47937
+ x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
47938
+ inside = !inside;
47939
+ }
47940
+ }
47941
+ return inside;
47942
+ }
47943
+
46032
47944
  function positiveBufferHoleIsArtifact(ring, ctx) {
46033
47945
  var n = ring.length - 1; // skip duplicate endpoint
46034
47946
  var step = Math.max(1, Math.floor(n / 20));
@@ -48528,6 +50440,9 @@ ${svg}
48528
50440
  if (opts.topological && lyr.geometry_type != 'polygon') {
48529
50441
  stop$1('The topological buffer option requires a polygon layer');
48530
50442
  }
50443
+ if (opts.fill_gaps && lyr.geometry_type != 'polygon') {
50444
+ stop$1('The fill-gaps option requires a polygon layer');
50445
+ }
48531
50446
  if (opts.geodesic && !isLatLngCRS(getDatasetCRS(dataset))) {
48532
50447
  // Geodesic buffer of projected data: reproject the source paths through
48533
50448
  // WGS84 lng/lat, run the ordinary (spherical) buffer pipeline, then
@@ -53900,219 +55815,73 @@ ${svg}
53900
55815
  var rec = records[i] || {};
53901
55816
  if (!shp) {
53902
55817
  // 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;
55818
+ shapes2.push(null);
55819
+ records2.push(rec);
55820
+ return;
54056
55821
  }
54057
- return idx;
54058
- }
54059
-
54060
- // Swap item at @idx with any lighter children
54061
- function downHeap(idx) {
54062
- var minIdx = compareDown(idx);
55822
+ outputLines = [];
55823
+ outputKeys = [];
55824
+ outputMatches = [];
55825
+ forEachShapePart(shp, onPart);
55826
+ outputLines.forEach(function(shape2, i) {
55827
+ shapes2.push(shape2);
55828
+ records2.push(utils.extend({}, rec));
55829
+ index2.push(outputMatches[i]);
55830
+ });
55831
+ });
55832
+ polylineLyr.shapes = shapes2;
55833
+ polylineLyr.data = new DataTable(records2);
55834
+ markLayerChanged(polylineLyr, {operation: 'divide', unit: 'shapes-data'});
55835
+ joinTables(polylineLyr.data, polygonLyr.data, function(i) {
55836
+ return index2[i] || [];
55837
+ }, opts);
54063
55838
 
54064
- while (minIdx > idx) {
54065
- swapItems(idx, minIdx);
54066
- idx = minIdx; // descend in the heap
54067
- minIdx = compareDown(idx);
55839
+ function addDividedParts(parts, keys, matches) {
55840
+ var keyId, key;
55841
+ for (var i=0; i<parts.length; i++) {
55842
+ key = keys[i];
55843
+ keyId = outputKeys.indexOf(key);
55844
+ if (keyId == -1) {
55845
+ outputKeys.push(key);
55846
+ outputLines.push([parts[i]]);
55847
+ outputMatches.push(matches[i]);
55848
+ } else {
55849
+ outputLines[keyId].push(parts[i]);
55850
+ }
54068
55851
  }
54069
55852
  }
54070
55853
 
54071
- function swapItems(a, b) {
54072
- var i = heapArr[a];
54073
- insertValue(a, heapArr[b]);
54074
- insertValue(b, i);
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];
55854
+ function getKey(shapeIds) {
55855
+ return shapeIds.sort().join(',');
55856
+ // multiple matches: treat like no match
55857
+ // return shapeIds.length == 1 ? String(shapeIds[0]) : '-1';
54103
55858
  }
54104
55859
 
54105
- function compareDown(idx) {
54106
- var a = 2 * idx + 1,
54107
- b = a + 1,
54108
- n = itemsInHeap;
54109
- if (a < n && heavierThan(idx, a)) {
54110
- idx = a;
54111
- }
54112
- if (b < n && heavierThan(idx, b)) {
54113
- idx = b;
55860
+ // Partition each part
55861
+ function onPart(ids) {
55862
+ var parts2 = [];
55863
+ var keys2 = [];
55864
+ var matches2 = [];
55865
+ var prevKey = null;
55866
+ var containingIds, key, part2, arcId;
55867
+ // assign each arc to a divided shape
55868
+ for (var i=0, n=ids.length; i<n; i++) {
55869
+ arcId = ids[i];
55870
+ containingIds = index.findShapesEnclosingArc(absArcId(arcId));
55871
+ key = getKey(containingIds);
55872
+ if (key === prevKey) {
55873
+ // case: continuation of a part
55874
+ part2.push(arcId);
55875
+ } else {
55876
+ // case: start of a new part
55877
+ part2 = [arcId];
55878
+ parts2.push(part2);
55879
+ keys2.push(key);
55880
+ matches2.push(containingIds);
55881
+ }
55882
+ prevKey = key;
54114
55883
  }
54115
- return idx;
55884
+ addDividedParts(parts2, keys2, matches2);
54116
55885
  }
54117
55886
  }
54118
55887
 
@@ -59573,151 +61342,6 @@ ${svg}
59573
61342
  return m;
59574
61343
  }
59575
61344
 
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
61345
  function getSimplifyMethodLabel(slug) {
59722
61346
  return {
59723
61347
  dp: "Ramer-Douglas-Peucker",
@@ -62073,7 +63697,7 @@ ${svg}
62073
63697
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
62074
63698
  }
62075
63699
 
62076
- var version = "0.7.27";
63700
+ var version = "0.7.28";
62077
63701
 
62078
63702
  // Parse command line args into commands and run them
62079
63703
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.