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.
- package/mapshaper.js +1941 -1686
- package/package.json +1 -1
- 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
|
-
|
|
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
|
|
41395
|
-
|
|
41396
|
-
|
|
41397
|
-
|
|
41398
|
-
|
|
41399
|
-
|
|
41400
|
-
|
|
41401
|
-
|
|
41402
|
-
|
|
41403
|
-
|
|
41404
|
-
|
|
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 &&
|
|
41410
|
-
|
|
41411
|
-
|
|
41412
|
-
|
|
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 (
|
|
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 (
|
|
41418
|
-
|
|
41419
|
-
return
|
|
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
|
|
41423
|
-
// maxTurn (a covered overshoot, safe to collapse).
|
|
41424
|
-
//
|
|
41425
|
-
|
|
41426
|
-
|
|
41427
|
-
|
|
41428
|
-
|
|
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 <
|
|
41431
|
-
if (p >
|
|
41487
|
+
if (p < posLo) posLo = p;
|
|
41488
|
+
if (p > posHi) posHi = p;
|
|
41432
41489
|
}
|
|
41433
|
-
return (turnPrefix[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
42250
|
-
// doubly-covered (safe to collapse), but
|
|
42251
|
-
// topological polygon's unbuffered-boundary
|
|
42252
|
-
// can have a concave-join dent that is its
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
43293
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
44227
|
-
//
|
|
44228
|
-
// TODO:
|
|
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
|
-
|
|
44231
|
-
|
|
44232
|
-
|
|
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
|
-
|
|
44245
|
-
utils.
|
|
44246
|
-
|
|
44247
|
-
|
|
44248
|
-
|
|
44249
|
-
|
|
44250
|
-
|
|
44251
|
-
|
|
44252
|
-
|
|
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
|
-
|
|
44287
|
-
|
|
44288
|
-
|
|
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
|
-
|
|
44292
|
-
|
|
44293
|
-
|
|
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
|
|
44438
|
+
return removed;
|
|
44314
44439
|
}
|
|
44315
44440
|
|
|
44316
|
-
function
|
|
44317
|
-
var
|
|
44318
|
-
|
|
44319
|
-
var
|
|
44320
|
-
var
|
|
44321
|
-
var
|
|
44322
|
-
|
|
44323
|
-
|
|
44324
|
-
|
|
44325
|
-
|
|
44326
|
-
|
|
44327
|
-
|
|
44328
|
-
|
|
44329
|
-
|
|
44330
|
-
|
|
44331
|
-
|
|
44332
|
-
|
|
44333
|
-
|
|
44334
|
-
|
|
44335
|
-
|
|
44336
|
-
|
|
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
|
-
|
|
44379
|
-
|
|
44380
|
-
|
|
44381
|
-
|
|
44382
|
-
|
|
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
|
-
|
|
44388
|
-
|
|
44389
|
-
|
|
44390
|
-
var
|
|
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
|
-
|
|
44398
|
-
|
|
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
|
-
|
|
44410
|
-
var
|
|
44411
|
-
|
|
44412
|
-
|
|
44413
|
-
|
|
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
|
-
|
|
44434
|
-
|
|
44435
|
-
|
|
44436
|
-
|
|
44437
|
-
|
|
44438
|
-
|
|
44439
|
-
|
|
44440
|
-
|
|
44441
|
-
|
|
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
|
-
|
|
44453
|
-
|
|
44454
|
-
|
|
44455
|
-
|
|
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
|
-
|
|
44473
|
-
|
|
44474
|
-
|
|
44475
|
-
var
|
|
44476
|
-
|
|
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
|
|
44517
|
+
return cw;
|
|
44484
44518
|
}
|
|
44485
44519
|
|
|
44486
|
-
function
|
|
44487
|
-
|
|
44488
|
-
|
|
44489
|
-
|
|
44490
|
-
|
|
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
|
-
|
|
44495
|
-
|
|
44496
|
-
|
|
44497
|
-
|
|
44498
|
-
}
|
|
44526
|
+
return function(shp) {
|
|
44527
|
+
if (!shp) return null;
|
|
44528
|
+
var cw = [],
|
|
44529
|
+
ccw = [];
|
|
44499
44530
|
|
|
44500
|
-
|
|
44501
|
-
|
|
44502
|
-
|
|
44503
|
-
|
|
44504
|
-
|
|
44505
|
-
var
|
|
44506
|
-
|
|
44507
|
-
|
|
44508
|
-
|
|
44509
|
-
|
|
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
|
-
|
|
44518
|
-
|
|
44519
|
-
return Math.abs(getShapeArea(shape, arcs));
|
|
44520
|
-
});
|
|
44543
|
+
return dissolved.length > 0 ? dissolved : null;
|
|
44544
|
+
};
|
|
44521
44545
|
}
|
|
44522
44546
|
|
|
44523
|
-
|
|
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
|
-
|
|
44532
|
-
|
|
44533
|
-
|
|
44534
|
-
|
|
44535
|
-
}
|
|
44536
|
-
|
|
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
|
-
|
|
44540
|
-
|
|
44541
|
-
|
|
44542
|
-
|
|
44543
|
-
|
|
44544
|
-
|
|
44545
|
-
|
|
44546
|
-
|
|
44547
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44574
|
-
|
|
44575
|
-
|
|
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
|
-
|
|
44582
|
-
|
|
44583
|
-
|
|
44584
|
-
|
|
44585
|
-
|
|
44586
|
-
|
|
44587
|
-
|
|
44588
|
-
|
|
44589
|
-
|
|
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
|
-
|
|
44594
|
-
type: 'MultiPolygon',
|
|
44595
|
-
coordinates: coords
|
|
44596
|
-
} : null;
|
|
44597
|
-
}
|
|
44600
|
+
profileEnd('cp.findInteriorPaths');
|
|
44598
44601
|
|
|
44599
|
-
|
|
44600
|
-
|
|
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
|
-
|
|
44626
|
-
|
|
44627
|
-
|
|
44628
|
-
|
|
44629
|
-
|
|
44630
|
-
|
|
44631
|
-
|
|
44632
|
-
|
|
44633
|
-
|
|
44634
|
-
|
|
44635
|
-
|
|
44636
|
-
|
|
44637
|
-
|
|
44638
|
-
|
|
44639
|
-
|
|
44640
|
-
|
|
44641
|
-
|
|
44642
|
-
|
|
44643
|
-
|
|
44644
|
-
|
|
44645
|
-
|
|
44646
|
-
|
|
44647
|
-
|
|
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
|
-
|
|
44655
|
-
|
|
44656
|
-
|
|
44657
|
-
|
|
44658
|
-
|
|
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
|
-
|
|
44662
|
-
|
|
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
|
-
|
|
44680
|
-
|
|
44681
|
-
|
|
44682
|
-
|
|
44683
|
-
|
|
44684
|
-
|
|
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
|
-
|
|
44713
|
-
|
|
44714
|
-
|
|
44715
|
-
|
|
44716
|
-
|
|
44717
|
-
|
|
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
|
-
|
|
44723
|
-
|
|
44724
|
-
|
|
44725
|
-
|
|
44726
|
-
|
|
44727
|
-
|
|
44728
|
-
|
|
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
|
-
|
|
44757
|
-
|
|
44758
|
-
|
|
44759
|
-
|
|
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
|
-
|
|
44765
|
-
|
|
44766
|
-
|
|
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
|
-
|
|
44786
|
-
|
|
44787
|
-
if (
|
|
44788
|
-
|
|
44789
|
-
|
|
44790
|
-
|
|
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
|
-
|
|
44803
|
-
|
|
44804
|
-
|
|
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
|
-
|
|
44817
|
-
|
|
44818
|
-
|
|
44819
|
-
|
|
44820
|
-
|
|
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
|
-
|
|
44826
|
-
|
|
44827
|
-
var
|
|
44828
|
-
|
|
44829
|
-
|
|
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
|
-
|
|
44835
|
-
|
|
44836
|
-
|
|
44837
|
-
|
|
44838
|
-
}
|
|
44743
|
+
function arcIsVisible(id, flags) {
|
|
44744
|
+
var flag = flags[absArcId(id)];
|
|
44745
|
+
return (flag & 0x11) > 0;
|
|
44746
|
+
}
|
|
44839
44747
|
|
|
44840
|
-
|
|
44841
|
-
|
|
44842
|
-
|
|
44843
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44858
|
-
|
|
44859
|
-
|
|
44860
|
-
|
|
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
|
-
|
|
44871
|
-
|
|
44872
|
-
|
|
44873
|
-
|
|
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 (
|
|
44876
|
-
|
|
44877
|
-
|
|
44878
|
-
return
|
|
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
|
-
|
|
44886
|
-
|
|
44887
|
-
|
|
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
|
|
44891
|
-
var
|
|
44892
|
-
|
|
44893
|
-
|
|
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
|
|
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
|
|
44906
|
-
|
|
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
|
-
|
|
44910
|
-
|
|
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
|
-
|
|
44914
|
-
|
|
44915
|
-
|
|
44916
|
-
|
|
44917
|
-
|
|
44918
|
-
|
|
44919
|
-
|
|
44920
|
-
|
|
44921
|
-
|
|
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
|
|
44917
|
+
return {
|
|
44918
|
+
arcs: new ArcCollection(arcs),
|
|
44919
|
+
layers: [layer]
|
|
44920
|
+
};
|
|
44924
44921
|
}
|
|
44925
44922
|
|
|
44926
|
-
function
|
|
44927
|
-
var
|
|
44928
|
-
|
|
44929
|
-
|
|
44930
|
-
|
|
44931
|
-
|
|
44932
|
-
|
|
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
|
-
|
|
44939
|
-
|
|
44940
|
-
|
|
44941
|
-
|
|
44942
|
-
|
|
44943
|
-
|
|
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
|
|
44947
|
-
return
|
|
44948
|
-
|
|
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
|
-
|
|
44957
|
-
|
|
44958
|
-
|
|
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
|
-
|
|
44972
|
-
|
|
44973
|
-
|
|
44974
|
-
|
|
44975
|
-
|
|
44976
|
-
|
|
44977
|
-
|
|
44978
|
-
|
|
44979
|
-
|
|
44980
|
-
|
|
44981
|
-
|
|
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
|
-
|
|
44985
|
-
|
|
44986
|
-
|
|
44987
|
-
|
|
44988
|
-
|
|
44989
|
-
|
|
44990
|
-
|
|
44991
|
-
|
|
44992
|
-
|
|
44993
|
-
|
|
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
|
-
|
|
45047
|
+
dissolveArcs(dataset);
|
|
44998
45048
|
}
|
|
44999
45049
|
|
|
45000
|
-
|
|
45001
|
-
|
|
45002
|
-
|
|
45003
|
-
|
|
45004
|
-
|
|
45005
|
-
var
|
|
45006
|
-
|
|
45007
|
-
|
|
45008
|
-
|
|
45009
|
-
|
|
45010
|
-
|
|
45011
|
-
|
|
45012
|
-
|
|
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
|
-
|
|
45018
|
-
|
|
45064
|
+
clipDataset = normalizeOverlaySource(clipSrc, targetDataset, opts);
|
|
45065
|
+
if (!opts.no_warn) {
|
|
45066
|
+
warnIfBoundsDontOverlap(targetLayers, targetDataset, clipDataset, type);
|
|
45019
45067
|
}
|
|
45020
|
-
|
|
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
|
|
45024
|
-
var
|
|
45025
|
-
|
|
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
|
|
45033
|
-
var
|
|
45034
|
-
|
|
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
|
|
45038
|
-
|
|
45039
|
-
return
|
|
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
|
-
|
|
45043
|
-
|
|
45044
|
-
|
|
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
|
-
|
|
45055
|
-
|
|
45056
|
-
var
|
|
45057
|
-
|
|
45058
|
-
|
|
45059
|
-
|
|
45060
|
-
|
|
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
|
|
45074
|
-
var
|
|
45075
|
-
|
|
45076
|
-
|
|
45077
|
-
|
|
45078
|
-
|
|
45079
|
-
|
|
45080
|
-
|
|
45081
|
-
|
|
45082
|
-
|
|
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
|
-
|
|
45103
|
-
|
|
45104
|
-
|
|
45105
|
-
|
|
45106
|
-
|
|
45107
|
-
|
|
45108
|
-
|
|
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
|
-
|
|
45111
|
-
|
|
45112
|
-
|
|
45113
|
-
|
|
45114
|
-
|
|
45115
|
-
|
|
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
|
-
|
|
45120
|
-
|
|
45121
|
-
|
|
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
|
-
|
|
45143
|
-
|
|
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
|
-
|
|
45149
|
-
|
|
45150
|
-
|
|
45151
|
-
|
|
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
|
-
|
|
45157
|
-
|
|
45158
|
-
|
|
45159
|
-
|
|
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
|
-
|
|
45196
|
-
|
|
45197
|
-
|
|
45198
|
-
|
|
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
|
|
45205
|
-
}
|
|
45245
|
+
return '';
|
|
45246
|
+
}
|
|
45206
45247
|
|
|
45207
|
-
|
|
45208
|
-
|
|
45209
|
-
|
|
45210
|
-
|
|
45211
|
-
|
|
45212
|
-
|
|
45213
|
-
|
|
45214
|
-
|
|
45215
|
-
|
|
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
|
-
|
|
45220
|
-
|
|
45221
|
-
|
|
45222
|
-
|
|
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
|
-
|
|
45225
|
-
|
|
45226
|
-
|
|
45227
|
-
|
|
45228
|
-
return
|
|
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
|
|
45232
|
-
|
|
45233
|
-
//
|
|
45234
|
-
|
|
45235
|
-
var
|
|
45236
|
-
var
|
|
45237
|
-
|
|
45238
|
-
|
|
45239
|
-
|
|
45240
|
-
|
|
45241
|
-
|
|
45242
|
-
|
|
45243
|
-
|
|
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
|
-
|
|
45256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45265
|
-
|
|
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
|
-
|
|
45268
|
-
|
|
45269
|
-
});
|
|
45343
|
+
utils.initializeArray(a, -1);
|
|
45344
|
+
utils.initializeArray(b, -1);
|
|
45270
45345
|
|
|
45271
|
-
function
|
|
45272
|
-
var
|
|
45273
|
-
|
|
45274
|
-
|
|
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
|
|
45278
|
-
var
|
|
45279
|
-
|
|
45280
|
-
|
|
45281
|
-
|
|
45282
|
-
|
|
45283
|
-
|
|
45284
|
-
|
|
45285
|
-
|
|
45286
|
-
|
|
45287
|
-
|
|
45288
|
-
|
|
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
|
-
|
|
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
|
|
45385
|
+
var ArcClassifier = /*#__PURE__*/Object.freeze({
|
|
45297
45386
|
__proto__: null,
|
|
45298
|
-
|
|
45387
|
+
getArcClassifier: getArcClassifier
|
|
45299
45388
|
});
|
|
45300
45389
|
|
|
45301
|
-
|
|
45302
|
-
|
|
45303
|
-
|
|
45304
|
-
for
|
|
45305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45311
|
-
|
|
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
|
-
|
|
45330
|
-
|
|
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
|
-
|
|
45334
|
-
|
|
45445
|
+
clipDatasetToWorldRect(dataset2);
|
|
45446
|
+
}
|
|
45447
|
+
return dataset2;
|
|
45335
45448
|
}
|
|
45336
45449
|
|
|
45337
|
-
|
|
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
|
-
|
|
45340
|
-
|
|
45341
|
-
|
|
45342
|
-
|
|
45343
|
-
|
|
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
|
-
|
|
45353
|
-
|
|
45354
|
-
|
|
45355
|
-
|
|
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
|
-
|
|
45359
|
-
|
|
45360
|
-
|
|
45361
|
-
|
|
45362
|
-
|
|
45363
|
-
|
|
45364
|
-
|
|
45365
|
-
var
|
|
45366
|
-
|
|
45367
|
-
|
|
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
|
-
|
|
45370
|
-
|
|
45371
|
-
|
|
45372
|
-
|
|
45373
|
-
|
|
45374
|
-
|
|
45375
|
-
|
|
45376
|
-
|
|
45377
|
-
|
|
45378
|
-
|
|
45379
|
-
|
|
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
|
-
|
|
45509
|
+
return {
|
|
45510
|
+
geojson: {
|
|
45511
|
+
type: 'GeometryCollection',
|
|
45512
|
+
geometries: geometries
|
|
45513
|
+
},
|
|
45514
|
+
dissolveAfterSplit: hasPositiveDistance && !hasNegativeDistance
|
|
45515
|
+
};
|
|
45516
|
+
}
|
|
45391
45517
|
|
|
45392
|
-
|
|
45393
|
-
|
|
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
|
-
|
|
45396
|
-
|
|
45397
|
-
|
|
45398
|
-
|
|
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
|
-
|
|
45401
|
-
|
|
45402
|
-
|
|
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
|
-
|
|
45405
|
-
|
|
45406
|
-
|
|
45407
|
-
|
|
45408
|
-
|
|
45409
|
-
|
|
45410
|
-
|
|
45411
|
-
|
|
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
|
-
|
|
45430
|
-
|
|
45431
|
-
|
|
45432
|
-
|
|
45433
|
-
|
|
45434
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45441
|
-
|
|
45442
|
-
|
|
45443
|
-
|
|
45444
|
-
|
|
45445
|
-
|
|
45446
|
-
|
|
45447
|
-
|
|
45448
|
-
|
|
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
|
-
|
|
45539
|
-
|
|
45540
|
-
|
|
45541
|
-
|
|
45542
|
-
|
|
45543
|
-
|
|
45544
|
-
|
|
45545
|
-
|
|
45546
|
-
|
|
45547
|
-
|
|
45548
|
-
|
|
45549
|
-
|
|
45550
|
-
|
|
45551
|
-
|
|
45552
|
-
|
|
45553
|
-
|
|
45554
|
-
|
|
45555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45658
|
+
return ids;
|
|
45659
|
+
}
|
|
45564
45660
|
|
|
45565
|
-
|
|
45566
|
-
|
|
45567
|
-
|
|
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
|
-
|
|
45570
|
-
|
|
45571
|
-
|
|
45572
|
-
|
|
45669
|
+
function tileHasSourcePolygon(tileId, mosaicIndex, sourceIdIndex) {
|
|
45670
|
+
return mosaicIndex.getSourceIdsByTileId(tileId).some(function(shapeId) {
|
|
45671
|
+
return sourceIdIndex[shapeId];
|
|
45672
|
+
});
|
|
45673
|
+
}
|
|
45573
45674
|
|
|
45574
|
-
|
|
45575
|
-
|
|
45576
|
-
|
|
45577
|
-
|
|
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
|
-
|
|
45582
|
-
return memo;
|
|
45583
|
-
}, []);
|
|
45584
|
-
|
|
45585
|
-
return points2;
|
|
45688
|
+
});
|
|
45689
|
+
return ownerId;
|
|
45586
45690
|
}
|
|
45587
45691
|
|
|
45588
|
-
|
|
45589
|
-
|
|
45590
|
-
|
|
45591
|
-
|
|
45692
|
+
function getSourceShapeAreas(shapes, arcs) {
|
|
45693
|
+
return shapes.map(function(shape) {
|
|
45694
|
+
return Math.abs(getShapeArea(shape, arcs));
|
|
45695
|
+
});
|
|
45696
|
+
}
|
|
45592
45697
|
|
|
45593
|
-
|
|
45594
|
-
|
|
45595
|
-
|
|
45596
|
-
|
|
45597
|
-
|
|
45598
|
-
|
|
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
|
-
|
|
45622
|
-
|
|
45623
|
-
|
|
45624
|
-
|
|
45625
|
-
|
|
45626
|
-
return
|
|
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
|
|
45630
|
-
var
|
|
45631
|
-
var
|
|
45632
|
-
|
|
45633
|
-
|
|
45634
|
-
|
|
45635
|
-
|
|
45636
|
-
|
|
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
|
-
|
|
45639
|
-
|
|
45640
|
-
|
|
45641
|
-
|
|
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
|
|
45729
|
+
return shp && shp.length > 0 ? getPolygonGeometry(shp, mosaicIndex.nodes.arcs) : null;
|
|
45650
45730
|
}
|
|
45651
45731
|
|
|
45652
|
-
function
|
|
45653
|
-
var
|
|
45654
|
-
|
|
45655
|
-
|
|
45656
|
-
|
|
45657
|
-
|
|
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
|
-
|
|
45660
|
-
|
|
45661
|
-
|
|
45662
|
-
|
|
45663
|
-
|
|
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
|
-
|
|
45670
|
-
|
|
45671
|
-
|
|
45672
|
-
|
|
45673
|
-
|
|
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
|
|
45697
|
-
|
|
45698
|
-
var
|
|
45699
|
-
|
|
45700
|
-
|
|
45701
|
-
|
|
45702
|
-
|
|
45703
|
-
|
|
45704
|
-
|
|
45705
|
-
|
|
45706
|
-
|
|
45707
|
-
|
|
45708
|
-
|
|
45709
|
-
|
|
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
|
|
45714
|
-
|
|
45715
|
-
|
|
45716
|
-
|
|
45717
|
-
|
|
45718
|
-
|
|
45719
|
-
|
|
45720
|
-
|
|
45721
|
-
|
|
45722
|
-
|
|
45723
|
-
|
|
45724
|
-
|
|
45725
|
-
|
|
45726
|
-
|
|
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
|
-
|
|
45748
|
-
|
|
45749
|
-
|
|
45789
|
+
if (coords.length === 0) return null;
|
|
45790
|
+
var geom = {
|
|
45791
|
+
type: 'MultiPolygon',
|
|
45792
|
+
coordinates: coords
|
|
45750
45793
|
};
|
|
45751
|
-
|
|
45752
|
-
|
|
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
|
-
|
|
45761
|
-
|
|
45762
|
-
|
|
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
|
-
|
|
45767
|
-
|
|
45768
|
-
|
|
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
|
-
|
|
45772
|
-
|
|
45773
|
-
|
|
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
|
-
|
|
45777
|
-
|
|
45778
|
-
|
|
45779
|
-
|
|
45780
|
-
|
|
45781
|
-
|
|
45782
|
-
|
|
45783
|
-
|
|
45784
|
-
|
|
45785
|
-
|
|
45786
|
-
|
|
45787
|
-
|
|
45788
|
-
|
|
45789
|
-
|
|
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
|
-
|
|
45795
|
-
|
|
45796
|
-
|
|
45797
|
-
|
|
45798
|
-
|
|
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
|
-
|
|
45841
|
-
|
|
45842
|
-
|
|
45843
|
-
|
|
45844
|
-
|
|
45845
|
-
|
|
45846
|
-
|
|
45847
|
-
|
|
45848
|
-
|
|
45849
|
-
|
|
45850
|
-
|
|
45851
|
-
|
|
45852
|
-
|
|
45853
|
-
|
|
45854
|
-
|
|
45855
|
-
|
|
45856
|
-
|
|
45857
|
-
|
|
45858
|
-
|
|
45859
|
-
|
|
45860
|
-
|
|
45861
|
-
|
|
45862
|
-
|
|
45863
|
-
|
|
45864
|
-
|
|
45865
|
-
|
|
45866
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45909
|
-
|
|
45910
|
-
|
|
45911
|
-
|
|
45912
|
-
|
|
45913
|
-
|
|
45914
|
-
|
|
45915
|
-
|
|
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
|
-
|
|
45919
|
-
|
|
45920
|
-
|
|
45921
|
-
|
|
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
|
-
|
|
45924
|
-
|
|
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
|
-
|
|
45927
|
-
|
|
45928
|
-
|
|
45929
|
-
|
|
45930
|
-
|
|
45931
|
-
|
|
45932
|
-
|
|
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
|
|
45937
|
-
clipRasterToBBox(lyr, bbox, opts);
|
|
45938
|
-
return lyr;
|
|
45939
|
-
});
|
|
46006
|
+
return bestSq;
|
|
45940
46007
|
}
|
|
45941
46008
|
|
|
45942
|
-
function
|
|
45943
|
-
var
|
|
45944
|
-
var
|
|
45945
|
-
|
|
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
|
|
45951
|
-
|
|
45952
|
-
|
|
45953
|
-
|
|
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
|
|
45963
|
-
|
|
45964
|
-
return
|
|
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
|
|
45968
|
-
//
|
|
45969
|
-
var
|
|
45970
|
-
|
|
45971
|
-
|
|
45972
|
-
|
|
45973
|
-
return
|
|
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
|
|
45978
|
-
|
|
45979
|
-
|
|
45980
|
-
|
|
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
|
-
|
|
45990
|
-
|
|
45991
|
-
|
|
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
|
-
|
|
46001
|
-
|
|
46002
|
-
|
|
46003
|
-
|
|
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
|
-
|
|
46008
|
-
|
|
46009
|
-
|
|
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
|
-
|
|
46013
|
-
|
|
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
|
-
|
|
46016
|
-
|
|
46017
|
-
|
|
46018
|
-
|
|
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
|
-
|
|
46022
|
-
|
|
46023
|
-
if (
|
|
46024
|
-
|
|
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
|
-
|
|
46192
|
+
if (chain.length > 0) {
|
|
46193
|
+
chains.push(chain);
|
|
46194
|
+
}
|
|
46195
|
+
return chains;
|
|
46027
46196
|
}
|
|
46028
46197
|
|
|
46029
|
-
function
|
|
46030
|
-
var
|
|
46031
|
-
|
|
46032
|
-
|
|
46033
|
-
|
|
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
|
-
//
|
|
46039
|
-
|
|
46040
|
-
|
|
46041
|
-
|
|
46042
|
-
|
|
46043
|
-
|
|
46044
|
-
|
|
46045
|
-
|
|
46046
|
-
|
|
46047
|
-
|
|
46048
|
-
|
|
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
|
|
46078
|
-
var
|
|
46079
|
-
|
|
46080
|
-
var
|
|
46081
|
-
|
|
46082
|
-
|
|
46083
|
-
|
|
46084
|
-
|
|
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
|
|
46088
|
-
|
|
46089
|
-
|
|
46090
|
-
|
|
46091
|
-
|
|
46092
|
-
|
|
46093
|
-
|
|
46094
|
-
|
|
46095
|
-
return
|
|
46096
|
-
|
|
46097
|
-
|
|
46098
|
-
|
|
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
|
-
|
|
46102
|
-
|
|
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
|
-
|
|
46106
|
-
|
|
46107
|
-
|
|
46108
|
-
|
|
46109
|
-
|
|
46110
|
-
|
|
46111
|
-
|
|
46112
|
-
|
|
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.
|
|
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.
|