mapshaper 0.7.33 → 0.7.34
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 +1488 -526
- package/package.json +1 -1
- package/www/mapshaper.js +1488 -526
package/www/mapshaper.js
CHANGED
|
@@ -17694,7 +17694,7 @@
|
|
|
17694
17694
|
// construction and are left in place -- removing them at the tile level
|
|
17695
17695
|
// (topological flood or per-tile geometric vote) was measured to add more
|
|
17696
17696
|
// artifacts (coverage dents) than it removed.
|
|
17697
|
-
this.getWindingTilesByShapeId = function(shapeId) {
|
|
17697
|
+
this.getWindingTilesByShapeId = function(shapeId, fillGaps) {
|
|
17698
17698
|
var shp = shapes[shapeId];
|
|
17699
17699
|
if (!shp || !shp.length) return [];
|
|
17700
17700
|
var i, r, ring, d, fwd;
|
|
@@ -17720,7 +17720,7 @@
|
|
|
17720
17720
|
if (!shp[r].length) continue;
|
|
17721
17721
|
var seedTile = arcTileIndex.getShapeIdByArcId(shp[r][0]);
|
|
17722
17722
|
if (seedTile < 0 || windStamp[seedTile] === stamp) continue;
|
|
17723
|
-
floodComponent(seedTile, flux, sb, stamp, ids);
|
|
17723
|
+
floodComponent(seedTile, flux, sb, stamp, ids, fillGaps);
|
|
17724
17724
|
}
|
|
17725
17725
|
return ids.map(tileIdToTile);
|
|
17726
17726
|
};
|
|
@@ -17933,12 +17933,16 @@
|
|
|
17933
17933
|
// any neighbor known to be outside the buffer (the true exterior, or a tile
|
|
17934
17934
|
// entirely outside the shape bbox) at winding 0. Nonzero tiles are pushed to
|
|
17935
17935
|
// @ids. The flood is bounded to the shape bbox for speed.
|
|
17936
|
-
function floodComponent(seedTile, flux, sb, stamp, ids) {
|
|
17936
|
+
function floodComponent(seedTile, flux, sb, stamp, ids, fillGaps) {
|
|
17937
17937
|
windVal[seedTile] = 0;
|
|
17938
17938
|
windStamp[seedTile] = stamp;
|
|
17939
17939
|
var stack = [seedTile];
|
|
17940
17940
|
var visited = [seedTile];
|
|
17941
17941
|
var C = null;
|
|
17942
|
+
// Node keys of arcs on the buffer's true outer edge (nb < 0). The exterior
|
|
17943
|
+
// is not a mosaic tile, so a pinched fold-back wedge is detected by sharing
|
|
17944
|
+
// one of these nodes (see fillPinchedGaps).
|
|
17945
|
+
var outerKeys = fillGaps ? {} : null;
|
|
17942
17946
|
var cur, ctile, cw, pp, ring2, kk, d2, nb, fwd2, nbb, delta, i;
|
|
17943
17947
|
while (stack.length) {
|
|
17944
17948
|
cur = stack.pop();
|
|
@@ -17956,6 +17960,10 @@
|
|
|
17956
17960
|
if (nb < 0 || nbb.xmax < sb.xmin || nbb.xmin > sb.xmax ||
|
|
17957
17961
|
nbb.ymax < sb.ymin || nbb.ymin > sb.ymax) {
|
|
17958
17962
|
if (C === null) C = -(cw + delta); // anchor: this neighbor is winding 0
|
|
17963
|
+
if (outerKeys && nb < 0) {
|
|
17964
|
+
outerKeys[arcEndpointKey(d2, 0)] = true;
|
|
17965
|
+
outerKeys[arcEndpointKey(d2, -1)] = true;
|
|
17966
|
+
}
|
|
17959
17967
|
continue;
|
|
17960
17968
|
}
|
|
17961
17969
|
windVal[nb] = cw + delta;
|
|
@@ -17969,6 +17977,93 @@
|
|
|
17969
17977
|
for (i = 0; i < visited.length; i++) {
|
|
17970
17978
|
if (windVal[visited[i]] + C !== 0) ids.push(visited[i]);
|
|
17971
17979
|
}
|
|
17980
|
+
if (fillGaps) {
|
|
17981
|
+
fillPinchedGaps(visited, stamp, C, outerKeys, ids);
|
|
17982
|
+
}
|
|
17983
|
+
}
|
|
17984
|
+
|
|
17985
|
+
// Reclaim winding-zero gaps that the winding rule dropped but that are not
|
|
17986
|
+
// really open. Where variable geodesic offset distance breaks the planar
|
|
17987
|
+
// self-crossing assumption, a fold-back leaves a thin winding-zero wedge that
|
|
17988
|
+
// is pinched to the open exterior at a single point: a shared node (arc
|
|
17989
|
+
// endpoint on the buffer's outer edge), NOT a shared arc. So a wedge is
|
|
17990
|
+
// absorbed when its winding-zero component:
|
|
17991
|
+
// - borders the outer edge nowhere across an arc (it is not the open
|
|
17992
|
+
// exterior or an open concavity, which the winding rule legitimately
|
|
17993
|
+
// leaves out), and
|
|
17994
|
+
// - shares a boundary node with the outer edge (the pinch point).
|
|
17995
|
+
// This is purely topological (the analogue of -clean/removeGaps); unlike an
|
|
17996
|
+
// area threshold it cannot fill a genuine interior hole, whose boundary nodes
|
|
17997
|
+
// are all interior and never coincide with the buffer's outer edge.
|
|
17998
|
+
function fillPinchedGaps(visited, stamp, C, outerKeys, ids) {
|
|
17999
|
+
if (!outerKeys) return;
|
|
18000
|
+
var candidates = [];
|
|
18001
|
+
for (var i = 0; i < visited.length; i++) {
|
|
18002
|
+
if (windVal[visited[i]] + C === 0) candidates.push(visited[i]);
|
|
18003
|
+
}
|
|
18004
|
+
var isGap = function(tileId) {
|
|
18005
|
+
return windStamp[tileId] === stamp && windVal[tileId] + C === 0;
|
|
18006
|
+
};
|
|
18007
|
+
var pinched = collectPinchedGapTiles(candidates, isGap, outerKeys);
|
|
18008
|
+
for (i = 0; i < pinched.length; i++) ids.push(pinched[i]);
|
|
18009
|
+
}
|
|
18010
|
+
|
|
18011
|
+
// Group @candidates (gap-region tile ids) into arc-connected components, then
|
|
18012
|
+
// return the ids of components that are pinched to the buffer's outer edge: a
|
|
18013
|
+
// component that borders the exterior (nb < 0) nowhere across an arc, yet
|
|
18014
|
+
// shares a boundary node with @outerKeys (the outer-edge nodes). @isGap tells
|
|
18015
|
+
// whether a neighbor tile belongs to the gap region (grows the component).
|
|
18016
|
+
// Purely topological, so it never reclaims a genuine interior hole (whose
|
|
18017
|
+
// boundary nodes are all interior).
|
|
18018
|
+
function collectPinchedGapTiles(candidates, isGap, outerKeys) {
|
|
18019
|
+
var comp = {}, out = [];
|
|
18020
|
+
var i, t, tile, p, ring, k, d, nb, tiles, keys, arcOpen, stack;
|
|
18021
|
+
for (i = 0; i < candidates.length; i++) {
|
|
18022
|
+
if (comp[candidates[i]] !== undefined) continue;
|
|
18023
|
+
tiles = [candidates[i]];
|
|
18024
|
+
keys = [];
|
|
18025
|
+
arcOpen = false;
|
|
18026
|
+
comp[candidates[i]] = true;
|
|
18027
|
+
stack = [candidates[i]];
|
|
18028
|
+
while (stack.length) {
|
|
18029
|
+
t = stack.pop();
|
|
18030
|
+
tile = mosaic[t];
|
|
18031
|
+
for (p = 0; p < tile.length; p++) {
|
|
18032
|
+
ring = tile[p];
|
|
18033
|
+
for (k = 0; k < ring.length; k++) {
|
|
18034
|
+
d = ring[k];
|
|
18035
|
+
keys.push(arcEndpointKey(d, 0), arcEndpointKey(d, -1));
|
|
18036
|
+
nb = arcTileIndex.getShapeIdByArcId(~d);
|
|
18037
|
+
if (nb < 0) {
|
|
18038
|
+
arcOpen = true; // borders the outer edge across an arc
|
|
18039
|
+
continue;
|
|
18040
|
+
}
|
|
18041
|
+
if (!isGap(nb)) continue; // selected boundary of the gap
|
|
18042
|
+
if (comp[nb] === undefined) {
|
|
18043
|
+
comp[nb] = true;
|
|
18044
|
+
tiles.push(nb);
|
|
18045
|
+
stack.push(nb);
|
|
18046
|
+
}
|
|
18047
|
+
}
|
|
18048
|
+
}
|
|
18049
|
+
}
|
|
18050
|
+
if (!arcOpen && sharesKey(keys, outerKeys)) {
|
|
18051
|
+
for (k = 0; k < tiles.length; k++) out.push(tiles[k]);
|
|
18052
|
+
}
|
|
18053
|
+
}
|
|
18054
|
+
return out;
|
|
18055
|
+
}
|
|
18056
|
+
|
|
18057
|
+
function arcEndpointKey(arcId, nth) {
|
|
18058
|
+
var v = nodes.arcs.getVertex(arcId, nth);
|
|
18059
|
+
return v.x + ',' + v.y;
|
|
18060
|
+
}
|
|
18061
|
+
|
|
18062
|
+
function sharesKey(keys, set) {
|
|
18063
|
+
for (var i = 0; i < keys.length; i++) {
|
|
18064
|
+
if (set[keys[i]]) return true;
|
|
18065
|
+
}
|
|
18066
|
+
return false;
|
|
17972
18067
|
}
|
|
17973
18068
|
|
|
17974
18069
|
function getOverlapPriorityFunction(shapes, arcs, rule) {
|
|
@@ -31779,6 +31874,13 @@ ${svg}
|
|
|
31779
31874
|
describe: '[projected data] buffer using geodesic distances',
|
|
31780
31875
|
type: 'flag'
|
|
31781
31876
|
})
|
|
31877
|
+
.option('geodesic2', {
|
|
31878
|
+
// undocumented/experimental: geodesic buffering for projected data done
|
|
31879
|
+
// in-place in the projected plane via per-point scale correction (no
|
|
31880
|
+
// web-Mercator round-trip; avoids pole/antimeridian edge cases). Compare
|
|
31881
|
+
// with the 'geodesic' flag, which reprojects through lng/lat instead.
|
|
31882
|
+
type: 'flag'
|
|
31883
|
+
})
|
|
31782
31884
|
.option('polar', {
|
|
31783
31885
|
// describe: 'keep lat-long buffers within the valid extent (+/-180, +/-90); for growing polygons sliced at the antimeridian/poles (erode not yet supported)',
|
|
31784
31886
|
type: 'flag'
|
|
@@ -31837,12 +31939,6 @@ ${svg}
|
|
|
31837
31939
|
// before the dissolve) is on by default; this opts out.
|
|
31838
31940
|
type: 'flag'
|
|
31839
31941
|
})
|
|
31840
|
-
.option('loop-removal-turn-gate', {
|
|
31841
|
-
// Undocumented: for two-sided open-path buffers, use the source-turn-gate
|
|
31842
|
-
// loop-removal method instead of the default crossing-direction method.
|
|
31843
|
-
// Kept as an alternative for A/B comparison and as a conservative fallback.
|
|
31844
|
-
type: 'flag'
|
|
31845
|
-
})
|
|
31846
31942
|
.option('band-method', {
|
|
31847
31943
|
// Undocumented escape hatch: build buffers with the older band (sector-
|
|
31848
31944
|
// band) construction -- per-segment offset bands + join-sector rings + a
|
|
@@ -31855,6 +31951,28 @@ ${svg}
|
|
|
31855
31951
|
// debugging aid in case the default construction mishandles some input.
|
|
31856
31952
|
type: 'flag'
|
|
31857
31953
|
})
|
|
31954
|
+
.option('clean-outline-winding', {
|
|
31955
|
+
// Undocumented: build the polygon-grow outer ring with the constant-winding
|
|
31956
|
+
// concave-join construction used by the open-path two-sided outline (a
|
|
31957
|
+
// reversed arc at the offset radius). This is now the DEFAULT polygon-grow
|
|
31958
|
+
// construction; the flag is retained as an explicit/no-op selector.
|
|
31959
|
+
type: 'flag'
|
|
31960
|
+
})
|
|
31961
|
+
.option('coarse-bridge', {
|
|
31962
|
+
// Undocumented: in the clean-outline-winding construction, bridge concave
|
|
31963
|
+
// bends with the low-resolution makeCoarseConcaveJoin (as few as one
|
|
31964
|
+
// reversed arc vertex) instead of the full-resolution makeConcaveJoin.
|
|
31965
|
+
// The reversed bridge only bounds a self-overlap loop the direction remover
|
|
31966
|
+
// collapses, so this leaves the final boundary unchanged while producing a
|
|
31967
|
+
// smaller ring for the winding dissolve -- a construction-speed tradeoff.
|
|
31968
|
+
type: 'flag'
|
|
31969
|
+
})
|
|
31970
|
+
.option('no-gap-patch', {
|
|
31971
|
+
// Undocumented: disable the default geodesic gap-patch stadium union
|
|
31972
|
+
// (see useGapPatch in mapshaper-buffer-common.mjs). Useful for comparing
|
|
31973
|
+
// raw dissolve behavior against the patched outline construction.
|
|
31974
|
+
type: 'flag'
|
|
31975
|
+
})
|
|
31858
31976
|
.option('no-cleanup', {
|
|
31859
31977
|
type: 'flag'
|
|
31860
31978
|
})
|
|
@@ -41198,6 +41316,24 @@ ${svg}
|
|
|
41198
41316
|
return dataset;
|
|
41199
41317
|
}
|
|
41200
41318
|
|
|
41319
|
+
// Dissolve options shared by clean-outline polygon grow and two-sided line
|
|
41320
|
+
// outline buffers. per_part_holes treats each input ring as an independent
|
|
41321
|
+
// overlapping union part (not shape-wide hole nesting). Boundary-flood
|
|
41322
|
+
// membership (no winding_fill) matches the line buffer dissolve; spurious
|
|
41323
|
+
// interior rings are removed afterward by applyOutlineArtifactHoleFilter.
|
|
41324
|
+
function getOutlineBufferDissolveOpts(opts) {
|
|
41325
|
+
return Object.assign({}, opts, {per_part_holes: true});
|
|
41326
|
+
}
|
|
41327
|
+
|
|
41328
|
+
// True when geodesic gap-patch stadiums should be unioned into the dissolve.
|
|
41329
|
+
// Disabled in line debug-offset (patch rings broke the m_loops debug view) but
|
|
41330
|
+
// kept on for polygon debug-offset so the extra stadium rings are visible.
|
|
41331
|
+
function useGapPatch(opts, geodesic) {
|
|
41332
|
+
if (!geodesic || opts.no_gap_patch) return false;
|
|
41333
|
+
if (opts.debug_offset) return opts.geometry_type == 'polygon';
|
|
41334
|
+
return true;
|
|
41335
|
+
}
|
|
41336
|
+
|
|
41201
41337
|
function dissolveBufferDataset2(dataset, optsArg) {
|
|
41202
41338
|
var opts = optsArg || {};
|
|
41203
41339
|
var lyr = dataset.layers.filter(function(l) { return l.geometry_type == 'polygon'; })[0] ||
|
|
@@ -41232,7 +41368,7 @@ ${svg}
|
|
|
41232
41368
|
var pathfind = getRingIntersector(mosaicIndex.nodes);
|
|
41233
41369
|
var shapes2 = lyr.shapes.map(function(shp, shapeId) {
|
|
41234
41370
|
var tiles = opts.winding_fill ?
|
|
41235
|
-
mosaicIndex.getWindingTilesByShapeId(shapeId) :
|
|
41371
|
+
mosaicIndex.getWindingTilesByShapeId(shapeId, false) :
|
|
41236
41372
|
mosaicIndex.getTilesByShapeIds([shapeId]);
|
|
41237
41373
|
var rings = [];
|
|
41238
41374
|
var holes = [];
|
|
@@ -41321,12 +41457,10 @@ ${svg}
|
|
|
41321
41457
|
// pre-simplification is disabled. Enabled by default with a tolerance of
|
|
41322
41458
|
// 1% of the buffer radius; pass an explicit tolerance to change the error
|
|
41323
41459
|
// budget, or tolerance=0 to disable pre-simplification.
|
|
41324
|
-
//
|
|
41325
|
-
//
|
|
41326
|
-
//
|
|
41327
|
-
// simplification
|
|
41328
|
-
// and capping the turning concentrated by removed sub-paths (see
|
|
41329
|
-
// presimplifyPathVerts in mapshaper-path-buffer-v4.mjs).
|
|
41460
|
+
// The error budget is expressed as a positional Douglas-Peucker interval
|
|
41461
|
+
// (see presimplifyPathVerts in mapshaper-path-buffer-v4.mjs). End-segment
|
|
41462
|
+
// bearings are pinned so cap geometry stays exact; pass tolerance=0 to
|
|
41463
|
+
// disable pre-simplification entirely.
|
|
41330
41464
|
function getBufferSimplifyFunction(dataset, opts) {
|
|
41331
41465
|
if (opts.tolerance === 0 || opts.tolerance == '0' || opts.tolerance == '0%') return null;
|
|
41332
41466
|
var tolFn = getBufferToleranceFunction(dataset, opts);
|
|
@@ -41396,7 +41530,7 @@ ${svg}
|
|
|
41396
41530
|
// that a collapsible overshoot pocket must never span.
|
|
41397
41531
|
function BufferBuilder() {
|
|
41398
41532
|
var self = {};
|
|
41399
|
-
var buffer, path, bufferPos, pathPos;
|
|
41533
|
+
var buffer, path, bufferPos, pathPos, bufferTags, pathTags;
|
|
41400
41534
|
|
|
41401
41535
|
init();
|
|
41402
41536
|
|
|
@@ -41405,6 +41539,12 @@ ${svg}
|
|
|
41405
41539
|
path = [];
|
|
41406
41540
|
bufferPos = [];
|
|
41407
41541
|
pathPos = [];
|
|
41542
|
+
// Parallel reversed-arc ("dip") tags: 1 marks a vertex emitted as part of a
|
|
41543
|
+
// reversed concave-join arc (a pure self-overlap construction artifact) so
|
|
41544
|
+
// the coverage-based loop remover can key on provenance. Path-side vertices
|
|
41545
|
+
// (the source-path edge) and untagged callers default to 0.
|
|
41546
|
+
bufferTags = [];
|
|
41547
|
+
pathTags = [];
|
|
41408
41548
|
}
|
|
41409
41549
|
|
|
41410
41550
|
self.size = function() {
|
|
@@ -41414,31 +41554,35 @@ ${svg}
|
|
|
41414
41554
|
self.addPathVertex = function(p) {
|
|
41415
41555
|
path.push(p);
|
|
41416
41556
|
pathPos.push(NaN);
|
|
41557
|
+
pathTags.push(0);
|
|
41417
41558
|
};
|
|
41418
41559
|
|
|
41419
|
-
self.addBufferVertices = function(arr, pos) {
|
|
41560
|
+
self.addBufferVertices = function(arr, pos, tag) {
|
|
41420
41561
|
for (var i=0; i<arr.length; i++) {
|
|
41421
|
-
self.addBufferVertex(arr[i], pos);
|
|
41562
|
+
self.addBufferVertex(arr[i], pos, tag);
|
|
41422
41563
|
}
|
|
41423
41564
|
};
|
|
41424
41565
|
|
|
41425
|
-
self.addBufferVertex = function(p, pos) {
|
|
41566
|
+
self.addBufferVertex = function(p, pos, tag) {
|
|
41426
41567
|
var prevP = buffer[buffer.length - 1];
|
|
41427
41568
|
if (prevP && pointsAreSame(prevP, p)) {
|
|
41428
41569
|
return;
|
|
41429
41570
|
}
|
|
41430
41571
|
buffer.push(p);
|
|
41431
41572
|
bufferPos.push(pos === undefined ? NaN : pos);
|
|
41573
|
+
bufferTags.push(tag ? 1 : 0);
|
|
41432
41574
|
};
|
|
41433
41575
|
|
|
41434
|
-
// Returns {ring, srcPos}: ring is the closed coordinate ring (first
|
|
41435
|
-
// repeated as last); srcPos
|
|
41576
|
+
// Returns {ring, srcPos, dipTags}: ring is the closed coordinate ring (first
|
|
41577
|
+
// point repeated as last); srcPos and dipTags are parallel arrays (source
|
|
41578
|
+
// position and reversed-arc tag).
|
|
41436
41579
|
// @allowDegenerate: return null instead of erroring when the ring collapsed
|
|
41437
41580
|
// to fewer than 3 points (an offset loop whose source ring shrank away, e.g.
|
|
41438
41581
|
// a hole smaller than the buffer radius) -- a normal outcome, not a bug.
|
|
41439
41582
|
self.done = function(allowDegenerate) {
|
|
41440
41583
|
var ring = path.slice().reverse().concat(buffer);
|
|
41441
41584
|
var srcPos = pathPos.slice().reverse().concat(bufferPos);
|
|
41585
|
+
var dipTags = pathTags.slice().reverse().concat(bufferTags);
|
|
41442
41586
|
if (ring.length < 3) {
|
|
41443
41587
|
if (allowDegenerate) {
|
|
41444
41588
|
init();
|
|
@@ -41448,8 +41592,9 @@ ${svg}
|
|
|
41448
41592
|
}
|
|
41449
41593
|
ring.push(ring[0].concat());
|
|
41450
41594
|
srcPos.push(srcPos[0]);
|
|
41595
|
+
dipTags.push(dipTags[0]);
|
|
41451
41596
|
init();
|
|
41452
|
-
return {ring: ring, srcPos: srcPos};
|
|
41597
|
+
return {ring: ring, srcPos: srcPos, dipTags: dipTags};
|
|
41453
41598
|
};
|
|
41454
41599
|
|
|
41455
41600
|
return self;
|
|
@@ -41459,6 +41604,298 @@ ${svg}
|
|
|
41459
41604
|
return a[0] === b[0] && a[1] === b[1];
|
|
41460
41605
|
}
|
|
41461
41606
|
|
|
41607
|
+
// Chunk size for the per-ring source segment index used by wedgeIsExposed().
|
|
41608
|
+
var WEDGE_SEGMENT_CHUNK_SIZE = 48;
|
|
41609
|
+
|
|
41610
|
+
// Build a flat segment index for a source path vertex list (one ring). Each chunk
|
|
41611
|
+
// stores up to WEDGE_SEGMENT_CHUNK_SIZE consecutive segments with a bounding box
|
|
41612
|
+
// so probeIsExposed() can skip segments far from the probe.
|
|
41613
|
+
function buildVertsSegmentIndex(verts, chunkSize) {
|
|
41614
|
+
chunkSize = chunkSize || WEDGE_SEGMENT_CHUNK_SIZE;
|
|
41615
|
+
var coords = [];
|
|
41616
|
+
var chunks = [];
|
|
41617
|
+
var n = verts.length;
|
|
41618
|
+
if (n < 2) return {coords: coords, chunks: chunks};
|
|
41619
|
+
var inChunk = 0;
|
|
41620
|
+
var chunkSegStart = 0;
|
|
41621
|
+
var start = 0;
|
|
41622
|
+
var xmin = 0, ymin = 0, xmax = 0, ymax = 0;
|
|
41623
|
+
var ax, ay, bx, by, i;
|
|
41624
|
+
for (i = 0; i < n - 1; i++) {
|
|
41625
|
+
ax = verts[i][0];
|
|
41626
|
+
ay = verts[i][1];
|
|
41627
|
+
bx = verts[i + 1][0];
|
|
41628
|
+
by = verts[i + 1][1];
|
|
41629
|
+
if (inChunk === 0) {
|
|
41630
|
+
chunkSegStart = i;
|
|
41631
|
+
start = coords.length / 4;
|
|
41632
|
+
xmin = Math.min(ax, bx);
|
|
41633
|
+
xmax = Math.max(ax, bx);
|
|
41634
|
+
ymin = Math.min(ay, by);
|
|
41635
|
+
ymax = Math.max(ay, by);
|
|
41636
|
+
} else {
|
|
41637
|
+
if (ax < xmin) xmin = ax; else if (ax > xmax) xmax = ax;
|
|
41638
|
+
if (bx < xmin) xmin = bx; else if (bx > xmax) xmax = bx;
|
|
41639
|
+
if (ay < ymin) ymin = ay; else if (ay > ymax) ymax = ay;
|
|
41640
|
+
if (by < ymin) ymin = by; else if (by > ymax) ymax = by;
|
|
41641
|
+
}
|
|
41642
|
+
coords.push(ax, ay, bx, by);
|
|
41643
|
+
inChunk++;
|
|
41644
|
+
if (inChunk === chunkSize) {
|
|
41645
|
+
chunks.push({
|
|
41646
|
+
start: start,
|
|
41647
|
+
end: coords.length / 4,
|
|
41648
|
+
segStart: chunkSegStart,
|
|
41649
|
+
segEnd: i + 1,
|
|
41650
|
+
xmin: xmin, ymin: ymin, xmax: xmax, ymax: ymax
|
|
41651
|
+
});
|
|
41652
|
+
inChunk = 0;
|
|
41653
|
+
}
|
|
41654
|
+
}
|
|
41655
|
+
if (inChunk > 0) {
|
|
41656
|
+
chunks.push({
|
|
41657
|
+
start: start,
|
|
41658
|
+
end: coords.length / 4,
|
|
41659
|
+
segStart: chunkSegStart,
|
|
41660
|
+
segEnd: n - 1,
|
|
41661
|
+
xmin: xmin, ymin: ymin, xmax: xmax, ymax: ymax
|
|
41662
|
+
});
|
|
41663
|
+
}
|
|
41664
|
+
return {coords: coords, chunks: chunks};
|
|
41665
|
+
}
|
|
41666
|
+
|
|
41667
|
+
// True if any point of the round-join wedge at a fan-apart concave bend is NOT
|
|
41668
|
+
// covered by another source segment. Probes the concave-bridge arc plus the two
|
|
41669
|
+
// offset tips; tips alone miss bends whose tips are covered but whose arc flank
|
|
41670
|
+
// is still exposed (see idaho 150km regression).
|
|
41671
|
+
function wedgeIsExposed(index, skipA, skipB, vx, vy, arc, tipA, tipB) {
|
|
41672
|
+
var i;
|
|
41673
|
+
if (probeIsExposed(index, skipA, skipB, vx, vy, tipA) ||
|
|
41674
|
+
probeIsExposed(index, skipA, skipB, vx, vy, tipB)) {
|
|
41675
|
+
return true;
|
|
41676
|
+
}
|
|
41677
|
+
for (i = 0; i < arc.length; i++) {
|
|
41678
|
+
if (probeIsExposed(index, skipA, skipB, vx, vy, arc[i])) return true;
|
|
41679
|
+
}
|
|
41680
|
+
return false;
|
|
41681
|
+
}
|
|
41682
|
+
|
|
41683
|
+
function probeIsExposed(index, skipA, skipB, vx, vy, p) {
|
|
41684
|
+
var r = distance2D(vx, vy, p[0], p[1]);
|
|
41685
|
+
if (!(r > 0)) return false;
|
|
41686
|
+
var r2 = r * r * 0.98 * 0.98;
|
|
41687
|
+
var px = p[0], py = p[1];
|
|
41688
|
+
var chunks = index.chunks;
|
|
41689
|
+
var coords = index.coords;
|
|
41690
|
+
var c, chunk, s, o, d;
|
|
41691
|
+
for (c = 0; c < chunks.length; c++) {
|
|
41692
|
+
chunk = chunks[c];
|
|
41693
|
+
if (chunkBoxDistSq(px, py, chunk) >= r2) continue;
|
|
41694
|
+
for (s = chunk.segStart; s < chunk.segEnd; s++) {
|
|
41695
|
+
if (s === skipA || s === skipB) continue;
|
|
41696
|
+
o = (chunk.start + (s - chunk.segStart)) * 4;
|
|
41697
|
+
d = pointSegDistSq2(px, py, coords[o], coords[o + 1], coords[o + 2], coords[o + 3]);
|
|
41698
|
+
if (d < r2) return false;
|
|
41699
|
+
}
|
|
41700
|
+
}
|
|
41701
|
+
return true;
|
|
41702
|
+
}
|
|
41703
|
+
|
|
41704
|
+
function chunkBoxDistSq(px, py, chunk) {
|
|
41705
|
+
var dx = px < chunk.xmin ? chunk.xmin - px : (px > chunk.xmax ? px - chunk.xmax : 0);
|
|
41706
|
+
var dy = py < chunk.ymin ? chunk.ymin - py : (py > chunk.ymax ? py - chunk.ymax : 0);
|
|
41707
|
+
return dx * dx + dy * dy;
|
|
41708
|
+
}
|
|
41709
|
+
|
|
41710
|
+
var R$2 = WGS84.SEMIMAJOR_AXIS;
|
|
41711
|
+
|
|
41712
|
+
// GeographicLib docs: https://geographiclib.sourceforge.io/html/js/
|
|
41713
|
+
// https://geographiclib.sourceforge.io/html/js/module-GeographicLib_Geodesic.Geodesic.html
|
|
41714
|
+
// https://geographiclib.sourceforge.io/html/js/tutorial-2-interface.html
|
|
41715
|
+
function getGeodesic(P) {
|
|
41716
|
+
if (!isLatLngCRS(P)) error('Expected an unprojected CRS');
|
|
41717
|
+
var f = P.es / (1 + Math.sqrt(P.one_es));
|
|
41718
|
+
// var GeographicLib = require('mproj').internal.GeographicLib;
|
|
41719
|
+
var GeographicLib = require$1('geographiclib-geodesic');
|
|
41720
|
+
// return new GeographicLib.Geodesic.Geodesic(P.a, 0)
|
|
41721
|
+
return new GeographicLib.Geodesic.Geodesic(P.a, f);
|
|
41722
|
+
}
|
|
41723
|
+
|
|
41724
|
+
function interpolatePoint2D(ax, ay, bx, by, k) {
|
|
41725
|
+
var j = 1 - k;
|
|
41726
|
+
return [ax * j + bx * k, ay * j + by * k];
|
|
41727
|
+
}
|
|
41728
|
+
|
|
41729
|
+
function getInterpolationFunction(P) {
|
|
41730
|
+
var spherical = P && isLatLngCRS(P);
|
|
41731
|
+
if (!spherical) return interpolatePoint2D;
|
|
41732
|
+
var geod = getGeodesic(P);
|
|
41733
|
+
return function(lng, lat, lng2, lat2, k) {
|
|
41734
|
+
var r = geod.Inverse(lat, lng, lat2, lng2);
|
|
41735
|
+
var dist = r.s12 * k;
|
|
41736
|
+
var r2 = geod.Direct(lat, lng, r.azi1, dist);
|
|
41737
|
+
return [r2.lon2, r2.lat2];
|
|
41738
|
+
};
|
|
41739
|
+
}
|
|
41740
|
+
|
|
41741
|
+
function getPlanarSegmentEndpoint(x, y, bearing, meterDist) {
|
|
41742
|
+
var rad = bearing / 180 * Math.PI;
|
|
41743
|
+
var dx = Math.sin(rad) * meterDist;
|
|
41744
|
+
var dy = Math.cos(rad) * meterDist;
|
|
41745
|
+
return [x + dx, y + dy];
|
|
41746
|
+
}
|
|
41747
|
+
|
|
41748
|
+
// source: https://github.com/mapbox/cheap-ruler/blob/master/index.js
|
|
41749
|
+
function fastGeodeticSegmentFunction(lng, lat, bearing, meterDist) {
|
|
41750
|
+
var D2R = Math.PI / 180;
|
|
41751
|
+
var cos = Math.cos(lat * D2R);
|
|
41752
|
+
var cos2 = 2 * cos * cos - 1;
|
|
41753
|
+
var cos3 = 2 * cos * cos2 - cos;
|
|
41754
|
+
var cos4 = 2 * cos * cos3 - cos2;
|
|
41755
|
+
var cos5 = 2 * cos * cos4 - cos3;
|
|
41756
|
+
var kx = (111.41513 * cos - 0.09455 * cos3 + 0.00012 * cos5) * 1000;
|
|
41757
|
+
var ky = (111.13209 - 0.56605 * cos2 + 0.0012 * cos4) * 1000;
|
|
41758
|
+
var bearingRad = bearing * D2R;
|
|
41759
|
+
var lat2 = lat + Math.cos(bearingRad) * meterDist / ky;
|
|
41760
|
+
var lng2 = lng + Math.sin(bearingRad) * meterDist / kx;
|
|
41761
|
+
return [lng2, lat2];
|
|
41762
|
+
}
|
|
41763
|
+
|
|
41764
|
+
|
|
41765
|
+
function wrap(deg) {
|
|
41766
|
+
while (deg < -180) deg += 360;
|
|
41767
|
+
while (deg > 180) deg -= 360;
|
|
41768
|
+
return deg;
|
|
41769
|
+
}
|
|
41770
|
+
|
|
41771
|
+
function fastGeodeticBearingFunction(lng1, lat1, lng2, lat2) {
|
|
41772
|
+
var D2R = Math.PI / 180;
|
|
41773
|
+
var f = 1 / 298.257223563;
|
|
41774
|
+
var e2 = f * (2 - f);
|
|
41775
|
+
var m = R$2 * D2R;
|
|
41776
|
+
var coslat = Math.cos(lat1 * D2R);
|
|
41777
|
+
var w2 = 1 / (1 - e2 * (1 - coslat * coslat));
|
|
41778
|
+
var w = Math.sqrt(w2);
|
|
41779
|
+
var kx = m * w * coslat;
|
|
41780
|
+
var ky = m * w * w2 * (1 - e2);
|
|
41781
|
+
var dx = wrap(lng2 - lng1) * kx;
|
|
41782
|
+
var dy = (lat2 - lat1) * ky;
|
|
41783
|
+
return Math.atan2(dx, dy) / D2R;
|
|
41784
|
+
}
|
|
41785
|
+
|
|
41786
|
+
function getGeodeticSegmentFunction(P) {
|
|
41787
|
+
if (!isLatLngCRS(P)) {
|
|
41788
|
+
return getPlanarSegmentEndpoint;
|
|
41789
|
+
}
|
|
41790
|
+
var g = getGeodesic(P);
|
|
41791
|
+
return function(lng, lat, bearing, meterDist) {
|
|
41792
|
+
var o = g.Direct(lat, lng, bearing, meterDist);
|
|
41793
|
+
var p = [o.lon2, o.lat2];
|
|
41794
|
+
return p;
|
|
41795
|
+
};
|
|
41796
|
+
}
|
|
41797
|
+
|
|
41798
|
+
function getFastGeodeticSegmentFunction(P) {
|
|
41799
|
+
// CAREFUL: this function has higher error at very large distances and at the poles
|
|
41800
|
+
// also, it wouldn't work for other planets than Earth
|
|
41801
|
+
return isLatLngCRS(P) ? fastGeodeticSegmentFunction : getPlanarSegmentEndpoint;
|
|
41802
|
+
}
|
|
41803
|
+
|
|
41804
|
+
// return function to calculate bearing of a segment in degrees
|
|
41805
|
+
function getBearingFunction(dataset) {
|
|
41806
|
+
var P = getDatasetCRS(dataset);
|
|
41807
|
+
// return isLatLngCRS(P) ? bearingDegrees : bearingDegrees2D;
|
|
41808
|
+
return isLatLngCRS(P) ? fastGeodeticBearingFunction : bearingDegrees2D;
|
|
41809
|
+
}
|
|
41810
|
+
|
|
41811
|
+
// get bearing in degrees from point ab to point cd
|
|
41812
|
+
function bearingDegrees(a, b, c, d) {
|
|
41813
|
+
return geom.bearing(a, b, c, d) * 180 / Math.PI;
|
|
41814
|
+
}
|
|
41815
|
+
|
|
41816
|
+
function bearingDegrees2D(a, b, c, d) {
|
|
41817
|
+
return geom.bearing2D(a, b, c, d) * 180 / Math.PI;
|
|
41818
|
+
}
|
|
41819
|
+
|
|
41820
|
+
var Geodesic = /*#__PURE__*/Object.freeze({
|
|
41821
|
+
__proto__: null,
|
|
41822
|
+
bearingDegrees: bearingDegrees,
|
|
41823
|
+
bearingDegrees2D: bearingDegrees2D,
|
|
41824
|
+
getBearingFunction: getBearingFunction,
|
|
41825
|
+
getFastGeodeticSegmentFunction: getFastGeodeticSegmentFunction,
|
|
41826
|
+
getGeodeticSegmentFunction: getGeodeticSegmentFunction,
|
|
41827
|
+
getInterpolationFunction: getInterpolationFunction,
|
|
41828
|
+
getPlanarSegmentEndpoint: getPlanarSegmentEndpoint,
|
|
41829
|
+
interpolatePoint2D: interpolatePoint2D,
|
|
41830
|
+
wrap: wrap
|
|
41831
|
+
});
|
|
41832
|
+
|
|
41833
|
+
function getJoinAngle(direction1, direction2) {
|
|
41834
|
+
var delta = direction2 - direction1;
|
|
41835
|
+
if (delta > 180) delta -= 360;
|
|
41836
|
+
if (delta < -180) delta += 360;
|
|
41837
|
+
return delta;
|
|
41838
|
+
}
|
|
41839
|
+
|
|
41840
|
+
// Ring-step distance from each vertex to the nearest concave vertex, in O(n).
|
|
41841
|
+
function minDistToConcaveOnRing(concave, n) {
|
|
41842
|
+
var dist = new Array(n);
|
|
41843
|
+
var d, i, j;
|
|
41844
|
+
for (j = 0; j < n; j++) dist[j] = n;
|
|
41845
|
+
d = n;
|
|
41846
|
+
for (i = 0; i < 2 * n; i++) {
|
|
41847
|
+
j = i % n;
|
|
41848
|
+
if (concave[j]) d = 0;
|
|
41849
|
+
else d++;
|
|
41850
|
+
if (d < dist[j]) dist[j] = d;
|
|
41851
|
+
}
|
|
41852
|
+
d = n;
|
|
41853
|
+
for (i = 2 * n - 1; i >= 0; i--) {
|
|
41854
|
+
j = i % n;
|
|
41855
|
+
if (concave[j]) d = 0;
|
|
41856
|
+
else d++;
|
|
41857
|
+
if (d < dist[j]) dist[j] = d;
|
|
41858
|
+
}
|
|
41859
|
+
return dist;
|
|
41860
|
+
}
|
|
41861
|
+
|
|
41862
|
+
// Pick the index of the edge (vk -> vk+1) whose midpoint makes the best offset
|
|
41863
|
+
// seam: an edge with two convex endpoints that lies as far as possible (in ring
|
|
41864
|
+
// steps) from any concave corner, breaking ties toward the longest such edge.
|
|
41865
|
+
function chooseSeamEdge(verts) {
|
|
41866
|
+
var n = verts.length - 1; // distinct vertices
|
|
41867
|
+
if (n < 3) return 0;
|
|
41868
|
+
var concave = [];
|
|
41869
|
+
var anyConcave = false;
|
|
41870
|
+
var bPrev = bearingDegrees2D(verts[n - 1][0], verts[n - 1][1], verts[0][0], verts[0][1]);
|
|
41871
|
+
var i, ni, b, ja;
|
|
41872
|
+
for (i = 0; i < n; i++) {
|
|
41873
|
+
ni = (i + 1) % n;
|
|
41874
|
+
b = bearingDegrees2D(verts[i][0], verts[i][1], verts[ni][0], verts[ni][1]);
|
|
41875
|
+
ja = getJoinAngle(bPrev, b);
|
|
41876
|
+
concave[i] = ja < 0;
|
|
41877
|
+
if (concave[i]) anyConcave = true;
|
|
41878
|
+
bPrev = b;
|
|
41879
|
+
}
|
|
41880
|
+
if (!anyConcave) return 0;
|
|
41881
|
+
var minDist = minDistToConcaveOnRing(concave, n);
|
|
41882
|
+
var bestK = -1, bestScore = -1, bestLen = -1;
|
|
41883
|
+
var fallbackK = 0, fallbackLen = -1;
|
|
41884
|
+
var k, len, score, a, c;
|
|
41885
|
+
for (k = 0; k < n; k++) {
|
|
41886
|
+
a = verts[k];
|
|
41887
|
+
c = verts[(k + 1) % n];
|
|
41888
|
+
len = Math.abs(a[0] - c[0]) + Math.abs(a[1] - c[1]);
|
|
41889
|
+
if (len > fallbackLen) { fallbackLen = len; fallbackK = k; }
|
|
41890
|
+
if (concave[k] || concave[(k + 1) % n]) continue;
|
|
41891
|
+
score = Math.min(minDist[k], minDist[(k + 1) % n]);
|
|
41892
|
+
if (score > bestScore || (score === bestScore && len > bestLen)) {
|
|
41893
|
+
bestScore = score; bestLen = len; bestK = k;
|
|
41894
|
+
}
|
|
41895
|
+
}
|
|
41896
|
+
return bestK >= 0 ? bestK : fallbackK;
|
|
41897
|
+
}
|
|
41898
|
+
|
|
41462
41899
|
// Removes small self-overlap "fold-back" loops from a constructed two-sided
|
|
41463
41900
|
// buffer outline ring before it is handed to the dissolve.
|
|
41464
41901
|
//
|
|
@@ -41487,9 +41924,8 @@ ${svg}
|
|
|
41487
41924
|
// of pocket orientation; otherwise it is left for the dissolve.
|
|
41488
41925
|
|
|
41489
41926
|
// Look-ahead window: how many ring segments ahead to test for a crossing. The
|
|
41490
|
-
// turn gate (not the window) is the safety criterion
|
|
41491
|
-
//
|
|
41492
|
-
// overshoot loop, which can include many round-join vertices at large radii.
|
|
41927
|
+
// turn gate (not the window) is the safety criterion in removeBufferRingLoops;
|
|
41928
|
+
// in the dip+coverage iterative path it only bounds cost (O(n * window)).
|
|
41493
41929
|
var BUFFER_LOOP_WINDOW = 30;
|
|
41494
41930
|
|
|
41495
41931
|
// Max source-path turn (degrees) a collapsible loop may span. Below this the
|
|
@@ -41498,6 +41934,27 @@ ${svg}
|
|
|
41498
41934
|
// for the dissolve.
|
|
41499
41935
|
var BUFFER_LOOP_MAX_TURN = 150;
|
|
41500
41936
|
|
|
41937
|
+
// Dip-tag iterative remover: a candidate collapse is allowed only if the region
|
|
41938
|
+
// it drops stays covered by the rest of the outline (collapseKeepsAreaCovered).
|
|
41939
|
+
// That per-collapse winding sweep is O(spanLen * ringLen), so it is skipped when
|
|
41940
|
+
// the dropped loop's own area is below this threshold -- such a loop cannot
|
|
41941
|
+
// create a clip larger than the threshold, so nothing "big" is missed. In ring
|
|
41942
|
+
// units (m^2 for planar; web-Mercator m^2 for lat/lng, whose scale factor >= 1
|
|
41943
|
+
// keeps this an upper bound on the real area, so latitude never hides a big clip).
|
|
41944
|
+
var BUFFER_LOOP_CHECK_MIN_AREA = 3e4;
|
|
41945
|
+
|
|
41946
|
+
// Hole-fill guard (dip+coverage path). A collapse is also refused if it would
|
|
41947
|
+
// swallow a winding-0 region (a real buffer hole or an open outer-wall notch)
|
|
41948
|
+
// larger than this fraction of the buffer disk (pi*dist^2, passed as fillFloor =
|
|
41949
|
+
// dist^2 * this). A genuine hole is a fixed fraction of the disk (the line wound
|
|
41950
|
+
// far enough to leave a region the radius can't reach), while a self-overlap fold
|
|
41951
|
+
// only pinches off a sliver orders of magnitude smaller relative to the radius --
|
|
41952
|
+
// so a disk-relative floor separates them with a wide margin where an absolute
|
|
41953
|
+
// area threshold does not (a 10km fold sliver and a 650m real hole have similar
|
|
41954
|
+
// absolute areas). Leans low (toward preserving holes): a false veto only leaves
|
|
41955
|
+
// a self-overlap for the dissolve, while a false pass deletes a real hole.
|
|
41956
|
+
var BUFFER_LOOP_FILL_AREA_FRAC = 5e-4;
|
|
41957
|
+
|
|
41501
41958
|
// ring: closed ring (first point repeated as last) of [x, y] points.
|
|
41502
41959
|
// srcPos (optional): source-vertex position parallel to ring; NaN for points
|
|
41503
41960
|
// (caps) with no single source segment.
|
|
@@ -41586,98 +42043,180 @@ ${svg}
|
|
|
41586
42043
|
return s;
|
|
41587
42044
|
}
|
|
41588
42045
|
|
|
41589
|
-
//
|
|
41590
|
-
//
|
|
41591
|
-
//
|
|
41592
|
-
|
|
41593
|
-
|
|
41594
|
-
|
|
41595
|
-
|
|
41596
|
-
|
|
41597
|
-
|
|
41598
|
-
|
|
41599
|
-
|
|
41600
|
-
|
|
41601
|
-
|
|
41602
|
-
|
|
41603
|
-
|
|
41604
|
-
|
|
41605
|
-
|
|
42046
|
+
// Absolute source turn spanned by a crossing candidate. The span covers the
|
|
42047
|
+
// anchor position `apos`, its successor `bpos`, and ring positions srcPos[lo..hi].
|
|
42048
|
+
// A pocket touching a cap (NaN position) is never treated as a covered overshoot.
|
|
42049
|
+
function getSpanTurn(apos, bpos, srcPos, lo, hi, turnPrefix) {
|
|
42050
|
+
if (apos !== apos || bpos !== bpos) return Infinity; // NaN cap
|
|
42051
|
+
var posLo = apos < bpos ? apos : bpos;
|
|
42052
|
+
var posHi = apos > bpos ? apos : bpos;
|
|
42053
|
+
for (var k = lo; k <= hi; k++) {
|
|
42054
|
+
var p = srcPos[k];
|
|
42055
|
+
if (p !== p) return Infinity; // NaN
|
|
42056
|
+
if (p < posLo) posLo = p;
|
|
42057
|
+
if (p > posHi) posHi = p;
|
|
42058
|
+
}
|
|
42059
|
+
return turnPrefix[posHi] - turnPrefix[posLo];
|
|
42060
|
+
}
|
|
42061
|
+
|
|
42062
|
+
// Multi-pass collapse gated by the offset ring's own cumulative absolute turn over the collapsed span, a
|
|
42063
|
+
// provenance-free proxy for the source-path turn gate (each offset join's turn
|
|
42064
|
+
// angle equals the source bend angle it derives from, and straight offset
|
|
42065
|
+
// segments contribute zero turn, so summing |turn| over a ring span with no cap
|
|
42066
|
+
// reconstructs the source stretch's cumulative turn). Iterating lets the
|
|
42067
|
+
// collapse of tight inner overshoot loops shorten the spans of the loops that
|
|
42068
|
+
// wrap them, so a wrapper that was too-large-turn on one pass can become a
|
|
42069
|
+
// single-bend covered overshoot on the next -- the multi-pass "peel simple
|
|
42070
|
+
// interior loops first" idea. maxTurn is the same gate constant as the source
|
|
42071
|
+
// turn gate; caps (180-degree arcs) exceed it and so are never collapsed.
|
|
42072
|
+
// @fillFloor (dip+coverage path): max winding-0 area a collapse may swallow; a
|
|
42073
|
+
// collapse filling a larger hole/notch is refused (see BUFFER_LOOP_FILL_AREA_FRAC).
|
|
42074
|
+
// Callers pass dist^2 * BUFFER_LOOP_FILL_AREA_FRAC; defaults to the uncovered
|
|
42075
|
+
// floor when omitted (unit tests without a buffer distance).
|
|
42076
|
+
function removeBufferRingLoopsIterative(ring, maxGap, srcPos, turnPrefix, maxTurn, dipTags, maxPasses, fillFloor) {
|
|
41606
42077
|
if (!ring || ring.length < 6) return ring;
|
|
42078
|
+
if (maxTurn === undefined) maxTurn = BUFFER_LOOP_MAX_TURN;
|
|
42079
|
+
if (!(maxPasses >= 1)) maxPasses = 12;
|
|
42080
|
+
if (!(fillFloor >= 0)) fillFloor = BUFFER_LOOP_CHECK_MIN_AREA;
|
|
42081
|
+
var work = ring, workPos = null, workTags = dipTags || null;
|
|
42082
|
+
for (var pass = 0; pass < maxPasses; pass++) {
|
|
42083
|
+
var res = collapseRingLoopsPass(work, maxGap, workPos, turnPrefix, maxTurn, workTags, fillFloor);
|
|
42084
|
+
if (res.ring.length === work.length) return res.ring; // stable
|
|
42085
|
+
work = res.ring; workPos = res.srcPos; workTags = res.dipTags;
|
|
42086
|
+
}
|
|
42087
|
+
return work;
|
|
42088
|
+
}
|
|
42089
|
+
|
|
42090
|
+
// One greedy forward-collapse pass (mirrors removeBufferRingLoops' compaction).
|
|
42091
|
+
// The span gate is, in order of preference:
|
|
42092
|
+
// - the reliable source-path turn (getSpanTurn) when srcPos+turnPrefix given;
|
|
42093
|
+
// - else the ring's cumulative turn with tagged fold cusps excluded, when
|
|
42094
|
+
// dipTags (construction-tagged reversed concave-join arcs) are supplied;
|
|
42095
|
+
// - else the same but with a geometric cusp threshold (no provenance).
|
|
42096
|
+
// Returns {ring, srcPos, dipTags} so the caller can iterate.
|
|
42097
|
+
function collapseRingLoopsPass(ring, maxGap, srcPos, turnPrefix, maxTurn, dipTags, fillFloor) {
|
|
42098
|
+
var gated = !!(srcPos && turnPrefix);
|
|
41607
42099
|
var n = ring.length - 1;
|
|
41608
|
-
|
|
41609
|
-
//
|
|
41610
|
-
//
|
|
41611
|
-
|
|
41612
|
-
var
|
|
41613
|
-
|
|
41614
|
-
|
|
41615
|
-
|
|
41616
|
-
|
|
41617
|
-
|
|
41618
|
-
|
|
41619
|
-
|
|
41620
|
-
|
|
41621
|
-
|
|
41622
|
-
|
|
41623
|
-
|
|
41624
|
-
|
|
41625
|
-
|
|
41626
|
-
|
|
41627
|
-
// Pass 2: collapse sub-loops wound the SAME way as the parent (covered
|
|
41628
|
-
// fold-back overlaps) unless they would eat a hole vertex.
|
|
42100
|
+
// The dip path defers entirely to the exact coverage check, so it needs neither
|
|
42101
|
+
// the tag-excluded turn gate nor its cumulative-turn prefix; the other two paths
|
|
42102
|
+
// (source-turn, geometric) have no coverage check and keep the turn gate.
|
|
42103
|
+
var coveragePath = !gated && !!dipTags;
|
|
42104
|
+
var ringTurn = (!gated && !dipTags) ? ringAbsTurnPrefix(ring, dipTags) : null;
|
|
42105
|
+
// Build the ring's y-band edge index once per pass so the coverage check's
|
|
42106
|
+
// per-loop edge lookup is local rather than O(ring length).
|
|
42107
|
+
var covIndex = coveragePath ? buildEdgeYIndex(ring, n) : null;
|
|
42108
|
+
// Opposite-wound hole protection. The coverage check only guards against
|
|
42109
|
+
// UNCOVERING area, so it cannot catch a collapse that FILLS a real winding-0
|
|
42110
|
+
// hole (filling adds coverage), and its area pre-filter treats any sub-floor
|
|
42111
|
+
// loop as safe to drop. A dropped sub-loop wound OPPOSITE to the parent ring
|
|
42112
|
+
// bounds such a hole, so refuse the collapse outright -- this keeps real holes
|
|
42113
|
+
// (annulus interiors, self-crossing-line pockets) the coverage check alone
|
|
42114
|
+
// misses, at the cost of only an O(span) signed-area test on the crossings the
|
|
42115
|
+
// collapse actually evaluates (far cheaper than the old O(n*maxGap) per-pass
|
|
42116
|
+
// pre-scan). A same-wound overshoot fold that happens to WRAP a hole is caught
|
|
42117
|
+
// instead by the fill guard in the coverage check (it measures filled area).
|
|
42118
|
+
var parentCCW = coveragePath ? (ringSignedArea(ring) >= 0) : false;
|
|
41629
42119
|
var out = [ring[0]];
|
|
42120
|
+
var outPos = gated ? [srcPos[0]] : null;
|
|
42121
|
+
var outTags = dipTags ? [dipTags[0]] : null;
|
|
41630
42122
|
var segmentEnd = ring[1];
|
|
42123
|
+
var segmentEndPos = gated ? srcPos[1] : 0;
|
|
42124
|
+
var segmentEndTag = dipTags ? dipTags[1] : 0;
|
|
41631
42125
|
var nextRingIndex = 2;
|
|
41632
42126
|
while (true) {
|
|
41633
42127
|
var anchor = out[out.length - 1];
|
|
42128
|
+
var anchorPos = gated ? outPos[outPos.length - 1] : 0;
|
|
41634
42129
|
var ax = anchor[0], ay = anchor[1], bx = segmentEnd[0], by = segmentEnd[1];
|
|
41635
42130
|
var lastScanIndex = Math.min(nextRingIndex + maxGap - 2, n - 2);
|
|
41636
42131
|
var crossing = null, crossingEndIndex = 0;
|
|
41637
42132
|
for (var s = nextRingIndex; s <= lastScanIndex; s++) {
|
|
41638
|
-
var
|
|
41639
|
-
var
|
|
41640
|
-
if (!
|
|
41641
|
-
|
|
41642
|
-
|
|
41643
|
-
|
|
41644
|
-
|
|
41645
|
-
if (
|
|
42133
|
+
var c = ring[s], d = ring[s + 1];
|
|
42134
|
+
var hit = segHit(ax, ay, bx, by, c[0], c[1], d[0], d[1]);
|
|
42135
|
+
if (!hit) continue;
|
|
42136
|
+
if (coveragePath) {
|
|
42137
|
+
// Never collapse a sub-loop wound opposite to the parent: it bounds a
|
|
42138
|
+
// real winding-0 hole the coverage check cannot see being filled (see the
|
|
42139
|
+
// hole-protection note above).
|
|
42140
|
+
if ((loopAreaSign(hit[0], hit[1], bx, by, ring, nextRingIndex, s) >= 0) !== parentCCW) {
|
|
42141
|
+
continue;
|
|
42142
|
+
}
|
|
42143
|
+
// No turn gate on the dip path: the exact coverage check is the arbiter.
|
|
42144
|
+
// It measures how much of the dropped region would become uncovered and
|
|
42145
|
+
// refuses collapses that would clip a significant lobe (see
|
|
42146
|
+
// docs/development/buffer-line-notes.md), catching real lobes and end caps
|
|
42147
|
+
// that a cheap turn/area/winding signal cannot separate from safe folds.
|
|
42148
|
+
// The turn gate was both leaving valid interior loops uncollapsed (tight
|
|
42149
|
+
// hairpins whose source turn exceeds the cap) and slowing the pipeline by
|
|
42150
|
+
// deferring their removal to the dissolve.
|
|
42151
|
+
if (!collapseKeepsAreaCovered(ring, n, hit, segmentEnd, nextRingIndex, s, covIndex, fillFloor)) {
|
|
42152
|
+
continue; // dropping this loop would uncover or fill real area -- leave it
|
|
42153
|
+
}
|
|
42154
|
+
} else {
|
|
42155
|
+
// Source-turn / geometric paths: no coverage check, so gate on cumulative
|
|
42156
|
+
// turn (caps and large real bends exceed maxTurn and are never collapsed).
|
|
42157
|
+
var spanTurn = gated ?
|
|
42158
|
+
getSpanTurn(anchorPos, segmentEndPos, srcPos, nextRingIndex, s + 1, turnPrefix) :
|
|
42159
|
+
(ringTurn[s] - ringTurn[nextRingIndex - 1]);
|
|
42160
|
+
if (spanTurn >= maxTurn) continue;
|
|
41646
42161
|
}
|
|
41647
|
-
|
|
41648
|
-
crossing = hit2;
|
|
42162
|
+
crossing = hit;
|
|
41649
42163
|
crossingEndIndex = s + 1;
|
|
41650
42164
|
break;
|
|
41651
42165
|
}
|
|
41652
42166
|
if (crossing) {
|
|
41653
42167
|
segmentEnd = crossing;
|
|
42168
|
+
if (gated) segmentEndPos = anchorPos;
|
|
42169
|
+
segmentEndTag = 0; // an offset crossing is not a dip vertex
|
|
41654
42170
|
nextRingIndex = crossingEndIndex;
|
|
41655
42171
|
continue;
|
|
41656
42172
|
}
|
|
41657
42173
|
out.push(segmentEnd);
|
|
42174
|
+
if (gated) outPos.push(segmentEndPos);
|
|
42175
|
+
if (dipTags) outTags.push(segmentEndTag);
|
|
41658
42176
|
if (nextRingIndex > n - 1) break;
|
|
41659
42177
|
segmentEnd = ring[nextRingIndex];
|
|
42178
|
+
if (gated) segmentEndPos = srcPos[nextRingIndex];
|
|
42179
|
+
if (dipTags) segmentEndTag = dipTags[nextRingIndex];
|
|
41660
42180
|
nextRingIndex++;
|
|
41661
42181
|
}
|
|
41662
|
-
if (out.length < 4) return ring;
|
|
42182
|
+
if (out.length < 4) return {ring: ring, srcPos: srcPos, dipTags: dipTags};
|
|
41663
42183
|
out.push(out[0].concat());
|
|
41664
|
-
|
|
41665
|
-
|
|
41666
|
-
|
|
41667
|
-
|
|
41668
|
-
|
|
41669
|
-
//
|
|
41670
|
-
|
|
41671
|
-
|
|
41672
|
-
|
|
41673
|
-
|
|
41674
|
-
|
|
41675
|
-
|
|
41676
|
-
|
|
41677
|
-
|
|
41678
|
-
|
|
41679
|
-
|
|
41680
|
-
|
|
42184
|
+
if (gated) outPos.push(outPos[0]);
|
|
42185
|
+
if (dipTags) outTags.push(outTags[0]);
|
|
42186
|
+
return {ring: out, srcPos: gated ? outPos : null, dipTags: dipTags ? outTags : null};
|
|
42187
|
+
}
|
|
42188
|
+
|
|
42189
|
+
// Cumulative absolute turn (degrees) indexed by ring vertex, EXCLUDING fold
|
|
42190
|
+
// cusps. prefix[k] = sum of |turn| at ring vertices 1..k-1, skipping the
|
|
42191
|
+
// near-180-degree cusps where the offset doubles back on itself (an artifact of
|
|
42192
|
+
// the self-crossing offset, with no corresponding source bend). A normal offset
|
|
42193
|
+
// join's turn equals its source bend angle, so the remaining sum reconstructs
|
|
42194
|
+
// the source stretch's cumulative turn -- the reliable loop-removal signal --
|
|
42195
|
+
// without source provenance.
|
|
42196
|
+
//
|
|
42197
|
+
// A cusp is identified by construction tag when dipTags are supplied (a vertex
|
|
42198
|
+
// on a reversed concave-join arc AND turning sharply); otherwise by a purely
|
|
42199
|
+
// geometric turn threshold. Tag-gating is the point: a real sharp bend (e.g. a
|
|
42200
|
+
// fjord mouth, emitted as a miter, never tagged) keeps its turn and so is never
|
|
42201
|
+
// mistaken for a fold, which is what makes the geometric-only threshold
|
|
42202
|
+
// over-collapse sharp coastlines.
|
|
42203
|
+
var CUSP_TURN = 135;
|
|
42204
|
+
function ringAbsTurnPrefix(ring, dipTags) {
|
|
42205
|
+
var n = ring.length - 1;
|
|
42206
|
+
var prefix = new Float64Array(n + 1);
|
|
42207
|
+
var RAD = 180 / Math.PI;
|
|
42208
|
+
for (var k = 1; k < n; k++) {
|
|
42209
|
+
var a = ring[k - 1], b = ring[k], c = ring[k + 1];
|
|
42210
|
+
var e1x = b[0] - a[0], e1y = b[1] - a[1];
|
|
42211
|
+
var e2x = c[0] - b[0], e2y = c[1] - b[1];
|
|
42212
|
+
var cross = e1x * e2y - e1y * e2x;
|
|
42213
|
+
var dot = e1x * e2x + e1y * e2y;
|
|
42214
|
+
var ang = Math.abs(Math.atan2(cross, dot)) * RAD;
|
|
42215
|
+
var isCusp = ang > CUSP_TURN && (dipTags ? dipTags[k] : true);
|
|
42216
|
+
prefix[k] = prefix[k - 1] + (isCusp ? 0 : ang);
|
|
42217
|
+
}
|
|
42218
|
+
prefix[n] = prefix[n - 1];
|
|
42219
|
+
return prefix;
|
|
41681
42220
|
}
|
|
41682
42221
|
|
|
41683
42222
|
// Fast strict-interior segment crossing; returns [x, y] or null. Buffer join
|
|
@@ -41699,6 +42238,213 @@ ${svg}
|
|
|
41699
42238
|
return [ax + t * abx, ay + t * aby];
|
|
41700
42239
|
}
|
|
41701
42240
|
|
|
42241
|
+
// Exact per-collapse coverage test. A collapse drops the loop
|
|
42242
|
+
// L = [X, b, ring[nextRingIndex..s], X] and replaces anchor->b..c->d with
|
|
42243
|
+
// anchor->X->d; it is area-neutral iff the dropped region stays covered by the
|
|
42244
|
+
// rest of the outline. The dropped span is mostly reversed-arc "dip" folds
|
|
42245
|
+
// (double-covered self-overlap, safe to drop), but it may also swallow a genuine
|
|
42246
|
+
// offset lobe (single-covered real boundary). We measure exactly how much of the
|
|
42247
|
+
// dropped region would become UNCOVERED and refuse the collapse when that exceeds
|
|
42248
|
+
// a "big clip" threshold; small residual clips are left for the dissolve.
|
|
42249
|
+
//
|
|
42250
|
+
// Coverage is decided by winding against the stable pass-input ring (not the
|
|
42251
|
+
// mid-pass output), so it is independent of collapse order: a point in the loop
|
|
42252
|
+
// is still covered afterward iff windingFullRing(point) - windingLoop(point) != 0.
|
|
42253
|
+
// For a two-sided line outline the main body always covers the buffer interior,
|
|
42254
|
+
// so a fold has |winding| >= 2 (body + fold) and survives losing one layer, while
|
|
42255
|
+
// real boundary has |winding| == 1 and drops to 0. The uncovered area is
|
|
42256
|
+
// integrated with a horizontal scanline (below) rather than point-sampled, so an
|
|
42257
|
+
// interior uncovered pocket cannot be missed.
|
|
42258
|
+
//
|
|
42259
|
+
// An area pre-filter keeps this off the hot path: a loop whose own area is below
|
|
42260
|
+
// the threshold cannot produce a clip above it, so only large loops are swept.
|
|
42261
|
+
// The threshold is in ring units; under web Mercator the scale factor is >= 1
|
|
42262
|
+
// everywhere, so it is an upper bound on the real m^2 area and no genuinely large
|
|
42263
|
+
// clip is skipped regardless of latitude.
|
|
42264
|
+
function collapseKeepsAreaCovered(ring, n, X, b, nextRingIndex, s, index, fillFloor) {
|
|
42265
|
+
// Area pre-filter keeps the scanline off the hot path: a loop can neither clip
|
|
42266
|
+
// nor fill more than its own area, so it is safe to skip whenever the loop is
|
|
42267
|
+
// below BOTH thresholds. (Filling a winding-0 region never reaches the uncovered
|
|
42268
|
+
// floor, so the fill floor must join the skip test -- otherwise small folds that
|
|
42269
|
+
// could fill a small hole would be skipped, or every collapse would be scanned.)
|
|
42270
|
+
var floor = fillFloor < BUFFER_LOOP_CHECK_MIN_AREA ? fillFloor : BUFFER_LOOP_CHECK_MIN_AREA;
|
|
42271
|
+
var u = collapseUncoveredArea(ring, n, X, b, nextRingIndex, s, index, floor);
|
|
42272
|
+
if (u < 0) return true; // loop below both floors -- too small to clip or fill
|
|
42273
|
+
// Reject if the collapse would clip a real lobe (uncovered) OR swallow a real
|
|
42274
|
+
// hole/notch (filled). The uncovered floor is an absolute "big clip" bound; the
|
|
42275
|
+
// fill floor scales with the buffer disk (dist^2) because a real hole's area is
|
|
42276
|
+
// a fixed fraction of the disk while a fold sliver is orders of magnitude
|
|
42277
|
+
// smaller relative to the radius (see BUFFER_LOOP_FILL_AREA_FRAC).
|
|
42278
|
+
return u < BUFFER_LOOP_CHECK_MIN_AREA && _lastFillArea < fillFloor;
|
|
42279
|
+
}
|
|
42280
|
+
|
|
42281
|
+
// Returns the area of the dropped region a collapse would leave uncovered, or -1
|
|
42282
|
+
// when the loop's own area is below `floor` (too small to matter -- scanline
|
|
42283
|
+
// skipped). See collapseKeepsAreaCovered for the winding rationale.
|
|
42284
|
+
function collapseUncoveredArea(ring, n, X, b, nextRingIndex, s, index, floor) {
|
|
42285
|
+
var i, loopLen = s - nextRingIndex + 3; // X, b, ring[next..s]
|
|
42286
|
+
// Loop area (shoelace over X, b, ring[next..s]) and bounding box.
|
|
42287
|
+
var area2 = 0, px = X[0], py = X[1];
|
|
42288
|
+
var minx = X[0], maxx = X[0], miny = X[1], maxy = X[1];
|
|
42289
|
+
function bbox(qx, qy) {
|
|
42290
|
+
if (qx < minx) minx = qx; else if (qx > maxx) maxx = qx;
|
|
42291
|
+
if (qy < miny) miny = qy; else if (qy > maxy) maxy = qy;
|
|
42292
|
+
}
|
|
42293
|
+
bbox(b[0], b[1]);
|
|
42294
|
+
area2 += px * b[1] - b[0] * py; px = b[0]; py = b[1];
|
|
42295
|
+
for (i = nextRingIndex; i <= s; i++) {
|
|
42296
|
+
area2 += px * ring[i][1] - ring[i][0] * py; px = ring[i][0]; py = ring[i][1];
|
|
42297
|
+
bbox(ring[i][0], ring[i][1]);
|
|
42298
|
+
}
|
|
42299
|
+
area2 += px * X[1] - X[0] * py;
|
|
42300
|
+
if (Math.abs(area2) / 2 < floor) return -1; // too small to matter
|
|
42301
|
+
// Collect the ring edges whose y-range meets the loop's band (the only ones
|
|
42302
|
+
// that can cross any scanline); reuse module scratch to avoid per-call garbage.
|
|
42303
|
+
var scr = coverageScratch(n + loopLen);
|
|
42304
|
+
var lx0 = scr.lx0, ly0 = scr.ly0, lx1 = scr.lx1, ly1 = scr.ly1;
|
|
42305
|
+
var band = scr.band, le = 0;
|
|
42306
|
+
var gen = ++index.gen, stamp = index.stamp, start = index.start, edges = index.edges;
|
|
42307
|
+
var kb, klo = index.binOf(miny), khi = index.binOf(maxy);
|
|
42308
|
+
for (kb = klo; kb <= khi; kb++) {
|
|
42309
|
+
for (var p = start[kb]; p < start[kb + 1]; p++) {
|
|
42310
|
+
var ei = edges[p];
|
|
42311
|
+
if (stamp[ei] === gen) continue;
|
|
42312
|
+
stamp[ei] = gen;
|
|
42313
|
+
var ay = ring[ei][1], by = ring[ei + 1][1];
|
|
42314
|
+
if (ay < miny && by < miny || ay > maxy && by > maxy) continue;
|
|
42315
|
+
if (ring[ei][0] > maxx && ring[ei + 1][0] > maxx) continue; // entirely right of loop
|
|
42316
|
+
band[le++] = ei;
|
|
42317
|
+
}
|
|
42318
|
+
}
|
|
42319
|
+
// Loop edges (X->b, b->ring[next..s], ring[s]->X).
|
|
42320
|
+
var lc = 0;
|
|
42321
|
+
lx0[lc] = X[0]; ly0[lc] = X[1]; lx1[lc] = b[0]; ly1[lc] = b[1]; lc++;
|
|
42322
|
+
lx0[lc] = b[0]; ly0[lc] = b[1]; lx1[lc] = ring[nextRingIndex][0]; ly1[lc] = ring[nextRingIndex][1]; lc++;
|
|
42323
|
+
for (i = nextRingIndex; i < s; i++) {
|
|
42324
|
+
lx0[lc] = ring[i][0]; ly0[lc] = ring[i][1]; lx1[lc] = ring[i + 1][0]; ly1[lc] = ring[i + 1][1]; lc++;
|
|
42325
|
+
}
|
|
42326
|
+
lx0[lc] = ring[s][0]; ly0[lc] = ring[s][1]; lx1[lc] = X[0]; ly1[lc] = X[1]; lc++;
|
|
42327
|
+
return loopUncoveredArea(ring, band, le, lx0, ly0, lx1, ly1, lc, minx, maxx, miny, maxy, scr);
|
|
42328
|
+
}
|
|
42329
|
+
|
|
42330
|
+
// y-band index of ring edges: bins the ring's y-range so a scanline query at
|
|
42331
|
+
// [miny, maxy] returns only edges reaching that band instead of scanning all n.
|
|
42332
|
+
// start[k]..start[k+1] are indices into `edges` for bin k; `stamp`/`gen` dedup an
|
|
42333
|
+
// edge that spans several bins during a single query.
|
|
42334
|
+
function buildEdgeYIndex(ring, n) {
|
|
42335
|
+
var yMin = Infinity, yMax = -Infinity, i;
|
|
42336
|
+
for (i = 0; i < n; i++) { var y = ring[i][1]; if (y < yMin) yMin = y; if (y > yMax) yMax = y; }
|
|
42337
|
+
var B = n < 32 ? 1 : Math.min(4096, Math.max(16, n >> 2));
|
|
42338
|
+
var binH = (yMax - yMin) / B || 1;
|
|
42339
|
+
var binOf = function (y) { var k = ((y - yMin) / binH) | 0; return k < 0 ? 0 : (k >= B ? B - 1 : k); };
|
|
42340
|
+
var start = new Int32Array(B + 1);
|
|
42341
|
+
for (i = 0; i < n; i++) {
|
|
42342
|
+
var ya = ring[i][1], yb = ring[i + 1][1];
|
|
42343
|
+
var lo = binOf(ya < yb ? ya : yb), hi = binOf(ya < yb ? yb : ya);
|
|
42344
|
+
for (var k = lo; k <= hi; k++) start[k + 1]++;
|
|
42345
|
+
}
|
|
42346
|
+
for (i = 0; i < B; i++) start[i + 1] += start[i];
|
|
42347
|
+
var edges = new Int32Array(start[B]);
|
|
42348
|
+
var cur = start.slice(0, B);
|
|
42349
|
+
for (i = 0; i < n; i++) {
|
|
42350
|
+
var a2 = ring[i][1], b2 = ring[i + 1][1];
|
|
42351
|
+
var lo2 = binOf(a2 < b2 ? a2 : b2), hi2 = binOf(a2 < b2 ? b2 : a2);
|
|
42352
|
+
for (var k2 = lo2; k2 <= hi2; k2++) edges[cur[k2]++] = i;
|
|
42353
|
+
}
|
|
42354
|
+
return { binOf: binOf, start: start, edges: edges, stamp: new Int32Array(n), gen: 0 };
|
|
42355
|
+
}
|
|
42356
|
+
|
|
42357
|
+
var _covScratch = null;
|
|
42358
|
+
function coverageScratch(cap) {
|
|
42359
|
+
if (!_covScratch || _covScratch.cap < cap) {
|
|
42360
|
+
_covScratch = {
|
|
42361
|
+
cap: cap,
|
|
42362
|
+
lx0: new Float64Array(cap), ly0: new Float64Array(cap),
|
|
42363
|
+
lx1: new Float64Array(cap), ly1: new Float64Array(cap),
|
|
42364
|
+
band: new Int32Array(cap),
|
|
42365
|
+
xs: new Float64Array(cap), df: new Int8Array(cap),
|
|
42366
|
+
dl: new Int8Array(cap), order: new Int32Array(cap)
|
|
42367
|
+
};
|
|
42368
|
+
}
|
|
42369
|
+
return _covScratch;
|
|
42370
|
+
}
|
|
42371
|
+
|
|
42372
|
+
// Sweep the removed loop L and measure two area quantities the collapse would
|
|
42373
|
+
// change, both integrated with horizontal scanlines across L's bounding box (at
|
|
42374
|
+
// each scanline the winding of the full ring and of the loop are step functions
|
|
42375
|
+
// of x, reconstructed from the sorted edge crossings). For a point inside L
|
|
42376
|
+
// (windingLoop != 0), the winding after the collapse is windingFull - windingLoop:
|
|
42377
|
+
// - UNCOVERED: was covered (windingFull == windingLoop, so it drops to 0). This
|
|
42378
|
+
// is the "big clip" a collapse of a real single-covered lobe would make.
|
|
42379
|
+
// - FILLED: was a winding-0 hole/exterior (windingFull == 0, so it flips to
|
|
42380
|
+
// covered). This is a real buffer hole (or an open outer-wall notch) the
|
|
42381
|
+
// collapse would swallow -- undetectable by the uncovered measure alone,
|
|
42382
|
+
// since filling adds coverage rather than removing it.
|
|
42383
|
+
// Returns the uncovered area and writes the filled area to _lastFillArea.
|
|
42384
|
+
// `band` lists the indices of ring edges that can reach the loop's y-band.
|
|
42385
|
+
var _lastFillArea = 0;
|
|
42386
|
+
function loopUncoveredArea(ring, band, be, lx0, ly0, lx1, ly1, le, minx, maxx, miny, maxy, scr) {
|
|
42387
|
+
var h = maxy - miny;
|
|
42388
|
+
_lastFillArea = 0;
|
|
42389
|
+
if (h <= 0 || maxx <= minx) return 0;
|
|
42390
|
+
var target = Math.sqrt(BUFFER_LOOP_CHECK_MIN_AREA) / 4;
|
|
42391
|
+
var rows = Math.round(h / (target > 0 ? target : h));
|
|
42392
|
+
if (rows < 8) rows = 8; else if (rows > 40) rows = 40;
|
|
42393
|
+
var dy = h / rows;
|
|
42394
|
+
var xs = scr.xs, df = scr.df, dl = scr.dl, order = scr.order;
|
|
42395
|
+
var total = 0, fill = 0, r, k, i;
|
|
42396
|
+
for (r = 0; r < rows; r++) {
|
|
42397
|
+
var y = miny + (r + 0.5) * dy;
|
|
42398
|
+
// Winding just left of the loop's x-range (base), plus the crossings that
|
|
42399
|
+
// fall inside [minx, maxx] (only these need sorting). Crossings right of the
|
|
42400
|
+
// loop are irrelevant to intervals within the range and are dropped.
|
|
42401
|
+
var baseWf = 0, baseWl = 0, m = 0;
|
|
42402
|
+
for (k = 0; k < be; k++) {
|
|
42403
|
+
i = band[k];
|
|
42404
|
+
var y1 = ring[i][1], y2 = ring[i + 1][1];
|
|
42405
|
+
if ((y1 <= y) === (y2 <= y)) continue;
|
|
42406
|
+
var x = ring[i][0] + (y - y1) / (y2 - y1) * (ring[i + 1][0] - ring[i][0]);
|
|
42407
|
+
var sgnF = y2 > y1 ? 1 : -1;
|
|
42408
|
+
if (x <= minx) baseWf += sgnF;
|
|
42409
|
+
else if (x < maxx) { xs[m] = x; df[m] = sgnF; dl[m] = 0; order[m] = m; m++; }
|
|
42410
|
+
}
|
|
42411
|
+
for (i = 0; i < le; i++) {
|
|
42412
|
+
var ya = ly0[i], yb = ly1[i];
|
|
42413
|
+
if ((ya <= y) === (yb <= y)) continue;
|
|
42414
|
+
var xl = lx0[i] + (y - ya) / (yb - ya) * (lx1[i] - lx0[i]);
|
|
42415
|
+
var sl = yb > ya ? 1 : -1;
|
|
42416
|
+
if (xl <= minx) { baseWl += sl; }
|
|
42417
|
+
else if (xl < maxx) { xs[m] = xl; df[m] = 0; dl[m] = sl; order[m] = m; m++; }
|
|
42418
|
+
}
|
|
42419
|
+
sortByX(order, xs, m);
|
|
42420
|
+
var wf = baseWf, wl = baseWl, prevx = minx;
|
|
42421
|
+
for (i = 0; i < m; i++) {
|
|
42422
|
+
var o = order[i], xk = xs[o];
|
|
42423
|
+
if (wl !== 0 && xk > prevx) {
|
|
42424
|
+
if (wf - wl === 0) total += xk - prevx;
|
|
42425
|
+
else if (wf === 0) fill += xk - prevx;
|
|
42426
|
+
}
|
|
42427
|
+
wf += df[o]; wl += dl[o]; prevx = xk;
|
|
42428
|
+
}
|
|
42429
|
+
if (wl !== 0 && maxx > prevx) {
|
|
42430
|
+
if (wf - wl === 0) total += maxx - prevx;
|
|
42431
|
+
else if (wf === 0) fill += maxx - prevx;
|
|
42432
|
+
}
|
|
42433
|
+
}
|
|
42434
|
+
_lastFillArea = fill * dy;
|
|
42435
|
+
return total * dy;
|
|
42436
|
+
}
|
|
42437
|
+
|
|
42438
|
+
// Insertion sort of index array `order[0..m)` by xs[order[k]] ascending. m is
|
|
42439
|
+
// small per scanline (edges straddling the row), so this beats a comparator sort.
|
|
42440
|
+
function sortByX(order, xs, m) {
|
|
42441
|
+
for (var i = 1; i < m; i++) {
|
|
42442
|
+
var oi = order[i], xi = xs[oi], j = i - 1;
|
|
42443
|
+
while (j >= 0 && xs[order[j]] > xi) { order[j + 1] = order[j]; j--; }
|
|
42444
|
+
order[j + 1] = oi;
|
|
42445
|
+
}
|
|
42446
|
+
}
|
|
42447
|
+
|
|
41702
42448
|
var DouglasPeucker = {};
|
|
41703
42449
|
|
|
41704
42450
|
DouglasPeucker.metricSq3D = geom.pointSegDistSq3D;
|
|
@@ -41781,7 +42527,7 @@ ${svg}
|
|
|
41781
42527
|
var T = 90 - e;
|
|
41782
42528
|
var L = -180 + e;
|
|
41783
42529
|
var B$1 = -90 + e;
|
|
41784
|
-
var R$
|
|
42530
|
+
var R$1 = 180 - e;
|
|
41785
42531
|
|
|
41786
42532
|
function lastEl(arr) {
|
|
41787
42533
|
return arr[arr.length - 1];
|
|
@@ -41798,7 +42544,7 @@ ${svg}
|
|
|
41798
42544
|
// remove likely rounding errors
|
|
41799
42545
|
function snapToEdge(p) {
|
|
41800
42546
|
if (p[0] <= L) p[0] = -180;
|
|
41801
|
-
if (p[0] >= R$
|
|
42547
|
+
if (p[0] >= R$1) p[0] = 180;
|
|
41802
42548
|
if (p[1] <= B$1) p[1] = -90;
|
|
41803
42549
|
if (p[1] >= T) p[1] = 90;
|
|
41804
42550
|
}
|
|
@@ -41826,11 +42572,11 @@ ${svg}
|
|
|
41826
42572
|
// TODO: handle segments between pole and non-edge point
|
|
41827
42573
|
// (these shoudn't exist in a properly clipped path)
|
|
41828
42574
|
return (onPole(a) || onPole(b)) ||
|
|
41829
|
-
a[0] <= L && b[0] <= L || a[0] >= R$
|
|
42575
|
+
a[0] <= L && b[0] <= L || a[0] >= R$1 && b[0] >= R$1;
|
|
41830
42576
|
}
|
|
41831
42577
|
|
|
41832
42578
|
function isEdgePoint(p) {
|
|
41833
|
-
return p[1] <= B$1 || p[1] >= T || p[0] <= L || p[0] >= R$
|
|
42579
|
+
return p[1] <= B$1 || p[1] >= T || p[0] <= L || p[0] >= R$1;
|
|
41834
42580
|
}
|
|
41835
42581
|
|
|
41836
42582
|
// Remove segments that belong solely to cut points
|
|
@@ -42095,129 +42841,6 @@ ${svg}
|
|
|
42095
42841
|
return (dx2 * p1[1] + dx1 * p2[1]) / (dx1 + dx2);
|
|
42096
42842
|
}
|
|
42097
42843
|
|
|
42098
|
-
var R$1 = WGS84.SEMIMAJOR_AXIS;
|
|
42099
|
-
|
|
42100
|
-
// GeographicLib docs: https://geographiclib.sourceforge.io/html/js/
|
|
42101
|
-
// https://geographiclib.sourceforge.io/html/js/module-GeographicLib_Geodesic.Geodesic.html
|
|
42102
|
-
// https://geographiclib.sourceforge.io/html/js/tutorial-2-interface.html
|
|
42103
|
-
function getGeodesic(P) {
|
|
42104
|
-
if (!isLatLngCRS(P)) error('Expected an unprojected CRS');
|
|
42105
|
-
var f = P.es / (1 + Math.sqrt(P.one_es));
|
|
42106
|
-
// var GeographicLib = require('mproj').internal.GeographicLib;
|
|
42107
|
-
var GeographicLib = require$1('geographiclib-geodesic');
|
|
42108
|
-
// return new GeographicLib.Geodesic.Geodesic(P.a, 0)
|
|
42109
|
-
return new GeographicLib.Geodesic.Geodesic(P.a, f);
|
|
42110
|
-
}
|
|
42111
|
-
|
|
42112
|
-
function interpolatePoint2D(ax, ay, bx, by, k) {
|
|
42113
|
-
var j = 1 - k;
|
|
42114
|
-
return [ax * j + bx * k, ay * j + by * k];
|
|
42115
|
-
}
|
|
42116
|
-
|
|
42117
|
-
function getInterpolationFunction(P) {
|
|
42118
|
-
var spherical = P && isLatLngCRS(P);
|
|
42119
|
-
if (!spherical) return interpolatePoint2D;
|
|
42120
|
-
var geod = getGeodesic(P);
|
|
42121
|
-
return function(lng, lat, lng2, lat2, k) {
|
|
42122
|
-
var r = geod.Inverse(lat, lng, lat2, lng2);
|
|
42123
|
-
var dist = r.s12 * k;
|
|
42124
|
-
var r2 = geod.Direct(lat, lng, r.azi1, dist);
|
|
42125
|
-
return [r2.lon2, r2.lat2];
|
|
42126
|
-
};
|
|
42127
|
-
}
|
|
42128
|
-
|
|
42129
|
-
function getPlanarSegmentEndpoint(x, y, bearing, meterDist) {
|
|
42130
|
-
var rad = bearing / 180 * Math.PI;
|
|
42131
|
-
var dx = Math.sin(rad) * meterDist;
|
|
42132
|
-
var dy = Math.cos(rad) * meterDist;
|
|
42133
|
-
return [x + dx, y + dy];
|
|
42134
|
-
}
|
|
42135
|
-
|
|
42136
|
-
// source: https://github.com/mapbox/cheap-ruler/blob/master/index.js
|
|
42137
|
-
function fastGeodeticSegmentFunction(lng, lat, bearing, meterDist) {
|
|
42138
|
-
var D2R = Math.PI / 180;
|
|
42139
|
-
var cos = Math.cos(lat * D2R);
|
|
42140
|
-
var cos2 = 2 * cos * cos - 1;
|
|
42141
|
-
var cos3 = 2 * cos * cos2 - cos;
|
|
42142
|
-
var cos4 = 2 * cos * cos3 - cos2;
|
|
42143
|
-
var cos5 = 2 * cos * cos4 - cos3;
|
|
42144
|
-
var kx = (111.41513 * cos - 0.09455 * cos3 + 0.00012 * cos5) * 1000;
|
|
42145
|
-
var ky = (111.13209 - 0.56605 * cos2 + 0.0012 * cos4) * 1000;
|
|
42146
|
-
var bearingRad = bearing * D2R;
|
|
42147
|
-
var lat2 = lat + Math.cos(bearingRad) * meterDist / ky;
|
|
42148
|
-
var lng2 = lng + Math.sin(bearingRad) * meterDist / kx;
|
|
42149
|
-
return [lng2, lat2];
|
|
42150
|
-
}
|
|
42151
|
-
|
|
42152
|
-
|
|
42153
|
-
function wrap(deg) {
|
|
42154
|
-
while (deg < -180) deg += 360;
|
|
42155
|
-
while (deg > 180) deg -= 360;
|
|
42156
|
-
return deg;
|
|
42157
|
-
}
|
|
42158
|
-
|
|
42159
|
-
function fastGeodeticBearingFunction(lng1, lat1, lng2, lat2) {
|
|
42160
|
-
var D2R = Math.PI / 180;
|
|
42161
|
-
var f = 1 / 298.257223563;
|
|
42162
|
-
var e2 = f * (2 - f);
|
|
42163
|
-
var m = R$1 * D2R;
|
|
42164
|
-
var coslat = Math.cos(lat1 * D2R);
|
|
42165
|
-
var w2 = 1 / (1 - e2 * (1 - coslat * coslat));
|
|
42166
|
-
var w = Math.sqrt(w2);
|
|
42167
|
-
var kx = m * w * coslat;
|
|
42168
|
-
var ky = m * w * w2 * (1 - e2);
|
|
42169
|
-
var dx = wrap(lng2 - lng1) * kx;
|
|
42170
|
-
var dy = (lat2 - lat1) * ky;
|
|
42171
|
-
return Math.atan2(dx, dy) / D2R;
|
|
42172
|
-
}
|
|
42173
|
-
|
|
42174
|
-
function getGeodeticSegmentFunction(P) {
|
|
42175
|
-
if (!isLatLngCRS(P)) {
|
|
42176
|
-
return getPlanarSegmentEndpoint;
|
|
42177
|
-
}
|
|
42178
|
-
var g = getGeodesic(P);
|
|
42179
|
-
return function(lng, lat, bearing, meterDist) {
|
|
42180
|
-
var o = g.Direct(lat, lng, bearing, meterDist);
|
|
42181
|
-
var p = [o.lon2, o.lat2];
|
|
42182
|
-
return p;
|
|
42183
|
-
};
|
|
42184
|
-
}
|
|
42185
|
-
|
|
42186
|
-
function getFastGeodeticSegmentFunction(P) {
|
|
42187
|
-
// CAREFUL: this function has higher error at very large distances and at the poles
|
|
42188
|
-
// also, it wouldn't work for other planets than Earth
|
|
42189
|
-
return isLatLngCRS(P) ? fastGeodeticSegmentFunction : getPlanarSegmentEndpoint;
|
|
42190
|
-
}
|
|
42191
|
-
|
|
42192
|
-
// return function to calculate bearing of a segment in degrees
|
|
42193
|
-
function getBearingFunction(dataset) {
|
|
42194
|
-
var P = getDatasetCRS(dataset);
|
|
42195
|
-
// return isLatLngCRS(P) ? bearingDegrees : bearingDegrees2D;
|
|
42196
|
-
return isLatLngCRS(P) ? fastGeodeticBearingFunction : bearingDegrees2D;
|
|
42197
|
-
}
|
|
42198
|
-
|
|
42199
|
-
// get bearing in degrees from point ab to point cd
|
|
42200
|
-
function bearingDegrees(a, b, c, d) {
|
|
42201
|
-
return geom.bearing(a, b, c, d) * 180 / Math.PI;
|
|
42202
|
-
}
|
|
42203
|
-
|
|
42204
|
-
function bearingDegrees2D(a, b, c, d) {
|
|
42205
|
-
return geom.bearing2D(a, b, c, d) * 180 / Math.PI;
|
|
42206
|
-
}
|
|
42207
|
-
|
|
42208
|
-
var Geodesic = /*#__PURE__*/Object.freeze({
|
|
42209
|
-
__proto__: null,
|
|
42210
|
-
bearingDegrees: bearingDegrees,
|
|
42211
|
-
bearingDegrees2D: bearingDegrees2D,
|
|
42212
|
-
getBearingFunction: getBearingFunction,
|
|
42213
|
-
getFastGeodeticSegmentFunction: getFastGeodeticSegmentFunction,
|
|
42214
|
-
getGeodeticSegmentFunction: getGeodeticSegmentFunction,
|
|
42215
|
-
getInterpolationFunction: getInterpolationFunction,
|
|
42216
|
-
getPlanarSegmentEndpoint: getPlanarSegmentEndpoint,
|
|
42217
|
-
interpolatePoint2D: interpolatePoint2D,
|
|
42218
|
-
wrap: wrap
|
|
42219
|
-
});
|
|
42220
|
-
|
|
42221
42844
|
var R = WGS84.SEMIMAJOR_AXIS;
|
|
42222
42845
|
var D2R = Math.PI / 180;
|
|
42223
42846
|
var R2D = 180 / Math.PI;
|
|
@@ -42340,6 +42963,19 @@ ${svg}
|
|
|
42340
42963
|
|
|
42341
42964
|
function getOffsetFunction(crs, opts) {
|
|
42342
42965
|
if (!isLatLngCRS(crs)) {
|
|
42966
|
+
if (opts && opts.geodesic2) {
|
|
42967
|
+
// Experimental geodesic buffering for projected data that stays entirely
|
|
42968
|
+
// in the source projected plane (no web-Mercator round-trip), so it has no
|
|
42969
|
+
// pole singularity or antimeridian wrap. Each offset is taken in the
|
|
42970
|
+
// ordinary projected (cartesian) direction, but its magnitude is corrected
|
|
42971
|
+
// so the endpoint lands at a true ground distance, measured with a
|
|
42972
|
+
// first-party spherical great-circle formula (the same sphere model the
|
|
42973
|
+
// lat-long buffer already uses). Falls back to planar if the CRS can't be
|
|
42974
|
+
// inverted to measure ground distance.
|
|
42975
|
+
var fn = makeScaleCorrectedOffset(crs);
|
|
42976
|
+
if (fn) return fn;
|
|
42977
|
+
message('[buffer] Ignoring "geodesic2": the dataset CRS is unknown or not invertible; using planar offsets.');
|
|
42978
|
+
}
|
|
42343
42979
|
return getPlanarSegmentEndpoint;
|
|
42344
42980
|
}
|
|
42345
42981
|
// Offset each point by a true geodesic distance, working directly in spherical
|
|
@@ -42360,6 +42996,90 @@ ${svg}
|
|
|
42360
42996
|
return opts && opts.polar ? clampPolar(offset) : offset;
|
|
42361
42997
|
}
|
|
42362
42998
|
|
|
42999
|
+
// Convergence target for the scale-corrected offset: stop when the great-circle
|
|
43000
|
+
// distance reached is within SCALE_OFFSET_TOL (relative) or SCALE_OFFSET_ABS
|
|
43001
|
+
// (absolute, meters) of the requested ground distance.
|
|
43002
|
+
//
|
|
43003
|
+
// The iteration is a linear fixed point (m *= d/g), so the residual shrinks by a
|
|
43004
|
+
// roughly constant factor per step: a tighter tolerance costs extra tail
|
|
43005
|
+
// iterations no matter how good the warm-start guess is, and below ~1e-10 the
|
|
43006
|
+
// projection round-trip + haversine noise floor makes the test unreachable for
|
|
43007
|
+
// small d (it never converges and burns the iteration cap). 1e-8 relative is
|
|
43008
|
+
// 0.25 mm at a 25 km radius -- far below cartographic relevance -- and a sweep
|
|
43009
|
+
// over real data showed it sits just before the cost curve and the noise floor
|
|
43010
|
+
// bite (1e-7 and 1e-8 cost the same; 1e-9 adds ~5%; 1e-12 roughly doubles the
|
|
43011
|
+
// inverse-projection count and caps out on ~11% of points at 1 km).
|
|
43012
|
+
//
|
|
43013
|
+
// The tolerance does NOT need to guarantee bit-identical results for the same
|
|
43014
|
+
// (x, y, bearing, dist) across call paths: the only place two independent offset
|
|
43015
|
+
// computations must coincide -- a closed ring's seam -- closes by reusing the
|
|
43016
|
+
// first offset vertex's reference exactly (see makeFinalJoin in
|
|
43017
|
+
// mapshaper-path-buffer-v4), not by recomputing and comparing. Within a single
|
|
43018
|
+
// run the call order is fixed, so the warm-started result is deterministic.
|
|
43019
|
+
//
|
|
43020
|
+
// The absolute floor keeps tiny-distance helper calls (inset/probe offsets at
|
|
43021
|
+
// dist*1e-4 etc.) from chasing a sub-noise relative target; it is well below the
|
|
43022
|
+
// relative target for any d above ~100 m, so it never loosens normal offsets.
|
|
43023
|
+
var SCALE_OFFSET_TOL = 1e-8;
|
|
43024
|
+
var SCALE_OFFSET_ABS = 1e-6;
|
|
43025
|
+
var SCALE_OFFSET_MAX_ITER = 16;
|
|
43026
|
+
|
|
43027
|
+
// Build a deterministic offset function for projected data that corrects the
|
|
43028
|
+
// offset magnitude so the endpoint sits at a true ground distance. The cartesian
|
|
43029
|
+
// offset direction is kept (so all the builder's planar constructions still hold)
|
|
43030
|
+
// and only the distance is solved: starting from the cartesian magnitude, measure
|
|
43031
|
+
// the great-circle distance actually reached, scale by the error ratio, and
|
|
43032
|
+
// iterate to convergence. The converged scale (projected units per ground meter)
|
|
43033
|
+
// is remembered to warm-start the next point -- adjacent points share nearly the
|
|
43034
|
+
// same projection scale, so the seeded guess is close and the loop converges in
|
|
43035
|
+
// ~2-3 iterations. The warm-start only seeds the initial guess and the loop is
|
|
43036
|
+
// driven to a tolerance (see SCALE_OFFSET_TOL), so within a run the result is
|
|
43037
|
+
// deterministic.
|
|
43038
|
+
// Returns null if the CRS cannot be inverted (no way to measure ground distance).
|
|
43039
|
+
function makeScaleCorrectedOffset(crs) {
|
|
43040
|
+
if (!isInvertibleCRS(crs)) return null;
|
|
43041
|
+
var toLngLat = getProjTransform2(crs, parseCrsString$1('wgs84')); // projected -> lng,lat
|
|
43042
|
+
var warmScale = 1;
|
|
43043
|
+
// Small LRU of inverted source coordinates. Every path vertex is unprojected at
|
|
43044
|
+
// least twice (it is the shared endpoint of two consecutive segment offsets) and
|
|
43045
|
+
// again for each round-join/cap arc point centered on it; a single slot is
|
|
43046
|
+
// clobbered by the join construction between segments. A few slots capture that
|
|
43047
|
+
// reuse and are dropped as construction moves along the path. Returns the exact
|
|
43048
|
+
// toLngLat value (possibly null, out of domain), so caching changes nothing but speed.
|
|
43049
|
+
var SCALE_LL_CACHE = 8;
|
|
43050
|
+
var ckx = [], cky = [], cll = [];
|
|
43051
|
+
function invert(x, y) {
|
|
43052
|
+
for (var j = cll.length - 1; j >= 0; j--) {
|
|
43053
|
+
if (ckx[j] === x && cky[j] === y) return cll[j];
|
|
43054
|
+
}
|
|
43055
|
+
var ll = toLngLat(x, y);
|
|
43056
|
+
ckx.push(x); cky.push(y); cll.push(ll);
|
|
43057
|
+
if (cll.length > SCALE_LL_CACHE) { ckx.shift(); cky.shift(); cll.shift(); }
|
|
43058
|
+
return ll;
|
|
43059
|
+
}
|
|
43060
|
+
return function(x, y, bearing, meterDist) {
|
|
43061
|
+
if (!meterDist) return [x, y];
|
|
43062
|
+
var rad = bearing * D2R;
|
|
43063
|
+
var sign = meterDist < 0 ? -1 : 1;
|
|
43064
|
+
var d = meterDist * sign; // positive ground distance to reach
|
|
43065
|
+
var ux = Math.sin(rad) * sign, uy = Math.cos(rad) * sign;
|
|
43066
|
+
var ll0 = invert(x, y);
|
|
43067
|
+
if (!ll0) return getPlanarSegmentEndpoint(x, y, bearing, meterDist);
|
|
43068
|
+
var m = d * warmScale, qx = x, qy = y;
|
|
43069
|
+
for (var i = 0; i < SCALE_OFFSET_MAX_ITER; i++) {
|
|
43070
|
+
qx = x + ux * m; qy = y + uy * m;
|
|
43071
|
+
var ll1 = toLngLat(qx, qy);
|
|
43072
|
+
if (!ll1) break; // offset left the projection's domain; keep current estimate
|
|
43073
|
+
var g = greatCircleDistance(ll0[0], ll0[1], ll1[0], ll1[1]);
|
|
43074
|
+
if (!(g > 0)) { m *= 2; continue; }
|
|
43075
|
+
if (Math.abs(g - d) <= SCALE_OFFSET_TOL * d + SCALE_OFFSET_ABS) break;
|
|
43076
|
+
m *= d / g;
|
|
43077
|
+
}
|
|
43078
|
+
warmScale = m / d;
|
|
43079
|
+
return [qx, qy];
|
|
43080
|
+
};
|
|
43081
|
+
}
|
|
43082
|
+
|
|
42363
43083
|
// Great-circle (spherical geodesic) offset, computed directly in Mercator coords.
|
|
42364
43084
|
// bearing: compass degrees; meterDist: meters; x, y and the result: Mercator.
|
|
42365
43085
|
function greatCircleOffset(x, y, bearing, meterDist) {
|
|
@@ -42429,6 +43149,12 @@ ${svg}
|
|
|
42429
43149
|
var getOffsetPoint = getOffsetFunction(crs, opts);
|
|
42430
43150
|
var roundJoinSegsPerQuadrant = opts.quad_segs >= 2 ? opts.quad_segs : 8;
|
|
42431
43151
|
var roundJoinSegAngle = 90 / roundJoinSegsPerQuadrant;
|
|
43152
|
+
// Max arc step (degrees) for the coarse concave bridge (makeCoarseConcaveJoin),
|
|
43153
|
+
// the optional low-resolution alternative to makeConcaveJoin in
|
|
43154
|
+
// traceCleanOffsetSide. Larger = fewer points = faster dissolve. The reversed
|
|
43155
|
+
// bridge only bounds a self-overlap loop that the direction remover collapses,
|
|
43156
|
+
// so its resolution does not affect the final boundary.
|
|
43157
|
+
var CLEAN_OUTLINE_BRIDGE_STEP = 90;
|
|
42432
43158
|
var capStyle = opts.cap_style || 'round'; // expect 'round' or 'flat'
|
|
42433
43159
|
var pathIter = useMercator ?
|
|
42434
43160
|
getProjectingPathIterator(dataset.arcs, opts) : new ShapeIter(dataset.arcs);
|
|
@@ -42454,8 +43180,7 @@ ${svg}
|
|
|
42454
43180
|
properties: null,
|
|
42455
43181
|
geometry: {
|
|
42456
43182
|
type: 'MultiPolygon',
|
|
42457
|
-
|
|
42458
|
-
coordinates: rings.map(ring => [ring])
|
|
43183
|
+
coordinates: rings.map(function(ring) { return [ring]; })
|
|
42459
43184
|
}
|
|
42460
43185
|
}];
|
|
42461
43186
|
if (useMercator) {
|
|
@@ -42523,14 +43248,26 @@ ${svg}
|
|
|
42523
43248
|
|
|
42524
43249
|
// each path may be converted into multiple buffer rings, which later
|
|
42525
43250
|
// need to be dissolved
|
|
42526
|
-
// Re-anchor a closed ring [v0, v1, ..., v0]
|
|
42527
|
-
// returns [m,
|
|
42528
|
-
// starts/ends mid-edge (a collinear
|
|
43251
|
+
// Re-anchor a closed ring [v0, v1, ..., v0] so its offset seam falls at the
|
|
43252
|
+
// midpoint of a chosen edge (vk -> vk+1): returns [m, vk+1, ..., vk, m] where
|
|
43253
|
+
// m = midpoint(vk, vk+1). The offset then starts/ends mid-edge (a collinear
|
|
43254
|
+
// seam) and every original vertex becomes an interior join.
|
|
43255
|
+
//
|
|
43256
|
+
// The seam edge is chosen by chooseSeamEdge() to sit away from concave (reflex)
|
|
43257
|
+
// corners. In winding-fill mode a concave corner dips the offset back to its
|
|
43258
|
+
// source vertex (resolved later by the dissolve); a seam landing next to such a
|
|
43259
|
+
// dip leaves the pre-dissolve overshoot-loop remover (removeBufferRingLoops*)
|
|
43260
|
+
// starting from an anchor buried inside that tangle, where it can collapse the
|
|
43261
|
+
// true outer boundary instead of the self-overlap (an inward notch). Putting the
|
|
43262
|
+
// seam in a clean convex stretch keeps the remover's first-vertex anchor on the
|
|
43263
|
+
// real boundary.
|
|
42529
43264
|
function startRingAtEdgeMidpoint(verts) {
|
|
42530
|
-
var
|
|
43265
|
+
var k = chooseSeamEdge(verts);
|
|
43266
|
+
var n = verts.length - 1; // distinct vertices (verts[0] == verts[n])
|
|
43267
|
+
var a = verts[k], b = verts[(k + 1) % n];
|
|
42531
43268
|
var m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
42532
43269
|
var out = [m];
|
|
42533
|
-
for (var
|
|
43270
|
+
for (var t = 1; t <= n; t++) out.push(verts[(k + t) % n]);
|
|
42534
43271
|
out.push(m.concat());
|
|
42535
43272
|
return out;
|
|
42536
43273
|
}
|
|
@@ -42540,25 +43277,49 @@ ${svg}
|
|
|
42540
43277
|
var pathSideVerts = collectPathVertices(pathArcs);
|
|
42541
43278
|
var verts = pathSideVerts;
|
|
42542
43279
|
if (simplifyIntervalFn) {
|
|
42543
|
-
verts = presimplifyPathVerts(verts, simplifyIntervalFn(dist)
|
|
43280
|
+
verts = presimplifyPathVerts(verts, simplifyIntervalFn(dist));
|
|
43281
|
+
}
|
|
43282
|
+
// Closed two-sided line rings: open with a sub-tolerance gap at the first
|
|
43283
|
+
// vertex so the open-path outline builder applies (round caps close the
|
|
43284
|
+
// seam). Gap size is in the same coordinate units as verts.
|
|
43285
|
+
if (!oneSidedBuffer && !opts.band_method && !pathIsOpen(verts)) {
|
|
43286
|
+
verts = openClosedRingWithMicroGap(verts);
|
|
42544
43287
|
}
|
|
42545
|
-
if (!
|
|
43288
|
+
if (!opts.band_method && pathIsOpen(verts) && (!oneSidedBuffer || opts.outline)) {
|
|
42546
43289
|
// Fast path for ordinary two-sided line buffers: emit one closed
|
|
42547
43290
|
// outline instead of many per-segment bands that must be dissolved.
|
|
42548
43291
|
// The band-method escape hatch skips it to fall through to the
|
|
42549
43292
|
// per-segment band construction (makeLeftBufferRings, no winding fill).
|
|
43293
|
+
//
|
|
43294
|
+
// Also used for the topological polygon grow's OPEN boundary chains
|
|
43295
|
+
// (outline mode, one-sided 'left'): an open unshared-boundary chain must
|
|
43296
|
+
// be capped on BOTH ends. The one-sided outline (buildCleanOutlineRings)
|
|
43297
|
+
// offsets a single side and self-closes with a straight chord between the
|
|
43298
|
+
// chain's endpoints -- harmless when they nearly coincide, but for a chain
|
|
43299
|
+
// spanning a whole border (e.g. a state's Canada boundary) that chord is a
|
|
43300
|
+
// multi-degree spike. The two-sided stadium caps both ends instead; the
|
|
43301
|
+
// mosaic union with the source polygon absorbs the inner (interior) half.
|
|
42550
43302
|
var built = makeTwoSidedOutlineRing(verts, dist);
|
|
42551
|
-
|
|
42552
|
-
|
|
42553
|
-
|
|
42554
|
-
|
|
42555
|
-
|
|
42556
|
-
|
|
42557
|
-
|
|
42558
|
-
|
|
42559
|
-
|
|
43303
|
+
var out;
|
|
43304
|
+
if (opts.no_loop_removal) {
|
|
43305
|
+
out = [built.ring];
|
|
43306
|
+
} else {
|
|
43307
|
+
// Multi-pass dip+coverage remover: construction-tagged reversed concave-join
|
|
43308
|
+
// ("dip") cusps mark self-overlap folds, and an exact scanline coverage
|
|
43309
|
+
// check refuses any collapse that would uncover a real boundary lobe OR
|
|
43310
|
+
// swallow a real hole/notch.
|
|
43311
|
+
out = [removeBufferRingLoopsIterative(built.ring, BUFFER_LOOP_WINDOW,
|
|
43312
|
+
null, null, undefined, built.dipTags, undefined,
|
|
43313
|
+
dist * dist * BUFFER_LOOP_FILL_AREA_FRAC)];
|
|
42560
43314
|
}
|
|
42561
|
-
|
|
43315
|
+
// Geodesic fan-apart gap patches (same mechanism as the polygon outline):
|
|
43316
|
+
// union a single-segment round-cap stadium for the source segments at each
|
|
43317
|
+
// exposed fan-apart bend so the winding dissolve fills the sliver gap.
|
|
43318
|
+
if (built.fanApartBends.length) {
|
|
43319
|
+
addFanApartGapPatches(out, verts, built.fanApartBends, dist,
|
|
43320
|
+
ringSignedArea(out[0]) >= 0);
|
|
43321
|
+
}
|
|
43322
|
+
return out;
|
|
42562
43323
|
}
|
|
42563
43324
|
if (!opts.right || opts.left) {
|
|
42564
43325
|
rings = rings.concat(buildOneSidedRings(verts));
|
|
@@ -42597,22 +43358,17 @@ ${svg}
|
|
|
42597
43358
|
if (opts.outline && sideVerts.length > 2 && !pathIsOpen(sideVerts)) {
|
|
42598
43359
|
sideVerts = startRingAtEdgeMidpoint(sideVerts);
|
|
42599
43360
|
}
|
|
43361
|
+
// Polygon-grow outline: the shared constant-radius construction used by
|
|
43362
|
+
// two-sided line outlines (traceCleanOffsetSide + dip+coverage loop
|
|
43363
|
+
// removal inside buildCleanOutlineRings).
|
|
43364
|
+
if (opts.outline) {
|
|
43365
|
+
return buildCleanOutlineRings(sideVerts, dist);
|
|
43366
|
+
}
|
|
42600
43367
|
var built = makeLeftBufferRings(sideVerts, dist,
|
|
42601
43368
|
oneSidedBuffer ? pathSideVerts : null);
|
|
42602
43369
|
if (opts.no_loop_removal || !opts.winding_fill) {
|
|
42603
43370
|
return built.rings;
|
|
42604
43371
|
}
|
|
42605
|
-
if (opts.outline) {
|
|
42606
|
-
// Outline mode rings are clean offset-only loops (no source-path edge),
|
|
42607
|
-
// so each has a consistent +/-1 base winding and the crossing-direction
|
|
42608
|
-
// remover classifies overshoot loops exactly from the ring geometry --
|
|
42609
|
-
// the same condition that makes it safe for the open-path two-sided
|
|
42610
|
-
// outline. (The band-ribbon rings of the default construction do not,
|
|
42611
|
-
// which is why they use the source-turn gate below.)
|
|
42612
|
-
return built.rings.map(function(ring) {
|
|
42613
|
-
return removeBufferRingLoopsByDirection(ring, BUFFER_LOOP_WINDOW);
|
|
42614
|
-
});
|
|
42615
|
-
}
|
|
42616
43372
|
var turnPrefix = getSourceTurnPrefix(sideVerts);
|
|
42617
43373
|
return built.rings.map(function(ring, i) {
|
|
42618
43374
|
var srcPos = built.srcPositions[i];
|
|
@@ -42622,6 +43378,86 @@ ${svg}
|
|
|
42622
43378
|
}
|
|
42623
43379
|
}
|
|
42624
43380
|
|
|
43381
|
+
// Build the clean-outline-winding ring for one offset side from the shared
|
|
43382
|
+
// traceCleanOffsetSide construction (the same construction the open two-sided
|
|
43383
|
+
// line outline uses), then strip self-overlap loops before the dissolve.
|
|
43384
|
+
//
|
|
43385
|
+
// A closed source ring closes on its first offset vertex: the midpoint seam is
|
|
43386
|
+
// collinear (see startRingAtEdgeMidpoint), so the recomputed final offset point
|
|
43387
|
+
// is a sub-ULP duplicate of the first and is dropped (done() repeats the first
|
|
43388
|
+
// vertex exactly). An open arc appends the final offset endpoint and an end cap.
|
|
43389
|
+
//
|
|
43390
|
+
// Loop removal: multi-pass dip+coverage (removeBufferRingLoopsIterative with
|
|
43391
|
+
// construction dip tags), the same method used for two-sided line outlines.
|
|
43392
|
+
function removePolygonOutlineLoops(ring, dipTags, dist) {
|
|
43393
|
+
return removeBufferRingLoopsIterative(ring, BUFFER_LOOP_WINDOW,
|
|
43394
|
+
null, null, undefined, dipTags, undefined,
|
|
43395
|
+
dist * dist * BUFFER_LOOP_FILL_AREA_FRAC);
|
|
43396
|
+
}
|
|
43397
|
+
|
|
43398
|
+
function buildCleanOutlineRings(sideVerts, dist) {
|
|
43399
|
+
if (sideVerts.length < 2) return [];
|
|
43400
|
+
var closed = !pathIsOpen(sideVerts);
|
|
43401
|
+
var info = traceCleanOffsetSide(sideVerts, dist);
|
|
43402
|
+
var pts = info.points, segs = info.segs, tags = info.dipTags;
|
|
43403
|
+
for (var i = 0; i < pts.length; i++) builder.addBufferVertex(pts[i], segs[i], tags[i]);
|
|
43404
|
+
if (!closed) {
|
|
43405
|
+
if (info.lastPoint) builder.addBufferVertex(info.lastPoint, info.lastSeg);
|
|
43406
|
+
if (capStyle == 'round') {
|
|
43407
|
+
var end = sideVerts[sideVerts.length - 1];
|
|
43408
|
+
builder.addBufferVertices(
|
|
43409
|
+
makeRoundCap(end[0], end[1], info.lastBearing - 90, dist), NaN);
|
|
43410
|
+
}
|
|
43411
|
+
}
|
|
43412
|
+
var d = builder.done(true);
|
|
43413
|
+
if (!d) return [];
|
|
43414
|
+
var mainRing;
|
|
43415
|
+
if (opts.no_loop_removal) {
|
|
43416
|
+
mainRing = d.ring;
|
|
43417
|
+
} else {
|
|
43418
|
+
mainRing = removePolygonOutlineLoops(d.ring, d.dipTags, dist);
|
|
43419
|
+
}
|
|
43420
|
+
var out = [mainRing];
|
|
43421
|
+
// Union in a single-segment round-cap patch for every fan-apart concave bend
|
|
43422
|
+
// (offsetEdgesFanApart). Each patch is the exact buffer of one source
|
|
43423
|
+
// segment, hence a subset of the true buffer, so it can only fill the
|
|
43424
|
+
// winding-0 sliver the pinched bridge leaves -- it can never push geometry
|
|
43425
|
+
// through the outer wall. Oriented to match the main ring so the winding fill
|
|
43426
|
+
// adds (not subtracts) its area.
|
|
43427
|
+
if (info.fanApartBends.length) {
|
|
43428
|
+
addFanApartGapPatches(out, sideVerts, info.fanApartBends, dist,
|
|
43429
|
+
ringSignedArea(mainRing) >= 0);
|
|
43430
|
+
}
|
|
43431
|
+
return out;
|
|
43432
|
+
}
|
|
43433
|
+
|
|
43434
|
+
// Append a round-cap stadium patch for the two source segments meeting at each
|
|
43435
|
+
// recorded fan-apart bend vertex. A single segment has no bend (so its buffer
|
|
43436
|
+
// is the exact, gap-free stadium); the two patches' caps at the shared vertex
|
|
43437
|
+
// cover the sliver wedge the pinched bridge failed to fill.
|
|
43438
|
+
function addFanApartGapPatches(out, sideVerts, bends, dist, parentCCW) {
|
|
43439
|
+
for (var b = 0; b < bends.length; b++) {
|
|
43440
|
+
var k = bends[b];
|
|
43441
|
+
if (k - 1 >= 0) pushPatch(out, [sideVerts[k - 1], sideVerts[k]], dist, parentCCW);
|
|
43442
|
+
if (k + 1 < sideVerts.length) pushPatch(out, [sideVerts[k], sideVerts[k + 1]], dist, parentCCW);
|
|
43443
|
+
}
|
|
43444
|
+
}
|
|
43445
|
+
|
|
43446
|
+
function pushPatch(out, seg, dist, parentCCW) {
|
|
43447
|
+
if (seg[0][0] === seg[1][0] && seg[0][1] === seg[1][1]) return;
|
|
43448
|
+
var ring = makeTwoSidedOutlineRing(seg, dist).ring;
|
|
43449
|
+
if ((ringSignedArea(ring) >= 0) !== parentCCW) ring.reverse();
|
|
43450
|
+
out.push(ring);
|
|
43451
|
+
}
|
|
43452
|
+
|
|
43453
|
+
function ringSignedArea(ring) {
|
|
43454
|
+
var s = 0;
|
|
43455
|
+
for (var i = 0; i < ring.length - 1; i++) {
|
|
43456
|
+
s += ring[i][0] * ring[i + 1][1] - ring[i + 1][0] * ring[i][1];
|
|
43457
|
+
}
|
|
43458
|
+
return s;
|
|
43459
|
+
}
|
|
43460
|
+
|
|
42625
43461
|
function makeLeftBufferRings(verts, dist, pathSideVerts) {
|
|
42626
43462
|
var rings = [];
|
|
42627
43463
|
// Parallel to rings[]: each entry is the source-position array for the
|
|
@@ -42689,21 +43525,18 @@ ${svg}
|
|
|
42689
43525
|
// original path vertex, then walk out the full outgoing offset (p1).
|
|
42690
43526
|
// The self-overlap is resolved by the winding-number union, so no
|
|
42691
43527
|
// section splits, join-sector rings, or band-coverage audit are needed.
|
|
43528
|
+
// (The clean-outline-winding grow does NOT reach here -- it is routed to
|
|
43529
|
+
// buildCleanOutlineRings, which bridges concave corners with
|
|
43530
|
+
// makeConcaveJoin to keep a constant +/-1 winding.)
|
|
42692
43531
|
builder.addBufferVertex(p2Prev, segId);
|
|
42693
43532
|
builder.addBufferVertex([x1, y1], segId);
|
|
42694
43533
|
builder.addBufferVertex(p1, segId);
|
|
42695
43534
|
} else if (joinAngle > roundJoinSegAngle * 1.5) {
|
|
42696
|
-
//
|
|
42697
|
-
//
|
|
42698
|
-
|
|
42699
|
-
// by extending the last segment to make an outside join
|
|
42700
|
-
// builder.addBufferVertex(p2Prev, false)
|
|
42701
|
-
joinPoints = makeOutsideRoundJoin(x1, y1, bearingPrev - 90, joinAngle, dist);
|
|
43535
|
+
// Large convex bend: arc vertices on the offset circle replace the
|
|
43536
|
+
// previous segment end (p2Prev) and current segment start (p1).
|
|
43537
|
+
joinPoints = makeInscribedRoundJoin(x1, y1, bearingPrev - 90, joinAngle, dist);
|
|
42702
43538
|
builder.addBufferVertices(joinPoints, segId);
|
|
42703
|
-
|
|
42704
|
-
// added an extended vertex to replace it.
|
|
42705
|
-
// builder.addBufferVertex(p1, false)
|
|
42706
|
-
p1 = joinPoints.pop();
|
|
43539
|
+
p1 = joinPoints.pop(); // track outgoing segment start (already added)
|
|
42707
43540
|
} else if (joinAngle > -1e-10 && joinAngle < 1e-10) {
|
|
42708
43541
|
// nearly collinear segments - add one point to the buffer
|
|
42709
43542
|
// TODO: confirm that p1 and p2Prev are always very close
|
|
@@ -43160,6 +43993,31 @@ ${svg}
|
|
|
43160
43993
|
return verts[0][0] !== verts[n-1][0] || verts[0][1] !== verts[n-1][1];
|
|
43161
43994
|
}
|
|
43162
43995
|
|
|
43996
|
+
// Open a closed ring at its first vertex: nudge the corner into two points
|
|
43997
|
+
// straddling it along the incoming and outgoing edges (sub-tolerance gap).
|
|
43998
|
+
function openClosedRingWithMicroGap(verts) {
|
|
43999
|
+
var gap = 1e-6;
|
|
44000
|
+
var n = verts.length;
|
|
44001
|
+
if (n < 4 || pathIsOpen(verts)) return verts;
|
|
44002
|
+
var ring = [], i;
|
|
44003
|
+
for (i = 0; i < n; i++) ring.push(verts[i].concat());
|
|
44004
|
+
if (ring[0][0] === ring[n - 1][0] && ring[0][1] === ring[n - 1][1]) ring.pop();
|
|
44005
|
+
if (ring.length < 3) return verts;
|
|
44006
|
+
var p0 = ring[0], p1 = ring[1], pPrev = ring[ring.length - 1];
|
|
44007
|
+
ring[0] = nudgeVertexFrom(p0, p1, gap);
|
|
44008
|
+
ring.push(nudgeVertexFrom(p0, pPrev, gap));
|
|
44009
|
+
return ring;
|
|
44010
|
+
}
|
|
44011
|
+
|
|
44012
|
+
function nudgeVertexFrom(origin, toward, dist) {
|
|
44013
|
+
var dx = toward[0] - origin[0], dy = toward[1] - origin[1];
|
|
44014
|
+
var len = Math.hypot(dx, dy);
|
|
44015
|
+
if (len > 0) {
|
|
44016
|
+
return [origin[0] + dx / len * dist, origin[1] + dy / len * dist];
|
|
44017
|
+
}
|
|
44018
|
+
return [origin[0] + dist, origin[1]];
|
|
44019
|
+
}
|
|
44020
|
+
|
|
43163
44021
|
// get angle between two extruded segments in degrees
|
|
43164
44022
|
// positive angle means join is convex; negative angle means join is concave
|
|
43165
44023
|
function getJoinAngle(direction1, direction2) {
|
|
@@ -43193,10 +44051,22 @@ ${svg}
|
|
|
43193
44051
|
// reverse, so its positions map back to source order; cap points get NaN so
|
|
43194
44052
|
// a pocket spanning a cap is never treated as a single-bend overshoot.
|
|
43195
44053
|
var nan = function(arr) { return arr.map(function() { return NaN; }); };
|
|
44054
|
+
var zeros = function(arr) { return arr.map(function() { return 0; }); };
|
|
43196
44055
|
var rightPos = right.srcPos.map(function(p) { return (n - 1) - p; });
|
|
43197
44056
|
var srcPos = left.srcPos.concat(nan(endCap), rightPos, nan(startCap));
|
|
43198
44057
|
srcPos.push(srcPos[0]);
|
|
43199
|
-
|
|
44058
|
+
// Parallel reversed-arc ("dip") tags: caps are never dips.
|
|
44059
|
+
var dipTags = left.dipTags.concat(zeros(endCap), right.dipTags, zeros(startCap));
|
|
44060
|
+
dipTags.push(dipTags[0]);
|
|
44061
|
+
// Fan-apart gap-patch bends from both offset sides, as source-vertex indices.
|
|
44062
|
+
// A bend is concave from only one side, so the two sides flag disjoint
|
|
44063
|
+
// vertices; the right side was traced in reverse, so map its indices back to
|
|
44064
|
+
// source order ((n-1) - r).
|
|
44065
|
+
var bendSet = {};
|
|
44066
|
+
(left.fanApartBends || []).forEach(function(j) { bendSet[j] = 1; });
|
|
44067
|
+
(right.fanApartBends || []).forEach(function(j) { bendSet[(n - 1) - j] = 1; });
|
|
44068
|
+
var bends = Object.keys(bendSet).map(Number);
|
|
44069
|
+
return {ring: ring, srcPos: srcPos, dipTags: dipTags, fanApartBends: bends};
|
|
43200
44070
|
}
|
|
43201
44071
|
|
|
43202
44072
|
// Cumulative absolute turn of the source path, indexed by vertex. The turn
|
|
@@ -43218,26 +44088,45 @@ ${svg}
|
|
|
43218
44088
|
return prefix;
|
|
43219
44089
|
}
|
|
43220
44090
|
|
|
43221
|
-
|
|
43222
|
-
|
|
43223
|
-
|
|
43224
|
-
|
|
43225
|
-
|
|
43226
|
-
|
|
43227
|
-
|
|
43228
|
-
|
|
44091
|
+
// Shared per-segment offset construction for the constant-radius "clean"
|
|
44092
|
+
// buffer outline. Walks the left offset of `verts`, joining adjacent offset
|
|
44093
|
+
// segments with inscribed round joins (convex bends), elbow/shallow joins, and
|
|
44094
|
+
// reversed makeConcaveJoin arcs (concave bends) -- every emitted vertex stays
|
|
44095
|
+
// at distance `dist`, so the traced side keeps a true +/-1 winding. This is
|
|
44096
|
+
// the single construction used by BOTH the open two-sided line outline
|
|
44097
|
+
// (makeOffsetSide) and the clean-outline-winding polygon grow
|
|
44098
|
+
// (buildCleanOutlineRings), so a construction fix lands in one place for both.
|
|
44099
|
+
//
|
|
44100
|
+
// Returns parallel arrays { points, segs }: each offset vertex (consecutive
|
|
44101
|
+
// duplicates dropped) and the source segment it derives from, in trace order.
|
|
44102
|
+
// Returning the finished arrays rather than taking a per-vertex `emit` callback
|
|
44103
|
+
// keeps the tracer a pure (verts, dist) -> data function (easy to unit-test in
|
|
44104
|
+
// isolation) and puts the consecutive-duplicate dedup in one place, so the line
|
|
44105
|
+
// and polygon callers can't drift apart on it. (Both styles measure the same;
|
|
44106
|
+
// the line caller reuses `points`/`segs` in place, so construction is still a
|
|
44107
|
+
// single pass.) The final segment endpoint is NOT pushed; it is returned as
|
|
44108
|
+
// `lastPoint` so the caller can either append it (open side) or close the ring
|
|
44109
|
+
// on its first
|
|
44110
|
+
// vertex (closed outline -- its collinear midpoint seam makes the recomputed
|
|
44111
|
+
// final point a sub-ULP duplicate of the first offset vertex, which must be
|
|
44112
|
+
// dropped rather than emitted, see makeLeftBufferRings closure notes).
|
|
44113
|
+
function traceCleanOffsetSide(verts, dist) {
|
|
43229
44114
|
var x1, y1, x2, y2, bearing, bearingPrev, joinAngle, hit;
|
|
43230
|
-
var p1, p2, p1Prev, p2Prev;
|
|
43231
|
-
var
|
|
43232
|
-
var
|
|
43233
|
-
|
|
43234
|
-
|
|
43235
|
-
|
|
43236
|
-
|
|
43237
|
-
|
|
43238
|
-
|
|
43239
|
-
|
|
43240
|
-
|
|
44115
|
+
var p1, p2, p1Prev, p2Prev, firstBearing, lastBearing, joinPoints, i;
|
|
44116
|
+
var points = [], segs = [], dipTags = [], fanApartBends = [];
|
|
44117
|
+
var vertsSegIndex = null;
|
|
44118
|
+
// tag: 1 marks a vertex emitted as part of a reversed concave-join arc
|
|
44119
|
+
// (the "dip" the construction inserts when adjacent offset segments do not
|
|
44120
|
+
// meet locally). These runs are pure self-overlap artifacts, so loop
|
|
44121
|
+
// removal can key on them directly instead of guessing from ring geometry.
|
|
44122
|
+
function add(p, segId, tag) {
|
|
44123
|
+
var prev = points[points.length - 1];
|
|
44124
|
+
if (prev && prev[0] === p[0] && prev[1] === p[1]) return;
|
|
44125
|
+
points.push(p);
|
|
44126
|
+
segs.push(segId);
|
|
44127
|
+
dipTags.push(tag ? 1 : 0);
|
|
44128
|
+
}
|
|
44129
|
+
var concaveJoin = opts.coarse_bridge ? makeCoarseConcaveJoin : makeConcaveJoin;
|
|
43241
44130
|
for (var segId = 0; segId < verts.length - 1; segId++) {
|
|
43242
44131
|
x1 = verts[segId][0];
|
|
43243
44132
|
y1 = verts[segId][1];
|
|
@@ -43246,42 +44135,40 @@ ${svg}
|
|
|
43246
44135
|
bearing = bearingDegrees2D(x1, y1, x2, y2);
|
|
43247
44136
|
p1 = getOffsetPoint(x1, y1, bearing - 90, dist);
|
|
43248
44137
|
p2 = getOffsetPoint(x2, y2, bearing - 90, dist);
|
|
43249
|
-
pos = segId;
|
|
43250
44138
|
if (segId === 0) {
|
|
43251
|
-
|
|
44139
|
+
add(p1, segId);
|
|
43252
44140
|
firstBearing = bearing;
|
|
43253
44141
|
} else {
|
|
43254
44142
|
joinAngle = getJoinAngle(bearingPrev, bearing);
|
|
43255
|
-
if (
|
|
43256
|
-
|
|
43257
|
-
|
|
43258
|
-
// walk out the full outgoing offset (p1). The self-overlapping
|
|
43259
|
-
// pocket this creates is resolved by the winding-number union
|
|
43260
|
-
// (it cancels where another band covers it, survives where the
|
|
43261
|
-
// concavity is real), so no band-coverage audit is needed.
|
|
43262
|
-
pushPt(p2Prev);
|
|
43263
|
-
pushPt([x1, y1]);
|
|
43264
|
-
pushPt(p1);
|
|
43265
|
-
} else if (joinAngle > roundJoinSegAngle * 1.5) {
|
|
43266
|
-
joinPoints = makeOutsideRoundJoin(x1, y1, bearingPrev - 90, joinAngle, dist);
|
|
43267
|
-
pushPts(joinPoints);
|
|
44143
|
+
if (joinAngle > roundJoinSegAngle * 1.5) {
|
|
44144
|
+
joinPoints = makeInscribedRoundJoin(x1, y1, bearingPrev - 90, joinAngle, dist);
|
|
44145
|
+
for (i = 0; i < joinPoints.length; i++) add(joinPoints[i], segId);
|
|
43268
44146
|
p1 = joinPoints[joinPoints.length - 1];
|
|
43269
44147
|
} else if (joinAngle > -1e-10 && joinAngle < 1e-10) {
|
|
43270
|
-
|
|
44148
|
+
add(p1, segId);
|
|
43271
44149
|
} else if (joinAngle > 0 && (hit = elbowJoin(p1Prev, p2Prev, p1, p2,
|
|
43272
44150
|
bearingPrev, bearing, x1, y1, dist)) ||
|
|
43273
44151
|
joinAngle < 0 && (hit = bufferSegmentIntersection(p1Prev, p2Prev, p1, p2))) {
|
|
43274
|
-
|
|
44152
|
+
add(hit, segId);
|
|
43275
44153
|
p1 = hit;
|
|
43276
44154
|
} else if (joinAngle > 0) {
|
|
43277
|
-
|
|
44155
|
+
add(p1, segId);
|
|
43278
44156
|
} else if (joinAngle < 0 && (hit = shallowAngleJoin(p2Prev, p1, x1, y1, dist))) {
|
|
43279
|
-
|
|
44157
|
+
add(hit, segId);
|
|
43280
44158
|
p1 = hit;
|
|
43281
44159
|
} else {
|
|
43282
|
-
|
|
43283
|
-
|
|
43284
|
-
|
|
44160
|
+
add(p2Prev, segId, 1);
|
|
44161
|
+
joinPoints = concaveJoin(x1, y1, bearing - 90, -joinAngle, dist);
|
|
44162
|
+
if (useGapPatch(opts, useMercator) &&
|
|
44163
|
+
offsetEdgesFanApart(p1Prev, p2Prev, p1, p2)) {
|
|
44164
|
+
if (!vertsSegIndex) vertsSegIndex = buildVertsSegmentIndex(verts);
|
|
44165
|
+
if (wedgeIsExposed(vertsSegIndex, segId - 1, segId, x1, y1,
|
|
44166
|
+
joinPoints, p2Prev, p1)) {
|
|
44167
|
+
fanApartBends.push(segId);
|
|
44168
|
+
}
|
|
44169
|
+
}
|
|
44170
|
+
for (i = 0; i < joinPoints.length; i++) add(joinPoints[i], segId, 1);
|
|
44171
|
+
add(p1, segId, 1);
|
|
43285
44172
|
}
|
|
43286
44173
|
}
|
|
43287
44174
|
bearingPrev = bearing;
|
|
@@ -43289,29 +44176,57 @@ ${svg}
|
|
|
43289
44176
|
p2Prev = p2;
|
|
43290
44177
|
lastBearing = bearing;
|
|
43291
44178
|
}
|
|
43292
|
-
pos = verts.length - 1;
|
|
43293
|
-
pushPt(p2Prev);
|
|
43294
44179
|
return {
|
|
43295
44180
|
points: points,
|
|
43296
|
-
|
|
44181
|
+
segs: segs,
|
|
44182
|
+
dipTags: dipTags,
|
|
44183
|
+
fanApartBends: fanApartBends,
|
|
43297
44184
|
firstBearing: firstBearing,
|
|
43298
|
-
lastBearing: lastBearing
|
|
44185
|
+
lastBearing: lastBearing,
|
|
44186
|
+
lastPoint: p2Prev,
|
|
44187
|
+
lastSeg: verts.length - 1
|
|
43299
44188
|
};
|
|
43300
44189
|
}
|
|
43301
44190
|
|
|
43302
|
-
function
|
|
43303
|
-
|
|
43304
|
-
|
|
43305
|
-
|
|
44191
|
+
function makeOffsetSide(verts, dist) {
|
|
44192
|
+
// Thin wrapper over the shared traceCleanOffsetSide construction. The tracer
|
|
44193
|
+
// already returns the deduped offset polyline (points[]) and a parallel
|
|
44194
|
+
// srcPos[] of each point's source segment (so loop removal can judge a
|
|
44195
|
+
// self-crossing by the turn of the originating path span rather than by the
|
|
44196
|
+
// unreliable geometry of the offset itself), so we reuse those arrays in
|
|
44197
|
+
// place and only append the final segment endpoint here (with the same
|
|
44198
|
+
// consecutive-duplicate guard); endpoint caps are added by the caller.
|
|
44199
|
+
var info = traceCleanOffsetSide(verts, dist);
|
|
44200
|
+
var points = info.points;
|
|
44201
|
+
var srcPos = info.segs;
|
|
44202
|
+
var dipTags = info.dipTags;
|
|
44203
|
+
var last = info.lastPoint;
|
|
44204
|
+
if (last) {
|
|
44205
|
+
var prev = points[points.length - 1];
|
|
44206
|
+
if (!prev || prev[0] !== last[0] || prev[1] !== last[1]) {
|
|
44207
|
+
points.push(last);
|
|
44208
|
+
srcPos.push(info.lastSeg);
|
|
44209
|
+
dipTags.push(0);
|
|
44210
|
+
}
|
|
43306
44211
|
}
|
|
44212
|
+
return {
|
|
44213
|
+
points: points,
|
|
44214
|
+
srcPos: srcPos,
|
|
44215
|
+
dipTags: dipTags,
|
|
44216
|
+
fanApartBends: info.fanApartBends,
|
|
44217
|
+
firstBearing: info.firstBearing,
|
|
44218
|
+
lastBearing: info.lastBearing
|
|
44219
|
+
};
|
|
43307
44220
|
}
|
|
43308
44221
|
|
|
43309
44222
|
// Reduce a path's vertex count with Douglas-Peucker simplification
|
|
43310
44223
|
// before buffering. Removed vertices lie within the interval of the
|
|
43311
44224
|
// simplified path, so the buffer outline deviates from the unsimplified
|
|
43312
|
-
// buffer by at most the interval
|
|
43313
|
-
//
|
|
43314
|
-
//
|
|
44225
|
+
// buffer by at most the interval (see getBufferSimplifyFunction for the
|
|
44226
|
+
// empirical calibration). D.P. retains locally extreme vertices (the ones
|
|
44227
|
+
// that determine the outline), so the typical deviation is much smaller.
|
|
44228
|
+
// End-segment bearings are pinned (kk[1] and kk[n-2]) so cap geometry
|
|
44229
|
+
// stays exact. Collapsed paths fall back to their original vertices: a
|
|
43315
44230
|
// small ring is below the error budget, but its buffer is a whole disk.
|
|
43316
44231
|
function presimplifyPathVerts(verts, interval, dist) {
|
|
43317
44232
|
var n = verts.length;
|
|
@@ -43340,14 +44255,6 @@ ${svg}
|
|
|
43340
44255
|
// segments are preserved exactly: cap geometry at path endpoints
|
|
43341
44256
|
// (flat caps especially) depends on them.
|
|
43342
44257
|
kk[1] = kk[n-2] = Infinity;
|
|
43343
|
-
if (oneSidedBuffer) {
|
|
43344
|
-
// One-sided coverage is directional, so the turning that
|
|
43345
|
-
// simplification concentrates into single bends must stay below the
|
|
43346
|
-
// angle whose corner cut at the buffer radius would exceed the
|
|
43347
|
-
// simplification interval (see limitGapTurning).
|
|
43348
|
-
limitGapTurning(verts, kk, interval,
|
|
43349
|
-
2 * Math.acos(1 / (1 + interval / (dist * mercScale))));
|
|
43350
|
-
}
|
|
43351
44258
|
var verts2 = [];
|
|
43352
44259
|
for (i = 0; i < n; i++) {
|
|
43353
44260
|
if (kk[i] >= interval) verts2.push(verts[i]);
|
|
@@ -43356,74 +44263,6 @@ ${svg}
|
|
|
43356
44263
|
return verts2.length >= (closed ? 4 : 2) ? verts2 : verts;
|
|
43357
44264
|
}
|
|
43358
44265
|
|
|
43359
|
-
// One-sided buffer coverage is directional: each segment's band lies on
|
|
43360
|
-
// one side of its own bearing, so the angular structure of the path
|
|
43361
|
-
// matters with weight proportional to the buffer radius, not just its
|
|
43362
|
-
// positional structure. Replacing a sub-path with a chord concentrates
|
|
43363
|
-
// the sub-path's turning into the chord's two end joints: convex
|
|
43364
|
-
// turning is harmless (round joins reproduce the full fan of coverage
|
|
43365
|
-
// around a retained vertex), but concentrated concave turning B deepens
|
|
43366
|
-
// the corner cut at the joint from gradual elbow cuts to a single cut
|
|
43367
|
-
// ~ r * (1/cos(B/2) - 1) deeper, and a removed self-loop's 360 degrees
|
|
43368
|
-
// of turning would cost a whole swept disk. Capping each gap's total
|
|
43369
|
-
// absolute turning (interior bends plus the bearing mismatch between
|
|
43370
|
-
// the chord and the gap's end segments) at the angle whose corner cut
|
|
43371
|
-
// equals the simplification interval keeps the one-sided buffer's error
|
|
43372
|
-
// within the interval; where a gap exceeds the cap, re-retain its most
|
|
43373
|
-
// prominent vertex (the D.P. split point) and re-check the halves. A
|
|
43374
|
-
// self-loop subdivides into a coarse polygon with the same total
|
|
43375
|
-
// turning, which sweeps the same disk.
|
|
43376
|
-
function limitGapTurning(verts, kk, interval, maxTurn) {
|
|
43377
|
-
var stack = [];
|
|
43378
|
-
var prev = 0;
|
|
43379
|
-
var i, a, b, gap;
|
|
43380
|
-
for (i = 1; i < verts.length; i++) {
|
|
43381
|
-
if (kk[i] >= interval) {
|
|
43382
|
-
if (i - prev > 1) stack.push([prev, i]);
|
|
43383
|
-
prev = i;
|
|
43384
|
-
}
|
|
43385
|
-
}
|
|
43386
|
-
while (stack.length > 0) {
|
|
43387
|
-
gap = stack.pop();
|
|
43388
|
-
a = gap[0];
|
|
43389
|
-
b = gap[1];
|
|
43390
|
-
if (b - a < 2 || gapTurning(verts, a, b) <= maxTurn) continue;
|
|
43391
|
-
var maxI = a + 1;
|
|
43392
|
-
for (i = a + 2; i < b; i++) {
|
|
43393
|
-
if (kk[i] > kk[maxI]) maxI = i;
|
|
43394
|
-
}
|
|
43395
|
-
kk[maxI] = Infinity;
|
|
43396
|
-
stack.push([a, maxI], [maxI, b]);
|
|
43397
|
-
}
|
|
43398
|
-
}
|
|
43399
|
-
|
|
43400
|
-
// Total absolute turning (in radians) concentrated by replacing the
|
|
43401
|
-
// sub-path verts[a..b] with a single chord: bends interior to the gap,
|
|
43402
|
-
// plus the mismatch between the chord bearing and the bearings of the
|
|
43403
|
-
// gap's first and last segments.
|
|
43404
|
-
function gapTurning(verts, a, b) {
|
|
43405
|
-
var chord = Math.atan2(verts[b][0] - verts[a][0], verts[b][1] - verts[a][1]);
|
|
43406
|
-
if (verts[b][0] === verts[a][0] && verts[b][1] === verts[a][1]) {
|
|
43407
|
-
return Infinity; // gap closes on itself (a loop)
|
|
43408
|
-
}
|
|
43409
|
-
var total = 0;
|
|
43410
|
-
var prev = chord;
|
|
43411
|
-
for (var i = a; i < b; i++) {
|
|
43412
|
-
var bearing = Math.atan2(verts[i+1][0] - verts[i][0], verts[i+1][1] - verts[i][1]);
|
|
43413
|
-
total += Math.abs(angleDelta(prev, bearing));
|
|
43414
|
-
prev = bearing;
|
|
43415
|
-
}
|
|
43416
|
-
total += Math.abs(angleDelta(prev, chord));
|
|
43417
|
-
return total;
|
|
43418
|
-
}
|
|
43419
|
-
|
|
43420
|
-
function angleDelta(a, b) {
|
|
43421
|
-
var d = b - a;
|
|
43422
|
-
if (d > Math.PI) d -= 2 * Math.PI;
|
|
43423
|
-
if (d < -Math.PI) d += 2 * Math.PI;
|
|
43424
|
-
return d;
|
|
43425
|
-
}
|
|
43426
|
-
|
|
43427
44266
|
// Traverse a path with the (possibly projecting) path iterator,
|
|
43428
44267
|
// collecting its vertices into an array; skips duplicate points.
|
|
43429
44268
|
function collectPathVertices(path) {
|
|
@@ -43456,46 +44295,37 @@ ${svg}
|
|
|
43456
44295
|
[getOffsetPoint(x, y, startDir + 180, dist)];
|
|
43457
44296
|
}
|
|
43458
44297
|
|
|
43459
|
-
//
|
|
43460
|
-
//
|
|
43461
|
-
|
|
43462
|
-
// side of the join.
|
|
43463
|
-
function makeOutsideRoundJoin(cx, cy, startBearing, arcAngle, dist) {
|
|
43464
|
-
// point count of 1 would be an elbow joint
|
|
43465
|
-
// (elbow joins should be created elsewhere)
|
|
44298
|
+
// Inscribed round join: vertices on the offset arc at equal angle steps,
|
|
44299
|
+
// each at the true offset distance (geodesic or planar).
|
|
44300
|
+
function makeInscribedRoundJoin(cx, cy, startBearing, arcAngle, dist) {
|
|
43466
44301
|
var pointCount = Math.max(1, Math.round(arcAngle / roundJoinSegAngle));
|
|
43467
44302
|
var stepAngle = arcAngle / pointCount;
|
|
43468
44303
|
var points = [];
|
|
43469
|
-
var i =
|
|
43470
|
-
|
|
43471
|
-
while (i <= pointCount) {
|
|
43472
|
-
bearing = startBearing + stepAngle * i;
|
|
43473
|
-
tanP = getOffsetPoint(cx, cy, bearing, dist);
|
|
43474
|
-
c = getOffsetPoint(tanP[0], tanP[1], bearing - 90, dist * 2);
|
|
43475
|
-
d = getOffsetPoint(tanP[0], tanP[1], bearing + 90, dist * 2);
|
|
43476
|
-
if (i > 0) {
|
|
43477
|
-
joinP = bufferSegmentIntersection(a, b, c, d);
|
|
43478
|
-
if (!joinP) {
|
|
43479
|
-
if (opts.polar) {
|
|
43480
|
-
// Near a clamped pole/antimeridian corner the swept offset tangents
|
|
43481
|
-
// collapse onto the boundary and stop intersecting; hug the clamped
|
|
43482
|
-
// tangent point so the round join degenerates to the pinned corner
|
|
43483
|
-
// instead of throwing.
|
|
43484
|
-
points.push(tanP);
|
|
43485
|
-
} else {
|
|
43486
|
-
throw Error(`no intersection on ${i} of ${pointCount}`);
|
|
43487
|
-
}
|
|
43488
|
-
} else {
|
|
43489
|
-
points.push(joinP);
|
|
43490
|
-
}
|
|
43491
|
-
}
|
|
43492
|
-
a = c;
|
|
43493
|
-
b = d;
|
|
43494
|
-
i++;
|
|
44304
|
+
for (var i = 1; i <= pointCount; i++) {
|
|
44305
|
+
points.push(getOffsetPoint(cx, cy, startBearing + stepAngle * i, dist));
|
|
43495
44306
|
}
|
|
43496
44307
|
return points;
|
|
43497
44308
|
}
|
|
43498
44309
|
|
|
44310
|
+
// True when the two consecutive offset edges of a negative-angle (concave)
|
|
44311
|
+
// bend do not overlap but instead diverge: the infinite-line intersection of
|
|
44312
|
+
// the incoming edge (a->b) and outgoing edge (c->d) lies behind the incoming
|
|
44313
|
+
// edge (t < 0) and beyond the outgoing edge (u > 1). A planar concave bend
|
|
44314
|
+
// always overlaps (the crossing is in front: t > 1, u < 0); this fan-apart
|
|
44315
|
+
// configuration only arises when a variable geodesic offset distance stretches
|
|
44316
|
+
// the two edges past each other, leaving the outer-edge gap we want to bridge
|
|
44317
|
+
// with a forward round join instead of a reversed (doubling-back) arc.
|
|
44318
|
+
function offsetEdgesFanApart(a, b, c, d) {
|
|
44319
|
+
var rx = b[0] - a[0], ry = b[1] - a[1];
|
|
44320
|
+
var sx = d[0] - c[0], sy = d[1] - c[1];
|
|
44321
|
+
var den = rx * sy - ry * sx;
|
|
44322
|
+
if (den === 0) return false; // parallel: keep the reversed bridge
|
|
44323
|
+
var wx = c[0] - a[0], wy = c[1] - a[1];
|
|
44324
|
+
var t = (wx * sy - wy * sx) / den;
|
|
44325
|
+
var u = (wx * ry - wy * rx) / den;
|
|
44326
|
+
return t < 0 && u > 1;
|
|
44327
|
+
}
|
|
44328
|
+
|
|
43499
44329
|
function shallowAngleJoin(a, b, cx, cy, dist) {
|
|
43500
44330
|
var gap = distance2D(a[0], a[1], b[0], b[1]);
|
|
43501
44331
|
var radius = getJoinExtensionDistance(cx, cy, a, b, dist);
|
|
@@ -43537,6 +44367,24 @@ ${svg}
|
|
|
43537
44367
|
return makeRoundJoin(cx, cy, startBearing, arcAngle, dist).reverse();
|
|
43538
44368
|
}
|
|
43539
44369
|
|
|
44370
|
+
// Coarse alternative to makeConcaveJoin (selected by opts.coarse_bridge in
|
|
44371
|
+
// traceCleanOffsetSide): bridges a concave bend with as few as one reversed
|
|
44372
|
+
// arc vertex (CLEAN_OUTLINE_BRIDGE_STEP), producing a smaller ring for the
|
|
44373
|
+
// winding dissolve to chew through. The reversed bridge only bounds a self-
|
|
44374
|
+
// overlap loop the direction remover collapses, so the coarser resolution
|
|
44375
|
+
// does not change the final boundary -- it just trades a little construction
|
|
44376
|
+
// fidelity for dissolve speed.
|
|
44377
|
+
function makeCoarseConcaveJoin(cx, cy, startBearing, arcAngle, dist) {
|
|
44378
|
+
var segs = Math.min(roundJoinSegsPerQuadrant,
|
|
44379
|
+
Math.max(1, Math.ceil(arcAngle / CLEAN_OUTLINE_BRIDGE_STEP)));
|
|
44380
|
+
var points = [];
|
|
44381
|
+
var increment = arcAngle / (segs + 1);
|
|
44382
|
+
for (var i = 1; i <= segs; i++) {
|
|
44383
|
+
points.push(getOffsetPoint(cx, cy, startBearing + increment * i, dist));
|
|
44384
|
+
}
|
|
44385
|
+
return points.reverse();
|
|
44386
|
+
}
|
|
44387
|
+
|
|
43540
44388
|
// get interior vertices of an interpolated CW arc
|
|
43541
44389
|
function makeRoundJoin(cx, cy, startBearing, arcAngle, dist) {
|
|
43542
44390
|
var points = [];
|
|
@@ -43666,18 +44514,9 @@ ${svg}
|
|
|
43666
44514
|
if (debug) {
|
|
43667
44515
|
// Debug visualizations (raw offset rings, mosaic) want the whole layer's
|
|
43668
44516
|
// geometry/topology in one dataset; keep the original global dissolve for
|
|
43669
|
-
// them (no artifact-hole filter runs in debug mode anyway).
|
|
43670
|
-
|
|
43671
|
-
|
|
43672
|
-
// + loop-removal construction (and a winding-number dissolve), so
|
|
43673
|
-
// debug-offset shows the loop-removed offset rings and the no-loop-removal
|
|
43674
|
-
// flag has a visible effect. (See makePolylineBufferTwoSidedPerFeature.)
|
|
43675
|
-
var useWindingConstruction = !oneSided && layerIsAllClosed(lyr, dataset.arcs);
|
|
43676
|
-
var debugMakerOpts = useWindingConstruction ?
|
|
43677
|
-
Object.assign({}, opts, {winding_fill: true}) : opts;
|
|
43678
|
-
var debugDissolveOpts = Object.assign({}, opts, {per_part_holes: true},
|
|
43679
|
-
useWindingConstruction ? {winding_fill: true} : null);
|
|
43680
|
-
dataset2 = importGeoJSON(makeShapeBufferGeoJSON(lyr, dataset, debugMakerOpts), {});
|
|
44517
|
+
// them (no artifact-hole filter runs in debug mode anyway).
|
|
44518
|
+
var debugDissolveOpts = getOutlineBufferDissolveOpts(opts);
|
|
44519
|
+
dataset2 = importGeoJSON(makeShapeBufferGeoJSON(lyr, dataset, opts), {});
|
|
43681
44520
|
dissolveBufferDataset2(dataset2, debugDissolveOpts);
|
|
43682
44521
|
} else {
|
|
43683
44522
|
dataset2 = makePolylineBufferTwoSidedPerFeature(lyr, dataset, opts, spherical);
|
|
@@ -43692,48 +44531,13 @@ ${svg}
|
|
|
43692
44531
|
// Two-sided buffer pipeline, run per source feature. Each feature's outline
|
|
43693
44532
|
// rings are dissolved (and artifact-hole filtered) in isolation, then the
|
|
43694
44533
|
// per-feature results are merged into one polygon layer (one shape per buffered
|
|
43695
|
-
// source feature, in input order).
|
|
43696
|
-
// 560 input features yield 560 output shapes -- so a single global dissolve of
|
|
43697
|
-
// every feature's rings pays to intersect overlapping buffers of distinct
|
|
43698
|
-
// features whose tiles are then selected per shape anyway. On a world-scale
|
|
43699
|
-
// line layer that global planar arrangement exploded the arc count ~20x and ran
|
|
43700
|
-
// the process out of memory; per-feature isolation bounds peak memory to the
|
|
43701
|
-
// most complex single feature. The dissolve collapses any internal cuts, so
|
|
43702
|
-
// each feature's output boundary is identical to the global pipeline's.
|
|
43703
|
-
// True if every part of a polyline shape is a closed ring (first vertex ==
|
|
43704
|
-
// last vertex), so its two-sided buffer is an annulus per ring.
|
|
43705
|
-
function shapeIsAllClosed(shape, arcs) {
|
|
43706
|
-
return !!shape && shape.length > 0 && shape.every(function(part) {
|
|
43707
|
-
return pathIsClosed(part, arcs);
|
|
43708
|
-
});
|
|
43709
|
-
}
|
|
43710
|
-
|
|
43711
|
-
// True if every shape in the layer is made entirely of closed rings.
|
|
43712
|
-
function layerIsAllClosed(lyr, arcs) {
|
|
43713
|
-
var shapes = lyr.shapes || [];
|
|
43714
|
-
return shapes.length > 0 && shapes.every(function(shape) {
|
|
43715
|
-
return shapeIsAllClosed(shape, arcs);
|
|
43716
|
-
});
|
|
43717
|
-
}
|
|
43718
|
-
|
|
44534
|
+
// source feature, in input order).
|
|
43719
44535
|
function makePolylineBufferTwoSidedPerFeature(lyr, dataset, opts, spherical) {
|
|
43720
44536
|
var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
|
|
43721
|
-
var simplifyFn = getBufferSimplifyFunction(dataset, opts); // null if tolerance=0
|
|
43722
44537
|
var makerOpts = Object.assign({geometry_type: lyr.geometry_type}, opts);
|
|
43723
44538
|
var makeShapeBuffer = getPolylineBufferMaker(dataset, makerOpts);
|
|
43724
|
-
// A shape whose parts are all closed rings buffers to an annulus per ring. The
|
|
43725
|
-
// default (open-path) construction builds each side as many split sections plus
|
|
43726
|
-
// join-sector rings (no loop removal), which floods the dissolve with raw rings
|
|
43727
|
-
// (~8x more on a dense coastline). Instead route these shapes through the same
|
|
43728
|
-
// winding-fill + loop-removal construction the polygon-ring buffer uses: one
|
|
43729
|
-
// continuous offset ring per side, with self-overlap loops stripped, resolved
|
|
43730
|
-
// by a winding-number dissolve. Open or mixed shapes keep the tuned outline +
|
|
43731
|
-
// boundary-flood path unchanged.
|
|
43732
|
-
var closedMaker = getPolylineBufferMaker(dataset,
|
|
43733
|
-
Object.assign({}, makerOpts, {winding_fill: true}));
|
|
43734
44539
|
var useFilter = useArtifactHoleFilter(opts);
|
|
43735
|
-
var
|
|
43736
|
-
var sagPct = 1 - Math.cos(Math.PI / 4 / quadSegs);
|
|
44540
|
+
var outlineDissolveOpts = getOutlineBufferDissolveOpts(opts);
|
|
43737
44541
|
// The two-sided pipeline is also reached by a one-sided buffer when the
|
|
43738
44542
|
// winding fill is explicitly disabled (winding_fill: false); the hole filter
|
|
43739
44543
|
// must then use its one-sided coverage test (see filterOutlineArtifactHolesFromShape).
|
|
@@ -43742,28 +44546,18 @@ ${svg}
|
|
|
43742
44546
|
side: opts.right ? -1 : 1,
|
|
43743
44547
|
roundCaps: (opts.cap_style || 'round') == 'round'
|
|
43744
44548
|
} : null;
|
|
43745
|
-
var dissolveOpts = Object.assign({}, opts, {per_part_holes: true});
|
|
43746
|
-
var closedDissolveOpts = Object.assign({}, dissolveOpts, {winding_fill: true});
|
|
43747
44549
|
var datasets = [];
|
|
43748
44550
|
lyr.shapes.forEach(function(shape, i) {
|
|
43749
44551
|
var distance = distanceFn(i);
|
|
43750
44552
|
if (!distance || !shape) return;
|
|
43751
|
-
|
|
43752
|
-
// construction reaches this function with winding_fill:false and needs its
|
|
43753
|
-
// own coverage handling); mixed open/closed shapes fall back too.
|
|
43754
|
-
var allClosed = !oneSided && shapeIsAllClosed(shape, dataset.arcs);
|
|
43755
|
-
var retn = (allClosed ? closedMaker : makeShapeBuffer)(shape, distance);
|
|
44553
|
+
var retn = makeShapeBuffer(shape, distance);
|
|
43756
44554
|
var feats = (Array.isArray(retn) ? retn : [retn]).filter(Boolean);
|
|
43757
44555
|
if (!feats.length) return;
|
|
43758
44556
|
var ds = importGeoJSON(getBufferGeoJSON(feats), {});
|
|
43759
|
-
dissolveBufferDataset2(ds,
|
|
43760
|
-
if (useFilter
|
|
43761
|
-
|
|
43762
|
-
|
|
43763
|
-
bufLyr.shapes = bufLyr.shapes.map(function(bufShape) {
|
|
43764
|
-
return filterOutlineArtifactHolesFromShape(bufShape, ds.arcs, shape,
|
|
43765
|
-
dataset.arcs, distance, intervalPct, sagPct, oneSided, sideOpts, spherical);
|
|
43766
|
-
});
|
|
44557
|
+
dissolveBufferDataset2(ds, outlineDissolveOpts);
|
|
44558
|
+
if (useFilter) {
|
|
44559
|
+
applyOutlineArtifactHoleFilter(ds.layers[0], ds.arcs, lyr, dataset, opts,
|
|
44560
|
+
spherical, {oneSided: oneSided, sideOpts: sideOpts, srcIndex: i});
|
|
43767
44561
|
}
|
|
43768
44562
|
datasets.push(ds);
|
|
43769
44563
|
});
|
|
@@ -44086,6 +44880,32 @@ ${svg}
|
|
|
44086
44880
|
return !opts.debug_offset && !opts.debug_mosaic;
|
|
44087
44881
|
}
|
|
44088
44882
|
|
|
44883
|
+
// Apply the outline artifact-hole filter to every shape in a buffer layer,
|
|
44884
|
+
// using the parallel source layer for distance-to-path tests. Shared by the
|
|
44885
|
+
// two-sided line buffer (per feature) and the clean-outline polygon grow.
|
|
44886
|
+
function applyOutlineArtifactHoleFilter(bufLyr, bufArcs, srcLyr, srcDataset,
|
|
44887
|
+
opts, spherical, filterOpts) {
|
|
44888
|
+
filterOpts = filterOpts || {};
|
|
44889
|
+
if (!useArtifactHoleFilter(opts)) return;
|
|
44890
|
+
var distanceFn = getBufferDistanceFunction(srcLyr, srcDataset, opts);
|
|
44891
|
+
var simplifyFn = getBufferSimplifyFunction(srcDataset, opts);
|
|
44892
|
+
var quadSegs = opts.quad_segs >= 2 ? opts.quad_segs : 8;
|
|
44893
|
+
var sagPct = 1 - Math.cos(Math.PI / 4 / quadSegs);
|
|
44894
|
+
var oneSided = !!filterOpts.oneSided;
|
|
44895
|
+
var sideOpts = filterOpts.sideOpts || null;
|
|
44896
|
+
var srcArcs = srcDataset.arcs;
|
|
44897
|
+
bufLyr.shapes = bufLyr.shapes.map(function(bufShape, i) {
|
|
44898
|
+
var srcIdx = filterOpts.srcIndex != null ? filterOpts.srcIndex : i;
|
|
44899
|
+
var srcShape = srcLyr.shapes[srcIdx];
|
|
44900
|
+
var distance = distanceFn(srcIdx);
|
|
44901
|
+
if (!bufShape || !srcShape || !(distance > 0)) return bufShape;
|
|
44902
|
+
if (filterOpts.skipShape && filterOpts.skipShape(srcShape, srcArcs)) return bufShape;
|
|
44903
|
+
var intervalPct = simplifyFn ? simplifyFn(distance) / distance : 0;
|
|
44904
|
+
return filterOutlineArtifactHolesFromShape(bufShape, bufArcs, srcShape,
|
|
44905
|
+
srcArcs, distance, intervalPct, sagPct, oneSided, sideOpts, spherical);
|
|
44906
|
+
});
|
|
44907
|
+
}
|
|
44908
|
+
|
|
44089
44909
|
// Remove artifact rings left by dissolving the self-intersecting outline rings
|
|
44090
44910
|
// made by the two-sided buffer fast path. The outline's concave-join loops can
|
|
44091
44911
|
// survive the dissolve as spurious holes (and, degenerately, as zero-area rings
|
|
@@ -44999,12 +45819,57 @@ ${svg}
|
|
|
44999
45819
|
});
|
|
45000
45820
|
profileEnd('medial:simplify');
|
|
45001
45821
|
}
|
|
45822
|
+
// Extend each chain's endpoints outward along their terminal tangent. A medial
|
|
45823
|
+
// chain is a cut-line: it only subdivides a contested buffer tile if it spans
|
|
45824
|
+
// from boundary to boundary, so the mosaic builder keeps it (an end that
|
|
45825
|
+
// terminates in a tile's interior is acyclic and detachAcyclicArcs prunes the
|
|
45826
|
+
// whole path). The sampled-site Voronoi stops a fraction of the site spacing
|
|
45827
|
+
// short of where two source rings meet (the gap pinches shut), leaving that end
|
|
45828
|
+
// dangling INSIDE the buffer. Extending past the source boundary lets the cut
|
|
45829
|
+
// node against it; the overshoot lands outside the contested region and is
|
|
45830
|
+
// self-pruned. Without this, a whole river-gap tile is left uncut and assigned
|
|
45831
|
+
// wholesale to one feature (e.g. the Columbia between Oregon and Washington).
|
|
45832
|
+
var extendDist = 0;
|
|
45833
|
+
for (var di = 0; di < coordDistances.length; di++) {
|
|
45834
|
+
if (coordDistances[di] > extendDist) extendDist = coordDistances[di];
|
|
45835
|
+
}
|
|
45836
|
+
if (extendDist > 0) {
|
|
45837
|
+
chains = chains.map(function(chain) {
|
|
45838
|
+
return extendChainEndpoints(chain, extendDist);
|
|
45839
|
+
});
|
|
45840
|
+
}
|
|
45002
45841
|
return {
|
|
45003
45842
|
type: 'MultiLineString',
|
|
45004
45843
|
coordinates: chains
|
|
45005
45844
|
};
|
|
45006
45845
|
}
|
|
45007
45846
|
|
|
45847
|
+
// Extend an open chain past both endpoints by @len along the direction of the
|
|
45848
|
+
// terminal segment (so the cut-line pokes out of the contested tile at each end
|
|
45849
|
+
// and nodes against the enclosing boundary). Zero-length terminal segments and
|
|
45850
|
+
// chains shorter than 2 points are left unchanged.
|
|
45851
|
+
function extendChainEndpoints(chain, len) {
|
|
45852
|
+
if (!chain || chain.length < 2) return chain;
|
|
45853
|
+
var out = chain.concat();
|
|
45854
|
+
var head = projectPast(out[0], out[1], len);
|
|
45855
|
+
if (head) out.unshift(head);
|
|
45856
|
+
var n = out.length;
|
|
45857
|
+
var tail = projectPast(out[n - 1], out[n - 2], len);
|
|
45858
|
+
if (tail) out.push(tail);
|
|
45859
|
+
return out;
|
|
45860
|
+
}
|
|
45861
|
+
|
|
45862
|
+
// Point at distance @len beyond @from, going away from @toward (i.e. continuing
|
|
45863
|
+
// the from->beyond ray that the toward->from segment defines). Returns null for a
|
|
45864
|
+
// degenerate (coincident) segment.
|
|
45865
|
+
function projectPast(from, toward, len) {
|
|
45866
|
+
var dx = from[0] - toward[0];
|
|
45867
|
+
var dy = from[1] - toward[1];
|
|
45868
|
+
var d = Math.sqrt(dx * dx + dy * dy);
|
|
45869
|
+
if (d === 0) return null;
|
|
45870
|
+
return [from[0] + dx / d * len, from[1] + dy / d * len];
|
|
45871
|
+
}
|
|
45872
|
+
|
|
45008
45873
|
// Build the medial-construction triangles for the -buffer debug-delaunay option
|
|
45009
45874
|
// as a GeometryCollection of triangle polygons. collectSites returns only the
|
|
45010
45875
|
// contested sites, so the Delaunay is already the per-region mesh from which the
|
|
@@ -46698,6 +47563,14 @@ ${svg}
|
|
|
46698
47563
|
warn('debug-mosaic is not implemented for polygon buffers; ignoring');
|
|
46699
47564
|
opts = Object.assign({}, opts, {debug_mosaic: false});
|
|
46700
47565
|
}
|
|
47566
|
+
// The clean-outline-winding construction is the default polygon-grow outline
|
|
47567
|
+
// and the topological per-feature offset (band-method restores the older band
|
|
47568
|
+
// ribbon). Spurious dissolve holes on the default grow are removed by the
|
|
47569
|
+
// shared outline artifact-hole filter; gap-patch handles geodesic fan-apart
|
|
47570
|
+
// outer-wall gaps at construction time.
|
|
47571
|
+
if (!opts.band_method) {
|
|
47572
|
+
opts = Object.assign({}, opts, {clean_outline_winding: true});
|
|
47573
|
+
}
|
|
46701
47574
|
if (opts.fill_gaps) {
|
|
46702
47575
|
// Fill enclosed holes and narrow-mouthed inlets without growing the outer
|
|
46703
47576
|
// boundary -- a topology-aware morphological closing (see
|
|
@@ -46756,10 +47629,23 @@ ${svg}
|
|
|
46756
47629
|
dissolveBufferDataset2(dataset2, opts);
|
|
46757
47630
|
}
|
|
46758
47631
|
}
|
|
47632
|
+
if (useOutlineGrowArtifactFilter(opts)) {
|
|
47633
|
+
applyOutlineArtifactHoleFilter(dataset2.layers[0], dataset2.arcs,
|
|
47634
|
+
lyr, dataset, opts, spherical, {
|
|
47635
|
+
skipShape: function(shape, arcs) {
|
|
47636
|
+
return shapeHasFillInsideHole(shape, arcs);
|
|
47637
|
+
}
|
|
47638
|
+
});
|
|
47639
|
+
}
|
|
46759
47640
|
cullSubTolerancePolygonArtifacts(dataset2, lyr, dataset, opts);
|
|
46760
47641
|
return dataset2;
|
|
46761
47642
|
}
|
|
46762
47643
|
|
|
47644
|
+
// True for clean-outline polygon grow and topological (not band-method or debug).
|
|
47645
|
+
function useOutlineGrowArtifactFilter(opts) {
|
|
47646
|
+
return !opts.band_method && !bufferOutputIsDebug(opts);
|
|
47647
|
+
}
|
|
47648
|
+
|
|
46763
47649
|
// True when the buffer is producing a debug view (raw offsets or medial
|
|
46764
47650
|
// construction) rather than a real buffer; those must skip the artifact cull.
|
|
46765
47651
|
function bufferOutputIsDebug(opts) {
|
|
@@ -46775,10 +47661,9 @@ ${svg}
|
|
|
46775
47661
|
// is dropped. The smallest legitimate buffer part (the grow of a point-like
|
|
46776
47662
|
// feature, ~pi*d^2) is ~4 orders of magnitude larger, so the threshold never
|
|
46777
47663
|
// touches real geometry, and a genuinely thin-but-long fill (area =
|
|
46778
|
-
// tol*length >> tol^2) is kept. Holes are intentionally left alone
|
|
46779
|
-
//
|
|
46780
|
-
//
|
|
46781
|
-
// is turned off (tolerance=0), matching the medial-simplify contract.
|
|
47664
|
+
// tol*length >> tol^2) is kept. Holes are intentionally left alone; the outline
|
|
47665
|
+
// artifact-hole filter runs after dissolve and keeps legitimate carved holes.
|
|
47666
|
+
// Disabled when tolerance is turned off (tolerance=0), matching the medial-simplify contract.
|
|
46782
47667
|
function cullSubTolerancePolygonArtifacts(outDataset, srcLyr, srcDataset, opts) {
|
|
46783
47668
|
if (opts.tolerance === 0 || opts.tolerance == '0' || opts.tolerance == '0%') return;
|
|
46784
47669
|
if (!outDataset || !outDataset.arcs) return;
|
|
@@ -46810,8 +47695,9 @@ ${svg}
|
|
|
46810
47695
|
// - The clean-outline construction is the default for ordinary polygon grow
|
|
46811
47696
|
// (outer rings offset to a single self-contained loop; far fewer rings and
|
|
46812
47697
|
// self-intersections into the winding dissolve than the band ribbon).
|
|
46813
|
-
// - The
|
|
46814
|
-
//
|
|
47698
|
+
// - The band-method escape hatch keeps the band-ribbon construction; the
|
|
47699
|
+
// topological pipeline uses the same clean-outline grow (with shared-arc path
|
|
47700
|
+
// splitting) unless band-method is set.
|
|
46815
47701
|
// - Negative buffers and hole shrink fall back to the band erode inside the
|
|
46816
47702
|
// outline path itself.
|
|
46817
47703
|
// Shared by makePolygonBuffer and makePolarPolygonBuffer so the polar option is
|
|
@@ -47006,13 +47892,13 @@ ${svg}
|
|
|
47006
47892
|
// Drives the same per-ring makers the real construction uses, so the view shows
|
|
47007
47893
|
// exactly what is built and 'no-loop-removal' has a visible effect (loop removal
|
|
47008
47894
|
// runs inside the maker -- see buildOneSidedRings). Positive grow uses the
|
|
47009
|
-
// clean-outline maker by default (the band maker under band-method
|
|
47010
|
-
//
|
|
47895
|
+
// clean-outline maker by default (the band maker under band-method); holes are
|
|
47896
|
+
// eroded with the band maker reversed to outer orientation (matching
|
|
47011
47897
|
// makeOutlineBufferGeometry); negative (erode) buffers offset every ring inward
|
|
47012
47898
|
// with the band maker.
|
|
47013
47899
|
function makePolygonDebugOffsetGeoJSON(lyr, dataset, opts) {
|
|
47014
47900
|
var distanceFn = getBufferDistanceFunction(lyr, dataset, opts);
|
|
47015
|
-
var useOutline = !opts.band_method
|
|
47901
|
+
var useOutline = !opts.band_method;
|
|
47016
47902
|
var leftOpts = useOutline ? Object.assign({}, opts, {outline: true}) : opts;
|
|
47017
47903
|
var leftMaker = getPolygonRingBufferMaker(dataset, leftOpts, 'left');
|
|
47018
47904
|
var rightMaker = getPolygonRingBufferMaker(dataset,
|
|
@@ -47158,17 +48044,7 @@ ${svg}
|
|
|
47158
48044
|
getBufferMultiPolygonCoords(rings.outer, distance, outerMaker) : [];
|
|
47159
48045
|
if (outerLoops.length === 0) return null;
|
|
47160
48046
|
// Resolve the outer offset loops' self-overlaps into clean grown polygons.
|
|
47161
|
-
var coords = dissolveOffsetRingsToCoords(outerLoops, opts);
|
|
47162
|
-
if (coords.length === 0) return null;
|
|
47163
|
-
// Strip artifact holes left by the outer grow (the offset loops dissolve into
|
|
47164
|
-
// a clean fill, so any interior ring is a self-overlap artifact) BEFORE
|
|
47165
|
-
// carving the real holes. The artifact filter's heuristic can otherwise
|
|
47166
|
-
// delete a legitimately-shrunk hole whose eroded boundary happens to run near
|
|
47167
|
-
// the source outline; the carved holes below are explicit and known-good, so
|
|
47168
|
-
// they must not pass through it.
|
|
47169
|
-
var grown = removePositiveBufferArtifactHoles(
|
|
47170
|
-
{type: 'MultiPolygon', coordinates: coords}, shape, arcs, distance);
|
|
47171
|
-
coords = grown ? grown.coordinates : [];
|
|
48047
|
+
var coords = dissolveOffsetRingsToCoords(outerLoops, opts, true);
|
|
47172
48048
|
if (coords.length === 0) return null;
|
|
47173
48049
|
if (rings.holes.length > 0) {
|
|
47174
48050
|
// Shrink the holes (an inward offset) with the band erode: treat each hole
|
|
@@ -47184,6 +48060,42 @@ ${svg}
|
|
|
47184
48060
|
return {type: 'MultiPolygon', coordinates: coords};
|
|
47185
48061
|
}
|
|
47186
48062
|
|
|
48063
|
+
// Clean-outline grow for one topological feature: offset each shared-arc path
|
|
48064
|
+
// chain (see getPolygonBufferPathData), grow outers with the outline maker,
|
|
48065
|
+
// shrink holes with the band erode, then union -- same hole semantics as
|
|
48066
|
+
// makeOutlineBufferGeometry but with inter-feature path splitting preserved.
|
|
48067
|
+
//
|
|
48068
|
+
// A chain is classified as outer (grow) vs hole (shrink) by the signed area of
|
|
48069
|
+
// the CLOSED SOURCE RING it was split from, not by the chain's own area: an
|
|
48070
|
+
// unshared-boundary chain is an OPEN fragment (e.g. a state's international or
|
|
48071
|
+
// coastline segment between two shared interior borders), whose planar signed
|
|
48072
|
+
// area has an arbitrary sign and would misclassify a real outer boundary as a
|
|
48073
|
+
// hole -- eroding it inward instead of growing it (Ohio's Lake Erie coast, New
|
|
48074
|
+
// Mexico's Mexico border).
|
|
48075
|
+
function makeTopologicalOutlineBufferCoords(shape, arcs, distance, uniqueArcTest,
|
|
48076
|
+
opts, outerMaker, holeEroder) {
|
|
48077
|
+
var outer = [];
|
|
48078
|
+
var holes = [];
|
|
48079
|
+
(shape || []).forEach(function(ring) {
|
|
48080
|
+
var target = getPlanarPathArea(ring, arcs) < 0 ? holes : outer;
|
|
48081
|
+
var chains = uniqueArcTest ? splitPathAtSharedArcs(ring, uniqueArcTest) :
|
|
48082
|
+
[ring.concat()];
|
|
48083
|
+
chains.forEach(function(chain) { target.push(chain); });
|
|
48084
|
+
});
|
|
48085
|
+
var outerLoops = outer.length > 0 ?
|
|
48086
|
+
getBufferMultiPolygonCoords(outer, distance, outerMaker) : [];
|
|
48087
|
+
if (outerLoops.length === 0) return [];
|
|
48088
|
+
var coords = dissolveOffsetRingsToCoords(outerLoops, opts, true);
|
|
48089
|
+
if (coords.length === 0) return [];
|
|
48090
|
+
if (holes.length > 0) {
|
|
48091
|
+
var holeGeom = holeEroder(holes.map(reversePath), distance);
|
|
48092
|
+
if (holeGeom && holeGeom.coordinates.length > 0) {
|
|
48093
|
+
coords = subtractHolesFromOuter(coords, holeGeom.coordinates);
|
|
48094
|
+
}
|
|
48095
|
+
}
|
|
48096
|
+
return coords;
|
|
48097
|
+
}
|
|
48098
|
+
|
|
47187
48099
|
// Carve clean shrunk-hole regions out of clean grown-outer polygons. Both arrive
|
|
47188
48100
|
// as positive (CCW) rings. The winding union can't subtract one nested loop from
|
|
47189
48101
|
// another -- GeoJSON import rewinds every outer ring to CCW, so two separately
|
|
@@ -47260,13 +48172,23 @@ ${svg}
|
|
|
47260
48172
|
var hasNegativeDistance = false;
|
|
47261
48173
|
if (useTopologicalMode) {
|
|
47262
48174
|
// The topological pipeline selects mosaic tiles by source membership
|
|
47263
|
-
// (boundary flood), which cannot resolve
|
|
47264
|
-
//
|
|
47265
|
-
//
|
|
47266
|
-
//
|
|
47267
|
-
//
|
|
48175
|
+
// (boundary flood), which cannot resolve self-overlapping offset rings.
|
|
48176
|
+
// Each feature's offset is pre-dissolved into a clean polygon before it
|
|
48177
|
+
// enters the shared mosaic. By default this uses the same clean-outline grow
|
|
48178
|
+
// as ordinary polygon buffers (gap-patch, loop removal); band-method keeps
|
|
48179
|
+
// the older band ribbon.
|
|
48180
|
+
var topoLeftOpts = opts.band_method ? opts :
|
|
48181
|
+
Object.assign({}, opts, {outline: true});
|
|
48182
|
+
var outerMaker = getPolygonRingBufferMaker(dataset, topoLeftOpts, 'left');
|
|
48183
|
+
var bandOpts = Object.assign({}, opts, {outline: false});
|
|
48184
|
+
var bandLeftMaker = getPolygonRingBufferMaker(dataset, bandOpts, 'left');
|
|
48185
|
+
var bandRightMaker = getPolygonRingBufferMaker(dataset, bandOpts, 'right');
|
|
48186
|
+
var holeEroder = function(holeShape, dist) {
|
|
48187
|
+
return makeNegativePolygonBufferGeometry(holeShape, dist, dataset, bandOpts,
|
|
48188
|
+
bandRightMaker);
|
|
48189
|
+
};
|
|
47268
48190
|
return makeTopologicalPolygonBufferGeoJSON(lyr, dataset, opts, distanceFn,
|
|
47269
|
-
uniqueArcTest,
|
|
48191
|
+
uniqueArcTest, outerMaker, holeEroder, bandLeftMaker);
|
|
47270
48192
|
}
|
|
47271
48193
|
// Closed source rings are offset with the winding-fill construction: one
|
|
47272
48194
|
// self-overlapping ring per source ring (its overshoot loops resolved by the
|
|
@@ -47332,6 +48254,7 @@ ${svg}
|
|
|
47332
48254
|
// for callers that request winding-fill (see the 'band-method' option).
|
|
47333
48255
|
var useWinding = !opts.band_method;
|
|
47334
48256
|
var makerOpts = Object.assign({}, opts, {
|
|
48257
|
+
geometry_type: 'polygon',
|
|
47335
48258
|
left: side == 'left',
|
|
47336
48259
|
right: side == 'right',
|
|
47337
48260
|
// Winding-fill construction also enables overshoot-loop removal on the single
|
|
@@ -47352,7 +48275,7 @@ ${svg}
|
|
|
47352
48275
|
}
|
|
47353
48276
|
|
|
47354
48277
|
function makeTopologicalPolygonBufferGeoJSON(lyr, dataset, opts, distanceFn,
|
|
47355
|
-
uniqueArcTest,
|
|
48278
|
+
uniqueArcTest, outerMaker, holeEroder, bandFallbackMaker) {
|
|
47356
48279
|
var shapes = lyr.shapes || [];
|
|
47357
48280
|
var distances = [];
|
|
47358
48281
|
var sourceIds = [];
|
|
@@ -47376,18 +48299,22 @@ ${svg}
|
|
|
47376
48299
|
profileStart('topo:offsets');
|
|
47377
48300
|
shapes.forEach(function(shape, i) {
|
|
47378
48301
|
var distance = distances[i];
|
|
47379
|
-
var
|
|
48302
|
+
var bufferCoords;
|
|
47380
48303
|
if (!distance || !shape) return;
|
|
47381
48304
|
hasPositiveDistance = true;
|
|
47382
|
-
|
|
47383
|
-
|
|
47384
|
-
|
|
47385
|
-
|
|
47386
|
-
|
|
47387
|
-
|
|
47388
|
-
|
|
47389
|
-
|
|
47390
|
-
|
|
48305
|
+
if (!opts.band_method && opts.clean_outline_winding &&
|
|
48306
|
+
!shapeHasFillInsideHole(shape, dataset.arcs)) {
|
|
48307
|
+
bufferCoords = makeTopologicalOutlineBufferCoords(shape, dataset.arcs,
|
|
48308
|
+
distance, uniqueArcTest, opts, outerMaker, holeEroder);
|
|
48309
|
+
} else {
|
|
48310
|
+
var pathData = getPolygonBufferPathData(shape, uniqueArcTest);
|
|
48311
|
+
var maker = bandFallbackMaker || outerMaker;
|
|
48312
|
+
bufferCoords = getBufferMultiPolygonCoords(pathData.paths, distance, maker);
|
|
48313
|
+
// Resolve winding-fill band rings' self-overlaps (the mosaic's boundary-
|
|
48314
|
+
// flood membership cannot). band-method feeds bands directly.
|
|
48315
|
+
if (!opts.band_method) {
|
|
48316
|
+
bufferCoords = dissolveOffsetRingsToCoords(bufferCoords, opts);
|
|
48317
|
+
}
|
|
47391
48318
|
}
|
|
47392
48319
|
if (bufferCoords.length > 0) {
|
|
47393
48320
|
bufferIds[i] = tmpGeometries.length;
|
|
@@ -48062,13 +48989,36 @@ ${svg}
|
|
|
48062
48989
|
function ringEnclosesOtherTerritory(ring, ctx) {
|
|
48063
48990
|
var points = ctx.territoryPoints;
|
|
48064
48991
|
if (!points) return false;
|
|
48992
|
+
var bounds = getGeoJSONRingBounds(ring);
|
|
48065
48993
|
for (var i = 0; i < points.length; i++) {
|
|
48066
48994
|
if (points[i].featureId === ctx.featureId) continue;
|
|
48995
|
+
if (!pointInGeoJSONRingBounds(points[i].x, points[i].y, bounds)) continue;
|
|
48067
48996
|
if (pointInGeoJSONRing(points[i].x, points[i].y, ring)) return true;
|
|
48068
48997
|
}
|
|
48069
48998
|
return false;
|
|
48070
48999
|
}
|
|
48071
49000
|
|
|
49001
|
+
function getGeoJSONRingBounds(ring) {
|
|
49002
|
+
var n = ring.length - 1; // skip duplicate closing vertex
|
|
49003
|
+
if (n <= 0) n = ring.length;
|
|
49004
|
+
var xmin = Infinity, ymin = Infinity, xmax = -Infinity, ymax = -Infinity;
|
|
49005
|
+
var i, x, y;
|
|
49006
|
+
for (i = 0; i < n; i++) {
|
|
49007
|
+
x = ring[i][0];
|
|
49008
|
+
y = ring[i][1];
|
|
49009
|
+
if (x < xmin) xmin = x;
|
|
49010
|
+
if (x > xmax) xmax = x;
|
|
49011
|
+
if (y < ymin) ymin = y;
|
|
49012
|
+
if (y > ymax) ymax = y;
|
|
49013
|
+
}
|
|
49014
|
+
return {xmin: xmin, ymin: ymin, xmax: xmax, ymax: ymax};
|
|
49015
|
+
}
|
|
49016
|
+
|
|
49017
|
+
function pointInGeoJSONRingBounds(x, y, bounds) {
|
|
49018
|
+
return x >= bounds.xmin && x <= bounds.xmax &&
|
|
49019
|
+
y >= bounds.ymin && y <= bounds.ymax;
|
|
49020
|
+
}
|
|
49021
|
+
|
|
48072
49022
|
// Ray-casting point-in-ring test for a closed GeoJSON ring (array of [x, y],
|
|
48073
49023
|
// first == last). Boundary cases are irrelevant here: territory probe points are
|
|
48074
49024
|
// well inside their source part, far from any hole-ring edge.
|
|
@@ -48113,9 +49063,18 @@ ${svg}
|
|
|
48113
49063
|
return getCoordinateDistance(distance, arcs) * 0.5;
|
|
48114
49064
|
}
|
|
48115
49065
|
|
|
49066
|
+
// Minimum area for a grow-generated interior ring to be treated as a real hole
|
|
49067
|
+
// rather than numerical noise. A positive buffer can legitimately enclose a hole
|
|
49068
|
+
// far smaller than the buffer disk (a pocket between source arms whose mouth just
|
|
49069
|
+
// closed leaves an arbitrarily small gap), so this is only a degenerate-sliver
|
|
49070
|
+
// floor -- a tiny fraction of the buffer-disk area -- NOT a "holes smaller than
|
|
49071
|
+
// the radius are artifacts" rule. (It used to be the full disk area d*d, which
|
|
49072
|
+
// silently deleted real holes whose area was less than the radius squared; the
|
|
49073
|
+
// near-source boundary classifier in positiveBufferHoleIsArtifact is what
|
|
49074
|
+
// actually distinguishes self-overlap artifacts from real holes.)
|
|
48116
49075
|
function getPositiveHoleArtifactAreaThreshold(distance, arcs) {
|
|
48117
49076
|
var d = getCoordinateDistance(distance, arcs);
|
|
48118
|
-
return d * d;
|
|
49077
|
+
return d * d * 0.01;
|
|
48119
49078
|
}
|
|
48120
49079
|
|
|
48121
49080
|
function getGeoJSONRingArea(ring) {
|
|
@@ -48189,11 +49148,14 @@ ${svg}
|
|
|
48189
49148
|
// dissolve. Used by the topological pipeline to feed an ordinary polygon into
|
|
48190
49149
|
// the shared mosaic (whose boundary-flood membership cannot resolve the
|
|
48191
49150
|
// self-overlapping construction ring directly).
|
|
48192
|
-
function dissolveOffsetRingsToCoords(coords, opts) {
|
|
49151
|
+
function dissolveOffsetRingsToCoords(coords, opts, outlineDissolve) {
|
|
48193
49152
|
if (!coords || coords.length === 0) return [];
|
|
48194
49153
|
var dataset = getBufferDataset(coords);
|
|
48195
49154
|
if (!dataset.arcs) return [];
|
|
48196
|
-
|
|
49155
|
+
var dissolveOpts = outlineDissolve ?
|
|
49156
|
+
getOutlineBufferDissolveOpts(opts) :
|
|
49157
|
+
Object.assign({}, opts, {winding_fill: true});
|
|
49158
|
+
dissolveBufferDataset2(dataset, dissolveOpts);
|
|
48197
49159
|
var lyr = dataset.layers[0];
|
|
48198
49160
|
var shape = lyr.shapes && lyr.shapes[0];
|
|
48199
49161
|
return shape ? getPolygonMultiPolygonCoords(shape, dataset.arcs) : [];
|
|
@@ -65123,7 +66085,7 @@ ${svg}
|
|
|
65123
66085
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
65124
66086
|
}
|
|
65125
66087
|
|
|
65126
|
-
var version = "0.7.
|
|
66088
|
+
var version = "0.7.34";
|
|
65127
66089
|
|
|
65128
66090
|
// Parse command line args into commands and run them
|
|
65129
66091
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|