mapshaper 0.7.51 → 0.7.53
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 +2519 -216
- package/package.json +1 -1
- package/www/mapshaper-gui.js +419 -117
- package/www/mapshaper.js +2519 -216
- package/www/page.css +5 -0
package/www/mapshaper.js
CHANGED
|
@@ -2061,6 +2061,10 @@
|
|
|
2061
2061
|
return deg * D2R$8 * R$3;
|
|
2062
2062
|
}
|
|
2063
2063
|
|
|
2064
|
+
function metersToDegrees(m) {
|
|
2065
|
+
return m / (D2R$8 * R$3);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2064
2068
|
function distance3D(ax, ay, az, bx, by, bz) {
|
|
2065
2069
|
var dx = ax - bx,
|
|
2066
2070
|
dy = ay - by,
|
|
@@ -2240,6 +2244,15 @@
|
|
|
2240
2244
|
return dist * R$3;
|
|
2241
2245
|
}
|
|
2242
2246
|
|
|
2247
|
+
// Fast local meters distance for geographic decimal-degree coordinates.
|
|
2248
|
+
// Equirectangular; one cosine. Prefer greatCircleDistance only when
|
|
2249
|
+
// global-scale accuracy matters.
|
|
2250
|
+
function fastLonLatDistance(lng1, lat1, lng2, lat2) {
|
|
2251
|
+
var dx = (lng2 - lng1) * Math.cos(lat1 * D2R$8);
|
|
2252
|
+
var dy = lat2 - lat1;
|
|
2253
|
+
return degreesToMeters(Math.sqrt(dx * dx + dy * dy));
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2243
2256
|
|
|
2244
2257
|
function triangleArea(ax, ay, bx, by, cx, cy) {
|
|
2245
2258
|
var area = Math.abs(((ay - cy) * (bx - cx) + (by - cy) * (cx - ax)) / 2);
|
|
@@ -2348,9 +2361,11 @@
|
|
|
2348
2361
|
distance3D: distance3D,
|
|
2349
2362
|
distanceSq: distanceSq,
|
|
2350
2363
|
distanceSq3D: distanceSq3D,
|
|
2364
|
+
fastLonLatDistance: fastLonLatDistance,
|
|
2351
2365
|
greatCircleDistance: greatCircleDistance,
|
|
2352
2366
|
innerAngle2: innerAngle2,
|
|
2353
2367
|
lngLatToXYZ: lngLatToXYZ,
|
|
2368
|
+
metersToDegrees: metersToDegrees,
|
|
2354
2369
|
pointSegDistSq: pointSegDistSq,
|
|
2355
2370
|
pointSegDistSq2: pointSegDistSq2,
|
|
2356
2371
|
pointSegDistSq3D: pointSegDistSq3D,
|
|
@@ -17991,7 +18006,10 @@
|
|
|
17991
18006
|
k = 1;
|
|
17992
18007
|
} else if (fromParam && !fromCRS) {
|
|
17993
18008
|
// known param units, unknown CRS -- error condition, not convertible
|
|
17994
|
-
stop$1('Unable to convert', paramUnits,
|
|
18009
|
+
stop$1('Unable to convert', paramUnits,
|
|
18010
|
+
'to unknown coordinates: the data has no coordinate reference system. ' +
|
|
18011
|
+
'Give one with -proj init=<crs> (e.g. -proj init=EPSG:3857), ' +
|
|
18012
|
+
'or use a distance in the data\'s own units.');
|
|
17995
18013
|
} else if (!fromParam && fromCRS) {
|
|
17996
18014
|
// unknown param units, known CRS -- assume param in meters (bw compatibility)
|
|
17997
18015
|
k = 1 / fromCRS;
|
|
@@ -18117,10 +18135,47 @@
|
|
|
18117
18135
|
parseSizeParam: parseSizeParam
|
|
18118
18136
|
});
|
|
18119
18137
|
|
|
18120
|
-
//
|
|
18121
|
-
//
|
|
18122
|
-
|
|
18138
|
+
// Default gap-width as a fraction of the layer's median polygon-ring segment
|
|
18139
|
+
// length. Shared by interior fill and exterior close-outer-gaps.
|
|
18140
|
+
var GAP_WIDTH_SEGMENT_FRACTION = 0.01;
|
|
18141
|
+
|
|
18142
|
+
// Used by -clean -dissolve -filter-slivers -filter-islands to generate filters
|
|
18143
|
+
// for removing small polygon rings / filling mosaic gaps.
|
|
18144
|
+
// Prefers gap_width= (including 'auto'). Legacy gap_fill_area / sliver_control
|
|
18145
|
+
// remain for compatibility when gap_width is not set.
|
|
18123
18146
|
function getSliverFilter(lyr, dataset, opts) {
|
|
18147
|
+
opts = opts || {};
|
|
18148
|
+
if (opts.gap_width != null) {
|
|
18149
|
+
return getGapWidthFilter(lyr, dataset, opts);
|
|
18150
|
+
}
|
|
18151
|
+
return getLegacyAreaFilter(lyr, dataset, opts);
|
|
18152
|
+
}
|
|
18153
|
+
|
|
18154
|
+
function getGapWidthFilter(lyr, dataset, opts) {
|
|
18155
|
+
var crs = getDatasetCRS(dataset);
|
|
18156
|
+
var widthArg = opts.gap_width;
|
|
18157
|
+
if (+widthArg === 0) {
|
|
18158
|
+
return {
|
|
18159
|
+
filter: function() { return false; },
|
|
18160
|
+
threshold: 0,
|
|
18161
|
+
label: '0 width threshold'
|
|
18162
|
+
};
|
|
18163
|
+
}
|
|
18164
|
+
var threshold = (widthArg === 'auto') ?
|
|
18165
|
+
getDefaultGapWidth(lyr, dataset.arcs) :
|
|
18166
|
+
convertDistanceParam(widthArg, crs);
|
|
18167
|
+
var filter = getGapWidthTest(dataset.arcs, threshold);
|
|
18168
|
+
if (opts.keep_shapes) {
|
|
18169
|
+
filter = keepShapes(filter);
|
|
18170
|
+
}
|
|
18171
|
+
return {
|
|
18172
|
+
threshold: threshold,
|
|
18173
|
+
filter: filter,
|
|
18174
|
+
label: getGapWidthLabel(threshold, crs)
|
|
18175
|
+
};
|
|
18176
|
+
}
|
|
18177
|
+
|
|
18178
|
+
function getLegacyAreaFilter(lyr, dataset, opts) {
|
|
18124
18179
|
var areaArg = opts.min_gap_area || opts.min_area || opts.gap_fill_area;
|
|
18125
18180
|
if (+areaArg == 0) {
|
|
18126
18181
|
return {
|
|
@@ -18128,7 +18183,7 @@
|
|
|
18128
18183
|
threshold: 0
|
|
18129
18184
|
};
|
|
18130
18185
|
}
|
|
18131
|
-
var sliverControl = opts.sliver_control >= 0 ? opts.sliver_control : 0;
|
|
18186
|
+
var sliverControl = opts.sliver_control >= 0 ? opts.sliver_control : 0;
|
|
18132
18187
|
var crs = getDatasetCRS(dataset);
|
|
18133
18188
|
var threshold = areaArg && areaArg != 'auto' ?
|
|
18134
18189
|
convertAreaParam(areaArg, crs) :
|
|
@@ -18152,7 +18207,6 @@
|
|
|
18152
18207
|
function keepShapes(filter) {
|
|
18153
18208
|
var flags;
|
|
18154
18209
|
return function(path, pathId, paths) {
|
|
18155
|
-
// console.log("keepShapes()", path, pathId, paths)
|
|
18156
18210
|
if (pathId === 0) {
|
|
18157
18211
|
flags = paths.map(path => filter(path));
|
|
18158
18212
|
}
|
|
@@ -18171,6 +18225,19 @@
|
|
|
18171
18225
|
return areaStr + ' threshold';
|
|
18172
18226
|
}
|
|
18173
18227
|
|
|
18228
|
+
function getGapWidthLabel(width, crs) {
|
|
18229
|
+
var meters = width;
|
|
18230
|
+
if (crs && crs.to_meter > 0 && !crs.is_latlong) {
|
|
18231
|
+
meters = width * crs.to_meter;
|
|
18232
|
+
}
|
|
18233
|
+
if (crs && (crs.is_latlong || crs.to_meter > 0)) {
|
|
18234
|
+
if (meters < 1) return roundToSignificantDigits(meters * 1000, 2) + 'mm width threshold';
|
|
18235
|
+
if (meters < 1000) return roundToSignificantDigits(meters, 2) + 'm width threshold';
|
|
18236
|
+
return roundToSignificantDigits(meters / 1000, 2) + 'km width threshold';
|
|
18237
|
+
}
|
|
18238
|
+
return roundToSignificantDigits(width, 2) + ' width threshold';
|
|
18239
|
+
}
|
|
18240
|
+
|
|
18174
18241
|
function getMinAreaTest(minArea, dataset) {
|
|
18175
18242
|
var pathArea = dataset.arcs.isPlanar() ? geom.getPlanarPathArea : geom.getSphericalPathArea;
|
|
18176
18243
|
return function(path) {
|
|
@@ -18179,6 +18246,19 @@
|
|
|
18179
18246
|
};
|
|
18180
18247
|
}
|
|
18181
18248
|
|
|
18249
|
+
// Fill when characteristic width 2A/P is below the threshold. Equivalent to
|
|
18250
|
+
// the old sliver-control=1 effective-area test when threshold = sqrt(A0/π).
|
|
18251
|
+
function getGapWidthTest(arcs, maxWidth) {
|
|
18252
|
+
var pathArea = arcs.isPlanar() ? geom.getPlanarPathArea : geom.getSphericalPathArea;
|
|
18253
|
+
var pathPerim = arcs.isPlanar() ? geom.getPlanarPathPerimeter : geom.getSphericalPathPerimeter;
|
|
18254
|
+
return function(ring) {
|
|
18255
|
+
var area = Math.abs(pathArea(ring, arcs));
|
|
18256
|
+
var perim = pathPerim(ring, arcs);
|
|
18257
|
+
if (!(perim > 0)) return false;
|
|
18258
|
+
return 2 * area / perim < maxWidth;
|
|
18259
|
+
};
|
|
18260
|
+
}
|
|
18261
|
+
|
|
18182
18262
|
function getSliverTest(arcs, threshold, strength) {
|
|
18183
18263
|
if (strength >= 0 === false) {
|
|
18184
18264
|
strength = 1; // default is 1 (full-strength)
|
|
@@ -18204,10 +18284,46 @@
|
|
|
18204
18284
|
};
|
|
18205
18285
|
}
|
|
18206
18286
|
|
|
18287
|
+
// Median polygon-ring segment length * GAP_WIDTH_SEGMENT_FRACTION.
|
|
18288
|
+
function getDefaultGapWidth(lyr, arcs) {
|
|
18289
|
+
return getMedianPolygonSegmentLength(lyr, arcs) * GAP_WIDTH_SEGMENT_FRACTION;
|
|
18290
|
+
}
|
|
18291
|
+
|
|
18292
|
+
function getMedianPolygonSegmentLength(lyr, arcs) {
|
|
18293
|
+
var lengths = collectPolygonSegmentLengths(lyr, arcs);
|
|
18294
|
+
if (lengths.length === 0) return 0;
|
|
18295
|
+
lengths.sort(); // numeric sort: lengths is a Float64Array
|
|
18296
|
+
return lengths[Math.floor(lengths.length / 2)];
|
|
18297
|
+
}
|
|
18298
|
+
|
|
18299
|
+
// Returns a Float64Array, so that the median can use a comparator-free numeric
|
|
18300
|
+
// sort. This measures every segment of every polygon ring, so on a detailed
|
|
18301
|
+
// mosaic the sort dominates.
|
|
18302
|
+
function collectPolygonSegmentLengths(lyr, arcs) {
|
|
18303
|
+
var lengths = new Float64Array(1024);
|
|
18304
|
+
var n = 0;
|
|
18305
|
+
var calcLen = arcs.isPlanar() ? distance2D$1 : fastLonLatDistance;
|
|
18306
|
+
var onSeg = function(i, j, xx, yy) {
|
|
18307
|
+
var len = calcLen(xx[i], yy[i], xx[j], yy[j]);
|
|
18308
|
+
if (len > 0 && isFinite(len)) {
|
|
18309
|
+
if (n === lengths.length) {
|
|
18310
|
+
var grown = new Float64Array(n * 2);
|
|
18311
|
+
grown.set(lengths);
|
|
18312
|
+
lengths = grown;
|
|
18313
|
+
}
|
|
18314
|
+
lengths[n++] = len;
|
|
18315
|
+
}
|
|
18316
|
+
};
|
|
18317
|
+
editShapes(lyr.shapes, function(path) {
|
|
18318
|
+
forEachSegmentInPath(path, arcs, onSeg);
|
|
18319
|
+
});
|
|
18320
|
+
return lengths.subarray(0, n);
|
|
18321
|
+
}
|
|
18322
|
+
|
|
18207
18323
|
// Calculate a default area threshold using average segment length,
|
|
18208
18324
|
// but increase the threshold for high-detail datasets and decrease it for
|
|
18209
18325
|
// low-detail datasets (using segments per ring as a measure of detail).
|
|
18210
|
-
//
|
|
18326
|
+
// Kept for legacy gap_fill_area=auto.
|
|
18211
18327
|
function getDefaultSliverThreshold(lyr, arcs) {
|
|
18212
18328
|
var ringCount = 0;
|
|
18213
18329
|
var calcLen = arcs.isPlanar() ? geom.distance2D : geom.greatCircleDistance;
|
|
@@ -18250,8 +18366,13 @@
|
|
|
18250
18366
|
|
|
18251
18367
|
var Slivers = /*#__PURE__*/Object.freeze({
|
|
18252
18368
|
__proto__: null,
|
|
18369
|
+
GAP_WIDTH_SEGMENT_FRACTION: GAP_WIDTH_SEGMENT_FRACTION,
|
|
18253
18370
|
calcMaxSliverArea: calcMaxSliverArea,
|
|
18371
|
+
getDefaultGapWidth: getDefaultGapWidth,
|
|
18254
18372
|
getDefaultSliverThreshold: getDefaultSliverThreshold,
|
|
18373
|
+
getGapWidthLabel: getGapWidthLabel,
|
|
18374
|
+
getGapWidthTest: getGapWidthTest,
|
|
18375
|
+
getMedianPolygonSegmentLength: getMedianPolygonSegmentLength,
|
|
18255
18376
|
getSliverAreaFunction: getSliverAreaFunction,
|
|
18256
18377
|
getSliverFilter: getSliverFilter,
|
|
18257
18378
|
getSliverTest: getSliverTest
|
|
@@ -18843,7 +18964,13 @@
|
|
|
18843
18964
|
// Look for even better fit close to best-fit point
|
|
18844
18965
|
p2 = probeForBestAnchorPoint(shp, arcs, p.x - hstep / 2,
|
|
18845
18966
|
p.x + hstep / 2, 2, weight);
|
|
18846
|
-
|
|
18967
|
+
// The refinement can come back empty even though the coarse sweep did not,
|
|
18968
|
+
// because it samples only two x-values inside a narrow band. On a ring that
|
|
18969
|
+
// encloses no real area -- a collapsed sliver that runs out to a point and
|
|
18970
|
+
// back along itself, which a polygon buffer readily produces -- a ray at
|
|
18971
|
+
// either of those two can enclose no interval at all. The coarse result
|
|
18972
|
+
// already in hand stands in that case.
|
|
18973
|
+
if (p2 && p2.distance > p.distance) {
|
|
18847
18974
|
p = p2;
|
|
18848
18975
|
}
|
|
18849
18976
|
}
|
|
@@ -18912,16 +19039,42 @@
|
|
|
18912
19039
|
return p;
|
|
18913
19040
|
}
|
|
18914
19041
|
|
|
19042
|
+
// Ceiling on the vertical scan below, which is otherwise unbounded: the scan
|
|
19043
|
+
// walks its chord in steps of vstep, so it runs for roughly the shape's height
|
|
19044
|
+
// over vstep, and nothing constrains that ratio. A tall hairline sliver -- the
|
|
19045
|
+
// kind a buffer mosaic produces between two nearly-coincident arcs -- makes it
|
|
19046
|
+
// astronomical.
|
|
19047
|
+
//
|
|
19048
|
+
// Truncating is safe rather than free: the scan only replaces p when it finds a
|
|
19049
|
+
// strictly better point, so stopping early keeps the best point so far, which
|
|
19050
|
+
// is still inside the polygon -- but on a shape thin enough to need more steps
|
|
19051
|
+
// than this, that point can differ from the one an unbounded scan would reach
|
|
19052
|
+
// (measured on a 1.1m x 111km rectangle: same polygon, ~12% less clearance).
|
|
19053
|
+
// The ceiling does bind on real data -- two of the -clean fixtures reach
|
|
19054
|
+
// ~180,000 steps here -- yet cutting every scan off at 10,000 left all 744
|
|
19055
|
+
// buffer fixture cases byte-identical, because the long tail is usually a
|
|
19056
|
+
// plateau the scan walks without improving on.
|
|
19057
|
+
var MAX_SCAN_STEPS = 10000;
|
|
19058
|
+
|
|
18915
19059
|
// Try to find a better-fit point than @p by scanning vertically
|
|
18916
19060
|
// Modify p in-place
|
|
18917
19061
|
function scanForBetterPoint(p, shp, arcs, vstep, weight) {
|
|
18918
19062
|
var x = p.x,
|
|
18919
19063
|
y = p.y,
|
|
18920
19064
|
dmax = p.distance,
|
|
18921
|
-
d;
|
|
18922
|
-
|
|
18923
|
-
|
|
18924
|
-
y
|
|
19065
|
+
d, y2;
|
|
19066
|
+
|
|
19067
|
+
for (var i = 0; i < MAX_SCAN_STEPS; i++) {
|
|
19068
|
+
y2 = y + vstep;
|
|
19069
|
+
// Stop when the step cannot move the point at all. vstep is the width of
|
|
19070
|
+
// the probe band, and on a shape narrower than the floating-point spacing
|
|
19071
|
+
// at its own coordinates that band collapses to nothing (see the
|
|
19072
|
+
// refinement probe in findAnchorPoint2), leaving y + vstep === y. Every
|
|
19073
|
+
// iteration would then retest the same point forever: a collapsed mosaic
|
|
19074
|
+
// tile 20nm wide, at longitude -119, hung here indefinitely rather than
|
|
19075
|
+
// merely running slowly.
|
|
19076
|
+
if (y2 === y) break;
|
|
19077
|
+
y = y2;
|
|
18925
19078
|
d = geom.getPointToShapeDistance(x, y, shp, arcs) * weight(x, y);
|
|
18926
19079
|
// overcome vary small local minima
|
|
18927
19080
|
if (d > dmax * 0.90 && geom.testPointInPolygon(x, y, shp, arcs)) {
|
|
@@ -21677,7 +21830,22 @@
|
|
|
21677
21830
|
var tile = mosaic[tileId];
|
|
21678
21831
|
return filter(tile[0]); // test tile ring, ignoring any holes (does this matter?)
|
|
21679
21832
|
});
|
|
21680
|
-
|
|
21833
|
+
// Choose every tile's owner before filling any of them. A tile that has just
|
|
21834
|
+
// been filled would otherwise present itself to the next one as a feature
|
|
21835
|
+
// boundary, so a gap divided into sections could award every section to
|
|
21836
|
+
// whichever feature received the first -- along a cut as long as the
|
|
21837
|
+
// boundaries it was meant to divide between.
|
|
21838
|
+
var chosen = filledIds.map(findAdjacentShapeId);
|
|
21839
|
+
var deferredIds = [];
|
|
21840
|
+
filledIds.forEach(function(tileId, i) {
|
|
21841
|
+
if (chosen[i] > -1) {
|
|
21842
|
+
tileShapeIndex.addTileToShape(chosen[i], tileId);
|
|
21843
|
+
} else {
|
|
21844
|
+
deferredIds.push(tileId);
|
|
21845
|
+
}
|
|
21846
|
+
});
|
|
21847
|
+
// A tile bordered only by other gaps can be filled once they have been.
|
|
21848
|
+
deferredIds.forEach(assignTileToAdjacentShape);
|
|
21681
21849
|
return {
|
|
21682
21850
|
removed: filledIds.length,
|
|
21683
21851
|
remaining: remainingIds.length - filledIds.length
|
|
@@ -21688,6 +21856,26 @@
|
|
|
21688
21856
|
return tileShapeIndex.getUnusedTileIds().map(tileIdToTile);
|
|
21689
21857
|
};
|
|
21690
21858
|
|
|
21859
|
+
// Return unused tiles together with the source-feature owner of each boundary
|
|
21860
|
+
// arc. Used by -clean to partition multi-owner gaps before assigning them.
|
|
21861
|
+
this.getUnusedTileData = function(filter) {
|
|
21862
|
+
return tileShapeIndex.getUnusedTileIds().reduce(function(out, tileId) {
|
|
21863
|
+
var tile = mosaic[tileId];
|
|
21864
|
+
if (filter && !filter(tile[0])) return out;
|
|
21865
|
+
var boundary = [];
|
|
21866
|
+
tile[0].forEach(function(arcId) {
|
|
21867
|
+
var neighborTileId = arcTileIndex.getShapeIdByArcId(~arcId);
|
|
21868
|
+
var shapeId = neighborTileId < 0 ? -1 :
|
|
21869
|
+
tileShapeIndex.getShapeIdByTileId(neighborTileId);
|
|
21870
|
+
if (shapeId >= 0) {
|
|
21871
|
+
boundary.push({arcId: arcId, shapeId: shapeId});
|
|
21872
|
+
}
|
|
21873
|
+
});
|
|
21874
|
+
out.push({tileId: tileId, tile: tile, boundary: boundary});
|
|
21875
|
+
return out;
|
|
21876
|
+
}, []);
|
|
21877
|
+
};
|
|
21878
|
+
|
|
21691
21879
|
this.getTilesByShapeIds = function(shapeIds) {
|
|
21692
21880
|
return getTileIdsByShapeIds(shapeIds).map(tileIdToTile);
|
|
21693
21881
|
};
|
|
@@ -22137,7 +22325,9 @@
|
|
|
22137
22325
|
return mosaic[id];
|
|
22138
22326
|
}
|
|
22139
22327
|
|
|
22140
|
-
|
|
22328
|
+
// The shape sharing the longest boundary with a tile, or -1 if the tile is
|
|
22329
|
+
// bordered only by unassigned tiles.
|
|
22330
|
+
function findAdjacentShapeId(tileId) {
|
|
22141
22331
|
var ring = mosaic[tileId][0];
|
|
22142
22332
|
var arcs = nodes.arcs;
|
|
22143
22333
|
var arcId, neighborShapeId, neighborTileId, arcLen;
|
|
@@ -22154,6 +22344,11 @@
|
|
|
22154
22344
|
maxArcLen = arcLen;
|
|
22155
22345
|
}
|
|
22156
22346
|
}
|
|
22347
|
+
return shapeId;
|
|
22348
|
+
}
|
|
22349
|
+
|
|
22350
|
+
function assignTileToAdjacentShape(tileId) {
|
|
22351
|
+
var shapeId = findAdjacentShapeId(tileId);
|
|
22157
22352
|
if (shapeId > -1) {
|
|
22158
22353
|
tileShapeIndex.addTileToShape(shapeId, tileId);
|
|
22159
22354
|
}
|
|
@@ -22179,6 +22374,26 @@
|
|
|
22179
22374
|
}
|
|
22180
22375
|
}
|
|
22181
22376
|
|
|
22377
|
+
// Flags, by absolute arc id, for arcs with no mosaic tile on one of their sides:
|
|
22378
|
+
// the border between the mosaic and the space outside it. Space that polygons
|
|
22379
|
+
// enclose becomes a tile, however narrow, so an arc facing untiled space faces
|
|
22380
|
+
// the outside, and a gap there is one that gap filling cannot reach.
|
|
22381
|
+
//
|
|
22382
|
+
// Only the mosaic is needed, not the tile assignment a MosaicIndex goes on to
|
|
22383
|
+
// make, so this builds the cheaper half on its own.
|
|
22384
|
+
function getOutsideFacingArcFlags(nodes) {
|
|
22385
|
+
var mosaic = buildPolygonMosaic(nodes).mosaic;
|
|
22386
|
+
var arcTileIndex = new ShapeArcIndex(mosaic, nodes.arcs);
|
|
22387
|
+
var flags = new Uint8Array(nodes.arcs.size());
|
|
22388
|
+
for (var i = 0; i < flags.length; i++) {
|
|
22389
|
+
if (arcTileIndex.getShapeIdByArcId(i) < 0 ||
|
|
22390
|
+
arcTileIndex.getShapeIdByArcId(~i) < 0) {
|
|
22391
|
+
flags[i] = 1;
|
|
22392
|
+
}
|
|
22393
|
+
}
|
|
22394
|
+
return flags;
|
|
22395
|
+
}
|
|
22396
|
+
|
|
22182
22397
|
// Map arc ids to shape ids, assuming perfect topology
|
|
22183
22398
|
// (an arcId maps to at most one shape)
|
|
22184
22399
|
// Supports looking up a shape id using an arc id.
|
|
@@ -22209,7 +22424,8 @@
|
|
|
22209
22424
|
var MosaicIndex$1 = /*#__PURE__*/Object.freeze({
|
|
22210
22425
|
__proto__: null,
|
|
22211
22426
|
MosaicIndex: MosaicIndex,
|
|
22212
|
-
ShapeArcIndex: ShapeArcIndex
|
|
22427
|
+
ShapeArcIndex: ShapeArcIndex,
|
|
22428
|
+
getOutsideFacingArcFlags: getOutsideFacingArcFlags
|
|
22213
22429
|
});
|
|
22214
22430
|
|
|
22215
22431
|
// Assumes that arcs do not intersect except at endpoints
|
|
@@ -22256,6 +22472,13 @@
|
|
|
22256
22472
|
profileStart('dissolvePolygonGroups2');
|
|
22257
22473
|
profileStart('dpg2.NodeCollection');
|
|
22258
22474
|
var arcFilter = getArcPresenceTest(lyr.shapes, dataset.arcs);
|
|
22475
|
+
if (opts._mosaic_cut_arcs) {
|
|
22476
|
+
var layerArcFilter = arcFilter;
|
|
22477
|
+
arcFilter = function(arcId) {
|
|
22478
|
+
var id = arcId < 0 ? ~arcId : arcId;
|
|
22479
|
+
return layerArcFilter(arcId) || opts._mosaic_cut_arcs[id];
|
|
22480
|
+
};
|
|
22481
|
+
}
|
|
22259
22482
|
var nodes = new NodeCollection(dataset.arcs, arcFilter);
|
|
22260
22483
|
profileEnd('dpg2.NodeCollection');
|
|
22261
22484
|
var mosaicOpts = {
|
|
@@ -22267,12 +22490,17 @@
|
|
|
22267
22490
|
var mosaicIndex = new MosaicIndex(lyr, nodes, mosaicOpts);
|
|
22268
22491
|
profileEnd('dpg2.MosaicIndex');
|
|
22269
22492
|
// gap fill doesn't work yet with overlapping shapes
|
|
22270
|
-
var fillGaps = !opts.allow_overlaps &&
|
|
22493
|
+
var fillGaps = !opts.allow_overlaps &&
|
|
22494
|
+
(opts.gap_width || opts.sliver_control || opts.gap_fill_area);
|
|
22271
22495
|
var cleanupData, filterData;
|
|
22272
22496
|
if (fillGaps) {
|
|
22273
22497
|
profileStart('dpg2.removeGaps');
|
|
22274
|
-
var
|
|
22275
|
-
|
|
22498
|
+
var filterOpts = utils.extend({}, opts);
|
|
22499
|
+
// Legacy area path: keep the historical default of full sliver control.
|
|
22500
|
+
if (filterOpts.gap_width == null && filterOpts.sliver_control == null) {
|
|
22501
|
+
filterOpts.sliver_control = 1;
|
|
22502
|
+
}
|
|
22503
|
+
filterData = getSliverFilter(lyr, dataset, filterOpts);
|
|
22276
22504
|
cleanupData = mosaicIndex.removeGaps(filterData.filter);
|
|
22277
22505
|
profileEnd('dpg2.removeGaps');
|
|
22278
22506
|
}
|
|
@@ -22513,6 +22741,1245 @@
|
|
|
22513
22741
|
target.push.apply(target, source);
|
|
22514
22742
|
}
|
|
22515
22743
|
|
|
22744
|
+
// Two boundaries that should be one, but were digitized or computed twice, drift
|
|
22745
|
+
// apart by more than the single interval that ordinary vertex snapping repairs,
|
|
22746
|
+
// and their vertices are staggered, so no pairwise snapping ever sees them. In
|
|
22747
|
+
// the test corpus the worst of them sit ~20 intervals apart. This multiple keeps
|
|
22748
|
+
// the default repair at sub-micron scale, far below any gap a map could show,
|
|
22749
|
+
// while still catching them.
|
|
22750
|
+
var PRECISION_SEAM_INTERVALS = 100;
|
|
22751
|
+
|
|
22752
|
+
// Minimum accepted seam length, as a multiple of the median segment length. Two
|
|
22753
|
+
// boundaries that merely pass close to each other for a vertex or two are not a
|
|
22754
|
+
// seam.
|
|
22755
|
+
var MIN_SEAM_SEGMENT_FACTOR = 3;
|
|
22756
|
+
|
|
22757
|
+
// Collapse duplicate boundaries: two arcs carrying what should be one line,
|
|
22758
|
+
// digitized or computed twice and drifting apart by no more than floating-point
|
|
22759
|
+
// rounding (see PRECISION_SEAM_INTERVALS). Vertices on both banks are snapped to
|
|
22760
|
+
// the line midway between them, leaving a single shared boundary.
|
|
22761
|
+
//
|
|
22762
|
+
// Runs unconditionally. Left in place, such a seam becomes an enclosed sliver
|
|
22763
|
+
// that gap filling awards to one neighbor, giving that feature a zero-width spike
|
|
22764
|
+
// along the shared border.
|
|
22765
|
+
//
|
|
22766
|
+
// Returns the number of duplicate boundaries collapsed (see countConnectedGaps).
|
|
22767
|
+
function collapseDuplicateBoundaries(layers, dataset) {
|
|
22768
|
+
var polygonLayers = layers.filter(function(lyr) {
|
|
22769
|
+
return lyr.geometry_type == 'polygon' && lyr.shapes && lyr.shapes.length > 1;
|
|
22770
|
+
});
|
|
22771
|
+
if (polygonLayers.length === 0 || !dataset.arcs) return 0;
|
|
22772
|
+
|
|
22773
|
+
var spherical = isLatLngCRS(getDatasetCRS(dataset));
|
|
22774
|
+
var changed = 0;
|
|
22775
|
+
polygonLayers.forEach(function(lyr) {
|
|
22776
|
+
changed += collapseLayerDuplicates(lyr, dataset.arcs, spherical);
|
|
22777
|
+
});
|
|
22778
|
+
if (changed > 0) {
|
|
22779
|
+
rebuildTopology(dataset);
|
|
22780
|
+
verbose(utils.format('Closed %s duplicate boundar%s', changed,
|
|
22781
|
+
changed == 1 ? 'y' : 'ies'));
|
|
22782
|
+
}
|
|
22783
|
+
return changed;
|
|
22784
|
+
}
|
|
22785
|
+
|
|
22786
|
+
function collapseLayerDuplicates(lyr, arcs, spherical) {
|
|
22787
|
+
var medianSeg = measureMedianSegment(lyr, arcs);
|
|
22788
|
+
var distance = getPrecisionSeamDistance(arcs.getBounds().toArray(), spherical);
|
|
22789
|
+
if (!(distance > 0)) return 0;
|
|
22790
|
+
|
|
22791
|
+
// Keep the automatic search radius when the closing distance is smaller. This
|
|
22792
|
+
// lets us see the complete seam and reject it as a unit instead of partially
|
|
22793
|
+
// snapping the already-near portion while leaving its wider mouth open. A
|
|
22794
|
+
// duplicate boundary often has a mouth several orders of magnitude wider than
|
|
22795
|
+
// the seam behind it.
|
|
22796
|
+
var searchDistance = Math.max(distance, medianSeg * GAP_WIDTH_SEGMENT_FRACTION);
|
|
22797
|
+
var found = findLayerSeams(lyr, arcs, distance, searchDistance, spherical,
|
|
22798
|
+
medianSeg * MIN_SEAM_SEGMENT_FACTOR, null);
|
|
22799
|
+
if (!found) return 0;
|
|
22800
|
+
var snapped = profileWrap('cg.snapSeams', function() {
|
|
22801
|
+
return snapSeams(found.seams, found.seeds, found.arcsById, arcs,
|
|
22802
|
+
found.footpoints, distance);
|
|
22803
|
+
});
|
|
22804
|
+
return countConnectedGaps(snapped);
|
|
22805
|
+
}
|
|
22806
|
+
|
|
22807
|
+
// A crack that opens onto the space outside the mosaic is not enclosed by
|
|
22808
|
+
// polygons, so no tile covers it and gap filling cannot reach it. Snapping the
|
|
22809
|
+
// facing pair of vertices at each open end together encloses the crack, handing
|
|
22810
|
+
// it to the mosaic as an ordinary gap to be filled -- or divided between its
|
|
22811
|
+
// neighbors, where it borders three or more of them -- like any other.
|
|
22812
|
+
//
|
|
22813
|
+
// Only those pairs move: two coordinates per mouth, each by half the width of the
|
|
22814
|
+
// mouth. Interior gaps are left exactly as they were found, so they are filled the
|
|
22815
|
+
// same way whether or not this pass runs.
|
|
22816
|
+
//
|
|
22817
|
+
// Returns the number of mouths pinched shut.
|
|
22818
|
+
function pinchOuterCrackMouths(lyr, dataset, nodes, opts) {
|
|
22819
|
+
var arcs = dataset.arcs;
|
|
22820
|
+
if (!lyr.shapes || lyr.shapes.length < 2 || !arcs) return 0;
|
|
22821
|
+
var crs = getDatasetCRS(dataset);
|
|
22822
|
+
var spherical = isLatLngCRS(crs);
|
|
22823
|
+
var medianSeg = measureMedianSegment(lyr, arcs);
|
|
22824
|
+
// Same width as interior gap filling, from the same automatic default.
|
|
22825
|
+
var distance = resolveCloseDistance(opts, crs,
|
|
22826
|
+
medianSeg * GAP_WIDTH_SEGMENT_FRACTION);
|
|
22827
|
+
if (!(distance > 0)) return 0;
|
|
22828
|
+
|
|
22829
|
+
profileStart('cg.outsideFacingArcs');
|
|
22830
|
+
var outsideFacing = getOutsideFacingArcFlags(nodes);
|
|
22831
|
+
profileEnd('cg.outsideFacingArcs');
|
|
22832
|
+
var found = findLayerSeams(lyr, arcs, distance, distance, spherical,
|
|
22833
|
+
medianSeg * MIN_SEAM_SEGMENT_FACTOR, function(arcId) {
|
|
22834
|
+
return outsideFacing[arcId] === 1;
|
|
22835
|
+
});
|
|
22836
|
+
if (!found) return 0;
|
|
22837
|
+
|
|
22838
|
+
var pinched = profileWrap('cg.pinchMouths', function() {
|
|
22839
|
+
return pinchSeamMouths(found.seams, found.arcsById, arcs, nodes,
|
|
22840
|
+
found.footpoints, distance, spherical);
|
|
22841
|
+
});
|
|
22842
|
+
if (pinched > 0) {
|
|
22843
|
+
rebuildTopology(dataset);
|
|
22844
|
+
message(utils.format('Closed %s external gap%s', pinched,
|
|
22845
|
+
utils.pluralSuffix(pinched)));
|
|
22846
|
+
}
|
|
22847
|
+
return pinched;
|
|
22848
|
+
}
|
|
22849
|
+
|
|
22850
|
+
// Seams between facing arcs owned by different features: stretches where two
|
|
22851
|
+
// boundaries run within @distance of each other for at least @minSeamLength.
|
|
22852
|
+
// @keepArc, when given, limits which arcs are considered.
|
|
22853
|
+
function findLayerSeams(lyr, arcs, distance, searchDistance, spherical,
|
|
22854
|
+
minSeamLength, keepArc) {
|
|
22855
|
+
profileStart('cg.collectVertices');
|
|
22856
|
+
var owners = getArcOwners(lyr.shapes, arcs.size());
|
|
22857
|
+
var records = collectExternalVertices(arcs, owners, keepArc);
|
|
22858
|
+
profileEnd('cg.collectVertices');
|
|
22859
|
+
if (records.length < 4) return null;
|
|
22860
|
+
|
|
22861
|
+
profileStart('cg.findMouthSeeds');
|
|
22862
|
+
var seeds = findMouthSeeds(records, searchDistance, spherical);
|
|
22863
|
+
profileEnd('cg.findMouthSeeds');
|
|
22864
|
+
if (seeds.length === 0) return null;
|
|
22865
|
+
|
|
22866
|
+
var arcsById = indexArcRecords(records);
|
|
22867
|
+
var footpoints = new ArcFootpoints(arcs, spherical);
|
|
22868
|
+
profileStart('cg.growSeams');
|
|
22869
|
+
var seams = growSeamsFromSeeds(seeds, arcsById, footpoints, distance, spherical,
|
|
22870
|
+
minSeamLength);
|
|
22871
|
+
profileEnd('cg.growSeams');
|
|
22872
|
+
if (seams.length === 0) return null;
|
|
22873
|
+
return {seams: seams, seeds: seeds, arcsById: arcsById, footpoints: footpoints};
|
|
22874
|
+
}
|
|
22875
|
+
|
|
22876
|
+
// Same segment median as interior gap-width=auto (all polygon-ring segments).
|
|
22877
|
+
function measureMedianSegment(lyr, arcs) {
|
|
22878
|
+
return profileWrap('cg.medianSegment', function() {
|
|
22879
|
+
return getMedianPolygonSegmentLength(lyr, arcs);
|
|
22880
|
+
});
|
|
22881
|
+
}
|
|
22882
|
+
|
|
22883
|
+
function rebuildTopology(dataset) {
|
|
22884
|
+
profileStart('cg.rebuildTopology');
|
|
22885
|
+
dataset.arcs.dedupCoords();
|
|
22886
|
+
buildTopology(dataset);
|
|
22887
|
+
profileEnd('cg.rebuildTopology');
|
|
22888
|
+
}
|
|
22889
|
+
|
|
22890
|
+
// One crack presents many seams: a seam is a stretch of two facing arcs, and
|
|
22891
|
+
// every node along either side ends one arc and starts another -- where a third
|
|
22892
|
+
// feature meets the boundary, or where an intersection cut fell. Reporting seams
|
|
22893
|
+
// would give a number several times the number of cracks there are to see, so
|
|
22894
|
+
// seams meeting along a shared arc count once.
|
|
22895
|
+
//
|
|
22896
|
+
// Two cracks that face the same arc at different stretches of it are counted as
|
|
22897
|
+
// one. Erring towards the smaller number suits a count of gaps closed better than
|
|
22898
|
+
// splitting one crack into the arcs it happens to be divided into.
|
|
22899
|
+
function countConnectedGaps(seams) {
|
|
22900
|
+
var parents = new Map();
|
|
22901
|
+
|
|
22902
|
+
function find(id) {
|
|
22903
|
+
var parent = parents.get(id);
|
|
22904
|
+
if (parent === undefined) {
|
|
22905
|
+
parents.set(id, id);
|
|
22906
|
+
return id;
|
|
22907
|
+
}
|
|
22908
|
+
while (parent !== id) {
|
|
22909
|
+
id = parent;
|
|
22910
|
+
parent = parents.get(id);
|
|
22911
|
+
}
|
|
22912
|
+
return id;
|
|
22913
|
+
}
|
|
22914
|
+
|
|
22915
|
+
seams.forEach(function(seam) {
|
|
22916
|
+
var a = find(seam.arcA);
|
|
22917
|
+
var b = find(seam.arcB);
|
|
22918
|
+
if (a !== b) parents.set(a, b);
|
|
22919
|
+
});
|
|
22920
|
+
var roots = new Set();
|
|
22921
|
+
seams.forEach(function(seam) { roots.add(find(seam.arcA)); });
|
|
22922
|
+
return roots.size;
|
|
22923
|
+
}
|
|
22924
|
+
|
|
22925
|
+
// Closing distance for the default pass, which collapses duplicate boundaries
|
|
22926
|
+
// and nothing else. @bounds is the arc bounding box in coordinate units; the seam
|
|
22927
|
+
// walk measures distances in meters when those units are degrees.
|
|
22928
|
+
function getPrecisionSeamDistance(bounds, spherical) {
|
|
22929
|
+
var interval = getHighPrecisionSnapInterval(bounds) * PRECISION_SEAM_INTERVALS;
|
|
22930
|
+
return spherical ? degreesToMeters(interval) : interval;
|
|
22931
|
+
}
|
|
22932
|
+
|
|
22933
|
+
function resolveCloseDistance(opts, crs, autoDistance) {
|
|
22934
|
+
var arg = opts.gap_width;
|
|
22935
|
+
if (arg == null || arg === 'auto') return autoDistance;
|
|
22936
|
+
if (+arg === 0) return 0;
|
|
22937
|
+
return convertDistanceParam(arg, crs);
|
|
22938
|
+
}
|
|
22939
|
+
|
|
22940
|
+
// An external arc belongs to exactly one feature. Shared arcs are excluded: they
|
|
22941
|
+
// already represent clean topology and must not be moved by this repair.
|
|
22942
|
+
function getArcOwners(shapes, arcCount) {
|
|
22943
|
+
var first = new Int32Array(arcCount);
|
|
22944
|
+
var second = new Int32Array(arcCount);
|
|
22945
|
+
for (var i = 0; i < arcCount; i++) {
|
|
22946
|
+
first[i] = -1;
|
|
22947
|
+
second[i] = -1;
|
|
22948
|
+
}
|
|
22949
|
+
traversePaths(shapes, function(o) {
|
|
22950
|
+
var id = o.arcId < 0 ? ~o.arcId : o.arcId;
|
|
22951
|
+
var owner = o.shapeId;
|
|
22952
|
+
if (first[id] == -1) first[id] = owner;
|
|
22953
|
+
else if (first[id] != owner) second[id] = owner;
|
|
22954
|
+
});
|
|
22955
|
+
return {first: first, second: second};
|
|
22956
|
+
}
|
|
22957
|
+
|
|
22958
|
+
function collectExternalVertices(arcs, owners, keepArc) {
|
|
22959
|
+
var data = arcs.getVertexData();
|
|
22960
|
+
var records = [];
|
|
22961
|
+
var offs = 0;
|
|
22962
|
+
for (var arcId = 0; arcId < data.nn.length; arcId++) {
|
|
22963
|
+
var n = data.nn[arcId];
|
|
22964
|
+
if (owners.first[arcId] >= 0 && owners.second[arcId] == -1 &&
|
|
22965
|
+
(!keepArc || keepArc(arcId))) {
|
|
22966
|
+
for (var k = 0; k < n; k++) {
|
|
22967
|
+
var i = offs + k;
|
|
22968
|
+
records.push({
|
|
22969
|
+
x: data.xx[i],
|
|
22970
|
+
y: data.yy[i],
|
|
22971
|
+
i: i,
|
|
22972
|
+
arc: arcId,
|
|
22973
|
+
k: k,
|
|
22974
|
+
owner: owners.first[arcId]
|
|
22975
|
+
});
|
|
22976
|
+
}
|
|
22977
|
+
}
|
|
22978
|
+
offs += n;
|
|
22979
|
+
}
|
|
22980
|
+
return records;
|
|
22981
|
+
}
|
|
22982
|
+
|
|
22983
|
+
function indexArcRecords(records) {
|
|
22984
|
+
var byArc = {};
|
|
22985
|
+
records.forEach(function(o) {
|
|
22986
|
+
if (!byArc[o.arc]) byArc[o.arc] = [];
|
|
22987
|
+
byArc[o.arc].push(o);
|
|
22988
|
+
});
|
|
22989
|
+
Object.keys(byArc).forEach(function(id) {
|
|
22990
|
+
byArc[id].sort(function(a, b) { return a.k - b.k; });
|
|
22991
|
+
});
|
|
22992
|
+
return byArc;
|
|
22993
|
+
}
|
|
22994
|
+
|
|
22995
|
+
// Mouth seeds: mutual nearest external vertices on different owners/arcs.
|
|
22996
|
+
// A single positive-distance pair is enough to start an edge walk.
|
|
22997
|
+
function findMouthSeeds(records, distance, spherical) {
|
|
22998
|
+
var index = new Flatbush(records.length);
|
|
22999
|
+
records.forEach(function(o) { index.add(o.x, o.y, o.x, o.y); });
|
|
23000
|
+
index.finish();
|
|
23001
|
+
|
|
23002
|
+
var nearest = new Int32Array(records.length);
|
|
23003
|
+
var distances = new Float64Array(records.length);
|
|
23004
|
+
nearest.fill(-1);
|
|
23005
|
+
records.forEach(function(a, ai) {
|
|
23006
|
+
var padY = getCoordinatePad(distance, a.y, spherical, false);
|
|
23007
|
+
var padX = getCoordinatePad(distance, a.y, spherical, true);
|
|
23008
|
+
var ids = index.search(a.x - padX, a.y - padY, a.x + padX, a.y + padY);
|
|
23009
|
+
var best = -1, bestDist = Infinity;
|
|
23010
|
+
ids.forEach(function(bi) {
|
|
23011
|
+
var b = records[bi];
|
|
23012
|
+
if (bi == ai || b.owner == a.owner || b.arc == a.arc) return;
|
|
23013
|
+
var d = spherical ?
|
|
23014
|
+
fastLonLatDistance(a.x, a.y, b.x, b.y) :
|
|
23015
|
+
distance2D$1(a.x, a.y, b.x, b.y);
|
|
23016
|
+
if (d <= distance && d < bestDist) {
|
|
23017
|
+
best = bi;
|
|
23018
|
+
bestDist = d;
|
|
23019
|
+
}
|
|
23020
|
+
});
|
|
23021
|
+
nearest[ai] = best;
|
|
23022
|
+
distances[ai] = bestDist;
|
|
23023
|
+
});
|
|
23024
|
+
|
|
23025
|
+
var seen = {};
|
|
23026
|
+
var seeds = [];
|
|
23027
|
+
records.forEach(function(a, ai) {
|
|
23028
|
+
var bi = nearest[ai];
|
|
23029
|
+
if (bi < 0 || !(distances[ai] > 0)) return;
|
|
23030
|
+
var best = records[bi];
|
|
23031
|
+
// Positive-distance matches must be mutual nearest neighbors.
|
|
23032
|
+
if (nearest[bi] != ai) return;
|
|
23033
|
+
var lo = Math.min(a.i, best.i), hi = Math.max(a.i, best.i);
|
|
23034
|
+
var key = lo + ':' + hi;
|
|
23035
|
+
if (seen[key]) return;
|
|
23036
|
+
seen[key] = true;
|
|
23037
|
+
seeds.push({a: a, b: best, distance: distances[ai]});
|
|
23038
|
+
});
|
|
23039
|
+
return seeds;
|
|
23040
|
+
}
|
|
23041
|
+
|
|
23042
|
+
function getCoordinatePad(distance, lat, spherical, longitude) {
|
|
23043
|
+
if (spherical) {
|
|
23044
|
+
var deg = distance / R$3 * R2D$8;
|
|
23045
|
+
return longitude ? deg / Math.max(Math.cos(lat / R2D$8), 0.01) : deg;
|
|
23046
|
+
}
|
|
23047
|
+
// convertDistanceParam() has already converted physical units into projected
|
|
23048
|
+
// coordinate units (or left unitless values unchanged).
|
|
23049
|
+
return distance;
|
|
23050
|
+
}
|
|
23051
|
+
|
|
23052
|
+
// Grow a seam from each mouth seed by walking both arcs while each vertex stays
|
|
23053
|
+
// within closeDistance of the opposite arc's edges. Accept only seams whose
|
|
23054
|
+
// walked length on both sides meets minSeamLength. Overlapping seeds that grow
|
|
23055
|
+
// into the same arc-pair range are merged.
|
|
23056
|
+
function growSeamsFromSeeds(seeds, arcsById, footpoints, closeDistance,
|
|
23057
|
+
spherical, minSeamLength) {
|
|
23058
|
+
var seams = [];
|
|
23059
|
+
// Seeds crowd along a crack: every facing pair of vertices along it is one, and
|
|
23060
|
+
// each grows into the same stretch of the same two arcs, since what the walk
|
|
23061
|
+
// finds is the whole run of in-tolerance vertices around the seed rather than
|
|
23062
|
+
// anything particular to where it started. Walking that run once per seed
|
|
23063
|
+
// instead of once is what made this repair take minutes on a detailed mosaic,
|
|
23064
|
+
// so a seed already inside a run grown for its own two arcs is passed over.
|
|
23065
|
+
var grownByPair = new Map();
|
|
23066
|
+
seeds.forEach(function(seed) {
|
|
23067
|
+
var key = arcPairKey(seed.a.arc, seed.b.arc);
|
|
23068
|
+
var grown = grownByPair.get(key);
|
|
23069
|
+
if (!grown) {
|
|
23070
|
+
grown = [];
|
|
23071
|
+
grownByPair.set(key, grown);
|
|
23072
|
+
}
|
|
23073
|
+
if (seamContainsSeed(grown, seed)) return;
|
|
23074
|
+
var seam = growSeamFromSeed(seed, arcsById, footpoints, closeDistance,
|
|
23075
|
+
spherical);
|
|
23076
|
+
if (!seam) return;
|
|
23077
|
+
// Runs rejected as too short are recorded too, so that the seeds along them
|
|
23078
|
+
// are not walked again either.
|
|
23079
|
+
grown.push(seam);
|
|
23080
|
+
if (seam.lengthA < minSeamLength || seam.lengthB < minSeamLength) return;
|
|
23081
|
+
seams.push(seam);
|
|
23082
|
+
});
|
|
23083
|
+
return mergeOverlappingSeams(seams);
|
|
23084
|
+
}
|
|
23085
|
+
|
|
23086
|
+
function arcPairKey(arcA, arcB) {
|
|
23087
|
+
return arcA < arcB ? arcA + '|' + arcB : arcB + '|' + arcA;
|
|
23088
|
+
}
|
|
23089
|
+
|
|
23090
|
+
function indexSeamsByArcPair(seams) {
|
|
23091
|
+
var byPair = new Map();
|
|
23092
|
+
seams.forEach(function(seam) {
|
|
23093
|
+
var key = arcPairKey(seam.arcA, seam.arcB);
|
|
23094
|
+
var group = byPair.get(key);
|
|
23095
|
+
if (!group) {
|
|
23096
|
+
group = [];
|
|
23097
|
+
byPair.set(key, group);
|
|
23098
|
+
}
|
|
23099
|
+
group.push(seam);
|
|
23100
|
+
});
|
|
23101
|
+
return byPair;
|
|
23102
|
+
}
|
|
23103
|
+
|
|
23104
|
+
function growSeamFromSeed(seed, arcsById, footpoints, closeDistance, spherical) {
|
|
23105
|
+
var sideA = arcsById[seed.a.arc];
|
|
23106
|
+
var sideB = arcsById[seed.b.arc];
|
|
23107
|
+
if (!sideA || !sideB) return null;
|
|
23108
|
+
|
|
23109
|
+
var rangeA = expandRangeOnArc(sideA, seed.a.k, sideB, footpoints, closeDistance,
|
|
23110
|
+
spherical);
|
|
23111
|
+
var rangeB = expandRangeOnArc(sideB, seed.b.k, sideA, footpoints, closeDistance,
|
|
23112
|
+
spherical);
|
|
23113
|
+
if (!rangeA || !rangeB) return null;
|
|
23114
|
+
|
|
23115
|
+
return {
|
|
23116
|
+
arcA: seed.a.arc,
|
|
23117
|
+
arcB: seed.b.arc,
|
|
23118
|
+
loA: rangeA.lo,
|
|
23119
|
+
hiA: rangeA.hi,
|
|
23120
|
+
loB: rangeB.lo,
|
|
23121
|
+
hiB: rangeB.hi,
|
|
23122
|
+
lengthA: rangeA.length,
|
|
23123
|
+
lengthB: rangeB.length
|
|
23124
|
+
};
|
|
23125
|
+
}
|
|
23126
|
+
|
|
23127
|
+
// Expand along @side from seed index @seedK while each vertex remains within
|
|
23128
|
+
// closeDistance of @other's edges. Returns the inclusive index range and the
|
|
23129
|
+
// path length covered by the accepted stretch.
|
|
23130
|
+
function expandRangeOnArc(side, seedK, other, footpoints, closeDistance, spherical) {
|
|
23131
|
+
if (seedK < 0 || seedK >= side.length) return null;
|
|
23132
|
+
if (!vertexWithinTolerance(side[seedK], other, footpoints, closeDistance)) {
|
|
23133
|
+
return null;
|
|
23134
|
+
}
|
|
23135
|
+
var lo = seedK;
|
|
23136
|
+
var hi = seedK;
|
|
23137
|
+
while (lo > 0 &&
|
|
23138
|
+
vertexWithinTolerance(side[lo - 1], other, footpoints, closeDistance)) {
|
|
23139
|
+
lo--;
|
|
23140
|
+
}
|
|
23141
|
+
while (hi + 1 < side.length &&
|
|
23142
|
+
vertexWithinTolerance(side[hi + 1], other, footpoints, closeDistance)) {
|
|
23143
|
+
hi++;
|
|
23144
|
+
}
|
|
23145
|
+
return {
|
|
23146
|
+
lo: lo,
|
|
23147
|
+
hi: hi,
|
|
23148
|
+
length: pathLengthBetween(side, lo, hi, spherical)
|
|
23149
|
+
};
|
|
23150
|
+
}
|
|
23151
|
+
|
|
23152
|
+
function vertexWithinTolerance(vertex, otherSide, footpoints, closeDistance) {
|
|
23153
|
+
return !!footpoints.closestPoint(vertex.x, vertex.y, otherSide, closeDistance);
|
|
23154
|
+
}
|
|
23155
|
+
|
|
23156
|
+
function pathLengthBetween(side, lo, hi, spherical) {
|
|
23157
|
+
var len = 0;
|
|
23158
|
+
for (var k = lo; k < hi; k++) {
|
|
23159
|
+
var a = side[k], b = side[k + 1];
|
|
23160
|
+
len += spherical ?
|
|
23161
|
+
fastLonLatDistance(a.x, a.y, b.x, b.y) :
|
|
23162
|
+
distance2D$1(a.x, a.y, b.x, b.y);
|
|
23163
|
+
}
|
|
23164
|
+
return len;
|
|
23165
|
+
}
|
|
23166
|
+
|
|
23167
|
+
function mergeOverlappingSeams(seams) {
|
|
23168
|
+
if (seams.length <= 1) return seams;
|
|
23169
|
+
var out = [];
|
|
23170
|
+
// Only seams on the same two arcs can overlap, so each one is compared against
|
|
23171
|
+
// those alone rather than against every seam found so far.
|
|
23172
|
+
var byPair = new Map();
|
|
23173
|
+
seams.forEach(function(seam) {
|
|
23174
|
+
var a = Math.min(seam.arcA, seam.arcB);
|
|
23175
|
+
var b = Math.max(seam.arcA, seam.arcB);
|
|
23176
|
+
var swapped = seam.arcA > seam.arcB;
|
|
23177
|
+
var norm = {
|
|
23178
|
+
arcA: a,
|
|
23179
|
+
arcB: b,
|
|
23180
|
+
loA: swapped ? seam.loB : seam.loA,
|
|
23181
|
+
hiA: swapped ? seam.hiB : seam.hiA,
|
|
23182
|
+
loB: swapped ? seam.loA : seam.loB,
|
|
23183
|
+
hiB: swapped ? seam.hiA : seam.hiB,
|
|
23184
|
+
lengthA: swapped ? seam.lengthB : seam.lengthA,
|
|
23185
|
+
lengthB: swapped ? seam.lengthA : seam.lengthB
|
|
23186
|
+
};
|
|
23187
|
+
var key = arcPairKey(norm.arcA, norm.arcB);
|
|
23188
|
+
var group = byPair.get(key);
|
|
23189
|
+
if (!group) {
|
|
23190
|
+
group = [];
|
|
23191
|
+
byPair.set(key, group);
|
|
23192
|
+
}
|
|
23193
|
+
var existing = group.find(function(o) {
|
|
23194
|
+
return rangesOverlap(o.loA, o.hiA, norm.loA, norm.hiA) &&
|
|
23195
|
+
rangesOverlap(o.loB, o.hiB, norm.loB, norm.hiB);
|
|
23196
|
+
});
|
|
23197
|
+
if (existing) {
|
|
23198
|
+
existing.loA = Math.min(existing.loA, norm.loA);
|
|
23199
|
+
existing.hiA = Math.max(existing.hiA, norm.hiA);
|
|
23200
|
+
existing.loB = Math.min(existing.loB, norm.loB);
|
|
23201
|
+
existing.hiB = Math.max(existing.hiB, norm.hiB);
|
|
23202
|
+
existing.lengthA = Math.max(existing.lengthA, norm.lengthA);
|
|
23203
|
+
existing.lengthB = Math.max(existing.lengthB, norm.lengthB);
|
|
23204
|
+
} else {
|
|
23205
|
+
group.push(norm);
|
|
23206
|
+
out.push(norm);
|
|
23207
|
+
}
|
|
23208
|
+
});
|
|
23209
|
+
return out;
|
|
23210
|
+
}
|
|
23211
|
+
|
|
23212
|
+
function rangesOverlap(lo1, hi1, lo2, hi2) {
|
|
23213
|
+
return lo1 <= hi2 && lo2 <= hi1;
|
|
23214
|
+
}
|
|
23215
|
+
|
|
23216
|
+
// Snap every vertex in an accepted seam range to the midpoint between itself
|
|
23217
|
+
// and its nearest footpoint on the opposite arc. Works with staggered vertices:
|
|
23218
|
+
// the opposite side need not have a coinciding vertex. Mouth seed pairs are
|
|
23219
|
+
// forced to their shared vertex midpoint so the crack tip closes exactly.
|
|
23220
|
+
// Returns the seams that moved coordinates.
|
|
23221
|
+
function snapSeams(seams, seeds, arcsById, arcs, footpoints, closeDistance) {
|
|
23222
|
+
arcs.getVertexData();
|
|
23223
|
+
var targets = new CoordinateTargets();
|
|
23224
|
+
var snapped = [];
|
|
23225
|
+
|
|
23226
|
+
seams.forEach(function(seam) {
|
|
23227
|
+
var sideA = arcsById[seam.arcA];
|
|
23228
|
+
var sideB = arcsById[seam.arcB];
|
|
23229
|
+
if (!sideA || !sideB) return;
|
|
23230
|
+
var before = targets.size();
|
|
23231
|
+
snapSideToOpposite(sideA, seam.loA, seam.hiA, sideB, arcs, footpoints,
|
|
23232
|
+
closeDistance, targets);
|
|
23233
|
+
snapSideToOpposite(sideB, seam.loB, seam.hiB, sideA, arcs, footpoints,
|
|
23234
|
+
closeDistance, targets);
|
|
23235
|
+
if (targets.size() > before) snapped.push(seam);
|
|
23236
|
+
});
|
|
23237
|
+
|
|
23238
|
+
// Mouth pairs win over footpoint midpoints so both tips land on one point.
|
|
23239
|
+
var seamsByPair = indexSeamsByArcPair(seams);
|
|
23240
|
+
seeds.forEach(function(seed) {
|
|
23241
|
+
if (!(seed.distance > 0)) return;
|
|
23242
|
+
var group = seamsByPair.get(arcPairKey(seed.a.arc, seed.b.arc));
|
|
23243
|
+
if (!group || !seamContainsSeed(group, seed)) return;
|
|
23244
|
+
var mid = [(seed.a.x + seed.b.x) / 2, (seed.a.y + seed.b.y) / 2];
|
|
23245
|
+
targets.set(seed.a.x, seed.a.y, mid);
|
|
23246
|
+
targets.set(seed.b.x, seed.b.y, mid);
|
|
23247
|
+
});
|
|
23248
|
+
|
|
23249
|
+
return applyTargets(arcs, targets) ? snapped : [];
|
|
23250
|
+
}
|
|
23251
|
+
|
|
23252
|
+
// Snap the facing pair of vertices at each open end of a seam together, which
|
|
23253
|
+
// closes the crack off there without disturbing the boundary between.
|
|
23254
|
+
//
|
|
23255
|
+
// An end counts as open when the two banks turn away from each other there. Where
|
|
23256
|
+
// the crack carries on instead, into the next pair of arcs, pinching would divide
|
|
23257
|
+
// one gap into a row of them -- each awarded separately, so the boundary would
|
|
23258
|
+
// zigzag from bank to bank along a crack that should have been resolved whole.
|
|
23259
|
+
//
|
|
23260
|
+
// Returns the number of mouths pinched shut.
|
|
23261
|
+
function pinchSeamMouths(seams, arcsById, arcs, nodes, footpoints, distance,
|
|
23262
|
+
spherical) {
|
|
23263
|
+
var data = arcs.getVertexData();
|
|
23264
|
+
var targets = new CoordinateTargets();
|
|
23265
|
+
var pinched = 0;
|
|
23266
|
+
|
|
23267
|
+
seams.forEach(function(seam) {
|
|
23268
|
+
var sideA = arcsById[seam.arcA];
|
|
23269
|
+
var sideB = arcsById[seam.arcB];
|
|
23270
|
+
if (!sideA || !sideB) return;
|
|
23271
|
+
[seam.loA, seam.hiA].forEach(function(kA, i) {
|
|
23272
|
+
var outwardA = i === 0 ? -1 : 1;
|
|
23273
|
+
var a = sideA[kA];
|
|
23274
|
+
// The two runs were grown from the seed independently, so they need not end
|
|
23275
|
+
// opposite each other; the nearest vertex within the facing run is the one
|
|
23276
|
+
// to close against.
|
|
23277
|
+
var kB = nearestVertexInRange(a, sideB, seam.loB, seam.hiB, distance,
|
|
23278
|
+
spherical);
|
|
23279
|
+
if (kB < 0) return;
|
|
23280
|
+
var outwardB = kB - seam.loB < seam.hiB - kB ? -1 : 1;
|
|
23281
|
+
if (!endIsOpen(sideA, seam.arcA, kA, outwardA, sideB, arcs, nodes,
|
|
23282
|
+
footpoints, distance) &&
|
|
23283
|
+
!endIsOpen(sideB, seam.arcB, kB, outwardB, sideA, arcs, nodes,
|
|
23284
|
+
footpoints, distance)) {
|
|
23285
|
+
return;
|
|
23286
|
+
}
|
|
23287
|
+
var b = sideB[kB];
|
|
23288
|
+
var mid = [(a.x + b.x) / 2, (a.y + b.y) / 2];
|
|
23289
|
+
var before = targets.size();
|
|
23290
|
+
targets.set(data.xx[a.i], data.yy[a.i], mid);
|
|
23291
|
+
targets.set(data.xx[b.i], data.yy[b.i], mid);
|
|
23292
|
+
if (targets.size() > before) pinched++;
|
|
23293
|
+
});
|
|
23294
|
+
});
|
|
23295
|
+
|
|
23296
|
+
return applyTargets(arcs, targets) ? pinched : 0;
|
|
23297
|
+
}
|
|
23298
|
+
|
|
23299
|
+
// True if the boundary has left the facing arc behind by the vertex just past
|
|
23300
|
+
// @k, so that the crack ends there.
|
|
23301
|
+
//
|
|
23302
|
+
// Where the arc itself ends at @k, the question is what carries on past the node:
|
|
23303
|
+
// an arc continuing along the crack keeps its next vertex within tolerance of the
|
|
23304
|
+
// facing bank, whereas one turning away from the crack -- the coverage boundary at
|
|
23305
|
+
// the crack's mouth, say -- does not. A crack whose banks are split into several
|
|
23306
|
+
// arcs is walked as several seams, and this is what keeps their inner ends from
|
|
23307
|
+
// being taken for mouths.
|
|
23308
|
+
function endIsOpen(side, arcId, k, outward, other, arcs, nodes, footpoints,
|
|
23309
|
+
distance) {
|
|
23310
|
+
var next = k + outward;
|
|
23311
|
+
if (next >= 0 && next < side.length) {
|
|
23312
|
+
return !footpoints.closestPoint(side[next].x, side[next].y, other, distance);
|
|
23313
|
+
}
|
|
23314
|
+
var entering = outward > 0 ? arcId : ~arcId;
|
|
23315
|
+
var connected = nodes.getConnectedArcs(entering);
|
|
23316
|
+
for (var i = 0; i < connected.length; i++) {
|
|
23317
|
+
// Connected ids lead into the node, so the vertex adjacent to it is the last
|
|
23318
|
+
// but one along each.
|
|
23319
|
+
var v = arcs.getVertex(connected[i], -2);
|
|
23320
|
+
if (footpoints.closestPoint(v.x, v.y, other, distance)) return false;
|
|
23321
|
+
}
|
|
23322
|
+
return true;
|
|
23323
|
+
}
|
|
23324
|
+
|
|
23325
|
+
// Index of the vertex in @side[lo..hi] nearest @vertex, or -1 if the nearest is
|
|
23326
|
+
// further off than @maxDist or already coincides with it. A coinciding pair marks
|
|
23327
|
+
// a tip where the two banks already meet, which needs no pinching.
|
|
23328
|
+
function nearestVertexInRange(vertex, side, lo, hi, maxDist, spherical) {
|
|
23329
|
+
var best = -1;
|
|
23330
|
+
var bestDist = Infinity;
|
|
23331
|
+
for (var k = lo; k <= hi; k++) {
|
|
23332
|
+
var d = spherical ?
|
|
23333
|
+
fastLonLatDistance(vertex.x, vertex.y, side[k].x, side[k].y) :
|
|
23334
|
+
distance2D$1(vertex.x, vertex.y, side[k].x, side[k].y);
|
|
23335
|
+
if (d < bestDist) {
|
|
23336
|
+
bestDist = d;
|
|
23337
|
+
best = k;
|
|
23338
|
+
}
|
|
23339
|
+
}
|
|
23340
|
+
if (best < 0 || !(bestDist > 0) || bestDist > maxDist) return -1;
|
|
23341
|
+
return best;
|
|
23342
|
+
}
|
|
23343
|
+
|
|
23344
|
+
// Move every occurrence of a selected coordinate, including coincident arc
|
|
23345
|
+
// endpoints, so existing nodes remain connected. Coordinates that were not
|
|
23346
|
+
// selected are untouched. Returns false if there was nothing to move.
|
|
23347
|
+
function applyTargets(arcs, targets) {
|
|
23348
|
+
if (targets.size() === 0) return false;
|
|
23349
|
+
var data = arcs.getVertexData();
|
|
23350
|
+
for (var i = 0; i < data.xx.length; i++) {
|
|
23351
|
+
var target = targets.get(data.xx[i], data.yy[i]);
|
|
23352
|
+
if (target) {
|
|
23353
|
+
data.xx[i] = target[0];
|
|
23354
|
+
data.yy[i] = target[1];
|
|
23355
|
+
}
|
|
23356
|
+
}
|
|
23357
|
+
arcs.updateVertexData(data.nn, data.xx, data.yy, data.zz);
|
|
23358
|
+
return true;
|
|
23359
|
+
}
|
|
23360
|
+
|
|
23361
|
+
// Replacement coordinates, looked up by exact x/y. Keyed by x in a Map, with a
|
|
23362
|
+
// short bucket per x, because the lookup runs once per vertex in the whole arc
|
|
23363
|
+
// table: building a string key for each of those is slower than the seam
|
|
23364
|
+
// detection that precedes it.
|
|
23365
|
+
function CoordinateTargets() {
|
|
23366
|
+
var byX = new Map();
|
|
23367
|
+
var count = 0;
|
|
23368
|
+
|
|
23369
|
+
this.set = function(x, y, target) {
|
|
23370
|
+
var bucket = byX.get(x);
|
|
23371
|
+
if (!bucket) {
|
|
23372
|
+
bucket = [];
|
|
23373
|
+
byX.set(x, bucket);
|
|
23374
|
+
}
|
|
23375
|
+
for (var i = 0; i < bucket.length; i++) {
|
|
23376
|
+
if (bucket[i].y === y) {
|
|
23377
|
+
bucket[i].target = target;
|
|
23378
|
+
return;
|
|
23379
|
+
}
|
|
23380
|
+
}
|
|
23381
|
+
bucket.push({y: y, target: target});
|
|
23382
|
+
count++;
|
|
23383
|
+
};
|
|
23384
|
+
|
|
23385
|
+
this.get = function(x, y) {
|
|
23386
|
+
var bucket = byX.get(x);
|
|
23387
|
+
if (!bucket) return null;
|
|
23388
|
+
for (var i = 0; i < bucket.length; i++) {
|
|
23389
|
+
if (bucket[i].y === y) return bucket[i].target;
|
|
23390
|
+
}
|
|
23391
|
+
return null;
|
|
23392
|
+
};
|
|
23393
|
+
|
|
23394
|
+
this.size = function() { return count; };
|
|
23395
|
+
}
|
|
23396
|
+
|
|
23397
|
+
function seamContainsSeed(seams, seed) {
|
|
23398
|
+
return seams.some(function(seam) {
|
|
23399
|
+
var aOnA = seed.a.arc === seam.arcA &&
|
|
23400
|
+
seed.a.k >= seam.loA && seed.a.k <= seam.hiA;
|
|
23401
|
+
var aOnB = seed.a.arc === seam.arcB &&
|
|
23402
|
+
seed.a.k >= seam.loB && seed.a.k <= seam.hiB;
|
|
23403
|
+
var bOnA = seed.b.arc === seam.arcA &&
|
|
23404
|
+
seed.b.k >= seam.loA && seed.b.k <= seam.hiA;
|
|
23405
|
+
var bOnB = seed.b.arc === seam.arcB &&
|
|
23406
|
+
seed.b.k >= seam.loB && seed.b.k <= seam.hiB;
|
|
23407
|
+
return (aOnA && bOnB) || (aOnB && bOnA);
|
|
23408
|
+
});
|
|
23409
|
+
}
|
|
23410
|
+
|
|
23411
|
+
function snapSideToOpposite(side, lo, hi, other, arcs, footpoints, closeDistance,
|
|
23412
|
+
targets) {
|
|
23413
|
+
var data = arcs.getVertexData();
|
|
23414
|
+
for (var k = lo; k <= hi; k++) {
|
|
23415
|
+
var v = side[k];
|
|
23416
|
+
var foot = footpoints.closestPoint(v.x, v.y, other, closeDistance);
|
|
23417
|
+
if (!foot || !(foot.distance > 0)) continue;
|
|
23418
|
+
var mid = [(v.x + foot.x) / 2, (v.y + foot.y) / 2];
|
|
23419
|
+
targets.set(data.xx[v.i], data.yy[v.i], mid);
|
|
23420
|
+
}
|
|
23421
|
+
}
|
|
23422
|
+
|
|
23423
|
+
// Nearest-point queries against an arc, indexing each arc once rather than
|
|
23424
|
+
// scanning it per query.
|
|
23425
|
+
//
|
|
23426
|
+
// Growing a seam tests every vertex along one arc against the whole of the
|
|
23427
|
+
// facing arc, and snapping repeats that for the vertices it accepted, so a scan
|
|
23428
|
+
// per query costs the product of the two arcs' lengths. That is unnoticeable
|
|
23429
|
+
// where a mosaic's arcs run between neighboring junctions a few vertices apart,
|
|
23430
|
+
// but a detailed boundary is one arc of thousands of vertices, and a mosaic of
|
|
23431
|
+
// those presents thousands of seams to walk: enough for the same repair to run
|
|
23432
|
+
// for many minutes.
|
|
23433
|
+
//
|
|
23434
|
+
// The index is built from live coordinates, and seam snapping moves coordinates
|
|
23435
|
+
// only once every seam has been measured, so one instance serves a whole pass
|
|
23436
|
+
// over a layer and must not outlive it.
|
|
23437
|
+
function ArcFootpoints(arcs, spherical) {
|
|
23438
|
+
var data = arcs.getVertexData();
|
|
23439
|
+
var indexes = new Map();
|
|
23440
|
+
|
|
23441
|
+
function getIndex(side) {
|
|
23442
|
+
if (!indexes.has(side)) {
|
|
23443
|
+
indexes.set(side, side.length > 1 ? buildIndex(side) : null);
|
|
23444
|
+
}
|
|
23445
|
+
return indexes.get(side);
|
|
23446
|
+
}
|
|
23447
|
+
|
|
23448
|
+
function buildIndex(side) {
|
|
23449
|
+
var index = new Flatbush(side.length - 1);
|
|
23450
|
+
for (var i = 0; i + 1 < side.length; i++) {
|
|
23451
|
+
var a = side[i].i, b = side[i + 1].i;
|
|
23452
|
+
index.add(
|
|
23453
|
+
Math.min(data.xx[a], data.xx[b]), Math.min(data.yy[a], data.yy[b]),
|
|
23454
|
+
Math.max(data.xx[a], data.xx[b]), Math.max(data.yy[a], data.yy[b]));
|
|
23455
|
+
}
|
|
23456
|
+
index.finish();
|
|
23457
|
+
return index;
|
|
23458
|
+
}
|
|
23459
|
+
|
|
23460
|
+
// The nearest point on @side within @maxDist of (x, y), with that distance, or
|
|
23461
|
+
// null if the arc stays clear of it. A segment within @maxDist has its nearest
|
|
23462
|
+
// point inside the query box, so its own bounding box meets that box: no
|
|
23463
|
+
// segment near enough is missed. Nothing beyond @maxDist is reported, which the
|
|
23464
|
+
// callers discarded anyway.
|
|
23465
|
+
this.closestPoint = function(x, y, side, maxDist) {
|
|
23466
|
+
var index = getIndex(side);
|
|
23467
|
+
if (!index) return null;
|
|
23468
|
+
var padX = getCoordinatePad(maxDist, y, spherical, true);
|
|
23469
|
+
var padY = getCoordinatePad(maxDist, y, spherical, false);
|
|
23470
|
+
var ids = index.search(x - padX, y - padY, x + padX, y + padY);
|
|
23471
|
+
var bestD2 = Infinity, bestX = 0, bestY = 0;
|
|
23472
|
+
for (var k = 0; k < ids.length; k++) {
|
|
23473
|
+
var i = ids[k];
|
|
23474
|
+
var ax = data.xx[side[i].i], ay = data.yy[side[i].i];
|
|
23475
|
+
var dx = data.xx[side[i + 1].i] - ax, dy = data.yy[side[i + 1].i] - ay;
|
|
23476
|
+
var len2 = dx * dx + dy * dy;
|
|
23477
|
+
var t = len2 > 0 ? ((x - ax) * dx + (y - ay) * dy) / len2 : 0;
|
|
23478
|
+
if (t < 0) t = 0;
|
|
23479
|
+
if (t > 1) t = 1;
|
|
23480
|
+
var fx = ax + t * dx, fy = ay + t * dy;
|
|
23481
|
+
var d2 = (x - fx) * (x - fx) + (y - fy) * (y - fy);
|
|
23482
|
+
if (d2 < bestD2) {
|
|
23483
|
+
bestD2 = d2;
|
|
23484
|
+
bestX = fx;
|
|
23485
|
+
bestY = fy;
|
|
23486
|
+
}
|
|
23487
|
+
}
|
|
23488
|
+
if (bestD2 === Infinity) return null;
|
|
23489
|
+
var distance = spherical ?
|
|
23490
|
+
fastLonLatDistance(x, y, bestX, bestY) :
|
|
23491
|
+
distance2D$1(x, y, bestX, bestY);
|
|
23492
|
+
if (!isFinite(distance) || distance > maxDist) return null;
|
|
23493
|
+
return {x: bestX, y: bestY, distance: distance};
|
|
23494
|
+
};
|
|
23495
|
+
}
|
|
23496
|
+
|
|
23497
|
+
var MIN_GAP_OWNERS = 3;
|
|
23498
|
+
var WIDTH_FACTOR = 4;
|
|
23499
|
+
|
|
23500
|
+
|
|
23501
|
+
// Split enclosed sliver gaps that border three or more features along the lines
|
|
23502
|
+
// midway between their neighbors' boundaries. The existing mosaic assignment can
|
|
23503
|
+
// then distribute the resulting pieces among those features instead of handing
|
|
23504
|
+
// the entire long gap to one owner.
|
|
23505
|
+
//
|
|
23506
|
+
// Only the boundaries of the gaps being repaired are examined, so the cost
|
|
23507
|
+
// scales with those gaps rather than with the full mosaic.
|
|
23508
|
+
// Returns a layer of cut lines added to the dataset.
|
|
23509
|
+
function partitionPolygonMosaicGaps(lyr, dataset, nodes, opts) {
|
|
23510
|
+
if (!lyr.shapes || lyr.shapes.length < MIN_GAP_OWNERS) return false;
|
|
23511
|
+
var mosaicIndex = new MosaicIndex(lyr, nodes, {flat: true});
|
|
23512
|
+
var sliverOpts = utils.extend({}, opts);
|
|
23513
|
+
if (sliverOpts.gap_width == null) {
|
|
23514
|
+
if (sliverOpts.gap_fill_area != null || sliverOpts.min_gap_area != null ||
|
|
23515
|
+
sliverOpts.min_area != null || sliverOpts.sliver_control != null) {
|
|
23516
|
+
if (sliverOpts.gap_fill_area == null && sliverOpts.min_gap_area == null &&
|
|
23517
|
+
sliverOpts.min_area == null) {
|
|
23518
|
+
sliverOpts.gap_fill_area = 'auto';
|
|
23519
|
+
}
|
|
23520
|
+
} else {
|
|
23521
|
+
sliverOpts.gap_width = 'auto';
|
|
23522
|
+
}
|
|
23523
|
+
}
|
|
23524
|
+
var filter = getSliverFilter(lyr, dataset, sliverOpts).filter;
|
|
23525
|
+
var gaps = mosaicIndex.getUnusedTileData(filter).filter(function(gap) {
|
|
23526
|
+
return countOwners(gap.boundary) >= MIN_GAP_OWNERS;
|
|
23527
|
+
});
|
|
23528
|
+
if (gaps.length === 0) return false;
|
|
23529
|
+
|
|
23530
|
+
var minWidth = getMinDivisibleGapWidth(lyr, nodes.arcs);
|
|
23531
|
+
var cuts = [];
|
|
23532
|
+
var partitioned = 0;
|
|
23533
|
+
gaps.forEach(function(gap) {
|
|
23534
|
+
var result = buildGapPartitionCuts(gap, nodes.arcs, minWidth);
|
|
23535
|
+
if (!result) return;
|
|
23536
|
+
var before = cuts.length;
|
|
23537
|
+
result.lines.forEach(function(line) {
|
|
23538
|
+
if (line.length < 2) return;
|
|
23539
|
+
cuts.push(line);
|
|
23540
|
+
});
|
|
23541
|
+
if (cuts.length > before) partitioned++;
|
|
23542
|
+
});
|
|
23543
|
+
if (cuts.length === 0) return false;
|
|
23544
|
+
|
|
23545
|
+
// Both mutations below have to be announced before either happens: undo
|
|
23546
|
+
// captures the dataset's arc collection and layer list together, and it needs
|
|
23547
|
+
// the originals -- the collection this replaces, and a layer list without the
|
|
23548
|
+
// temporary layer added here. Restoring the arc coordinates alone would leave
|
|
23549
|
+
// the dataset pointing at the merged collection, with layer shapes referring to
|
|
23550
|
+
// arc ids from the original one.
|
|
23551
|
+
noteDatasetWillChange(dataset, {operation: 'partitionGaps', unit: 'arcs'});
|
|
23552
|
+
var firstCutArc = dataset.arcs.size();
|
|
23553
|
+
dataset.arcs = mergeArcs([dataset.arcs, new ArcCollection(cuts)]);
|
|
23554
|
+
// Keep cut arcs in a temporary line layer while addIntersectionCuts() nodes
|
|
23555
|
+
// them against the gap boundary. The caller removes this layer before polygon
|
|
23556
|
+
// cleaning and passes its updated arc ids to the mosaic node filter.
|
|
23557
|
+
var cutLayer = {
|
|
23558
|
+
geometry_type: 'polyline',
|
|
23559
|
+
shapes: cuts.map(function(line, i) {
|
|
23560
|
+
return [[firstCutArc + i]];
|
|
23561
|
+
})
|
|
23562
|
+
};
|
|
23563
|
+
dataset.layers.push(cutLayer);
|
|
23564
|
+
markDatasetChanged(dataset, {operation: 'partitionGaps', unit: 'arcs'});
|
|
23565
|
+
verbose(utils.format('Partitioned %s multi-feature interior gap%s',
|
|
23566
|
+
partitioned, utils.pluralSuffix(partitioned)));
|
|
23567
|
+
return cutLayer;
|
|
23568
|
+
}
|
|
23569
|
+
|
|
23570
|
+
function countOwners(boundary) {
|
|
23571
|
+
var owners = {};
|
|
23572
|
+
boundary.forEach(function(o) { owners[o.shapeId] = true; });
|
|
23573
|
+
return Object.keys(owners).length;
|
|
23574
|
+
}
|
|
23575
|
+
|
|
23576
|
+
// Divide a gap along the centerlines between the boundary runs that face each
|
|
23577
|
+
// other across it.
|
|
23578
|
+
//
|
|
23579
|
+
// The gap's boundary is a cyclic sequence of arcs, each owned by one adjacent
|
|
23580
|
+
// feature, so it decomposes into one run per owner. The dividing line between
|
|
23581
|
+
// two runs is the locus of points equidistant from both; taking the midpoint
|
|
23582
|
+
// between each run's vertices and its footpoint on the other run gives a line
|
|
23583
|
+
// that bends wherever either bank bends, and between two consecutive bends both
|
|
23584
|
+
// banks are straight, where the exact bisector is straight as well.
|
|
23585
|
+
//
|
|
23586
|
+
// Cost and output size track the boundary's own vertex count, so a gap that is
|
|
23587
|
+
// long and hairline costs no more than a compact one. (A sampled Voronoi medial
|
|
23588
|
+
// axis cannot make that promise: its circumcenters are only meaningful when the
|
|
23589
|
+
// sample spacing is finer than the channel is wide, so the work it takes grows
|
|
23590
|
+
// with the gap's length-to-width ratio.)
|
|
23591
|
+
function buildGapPartitionCuts(gap, arcs, minWidth) {
|
|
23592
|
+
var runs = getOwnerRuns(gap.boundary);
|
|
23593
|
+
if (runs.length < MIN_GAP_OWNERS) return null;
|
|
23594
|
+
var ring = gap.tile[0];
|
|
23595
|
+
// Both measures must be planar even when the source CRS is geographic: the
|
|
23596
|
+
// cuts are built in source-coordinate space.
|
|
23597
|
+
var area = Math.abs(geom.getPlanarPathArea(ring, arcs));
|
|
23598
|
+
var perimeter = geom.getPlanarPathPerimeter(ring, arcs);
|
|
23599
|
+
if (!(area > 0) || !(perimeter > 0)) return null;
|
|
23600
|
+
// For a long narrow polygon, 2A/P approximates its width.
|
|
23601
|
+
var gapWidth = area / perimeter * 2;
|
|
23602
|
+
if (gapWidth < minWidth) return null;
|
|
23603
|
+
// A modest multiplier gives facing banks enough reach where the gap is locally
|
|
23604
|
+
// wider, e.g. at a junction.
|
|
23605
|
+
var width = gapWidth * WIDTH_FACTOR;
|
|
23606
|
+
var ringPoints = getPathPoints(ring, arcs);
|
|
23607
|
+
var ringIndex = new SegmentIndex(ringPoints);
|
|
23608
|
+
var banks = runs.map(function(run) {
|
|
23609
|
+
return makeBank(getRunPoints(run.arcIds, arcs));
|
|
23610
|
+
});
|
|
23611
|
+
var lines = [];
|
|
23612
|
+
for (var i = 0; i < banks.length; i++) {
|
|
23613
|
+
for (var j = i + 1; j < banks.length; j++) {
|
|
23614
|
+
var line = buildCenterlineBetweenBanks(banks[i], banks[j], width, ringIndex,
|
|
23615
|
+
collectEndAnchors(banks, i, j, width));
|
|
23616
|
+
if (line.length >= 2) lines.push(line);
|
|
23617
|
+
}
|
|
23618
|
+
}
|
|
23619
|
+
if (lines.length === 0) return null;
|
|
23620
|
+
return {lines: anchorCutLines(lines, width, ringPoints)};
|
|
23621
|
+
}
|
|
23622
|
+
|
|
23623
|
+
function makeBank(points) {
|
|
23624
|
+
return {
|
|
23625
|
+
points: points,
|
|
23626
|
+
index: points.length > 1 ? new SegmentIndex(points) : null
|
|
23627
|
+
};
|
|
23628
|
+
}
|
|
23629
|
+
|
|
23630
|
+
// Make a set of centerlines divide the gap: every line has to reach the gap's
|
|
23631
|
+
// boundary at each end, either directly or through a shared junction with other
|
|
23632
|
+
// lines, or the mosaic treats it as a dangling arc and ignores it.
|
|
23633
|
+
//
|
|
23634
|
+
// A line ending on a vertex of the boundary is already joined to it, that vertex
|
|
23635
|
+
// being a node of the mosaic. Anywhere else, including a point part way along a
|
|
23636
|
+
// boundary segment, the line has to be projected past the boundary to cross it:
|
|
23637
|
+
// starting an extension on the boundary rather than crossing it creates no node
|
|
23638
|
+
// at all, and the cut is ignored.
|
|
23639
|
+
function anchorCutLines(lines, width, ringPoints) {
|
|
23640
|
+
var ringVertices = new Set(ringPoints.map(function(p) {
|
|
23641
|
+
return p[0] + '|' + p[1];
|
|
23642
|
+
}));
|
|
23643
|
+
var ends = [];
|
|
23644
|
+
lines.forEach(function(line, lineId) {
|
|
23645
|
+
[line[0], line[line.length - 1]].forEach(function(point, side) {
|
|
23646
|
+
ends.push({
|
|
23647
|
+
lineId: lineId,
|
|
23648
|
+
side: side,
|
|
23649
|
+
point: point,
|
|
23650
|
+
onBoundary: ringVertices.has(point[0] + '|' + point[1])
|
|
23651
|
+
});
|
|
23652
|
+
});
|
|
23653
|
+
});
|
|
23654
|
+
var connectors = [];
|
|
23655
|
+
var joined = new Uint8Array(ends.length);
|
|
23656
|
+
clusterCutEnds(ends, width * 3).forEach(function(cluster) {
|
|
23657
|
+
var hub = [0, 0];
|
|
23658
|
+
cluster.forEach(function(i) {
|
|
23659
|
+
hub[0] += ends[i].point[0];
|
|
23660
|
+
hub[1] += ends[i].point[1];
|
|
23661
|
+
});
|
|
23662
|
+
hub[0] /= cluster.length;
|
|
23663
|
+
hub[1] /= cluster.length;
|
|
23664
|
+
cluster.forEach(function(i) {
|
|
23665
|
+
joined[i] = 1;
|
|
23666
|
+
connectors.push([ends[i].point, hub]);
|
|
23667
|
+
});
|
|
23668
|
+
});
|
|
23669
|
+
var anchored = lines.map(function(line, lineId) {
|
|
23670
|
+
var out = line.concat();
|
|
23671
|
+
ends.forEach(function(end, i) {
|
|
23672
|
+
if (end.lineId !== lineId || joined[i] || end.onBoundary) return;
|
|
23673
|
+
var inward = end.side ? out[out.length - 2] : out[1];
|
|
23674
|
+
var beyond = projectPast$1(end.point, inward, width);
|
|
23675
|
+
if (!beyond) return;
|
|
23676
|
+
if (end.side) out.push(beyond);
|
|
23677
|
+
else out.unshift(beyond);
|
|
23678
|
+
});
|
|
23679
|
+
return out;
|
|
23680
|
+
});
|
|
23681
|
+
return anchored.concat(connectors);
|
|
23682
|
+
}
|
|
23683
|
+
|
|
23684
|
+
// Group line ends that meet in the interior of the gap, where three or more
|
|
23685
|
+
// centerlines converge on the point equidistant from three boundary runs. Ends
|
|
23686
|
+
// already on the boundary are not candidates, and neither is a pair of ends
|
|
23687
|
+
// from the same line.
|
|
23688
|
+
function clusterCutEnds(ends, maxDist) {
|
|
23689
|
+
var used = new Uint8Array(ends.length);
|
|
23690
|
+
var clusters = [];
|
|
23691
|
+
ends.forEach(function(end, i) {
|
|
23692
|
+
if (used[i] || end.onBoundary) return;
|
|
23693
|
+
var cluster = [];
|
|
23694
|
+
var stack = [i];
|
|
23695
|
+
used[i] = 1;
|
|
23696
|
+
while (stack.length) {
|
|
23697
|
+
var id = stack.pop();
|
|
23698
|
+
cluster.push(id);
|
|
23699
|
+
ends.forEach(function(other, j) {
|
|
23700
|
+
if (used[j] || other.onBoundary || other.lineId === ends[id].lineId) return;
|
|
23701
|
+
if (geom.distance2D(ends[id].point[0], ends[id].point[1],
|
|
23702
|
+
other.point[0], other.point[1]) <= maxDist) {
|
|
23703
|
+
used[j] = 1;
|
|
23704
|
+
stack.push(j);
|
|
23705
|
+
}
|
|
23706
|
+
});
|
|
23707
|
+
}
|
|
23708
|
+
var lineIds = {};
|
|
23709
|
+
cluster.forEach(function(id) { lineIds[ends[id].lineId] = true; });
|
|
23710
|
+
if (Object.keys(lineIds).length >= MIN_GAP_OWNERS) clusters.push(cluster);
|
|
23711
|
+
});
|
|
23712
|
+
return clusters;
|
|
23713
|
+
}
|
|
23714
|
+
|
|
23715
|
+
// One run per owner, in ring order. The ring is cyclic, so a run split across
|
|
23716
|
+
// the array ends is rejoined.
|
|
23717
|
+
function getOwnerRuns(boundary) {
|
|
23718
|
+
var runs = [];
|
|
23719
|
+
boundary.forEach(function(o) {
|
|
23720
|
+
var last = runs[runs.length - 1];
|
|
23721
|
+
if (last && last.shapeId === o.shapeId) {
|
|
23722
|
+
last.arcIds.push(o.arcId);
|
|
23723
|
+
} else {
|
|
23724
|
+
runs.push({shapeId: o.shapeId, arcIds: [o.arcId]});
|
|
23725
|
+
}
|
|
23726
|
+
});
|
|
23727
|
+
if (runs.length > 2 && runs[0].shapeId === runs[runs.length - 1].shapeId) {
|
|
23728
|
+
runs[0].arcIds = runs.pop().arcIds.concat(runs[0].arcIds);
|
|
23729
|
+
}
|
|
23730
|
+
return runs;
|
|
23731
|
+
}
|
|
23732
|
+
|
|
23733
|
+
function getPathPoints(ids, arcs) {
|
|
23734
|
+
var points = [];
|
|
23735
|
+
var iter = arcs.getShapeIter(ids);
|
|
23736
|
+
while (iter.hasNext()) {
|
|
23737
|
+
points.push([iter.x, iter.y]);
|
|
23738
|
+
}
|
|
23739
|
+
return points;
|
|
23740
|
+
}
|
|
23741
|
+
|
|
23742
|
+
function getRunPoints(arcIds, arcs) {
|
|
23743
|
+
var points = [];
|
|
23744
|
+
arcIds.forEach(function(arcId) {
|
|
23745
|
+
var iter = arcs.getArcIter(arcId);
|
|
23746
|
+
while (iter.hasNext()) {
|
|
23747
|
+
var last = points[points.length - 1];
|
|
23748
|
+
// consecutive arcs share their joining vertex
|
|
23749
|
+
if (!last || last[0] !== iter.x || last[1] !== iter.y) {
|
|
23750
|
+
points.push([iter.x, iter.y]);
|
|
23751
|
+
}
|
|
23752
|
+
}
|
|
23753
|
+
});
|
|
23754
|
+
return points;
|
|
23755
|
+
}
|
|
23756
|
+
|
|
23757
|
+
function buildCenterlineBetweenBanks(bankA, bankB, maxDist, ringIndex, anchors) {
|
|
23758
|
+
var events = [];
|
|
23759
|
+
// Each vertex contributes a midpoint, positioned along bank A so that
|
|
23760
|
+
// contributions from both banks interleave in one order.
|
|
23761
|
+
bankA.points.forEach(function(p, i) {
|
|
23762
|
+
var mid = crossGapMidpoint(p, bankB, maxDist, ringIndex);
|
|
23763
|
+
if (mid) events.push({pos: i, point: mid.point});
|
|
23764
|
+
});
|
|
23765
|
+
bankB.points.forEach(function(p, i) {
|
|
23766
|
+
var mid = crossGapMidpoint(p, bankA, maxDist, ringIndex);
|
|
23767
|
+
if (mid) events.push({pos: mid.pos, point: mid.point});
|
|
23768
|
+
});
|
|
23769
|
+
anchors.forEach(function(event) { events.push(event); });
|
|
23770
|
+
events.sort(function(a, b) { return a.pos - b.pos; });
|
|
23771
|
+
var points = [];
|
|
23772
|
+
events.forEach(function(o) {
|
|
23773
|
+
var last = points[points.length - 1];
|
|
23774
|
+
if (!last || last[0] !== o.point[0] || last[1] !== o.point[1]) {
|
|
23775
|
+
points.push(o.point);
|
|
23776
|
+
}
|
|
23777
|
+
});
|
|
23778
|
+
return points;
|
|
23779
|
+
}
|
|
23780
|
+
|
|
23781
|
+
// Where the centerline between two boundary runs meets the ends of the gap.
|
|
23782
|
+
// Ordered outside bank A's own vertex positions, so they bracket the midpoints
|
|
23783
|
+
// between the two banks.
|
|
23784
|
+
//
|
|
23785
|
+
// Midpoints alone do not reach the ends: approaching one, a vertex's nearest point
|
|
23786
|
+
// on the facing bank swings round towards the end itself, and the midpoints stop
|
|
23787
|
+
// describing the middle of the gap. Taking the ends from the boundary is both
|
|
23788
|
+
// exact and what makes the cut divide the gap, since a cut has to reach the
|
|
23789
|
+
// boundary to have any effect.
|
|
23790
|
+
function collectEndAnchors(banks, i, j, maxDist) {
|
|
23791
|
+
var points = banks[i].points;
|
|
23792
|
+
var out = [];
|
|
23793
|
+
// walking the boundary each way round from bank A: A's last vertex leads
|
|
23794
|
+
// forwards, its first vertex backwards
|
|
23795
|
+
[[j, points.length], [i, -1]].forEach(function(o) {
|
|
23796
|
+
var between = runsBetween(banks, o[0] === j ? i : j, o[0]);
|
|
23797
|
+
var point = null;
|
|
23798
|
+
if (between.length === 0) {
|
|
23799
|
+
// The two runs meet, pinching the gap shut, and their shared node is
|
|
23800
|
+
// equidistant from both.
|
|
23801
|
+
point = o[1] < 0 ? points[0] : points[points.length - 1];
|
|
23802
|
+
} else if (between.length === 1 && pathLength(between[0].points) <= maxDist) {
|
|
23803
|
+
// A single run short enough to be a cap rather than a third bank: the gap
|
|
23804
|
+
// ends across it, midway along.
|
|
23805
|
+
point = midpointOfPath(between[0].points);
|
|
23806
|
+
}
|
|
23807
|
+
if (point) out.push({pos: o[1], point: point});
|
|
23808
|
+
});
|
|
23809
|
+
return out;
|
|
23810
|
+
}
|
|
23811
|
+
|
|
23812
|
+
// The runs lying between two others, walking the boundary forwards.
|
|
23813
|
+
function runsBetween(banks, from, to) {
|
|
23814
|
+
var out = [];
|
|
23815
|
+
for (var k = (from + 1) % banks.length; k !== to; k = (k + 1) % banks.length) {
|
|
23816
|
+
out.push(banks[k]);
|
|
23817
|
+
}
|
|
23818
|
+
return out;
|
|
23819
|
+
}
|
|
23820
|
+
|
|
23821
|
+
function pathLength(points) {
|
|
23822
|
+
var len = 0;
|
|
23823
|
+
for (var i = 1; i < points.length; i++) {
|
|
23824
|
+
len += geom.distance2D(points[i - 1][0], points[i - 1][1],
|
|
23825
|
+
points[i][0], points[i][1]);
|
|
23826
|
+
}
|
|
23827
|
+
return len;
|
|
23828
|
+
}
|
|
23829
|
+
|
|
23830
|
+
function midpointOfPath(points) {
|
|
23831
|
+
var half = pathLength(points) / 2;
|
|
23832
|
+
var walked = 0;
|
|
23833
|
+
for (var i = 1; i < points.length; i++) {
|
|
23834
|
+
var seg = geom.distance2D(points[i - 1][0], points[i - 1][1],
|
|
23835
|
+
points[i][0], points[i][1]);
|
|
23836
|
+
if (walked + seg >= half) {
|
|
23837
|
+
var t = seg > 0 ? (half - walked) / seg : 0;
|
|
23838
|
+
return [
|
|
23839
|
+
points[i - 1][0] + (points[i][0] - points[i - 1][0]) * t,
|
|
23840
|
+
points[i - 1][1] + (points[i][1] - points[i - 1][1]) * t
|
|
23841
|
+
];
|
|
23842
|
+
}
|
|
23843
|
+
walked += seg;
|
|
23844
|
+
}
|
|
23845
|
+
return points[points.length - 1];
|
|
23846
|
+
}
|
|
23847
|
+
|
|
23848
|
+
// The midpoint between a vertex of one boundary run and its footpoint on
|
|
23849
|
+
// another, where the two runs face each other across the gap.
|
|
23850
|
+
//
|
|
23851
|
+
// Two runs can instead meet at a node and carry on along the same side of the
|
|
23852
|
+
// gap. Those produce midpoints too, but ones lying on the boundary rather than
|
|
23853
|
+
// between two banks, and a cut along them shaves a hairline strip off one feature
|
|
23854
|
+
// to award it to another -- the very artifact partitioning exists to avoid. So a
|
|
23855
|
+
// midpoint counts only if it lies inside the gap, clear of the boundary by an
|
|
23856
|
+
// amount comparable to the separation it came from.
|
|
23857
|
+
function crossGapMidpoint(point, otherBank, maxDist, ringIndex) {
|
|
23858
|
+
if (!otherBank.index) return null;
|
|
23859
|
+
var foot = otherBank.index.closestPoint(point[0], point[1], maxDist);
|
|
23860
|
+
if (!foot || !(foot.distSq > 0)) return null;
|
|
23861
|
+
var mid = [(point[0] + foot.x) / 2, (point[1] + foot.y) / 2];
|
|
23862
|
+
// Half the separation is what a midpoint between facing banks has. A quarter
|
|
23863
|
+
// allows for a gap that narrows, bringing a third stretch of its boundary
|
|
23864
|
+
// closer to the midpoint than the banks it came from.
|
|
23865
|
+
if (ringIndex.hasSegmentWithin(mid[0], mid[1], Math.sqrt(foot.distSq) / 4)) {
|
|
23866
|
+
return null;
|
|
23867
|
+
}
|
|
23868
|
+
// Two runs can also face each other across a feature, in a gap that bends back
|
|
23869
|
+
// on itself.
|
|
23870
|
+
if (!ringIndex.containsPoint(mid[0], mid[1])) return null;
|
|
23871
|
+
return {point: mid, pos: foot.pos};
|
|
23872
|
+
}
|
|
23873
|
+
|
|
23874
|
+
// The vertex tests above run once per boundary vertex, and the boundary of a long
|
|
23875
|
+
// gap in detailed data carries thousands of them, so each test has to examine a
|
|
23876
|
+
// bounded number of segments rather than the whole path. @points is an open
|
|
23877
|
+
// polyline, or a closed ring for containsPoint().
|
|
23878
|
+
function SegmentIndex(points) {
|
|
23879
|
+
var n = points.length - 1;
|
|
23880
|
+
var index = new Flatbush(n);
|
|
23881
|
+
var xmax = -Infinity;
|
|
23882
|
+
for (var i = 0; i < n; i++) {
|
|
23883
|
+
var ax = points[i][0], ay = points[i][1];
|
|
23884
|
+
var bx = points[i + 1][0], by = points[i + 1][1];
|
|
23885
|
+
index.add(Math.min(ax, bx), Math.min(ay, by), Math.max(ax, bx), Math.max(ay, by));
|
|
23886
|
+
if (ax > xmax) xmax = ax;
|
|
23887
|
+
if (bx > xmax) xmax = bx;
|
|
23888
|
+
}
|
|
23889
|
+
index.finish();
|
|
23890
|
+
|
|
23891
|
+
// Nearest point on the path within @maxDist, with its position along the path
|
|
23892
|
+
// (segment index plus the fraction into that segment) so that midpoints from
|
|
23893
|
+
// either bank can be ordered on a common parameter. A segment within @maxDist
|
|
23894
|
+
// of the point has its nearest point inside the query box, so its own bounding
|
|
23895
|
+
// box meets that box: nothing near enough is missed.
|
|
23896
|
+
this.closestPoint = function(x, y, maxDist) {
|
|
23897
|
+
var ids = index.search(x - maxDist, y - maxDist, x + maxDist, y + maxDist);
|
|
23898
|
+
var maxSq = maxDist * maxDist;
|
|
23899
|
+
var best = null;
|
|
23900
|
+
for (var k = 0; k < ids.length; k++) {
|
|
23901
|
+
var i = ids[k];
|
|
23902
|
+
var ax = points[i][0], ay = points[i][1];
|
|
23903
|
+
var dx = points[i + 1][0] - ax, dy = points[i + 1][1] - ay;
|
|
23904
|
+
var len2 = dx * dx + dy * dy;
|
|
23905
|
+
var t = len2 > 0 ? ((x - ax) * dx + (y - ay) * dy) / len2 : 0;
|
|
23906
|
+
if (t < 0) t = 0;
|
|
23907
|
+
if (t > 1) t = 1;
|
|
23908
|
+
var fx = ax + t * dx, fy = ay + t * dy;
|
|
23909
|
+
var distSq = (x - fx) * (x - fx) + (y - fy) * (y - fy);
|
|
23910
|
+
if (distSq <= maxSq && (!best || distSq < best.distSq)) {
|
|
23911
|
+
best = {x: fx, y: fy, distSq: distSq, pos: i + t};
|
|
23912
|
+
}
|
|
23913
|
+
}
|
|
23914
|
+
return best;
|
|
23915
|
+
};
|
|
23916
|
+
|
|
23917
|
+
this.hasSegmentWithin = function(x, y, dist) {
|
|
23918
|
+
var ids = index.search(x - dist, y - dist, x + dist, y + dist);
|
|
23919
|
+
var distSq = dist * dist;
|
|
23920
|
+
for (var k = 0; k < ids.length; k++) {
|
|
23921
|
+
var i = ids[k];
|
|
23922
|
+
if (geom.pointSegDistSq2(x, y, points[i][0], points[i][1],
|
|
23923
|
+
points[i + 1][0], points[i + 1][1]) < distSq) {
|
|
23924
|
+
return true;
|
|
23925
|
+
}
|
|
23926
|
+
}
|
|
23927
|
+
return false;
|
|
23928
|
+
};
|
|
23929
|
+
|
|
23930
|
+
// Crossings of the ray running right from the point: odd means inside. A point
|
|
23931
|
+
// on the ring counts as inside, as the linear test this replaces had it.
|
|
23932
|
+
this.containsPoint = function(x, y) {
|
|
23933
|
+
var ids = index.search(x, y, xmax, y);
|
|
23934
|
+
var inside = false;
|
|
23935
|
+
for (var k = 0; k < ids.length; k++) {
|
|
23936
|
+
var i = ids[k];
|
|
23937
|
+
var ay = points[i][1], by = points[i + 1][1];
|
|
23938
|
+
if (ay > y === by > y) continue;
|
|
23939
|
+
var ax = points[i][0], bx = points[i + 1][0];
|
|
23940
|
+
if (ax + (y - ay) / (by - ay) * (bx - ax) > x) inside = !inside;
|
|
23941
|
+
}
|
|
23942
|
+
return inside;
|
|
23943
|
+
};
|
|
23944
|
+
}
|
|
23945
|
+
|
|
23946
|
+
// The narrowest gap worth dividing, in coordinate units: the automatic
|
|
23947
|
+
// gap-width, one hundredth of the median distance between vertices.
|
|
23948
|
+
//
|
|
23949
|
+
// Dividing a gap is only an improvement while the gap has a width worth
|
|
23950
|
+
// dividing. Awarding a whole gap to one feature moves that feature's boundary
|
|
23951
|
+
// across to the far bank, by the width of the gap; dividing it instead gives
|
|
23952
|
+
// every neighbor a strip of its own, and the boundary between two strips runs
|
|
23953
|
+
// half the gap's width from the bank it was cut from, out and back. Wide gap:
|
|
23954
|
+
// the first is a visible displacement and the second is a fair division. Gap a
|
|
23955
|
+
// few millimeters wide: the first is imperceptible, and the second decorates
|
|
23956
|
+
// several features with hairline slivers, which is worse for having more of them.
|
|
23957
|
+
//
|
|
23958
|
+
// A layer's median segment length is the scale at which its boundaries were
|
|
23959
|
+
// drawn, so the same fraction of it that serves as the default gap-width -- what
|
|
23960
|
+
// mapshaper already treats as too narrow to be intended -- marks where dividing
|
|
23961
|
+
// stops being worth it. This is deliberately independent of the gap-width in
|
|
23962
|
+
// effect: raising that to fill wider gaps should not coarsen the data's own scale.
|
|
23963
|
+
function getMinDivisibleGapWidth(lyr, arcs) {
|
|
23964
|
+
var width = getMedianPolygonSegmentLength(lyr, arcs) *
|
|
23965
|
+
GAP_WIDTH_SEGMENT_FRACTION;
|
|
23966
|
+
// That median comes back in meters for geographic coordinates, whereas cuts
|
|
23967
|
+
// are built in the source coordinate space. Converting it back with the same
|
|
23968
|
+
// test it was measured under keeps the two in step.
|
|
23969
|
+
return arcs.isPlanar() ? width : metersToDegrees(width);
|
|
23970
|
+
}
|
|
23971
|
+
|
|
23972
|
+
function projectPast$1(from, toward, distance) {
|
|
23973
|
+
var dx = from[0] - toward[0];
|
|
23974
|
+
var dy = from[1] - toward[1];
|
|
23975
|
+
var len = Math.sqrt(dx * dx + dy * dy);
|
|
23976
|
+
if (!(len > 0)) return null;
|
|
23977
|
+
return [
|
|
23978
|
+
from[0] + dx / len * distance,
|
|
23979
|
+
from[1] + dy / len * distance
|
|
23980
|
+
];
|
|
23981
|
+
}
|
|
23982
|
+
|
|
22516
23983
|
cmd.cleanLayers = cleanLayers;
|
|
22517
23984
|
|
|
22518
23985
|
function cleanLayers(layers, dataset, optsArg) {
|
|
@@ -22521,6 +23988,27 @@
|
|
|
22521
23988
|
var deepClean = !opts.only_arcs;
|
|
22522
23989
|
var pathClean = utils.some(layers, layerHasPaths);
|
|
22523
23990
|
var nodes;
|
|
23991
|
+
// Collapsing duplicate boundaries runs unconditionally, and only ever merges
|
|
23992
|
+
// banks that differ by floating-point rounding. Left in place, such a seam
|
|
23993
|
+
// becomes an enclosed sliver that gap filling awards to one neighbor, giving
|
|
23994
|
+
// that feature a zero-width spike along the shared border.
|
|
23995
|
+
if (pathClean && dataset.arcs) {
|
|
23996
|
+
profileStart('collapseDuplicateBoundaries');
|
|
23997
|
+
noteArcsWillChange(dataset.arcs, {operation: 'clean-closeGaps'});
|
|
23998
|
+
var polygonLayers = layers.filter(function(lyr) {
|
|
23999
|
+
return lyr.geometry_type == 'polygon';
|
|
24000
|
+
});
|
|
24001
|
+
polygonLayers.forEach(function(lyr) {
|
|
24002
|
+
noteLayerWillChange(lyr, {operation: 'clean-closeGaps', unit: 'arc-ids'});
|
|
24003
|
+
});
|
|
24004
|
+
if (collapseDuplicateBoundaries(layers, dataset)) {
|
|
24005
|
+
markArcsChanged(dataset.arcs, {operation: 'clean-closeGaps'});
|
|
24006
|
+
polygonLayers.forEach(function(lyr) {
|
|
24007
|
+
markLayerChanged(lyr, {operation: 'clean-closeGaps', unit: 'arc-ids'});
|
|
24008
|
+
});
|
|
24009
|
+
}
|
|
24010
|
+
profileEnd('collapseDuplicateBoundaries');
|
|
24011
|
+
}
|
|
22524
24012
|
if (opts.debug) {
|
|
22525
24013
|
addIntersectionCuts(dataset, opts);
|
|
22526
24014
|
profileEnd('cleanLayers');
|
|
@@ -22538,9 +24026,49 @@
|
|
|
22538
24026
|
nodes = addIntersectionCuts(dataset, opts);
|
|
22539
24027
|
}
|
|
22540
24028
|
if (lyr.geometry_type == 'polygon') {
|
|
24029
|
+
// Pinching the mouths of cracks that open to the outside encloses them,
|
|
24030
|
+
// so that the passes below fill or divide them like any other gap. It
|
|
24031
|
+
// comes first for that reason, and only the pinched vertices move.
|
|
24032
|
+
if (opts.close_outer_gaps) {
|
|
24033
|
+
profileStart('pinchOuterCrackMouths');
|
|
24034
|
+
noteArcsWillChange(dataset.arcs, {operation: 'clean-pinchGapMouths'});
|
|
24035
|
+
noteLayerWillChange(lyr,
|
|
24036
|
+
{operation: 'clean-pinchGapMouths', unit: 'arc-ids'});
|
|
24037
|
+
if (pinchOuterCrackMouths(lyr, dataset, nodes, opts)) {
|
|
24038
|
+
markArcsChanged(dataset.arcs, {operation: 'clean-pinchGapMouths'});
|
|
24039
|
+
markLayerChanged(lyr,
|
|
24040
|
+
{operation: 'clean-pinchGapMouths', unit: 'arc-ids'});
|
|
24041
|
+
// A pinched pair only touches: the cutter turns each touch into a
|
|
24042
|
+
// node, which is what makes the crack a tile of the mosaic. Undo
|
|
24043
|
+
// already holds everything this re-cut changes, captured above.
|
|
24044
|
+
withActiveUndoTransaction(null, function() {
|
|
24045
|
+
nodes = addIntersectionCuts(dataset, opts);
|
|
24046
|
+
});
|
|
24047
|
+
}
|
|
24048
|
+
profileEnd('pinchOuterCrackMouths');
|
|
24049
|
+
}
|
|
24050
|
+
delete opts._mosaic_cut_arcs;
|
|
24051
|
+
profileStart('partitionPolygonMosaicGaps');
|
|
24052
|
+
noteArcsWillChange(dataset.arcs, {operation: 'clean-partitionGaps'});
|
|
24053
|
+
var cutLayer = partitionPolygonMosaicGaps(lyr, dataset, nodes, opts);
|
|
24054
|
+
if (cutLayer) {
|
|
24055
|
+
markArcsChanged(dataset.arcs, {operation: 'clean-partitionGaps'});
|
|
24056
|
+
// The arc collection the partition just installed is a throwaway: undo
|
|
24057
|
+
// puts back the collection this command started with, which the
|
|
24058
|
+
// partition captured. Capturing this one too would store a second copy
|
|
24059
|
+
// of the arc table to no purpose. Every layer baseline the re-cut needs
|
|
24060
|
+
// was captured by the addIntersectionCuts() call above.
|
|
24061
|
+
withActiveUndoTransaction(null, function() {
|
|
24062
|
+
nodes = addIntersectionCuts(dataset, opts);
|
|
24063
|
+
});
|
|
24064
|
+
opts._mosaic_cut_arcs = collectArcIds(cutLayer.shapes);
|
|
24065
|
+
dataset.layers.splice(dataset.layers.indexOf(cutLayer), 1);
|
|
24066
|
+
}
|
|
24067
|
+
profileEnd('partitionPolygonMosaicGaps');
|
|
22541
24068
|
profileStart('cleanPolygonLayerGeometry');
|
|
22542
24069
|
cleanPolygonLayerGeometry(lyr, dataset, opts);
|
|
22543
24070
|
profileEnd('cleanPolygonLayerGeometry');
|
|
24071
|
+
delete opts._mosaic_cut_arcs;
|
|
22544
24072
|
} else if (lyr.geometry_type == 'polyline') {
|
|
22545
24073
|
profileStart('cleanPolylineLayerGeometry');
|
|
22546
24074
|
cleanPolylineLayerGeometry(lyr, dataset);
|
|
@@ -22568,7 +24096,7 @@
|
|
|
22568
24096
|
|
|
22569
24097
|
function cleanPolygonLayerGeometry(lyr, dataset, opts) {
|
|
22570
24098
|
// clean polygons by apply the 'dissolve2' function to each feature
|
|
22571
|
-
opts =
|
|
24099
|
+
opts = withDefaultGapWidth(opts);
|
|
22572
24100
|
var groups = lyr.shapes.map(function(shp, i) {
|
|
22573
24101
|
return [i];
|
|
22574
24102
|
});
|
|
@@ -22577,6 +24105,30 @@
|
|
|
22577
24105
|
markLayerChanged(lyr, {operation: 'cleanPolygonLayerGeometry', unit: 'shapes'});
|
|
22578
24106
|
}
|
|
22579
24107
|
|
|
24108
|
+
function withDefaultGapWidth(opts) {
|
|
24109
|
+
opts = Object.assign({}, opts);
|
|
24110
|
+
if (opts.gap_width != null) return opts;
|
|
24111
|
+
// Legacy area/sliver options: keep the historical gap_fill_area=auto default.
|
|
24112
|
+
if (opts.gap_fill_area != null || opts.min_gap_area != null ||
|
|
24113
|
+
opts.min_area != null || opts.sliver_control != null) {
|
|
24114
|
+
if (opts.gap_fill_area == null && opts.min_gap_area == null &&
|
|
24115
|
+
opts.min_area == null) {
|
|
24116
|
+
opts.gap_fill_area = 'auto';
|
|
24117
|
+
}
|
|
24118
|
+
return opts;
|
|
24119
|
+
}
|
|
24120
|
+
opts.gap_width = 'auto';
|
|
24121
|
+
return opts;
|
|
24122
|
+
}
|
|
24123
|
+
|
|
24124
|
+
function collectArcIds(shapes) {
|
|
24125
|
+
var ids = {};
|
|
24126
|
+
traversePaths(shapes, function(o) {
|
|
24127
|
+
ids[o.arcId < 0 ? ~o.arcId : o.arcId] = true;
|
|
24128
|
+
});
|
|
24129
|
+
return ids;
|
|
24130
|
+
}
|
|
24131
|
+
|
|
22580
24132
|
// Remove duplicate points from multipoint geometries
|
|
22581
24133
|
// TODO: consider checking for invalid coordinates
|
|
22582
24134
|
function cleanPointLayerGeometry(lyr, dataset, opts) {
|
|
@@ -31872,13 +33424,27 @@ ${svg}
|
|
|
31872
33424
|
var featureLength = bb.readUint32(offset);
|
|
31873
33425
|
bb.setPosition(offset);
|
|
31874
33426
|
var feature = Feature.getSizePrefixedRootAsFeature(bb);
|
|
31875
|
-
geojsonFeature =
|
|
33427
|
+
geojsonFeature = readGeoJSONFeature(feature, headerMeta, id++);
|
|
31876
33428
|
delete geojsonFeature.id;
|
|
31877
33429
|
offset += SIZE_PREFIX_LEN + featureLength;
|
|
31878
33430
|
return geojsonFeature;
|
|
31879
33431
|
};
|
|
31880
33432
|
}
|
|
31881
33433
|
|
|
33434
|
+
// The geometry field of a FlatGeobuf Feature is optional; fromFeature() assumes
|
|
33435
|
+
// it is present, so features without one are converted here.
|
|
33436
|
+
function readGeoJSONFeature(feature, headerMeta, id) {
|
|
33437
|
+
if (feature.geometry() === null) {
|
|
33438
|
+
return {
|
|
33439
|
+
type: 'Feature',
|
|
33440
|
+
id: id,
|
|
33441
|
+
geometry: null,
|
|
33442
|
+
properties: parseProperties$1(feature, headerMeta.columns)
|
|
33443
|
+
};
|
|
33444
|
+
}
|
|
33445
|
+
return fromFeature(id, feature, headerMeta);
|
|
33446
|
+
}
|
|
33447
|
+
|
|
31882
33448
|
function buildHeaderWithCRS(headerMeta, crsMeta) {
|
|
31883
33449
|
var builder = new Builder();
|
|
31884
33450
|
var columnsOffset = createColumnsVector(builder, headerMeta.columns || []);
|
|
@@ -31923,24 +33489,31 @@ ${svg}
|
|
|
31923
33489
|
function encodeCollection(source, columns, forcedType) {
|
|
31924
33490
|
var headerMeta = null;
|
|
31925
33491
|
var sink = null;
|
|
33492
|
+
// Null-geometry features encountered before the collection's geometry type is
|
|
33493
|
+
// known, and therefore before the header can be written.
|
|
33494
|
+
var deferred = [];
|
|
31926
33495
|
for (var i = 0; i < source.length; i++) {
|
|
31927
33496
|
var feature = source.getFeature(i);
|
|
33497
|
+
var properties = normalizeProperties(feature.properties, columns);
|
|
33498
|
+
if (!feature.geometry) {
|
|
33499
|
+
// A feature's geometry can go missing during export -- e.g. a path that
|
|
33500
|
+
// collapses to a single point at the output coordinate precision -- and a
|
|
33501
|
+
// layer can also contain null shapes to begin with. Write a record with
|
|
33502
|
+
// no geometry, matching what the GeoJSON and Shapefile exporters do.
|
|
33503
|
+
// Such a feature says nothing about the collection's geometry type, so it
|
|
33504
|
+
// must not make a homogeneous layer look mixed.
|
|
33505
|
+
var nullFeature = buildNullGeometryFeature(properties, columns);
|
|
33506
|
+
if (sink) {
|
|
33507
|
+
sink.append(nullFeature);
|
|
33508
|
+
} else {
|
|
33509
|
+
deferred.push(nullFeature);
|
|
33510
|
+
}
|
|
33511
|
+
continue;
|
|
33512
|
+
}
|
|
31928
33513
|
var type = GeometryType[feature.geometry.type] || GeometryType.Unknown;
|
|
31929
33514
|
if (!headerMeta) {
|
|
31930
|
-
headerMeta =
|
|
31931
|
-
|
|
31932
|
-
columns: columns,
|
|
31933
|
-
envelope: null,
|
|
31934
|
-
featuresCount: source.length,
|
|
31935
|
-
indexNodeSize: 0,
|
|
31936
|
-
crs: null,
|
|
31937
|
-
title: null,
|
|
31938
|
-
description: null,
|
|
31939
|
-
metadata: null
|
|
31940
|
-
};
|
|
31941
|
-
sink = new ByteSink(magicbytes.length + estimateEncodedBytes(source));
|
|
31942
|
-
sink.append(magicbytes);
|
|
31943
|
-
sink.append(buildHeader(headerMeta));
|
|
33515
|
+
headerMeta = getEncodedHeaderMeta(forcedType === null ? type : forcedType, columns, source.length);
|
|
33516
|
+
sink = initSink(source, headerMeta, deferred);
|
|
31944
33517
|
} else if (forcedType === null && type != headerMeta.geometryType) {
|
|
31945
33518
|
return null;
|
|
31946
33519
|
}
|
|
@@ -31948,11 +33521,66 @@ ${svg}
|
|
|
31948
33521
|
parseGC(feature.geometry) :
|
|
31949
33522
|
parseGeometry(feature.geometry);
|
|
31950
33523
|
omitRedundantGeometryType(geometry, headerMeta.geometryType);
|
|
31951
|
-
sink.append(buildFeature(geometry,
|
|
33524
|
+
sink.append(buildFeature(geometry, properties, headerMeta));
|
|
33525
|
+
}
|
|
33526
|
+
if (!sink) {
|
|
33527
|
+
// No feature had a geometry, so the collection has no geometry type.
|
|
33528
|
+
headerMeta = getEncodedHeaderMeta(forcedType === null ? GeometryType.Unknown : forcedType,
|
|
33529
|
+
columns, source.length);
|
|
33530
|
+
sink = initSink(source, headerMeta, deferred);
|
|
31952
33531
|
}
|
|
31953
33532
|
return sink.toBytes();
|
|
31954
33533
|
}
|
|
31955
33534
|
|
|
33535
|
+
function getEncodedHeaderMeta(geometryType, columns, featuresCount) {
|
|
33536
|
+
return {
|
|
33537
|
+
geometryType: geometryType,
|
|
33538
|
+
columns: columns,
|
|
33539
|
+
envelope: null,
|
|
33540
|
+
featuresCount: featuresCount,
|
|
33541
|
+
indexNodeSize: 0,
|
|
33542
|
+
crs: null,
|
|
33543
|
+
title: null,
|
|
33544
|
+
description: null,
|
|
33545
|
+
metadata: null
|
|
33546
|
+
};
|
|
33547
|
+
}
|
|
33548
|
+
|
|
33549
|
+
function initSink(source, headerMeta, deferredFeatures) {
|
|
33550
|
+
var sink = new ByteSink(magicbytes.length + estimateEncodedBytes(source));
|
|
33551
|
+
sink.append(magicbytes);
|
|
33552
|
+
sink.append(buildHeader(headerMeta));
|
|
33553
|
+
for (var i = 0; i < deferredFeatures.length; i++) {
|
|
33554
|
+
sink.append(deferredFeatures[i]);
|
|
33555
|
+
}
|
|
33556
|
+
return sink;
|
|
33557
|
+
}
|
|
33558
|
+
|
|
33559
|
+
// Encodes a feature with attributes but no geometry, which FlatGeobuf supports
|
|
33560
|
+
// by omitting the Feature table's optional geometry field. buildFeature() always
|
|
33561
|
+
// writes a geometry, so the properties are encoded with it and then re-emitted:
|
|
33562
|
+
// property values are written in a typed binary layout that is better read from
|
|
33563
|
+
// one implementation than copied.
|
|
33564
|
+
function buildNullGeometryFeature(properties, columns) {
|
|
33565
|
+
var meta = {columns: columns};
|
|
33566
|
+
var withGeometry = buildFeature({xy: [], type: GeometryType.Unknown}, properties, meta);
|
|
33567
|
+
var bb = new ByteBuffer(withGeometry);
|
|
33568
|
+
var propertyBytes = Feature.getSizePrefixedRootAsFeature(bb).propertiesArray();
|
|
33569
|
+
var builder = new Builder();
|
|
33570
|
+
// Features are concatenated in the file and read in place, so every feature
|
|
33571
|
+
// buffer has to leave the next one 8-byte aligned or reading a Float64 array
|
|
33572
|
+
// of coordinates from it throws. Buffers holding coordinates are padded to a
|
|
33573
|
+
// multiple of 8 because of those arrays; a feature with only property bytes
|
|
33574
|
+
// needs the alignment requested explicitly.
|
|
33575
|
+
builder.prep(8, 0);
|
|
33576
|
+
var propertiesOffset = propertyBytes && propertyBytes.length > 0 ?
|
|
33577
|
+
Feature.createPropertiesVector(builder, propertyBytes) : 0;
|
|
33578
|
+
Feature.startFeature(builder);
|
|
33579
|
+
if (propertiesOffset) Feature.addProperties(builder, propertiesOffset);
|
|
33580
|
+
builder.finishSizePrefixed(Feature.endFeature(builder));
|
|
33581
|
+
return builder.asUint8Array();
|
|
33582
|
+
}
|
|
33583
|
+
|
|
31956
33584
|
// Encoding the first feature is the only sample available before the buffer
|
|
31957
33585
|
// has to be allocated; overshooting costs a resize, which is why it is loose.
|
|
31958
33586
|
function estimateEncodedBytes(source) {
|
|
@@ -36724,11 +38352,15 @@ ${svg}
|
|
|
36724
38352
|
},
|
|
36725
38353
|
minGapAreaOpt = {
|
|
36726
38354
|
old_alias: 'min-gap-area',
|
|
36727
|
-
describe: '
|
|
38355
|
+
describe: '(deprecated) use gap-width= instead',
|
|
36728
38356
|
type: 'area'
|
|
36729
38357
|
},
|
|
38358
|
+
gapWidthOpt = {
|
|
38359
|
+
describe: 'fill gaps narrower than this width, e.g. 2m (default is automatic)',
|
|
38360
|
+
type: 'distance'
|
|
38361
|
+
},
|
|
36730
38362
|
sliverControlOpt = {
|
|
36731
|
-
describe: '
|
|
38363
|
+
describe: '(deprecated) use gap-width= instead',
|
|
36732
38364
|
type: 'number'
|
|
36733
38365
|
},
|
|
36734
38366
|
calcOpt = {
|
|
@@ -37456,6 +39088,11 @@ ${svg}
|
|
|
37456
39088
|
|
|
37457
39089
|
parser.command('clean')
|
|
37458
39090
|
.describe('fixes geometry issues, such as polygon overlaps and gaps')
|
|
39091
|
+
.option('close-outer-gaps', {
|
|
39092
|
+
describe: 'close cracks that open to the outside of the mosaic',
|
|
39093
|
+
type: 'flag'
|
|
39094
|
+
})
|
|
39095
|
+
.option('gap-width', gapWidthOpt)
|
|
37459
39096
|
.option('gap-fill-area', minGapAreaOpt)
|
|
37460
39097
|
.option('sliver-control', sliverControlOpt)
|
|
37461
39098
|
.option('snap-interval', snapIntervalOpt)
|
|
@@ -37655,8 +39292,9 @@ ${svg}
|
|
|
37655
39292
|
type: 'flag',
|
|
37656
39293
|
describe: '[points] use 2D math to find centroids of latlong points'
|
|
37657
39294
|
})
|
|
39295
|
+
.option('gap-width', gapWidthOpt)
|
|
37658
39296
|
.option('gap-fill-area', {
|
|
37659
|
-
describe: '[polygons]
|
|
39297
|
+
describe: '[polygons] (deprecated) use gap-width= instead',
|
|
37660
39298
|
type: 'area'
|
|
37661
39299
|
})
|
|
37662
39300
|
.option('sliver-control', sliverControlOpt)
|
|
@@ -37685,8 +39323,9 @@ ${svg}
|
|
|
37685
39323
|
.option('calc', calcOpt)
|
|
37686
39324
|
.option('sum-fields', sumFieldsOpt)
|
|
37687
39325
|
.option('copy-fields', copyFieldsOpt)
|
|
39326
|
+
.option('gap-width', gapWidthOpt)
|
|
37688
39327
|
.option('gap-fill-area', {
|
|
37689
|
-
describe: '
|
|
39328
|
+
describe: '(deprecated) use gap-width= instead',
|
|
37690
39329
|
type: 'area'
|
|
37691
39330
|
})
|
|
37692
39331
|
.option('sliver-control', sliverControlOpt)
|
|
@@ -45853,10 +47492,10 @@ ${svg}
|
|
|
45853
47492
|
}
|
|
45854
47493
|
|
|
45855
47494
|
function isClosedPath$1(arr) {
|
|
45856
|
-
return isArrayOfPoints(arr) && arr.length > 3 && samePoint$
|
|
47495
|
+
return isArrayOfPoints(arr) && arr.length > 3 && samePoint$2(arr[0], arr[arr.length - 1]);
|
|
45857
47496
|
}
|
|
45858
47497
|
|
|
45859
|
-
function samePoint$
|
|
47498
|
+
function samePoint$2(a, b) {
|
|
45860
47499
|
return a[0] == b[0] && a[1] == b[1];
|
|
45861
47500
|
}
|
|
45862
47501
|
|
|
@@ -48205,12 +49844,12 @@ ${svg}
|
|
|
48205
49844
|
return arr[arr.length - 1];
|
|
48206
49845
|
}
|
|
48207
49846
|
|
|
48208
|
-
function samePoint(a, b) {
|
|
49847
|
+
function samePoint$1(a, b) {
|
|
48209
49848
|
return a && b && a[0] === b[0] && a[1] === b[1];
|
|
48210
49849
|
}
|
|
48211
49850
|
|
|
48212
49851
|
function isClosedPath(arr) {
|
|
48213
|
-
return samePoint(arr[0], lastEl(arr));
|
|
49852
|
+
return samePoint$1(arr[0], lastEl(arr));
|
|
48214
49853
|
}
|
|
48215
49854
|
|
|
48216
49855
|
// remove likely rounding errors
|
|
@@ -48371,7 +50010,7 @@ ${svg}
|
|
|
48371
50010
|
if (!nextPart) {
|
|
48372
50011
|
return null;
|
|
48373
50012
|
}
|
|
48374
|
-
if (samePoint(ring[0], nextPart[0])) {
|
|
50013
|
+
if (samePoint$1(ring[0], nextPart[0])) {
|
|
48375
50014
|
// done!
|
|
48376
50015
|
ring.push(ring[0]); // close the ring
|
|
48377
50016
|
return ring;
|
|
@@ -48420,7 +50059,7 @@ ${svg}
|
|
|
48420
50059
|
|
|
48421
50060
|
function findPartStartingAt(parts, firstPoint) {
|
|
48422
50061
|
for (var i=0; i<parts.length; i++) {
|
|
48423
|
-
if (samePoint(parts[i][0], firstPoint)) {
|
|
50062
|
+
if (samePoint$1(parts[i][0], firstPoint)) {
|
|
48424
50063
|
return parts[i];
|
|
48425
50064
|
}
|
|
48426
50065
|
}
|
|
@@ -48444,7 +50083,7 @@ ${svg}
|
|
|
48444
50083
|
var part = [];
|
|
48445
50084
|
var firstPoint = path[0];
|
|
48446
50085
|
var lastPoint = lastEl(path);
|
|
48447
|
-
var closed = samePoint(firstPoint, lastPoint);
|
|
50086
|
+
var closed = samePoint$1(firstPoint, lastPoint);
|
|
48448
50087
|
var p, pp, y;
|
|
48449
50088
|
for (var i=0, n=path.length; i<n; i++) {
|
|
48450
50089
|
p = path[i];
|
|
@@ -48812,6 +50451,16 @@ ${svg}
|
|
|
48812
50451
|
|
|
48813
50452
|
var POLAR_BUFFER_MARGIN_DEGREES$1 = 1e-4;
|
|
48814
50453
|
|
|
50454
|
+
// Distance at which to place the vertices of a round join whose chords span
|
|
50455
|
+
// stepAngle degrees, so that the chord MIDPOINTS -- not the vertices -- lie on
|
|
50456
|
+
// the offset circle of radius dist. The join then brackets the true arc instead
|
|
50457
|
+
// of being inscribed in it (see makeInscribedRoundJoin). The correction grows
|
|
50458
|
+
// with the step angle, so a gentle bend is barely moved and a sharp corner gets
|
|
50459
|
+
// the full ~0.5% of dist (at the default 8 segments/quadrant).
|
|
50460
|
+
function circumscribedJoinDist(dist, stepAngle) {
|
|
50461
|
+
return dist / Math.cos(stepAngle / 2 / R2D$8);
|
|
50462
|
+
}
|
|
50463
|
+
|
|
48815
50464
|
|
|
48816
50465
|
// Returns a function for generating GeoJSON MultiPolygon geometries
|
|
48817
50466
|
function getPolylineBufferMaker(dataset, opts) {
|
|
@@ -48821,6 +50470,7 @@ ${svg}
|
|
|
48821
50470
|
var getOffsetPoint = getOffsetFunction(crs, opts);
|
|
48822
50471
|
var roundJoinSegsPerQuadrant = opts.quad_segs >= 2 ? opts.quad_segs : 8;
|
|
48823
50472
|
var roundJoinSegAngle = 90 / roundJoinSegsPerQuadrant;
|
|
50473
|
+
var circumscribeJoins = !!opts.circumscribe_joins; // see makeInscribedRoundJoin
|
|
48824
50474
|
// Max arc step (degrees) for the coarse concave bridge (makeCoarseConcaveJoin),
|
|
48825
50475
|
// the optional low-resolution alternative to makeConcaveJoin in
|
|
48826
50476
|
// traceCleanOffsetSide. Larger = fewer points = faster dissolve. NOTE: a
|
|
@@ -49994,14 +51644,36 @@ ${svg}
|
|
|
49994
51644
|
[getOffsetPoint(x, y, startDir + 180, dist)];
|
|
49995
51645
|
}
|
|
49996
51646
|
|
|
49997
|
-
//
|
|
49998
|
-
//
|
|
51647
|
+
// Round join: an equal-angle-step polygonal approximation of the arc between
|
|
51648
|
+
// the incoming and outgoing offset edges, replacing the incoming edge's end
|
|
51649
|
+
// (p2Prev) and the outgoing edge's start (p1).
|
|
51650
|
+
//
|
|
51651
|
+
// Inscribed (default): vertices at the true offset distance, stepping from the
|
|
51652
|
+
// incoming perpendicular round to the outgoing one. Because its chords cut the
|
|
51653
|
+
// corner, the join lies INSIDE the true circular offset, falling short of it by
|
|
51654
|
+
// up to one sagitta (~0.5% of dist at the default 8 segments/quadrant).
|
|
51655
|
+
//
|
|
51656
|
+
// Circumscribed (circumscribe_joins): the same vertices, moved out to
|
|
51657
|
+
// circumscribedJoinDist so the chords span the offset circle instead of cutting
|
|
51658
|
+
// inside it. The LAST vertex stays at the true distance because the outgoing
|
|
51659
|
+
// straight edge attaches there: moving it radially outward would carry that edge
|
|
51660
|
+
// outward too, and since it also seeds the elbow joins of the segments that
|
|
51661
|
+
// follow, the bias propagates into a collar of offset-vs-source overshoot along
|
|
51662
|
+
// the whole outline -- which the fill-gaps mask cannot tell from a real gap fill
|
|
51663
|
+
// (its coast test tolerance is itself calibrated to the join sagitta). Leaving
|
|
51664
|
+
// that one vertex on the circle costs some accuracy: the final chord still dips
|
|
51665
|
+
// inside by about half a sagitta, so a gap tip whose closure happens to fall on
|
|
51666
|
+
// that chord is still short by ~0.25% of dist. Raise quad-segs to shrink it.
|
|
49999
51667
|
function makeInscribedRoundJoin(cx, cy, startBearing, arcAngle, dist) {
|
|
50000
51668
|
var pointCount = Math.max(1, Math.round(arcAngle / roundJoinSegAngle));
|
|
50001
51669
|
var stepAngle = arcAngle / pointCount;
|
|
50002
51670
|
var points = [];
|
|
50003
|
-
|
|
50004
|
-
|
|
51671
|
+
var i;
|
|
51672
|
+
var outerDist = circumscribeJoins ?
|
|
51673
|
+
circumscribedJoinDist(dist, stepAngle) : dist;
|
|
51674
|
+
for (i = 1; i <= pointCount; i++) {
|
|
51675
|
+
points.push(getOffsetPoint(cx, cy, startBearing + stepAngle * i,
|
|
51676
|
+
i == pointCount ? dist : outerDist));
|
|
50005
51677
|
}
|
|
50006
51678
|
return points;
|
|
50007
51679
|
}
|
|
@@ -50610,10 +52282,24 @@ ${svg}
|
|
|
50610
52282
|
if (filterOpts.skipShape && filterOpts.skipShape(srcShape, srcArcs)) return bufShape;
|
|
50611
52283
|
var intervalPct = simplifyFn ? simplifyFn(distance) / distance : 0;
|
|
50612
52284
|
return filterOutlineArtifactHolesFromShape(bufShape, bufArcs, srcShape,
|
|
50613
|
-
srcArcs, distance, intervalPct, sagPct, oneSided, sideOpts, spherical
|
|
52285
|
+
srcArcs, distance, intervalPct, sagPct, oneSided, sideOpts, spherical,
|
|
52286
|
+
filterOpts.coverageIndex || null);
|
|
50614
52287
|
});
|
|
50615
52288
|
}
|
|
50616
52289
|
|
|
52290
|
+
// True if some other output feature covers the inside of this hole, making it
|
|
52291
|
+
// that feature's territory rather than an artifact of this one's
|
|
52292
|
+
// construction. @index is a PathIndex over every output shape: a hole is
|
|
52293
|
+
// outside its own feature by definition, so an enclosed probe can only be
|
|
52294
|
+
// another feature's.
|
|
52295
|
+
function holeIsNeighborTerritory(path, arcs, index) {
|
|
52296
|
+
var probes = getHoleInteriorProbes(getProjectedRingPoints(path, arcs, false));
|
|
52297
|
+
for (var i = 0; i < probes.length; i++) {
|
|
52298
|
+
if (index.pointIsEnclosed(probes[i])) return true;
|
|
52299
|
+
}
|
|
52300
|
+
return false;
|
|
52301
|
+
}
|
|
52302
|
+
|
|
50617
52303
|
// Remove artifact rings left by dissolving the self-intersecting outline rings
|
|
50618
52304
|
// made by the two-sided buffer fast path. The outline's concave-join loops can
|
|
50619
52305
|
// survive the dissolve as spurious holes (and, degenerately, as zero-area rings
|
|
@@ -50623,8 +52309,12 @@ ${svg}
|
|
|
50623
52309
|
// that its entire region lies inside the true buffer, and kept otherwise.
|
|
50624
52310
|
// Filters one buffer shape (one source feature's dissolved outline); returns
|
|
50625
52311
|
// the filtered shape, or null if nothing survives.
|
|
52312
|
+
// @coverageIndex: optional PathIndex over every output shape; a hole another
|
|
52313
|
+
// feature covers is kept whatever the distance test says. See the topological
|
|
52314
|
+
// polygon buffer, whose holes can be a neighbour's territory.
|
|
50626
52315
|
function filterOutlineArtifactHolesFromShape(bufShape, bufArcs, srcShape,
|
|
50627
|
-
srcArcs, distance, intervalPct, sagPct, oneSided, sideOpts, spherical
|
|
52316
|
+
srcArcs, distance, intervalPct, sagPct, oneSided, sideOpts, spherical,
|
|
52317
|
+
coverageIndex) {
|
|
50628
52318
|
var sourceSegments = null;
|
|
50629
52319
|
var sourceParts = null;
|
|
50630
52320
|
var shape2 = (bufShape || []).filter(function(path) {
|
|
@@ -50637,6 +52327,9 @@ ${svg}
|
|
|
50637
52327
|
return false;
|
|
50638
52328
|
}
|
|
50639
52329
|
if (!isHole) return true;
|
|
52330
|
+
if (coverageIndex && holeIsNeighborTerritory(path, bufArcs, coverageIndex)) {
|
|
52331
|
+
return true;
|
|
52332
|
+
}
|
|
50640
52333
|
if (oneSided) {
|
|
50641
52334
|
if (!sourceParts) {
|
|
50642
52335
|
sourceParts = getSourcePathParts(srcShape, srcArcs, spherical);
|
|
@@ -52767,6 +54460,43 @@ ${svg}
|
|
|
52767
54460
|
// floor/maxSpacing ratio at which small mosaics already sample cleanly.
|
|
52768
54461
|
var FLOOR_DISTANCE_FRACTION = 0.1;
|
|
52769
54462
|
|
|
54463
|
+
// Soft cap on the number of entries in the boundary-segment grid, used to floor
|
|
54464
|
+
// the cell size (see buildSegmentGrid). The cell otherwise tracks the buffer
|
|
54465
|
+
// distance alone, so a small distance on a large input gives cells far shorter
|
|
54466
|
+
// than a single boundary segment and the index grows in inverse proportion to
|
|
54467
|
+
// the distance: a nationwide mosaic buffered at 1m spans ~1e7 cells, which costs
|
|
54468
|
+
// hundreds of megabytes and seconds of build time before any distance is
|
|
54469
|
+
// measured. A wider cell is always correct -- the 3x3 query window only requires
|
|
54470
|
+
// cell >= reach -- it just puts more segments in each bucket, so the budget
|
|
54471
|
+
// trades index size against bucket scan length. This value keeps the average
|
|
54472
|
+
// bucket near one segment on a nationwide mosaic, so the queries stay as cheap as
|
|
54473
|
+
// they are at large buffer distances.
|
|
54474
|
+
var GRID_INSERTION_BUDGET = 5e5;
|
|
54475
|
+
|
|
54476
|
+
// Rungs of the widening box search in treeGapAtPoint, and the factor between
|
|
54477
|
+
// them: four rungs a factor of 16 apart, so the search starts at 1/4096 of the
|
|
54478
|
+
// reach and climbs to it in three more steps. The step is coarse deliberately.
|
|
54479
|
+
// The rungs cost a geometric series dominated by the last, so a query that has
|
|
54480
|
+
// to climb the whole ladder pays only a few percent more than one that jumps
|
|
54481
|
+
// straight to the top, while a short ladder keeps the fixed per-search cost
|
|
54482
|
+
// down. Both numbers are fixed, and the last rung is always the full reach,
|
|
54483
|
+
// which is what bounds the query: it makes at most LADDER_RUNGS searches
|
|
54484
|
+
// whatever the input, and cannot widen past the reach.
|
|
54485
|
+
var LADDER_RUNGS = 4;
|
|
54486
|
+
var LADDER_STEP = 16;
|
|
54487
|
+
var LADDER_BASE = Math.pow(LADDER_STEP, LADDER_RUNGS - 1);
|
|
54488
|
+
|
|
54489
|
+
// Window size (segments in the 3x3 cell neighborhood) above which gapAtPoint
|
|
54490
|
+
// asks the tree instead of scanning. A scan rejects a segment belonging to the
|
|
54491
|
+
// query point's own feature in a couple of nanoseconds, so a window of a few
|
|
54492
|
+
// thousand still costs about what one tree query does -- that equivalence is
|
|
54493
|
+
// what the threshold marks, and below it the scan wins on fixed costs alone.
|
|
54494
|
+
// Purely a speed tradeoff: the two paths return the same distance, so this can
|
|
54495
|
+
// be retuned freely. Measured on two county mosaics, scanning up to this size
|
|
54496
|
+
// left the cheapest cases at their original speed while still handing over
|
|
54497
|
+
// early enough to keep the widest buffers off the scan's quadratic path.
|
|
54498
|
+
var WINDOW_SCAN_LIMIT = 3000;
|
|
54499
|
+
|
|
52770
54500
|
// Soft target total site count. coarsen scales the gap-proportional spacing up
|
|
52771
54501
|
// until the predicted total falls under this, keeping the Delaunay bounded on
|
|
52772
54502
|
// dense shared-border mosaics while leaving sparse inputs at coarsen=1 (fully
|
|
@@ -52793,10 +54523,18 @@ ${svg}
|
|
|
52793
54523
|
profileStart('medial:assembleChains');
|
|
52794
54524
|
var chains = assembleChains(medial.segments, medial.coords);
|
|
52795
54525
|
profileEnd('medial:assembleChains');
|
|
54526
|
+
profileStart('medial:recenter');
|
|
54527
|
+
chains = chains.map(function(chain) {
|
|
54528
|
+
return recenterMedialChain(chain, sites.grid);
|
|
54529
|
+
});
|
|
54530
|
+
profileEnd('medial:recenter');
|
|
52796
54531
|
if (opts.smooth) {
|
|
52797
54532
|
profileStart('medial:smooth');
|
|
52798
54533
|
chains = chains.map(function(chain) {
|
|
52799
|
-
|
|
54534
|
+
// Smoothing uses one kernel width for the whole chain (see
|
|
54535
|
+
// smoothMedialChain), so on a channel whose width varies it can bow the
|
|
54536
|
+
// line out of the narrow stretches; re-center again to pull it back.
|
|
54537
|
+
return recenterMedialChain(smoothMedialChain(chain, sites.grid), sites.grid);
|
|
52800
54538
|
});
|
|
52801
54539
|
profileEnd('medial:smooth');
|
|
52802
54540
|
}
|
|
@@ -52814,7 +54552,9 @@ ${svg}
|
|
|
52814
54552
|
for (var di = 0; di < coordDistances.length; di++) {
|
|
52815
54553
|
if (coordDistances[di] > extendDist) extendDist = coordDistances[di];
|
|
52816
54554
|
}
|
|
52817
|
-
|
|
54555
|
+
// Local gap partitioning joins multi-owner junctions before extending only
|
|
54556
|
+
// the remaining outer endpoints; it opts out of this blanket extension.
|
|
54557
|
+
if (extendDist > 0 && !opts.no_extend) {
|
|
52818
54558
|
chains = chains.map(function(chain) {
|
|
52819
54559
|
return extendChainEndpoints(chain, extendDist);
|
|
52820
54560
|
});
|
|
@@ -53036,6 +54776,150 @@ ${svg}
|
|
|
53036
54776
|
return Math.sqrt(best);
|
|
53037
54777
|
}
|
|
53038
54778
|
|
|
54779
|
+
// A medial vertex counts as off-center when its distance to the nearer bank
|
|
54780
|
+
// falls below this fraction of its distance to the opposite bank (1 = exactly
|
|
54781
|
+
// centered). Where a channel narrows past the site spacing the sampled Voronoi
|
|
54782
|
+
// can no longer resolve it: the triangles spanning the neck are slivers whose
|
|
54783
|
+
// circumcenters land on, or beyond, one bank, so the assembled chain zigzags
|
|
54784
|
+
// from side to side instead of running down the middle. Finer sampling cannot
|
|
54785
|
+
// cure this -- a channel that pinches to a point has zero width while the
|
|
54786
|
+
// spacing has a floor (see spacingFromGap) -- so the geometry is corrected
|
|
54787
|
+
// afterwards instead. 0.5 leaves the well-sampled interior (measured balance
|
|
54788
|
+
// ~0.9) untouched and catches the zigzag (~0.1 and below).
|
|
54789
|
+
var MEDIAL_BALANCE_TOLERANCE = 0.5;
|
|
54790
|
+
|
|
54791
|
+
// Pull medial vertices that are off-center, or have slipped inside a source
|
|
54792
|
+
// polygon, back onto the local centerline: each is replaced by the midpoint of
|
|
54793
|
+
// its nearest footpoint on either bank. That midpoint is equidistant from the two
|
|
54794
|
+
// banks by construction and lies between them, so it stays inside the channel
|
|
54795
|
+
// however narrow it gets; where the channel pinches shut both footpoints converge
|
|
54796
|
+
// on the neck and the line passes straight through it rather than bouncing off
|
|
54797
|
+
// the sides.
|
|
54798
|
+
//
|
|
54799
|
+
// Valid vertices are left exactly as sampled, so a well-resolved channel keeps
|
|
54800
|
+
// its true Voronoi geometry. Chain endpoints are also left alone: they sit
|
|
54801
|
+
// outside the overlap on purpose (hull rays, junction ends) so the cut-line can
|
|
54802
|
+
// reach the enclosing boundary, and re-centering them would pull the cut back
|
|
54803
|
+
// inside the tile it has to span.
|
|
54804
|
+
function recenterMedialChain(points, ctx) {
|
|
54805
|
+
if (!ctx || points.length < 3) return points;
|
|
54806
|
+
points = straightenChainTails(points, ctx);
|
|
54807
|
+
if (points.length < 3) return points;
|
|
54808
|
+
var out = [points[0]];
|
|
54809
|
+
for (var i = 1; i < points.length - 1; i++) {
|
|
54810
|
+
var p = recenteredVertex(ctx, points[i]);
|
|
54811
|
+
// Successive off-center vertices can re-center onto the same neck point;
|
|
54812
|
+
// keep one copy so the chain has no zero-length segments.
|
|
54813
|
+
if (!samePoint(p, out[out.length - 1])) out.push(p);
|
|
54814
|
+
}
|
|
54815
|
+
var last = points[points.length - 1];
|
|
54816
|
+
if (!samePoint(last, out[out.length - 1])) out.push(last);
|
|
54817
|
+
return out.length >= 2 ? out : points;
|
|
54818
|
+
}
|
|
54819
|
+
|
|
54820
|
+
// Straighten the runs of vertices at each end of a chain that lie inside a
|
|
54821
|
+
// source polygon. Such a vertex cannot be a medial point (its distance to that
|
|
54822
|
+
// polygon's boundary would be 0), and it cannot be re-centered either: these
|
|
54823
|
+
// runs trail past the mouth of a gap, where the banks converge into a border the
|
|
54824
|
+
// medial construction deliberately does not see (a shared arc partitions any
|
|
54825
|
+
// overlap by itself, so collectCandidateArcPaths prunes it), leaving no channel
|
|
54826
|
+
// to center them in. They are not dropped, because a chain has to poke out past
|
|
54827
|
+
// the gap it divides to node against the enclosing boundary (see
|
|
54828
|
+
// extendChainEndpoints): the run is collapsed to one straight segment, which
|
|
54829
|
+
// keeps the chain's reach and loses only its wandering.
|
|
54830
|
+
function straightenChainTails(points, ctx) {
|
|
54831
|
+
var last = points.length - 1;
|
|
54832
|
+
var lo = 0, hi = last;
|
|
54833
|
+
while (lo < hi && insideAnyPolygon(ctx, points[lo])) lo++;
|
|
54834
|
+
while (hi > lo && insideAnyPolygon(ctx, points[hi])) hi--;
|
|
54835
|
+
if (hi - lo < 1) return points; // no interior vertex survives: leave it alone
|
|
54836
|
+
if (lo === 0 && hi === last) return points;
|
|
54837
|
+
var out = points.slice(lo, hi + 1);
|
|
54838
|
+
if (lo > 0) out.unshift(points[0]);
|
|
54839
|
+
if (hi < last) out.push(points[last]);
|
|
54840
|
+
return out;
|
|
54841
|
+
}
|
|
54842
|
+
|
|
54843
|
+
function insideAnyPolygon(ctx, p) {
|
|
54844
|
+
var near = nearestSegment(ctx, p[0], p[1], -1);
|
|
54845
|
+
return near.id !== -1 && insideOwnerPolygon(ctx.seg, near.id, p[0], p[1]);
|
|
54846
|
+
}
|
|
54847
|
+
|
|
54848
|
+
// @p re-centered between its two nearest banks, or @p itself when it is already
|
|
54849
|
+
// a valid centerline point or only one feature is in range.
|
|
54850
|
+
var footA = [0, 0], footB = [0, 0]; // scratch, not retained
|
|
54851
|
+
function recenteredVertex(ctx, p) {
|
|
54852
|
+
var near = nearestSegment(ctx, p[0], p[1], -1);
|
|
54853
|
+
if (near.id === -1) return p;
|
|
54854
|
+
var far = nearestSegment(ctx, p[0], p[1], ctx.seg.feat[near.id]);
|
|
54855
|
+
if (far.id === -1) return p;
|
|
54856
|
+
// Only a pair of banks within their combined buffer reach forms a channel this
|
|
54857
|
+
// vertex could be the centerline of. Past the mouth of a gap the opposite bank
|
|
54858
|
+
// can be arbitrarily far away, and pulling the vertex to the midpoint of that
|
|
54859
|
+
// pair would move it hundreds of metres off any boundary.
|
|
54860
|
+
if (far.dist > ctx.seg.reach[near.id] + ctx.seg.reach[far.id]) return p;
|
|
54861
|
+
var offCenter = near.dist < far.dist * MEDIAL_BALANCE_TOLERANCE;
|
|
54862
|
+
if (!offCenter && !insideOwnerPolygon(ctx.seg, near.id, p[0], p[1])) return p;
|
|
54863
|
+
segmentFoot(ctx.seg, near.id, p[0], p[1], footA);
|
|
54864
|
+
segmentFoot(ctx.seg, far.id, p[0], p[1], footB);
|
|
54865
|
+
return [(footA[0] + footB[0]) / 2, (footA[1] + footB[1]) / 2];
|
|
54866
|
+
}
|
|
54867
|
+
|
|
54868
|
+
// True when (x, y) lies on the interior side of indexed segment @s, i.e. inside
|
|
54869
|
+
// the feature that owns it. Source rings are traversed with the polygon interior
|
|
54870
|
+
// on their right, and seg.side records whether the stored segment direction
|
|
54871
|
+
// matches its owner's traversal, so the sign of the cross product places the
|
|
54872
|
+
// point. Judged against the point's nearest segment, this is the test that
|
|
54873
|
+
// catches a medial vertex thrown out of a pinching gap into a neighbouring
|
|
54874
|
+
// polygon -- distance cannot: such a vertex is a channel width behind one bank
|
|
54875
|
+
// and two channel widths from the other, the same ratio as a merely off-center
|
|
54876
|
+
// point still inside the channel. A true medial vertex is never inside a source
|
|
54877
|
+
// polygon (its distance to that polygon's boundary would be 0).
|
|
54878
|
+
function insideOwnerPolygon(seg, s, x, y) {
|
|
54879
|
+
var dx = seg.x1[s] - seg.x0[s], dy = seg.y1[s] - seg.y0[s];
|
|
54880
|
+
var cross = dx * (y - seg.y0[s]) - dy * (x - seg.x0[s]);
|
|
54881
|
+
return cross * seg.side[s] < 0;
|
|
54882
|
+
}
|
|
54883
|
+
|
|
54884
|
+
// The nearest indexed source segment to (x, y), skipping segments owned by
|
|
54885
|
+
// @excludeFeat (-1 excludes nothing). Probes the 3x3 grid-cell neighborhood
|
|
54886
|
+
// (cell == max reach). id is -1 when the window holds no eligible segment.
|
|
54887
|
+
function nearestSegment(ctx, x, y, excludeFeat) {
|
|
54888
|
+
var seg = ctx.seg;
|
|
54889
|
+
var cx = ctx.colOf(x), cy = ctx.rowOf(y);
|
|
54890
|
+
var bestId = -1, best = Infinity;
|
|
54891
|
+
for (var gx = cx - 1; gx <= cx + 1; gx++) {
|
|
54892
|
+
for (var gy = cy - 1; gy <= cy + 1; gy++) {
|
|
54893
|
+
var bucket = ctx.grid.get(ctx.cellKey(gx, gy));
|
|
54894
|
+
if (!bucket) continue;
|
|
54895
|
+
for (var b = 0; b < bucket.length; b++) {
|
|
54896
|
+
var s = bucket[b];
|
|
54897
|
+
if (seg.feat[s] === excludeFeat) continue;
|
|
54898
|
+
var d2 = pointSegDistSq2(x, y, seg.x0[s], seg.y0[s], seg.x1[s], seg.y1[s]);
|
|
54899
|
+
if (d2 < best) { best = d2; bestId = s; }
|
|
54900
|
+
}
|
|
54901
|
+
}
|
|
54902
|
+
}
|
|
54903
|
+
return {id: bestId, dist: Math.sqrt(best)};
|
|
54904
|
+
}
|
|
54905
|
+
|
|
54906
|
+
// Closest point to (x, y) on indexed segment @s, clamped to its endpoints,
|
|
54907
|
+
// written into @out.
|
|
54908
|
+
function segmentFoot(seg, s, x, y, out) {
|
|
54909
|
+
var ax = seg.x0[s], ay = seg.y0[s];
|
|
54910
|
+
var dx = seg.x1[s] - ax, dy = seg.y1[s] - ay;
|
|
54911
|
+
var len2 = dx * dx + dy * dy;
|
|
54912
|
+
var t = len2 > 0 ? ((x - ax) * dx + (y - ay) * dy) / len2 : 0;
|
|
54913
|
+
if (t < 0) t = 0;
|
|
54914
|
+
if (t > 1) t = 1;
|
|
54915
|
+
out[0] = ax + t * dx;
|
|
54916
|
+
out[1] = ay + t * dy;
|
|
54917
|
+
}
|
|
54918
|
+
|
|
54919
|
+
function samePoint(a, b) {
|
|
54920
|
+
return a[0] === b[0] && a[1] === b[1];
|
|
54921
|
+
}
|
|
54922
|
+
|
|
53039
54923
|
// Boundary sample spacing as a fraction of the local gap width: smaller gives a
|
|
53040
54924
|
// smoother medial axis (more sites) in narrow channels. 0.5 keeps the spacing
|
|
53041
54925
|
// at most half the gap, so a channel of width w has at least two samples per
|
|
@@ -53083,7 +54967,12 @@ ${svg}
|
|
|
53083
54967
|
if (f === -1 && r === -1) continue; // arc not used by a buffered feature
|
|
53084
54968
|
if (f !== -1 && r !== -1) continue; // shared interior border -- not a gap
|
|
53085
54969
|
var pts = arcCoords(arcs, i);
|
|
53086
|
-
|
|
54970
|
+
// forward records whether the owner traverses the arc in the stored
|
|
54971
|
+
// direction, which is what fixes the interior side of its segments (see
|
|
54972
|
+
// insideOwnerPolygon).
|
|
54973
|
+
if (pts.length >= 2) {
|
|
54974
|
+
paths.push({owner: f !== -1 ? f : r, points: pts, forward: f !== -1});
|
|
54975
|
+
}
|
|
53087
54976
|
}
|
|
53088
54977
|
return paths;
|
|
53089
54978
|
}
|
|
@@ -53136,8 +55025,15 @@ ${svg}
|
|
|
53136
55025
|
// in a single pass, then we densify once.
|
|
53137
55026
|
var grid = buildSegmentGrid$1(verts, coordDistances);
|
|
53138
55027
|
var gaps = computeVertexGaps(grid, verts, coordDistances);
|
|
53139
|
-
|
|
53140
|
-
|
|
55028
|
+
// No contested channel anywhere: every vertex is either out of reach of every
|
|
55029
|
+
// other feature (open coast) or within the touching threshold of one (where the
|
|
55030
|
+
// source boundary already partitions). densify + Delaunay would only allocate
|
|
55031
|
+
// sites that keptSites then discards, so stop here. Single-feature inputs and
|
|
55032
|
+
// mosaics whose features are all farther than a buffer-diameter apart take this
|
|
55033
|
+
// path; a mosaic with even one real gap continues.
|
|
55034
|
+
if (!hasContestedGap(gaps, maxDistance)) return null;
|
|
55035
|
+
var coarsen = fitCoarsen(verts, gaps, coordDistances, spacingFloor, grid);
|
|
55036
|
+
var sites = densifyVertices(verts, gaps, coordDistances, spacingFloor, coarsen, grid);
|
|
53141
55037
|
// Triangulate only the sites bordering a real gap. Its medial segments come
|
|
53142
55038
|
// exclusively from cross-feature edges, and the well-shaped triangles that
|
|
53143
55039
|
// bridge a gap have their apex within reach too (a far apex makes a thin
|
|
@@ -53154,65 +55050,232 @@ ${svg}
|
|
|
53154
55050
|
|
|
53155
55051
|
// Bucket every boundary segment into a uniform grid so the nearest cross-feature
|
|
53156
55052
|
// segment to an arbitrary point can be found by probing its 3x3 cell
|
|
53157
|
-
// neighborhood. The cell
|
|
53158
|
-
// distances), so any in-reach segment is guaranteed to fall in that 3x3
|
|
53159
|
-
// Returns null when there is no positive reach. Reused for both the
|
|
53160
|
-
// gap (drives adaptive sampling) and the per-site keep test
|
|
55053
|
+
// neighborhood. The cell is at least the maximum reach (sum of the two largest
|
|
55054
|
+
// buffer distances), so any in-reach segment is guaranteed to fall in that 3x3
|
|
55055
|
+
// window. Returns null when there is no positive reach. Reused for both the
|
|
55056
|
+
// per-vertex gap (drives adaptive sampling) and the per-site keep test
|
|
55057
|
+
// (gapAtPoint).
|
|
53161
55058
|
function buildSegmentGrid$1(verts, coordDistances) {
|
|
53162
55059
|
var paths = verts.paths;
|
|
53163
55060
|
var maxDist = 0;
|
|
53164
55061
|
for (var d = 0; d < coordDistances.length; d++) {
|
|
53165
55062
|
if (coordDistances[d] > maxDist) maxDist = coordDistances[d];
|
|
53166
55063
|
}
|
|
53167
|
-
var
|
|
53168
|
-
if (!(
|
|
55064
|
+
var reach = 2 * maxDist; // upper bound on any pair's reach
|
|
55065
|
+
if (!(reach > 0)) return null;
|
|
53169
55066
|
var xmin = Infinity, ymin = Infinity, ymax = -Infinity;
|
|
55067
|
+
var totalLen = 0;
|
|
53170
55068
|
paths.forEach(function(path) {
|
|
53171
55069
|
var pts = path.points;
|
|
53172
55070
|
for (var i = 0; i < pts.length; i++) {
|
|
53173
55071
|
if (pts[i][0] < xmin) xmin = pts[i][0];
|
|
53174
55072
|
if (pts[i][1] < ymin) ymin = pts[i][1];
|
|
53175
55073
|
if (pts[i][1] > ymax) ymax = pts[i][1];
|
|
55074
|
+
if (i > 0) {
|
|
55075
|
+
var sdx = pts[i][0] - pts[i - 1][0];
|
|
55076
|
+
var sdy = pts[i][1] - pts[i - 1][1];
|
|
55077
|
+
totalLen += Math.sqrt(sdx * sdx + sdy * sdy);
|
|
55078
|
+
}
|
|
53176
55079
|
}
|
|
53177
55080
|
});
|
|
55081
|
+
// A segment occupies about len/cell cells, so the whole index is about
|
|
55082
|
+
// totalLen/cell entries; widening the cell to fit the budget bounds it (see
|
|
55083
|
+
// GRID_INSERTION_BUDGET). The reach is the floor, not the target: a cell
|
|
55084
|
+
// narrower than the reach would break the 3x3 query window.
|
|
55085
|
+
var cell = Math.max(reach, totalLen / GRID_INSERTION_BUDGET);
|
|
53178
55086
|
// +1 cell index shift keeps probed -1 neighbors non-negative; rowSpan packs
|
|
53179
55087
|
// (col, row) into a collision-free integer key.
|
|
53180
55088
|
var rowSpan = Math.floor((ymax - ymin) / cell) + 3;
|
|
53181
55089
|
function cellKey(cx, cy) { return (cx + 1) * rowSpan + (cy + 1); }
|
|
53182
55090
|
function colOf(x) { return Math.floor((x - xmin) / cell); }
|
|
53183
55091
|
function rowOf(y) { return Math.floor((y - ymin) / cell); }
|
|
53184
|
-
var seg = {x0: [], y0: [], x1: [], y1: [], feat: [], reach: []};
|
|
55092
|
+
var seg = {x0: [], y0: [], x1: [], y1: [], feat: [], reach: [], side: []};
|
|
53185
55093
|
var grid = new Map();
|
|
55094
|
+
|
|
55095
|
+
// Index a segment into every cell it crosses, column by column: within one
|
|
55096
|
+
// column the segment covers a single y-range (it is a straight line clipped to
|
|
55097
|
+
// that column's x-range), so the exact row span is two floor()s. This costs
|
|
55098
|
+
// O(len/cell) entries, where stamping the segment's bounding box instead costs
|
|
55099
|
+
// O((len/cell)^2) for a diagonal segment -- on a nationwide input buffered by a
|
|
55100
|
+
// few meters that quadratic term ran to ~1e9 entries and exceeded the runtime's
|
|
55101
|
+
// maximum Map size.
|
|
55102
|
+
function addSegment(ax, ay, bx, by, idx) {
|
|
55103
|
+
if (ax > bx) {
|
|
55104
|
+
var tx = ax; ax = bx; bx = tx;
|
|
55105
|
+
var ty = ay; ay = by; by = ty;
|
|
55106
|
+
}
|
|
55107
|
+
var dx = bx - ax, dy = by - ay;
|
|
55108
|
+
var cxa = colOf(ax), cxb = colOf(bx);
|
|
55109
|
+
for (var gx = cxa; gx <= cxb; gx++) {
|
|
55110
|
+
var yLo = ay, yHi = by;
|
|
55111
|
+
if (dx > 0) {
|
|
55112
|
+
// clip the segment to this column and take the y-range of the piece
|
|
55113
|
+
var xLo = Math.max(ax, xmin + gx * cell);
|
|
55114
|
+
var xHi = Math.min(bx, xmin + (gx + 1) * cell);
|
|
55115
|
+
yLo = ay + dy * (xLo - ax) / dx;
|
|
55116
|
+
yHi = ay + dy * (xHi - ax) / dx;
|
|
55117
|
+
}
|
|
55118
|
+
var gya = rowOf(Math.min(yLo, yHi));
|
|
55119
|
+
var gyb = rowOf(Math.max(yLo, yHi));
|
|
55120
|
+
for (var gy = gya; gy <= gyb; gy++) {
|
|
55121
|
+
var key = cellKey(gx, gy);
|
|
55122
|
+
var bucket = grid.get(key);
|
|
55123
|
+
if (bucket) bucket.push(idx); else grid.set(key, [idx]);
|
|
55124
|
+
}
|
|
55125
|
+
}
|
|
55126
|
+
}
|
|
55127
|
+
|
|
53186
55128
|
paths.forEach(function(path) {
|
|
53187
55129
|
var pts = path.points;
|
|
53188
55130
|
var feat = path.owner;
|
|
53189
|
-
var
|
|
55131
|
+
var featReach = coordDistances[feat];
|
|
55132
|
+
var side = path.forward === false ? -1 : 1;
|
|
53190
55133
|
for (var k = 0; k + 1 < pts.length; k++) {
|
|
53191
55134
|
var ax = pts[k][0], ay = pts[k][1], bx = pts[k + 1][0], by = pts[k + 1][1];
|
|
53192
55135
|
var idx = seg.feat.length;
|
|
53193
55136
|
seg.x0.push(ax); seg.y0.push(ay); seg.x1.push(bx); seg.y1.push(by);
|
|
53194
|
-
seg.feat.push(feat); seg.reach.push(
|
|
53195
|
-
|
|
53196
|
-
var cya = rowOf(Math.min(ay, by)), cyb = rowOf(Math.max(ay, by));
|
|
53197
|
-
for (var gx = cxa; gx <= cxb; gx++) {
|
|
53198
|
-
for (var gy = cya; gy <= cyb; gy++) {
|
|
53199
|
-
var key = cellKey(gx, gy);
|
|
53200
|
-
var bucket = grid.get(key);
|
|
53201
|
-
if (bucket) bucket.push(idx); else grid.set(key, [idx]);
|
|
53202
|
-
}
|
|
53203
|
-
}
|
|
55137
|
+
seg.feat.push(feat); seg.reach.push(featReach); seg.side.push(side);
|
|
55138
|
+
addSegment(ax, ay, bx, by, idx);
|
|
53204
55139
|
}
|
|
53205
55140
|
});
|
|
53206
|
-
return {seg: seg, grid: grid, cellKey: cellKey, colOf: colOf, rowOf: rowOf
|
|
55141
|
+
return {seg: seg, grid: grid, cellKey: cellKey, colOf: colOf, rowOf: rowOf,
|
|
55142
|
+
index: buildSegmentTree(seg), maxReach: maxDist};
|
|
55143
|
+
}
|
|
55144
|
+
|
|
55145
|
+
// R-tree over the same segments, used only by gapAtPoint and only where the
|
|
55146
|
+
// grid's query window has grown expensive (see there). The grid remains the
|
|
55147
|
+
// index of record: it answers every other probe in this file.
|
|
55148
|
+
function buildSegmentTree(seg) {
|
|
55149
|
+
var n = seg.feat.length;
|
|
55150
|
+
if (n === 0) return null;
|
|
55151
|
+
var index = new Flatbush(n);
|
|
55152
|
+
for (var i = 0; i < n; i++) {
|
|
55153
|
+
var x0 = seg.x0[i], x1 = seg.x1[i], y0 = seg.y0[i], y1 = seg.y1[i];
|
|
55154
|
+
index.add(Math.min(x0, x1), Math.min(y0, y1), Math.max(x0, x1), Math.max(y0, y1));
|
|
55155
|
+
}
|
|
55156
|
+
index.finish();
|
|
55157
|
+
return index;
|
|
53207
55158
|
}
|
|
53208
55159
|
|
|
53209
55160
|
// The channel width at (x, y): distance to the nearest different-feature segment
|
|
53210
55161
|
// within their combined reach, or Infinity if none. Works for any point, not
|
|
53211
55162
|
// just original vertices, so a long edge whose endpoints are out of reach but
|
|
53212
55163
|
// whose middle crosses a gap is still measured correctly at the interior sites.
|
|
55164
|
+
// Answered from whichever index is cheaper here. Both return the same number,
|
|
55165
|
+
// so the choice is a speed decision only: the window scan costs one pass over
|
|
55166
|
+
// the segments in the point's grid cells, which is nothing on a sparse
|
|
55167
|
+
// neighborhood but grows as the square of the buffer distance (the cell is
|
|
55168
|
+
// floored at the reach), while the tree search does not grow that way but pays
|
|
55169
|
+
// fixed costs on every call.
|
|
53213
55170
|
function gapAtPoint(ctx, x, y, feat, reachF) {
|
|
53214
|
-
|
|
53215
|
-
|
|
55171
|
+
var n = collectWindow(ctx, x, y);
|
|
55172
|
+
if (ctx.index && windowSize > WINDOW_SCAN_LIMIT) {
|
|
55173
|
+
return treeGapAtPoint(ctx, x, y, feat, reachF);
|
|
55174
|
+
}
|
|
55175
|
+
return scanWindow(ctx.seg, n, x, y, feat, reachF, Infinity);
|
|
55176
|
+
}
|
|
55177
|
+
|
|
55178
|
+
// Number of segments in the 3x3 cell window last collected by collectWindow.
|
|
55179
|
+
var windowSize = 0;
|
|
55180
|
+
// Scratch list of that window's buckets, reused across calls to keep the query
|
|
55181
|
+
// allocation-free. Safe because a query never re-enters gapAtPoint: the buckets
|
|
55182
|
+
// are consumed by scanWindow before anything else runs.
|
|
55183
|
+
var windowBuckets = [];
|
|
55184
|
+
|
|
55185
|
+
// Gather the buckets of the 3x3 cell neighborhood around (x, y) into the
|
|
55186
|
+
// scratch list, returning how many there are and setting windowSize to the
|
|
55187
|
+
// total segment count. The cell is at least the maximum reach, so this window
|
|
55188
|
+
// is guaranteed to hold every segment within reach of the point.
|
|
55189
|
+
function collectWindow(ctx, x, y) {
|
|
55190
|
+
var cx = ctx.colOf(x), cy = ctx.rowOf(y);
|
|
55191
|
+
var n = 0;
|
|
55192
|
+
windowSize = 0;
|
|
55193
|
+
for (var gx = cx - 1; gx <= cx + 1; gx++) {
|
|
55194
|
+
for (var gy = cy - 1; gy <= cy + 1; gy++) {
|
|
55195
|
+
var bucket = ctx.grid.get(ctx.cellKey(gx, gy));
|
|
55196
|
+
if (!bucket) continue;
|
|
55197
|
+
windowBuckets[n++] = bucket;
|
|
55198
|
+
windowSize += bucket.length;
|
|
55199
|
+
}
|
|
55200
|
+
}
|
|
55201
|
+
return n;
|
|
55202
|
+
}
|
|
55203
|
+
|
|
55204
|
+
// Exhaustive scan of the collected window: distance to the nearest
|
|
55205
|
+
// different-feature segment within their combined reach, or @best if none is
|
|
55206
|
+
// closer.
|
|
55207
|
+
function scanWindow(seg, n, x, y, feat, reachF, best) {
|
|
55208
|
+
for (var i = 0; i < n; i++) {
|
|
55209
|
+
var bucket = windowBuckets[i];
|
|
55210
|
+
for (var b = 0; b < bucket.length; b++) {
|
|
55211
|
+
var s = bucket[b];
|
|
55212
|
+
if (seg.feat[s] === feat) continue;
|
|
55213
|
+
var reach = reachF + seg.reach[s];
|
|
55214
|
+
var dsq = pointSegDistSq2(x, y, seg.x0[s], seg.y0[s], seg.x1[s], seg.y1[s]);
|
|
55215
|
+
if (dsq <= reach * reach) {
|
|
55216
|
+
var dist = Math.sqrt(dsq);
|
|
55217
|
+
if (dist < best) best = dist;
|
|
55218
|
+
}
|
|
55219
|
+
}
|
|
55220
|
+
}
|
|
55221
|
+
return best;
|
|
55222
|
+
}
|
|
55223
|
+
|
|
55224
|
+
// Same query answered from the R-tree, by widening a box search along a fixed
|
|
55225
|
+
// ladder of radii until the answer it holds is provably the global one.
|
|
55226
|
+
//
|
|
55227
|
+
// The invariant that makes an intermediate rung conclusive: a box of half-width
|
|
55228
|
+
// r contains the whole disc of radius r, so every segment closer than r to the
|
|
55229
|
+
// point is among the hits. If the nearest in-reach hit is itself within r, no
|
|
55230
|
+
// unreturned segment can beat it and the search can stop. Otherwise the answer
|
|
55231
|
+
// might lie in the annulus beyond r and the next rung has to look.
|
|
55232
|
+
//
|
|
55233
|
+
// The ladder is what keeps the cost sane in both directions. Starting small
|
|
55234
|
+
// makes the common case -- a point on one bank of a narrow gap, its neighbor a
|
|
55235
|
+
// few meters away -- cost one search of a tiny box, independent of the buffer
|
|
55236
|
+
// distance. Ending at the full reach makes the rare case -- a point with no
|
|
55237
|
+
// feature anywhere near it, so nothing can be proven short of exhausting the
|
|
55238
|
+
// reach -- cost the geometric sum of the rungs, a small multiple of going there
|
|
55239
|
+
// directly. The step is coarse so that there are only a handful of rungs.
|
|
55240
|
+
//
|
|
55241
|
+
// A previous attempt used neighbors() to take the k nearest instead. It was
|
|
55242
|
+
// much worse here, because a query that cannot find k in-reach candidates makes
|
|
55243
|
+
// neighbors() heap-walk every leaf in the whole reach disc -- and points with
|
|
55244
|
+
// no feature within reach are exactly what a coastline is made of.
|
|
55245
|
+
function treeGapAtPoint(ctx, x, y, feat, reachF) {
|
|
55246
|
+
var seg = ctx.seg;
|
|
55247
|
+
var index = ctx.index;
|
|
55248
|
+
// Nothing beyond the largest possible pair reach can pass the test below, so
|
|
55249
|
+
// the ladder tops out there rather than searching the whole extent.
|
|
55250
|
+
var maxDist = reachF + ctx.maxReach;
|
|
55251
|
+
var filter = function(s) { return seg.feat[s] !== feat; };
|
|
55252
|
+
var r = maxDist / LADDER_BASE;
|
|
55253
|
+
for (var k = 0; k < LADDER_RUNGS - 1; k++) {
|
|
55254
|
+
var best = nearestInBox(seg, index, x, y, r, reachF, filter);
|
|
55255
|
+
if (best <= r) return best;
|
|
55256
|
+
r *= LADDER_STEP;
|
|
55257
|
+
}
|
|
55258
|
+
// The top rung covers every segment that could be in reach at all, so its
|
|
55259
|
+
// answer is final even when nothing was found.
|
|
55260
|
+
return nearestInBox(seg, index, x, y, maxDist, reachF, filter);
|
|
55261
|
+
}
|
|
55262
|
+
|
|
55263
|
+
// Distance from (x, y) to the nearest segment that passes @filter and lies
|
|
55264
|
+
// within its pair reach, considering only segments whose bounding box meets the
|
|
55265
|
+
// box of half-width @r. Infinity if there is none.
|
|
55266
|
+
function nearestInBox(seg, index, x, y, r, reachF, filter) {
|
|
55267
|
+
var ids = index.search(x - r, y - r, x + r, y + r, filter);
|
|
55268
|
+
var best = Infinity;
|
|
55269
|
+
for (var i = 0; i < ids.length; i++) {
|
|
55270
|
+
var s = ids[i];
|
|
55271
|
+
var reach = reachF + seg.reach[s];
|
|
55272
|
+
var dsq = pointSegDistSq2(x, y, seg.x0[s], seg.y0[s], seg.x1[s], seg.y1[s]);
|
|
55273
|
+
if (dsq <= reach * reach) {
|
|
55274
|
+
var dist = Math.sqrt(dsq);
|
|
55275
|
+
if (dist < best) best = dist;
|
|
55276
|
+
}
|
|
55277
|
+
}
|
|
55278
|
+
return best;
|
|
53216
55279
|
}
|
|
53217
55280
|
|
|
53218
55281
|
// Local gap at each original vertex (drives adaptive sampling, see
|
|
@@ -53239,31 +55302,6 @@ ${svg}
|
|
|
53239
55302
|
return gaps;
|
|
53240
55303
|
}
|
|
53241
55304
|
|
|
53242
|
-
// Distance from (vx, vy) (owned by @feat, reach @reachF) to the nearest
|
|
53243
|
-
// different-feature segment within their combined reach, or @best if none is
|
|
53244
|
-
// closer. Probes the 3x3 grid-cell neighborhood (cell == max reach).
|
|
53245
|
-
function nearestCrossFeatureSegmentDist(vx, vy, feat, reachF, seg, grid, cellKey,
|
|
53246
|
-
colOf, rowOf, best) {
|
|
53247
|
-
var cx = colOf(vx), cy = rowOf(vy);
|
|
53248
|
-
for (var gx = cx - 1; gx <= cx + 1; gx++) {
|
|
53249
|
-
for (var gy = cy - 1; gy <= cy + 1; gy++) {
|
|
53250
|
-
var bucket = grid.get(cellKey(gx, gy));
|
|
53251
|
-
if (!bucket) continue;
|
|
53252
|
-
for (var b = 0; b < bucket.length; b++) {
|
|
53253
|
-
var s = bucket[b];
|
|
53254
|
-
if (seg.feat[s] === feat) continue;
|
|
53255
|
-
var reach = reachF + seg.reach[s];
|
|
53256
|
-
var dsq = pointSegDistSq2(vx, vy, seg.x0[s], seg.y0[s], seg.x1[s], seg.y1[s]);
|
|
53257
|
-
if (dsq <= reach * reach) {
|
|
53258
|
-
var dist = Math.sqrt(dsq);
|
|
53259
|
-
if (dist < best) best = dist;
|
|
53260
|
-
}
|
|
53261
|
-
}
|
|
53262
|
-
}
|
|
53263
|
-
}
|
|
53264
|
-
return best;
|
|
53265
|
-
}
|
|
53266
|
-
|
|
53267
55305
|
// True when medial vertex c lies in the buffer overlap of features fp and fq:
|
|
53268
55306
|
// within fp's radius of an fp-owned source segment AND within fq's radius of an
|
|
53269
55307
|
// fq-owned source segment. Measured against the actual source segments via the
|
|
@@ -53376,21 +55414,52 @@ ${svg}
|
|
|
53376
55414
|
return {paths: layout, count: count};
|
|
53377
55415
|
}
|
|
53378
55416
|
|
|
55417
|
+
// True when any vertex borders a real gap: within reach of another feature, but
|
|
55418
|
+
// farther than the touching threshold. Matching keptSites' keep rule, so when
|
|
55419
|
+
// this is false every densified site would be discarded and there is nothing for
|
|
55420
|
+
// the medial to cut.
|
|
55421
|
+
function hasContestedGap(gaps, maxDistance) {
|
|
55422
|
+
var touch = maxDistance * TOUCHING_GAP_FRACTION;
|
|
55423
|
+
for (var i = 0; i < gaps.length; i++) {
|
|
55424
|
+
if (isFinite(gaps[i]) && gaps[i] > touch) return true;
|
|
55425
|
+
}
|
|
55426
|
+
return false;
|
|
55427
|
+
}
|
|
55428
|
+
|
|
53379
55429
|
// The spacing for a path segment: the tighter of its two endpoints' gap-derived
|
|
53380
55430
|
// spacings (so a segment straddling a narrowing gap samples at the finer rate).
|
|
53381
|
-
|
|
53382
|
-
|
|
53383
|
-
|
|
53384
|
-
|
|
55431
|
+
// Infinity means "do not densify" (see spacingFromGap). When both endpoints are
|
|
55432
|
+
// open-coast but the segment is long enough that its middle could still pass
|
|
55433
|
+
// within reach of another feature, the midpoint is probed via @ctx so a
|
|
55434
|
+
// contested channel bounded by long unvertexed edges is not missed.
|
|
55435
|
+
function segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen, ctx) {
|
|
55436
|
+
var gA = gaps[path.vids[k]], gB = gaps[path.vids[k + 1]];
|
|
55437
|
+
var sA = spacingFromGap(gA, maxSpacing, spacingFloor, coarsen);
|
|
55438
|
+
var sB = spacingFromGap(gB, maxSpacing, spacingFloor, coarsen);
|
|
55439
|
+
var s = Math.min(sA, sB);
|
|
55440
|
+
if (isFinite(s) || !ctx) return s;
|
|
55441
|
+
// both endpoints open: only a long segment can hide a contested middle
|
|
55442
|
+
// (shorter than the reach, either endpoint would have seen it)
|
|
55443
|
+
var a = path.points[k], b = path.points[k + 1];
|
|
55444
|
+
var dx = b[0] - a[0], dy = b[1] - a[1];
|
|
55445
|
+
if (dx * dx + dy * dy <= 4 * maxSpacing * maxSpacing) return s;
|
|
55446
|
+
var midGap = gapAtPoint(ctx, (a[0] + b[0]) / 2, (a[1] + b[1]) / 2,
|
|
55447
|
+
path.owner, maxSpacing);
|
|
55448
|
+
return spacingFromGap(midGap, maxSpacing, spacingFloor, coarsen);
|
|
53385
55449
|
}
|
|
53386
55450
|
|
|
53387
55451
|
// Re-sample every candidate path: emit each original vertex (tagged with its
|
|
53388
55452
|
// vid) plus interior points spaced by the local gap-derived spacing (see
|
|
53389
55453
|
// segmentSpacing). Paths are open, so the last vertex has no following segment.
|
|
53390
|
-
//
|
|
53391
|
-
//
|
|
53392
|
-
//
|
|
53393
|
-
|
|
55454
|
+
// Open-coast segments (both endpoints out of reach of every other feature) are
|
|
55455
|
+
// not densified -- keptSites would discard those interior points, and densifying
|
|
55456
|
+
// them at spacing = buffer distance is what used to emit tens of millions of
|
|
55457
|
+
// sites along a nationwide coastline. A segment with one contested endpoint
|
|
55458
|
+
// still densifies at that endpoint's rate, so a channel that pinches shut is
|
|
55459
|
+
// sampled into its mouth. Long open-coast edges are midpoint-probed (see
|
|
55460
|
+
// segmentSpacing) so a contested channel whose bounding edges lack a vertex
|
|
55461
|
+
// within reach is still sampled.
|
|
55462
|
+
function densifyVertices(verts, gaps, coordDistances, spacingFloor, coarsen, ctx) {
|
|
53394
55463
|
var coords = [];
|
|
53395
55464
|
var owner = [];
|
|
53396
55465
|
var origin = []; // vid for original vertices, -1 for interpolated points
|
|
@@ -53404,10 +55473,11 @@ ${svg}
|
|
|
53404
55473
|
origin.push(path.vids[k]);
|
|
53405
55474
|
if (k + 1 >= m) continue; // open path: no segment past the last vertex
|
|
53406
55475
|
var b = path.points[k + 1];
|
|
53407
|
-
var s = segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen);
|
|
55476
|
+
var s = segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen, ctx);
|
|
53408
55477
|
var dx = b[0] - a[0], dy = b[1] - a[1];
|
|
53409
55478
|
var len = Math.sqrt(dx * dx + dy * dy);
|
|
53410
|
-
|
|
55479
|
+
// s == Infinity for open-coast segments (see spacingFromGap): skip
|
|
55480
|
+
if (s > 0 && isFinite(s) && len > s) {
|
|
53411
55481
|
var steps = Math.floor(len / s);
|
|
53412
55482
|
for (var t = 1; t <= steps; t++) {
|
|
53413
55483
|
var f = t / (steps + 1);
|
|
@@ -53421,8 +55491,15 @@ ${svg}
|
|
|
53421
55491
|
return {coords: coords, owner: owner, origin: origin};
|
|
53422
55492
|
}
|
|
53423
55493
|
|
|
55494
|
+
// Spacing used to densify a segment endpoint. Open coast (no other feature
|
|
55495
|
+
// within reach) returns Infinity so densifyVertices emits no interior points --
|
|
55496
|
+
// those sites cannot shape the medial and used to dominate the site count on
|
|
55497
|
+
// large inputs. Touching/coincident borders keep the coarse buffer-distance
|
|
55498
|
+
// spacing (the source boundary already partitions them; fine sampling would only
|
|
55499
|
+
// flood the triangulation with collinear sites). Real gaps densify at a fraction
|
|
55500
|
+
// of the local width, floored and capped.
|
|
53424
55501
|
function spacingFromGap(gap, maxSpacing, spacingFloor, coarsen) {
|
|
53425
|
-
if (!isFinite(gap)) return
|
|
55502
|
+
if (!isFinite(gap)) return Infinity;
|
|
53426
55503
|
// A gap at or below the buffer's positional tolerance means the two features
|
|
53427
55504
|
// effectively touch: there is no contested channel to run a medial down, and
|
|
53428
55505
|
// the shared source boundary already partitions the overlap. Densifying it
|
|
@@ -53440,18 +55517,18 @@ ${svg}
|
|
|
53440
55517
|
// interior points per segment). Pure counting, no Delaunay -- cheap enough to
|
|
53441
55518
|
// binary-search coarsen against. Counts pre-keep sites (the densification work),
|
|
53442
55519
|
// which is what coarsen actually bounds.
|
|
53443
|
-
function predictSiteCount(verts, gaps, coordDistances, spacingFloor, coarsen) {
|
|
55520
|
+
function predictSiteCount(verts, gaps, coordDistances, spacingFloor, coarsen, ctx) {
|
|
53444
55521
|
var total = verts.count; // every original vertex is emitted
|
|
53445
55522
|
verts.paths.forEach(function(path) {
|
|
53446
55523
|
var maxSpacing = coordDistances[path.owner];
|
|
53447
55524
|
var m = path.vids.length;
|
|
53448
55525
|
for (var k = 0; k + 1 < m; k++) {
|
|
53449
|
-
var s = segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen);
|
|
55526
|
+
var s = segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen, ctx);
|
|
53450
55527
|
var a = path.points[k];
|
|
53451
55528
|
var b = path.points[k + 1];
|
|
53452
55529
|
var dx = b[0] - a[0], dy = b[1] - a[1];
|
|
53453
55530
|
var len = Math.sqrt(dx * dx + dy * dy);
|
|
53454
|
-
if (s > 0 && len > s) total += Math.floor(len / s);
|
|
55531
|
+
if (s > 0 && isFinite(s) && len > s) total += Math.floor(len / s);
|
|
53455
55532
|
}
|
|
53456
55533
|
});
|
|
53457
55534
|
return total;
|
|
@@ -53461,17 +55538,17 @@ ${svg}
|
|
|
53461
55538
|
// count decreases monotonically as coarsen grows (spacing widens), so binary
|
|
53462
55539
|
// search converges; capped because near-coincident gaps (gap ~ 0) can't be
|
|
53463
55540
|
// thinned by coarsen and are bounded by spacingFloor instead.
|
|
53464
|
-
function fitCoarsen(verts, gaps, coordDistances, spacingFloor) {
|
|
53465
|
-
if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, 1) <= SITE_BUDGET) {
|
|
55541
|
+
function fitCoarsen(verts, gaps, coordDistances, spacingFloor, ctx) {
|
|
55542
|
+
if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, 1, ctx) <= SITE_BUDGET) {
|
|
53466
55543
|
return 1;
|
|
53467
55544
|
}
|
|
53468
55545
|
var lo = 1, hi = 1024;
|
|
53469
|
-
if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, hi) > SITE_BUDGET) {
|
|
55546
|
+
if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, hi, ctx) > SITE_BUDGET) {
|
|
53470
55547
|
return hi; // even fully coarsened we can't fit; accept the floor-bounded count
|
|
53471
55548
|
}
|
|
53472
55549
|
for (var i = 0; i < 20; i++) {
|
|
53473
55550
|
var mid = (lo + hi) / 2;
|
|
53474
|
-
if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, mid) > SITE_BUDGET) {
|
|
55551
|
+
if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, mid, ctx) > SITE_BUDGET) {
|
|
53475
55552
|
lo = mid;
|
|
53476
55553
|
} else {
|
|
53477
55554
|
hi = mid;
|
|
@@ -54692,6 +56769,15 @@ ${svg}
|
|
|
54692
56769
|
}
|
|
54693
56770
|
var output = buildPolygonBufferOutput(lyr, dataset, opts);
|
|
54694
56771
|
var dataset2 = importGeoJSON(output.geojson, {type: 'polygon'});
|
|
56772
|
+
// A buffer is in the same CRS as its input, and the GeoJSON round trip above
|
|
56773
|
+
// does not carry one. Say so, because a caller that buffers this result again
|
|
56774
|
+
// has to parse a distance against it -- fill-gaps does exactly that when it
|
|
56775
|
+
// erodes the union of the dilated features. Without this, such a distance can
|
|
56776
|
+
// only be resolved for a lat-long input, where getDatasetCrsInfo guesses
|
|
56777
|
+
// wgs84 from the coordinate bounds; on projected input there is nothing to
|
|
56778
|
+
// guess from and the buffer failed with 'Unable to convert kilometers to
|
|
56779
|
+
// unknown coordinates'.
|
|
56780
|
+
setDatasetCrsInfo(dataset2, getDatasetCrsInfo(dataset));
|
|
54695
56781
|
if (spherical) {
|
|
54696
56782
|
splitAntimeridianBufferDataset(dataset2);
|
|
54697
56783
|
if (output.dissolveAfterSplit) {
|
|
@@ -54703,7 +56789,21 @@ ${svg}
|
|
|
54703
56789
|
lyr, dataset, opts, spherical, {
|
|
54704
56790
|
skipShape: function(shape, arcs) {
|
|
54705
56791
|
return shapeHasFillInsideHole(shape, arcs);
|
|
54706
|
-
}
|
|
56792
|
+
},
|
|
56793
|
+
// Under the topological path a hole is often a tile withheld from this
|
|
56794
|
+
// feature because it is a neighbour's territory (see
|
|
56795
|
+
// getTopologicalBufferTileIds). The distance test cannot tell those from
|
|
56796
|
+
// dissolve artifacts -- both lie inside this feature's own buffer -- so
|
|
56797
|
+
// it deleted them, giving this feature ground another feature also
|
|
56798
|
+
// claims. It got worse as the radius grew, since a bigger buffer leaves
|
|
56799
|
+
// a shallower ring behind: across 3108 US counties it double-covered
|
|
56800
|
+
// 7.6km2 at R=500m and 507km2 at R=5km, mostly Virginia's independent
|
|
56801
|
+
// cities inside the counties enclosing them, and on 01_thin_gap_detail2
|
|
56802
|
+
// at 5km it consumed one of the two features outright. Holes that no
|
|
56803
|
+
// other feature covers are still the filter's to judge.
|
|
56804
|
+
coverageIndex: opts.topological ?
|
|
56805
|
+
new PathIndex(dataset2.layers[0].shapes.filter(Boolean), dataset2.arcs) :
|
|
56806
|
+
null
|
|
54707
56807
|
});
|
|
54708
56808
|
}
|
|
54709
56809
|
cullSubTolerancePolygonArtifacts(dataset2, lyr, dataset, opts);
|
|
@@ -54878,17 +56978,30 @@ ${svg}
|
|
|
54878
56978
|
// Mouth-gating mask: closing of the land by the mouth radius r (dilate by r,
|
|
54879
56979
|
// union, erode by r). Built separately from the fill dilation because the
|
|
54880
56980
|
// mouth threshold stays at the mouth size while the fill reaches further.
|
|
56981
|
+
// Two settings keep the dilation from falling short at the tips of a hairline
|
|
56982
|
+
// gap, which is where the mouth of the fill ends up: whatever the dilation fails
|
|
56983
|
+
// to bridge there is left as a residual indentation at the mouth, in proportion
|
|
56984
|
+
// to r, and no later stage can recover it.
|
|
56985
|
+
// - circumscribe_joins: the round joins are otherwise inscribed in the true
|
|
56986
|
+
// circular offset and fall a join sagitta short of it (~0.5% of r at the
|
|
56987
|
+
// default 8 segments/quadrant). See makeInscribedRoundJoin: the correction
|
|
56988
|
+
// costs no vertices, in contrast to raising quad-segs, whose cost is linear
|
|
56989
|
+
// in vertices through every stage below (dilate, union, erode, mask).
|
|
56990
|
+
// - tolerance 0: the default pre-simplification budget is 1% of the radius,
|
|
56991
|
+
// several times a hairline gap's width, so it smooths away the very tips
|
|
56992
|
+
// being bridged (and undoes the join correction with them). Skipping it
|
|
56993
|
+
// costs only a few percent here, since the joins dominate the vertex count.
|
|
54881
56994
|
var dilatedAtR = makePolygonBuffer(lyr, dataset,
|
|
54882
|
-
Object.assign({}, baseOpts, {radius: mouthRadius, topological: false
|
|
56995
|
+
Object.assign({}, baseOpts, {radius: mouthRadius, topological: false,
|
|
56996
|
+
circumscribe_joins: true, tolerance: 0}));
|
|
54883
56997
|
var union = unionBufferDataset(dilatedAtR, baseOpts);
|
|
54884
56998
|
if (!union || !union.arcs) return dilated;
|
|
54885
56999
|
// tolerance: 0 disables the buffer's Douglas-Peucker pre-simplification for the
|
|
54886
57000
|
// erosion. Pre-simplifying before an INWARD offset can push a simplified concave
|
|
54887
57001
|
// vertex past its neighbours and self-intersect the eroded ring; on a large,
|
|
54888
57002
|
// dense outline (e.g. a whole-country coastline) the dissolve then keeps the
|
|
54889
|
-
// wrong side and the ring collapses, wiping the gap-fill extent. The dilation
|
|
54890
|
-
//
|
|
54891
|
-
// erode pays the full-resolution cost.
|
|
57003
|
+
// wrong side and the ring collapses, wiping the gap-fill extent. (The dilation
|
|
57004
|
+
// skips pre-simplification for its own reason -- see above.)
|
|
54892
57005
|
var closing = makePolygonBuffer(union.layers[0], union,
|
|
54893
57006
|
Object.assign({}, baseOpts, {radius: '-' + mouthRadius, topological: false,
|
|
54894
57007
|
tolerance: 0}));
|
|
@@ -54976,8 +57089,12 @@ ${svg}
|
|
|
54976
57089
|
// clears the arc/mouth tortuosity clause despite little depth. Requiring the patch
|
|
54977
57090
|
// depth to reach this fraction of the mouth radius drops both while keeping real
|
|
54978
57091
|
// inlets (which run far deeper than their mouth radius; on the Columbia the kept
|
|
54979
|
-
// gaps reach depth/r >= 1.3, the shallow scallops only ~0.2).
|
|
54980
|
-
|
|
57092
|
+
// gaps reach depth/r >= 1.3, the shallow scallops only ~0.2). The margin matters,
|
|
57093
|
+
// because a coast-hugging strip only a few dozen metres thick can still measure a
|
|
57094
|
+
// large depth: depth is taken from the mouth CHORD, and that chord cuts straight
|
|
57095
|
+
// across whatever bay the strip follows. Those strips land around depth/r ~0.3, so
|
|
57096
|
+
// the floor sits above them rather than on them.
|
|
57097
|
+
var INLET_MIN_ABS_DEPTH = 0.4; // min patch depth as a fraction of the mouth radius
|
|
54981
57098
|
|
|
54982
57099
|
// Island-bridge classification of a fill that joins >= 2 source parts, one small
|
|
54983
57100
|
// (see bridgesSmallIsland). Two independent signatures mark an island bridge:
|
|
@@ -55135,9 +57252,14 @@ ${svg}
|
|
|
55135
57252
|
var islandDataset = importGeoJSON(
|
|
55136
57253
|
{type: 'GeometryCollection', geometries: islandGeoms}, {type: 'polygon'});
|
|
55137
57254
|
setDatasetCrsInfo(islandDataset, crsInfo);
|
|
57255
|
+
// circumscribe_joins matches the closing's dilation (see makeGapFillPolygonBuffer):
|
|
57256
|
+
// this buffer has to cancel the island's mouth-radius reach, so if it grew a join
|
|
57257
|
+
// sagitta less than that dilation did, a hairline of bridge would survive the
|
|
57258
|
+
// erase and reconnect the island.
|
|
55138
57259
|
var islandBuf = makePolygonBuffer(islandDataset.layers[0], islandDataset,
|
|
55139
57260
|
Object.assign({}, opts, {fill_gaps: false, max_widening: null,
|
|
55140
|
-
radius: mouthRadiusStr, topological: false, no_replace: true
|
|
57261
|
+
radius: mouthRadiusStr, topological: false, no_replace: true,
|
|
57262
|
+
circumscribe_joins: true}));
|
|
55141
57263
|
if (!islandBuf || !islandBuf.arcs) return;
|
|
55142
57264
|
var bridgeDataset = importGeoJSON(
|
|
55143
57265
|
{type: 'GeometryCollection', geometries: bridgeGeoms}, {type: 'polygon'});
|
|
@@ -55413,6 +57535,10 @@ ${svg}
|
|
|
55413
57535
|
if (coords.length === 0) return null;
|
|
55414
57536
|
var unionDataset = getBufferDataset(coords);
|
|
55415
57537
|
if (!unionDataset.arcs) return null;
|
|
57538
|
+
// getBufferDataset builds from bare coordinates, so carry the CRS across:
|
|
57539
|
+
// fill-gaps buffers this union again to erode it, and that needs a CRS to
|
|
57540
|
+
// resolve a distance given in real-world units.
|
|
57541
|
+
setDatasetCrsInfo(unionDataset, getDatasetCrsInfo(dataset));
|
|
55416
57542
|
dissolveBufferDataset2(unionDataset, Object.assign({}, opts, {winding_fill: false}));
|
|
55417
57543
|
return unionDataset;
|
|
55418
57544
|
}
|
|
@@ -56193,22 +58319,22 @@ ${svg}
|
|
|
56193
58319
|
var sourceIdIndex = getIdLookup(sourceIds);
|
|
56194
58320
|
var bufferIdIndex = getIdToFeatureIdLookup(bufferIds);
|
|
56195
58321
|
var sourceAreas = getSourceShapeAreas(shapes, sourceArcs);
|
|
56196
|
-
var sourceInteriorPoints = getSourceInteriorPoints(shapes, sourceArcs);
|
|
56197
58322
|
var ownerCtx = createTileOwnerContext(shapes, sourceArcs, sourceAreas,
|
|
56198
58323
|
mosaicIndex.nodes.arcs);
|
|
56199
58324
|
profileStart('topo:assignTiles');
|
|
56200
58325
|
var result = shapes.map(function(shape, i) {
|
|
56201
58326
|
var distance = distances[i];
|
|
56202
|
-
var tileIds
|
|
58327
|
+
var tileIds;
|
|
56203
58328
|
if (!distance || !shape) return null;
|
|
56204
58329
|
tileIds = getTopologicalBufferTileIds(sourceIds[i], bufferIds[i],
|
|
56205
58330
|
i, mosaicIndex, sourceIdIndex, bufferIdIndex, ownerCtx);
|
|
56206
|
-
|
|
56207
|
-
//
|
|
56208
|
-
//
|
|
56209
|
-
// the
|
|
56210
|
-
|
|
56211
|
-
|
|
58331
|
+
// No artifact-hole filtering here. Every hole in this geometry is a tile
|
|
58332
|
+
// withheld from this feature on purpose (see getTopologicalBufferTileIds),
|
|
58333
|
+
// so there is nothing for a single-shape heuristic to clean up and it can
|
|
58334
|
+
// only do harm: the interior-point territory test it used to run missed the
|
|
58335
|
+
// holes it could not sample, and filling those handed a neighbour's ground
|
|
58336
|
+
// to this feature -- 18.6 km2 of it across 3108 US counties at R=5km.
|
|
58337
|
+
return getTileIdsGeometry(tileIds, mosaicIndex, pathfind);
|
|
56212
58338
|
});
|
|
56213
58339
|
profileEnd('topo:assignTiles');
|
|
56214
58340
|
return result;
|
|
@@ -56427,22 +58553,6 @@ ${svg}
|
|
|
56427
58553
|
});
|
|
56428
58554
|
}
|
|
56429
58555
|
|
|
56430
|
-
// One interior point per positive ring-group (part) of every source shape,
|
|
56431
|
-
// tagged with its feature id. A multipart source contributes a point for each
|
|
56432
|
-
// detached part (e.g. a small island ring), so the topological hole filter can
|
|
56433
|
-
// recognize a neighbor's territory even when it is one part of a multipolygon.
|
|
56434
|
-
function getSourceInteriorPoints(shapes, arcs) {
|
|
56435
|
-
var points = [];
|
|
56436
|
-
shapes.forEach(function(shape, featureId) {
|
|
56437
|
-
if (!shape) return;
|
|
56438
|
-
getPolygonRingGroupShapes(shape, arcs).forEach(function(group) {
|
|
56439
|
-
var p = findAnchorPoint(group, arcs);
|
|
56440
|
-
if (p) points.push({x: p.x, y: p.y, featureId: featureId});
|
|
56441
|
-
});
|
|
56442
|
-
});
|
|
56443
|
-
return points;
|
|
56444
|
-
}
|
|
56445
|
-
|
|
56446
58556
|
function getIdLookup(ids) {
|
|
56447
58557
|
var index = [];
|
|
56448
58558
|
ids.forEach(function(id) {
|
|
@@ -56654,11 +58764,7 @@ ${svg}
|
|
|
56654
58764
|
return false;
|
|
56655
58765
|
}
|
|
56656
58766
|
|
|
56657
|
-
|
|
56658
|
-
// points are source-part interior points tagged by feature id; a candidate hole
|
|
56659
|
-
// enclosing another feature's point is that neighbor's territory (excluded to
|
|
56660
|
-
// prevent overlap) and is always kept.
|
|
56661
|
-
function removePositiveBufferArtifactHoles(geom, shape, arcs, distance, territory) {
|
|
58767
|
+
function removePositiveBufferArtifactHoles(geom, shape, arcs, distance) {
|
|
56662
58768
|
if (!geom) return null;
|
|
56663
58769
|
// Nothing to filter unless the result actually has interior rings. Skip the
|
|
56664
58770
|
// index build (filterArtifactHoles is itself a no-op on hole-free polygons,
|
|
@@ -56682,9 +58788,7 @@ ${svg}
|
|
|
56682
58788
|
shapeIndex: buildShapeSegmentIndex(shape, arcs),
|
|
56683
58789
|
pathIndex: shape && shape.length > 0 ? new PathIndex([shape], arcs) : null,
|
|
56684
58790
|
holeIndex: sourceHoles.length > 0 ?
|
|
56685
|
-
buildShapeSegmentIndex(sourceHoles.map(function(h) {return h[0];}), arcs) : null
|
|
56686
|
-
territoryPoints: territory ? territory.points : null,
|
|
56687
|
-
featureId: territory ? territory.featureId : -1
|
|
58791
|
+
buildShapeSegmentIndex(sourceHoles.map(function(h) {return h[0];}), arcs) : null
|
|
56688
58792
|
};
|
|
56689
58793
|
if (geom.type == 'Polygon') {
|
|
56690
58794
|
geom.coordinates = filterArtifactHoles(geom.coordinates, minHoleArea, ctx);
|
|
@@ -56794,63 +58898,11 @@ ${svg}
|
|
|
56794
58898
|
function filterArtifactHoles(polygon, minHoleArea, ctx) {
|
|
56795
58899
|
if (polygon.length < 2) return polygon;
|
|
56796
58900
|
return [polygon[0]].concat(polygon.slice(1).filter(function(ring) {
|
|
56797
|
-
// A hole over another feature's source is real territory (kept regardless of
|
|
56798
|
-
// size or how deep it sits in this feature's buffer); see getSourceInteriorPoints.
|
|
56799
|
-
if (ringEnclosesOtherTerritory(ring, ctx)) return true;
|
|
56800
58901
|
return Math.abs(getGeoJSONRingArea(ring)) > minHoleArea &&
|
|
56801
58902
|
!positiveBufferHoleIsArtifact(ring, ctx);
|
|
56802
58903
|
}));
|
|
56803
58904
|
}
|
|
56804
58905
|
|
|
56805
|
-
function ringEnclosesOtherTerritory(ring, ctx) {
|
|
56806
|
-
var points = ctx.territoryPoints;
|
|
56807
|
-
if (!points) return false;
|
|
56808
|
-
var bounds = getGeoJSONRingBounds(ring);
|
|
56809
|
-
for (var i = 0; i < points.length; i++) {
|
|
56810
|
-
if (points[i].featureId === ctx.featureId) continue;
|
|
56811
|
-
if (!pointInGeoJSONRingBounds(points[i].x, points[i].y, bounds)) continue;
|
|
56812
|
-
if (pointInGeoJSONRing(points[i].x, points[i].y, ring)) return true;
|
|
56813
|
-
}
|
|
56814
|
-
return false;
|
|
56815
|
-
}
|
|
56816
|
-
|
|
56817
|
-
function getGeoJSONRingBounds(ring) {
|
|
56818
|
-
var n = ring.length - 1; // skip duplicate closing vertex
|
|
56819
|
-
if (n <= 0) n = ring.length;
|
|
56820
|
-
var xmin = Infinity, ymin = Infinity, xmax = -Infinity, ymax = -Infinity;
|
|
56821
|
-
var i, x, y;
|
|
56822
|
-
for (i = 0; i < n; i++) {
|
|
56823
|
-
x = ring[i][0];
|
|
56824
|
-
y = ring[i][1];
|
|
56825
|
-
if (x < xmin) xmin = x;
|
|
56826
|
-
if (x > xmax) xmax = x;
|
|
56827
|
-
if (y < ymin) ymin = y;
|
|
56828
|
-
if (y > ymax) ymax = y;
|
|
56829
|
-
}
|
|
56830
|
-
return {xmin: xmin, ymin: ymin, xmax: xmax, ymax: ymax};
|
|
56831
|
-
}
|
|
56832
|
-
|
|
56833
|
-
function pointInGeoJSONRingBounds(x, y, bounds) {
|
|
56834
|
-
return x >= bounds.xmin && x <= bounds.xmax &&
|
|
56835
|
-
y >= bounds.ymin && y <= bounds.ymax;
|
|
56836
|
-
}
|
|
56837
|
-
|
|
56838
|
-
// Ray-casting point-in-ring test for a closed GeoJSON ring (array of [x, y],
|
|
56839
|
-
// first == last). Boundary cases are irrelevant here: territory probe points are
|
|
56840
|
-
// well inside their source part, far from any hole-ring edge.
|
|
56841
|
-
function pointInGeoJSONRing(x, y, ring) {
|
|
56842
|
-
var inside = false;
|
|
56843
|
-
for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
56844
|
-
var xi = ring[i][0], yi = ring[i][1];
|
|
56845
|
-
var xj = ring[j][0], yj = ring[j][1];
|
|
56846
|
-
if ((yi > y) !== (yj > y) &&
|
|
56847
|
-
x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
|
|
56848
|
-
inside = !inside;
|
|
56849
|
-
}
|
|
56850
|
-
}
|
|
56851
|
-
return inside;
|
|
56852
|
-
}
|
|
56853
|
-
|
|
56854
58906
|
function positiveBufferHoleIsArtifact(ring, ctx) {
|
|
56855
58907
|
var n = ring.length - 1; // skip duplicate endpoint
|
|
56856
58908
|
var step = Math.max(1, Math.floor(n / 20));
|
|
@@ -57753,7 +59805,7 @@ ${svg}
|
|
|
57753
59805
|
debug('Short ring', coords);
|
|
57754
59806
|
return;
|
|
57755
59807
|
}
|
|
57756
|
-
if (!samePoint(coords[0], lastEl(coords))) {
|
|
59808
|
+
if (!samePoint$1(coords[0], lastEl(coords))) {
|
|
57757
59809
|
error('Open polygon ring');
|
|
57758
59810
|
}
|
|
57759
59811
|
rings.push(coords); // accumulate rings
|
|
@@ -66130,7 +68182,7 @@ ${svg}
|
|
|
66130
68182
|
|
|
66131
68183
|
// Options that require the topology-repair algorithm and are not supported
|
|
66132
68184
|
// by the no-repair fast path.
|
|
66133
|
-
var REPAIR_REQUIRED_OPTS = ['gap_fill_area', 'sliver_control', 'allow_overlaps'];
|
|
68185
|
+
var REPAIR_REQUIRED_OPTS = ['gap_width', 'gap_fill_area', 'sliver_control', 'allow_overlaps'];
|
|
66134
68186
|
|
|
66135
68187
|
// Sample size used to detect intersections in no-repair mode. Detection stops
|
|
66136
68188
|
// after this many hits, so the warning message can include sample locations
|
|
@@ -75897,7 +77949,7 @@ ${svg}
|
|
|
75897
77949
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
75898
77950
|
}
|
|
75899
77951
|
|
|
75900
|
-
var version = "0.7.
|
|
77952
|
+
var version = "0.7.53";
|
|
75901
77953
|
|
|
75902
77954
|
// Parse command line args into commands and run them
|
|
75903
77955
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|
|
@@ -76559,6 +78611,256 @@ ${svg}
|
|
|
76559
78611
|
return false;
|
|
76560
78612
|
}
|
|
76561
78613
|
|
|
78614
|
+
// Utilities for dividing a path at one or two points ("snipping").
|
|
78615
|
+
//
|
|
78616
|
+
// The new arcs are appended to the end of the collection; the original arc is
|
|
78617
|
+
// left in place (it may still be referenced by other shapes). Callers are
|
|
78618
|
+
// responsible for updating the shapes that should use the new arcs.
|
|
78619
|
+
|
|
78620
|
+
// Divide an arc's vertices into slices at one or two cut locations.
|
|
78621
|
+
// n: number of vertices in the arc
|
|
78622
|
+
// cuts: one or two cut objects, ordered from the start of the arc:
|
|
78623
|
+
// {offset: i, point: [x, y]} cuts the segment between vertices i and i + 1
|
|
78624
|
+
// {offset: i, point: null} cuts at vertex i (which is duplicated, so that
|
|
78625
|
+
// both adjacent slices contain it)
|
|
78626
|
+
// Returns slice descriptors {start, end, prefix, suffix}, where start and end
|
|
78627
|
+
// are an inclusive vertex range (empty if start > end) and prefix/suffix are
|
|
78628
|
+
// points inserted at a cut location.
|
|
78629
|
+
function getArcSlices(n, cuts) {
|
|
78630
|
+
var slices = [];
|
|
78631
|
+
var start = 0;
|
|
78632
|
+
var prefix = null;
|
|
78633
|
+
cuts.forEach(function(cut) {
|
|
78634
|
+
var i = cut.offset;
|
|
78635
|
+
if (cut.point) {
|
|
78636
|
+
if (i < 0 || i > n - 2) error('Out-of-range arc cut offset:', i);
|
|
78637
|
+
slices.push({start: start, end: i, prefix: prefix, suffix: cut.point});
|
|
78638
|
+
prefix = cut.point;
|
|
78639
|
+
start = i + 1;
|
|
78640
|
+
} else {
|
|
78641
|
+
if (i < 1 || i > n - 2) error('Out-of-range arc cut offset:', i);
|
|
78642
|
+
slices.push({start: start, end: i, prefix: prefix, suffix: null});
|
|
78643
|
+
prefix = null;
|
|
78644
|
+
start = i;
|
|
78645
|
+
}
|
|
78646
|
+
});
|
|
78647
|
+
slices.push({start: start, end: n - 1, prefix: prefix, suffix: null});
|
|
78648
|
+
return slices;
|
|
78649
|
+
}
|
|
78650
|
+
|
|
78651
|
+
function getArcSliceLength(slice) {
|
|
78652
|
+
var n = slice.end >= slice.start ? slice.end - slice.start + 1 : 0;
|
|
78653
|
+
if (slice.prefix) n++;
|
|
78654
|
+
if (slice.suffix) n++;
|
|
78655
|
+
return n;
|
|
78656
|
+
}
|
|
78657
|
+
|
|
78658
|
+
// Append the slices of a divided arc to an ArcCollection.
|
|
78659
|
+
// Returns the ids of the new arcs, ordered from the start of the original arc.
|
|
78660
|
+
function splitArcAtCuts(arcs, arcId, cuts) {
|
|
78661
|
+
var data = arcs.getVertexData();
|
|
78662
|
+
var zlimit = arcs.getRetainedInterval();
|
|
78663
|
+
var arcStart = data.ii[arcId];
|
|
78664
|
+
var arcCount = data.nn.length;
|
|
78665
|
+
var slices = getArcSlices(data.nn[arcId], cuts);
|
|
78666
|
+
var pointCount = data.xx.length;
|
|
78667
|
+
var added = slices.reduce(function(memo, slice) {
|
|
78668
|
+
return memo + getArcSliceLength(slice);
|
|
78669
|
+
}, 0);
|
|
78670
|
+
var xx2 = new Float64Array(pointCount + added);
|
|
78671
|
+
var yy2 = new Float64Array(pointCount + added);
|
|
78672
|
+
var zz2 = arcs.isFlat() ? null : new Float64Array(pointCount + added);
|
|
78673
|
+
var nn2 = new Uint32Array(arcCount + slices.length);
|
|
78674
|
+
var i = pointCount;
|
|
78675
|
+
var ids = [];
|
|
78676
|
+
|
|
78677
|
+
xx2.set(data.xx);
|
|
78678
|
+
yy2.set(data.yy);
|
|
78679
|
+
nn2.set(data.nn);
|
|
78680
|
+
if (zz2) zz2.set(data.zz);
|
|
78681
|
+
|
|
78682
|
+
slices.forEach(function(slice, sliceId) {
|
|
78683
|
+
var first = i;
|
|
78684
|
+
var j;
|
|
78685
|
+
if (slice.prefix) {
|
|
78686
|
+
xx2[i] = slice.prefix[0];
|
|
78687
|
+
yy2[i] = slice.prefix[1];
|
|
78688
|
+
i++;
|
|
78689
|
+
}
|
|
78690
|
+
for (j = slice.start; j <= slice.end; j++) {
|
|
78691
|
+
xx2[i] = data.xx[arcStart + j];
|
|
78692
|
+
yy2[i] = data.yy[arcStart + j];
|
|
78693
|
+
if (zz2) zz2[i] = data.zz[arcStart + j];
|
|
78694
|
+
i++;
|
|
78695
|
+
}
|
|
78696
|
+
if (slice.suffix) {
|
|
78697
|
+
xx2[i] = slice.suffix[0];
|
|
78698
|
+
yy2[i] = slice.suffix[1];
|
|
78699
|
+
i++;
|
|
78700
|
+
}
|
|
78701
|
+
if (zz2) {
|
|
78702
|
+
// arc endpoints must never be removed by simplification
|
|
78703
|
+
zz2[first] = Infinity;
|
|
78704
|
+
zz2[i - 1] = Infinity;
|
|
78705
|
+
}
|
|
78706
|
+
nn2[arcCount + sliceId] = i - first;
|
|
78707
|
+
ids.push(arcCount + sliceId);
|
|
78708
|
+
});
|
|
78709
|
+
|
|
78710
|
+
arcs.updateVertexData(nn2, xx2, yy2, zz2);
|
|
78711
|
+
arcs.setRetainedInterval(zlimit);
|
|
78712
|
+
return ids;
|
|
78713
|
+
}
|
|
78714
|
+
|
|
78715
|
+
// A cut identifies one location along a path (an array of arc ids):
|
|
78716
|
+
// seq: index into the path of the arc containing the cut
|
|
78717
|
+
// offset: vertex index within that arc, in traversal orientation
|
|
78718
|
+
// point: coords of the cut, or null to cut at vertex <offset>
|
|
78719
|
+
// displayPoint: coords of the cut in the GUI's display CRS, or null
|
|
78720
|
+
// t: position along the segment (0-1), used to order two cuts
|
|
78721
|
+
// that land on the same segment
|
|
78722
|
+
|
|
78723
|
+
// Work out how to divide a path at one cut (open path) or two cuts (ring),
|
|
78724
|
+
// without mutating anything.
|
|
78725
|
+
//
|
|
78726
|
+
// path: array of arc ids
|
|
78727
|
+
// cutsArg: one cut (open path) or two cuts (ring), in any order
|
|
78728
|
+
// arcLen: function(arcId) returning the number of vertices in an arc
|
|
78729
|
+
// baseId: arcs.size(), used to predict the ids of arcs that will be appended
|
|
78730
|
+
// closed: whether the path is a ring
|
|
78731
|
+
//
|
|
78732
|
+
// Returns {splits, a, b}, where splits are arc divisions to perform in order
|
|
78733
|
+
// and a and b are the two resulting paths, or null if the cuts are degenerate
|
|
78734
|
+
// (at the endpoint of an open path, or two cuts in the same place on a ring).
|
|
78735
|
+
//
|
|
78736
|
+
function planPathDivision(path, cutsArg, arcLen, baseId, closed) {
|
|
78737
|
+
var cuts = sortCuts(cutsArg);
|
|
78738
|
+
var boundaryCuts = []; // cuts that fall between two arcs
|
|
78739
|
+
var interiorCuts = {}; // cuts that fall inside an arc, by path index
|
|
78740
|
+
var expanded = []; // path with divided arcs replaced by their pieces
|
|
78741
|
+
var positions = []; // position of each cut within the expanded path
|
|
78742
|
+
var splits = [];
|
|
78743
|
+
var nextArcId = baseId;
|
|
78744
|
+
var a, b, q, q1, q2;
|
|
78745
|
+
|
|
78746
|
+
if (cuts.length != (closed ? 2 : 1)) return null;
|
|
78747
|
+
|
|
78748
|
+
cuts.forEach(function(cut, cutId) {
|
|
78749
|
+
var n = arcLen(absArcId(path[cut.seq]));
|
|
78750
|
+
var seq;
|
|
78751
|
+
if (!cut.point && (cut.offset === 0 || cut.offset === n - 1)) {
|
|
78752
|
+
// a cut at an arc endpoint doesn't divide the arc, it divides the
|
|
78753
|
+
// sequence of arcs
|
|
78754
|
+
seq = cut.offset === 0 ? cut.seq : cut.seq + 1;
|
|
78755
|
+
if (closed && seq === path.length) seq = 0; // a ring's seam is one place
|
|
78756
|
+
boundaryCuts.push({cutId: cutId, seq: seq});
|
|
78757
|
+
} else {
|
|
78758
|
+
if (!interiorCuts[cut.seq]) interiorCuts[cut.seq] = [];
|
|
78759
|
+
interiorCuts[cut.seq].push({cutId: cutId, cut: cut, arcLen: n});
|
|
78760
|
+
}
|
|
78761
|
+
});
|
|
78762
|
+
|
|
78763
|
+
for (var k=0; k<=path.length; k++) {
|
|
78764
|
+
boundaryCuts.forEach(function(item) {
|
|
78765
|
+
if (item.seq === k) positions[item.cutId] = expanded.length;
|
|
78766
|
+
});
|
|
78767
|
+
if (k === path.length) break;
|
|
78768
|
+
if (!interiorCuts[k]) {
|
|
78769
|
+
expanded.push(path[k]);
|
|
78770
|
+
continue;
|
|
78771
|
+
}
|
|
78772
|
+
addDividedArc(interiorCuts[k], path[k]);
|
|
78773
|
+
}
|
|
78774
|
+
|
|
78775
|
+
if (!closed) {
|
|
78776
|
+
q = positions[0];
|
|
78777
|
+
if (q === 0 || q === expanded.length) return null; // cut at a path endpoint
|
|
78778
|
+
a = expanded.slice(0, q);
|
|
78779
|
+
b = expanded.slice(q);
|
|
78780
|
+
} else {
|
|
78781
|
+
q1 = Math.min(positions[0], positions[1]);
|
|
78782
|
+
q2 = Math.max(positions[0], positions[1]);
|
|
78783
|
+
if (q1 === q2) return null; // both cuts in the same place
|
|
78784
|
+
a = expanded.slice(q1, q2);
|
|
78785
|
+
b = expanded.slice(q2).concat(expanded.slice(0, q1));
|
|
78786
|
+
}
|
|
78787
|
+
return {splits: splits, a: a, b: b};
|
|
78788
|
+
|
|
78789
|
+
function addDividedArc(group, arcId) {
|
|
78790
|
+
var fwd = arcId >= 0;
|
|
78791
|
+
var n = group[0].arcLen;
|
|
78792
|
+
// convert traversal offsets to forward-orientation offsets
|
|
78793
|
+
var fwdCuts = group.map(function(item) {
|
|
78794
|
+
return {
|
|
78795
|
+
offset: fwd ? item.cut.offset :
|
|
78796
|
+
n - (item.cut.point ? 2 : 1) - item.cut.offset,
|
|
78797
|
+
point: item.cut.point || null,
|
|
78798
|
+
displayPoint: item.cut.displayPoint || null
|
|
78799
|
+
};
|
|
78800
|
+
});
|
|
78801
|
+
// a reversed arc is traversed from its last vertex, so traversal order is
|
|
78802
|
+
// the reverse of forward order
|
|
78803
|
+
var pieces = getSplitArcIds(nextArcId, group.length, fwd);
|
|
78804
|
+
splits.push({
|
|
78805
|
+
arcId: absArcId(arcId),
|
|
78806
|
+
cuts: fwd ? fwdCuts : fwdCuts.concat().reverse()
|
|
78807
|
+
});
|
|
78808
|
+
nextArcId += group.length + 1;
|
|
78809
|
+
pieces.forEach(function(id, i) {
|
|
78810
|
+
if (i > 0) positions[group[i - 1].cutId] = expanded.length;
|
|
78811
|
+
expanded.push(id);
|
|
78812
|
+
});
|
|
78813
|
+
}
|
|
78814
|
+
}
|
|
78815
|
+
|
|
78816
|
+
// Ids of the pieces of a divided arc, in traversal order
|
|
78817
|
+
function getSplitArcIds(baseId, cutCount, fwd) {
|
|
78818
|
+
var ids = [];
|
|
78819
|
+
for (var i=0; i<=cutCount; i++) {
|
|
78820
|
+
ids.push(fwd ? baseId + i : ~(baseId + cutCount - i));
|
|
78821
|
+
}
|
|
78822
|
+
return ids;
|
|
78823
|
+
}
|
|
78824
|
+
|
|
78825
|
+
function sortCuts(cuts) {
|
|
78826
|
+
return cuts.concat().sort(function(a, b) {
|
|
78827
|
+
return a.seq - b.seq || getCutOffset(a) - getCutOffset(b);
|
|
78828
|
+
});
|
|
78829
|
+
}
|
|
78830
|
+
|
|
78831
|
+
// A sortable offset within one arc. A cut on the segment following a vertex
|
|
78832
|
+
// comes after the vertex itself; two cuts on the same segment are ordered by
|
|
78833
|
+
// their position along it.
|
|
78834
|
+
function getCutOffset(cut) {
|
|
78835
|
+
if (!cut.point) return cut.offset;
|
|
78836
|
+
return cut.offset + 0.5 + (cut.t > 0 ? Math.min(cut.t, 1) : 0) * 0.4;
|
|
78837
|
+
}
|
|
78838
|
+
|
|
78839
|
+
function getPlanarPathLength(ids, arcs) {
|
|
78840
|
+
var iter = arcs.getShapeIter(ids);
|
|
78841
|
+
var len = 0;
|
|
78842
|
+
var x, y;
|
|
78843
|
+
if (iter.hasNext()) {
|
|
78844
|
+
x = iter.x;
|
|
78845
|
+
y = iter.y;
|
|
78846
|
+
}
|
|
78847
|
+
while (iter.hasNext()) {
|
|
78848
|
+
len += distance2D$1(x, y, iter.x, iter.y);
|
|
78849
|
+
x = iter.x;
|
|
78850
|
+
y = iter.y;
|
|
78851
|
+
}
|
|
78852
|
+
return len;
|
|
78853
|
+
}
|
|
78854
|
+
|
|
78855
|
+
var ArcSplit = /*#__PURE__*/Object.freeze({
|
|
78856
|
+
__proto__: null,
|
|
78857
|
+
getArcSliceLength: getArcSliceLength,
|
|
78858
|
+
getArcSlices: getArcSlices,
|
|
78859
|
+
getPlanarPathLength: getPlanarPathLength,
|
|
78860
|
+
planPathDivision: planPathDivision,
|
|
78861
|
+
splitArcAtCuts: splitArcAtCuts
|
|
78862
|
+
});
|
|
78863
|
+
|
|
76562
78864
|
var BOX_BLUR_PASSES = 3;
|
|
76563
78865
|
|
|
76564
78866
|
function blurRasterGrid(raster, optsArg, crs) {
|
|
@@ -77735,6 +80037,7 @@ ${svg}
|
|
|
77735
80037
|
AnchorPoints,
|
|
77736
80038
|
ArcClassifier,
|
|
77737
80039
|
ArcDissolve,
|
|
80040
|
+
ArcSplit,
|
|
77738
80041
|
ArcUtils,
|
|
77739
80042
|
Bbox2Clipping,
|
|
77740
80043
|
BinArray$1,
|