mapshaper 0.7.26 → 0.7.27

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 +1941 -1686
  2. package/package.json +1 -1
  3. package/www/mapshaper.js +1941 -1686
package/www/mapshaper.js CHANGED
@@ -9248,6 +9248,21 @@
9248
9248
  return parseLocalPath(path).basename;
9249
9249
  }
9250
9250
 
9251
+ // True if a name would be unsafe to use directly as an output filename,
9252
+ // because it could escape the intended output directory. Used to reject
9253
+ // data-derived layer names (e.g. TopoJSON object keys) before they become
9254
+ // filenames. A bare or mid-name colon is allowed on purpose -- it is a valid
9255
+ // filename character on *nix and only addresses an NTFS stream (within the
9256
+ // same directory) on Windows; only a leading drive-letter prefix can escape.
9257
+ function layerNameIsUnsafeFilename(name) {
9258
+ name = String(name);
9259
+ // path separators (directory traversal on any OS) or NUL
9260
+ if (/[\/\\\0]/.test(name)) return true;
9261
+ // Windows drive-relative prefix, e.g. "C:foo" can write to another drive
9262
+ if (/^[A-Za-z]:/.test(name)) return true;
9263
+ return false;
9264
+ }
9265
+
9251
9266
  function getFileExtension(path) {
9252
9267
  return parseLocalPath(path).extension;
9253
9268
  }
@@ -9291,6 +9306,7 @@
9291
9306
  getFileExtension: getFileExtension,
9292
9307
  getOutputFileBase: getOutputFileBase,
9293
9308
  getPathBase: getPathBase,
9309
+ layerNameIsUnsafeFilename: layerNameIsUnsafeFilename,
9294
9310
  parseLocalPath: parseLocalPath,
9295
9311
  replaceFileExtension: replaceFileExtension,
9296
9312
  toLowerCaseExtension: toLowerCaseExtension
@@ -29134,7 +29150,13 @@ ${svg}
29134
29150
 
29135
29151
  function assignUniqueLayerNames(layers) {
29136
29152
  var names = layers.map(function(lyr) {
29137
- return lyr.name || 'layer';
29153
+ var name = lyr.name || 'layer';
29154
+ if (layerNameIsUnsafeFilename(name)) {
29155
+ // Block path-traversal via data-derived layer names (e.g. a malicious
29156
+ // TopoJSON object key like "../owned") before they reach a file write.
29157
+ stop$1('Layer name cannot be used as an output filename (path separator or drive prefix):', name);
29158
+ }
29159
+ return name;
29138
29160
  });
29139
29161
  var uniqueNames = utils.uniqifyNames(names);
29140
29162
  layers.forEach(function(lyr, i) {
@@ -31746,6 +31768,10 @@ ${svg}
31746
31768
  describe: '[projected data] buffer using geodesic distances',
31747
31769
  type: 'flag'
31748
31770
  })
31771
+ .option('polar', {
31772
+ // describe: 'keep lat-long buffers within the valid extent (+/-180, +/-90); for growing polygons sliced at the antimeridian/poles (erode not yet supported)',
31773
+ type: 'flag'
31774
+ })
31749
31775
  .option('vertices', {
31750
31776
  describe: 'number of vertices to use when buffering points (default is 72)',
31751
31777
  type: 'integer'
@@ -41391,46 +41417,77 @@ ${svg}
41391
41417
  if (!ring || ring.length < 6) return ring;
41392
41418
  if (maxTurn === undefined) maxTurn = BUFFER_LOOP_MAX_TURN;
41393
41419
  var gated = !!(srcPos && turnPrefix);
41394
- var pts = ring.slice(0, ring.length - 1); // drop closing duplicate
41395
- var pos = gated ? srcPos.slice(0, ring.length - 1) : null;
41396
- var i = 0;
41397
- while (i < pts.length - 1) {
41398
- var ax = pts[i][0], ay = pts[i][1];
41399
- var bx = pts[i + 1][0], by = pts[i + 1][1];
41400
- var maxJ = Math.min(i + maxGap, pts.length - 2);
41401
- var removed = false;
41402
- for (var j = i + 2; j <= maxJ; j++) {
41403
- var hit = segHit(ax, ay, bx, by,
41404
- pts[j][0], pts[j][1], pts[j + 1][0], pts[j + 1][1]);
41420
+ var n = ring.length - 1; // distinct points (the last repeats the first)
41421
+ // 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
+ var out = [ring[0]];
41432
+ var outPos = gated ? [srcPos[0]] : null;
41433
+ var b = ring[1];
41434
+ var bpos = gated ? srcPos[1] : 0;
41435
+ var r = 2;
41436
+ 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];
41445
+ var hit = segHit(ax, ay, bx, by, c[0], c[1], d[0], d[1]);
41405
41446
  if (!hit) continue;
41406
41447
  // Collapse only covered overshoots (small source turn). A larger source
41407
41448
  // turn means the span may enclose a real buffer hole, which must be left
41408
41449
  // for the dissolve -- whatever the pocket's winding orientation.
41409
- if (gated && !spanIsCovered(pos, i, j, turnPrefix, maxTurn)) continue;
41410
- pts.splice(i + 1, j - i, hit); // replace span i+1..j with the crossing
41411
- if (gated) pos.splice(i + 1, j - i, pos[i]);
41412
- removed = true;
41450
+ if (gated &&
41451
+ !spanIsCovered(apos, bpos, srcPos, r, r + t + 1, turnPrefix, maxTurn)) {
41452
+ continue;
41453
+ }
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;
41413
41461
  break;
41414
41462
  }
41415
- if (!removed) i++; // else re-scan from i: the new seg i may cross again
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++;
41416
41470
  }
41417
- if (pts.length < 4) return ring; // collapsed away; keep original
41418
- pts.push(pts[0].concat());
41419
- return pts;
41471
+ if (out.length < 4) return ring; // collapsed away; keep original
41472
+ out.push(out[0].concat());
41473
+ return out;
41420
41474
  }
41421
41475
 
41422
- // True when the source-path span feeding ring points i..j+1 turns by less than
41423
- // maxTurn (a covered overshoot, safe to collapse). A pocket touching a cap
41424
- // (NaN position) is never treated as a covered overshoot.
41425
- function spanIsCovered(pos, i, j, turnPrefix, maxTurn) {
41426
- var lo = Infinity, hi = -Infinity;
41427
- for (var k = i; k <= j + 1; k++) {
41428
- var p = pos[k];
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
41482
+ var posLo = apos < bpos ? apos : bpos;
41483
+ var posHi = apos > bpos ? apos : bpos;
41484
+ for (var k = lo; k <= hi; k++) {
41485
+ var p = srcPos[k];
41429
41486
  if (p !== p) return false; // NaN
41430
- if (p < lo) lo = p;
41431
- if (p > hi) hi = p;
41487
+ if (p < posLo) posLo = p;
41488
+ if (p > posHi) posHi = p;
41432
41489
  }
41433
- return (turnPrefix[hi] - turnPrefix[lo]) < maxTurn;
41490
+ return (turnPrefix[posHi] - turnPrefix[posLo]) < maxTurn;
41434
41491
  }
41435
41492
 
41436
41493
  // Fast strict-interior segment crossing; returns [x, y] or null. Buffer join
@@ -41976,36 +42033,66 @@ ${svg}
41976
42033
  var R2D = 180 / Math.PI;
41977
42034
  var WEBMERCATOR_WIDTH = R * Math.PI * 2;
41978
42035
 
42036
+ // Polar (clamp-to-extent) buffering bounds (see the 'polar' option). A polygon
42037
+ // sliced for display at the antimeridian/poles has artificial seam edges at
42038
+ // lng = +/-180 and lat = +/-90. The poles are unreachable in Mercator (y ->
42039
+ // +/-Infinity), so clamp construction coordinates to a finite latitude just shy
42040
+ // of the pole; output coords at the bound are snapped back to exactly +/-90.
42041
+ var POLAR_HALF_WIDTH = WEBMERCATOR_WIDTH / 2; // Mercator x at lng +/-180
42042
+ var POLAR_LAT_LIMIT = 90 - 1e-3; // finite near-pole latitude
42043
+ var POLAR_Y_LIMIT = R * Math.atanh(Math.sin(POLAR_LAT_LIMIT * D2R)); // Mercator y at the limit
42044
+ var POLAR_LAT_SNAP = 90 - 2e-3; // snap |lat| >= this to 90
42045
+ var POLAR_LNG_SNAP_EPS = 1e-6; // snap |lng| within this of 180 to exactly 180
42046
+
41979
42047
  // arr: array of MultiPolygon and/or Point Features
41980
- function unprojectFeatures(arr) {
42048
+ // opts.polar: snap near-boundary coords to exactly +/-180 / +/-90
42049
+ function unprojectFeatures(arr, opts) {
42050
+ var polar = !!(opts && opts.polar);
41981
42051
  arr.forEach(function(feat) {
41982
42052
  var coords = feat.geometry.coordinates;
41983
42053
  var type = feat.geometry.type;
41984
42054
  if (type == 'Point') {
41985
- unprojectPointCoords(coords);
42055
+ unprojectPointCoords(coords, polar);
41986
42056
  } else if (type == 'MultiPolygon') {
41987
- coords.forEach(unprojectPolygonCoords);
42057
+ coords.forEach(function(c) { unprojectPolygonCoords(c, polar); });
41988
42058
  } else {
41989
42059
  error('Unexpected geometry type:', type);
41990
42060
  }
41991
42061
  });
41992
42062
  }
41993
42063
 
41994
- function unprojectPolygonCoords(coords) {
42064
+ function unprojectPolygonCoords(coords, polar) {
41995
42065
  forEachPoint(coords, function(p) {
41996
- unprojectPointCoords(p);
42066
+ unprojectPointCoords(p, polar);
41997
42067
  });
41998
42068
  }
41999
42069
 
42000
- function unprojectPointCoords(p) {
42070
+ function unprojectPointCoords(p, polar) {
42001
42071
  var p2 = fromWebMercator(p[0], p[1]);
42072
+ if (polar) {
42073
+ // The buffer is allowed to expand past the antimeridian during construction
42074
+ // (walls run out to lng > 180 / < -180); it is clipped to the world
42075
+ // rectangle afterwards, so DON'T fold the overshoot here -- folding would
42076
+ // collapse it onto a spurious vertical edge at +/-180 instead of cutting it.
42077
+ // Pin pole latitudes (Mercator can't represent the pole, so offsets are
42078
+ // capped just shy of it) and snap values within rounding of +/-90 / +/-180
42079
+ // to the exact bound for clean clip edges.
42080
+ if (p2[1] >= POLAR_LAT_SNAP) p2[1] = 90;
42081
+ else if (p2[1] <= -POLAR_LAT_SNAP) p2[1] = -90;
42082
+ if (Math.abs(p2[0] - 180) < POLAR_LNG_SNAP_EPS) p2[0] = 180;
42083
+ else if (Math.abs(p2[0] + 180) < POLAR_LNG_SNAP_EPS) p2[0] = -180;
42084
+ }
42002
42085
  p[0] = p2[0];
42003
42086
  p[1] = p2[1];
42004
42087
  }
42005
42088
 
42006
- // Wrap a shape iterator to convert lng,lat coords to spherical Mercator coords
42007
- function getProjectingPathIterator(arcs) {
42089
+ // Wrap a shape iterator to convert lng,lat coords to spherical Mercator coords.
42090
+ // opts.polar: clamp an input latitude that sits exactly at a pole to a finite
42091
+ // near-pole value so construction is well-defined (Mercator y is infinite at the
42092
+ // pole). The antimeridian is left alone here (see getProjectingPathIterator body).
42093
+ function getProjectingPathIterator(arcs, opts) {
42008
42094
  var iter = new ShapeIter(arcs);
42095
+ var polar = !!(opts && opts.polar);
42009
42096
  var prevX;
42010
42097
  var iter2 = {
42011
42098
  x: 0,
@@ -42019,6 +42106,15 @@ ${svg}
42019
42106
  var p;
42020
42107
  if (val) {
42021
42108
  p = toWebMercator(iter.x, iter.y);
42109
+ if (polar) {
42110
+ // Pin a seam latitude that sits exactly at a pole (lat +/-90 -> y is
42111
+ // infinite in Mercator) to a finite near-pole value so construction is
42112
+ // well-defined; the result is snapped back to +/-90 on output. The
42113
+ // antimeridian walls are left unclamped here (clamping x mid-path
42114
+ // breaks the unwrap continuity and tangles corner joins).
42115
+ if (p[1] > POLAR_Y_LIMIT) p[1] = POLAR_Y_LIMIT;
42116
+ else if (p[1] < -POLAR_Y_LIMIT) p[1] = -POLAR_Y_LIMIT;
42117
+ }
42022
42118
  iter2.x = prevX === null ? p[0] : unwrapX(p[0], prevX);
42023
42119
  iter2.y = p[1];
42024
42120
  prevX = iter2.x;
@@ -42029,6 +42125,29 @@ ${svg}
42029
42125
  return iter2;
42030
42126
  }
42031
42127
 
42128
+ // Stop a great-circle offset from crossing a pole (see the 'polar' option). When
42129
+ // the offset would cross a pole it reappears at the opposite longitude (Mercator
42130
+ // x jumps by ~half the world width) at a latitude back inside the valid range;
42131
+ // detect that jump and pin the offset to the pole at the source longitude
42132
+ // instead, so the floor edge of a pole-sliced polygon stays near the pole rather
42133
+ // than folding to the far meridian. Latitudes are capped so a near-pole offset
42134
+ // cannot exceed the finite construction bound.
42135
+ function clampPolar(base) {
42136
+ return function(x, y, bearing, dist) {
42137
+ var p = base(x, y, bearing, dist);
42138
+ if (Math.abs(p[0] - x) > POLAR_HALF_WIDTH * 0.5) {
42139
+ // great-circle offset crossed a pole (reappears at the opposite longitude,
42140
+ // i.e. Mercator x jumps by ~half the world width); pin it to the pole at the
42141
+ // source longitude instead of folding across to the far meridian.
42142
+ p[0] = x;
42143
+ p[1] = p[1] < 0 ? -POLAR_Y_LIMIT : POLAR_Y_LIMIT;
42144
+ }
42145
+ if (p[1] < -POLAR_Y_LIMIT) p[1] = -POLAR_Y_LIMIT;
42146
+ else if (p[1] > POLAR_Y_LIMIT) p[1] = POLAR_Y_LIMIT;
42147
+ return p;
42148
+ };
42149
+ }
42150
+
42032
42151
  function getOffsetFunction(crs, opts) {
42033
42152
  if (!isLatLngCRS(crs)) {
42034
42153
  return getPlanarSegmentEndpoint;
@@ -42047,7 +42166,8 @@ ${svg}
42047
42166
  // line in Mercator), so the endpoint falls short of the great-circle distance
42048
42167
  // and drifts in direction as the radius grows -- large buffers come out
42049
42168
  // distorted (egg-shaped caps/joins) -- but it is occasionally useful.
42050
- return opts && opts.rhumb ? rhumbOffset : greatCircleOffset;
42169
+ var offset = opts && opts.rhumb ? rhumbOffset : greatCircleOffset;
42170
+ return opts && opts.polar ? clampPolar(offset) : offset;
42051
42171
  }
42052
42172
 
42053
42173
  // Great-circle (spherical geodesic) offset, computed directly in Mercator coords.
@@ -42121,7 +42241,7 @@ ${svg}
42121
42241
  var roundJoinSegAngle = 90 / roundJoinSegsPerQuadrant;
42122
42242
  var capStyle = opts.cap_style || 'round'; // expect 'round' or 'flat'
42123
42243
  var pathIter = useMercator ?
42124
- getProjectingPathIterator(dataset.arcs) : new ShapeIter(dataset.arcs);
42244
+ getProjectingPathIterator(dataset.arcs, opts) : new ShapeIter(dataset.arcs);
42125
42245
  var latLngPathIter = useMercator ? new ShapeIter(dataset.arcs) : null;
42126
42246
  var builder = new BufferBuilder();
42127
42247
  var simplifyIntervalFn = getBufferSimplifyFunction(dataset, opts);
@@ -42129,7 +42249,9 @@ ${svg}
42129
42249
 
42130
42250
  function makeBufferGeoJSON(shape, distance) {
42131
42251
  var rings = [];
42132
- if (useMercator) {
42252
+ if (useMercator && !opts.polar) {
42253
+ // With the polar option, the clamp pins offsets to the valid extent
42254
+ // instead of erroring near a pole (see getOffsetFunction / clampPolar).
42133
42255
  stopIfBufferReachesPole(shape, distance);
42134
42256
  }
42135
42257
  (shape || []).forEach(function(path, i) {
@@ -42147,7 +42269,7 @@ ${svg}
42147
42269
  }
42148
42270
  }];
42149
42271
  if (useMercator) {
42150
- unprojectFeatures(features);
42272
+ unprojectFeatures(features, opts);
42151
42273
  if (opts.debug_offset) {
42152
42274
  splitAntimeridianCrosses(features);
42153
42275
  }
@@ -42167,7 +42289,8 @@ ${svg}
42167
42289
  }
42168
42290
  });
42169
42291
  if (maxAbsLat + angularDist >= 90 - POLAR_BUFFER_MARGIN_DEGREES) {
42170
- stop$1('Buffering lat-long coordinates near the poles is not supported.');
42292
+ stop$1('Buffering lat-long coordinates near the poles is not supported; ' +
42293
+ 'use the polar option for polygons sliced at the antimeridian/poles.');
42171
42294
  }
42172
42295
  }
42173
42296
 
@@ -42217,7 +42340,7 @@ ${svg}
42217
42340
  if (simplifyIntervalFn) {
42218
42341
  verts = presimplifyPathVerts(verts, simplifyIntervalFn(dist), dist);
42219
42342
  }
42220
- if (!opts.no_outline && !oneSidedBuffer && !opts.sector_band && pathIsOpen(verts)) {
42343
+ if (!oneSidedBuffer && !opts.sector_band && pathIsOpen(verts)) {
42221
42344
  // Fast path for ordinary two-sided line buffers: emit one closed
42222
42345
  // outline instead of many per-segment bands that must be dissolved.
42223
42346
  // The sector-band escape hatch skips it to fall through to the
@@ -42246,15 +42369,17 @@ ${svg}
42246
42369
  // (no winding_fill) and the source-path edge get no provenance and pass
42247
42370
  // through unchanged.
42248
42371
  //
42249
- // Restricted to closed source rings: a closed ring's offset overshoots are
42250
- // doubly-covered (safe to collapse), but an OPEN one-sided arc (e.g. a
42251
- // topological polygon's unbuffered-boundary remnant, buffered with caps)
42252
- // can have a concave-join dent that is its region's only coverage, which
42253
- // collapsing would cut away.
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.
42254
42379
  function buildOneSidedRings(sideVerts) {
42255
42380
  var built = makeLeftBufferRings(sideVerts, dist,
42256
42381
  oneSidedBuffer ? pathSideVerts : null);
42257
- if (opts.no_loop_removal || !opts.buffer_ring_loops || pathIsOpen(sideVerts)) {
42382
+ if (opts.no_loop_removal || !opts.winding_fill || pathIsOpen(sideVerts)) {
42258
42383
  return built.rings;
42259
42384
  }
42260
42385
  var turnPrefix = getSourceTurnPrefix(sideVerts);
@@ -43111,7 +43236,15 @@ ${svg}
43111
43236
  if (i > 0) {
43112
43237
  joinP = bufferSegmentIntersection(a, b, c, d);
43113
43238
  if (!joinP) {
43114
- throw Error(`no intersection on ${i} of ${pointCount}`);
43239
+ if (opts.polar) {
43240
+ // Near a clamped pole/antimeridian corner the swept offset tangents
43241
+ // collapse onto the boundary and stop intersecting; hug the clamped
43242
+ // tangent point so the round join degenerates to the pinned corner
43243
+ // instead of throwing.
43244
+ points.push(tanP);
43245
+ } else {
43246
+ throw Error(`no intersection on ${i} of ${pointCount}`);
43247
+ }
43115
43248
  } else {
43116
43249
  points.push(joinP);
43117
43250
  }
@@ -43278,8 +43411,13 @@ ${svg}
43278
43411
  var useWinding = oneSided && !debug && opts.winding_fill !== false &&
43279
43412
  !opts.sector_band;
43280
43413
  if (useWinding) {
43414
+ // no_loop_removal: the one-sided construction's overshoot loops can be a
43415
+ // concave bend's only band coverage, so collapsing them would cut holes in
43416
+ // the buffer (see buildOneSidedRings); the winding dissolve fills the
43417
+ // self-overlap instead.
43281
43418
  var result = makePolylineBufferPerFeature(lyr, dataset,
43282
- Object.assign({}, opts, {winding_fill: true, remove_lobes: false}));
43419
+ Object.assign({}, opts, {winding_fill: true, remove_lobes: false,
43420
+ no_loop_removal: true}));
43283
43421
  if (spherical) splitAntimeridianBufferDataset(result);
43284
43422
  timeEnd$1('buffer');
43285
43423
  return result;
@@ -43289,8 +43427,18 @@ ${svg}
43289
43427
  // Debug visualizations (raw offset rings, winding tiles, mosaic) want the
43290
43428
  // whole layer's geometry/topology in one dataset; keep the original global
43291
43429
  // dissolve for them (no artifact-hole filter runs in debug mode anyway).
43292
- dataset2 = importGeoJSON(makeShapeBufferGeoJSON(lyr, dataset, opts), {});
43293
- dissolveBufferDataset2(dataset2, Object.assign({}, opts, {per_part_holes: true}));
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
43434
+ // flag has a visible effect. (See makePolylineBufferTwoSidedPerFeature.)
43435
+ var debugWinding = !oneSided && layerIsAllClosed(lyr, dataset.arcs);
43436
+ var debugMakerOpts = debugWinding ?
43437
+ Object.assign({}, opts, {winding_fill: true}) : opts;
43438
+ var debugDissolveOpts = Object.assign({}, opts, {per_part_holes: true},
43439
+ debugWinding ? {winding_fill: true} : null);
43440
+ dataset2 = importGeoJSON(makeShapeBufferGeoJSON(lyr, dataset, debugMakerOpts), {});
43441
+ dissolveBufferDataset2(dataset2, debugDissolveOpts);
43294
43442
  } else {
43295
43443
  dataset2 = makePolylineBufferTwoSidedPerFeature(lyr, dataset, opts, spherical);
43296
43444
  }
@@ -43312,11 +43460,37 @@ ${svg}
43312
43460
  // the process out of memory; per-feature isolation bounds peak memory to the
43313
43461
  // most complex single feature. The dissolve collapses any internal cuts, so
43314
43462
  // each feature's output boundary is identical to the global pipeline's.
43463
+ // True if every part of a polyline shape is a closed ring (first vertex ==
43464
+ // last vertex), so its two-sided buffer is an annulus per ring.
43465
+ function shapeIsAllClosed(shape, arcs) {
43466
+ return !!shape && shape.length > 0 && shape.every(function(part) {
43467
+ return pathIsClosed(part, arcs);
43468
+ });
43469
+ }
43470
+
43471
+ // True if every shape in the layer is made entirely of closed rings.
43472
+ function layerIsAllClosed(lyr, arcs) {
43473
+ var shapes = lyr.shapes || [];
43474
+ return shapes.length > 0 && shapes.every(function(shape) {
43475
+ return shapeIsAllClosed(shape, arcs);
43476
+ });
43477
+ }
43478
+
43315
43479
  function makePolylineBufferTwoSidedPerFeature(lyr, dataset, opts, spherical) {
43316
43480
  var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
43317
43481
  var simplifyFn = getBufferSimplifyFunction(dataset, opts); // null if tolerance=0
43318
43482
  var makerOpts = Object.assign({geometry_type: lyr.geometry_type}, opts);
43319
43483
  var makeShapeBuffer = getPolylineBufferMaker(dataset, makerOpts);
43484
+ // A shape whose parts are all closed rings buffers to an annulus per ring. The
43485
+ // default (open-path) construction builds each side as many split sections plus
43486
+ // join-sector rings (no loop removal), which floods the dissolve with raw rings
43487
+ // (~8x more on a dense coastline). Instead route these shapes through the same
43488
+ // winding-fill + loop-removal construction the polygon-ring buffer uses: one
43489
+ // continuous offset ring per side, with self-overlap loops stripped, resolved
43490
+ // by a winding-number dissolve. Open or mixed shapes keep the tuned outline +
43491
+ // boundary-flood path unchanged.
43492
+ var closedMaker = getPolylineBufferMaker(dataset,
43493
+ Object.assign({}, makerOpts, {winding_fill: true}));
43320
43494
  var useFilter = useArtifactHoleFilter(opts);
43321
43495
  var quadSegs = opts.quad_segs >= 2 ? opts.quad_segs : 8;
43322
43496
  var sagPct = 1 - Math.cos(Math.PI / 4 / quadSegs);
@@ -43329,16 +43503,21 @@ ${svg}
43329
43503
  roundCaps: (opts.cap_style || 'round') == 'round'
43330
43504
  } : null;
43331
43505
  var dissolveOpts = Object.assign({}, opts, {per_part_holes: true});
43506
+ var closedDissolveOpts = Object.assign({}, dissolveOpts, {winding_fill: true});
43332
43507
  var datasets = [];
43333
43508
  lyr.shapes.forEach(function(shape, i) {
43334
43509
  var distance = distanceFn(i);
43335
43510
  if (!distance || !shape) return;
43336
- var retn = makeShapeBuffer(shape, distance);
43511
+ // Closed-ring fast path only for ordinary two-sided buffers (the one-sided
43512
+ // construction reaches this function with winding_fill:false and needs its
43513
+ // own coverage handling); mixed open/closed shapes fall back too.
43514
+ var allClosed = !oneSided && shapeIsAllClosed(shape, dataset.arcs);
43515
+ var retn = (allClosed ? closedMaker : makeShapeBuffer)(shape, distance);
43337
43516
  var feats = (Array.isArray(retn) ? retn : [retn]).filter(Boolean);
43338
43517
  if (!feats.length) return;
43339
43518
  var ds = importGeoJSON(getBufferGeoJSON(feats), {});
43340
- dissolveBufferDataset2(ds, dissolveOpts);
43341
- if (useFilter) {
43519
+ dissolveBufferDataset2(ds, allClosed ? closedDissolveOpts : dissolveOpts);
43520
+ if (useFilter && !allClosed) {
43342
43521
  var bufLyr = ds.layers[0];
43343
43522
  var intervalPct = simplifyFn ? simplifyFn(distance) / distance : 0;
43344
43523
  bufLyr.shapes = bufLyr.shapes.map(function(bufShape) {
@@ -44223,1894 +44402,1970 @@ ${svg}
44223
44402
  return lng;
44224
44403
  }
44225
44404
 
44226
- // Returns a function for constructing a query function that accepts an arc id and
44227
- // returns information about the polygon or polygons that use the given arc.
44228
- // TODO: explain this better.
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
44229
44409
  //
44230
- // options:
44231
- // filter: optional filter function; signature: function(idA, idB or -1) : boolean
44232
- // reusable: flag that lets an arc be queried multiple times.
44233
- function getArcClassifier(lyr, arcs, optsArg) {
44234
- var opts = optsArg || {},
44235
- useOnce = !opts.reusable,
44236
- n = arcs.size(),
44237
- a = new Int32Array(n),
44238
- b = new Int32Array(n),
44239
- filter;
44240
- if (opts.where) {
44241
- filter = compileFeaturePairFilterExpression(opts.where, lyr, arcs);
44410
+ cmd.filterSlivers = function(lyr, dataset, opts) {
44411
+ if (lyr.geometry_type != 'polygon') {
44412
+ return 0;
44242
44413
  }
44414
+ return filterSlivers(lyr, dataset, opts);
44415
+ };
44243
44416
 
44244
- utils.initializeArray(a, -1);
44245
- utils.initializeArray(b, -1);
44246
-
44247
- traversePaths(lyr.shapes, function(o) {
44248
- var i = absArcId(o.arcId);
44249
- var shpId = o.shapeId;
44250
- var aval = a[i];
44251
- if (aval == -1) {
44252
- a[i] = shpId;
44253
- } else if (shpId < aval) {
44254
- b[i] = aval;
44255
- a[i] = shpId;
44256
- } else {
44257
- b[i] = shpId;
44258
- }
44259
- });
44260
-
44261
- function classify(arcId, getKey) {
44262
- var i = absArcId(arcId);
44263
- var shpA = a[i];
44264
- var shpB = b[i];
44265
- var key;
44266
- if (shpA == -1) return null;
44267
- key = getKey(shpA, shpB);
44268
- if (key === null || key === false) return null;
44269
- if (useOnce) {
44270
- // arc can only be queried once
44271
- a[i] = -1;
44272
- b[i] = -1;
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;
44273
44426
  }
44274
- // use optional filter to exclude some arcs
44275
- if (filter && !filter(shpA, shpB)) return null;
44276
- return key;
44277
- }
44278
-
44279
- return function(getKey) {
44280
- return function(arcId) {
44281
- return classify(arcId, getKey);
44282
- };
44283
44427
  };
44284
- }
44285
44428
 
44286
- var ArcClassifier = /*#__PURE__*/Object.freeze({
44287
- __proto__: null,
44288
- getArcClassifier: getArcClassifier
44289
- });
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));
44290
44433
 
44291
- function makePolygonBuffer(lyr, dataset, opts) {
44292
- var spherical = isLatLngCRS(getDatasetCRS(dataset));
44293
- // The debug-offset/debug-winding/debug-mosaic visualizations are implemented
44294
- // only for line buffers. They have no handling in the polygon pipeline; left
44295
- // unstripped they leak into the per-shape dissolve and produce malformed
44296
- // output, so drop them and warn rather than silently mislead.
44297
- if (opts.debug_offset || opts.debug_winding || opts.debug_mosaic) {
44298
- warn('debug-offset/debug-winding/debug-mosaic are not implemented for polygon buffers; ignoring');
44299
- opts = Object.assign({}, opts, {
44300
- debug_offset: false,
44301
- debug_winding: false,
44302
- debug_mosaic: false
44303
- });
44304
- }
44305
- var output = makePolygonBufferGeoJSON(lyr, dataset, opts);
44306
- var dataset2 = importGeoJSON(output.geojson, {type: 'polygon'});
44307
- if (spherical) {
44308
- splitAntimeridianBufferDataset(dataset2);
44309
- if (output.dissolveAfterSplit) {
44310
- dissolveBufferDataset2(dataset2, opts);
44311
- }
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});
44312
44437
  }
44313
- return dataset2;
44438
+ return removed;
44314
44439
  }
44315
44440
 
44316
- function makePolygonBufferGeoJSON(lyr, dataset, opts) {
44317
- var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
44318
- var useTopologicalMode = !!opts.topological;
44319
- var uniqueArcTest = useTopologicalMode ? getUniqueArcTest(lyr, dataset.arcs) : null;
44320
- var hasPositiveDistance = false;
44321
- var hasNegativeDistance = false;
44322
- if (useTopologicalMode) {
44323
- // The topological pipeline selects mosaic tiles by source membership
44324
- // (boundary flood), which cannot resolve the self-overlapping winding-fill
44325
- // ring. So each feature's winding-fill offset rings are pre-dissolved into a
44326
- // clean (non-self-overlapping) polygon before they enter the shared mosaic;
44327
- // the construction speedup (loop removal cutting per-feature self-
44328
- // intersections) is preserved, and the mosaic is unchanged.
44329
- return makeTopologicalPolygonBufferGeoJSON(lyr, dataset, opts, distanceFn,
44330
- uniqueArcTest, getPolygonRingBufferMaker(dataset, opts, 'left'));
44331
- }
44332
- // Closed source rings are offset with the winding-fill construction: one
44333
- // self-overlapping ring per source ring (its overshoot loops resolved by the
44334
- // winding-number dissolve in makeClosedRingBufferGeometry) instead of many
44335
- // overlapping per-segment section bands. The single ring carries far fewer
44336
- // rings and self-intersections into the dissolve, which dominates polygon-
44337
- // buffer runtime.
44338
- var leftBufferMaker = getPolygonRingBufferMaker(dataset, opts, 'left');
44339
- var rightBufferMaker = getPolygonRingBufferMaker(dataset, opts, 'right');
44340
- var geometries = lyr.shapes.map(function(shape, i) {
44341
- var distance = distanceFn(i);
44342
- if (!distance || !shape) return null;
44343
- if (distance < 0) {
44344
- hasNegativeDistance = true;
44345
- return makeNegativePolygonBufferGeometry(shape, -distance, dataset, opts,
44346
- rightBufferMaker);
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;
44347
44462
  }
44348
- hasPositiveDistance = true;
44349
- return makePositivePolygonBufferGeometry(shape, distance, dataset, opts,
44350
- leftBufferMaker);
44351
- });
44352
- return {
44353
- geojson: {
44354
- type: 'GeometryCollection',
44355
- geometries: geometries
44356
- },
44357
- dissolveAfterSplit: hasPositiveDistance && !hasNegativeDistance
44358
44463
  };
44359
- }
44360
-
44361
- function getPolygonRingBufferMaker(dataset, opts, side, winding) {
44362
- // The sector-band escape hatch forces the older non-winding construction even
44363
- // for callers that request winding-fill (see the 'sector-band' option).
44364
- var useWinding = !opts.sector_band;
44365
- var makerOpts = Object.assign({}, opts, {
44366
- left: side == 'left',
44367
- right: side == 'right',
44368
- winding_fill: useWinding,
44369
- // Enable overshoot-loop removal on the single winding-fill offset ring.
44370
- // Scoped to closed polygon rings: an open one-sided line buffer relies on
44371
- // the band-coverage audit (skipped under winding_fill) for concave-join
44372
- // dents, which loop removal would collapse.
44373
- buffer_ring_loops: useWinding
44374
- });
44375
- return getPolylineBufferMaker(dataset, makerOpts);
44376
- }
44377
44464
 
44378
- function makePositivePolygonBufferGeometry(shape, distance, dataset, opts,
44379
- leftBufferMaker) {
44380
- // Non-topological positive buffers are never path-split (only the topological
44381
- // pipeline splits, and it has its own function), so each shape's ring groups
44382
- // are offset and dissolved directly.
44383
- return makeClosedRingPositiveBufferGeometry(shape, dataset.arcs,
44384
- distance, opts, leftBufferMaker);
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;
44385
44470
  }
44386
44471
 
44387
- function makeTopologicalPolygonBufferGeoJSON(lyr, dataset, opts, distanceFn,
44388
- uniqueArcTest, bufferMaker) {
44389
- var shapes = lyr.shapes || [];
44390
- var distances = [];
44391
- var sourceIds = [];
44392
- var bufferIds = [];
44393
- var tmpGeometries = [];
44394
- var hasPositiveDistance = false;
44395
- var geometries, tmpDataset;
44472
+ // Assumes: Arcs have been divided
44473
+ //
44474
+ function clipPolylines(targetShapes, clipShapes, nodes, type) {
44475
+ var index = new PathIndex(clipShapes, nodes.arcs);
44396
44476
 
44397
- shapes.forEach(function(shape, i) {
44398
- sourceIds[i] = -1;
44399
- bufferIds[i] = -1;
44400
- distances[i] = distanceFn(i);
44401
- if (distances[i] < 0) {
44402
- stop$1('The topological buffer option does not support negative distances');
44403
- }
44404
- if (!shape) return;
44405
- sourceIds[i] = tmpGeometries.length;
44406
- tmpGeometries.push(getPolygonGeometry(shape, dataset.arcs));
44477
+ return targetShapes.map(function(shp) {
44478
+ return clipPolyline(shp);
44407
44479
  });
44408
44480
 
44409
- shapes.forEach(function(shape, i) {
44410
- var distance = distances[i];
44411
- var pathData, bufferCoords;
44412
- if (!distance || !shape) return;
44413
- hasPositiveDistance = true;
44414
- pathData = getPolygonBufferPathData(shape, uniqueArcTest);
44415
- bufferCoords = getBufferMultiPolygonCoords(pathData.paths, distance, bufferMaker);
44416
- // Resolve the winding-fill rings' self-overlaps into a clean polygon (the
44417
- // mosaic's boundary-flood membership cannot), so this feature enters the
44418
- // shared mosaic as an ordinary polygon. The sector-band fallback emits
44419
- // boundary-flood-resolvable bands, so it feeds the mosaic directly (as the
44420
- // topological pipeline did before the winding-fill construction).
44421
- if (!opts.sector_band) {
44422
- bufferCoords = dissolveOffsetRingsToCoords(bufferCoords, opts);
44423
- }
44424
- if (bufferCoords.length > 0) {
44425
- bufferIds[i] = tmpGeometries.length;
44426
- tmpGeometries.push({
44427
- type: 'MultiPolygon',
44428
- coordinates: bufferCoords
44429
- });
44430
- }
44431
- });
44481
+ function clipPolyline(shp) {
44482
+ var clipped = null;
44483
+ if (shp) clipped = shp.reduce(clipPath, []);
44484
+ return clipped && clipped.length > 0 ? clipped : null;
44485
+ }
44432
44486
 
44433
- if (!hasPositiveDistance || tmpGeometries.length === 0) {
44434
- geometries = shapes.map(function() { return null; });
44435
- } else {
44436
- tmpDataset = importGeoJSON({
44437
- type: 'GeometryCollection',
44438
- geometries: tmpGeometries
44439
- }, {type: 'polygon'});
44440
- geometries = makeTopologicalPolygonBufferGeometries(shapes, distances,
44441
- sourceIds, bufferIds, tmpDataset, dataset.arcs);
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;
44500
+ }
44501
+ }
44502
+ return memo;
44442
44503
  }
44443
- return {
44444
- geojson: {
44445
- type: 'GeometryCollection',
44446
- geometries: geometries
44447
- },
44448
- dissolveAfterSplit: hasPositiveDistance
44449
- };
44450
44504
  }
44451
44505
 
44452
- function makeTopologicalPolygonBufferGeometries(shapes, distances, sourceIds,
44453
- bufferIds, tmpDataset, sourceArcs) {
44454
- var tmpLyr = tmpDataset.layers[0];
44455
- var nodes = addIntersectionCuts(tmpDataset, {rebuild_topology: true});
44456
- var mosaicIndex = new MosaicIndex(tmpLyr, nodes, {flat: false, no_holes: false});
44457
- var pathfind = getRingIntersector(mosaicIndex.nodes);
44458
- var sourceIdIndex = getIdLookup(sourceIds);
44459
- var bufferIdIndex = getIdToFeatureIdLookup(bufferIds);
44460
- var sourceAreas = getSourceShapeAreas(shapes, sourceArcs);
44461
- return shapes.map(function(shape, i) {
44462
- var distance = distances[i];
44463
- var tileIds, geom;
44464
- if (!distance || !shape) return null;
44465
- tileIds = getTopologicalBufferTileIds(sourceIds[i], bufferIds[i],
44466
- i, mosaicIndex, sourceIdIndex, bufferIdIndex, sourceAreas);
44467
- geom = getTileIdsGeometry(tileIds, mosaicIndex, pathfind);
44468
- return removePositiveBufferArtifactHoles(geom, shape, sourceArcs, distance);
44469
- });
44470
- }
44506
+ var PolylineClipping = /*#__PURE__*/Object.freeze({
44507
+ __proto__: null,
44508
+ clipPolylines: clipPolylines
44509
+ });
44471
44510
 
44472
- function getTopologicalBufferTileIds(sourceId, bufferId, featureId, mosaicIndex,
44473
- sourceIdIndex, bufferIdIndex, sourceAreas) {
44474
- var ids = [];
44475
- var index = [];
44476
- addTileIds(ids, index, mosaicIndex.getTileIdsByShapeId(sourceId));
44477
- if (bufferId >= 0) {
44478
- addTileIds(ids, index, mosaicIndex.getTileIdsByShapeId(bufferId).filter(function(tileId) {
44479
- return !tileHasSourcePolygon(tileId, mosaicIndex, sourceIdIndex) &&
44480
- getBufferTileOwnerId(tileId, mosaicIndex, bufferIdIndex, sourceAreas) == featureId;
44481
- }));
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]);
44482
44516
  }
44483
- return ids;
44517
+ return cw;
44484
44518
  }
44485
44519
 
44486
- function addTileIds(memo, index, ids) {
44487
- ids.forEach(function(id) {
44488
- if (index[id]) return;
44489
- index[id] = true;
44490
- memo.push(id);
44491
- });
44492
- }
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);
44493
44525
 
44494
- function tileHasSourcePolygon(tileId, mosaicIndex, sourceIdIndex) {
44495
- return mosaicIndex.getSourceIdsByTileId(tileId).some(function(shapeId) {
44496
- return sourceIdIndex[shapeId];
44497
- });
44498
- }
44526
+ return function(shp) {
44527
+ if (!shp) return null;
44528
+ var cw = [],
44529
+ ccw = [];
44499
44530
 
44500
- function getBufferTileOwnerId(tileId, mosaicIndex, bufferIdIndex, sourceAreas) {
44501
- var ownerId = -1;
44502
- var ownerArea = -Infinity;
44503
- mosaicIndex.getSourceIdsByTileId(tileId).forEach(function(shapeId) {
44504
- var featureId = bufferIdIndex[shapeId];
44505
- var area;
44506
- if (featureId >= 0) {
44507
- area = sourceAreas[featureId];
44508
- if (area > ownerArea || area == ownerArea && featureId < ownerId) {
44509
- ownerId = featureId;
44510
- ownerArea = area;
44511
- }
44531
+ divide(shp, cw, ccw);
44532
+ cw = pathfind(cw, 'flatten');
44533
+ ccw.forEach(reversePath);
44534
+ ccw = pathfind(ccw, 'flatten');
44535
+ ccw.forEach(reversePath);
44536
+ var shp2 = appendHolesToRings(cw, ccw);
44537
+ var dissolved = pathfind(shp2, 'dissolve');
44538
+
44539
+ if (dissolved.length > 1) {
44540
+ dissolved = fixNestingErrors(dissolved, nodes.arcs);
44512
44541
  }
44513
- });
44514
- return ownerId;
44515
- }
44516
44542
 
44517
- function getSourceShapeAreas(shapes, arcs) {
44518
- return shapes.map(function(shape) {
44519
- return Math.abs(getShapeArea(shape, arcs));
44520
- });
44543
+ return dissolved.length > 0 ? dissolved : null;
44544
+ };
44521
44545
  }
44522
44546
 
44523
- function getIdLookup(ids) {
44524
- var index = [];
44525
- ids.forEach(function(id) {
44526
- if (id >= 0) index[id] = true;
44527
- });
44528
- return index;
44529
- }
44547
+ // TODO: remove dependency on old polygon dissolve function
44530
44548
 
44531
- function getIdToFeatureIdLookup(ids) {
44532
- var index = [];
44533
- ids.forEach(function(id, i) {
44534
- if (id >= 0) index[id] = i;
44535
- });
44536
- return index;
44537
- }
44549
+ // assumes layers and arcs have been prepared for clipping
44550
+ function clipPolygons(targetShapes, clipShapes, nodes, type, optsArg) {
44551
+ profileStart('clipPolygons');
44552
+ var arcs = nodes.arcs;
44553
+ var opts = optsArg || {};
44554
+ var clipFlags = new Uint8Array(arcs.size());
44555
+ var routeFlags = new Uint8Array(arcs.size());
44556
+ var clipArcTouches = 0;
44557
+ var clipArcUses = 0;
44558
+ var usedClipArcs = [];
44559
+ var findPath = getPathFinder(nodes, useRoute, routeIsActive);
44560
+ var dissolvePolygon = getPolygonDissolver(nodes);
44538
44561
 
44539
- function getTileIdsGeometry(tileIds, mosaicIndex, pathfind) {
44540
- var rings = [];
44541
- var holes = [];
44542
- var shp;
44543
- tileIds.forEach(function(tileId) {
44544
- var tile = mosaicIndex.mosaic[tileId];
44545
- rings.push(tile[0]);
44546
- if (tile.length > 1) {
44547
- holes = holes.concat(tile.slice(1));
44562
+ if (!opts.bbox2) {
44563
+ profileStart('cp.dissolveTargetRings');
44564
+ targetShapes = targetShapes.map(dissolvePolygon);
44565
+ profileEnd('cp.dissolveTargetRings');
44566
+ }
44567
+
44568
+ profileStart('cp.openClipRoutes');
44569
+ openArcRoutes(clipShapes, arcs, clipFlags, type == 'clip', type == 'erase', true, 0x11);
44570
+ profileEnd('cp.openClipRoutes');
44571
+ profileStart('cp.PathIndex#1');
44572
+ var index = new PathIndex(clipShapes, arcs);
44573
+ profileEnd('cp.PathIndex#1');
44574
+ profileStart('cp.clipShapes');
44575
+ var clippedShapes = targetShapes.map(function(shape, i) {
44576
+ if (shape) {
44577
+ return clipPolygon(shape, type, index);
44548
44578
  }
44579
+ return null;
44549
44580
  });
44550
- shp = pathfind(rings.concat(holes), 'dissolve');
44551
- if (shp && shp.length > 0) {
44552
- shp = fixNestingErrors(shp, mosaicIndex.nodes.arcs);
44553
- }
44554
- return shp && shp.length > 0 ? getPolygonGeometry(shp, mosaicIndex.nodes.arcs) : null;
44555
- }
44581
+ profileEnd('cp.clipShapes');
44556
44582
 
44557
- function dissolvePolygonBufferGeometry(geom, opts) {
44558
- var tmp = importGeoJSON({
44559
- type: 'GeometryCollection',
44560
- geometries: [geom]
44561
- }, {type: 'polygon'});
44562
- var lyr = tmp.layers[0];
44563
- if (tmp.arcs) {
44564
- dissolveBufferDataset2(tmp, opts);
44565
- }
44566
- if (lyr.shapes && lyr.shapes[0]) {
44567
- lyr.shapes[0] = fixNestingErrors(lyr.shapes[0], tmp.arcs);
44568
- }
44569
- return lyr.shapes && lyr.shapes[0] ?
44570
- getPolygonGeometry(lyr.shapes[0], tmp.arcs) : null;
44571
- }
44583
+ markPathsAsUsed(clippedShapes, routeFlags);
44572
44584
 
44573
- function makeNegativePolygonBufferGeometry(shape, distance, dataset, opts,
44574
- bufferMaker) {
44575
- // Non-topological negative buffers are never path-split, so each shape's ring
44576
- // groups are offset and eroded directly.
44577
- return makeClosedRingNegativeBufferGeometry(shape, dataset.arcs, distance,
44578
- opts, bufferMaker);
44579
- }
44585
+ profileStart('cp.findUndividedClip');
44586
+ var undividedClipShapes = findUndividedClipShapes(clipShapes);
44587
+ profileEnd('cp.findUndividedClip');
44580
44588
 
44581
- function makeClosedRingNegativeBufferGeometry(shape, arcs, distance, opts,
44582
- bufferMaker) {
44583
- var coords = [];
44584
- getPolygonRingGroupShapes(shape, arcs).forEach(function(groupShape) {
44585
- var bufferCoords = getBufferMultiPolygonCoords(groupShape, distance, bufferMaker);
44586
- var geom = bufferCoords.length > 0 ?
44587
- makeClosedRingBufferGeometry(groupShape, arcs, getBufferDataset(bufferCoords),
44588
- opts, distance, true) : null;
44589
- if (geom) {
44590
- coords = coords.concat(geom.coordinates);
44589
+ closeArcRoutes(clipShapes, arcs, routeFlags, true, true);
44590
+ profileStart('cp.PathIndex#2');
44591
+ index = new PathIndex(undividedClipShapes, arcs);
44592
+ profileEnd('cp.PathIndex#2');
44593
+ profileStart('cp.findInteriorPaths');
44594
+ targetShapes.forEach(function(shape, shapeId) {
44595
+ var paths = shape ? findInteriorPaths(shape, type, index) : null;
44596
+ if (paths) {
44597
+ clippedShapes[shapeId] = (clippedShapes[shapeId] || []).concat(paths);
44591
44598
  }
44592
44599
  });
44593
- return coords.length > 0 ? {
44594
- type: 'MultiPolygon',
44595
- coordinates: coords
44596
- } : null;
44597
- }
44600
+ profileEnd('cp.findInteriorPaths');
44598
44601
 
44599
- function makeClosedRingPositiveBufferGeometry(shape, arcs, distance, opts,
44600
- bufferMaker) {
44601
- var coords = [];
44602
- var groupShapes = getPolygonRingGroupShapes(shape, arcs);
44603
- groupShapes.forEach(function(groupShape) {
44604
- var bufferCoords = getBufferMultiPolygonCoords(groupShape, distance, bufferMaker);
44605
- var geom = bufferCoords.length > 0 ?
44606
- makeClosedRingBufferGeometry(groupShape, arcs, getBufferDataset(bufferCoords),
44607
- opts, distance, false) : null;
44608
- if (geom) {
44609
- coords = coords.concat(geom.coordinates);
44610
- } else {
44611
- coords = coords.concat(getPolygonMultiPolygonCoords(groupShape, arcs));
44612
- }
44613
- });
44614
- if (coords.length === 0) return null;
44615
- var geom = {
44616
- type: 'MultiPolygon',
44617
- coordinates: coords
44618
- };
44619
- if (shouldDissolveBufferedRingGroups(groupShapes, arcs, distance)) {
44620
- geom = dissolvePolygonBufferGeometry(geom, opts);
44621
- }
44622
- return removePositiveBufferArtifactHoles(geom, shape, arcs, distance);
44623
- }
44602
+ profileEnd('clipPolygons');
44603
+ return clippedShapes;
44624
44604
 
44625
- // True if any two of a feature's buffered ring groups are close enough that
44626
- // their buffers might merge (so the group buffers should be dissolved together
44627
- // rather than emitted as separate polygons).
44628
- function shouldDissolveBufferedRingGroups(groupShapes, arcs, distance) {
44629
- var threshold = getCoordinateDistance(distance, arcs) * 2;
44630
- var n = groupShapes.length;
44631
- if (n < 2) return false;
44632
- // Bounding-box prefilter + early exit. The old code computed the exact min
44633
- // distance over every group pair (O(groups^2 * verts * segs)) -- the dominant
44634
- // cost when a feature has many islands. Two groups can only be within
44635
- // @threshold if their bounding boxes are, so the box test skips the costly
44636
- // vertex-by-vertex distance for all far-apart pairs (the common case), and we
44637
- // return as soon as one near pair is found. Result is identical to
44638
- // (min inter-group distance <= threshold).
44639
- var bounds = groupShapes.map(function(shp) {
44640
- return arcs.getMultiShapeBounds(shp);
44641
- });
44642
- for (var i = 0; i < n - 1; i++) {
44643
- for (var j = i + 1; j < n; j++) {
44644
- if (boundsToBoundsDistance(bounds[i], bounds[j]) > threshold) continue;
44645
- if (getShapeToShapeDistance(groupShapes[i], groupShapes[j], arcs, threshold) <= threshold ||
44646
- getShapeToShapeDistance(groupShapes[j], groupShapes[i], arcs, threshold) <= threshold) {
44647
- return true;
44605
+ function clipPolygon(shape, type, index) {
44606
+ var dividedShape = [],
44607
+ clipping = type == 'clip',
44608
+ erasing = type == 'erase';
44609
+
44610
+ // open pathways for entire polygon rather than one ring at a time --
44611
+ // need to create polygons that connect positive-space rings and holes
44612
+ openArcRoutes(shape, arcs, routeFlags, true, false, false);
44613
+
44614
+ forEachShapePart(shape, function(ids) {
44615
+ var path;
44616
+ for (var i=0, n=ids.length; i<n; i++) {
44617
+ clipArcTouches = 0;
44618
+ clipArcUses = 0;
44619
+ path = findPath(ids[i]);
44620
+ if (path) {
44621
+ // if ring doesn't touch/intersect a clip/erase polygon, check if it is contained
44622
+ // if (clipArcTouches === 0) {
44623
+ // if ring doesn't incorporate an arc from the clip/erase polygon,
44624
+ // check if it is contained (assumes clip shapes are dissolved)
44625
+ if (clipArcTouches === 0 || clipArcUses === 0) { //
44626
+ var contained = index.pathIsEnclosed(path);
44627
+ if (clipping && contained || erasing && !contained) {
44628
+ dividedShape.push(path);
44629
+ }
44630
+ // TODO: Consider breaking if polygon is unchanged
44631
+ } else {
44632
+ dividedShape.push(path);
44633
+ }
44634
+ }
44648
44635
  }
44636
+ });
44637
+
44638
+
44639
+ // Clear pathways of current target shape to hidden/closed
44640
+ closeArcRoutes(shape, arcs, routeFlags, true, true, true);
44641
+ // Also clear pathways of any clip arcs that were used
44642
+ if (usedClipArcs.length > 0) {
44643
+ closeArcRoutes(usedClipArcs, arcs, routeFlags, true, true, true);
44644
+ usedClipArcs = [];
44649
44645
  }
44646
+
44647
+ return dividedShape.length === 0 ? null : dividedShape;
44650
44648
  }
44651
- return false;
44652
- }
44653
44649
 
44654
- // Minimum distance between two axis-aligned bounding boxes (0 if they overlap).
44655
- function boundsToBoundsDistance(a, b) {
44656
- var dx = Math.max(0, a.xmin - b.xmax, b.xmin - a.xmax);
44657
- var dy = Math.max(0, a.ymin - b.ymax, b.ymin - a.ymax);
44658
- return Math.sqrt(dx * dx + dy * dy);
44659
- }
44650
+ function routeIsActive(id) {
44651
+ var fw = id >= 0,
44652
+ abs = fw ? id : ~id,
44653
+ visibleBit = fw ? 1 : 0x10,
44654
+ targetBits = routeFlags[abs],
44655
+ clipBits = clipFlags[abs];
44660
44656
 
44661
- // Min distance from the vertices of @shape1 to @shape2. Stops early once a
44662
- // vertex within @maxDist (optional) is found, since callers only compare the
44663
- // result against a threshold.
44664
- function getShapeToShapeDistance(shape1, shape2, arcs, maxDist) {
44665
- var data = exportPathData(shape1, arcs, 'polygon');
44666
- var minDist = Infinity;
44667
- var paths = data.pathData;
44668
- for (var i = 0; i < paths.length; i++) {
44669
- var points = paths[i].points;
44670
- for (var j = 0; j < points.length; j++) {
44671
- var d = getPointToShapeDistance(points[j][0], points[j][1], shape2, arcs);
44672
- if (d < minDist) minDist = d;
44673
- if (maxDist != null && minDist <= maxDist) return minDist;
44674
- }
44657
+ if (clipBits > 0) clipArcTouches++;
44658
+ return (targetBits & visibleBit) > 0 || (clipBits & visibleBit) > 0;
44675
44659
  }
44676
- return minDist;
44677
- }
44678
44660
 
44679
- function makeClosedRingBufferGeometry(shape, arcs, bufferDataset, opts, distance,
44680
- reverse) {
44681
- var sourceAreas = exportPathData(shape, arcs, 'polygon').pathData.map(function(path) {
44682
- return path.area;
44683
- });
44684
- var sourceBoundaryThreshold = getSourceBoundaryThreshold(distance, arcs);
44685
- var bufferLyr = bufferDataset.layers[0];
44686
- var bufferShape, bufferData, erodedShape;
44687
- if (!bufferDataset.arcs) return null;
44688
- // The default offset rings come from the winding-fill maker (one self-
44689
- // overlapping ring per source ring) and must be unioned by winding number;
44690
- // the sector-band fallback emits overlapping bands that a boundary flood
44691
- // resolves instead (its maker leaves winding_fill off to match).
44692
- dissolveBufferDataset2(bufferDataset,
44693
- Object.assign({}, opts, {winding_fill: !opts.sector_band}));
44694
- bufferShape = bufferLyr.shapes && bufferLyr.shapes[0];
44695
- if (!bufferShape) return null;
44696
- bufferData = exportPathData(bufferShape, bufferDataset.arcs, 'polygon');
44697
- // Build the source-shape segment index once: ringIsOnSourceBoundary probes
44698
- // ~20 points of every eroded buffer ring against the same source shape, and
44699
- // on large rings the unindexed per-point distance scan dominated runtime.
44700
- var sourceIndex = buildShapeSegmentIndex(shape, arcs);
44701
- erodedShape = bufferData.pathData.reduce(function(memo, path) {
44702
- if (!areaMatchesAny(path.area, sourceAreas) &&
44703
- !ringIsOnSourceBoundary(path.points, sourceIndex, sourceBoundaryThreshold)) {
44704
- memo.push(reverse ? reversePath(path.ids.concat()) : path.ids.concat());
44705
- }
44706
- return memo;
44707
- }, []);
44708
- return erodedShape.length > 0 ?
44709
- getPolygonGeometry(erodedShape, bufferDataset.arcs) : null;
44710
- }
44661
+ function useRoute(id) {
44662
+ var fw = id >= 0,
44663
+ abs = fw ? id : ~id,
44664
+ targetBits = routeFlags[abs],
44665
+ clipBits = clipFlags[abs],
44666
+ targetRoute, clipRoute;
44711
44667
 
44712
- function getPolygonRingGroupShapes(shape, arcs) {
44713
- var data = exportPathData(shape, arcs, 'polygon');
44714
- if (data.pointCount === 0) return [];
44715
- return groupPolygonRings(data.pathData, arcs, false).map(function(paths) {
44716
- return paths.map(function(path) {
44717
- return path.ids.concat();
44718
- });
44719
- });
44720
- }
44668
+ if (fw) {
44669
+ targetRoute = targetBits;
44670
+ clipRoute = clipBits;
44671
+ } else {
44672
+ targetRoute = targetBits >> 4;
44673
+ clipRoute = clipBits >> 4;
44674
+ }
44675
+ targetRoute &= 3;
44676
+ clipRoute &= 3;
44721
44677
 
44722
- function removePositiveBufferArtifactHoles(geom, shape, arcs, distance) {
44723
- if (!geom) return null;
44724
- var threshold = getPositiveHoleArtifactThreshold(distance, arcs);
44725
- var minHoleArea = getPositiveHoleArtifactAreaThreshold(distance, arcs);
44726
- var sourceHoles = getSourceHoleShapes(shape, arcs);
44727
- // Each candidate hole is classified by probing many points against the
44728
- // source shape. Both per-probe tests used to rescan the whole source shape:
44729
- // - "is the probe inside the source shape?" (testPointInPolygon)
44730
- // - "is the probe near a source hole boundary?" (point-to-shape distance)
44731
- // On large rings (e.g. a U.S. state buffer) this point-in-ring scan dominated
44732
- // runtime. Build the spatial indexes once per feature instead:
44733
- // - PathIndex.pointIsEnclosed() runs point-in-polygon via a per-ring
44734
- // scanline index (O(log n) per probe instead of O(n)).
44735
- // - shapeIndex / holeIndex are chunk-bounds indexes that prune far segments
44736
- // for the point-to-shape distance queries.
44737
- var ctx = {
44738
- threshold: threshold,
44739
- shapeIndex: buildShapeSegmentIndex(shape, arcs),
44740
- pathIndex: shape && shape.length > 0 ? new PathIndex([shape], arcs) : null,
44741
- holeIndex: sourceHoles.length > 0 ?
44742
- buildShapeSegmentIndex(sourceHoles.map(function(h) {return h[0];}), arcs) : null
44743
- };
44744
- if (geom.type == 'Polygon') {
44745
- geom.coordinates = filterArtifactHoles(geom.coordinates, minHoleArea, ctx);
44746
- } else if (geom.type == 'MultiPolygon') {
44747
- geom.coordinates = geom.coordinates.map(function(polygon) {
44748
- return filterArtifactHoles(polygon, minHoleArea, ctx);
44749
- }).filter(function(polygon) {
44750
- return polygon.length > 0;
44751
- });
44752
- }
44753
- return geom;
44754
- }
44678
+ var usable = false;
44679
+ // var usable = targetRoute === 3 || targetRoute === 0 && clipRoute == 3;
44680
+ if (targetRoute == 3) {
44681
+ // special cases where clip route and target route both follow this arc
44682
+ if (clipRoute == 1) ; else if (clipRoute == 2 && type == 'erase') ; else {
44683
+ usable = true;
44684
+ }
44755
44685
 
44756
- // Flatten a shape's path segments into fixed-size chunks (per path) with a
44757
- // bounding box each. A chunk's box distance is a lower bound on the distance to
44758
- // any of its segments, so a point-to-shape distance query can skip whole chunks
44759
- // whose box is already farther than the closest segment found so far. Source
44760
- // paths are spatially coherent, so each chunk's box is tight. Coords are stored
44761
- // flat ([ax, ay, bx, by, ...]) to avoid per-segment array allocation.
44762
- var SHAPE_SEGMENT_CHUNK_SIZE = 32;
44686
+ } else if (targetRoute === 0 && clipRoute == 3) {
44687
+ usedClipArcs.push(id);
44688
+ usable = true;
44689
+ }
44763
44690
 
44764
- function buildShapeSegmentIndex(shape, arcs) {
44765
- var coords = [];
44766
- var chunks = [];
44767
- (shape || []).forEach(function(ids) {
44768
- var iter = arcs.getShapeIter(ids);
44769
- if (!iter.hasNext()) return;
44770
- var ax = iter.x, ay = iter.y;
44771
- var inChunk = 0;
44772
- var xmin = 0, ymin = 0, xmax = 0, ymax = 0, start = 0;
44773
- while (iter.hasNext()) {
44774
- var bx = iter.x, by = iter.y;
44775
- if (inChunk === 0) {
44776
- start = coords.length / 4;
44777
- xmin = Math.min(ax, bx); xmax = Math.max(ax, bx);
44778
- ymin = Math.min(ay, by); ymax = Math.max(ay, by);
44779
- } else {
44780
- if (ax < xmin) xmin = ax; else if (ax > xmax) xmax = ax;
44781
- if (bx < xmin) xmin = bx; else if (bx > xmax) xmax = bx;
44782
- if (ay < ymin) ymin = ay; else if (ay > ymax) ymax = ay;
44783
- if (by < ymin) ymin = by; else if (by > ymax) ymax = by;
44691
+ if (usable) {
44692
+ if (clipRoute == 3) {
44693
+ clipArcUses++;
44784
44694
  }
44785
- coords.push(ax, ay, bx, by);
44786
- inChunk++;
44787
- if (inChunk === SHAPE_SEGMENT_CHUNK_SIZE) {
44788
- chunks.push({start: start, end: coords.length / 4,
44789
- xmin: xmin, ymin: ymin, xmax: xmax, ymax: ymax});
44790
- inChunk = 0;
44695
+ // Need to close all arcs after visiting them -- or could cause a cycle
44696
+ // on layers with strange topology
44697
+ if (fw) {
44698
+ targetBits = setBits(targetBits, 1, 3);
44699
+ } else {
44700
+ targetBits = setBits(targetBits, 0x10, 0x30);
44791
44701
  }
44792
- ax = bx; ay = by;
44793
- }
44794
- if (inChunk > 0) {
44795
- chunks.push({start: start, end: coords.length / 4,
44796
- xmin: xmin, ymin: ymin, xmax: xmax, ymax: ymax});
44797
44702
  }
44798
- });
44799
- return {coords: coords, chunks: chunks};
44800
- }
44801
44703
 
44802
- // Same result as getPointToShapeDistance(px, py, shape, arcs), but using the
44803
- // chunk bounding boxes to prune. Scan the nearest-box chunk first to seed a
44804
- // tight bound, then skip any chunk whose box is farther than that bound. No
44805
- // per-query allocation or sorting.
44806
- function getPointToIndexedShapeDistance(px, py, index) {
44807
- var chunks = index.chunks, coords = index.coords;
44808
- var n = chunks.length;
44809
- if (n === 0) return Infinity;
44810
- var bestSq = Infinity;
44811
- var nearIdx = -1, nearBoxSq = Infinity, boxSq, c, k;
44812
- for (c = 0; c < n; c++) {
44813
- boxSq = shapeChunkBoxDistSq(px, py, chunks[c]);
44814
- if (boxSq < nearBoxSq) { nearBoxSq = boxSq; nearIdx = c; }
44704
+ targetBits |= fw ? 4 : 0x40; // record as visited
44705
+ routeFlags[abs] = targetBits;
44706
+ return usable;
44815
44707
  }
44816
- bestSq = scanShapeChunk(px, py, coords, chunks[nearIdx], bestSq);
44817
- for (k = 0; k < n; k++) {
44818
- if (k === nearIdx) continue;
44819
- if (shapeChunkBoxDistSq(px, py, chunks[k]) >= bestSq) continue;
44820
- bestSq = scanShapeChunk(px, py, coords, chunks[k], bestSq);
44708
+
44709
+
44710
+ // Filter a collection of shapes to exclude paths that incorporate parts of
44711
+ // clip/erase polygons and paths that are hidden (e.g. internal boundaries)
44712
+ function findUndividedClipShapes(clipShapes) {
44713
+ return clipShapes.map(function(shape) {
44714
+ var usableParts = [];
44715
+ forEachShapePart(shape, function(ids) {
44716
+ var pathIsClean = true,
44717
+ pathIsVisible = false;
44718
+ for (var i=0; i<ids.length; i++) {
44719
+ // check if arc was used in fw or rev direction
44720
+ if (!arcIsUnused(ids[i], routeFlags)) {
44721
+ pathIsClean = false;
44722
+ break;
44723
+ }
44724
+ // check if clip arc is visible
44725
+ if (!pathIsVisible && arcIsVisible(ids[i], clipFlags)) {
44726
+ pathIsVisible = true;
44727
+ }
44728
+ }
44729
+ if (pathIsClean && pathIsVisible) usableParts.push(ids);
44730
+ });
44731
+ return usableParts.length > 0 ? usableParts : null;
44732
+ });
44821
44733
  }
44822
- return Math.sqrt(bestSq);
44823
- }
44824
44734
 
44825
- function scanShapeChunk(px, py, coords, chunk, bestSq) {
44826
- for (var i = chunk.start; i < chunk.end; i++) {
44827
- var o = i * 4;
44828
- var d = pointSegDistSq2(px, py, coords[o], coords[o + 1], coords[o + 2], coords[o + 3]);
44829
- if (d < bestSq) bestSq = d;
44735
+
44736
+ function arcIsUnused(id, flags) {
44737
+ var abs = absArcId(id),
44738
+ flag = flags[abs];
44739
+ return (flag & 0x88) === 0;
44740
+ // return id < 0 ? (flag & 0x80) === 0 : (flag & 0x8) === 0;
44830
44741
  }
44831
- return bestSq;
44832
- }
44833
44742
 
44834
- function shapeChunkBoxDistSq(px, py, chunk) {
44835
- var dx = px < chunk.xmin ? chunk.xmin - px : (px > chunk.xmax ? px - chunk.xmax : 0);
44836
- var dy = py < chunk.ymin ? chunk.ymin - py : (py > chunk.ymax ? py - chunk.ymax : 0);
44837
- return dx * dx + dy * dy;
44838
- }
44743
+ function arcIsVisible(id, flags) {
44744
+ var flag = flags[absArcId(id)];
44745
+ return (flag & 0x11) > 0;
44746
+ }
44839
44747
 
44840
- function getSourceHoleShapes(shape, arcs) {
44841
- return exportPathData(shape, arcs, 'polygon').pathData.reduce(function(memo, path) {
44842
- if (path.area < 0) {
44843
- memo.push([path.ids]);
44748
+ // search for indexed clipping paths contained in a shape
44749
+ // dissolve them if needed
44750
+ function findInteriorPaths(shape, type, index) {
44751
+ var enclosedPaths = index.findPathsInsideShape(shape),
44752
+ dissolvedPaths = [];
44753
+ if (!enclosedPaths) return null;
44754
+ // ...
44755
+ if (type == 'erase') enclosedPaths.forEach(reversePath);
44756
+ if (enclosedPaths.length <= 1) {
44757
+ dissolvedPaths = enclosedPaths; // no need to dissolve single-part paths
44758
+ } else {
44759
+ openArcRoutes(enclosedPaths, arcs, routeFlags, true, false, true);
44760
+ enclosedPaths.forEach(function(ids) {
44761
+ var path;
44762
+ for (var j=0; j<ids.length; j++) {
44763
+ path = findPath(ids[j]);
44764
+ if (path) {
44765
+ dissolvedPaths.push(path);
44766
+ }
44767
+ }
44768
+ });
44769
+ }
44770
+
44771
+ return dissolvedPaths.length > 0 ? dissolvedPaths : null;
44772
+ }
44773
+ } // end clipPolygons()
44774
+
44775
+ //
44776
+ function clipPoints(points, clipShapes, arcs, type) {
44777
+ var index = new PathIndex(clipShapes, arcs);
44778
+
44779
+ var points2 = points.reduce(function(memo, feat) {
44780
+ var n = feat ? feat.length : 0,
44781
+ feat2 = [],
44782
+ enclosed;
44783
+
44784
+ for (var i=0; i<n; i++) {
44785
+ enclosed = index.findEnclosingShape(feat[i]) > -1;
44786
+ if (type == 'clip' && enclosed || type == 'erase' && !enclosed) {
44787
+ feat2.push(feat[i].concat());
44788
+ }
44844
44789
  }
44790
+
44791
+ memo.push(feat2.length > 0 ? feat2 : null);
44845
44792
  return memo;
44846
44793
  }, []);
44847
- }
44848
44794
 
44849
- function filterArtifactHoles(polygon, minHoleArea, ctx) {
44850
- if (polygon.length < 2) return polygon;
44851
- return [polygon[0]].concat(polygon.slice(1).filter(function(ring) {
44852
- return Math.abs(getGeoJSONRingArea(ring)) > minHoleArea &&
44853
- !positiveBufferHoleIsArtifact(ring, ctx);
44854
- }));
44795
+ return points2;
44855
44796
  }
44856
44797
 
44857
- function positiveBufferHoleIsArtifact(ring, ctx) {
44858
- var n = ring.length - 1; // skip duplicate endpoint
44859
- var step = Math.max(1, Math.floor(n / 20));
44860
- var p, p2;
44861
- for (var i = 0; i < n; i += step) {
44862
- p = ring[i];
44863
- if (pointIsDeepInsidePositiveBuffer(p, ctx)) return true;
44864
- p2 = ring[(i + 1) % n];
44865
- if (pointIsDeepInsidePositiveBuffer([(p[0] + p2[0]) / 2, (p[1] + p2[1]) / 2], ctx)) return true;
44866
- }
44867
- return false;
44868
- }
44798
+ var ClipPoints = /*#__PURE__*/Object.freeze({
44799
+ __proto__: null,
44800
+ clipPoints: clipPoints
44801
+ });
44869
44802
 
44870
- function pointIsDeepInsidePositiveBuffer(p, ctx) {
44871
- if (ctx.holeIndex &&
44872
- getPointToIndexedShapeDistance(p[0], p[1], ctx.holeIndex) < ctx.threshold) {
44873
- return false;
44803
+ // Convert a source parameter (which can take several forms) into
44804
+ // a dataset with a single layer.
44805
+ function normalizeOverlaySource(clipSrc, targetDataset, optsArg) {
44806
+ var opts = optsArg || {};
44807
+ var bbox = opts.bbox || opts.bbox2;
44808
+ var clipDataset;
44809
+ if (bbox) {
44810
+ clipDataset = convertClipBounds(bbox);
44811
+ } else if (!clipSrc) {
44812
+ stop$1('Command requires a source file, layer id or bbox');
44813
+ } else if (clipSrc.geometry_type) {
44814
+ // clipSrc is a layer (assumed in targetDataset)
44815
+ // TODO: update tests to remove this case (only used in tests)
44816
+ // error('Unsupported source format');
44817
+ clipDataset = utils.defaults({layers: [clipSrc], disposable: true}, targetDataset);
44818
+ } else if (clipSrc.layer && clipSrc.dataset) {
44819
+ clipDataset = utils.defaults({layers: [clipSrc.layer]}, clipSrc.dataset);
44820
+ } else if (clipSrc.layers?.length == 1) {
44821
+ clipDataset = clipSrc;
44822
+ } else {
44823
+ error('Invalid source format');
44874
44824
  }
44875
- if (ctx.pathIndex && ctx.pathIndex.pointIsEnclosed(p)) return true;
44876
- // A positive buffer can shrink legitimate source holes, leaving their
44877
- // boundaries near the original rings. Don't classify those as artifacts.
44878
- return getPointToIndexedShapeDistance(p[0], p[1], ctx.shapeIndex) < ctx.threshold;
44879
- }
44880
-
44881
- function getPositiveHoleArtifactThreshold(distance, arcs) {
44882
- return getCoordinateDistance(distance, arcs) * 0.5;
44825
+ if (clipSrc?.disposable) {
44826
+ clipDataset.disposable = true;
44827
+ }
44828
+ return clipDataset;
44883
44829
  }
44884
44830
 
44885
- function getPositiveHoleArtifactAreaThreshold(distance, arcs) {
44886
- var d = getCoordinateDistance(distance, arcs);
44887
- return d * d;
44831
+ // Create a merged dataset by appending the overlay layer to the target dataset
44832
+ // so it is last in the layers array.
44833
+ // DOES NOT insert clipping points
44834
+ function mergeLayersForOverlay(targetLayers, targetDataset, clipSrc, opts) {
44835
+ var clipDataset = normalizeOverlaySource(clipSrc, targetDataset, opts);
44836
+ return mergeLayersForOverlay2(targetLayers, targetDataset, clipDataset);
44888
44837
  }
44889
44838
 
44890
- function getGeoJSONRingArea(ring) {
44891
- var sum = 0;
44892
- for (var i = 0, n = ring.length - 1; i < n; i++) {
44893
- sum += ring[i][0] * ring[i + 1][1] - ring[i + 1][0] * ring[i][1];
44839
+ function mergeLayersForOverlay2(targetLayers, targetDataset, clipDataset) {
44840
+ var mergedDataset;
44841
+ var clipLyr = clipDataset.layers[0];
44842
+ if (targetDataset.arcs != clipDataset.arcs) {
44843
+ // using external dataset -- need to merge arcs
44844
+ if (!clipDataset.disposable) {
44845
+ // copy overlay layer shapes because arc ids will be reindexed during merging
44846
+ clipDataset.layers[0] = copyLayerShapes(clipDataset.layers[0]);
44847
+ }
44848
+ // merge external dataset with target dataset,
44849
+ // so arcs are shared between target layers and clipping lyr
44850
+ // Assumes that layers in clipDataset can be modified (if necessary, a copy should be passed in)
44851
+ mergedDataset = mergeDatasets([targetDataset, clipDataset]);
44852
+ buildTopology(mergedDataset); // identify any shared arcs between clipping layer and target dataset
44853
+ } else {
44854
+ // overlay layer belongs to the same dataset as target layers... move it to the end
44855
+ mergedDataset = utils.extend({}, targetDataset);
44856
+ mergedDataset.layers = targetDataset.layers.filter(function(lyr) {return lyr != clipLyr;});
44857
+ mergedDataset.layers.push(clipLyr);
44894
44858
  }
44895
- return sum / 2;
44896
- }
44897
-
44898
- function areaMatchesAny(area, arr) {
44899
- return arr.some(function(area2) {
44900
- var tol = Math.max(1e-8, Math.abs(area2) * 1e-9);
44901
- return Math.abs(Math.abs(area) - Math.abs(area2)) <= tol;
44902
- });
44859
+ return mergedDataset;
44903
44860
  }
44904
44861
 
44905
- function getSourceBoundaryThreshold(distance, arcs) {
44906
- return getCoordinateDistance(distance, arcs) * 0.25;
44907
- }
44862
+ function convertClipBounds(bb) {
44863
+ var x0 = bb[0], y0 = bb[1], x1 = bb[2], y1 = bb[3],
44864
+ arc = [[x0, y0], [x0, y1], [x1, y1], [x1, y0], [x0, y0]];
44908
44865
 
44909
- function getCoordinateDistance(distance, arcs) {
44910
- return arcs.isPlanar() ? distance : distance / R$3 * R2D$1;
44866
+ if (!(y1 > y0 && x1 > x0)) {
44867
+ stop$1("Invalid bbox (should be [xmin, ymin, xmax, ymax]):", bb);
44868
+ }
44869
+ return {
44870
+ arcs: new ArcCollection([arc]),
44871
+ layers: [{
44872
+ name: 'bbox',
44873
+ shapes: [[[0]]],
44874
+ geometry_type: 'polygon'
44875
+ }]
44876
+ };
44911
44877
  }
44912
44878
 
44913
- // @shapeIndex: chunk-bounds index of the source shape (see buildShapeSegmentIndex)
44914
- function ringIsOnSourceBoundary(points, shapeIndex, threshold) {
44915
- var n = points.length - 1; // skip duplicate endpoint
44916
- var step = Math.max(1, Math.floor(n / 20));
44917
- var sum = 0;
44918
- var count = 0;
44919
- for (var i = 0; i < n; i += step) {
44920
- sum += getPointToIndexedShapeDistance(points[i][0], points[i][1], shapeIndex);
44921
- count++;
44879
+ var OverlayUtils = /*#__PURE__*/Object.freeze({
44880
+ __proto__: null,
44881
+ mergeLayersForOverlay: mergeLayersForOverlay,
44882
+ mergeLayersForOverlay2: mergeLayersForOverlay2,
44883
+ normalizeOverlaySource: normalizeOverlaySource
44884
+ });
44885
+
44886
+ // Insert cutting points in arcs, where bbox intersects other shapes
44887
+ // Return a polygon layer containing the bounding box vectors, divided at cutting points.
44888
+ function divideDatasetByBBox(dataset, bbox) {
44889
+ var arcs = dataset.arcs;
44890
+ var data = findBBoxCutPoints(arcs, bbox);
44891
+ var map = insertCutPoints(data.cutPoints, arcs);
44892
+ arcs.dedupCoords();
44893
+ remapDividedArcs(dataset, map);
44894
+ // merge bbox dataset with target dataset,
44895
+ // so arcs are shared between target layers and bbox layer
44896
+ var clipDataset = bboxPointsToClipDataset(data.bboxPoints);
44897
+ var mergedDataset = mergeDatasets([dataset, clipDataset]);
44898
+ // TODO: detect if we need to rebuild topology (unlikely), like with the full clip command
44899
+ // buildTopology(mergedDataset);
44900
+ var clipLyr = mergedDataset.layers.pop();
44901
+ dataset.arcs = mergedDataset.arcs;
44902
+ dataset.layers = mergedDataset.layers;
44903
+ return clipLyr;
44904
+ }
44905
+
44906
+ function bboxPointsToClipDataset(arr) {
44907
+ var arcs = [];
44908
+ var shape = [];
44909
+ var layer = {geometry_type: 'polygon', shapes: [[shape]]};
44910
+ var p1, p2;
44911
+ for (var i=0, n=arr.length - 1; i<n; i++) {
44912
+ p1 = arr[i];
44913
+ p2 = arr[i+1];
44914
+ arcs.push([[p1.x, p1.y], [p2.x, p2.y]]);
44915
+ shape.push(i);
44922
44916
  }
44923
- return count > 0 && sum / count < threshold;
44917
+ return {
44918
+ arcs: new ArcCollection(arcs),
44919
+ layers: [layer]
44920
+ };
44924
44921
  }
44925
44922
 
44926
- function getPolygonMultiPolygonCoords(shape, arcs) {
44927
- var data = exportPathData(shape, arcs, 'polygon');
44928
- if (data.pointCount === 0) return [];
44929
- return groupPolygonRings(data.pathData, arcs, false).map(function(paths) {
44930
- return paths.map(function(path) {
44931
- return path.points.map(function(p) {
44932
- return p.concat();
44933
- });
44934
- });
44923
+ function findBBoxCutPoints(arcs, bbox) {
44924
+ var left = bbox[0],
44925
+ bottom = bbox[1],
44926
+ right = bbox[2],
44927
+ top = bbox[3];
44928
+
44929
+ // arrays of intersection points along each bbox edge
44930
+ var tt = [],
44931
+ rr = [],
44932
+ bb = [],
44933
+ ll = [];
44934
+
44935
+ arcs.forEachSegment(function(i, j, xx, yy) {
44936
+ var ax = xx[i],
44937
+ ay = yy[i],
44938
+ bx = xx[j],
44939
+ by = yy[j];
44940
+ var hit;
44941
+ if (segmentOutsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
44942
+ if (segmentInsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
44943
+
44944
+ hit = geom.segmentIntersection(left, top, right, top, ax, ay, bx, by);
44945
+ if (hit) addHit(tt, hit, i, j, xx, yy);
44946
+
44947
+ hit = geom.segmentIntersection(left, bottom, right, bottom, ax, ay, bx, by);
44948
+ if (hit) addHit(bb, hit, i, j, xx, yy);
44949
+
44950
+ hit = geom.segmentIntersection(left, bottom, left, top, ax, ay, bx, by);
44951
+ if (hit) addHit(ll, hit, i, j, xx, yy);
44952
+
44953
+ hit = geom.segmentIntersection(right, bottom, right, top, ax, ay, bx, by);
44954
+ if (hit) addHit(rr, hit, i, j, xx, yy);
44935
44955
  });
44936
- }
44937
44956
 
44938
- function getPolygonGeometry(shape, arcs) {
44939
- var coords = getPolygonMultiPolygonCoords(shape, arcs);
44940
- return coords.length > 0 ? {
44941
- type: 'MultiPolygon',
44942
- coordinates: coords
44943
- } : null;
44957
+ return {
44958
+ cutPoints: ll.concat(bb, rr, tt),
44959
+ bboxPoints: getDividedBBoxPoints(bbox, ll, tt, rr, bb)
44960
+ };
44961
+
44962
+ function addHit(arr, hit, i, j, xx, yy) {
44963
+ if (!hit) return;
44964
+ arr.push(formatHit(hit[0], hit[1], i, j, xx, yy));
44965
+ if (hit.length == 4) {
44966
+ arr.push(formatHit(hit[2], hit[3], i, j, xx, yy));
44967
+ }
44968
+ }
44969
+
44970
+ function formatHit(x, y, i, j, xx, yy) {
44971
+ var ids = formatIntersectingSegment(x, y, i, j, xx, yy);
44972
+ return getCutPoint(x, y, ids[0], ids[1]);
44973
+ }
44944
44974
  }
44945
44975
 
44946
- function getBufferDataset(coords) {
44947
- return importGeoJSON({
44948
- type: 'GeometryCollection',
44949
- geometries: [{
44950
- type: 'MultiPolygon',
44951
- coordinates: coords
44952
- }]
44953
- }, {type: 'polygon'});
44976
+ function segmentOutsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
44977
+ return ax < xmin && bx < xmin || ax > xmax && bx > xmax ||
44978
+ ay < ymin && by < ymin || ay > ymax && by > ymax;
44954
44979
  }
44955
44980
 
44956
- // Union a set of winding-fill offset rings (which self-overlap) into clean,
44957
- // non-self-overlapping MultiPolygon coordinates, via the winding-number
44958
- // dissolve. Used by the topological pipeline to feed an ordinary polygon into
44959
- // the shared mosaic (whose boundary-flood membership cannot resolve the
44960
- // self-overlapping construction ring directly).
44961
- function dissolveOffsetRingsToCoords(coords, opts) {
44962
- if (!coords || coords.length === 0) return [];
44963
- var dataset = getBufferDataset(coords);
44964
- if (!dataset.arcs) return [];
44965
- dissolveBufferDataset2(dataset, Object.assign({}, opts, {winding_fill: true}));
44966
- var lyr = dataset.layers[0];
44967
- var shape = lyr.shapes && lyr.shapes[0];
44968
- return shape ? getPolygonMultiPolygonCoords(shape, dataset.arcs) : [];
44981
+ function segmentInsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
44982
+ return ax > xmin && bx > xmin && ax < xmax && bx < xmax &&
44983
+ ay > ymin && by > ymin && ay < ymax && by < ymax;
44969
44984
  }
44970
44985
 
44971
- function getBufferMultiPolygonCoords(paths, distance, bufferMaker) {
44972
- var features, coords = [];
44973
- if (paths.length === 0) return coords;
44974
- features = bufferMaker(paths, distance) || [];
44975
- features.forEach(function(feat) {
44976
- var geom = feat && feat.geometry;
44977
- if (geom && geom.type == 'MultiPolygon') {
44978
- coords = coords.concat(geom.coordinates);
44979
- }
44980
- });
44981
- return coords;
44986
+ // Returns an array of points representing the vertices in
44987
+ // the bbox with cutting points inserted.
44988
+ function getDividedBBoxPoints(bbox, ll, tt, rr, bb) {
44989
+ var bl = {x: bbox[0], y: bbox[1]},
44990
+ tl = {x: bbox[0], y: bbox[3]},
44991
+ tr = {x: bbox[2], y: bbox[3]},
44992
+ br = {x: bbox[2], y: bbox[1]};
44993
+ ll = utils.sortOn(ll.concat([bl, tl]), 'y', true);
44994
+ tt = utils.sortOn(tt.concat([tl, tr]), 'x', true);
44995
+ rr = utils.sortOn(rr.concat([tr, br]), 'y', false);
44996
+ bb = utils.sortOn(bb.concat([br, bl]), 'x', false);
44997
+ return ll.concat(tt, rr, bb).reduce(function(memo, p2) {
44998
+ var p1 = memo.length > 0 ? memo[memo.length-1] : null;
44999
+ if (p1 === null || p1.x != p2.x || p1.y != p2.y) memo.push(p2);
45000
+ return memo;
45001
+ }, []);
44982
45002
  }
44983
45003
 
44984
- function getPolygonBufferPathData(shape, uniqueArcTest) {
44985
- var data = {paths: [], split: false};
44986
- (shape || []).forEach(function(path) {
44987
- var paths = uniqueArcTest ? splitPathAtSharedArcs(path, uniqueArcTest) :
44988
- [path.concat()];
44989
- if (paths.length != 1 || paths[0].length != path.length ||
44990
- !paths[0].every(function(arcId, i) {
44991
- return arcId == path[i];
44992
- })) {
44993
- data.split = true;
45004
+ var Bbox2Clipping = /*#__PURE__*/Object.freeze({
45005
+ __proto__: null,
45006
+ divideDatasetByBBox: divideDatasetByBBox,
45007
+ segmentInsideBBox: segmentInsideBBox,
45008
+ segmentOutsideBBox: segmentOutsideBBox
45009
+ });
45010
+
45011
+ cmd.clipLayers = function(target, src, dataset, opts) {
45012
+ return clipLayers(target, src, dataset, "clip", opts);
45013
+ };
45014
+
45015
+ cmd.eraseLayers = function(target, src, dataset, opts) {
45016
+ return clipLayers(target, src, dataset, "erase", opts);
45017
+ };
45018
+
45019
+ cmd.clipLayer = function(targetLyr, src, dataset, opts) {
45020
+ return cmd.clipLayers([targetLyr], src, dataset, opts)[0];
45021
+ };
45022
+
45023
+ cmd.eraseLayer = function(targetLyr, src, dataset, opts) {
45024
+ return cmd.eraseLayers([targetLyr], src, dataset, opts)[0];
45025
+ };
45026
+
45027
+ cmd.sliceLayers = function(target, src, dataset, opts) {
45028
+ return clipLayers(target, src, dataset, "slice", opts);
45029
+ };
45030
+
45031
+ cmd.sliceLayer = function(targetLyr, src, dataset, opts) {
45032
+ return cmd.sliceLayers([targetLyr], src, dataset, opts);
45033
+ };
45034
+
45035
+ function clipLayersInPlace(layers, clipSrc, dataset, type, opts) {
45036
+ var outputLayers = clipLayers(layers, clipSrc, dataset, type, opts);
45037
+ // remove arcs from the clipping dataset, if they are not used by any layer
45038
+ layers.forEach(function(lyr, i) {
45039
+ var lyr2 = outputLayers[i];
45040
+ lyr.shapes = lyr2.shapes;
45041
+ lyr.data = lyr2.data;
45042
+ if (lyr2.raster) {
45043
+ lyr.raster = lyr2.raster;
45044
+ lyr.raster_type = lyr2.raster_type;
44994
45045
  }
44995
- data.paths = data.paths.concat(paths);
44996
45046
  });
44997
- return data;
45047
+ dissolveArcs(dataset);
44998
45048
  }
44999
45049
 
45000
- function splitPathAtSharedArcs(path, uniqueArcTest) {
45001
- var flags = path.map(uniqueArcTest);
45002
- var firstShared = flags.indexOf(false);
45003
- var chains = [];
45004
- var chain = [];
45005
- var start, i, arcId;
45006
- if (firstShared == -1) return [path.concat()];
45007
- start = (firstShared + 1) % path.length;
45008
- for (i = 0; i < path.length; i++) {
45009
- arcId = path[(start + i) % path.length];
45010
- if (uniqueArcTest(arcId)) {
45011
- chain.push(arcId);
45012
- } else if (chain.length > 0) {
45013
- chains.push(chain);
45014
- chain = [];
45015
- }
45050
+ // @clipSrc: layer in @dataset or filename
45051
+ // @type: 'clip' or 'erase'
45052
+ function clipLayers(targetLayers, clipSrc, targetDataset, type, opts) {
45053
+ profileStart('clipLayers');
45054
+ opts = opts || {no_cleanup: true}; // TODO: update testing functions
45055
+ var usingPathClip = utils.some(targetLayers, layerHasPaths);
45056
+ var usingRasterClip = utils.some(targetLayers, layerHasRaster);
45057
+ var mergedDataset, clipLyr, nodes, result;
45058
+ var clipDataset;
45059
+ if (usingRasterClip) {
45060
+ result = clipRasterLayers(targetLayers, clipSrc, targetDataset, type, opts);
45061
+ profileEnd('clipLayers');
45062
+ return result;
45016
45063
  }
45017
- if (chain.length > 0) {
45018
- chains.push(chain);
45064
+ clipDataset = normalizeOverlaySource(clipSrc, targetDataset, opts);
45065
+ if (!opts.no_warn) {
45066
+ warnIfBoundsDontOverlap(targetLayers, targetDataset, clipDataset, type);
45019
45067
  }
45020
- return chains;
45068
+ if (opts.bbox2 && usingPathClip) { // assumes target dataset has arcs
45069
+ result = clipLayersByBBox(targetLayers, targetDataset, opts);
45070
+ profileEnd('clipLayers');
45071
+ return result;
45072
+ }
45073
+ if (!usingPathClip) {
45074
+ result = clipLayersByClipDataset(targetLayers, clipDataset, type, opts);
45075
+ profileEnd('clipLayers');
45076
+ return result;
45077
+ }
45078
+ // Merging the clip source into the target and cutting intersections builds a
45079
+ // throwaway combined arc collection. With a GUI undo transaction active,
45080
+ // addIntersectionCuts() would otherwise capture a full coordinate copy of the
45081
+ // merged target+clip arcs -- redundant work, since undo is driven by the
45082
+ // dataset-level reference swap below (the dataset unit records the original
45083
+ // arcs by reference before targetDataset.arcs is replaced).
45084
+ //
45085
+ // addIntersectionCuts() also remaps every shared layer's shapes in place
45086
+ // (rewriting arc ids for the split arcs), including the target layers. Those
45087
+ // original shapes ARE needed for undo, so capture each target layer's baseline
45088
+ // explicitly before suspending tracking for the throwaway construction.
45089
+ noteDatasetWillChange(targetDataset, {operation: type, unit: 'arcs'});
45090
+ targetLayers.forEach(function(lyr) {
45091
+ noteLayerWillChange(lyr, {operation: type});
45092
+ });
45093
+ // Build the clipped output with undo tracking suspended: the merged arcs, the
45094
+ // dissolved clip layer, and the per-layer clip results are all derived from
45095
+ // the throwaway combined dataset and never need restoring. Undo is driven by
45096
+ // the dataset reference swap plus the target-layer baseline captured above;
45097
+ // run-command captures the integration of the returned output layers.
45098
+ withActiveUndoTransaction(null, function() {
45099
+ profileStart('mergeLayersForOverlay');
45100
+ mergedDataset = mergeLayersForOverlay2(targetLayers, targetDataset, clipDataset);
45101
+ profileEnd('mergeLayersForOverlay');
45102
+ clipLyr = mergedDataset.layers[mergedDataset.layers.length-1];
45103
+ nodes = addIntersectionCuts(mergedDataset, opts);
45104
+ targetDataset.arcs = mergedDataset.arcs;
45105
+ profileStart('clipDissolvePolygonLayer2');
45106
+ clipLyr = utils.defaults({data: null}, clipLyr);
45107
+ clipLyr = dissolvePolygonLayer2(clipLyr, mergedDataset, {quiet: true, silent: true});
45108
+ profileEnd('clipDissolvePolygonLayer2');
45109
+ profileStart('clipLayersByLayer');
45110
+ result = clipLayersByLayer(targetLayers, clipLyr, nodes, type, opts);
45111
+ profileEnd('clipLayersByLayer');
45112
+ });
45113
+ markDatasetChanged(targetDataset, {operation: type, unit: 'arcs'});
45114
+ profileEnd('clipLayers');
45115
+ return result;
45021
45116
  }
45022
45117
 
45023
- function getUniqueArcTest(lyr, arcs) {
45024
- var classify = getArcClassifier(lyr, arcs, {reusable: true})(function(a, b) {
45025
- return b == -1 ? 'unique' : null;
45118
+ function clipLayersByClipDataset(targetLayers, clipDataset, type, opts) {
45119
+ var clipLyr = clipDataset.layers[0];
45120
+ var nodes = new NodeCollection(clipDataset.arcs);
45121
+ var result;
45122
+ profileStart('clipLayersByLayer');
45123
+ result = clipLayersByLayer(targetLayers, clipLyr, nodes, type, opts);
45124
+ profileEnd('clipLayersByLayer');
45125
+ return result;
45126
+ }
45127
+
45128
+ function clipRasterLayers(targetLayers, clipSrc, targetDataset, type, opts) {
45129
+ var clipDataset, clipBounds, bbox;
45130
+ if (type != 'clip') {
45131
+ stop$1('Raster layers only support clipping');
45132
+ }
45133
+ if (utils.some(targetLayers, function(lyr) {return !layerHasRaster(lyr);})) {
45134
+ stop$1('Raster clipping cannot be mixed with vector target layers');
45135
+ }
45136
+ if (opts.bbox2) {
45137
+ bbox = opts.bbox2;
45138
+ } else {
45139
+ clipDataset = normalizeOverlaySource(clipSrc, targetDataset, opts);
45140
+ clipBounds = getLayerBounds(clipDataset.layers[0], clipDataset.arcs);
45141
+ if (!clipBounds || !clipBounds.hasBounds()) {
45142
+ stop$1('Missing raster clipping bounds');
45143
+ }
45144
+ bbox = clipBounds.toArray();
45145
+ }
45146
+ return targetLayers.map(function(lyr) {
45147
+ clipRasterToBBox(lyr, bbox, opts);
45148
+ return lyr;
45026
45149
  });
45027
- return function(arcId) {
45028
- return !!classify(arcId);
45029
- };
45030
45150
  }
45031
45151
 
45032
- function ringArea(ring) {
45033
- var iter = new PointIter(ring);
45034
- return getSphericalPathArea2(iter);
45152
+ function clipLayersByBBox(layers, dataset, opts) {
45153
+ var bbox = opts.bbox2;
45154
+ var clipLyr = divideDatasetByBBox(dataset, bbox);
45155
+ var nodes = new NodeCollection(dataset.arcs);
45156
+ var retn = clipLayersByLayer(layers, clipLyr, nodes, 'clip', opts);
45157
+ return retn;
45035
45158
  }
45036
45159
 
45037
- function makePointBuffer(lyr, dataset, opts) {
45038
- var geojson = makePointBufferGeoJSON(lyr, dataset, opts);
45039
- return importGeoJSON(geojson, {});
45160
+ function clipLayersByLayer(targetLayers, clipLyr, nodes, type, opts) {
45161
+ requirePolygonLayer(clipLyr, "Requires a polygon clipping layer");
45162
+ return targetLayers.reduce(function(memo, targetLyr) {
45163
+ if (type == 'slice') {
45164
+ memo = memo.concat(sliceLayerByLayer(targetLyr, clipLyr, nodes, opts));
45165
+ } else {
45166
+ memo.push(clipLayerByLayer(targetLyr, clipLyr, nodes, type, opts));
45167
+ }
45168
+ return memo;
45169
+ }, []);
45040
45170
  }
45041
45171
 
45042
- // Make a single geodetic circle
45043
- function getCircleGeoJSON(center, radius, vertices, opts) {
45044
- var n = 360;
45045
- var geod = getGeodeticSegmentFunction(parseCrsString$1('wgs84')); // ?
45046
- if (opts.inset) {
45047
- radius -= opts.inset;
45048
- }
45049
- return opts.geometry_type == 'polyline' ?
45050
- getPointBufferLineString([center], radius, n, geod) :
45051
- getPointBufferPolygon([center], radius, n, geod, true);
45172
+ function getSliceLayerName(clipLyr, field, i) {
45173
+ var id = field ? clipLyr.data.getRecords()[0][field] : i + 1;
45174
+ return 'slice-' + id;
45052
45175
  }
45053
45176
 
45054
- // Convert a point layer to circles
45055
- function makePointBufferGeoJSON(lyr, dataset, opts) {
45056
- var vertices = opts.vertices || 72;
45057
- var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
45058
- var crs = getDatasetCRS(dataset);
45059
- var spherical = isLatLngCRS(crs);
45060
- var geod = getGeodeticSegmentFunction(crs);
45061
- var geometries = lyr.shapes.map(function(shape, i) {
45062
- var dist = distanceFn(i);
45063
- if (!dist || !shape) return null;
45064
- return getPointBufferPolygon(shape, dist, vertices, geod, spherical);
45177
+ function sliceLayerByLayer(targetLyr, clipLyr, nodes, opts) {
45178
+ // may not need no_replace
45179
+ var clipLayers = cmd.splitLayer(clipLyr, opts.id_field, {no_replace: true});
45180
+ return clipLayers.map(function(clipLyr, i) {
45181
+ var outputLyr = clipLayerByLayer(targetLyr, clipLyr, nodes, 'clip', opts);
45182
+ outputLyr.name = getSliceLayerName(clipLyr, opts.id_field, i);
45183
+ return outputLyr;
45065
45184
  });
45066
- // TODO: make sure that importer supports null geometries (nonstandard GeoJSON);
45067
- return {
45068
- type: 'GeometryCollection',
45069
- geometries: geometries
45070
- };
45071
45185
  }
45072
45186
 
45073
- function getPointBufferPolygon(points, distance, vertices, geod, spherical) {
45074
- var rings = [], coords, coords2;
45075
- if (!points || !points.length) return null;
45076
- for (var i=0; i<points.length; i++) {
45077
- coords = getPointBufferCoordinates(points[i], distance, vertices, geod);
45078
- if (!spherical) {
45079
- rings.push([coords]);
45080
- } else if (countCrosses(coords) > 0) {
45081
- coords2 = removePolygonCrosses([coords]);
45082
- while (coords2.length > 0) rings.push([coords2.pop()]); // geojson polygon coords, no hole
45083
- } else if (ringArea(coords) < 0) {
45084
- // negative spherical area: CCW ring, indicating a circle of >180 degrees
45085
- // that fully encloses both poles and the antimeridian.
45086
- // need to add an enclosure around the entire sphere
45087
- // TODO: compare to distance param as a sanity check
45088
- rings.push([
45089
- [[180, 90], [180, -90], [0, -90], [-180, -90], [-180, 90], [0, 90], [180, 90]],
45090
- coords
45091
- ]);
45092
- } else {
45093
- rings.push([coords]);
45094
- }
45187
+ function clipLayerByLayer(targetLyr, clipLyr, nodes, type, opts) {
45188
+ var arcs = nodes.arcs;
45189
+ var shapeCount = targetLyr.shapes ? targetLyr.shapes.length : 0;
45190
+ var nullCount = 0, sliverCount = 0;
45191
+ var clippedShapes, outputLyr;
45192
+ if (shapeCount === 0) {
45193
+ return targetLyr; // ignore empty layer
45194
+ }
45195
+ if (targetLyr === clipLyr) {
45196
+ stop$1('Can\'t clip a layer with itself');
45095
45197
  }
45096
- return {
45097
- type: 'MultiPolygon',
45098
- coordinates: rings
45099
- };
45100
- }
45101
45198
 
45102
- function getPointBufferLineString(points, distance, vertices, geod) {
45103
- var rings = [], coords;
45104
- if (!points || !points.length) return null;
45105
- for (var i=0; i<points.length; i++) {
45106
- coords = getPointBufferCoordinates(points[i], distance, vertices, geod);
45107
- coords = removePolylineCrosses(coords);
45108
- while (coords.length > 0) rings.push(coords.pop());
45199
+ // TODO: optimize some of these functions for bbox clipping
45200
+ if (targetLyr.geometry_type == 'point') {
45201
+ clippedShapes = clipPoints(targetLyr.shapes, clipLyr.shapes, arcs, type);
45202
+ } else if (targetLyr.geometry_type == 'polygon') {
45203
+ clippedShapes = clipPolygons(targetLyr.shapes, clipLyr.shapes, nodes, type, opts);
45204
+ } else if (targetLyr.geometry_type == 'polyline') {
45205
+ clippedShapes = clipPolylines(targetLyr.shapes, clipLyr.shapes, nodes, type);
45206
+ } else {
45207
+ stop$1('Invalid target layer:', targetLyr.name);
45109
45208
  }
45110
- return rings.length == 1 ? {
45111
- type: 'LineString',
45112
- coordinates: rings[0]
45113
- } : {
45114
- type: 'MultiLineString',
45115
- coordinates: rings
45209
+
45210
+ outputLyr = {
45211
+ name: targetLyr.name,
45212
+ geometry_type: targetLyr.geometry_type,
45213
+ shapes: clippedShapes,
45214
+ data: targetLyr.data // replaced post-filter
45116
45215
  };
45117
- }
45118
45216
 
45119
- // Returns array of [x, y] coordinates in a closed ring
45120
- function getPointBufferCoordinates(center, meterDist, vertices, geod) {
45121
- var coords = [],
45122
- angle = 360 / vertices,
45123
- theta;
45124
- for (var i=0; i<vertices; i++) {
45125
- // offsetting by half a step so 4 sides are flat, not pointy
45126
- // (looks better on low-vertex circles)
45127
- theta = (i + 0.5) * angle % 360;
45128
- coords.push(geod(center[0], center[1], theta, meterDist));
45217
+ // Remove sliver polygons
45218
+ if (opts.remove_slivers && outputLyr.geometry_type == 'polygon') {
45219
+ sliverCount = filterClipSlivers(outputLyr, clipLyr, arcs);
45129
45220
  }
45130
- coords.push(coords[0].concat());
45131
- return coords;
45132
- }
45133
-
45134
- // Returns number of arcs that were removed
45135
- function editArcs(arcs, onPoint) {
45136
- var nn2 = [],
45137
- xx2 = [],
45138
- yy2 = [],
45139
- errors = 0,
45140
- n;
45141
45221
 
45142
- arcs.forEach(function(arc, i) {
45143
- editArc(arc, onPoint);
45144
- });
45145
- arcs.updateVertexData(nn2, xx2, yy2);
45146
- return errors;
45222
+ // Remove null shapes (likely removed by clipping/erasing, although possibly already present)
45223
+ cmd.filterFeatures(outputLyr, arcs, {remove_empty: true, verbose: false});
45147
45224
 
45148
- function append(p) {
45149
- if (p) {
45150
- xx2.push(p[0]);
45151
- yy2.push(p[1]);
45152
- n++;
45153
- }
45225
+ // clone data records (to avoid sharing records between layers)
45226
+ // TODO: this is not needed when replacing target with a single layer
45227
+ if (outputLyr.data) {
45228
+ outputLyr.data = outputLyr.data.clone();
45154
45229
  }
45155
45230
 
45156
- function editArc(arc, cb) {
45157
- var x, y, xp, yp, retn;
45158
- var valid = true;
45159
- var i = 0;
45160
- n = 0;
45161
- while (arc.hasNext()) {
45162
- x = arc.x;
45163
- y = arc.y;
45164
- retn = cb(append, x, y, xp, yp, i++);
45165
- if (retn === false) {
45166
- valid = false;
45167
- // assumes that it's ok for the arc iterator to be interrupted.
45168
- break;
45169
- }
45170
- xp = x;
45171
- yp = y;
45172
- }
45173
- if (valid && n == 1) {
45174
- // only one valid point was added to this arc (invalid)
45175
- // e.g. this could happen during reprojection.
45176
- // making this arc empty
45177
- // error("An invalid arc was created");
45178
- message("An invalid arc was created");
45179
- valid = false;
45180
- }
45181
- if (valid) {
45182
- nn2.push(n);
45183
- } else {
45184
- // remove any points that were added for an invalid arc
45185
- while (n-- > 0) {
45186
- xx2.pop();
45187
- yy2.pop();
45188
- }
45189
- nn2.push(0); // add empty arc (to preserve mapping from paths to arcs)
45190
- errors++;
45191
- }
45231
+ // TODO: redo messages, now that many layers may be clipped
45232
+ nullCount = shapeCount - outputLyr.shapes.length;
45233
+ if (nullCount && sliverCount) {
45234
+ message(getClipMessage(nullCount, sliverCount));
45192
45235
  }
45236
+ return outputLyr;
45193
45237
  }
45194
45238
 
45195
- // Remove small-area polygon rings (very simple implementation of sliver removal)
45196
- // TODO: more sophisticated sliver detection (e.g. could consider ratio of area to perimeter)
45197
- // TODO: consider merging slivers into adjacent polygons to prevent gaps from forming
45198
- // TODO: consider separate gap removal function as an alternative to merging slivers
45199
- //
45200
- cmd.filterSlivers = function(lyr, dataset, opts) {
45201
- if (lyr.geometry_type != 'polygon') {
45202
- return 0;
45239
+ function getClipMessage(nullCount, sliverCount) {
45240
+ var nullMsg = nullCount ? utils.format('%,d null feature%s', nullCount, utils.pluralSuffix(nullCount)) : '';
45241
+ var sliverMsg = sliverCount ? utils.format('%,d sliver%s', sliverCount, utils.pluralSuffix(sliverCount)) : '';
45242
+ if (nullMsg || sliverMsg) {
45243
+ return utils.format('Removed %s%s%s', nullMsg, (nullMsg && sliverMsg ? ' and ' : ''), sliverMsg);
45203
45244
  }
45204
- return filterSlivers(lyr, dataset, opts);
45205
- };
45245
+ return '';
45246
+ }
45206
45247
 
45207
- function filterSlivers(lyr, dataset, optsArg) {
45208
- var opts = utils.extend({sliver_control: 1}, optsArg);
45209
- var filterData = getSliverFilter(lyr, dataset, opts);
45210
- var ringTest = filterData.filter;
45211
- var removed = 0;
45212
- var pathFilter = function(path, i, paths) {
45213
- if (ringTest(path, i, paths)) {
45214
- removed++;
45215
- return null;
45248
+ // Warn (once per source/target pair) when a -clip / -erase / slice almost
45249
+ // certainly won't do what the user wants. Two checks, in order of strength:
45250
+ //
45251
+ // 1. CRS mismatch -- one side is lat/lng and the other is projected. This
45252
+ // uses the same logic as requireDatasetsHaveCompatibleCRS() in
45253
+ // mapshaper-merging.mjs, but fires here as a friendlier, command-aware
45254
+ // warning before mergeDatasets() would otherwise stop() with a generic
45255
+ // message. It also catches a class of cases the bbox check misses: a
45256
+ // projected layer whose bbox straddles (0,0) often *does* overlap an
45257
+ // unprojected lat/lng bbox, even though the two are useless together.
45258
+ // 2. Bbox-disjoint -- a fallback for the case where CRSes look compatible
45259
+ // (or are unknown on both sides) but the layers clearly aren't in the
45260
+ // same place. Usually a wrong-source-picker error.
45261
+ //
45262
+ // Empty target layers are ignored (separate failure mode; would just be
45263
+ // noise). The CRS warning suppresses the bbox warning for the same target,
45264
+ // so users see one warning per problem, not two.
45265
+ // Skipped entirely if opts.no_warn is set.
45266
+ function warnIfBoundsDontOverlap(targetLayers, targetDataset, clipDataset, type) {
45267
+ var srcCRS = getDatasetCRS(clipDataset);
45268
+ var targetCRS = getDatasetCRS(targetDataset);
45269
+ var crsMismatch = srcCRS && targetCRS && isLatLngCRS(srcCRS) != isLatLngCRS(targetCRS);
45270
+ var srcName = clipDataset.layers[0].name || '<unnamed>';
45271
+ var srcBounds = getLayerBounds(clipDataset.layers[0], clipDataset.arcs);
45272
+ targetLayers.forEach(function(targetLyr) {
45273
+ var targetBounds = getLayerBounds(targetLyr, targetDataset.arcs);
45274
+ if (!targetBounds || !targetBounds.hasBounds()) return;
45275
+ if (crsMismatch) {
45276
+ warnOnce(formatCRSMismatchMessage(type, srcName, targetLyr.name,
45277
+ srcCRS, targetCRS));
45278
+ return; // Don't also fire the (likely-misleading) bbox warning.
45216
45279
  }
45217
- };
45218
-
45219
- noteLayerWillChange(lyr, {operation: 'filter-slivers', unit: 'shapes'});
45220
- editShapes(lyr.shapes, pathFilter);
45221
- markLayerChanged(lyr, {operation: 'filter-slivers', unit: 'shapes'});
45222
- message(utils.format("Removed %'d sliver%s using %s", removed, utils.pluralSuffix(removed), filterData.label));
45280
+ if (!srcBounds || !srcBounds.hasBounds()) return;
45281
+ if (srcBounds.intersects(targetBounds)) return;
45282
+ warnOnce(formatNoOverlapMessage(type, srcName, targetLyr.name,
45283
+ srcBounds, targetBounds));
45284
+ });
45285
+ }
45223
45286
 
45224
- // Remove null shapes (likely removed by clipping/erasing, although possibly already present)
45225
- if (opts.remove_empty) {
45226
- cmd.filterFeatures(lyr, dataset.arcs, {remove_empty: true, verbose: false});
45227
- }
45228
- return removed;
45287
+ function formatCRSMismatchMessage(type, srcName, targetName, srcCRS, targetCRS) {
45288
+ var verb = type === 'erase' ? 'erase' : 'clip';
45289
+ var srcKind = isLatLngCRS(srcCRS) ? 'lng/lat (geographic)' : 'projected';
45290
+ var targetKind = isLatLngCRS(targetCRS) ? 'lng/lat (geographic)' : 'projected';
45291
+ return '-' + verb + ': source "' + srcName + '" uses ' + srcKind +
45292
+ ' coordinates but target "' + (targetName || '<unnamed>') + '" uses ' +
45293
+ targetKind + ' coordinates. The -' + verb +
45294
+ ' will not produce a useful result; project one side to match the other first.';
45229
45295
  }
45230
45296
 
45231
- function filterClipSlivers(lyr, clipLyr, arcs) {
45232
- var threshold = getDefaultSliverThreshold(lyr, arcs);
45233
- // message('Using variable sliver threshold (based on ' + (threshold / 1e6) + ' sqkm)');
45234
- var ringTest = getSliverTest(arcs, threshold, 1);
45235
- var flags = new Uint8Array(arcs.size());
45236
- var removed = 0;
45237
- var pathFilter = function(path) {
45238
- var prevArcs = 0,
45239
- newArcs = 0;
45240
- for (var i=0, n=path && path.length || 0; i<n; i++) {
45241
- if (flags[absArcId(path[i])] > 0) {
45242
- newArcs++;
45243
- } else {
45244
- prevArcs++;
45245
- }
45246
- }
45247
- // filter paths that contain arcs from both original and clip/erase layers
45248
- // and are small
45249
- if (newArcs > 0 && prevArcs > 0 && ringTest(path)) {
45250
- removed++;
45251
- return null;
45252
- }
45253
- };
45297
+ function formatNoOverlapMessage(type, srcName, targetName, srcBounds, targetBounds) {
45298
+ // 'slice' is a per-feature -clip variant; in user terms it has the same
45299
+ // empty-output failure mode as clip, so we describe it under the same
45300
+ // verb to keep the message simple.
45301
+ var verb = type === 'erase' ? 'erase' : 'clip';
45302
+ var consequence = type === 'erase'
45303
+ ? 'will leave "' + (targetName || '<unnamed>') + '" unchanged'
45304
+ : 'will produce empty output for "' + (targetName || '<unnamed>') + '"';
45305
+ return '-' + verb + ': source "' + srcName + '" ' + bbToText(srcBounds) +
45306
+ ' does not overlap target "' + (targetName || '<unnamed>') + '" ' +
45307
+ bbToText(targetBounds) + '. The -' + verb + ' ' + consequence +
45308
+ '. This usually indicates a coordinate system mismatch.';
45309
+ }
45254
45310
 
45255
- countArcsInShapes(clipLyr.shapes, flags);
45256
- noteLayerWillChange(lyr, {operation: 'filter-clip-slivers', unit: 'shapes'});
45257
- editShapes(lyr.shapes, pathFilter);
45258
- markLayerChanged(lyr, {operation: 'filter-clip-slivers', unit: 'shapes'});
45259
- return removed;
45311
+ function bbToText(b) {
45312
+ return JSON.stringify(b.toArray());
45260
45313
  }
45261
45314
 
45262
- // Assumes: Arcs have been divided
45315
+ var ClipErase = /*#__PURE__*/Object.freeze({
45316
+ __proto__: null,
45317
+ clipLayers: clipLayers,
45318
+ clipLayersByBBox: clipLayersByBBox,
45319
+ clipLayersByLayer: clipLayersByLayer,
45320
+ clipLayersInPlace: clipLayersInPlace,
45321
+ getClipMessage: getClipMessage,
45322
+ warnIfBoundsDontOverlap: warnIfBoundsDontOverlap
45323
+ });
45324
+
45325
+ // Returns a function for constructing a query function that accepts an arc id and
45326
+ // returns information about the polygon or polygons that use the given arc.
45327
+ // TODO: explain this better.
45263
45328
  //
45264
- function clipPolylines(targetShapes, clipShapes, nodes, type) {
45265
- var index = new PathIndex(clipShapes, nodes.arcs);
45329
+ // options:
45330
+ // filter: optional filter function; signature: function(idA, idB or -1) : boolean
45331
+ // reusable: flag that lets an arc be queried multiple times.
45332
+ function getArcClassifier(lyr, arcs, optsArg) {
45333
+ var opts = optsArg || {},
45334
+ useOnce = !opts.reusable,
45335
+ n = arcs.size(),
45336
+ a = new Int32Array(n),
45337
+ b = new Int32Array(n),
45338
+ filter;
45339
+ if (opts.where) {
45340
+ filter = compileFeaturePairFilterExpression(opts.where, lyr, arcs);
45341
+ }
45266
45342
 
45267
- return targetShapes.map(function(shp) {
45268
- return clipPolyline(shp);
45269
- });
45343
+ utils.initializeArray(a, -1);
45344
+ utils.initializeArray(b, -1);
45270
45345
 
45271
- function clipPolyline(shp) {
45272
- var clipped = null;
45273
- if (shp) clipped = shp.reduce(clipPath, []);
45274
- return clipped && clipped.length > 0 ? clipped : null;
45275
- }
45346
+ traversePaths(lyr.shapes, function(o) {
45347
+ var i = absArcId(o.arcId);
45348
+ var shpId = o.shapeId;
45349
+ var aval = a[i];
45350
+ if (aval == -1) {
45351
+ a[i] = shpId;
45352
+ } else if (shpId < aval) {
45353
+ b[i] = aval;
45354
+ a[i] = shpId;
45355
+ } else {
45356
+ b[i] = shpId;
45357
+ }
45358
+ });
45276
45359
 
45277
- function clipPath(memo, path) {
45278
- var clippedPath = null,
45279
- arcId, enclosed;
45280
- for (var i=0; i<path.length; i++) {
45281
- arcId = path[i];
45282
- enclosed = index.arcIsEnclosed(arcId);
45283
- if (enclosed && type == 'clip' || !enclosed && type == 'erase') {
45284
- if (!clippedPath) {
45285
- memo.push(clippedPath = []);
45286
- }
45287
- clippedPath.push(arcId);
45288
- } else {
45289
- clippedPath = null;
45290
- }
45360
+ function classify(arcId, getKey) {
45361
+ var i = absArcId(arcId);
45362
+ var shpA = a[i];
45363
+ var shpB = b[i];
45364
+ var key;
45365
+ if (shpA == -1) return null;
45366
+ key = getKey(shpA, shpB);
45367
+ if (key === null || key === false) return null;
45368
+ if (useOnce) {
45369
+ // arc can only be queried once
45370
+ a[i] = -1;
45371
+ b[i] = -1;
45291
45372
  }
45292
- return memo;
45373
+ // use optional filter to exclude some arcs
45374
+ if (filter && !filter(shpA, shpB)) return null;
45375
+ return key;
45293
45376
  }
45377
+
45378
+ return function(getKey) {
45379
+ return function(arcId) {
45380
+ return classify(arcId, getKey);
45381
+ };
45382
+ };
45294
45383
  }
45295
45384
 
45296
- var PolylineClipping = /*#__PURE__*/Object.freeze({
45385
+ var ArcClassifier = /*#__PURE__*/Object.freeze({
45297
45386
  __proto__: null,
45298
- clipPolylines: clipPolylines
45387
+ getArcClassifier: getArcClassifier
45299
45388
  });
45300
45389
 
45301
- // TODO: to prevent invalid holes,
45302
- // could erase the holes from the space-enclosing rings.
45303
- function appendHolesToRings(cw, ccw) {
45304
- for (var i=0, n=ccw.length; i<n; i++) {
45305
- cw.push(ccw[i]);
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
+ });
45306
45403
  }
45307
- return cw;
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);
45416
+ }
45417
+ }
45418
+ return dataset2;
45308
45419
  }
45309
45420
 
45310
- function getPolygonDissolver(nodes, spherical) {
45311
- spherical = spherical && !nodes.arcs.isPlanar();
45312
- var flags = new Uint8Array(nodes.arcs.size());
45313
- var divide = getHoleDivider(nodes, spherical);
45314
- var pathfind = getRingIntersector(nodes, flags);
45315
-
45316
- return function(shp) {
45317
- if (!shp) return null;
45318
- var cw = [],
45319
- ccw = [];
45320
-
45321
- divide(shp, cw, ccw);
45322
- cw = pathfind(cw, 'flatten');
45323
- ccw.forEach(reversePath);
45324
- ccw = pathfind(ccw, 'flatten');
45325
- ccw.forEach(reversePath);
45326
- var shp2 = appendHolesToRings(cw, ccw);
45327
- var dissolved = pathfind(shp2, 'dissolve');
45421
+ // World rectangle (lng/lat) the polar buffer is clipped to.
45422
+ var POLAR_WORLD_BBOX = [-180, -90, 180, 90];
45328
45423
 
45329
- if (dissolved.length > 1) {
45330
- dissolved = fixNestingErrors(dissolved, nodes.arcs);
45424
+ // Buffer a polygon sliced at the antimeridian (lng +/-180) and/or a pole
45425
+ // (lat +/-90): build the offset (the pole-touching source rings are added back,
45426
+ // see makePolygonBufferGeoJSON), dissolve, and clip to the world rectangle
45427
+ // instead of wrapping at the antimeridian.
45428
+ //
45429
+ // Only positive (grow) distances are supported. A negative (erode) buffer would
45430
+ // have to keep the artificial seam edges pinned to the extent while only the
45431
+ // coastline moves inward; the winding-fill construction can't do that without
45432
+ // producing self-intersecting geometry (the pinned seam coincides with the
45433
+ // source boundary, which the dissolve cannot resolve), so we reject it with a
45434
+ // clear message rather than emit a result whose seams have crept inward.
45435
+ function makePolarPolygonBuffer(lyr, dataset, opts) {
45436
+ if (polarBufferHasNegativeDistance(lyr, dataset, opts)) {
45437
+ stop$1('The polar option does not support negative (erode) buffers yet.');
45438
+ }
45439
+ var output = makePolygonBufferGeoJSON(lyr, dataset, opts);
45440
+ var dataset2 = importGeoJSON(output.geojson, {type: 'polygon'});
45441
+ if (dataset2.arcs) {
45442
+ if (output.dissolveAfterSplit) {
45443
+ dissolveBufferDataset2(dataset2, opts);
45331
45444
  }
45332
-
45333
- return dissolved.length > 0 ? dissolved : null;
45334
- };
45445
+ clipDatasetToWorldRect(dataset2);
45446
+ }
45447
+ return dataset2;
45335
45448
  }
45336
45449
 
45337
- // TODO: remove dependency on old polygon dissolve function
45450
+ function polarBufferHasNegativeDistance(lyr, dataset, opts) {
45451
+ var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
45452
+ return (lyr.shapes || []).some(function(shape, i) {
45453
+ return shape && distanceFn(i) < 0;
45454
+ });
45455
+ }
45338
45456
 
45339
- // assumes layers and arcs have been prepared for clipping
45340
- function clipPolygons(targetShapes, clipShapes, nodes, type, optsArg) {
45341
- profileStart('clipPolygons');
45342
- var arcs = nodes.arcs;
45343
- var opts = optsArg || {};
45344
- var clipFlags = new Uint8Array(arcs.size());
45345
- var routeFlags = new Uint8Array(arcs.size());
45346
- var clipArcTouches = 0;
45347
- var clipArcUses = 0;
45348
- var usedClipArcs = [];
45349
- var findPath = getPathFinder(nodes, useRoute, routeIsActive);
45350
- var dissolvePolygon = getPolygonDissolver(nodes);
45457
+ function clipDatasetToWorldRect(dataset) {
45458
+ if (!dataset.arcs || !dataset.layers.length) return;
45459
+ dataset.layers = clipLayersByBBox(dataset.layers, dataset,
45460
+ {bbox2: POLAR_WORLD_BBOX});
45461
+ }
45351
45462
 
45352
- if (!opts.bbox2) {
45353
- profileStart('cp.dissolveTargetRings');
45354
- targetShapes = targetShapes.map(dissolvePolygon);
45355
- profileEnd('cp.dissolveTargetRings');
45463
+ function makePolygonBufferGeoJSON(lyr, dataset, opts) {
45464
+ var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
45465
+ var useTopologicalMode = !!opts.topological;
45466
+ var uniqueArcTest = useTopologicalMode ? getUniqueArcTest(lyr, dataset.arcs) : null;
45467
+ var hasPositiveDistance = false;
45468
+ var hasNegativeDistance = false;
45469
+ if (useTopologicalMode) {
45470
+ // The topological pipeline selects mosaic tiles by source membership
45471
+ // (boundary flood), which cannot resolve the self-overlapping winding-fill
45472
+ // ring. So each feature's winding-fill offset rings are pre-dissolved into a
45473
+ // clean (non-self-overlapping) polygon before they enter the shared mosaic;
45474
+ // the construction speedup (loop removal cutting per-feature self-
45475
+ // intersections) is preserved, and the mosaic is unchanged.
45476
+ return makeTopologicalPolygonBufferGeoJSON(lyr, dataset, opts, distanceFn,
45477
+ uniqueArcTest, getPolygonRingBufferMaker(dataset, opts, 'left'));
45356
45478
  }
45357
-
45358
- profileStart('cp.openClipRoutes');
45359
- openArcRoutes(clipShapes, arcs, clipFlags, type == 'clip', type == 'erase', true, 0x11);
45360
- profileEnd('cp.openClipRoutes');
45361
- profileStart('cp.PathIndex#1');
45362
- var index = new PathIndex(clipShapes, arcs);
45363
- profileEnd('cp.PathIndex#1');
45364
- profileStart('cp.clipShapes');
45365
- var clippedShapes = targetShapes.map(function(shape, i) {
45366
- if (shape) {
45367
- return clipPolygon(shape, type, index);
45479
+ // Closed source rings are offset with the winding-fill construction: one
45480
+ // self-overlapping ring per source ring (its overshoot loops resolved by the
45481
+ // winding-number dissolve in makeClosedRingBufferGeometry) instead of many
45482
+ // overlapping per-segment section bands. The single ring carries far fewer
45483
+ // rings and self-intersections into the dissolve, which dominates polygon-
45484
+ // buffer runtime.
45485
+ var leftBufferMaker = getPolygonRingBufferMaker(dataset, opts, 'left');
45486
+ var rightBufferMaker = getPolygonRingBufferMaker(dataset, opts, 'right');
45487
+ var geometries = lyr.shapes.map(function(shape, i) {
45488
+ var distance = distanceFn(i);
45489
+ if (!distance || !shape) return null;
45490
+ if (distance < 0) {
45491
+ hasNegativeDistance = true;
45492
+ return makeNegativePolygonBufferGeometry(shape, -distance, dataset, opts,
45493
+ rightBufferMaker);
45368
45494
  }
45369
- return null;
45370
- });
45371
- profileEnd('cp.clipShapes');
45372
-
45373
- markPathsAsUsed(clippedShapes, routeFlags);
45374
-
45375
- profileStart('cp.findUndividedClip');
45376
- var undividedClipShapes = findUndividedClipShapes(clipShapes);
45377
- profileEnd('cp.findUndividedClip');
45378
-
45379
- closeArcRoutes(clipShapes, arcs, routeFlags, true, true);
45380
- profileStart('cp.PathIndex#2');
45381
- index = new PathIndex(undividedClipShapes, arcs);
45382
- profileEnd('cp.PathIndex#2');
45383
- profileStart('cp.findInteriorPaths');
45384
- targetShapes.forEach(function(shape, shapeId) {
45385
- var paths = shape ? findInteriorPaths(shape, type, index) : null;
45386
- if (paths) {
45387
- clippedShapes[shapeId] = (clippedShapes[shapeId] || []).concat(paths);
45495
+ hasPositiveDistance = true;
45496
+ var geom = makePositivePolygonBufferGeometry(shape, distance, dataset, opts,
45497
+ leftBufferMaker);
45498
+ if (opts.polar && shapeReachesPole(shape, dataset.arcs)) {
45499
+ // A pole-touching ring collapses onto the pole line (Mercator can't reach
45500
+ // the pole) and the ring-filtering drops it, so the winding fill keeps only
45501
+ // the coastline band. Append the source rings; the dataset-level dissolve
45502
+ // unions them with the band into the full grown shape. (Shapes that don't
45503
+ // reach a pole -- e.g. the complement ocean in the negative path -- fill
45504
+ // correctly on their own, and the appended source would tangle them.)
45505
+ geom = appendSourceRings(geom, shape, dataset.arcs);
45388
45506
  }
45507
+ return geom;
45389
45508
  });
45390
- profileEnd('cp.findInteriorPaths');
45509
+ return {
45510
+ geojson: {
45511
+ type: 'GeometryCollection',
45512
+ geometries: geometries
45513
+ },
45514
+ dissolveAfterSplit: hasPositiveDistance && !hasNegativeDistance
45515
+ };
45516
+ }
45391
45517
 
45392
- profileEnd('clipPolygons');
45393
- return clippedShapes;
45518
+ // True if any vertex of the shape sits at a pole (lat +/-90). Such a ring gets
45519
+ // pinched onto the pole line during Mercator offset construction.
45520
+ function shapeReachesPole(shape, arcs) {
45521
+ var b = arcs.getMultiShapeBounds(shape);
45522
+ return b.ymax >= 90 - 1e-3 || b.ymin <= -90 + 1e-3;
45523
+ }
45394
45524
 
45395
- function clipPolygon(shape, type, index) {
45396
- var dividedShape = [],
45397
- clipping = type == 'clip',
45398
- erasing = type == 'erase';
45525
+ // Combine a buffer geometry with the source polygon's rings into one
45526
+ // MultiPolygon (overlapping); a later union dissolve merges them. Used by the
45527
+ // polar option to keep the polar interior that the pole-pinched ribbon drops.
45528
+ function appendSourceRings(geom, shape, arcs) {
45529
+ var sourceCoords = getPolygonMultiPolygonCoords(shape, arcs);
45530
+ var coords = [];
45531
+ if (geom && geom.type == 'MultiPolygon') coords = coords.concat(geom.coordinates);
45532
+ else if (geom && geom.type == 'Polygon') coords.push(geom.coordinates);
45533
+ coords = coords.concat(sourceCoords);
45534
+ if (coords.length === 0) return null;
45535
+ return {type: 'MultiPolygon', coordinates: coords};
45536
+ }
45399
45537
 
45400
- // open pathways for entire polygon rather than one ring at a time --
45401
- // need to create polygons that connect positive-space rings and holes
45402
- openArcRoutes(shape, arcs, routeFlags, true, false, false);
45538
+ 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;
45542
+ var makerOpts = Object.assign({}, opts, {
45543
+ left: side == 'left',
45544
+ right: side == 'right',
45545
+ // Winding-fill construction also enables overshoot-loop removal on the single
45546
+ // offset ring (see buildOneSidedRings); both are safe here because polygon
45547
+ // rings are closed and their offsets are doubly-covered.
45548
+ winding_fill: useWinding
45549
+ });
45550
+ return getPolylineBufferMaker(dataset, makerOpts);
45551
+ }
45403
45552
 
45404
- forEachShapePart(shape, function(ids) {
45405
- var path;
45406
- for (var i=0, n=ids.length; i<n; i++) {
45407
- clipArcTouches = 0;
45408
- clipArcUses = 0;
45409
- path = findPath(ids[i]);
45410
- if (path) {
45411
- // if ring doesn't touch/intersect a clip/erase polygon, check if it is contained
45412
- // if (clipArcTouches === 0) {
45413
- // if ring doesn't incorporate an arc from the clip/erase polygon,
45414
- // check if it is contained (assumes clip shapes are dissolved)
45415
- if (clipArcTouches === 0 || clipArcUses === 0) { //
45416
- var contained = index.pathIsEnclosed(path);
45417
- if (clipping && contained || erasing && !contained) {
45418
- dividedShape.push(path);
45419
- }
45420
- // TODO: Consider breaking if polygon is unchanged
45421
- } else {
45422
- dividedShape.push(path);
45423
- }
45424
- }
45425
- }
45426
- });
45553
+ function makePositivePolygonBufferGeometry(shape, distance, dataset, opts,
45554
+ leftBufferMaker) {
45555
+ // Non-topological positive buffers are never path-split (only the topological
45556
+ // pipeline splits, and it has its own function), so each shape's ring groups
45557
+ // are offset and dissolved directly.
45558
+ return makeClosedRingPositiveBufferGeometry(shape, dataset.arcs,
45559
+ distance, opts, leftBufferMaker);
45560
+ }
45427
45561
 
45562
+ function makeTopologicalPolygonBufferGeoJSON(lyr, dataset, opts, distanceFn,
45563
+ uniqueArcTest, bufferMaker) {
45564
+ var shapes = lyr.shapes || [];
45565
+ var distances = [];
45566
+ var sourceIds = [];
45567
+ var bufferIds = [];
45568
+ var tmpGeometries = [];
45569
+ var hasPositiveDistance = false;
45570
+ var geometries, tmpDataset;
45428
45571
 
45429
- // Clear pathways of current target shape to hidden/closed
45430
- closeArcRoutes(shape, arcs, routeFlags, true, true, true);
45431
- // Also clear pathways of any clip arcs that were used
45432
- if (usedClipArcs.length > 0) {
45433
- closeArcRoutes(usedClipArcs, arcs, routeFlags, true, true, true);
45434
- usedClipArcs = [];
45572
+ shapes.forEach(function(shape, i) {
45573
+ sourceIds[i] = -1;
45574
+ bufferIds[i] = -1;
45575
+ distances[i] = distanceFn(i);
45576
+ if (distances[i] < 0) {
45577
+ stop$1('The topological buffer option does not support negative distances');
45435
45578
  }
45579
+ if (!shape) return;
45580
+ sourceIds[i] = tmpGeometries.length;
45581
+ tmpGeometries.push(getPolygonGeometry(shape, dataset.arcs));
45582
+ });
45436
45583
 
45437
- return dividedShape.length === 0 ? null : dividedShape;
45438
- }
45584
+ shapes.forEach(function(shape, i) {
45585
+ var distance = distances[i];
45586
+ var pathData, bufferCoords;
45587
+ if (!distance || !shape) return;
45588
+ hasPositiveDistance = true;
45589
+ pathData = getPolygonBufferPathData(shape, uniqueArcTest);
45590
+ bufferCoords = getBufferMultiPolygonCoords(pathData.paths, distance, bufferMaker);
45591
+ // Resolve the winding-fill rings' self-overlaps into a clean polygon (the
45592
+ // mosaic's boundary-flood membership cannot), so this feature enters the
45593
+ // shared mosaic as an ordinary polygon. The sector-band fallback emits
45594
+ // boundary-flood-resolvable bands, so it feeds the mosaic directly (as the
45595
+ // topological pipeline did before the winding-fill construction).
45596
+ if (!opts.sector_band) {
45597
+ bufferCoords = dissolveOffsetRingsToCoords(bufferCoords, opts);
45598
+ }
45599
+ if (bufferCoords.length > 0) {
45600
+ bufferIds[i] = tmpGeometries.length;
45601
+ tmpGeometries.push({
45602
+ type: 'MultiPolygon',
45603
+ coordinates: bufferCoords
45604
+ });
45605
+ }
45606
+ });
45439
45607
 
45440
- function routeIsActive(id) {
45441
- var fw = id >= 0,
45442
- abs = fw ? id : ~id,
45443
- visibleBit = fw ? 1 : 0x10,
45444
- targetBits = routeFlags[abs],
45445
- clipBits = clipFlags[abs];
45446
-
45447
- if (clipBits > 0) clipArcTouches++;
45448
- return (targetBits & visibleBit) > 0 || (clipBits & visibleBit) > 0;
45449
- }
45450
-
45451
- function useRoute(id) {
45452
- var fw = id >= 0,
45453
- abs = fw ? id : ~id,
45454
- targetBits = routeFlags[abs],
45455
- clipBits = clipFlags[abs],
45456
- targetRoute, clipRoute;
45457
-
45458
- if (fw) {
45459
- targetRoute = targetBits;
45460
- clipRoute = clipBits;
45461
- } else {
45462
- targetRoute = targetBits >> 4;
45463
- clipRoute = clipBits >> 4;
45464
- }
45465
- targetRoute &= 3;
45466
- clipRoute &= 3;
45467
-
45468
- var usable = false;
45469
- // var usable = targetRoute === 3 || targetRoute === 0 && clipRoute == 3;
45470
- if (targetRoute == 3) {
45471
- // special cases where clip route and target route both follow this arc
45472
- if (clipRoute == 1) ; else if (clipRoute == 2 && type == 'erase') ; else {
45473
- usable = true;
45474
- }
45475
-
45476
- } else if (targetRoute === 0 && clipRoute == 3) {
45477
- usedClipArcs.push(id);
45478
- usable = true;
45479
- }
45480
-
45481
- if (usable) {
45482
- if (clipRoute == 3) {
45483
- clipArcUses++;
45484
- }
45485
- // Need to close all arcs after visiting them -- or could cause a cycle
45486
- // on layers with strange topology
45487
- if (fw) {
45488
- targetBits = setBits(targetBits, 1, 3);
45489
- } else {
45490
- targetBits = setBits(targetBits, 0x10, 0x30);
45491
- }
45492
- }
45493
-
45494
- targetBits |= fw ? 4 : 0x40; // record as visited
45495
- routeFlags[abs] = targetBits;
45496
- return usable;
45497
- }
45498
-
45499
-
45500
- // Filter a collection of shapes to exclude paths that incorporate parts of
45501
- // clip/erase polygons and paths that are hidden (e.g. internal boundaries)
45502
- function findUndividedClipShapes(clipShapes) {
45503
- return clipShapes.map(function(shape) {
45504
- var usableParts = [];
45505
- forEachShapePart(shape, function(ids) {
45506
- var pathIsClean = true,
45507
- pathIsVisible = false;
45508
- for (var i=0; i<ids.length; i++) {
45509
- // check if arc was used in fw or rev direction
45510
- if (!arcIsUnused(ids[i], routeFlags)) {
45511
- pathIsClean = false;
45512
- break;
45513
- }
45514
- // check if clip arc is visible
45515
- if (!pathIsVisible && arcIsVisible(ids[i], clipFlags)) {
45516
- pathIsVisible = true;
45517
- }
45518
- }
45519
- if (pathIsClean && pathIsVisible) usableParts.push(ids);
45520
- });
45521
- return usableParts.length > 0 ? usableParts : null;
45522
- });
45523
- }
45524
-
45525
-
45526
- function arcIsUnused(id, flags) {
45527
- var abs = absArcId(id),
45528
- flag = flags[abs];
45529
- return (flag & 0x88) === 0;
45530
- // return id < 0 ? (flag & 0x80) === 0 : (flag & 0x8) === 0;
45531
- }
45532
-
45533
- function arcIsVisible(id, flags) {
45534
- var flag = flags[absArcId(id)];
45535
- return (flag & 0x11) > 0;
45608
+ if (!hasPositiveDistance || tmpGeometries.length === 0) {
45609
+ geometries = shapes.map(function() { return null; });
45610
+ } else {
45611
+ tmpDataset = importGeoJSON({
45612
+ type: 'GeometryCollection',
45613
+ geometries: tmpGeometries
45614
+ }, {type: 'polygon'});
45615
+ geometries = makeTopologicalPolygonBufferGeometries(shapes, distances,
45616
+ sourceIds, bufferIds, tmpDataset, dataset.arcs);
45536
45617
  }
45618
+ return {
45619
+ geojson: {
45620
+ type: 'GeometryCollection',
45621
+ geometries: geometries
45622
+ },
45623
+ dissolveAfterSplit: hasPositiveDistance
45624
+ };
45625
+ }
45537
45626
 
45538
- // search for indexed clipping paths contained in a shape
45539
- // dissolve them if needed
45540
- function findInteriorPaths(shape, type, index) {
45541
- var enclosedPaths = index.findPathsInsideShape(shape),
45542
- dissolvedPaths = [];
45543
- if (!enclosedPaths) return null;
45544
- // ...
45545
- if (type == 'erase') enclosedPaths.forEach(reversePath);
45546
- if (enclosedPaths.length <= 1) {
45547
- dissolvedPaths = enclosedPaths; // no need to dissolve single-part paths
45548
- } else {
45549
- openArcRoutes(enclosedPaths, arcs, routeFlags, true, false, true);
45550
- enclosedPaths.forEach(function(ids) {
45551
- var path;
45552
- for (var j=0; j<ids.length; j++) {
45553
- path = findPath(ids[j]);
45554
- if (path) {
45555
- dissolvedPaths.push(path);
45556
- }
45557
- }
45558
- });
45559
- }
45627
+ function makeTopologicalPolygonBufferGeometries(shapes, distances, sourceIds,
45628
+ bufferIds, tmpDataset, sourceArcs) {
45629
+ var tmpLyr = tmpDataset.layers[0];
45630
+ var nodes = addIntersectionCuts(tmpDataset, {rebuild_topology: true});
45631
+ var mosaicIndex = new MosaicIndex(tmpLyr, nodes, {flat: false, no_holes: false});
45632
+ var pathfind = getRingIntersector(mosaicIndex.nodes);
45633
+ var sourceIdIndex = getIdLookup(sourceIds);
45634
+ var bufferIdIndex = getIdToFeatureIdLookup(bufferIds);
45635
+ var sourceAreas = getSourceShapeAreas(shapes, sourceArcs);
45636
+ return shapes.map(function(shape, i) {
45637
+ var distance = distances[i];
45638
+ var tileIds, geom;
45639
+ if (!distance || !shape) return null;
45640
+ tileIds = getTopologicalBufferTileIds(sourceIds[i], bufferIds[i],
45641
+ i, mosaicIndex, sourceIdIndex, bufferIdIndex, sourceAreas);
45642
+ geom = getTileIdsGeometry(tileIds, mosaicIndex, pathfind);
45643
+ return removePositiveBufferArtifactHoles(geom, shape, sourceArcs, distance);
45644
+ });
45645
+ }
45560
45646
 
45561
- return dissolvedPaths.length > 0 ? dissolvedPaths : null;
45647
+ function getTopologicalBufferTileIds(sourceId, bufferId, featureId, mosaicIndex,
45648
+ sourceIdIndex, bufferIdIndex, sourceAreas) {
45649
+ var ids = [];
45650
+ var index = [];
45651
+ addTileIds(ids, index, mosaicIndex.getTileIdsByShapeId(sourceId));
45652
+ if (bufferId >= 0) {
45653
+ addTileIds(ids, index, mosaicIndex.getTileIdsByShapeId(bufferId).filter(function(tileId) {
45654
+ return !tileHasSourcePolygon(tileId, mosaicIndex, sourceIdIndex) &&
45655
+ getBufferTileOwnerId(tileId, mosaicIndex, bufferIdIndex, sourceAreas) == featureId;
45656
+ }));
45562
45657
  }
45563
- } // end clipPolygons()
45658
+ return ids;
45659
+ }
45564
45660
 
45565
- //
45566
- function clipPoints(points, clipShapes, arcs, type) {
45567
- var index = new PathIndex(clipShapes, arcs);
45661
+ function addTileIds(memo, index, ids) {
45662
+ ids.forEach(function(id) {
45663
+ if (index[id]) return;
45664
+ index[id] = true;
45665
+ memo.push(id);
45666
+ });
45667
+ }
45568
45668
 
45569
- var points2 = points.reduce(function(memo, feat) {
45570
- var n = feat ? feat.length : 0,
45571
- feat2 = [],
45572
- enclosed;
45669
+ function tileHasSourcePolygon(tileId, mosaicIndex, sourceIdIndex) {
45670
+ return mosaicIndex.getSourceIdsByTileId(tileId).some(function(shapeId) {
45671
+ return sourceIdIndex[shapeId];
45672
+ });
45673
+ }
45573
45674
 
45574
- for (var i=0; i<n; i++) {
45575
- enclosed = index.findEnclosingShape(feat[i]) > -1;
45576
- if (type == 'clip' && enclosed || type == 'erase' && !enclosed) {
45577
- feat2.push(feat[i].concat());
45675
+ function getBufferTileOwnerId(tileId, mosaicIndex, bufferIdIndex, sourceAreas) {
45676
+ var ownerId = -1;
45677
+ var ownerArea = -Infinity;
45678
+ mosaicIndex.getSourceIdsByTileId(tileId).forEach(function(shapeId) {
45679
+ 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;
45578
45686
  }
45579
45687
  }
45580
-
45581
- memo.push(feat2.length > 0 ? feat2 : null);
45582
- return memo;
45583
- }, []);
45584
-
45585
- return points2;
45688
+ });
45689
+ return ownerId;
45586
45690
  }
45587
45691
 
45588
- var ClipPoints = /*#__PURE__*/Object.freeze({
45589
- __proto__: null,
45590
- clipPoints: clipPoints
45591
- });
45692
+ function getSourceShapeAreas(shapes, arcs) {
45693
+ return shapes.map(function(shape) {
45694
+ return Math.abs(getShapeArea(shape, arcs));
45695
+ });
45696
+ }
45592
45697
 
45593
- // Convert a source parameter (which can take several forms) into
45594
- // a dataset with a single layer.
45595
- function normalizeOverlaySource(clipSrc, targetDataset, optsArg) {
45596
- var opts = optsArg || {};
45597
- var bbox = opts.bbox || opts.bbox2;
45598
- var clipDataset;
45599
- if (bbox) {
45600
- clipDataset = convertClipBounds(bbox);
45601
- } else if (!clipSrc) {
45602
- stop$1('Command requires a source file, layer id or bbox');
45603
- } else if (clipSrc.geometry_type) {
45604
- // clipSrc is a layer (assumed in targetDataset)
45605
- // TODO: update tests to remove this case (only used in tests)
45606
- // error('Unsupported source format');
45607
- clipDataset = utils.defaults({layers: [clipSrc], disposable: true}, targetDataset);
45608
- } else if (clipSrc.layer && clipSrc.dataset) {
45609
- clipDataset = utils.defaults({layers: [clipSrc.layer]}, clipSrc.dataset);
45610
- } else if (clipSrc.layers?.length == 1) {
45611
- clipDataset = clipSrc;
45612
- } else {
45613
- error('Invalid source format');
45614
- }
45615
- if (clipSrc?.disposable) {
45616
- clipDataset.disposable = true;
45617
- }
45618
- return clipDataset;
45698
+ function getIdLookup(ids) {
45699
+ var index = [];
45700
+ ids.forEach(function(id) {
45701
+ if (id >= 0) index[id] = true;
45702
+ });
45703
+ return index;
45619
45704
  }
45620
45705
 
45621
- // Create a merged dataset by appending the overlay layer to the target dataset
45622
- // so it is last in the layers array.
45623
- // DOES NOT insert clipping points
45624
- function mergeLayersForOverlay(targetLayers, targetDataset, clipSrc, opts) {
45625
- var clipDataset = normalizeOverlaySource(clipSrc, targetDataset, opts);
45626
- return mergeLayersForOverlay2(targetLayers, targetDataset, clipDataset);
45706
+ function getIdToFeatureIdLookup(ids) {
45707
+ var index = [];
45708
+ ids.forEach(function(id, i) {
45709
+ if (id >= 0) index[id] = i;
45710
+ });
45711
+ return index;
45627
45712
  }
45628
45713
 
45629
- function mergeLayersForOverlay2(targetLayers, targetDataset, clipDataset) {
45630
- var mergedDataset;
45631
- var clipLyr = clipDataset.layers[0];
45632
- if (targetDataset.arcs != clipDataset.arcs) {
45633
- // using external dataset -- need to merge arcs
45634
- if (!clipDataset.disposable) {
45635
- // copy overlay layer shapes because arc ids will be reindexed during merging
45636
- clipDataset.layers[0] = copyLayerShapes(clipDataset.layers[0]);
45714
+ function getTileIdsGeometry(tileIds, mosaicIndex, pathfind) {
45715
+ var rings = [];
45716
+ var holes = [];
45717
+ var shp;
45718
+ tileIds.forEach(function(tileId) {
45719
+ var tile = mosaicIndex.mosaic[tileId];
45720
+ rings.push(tile[0]);
45721
+ if (tile.length > 1) {
45722
+ holes = holes.concat(tile.slice(1));
45637
45723
  }
45638
- // merge external dataset with target dataset,
45639
- // so arcs are shared between target layers and clipping lyr
45640
- // Assumes that layers in clipDataset can be modified (if necessary, a copy should be passed in)
45641
- mergedDataset = mergeDatasets([targetDataset, clipDataset]);
45642
- buildTopology(mergedDataset); // identify any shared arcs between clipping layer and target dataset
45643
- } else {
45644
- // overlay layer belongs to the same dataset as target layers... move it to the end
45645
- mergedDataset = utils.extend({}, targetDataset);
45646
- mergedDataset.layers = targetDataset.layers.filter(function(lyr) {return lyr != clipLyr;});
45647
- mergedDataset.layers.push(clipLyr);
45724
+ });
45725
+ shp = pathfind(rings.concat(holes), 'dissolve');
45726
+ if (shp && shp.length > 0) {
45727
+ shp = fixNestingErrors(shp, mosaicIndex.nodes.arcs);
45648
45728
  }
45649
- return mergedDataset;
45729
+ return shp && shp.length > 0 ? getPolygonGeometry(shp, mosaicIndex.nodes.arcs) : null;
45650
45730
  }
45651
45731
 
45652
- function convertClipBounds(bb) {
45653
- var x0 = bb[0], y0 = bb[1], x1 = bb[2], y1 = bb[3],
45654
- arc = [[x0, y0], [x0, y1], [x1, y1], [x1, y0], [x0, y0]];
45655
-
45656
- if (!(y1 > y0 && x1 > x0)) {
45657
- stop$1("Invalid bbox (should be [xmin, ymin, xmax, ymax]):", bb);
45732
+ function dissolvePolygonBufferGeometry(geom, opts) {
45733
+ var tmp = importGeoJSON({
45734
+ type: 'GeometryCollection',
45735
+ geometries: [geom]
45736
+ }, {type: 'polygon'});
45737
+ var lyr = tmp.layers[0];
45738
+ if (tmp.arcs) {
45739
+ dissolveBufferDataset2(tmp, opts);
45658
45740
  }
45659
- return {
45660
- arcs: new ArcCollection([arc]),
45661
- layers: [{
45662
- name: 'bbox',
45663
- shapes: [[[0]]],
45664
- geometry_type: 'polygon'
45665
- }]
45666
- };
45741
+ if (lyr.shapes && lyr.shapes[0]) {
45742
+ lyr.shapes[0] = fixNestingErrors(lyr.shapes[0], tmp.arcs);
45743
+ }
45744
+ return lyr.shapes && lyr.shapes[0] ?
45745
+ getPolygonGeometry(lyr.shapes[0], tmp.arcs) : null;
45667
45746
  }
45668
45747
 
45669
- var OverlayUtils = /*#__PURE__*/Object.freeze({
45670
- __proto__: null,
45671
- mergeLayersForOverlay: mergeLayersForOverlay,
45672
- mergeLayersForOverlay2: mergeLayersForOverlay2,
45673
- normalizeOverlaySource: normalizeOverlaySource
45674
- });
45675
-
45676
- // Insert cutting points in arcs, where bbox intersects other shapes
45677
- // Return a polygon layer containing the bounding box vectors, divided at cutting points.
45678
- function divideDatasetByBBox(dataset, bbox) {
45679
- var arcs = dataset.arcs;
45680
- var data = findBBoxCutPoints(arcs, bbox);
45681
- var map = insertCutPoints(data.cutPoints, arcs);
45682
- arcs.dedupCoords();
45683
- remapDividedArcs(dataset, map);
45684
- // merge bbox dataset with target dataset,
45685
- // so arcs are shared between target layers and bbox layer
45686
- var clipDataset = bboxPointsToClipDataset(data.bboxPoints);
45687
- var mergedDataset = mergeDatasets([dataset, clipDataset]);
45688
- // TODO: detect if we need to rebuild topology (unlikely), like with the full clip command
45689
- // buildTopology(mergedDataset);
45690
- var clipLyr = mergedDataset.layers.pop();
45691
- dataset.arcs = mergedDataset.arcs;
45692
- dataset.layers = mergedDataset.layers;
45693
- return clipLyr;
45748
+ function makeNegativePolygonBufferGeometry(shape, distance, dataset, opts,
45749
+ bufferMaker) {
45750
+ // Non-topological negative buffers are never path-split, so each shape's ring
45751
+ // groups are offset and eroded directly.
45752
+ return makeClosedRingNegativeBufferGeometry(shape, dataset.arcs, distance,
45753
+ opts, bufferMaker);
45694
45754
  }
45695
45755
 
45696
- function bboxPointsToClipDataset(arr) {
45697
- var arcs = [];
45698
- var shape = [];
45699
- var layer = {geometry_type: 'polygon', shapes: [[shape]]};
45700
- var p1, p2;
45701
- for (var i=0, n=arr.length - 1; i<n; i++) {
45702
- p1 = arr[i];
45703
- p2 = arr[i+1];
45704
- arcs.push([[p1.x, p1.y], [p2.x, p2.y]]);
45705
- shape.push(i);
45706
- }
45707
- return {
45708
- arcs: new ArcCollection(arcs),
45709
- layers: [layer]
45710
- };
45756
+ function makeClosedRingNegativeBufferGeometry(shape, arcs, distance, opts,
45757
+ bufferMaker) {
45758
+ var coords = [];
45759
+ getPolygonRingGroupShapes(shape, arcs).forEach(function(groupShape) {
45760
+ var bufferCoords = getBufferMultiPolygonCoords(groupShape, distance, bufferMaker);
45761
+ var geom = bufferCoords.length > 0 ?
45762
+ makeClosedRingBufferGeometry(groupShape, arcs, getBufferDataset(bufferCoords),
45763
+ opts, distance, true) : null;
45764
+ if (geom) {
45765
+ coords = coords.concat(geom.coordinates);
45766
+ }
45767
+ });
45768
+ return coords.length > 0 ? {
45769
+ type: 'MultiPolygon',
45770
+ coordinates: coords
45771
+ } : null;
45711
45772
  }
45712
45773
 
45713
- function findBBoxCutPoints(arcs, bbox) {
45714
- var left = bbox[0],
45715
- bottom = bbox[1],
45716
- right = bbox[2],
45717
- top = bbox[3];
45718
-
45719
- // arrays of intersection points along each bbox edge
45720
- var tt = [],
45721
- rr = [],
45722
- bb = [],
45723
- ll = [];
45724
-
45725
- arcs.forEachSegment(function(i, j, xx, yy) {
45726
- var ax = xx[i],
45727
- ay = yy[i],
45728
- bx = xx[j],
45729
- by = yy[j];
45730
- var hit;
45731
- if (segmentOutsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
45732
- if (segmentInsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
45733
-
45734
- hit = geom.segmentIntersection(left, top, right, top, ax, ay, bx, by);
45735
- if (hit) addHit(tt, hit, i, j, xx, yy);
45736
-
45737
- hit = geom.segmentIntersection(left, bottom, right, bottom, ax, ay, bx, by);
45738
- if (hit) addHit(bb, hit, i, j, xx, yy);
45739
-
45740
- hit = geom.segmentIntersection(left, bottom, left, top, ax, ay, bx, by);
45741
- if (hit) addHit(ll, hit, i, j, xx, yy);
45742
-
45743
- hit = geom.segmentIntersection(right, bottom, right, top, ax, ay, bx, by);
45744
- if (hit) addHit(rr, hit, i, j, xx, yy);
45774
+ function makeClosedRingPositiveBufferGeometry(shape, arcs, distance, opts,
45775
+ bufferMaker) {
45776
+ var coords = [];
45777
+ var groupShapes = getPolygonRingGroupShapes(shape, arcs);
45778
+ groupShapes.forEach(function(groupShape) {
45779
+ var bufferCoords = getBufferMultiPolygonCoords(groupShape, distance, bufferMaker);
45780
+ var geom = bufferCoords.length > 0 ?
45781
+ makeClosedRingBufferGeometry(groupShape, arcs, getBufferDataset(bufferCoords),
45782
+ opts, distance, false) : null;
45783
+ if (geom) {
45784
+ coords = coords.concat(geom.coordinates);
45785
+ } else {
45786
+ coords = coords.concat(getPolygonMultiPolygonCoords(groupShape, arcs));
45787
+ }
45745
45788
  });
45746
-
45747
- return {
45748
- cutPoints: ll.concat(bb, rr, tt),
45749
- bboxPoints: getDividedBBoxPoints(bbox, ll, tt, rr, bb)
45789
+ if (coords.length === 0) return null;
45790
+ var geom = {
45791
+ type: 'MultiPolygon',
45792
+ coordinates: coords
45750
45793
  };
45751
-
45752
- function addHit(arr, hit, i, j, xx, yy) {
45753
- if (!hit) return;
45754
- arr.push(formatHit(hit[0], hit[1], i, j, xx, yy));
45755
- if (hit.length == 4) {
45756
- arr.push(formatHit(hit[2], hit[3], i, j, xx, yy));
45757
- }
45794
+ if (shouldDissolveBufferedRingGroups(groupShapes, arcs, distance)) {
45795
+ geom = dissolvePolygonBufferGeometry(geom, opts);
45758
45796
  }
45797
+ return removePositiveBufferArtifactHoles(geom, shape, arcs, distance);
45798
+ }
45759
45799
 
45760
- function formatHit(x, y, i, j, xx, yy) {
45761
- var ids = formatIntersectingSegment(x, y, i, j, xx, yy);
45762
- return getCutPoint(x, y, ids[0], ids[1]);
45800
+ // True if any two of a feature's buffered ring groups are close enough that
45801
+ // their buffers might merge (so the group buffers should be dissolved together
45802
+ // rather than emitted as separate polygons).
45803
+ function shouldDissolveBufferedRingGroups(groupShapes, arcs, distance) {
45804
+ var threshold = getCoordinateDistance(distance, arcs) * 2;
45805
+ var n = groupShapes.length;
45806
+ if (n < 2) return false;
45807
+ // Bounding-box prefilter + early exit. The old code computed the exact min
45808
+ // distance over every group pair (O(groups^2 * verts * segs)) -- the dominant
45809
+ // cost when a feature has many islands. Two groups can only be within
45810
+ // @threshold if their bounding boxes are, so the box test skips the costly
45811
+ // vertex-by-vertex distance for all far-apart pairs (the common case), and we
45812
+ // return as soon as one near pair is found. Result is identical to
45813
+ // (min inter-group distance <= threshold).
45814
+ var bounds = groupShapes.map(function(shp) {
45815
+ return arcs.getMultiShapeBounds(shp);
45816
+ });
45817
+ for (var i = 0; i < n - 1; i++) {
45818
+ for (var j = i + 1; j < n; j++) {
45819
+ if (boundsToBoundsDistance(bounds[i], bounds[j]) > threshold) continue;
45820
+ if (getShapeToShapeDistance(groupShapes[i], groupShapes[j], arcs, threshold) <= threshold ||
45821
+ getShapeToShapeDistance(groupShapes[j], groupShapes[i], arcs, threshold) <= threshold) {
45822
+ return true;
45823
+ }
45824
+ }
45763
45825
  }
45826
+ return false;
45764
45827
  }
45765
45828
 
45766
- function segmentOutsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
45767
- return ax < xmin && bx < xmin || ax > xmax && bx > xmax ||
45768
- ay < ymin && by < ymin || ay > ymax && by > ymax;
45829
+ // Minimum distance between two axis-aligned bounding boxes (0 if they overlap).
45830
+ function boundsToBoundsDistance(a, b) {
45831
+ var dx = Math.max(0, a.xmin - b.xmax, b.xmin - a.xmax);
45832
+ var dy = Math.max(0, a.ymin - b.ymax, b.ymin - a.ymax);
45833
+ return Math.sqrt(dx * dx + dy * dy);
45769
45834
  }
45770
45835
 
45771
- function segmentInsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
45772
- return ax > xmin && bx > xmin && ax < xmax && bx < xmax &&
45773
- ay > ymin && by > ymin && ay < ymax && by < ymax;
45836
+ // Min distance from the vertices of @shape1 to @shape2. Stops early once a
45837
+ // vertex within @maxDist (optional) is found, since callers only compare the
45838
+ // result against a threshold.
45839
+ function getShapeToShapeDistance(shape1, shape2, arcs, maxDist) {
45840
+ var data = exportPathData(shape1, arcs, 'polygon');
45841
+ var minDist = Infinity;
45842
+ var paths = data.pathData;
45843
+ for (var i = 0; i < paths.length; i++) {
45844
+ var points = paths[i].points;
45845
+ for (var j = 0; j < points.length; j++) {
45846
+ var d = getPointToShapeDistance(points[j][0], points[j][1], shape2, arcs);
45847
+ if (d < minDist) minDist = d;
45848
+ if (maxDist != null && minDist <= maxDist) return minDist;
45849
+ }
45850
+ }
45851
+ return minDist;
45774
45852
  }
45775
45853
 
45776
- // Returns an array of points representing the vertices in
45777
- // the bbox with cutting points inserted.
45778
- function getDividedBBoxPoints(bbox, ll, tt, rr, bb) {
45779
- var bl = {x: bbox[0], y: bbox[1]},
45780
- tl = {x: bbox[0], y: bbox[3]},
45781
- tr = {x: bbox[2], y: bbox[3]},
45782
- br = {x: bbox[2], y: bbox[1]};
45783
- ll = utils.sortOn(ll.concat([bl, tl]), 'y', true);
45784
- tt = utils.sortOn(tt.concat([tl, tr]), 'x', true);
45785
- rr = utils.sortOn(rr.concat([tr, br]), 'y', false);
45786
- bb = utils.sortOn(bb.concat([br, bl]), 'x', false);
45787
- return ll.concat(tt, rr, bb).reduce(function(memo, p2) {
45788
- var p1 = memo.length > 0 ? memo[memo.length-1] : null;
45789
- if (p1 === null || p1.x != p2.x || p1.y != p2.y) memo.push(p2);
45854
+ function makeClosedRingBufferGeometry(shape, arcs, bufferDataset, opts, distance,
45855
+ reverse) {
45856
+ var sourceAreas = exportPathData(shape, arcs, 'polygon').pathData.map(function(path) {
45857
+ return path.area;
45858
+ });
45859
+ var sourceBoundaryThreshold = getSourceBoundaryThreshold(distance, arcs);
45860
+ var bufferLyr = bufferDataset.layers[0];
45861
+ var bufferShape, bufferData, erodedShape;
45862
+ if (!bufferDataset.arcs) return null;
45863
+ // The default offset rings come from the winding-fill maker (one self-
45864
+ // overlapping ring per source ring) and must be unioned by winding number;
45865
+ // the sector-band fallback emits overlapping bands that a boundary flood
45866
+ // resolves instead (its maker leaves winding_fill off to match).
45867
+ dissolveBufferDataset2(bufferDataset,
45868
+ Object.assign({}, opts, {winding_fill: !opts.sector_band}));
45869
+ bufferShape = bufferLyr.shapes && bufferLyr.shapes[0];
45870
+ if (!bufferShape) return null;
45871
+ bufferData = exportPathData(bufferShape, bufferDataset.arcs, 'polygon');
45872
+ // Build the source-shape segment index once: ringIsOnSourceBoundary probes
45873
+ // ~20 points of every eroded buffer ring against the same source shape, and
45874
+ // on large rings the unindexed per-point distance scan dominated runtime.
45875
+ var sourceIndex = buildShapeSegmentIndex(shape, arcs);
45876
+ erodedShape = bufferData.pathData.reduce(function(memo, path) {
45877
+ if (!areaMatchesAny(path.area, sourceAreas) &&
45878
+ !ringIsOnSourceBoundary(path.points, sourceIndex, sourceBoundaryThreshold)) {
45879
+ memo.push(reverse ? reversePath(path.ids.concat()) : path.ids.concat());
45880
+ }
45790
45881
  return memo;
45791
45882
  }, []);
45883
+ return erodedShape.length > 0 ?
45884
+ getPolygonGeometry(erodedShape, bufferDataset.arcs) : null;
45792
45885
  }
45793
45886
 
45794
- var Bbox2Clipping = /*#__PURE__*/Object.freeze({
45795
- __proto__: null,
45796
- divideDatasetByBBox: divideDatasetByBBox,
45797
- segmentInsideBBox: segmentInsideBBox,
45798
- segmentOutsideBBox: segmentOutsideBBox
45799
- });
45800
-
45801
- cmd.clipLayers = function(target, src, dataset, opts) {
45802
- return clipLayers(target, src, dataset, "clip", opts);
45803
- };
45804
-
45805
- cmd.eraseLayers = function(target, src, dataset, opts) {
45806
- return clipLayers(target, src, dataset, "erase", opts);
45807
- };
45808
-
45809
- cmd.clipLayer = function(targetLyr, src, dataset, opts) {
45810
- return cmd.clipLayers([targetLyr], src, dataset, opts)[0];
45811
- };
45812
-
45813
- cmd.eraseLayer = function(targetLyr, src, dataset, opts) {
45814
- return cmd.eraseLayers([targetLyr], src, dataset, opts)[0];
45815
- };
45816
-
45817
- cmd.sliceLayers = function(target, src, dataset, opts) {
45818
- return clipLayers(target, src, dataset, "slice", opts);
45819
- };
45820
-
45821
- cmd.sliceLayer = function(targetLyr, src, dataset, opts) {
45822
- return cmd.sliceLayers([targetLyr], src, dataset, opts);
45823
- };
45824
-
45825
- function clipLayersInPlace(layers, clipSrc, dataset, type, opts) {
45826
- var outputLayers = clipLayers(layers, clipSrc, dataset, type, opts);
45827
- // remove arcs from the clipping dataset, if they are not used by any layer
45828
- layers.forEach(function(lyr, i) {
45829
- var lyr2 = outputLayers[i];
45830
- lyr.shapes = lyr2.shapes;
45831
- lyr.data = lyr2.data;
45832
- if (lyr2.raster) {
45833
- lyr.raster = lyr2.raster;
45834
- lyr.raster_type = lyr2.raster_type;
45835
- }
45887
+ function getPolygonRingGroupShapes(shape, arcs) {
45888
+ var data = exportPathData(shape, arcs, 'polygon');
45889
+ if (data.pointCount === 0) return [];
45890
+ return groupPolygonRings(data.pathData, arcs, false).map(function(paths) {
45891
+ return paths.map(function(path) {
45892
+ return path.ids.concat();
45893
+ });
45836
45894
  });
45837
- dissolveArcs(dataset);
45838
45895
  }
45839
45896
 
45840
- // @clipSrc: layer in @dataset or filename
45841
- // @type: 'clip' or 'erase'
45842
- function clipLayers(targetLayers, clipSrc, targetDataset, type, opts) {
45843
- profileStart('clipLayers');
45844
- opts = opts || {no_cleanup: true}; // TODO: update testing functions
45845
- var usingPathClip = utils.some(targetLayers, layerHasPaths);
45846
- var usingRasterClip = utils.some(targetLayers, layerHasRaster);
45847
- var mergedDataset, clipLyr, nodes, result;
45848
- var clipDataset;
45849
- if (usingRasterClip) {
45850
- result = clipRasterLayers(targetLayers, clipSrc, targetDataset, type, opts);
45851
- profileEnd('clipLayers');
45852
- return result;
45853
- }
45854
- clipDataset = normalizeOverlaySource(clipSrc, targetDataset, opts);
45855
- if (!opts.no_warn) {
45856
- warnIfBoundsDontOverlap(targetLayers, targetDataset, clipDataset, type);
45857
- }
45858
- if (opts.bbox2 && usingPathClip) { // assumes target dataset has arcs
45859
- result = clipLayersByBBox(targetLayers, targetDataset, opts);
45860
- profileEnd('clipLayers');
45861
- return result;
45862
- }
45863
- if (!usingPathClip) {
45864
- result = clipLayersByClipDataset(targetLayers, clipDataset, type, opts);
45865
- profileEnd('clipLayers');
45866
- return result;
45897
+ function removePositiveBufferArtifactHoles(geom, shape, arcs, distance) {
45898
+ if (!geom) return null;
45899
+ var threshold = getPositiveHoleArtifactThreshold(distance, arcs);
45900
+ var minHoleArea = getPositiveHoleArtifactAreaThreshold(distance, arcs);
45901
+ var sourceHoles = getSourceHoleShapes(shape, arcs);
45902
+ // Each candidate hole is classified by probing many points against the
45903
+ // source shape. Both per-probe tests used to rescan the whole source shape:
45904
+ // - "is the probe inside the source shape?" (testPointInPolygon)
45905
+ // - "is the probe near a source hole boundary?" (point-to-shape distance)
45906
+ // On large rings (e.g. a U.S. state buffer) this point-in-ring scan dominated
45907
+ // runtime. Build the spatial indexes once per feature instead:
45908
+ // - PathIndex.pointIsEnclosed() runs point-in-polygon via a per-ring
45909
+ // scanline index (O(log n) per probe instead of O(n)).
45910
+ // - shapeIndex / holeIndex are chunk-bounds indexes that prune far segments
45911
+ // for the point-to-shape distance queries.
45912
+ var ctx = {
45913
+ threshold: threshold,
45914
+ shapeIndex: buildShapeSegmentIndex(shape, arcs),
45915
+ pathIndex: shape && shape.length > 0 ? new PathIndex([shape], arcs) : null,
45916
+ holeIndex: sourceHoles.length > 0 ?
45917
+ buildShapeSegmentIndex(sourceHoles.map(function(h) {return h[0];}), arcs) : null
45918
+ };
45919
+ if (geom.type == 'Polygon') {
45920
+ geom.coordinates = filterArtifactHoles(geom.coordinates, minHoleArea, ctx);
45921
+ } else if (geom.type == 'MultiPolygon') {
45922
+ geom.coordinates = geom.coordinates.map(function(polygon) {
45923
+ return filterArtifactHoles(polygon, minHoleArea, ctx);
45924
+ }).filter(function(polygon) {
45925
+ return polygon.length > 0;
45926
+ });
45867
45927
  }
45868
- // Merging the clip source into the target and cutting intersections builds a
45869
- // throwaway combined arc collection. With a GUI undo transaction active,
45870
- // addIntersectionCuts() would otherwise capture a full coordinate copy of the
45871
- // merged target+clip arcs -- redundant work, since undo is driven by the
45872
- // dataset-level reference swap below (the dataset unit records the original
45873
- // arcs by reference before targetDataset.arcs is replaced).
45874
- //
45875
- // addIntersectionCuts() also remaps every shared layer's shapes in place
45876
- // (rewriting arc ids for the split arcs), including the target layers. Those
45877
- // original shapes ARE needed for undo, so capture each target layer's baseline
45878
- // explicitly before suspending tracking for the throwaway construction.
45879
- noteDatasetWillChange(targetDataset, {operation: type, unit: 'arcs'});
45880
- targetLayers.forEach(function(lyr) {
45881
- noteLayerWillChange(lyr, {operation: type});
45882
- });
45883
- // Build the clipped output with undo tracking suspended: the merged arcs, the
45884
- // dissolved clip layer, and the per-layer clip results are all derived from
45885
- // the throwaway combined dataset and never need restoring. Undo is driven by
45886
- // the dataset reference swap plus the target-layer baseline captured above;
45887
- // run-command captures the integration of the returned output layers.
45888
- withActiveUndoTransaction(null, function() {
45889
- profileStart('mergeLayersForOverlay');
45890
- mergedDataset = mergeLayersForOverlay2(targetLayers, targetDataset, clipDataset);
45891
- profileEnd('mergeLayersForOverlay');
45892
- clipLyr = mergedDataset.layers[mergedDataset.layers.length-1];
45893
- nodes = addIntersectionCuts(mergedDataset, opts);
45894
- targetDataset.arcs = mergedDataset.arcs;
45895
- profileStart('clipDissolvePolygonLayer2');
45896
- clipLyr = utils.defaults({data: null}, clipLyr);
45897
- clipLyr = dissolvePolygonLayer2(clipLyr, mergedDataset, {quiet: true, silent: true});
45898
- profileEnd('clipDissolvePolygonLayer2');
45899
- profileStart('clipLayersByLayer');
45900
- result = clipLayersByLayer(targetLayers, clipLyr, nodes, type, opts);
45901
- profileEnd('clipLayersByLayer');
45902
- });
45903
- markDatasetChanged(targetDataset, {operation: type, unit: 'arcs'});
45904
- profileEnd('clipLayers');
45905
- return result;
45928
+ return geom;
45906
45929
  }
45907
45930
 
45908
- function clipLayersByClipDataset(targetLayers, clipDataset, type, opts) {
45909
- var clipLyr = clipDataset.layers[0];
45910
- var nodes = new NodeCollection(clipDataset.arcs);
45911
- var result;
45912
- profileStart('clipLayersByLayer');
45913
- result = clipLayersByLayer(targetLayers, clipLyr, nodes, type, opts);
45914
- profileEnd('clipLayersByLayer');
45915
- return result;
45931
+ // Flatten a shape's path segments into fixed-size chunks (per path) with a
45932
+ // bounding box each. A chunk's box distance is a lower bound on the distance to
45933
+ // any of its segments, so a point-to-shape distance query can skip whole chunks
45934
+ // whose box is already farther than the closest segment found so far. Source
45935
+ // paths are spatially coherent, so each chunk's box is tight. Coords are stored
45936
+ // flat ([ax, ay, bx, by, ...]) to avoid per-segment array allocation.
45937
+ var SHAPE_SEGMENT_CHUNK_SIZE = 32;
45938
+
45939
+ function buildShapeSegmentIndex(shape, arcs) {
45940
+ var coords = [];
45941
+ var chunks = [];
45942
+ (shape || []).forEach(function(ids) {
45943
+ var iter = arcs.getShapeIter(ids);
45944
+ if (!iter.hasNext()) return;
45945
+ var ax = iter.x, ay = iter.y;
45946
+ var inChunk = 0;
45947
+ var xmin = 0, ymin = 0, xmax = 0, ymax = 0, start = 0;
45948
+ while (iter.hasNext()) {
45949
+ var bx = iter.x, by = iter.y;
45950
+ if (inChunk === 0) {
45951
+ start = coords.length / 4;
45952
+ xmin = Math.min(ax, bx); xmax = Math.max(ax, bx);
45953
+ ymin = Math.min(ay, by); ymax = Math.max(ay, by);
45954
+ } else {
45955
+ if (ax < xmin) xmin = ax; else if (ax > xmax) xmax = ax;
45956
+ if (bx < xmin) xmin = bx; else if (bx > xmax) xmax = bx;
45957
+ if (ay < ymin) ymin = ay; else if (ay > ymax) ymax = ay;
45958
+ if (by < ymin) ymin = by; else if (by > ymax) ymax = by;
45959
+ }
45960
+ coords.push(ax, ay, bx, by);
45961
+ inChunk++;
45962
+ if (inChunk === SHAPE_SEGMENT_CHUNK_SIZE) {
45963
+ chunks.push({start: start, end: coords.length / 4,
45964
+ xmin: xmin, ymin: ymin, xmax: xmax, ymax: ymax});
45965
+ inChunk = 0;
45966
+ }
45967
+ ax = bx; ay = by;
45968
+ }
45969
+ if (inChunk > 0) {
45970
+ chunks.push({start: start, end: coords.length / 4,
45971
+ xmin: xmin, ymin: ymin, xmax: xmax, ymax: ymax});
45972
+ }
45973
+ });
45974
+ return {coords: coords, chunks: chunks};
45916
45975
  }
45917
45976
 
45918
- function clipRasterLayers(targetLayers, clipSrc, targetDataset, type, opts) {
45919
- var clipDataset, clipBounds, bbox;
45920
- if (type != 'clip') {
45921
- stop$1('Raster layers only support clipping');
45977
+ // Same result as getPointToShapeDistance(px, py, shape, arcs), but using the
45978
+ // chunk bounding boxes to prune. Scan the nearest-box chunk first to seed a
45979
+ // tight bound, then skip any chunk whose box is farther than that bound. No
45980
+ // per-query allocation or sorting.
45981
+ function getPointToIndexedShapeDistance(px, py, index) {
45982
+ var chunks = index.chunks, coords = index.coords;
45983
+ var n = chunks.length;
45984
+ if (n === 0) return Infinity;
45985
+ var bestSq = Infinity;
45986
+ var nearIdx = -1, nearBoxSq = Infinity, boxSq, c, k;
45987
+ for (c = 0; c < n; c++) {
45988
+ boxSq = shapeChunkBoxDistSq(px, py, chunks[c]);
45989
+ if (boxSq < nearBoxSq) { nearBoxSq = boxSq; nearIdx = c; }
45922
45990
  }
45923
- if (utils.some(targetLayers, function(lyr) {return !layerHasRaster(lyr);})) {
45924
- stop$1('Raster clipping cannot be mixed with vector target layers');
45991
+ bestSq = scanShapeChunk(px, py, coords, chunks[nearIdx], bestSq);
45992
+ for (k = 0; k < n; k++) {
45993
+ if (k === nearIdx) continue;
45994
+ if (shapeChunkBoxDistSq(px, py, chunks[k]) >= bestSq) continue;
45995
+ bestSq = scanShapeChunk(px, py, coords, chunks[k], bestSq);
45925
45996
  }
45926
- if (opts.bbox2) {
45927
- bbox = opts.bbox2;
45928
- } else {
45929
- clipDataset = normalizeOverlaySource(clipSrc, targetDataset, opts);
45930
- clipBounds = getLayerBounds(clipDataset.layers[0], clipDataset.arcs);
45931
- if (!clipBounds || !clipBounds.hasBounds()) {
45932
- stop$1('Missing raster clipping bounds');
45933
- }
45934
- bbox = clipBounds.toArray();
45997
+ return Math.sqrt(bestSq);
45998
+ }
45999
+
46000
+ function scanShapeChunk(px, py, coords, chunk, bestSq) {
46001
+ for (var i = chunk.start; i < chunk.end; i++) {
46002
+ var o = i * 4;
46003
+ var d = pointSegDistSq2(px, py, coords[o], coords[o + 1], coords[o + 2], coords[o + 3]);
46004
+ if (d < bestSq) bestSq = d;
45935
46005
  }
45936
- return targetLayers.map(function(lyr) {
45937
- clipRasterToBBox(lyr, bbox, opts);
45938
- return lyr;
45939
- });
46006
+ return bestSq;
45940
46007
  }
45941
46008
 
45942
- function clipLayersByBBox(layers, dataset, opts) {
45943
- var bbox = opts.bbox2;
45944
- var clipLyr = divideDatasetByBBox(dataset, bbox);
45945
- var nodes = new NodeCollection(dataset.arcs);
45946
- var retn = clipLayersByLayer(layers, clipLyr, nodes, 'clip', opts);
45947
- return retn;
46009
+ function shapeChunkBoxDistSq(px, py, chunk) {
46010
+ var dx = px < chunk.xmin ? chunk.xmin - px : (px > chunk.xmax ? px - chunk.xmax : 0);
46011
+ var dy = py < chunk.ymin ? chunk.ymin - py : (py > chunk.ymax ? py - chunk.ymax : 0);
46012
+ return dx * dx + dy * dy;
45948
46013
  }
45949
46014
 
45950
- function clipLayersByLayer(targetLayers, clipLyr, nodes, type, opts) {
45951
- requirePolygonLayer(clipLyr, "Requires a polygon clipping layer");
45952
- return targetLayers.reduce(function(memo, targetLyr) {
45953
- if (type == 'slice') {
45954
- memo = memo.concat(sliceLayerByLayer(targetLyr, clipLyr, nodes, opts));
45955
- } else {
45956
- memo.push(clipLayerByLayer(targetLyr, clipLyr, nodes, type, opts));
46015
+ function getSourceHoleShapes(shape, arcs) {
46016
+ return exportPathData(shape, arcs, 'polygon').pathData.reduce(function(memo, path) {
46017
+ if (path.area < 0) {
46018
+ memo.push([path.ids]);
45957
46019
  }
45958
46020
  return memo;
45959
46021
  }, []);
45960
46022
  }
45961
46023
 
45962
- function getSliceLayerName(clipLyr, field, i) {
45963
- var id = field ? clipLyr.data.getRecords()[0][field] : i + 1;
45964
- return 'slice-' + id;
46024
+ function filterArtifactHoles(polygon, minHoleArea, ctx) {
46025
+ if (polygon.length < 2) return polygon;
46026
+ return [polygon[0]].concat(polygon.slice(1).filter(function(ring) {
46027
+ return Math.abs(getGeoJSONRingArea(ring)) > minHoleArea &&
46028
+ !positiveBufferHoleIsArtifact(ring, ctx);
46029
+ }));
45965
46030
  }
45966
46031
 
45967
- function sliceLayerByLayer(targetLyr, clipLyr, nodes, opts) {
45968
- // may not need no_replace
45969
- var clipLayers = cmd.splitLayer(clipLyr, opts.id_field, {no_replace: true});
45970
- return clipLayers.map(function(clipLyr, i) {
45971
- var outputLyr = clipLayerByLayer(targetLyr, clipLyr, nodes, 'clip', opts);
45972
- outputLyr.name = getSliceLayerName(clipLyr, opts.id_field, i);
45973
- return outputLyr;
45974
- });
46032
+ function positiveBufferHoleIsArtifact(ring, ctx) {
46033
+ var n = ring.length - 1; // skip duplicate endpoint
46034
+ var step = Math.max(1, Math.floor(n / 20));
46035
+ var p, p2;
46036
+ for (var i = 0; i < n; i += step) {
46037
+ p = ring[i];
46038
+ if (pointIsDeepInsidePositiveBuffer(p, ctx)) return true;
46039
+ p2 = ring[(i + 1) % n];
46040
+ if (pointIsDeepInsidePositiveBuffer([(p[0] + p2[0]) / 2, (p[1] + p2[1]) / 2], ctx)) return true;
46041
+ }
46042
+ return false;
45975
46043
  }
45976
46044
 
45977
- function clipLayerByLayer(targetLyr, clipLyr, nodes, type, opts) {
45978
- var arcs = nodes.arcs;
45979
- var shapeCount = targetLyr.shapes ? targetLyr.shapes.length : 0;
45980
- var nullCount = 0, sliverCount = 0;
45981
- var clippedShapes, outputLyr;
45982
- if (shapeCount === 0) {
45983
- return targetLyr; // ignore empty layer
45984
- }
45985
- if (targetLyr === clipLyr) {
45986
- stop$1('Can\'t clip a layer with itself');
46045
+ function pointIsDeepInsidePositiveBuffer(p, ctx) {
46046
+ if (ctx.holeIndex &&
46047
+ getPointToIndexedShapeDistance(p[0], p[1], ctx.holeIndex) < ctx.threshold) {
46048
+ return false;
45987
46049
  }
46050
+ if (ctx.pathIndex && ctx.pathIndex.pointIsEnclosed(p)) return true;
46051
+ // A positive buffer can shrink legitimate source holes, leaving their
46052
+ // boundaries near the original rings. Don't classify those as artifacts.
46053
+ return getPointToIndexedShapeDistance(p[0], p[1], ctx.shapeIndex) < ctx.threshold;
46054
+ }
45988
46055
 
45989
- // TODO: optimize some of these functions for bbox clipping
45990
- if (targetLyr.geometry_type == 'point') {
45991
- clippedShapes = clipPoints(targetLyr.shapes, clipLyr.shapes, arcs, type);
45992
- } else if (targetLyr.geometry_type == 'polygon') {
45993
- clippedShapes = clipPolygons(targetLyr.shapes, clipLyr.shapes, nodes, type, opts);
45994
- } else if (targetLyr.geometry_type == 'polyline') {
45995
- clippedShapes = clipPolylines(targetLyr.shapes, clipLyr.shapes, nodes, type);
45996
- } else {
45997
- stop$1('Invalid target layer:', targetLyr.name);
45998
- }
46056
+ function getPositiveHoleArtifactThreshold(distance, arcs) {
46057
+ return getCoordinateDistance(distance, arcs) * 0.5;
46058
+ }
45999
46059
 
46000
- outputLyr = {
46001
- name: targetLyr.name,
46002
- geometry_type: targetLyr.geometry_type,
46003
- shapes: clippedShapes,
46004
- data: targetLyr.data // replaced post-filter
46005
- };
46060
+ function getPositiveHoleArtifactAreaThreshold(distance, arcs) {
46061
+ var d = getCoordinateDistance(distance, arcs);
46062
+ return d * d;
46063
+ }
46006
46064
 
46007
- // Remove sliver polygons
46008
- if (opts.remove_slivers && outputLyr.geometry_type == 'polygon') {
46009
- sliverCount = filterClipSlivers(outputLyr, clipLyr, arcs);
46065
+ function getGeoJSONRingArea(ring) {
46066
+ var sum = 0;
46067
+ for (var i = 0, n = ring.length - 1; i < n; i++) {
46068
+ sum += ring[i][0] * ring[i + 1][1] - ring[i + 1][0] * ring[i][1];
46010
46069
  }
46070
+ return sum / 2;
46071
+ }
46011
46072
 
46012
- // Remove null shapes (likely removed by clipping/erasing, although possibly already present)
46013
- cmd.filterFeatures(outputLyr, arcs, {remove_empty: true, verbose: false});
46073
+ function areaMatchesAny(area, arr) {
46074
+ return arr.some(function(area2) {
46075
+ var tol = Math.max(1e-8, Math.abs(area2) * 1e-9);
46076
+ return Math.abs(Math.abs(area) - Math.abs(area2)) <= tol;
46077
+ });
46078
+ }
46014
46079
 
46015
- // clone data records (to avoid sharing records between layers)
46016
- // TODO: this is not needed when replacing target with a single layer
46017
- if (outputLyr.data) {
46018
- outputLyr.data = outputLyr.data.clone();
46080
+ function getSourceBoundaryThreshold(distance, arcs) {
46081
+ return getCoordinateDistance(distance, arcs) * 0.25;
46082
+ }
46083
+
46084
+ function getCoordinateDistance(distance, arcs) {
46085
+ return arcs.isPlanar() ? distance : distance / R$3 * R2D$1;
46086
+ }
46087
+
46088
+ // @shapeIndex: chunk-bounds index of the source shape (see buildShapeSegmentIndex)
46089
+ function ringIsOnSourceBoundary(points, shapeIndex, threshold) {
46090
+ var n = points.length - 1; // skip duplicate endpoint
46091
+ var step = Math.max(1, Math.floor(n / 20));
46092
+ var sum = 0;
46093
+ var count = 0;
46094
+ for (var i = 0; i < n; i += step) {
46095
+ sum += getPointToIndexedShapeDistance(points[i][0], points[i][1], shapeIndex);
46096
+ count++;
46019
46097
  }
46098
+ return count > 0 && sum / count < threshold;
46099
+ }
46020
46100
 
46021
- // TODO: redo messages, now that many layers may be clipped
46022
- nullCount = shapeCount - outputLyr.shapes.length;
46023
- if (nullCount && sliverCount) {
46024
- message(getClipMessage(nullCount, sliverCount));
46101
+ function getPolygonMultiPolygonCoords(shape, arcs) {
46102
+ var data = exportPathData(shape, arcs, 'polygon');
46103
+ if (data.pointCount === 0) return [];
46104
+ return groupPolygonRings(data.pathData, arcs, false).map(function(paths) {
46105
+ return paths.map(function(path) {
46106
+ return path.points.map(function(p) {
46107
+ return p.concat();
46108
+ });
46109
+ });
46110
+ });
46111
+ }
46112
+
46113
+ function getPolygonGeometry(shape, arcs) {
46114
+ var coords = getPolygonMultiPolygonCoords(shape, arcs);
46115
+ return coords.length > 0 ? {
46116
+ type: 'MultiPolygon',
46117
+ coordinates: coords
46118
+ } : null;
46119
+ }
46120
+
46121
+ function getBufferDataset(coords) {
46122
+ return importGeoJSON({
46123
+ type: 'GeometryCollection',
46124
+ geometries: [{
46125
+ type: 'MultiPolygon',
46126
+ coordinates: coords
46127
+ }]
46128
+ }, {type: 'polygon'});
46129
+ }
46130
+
46131
+ // Union a set of winding-fill offset rings (which self-overlap) into clean,
46132
+ // non-self-overlapping MultiPolygon coordinates, via the winding-number
46133
+ // dissolve. Used by the topological pipeline to feed an ordinary polygon into
46134
+ // the shared mosaic (whose boundary-flood membership cannot resolve the
46135
+ // self-overlapping construction ring directly).
46136
+ function dissolveOffsetRingsToCoords(coords, opts) {
46137
+ if (!coords || coords.length === 0) return [];
46138
+ var dataset = getBufferDataset(coords);
46139
+ if (!dataset.arcs) return [];
46140
+ dissolveBufferDataset2(dataset, Object.assign({}, opts, {winding_fill: true}));
46141
+ var lyr = dataset.layers[0];
46142
+ var shape = lyr.shapes && lyr.shapes[0];
46143
+ return shape ? getPolygonMultiPolygonCoords(shape, dataset.arcs) : [];
46144
+ }
46145
+
46146
+ function getBufferMultiPolygonCoords(paths, distance, bufferMaker) {
46147
+ var features, coords = [];
46148
+ if (paths.length === 0) return coords;
46149
+ features = bufferMaker(paths, distance) || [];
46150
+ features.forEach(function(feat) {
46151
+ var geom = feat && feat.geometry;
46152
+ if (geom && geom.type == 'MultiPolygon') {
46153
+ coords = coords.concat(geom.coordinates);
46154
+ }
46155
+ });
46156
+ return coords;
46157
+ }
46158
+
46159
+ function getPolygonBufferPathData(shape, uniqueArcTest) {
46160
+ var data = {paths: [], split: false};
46161
+ (shape || []).forEach(function(path) {
46162
+ var paths = uniqueArcTest ? splitPathAtSharedArcs(path, uniqueArcTest) :
46163
+ [path.concat()];
46164
+ if (paths.length != 1 || paths[0].length != path.length ||
46165
+ !paths[0].every(function(arcId, i) {
46166
+ return arcId == path[i];
46167
+ })) {
46168
+ data.split = true;
46169
+ }
46170
+ data.paths = data.paths.concat(paths);
46171
+ });
46172
+ return data;
46173
+ }
46174
+
46175
+ function splitPathAtSharedArcs(path, uniqueArcTest) {
46176
+ var flags = path.map(uniqueArcTest);
46177
+ var firstShared = flags.indexOf(false);
46178
+ var chains = [];
46179
+ var chain = [];
46180
+ var start, i, arcId;
46181
+ if (firstShared == -1) return [path.concat()];
46182
+ start = (firstShared + 1) % path.length;
46183
+ for (i = 0; i < path.length; i++) {
46184
+ arcId = path[(start + i) % path.length];
46185
+ if (uniqueArcTest(arcId)) {
46186
+ chain.push(arcId);
46187
+ } else if (chain.length > 0) {
46188
+ chains.push(chain);
46189
+ chain = [];
46190
+ }
46025
46191
  }
46026
- return outputLyr;
46192
+ if (chain.length > 0) {
46193
+ chains.push(chain);
46194
+ }
46195
+ return chains;
46027
46196
  }
46028
46197
 
46029
- function getClipMessage(nullCount, sliverCount) {
46030
- var nullMsg = nullCount ? utils.format('%,d null feature%s', nullCount, utils.pluralSuffix(nullCount)) : '';
46031
- var sliverMsg = sliverCount ? utils.format('%,d sliver%s', sliverCount, utils.pluralSuffix(sliverCount)) : '';
46032
- if (nullMsg || sliverMsg) {
46033
- return utils.format('Removed %s%s%s', nullMsg, (nullMsg && sliverMsg ? ' and ' : ''), sliverMsg);
46198
+ function getUniqueArcTest(lyr, arcs) {
46199
+ var classify = getArcClassifier(lyr, arcs, {reusable: true})(function(a, b) {
46200
+ return b == -1 ? 'unique' : null;
46201
+ });
46202
+ return function(arcId) {
46203
+ return !!classify(arcId);
46204
+ };
46205
+ }
46206
+
46207
+ function ringArea(ring) {
46208
+ var iter = new PointIter(ring);
46209
+ return getSphericalPathArea2(iter);
46210
+ }
46211
+
46212
+ function makePointBuffer(lyr, dataset, opts) {
46213
+ var geojson = makePointBufferGeoJSON(lyr, dataset, opts);
46214
+ return importGeoJSON(geojson, {});
46215
+ }
46216
+
46217
+ // Make a single geodetic circle
46218
+ function getCircleGeoJSON(center, radius, vertices, opts) {
46219
+ var n = 360;
46220
+ var geod = getGeodeticSegmentFunction(parseCrsString$1('wgs84')); // ?
46221
+ if (opts.inset) {
46222
+ radius -= opts.inset;
46034
46223
  }
46035
- return '';
46224
+ return opts.geometry_type == 'polyline' ?
46225
+ getPointBufferLineString([center], radius, n, geod) :
46226
+ getPointBufferPolygon([center], radius, n, geod, true);
46036
46227
  }
46037
46228
 
46038
- // Warn (once per source/target pair) when a -clip / -erase / slice almost
46039
- // certainly won't do what the user wants. Two checks, in order of strength:
46040
- //
46041
- // 1. CRS mismatch -- one side is lat/lng and the other is projected. This
46042
- // uses the same logic as requireDatasetsHaveCompatibleCRS() in
46043
- // mapshaper-merging.mjs, but fires here as a friendlier, command-aware
46044
- // warning before mergeDatasets() would otherwise stop() with a generic
46045
- // message. It also catches a class of cases the bbox check misses: a
46046
- // projected layer whose bbox straddles (0,0) often *does* overlap an
46047
- // unprojected lat/lng bbox, even though the two are useless together.
46048
- // 2. Bbox-disjoint -- a fallback for the case where CRSes look compatible
46049
- // (or are unknown on both sides) but the layers clearly aren't in the
46050
- // same place. Usually a wrong-source-picker error.
46051
- //
46052
- // Empty target layers are ignored (separate failure mode; would just be
46053
- // noise). The CRS warning suppresses the bbox warning for the same target,
46054
- // so users see one warning per problem, not two.
46055
- // Skipped entirely if opts.no_warn is set.
46056
- function warnIfBoundsDontOverlap(targetLayers, targetDataset, clipDataset, type) {
46057
- var srcCRS = getDatasetCRS(clipDataset);
46058
- var targetCRS = getDatasetCRS(targetDataset);
46059
- var crsMismatch = srcCRS && targetCRS && isLatLngCRS(srcCRS) != isLatLngCRS(targetCRS);
46060
- var srcName = clipDataset.layers[0].name || '<unnamed>';
46061
- var srcBounds = getLayerBounds(clipDataset.layers[0], clipDataset.arcs);
46062
- targetLayers.forEach(function(targetLyr) {
46063
- var targetBounds = getLayerBounds(targetLyr, targetDataset.arcs);
46064
- if (!targetBounds || !targetBounds.hasBounds()) return;
46065
- if (crsMismatch) {
46066
- warnOnce(formatCRSMismatchMessage(type, srcName, targetLyr.name,
46067
- srcCRS, targetCRS));
46068
- return; // Don't also fire the (likely-misleading) bbox warning.
46069
- }
46070
- if (!srcBounds || !srcBounds.hasBounds()) return;
46071
- if (srcBounds.intersects(targetBounds)) return;
46072
- warnOnce(formatNoOverlapMessage(type, srcName, targetLyr.name,
46073
- srcBounds, targetBounds));
46229
+ // Convert a point layer to circles
46230
+ function makePointBufferGeoJSON(lyr, dataset, opts) {
46231
+ var vertices = opts.vertices || 72;
46232
+ var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
46233
+ var crs = getDatasetCRS(dataset);
46234
+ var spherical = isLatLngCRS(crs);
46235
+ var geod = getGeodeticSegmentFunction(crs);
46236
+ var geometries = lyr.shapes.map(function(shape, i) {
46237
+ var dist = distanceFn(i);
46238
+ if (!dist || !shape) return null;
46239
+ return getPointBufferPolygon(shape, dist, vertices, geod, spherical);
46074
46240
  });
46241
+ // TODO: make sure that importer supports null geometries (nonstandard GeoJSON);
46242
+ return {
46243
+ type: 'GeometryCollection',
46244
+ geometries: geometries
46245
+ };
46075
46246
  }
46076
46247
 
46077
- function formatCRSMismatchMessage(type, srcName, targetName, srcCRS, targetCRS) {
46078
- var verb = type === 'erase' ? 'erase' : 'clip';
46079
- var srcKind = isLatLngCRS(srcCRS) ? 'lng/lat (geographic)' : 'projected';
46080
- var targetKind = isLatLngCRS(targetCRS) ? 'lng/lat (geographic)' : 'projected';
46081
- return '-' + verb + ': source "' + srcName + '" uses ' + srcKind +
46082
- ' coordinates but target "' + (targetName || '<unnamed>') + '" uses ' +
46083
- targetKind + ' coordinates. The -' + verb +
46084
- ' will not produce a useful result; project one side to match the other first.';
46248
+ function getPointBufferPolygon(points, distance, vertices, geod, spherical) {
46249
+ var rings = [], coords, coords2;
46250
+ if (!points || !points.length) return null;
46251
+ for (var i=0; i<points.length; i++) {
46252
+ coords = getPointBufferCoordinates(points[i], distance, vertices, geod);
46253
+ if (!spherical) {
46254
+ rings.push([coords]);
46255
+ } else if (countCrosses(coords) > 0) {
46256
+ coords2 = removePolygonCrosses([coords]);
46257
+ while (coords2.length > 0) rings.push([coords2.pop()]); // geojson polygon coords, no hole
46258
+ } else if (ringArea(coords) < 0) {
46259
+ // negative spherical area: CCW ring, indicating a circle of >180 degrees
46260
+ // that fully encloses both poles and the antimeridian.
46261
+ // need to add an enclosure around the entire sphere
46262
+ // TODO: compare to distance param as a sanity check
46263
+ rings.push([
46264
+ [[180, 90], [180, -90], [0, -90], [-180, -90], [-180, 90], [0, 90], [180, 90]],
46265
+ coords
46266
+ ]);
46267
+ } else {
46268
+ rings.push([coords]);
46269
+ }
46270
+ }
46271
+ return {
46272
+ type: 'MultiPolygon',
46273
+ coordinates: rings
46274
+ };
46085
46275
  }
46086
46276
 
46087
- function formatNoOverlapMessage(type, srcName, targetName, srcBounds, targetBounds) {
46088
- // 'slice' is a per-feature -clip variant; in user terms it has the same
46089
- // empty-output failure mode as clip, so we describe it under the same
46090
- // verb to keep the message simple.
46091
- var verb = type === 'erase' ? 'erase' : 'clip';
46092
- var consequence = type === 'erase'
46093
- ? 'will leave "' + (targetName || '<unnamed>') + '" unchanged'
46094
- : 'will produce empty output for "' + (targetName || '<unnamed>') + '"';
46095
- return '-' + verb + ': source "' + srcName + '" ' + bbToText(srcBounds) +
46096
- ' does not overlap target "' + (targetName || '<unnamed>') + '" ' +
46097
- bbToText(targetBounds) + '. The -' + verb + ' ' + consequence +
46098
- '. This usually indicates a coordinate system mismatch.';
46277
+ function getPointBufferLineString(points, distance, vertices, geod) {
46278
+ var rings = [], coords;
46279
+ if (!points || !points.length) return null;
46280
+ for (var i=0; i<points.length; i++) {
46281
+ coords = getPointBufferCoordinates(points[i], distance, vertices, geod);
46282
+ coords = removePolylineCrosses(coords);
46283
+ while (coords.length > 0) rings.push(coords.pop());
46284
+ }
46285
+ return rings.length == 1 ? {
46286
+ type: 'LineString',
46287
+ coordinates: rings[0]
46288
+ } : {
46289
+ type: 'MultiLineString',
46290
+ coordinates: rings
46291
+ };
46099
46292
  }
46100
46293
 
46101
- function bbToText(b) {
46102
- return JSON.stringify(b.toArray());
46294
+ // Returns array of [x, y] coordinates in a closed ring
46295
+ function getPointBufferCoordinates(center, meterDist, vertices, geod) {
46296
+ var coords = [],
46297
+ angle = 360 / vertices,
46298
+ theta;
46299
+ for (var i=0; i<vertices; i++) {
46300
+ // offsetting by half a step so 4 sides are flat, not pointy
46301
+ // (looks better on low-vertex circles)
46302
+ theta = (i + 0.5) * angle % 360;
46303
+ coords.push(geod(center[0], center[1], theta, meterDist));
46304
+ }
46305
+ coords.push(coords[0].concat());
46306
+ return coords;
46103
46307
  }
46104
46308
 
46105
- var ClipErase = /*#__PURE__*/Object.freeze({
46106
- __proto__: null,
46107
- clipLayers: clipLayers,
46108
- clipLayersByBBox: clipLayersByBBox,
46109
- clipLayersByLayer: clipLayersByLayer,
46110
- clipLayersInPlace: clipLayersInPlace,
46111
- getClipMessage: getClipMessage,
46112
- warnIfBoundsDontOverlap: warnIfBoundsDontOverlap
46113
- });
46309
+ // Returns number of arcs that were removed
46310
+ function editArcs(arcs, onPoint) {
46311
+ var nn2 = [],
46312
+ xx2 = [],
46313
+ yy2 = [],
46314
+ errors = 0,
46315
+ n;
46316
+
46317
+ arcs.forEach(function(arc, i) {
46318
+ editArc(arc, onPoint);
46319
+ });
46320
+ arcs.updateVertexData(nn2, xx2, yy2);
46321
+ return errors;
46322
+
46323
+ function append(p) {
46324
+ if (p) {
46325
+ xx2.push(p[0]);
46326
+ yy2.push(p[1]);
46327
+ n++;
46328
+ }
46329
+ }
46330
+
46331
+ function editArc(arc, cb) {
46332
+ var x, y, xp, yp, retn;
46333
+ var valid = true;
46334
+ var i = 0;
46335
+ n = 0;
46336
+ while (arc.hasNext()) {
46337
+ x = arc.x;
46338
+ y = arc.y;
46339
+ retn = cb(append, x, y, xp, yp, i++);
46340
+ if (retn === false) {
46341
+ valid = false;
46342
+ // assumes that it's ok for the arc iterator to be interrupted.
46343
+ break;
46344
+ }
46345
+ xp = x;
46346
+ yp = y;
46347
+ }
46348
+ if (valid && n == 1) {
46349
+ // only one valid point was added to this arc (invalid)
46350
+ // e.g. this could happen during reprojection.
46351
+ // making this arc empty
46352
+ // error("An invalid arc was created");
46353
+ message("An invalid arc was created");
46354
+ valid = false;
46355
+ }
46356
+ if (valid) {
46357
+ nn2.push(n);
46358
+ } else {
46359
+ // remove any points that were added for an invalid arc
46360
+ while (n-- > 0) {
46361
+ xx2.pop();
46362
+ yy2.pop();
46363
+ }
46364
+ nn2.push(0); // add empty arc (to preserve mapping from paths to arcs)
46365
+ errors++;
46366
+ }
46367
+ }
46368
+ }
46114
46369
 
46115
46370
  // Planar densification by an interval
46116
46371
  function densifyPathByInterval(coords, interval, interpolate) {
@@ -61818,7 +62073,7 @@ ${svg}
61818
62073
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
61819
62074
  }
61820
62075
 
61821
- var version = "0.7.26";
62076
+ var version = "0.7.27";
61822
62077
 
61823
62078
  // Parse command line args into commands and run them
61824
62079
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.