mapshaper 0.7.52 → 0.7.54
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 +3388 -319
- package/package.json +1 -2
- package/www/mapshaper-gui.js +34 -11
- package/www/mapshaper.js +3388 -319
package/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,
|
|
@@ -8432,7 +8447,7 @@
|
|
|
8432
8447
|
var D2R$2 = Math.PI / 180;
|
|
8433
8448
|
var R2D$3 = 180 / Math.PI;
|
|
8434
8449
|
var HALF_PI = Math.PI / 2;
|
|
8435
|
-
var SQRT2$
|
|
8450
|
+
var SQRT2$2 = Math.sqrt(2);
|
|
8436
8451
|
var SQRT3$1 = Math.sqrt(3);
|
|
8437
8452
|
var ASIN_ONE_THIRD$1 = Math.asin(1 / 3);
|
|
8438
8453
|
var EDGE_SCALE = Math.acos(-1 / 3) / 2;
|
|
@@ -9003,7 +9018,7 @@
|
|
|
9003
9018
|
function narukawaFaceForward(lam, phi) {
|
|
9004
9019
|
var a = lam - Math.asin(Math.sin(lam) / SQRT3$1);
|
|
9005
9020
|
var theta = Math.atan(2 * SQRT3$1 / Math.PI * a);
|
|
9006
|
-
var denominator = 2 + SQRT2$
|
|
9021
|
+
var denominator = 2 + SQRT2$2 * Math.tan(phi);
|
|
9007
9022
|
var q = denominator > 0 ? (2 + Math.cos(lam)) / denominator : 0;
|
|
9008
9023
|
var r = q * SQRT3$1 / Math.cos(theta);
|
|
9009
9024
|
return [r, theta];
|
|
@@ -9023,7 +9038,7 @@
|
|
|
9023
9038
|
lam = (lo + hi) / 2;
|
|
9024
9039
|
var q = r * Math.cos(theta) / SQRT3$1;
|
|
9025
9040
|
var phi = q < EPS$1 ? HALF_PI :
|
|
9026
|
-
Math.atan(((2 + Math.cos(lam)) / q - 2) / SQRT2$
|
|
9041
|
+
Math.atan(((2 + Math.cos(lam)) / q - 2) / SQRT2$2);
|
|
9027
9042
|
return [phi, lam];
|
|
9028
9043
|
}
|
|
9029
9044
|
|
|
@@ -9463,7 +9478,7 @@
|
|
|
9463
9478
|
|
|
9464
9479
|
var D2R$1 = Math.PI / 180;
|
|
9465
9480
|
var R2D$2 = 180 / Math.PI;
|
|
9466
|
-
var SQRT2 = Math.sqrt(2);
|
|
9481
|
+
var SQRT2$1 = Math.sqrt(2);
|
|
9467
9482
|
var SQRT3 = Math.sqrt(3);
|
|
9468
9483
|
var ASIN_ONE_THIRD = Math.asin(1 / 3);
|
|
9469
9484
|
var MARKLEY_LATITUDE = Math.acos(1 / 3) * 0.5 * R2D$2;
|
|
@@ -10047,7 +10062,7 @@
|
|
|
10047
10062
|
|
|
10048
10063
|
function leeRaw(lam, phi) {
|
|
10049
10064
|
var s = stereographicRaw(lam, phi);
|
|
10050
|
-
var z = [s[0] * SQRT2, s[1] * SQRT2];
|
|
10065
|
+
var z = [s[0] * SQRT2$1, s[1] * SQRT2$1];
|
|
10051
10066
|
var i, j;
|
|
10052
10067
|
// Pick the sector whose rotation maximizes the real part of rot * z.
|
|
10053
10068
|
var sector = 0;
|
|
@@ -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
|
|
@@ -18705,6 +18826,602 @@
|
|
|
18705
18826
|
});
|
|
18706
18827
|
}
|
|
18707
18828
|
|
|
18829
|
+
class TinyQueue {
|
|
18830
|
+
constructor(data = [], compare = defaultCompare) {
|
|
18831
|
+
this.data = data;
|
|
18832
|
+
this.length = this.data.length;
|
|
18833
|
+
this.compare = compare;
|
|
18834
|
+
|
|
18835
|
+
if (this.length > 0) {
|
|
18836
|
+
for (let i = (this.length >> 1) - 1; i >= 0; i--) this._down(i);
|
|
18837
|
+
}
|
|
18838
|
+
}
|
|
18839
|
+
|
|
18840
|
+
push(item) {
|
|
18841
|
+
this.data.push(item);
|
|
18842
|
+
this.length++;
|
|
18843
|
+
this._up(this.length - 1);
|
|
18844
|
+
}
|
|
18845
|
+
|
|
18846
|
+
pop() {
|
|
18847
|
+
if (this.length === 0) return undefined;
|
|
18848
|
+
|
|
18849
|
+
const top = this.data[0];
|
|
18850
|
+
const bottom = this.data.pop();
|
|
18851
|
+
this.length--;
|
|
18852
|
+
|
|
18853
|
+
if (this.length > 0) {
|
|
18854
|
+
this.data[0] = bottom;
|
|
18855
|
+
this._down(0);
|
|
18856
|
+
}
|
|
18857
|
+
|
|
18858
|
+
return top;
|
|
18859
|
+
}
|
|
18860
|
+
|
|
18861
|
+
peek() {
|
|
18862
|
+
return this.data[0];
|
|
18863
|
+
}
|
|
18864
|
+
|
|
18865
|
+
_up(pos) {
|
|
18866
|
+
const {data, compare} = this;
|
|
18867
|
+
const item = data[pos];
|
|
18868
|
+
|
|
18869
|
+
while (pos > 0) {
|
|
18870
|
+
const parent = (pos - 1) >> 1;
|
|
18871
|
+
const current = data[parent];
|
|
18872
|
+
if (compare(item, current) >= 0) break;
|
|
18873
|
+
data[pos] = current;
|
|
18874
|
+
pos = parent;
|
|
18875
|
+
}
|
|
18876
|
+
|
|
18877
|
+
data[pos] = item;
|
|
18878
|
+
}
|
|
18879
|
+
|
|
18880
|
+
_down(pos) {
|
|
18881
|
+
const {data, compare} = this;
|
|
18882
|
+
const halfLength = this.length >> 1;
|
|
18883
|
+
const item = data[pos];
|
|
18884
|
+
|
|
18885
|
+
while (pos < halfLength) {
|
|
18886
|
+
let left = (pos << 1) + 1;
|
|
18887
|
+
let best = data[left];
|
|
18888
|
+
const right = left + 1;
|
|
18889
|
+
|
|
18890
|
+
if (right < this.length && compare(data[right], best) < 0) {
|
|
18891
|
+
left = right;
|
|
18892
|
+
best = data[right];
|
|
18893
|
+
}
|
|
18894
|
+
if (compare(best, item) >= 0) break;
|
|
18895
|
+
|
|
18896
|
+
data[pos] = best;
|
|
18897
|
+
pos = left;
|
|
18898
|
+
}
|
|
18899
|
+
|
|
18900
|
+
data[pos] = item;
|
|
18901
|
+
}
|
|
18902
|
+
}
|
|
18903
|
+
|
|
18904
|
+
function defaultCompare(a, b) {
|
|
18905
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
18906
|
+
}
|
|
18907
|
+
|
|
18908
|
+
/*
|
|
18909
|
+
Copyright (c) 2016-2026, Vladimir Agafonkin
|
|
18910
|
+
|
|
18911
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
18912
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
18913
|
+
copyright notice and this permission notice appear in all copies.
|
|
18914
|
+
|
|
18915
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
18916
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
18917
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
18918
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
18919
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
18920
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
18921
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
18922
|
+
|
|
18923
|
+
Based on mapbox/polylabel 2.1.0. Adapted for mapshaper to operate on
|
|
18924
|
+
topological polygon shapes, use a bounded single-cell search and support
|
|
18925
|
+
alternative centroid-aware objectives.
|
|
18926
|
+
*/
|
|
18927
|
+
|
|
18928
|
+
|
|
18929
|
+
var BLOCK_SIZE = 32;
|
|
18930
|
+
var DEFAULT_PRECISION = 0.01;
|
|
18931
|
+
var DEFAULT_PRECISION_FLOOR = 1e-4;
|
|
18932
|
+
// Hard ceiling on cell creation. Besides bounding runtime, this limits the
|
|
18933
|
+
// priority queue to O(DEFAULT_PROBE_LIMIT) entries on pathological rings.
|
|
18934
|
+
var DEFAULT_PROBE_LIMIT = 100000;
|
|
18935
|
+
var DEFAULT_TOLERANCE = 0.1;
|
|
18936
|
+
var DEFAULT_WEIGHT = 0.6;
|
|
18937
|
+
var MAX_WEIGHT_PENALTY = 0.25;
|
|
18938
|
+
var SQRT2 = Math.SQRT2;
|
|
18939
|
+
|
|
18940
|
+
// Find an inner point using one of four polylabel-based policies.
|
|
18941
|
+
// Returns null for collapsed polygons, so callers can fall back to the legacy
|
|
18942
|
+
// anchor-point finder.
|
|
18943
|
+
function findPolylabelPoint(shp, arcs, opts) {
|
|
18944
|
+
var polygon = buildPolygon(shp, arcs);
|
|
18945
|
+
if (!polygon) return null;
|
|
18946
|
+
|
|
18947
|
+
opts = opts || {};
|
|
18948
|
+
var precision = getPrecision(polygon, opts);
|
|
18949
|
+
if (!(precision > 0)) return null;
|
|
18950
|
+
|
|
18951
|
+
var limit = opts.probe_limit > 0 ? opts.probe_limit : DEFAULT_PROBE_LIMIT;
|
|
18952
|
+
var pole = findPole(polygon, precision, limit);
|
|
18953
|
+
if (!isUsablePoint(pole)) return null;
|
|
18954
|
+
|
|
18955
|
+
var method = opts.method || 'centroid2';
|
|
18956
|
+
if (method == 'pole') return pole;
|
|
18957
|
+
|
|
18958
|
+
var centroid = getCentroidCell(polygon);
|
|
18959
|
+
if (!(centroid.d > 0)) return pole;
|
|
18960
|
+
|
|
18961
|
+
var tolerance = opts.tolerance;
|
|
18962
|
+
if (!(tolerance >= 0 && tolerance <= 1)) tolerance = DEFAULT_TOLERANCE;
|
|
18963
|
+
var minClearance = pole.distance * (1 - tolerance);
|
|
18964
|
+
|
|
18965
|
+
if (method == 'centroid') {
|
|
18966
|
+
return centroid.d >= minClearance ? toResult(centroid, pole.probes) : pole;
|
|
18967
|
+
}
|
|
18968
|
+
if (method == 'centroid2') {
|
|
18969
|
+
if (centroid.d >= minClearance) return toResult(centroid, pole.probes);
|
|
18970
|
+
return findClosestAcceptablePoint(
|
|
18971
|
+
polygon, centroid.x, centroid.y, pole, minClearance, precision, limit
|
|
18972
|
+
);
|
|
18973
|
+
}
|
|
18974
|
+
if (method == 'weighted') {
|
|
18975
|
+
var weight = opts.weight;
|
|
18976
|
+
if (!(weight >= 0)) weight = DEFAULT_WEIGHT;
|
|
18977
|
+
return findWeightedPoint(polygon, centroid, pole, weight, precision, limit);
|
|
18978
|
+
}
|
|
18979
|
+
return null;
|
|
18980
|
+
}
|
|
18981
|
+
|
|
18982
|
+
function isUsablePoint(p) {
|
|
18983
|
+
return p && isFinite(p.x) && isFinite(p.y) && p.distance > 0;
|
|
18984
|
+
}
|
|
18985
|
+
|
|
18986
|
+
function buildPolygon(shp, arcs) {
|
|
18987
|
+
var outerIds = shp && geom.getMaxPath(shp, arcs);
|
|
18988
|
+
if (!outerIds) return null;
|
|
18989
|
+
|
|
18990
|
+
var outer = pathToRing(outerIds, arcs);
|
|
18991
|
+
var outerArea = geom.getPlanarPathArea(outerIds, arcs);
|
|
18992
|
+
if (!outer || outerArea === 0) return null;
|
|
18993
|
+
|
|
18994
|
+
var outerBounds = arcs.getSimpleShapeBounds(outerIds);
|
|
18995
|
+
var rings = [outer];
|
|
18996
|
+
var area = Math.abs(outerArea);
|
|
18997
|
+
var perimeter = getRingPerimeter$1(outer);
|
|
18998
|
+
|
|
18999
|
+
// Mapshaper stores multipart polygons as a flat list of rings. Polylabel
|
|
19000
|
+
// expects one outer ring followed by its holes, so omit other outer rings.
|
|
19001
|
+
(shp || []).forEach(function(ids) {
|
|
19002
|
+
if (ids === outerIds) return;
|
|
19003
|
+
var ringArea = geom.getPlanarPathArea(ids, arcs);
|
|
19004
|
+
if (ringArea === 0 || ringArea * outerArea >= 0) return;
|
|
19005
|
+
var bounds = arcs.getSimpleShapeBounds(ids);
|
|
19006
|
+
if (!outerBounds.intersects(bounds)) return;
|
|
19007
|
+
var ring = pathToRing(ids, arcs);
|
|
19008
|
+
if (!ring || !geom.testPointInRing(ring[0][0], ring[0][1], outerIds, arcs)) {
|
|
19009
|
+
return;
|
|
19010
|
+
}
|
|
19011
|
+
rings.push(ring);
|
|
19012
|
+
area -= Math.abs(ringArea);
|
|
19013
|
+
perimeter += getRingPerimeter$1(ring);
|
|
19014
|
+
});
|
|
19015
|
+
|
|
19016
|
+
if (!(area > 0) || !(perimeter > 0)) return null;
|
|
19017
|
+
return createPolygonContext(rings, outerBounds, area, perimeter);
|
|
19018
|
+
}
|
|
19019
|
+
|
|
19020
|
+
function pathToRing(ids, arcs) {
|
|
19021
|
+
var iter = arcs.getShapeIter(ids);
|
|
19022
|
+
var points = [];
|
|
19023
|
+
var prevX, prevY;
|
|
19024
|
+
while (iter.hasNext()) {
|
|
19025
|
+
if (points.length === 0 || iter.x != prevX || iter.y != prevY) {
|
|
19026
|
+
points.push([iter.x, iter.y]);
|
|
19027
|
+
prevX = iter.x;
|
|
19028
|
+
prevY = iter.y;
|
|
19029
|
+
}
|
|
19030
|
+
}
|
|
19031
|
+
if (points.length < 3) return null;
|
|
19032
|
+
var a = points[0];
|
|
19033
|
+
var b = points[points.length - 1];
|
|
19034
|
+
if (a[0] != b[0] || a[1] != b[1]) points.push([a[0], a[1]]);
|
|
19035
|
+
return points.length > 3 ? points : null;
|
|
19036
|
+
}
|
|
19037
|
+
|
|
19038
|
+
function getRingPerimeter$1(ring) {
|
|
19039
|
+
var sum = 0;
|
|
19040
|
+
for (var i = 1; i < ring.length; i++) {
|
|
19041
|
+
sum += geom.distance2D(
|
|
19042
|
+
ring[i - 1][0], ring[i - 1][1], ring[i][0], ring[i][1]
|
|
19043
|
+
);
|
|
19044
|
+
}
|
|
19045
|
+
return sum;
|
|
19046
|
+
}
|
|
19047
|
+
|
|
19048
|
+
function createPolygonContext(rings, bounds, area, perimeter) {
|
|
19049
|
+
var pointCount = rings.reduce(function(sum, ring) {
|
|
19050
|
+
return sum + ring.length;
|
|
19051
|
+
}, 0);
|
|
19052
|
+
var coords = new Float64Array(pointCount * 2);
|
|
19053
|
+
var ringEnds = new Uint32Array(rings.length);
|
|
19054
|
+
var k = 0;
|
|
19055
|
+
rings.forEach(function(ring, i) {
|
|
19056
|
+
ring.forEach(function(p) {
|
|
19057
|
+
coords[k++] = p[0];
|
|
19058
|
+
coords[k++] = p[1];
|
|
19059
|
+
});
|
|
19060
|
+
ringEnds[i] = k;
|
|
19061
|
+
});
|
|
19062
|
+
return {
|
|
19063
|
+
area: area,
|
|
19064
|
+
perimeter: perimeter,
|
|
19065
|
+
bounds: bounds,
|
|
19066
|
+
coords: coords,
|
|
19067
|
+
ringEnds: ringEnds,
|
|
19068
|
+
blocks: buildBlocks(coords, ringEnds)
|
|
19069
|
+
};
|
|
19070
|
+
}
|
|
19071
|
+
|
|
19072
|
+
function getPrecision(polygon, opts) {
|
|
19073
|
+
if (opts.precision > 0) return opts.precision;
|
|
19074
|
+
var extent = Math.max(polygon.bounds.width(), polygon.bounds.height());
|
|
19075
|
+
var relative = opts.precision_fraction > 0 ?
|
|
19076
|
+
opts.precision_fraction : DEFAULT_PRECISION;
|
|
19077
|
+
var floor = opts.precision_floor >= 0 ?
|
|
19078
|
+
opts.precision_floor : DEFAULT_PRECISION_FLOOR;
|
|
19079
|
+
// 2A/P is rotation-invariant and approximates a polygon's local thickness.
|
|
19080
|
+
return Math.max(relative * 2 * polygon.area / polygon.perimeter, floor * extent);
|
|
19081
|
+
}
|
|
19082
|
+
|
|
19083
|
+
function findPole(polygon, precision, limit) {
|
|
19084
|
+
var queue = new TinyQueue([], compareMax);
|
|
19085
|
+
var centroid = getCentroidCell(polygon);
|
|
19086
|
+
var best = centroid.d > 0 ? centroid :
|
|
19087
|
+
new Cell(polygon.coords[0], polygon.coords[1], 0, polygon);
|
|
19088
|
+
var bounds = polygon.bounds;
|
|
19089
|
+
var bboxCell = new Cell(bounds.centerX(), bounds.centerY(), 0, polygon);
|
|
19090
|
+
if (bboxCell.d > best.d) best = bboxCell;
|
|
19091
|
+
|
|
19092
|
+
var probes = 2;
|
|
19093
|
+
var exhausted = false;
|
|
19094
|
+
|
|
19095
|
+
function addCell(x, y, h, seed) {
|
|
19096
|
+
// If the cell center is already too close to an edge for this cell to
|
|
19097
|
+
// improve the result, the distance scan can stop at that edge. A bailed
|
|
19098
|
+
// scan is never scored or queued because its returned distance is only a
|
|
19099
|
+
// threshold, not the center's actual signed distance.
|
|
19100
|
+
var threshold = best.d - Math.max(0, h * SQRT2 - precision);
|
|
19101
|
+
var cell = new Cell(x, y, h, polygon, threshold, seed);
|
|
19102
|
+
probes++;
|
|
19103
|
+
if (cell.bailed) return;
|
|
19104
|
+
if (cell.d > best.d) best = cell;
|
|
19105
|
+
if (cell.max > best.d + precision) queue.push(cell);
|
|
19106
|
+
}
|
|
19107
|
+
|
|
19108
|
+
var h = Math.max(bounds.width(), bounds.height()) / 2;
|
|
19109
|
+
addCell(bounds.centerX(), bounds.centerY(), h, null);
|
|
19110
|
+
while (queue.length > 0) {
|
|
19111
|
+
if (probes + 4 > limit) {
|
|
19112
|
+
exhausted = true;
|
|
19113
|
+
break;
|
|
19114
|
+
}
|
|
19115
|
+
var cell = queue.pop();
|
|
19116
|
+
if (cell.max - best.d <= precision) continue;
|
|
19117
|
+
h = cell.h / 2;
|
|
19118
|
+
addCell(cell.x - h, cell.y - h, h, cell);
|
|
19119
|
+
addCell(cell.x + h, cell.y - h, h, cell);
|
|
19120
|
+
addCell(cell.x - h, cell.y + h, h, cell);
|
|
19121
|
+
addCell(cell.x + h, cell.y + h, h, cell);
|
|
19122
|
+
}
|
|
19123
|
+
return toResult(best, probes, exhausted);
|
|
19124
|
+
}
|
|
19125
|
+
|
|
19126
|
+
// Among points whose clearance is near the maximum, find the point closest to
|
|
19127
|
+
// the centroid. This is a constrained objective, not a general centroid bias.
|
|
19128
|
+
function findClosestAcceptablePoint(
|
|
19129
|
+
polygon, centroidX, centroidY, pole, minClearance, precision, limit
|
|
19130
|
+
) {
|
|
19131
|
+
var queue = new TinyQueue([], compareOffset);
|
|
19132
|
+
var bounds = polygon.bounds;
|
|
19133
|
+
var h = Math.max(bounds.width(), bounds.height()) / 2;
|
|
19134
|
+
var root = new Cell(bounds.centerX(), bounds.centerY(), h, polygon);
|
|
19135
|
+
setOffsetBounds(root, centroidX, centroidY);
|
|
19136
|
+
queue.push(root);
|
|
19137
|
+
|
|
19138
|
+
var best = {
|
|
19139
|
+
x: pole.x,
|
|
19140
|
+
y: pole.y,
|
|
19141
|
+
d: pole.distance,
|
|
19142
|
+
offset: geom.distance2D(pole.x, pole.y, centroidX, centroidY)
|
|
19143
|
+
};
|
|
19144
|
+
var probes = pole.probes + 1;
|
|
19145
|
+
var exhausted = false;
|
|
19146
|
+
|
|
19147
|
+
while (queue.length > 0) {
|
|
19148
|
+
if (probes + 4 > limit) {
|
|
19149
|
+
exhausted = true;
|
|
19150
|
+
break;
|
|
19151
|
+
}
|
|
19152
|
+
var cell = queue.pop();
|
|
19153
|
+
if (cell.minOffset >= best.offset || cell.max < minClearance) continue;
|
|
19154
|
+
if (cell.d >= minClearance && cell.offset < best.offset) {
|
|
19155
|
+
best = cell;
|
|
19156
|
+
}
|
|
19157
|
+
if (cell.h <= precision / 2) continue;
|
|
19158
|
+
h = cell.h / 2;
|
|
19159
|
+
[
|
|
19160
|
+
[cell.x - h, cell.y - h],
|
|
19161
|
+
[cell.x + h, cell.y - h],
|
|
19162
|
+
[cell.x - h, cell.y + h],
|
|
19163
|
+
[cell.x + h, cell.y + h]
|
|
19164
|
+
].forEach(function(xy) {
|
|
19165
|
+
var child = new Cell(xy[0], xy[1], h, polygon);
|
|
19166
|
+
setOffsetBounds(child, centroidX, centroidY);
|
|
19167
|
+
if (child.minOffset < best.offset && child.max >= minClearance) {
|
|
19168
|
+
queue.push(child);
|
|
19169
|
+
}
|
|
19170
|
+
});
|
|
19171
|
+
probes += 4;
|
|
19172
|
+
}
|
|
19173
|
+
return toResult(best, probes, exhausted);
|
|
19174
|
+
}
|
|
19175
|
+
|
|
19176
|
+
function setOffsetBounds(cell, x, y) {
|
|
19177
|
+
cell.offset = geom.distance2D(cell.x, cell.y, x, y);
|
|
19178
|
+
cell.minOffset = Math.max(0, cell.offset - cell.h * SQRT2);
|
|
19179
|
+
}
|
|
19180
|
+
|
|
19181
|
+
function findWeightedPoint(polygon, centroid, pole, weight, precision, limit) {
|
|
19182
|
+
if (weight === 0) return pole;
|
|
19183
|
+
var bounds = polygon.bounds;
|
|
19184
|
+
var refDist = Math.max(bounds.width(), bounds.height()) / 2;
|
|
19185
|
+
var queue = new TinyQueue([], compareScore);
|
|
19186
|
+
var best = new Cell(pole.x, pole.y, 0, polygon);
|
|
19187
|
+
setWeightedScore(best, centroid, refDist, weight);
|
|
19188
|
+
|
|
19189
|
+
var h = Math.max(bounds.width(), bounds.height()) / 2;
|
|
19190
|
+
var root = new Cell(bounds.centerX(), bounds.centerY(), h, polygon);
|
|
19191
|
+
setWeightedScore(root, centroid, refDist, weight);
|
|
19192
|
+
queue.push(root);
|
|
19193
|
+
var probes = pole.probes + 1;
|
|
19194
|
+
var exhausted = false;
|
|
19195
|
+
|
|
19196
|
+
while (queue.length > 0) {
|
|
19197
|
+
if (probes + 4 > limit) {
|
|
19198
|
+
exhausted = true;
|
|
19199
|
+
break;
|
|
19200
|
+
}
|
|
19201
|
+
var cell = queue.pop();
|
|
19202
|
+
if (cell.score > best.score) best = cell;
|
|
19203
|
+
if (cell.maxScore - best.score <= precision) continue;
|
|
19204
|
+
h = cell.h / 2;
|
|
19205
|
+
[
|
|
19206
|
+
[cell.x - h, cell.y - h],
|
|
19207
|
+
[cell.x + h, cell.y - h],
|
|
19208
|
+
[cell.x - h, cell.y + h],
|
|
19209
|
+
[cell.x + h, cell.y + h]
|
|
19210
|
+
].forEach(function(xy) {
|
|
19211
|
+
var child = new Cell(xy[0], xy[1], h, polygon);
|
|
19212
|
+
setWeightedScore(child, centroid, refDist, weight);
|
|
19213
|
+
if (child.maxScore > best.score + precision) queue.push(child);
|
|
19214
|
+
if (child.score > best.score) best = child;
|
|
19215
|
+
});
|
|
19216
|
+
probes += 4;
|
|
19217
|
+
}
|
|
19218
|
+
return toResult(best, probes, exhausted);
|
|
19219
|
+
}
|
|
19220
|
+
|
|
19221
|
+
function setWeightedScore(cell, centroid, refDist, weight) {
|
|
19222
|
+
var offset = geom.distance2D(cell.x, cell.y, centroid.x, centroid.y);
|
|
19223
|
+
var minOffset = Math.max(0, offset - cell.h * SQRT2);
|
|
19224
|
+
var pointWeight = getCentroidWeight(offset, refDist, weight);
|
|
19225
|
+
var maxWeight = getCentroidWeight(minOffset, refDist, weight);
|
|
19226
|
+
cell.score = cell.d * pointWeight;
|
|
19227
|
+
cell.maxScore = cell.max * maxWeight;
|
|
19228
|
+
}
|
|
19229
|
+
|
|
19230
|
+
function getCentroidWeight(offset, refDist, weight) {
|
|
19231
|
+
return refDist > 0 ?
|
|
19232
|
+
1 - Math.min(weight * offset / refDist, MAX_WEIGHT_PENALTY) : 1;
|
|
19233
|
+
}
|
|
19234
|
+
|
|
19235
|
+
function getCentroidCell(polygon) {
|
|
19236
|
+
var coords = polygon.coords;
|
|
19237
|
+
var end = polygon.ringEnds[0];
|
|
19238
|
+
var area = 0;
|
|
19239
|
+
var x = 0;
|
|
19240
|
+
var y = 0;
|
|
19241
|
+
for (var i = 0, j = end - 2; i < end; j = i, i += 2) {
|
|
19242
|
+
var ax = coords[i];
|
|
19243
|
+
var ay = coords[i + 1];
|
|
19244
|
+
var bx = coords[j];
|
|
19245
|
+
var by = coords[j + 1];
|
|
19246
|
+
var f = ax * by - bx * ay;
|
|
19247
|
+
x += (ax + bx) * f;
|
|
19248
|
+
y += (ay + by) * f;
|
|
19249
|
+
area += f * 3;
|
|
19250
|
+
}
|
|
19251
|
+
if (area === 0) return new Cell(coords[0], coords[1], 0, polygon);
|
|
19252
|
+
return new Cell(x / area, y / area, 0, polygon);
|
|
19253
|
+
}
|
|
19254
|
+
|
|
19255
|
+
function Cell(x, y, h, polygon, maxD, seed) {
|
|
19256
|
+
this.x = x;
|
|
19257
|
+
this.y = y;
|
|
19258
|
+
this.h = h;
|
|
19259
|
+
this.nsx1 = 0;
|
|
19260
|
+
this.nsy1 = 0;
|
|
19261
|
+
this.nsx2 = 0;
|
|
19262
|
+
this.nsy2 = 0;
|
|
19263
|
+
this.bailed = false;
|
|
19264
|
+
this.d = pointToPolygonDistance(
|
|
19265
|
+
this, polygon, maxD === undefined ? -Infinity : maxD, seed || null
|
|
19266
|
+
);
|
|
19267
|
+
this.max = this.d + h * SQRT2;
|
|
19268
|
+
}
|
|
19269
|
+
|
|
19270
|
+
function pointToPolygonDistance(cell, polygon, maxD, seed) {
|
|
19271
|
+
var coords = polygon.coords;
|
|
19272
|
+
var ringEnds = polygon.ringEnds;
|
|
19273
|
+
var blocks = polygon.blocks;
|
|
19274
|
+
var x = cell.x;
|
|
19275
|
+
var y = cell.y;
|
|
19276
|
+
var inside = false;
|
|
19277
|
+
var minDistSq = Infinity;
|
|
19278
|
+
var thresholdSq = maxD > 0 ? maxD * maxD : -1;
|
|
19279
|
+
var stride = BLOCK_SIZE * 2;
|
|
19280
|
+
var blockIndex = 0;
|
|
19281
|
+
var ringStart = 0;
|
|
19282
|
+
|
|
19283
|
+
if (seed) {
|
|
19284
|
+
cell.nsx1 = seed.nsx1;
|
|
19285
|
+
cell.nsy1 = seed.nsy1;
|
|
19286
|
+
cell.nsx2 = seed.nsx2;
|
|
19287
|
+
cell.nsy2 = seed.nsy2;
|
|
19288
|
+
minDistSq = getSegmentDistanceSq(
|
|
19289
|
+
x, y, seed.nsx1, seed.nsy1, seed.nsx2, seed.nsy2
|
|
19290
|
+
);
|
|
19291
|
+
if (minDistSq <= thresholdSq) {
|
|
19292
|
+
cell.bailed = true;
|
|
19293
|
+
return maxD;
|
|
19294
|
+
}
|
|
19295
|
+
}
|
|
19296
|
+
|
|
19297
|
+
for (var r = 0; r < ringEnds.length; r++) {
|
|
19298
|
+
var ringEnd = ringEnds[r];
|
|
19299
|
+
var bx = coords[ringEnd - 2];
|
|
19300
|
+
var by = coords[ringEnd - 1];
|
|
19301
|
+
for (var start = ringStart; start < ringEnd;
|
|
19302
|
+
start += stride, blockIndex += 4) {
|
|
19303
|
+
var end = Math.min(start + stride, ringEnd);
|
|
19304
|
+
var xmin = blocks[blockIndex];
|
|
19305
|
+
var ymin = blocks[blockIndex + 1];
|
|
19306
|
+
var xmax = blocks[blockIndex + 2];
|
|
19307
|
+
var ymax = blocks[blockIndex + 3];
|
|
19308
|
+
var dx = x < xmin ? xmin - x : x > xmax ? x - xmax : 0;
|
|
19309
|
+
var dy = y < ymin ? ymin - y : y > ymax ? y - ymax : 0;
|
|
19310
|
+
var skipDist = dx * dx + dy * dy >= minDistSq;
|
|
19311
|
+
var skipCross = y < ymin || y >= ymax || x > xmax;
|
|
19312
|
+
if (skipDist && skipCross) {
|
|
19313
|
+
bx = coords[end - 2];
|
|
19314
|
+
by = coords[end - 1];
|
|
19315
|
+
continue;
|
|
19316
|
+
}
|
|
19317
|
+
for (var i = start; i < end; i += 2) {
|
|
19318
|
+
var ax = coords[i];
|
|
19319
|
+
var ay = coords[i + 1];
|
|
19320
|
+
if (!skipCross && (ay > y !== by > y) &&
|
|
19321
|
+
x < (bx - ax) * (y - ay) / (by - ay) + ax) {
|
|
19322
|
+
inside = !inside;
|
|
19323
|
+
}
|
|
19324
|
+
if (!skipDist) {
|
|
19325
|
+
var distSq = getSegmentDistanceSq(x, y, ax, ay, bx, by);
|
|
19326
|
+
if (distSq < minDistSq) {
|
|
19327
|
+
minDistSq = distSq;
|
|
19328
|
+
cell.nsx1 = ax;
|
|
19329
|
+
cell.nsy1 = ay;
|
|
19330
|
+
cell.nsx2 = bx;
|
|
19331
|
+
cell.nsy2 = by;
|
|
19332
|
+
if (minDistSq <= thresholdSq) {
|
|
19333
|
+
cell.bailed = true;
|
|
19334
|
+
return maxD;
|
|
19335
|
+
}
|
|
19336
|
+
}
|
|
19337
|
+
}
|
|
19338
|
+
bx = ax;
|
|
19339
|
+
by = ay;
|
|
19340
|
+
}
|
|
19341
|
+
}
|
|
19342
|
+
ringStart = ringEnd;
|
|
19343
|
+
}
|
|
19344
|
+
return minDistSq === 0 ? 0 : (inside ? 1 : -1) * Math.sqrt(minDistSq);
|
|
19345
|
+
}
|
|
19346
|
+
|
|
19347
|
+
function buildBlocks(coords, ringEnds) {
|
|
19348
|
+
var stride = BLOCK_SIZE * 2;
|
|
19349
|
+
var blockCount = 0;
|
|
19350
|
+
var ringStart = 0;
|
|
19351
|
+
for (var r = 0; r < ringEnds.length; r++) {
|
|
19352
|
+
blockCount += Math.ceil((ringEnds[r] - ringStart) / stride);
|
|
19353
|
+
ringStart = ringEnds[r];
|
|
19354
|
+
}
|
|
19355
|
+
var blocks = new Float64Array(blockCount * 4);
|
|
19356
|
+
var k = 0;
|
|
19357
|
+
ringStart = 0;
|
|
19358
|
+
for (r = 0; r < ringEnds.length; r++) {
|
|
19359
|
+
var ringEnd = ringEnds[r];
|
|
19360
|
+
for (var start = ringStart; start < ringEnd; start += stride) {
|
|
19361
|
+
var end = Math.min(start + stride, ringEnd);
|
|
19362
|
+
var prev = start === ringStart ? ringEnd - 2 : start - 2;
|
|
19363
|
+
var xmin = coords[prev];
|
|
19364
|
+
var ymin = coords[prev + 1];
|
|
19365
|
+
var xmax = xmin;
|
|
19366
|
+
var ymax = ymin;
|
|
19367
|
+
for (var i = start; i < end; i += 2) {
|
|
19368
|
+
var x = coords[i];
|
|
19369
|
+
var y = coords[i + 1];
|
|
19370
|
+
if (x < xmin) xmin = x;
|
|
19371
|
+
if (x > xmax) xmax = x;
|
|
19372
|
+
if (y < ymin) ymin = y;
|
|
19373
|
+
if (y > ymax) ymax = y;
|
|
19374
|
+
}
|
|
19375
|
+
blocks[k++] = xmin;
|
|
19376
|
+
blocks[k++] = ymin;
|
|
19377
|
+
blocks[k++] = xmax;
|
|
19378
|
+
blocks[k++] = ymax;
|
|
19379
|
+
}
|
|
19380
|
+
ringStart = ringEnd;
|
|
19381
|
+
}
|
|
19382
|
+
return blocks;
|
|
19383
|
+
}
|
|
19384
|
+
|
|
19385
|
+
function getSegmentDistanceSq(px, py, x, y, bx, by) {
|
|
19386
|
+
var dx = bx - x;
|
|
19387
|
+
var dy = by - y;
|
|
19388
|
+
if (dx !== 0 || dy !== 0) {
|
|
19389
|
+
var t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy);
|
|
19390
|
+
if (t > 1) {
|
|
19391
|
+
x = bx;
|
|
19392
|
+
y = by;
|
|
19393
|
+
} else if (t > 0) {
|
|
19394
|
+
x += dx * t;
|
|
19395
|
+
y += dy * t;
|
|
19396
|
+
}
|
|
19397
|
+
}
|
|
19398
|
+
dx = px - x;
|
|
19399
|
+
dy = py - y;
|
|
19400
|
+
return dx * dx + dy * dy;
|
|
19401
|
+
}
|
|
19402
|
+
|
|
19403
|
+
function toResult(cell, probes, exhausted) {
|
|
19404
|
+
return {
|
|
19405
|
+
x: cell.x,
|
|
19406
|
+
y: cell.y,
|
|
19407
|
+
distance: cell.d,
|
|
19408
|
+
probes: probes || 0,
|
|
19409
|
+
exhausted: !!exhausted
|
|
19410
|
+
};
|
|
19411
|
+
}
|
|
19412
|
+
|
|
19413
|
+
function compareMax(a, b) {
|
|
19414
|
+
return b.max - a.max;
|
|
19415
|
+
}
|
|
19416
|
+
|
|
19417
|
+
function compareOffset(a, b) {
|
|
19418
|
+
return a.minOffset - b.minOffset;
|
|
19419
|
+
}
|
|
19420
|
+
|
|
19421
|
+
function compareScore(a, b) {
|
|
19422
|
+
return b.maxScore - a.maxScore;
|
|
19423
|
+
}
|
|
19424
|
+
|
|
18708
19425
|
function simplifyArcsFast(arcs, dist) {
|
|
18709
19426
|
var xx = [],
|
|
18710
19427
|
yy = [],
|
|
@@ -18776,8 +19493,20 @@
|
|
|
18776
19493
|
simplifyPolygonFast: simplifyPolygonFast
|
|
18777
19494
|
});
|
|
18778
19495
|
|
|
18779
|
-
// Find
|
|
18780
|
-
//
|
|
19496
|
+
// Find an interior point for label or symbol placement. The default method
|
|
19497
|
+
// finds a pole of inaccessibility, then moves toward the centroid while
|
|
19498
|
+
// retaining at least 90% of the pole's clearance.
|
|
19499
|
+
function findAnchorPoint(shp, arcs, opts) {
|
|
19500
|
+
var method = opts && opts.method || 'centroid2';
|
|
19501
|
+
var p;
|
|
19502
|
+
if (method != 'legacy') {
|
|
19503
|
+
p = findPolylabelPoint(shp, arcs, opts);
|
|
19504
|
+
if (p) return p;
|
|
19505
|
+
}
|
|
19506
|
+
return findLegacyAnchorPoint(shp, arcs);
|
|
19507
|
+
}
|
|
19508
|
+
|
|
19509
|
+
// Legacy sampled-ray method:
|
|
18781
19510
|
// - get the largest ring of the polygon
|
|
18782
19511
|
// - get an array of x-values distributed along the horizontal extent of the ring
|
|
18783
19512
|
// - for each x:
|
|
@@ -18788,8 +19517,7 @@
|
|
|
18788
19517
|
// - return the adjusted point having the maximum weighted distance from the edge
|
|
18789
19518
|
//
|
|
18790
19519
|
// (distance is weighted to slightly favor points near centroid)
|
|
18791
|
-
|
|
18792
|
-
function findAnchorPoint(shp, arcs) {
|
|
19520
|
+
function findLegacyAnchorPoint(shp, arcs) {
|
|
18793
19521
|
var maxPath = shp && geom.getMaxPath(shp, arcs),
|
|
18794
19522
|
pathBounds = maxPath && arcs.getSimpleShapeBounds(maxPath),
|
|
18795
19523
|
thresh, simple;
|
|
@@ -18843,7 +19571,13 @@
|
|
|
18843
19571
|
// Look for even better fit close to best-fit point
|
|
18844
19572
|
p2 = probeForBestAnchorPoint(shp, arcs, p.x - hstep / 2,
|
|
18845
19573
|
p.x + hstep / 2, 2, weight);
|
|
18846
|
-
|
|
19574
|
+
// The refinement can come back empty even though the coarse sweep did not,
|
|
19575
|
+
// because it samples only two x-values inside a narrow band. On a ring that
|
|
19576
|
+
// encloses no real area -- a collapsed sliver that runs out to a point and
|
|
19577
|
+
// back along itself, which a polygon buffer readily produces -- a ray at
|
|
19578
|
+
// either of those two can enclose no interval at all. The coarse result
|
|
19579
|
+
// already in hand stands in that case.
|
|
19580
|
+
if (p2 && p2.distance > p.distance) {
|
|
18847
19581
|
p = p2;
|
|
18848
19582
|
}
|
|
18849
19583
|
}
|
|
@@ -18912,16 +19646,42 @@
|
|
|
18912
19646
|
return p;
|
|
18913
19647
|
}
|
|
18914
19648
|
|
|
19649
|
+
// Ceiling on the vertical scan below, which is otherwise unbounded: the scan
|
|
19650
|
+
// walks its chord in steps of vstep, so it runs for roughly the shape's height
|
|
19651
|
+
// over vstep, and nothing constrains that ratio. A tall hairline sliver -- the
|
|
19652
|
+
// kind a buffer mosaic produces between two nearly-coincident arcs -- makes it
|
|
19653
|
+
// astronomical.
|
|
19654
|
+
//
|
|
19655
|
+
// Truncating is safe rather than free: the scan only replaces p when it finds a
|
|
19656
|
+
// strictly better point, so stopping early keeps the best point so far, which
|
|
19657
|
+
// is still inside the polygon -- but on a shape thin enough to need more steps
|
|
19658
|
+
// than this, that point can differ from the one an unbounded scan would reach
|
|
19659
|
+
// (measured on a 1.1m x 111km rectangle: same polygon, ~12% less clearance).
|
|
19660
|
+
// The ceiling does bind on real data -- two of the -clean fixtures reach
|
|
19661
|
+
// ~180,000 steps here -- yet cutting every scan off at 10,000 left all 744
|
|
19662
|
+
// buffer fixture cases byte-identical, because the long tail is usually a
|
|
19663
|
+
// plateau the scan walks without improving on.
|
|
19664
|
+
var MAX_SCAN_STEPS = 10000;
|
|
19665
|
+
|
|
18915
19666
|
// Try to find a better-fit point than @p by scanning vertically
|
|
18916
19667
|
// Modify p in-place
|
|
18917
19668
|
function scanForBetterPoint(p, shp, arcs, vstep, weight) {
|
|
18918
19669
|
var x = p.x,
|
|
18919
19670
|
y = p.y,
|
|
18920
19671
|
dmax = p.distance,
|
|
18921
|
-
d;
|
|
18922
|
-
|
|
18923
|
-
|
|
18924
|
-
y
|
|
19672
|
+
d, y2;
|
|
19673
|
+
|
|
19674
|
+
for (var i = 0; i < MAX_SCAN_STEPS; i++) {
|
|
19675
|
+
y2 = y + vstep;
|
|
19676
|
+
// Stop when the step cannot move the point at all. vstep is the width of
|
|
19677
|
+
// the probe band, and on a shape narrower than the floating-point spacing
|
|
19678
|
+
// at its own coordinates that band collapses to nothing (see the
|
|
19679
|
+
// refinement probe in findAnchorPoint2), leaving y + vstep === y. Every
|
|
19680
|
+
// iteration would then retest the same point forever: a collapsed mosaic
|
|
19681
|
+
// tile 20nm wide, at longitude -119, hung here indefinitely rather than
|
|
19682
|
+
// merely running slowly.
|
|
19683
|
+
if (y2 === y) break;
|
|
19684
|
+
y = y2;
|
|
18925
19685
|
d = geom.getPointToShapeDistance(x, y, shp, arcs) * weight(x, y);
|
|
18926
19686
|
// overcome vary small local minima
|
|
18927
19687
|
if (d > dmax * 0.90 && geom.testPointInPolygon(x, y, shp, arcs)) {
|
|
@@ -18999,7 +19759,8 @@
|
|
|
18999
19759
|
|
|
19000
19760
|
var AnchorPoints = /*#__PURE__*/Object.freeze({
|
|
19001
19761
|
__proto__: null,
|
|
19002
|
-
findAnchorPoint: findAnchorPoint
|
|
19762
|
+
findAnchorPoint: findAnchorPoint,
|
|
19763
|
+
findLegacyAnchorPoint: findLegacyAnchorPoint
|
|
19003
19764
|
});
|
|
19004
19765
|
|
|
19005
19766
|
// Returns a function for calculating the percentage of a shape's perimeter by length that
|
|
@@ -19250,6 +20011,8 @@
|
|
|
19250
20011
|
return bbox;
|
|
19251
20012
|
}
|
|
19252
20013
|
|
|
20014
|
+
var EXPRESSION_INNER_POINT_OPTS = {method: 'centroid2', tolerance: 0.1};
|
|
20015
|
+
|
|
19253
20016
|
// Returns a function to return a feature proxy by id
|
|
19254
20017
|
// (the proxy appears as "this" or "$" in a feature expression)
|
|
19255
20018
|
function initFeatureProxy(lyr, arcs, optsArg) {
|
|
@@ -19465,7 +20228,8 @@
|
|
|
19465
20228
|
}
|
|
19466
20229
|
|
|
19467
20230
|
function innerXY() {
|
|
19468
|
-
_innerXY = _innerXY ||
|
|
20231
|
+
_innerXY = _innerXY ||
|
|
20232
|
+
findAnchorPoint(_ids, arcs, EXPRESSION_INNER_POINT_OPTS);
|
|
19469
20233
|
return _innerXY;
|
|
19470
20234
|
}
|
|
19471
20235
|
|
|
@@ -21677,7 +22441,22 @@
|
|
|
21677
22441
|
var tile = mosaic[tileId];
|
|
21678
22442
|
return filter(tile[0]); // test tile ring, ignoring any holes (does this matter?)
|
|
21679
22443
|
});
|
|
21680
|
-
|
|
22444
|
+
// Choose every tile's owner before filling any of them. A tile that has just
|
|
22445
|
+
// been filled would otherwise present itself to the next one as a feature
|
|
22446
|
+
// boundary, so a gap divided into sections could award every section to
|
|
22447
|
+
// whichever feature received the first -- along a cut as long as the
|
|
22448
|
+
// boundaries it was meant to divide between.
|
|
22449
|
+
var chosen = filledIds.map(findAdjacentShapeId);
|
|
22450
|
+
var deferredIds = [];
|
|
22451
|
+
filledIds.forEach(function(tileId, i) {
|
|
22452
|
+
if (chosen[i] > -1) {
|
|
22453
|
+
tileShapeIndex.addTileToShape(chosen[i], tileId);
|
|
22454
|
+
} else {
|
|
22455
|
+
deferredIds.push(tileId);
|
|
22456
|
+
}
|
|
22457
|
+
});
|
|
22458
|
+
// A tile bordered only by other gaps can be filled once they have been.
|
|
22459
|
+
deferredIds.forEach(assignTileToAdjacentShape);
|
|
21681
22460
|
return {
|
|
21682
22461
|
removed: filledIds.length,
|
|
21683
22462
|
remaining: remainingIds.length - filledIds.length
|
|
@@ -21688,6 +22467,26 @@
|
|
|
21688
22467
|
return tileShapeIndex.getUnusedTileIds().map(tileIdToTile);
|
|
21689
22468
|
};
|
|
21690
22469
|
|
|
22470
|
+
// Return unused tiles together with the source-feature owner of each boundary
|
|
22471
|
+
// arc. Used by -clean to partition multi-owner gaps before assigning them.
|
|
22472
|
+
this.getUnusedTileData = function(filter) {
|
|
22473
|
+
return tileShapeIndex.getUnusedTileIds().reduce(function(out, tileId) {
|
|
22474
|
+
var tile = mosaic[tileId];
|
|
22475
|
+
if (filter && !filter(tile[0])) return out;
|
|
22476
|
+
var boundary = [];
|
|
22477
|
+
tile[0].forEach(function(arcId) {
|
|
22478
|
+
var neighborTileId = arcTileIndex.getShapeIdByArcId(~arcId);
|
|
22479
|
+
var shapeId = neighborTileId < 0 ? -1 :
|
|
22480
|
+
tileShapeIndex.getShapeIdByTileId(neighborTileId);
|
|
22481
|
+
if (shapeId >= 0) {
|
|
22482
|
+
boundary.push({arcId: arcId, shapeId: shapeId});
|
|
22483
|
+
}
|
|
22484
|
+
});
|
|
22485
|
+
out.push({tileId: tileId, tile: tile, boundary: boundary});
|
|
22486
|
+
return out;
|
|
22487
|
+
}, []);
|
|
22488
|
+
};
|
|
22489
|
+
|
|
21691
22490
|
this.getTilesByShapeIds = function(shapeIds) {
|
|
21692
22491
|
return getTileIdsByShapeIds(shapeIds).map(tileIdToTile);
|
|
21693
22492
|
};
|
|
@@ -22137,7 +22936,9 @@
|
|
|
22137
22936
|
return mosaic[id];
|
|
22138
22937
|
}
|
|
22139
22938
|
|
|
22140
|
-
|
|
22939
|
+
// The shape sharing the longest boundary with a tile, or -1 if the tile is
|
|
22940
|
+
// bordered only by unassigned tiles.
|
|
22941
|
+
function findAdjacentShapeId(tileId) {
|
|
22141
22942
|
var ring = mosaic[tileId][0];
|
|
22142
22943
|
var arcs = nodes.arcs;
|
|
22143
22944
|
var arcId, neighborShapeId, neighborTileId, arcLen;
|
|
@@ -22154,6 +22955,11 @@
|
|
|
22154
22955
|
maxArcLen = arcLen;
|
|
22155
22956
|
}
|
|
22156
22957
|
}
|
|
22958
|
+
return shapeId;
|
|
22959
|
+
}
|
|
22960
|
+
|
|
22961
|
+
function assignTileToAdjacentShape(tileId) {
|
|
22962
|
+
var shapeId = findAdjacentShapeId(tileId);
|
|
22157
22963
|
if (shapeId > -1) {
|
|
22158
22964
|
tileShapeIndex.addTileToShape(shapeId, tileId);
|
|
22159
22965
|
}
|
|
@@ -22179,6 +22985,26 @@
|
|
|
22179
22985
|
}
|
|
22180
22986
|
}
|
|
22181
22987
|
|
|
22988
|
+
// Flags, by absolute arc id, for arcs with no mosaic tile on one of their sides:
|
|
22989
|
+
// the border between the mosaic and the space outside it. Space that polygons
|
|
22990
|
+
// enclose becomes a tile, however narrow, so an arc facing untiled space faces
|
|
22991
|
+
// the outside, and a gap there is one that gap filling cannot reach.
|
|
22992
|
+
//
|
|
22993
|
+
// Only the mosaic is needed, not the tile assignment a MosaicIndex goes on to
|
|
22994
|
+
// make, so this builds the cheaper half on its own.
|
|
22995
|
+
function getOutsideFacingArcFlags(nodes) {
|
|
22996
|
+
var mosaic = buildPolygonMosaic(nodes).mosaic;
|
|
22997
|
+
var arcTileIndex = new ShapeArcIndex(mosaic, nodes.arcs);
|
|
22998
|
+
var flags = new Uint8Array(nodes.arcs.size());
|
|
22999
|
+
for (var i = 0; i < flags.length; i++) {
|
|
23000
|
+
if (arcTileIndex.getShapeIdByArcId(i) < 0 ||
|
|
23001
|
+
arcTileIndex.getShapeIdByArcId(~i) < 0) {
|
|
23002
|
+
flags[i] = 1;
|
|
23003
|
+
}
|
|
23004
|
+
}
|
|
23005
|
+
return flags;
|
|
23006
|
+
}
|
|
23007
|
+
|
|
22182
23008
|
// Map arc ids to shape ids, assuming perfect topology
|
|
22183
23009
|
// (an arcId maps to at most one shape)
|
|
22184
23010
|
// Supports looking up a shape id using an arc id.
|
|
@@ -22209,7 +23035,8 @@
|
|
|
22209
23035
|
var MosaicIndex$1 = /*#__PURE__*/Object.freeze({
|
|
22210
23036
|
__proto__: null,
|
|
22211
23037
|
MosaicIndex: MosaicIndex,
|
|
22212
|
-
ShapeArcIndex: ShapeArcIndex
|
|
23038
|
+
ShapeArcIndex: ShapeArcIndex,
|
|
23039
|
+
getOutsideFacingArcFlags: getOutsideFacingArcFlags
|
|
22213
23040
|
});
|
|
22214
23041
|
|
|
22215
23042
|
// Assumes that arcs do not intersect except at endpoints
|
|
@@ -22256,6 +23083,13 @@
|
|
|
22256
23083
|
profileStart('dissolvePolygonGroups2');
|
|
22257
23084
|
profileStart('dpg2.NodeCollection');
|
|
22258
23085
|
var arcFilter = getArcPresenceTest(lyr.shapes, dataset.arcs);
|
|
23086
|
+
if (opts._mosaic_cut_arcs) {
|
|
23087
|
+
var layerArcFilter = arcFilter;
|
|
23088
|
+
arcFilter = function(arcId) {
|
|
23089
|
+
var id = arcId < 0 ? ~arcId : arcId;
|
|
23090
|
+
return layerArcFilter(arcId) || opts._mosaic_cut_arcs[id];
|
|
23091
|
+
};
|
|
23092
|
+
}
|
|
22259
23093
|
var nodes = new NodeCollection(dataset.arcs, arcFilter);
|
|
22260
23094
|
profileEnd('dpg2.NodeCollection');
|
|
22261
23095
|
var mosaicOpts = {
|
|
@@ -22267,12 +23101,17 @@
|
|
|
22267
23101
|
var mosaicIndex = new MosaicIndex(lyr, nodes, mosaicOpts);
|
|
22268
23102
|
profileEnd('dpg2.MosaicIndex');
|
|
22269
23103
|
// gap fill doesn't work yet with overlapping shapes
|
|
22270
|
-
var fillGaps = !opts.allow_overlaps &&
|
|
23104
|
+
var fillGaps = !opts.allow_overlaps &&
|
|
23105
|
+
(opts.gap_width || opts.sliver_control || opts.gap_fill_area);
|
|
22271
23106
|
var cleanupData, filterData;
|
|
22272
23107
|
if (fillGaps) {
|
|
22273
23108
|
profileStart('dpg2.removeGaps');
|
|
22274
|
-
var
|
|
22275
|
-
|
|
23109
|
+
var filterOpts = utils.extend({}, opts);
|
|
23110
|
+
// Legacy area path: keep the historical default of full sliver control.
|
|
23111
|
+
if (filterOpts.gap_width == null && filterOpts.sliver_control == null) {
|
|
23112
|
+
filterOpts.sliver_control = 1;
|
|
23113
|
+
}
|
|
23114
|
+
filterData = getSliverFilter(lyr, dataset, filterOpts);
|
|
22276
23115
|
cleanupData = mosaicIndex.removeGaps(filterData.filter);
|
|
22277
23116
|
profileEnd('dpg2.removeGaps');
|
|
22278
23117
|
}
|
|
@@ -22513,6 +23352,1245 @@
|
|
|
22513
23352
|
target.push.apply(target, source);
|
|
22514
23353
|
}
|
|
22515
23354
|
|
|
23355
|
+
// Two boundaries that should be one, but were digitized or computed twice, drift
|
|
23356
|
+
// apart by more than the single interval that ordinary vertex snapping repairs,
|
|
23357
|
+
// and their vertices are staggered, so no pairwise snapping ever sees them. In
|
|
23358
|
+
// the test corpus the worst of them sit ~20 intervals apart. This multiple keeps
|
|
23359
|
+
// the default repair at sub-micron scale, far below any gap a map could show,
|
|
23360
|
+
// while still catching them.
|
|
23361
|
+
var PRECISION_SEAM_INTERVALS = 100;
|
|
23362
|
+
|
|
23363
|
+
// Minimum accepted seam length, as a multiple of the median segment length. Two
|
|
23364
|
+
// boundaries that merely pass close to each other for a vertex or two are not a
|
|
23365
|
+
// seam.
|
|
23366
|
+
var MIN_SEAM_SEGMENT_FACTOR = 3;
|
|
23367
|
+
|
|
23368
|
+
// Collapse duplicate boundaries: two arcs carrying what should be one line,
|
|
23369
|
+
// digitized or computed twice and drifting apart by no more than floating-point
|
|
23370
|
+
// rounding (see PRECISION_SEAM_INTERVALS). Vertices on both banks are snapped to
|
|
23371
|
+
// the line midway between them, leaving a single shared boundary.
|
|
23372
|
+
//
|
|
23373
|
+
// Runs unconditionally. Left in place, such a seam becomes an enclosed sliver
|
|
23374
|
+
// that gap filling awards to one neighbor, giving that feature a zero-width spike
|
|
23375
|
+
// along the shared border.
|
|
23376
|
+
//
|
|
23377
|
+
// Returns the number of duplicate boundaries collapsed (see countConnectedGaps).
|
|
23378
|
+
function collapseDuplicateBoundaries(layers, dataset) {
|
|
23379
|
+
var polygonLayers = layers.filter(function(lyr) {
|
|
23380
|
+
return lyr.geometry_type == 'polygon' && lyr.shapes && lyr.shapes.length > 1;
|
|
23381
|
+
});
|
|
23382
|
+
if (polygonLayers.length === 0 || !dataset.arcs) return 0;
|
|
23383
|
+
|
|
23384
|
+
var spherical = isLatLngCRS(getDatasetCRS(dataset));
|
|
23385
|
+
var changed = 0;
|
|
23386
|
+
polygonLayers.forEach(function(lyr) {
|
|
23387
|
+
changed += collapseLayerDuplicates(lyr, dataset.arcs, spherical);
|
|
23388
|
+
});
|
|
23389
|
+
if (changed > 0) {
|
|
23390
|
+
rebuildTopology(dataset);
|
|
23391
|
+
verbose(utils.format('Closed %s duplicate boundar%s', changed,
|
|
23392
|
+
changed == 1 ? 'y' : 'ies'));
|
|
23393
|
+
}
|
|
23394
|
+
return changed;
|
|
23395
|
+
}
|
|
23396
|
+
|
|
23397
|
+
function collapseLayerDuplicates(lyr, arcs, spherical) {
|
|
23398
|
+
var medianSeg = measureMedianSegment(lyr, arcs);
|
|
23399
|
+
var distance = getPrecisionSeamDistance(arcs.getBounds().toArray(), spherical);
|
|
23400
|
+
if (!(distance > 0)) return 0;
|
|
23401
|
+
|
|
23402
|
+
// Keep the automatic search radius when the closing distance is smaller. This
|
|
23403
|
+
// lets us see the complete seam and reject it as a unit instead of partially
|
|
23404
|
+
// snapping the already-near portion while leaving its wider mouth open. A
|
|
23405
|
+
// duplicate boundary often has a mouth several orders of magnitude wider than
|
|
23406
|
+
// the seam behind it.
|
|
23407
|
+
var searchDistance = Math.max(distance, medianSeg * GAP_WIDTH_SEGMENT_FRACTION);
|
|
23408
|
+
var found = findLayerSeams(lyr, arcs, distance, searchDistance, spherical,
|
|
23409
|
+
medianSeg * MIN_SEAM_SEGMENT_FACTOR, null);
|
|
23410
|
+
if (!found) return 0;
|
|
23411
|
+
var snapped = profileWrap('cg.snapSeams', function() {
|
|
23412
|
+
return snapSeams(found.seams, found.seeds, found.arcsById, arcs,
|
|
23413
|
+
found.footpoints, distance);
|
|
23414
|
+
});
|
|
23415
|
+
return countConnectedGaps(snapped);
|
|
23416
|
+
}
|
|
23417
|
+
|
|
23418
|
+
// A crack that opens onto the space outside the mosaic is not enclosed by
|
|
23419
|
+
// polygons, so no tile covers it and gap filling cannot reach it. Snapping the
|
|
23420
|
+
// facing pair of vertices at each open end together encloses the crack, handing
|
|
23421
|
+
// it to the mosaic as an ordinary gap to be filled -- or divided between its
|
|
23422
|
+
// neighbors, where it borders three or more of them -- like any other.
|
|
23423
|
+
//
|
|
23424
|
+
// Only those pairs move: two coordinates per mouth, each by half the width of the
|
|
23425
|
+
// mouth. Interior gaps are left exactly as they were found, so they are filled the
|
|
23426
|
+
// same way whether or not this pass runs.
|
|
23427
|
+
//
|
|
23428
|
+
// Returns the number of mouths pinched shut.
|
|
23429
|
+
function pinchOuterCrackMouths(lyr, dataset, nodes, opts) {
|
|
23430
|
+
var arcs = dataset.arcs;
|
|
23431
|
+
if (!lyr.shapes || lyr.shapes.length < 2 || !arcs) return 0;
|
|
23432
|
+
var crs = getDatasetCRS(dataset);
|
|
23433
|
+
var spherical = isLatLngCRS(crs);
|
|
23434
|
+
var medianSeg = measureMedianSegment(lyr, arcs);
|
|
23435
|
+
// Same width as interior gap filling, from the same automatic default.
|
|
23436
|
+
var distance = resolveCloseDistance(opts, crs,
|
|
23437
|
+
medianSeg * GAP_WIDTH_SEGMENT_FRACTION);
|
|
23438
|
+
if (!(distance > 0)) return 0;
|
|
23439
|
+
|
|
23440
|
+
profileStart('cg.outsideFacingArcs');
|
|
23441
|
+
var outsideFacing = getOutsideFacingArcFlags(nodes);
|
|
23442
|
+
profileEnd('cg.outsideFacingArcs');
|
|
23443
|
+
var found = findLayerSeams(lyr, arcs, distance, distance, spherical,
|
|
23444
|
+
medianSeg * MIN_SEAM_SEGMENT_FACTOR, function(arcId) {
|
|
23445
|
+
return outsideFacing[arcId] === 1;
|
|
23446
|
+
});
|
|
23447
|
+
if (!found) return 0;
|
|
23448
|
+
|
|
23449
|
+
var pinched = profileWrap('cg.pinchMouths', function() {
|
|
23450
|
+
return pinchSeamMouths(found.seams, found.arcsById, arcs, nodes,
|
|
23451
|
+
found.footpoints, distance, spherical);
|
|
23452
|
+
});
|
|
23453
|
+
if (pinched > 0) {
|
|
23454
|
+
rebuildTopology(dataset);
|
|
23455
|
+
message(utils.format('Closed %s external gap%s', pinched,
|
|
23456
|
+
utils.pluralSuffix(pinched)));
|
|
23457
|
+
}
|
|
23458
|
+
return pinched;
|
|
23459
|
+
}
|
|
23460
|
+
|
|
23461
|
+
// Seams between facing arcs owned by different features: stretches where two
|
|
23462
|
+
// boundaries run within @distance of each other for at least @minSeamLength.
|
|
23463
|
+
// @keepArc, when given, limits which arcs are considered.
|
|
23464
|
+
function findLayerSeams(lyr, arcs, distance, searchDistance, spherical,
|
|
23465
|
+
minSeamLength, keepArc) {
|
|
23466
|
+
profileStart('cg.collectVertices');
|
|
23467
|
+
var owners = getArcOwners(lyr.shapes, arcs.size());
|
|
23468
|
+
var records = collectExternalVertices(arcs, owners, keepArc);
|
|
23469
|
+
profileEnd('cg.collectVertices');
|
|
23470
|
+
if (records.length < 4) return null;
|
|
23471
|
+
|
|
23472
|
+
profileStart('cg.findMouthSeeds');
|
|
23473
|
+
var seeds = findMouthSeeds(records, searchDistance, spherical);
|
|
23474
|
+
profileEnd('cg.findMouthSeeds');
|
|
23475
|
+
if (seeds.length === 0) return null;
|
|
23476
|
+
|
|
23477
|
+
var arcsById = indexArcRecords(records);
|
|
23478
|
+
var footpoints = new ArcFootpoints(arcs, spherical);
|
|
23479
|
+
profileStart('cg.growSeams');
|
|
23480
|
+
var seams = growSeamsFromSeeds(seeds, arcsById, footpoints, distance, spherical,
|
|
23481
|
+
minSeamLength);
|
|
23482
|
+
profileEnd('cg.growSeams');
|
|
23483
|
+
if (seams.length === 0) return null;
|
|
23484
|
+
return {seams: seams, seeds: seeds, arcsById: arcsById, footpoints: footpoints};
|
|
23485
|
+
}
|
|
23486
|
+
|
|
23487
|
+
// Same segment median as interior gap-width=auto (all polygon-ring segments).
|
|
23488
|
+
function measureMedianSegment(lyr, arcs) {
|
|
23489
|
+
return profileWrap('cg.medianSegment', function() {
|
|
23490
|
+
return getMedianPolygonSegmentLength(lyr, arcs);
|
|
23491
|
+
});
|
|
23492
|
+
}
|
|
23493
|
+
|
|
23494
|
+
function rebuildTopology(dataset) {
|
|
23495
|
+
profileStart('cg.rebuildTopology');
|
|
23496
|
+
dataset.arcs.dedupCoords();
|
|
23497
|
+
buildTopology(dataset);
|
|
23498
|
+
profileEnd('cg.rebuildTopology');
|
|
23499
|
+
}
|
|
23500
|
+
|
|
23501
|
+
// One crack presents many seams: a seam is a stretch of two facing arcs, and
|
|
23502
|
+
// every node along either side ends one arc and starts another -- where a third
|
|
23503
|
+
// feature meets the boundary, or where an intersection cut fell. Reporting seams
|
|
23504
|
+
// would give a number several times the number of cracks there are to see, so
|
|
23505
|
+
// seams meeting along a shared arc count once.
|
|
23506
|
+
//
|
|
23507
|
+
// Two cracks that face the same arc at different stretches of it are counted as
|
|
23508
|
+
// one. Erring towards the smaller number suits a count of gaps closed better than
|
|
23509
|
+
// splitting one crack into the arcs it happens to be divided into.
|
|
23510
|
+
function countConnectedGaps(seams) {
|
|
23511
|
+
var parents = new Map();
|
|
23512
|
+
|
|
23513
|
+
function find(id) {
|
|
23514
|
+
var parent = parents.get(id);
|
|
23515
|
+
if (parent === undefined) {
|
|
23516
|
+
parents.set(id, id);
|
|
23517
|
+
return id;
|
|
23518
|
+
}
|
|
23519
|
+
while (parent !== id) {
|
|
23520
|
+
id = parent;
|
|
23521
|
+
parent = parents.get(id);
|
|
23522
|
+
}
|
|
23523
|
+
return id;
|
|
23524
|
+
}
|
|
23525
|
+
|
|
23526
|
+
seams.forEach(function(seam) {
|
|
23527
|
+
var a = find(seam.arcA);
|
|
23528
|
+
var b = find(seam.arcB);
|
|
23529
|
+
if (a !== b) parents.set(a, b);
|
|
23530
|
+
});
|
|
23531
|
+
var roots = new Set();
|
|
23532
|
+
seams.forEach(function(seam) { roots.add(find(seam.arcA)); });
|
|
23533
|
+
return roots.size;
|
|
23534
|
+
}
|
|
23535
|
+
|
|
23536
|
+
// Closing distance for the default pass, which collapses duplicate boundaries
|
|
23537
|
+
// and nothing else. @bounds is the arc bounding box in coordinate units; the seam
|
|
23538
|
+
// walk measures distances in meters when those units are degrees.
|
|
23539
|
+
function getPrecisionSeamDistance(bounds, spherical) {
|
|
23540
|
+
var interval = getHighPrecisionSnapInterval(bounds) * PRECISION_SEAM_INTERVALS;
|
|
23541
|
+
return spherical ? degreesToMeters(interval) : interval;
|
|
23542
|
+
}
|
|
23543
|
+
|
|
23544
|
+
function resolveCloseDistance(opts, crs, autoDistance) {
|
|
23545
|
+
var arg = opts.gap_width;
|
|
23546
|
+
if (arg == null || arg === 'auto') return autoDistance;
|
|
23547
|
+
if (+arg === 0) return 0;
|
|
23548
|
+
return convertDistanceParam(arg, crs);
|
|
23549
|
+
}
|
|
23550
|
+
|
|
23551
|
+
// An external arc belongs to exactly one feature. Shared arcs are excluded: they
|
|
23552
|
+
// already represent clean topology and must not be moved by this repair.
|
|
23553
|
+
function getArcOwners(shapes, arcCount) {
|
|
23554
|
+
var first = new Int32Array(arcCount);
|
|
23555
|
+
var second = new Int32Array(arcCount);
|
|
23556
|
+
for (var i = 0; i < arcCount; i++) {
|
|
23557
|
+
first[i] = -1;
|
|
23558
|
+
second[i] = -1;
|
|
23559
|
+
}
|
|
23560
|
+
traversePaths(shapes, function(o) {
|
|
23561
|
+
var id = o.arcId < 0 ? ~o.arcId : o.arcId;
|
|
23562
|
+
var owner = o.shapeId;
|
|
23563
|
+
if (first[id] == -1) first[id] = owner;
|
|
23564
|
+
else if (first[id] != owner) second[id] = owner;
|
|
23565
|
+
});
|
|
23566
|
+
return {first: first, second: second};
|
|
23567
|
+
}
|
|
23568
|
+
|
|
23569
|
+
function collectExternalVertices(arcs, owners, keepArc) {
|
|
23570
|
+
var data = arcs.getVertexData();
|
|
23571
|
+
var records = [];
|
|
23572
|
+
var offs = 0;
|
|
23573
|
+
for (var arcId = 0; arcId < data.nn.length; arcId++) {
|
|
23574
|
+
var n = data.nn[arcId];
|
|
23575
|
+
if (owners.first[arcId] >= 0 && owners.second[arcId] == -1 &&
|
|
23576
|
+
(!keepArc || keepArc(arcId))) {
|
|
23577
|
+
for (var k = 0; k < n; k++) {
|
|
23578
|
+
var i = offs + k;
|
|
23579
|
+
records.push({
|
|
23580
|
+
x: data.xx[i],
|
|
23581
|
+
y: data.yy[i],
|
|
23582
|
+
i: i,
|
|
23583
|
+
arc: arcId,
|
|
23584
|
+
k: k,
|
|
23585
|
+
owner: owners.first[arcId]
|
|
23586
|
+
});
|
|
23587
|
+
}
|
|
23588
|
+
}
|
|
23589
|
+
offs += n;
|
|
23590
|
+
}
|
|
23591
|
+
return records;
|
|
23592
|
+
}
|
|
23593
|
+
|
|
23594
|
+
function indexArcRecords(records) {
|
|
23595
|
+
var byArc = {};
|
|
23596
|
+
records.forEach(function(o) {
|
|
23597
|
+
if (!byArc[o.arc]) byArc[o.arc] = [];
|
|
23598
|
+
byArc[o.arc].push(o);
|
|
23599
|
+
});
|
|
23600
|
+
Object.keys(byArc).forEach(function(id) {
|
|
23601
|
+
byArc[id].sort(function(a, b) { return a.k - b.k; });
|
|
23602
|
+
});
|
|
23603
|
+
return byArc;
|
|
23604
|
+
}
|
|
23605
|
+
|
|
23606
|
+
// Mouth seeds: mutual nearest external vertices on different owners/arcs.
|
|
23607
|
+
// A single positive-distance pair is enough to start an edge walk.
|
|
23608
|
+
function findMouthSeeds(records, distance, spherical) {
|
|
23609
|
+
var index = new Flatbush(records.length);
|
|
23610
|
+
records.forEach(function(o) { index.add(o.x, o.y, o.x, o.y); });
|
|
23611
|
+
index.finish();
|
|
23612
|
+
|
|
23613
|
+
var nearest = new Int32Array(records.length);
|
|
23614
|
+
var distances = new Float64Array(records.length);
|
|
23615
|
+
nearest.fill(-1);
|
|
23616
|
+
records.forEach(function(a, ai) {
|
|
23617
|
+
var padY = getCoordinatePad(distance, a.y, spherical, false);
|
|
23618
|
+
var padX = getCoordinatePad(distance, a.y, spherical, true);
|
|
23619
|
+
var ids = index.search(a.x - padX, a.y - padY, a.x + padX, a.y + padY);
|
|
23620
|
+
var best = -1, bestDist = Infinity;
|
|
23621
|
+
ids.forEach(function(bi) {
|
|
23622
|
+
var b = records[bi];
|
|
23623
|
+
if (bi == ai || b.owner == a.owner || b.arc == a.arc) return;
|
|
23624
|
+
var d = spherical ?
|
|
23625
|
+
fastLonLatDistance(a.x, a.y, b.x, b.y) :
|
|
23626
|
+
distance2D$1(a.x, a.y, b.x, b.y);
|
|
23627
|
+
if (d <= distance && d < bestDist) {
|
|
23628
|
+
best = bi;
|
|
23629
|
+
bestDist = d;
|
|
23630
|
+
}
|
|
23631
|
+
});
|
|
23632
|
+
nearest[ai] = best;
|
|
23633
|
+
distances[ai] = bestDist;
|
|
23634
|
+
});
|
|
23635
|
+
|
|
23636
|
+
var seen = {};
|
|
23637
|
+
var seeds = [];
|
|
23638
|
+
records.forEach(function(a, ai) {
|
|
23639
|
+
var bi = nearest[ai];
|
|
23640
|
+
if (bi < 0 || !(distances[ai] > 0)) return;
|
|
23641
|
+
var best = records[bi];
|
|
23642
|
+
// Positive-distance matches must be mutual nearest neighbors.
|
|
23643
|
+
if (nearest[bi] != ai) return;
|
|
23644
|
+
var lo = Math.min(a.i, best.i), hi = Math.max(a.i, best.i);
|
|
23645
|
+
var key = lo + ':' + hi;
|
|
23646
|
+
if (seen[key]) return;
|
|
23647
|
+
seen[key] = true;
|
|
23648
|
+
seeds.push({a: a, b: best, distance: distances[ai]});
|
|
23649
|
+
});
|
|
23650
|
+
return seeds;
|
|
23651
|
+
}
|
|
23652
|
+
|
|
23653
|
+
function getCoordinatePad(distance, lat, spherical, longitude) {
|
|
23654
|
+
if (spherical) {
|
|
23655
|
+
var deg = distance / R$3 * R2D$8;
|
|
23656
|
+
return longitude ? deg / Math.max(Math.cos(lat / R2D$8), 0.01) : deg;
|
|
23657
|
+
}
|
|
23658
|
+
// convertDistanceParam() has already converted physical units into projected
|
|
23659
|
+
// coordinate units (or left unitless values unchanged).
|
|
23660
|
+
return distance;
|
|
23661
|
+
}
|
|
23662
|
+
|
|
23663
|
+
// Grow a seam from each mouth seed by walking both arcs while each vertex stays
|
|
23664
|
+
// within closeDistance of the opposite arc's edges. Accept only seams whose
|
|
23665
|
+
// walked length on both sides meets minSeamLength. Overlapping seeds that grow
|
|
23666
|
+
// into the same arc-pair range are merged.
|
|
23667
|
+
function growSeamsFromSeeds(seeds, arcsById, footpoints, closeDistance,
|
|
23668
|
+
spherical, minSeamLength) {
|
|
23669
|
+
var seams = [];
|
|
23670
|
+
// Seeds crowd along a crack: every facing pair of vertices along it is one, and
|
|
23671
|
+
// each grows into the same stretch of the same two arcs, since what the walk
|
|
23672
|
+
// finds is the whole run of in-tolerance vertices around the seed rather than
|
|
23673
|
+
// anything particular to where it started. Walking that run once per seed
|
|
23674
|
+
// instead of once is what made this repair take minutes on a detailed mosaic,
|
|
23675
|
+
// so a seed already inside a run grown for its own two arcs is passed over.
|
|
23676
|
+
var grownByPair = new Map();
|
|
23677
|
+
seeds.forEach(function(seed) {
|
|
23678
|
+
var key = arcPairKey(seed.a.arc, seed.b.arc);
|
|
23679
|
+
var grown = grownByPair.get(key);
|
|
23680
|
+
if (!grown) {
|
|
23681
|
+
grown = [];
|
|
23682
|
+
grownByPair.set(key, grown);
|
|
23683
|
+
}
|
|
23684
|
+
if (seamContainsSeed(grown, seed)) return;
|
|
23685
|
+
var seam = growSeamFromSeed(seed, arcsById, footpoints, closeDistance,
|
|
23686
|
+
spherical);
|
|
23687
|
+
if (!seam) return;
|
|
23688
|
+
// Runs rejected as too short are recorded too, so that the seeds along them
|
|
23689
|
+
// are not walked again either.
|
|
23690
|
+
grown.push(seam);
|
|
23691
|
+
if (seam.lengthA < minSeamLength || seam.lengthB < minSeamLength) return;
|
|
23692
|
+
seams.push(seam);
|
|
23693
|
+
});
|
|
23694
|
+
return mergeOverlappingSeams(seams);
|
|
23695
|
+
}
|
|
23696
|
+
|
|
23697
|
+
function arcPairKey(arcA, arcB) {
|
|
23698
|
+
return arcA < arcB ? arcA + '|' + arcB : arcB + '|' + arcA;
|
|
23699
|
+
}
|
|
23700
|
+
|
|
23701
|
+
function indexSeamsByArcPair(seams) {
|
|
23702
|
+
var byPair = new Map();
|
|
23703
|
+
seams.forEach(function(seam) {
|
|
23704
|
+
var key = arcPairKey(seam.arcA, seam.arcB);
|
|
23705
|
+
var group = byPair.get(key);
|
|
23706
|
+
if (!group) {
|
|
23707
|
+
group = [];
|
|
23708
|
+
byPair.set(key, group);
|
|
23709
|
+
}
|
|
23710
|
+
group.push(seam);
|
|
23711
|
+
});
|
|
23712
|
+
return byPair;
|
|
23713
|
+
}
|
|
23714
|
+
|
|
23715
|
+
function growSeamFromSeed(seed, arcsById, footpoints, closeDistance, spherical) {
|
|
23716
|
+
var sideA = arcsById[seed.a.arc];
|
|
23717
|
+
var sideB = arcsById[seed.b.arc];
|
|
23718
|
+
if (!sideA || !sideB) return null;
|
|
23719
|
+
|
|
23720
|
+
var rangeA = expandRangeOnArc(sideA, seed.a.k, sideB, footpoints, closeDistance,
|
|
23721
|
+
spherical);
|
|
23722
|
+
var rangeB = expandRangeOnArc(sideB, seed.b.k, sideA, footpoints, closeDistance,
|
|
23723
|
+
spherical);
|
|
23724
|
+
if (!rangeA || !rangeB) return null;
|
|
23725
|
+
|
|
23726
|
+
return {
|
|
23727
|
+
arcA: seed.a.arc,
|
|
23728
|
+
arcB: seed.b.arc,
|
|
23729
|
+
loA: rangeA.lo,
|
|
23730
|
+
hiA: rangeA.hi,
|
|
23731
|
+
loB: rangeB.lo,
|
|
23732
|
+
hiB: rangeB.hi,
|
|
23733
|
+
lengthA: rangeA.length,
|
|
23734
|
+
lengthB: rangeB.length
|
|
23735
|
+
};
|
|
23736
|
+
}
|
|
23737
|
+
|
|
23738
|
+
// Expand along @side from seed index @seedK while each vertex remains within
|
|
23739
|
+
// closeDistance of @other's edges. Returns the inclusive index range and the
|
|
23740
|
+
// path length covered by the accepted stretch.
|
|
23741
|
+
function expandRangeOnArc(side, seedK, other, footpoints, closeDistance, spherical) {
|
|
23742
|
+
if (seedK < 0 || seedK >= side.length) return null;
|
|
23743
|
+
if (!vertexWithinTolerance(side[seedK], other, footpoints, closeDistance)) {
|
|
23744
|
+
return null;
|
|
23745
|
+
}
|
|
23746
|
+
var lo = seedK;
|
|
23747
|
+
var hi = seedK;
|
|
23748
|
+
while (lo > 0 &&
|
|
23749
|
+
vertexWithinTolerance(side[lo - 1], other, footpoints, closeDistance)) {
|
|
23750
|
+
lo--;
|
|
23751
|
+
}
|
|
23752
|
+
while (hi + 1 < side.length &&
|
|
23753
|
+
vertexWithinTolerance(side[hi + 1], other, footpoints, closeDistance)) {
|
|
23754
|
+
hi++;
|
|
23755
|
+
}
|
|
23756
|
+
return {
|
|
23757
|
+
lo: lo,
|
|
23758
|
+
hi: hi,
|
|
23759
|
+
length: pathLengthBetween(side, lo, hi, spherical)
|
|
23760
|
+
};
|
|
23761
|
+
}
|
|
23762
|
+
|
|
23763
|
+
function vertexWithinTolerance(vertex, otherSide, footpoints, closeDistance) {
|
|
23764
|
+
return !!footpoints.closestPoint(vertex.x, vertex.y, otherSide, closeDistance);
|
|
23765
|
+
}
|
|
23766
|
+
|
|
23767
|
+
function pathLengthBetween(side, lo, hi, spherical) {
|
|
23768
|
+
var len = 0;
|
|
23769
|
+
for (var k = lo; k < hi; k++) {
|
|
23770
|
+
var a = side[k], b = side[k + 1];
|
|
23771
|
+
len += spherical ?
|
|
23772
|
+
fastLonLatDistance(a.x, a.y, b.x, b.y) :
|
|
23773
|
+
distance2D$1(a.x, a.y, b.x, b.y);
|
|
23774
|
+
}
|
|
23775
|
+
return len;
|
|
23776
|
+
}
|
|
23777
|
+
|
|
23778
|
+
function mergeOverlappingSeams(seams) {
|
|
23779
|
+
if (seams.length <= 1) return seams;
|
|
23780
|
+
var out = [];
|
|
23781
|
+
// Only seams on the same two arcs can overlap, so each one is compared against
|
|
23782
|
+
// those alone rather than against every seam found so far.
|
|
23783
|
+
var byPair = new Map();
|
|
23784
|
+
seams.forEach(function(seam) {
|
|
23785
|
+
var a = Math.min(seam.arcA, seam.arcB);
|
|
23786
|
+
var b = Math.max(seam.arcA, seam.arcB);
|
|
23787
|
+
var swapped = seam.arcA > seam.arcB;
|
|
23788
|
+
var norm = {
|
|
23789
|
+
arcA: a,
|
|
23790
|
+
arcB: b,
|
|
23791
|
+
loA: swapped ? seam.loB : seam.loA,
|
|
23792
|
+
hiA: swapped ? seam.hiB : seam.hiA,
|
|
23793
|
+
loB: swapped ? seam.loA : seam.loB,
|
|
23794
|
+
hiB: swapped ? seam.hiA : seam.hiB,
|
|
23795
|
+
lengthA: swapped ? seam.lengthB : seam.lengthA,
|
|
23796
|
+
lengthB: swapped ? seam.lengthA : seam.lengthB
|
|
23797
|
+
};
|
|
23798
|
+
var key = arcPairKey(norm.arcA, norm.arcB);
|
|
23799
|
+
var group = byPair.get(key);
|
|
23800
|
+
if (!group) {
|
|
23801
|
+
group = [];
|
|
23802
|
+
byPair.set(key, group);
|
|
23803
|
+
}
|
|
23804
|
+
var existing = group.find(function(o) {
|
|
23805
|
+
return rangesOverlap(o.loA, o.hiA, norm.loA, norm.hiA) &&
|
|
23806
|
+
rangesOverlap(o.loB, o.hiB, norm.loB, norm.hiB);
|
|
23807
|
+
});
|
|
23808
|
+
if (existing) {
|
|
23809
|
+
existing.loA = Math.min(existing.loA, norm.loA);
|
|
23810
|
+
existing.hiA = Math.max(existing.hiA, norm.hiA);
|
|
23811
|
+
existing.loB = Math.min(existing.loB, norm.loB);
|
|
23812
|
+
existing.hiB = Math.max(existing.hiB, norm.hiB);
|
|
23813
|
+
existing.lengthA = Math.max(existing.lengthA, norm.lengthA);
|
|
23814
|
+
existing.lengthB = Math.max(existing.lengthB, norm.lengthB);
|
|
23815
|
+
} else {
|
|
23816
|
+
group.push(norm);
|
|
23817
|
+
out.push(norm);
|
|
23818
|
+
}
|
|
23819
|
+
});
|
|
23820
|
+
return out;
|
|
23821
|
+
}
|
|
23822
|
+
|
|
23823
|
+
function rangesOverlap(lo1, hi1, lo2, hi2) {
|
|
23824
|
+
return lo1 <= hi2 && lo2 <= hi1;
|
|
23825
|
+
}
|
|
23826
|
+
|
|
23827
|
+
// Snap every vertex in an accepted seam range to the midpoint between itself
|
|
23828
|
+
// and its nearest footpoint on the opposite arc. Works with staggered vertices:
|
|
23829
|
+
// the opposite side need not have a coinciding vertex. Mouth seed pairs are
|
|
23830
|
+
// forced to their shared vertex midpoint so the crack tip closes exactly.
|
|
23831
|
+
// Returns the seams that moved coordinates.
|
|
23832
|
+
function snapSeams(seams, seeds, arcsById, arcs, footpoints, closeDistance) {
|
|
23833
|
+
arcs.getVertexData();
|
|
23834
|
+
var targets = new CoordinateTargets();
|
|
23835
|
+
var snapped = [];
|
|
23836
|
+
|
|
23837
|
+
seams.forEach(function(seam) {
|
|
23838
|
+
var sideA = arcsById[seam.arcA];
|
|
23839
|
+
var sideB = arcsById[seam.arcB];
|
|
23840
|
+
if (!sideA || !sideB) return;
|
|
23841
|
+
var before = targets.size();
|
|
23842
|
+
snapSideToOpposite(sideA, seam.loA, seam.hiA, sideB, arcs, footpoints,
|
|
23843
|
+
closeDistance, targets);
|
|
23844
|
+
snapSideToOpposite(sideB, seam.loB, seam.hiB, sideA, arcs, footpoints,
|
|
23845
|
+
closeDistance, targets);
|
|
23846
|
+
if (targets.size() > before) snapped.push(seam);
|
|
23847
|
+
});
|
|
23848
|
+
|
|
23849
|
+
// Mouth pairs win over footpoint midpoints so both tips land on one point.
|
|
23850
|
+
var seamsByPair = indexSeamsByArcPair(seams);
|
|
23851
|
+
seeds.forEach(function(seed) {
|
|
23852
|
+
if (!(seed.distance > 0)) return;
|
|
23853
|
+
var group = seamsByPair.get(arcPairKey(seed.a.arc, seed.b.arc));
|
|
23854
|
+
if (!group || !seamContainsSeed(group, seed)) return;
|
|
23855
|
+
var mid = [(seed.a.x + seed.b.x) / 2, (seed.a.y + seed.b.y) / 2];
|
|
23856
|
+
targets.set(seed.a.x, seed.a.y, mid);
|
|
23857
|
+
targets.set(seed.b.x, seed.b.y, mid);
|
|
23858
|
+
});
|
|
23859
|
+
|
|
23860
|
+
return applyTargets(arcs, targets) ? snapped : [];
|
|
23861
|
+
}
|
|
23862
|
+
|
|
23863
|
+
// Snap the facing pair of vertices at each open end of a seam together, which
|
|
23864
|
+
// closes the crack off there without disturbing the boundary between.
|
|
23865
|
+
//
|
|
23866
|
+
// An end counts as open when the two banks turn away from each other there. Where
|
|
23867
|
+
// the crack carries on instead, into the next pair of arcs, pinching would divide
|
|
23868
|
+
// one gap into a row of them -- each awarded separately, so the boundary would
|
|
23869
|
+
// zigzag from bank to bank along a crack that should have been resolved whole.
|
|
23870
|
+
//
|
|
23871
|
+
// Returns the number of mouths pinched shut.
|
|
23872
|
+
function pinchSeamMouths(seams, arcsById, arcs, nodes, footpoints, distance,
|
|
23873
|
+
spherical) {
|
|
23874
|
+
var data = arcs.getVertexData();
|
|
23875
|
+
var targets = new CoordinateTargets();
|
|
23876
|
+
var pinched = 0;
|
|
23877
|
+
|
|
23878
|
+
seams.forEach(function(seam) {
|
|
23879
|
+
var sideA = arcsById[seam.arcA];
|
|
23880
|
+
var sideB = arcsById[seam.arcB];
|
|
23881
|
+
if (!sideA || !sideB) return;
|
|
23882
|
+
[seam.loA, seam.hiA].forEach(function(kA, i) {
|
|
23883
|
+
var outwardA = i === 0 ? -1 : 1;
|
|
23884
|
+
var a = sideA[kA];
|
|
23885
|
+
// The two runs were grown from the seed independently, so they need not end
|
|
23886
|
+
// opposite each other; the nearest vertex within the facing run is the one
|
|
23887
|
+
// to close against.
|
|
23888
|
+
var kB = nearestVertexInRange(a, sideB, seam.loB, seam.hiB, distance,
|
|
23889
|
+
spherical);
|
|
23890
|
+
if (kB < 0) return;
|
|
23891
|
+
var outwardB = kB - seam.loB < seam.hiB - kB ? -1 : 1;
|
|
23892
|
+
if (!endIsOpen(sideA, seam.arcA, kA, outwardA, sideB, arcs, nodes,
|
|
23893
|
+
footpoints, distance) &&
|
|
23894
|
+
!endIsOpen(sideB, seam.arcB, kB, outwardB, sideA, arcs, nodes,
|
|
23895
|
+
footpoints, distance)) {
|
|
23896
|
+
return;
|
|
23897
|
+
}
|
|
23898
|
+
var b = sideB[kB];
|
|
23899
|
+
var mid = [(a.x + b.x) / 2, (a.y + b.y) / 2];
|
|
23900
|
+
var before = targets.size();
|
|
23901
|
+
targets.set(data.xx[a.i], data.yy[a.i], mid);
|
|
23902
|
+
targets.set(data.xx[b.i], data.yy[b.i], mid);
|
|
23903
|
+
if (targets.size() > before) pinched++;
|
|
23904
|
+
});
|
|
23905
|
+
});
|
|
23906
|
+
|
|
23907
|
+
return applyTargets(arcs, targets) ? pinched : 0;
|
|
23908
|
+
}
|
|
23909
|
+
|
|
23910
|
+
// True if the boundary has left the facing arc behind by the vertex just past
|
|
23911
|
+
// @k, so that the crack ends there.
|
|
23912
|
+
//
|
|
23913
|
+
// Where the arc itself ends at @k, the question is what carries on past the node:
|
|
23914
|
+
// an arc continuing along the crack keeps its next vertex within tolerance of the
|
|
23915
|
+
// facing bank, whereas one turning away from the crack -- the coverage boundary at
|
|
23916
|
+
// the crack's mouth, say -- does not. A crack whose banks are split into several
|
|
23917
|
+
// arcs is walked as several seams, and this is what keeps their inner ends from
|
|
23918
|
+
// being taken for mouths.
|
|
23919
|
+
function endIsOpen(side, arcId, k, outward, other, arcs, nodes, footpoints,
|
|
23920
|
+
distance) {
|
|
23921
|
+
var next = k + outward;
|
|
23922
|
+
if (next >= 0 && next < side.length) {
|
|
23923
|
+
return !footpoints.closestPoint(side[next].x, side[next].y, other, distance);
|
|
23924
|
+
}
|
|
23925
|
+
var entering = outward > 0 ? arcId : ~arcId;
|
|
23926
|
+
var connected = nodes.getConnectedArcs(entering);
|
|
23927
|
+
for (var i = 0; i < connected.length; i++) {
|
|
23928
|
+
// Connected ids lead into the node, so the vertex adjacent to it is the last
|
|
23929
|
+
// but one along each.
|
|
23930
|
+
var v = arcs.getVertex(connected[i], -2);
|
|
23931
|
+
if (footpoints.closestPoint(v.x, v.y, other, distance)) return false;
|
|
23932
|
+
}
|
|
23933
|
+
return true;
|
|
23934
|
+
}
|
|
23935
|
+
|
|
23936
|
+
// Index of the vertex in @side[lo..hi] nearest @vertex, or -1 if the nearest is
|
|
23937
|
+
// further off than @maxDist or already coincides with it. A coinciding pair marks
|
|
23938
|
+
// a tip where the two banks already meet, which needs no pinching.
|
|
23939
|
+
function nearestVertexInRange(vertex, side, lo, hi, maxDist, spherical) {
|
|
23940
|
+
var best = -1;
|
|
23941
|
+
var bestDist = Infinity;
|
|
23942
|
+
for (var k = lo; k <= hi; k++) {
|
|
23943
|
+
var d = spherical ?
|
|
23944
|
+
fastLonLatDistance(vertex.x, vertex.y, side[k].x, side[k].y) :
|
|
23945
|
+
distance2D$1(vertex.x, vertex.y, side[k].x, side[k].y);
|
|
23946
|
+
if (d < bestDist) {
|
|
23947
|
+
bestDist = d;
|
|
23948
|
+
best = k;
|
|
23949
|
+
}
|
|
23950
|
+
}
|
|
23951
|
+
if (best < 0 || !(bestDist > 0) || bestDist > maxDist) return -1;
|
|
23952
|
+
return best;
|
|
23953
|
+
}
|
|
23954
|
+
|
|
23955
|
+
// Move every occurrence of a selected coordinate, including coincident arc
|
|
23956
|
+
// endpoints, so existing nodes remain connected. Coordinates that were not
|
|
23957
|
+
// selected are untouched. Returns false if there was nothing to move.
|
|
23958
|
+
function applyTargets(arcs, targets) {
|
|
23959
|
+
if (targets.size() === 0) return false;
|
|
23960
|
+
var data = arcs.getVertexData();
|
|
23961
|
+
for (var i = 0; i < data.xx.length; i++) {
|
|
23962
|
+
var target = targets.get(data.xx[i], data.yy[i]);
|
|
23963
|
+
if (target) {
|
|
23964
|
+
data.xx[i] = target[0];
|
|
23965
|
+
data.yy[i] = target[1];
|
|
23966
|
+
}
|
|
23967
|
+
}
|
|
23968
|
+
arcs.updateVertexData(data.nn, data.xx, data.yy, data.zz);
|
|
23969
|
+
return true;
|
|
23970
|
+
}
|
|
23971
|
+
|
|
23972
|
+
// Replacement coordinates, looked up by exact x/y. Keyed by x in a Map, with a
|
|
23973
|
+
// short bucket per x, because the lookup runs once per vertex in the whole arc
|
|
23974
|
+
// table: building a string key for each of those is slower than the seam
|
|
23975
|
+
// detection that precedes it.
|
|
23976
|
+
function CoordinateTargets() {
|
|
23977
|
+
var byX = new Map();
|
|
23978
|
+
var count = 0;
|
|
23979
|
+
|
|
23980
|
+
this.set = function(x, y, target) {
|
|
23981
|
+
var bucket = byX.get(x);
|
|
23982
|
+
if (!bucket) {
|
|
23983
|
+
bucket = [];
|
|
23984
|
+
byX.set(x, bucket);
|
|
23985
|
+
}
|
|
23986
|
+
for (var i = 0; i < bucket.length; i++) {
|
|
23987
|
+
if (bucket[i].y === y) {
|
|
23988
|
+
bucket[i].target = target;
|
|
23989
|
+
return;
|
|
23990
|
+
}
|
|
23991
|
+
}
|
|
23992
|
+
bucket.push({y: y, target: target});
|
|
23993
|
+
count++;
|
|
23994
|
+
};
|
|
23995
|
+
|
|
23996
|
+
this.get = function(x, y) {
|
|
23997
|
+
var bucket = byX.get(x);
|
|
23998
|
+
if (!bucket) return null;
|
|
23999
|
+
for (var i = 0; i < bucket.length; i++) {
|
|
24000
|
+
if (bucket[i].y === y) return bucket[i].target;
|
|
24001
|
+
}
|
|
24002
|
+
return null;
|
|
24003
|
+
};
|
|
24004
|
+
|
|
24005
|
+
this.size = function() { return count; };
|
|
24006
|
+
}
|
|
24007
|
+
|
|
24008
|
+
function seamContainsSeed(seams, seed) {
|
|
24009
|
+
return seams.some(function(seam) {
|
|
24010
|
+
var aOnA = seed.a.arc === seam.arcA &&
|
|
24011
|
+
seed.a.k >= seam.loA && seed.a.k <= seam.hiA;
|
|
24012
|
+
var aOnB = seed.a.arc === seam.arcB &&
|
|
24013
|
+
seed.a.k >= seam.loB && seed.a.k <= seam.hiB;
|
|
24014
|
+
var bOnA = seed.b.arc === seam.arcA &&
|
|
24015
|
+
seed.b.k >= seam.loA && seed.b.k <= seam.hiA;
|
|
24016
|
+
var bOnB = seed.b.arc === seam.arcB &&
|
|
24017
|
+
seed.b.k >= seam.loB && seed.b.k <= seam.hiB;
|
|
24018
|
+
return (aOnA && bOnB) || (aOnB && bOnA);
|
|
24019
|
+
});
|
|
24020
|
+
}
|
|
24021
|
+
|
|
24022
|
+
function snapSideToOpposite(side, lo, hi, other, arcs, footpoints, closeDistance,
|
|
24023
|
+
targets) {
|
|
24024
|
+
var data = arcs.getVertexData();
|
|
24025
|
+
for (var k = lo; k <= hi; k++) {
|
|
24026
|
+
var v = side[k];
|
|
24027
|
+
var foot = footpoints.closestPoint(v.x, v.y, other, closeDistance);
|
|
24028
|
+
if (!foot || !(foot.distance > 0)) continue;
|
|
24029
|
+
var mid = [(v.x + foot.x) / 2, (v.y + foot.y) / 2];
|
|
24030
|
+
targets.set(data.xx[v.i], data.yy[v.i], mid);
|
|
24031
|
+
}
|
|
24032
|
+
}
|
|
24033
|
+
|
|
24034
|
+
// Nearest-point queries against an arc, indexing each arc once rather than
|
|
24035
|
+
// scanning it per query.
|
|
24036
|
+
//
|
|
24037
|
+
// Growing a seam tests every vertex along one arc against the whole of the
|
|
24038
|
+
// facing arc, and snapping repeats that for the vertices it accepted, so a scan
|
|
24039
|
+
// per query costs the product of the two arcs' lengths. That is unnoticeable
|
|
24040
|
+
// where a mosaic's arcs run between neighboring junctions a few vertices apart,
|
|
24041
|
+
// but a detailed boundary is one arc of thousands of vertices, and a mosaic of
|
|
24042
|
+
// those presents thousands of seams to walk: enough for the same repair to run
|
|
24043
|
+
// for many minutes.
|
|
24044
|
+
//
|
|
24045
|
+
// The index is built from live coordinates, and seam snapping moves coordinates
|
|
24046
|
+
// only once every seam has been measured, so one instance serves a whole pass
|
|
24047
|
+
// over a layer and must not outlive it.
|
|
24048
|
+
function ArcFootpoints(arcs, spherical) {
|
|
24049
|
+
var data = arcs.getVertexData();
|
|
24050
|
+
var indexes = new Map();
|
|
24051
|
+
|
|
24052
|
+
function getIndex(side) {
|
|
24053
|
+
if (!indexes.has(side)) {
|
|
24054
|
+
indexes.set(side, side.length > 1 ? buildIndex(side) : null);
|
|
24055
|
+
}
|
|
24056
|
+
return indexes.get(side);
|
|
24057
|
+
}
|
|
24058
|
+
|
|
24059
|
+
function buildIndex(side) {
|
|
24060
|
+
var index = new Flatbush(side.length - 1);
|
|
24061
|
+
for (var i = 0; i + 1 < side.length; i++) {
|
|
24062
|
+
var a = side[i].i, b = side[i + 1].i;
|
|
24063
|
+
index.add(
|
|
24064
|
+
Math.min(data.xx[a], data.xx[b]), Math.min(data.yy[a], data.yy[b]),
|
|
24065
|
+
Math.max(data.xx[a], data.xx[b]), Math.max(data.yy[a], data.yy[b]));
|
|
24066
|
+
}
|
|
24067
|
+
index.finish();
|
|
24068
|
+
return index;
|
|
24069
|
+
}
|
|
24070
|
+
|
|
24071
|
+
// The nearest point on @side within @maxDist of (x, y), with that distance, or
|
|
24072
|
+
// null if the arc stays clear of it. A segment within @maxDist has its nearest
|
|
24073
|
+
// point inside the query box, so its own bounding box meets that box: no
|
|
24074
|
+
// segment near enough is missed. Nothing beyond @maxDist is reported, which the
|
|
24075
|
+
// callers discarded anyway.
|
|
24076
|
+
this.closestPoint = function(x, y, side, maxDist) {
|
|
24077
|
+
var index = getIndex(side);
|
|
24078
|
+
if (!index) return null;
|
|
24079
|
+
var padX = getCoordinatePad(maxDist, y, spherical, true);
|
|
24080
|
+
var padY = getCoordinatePad(maxDist, y, spherical, false);
|
|
24081
|
+
var ids = index.search(x - padX, y - padY, x + padX, y + padY);
|
|
24082
|
+
var bestD2 = Infinity, bestX = 0, bestY = 0;
|
|
24083
|
+
for (var k = 0; k < ids.length; k++) {
|
|
24084
|
+
var i = ids[k];
|
|
24085
|
+
var ax = data.xx[side[i].i], ay = data.yy[side[i].i];
|
|
24086
|
+
var dx = data.xx[side[i + 1].i] - ax, dy = data.yy[side[i + 1].i] - ay;
|
|
24087
|
+
var len2 = dx * dx + dy * dy;
|
|
24088
|
+
var t = len2 > 0 ? ((x - ax) * dx + (y - ay) * dy) / len2 : 0;
|
|
24089
|
+
if (t < 0) t = 0;
|
|
24090
|
+
if (t > 1) t = 1;
|
|
24091
|
+
var fx = ax + t * dx, fy = ay + t * dy;
|
|
24092
|
+
var d2 = (x - fx) * (x - fx) + (y - fy) * (y - fy);
|
|
24093
|
+
if (d2 < bestD2) {
|
|
24094
|
+
bestD2 = d2;
|
|
24095
|
+
bestX = fx;
|
|
24096
|
+
bestY = fy;
|
|
24097
|
+
}
|
|
24098
|
+
}
|
|
24099
|
+
if (bestD2 === Infinity) return null;
|
|
24100
|
+
var distance = spherical ?
|
|
24101
|
+
fastLonLatDistance(x, y, bestX, bestY) :
|
|
24102
|
+
distance2D$1(x, y, bestX, bestY);
|
|
24103
|
+
if (!isFinite(distance) || distance > maxDist) return null;
|
|
24104
|
+
return {x: bestX, y: bestY, distance: distance};
|
|
24105
|
+
};
|
|
24106
|
+
}
|
|
24107
|
+
|
|
24108
|
+
var MIN_GAP_OWNERS = 3;
|
|
24109
|
+
var WIDTH_FACTOR = 4;
|
|
24110
|
+
|
|
24111
|
+
|
|
24112
|
+
// Split enclosed sliver gaps that border three or more features along the lines
|
|
24113
|
+
// midway between their neighbors' boundaries. The existing mosaic assignment can
|
|
24114
|
+
// then distribute the resulting pieces among those features instead of handing
|
|
24115
|
+
// the entire long gap to one owner.
|
|
24116
|
+
//
|
|
24117
|
+
// Only the boundaries of the gaps being repaired are examined, so the cost
|
|
24118
|
+
// scales with those gaps rather than with the full mosaic.
|
|
24119
|
+
// Returns a layer of cut lines added to the dataset.
|
|
24120
|
+
function partitionPolygonMosaicGaps(lyr, dataset, nodes, opts) {
|
|
24121
|
+
if (!lyr.shapes || lyr.shapes.length < MIN_GAP_OWNERS) return false;
|
|
24122
|
+
var mosaicIndex = new MosaicIndex(lyr, nodes, {flat: true});
|
|
24123
|
+
var sliverOpts = utils.extend({}, opts);
|
|
24124
|
+
if (sliverOpts.gap_width == null) {
|
|
24125
|
+
if (sliverOpts.gap_fill_area != null || sliverOpts.min_gap_area != null ||
|
|
24126
|
+
sliverOpts.min_area != null || sliverOpts.sliver_control != null) {
|
|
24127
|
+
if (sliverOpts.gap_fill_area == null && sliverOpts.min_gap_area == null &&
|
|
24128
|
+
sliverOpts.min_area == null) {
|
|
24129
|
+
sliverOpts.gap_fill_area = 'auto';
|
|
24130
|
+
}
|
|
24131
|
+
} else {
|
|
24132
|
+
sliverOpts.gap_width = 'auto';
|
|
24133
|
+
}
|
|
24134
|
+
}
|
|
24135
|
+
var filter = getSliverFilter(lyr, dataset, sliverOpts).filter;
|
|
24136
|
+
var gaps = mosaicIndex.getUnusedTileData(filter).filter(function(gap) {
|
|
24137
|
+
return countOwners(gap.boundary) >= MIN_GAP_OWNERS;
|
|
24138
|
+
});
|
|
24139
|
+
if (gaps.length === 0) return false;
|
|
24140
|
+
|
|
24141
|
+
var minWidth = getMinDivisibleGapWidth(lyr, nodes.arcs);
|
|
24142
|
+
var cuts = [];
|
|
24143
|
+
var partitioned = 0;
|
|
24144
|
+
gaps.forEach(function(gap) {
|
|
24145
|
+
var result = buildGapPartitionCuts(gap, nodes.arcs, minWidth);
|
|
24146
|
+
if (!result) return;
|
|
24147
|
+
var before = cuts.length;
|
|
24148
|
+
result.lines.forEach(function(line) {
|
|
24149
|
+
if (line.length < 2) return;
|
|
24150
|
+
cuts.push(line);
|
|
24151
|
+
});
|
|
24152
|
+
if (cuts.length > before) partitioned++;
|
|
24153
|
+
});
|
|
24154
|
+
if (cuts.length === 0) return false;
|
|
24155
|
+
|
|
24156
|
+
// Both mutations below have to be announced before either happens: undo
|
|
24157
|
+
// captures the dataset's arc collection and layer list together, and it needs
|
|
24158
|
+
// the originals -- the collection this replaces, and a layer list without the
|
|
24159
|
+
// temporary layer added here. Restoring the arc coordinates alone would leave
|
|
24160
|
+
// the dataset pointing at the merged collection, with layer shapes referring to
|
|
24161
|
+
// arc ids from the original one.
|
|
24162
|
+
noteDatasetWillChange(dataset, {operation: 'partitionGaps', unit: 'arcs'});
|
|
24163
|
+
var firstCutArc = dataset.arcs.size();
|
|
24164
|
+
dataset.arcs = mergeArcs([dataset.arcs, new ArcCollection(cuts)]);
|
|
24165
|
+
// Keep cut arcs in a temporary line layer while addIntersectionCuts() nodes
|
|
24166
|
+
// them against the gap boundary. The caller removes this layer before polygon
|
|
24167
|
+
// cleaning and passes its updated arc ids to the mosaic node filter.
|
|
24168
|
+
var cutLayer = {
|
|
24169
|
+
geometry_type: 'polyline',
|
|
24170
|
+
shapes: cuts.map(function(line, i) {
|
|
24171
|
+
return [[firstCutArc + i]];
|
|
24172
|
+
})
|
|
24173
|
+
};
|
|
24174
|
+
dataset.layers.push(cutLayer);
|
|
24175
|
+
markDatasetChanged(dataset, {operation: 'partitionGaps', unit: 'arcs'});
|
|
24176
|
+
verbose(utils.format('Partitioned %s multi-feature interior gap%s',
|
|
24177
|
+
partitioned, utils.pluralSuffix(partitioned)));
|
|
24178
|
+
return cutLayer;
|
|
24179
|
+
}
|
|
24180
|
+
|
|
24181
|
+
function countOwners(boundary) {
|
|
24182
|
+
var owners = {};
|
|
24183
|
+
boundary.forEach(function(o) { owners[o.shapeId] = true; });
|
|
24184
|
+
return Object.keys(owners).length;
|
|
24185
|
+
}
|
|
24186
|
+
|
|
24187
|
+
// Divide a gap along the centerlines between the boundary runs that face each
|
|
24188
|
+
// other across it.
|
|
24189
|
+
//
|
|
24190
|
+
// The gap's boundary is a cyclic sequence of arcs, each owned by one adjacent
|
|
24191
|
+
// feature, so it decomposes into one run per owner. The dividing line between
|
|
24192
|
+
// two runs is the locus of points equidistant from both; taking the midpoint
|
|
24193
|
+
// between each run's vertices and its footpoint on the other run gives a line
|
|
24194
|
+
// that bends wherever either bank bends, and between two consecutive bends both
|
|
24195
|
+
// banks are straight, where the exact bisector is straight as well.
|
|
24196
|
+
//
|
|
24197
|
+
// Cost and output size track the boundary's own vertex count, so a gap that is
|
|
24198
|
+
// long and hairline costs no more than a compact one. (A sampled Voronoi medial
|
|
24199
|
+
// axis cannot make that promise: its circumcenters are only meaningful when the
|
|
24200
|
+
// sample spacing is finer than the channel is wide, so the work it takes grows
|
|
24201
|
+
// with the gap's length-to-width ratio.)
|
|
24202
|
+
function buildGapPartitionCuts(gap, arcs, minWidth) {
|
|
24203
|
+
var runs = getOwnerRuns(gap.boundary);
|
|
24204
|
+
if (runs.length < MIN_GAP_OWNERS) return null;
|
|
24205
|
+
var ring = gap.tile[0];
|
|
24206
|
+
// Both measures must be planar even when the source CRS is geographic: the
|
|
24207
|
+
// cuts are built in source-coordinate space.
|
|
24208
|
+
var area = Math.abs(geom.getPlanarPathArea(ring, arcs));
|
|
24209
|
+
var perimeter = geom.getPlanarPathPerimeter(ring, arcs);
|
|
24210
|
+
if (!(area > 0) || !(perimeter > 0)) return null;
|
|
24211
|
+
// For a long narrow polygon, 2A/P approximates its width.
|
|
24212
|
+
var gapWidth = area / perimeter * 2;
|
|
24213
|
+
if (gapWidth < minWidth) return null;
|
|
24214
|
+
// A modest multiplier gives facing banks enough reach where the gap is locally
|
|
24215
|
+
// wider, e.g. at a junction.
|
|
24216
|
+
var width = gapWidth * WIDTH_FACTOR;
|
|
24217
|
+
var ringPoints = getPathPoints(ring, arcs);
|
|
24218
|
+
var ringIndex = new SegmentIndex(ringPoints);
|
|
24219
|
+
var banks = runs.map(function(run) {
|
|
24220
|
+
return makeBank(getRunPoints(run.arcIds, arcs));
|
|
24221
|
+
});
|
|
24222
|
+
var lines = [];
|
|
24223
|
+
for (var i = 0; i < banks.length; i++) {
|
|
24224
|
+
for (var j = i + 1; j < banks.length; j++) {
|
|
24225
|
+
var line = buildCenterlineBetweenBanks(banks[i], banks[j], width, ringIndex,
|
|
24226
|
+
collectEndAnchors(banks, i, j, width));
|
|
24227
|
+
if (line.length >= 2) lines.push(line);
|
|
24228
|
+
}
|
|
24229
|
+
}
|
|
24230
|
+
if (lines.length === 0) return null;
|
|
24231
|
+
return {lines: anchorCutLines(lines, width, ringPoints)};
|
|
24232
|
+
}
|
|
24233
|
+
|
|
24234
|
+
function makeBank(points) {
|
|
24235
|
+
return {
|
|
24236
|
+
points: points,
|
|
24237
|
+
index: points.length > 1 ? new SegmentIndex(points) : null
|
|
24238
|
+
};
|
|
24239
|
+
}
|
|
24240
|
+
|
|
24241
|
+
// Make a set of centerlines divide the gap: every line has to reach the gap's
|
|
24242
|
+
// boundary at each end, either directly or through a shared junction with other
|
|
24243
|
+
// lines, or the mosaic treats it as a dangling arc and ignores it.
|
|
24244
|
+
//
|
|
24245
|
+
// A line ending on a vertex of the boundary is already joined to it, that vertex
|
|
24246
|
+
// being a node of the mosaic. Anywhere else, including a point part way along a
|
|
24247
|
+
// boundary segment, the line has to be projected past the boundary to cross it:
|
|
24248
|
+
// starting an extension on the boundary rather than crossing it creates no node
|
|
24249
|
+
// at all, and the cut is ignored.
|
|
24250
|
+
function anchorCutLines(lines, width, ringPoints) {
|
|
24251
|
+
var ringVertices = new Set(ringPoints.map(function(p) {
|
|
24252
|
+
return p[0] + '|' + p[1];
|
|
24253
|
+
}));
|
|
24254
|
+
var ends = [];
|
|
24255
|
+
lines.forEach(function(line, lineId) {
|
|
24256
|
+
[line[0], line[line.length - 1]].forEach(function(point, side) {
|
|
24257
|
+
ends.push({
|
|
24258
|
+
lineId: lineId,
|
|
24259
|
+
side: side,
|
|
24260
|
+
point: point,
|
|
24261
|
+
onBoundary: ringVertices.has(point[0] + '|' + point[1])
|
|
24262
|
+
});
|
|
24263
|
+
});
|
|
24264
|
+
});
|
|
24265
|
+
var connectors = [];
|
|
24266
|
+
var joined = new Uint8Array(ends.length);
|
|
24267
|
+
clusterCutEnds(ends, width * 3).forEach(function(cluster) {
|
|
24268
|
+
var hub = [0, 0];
|
|
24269
|
+
cluster.forEach(function(i) {
|
|
24270
|
+
hub[0] += ends[i].point[0];
|
|
24271
|
+
hub[1] += ends[i].point[1];
|
|
24272
|
+
});
|
|
24273
|
+
hub[0] /= cluster.length;
|
|
24274
|
+
hub[1] /= cluster.length;
|
|
24275
|
+
cluster.forEach(function(i) {
|
|
24276
|
+
joined[i] = 1;
|
|
24277
|
+
connectors.push([ends[i].point, hub]);
|
|
24278
|
+
});
|
|
24279
|
+
});
|
|
24280
|
+
var anchored = lines.map(function(line, lineId) {
|
|
24281
|
+
var out = line.concat();
|
|
24282
|
+
ends.forEach(function(end, i) {
|
|
24283
|
+
if (end.lineId !== lineId || joined[i] || end.onBoundary) return;
|
|
24284
|
+
var inward = end.side ? out[out.length - 2] : out[1];
|
|
24285
|
+
var beyond = projectPast$1(end.point, inward, width);
|
|
24286
|
+
if (!beyond) return;
|
|
24287
|
+
if (end.side) out.push(beyond);
|
|
24288
|
+
else out.unshift(beyond);
|
|
24289
|
+
});
|
|
24290
|
+
return out;
|
|
24291
|
+
});
|
|
24292
|
+
return anchored.concat(connectors);
|
|
24293
|
+
}
|
|
24294
|
+
|
|
24295
|
+
// Group line ends that meet in the interior of the gap, where three or more
|
|
24296
|
+
// centerlines converge on the point equidistant from three boundary runs. Ends
|
|
24297
|
+
// already on the boundary are not candidates, and neither is a pair of ends
|
|
24298
|
+
// from the same line.
|
|
24299
|
+
function clusterCutEnds(ends, maxDist) {
|
|
24300
|
+
var used = new Uint8Array(ends.length);
|
|
24301
|
+
var clusters = [];
|
|
24302
|
+
ends.forEach(function(end, i) {
|
|
24303
|
+
if (used[i] || end.onBoundary) return;
|
|
24304
|
+
var cluster = [];
|
|
24305
|
+
var stack = [i];
|
|
24306
|
+
used[i] = 1;
|
|
24307
|
+
while (stack.length) {
|
|
24308
|
+
var id = stack.pop();
|
|
24309
|
+
cluster.push(id);
|
|
24310
|
+
ends.forEach(function(other, j) {
|
|
24311
|
+
if (used[j] || other.onBoundary || other.lineId === ends[id].lineId) return;
|
|
24312
|
+
if (geom.distance2D(ends[id].point[0], ends[id].point[1],
|
|
24313
|
+
other.point[0], other.point[1]) <= maxDist) {
|
|
24314
|
+
used[j] = 1;
|
|
24315
|
+
stack.push(j);
|
|
24316
|
+
}
|
|
24317
|
+
});
|
|
24318
|
+
}
|
|
24319
|
+
var lineIds = {};
|
|
24320
|
+
cluster.forEach(function(id) { lineIds[ends[id].lineId] = true; });
|
|
24321
|
+
if (Object.keys(lineIds).length >= MIN_GAP_OWNERS) clusters.push(cluster);
|
|
24322
|
+
});
|
|
24323
|
+
return clusters;
|
|
24324
|
+
}
|
|
24325
|
+
|
|
24326
|
+
// One run per owner, in ring order. The ring is cyclic, so a run split across
|
|
24327
|
+
// the array ends is rejoined.
|
|
24328
|
+
function getOwnerRuns(boundary) {
|
|
24329
|
+
var runs = [];
|
|
24330
|
+
boundary.forEach(function(o) {
|
|
24331
|
+
var last = runs[runs.length - 1];
|
|
24332
|
+
if (last && last.shapeId === o.shapeId) {
|
|
24333
|
+
last.arcIds.push(o.arcId);
|
|
24334
|
+
} else {
|
|
24335
|
+
runs.push({shapeId: o.shapeId, arcIds: [o.arcId]});
|
|
24336
|
+
}
|
|
24337
|
+
});
|
|
24338
|
+
if (runs.length > 2 && runs[0].shapeId === runs[runs.length - 1].shapeId) {
|
|
24339
|
+
runs[0].arcIds = runs.pop().arcIds.concat(runs[0].arcIds);
|
|
24340
|
+
}
|
|
24341
|
+
return runs;
|
|
24342
|
+
}
|
|
24343
|
+
|
|
24344
|
+
function getPathPoints(ids, arcs) {
|
|
24345
|
+
var points = [];
|
|
24346
|
+
var iter = arcs.getShapeIter(ids);
|
|
24347
|
+
while (iter.hasNext()) {
|
|
24348
|
+
points.push([iter.x, iter.y]);
|
|
24349
|
+
}
|
|
24350
|
+
return points;
|
|
24351
|
+
}
|
|
24352
|
+
|
|
24353
|
+
function getRunPoints(arcIds, arcs) {
|
|
24354
|
+
var points = [];
|
|
24355
|
+
arcIds.forEach(function(arcId) {
|
|
24356
|
+
var iter = arcs.getArcIter(arcId);
|
|
24357
|
+
while (iter.hasNext()) {
|
|
24358
|
+
var last = points[points.length - 1];
|
|
24359
|
+
// consecutive arcs share their joining vertex
|
|
24360
|
+
if (!last || last[0] !== iter.x || last[1] !== iter.y) {
|
|
24361
|
+
points.push([iter.x, iter.y]);
|
|
24362
|
+
}
|
|
24363
|
+
}
|
|
24364
|
+
});
|
|
24365
|
+
return points;
|
|
24366
|
+
}
|
|
24367
|
+
|
|
24368
|
+
function buildCenterlineBetweenBanks(bankA, bankB, maxDist, ringIndex, anchors) {
|
|
24369
|
+
var events = [];
|
|
24370
|
+
// Each vertex contributes a midpoint, positioned along bank A so that
|
|
24371
|
+
// contributions from both banks interleave in one order.
|
|
24372
|
+
bankA.points.forEach(function(p, i) {
|
|
24373
|
+
var mid = crossGapMidpoint(p, bankB, maxDist, ringIndex);
|
|
24374
|
+
if (mid) events.push({pos: i, point: mid.point});
|
|
24375
|
+
});
|
|
24376
|
+
bankB.points.forEach(function(p, i) {
|
|
24377
|
+
var mid = crossGapMidpoint(p, bankA, maxDist, ringIndex);
|
|
24378
|
+
if (mid) events.push({pos: mid.pos, point: mid.point});
|
|
24379
|
+
});
|
|
24380
|
+
anchors.forEach(function(event) { events.push(event); });
|
|
24381
|
+
events.sort(function(a, b) { return a.pos - b.pos; });
|
|
24382
|
+
var points = [];
|
|
24383
|
+
events.forEach(function(o) {
|
|
24384
|
+
var last = points[points.length - 1];
|
|
24385
|
+
if (!last || last[0] !== o.point[0] || last[1] !== o.point[1]) {
|
|
24386
|
+
points.push(o.point);
|
|
24387
|
+
}
|
|
24388
|
+
});
|
|
24389
|
+
return points;
|
|
24390
|
+
}
|
|
24391
|
+
|
|
24392
|
+
// Where the centerline between two boundary runs meets the ends of the gap.
|
|
24393
|
+
// Ordered outside bank A's own vertex positions, so they bracket the midpoints
|
|
24394
|
+
// between the two banks.
|
|
24395
|
+
//
|
|
24396
|
+
// Midpoints alone do not reach the ends: approaching one, a vertex's nearest point
|
|
24397
|
+
// on the facing bank swings round towards the end itself, and the midpoints stop
|
|
24398
|
+
// describing the middle of the gap. Taking the ends from the boundary is both
|
|
24399
|
+
// exact and what makes the cut divide the gap, since a cut has to reach the
|
|
24400
|
+
// boundary to have any effect.
|
|
24401
|
+
function collectEndAnchors(banks, i, j, maxDist) {
|
|
24402
|
+
var points = banks[i].points;
|
|
24403
|
+
var out = [];
|
|
24404
|
+
// walking the boundary each way round from bank A: A's last vertex leads
|
|
24405
|
+
// forwards, its first vertex backwards
|
|
24406
|
+
[[j, points.length], [i, -1]].forEach(function(o) {
|
|
24407
|
+
var between = runsBetween(banks, o[0] === j ? i : j, o[0]);
|
|
24408
|
+
var point = null;
|
|
24409
|
+
if (between.length === 0) {
|
|
24410
|
+
// The two runs meet, pinching the gap shut, and their shared node is
|
|
24411
|
+
// equidistant from both.
|
|
24412
|
+
point = o[1] < 0 ? points[0] : points[points.length - 1];
|
|
24413
|
+
} else if (between.length === 1 && pathLength(between[0].points) <= maxDist) {
|
|
24414
|
+
// A single run short enough to be a cap rather than a third bank: the gap
|
|
24415
|
+
// ends across it, midway along.
|
|
24416
|
+
point = midpointOfPath(between[0].points);
|
|
24417
|
+
}
|
|
24418
|
+
if (point) out.push({pos: o[1], point: point});
|
|
24419
|
+
});
|
|
24420
|
+
return out;
|
|
24421
|
+
}
|
|
24422
|
+
|
|
24423
|
+
// The runs lying between two others, walking the boundary forwards.
|
|
24424
|
+
function runsBetween(banks, from, to) {
|
|
24425
|
+
var out = [];
|
|
24426
|
+
for (var k = (from + 1) % banks.length; k !== to; k = (k + 1) % banks.length) {
|
|
24427
|
+
out.push(banks[k]);
|
|
24428
|
+
}
|
|
24429
|
+
return out;
|
|
24430
|
+
}
|
|
24431
|
+
|
|
24432
|
+
function pathLength(points) {
|
|
24433
|
+
var len = 0;
|
|
24434
|
+
for (var i = 1; i < points.length; i++) {
|
|
24435
|
+
len += geom.distance2D(points[i - 1][0], points[i - 1][1],
|
|
24436
|
+
points[i][0], points[i][1]);
|
|
24437
|
+
}
|
|
24438
|
+
return len;
|
|
24439
|
+
}
|
|
24440
|
+
|
|
24441
|
+
function midpointOfPath(points) {
|
|
24442
|
+
var half = pathLength(points) / 2;
|
|
24443
|
+
var walked = 0;
|
|
24444
|
+
for (var i = 1; i < points.length; i++) {
|
|
24445
|
+
var seg = geom.distance2D(points[i - 1][0], points[i - 1][1],
|
|
24446
|
+
points[i][0], points[i][1]);
|
|
24447
|
+
if (walked + seg >= half) {
|
|
24448
|
+
var t = seg > 0 ? (half - walked) / seg : 0;
|
|
24449
|
+
return [
|
|
24450
|
+
points[i - 1][0] + (points[i][0] - points[i - 1][0]) * t,
|
|
24451
|
+
points[i - 1][1] + (points[i][1] - points[i - 1][1]) * t
|
|
24452
|
+
];
|
|
24453
|
+
}
|
|
24454
|
+
walked += seg;
|
|
24455
|
+
}
|
|
24456
|
+
return points[points.length - 1];
|
|
24457
|
+
}
|
|
24458
|
+
|
|
24459
|
+
// The midpoint between a vertex of one boundary run and its footpoint on
|
|
24460
|
+
// another, where the two runs face each other across the gap.
|
|
24461
|
+
//
|
|
24462
|
+
// Two runs can instead meet at a node and carry on along the same side of the
|
|
24463
|
+
// gap. Those produce midpoints too, but ones lying on the boundary rather than
|
|
24464
|
+
// between two banks, and a cut along them shaves a hairline strip off one feature
|
|
24465
|
+
// to award it to another -- the very artifact partitioning exists to avoid. So a
|
|
24466
|
+
// midpoint counts only if it lies inside the gap, clear of the boundary by an
|
|
24467
|
+
// amount comparable to the separation it came from.
|
|
24468
|
+
function crossGapMidpoint(point, otherBank, maxDist, ringIndex) {
|
|
24469
|
+
if (!otherBank.index) return null;
|
|
24470
|
+
var foot = otherBank.index.closestPoint(point[0], point[1], maxDist);
|
|
24471
|
+
if (!foot || !(foot.distSq > 0)) return null;
|
|
24472
|
+
var mid = [(point[0] + foot.x) / 2, (point[1] + foot.y) / 2];
|
|
24473
|
+
// Half the separation is what a midpoint between facing banks has. A quarter
|
|
24474
|
+
// allows for a gap that narrows, bringing a third stretch of its boundary
|
|
24475
|
+
// closer to the midpoint than the banks it came from.
|
|
24476
|
+
if (ringIndex.hasSegmentWithin(mid[0], mid[1], Math.sqrt(foot.distSq) / 4)) {
|
|
24477
|
+
return null;
|
|
24478
|
+
}
|
|
24479
|
+
// Two runs can also face each other across a feature, in a gap that bends back
|
|
24480
|
+
// on itself.
|
|
24481
|
+
if (!ringIndex.containsPoint(mid[0], mid[1])) return null;
|
|
24482
|
+
return {point: mid, pos: foot.pos};
|
|
24483
|
+
}
|
|
24484
|
+
|
|
24485
|
+
// The vertex tests above run once per boundary vertex, and the boundary of a long
|
|
24486
|
+
// gap in detailed data carries thousands of them, so each test has to examine a
|
|
24487
|
+
// bounded number of segments rather than the whole path. @points is an open
|
|
24488
|
+
// polyline, or a closed ring for containsPoint().
|
|
24489
|
+
function SegmentIndex(points) {
|
|
24490
|
+
var n = points.length - 1;
|
|
24491
|
+
var index = new Flatbush(n);
|
|
24492
|
+
var xmax = -Infinity;
|
|
24493
|
+
for (var i = 0; i < n; i++) {
|
|
24494
|
+
var ax = points[i][0], ay = points[i][1];
|
|
24495
|
+
var bx = points[i + 1][0], by = points[i + 1][1];
|
|
24496
|
+
index.add(Math.min(ax, bx), Math.min(ay, by), Math.max(ax, bx), Math.max(ay, by));
|
|
24497
|
+
if (ax > xmax) xmax = ax;
|
|
24498
|
+
if (bx > xmax) xmax = bx;
|
|
24499
|
+
}
|
|
24500
|
+
index.finish();
|
|
24501
|
+
|
|
24502
|
+
// Nearest point on the path within @maxDist, with its position along the path
|
|
24503
|
+
// (segment index plus the fraction into that segment) so that midpoints from
|
|
24504
|
+
// either bank can be ordered on a common parameter. A segment within @maxDist
|
|
24505
|
+
// of the point has its nearest point inside the query box, so its own bounding
|
|
24506
|
+
// box meets that box: nothing near enough is missed.
|
|
24507
|
+
this.closestPoint = function(x, y, maxDist) {
|
|
24508
|
+
var ids = index.search(x - maxDist, y - maxDist, x + maxDist, y + maxDist);
|
|
24509
|
+
var maxSq = maxDist * maxDist;
|
|
24510
|
+
var best = null;
|
|
24511
|
+
for (var k = 0; k < ids.length; k++) {
|
|
24512
|
+
var i = ids[k];
|
|
24513
|
+
var ax = points[i][0], ay = points[i][1];
|
|
24514
|
+
var dx = points[i + 1][0] - ax, dy = points[i + 1][1] - ay;
|
|
24515
|
+
var len2 = dx * dx + dy * dy;
|
|
24516
|
+
var t = len2 > 0 ? ((x - ax) * dx + (y - ay) * dy) / len2 : 0;
|
|
24517
|
+
if (t < 0) t = 0;
|
|
24518
|
+
if (t > 1) t = 1;
|
|
24519
|
+
var fx = ax + t * dx, fy = ay + t * dy;
|
|
24520
|
+
var distSq = (x - fx) * (x - fx) + (y - fy) * (y - fy);
|
|
24521
|
+
if (distSq <= maxSq && (!best || distSq < best.distSq)) {
|
|
24522
|
+
best = {x: fx, y: fy, distSq: distSq, pos: i + t};
|
|
24523
|
+
}
|
|
24524
|
+
}
|
|
24525
|
+
return best;
|
|
24526
|
+
};
|
|
24527
|
+
|
|
24528
|
+
this.hasSegmentWithin = function(x, y, dist) {
|
|
24529
|
+
var ids = index.search(x - dist, y - dist, x + dist, y + dist);
|
|
24530
|
+
var distSq = dist * dist;
|
|
24531
|
+
for (var k = 0; k < ids.length; k++) {
|
|
24532
|
+
var i = ids[k];
|
|
24533
|
+
if (geom.pointSegDistSq2(x, y, points[i][0], points[i][1],
|
|
24534
|
+
points[i + 1][0], points[i + 1][1]) < distSq) {
|
|
24535
|
+
return true;
|
|
24536
|
+
}
|
|
24537
|
+
}
|
|
24538
|
+
return false;
|
|
24539
|
+
};
|
|
24540
|
+
|
|
24541
|
+
// Crossings of the ray running right from the point: odd means inside. A point
|
|
24542
|
+
// on the ring counts as inside, as the linear test this replaces had it.
|
|
24543
|
+
this.containsPoint = function(x, y) {
|
|
24544
|
+
var ids = index.search(x, y, xmax, y);
|
|
24545
|
+
var inside = false;
|
|
24546
|
+
for (var k = 0; k < ids.length; k++) {
|
|
24547
|
+
var i = ids[k];
|
|
24548
|
+
var ay = points[i][1], by = points[i + 1][1];
|
|
24549
|
+
if (ay > y === by > y) continue;
|
|
24550
|
+
var ax = points[i][0], bx = points[i + 1][0];
|
|
24551
|
+
if (ax + (y - ay) / (by - ay) * (bx - ax) > x) inside = !inside;
|
|
24552
|
+
}
|
|
24553
|
+
return inside;
|
|
24554
|
+
};
|
|
24555
|
+
}
|
|
24556
|
+
|
|
24557
|
+
// The narrowest gap worth dividing, in coordinate units: the automatic
|
|
24558
|
+
// gap-width, one hundredth of the median distance between vertices.
|
|
24559
|
+
//
|
|
24560
|
+
// Dividing a gap is only an improvement while the gap has a width worth
|
|
24561
|
+
// dividing. Awarding a whole gap to one feature moves that feature's boundary
|
|
24562
|
+
// across to the far bank, by the width of the gap; dividing it instead gives
|
|
24563
|
+
// every neighbor a strip of its own, and the boundary between two strips runs
|
|
24564
|
+
// half the gap's width from the bank it was cut from, out and back. Wide gap:
|
|
24565
|
+
// the first is a visible displacement and the second is a fair division. Gap a
|
|
24566
|
+
// few millimeters wide: the first is imperceptible, and the second decorates
|
|
24567
|
+
// several features with hairline slivers, which is worse for having more of them.
|
|
24568
|
+
//
|
|
24569
|
+
// A layer's median segment length is the scale at which its boundaries were
|
|
24570
|
+
// drawn, so the same fraction of it that serves as the default gap-width -- what
|
|
24571
|
+
// mapshaper already treats as too narrow to be intended -- marks where dividing
|
|
24572
|
+
// stops being worth it. This is deliberately independent of the gap-width in
|
|
24573
|
+
// effect: raising that to fill wider gaps should not coarsen the data's own scale.
|
|
24574
|
+
function getMinDivisibleGapWidth(lyr, arcs) {
|
|
24575
|
+
var width = getMedianPolygonSegmentLength(lyr, arcs) *
|
|
24576
|
+
GAP_WIDTH_SEGMENT_FRACTION;
|
|
24577
|
+
// That median comes back in meters for geographic coordinates, whereas cuts
|
|
24578
|
+
// are built in the source coordinate space. Converting it back with the same
|
|
24579
|
+
// test it was measured under keeps the two in step.
|
|
24580
|
+
return arcs.isPlanar() ? width : metersToDegrees(width);
|
|
24581
|
+
}
|
|
24582
|
+
|
|
24583
|
+
function projectPast$1(from, toward, distance) {
|
|
24584
|
+
var dx = from[0] - toward[0];
|
|
24585
|
+
var dy = from[1] - toward[1];
|
|
24586
|
+
var len = Math.sqrt(dx * dx + dy * dy);
|
|
24587
|
+
if (!(len > 0)) return null;
|
|
24588
|
+
return [
|
|
24589
|
+
from[0] + dx / len * distance,
|
|
24590
|
+
from[1] + dy / len * distance
|
|
24591
|
+
];
|
|
24592
|
+
}
|
|
24593
|
+
|
|
22516
24594
|
cmd.cleanLayers = cleanLayers;
|
|
22517
24595
|
|
|
22518
24596
|
function cleanLayers(layers, dataset, optsArg) {
|
|
@@ -22521,6 +24599,27 @@
|
|
|
22521
24599
|
var deepClean = !opts.only_arcs;
|
|
22522
24600
|
var pathClean = utils.some(layers, layerHasPaths);
|
|
22523
24601
|
var nodes;
|
|
24602
|
+
// Collapsing duplicate boundaries runs unconditionally, and only ever merges
|
|
24603
|
+
// banks that differ by floating-point rounding. Left in place, such a seam
|
|
24604
|
+
// becomes an enclosed sliver that gap filling awards to one neighbor, giving
|
|
24605
|
+
// that feature a zero-width spike along the shared border.
|
|
24606
|
+
if (pathClean && dataset.arcs) {
|
|
24607
|
+
profileStart('collapseDuplicateBoundaries');
|
|
24608
|
+
noteArcsWillChange(dataset.arcs, {operation: 'clean-closeGaps'});
|
|
24609
|
+
var polygonLayers = layers.filter(function(lyr) {
|
|
24610
|
+
return lyr.geometry_type == 'polygon';
|
|
24611
|
+
});
|
|
24612
|
+
polygonLayers.forEach(function(lyr) {
|
|
24613
|
+
noteLayerWillChange(lyr, {operation: 'clean-closeGaps', unit: 'arc-ids'});
|
|
24614
|
+
});
|
|
24615
|
+
if (collapseDuplicateBoundaries(layers, dataset)) {
|
|
24616
|
+
markArcsChanged(dataset.arcs, {operation: 'clean-closeGaps'});
|
|
24617
|
+
polygonLayers.forEach(function(lyr) {
|
|
24618
|
+
markLayerChanged(lyr, {operation: 'clean-closeGaps', unit: 'arc-ids'});
|
|
24619
|
+
});
|
|
24620
|
+
}
|
|
24621
|
+
profileEnd('collapseDuplicateBoundaries');
|
|
24622
|
+
}
|
|
22524
24623
|
if (opts.debug) {
|
|
22525
24624
|
addIntersectionCuts(dataset, opts);
|
|
22526
24625
|
profileEnd('cleanLayers');
|
|
@@ -22538,9 +24637,49 @@
|
|
|
22538
24637
|
nodes = addIntersectionCuts(dataset, opts);
|
|
22539
24638
|
}
|
|
22540
24639
|
if (lyr.geometry_type == 'polygon') {
|
|
24640
|
+
// Pinching the mouths of cracks that open to the outside encloses them,
|
|
24641
|
+
// so that the passes below fill or divide them like any other gap. It
|
|
24642
|
+
// comes first for that reason, and only the pinched vertices move.
|
|
24643
|
+
if (opts.close_outer_gaps) {
|
|
24644
|
+
profileStart('pinchOuterCrackMouths');
|
|
24645
|
+
noteArcsWillChange(dataset.arcs, {operation: 'clean-pinchGapMouths'});
|
|
24646
|
+
noteLayerWillChange(lyr,
|
|
24647
|
+
{operation: 'clean-pinchGapMouths', unit: 'arc-ids'});
|
|
24648
|
+
if (pinchOuterCrackMouths(lyr, dataset, nodes, opts)) {
|
|
24649
|
+
markArcsChanged(dataset.arcs, {operation: 'clean-pinchGapMouths'});
|
|
24650
|
+
markLayerChanged(lyr,
|
|
24651
|
+
{operation: 'clean-pinchGapMouths', unit: 'arc-ids'});
|
|
24652
|
+
// A pinched pair only touches: the cutter turns each touch into a
|
|
24653
|
+
// node, which is what makes the crack a tile of the mosaic. Undo
|
|
24654
|
+
// already holds everything this re-cut changes, captured above.
|
|
24655
|
+
withActiveUndoTransaction(null, function() {
|
|
24656
|
+
nodes = addIntersectionCuts(dataset, opts);
|
|
24657
|
+
});
|
|
24658
|
+
}
|
|
24659
|
+
profileEnd('pinchOuterCrackMouths');
|
|
24660
|
+
}
|
|
24661
|
+
delete opts._mosaic_cut_arcs;
|
|
24662
|
+
profileStart('partitionPolygonMosaicGaps');
|
|
24663
|
+
noteArcsWillChange(dataset.arcs, {operation: 'clean-partitionGaps'});
|
|
24664
|
+
var cutLayer = partitionPolygonMosaicGaps(lyr, dataset, nodes, opts);
|
|
24665
|
+
if (cutLayer) {
|
|
24666
|
+
markArcsChanged(dataset.arcs, {operation: 'clean-partitionGaps'});
|
|
24667
|
+
// The arc collection the partition just installed is a throwaway: undo
|
|
24668
|
+
// puts back the collection this command started with, which the
|
|
24669
|
+
// partition captured. Capturing this one too would store a second copy
|
|
24670
|
+
// of the arc table to no purpose. Every layer baseline the re-cut needs
|
|
24671
|
+
// was captured by the addIntersectionCuts() call above.
|
|
24672
|
+
withActiveUndoTransaction(null, function() {
|
|
24673
|
+
nodes = addIntersectionCuts(dataset, opts);
|
|
24674
|
+
});
|
|
24675
|
+
opts._mosaic_cut_arcs = collectArcIds(cutLayer.shapes);
|
|
24676
|
+
dataset.layers.splice(dataset.layers.indexOf(cutLayer), 1);
|
|
24677
|
+
}
|
|
24678
|
+
profileEnd('partitionPolygonMosaicGaps');
|
|
22541
24679
|
profileStart('cleanPolygonLayerGeometry');
|
|
22542
24680
|
cleanPolygonLayerGeometry(lyr, dataset, opts);
|
|
22543
24681
|
profileEnd('cleanPolygonLayerGeometry');
|
|
24682
|
+
delete opts._mosaic_cut_arcs;
|
|
22544
24683
|
} else if (lyr.geometry_type == 'polyline') {
|
|
22545
24684
|
profileStart('cleanPolylineLayerGeometry');
|
|
22546
24685
|
cleanPolylineLayerGeometry(lyr, dataset);
|
|
@@ -22568,7 +24707,7 @@
|
|
|
22568
24707
|
|
|
22569
24708
|
function cleanPolygonLayerGeometry(lyr, dataset, opts) {
|
|
22570
24709
|
// clean polygons by apply the 'dissolve2' function to each feature
|
|
22571
|
-
opts =
|
|
24710
|
+
opts = withDefaultGapWidth(opts);
|
|
22572
24711
|
var groups = lyr.shapes.map(function(shp, i) {
|
|
22573
24712
|
return [i];
|
|
22574
24713
|
});
|
|
@@ -22577,6 +24716,30 @@
|
|
|
22577
24716
|
markLayerChanged(lyr, {operation: 'cleanPolygonLayerGeometry', unit: 'shapes'});
|
|
22578
24717
|
}
|
|
22579
24718
|
|
|
24719
|
+
function withDefaultGapWidth(opts) {
|
|
24720
|
+
opts = Object.assign({}, opts);
|
|
24721
|
+
if (opts.gap_width != null) return opts;
|
|
24722
|
+
// Legacy area/sliver options: keep the historical gap_fill_area=auto default.
|
|
24723
|
+
if (opts.gap_fill_area != null || opts.min_gap_area != null ||
|
|
24724
|
+
opts.min_area != null || opts.sliver_control != null) {
|
|
24725
|
+
if (opts.gap_fill_area == null && opts.min_gap_area == null &&
|
|
24726
|
+
opts.min_area == null) {
|
|
24727
|
+
opts.gap_fill_area = 'auto';
|
|
24728
|
+
}
|
|
24729
|
+
return opts;
|
|
24730
|
+
}
|
|
24731
|
+
opts.gap_width = 'auto';
|
|
24732
|
+
return opts;
|
|
24733
|
+
}
|
|
24734
|
+
|
|
24735
|
+
function collectArcIds(shapes) {
|
|
24736
|
+
var ids = {};
|
|
24737
|
+
traversePaths(shapes, function(o) {
|
|
24738
|
+
ids[o.arcId < 0 ? ~o.arcId : o.arcId] = true;
|
|
24739
|
+
});
|
|
24740
|
+
return ids;
|
|
24741
|
+
}
|
|
24742
|
+
|
|
22580
24743
|
// Remove duplicate points from multipoint geometries
|
|
22581
24744
|
// TODO: consider checking for invalid coordinates
|
|
22582
24745
|
function cleanPointLayerGeometry(lyr, dataset, opts) {
|
|
@@ -24239,6 +26402,10 @@
|
|
|
24239
26402
|
return !!(d && (d.icon || d['icon-size'] || (d['icon-color'] && d.r > 0)));
|
|
24240
26403
|
}
|
|
24241
26404
|
|
|
26405
|
+
// An unsupported icon name renders as nothing, without a message -- this
|
|
26406
|
+
// function runs once per feature per render, so warning here would repeat the
|
|
26407
|
+
// same message for every feature (and on every redraw in the GUI). The -style
|
|
26408
|
+
// command warns about unsupported names when they are assigned instead.
|
|
24242
26409
|
function renderIcon(d) {
|
|
24243
26410
|
var type = d.icon || 'circle';
|
|
24244
26411
|
var r = getIconRadius(d, type);
|
|
@@ -24247,7 +26414,6 @@
|
|
|
24247
26414
|
if (type == 'square') return square(getIconStyleData(d, r), 0, 0);
|
|
24248
26415
|
if (type == 'ring') return ring(getIconStyleData(d, r), 0, 0);
|
|
24249
26416
|
if (type == 'star') return star(getIconStyleData(d, r));
|
|
24250
|
-
message('Unknown icon type: ' + type);
|
|
24251
26417
|
return empty();
|
|
24252
26418
|
}
|
|
24253
26419
|
|
|
@@ -31872,13 +34038,27 @@ ${svg}
|
|
|
31872
34038
|
var featureLength = bb.readUint32(offset);
|
|
31873
34039
|
bb.setPosition(offset);
|
|
31874
34040
|
var feature = Feature.getSizePrefixedRootAsFeature(bb);
|
|
31875
|
-
geojsonFeature =
|
|
34041
|
+
geojsonFeature = readGeoJSONFeature(feature, headerMeta, id++);
|
|
31876
34042
|
delete geojsonFeature.id;
|
|
31877
34043
|
offset += SIZE_PREFIX_LEN + featureLength;
|
|
31878
34044
|
return geojsonFeature;
|
|
31879
34045
|
};
|
|
31880
34046
|
}
|
|
31881
34047
|
|
|
34048
|
+
// The geometry field of a FlatGeobuf Feature is optional; fromFeature() assumes
|
|
34049
|
+
// it is present, so features without one are converted here.
|
|
34050
|
+
function readGeoJSONFeature(feature, headerMeta, id) {
|
|
34051
|
+
if (feature.geometry() === null) {
|
|
34052
|
+
return {
|
|
34053
|
+
type: 'Feature',
|
|
34054
|
+
id: id,
|
|
34055
|
+
geometry: null,
|
|
34056
|
+
properties: parseProperties$1(feature, headerMeta.columns)
|
|
34057
|
+
};
|
|
34058
|
+
}
|
|
34059
|
+
return fromFeature(id, feature, headerMeta);
|
|
34060
|
+
}
|
|
34061
|
+
|
|
31882
34062
|
function buildHeaderWithCRS(headerMeta, crsMeta) {
|
|
31883
34063
|
var builder = new Builder();
|
|
31884
34064
|
var columnsOffset = createColumnsVector(builder, headerMeta.columns || []);
|
|
@@ -31923,24 +34103,31 @@ ${svg}
|
|
|
31923
34103
|
function encodeCollection(source, columns, forcedType) {
|
|
31924
34104
|
var headerMeta = null;
|
|
31925
34105
|
var sink = null;
|
|
34106
|
+
// Null-geometry features encountered before the collection's geometry type is
|
|
34107
|
+
// known, and therefore before the header can be written.
|
|
34108
|
+
var deferred = [];
|
|
31926
34109
|
for (var i = 0; i < source.length; i++) {
|
|
31927
34110
|
var feature = source.getFeature(i);
|
|
34111
|
+
var properties = normalizeProperties(feature.properties, columns);
|
|
34112
|
+
if (!feature.geometry) {
|
|
34113
|
+
// A feature's geometry can go missing during export -- e.g. a path that
|
|
34114
|
+
// collapses to a single point at the output coordinate precision -- and a
|
|
34115
|
+
// layer can also contain null shapes to begin with. Write a record with
|
|
34116
|
+
// no geometry, matching what the GeoJSON and Shapefile exporters do.
|
|
34117
|
+
// Such a feature says nothing about the collection's geometry type, so it
|
|
34118
|
+
// must not make a homogeneous layer look mixed.
|
|
34119
|
+
var nullFeature = buildNullGeometryFeature(properties, columns);
|
|
34120
|
+
if (sink) {
|
|
34121
|
+
sink.append(nullFeature);
|
|
34122
|
+
} else {
|
|
34123
|
+
deferred.push(nullFeature);
|
|
34124
|
+
}
|
|
34125
|
+
continue;
|
|
34126
|
+
}
|
|
31928
34127
|
var type = GeometryType[feature.geometry.type] || GeometryType.Unknown;
|
|
31929
34128
|
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));
|
|
34129
|
+
headerMeta = getEncodedHeaderMeta(forcedType === null ? type : forcedType, columns, source.length);
|
|
34130
|
+
sink = initSink(source, headerMeta, deferred);
|
|
31944
34131
|
} else if (forcedType === null && type != headerMeta.geometryType) {
|
|
31945
34132
|
return null;
|
|
31946
34133
|
}
|
|
@@ -31948,11 +34135,66 @@ ${svg}
|
|
|
31948
34135
|
parseGC(feature.geometry) :
|
|
31949
34136
|
parseGeometry(feature.geometry);
|
|
31950
34137
|
omitRedundantGeometryType(geometry, headerMeta.geometryType);
|
|
31951
|
-
sink.append(buildFeature(geometry,
|
|
34138
|
+
sink.append(buildFeature(geometry, properties, headerMeta));
|
|
34139
|
+
}
|
|
34140
|
+
if (!sink) {
|
|
34141
|
+
// No feature had a geometry, so the collection has no geometry type.
|
|
34142
|
+
headerMeta = getEncodedHeaderMeta(forcedType === null ? GeometryType.Unknown : forcedType,
|
|
34143
|
+
columns, source.length);
|
|
34144
|
+
sink = initSink(source, headerMeta, deferred);
|
|
31952
34145
|
}
|
|
31953
34146
|
return sink.toBytes();
|
|
31954
34147
|
}
|
|
31955
34148
|
|
|
34149
|
+
function getEncodedHeaderMeta(geometryType, columns, featuresCount) {
|
|
34150
|
+
return {
|
|
34151
|
+
geometryType: geometryType,
|
|
34152
|
+
columns: columns,
|
|
34153
|
+
envelope: null,
|
|
34154
|
+
featuresCount: featuresCount,
|
|
34155
|
+
indexNodeSize: 0,
|
|
34156
|
+
crs: null,
|
|
34157
|
+
title: null,
|
|
34158
|
+
description: null,
|
|
34159
|
+
metadata: null
|
|
34160
|
+
};
|
|
34161
|
+
}
|
|
34162
|
+
|
|
34163
|
+
function initSink(source, headerMeta, deferredFeatures) {
|
|
34164
|
+
var sink = new ByteSink(magicbytes.length + estimateEncodedBytes(source));
|
|
34165
|
+
sink.append(magicbytes);
|
|
34166
|
+
sink.append(buildHeader(headerMeta));
|
|
34167
|
+
for (var i = 0; i < deferredFeatures.length; i++) {
|
|
34168
|
+
sink.append(deferredFeatures[i]);
|
|
34169
|
+
}
|
|
34170
|
+
return sink;
|
|
34171
|
+
}
|
|
34172
|
+
|
|
34173
|
+
// Encodes a feature with attributes but no geometry, which FlatGeobuf supports
|
|
34174
|
+
// by omitting the Feature table's optional geometry field. buildFeature() always
|
|
34175
|
+
// writes a geometry, so the properties are encoded with it and then re-emitted:
|
|
34176
|
+
// property values are written in a typed binary layout that is better read from
|
|
34177
|
+
// one implementation than copied.
|
|
34178
|
+
function buildNullGeometryFeature(properties, columns) {
|
|
34179
|
+
var meta = {columns: columns};
|
|
34180
|
+
var withGeometry = buildFeature({xy: [], type: GeometryType.Unknown}, properties, meta);
|
|
34181
|
+
var bb = new ByteBuffer(withGeometry);
|
|
34182
|
+
var propertyBytes = Feature.getSizePrefixedRootAsFeature(bb).propertiesArray();
|
|
34183
|
+
var builder = new Builder();
|
|
34184
|
+
// Features are concatenated in the file and read in place, so every feature
|
|
34185
|
+
// buffer has to leave the next one 8-byte aligned or reading a Float64 array
|
|
34186
|
+
// of coordinates from it throws. Buffers holding coordinates are padded to a
|
|
34187
|
+
// multiple of 8 because of those arrays; a feature with only property bytes
|
|
34188
|
+
// needs the alignment requested explicitly.
|
|
34189
|
+
builder.prep(8, 0);
|
|
34190
|
+
var propertiesOffset = propertyBytes && propertyBytes.length > 0 ?
|
|
34191
|
+
Feature.createPropertiesVector(builder, propertyBytes) : 0;
|
|
34192
|
+
Feature.startFeature(builder);
|
|
34193
|
+
if (propertiesOffset) Feature.addProperties(builder, propertiesOffset);
|
|
34194
|
+
builder.finishSizePrefixed(Feature.endFeature(builder));
|
|
34195
|
+
return builder.asUint8Array();
|
|
34196
|
+
}
|
|
34197
|
+
|
|
31956
34198
|
// Encoding the first feature is the only sample available before the buffer
|
|
31957
34199
|
// has to be allocated; overshooting costs a resize, which is why it is loose.
|
|
31958
34200
|
function estimateEncodedBytes(source) {
|
|
@@ -32292,6 +34534,8 @@ ${svg}
|
|
|
32292
34534
|
var featureIndex = 0;
|
|
32293
34535
|
for (var i = 0; i < state.count; i++) {
|
|
32294
34536
|
var feat = exportFeatureAtIndex(state, i);
|
|
34537
|
+
// GeoPackage's GeoJSON insert path does not store true NULL geometries
|
|
34538
|
+
// (null becomes a NaN point). Skip them, matching prior behavior.
|
|
32295
34539
|
if (!feat || !feat.geometry) continue;
|
|
32296
34540
|
var normalized = normalizeFeature(feat, fields);
|
|
32297
34541
|
try {
|
|
@@ -34389,7 +36633,8 @@ ${svg}
|
|
|
34389
36633
|
datasets = [mergeDatasetsForExport(datasets)];
|
|
34390
36634
|
}
|
|
34391
36635
|
datasets.forEach(sortExportLayers);
|
|
34392
|
-
|
|
36636
|
+
// GeoPackage bypasses exportFileContent(), where precision is normally applied.
|
|
36637
|
+
files = await exportGeoPackage(applyExportPrecision(datasets[0], opts), opts);
|
|
34393
36638
|
} else if (format == 'geoparquet') {
|
|
34394
36639
|
var layerCount = datasets.reduce(function(sum, d) {
|
|
34395
36640
|
return sum + d.layers.length;
|
|
@@ -34398,7 +36643,8 @@ ${svg}
|
|
|
34398
36643
|
files = [];
|
|
34399
36644
|
for (var i = 0; i < datasets.length; i++) {
|
|
34400
36645
|
sortExportLayers(datasets[i]);
|
|
34401
|
-
|
|
36646
|
+
// GeoParquet bypasses exportFileContent(), where precision is normally applied.
|
|
36647
|
+
files = files.concat(await exportGeoParquet(applyExportPrecision(datasets[i], opts), opts, singleFileName));
|
|
34402
36648
|
}
|
|
34403
36649
|
} else {
|
|
34404
36650
|
files = datasets.reduce(function(memo, dataset) {
|
|
@@ -34452,6 +36698,15 @@ ${svg}
|
|
|
34452
36698
|
});
|
|
34453
36699
|
}
|
|
34454
36700
|
|
|
36701
|
+
// Apply -o precision= the same way exportFileContent() does for formats that
|
|
36702
|
+
// take their own export path (GeoPackage, GeoParquet).
|
|
36703
|
+
function applyExportPrecision(dataset, opts) {
|
|
36704
|
+
if (!opts.precision) return dataset;
|
|
36705
|
+
dataset = copyDatasetForExport(dataset);
|
|
36706
|
+
setCoordinatePrecision(dataset, opts.precision, !!opts.fix_geometry);
|
|
36707
|
+
return dataset;
|
|
36708
|
+
}
|
|
36709
|
+
|
|
34455
36710
|
// Return an array of objects with 'filename' and 'content' members.
|
|
34456
36711
|
//
|
|
34457
36712
|
function exportFileContent(dataset, opts) {
|
|
@@ -36724,11 +38979,15 @@ ${svg}
|
|
|
36724
38979
|
},
|
|
36725
38980
|
minGapAreaOpt = {
|
|
36726
38981
|
old_alias: 'min-gap-area',
|
|
36727
|
-
describe: '
|
|
38982
|
+
describe: '(deprecated) use gap-width= instead',
|
|
36728
38983
|
type: 'area'
|
|
36729
38984
|
},
|
|
38985
|
+
gapWidthOpt = {
|
|
38986
|
+
describe: 'fill gaps narrower than this width, e.g. 2m (default is automatic)',
|
|
38987
|
+
type: 'distance'
|
|
38988
|
+
},
|
|
36730
38989
|
sliverControlOpt = {
|
|
36731
|
-
describe: '
|
|
38990
|
+
describe: '(deprecated) use gap-width= instead',
|
|
36732
38991
|
type: 'number'
|
|
36733
38992
|
},
|
|
36734
38993
|
calcOpt = {
|
|
@@ -37456,6 +39715,11 @@ ${svg}
|
|
|
37456
39715
|
|
|
37457
39716
|
parser.command('clean')
|
|
37458
39717
|
.describe('fixes geometry issues, such as polygon overlaps and gaps')
|
|
39718
|
+
.option('close-outer-gaps', {
|
|
39719
|
+
describe: 'close cracks that open to the outside of the mosaic',
|
|
39720
|
+
type: 'flag'
|
|
39721
|
+
})
|
|
39722
|
+
.option('gap-width', gapWidthOpt)
|
|
37459
39723
|
.option('gap-fill-area', minGapAreaOpt)
|
|
37460
39724
|
.option('sliver-control', sliverControlOpt)
|
|
37461
39725
|
.option('snap-interval', snapIntervalOpt)
|
|
@@ -37555,7 +39819,7 @@ ${svg}
|
|
|
37555
39819
|
});
|
|
37556
39820
|
|
|
37557
39821
|
parser.command('contours')
|
|
37558
|
-
.describe('convert a raster layer to contour lines')
|
|
39822
|
+
.describe('convert a raster layer to contour lines or closed bands')
|
|
37559
39823
|
.option('interval', {
|
|
37560
39824
|
describe: 'spacing between contour levels (default is a round interval)',
|
|
37561
39825
|
type: 'number'
|
|
@@ -37575,6 +39839,10 @@ ${svg}
|
|
|
37575
39839
|
.option('field', {
|
|
37576
39840
|
describe: 'name of field to hold contour values (default is "value")'
|
|
37577
39841
|
})
|
|
39842
|
+
.option('closed', {
|
|
39843
|
+
describe: 'create closed contour bands with lower and upper fields',
|
|
39844
|
+
type: 'flag'
|
|
39845
|
+
})
|
|
37578
39846
|
.option('no-smoothing', {
|
|
37579
39847
|
describe: 'skip smoothing away the one-pixel contour staircase',
|
|
37580
39848
|
type: 'flag'
|
|
@@ -37655,8 +39923,9 @@ ${svg}
|
|
|
37655
39923
|
type: 'flag',
|
|
37656
39924
|
describe: '[points] use 2D math to find centroids of latlong points'
|
|
37657
39925
|
})
|
|
39926
|
+
.option('gap-width', gapWidthOpt)
|
|
37658
39927
|
.option('gap-fill-area', {
|
|
37659
|
-
describe: '[polygons]
|
|
39928
|
+
describe: '[polygons] (deprecated) use gap-width= instead',
|
|
37660
39929
|
type: 'area'
|
|
37661
39930
|
})
|
|
37662
39931
|
.option('sliver-control', sliverControlOpt)
|
|
@@ -37685,8 +39954,9 @@ ${svg}
|
|
|
37685
39954
|
.option('calc', calcOpt)
|
|
37686
39955
|
.option('sum-fields', sumFieldsOpt)
|
|
37687
39956
|
.option('copy-fields', copyFieldsOpt)
|
|
39957
|
+
.option('gap-width', gapWidthOpt)
|
|
37688
39958
|
.option('gap-fill-area', {
|
|
37689
|
-
describe: '
|
|
39959
|
+
describe: '(deprecated) use gap-width= instead',
|
|
37690
39960
|
type: 'area'
|
|
37691
39961
|
})
|
|
37692
39962
|
.option('sliver-control', sliverControlOpt)
|
|
@@ -38216,6 +40486,17 @@ ${svg}
|
|
|
38216
40486
|
describe: 'create an interior point for each polygon\'s largest ring',
|
|
38217
40487
|
type: 'flag'
|
|
38218
40488
|
})
|
|
40489
|
+
.option('inner-method', {
|
|
40490
|
+
// describe: 'centroid2, pole, centroid, weighted or legacy (default is centroid2)'
|
|
40491
|
+
})
|
|
40492
|
+
.option('inner-tolerance', {
|
|
40493
|
+
// describe: 'clearance loss allowed by centroid methods (default is 10%)',
|
|
40494
|
+
type: 'percent'
|
|
40495
|
+
})
|
|
40496
|
+
.option('inner-weight', {
|
|
40497
|
+
// describe: 'centroid weight used by inner-method=weighted (default is 0.6)',
|
|
40498
|
+
type: 'number'
|
|
40499
|
+
})
|
|
38219
40500
|
.option('centroid', {
|
|
38220
40501
|
describe: 'create a centroid point for each polygon\'s largest ring',
|
|
38221
40502
|
type: 'flag'
|
|
@@ -45853,10 +48134,10 @@ ${svg}
|
|
|
45853
48134
|
}
|
|
45854
48135
|
|
|
45855
48136
|
function isClosedPath$1(arr) {
|
|
45856
|
-
return isArrayOfPoints(arr) && arr.length > 3 && samePoint$
|
|
48137
|
+
return isArrayOfPoints(arr) && arr.length > 3 && samePoint$2(arr[0], arr[arr.length - 1]);
|
|
45857
48138
|
}
|
|
45858
48139
|
|
|
45859
|
-
function samePoint$
|
|
48140
|
+
function samePoint$2(a, b) {
|
|
45860
48141
|
return a[0] == b[0] && a[1] == b[1];
|
|
45861
48142
|
}
|
|
45862
48143
|
|
|
@@ -48205,12 +50486,12 @@ ${svg}
|
|
|
48205
50486
|
return arr[arr.length - 1];
|
|
48206
50487
|
}
|
|
48207
50488
|
|
|
48208
|
-
function samePoint(a, b) {
|
|
50489
|
+
function samePoint$1(a, b) {
|
|
48209
50490
|
return a && b && a[0] === b[0] && a[1] === b[1];
|
|
48210
50491
|
}
|
|
48211
50492
|
|
|
48212
50493
|
function isClosedPath(arr) {
|
|
48213
|
-
return samePoint(arr[0], lastEl(arr));
|
|
50494
|
+
return samePoint$1(arr[0], lastEl(arr));
|
|
48214
50495
|
}
|
|
48215
50496
|
|
|
48216
50497
|
// remove likely rounding errors
|
|
@@ -48371,7 +50652,7 @@ ${svg}
|
|
|
48371
50652
|
if (!nextPart) {
|
|
48372
50653
|
return null;
|
|
48373
50654
|
}
|
|
48374
|
-
if (samePoint(ring[0], nextPart[0])) {
|
|
50655
|
+
if (samePoint$1(ring[0], nextPart[0])) {
|
|
48375
50656
|
// done!
|
|
48376
50657
|
ring.push(ring[0]); // close the ring
|
|
48377
50658
|
return ring;
|
|
@@ -48420,7 +50701,7 @@ ${svg}
|
|
|
48420
50701
|
|
|
48421
50702
|
function findPartStartingAt(parts, firstPoint) {
|
|
48422
50703
|
for (var i=0; i<parts.length; i++) {
|
|
48423
|
-
if (samePoint(parts[i][0], firstPoint)) {
|
|
50704
|
+
if (samePoint$1(parts[i][0], firstPoint)) {
|
|
48424
50705
|
return parts[i];
|
|
48425
50706
|
}
|
|
48426
50707
|
}
|
|
@@ -48444,7 +50725,7 @@ ${svg}
|
|
|
48444
50725
|
var part = [];
|
|
48445
50726
|
var firstPoint = path[0];
|
|
48446
50727
|
var lastPoint = lastEl(path);
|
|
48447
|
-
var closed = samePoint(firstPoint, lastPoint);
|
|
50728
|
+
var closed = samePoint$1(firstPoint, lastPoint);
|
|
48448
50729
|
var p, pp, y;
|
|
48449
50730
|
for (var i=0, n=path.length; i<n; i++) {
|
|
48450
50731
|
p = path[i];
|
|
@@ -48812,6 +51093,16 @@ ${svg}
|
|
|
48812
51093
|
|
|
48813
51094
|
var POLAR_BUFFER_MARGIN_DEGREES$1 = 1e-4;
|
|
48814
51095
|
|
|
51096
|
+
// Distance at which to place the vertices of a round join whose chords span
|
|
51097
|
+
// stepAngle degrees, so that the chord MIDPOINTS -- not the vertices -- lie on
|
|
51098
|
+
// the offset circle of radius dist. The join then brackets the true arc instead
|
|
51099
|
+
// of being inscribed in it (see makeInscribedRoundJoin). The correction grows
|
|
51100
|
+
// with the step angle, so a gentle bend is barely moved and a sharp corner gets
|
|
51101
|
+
// the full ~0.5% of dist (at the default 8 segments/quadrant).
|
|
51102
|
+
function circumscribedJoinDist(dist, stepAngle) {
|
|
51103
|
+
return dist / Math.cos(stepAngle / 2 / R2D$8);
|
|
51104
|
+
}
|
|
51105
|
+
|
|
48815
51106
|
|
|
48816
51107
|
// Returns a function for generating GeoJSON MultiPolygon geometries
|
|
48817
51108
|
function getPolylineBufferMaker(dataset, opts) {
|
|
@@ -48821,6 +51112,7 @@ ${svg}
|
|
|
48821
51112
|
var getOffsetPoint = getOffsetFunction(crs, opts);
|
|
48822
51113
|
var roundJoinSegsPerQuadrant = opts.quad_segs >= 2 ? opts.quad_segs : 8;
|
|
48823
51114
|
var roundJoinSegAngle = 90 / roundJoinSegsPerQuadrant;
|
|
51115
|
+
var circumscribeJoins = !!opts.circumscribe_joins; // see makeInscribedRoundJoin
|
|
48824
51116
|
// Max arc step (degrees) for the coarse concave bridge (makeCoarseConcaveJoin),
|
|
48825
51117
|
// the optional low-resolution alternative to makeConcaveJoin in
|
|
48826
51118
|
// traceCleanOffsetSide. Larger = fewer points = faster dissolve. NOTE: a
|
|
@@ -49994,14 +52286,36 @@ ${svg}
|
|
|
49994
52286
|
[getOffsetPoint(x, y, startDir + 180, dist)];
|
|
49995
52287
|
}
|
|
49996
52288
|
|
|
49997
|
-
//
|
|
49998
|
-
//
|
|
52289
|
+
// Round join: an equal-angle-step polygonal approximation of the arc between
|
|
52290
|
+
// the incoming and outgoing offset edges, replacing the incoming edge's end
|
|
52291
|
+
// (p2Prev) and the outgoing edge's start (p1).
|
|
52292
|
+
//
|
|
52293
|
+
// Inscribed (default): vertices at the true offset distance, stepping from the
|
|
52294
|
+
// incoming perpendicular round to the outgoing one. Because its chords cut the
|
|
52295
|
+
// corner, the join lies INSIDE the true circular offset, falling short of it by
|
|
52296
|
+
// up to one sagitta (~0.5% of dist at the default 8 segments/quadrant).
|
|
52297
|
+
//
|
|
52298
|
+
// Circumscribed (circumscribe_joins): the same vertices, moved out to
|
|
52299
|
+
// circumscribedJoinDist so the chords span the offset circle instead of cutting
|
|
52300
|
+
// inside it. The LAST vertex stays at the true distance because the outgoing
|
|
52301
|
+
// straight edge attaches there: moving it radially outward would carry that edge
|
|
52302
|
+
// outward too, and since it also seeds the elbow joins of the segments that
|
|
52303
|
+
// follow, the bias propagates into a collar of offset-vs-source overshoot along
|
|
52304
|
+
// the whole outline -- which the fill-gaps mask cannot tell from a real gap fill
|
|
52305
|
+
// (its coast test tolerance is itself calibrated to the join sagitta). Leaving
|
|
52306
|
+
// that one vertex on the circle costs some accuracy: the final chord still dips
|
|
52307
|
+
// inside by about half a sagitta, so a gap tip whose closure happens to fall on
|
|
52308
|
+
// that chord is still short by ~0.25% of dist. Raise quad-segs to shrink it.
|
|
49999
52309
|
function makeInscribedRoundJoin(cx, cy, startBearing, arcAngle, dist) {
|
|
50000
52310
|
var pointCount = Math.max(1, Math.round(arcAngle / roundJoinSegAngle));
|
|
50001
52311
|
var stepAngle = arcAngle / pointCount;
|
|
50002
52312
|
var points = [];
|
|
50003
|
-
|
|
50004
|
-
|
|
52313
|
+
var i;
|
|
52314
|
+
var outerDist = circumscribeJoins ?
|
|
52315
|
+
circumscribedJoinDist(dist, stepAngle) : dist;
|
|
52316
|
+
for (i = 1; i <= pointCount; i++) {
|
|
52317
|
+
points.push(getOffsetPoint(cx, cy, startBearing + stepAngle * i,
|
|
52318
|
+
i == pointCount ? dist : outerDist));
|
|
50005
52319
|
}
|
|
50006
52320
|
return points;
|
|
50007
52321
|
}
|
|
@@ -50610,10 +52924,24 @@ ${svg}
|
|
|
50610
52924
|
if (filterOpts.skipShape && filterOpts.skipShape(srcShape, srcArcs)) return bufShape;
|
|
50611
52925
|
var intervalPct = simplifyFn ? simplifyFn(distance) / distance : 0;
|
|
50612
52926
|
return filterOutlineArtifactHolesFromShape(bufShape, bufArcs, srcShape,
|
|
50613
|
-
srcArcs, distance, intervalPct, sagPct, oneSided, sideOpts, spherical
|
|
52927
|
+
srcArcs, distance, intervalPct, sagPct, oneSided, sideOpts, spherical,
|
|
52928
|
+
filterOpts.coverageIndex || null);
|
|
50614
52929
|
});
|
|
50615
52930
|
}
|
|
50616
52931
|
|
|
52932
|
+
// True if some other output feature covers the inside of this hole, making it
|
|
52933
|
+
// that feature's territory rather than an artifact of this one's
|
|
52934
|
+
// construction. @index is a PathIndex over every output shape: a hole is
|
|
52935
|
+
// outside its own feature by definition, so an enclosed probe can only be
|
|
52936
|
+
// another feature's.
|
|
52937
|
+
function holeIsNeighborTerritory(path, arcs, index) {
|
|
52938
|
+
var probes = getHoleInteriorProbes(getProjectedRingPoints(path, arcs, false));
|
|
52939
|
+
for (var i = 0; i < probes.length; i++) {
|
|
52940
|
+
if (index.pointIsEnclosed(probes[i])) return true;
|
|
52941
|
+
}
|
|
52942
|
+
return false;
|
|
52943
|
+
}
|
|
52944
|
+
|
|
50617
52945
|
// Remove artifact rings left by dissolving the self-intersecting outline rings
|
|
50618
52946
|
// made by the two-sided buffer fast path. The outline's concave-join loops can
|
|
50619
52947
|
// survive the dissolve as spurious holes (and, degenerately, as zero-area rings
|
|
@@ -50623,8 +52951,12 @@ ${svg}
|
|
|
50623
52951
|
// that its entire region lies inside the true buffer, and kept otherwise.
|
|
50624
52952
|
// Filters one buffer shape (one source feature's dissolved outline); returns
|
|
50625
52953
|
// the filtered shape, or null if nothing survives.
|
|
52954
|
+
// @coverageIndex: optional PathIndex over every output shape; a hole another
|
|
52955
|
+
// feature covers is kept whatever the distance test says. See the topological
|
|
52956
|
+
// polygon buffer, whose holes can be a neighbour's territory.
|
|
50626
52957
|
function filterOutlineArtifactHolesFromShape(bufShape, bufArcs, srcShape,
|
|
50627
|
-
srcArcs, distance, intervalPct, sagPct, oneSided, sideOpts, spherical
|
|
52958
|
+
srcArcs, distance, intervalPct, sagPct, oneSided, sideOpts, spherical,
|
|
52959
|
+
coverageIndex) {
|
|
50628
52960
|
var sourceSegments = null;
|
|
50629
52961
|
var sourceParts = null;
|
|
50630
52962
|
var shape2 = (bufShape || []).filter(function(path) {
|
|
@@ -50637,6 +52969,9 @@ ${svg}
|
|
|
50637
52969
|
return false;
|
|
50638
52970
|
}
|
|
50639
52971
|
if (!isHole) return true;
|
|
52972
|
+
if (coverageIndex && holeIsNeighborTerritory(path, bufArcs, coverageIndex)) {
|
|
52973
|
+
return true;
|
|
52974
|
+
}
|
|
50640
52975
|
if (oneSided) {
|
|
50641
52976
|
if (!sourceParts) {
|
|
50642
52977
|
sourceParts = getSourcePathParts(srcShape, srcArcs, spherical);
|
|
@@ -52767,6 +55102,43 @@ ${svg}
|
|
|
52767
55102
|
// floor/maxSpacing ratio at which small mosaics already sample cleanly.
|
|
52768
55103
|
var FLOOR_DISTANCE_FRACTION = 0.1;
|
|
52769
55104
|
|
|
55105
|
+
// Soft cap on the number of entries in the boundary-segment grid, used to floor
|
|
55106
|
+
// the cell size (see buildSegmentGrid). The cell otherwise tracks the buffer
|
|
55107
|
+
// distance alone, so a small distance on a large input gives cells far shorter
|
|
55108
|
+
// than a single boundary segment and the index grows in inverse proportion to
|
|
55109
|
+
// the distance: a nationwide mosaic buffered at 1m spans ~1e7 cells, which costs
|
|
55110
|
+
// hundreds of megabytes and seconds of build time before any distance is
|
|
55111
|
+
// measured. A wider cell is always correct -- the 3x3 query window only requires
|
|
55112
|
+
// cell >= reach -- it just puts more segments in each bucket, so the budget
|
|
55113
|
+
// trades index size against bucket scan length. This value keeps the average
|
|
55114
|
+
// bucket near one segment on a nationwide mosaic, so the queries stay as cheap as
|
|
55115
|
+
// they are at large buffer distances.
|
|
55116
|
+
var GRID_INSERTION_BUDGET = 5e5;
|
|
55117
|
+
|
|
55118
|
+
// Rungs of the widening box search in treeGapAtPoint, and the factor between
|
|
55119
|
+
// them: four rungs a factor of 16 apart, so the search starts at 1/4096 of the
|
|
55120
|
+
// reach and climbs to it in three more steps. The step is coarse deliberately.
|
|
55121
|
+
// The rungs cost a geometric series dominated by the last, so a query that has
|
|
55122
|
+
// to climb the whole ladder pays only a few percent more than one that jumps
|
|
55123
|
+
// straight to the top, while a short ladder keeps the fixed per-search cost
|
|
55124
|
+
// down. Both numbers are fixed, and the last rung is always the full reach,
|
|
55125
|
+
// which is what bounds the query: it makes at most LADDER_RUNGS searches
|
|
55126
|
+
// whatever the input, and cannot widen past the reach.
|
|
55127
|
+
var LADDER_RUNGS = 4;
|
|
55128
|
+
var LADDER_STEP = 16;
|
|
55129
|
+
var LADDER_BASE = Math.pow(LADDER_STEP, LADDER_RUNGS - 1);
|
|
55130
|
+
|
|
55131
|
+
// Window size (segments in the 3x3 cell neighborhood) above which gapAtPoint
|
|
55132
|
+
// asks the tree instead of scanning. A scan rejects a segment belonging to the
|
|
55133
|
+
// query point's own feature in a couple of nanoseconds, so a window of a few
|
|
55134
|
+
// thousand still costs about what one tree query does -- that equivalence is
|
|
55135
|
+
// what the threshold marks, and below it the scan wins on fixed costs alone.
|
|
55136
|
+
// Purely a speed tradeoff: the two paths return the same distance, so this can
|
|
55137
|
+
// be retuned freely. Measured on two county mosaics, scanning up to this size
|
|
55138
|
+
// left the cheapest cases at their original speed while still handing over
|
|
55139
|
+
// early enough to keep the widest buffers off the scan's quadratic path.
|
|
55140
|
+
var WINDOW_SCAN_LIMIT = 3000;
|
|
55141
|
+
|
|
52770
55142
|
// Soft target total site count. coarsen scales the gap-proportional spacing up
|
|
52771
55143
|
// until the predicted total falls under this, keeping the Delaunay bounded on
|
|
52772
55144
|
// dense shared-border mosaics while leaving sparse inputs at coarsen=1 (fully
|
|
@@ -52793,10 +55165,18 @@ ${svg}
|
|
|
52793
55165
|
profileStart('medial:assembleChains');
|
|
52794
55166
|
var chains = assembleChains(medial.segments, medial.coords);
|
|
52795
55167
|
profileEnd('medial:assembleChains');
|
|
55168
|
+
profileStart('medial:recenter');
|
|
55169
|
+
chains = chains.map(function(chain) {
|
|
55170
|
+
return recenterMedialChain(chain, sites.grid);
|
|
55171
|
+
});
|
|
55172
|
+
profileEnd('medial:recenter');
|
|
52796
55173
|
if (opts.smooth) {
|
|
52797
55174
|
profileStart('medial:smooth');
|
|
52798
55175
|
chains = chains.map(function(chain) {
|
|
52799
|
-
|
|
55176
|
+
// Smoothing uses one kernel width for the whole chain (see
|
|
55177
|
+
// smoothMedialChain), so on a channel whose width varies it can bow the
|
|
55178
|
+
// line out of the narrow stretches; re-center again to pull it back.
|
|
55179
|
+
return recenterMedialChain(smoothMedialChain(chain, sites.grid), sites.grid);
|
|
52800
55180
|
});
|
|
52801
55181
|
profileEnd('medial:smooth');
|
|
52802
55182
|
}
|
|
@@ -52814,7 +55194,9 @@ ${svg}
|
|
|
52814
55194
|
for (var di = 0; di < coordDistances.length; di++) {
|
|
52815
55195
|
if (coordDistances[di] > extendDist) extendDist = coordDistances[di];
|
|
52816
55196
|
}
|
|
52817
|
-
|
|
55197
|
+
// Local gap partitioning joins multi-owner junctions before extending only
|
|
55198
|
+
// the remaining outer endpoints; it opts out of this blanket extension.
|
|
55199
|
+
if (extendDist > 0 && !opts.no_extend) {
|
|
52818
55200
|
chains = chains.map(function(chain) {
|
|
52819
55201
|
return extendChainEndpoints(chain, extendDist);
|
|
52820
55202
|
});
|
|
@@ -53036,6 +55418,150 @@ ${svg}
|
|
|
53036
55418
|
return Math.sqrt(best);
|
|
53037
55419
|
}
|
|
53038
55420
|
|
|
55421
|
+
// A medial vertex counts as off-center when its distance to the nearer bank
|
|
55422
|
+
// falls below this fraction of its distance to the opposite bank (1 = exactly
|
|
55423
|
+
// centered). Where a channel narrows past the site spacing the sampled Voronoi
|
|
55424
|
+
// can no longer resolve it: the triangles spanning the neck are slivers whose
|
|
55425
|
+
// circumcenters land on, or beyond, one bank, so the assembled chain zigzags
|
|
55426
|
+
// from side to side instead of running down the middle. Finer sampling cannot
|
|
55427
|
+
// cure this -- a channel that pinches to a point has zero width while the
|
|
55428
|
+
// spacing has a floor (see spacingFromGap) -- so the geometry is corrected
|
|
55429
|
+
// afterwards instead. 0.5 leaves the well-sampled interior (measured balance
|
|
55430
|
+
// ~0.9) untouched and catches the zigzag (~0.1 and below).
|
|
55431
|
+
var MEDIAL_BALANCE_TOLERANCE = 0.5;
|
|
55432
|
+
|
|
55433
|
+
// Pull medial vertices that are off-center, or have slipped inside a source
|
|
55434
|
+
// polygon, back onto the local centerline: each is replaced by the midpoint of
|
|
55435
|
+
// its nearest footpoint on either bank. That midpoint is equidistant from the two
|
|
55436
|
+
// banks by construction and lies between them, so it stays inside the channel
|
|
55437
|
+
// however narrow it gets; where the channel pinches shut both footpoints converge
|
|
55438
|
+
// on the neck and the line passes straight through it rather than bouncing off
|
|
55439
|
+
// the sides.
|
|
55440
|
+
//
|
|
55441
|
+
// Valid vertices are left exactly as sampled, so a well-resolved channel keeps
|
|
55442
|
+
// its true Voronoi geometry. Chain endpoints are also left alone: they sit
|
|
55443
|
+
// outside the overlap on purpose (hull rays, junction ends) so the cut-line can
|
|
55444
|
+
// reach the enclosing boundary, and re-centering them would pull the cut back
|
|
55445
|
+
// inside the tile it has to span.
|
|
55446
|
+
function recenterMedialChain(points, ctx) {
|
|
55447
|
+
if (!ctx || points.length < 3) return points;
|
|
55448
|
+
points = straightenChainTails(points, ctx);
|
|
55449
|
+
if (points.length < 3) return points;
|
|
55450
|
+
var out = [points[0]];
|
|
55451
|
+
for (var i = 1; i < points.length - 1; i++) {
|
|
55452
|
+
var p = recenteredVertex(ctx, points[i]);
|
|
55453
|
+
// Successive off-center vertices can re-center onto the same neck point;
|
|
55454
|
+
// keep one copy so the chain has no zero-length segments.
|
|
55455
|
+
if (!samePoint(p, out[out.length - 1])) out.push(p);
|
|
55456
|
+
}
|
|
55457
|
+
var last = points[points.length - 1];
|
|
55458
|
+
if (!samePoint(last, out[out.length - 1])) out.push(last);
|
|
55459
|
+
return out.length >= 2 ? out : points;
|
|
55460
|
+
}
|
|
55461
|
+
|
|
55462
|
+
// Straighten the runs of vertices at each end of a chain that lie inside a
|
|
55463
|
+
// source polygon. Such a vertex cannot be a medial point (its distance to that
|
|
55464
|
+
// polygon's boundary would be 0), and it cannot be re-centered either: these
|
|
55465
|
+
// runs trail past the mouth of a gap, where the banks converge into a border the
|
|
55466
|
+
// medial construction deliberately does not see (a shared arc partitions any
|
|
55467
|
+
// overlap by itself, so collectCandidateArcPaths prunes it), leaving no channel
|
|
55468
|
+
// to center them in. They are not dropped, because a chain has to poke out past
|
|
55469
|
+
// the gap it divides to node against the enclosing boundary (see
|
|
55470
|
+
// extendChainEndpoints): the run is collapsed to one straight segment, which
|
|
55471
|
+
// keeps the chain's reach and loses only its wandering.
|
|
55472
|
+
function straightenChainTails(points, ctx) {
|
|
55473
|
+
var last = points.length - 1;
|
|
55474
|
+
var lo = 0, hi = last;
|
|
55475
|
+
while (lo < hi && insideAnyPolygon(ctx, points[lo])) lo++;
|
|
55476
|
+
while (hi > lo && insideAnyPolygon(ctx, points[hi])) hi--;
|
|
55477
|
+
if (hi - lo < 1) return points; // no interior vertex survives: leave it alone
|
|
55478
|
+
if (lo === 0 && hi === last) return points;
|
|
55479
|
+
var out = points.slice(lo, hi + 1);
|
|
55480
|
+
if (lo > 0) out.unshift(points[0]);
|
|
55481
|
+
if (hi < last) out.push(points[last]);
|
|
55482
|
+
return out;
|
|
55483
|
+
}
|
|
55484
|
+
|
|
55485
|
+
function insideAnyPolygon(ctx, p) {
|
|
55486
|
+
var near = nearestSegment(ctx, p[0], p[1], -1);
|
|
55487
|
+
return near.id !== -1 && insideOwnerPolygon(ctx.seg, near.id, p[0], p[1]);
|
|
55488
|
+
}
|
|
55489
|
+
|
|
55490
|
+
// @p re-centered between its two nearest banks, or @p itself when it is already
|
|
55491
|
+
// a valid centerline point or only one feature is in range.
|
|
55492
|
+
var footA = [0, 0], footB = [0, 0]; // scratch, not retained
|
|
55493
|
+
function recenteredVertex(ctx, p) {
|
|
55494
|
+
var near = nearestSegment(ctx, p[0], p[1], -1);
|
|
55495
|
+
if (near.id === -1) return p;
|
|
55496
|
+
var far = nearestSegment(ctx, p[0], p[1], ctx.seg.feat[near.id]);
|
|
55497
|
+
if (far.id === -1) return p;
|
|
55498
|
+
// Only a pair of banks within their combined buffer reach forms a channel this
|
|
55499
|
+
// vertex could be the centerline of. Past the mouth of a gap the opposite bank
|
|
55500
|
+
// can be arbitrarily far away, and pulling the vertex to the midpoint of that
|
|
55501
|
+
// pair would move it hundreds of metres off any boundary.
|
|
55502
|
+
if (far.dist > ctx.seg.reach[near.id] + ctx.seg.reach[far.id]) return p;
|
|
55503
|
+
var offCenter = near.dist < far.dist * MEDIAL_BALANCE_TOLERANCE;
|
|
55504
|
+
if (!offCenter && !insideOwnerPolygon(ctx.seg, near.id, p[0], p[1])) return p;
|
|
55505
|
+
segmentFoot(ctx.seg, near.id, p[0], p[1], footA);
|
|
55506
|
+
segmentFoot(ctx.seg, far.id, p[0], p[1], footB);
|
|
55507
|
+
return [(footA[0] + footB[0]) / 2, (footA[1] + footB[1]) / 2];
|
|
55508
|
+
}
|
|
55509
|
+
|
|
55510
|
+
// True when (x, y) lies on the interior side of indexed segment @s, i.e. inside
|
|
55511
|
+
// the feature that owns it. Source rings are traversed with the polygon interior
|
|
55512
|
+
// on their right, and seg.side records whether the stored segment direction
|
|
55513
|
+
// matches its owner's traversal, so the sign of the cross product places the
|
|
55514
|
+
// point. Judged against the point's nearest segment, this is the test that
|
|
55515
|
+
// catches a medial vertex thrown out of a pinching gap into a neighbouring
|
|
55516
|
+
// polygon -- distance cannot: such a vertex is a channel width behind one bank
|
|
55517
|
+
// and two channel widths from the other, the same ratio as a merely off-center
|
|
55518
|
+
// point still inside the channel. A true medial vertex is never inside a source
|
|
55519
|
+
// polygon (its distance to that polygon's boundary would be 0).
|
|
55520
|
+
function insideOwnerPolygon(seg, s, x, y) {
|
|
55521
|
+
var dx = seg.x1[s] - seg.x0[s], dy = seg.y1[s] - seg.y0[s];
|
|
55522
|
+
var cross = dx * (y - seg.y0[s]) - dy * (x - seg.x0[s]);
|
|
55523
|
+
return cross * seg.side[s] < 0;
|
|
55524
|
+
}
|
|
55525
|
+
|
|
55526
|
+
// The nearest indexed source segment to (x, y), skipping segments owned by
|
|
55527
|
+
// @excludeFeat (-1 excludes nothing). Probes the 3x3 grid-cell neighborhood
|
|
55528
|
+
// (cell == max reach). id is -1 when the window holds no eligible segment.
|
|
55529
|
+
function nearestSegment(ctx, x, y, excludeFeat) {
|
|
55530
|
+
var seg = ctx.seg;
|
|
55531
|
+
var cx = ctx.colOf(x), cy = ctx.rowOf(y);
|
|
55532
|
+
var bestId = -1, best = Infinity;
|
|
55533
|
+
for (var gx = cx - 1; gx <= cx + 1; gx++) {
|
|
55534
|
+
for (var gy = cy - 1; gy <= cy + 1; gy++) {
|
|
55535
|
+
var bucket = ctx.grid.get(ctx.cellKey(gx, gy));
|
|
55536
|
+
if (!bucket) continue;
|
|
55537
|
+
for (var b = 0; b < bucket.length; b++) {
|
|
55538
|
+
var s = bucket[b];
|
|
55539
|
+
if (seg.feat[s] === excludeFeat) continue;
|
|
55540
|
+
var d2 = pointSegDistSq2(x, y, seg.x0[s], seg.y0[s], seg.x1[s], seg.y1[s]);
|
|
55541
|
+
if (d2 < best) { best = d2; bestId = s; }
|
|
55542
|
+
}
|
|
55543
|
+
}
|
|
55544
|
+
}
|
|
55545
|
+
return {id: bestId, dist: Math.sqrt(best)};
|
|
55546
|
+
}
|
|
55547
|
+
|
|
55548
|
+
// Closest point to (x, y) on indexed segment @s, clamped to its endpoints,
|
|
55549
|
+
// written into @out.
|
|
55550
|
+
function segmentFoot(seg, s, x, y, out) {
|
|
55551
|
+
var ax = seg.x0[s], ay = seg.y0[s];
|
|
55552
|
+
var dx = seg.x1[s] - ax, dy = seg.y1[s] - ay;
|
|
55553
|
+
var len2 = dx * dx + dy * dy;
|
|
55554
|
+
var t = len2 > 0 ? ((x - ax) * dx + (y - ay) * dy) / len2 : 0;
|
|
55555
|
+
if (t < 0) t = 0;
|
|
55556
|
+
if (t > 1) t = 1;
|
|
55557
|
+
out[0] = ax + t * dx;
|
|
55558
|
+
out[1] = ay + t * dy;
|
|
55559
|
+
}
|
|
55560
|
+
|
|
55561
|
+
function samePoint(a, b) {
|
|
55562
|
+
return a[0] === b[0] && a[1] === b[1];
|
|
55563
|
+
}
|
|
55564
|
+
|
|
53039
55565
|
// Boundary sample spacing as a fraction of the local gap width: smaller gives a
|
|
53040
55566
|
// smoother medial axis (more sites) in narrow channels. 0.5 keeps the spacing
|
|
53041
55567
|
// at most half the gap, so a channel of width w has at least two samples per
|
|
@@ -53083,7 +55609,12 @@ ${svg}
|
|
|
53083
55609
|
if (f === -1 && r === -1) continue; // arc not used by a buffered feature
|
|
53084
55610
|
if (f !== -1 && r !== -1) continue; // shared interior border -- not a gap
|
|
53085
55611
|
var pts = arcCoords(arcs, i);
|
|
53086
|
-
|
|
55612
|
+
// forward records whether the owner traverses the arc in the stored
|
|
55613
|
+
// direction, which is what fixes the interior side of its segments (see
|
|
55614
|
+
// insideOwnerPolygon).
|
|
55615
|
+
if (pts.length >= 2) {
|
|
55616
|
+
paths.push({owner: f !== -1 ? f : r, points: pts, forward: f !== -1});
|
|
55617
|
+
}
|
|
53087
55618
|
}
|
|
53088
55619
|
return paths;
|
|
53089
55620
|
}
|
|
@@ -53136,8 +55667,15 @@ ${svg}
|
|
|
53136
55667
|
// in a single pass, then we densify once.
|
|
53137
55668
|
var grid = buildSegmentGrid$1(verts, coordDistances);
|
|
53138
55669
|
var gaps = computeVertexGaps(grid, verts, coordDistances);
|
|
53139
|
-
|
|
53140
|
-
|
|
55670
|
+
// No contested channel anywhere: every vertex is either out of reach of every
|
|
55671
|
+
// other feature (open coast) or within the touching threshold of one (where the
|
|
55672
|
+
// source boundary already partitions). densify + Delaunay would only allocate
|
|
55673
|
+
// sites that keptSites then discards, so stop here. Single-feature inputs and
|
|
55674
|
+
// mosaics whose features are all farther than a buffer-diameter apart take this
|
|
55675
|
+
// path; a mosaic with even one real gap continues.
|
|
55676
|
+
if (!hasContestedGap(gaps, maxDistance)) return null;
|
|
55677
|
+
var coarsen = fitCoarsen(verts, gaps, coordDistances, spacingFloor, grid);
|
|
55678
|
+
var sites = densifyVertices(verts, gaps, coordDistances, spacingFloor, coarsen, grid);
|
|
53141
55679
|
// Triangulate only the sites bordering a real gap. Its medial segments come
|
|
53142
55680
|
// exclusively from cross-feature edges, and the well-shaped triangles that
|
|
53143
55681
|
// bridge a gap have their apex within reach too (a far apex makes a thin
|
|
@@ -53154,65 +55692,232 @@ ${svg}
|
|
|
53154
55692
|
|
|
53155
55693
|
// Bucket every boundary segment into a uniform grid so the nearest cross-feature
|
|
53156
55694
|
// 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
|
|
55695
|
+
// neighborhood. The cell is at least the maximum reach (sum of the two largest
|
|
55696
|
+
// buffer distances), so any in-reach segment is guaranteed to fall in that 3x3
|
|
55697
|
+
// window. Returns null when there is no positive reach. Reused for both the
|
|
55698
|
+
// per-vertex gap (drives adaptive sampling) and the per-site keep test
|
|
55699
|
+
// (gapAtPoint).
|
|
53161
55700
|
function buildSegmentGrid$1(verts, coordDistances) {
|
|
53162
55701
|
var paths = verts.paths;
|
|
53163
55702
|
var maxDist = 0;
|
|
53164
55703
|
for (var d = 0; d < coordDistances.length; d++) {
|
|
53165
55704
|
if (coordDistances[d] > maxDist) maxDist = coordDistances[d];
|
|
53166
55705
|
}
|
|
53167
|
-
var
|
|
53168
|
-
if (!(
|
|
55706
|
+
var reach = 2 * maxDist; // upper bound on any pair's reach
|
|
55707
|
+
if (!(reach > 0)) return null;
|
|
53169
55708
|
var xmin = Infinity, ymin = Infinity, ymax = -Infinity;
|
|
55709
|
+
var totalLen = 0;
|
|
53170
55710
|
paths.forEach(function(path) {
|
|
53171
55711
|
var pts = path.points;
|
|
53172
55712
|
for (var i = 0; i < pts.length; i++) {
|
|
53173
55713
|
if (pts[i][0] < xmin) xmin = pts[i][0];
|
|
53174
55714
|
if (pts[i][1] < ymin) ymin = pts[i][1];
|
|
53175
55715
|
if (pts[i][1] > ymax) ymax = pts[i][1];
|
|
55716
|
+
if (i > 0) {
|
|
55717
|
+
var sdx = pts[i][0] - pts[i - 1][0];
|
|
55718
|
+
var sdy = pts[i][1] - pts[i - 1][1];
|
|
55719
|
+
totalLen += Math.sqrt(sdx * sdx + sdy * sdy);
|
|
55720
|
+
}
|
|
53176
55721
|
}
|
|
53177
55722
|
});
|
|
55723
|
+
// A segment occupies about len/cell cells, so the whole index is about
|
|
55724
|
+
// totalLen/cell entries; widening the cell to fit the budget bounds it (see
|
|
55725
|
+
// GRID_INSERTION_BUDGET). The reach is the floor, not the target: a cell
|
|
55726
|
+
// narrower than the reach would break the 3x3 query window.
|
|
55727
|
+
var cell = Math.max(reach, totalLen / GRID_INSERTION_BUDGET);
|
|
53178
55728
|
// +1 cell index shift keeps probed -1 neighbors non-negative; rowSpan packs
|
|
53179
55729
|
// (col, row) into a collision-free integer key.
|
|
53180
55730
|
var rowSpan = Math.floor((ymax - ymin) / cell) + 3;
|
|
53181
55731
|
function cellKey(cx, cy) { return (cx + 1) * rowSpan + (cy + 1); }
|
|
53182
55732
|
function colOf(x) { return Math.floor((x - xmin) / cell); }
|
|
53183
55733
|
function rowOf(y) { return Math.floor((y - ymin) / cell); }
|
|
53184
|
-
var seg = {x0: [], y0: [], x1: [], y1: [], feat: [], reach: []};
|
|
55734
|
+
var seg = {x0: [], y0: [], x1: [], y1: [], feat: [], reach: [], side: []};
|
|
53185
55735
|
var grid = new Map();
|
|
55736
|
+
|
|
55737
|
+
// Index a segment into every cell it crosses, column by column: within one
|
|
55738
|
+
// column the segment covers a single y-range (it is a straight line clipped to
|
|
55739
|
+
// that column's x-range), so the exact row span is two floor()s. This costs
|
|
55740
|
+
// O(len/cell) entries, where stamping the segment's bounding box instead costs
|
|
55741
|
+
// O((len/cell)^2) for a diagonal segment -- on a nationwide input buffered by a
|
|
55742
|
+
// few meters that quadratic term ran to ~1e9 entries and exceeded the runtime's
|
|
55743
|
+
// maximum Map size.
|
|
55744
|
+
function addSegment(ax, ay, bx, by, idx) {
|
|
55745
|
+
if (ax > bx) {
|
|
55746
|
+
var tx = ax; ax = bx; bx = tx;
|
|
55747
|
+
var ty = ay; ay = by; by = ty;
|
|
55748
|
+
}
|
|
55749
|
+
var dx = bx - ax, dy = by - ay;
|
|
55750
|
+
var cxa = colOf(ax), cxb = colOf(bx);
|
|
55751
|
+
for (var gx = cxa; gx <= cxb; gx++) {
|
|
55752
|
+
var yLo = ay, yHi = by;
|
|
55753
|
+
if (dx > 0) {
|
|
55754
|
+
// clip the segment to this column and take the y-range of the piece
|
|
55755
|
+
var xLo = Math.max(ax, xmin + gx * cell);
|
|
55756
|
+
var xHi = Math.min(bx, xmin + (gx + 1) * cell);
|
|
55757
|
+
yLo = ay + dy * (xLo - ax) / dx;
|
|
55758
|
+
yHi = ay + dy * (xHi - ax) / dx;
|
|
55759
|
+
}
|
|
55760
|
+
var gya = rowOf(Math.min(yLo, yHi));
|
|
55761
|
+
var gyb = rowOf(Math.max(yLo, yHi));
|
|
55762
|
+
for (var gy = gya; gy <= gyb; gy++) {
|
|
55763
|
+
var key = cellKey(gx, gy);
|
|
55764
|
+
var bucket = grid.get(key);
|
|
55765
|
+
if (bucket) bucket.push(idx); else grid.set(key, [idx]);
|
|
55766
|
+
}
|
|
55767
|
+
}
|
|
55768
|
+
}
|
|
55769
|
+
|
|
53186
55770
|
paths.forEach(function(path) {
|
|
53187
55771
|
var pts = path.points;
|
|
53188
55772
|
var feat = path.owner;
|
|
53189
|
-
var
|
|
55773
|
+
var featReach = coordDistances[feat];
|
|
55774
|
+
var side = path.forward === false ? -1 : 1;
|
|
53190
55775
|
for (var k = 0; k + 1 < pts.length; k++) {
|
|
53191
55776
|
var ax = pts[k][0], ay = pts[k][1], bx = pts[k + 1][0], by = pts[k + 1][1];
|
|
53192
55777
|
var idx = seg.feat.length;
|
|
53193
55778
|
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
|
-
}
|
|
55779
|
+
seg.feat.push(feat); seg.reach.push(featReach); seg.side.push(side);
|
|
55780
|
+
addSegment(ax, ay, bx, by, idx);
|
|
53204
55781
|
}
|
|
53205
55782
|
});
|
|
53206
|
-
return {seg: seg, grid: grid, cellKey: cellKey, colOf: colOf, rowOf: rowOf
|
|
55783
|
+
return {seg: seg, grid: grid, cellKey: cellKey, colOf: colOf, rowOf: rowOf,
|
|
55784
|
+
index: buildSegmentTree(seg), maxReach: maxDist};
|
|
55785
|
+
}
|
|
55786
|
+
|
|
55787
|
+
// R-tree over the same segments, used only by gapAtPoint and only where the
|
|
55788
|
+
// grid's query window has grown expensive (see there). The grid remains the
|
|
55789
|
+
// index of record: it answers every other probe in this file.
|
|
55790
|
+
function buildSegmentTree(seg) {
|
|
55791
|
+
var n = seg.feat.length;
|
|
55792
|
+
if (n === 0) return null;
|
|
55793
|
+
var index = new Flatbush(n);
|
|
55794
|
+
for (var i = 0; i < n; i++) {
|
|
55795
|
+
var x0 = seg.x0[i], x1 = seg.x1[i], y0 = seg.y0[i], y1 = seg.y1[i];
|
|
55796
|
+
index.add(Math.min(x0, x1), Math.min(y0, y1), Math.max(x0, x1), Math.max(y0, y1));
|
|
55797
|
+
}
|
|
55798
|
+
index.finish();
|
|
55799
|
+
return index;
|
|
53207
55800
|
}
|
|
53208
55801
|
|
|
53209
55802
|
// The channel width at (x, y): distance to the nearest different-feature segment
|
|
53210
55803
|
// within their combined reach, or Infinity if none. Works for any point, not
|
|
53211
55804
|
// just original vertices, so a long edge whose endpoints are out of reach but
|
|
53212
55805
|
// whose middle crosses a gap is still measured correctly at the interior sites.
|
|
55806
|
+
// Answered from whichever index is cheaper here. Both return the same number,
|
|
55807
|
+
// so the choice is a speed decision only: the window scan costs one pass over
|
|
55808
|
+
// the segments in the point's grid cells, which is nothing on a sparse
|
|
55809
|
+
// neighborhood but grows as the square of the buffer distance (the cell is
|
|
55810
|
+
// floored at the reach), while the tree search does not grow that way but pays
|
|
55811
|
+
// fixed costs on every call.
|
|
53213
55812
|
function gapAtPoint(ctx, x, y, feat, reachF) {
|
|
53214
|
-
|
|
53215
|
-
|
|
55813
|
+
var n = collectWindow(ctx, x, y);
|
|
55814
|
+
if (ctx.index && windowSize > WINDOW_SCAN_LIMIT) {
|
|
55815
|
+
return treeGapAtPoint(ctx, x, y, feat, reachF);
|
|
55816
|
+
}
|
|
55817
|
+
return scanWindow(ctx.seg, n, x, y, feat, reachF, Infinity);
|
|
55818
|
+
}
|
|
55819
|
+
|
|
55820
|
+
// Number of segments in the 3x3 cell window last collected by collectWindow.
|
|
55821
|
+
var windowSize = 0;
|
|
55822
|
+
// Scratch list of that window's buckets, reused across calls to keep the query
|
|
55823
|
+
// allocation-free. Safe because a query never re-enters gapAtPoint: the buckets
|
|
55824
|
+
// are consumed by scanWindow before anything else runs.
|
|
55825
|
+
var windowBuckets = [];
|
|
55826
|
+
|
|
55827
|
+
// Gather the buckets of the 3x3 cell neighborhood around (x, y) into the
|
|
55828
|
+
// scratch list, returning how many there are and setting windowSize to the
|
|
55829
|
+
// total segment count. The cell is at least the maximum reach, so this window
|
|
55830
|
+
// is guaranteed to hold every segment within reach of the point.
|
|
55831
|
+
function collectWindow(ctx, x, y) {
|
|
55832
|
+
var cx = ctx.colOf(x), cy = ctx.rowOf(y);
|
|
55833
|
+
var n = 0;
|
|
55834
|
+
windowSize = 0;
|
|
55835
|
+
for (var gx = cx - 1; gx <= cx + 1; gx++) {
|
|
55836
|
+
for (var gy = cy - 1; gy <= cy + 1; gy++) {
|
|
55837
|
+
var bucket = ctx.grid.get(ctx.cellKey(gx, gy));
|
|
55838
|
+
if (!bucket) continue;
|
|
55839
|
+
windowBuckets[n++] = bucket;
|
|
55840
|
+
windowSize += bucket.length;
|
|
55841
|
+
}
|
|
55842
|
+
}
|
|
55843
|
+
return n;
|
|
55844
|
+
}
|
|
55845
|
+
|
|
55846
|
+
// Exhaustive scan of the collected window: distance to the nearest
|
|
55847
|
+
// different-feature segment within their combined reach, or @best if none is
|
|
55848
|
+
// closer.
|
|
55849
|
+
function scanWindow(seg, n, x, y, feat, reachF, best) {
|
|
55850
|
+
for (var i = 0; i < n; i++) {
|
|
55851
|
+
var bucket = windowBuckets[i];
|
|
55852
|
+
for (var b = 0; b < bucket.length; b++) {
|
|
55853
|
+
var s = bucket[b];
|
|
55854
|
+
if (seg.feat[s] === feat) continue;
|
|
55855
|
+
var reach = reachF + seg.reach[s];
|
|
55856
|
+
var dsq = pointSegDistSq2(x, y, seg.x0[s], seg.y0[s], seg.x1[s], seg.y1[s]);
|
|
55857
|
+
if (dsq <= reach * reach) {
|
|
55858
|
+
var dist = Math.sqrt(dsq);
|
|
55859
|
+
if (dist < best) best = dist;
|
|
55860
|
+
}
|
|
55861
|
+
}
|
|
55862
|
+
}
|
|
55863
|
+
return best;
|
|
55864
|
+
}
|
|
55865
|
+
|
|
55866
|
+
// Same query answered from the R-tree, by widening a box search along a fixed
|
|
55867
|
+
// ladder of radii until the answer it holds is provably the global one.
|
|
55868
|
+
//
|
|
55869
|
+
// The invariant that makes an intermediate rung conclusive: a box of half-width
|
|
55870
|
+
// r contains the whole disc of radius r, so every segment closer than r to the
|
|
55871
|
+
// point is among the hits. If the nearest in-reach hit is itself within r, no
|
|
55872
|
+
// unreturned segment can beat it and the search can stop. Otherwise the answer
|
|
55873
|
+
// might lie in the annulus beyond r and the next rung has to look.
|
|
55874
|
+
//
|
|
55875
|
+
// The ladder is what keeps the cost sane in both directions. Starting small
|
|
55876
|
+
// makes the common case -- a point on one bank of a narrow gap, its neighbor a
|
|
55877
|
+
// few meters away -- cost one search of a tiny box, independent of the buffer
|
|
55878
|
+
// distance. Ending at the full reach makes the rare case -- a point with no
|
|
55879
|
+
// feature anywhere near it, so nothing can be proven short of exhausting the
|
|
55880
|
+
// reach -- cost the geometric sum of the rungs, a small multiple of going there
|
|
55881
|
+
// directly. The step is coarse so that there are only a handful of rungs.
|
|
55882
|
+
//
|
|
55883
|
+
// A previous attempt used neighbors() to take the k nearest instead. It was
|
|
55884
|
+
// much worse here, because a query that cannot find k in-reach candidates makes
|
|
55885
|
+
// neighbors() heap-walk every leaf in the whole reach disc -- and points with
|
|
55886
|
+
// no feature within reach are exactly what a coastline is made of.
|
|
55887
|
+
function treeGapAtPoint(ctx, x, y, feat, reachF) {
|
|
55888
|
+
var seg = ctx.seg;
|
|
55889
|
+
var index = ctx.index;
|
|
55890
|
+
// Nothing beyond the largest possible pair reach can pass the test below, so
|
|
55891
|
+
// the ladder tops out there rather than searching the whole extent.
|
|
55892
|
+
var maxDist = reachF + ctx.maxReach;
|
|
55893
|
+
var filter = function(s) { return seg.feat[s] !== feat; };
|
|
55894
|
+
var r = maxDist / LADDER_BASE;
|
|
55895
|
+
for (var k = 0; k < LADDER_RUNGS - 1; k++) {
|
|
55896
|
+
var best = nearestInBox(seg, index, x, y, r, reachF, filter);
|
|
55897
|
+
if (best <= r) return best;
|
|
55898
|
+
r *= LADDER_STEP;
|
|
55899
|
+
}
|
|
55900
|
+
// The top rung covers every segment that could be in reach at all, so its
|
|
55901
|
+
// answer is final even when nothing was found.
|
|
55902
|
+
return nearestInBox(seg, index, x, y, maxDist, reachF, filter);
|
|
55903
|
+
}
|
|
55904
|
+
|
|
55905
|
+
// Distance from (x, y) to the nearest segment that passes @filter and lies
|
|
55906
|
+
// within its pair reach, considering only segments whose bounding box meets the
|
|
55907
|
+
// box of half-width @r. Infinity if there is none.
|
|
55908
|
+
function nearestInBox(seg, index, x, y, r, reachF, filter) {
|
|
55909
|
+
var ids = index.search(x - r, y - r, x + r, y + r, filter);
|
|
55910
|
+
var best = Infinity;
|
|
55911
|
+
for (var i = 0; i < ids.length; i++) {
|
|
55912
|
+
var s = ids[i];
|
|
55913
|
+
var reach = reachF + seg.reach[s];
|
|
55914
|
+
var dsq = pointSegDistSq2(x, y, seg.x0[s], seg.y0[s], seg.x1[s], seg.y1[s]);
|
|
55915
|
+
if (dsq <= reach * reach) {
|
|
55916
|
+
var dist = Math.sqrt(dsq);
|
|
55917
|
+
if (dist < best) best = dist;
|
|
55918
|
+
}
|
|
55919
|
+
}
|
|
55920
|
+
return best;
|
|
53216
55921
|
}
|
|
53217
55922
|
|
|
53218
55923
|
// Local gap at each original vertex (drives adaptive sampling, see
|
|
@@ -53239,31 +55944,6 @@ ${svg}
|
|
|
53239
55944
|
return gaps;
|
|
53240
55945
|
}
|
|
53241
55946
|
|
|
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
55947
|
// True when medial vertex c lies in the buffer overlap of features fp and fq:
|
|
53268
55948
|
// within fp's radius of an fp-owned source segment AND within fq's radius of an
|
|
53269
55949
|
// fq-owned source segment. Measured against the actual source segments via the
|
|
@@ -53376,21 +56056,52 @@ ${svg}
|
|
|
53376
56056
|
return {paths: layout, count: count};
|
|
53377
56057
|
}
|
|
53378
56058
|
|
|
56059
|
+
// True when any vertex borders a real gap: within reach of another feature, but
|
|
56060
|
+
// farther than the touching threshold. Matching keptSites' keep rule, so when
|
|
56061
|
+
// this is false every densified site would be discarded and there is nothing for
|
|
56062
|
+
// the medial to cut.
|
|
56063
|
+
function hasContestedGap(gaps, maxDistance) {
|
|
56064
|
+
var touch = maxDistance * TOUCHING_GAP_FRACTION;
|
|
56065
|
+
for (var i = 0; i < gaps.length; i++) {
|
|
56066
|
+
if (isFinite(gaps[i]) && gaps[i] > touch) return true;
|
|
56067
|
+
}
|
|
56068
|
+
return false;
|
|
56069
|
+
}
|
|
56070
|
+
|
|
53379
56071
|
// The spacing for a path segment: the tighter of its two endpoints' gap-derived
|
|
53380
56072
|
// spacings (so a segment straddling a narrowing gap samples at the finer rate).
|
|
53381
|
-
|
|
53382
|
-
|
|
53383
|
-
|
|
53384
|
-
|
|
56073
|
+
// Infinity means "do not densify" (see spacingFromGap). When both endpoints are
|
|
56074
|
+
// open-coast but the segment is long enough that its middle could still pass
|
|
56075
|
+
// within reach of another feature, the midpoint is probed via @ctx so a
|
|
56076
|
+
// contested channel bounded by long unvertexed edges is not missed.
|
|
56077
|
+
function segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen, ctx) {
|
|
56078
|
+
var gA = gaps[path.vids[k]], gB = gaps[path.vids[k + 1]];
|
|
56079
|
+
var sA = spacingFromGap(gA, maxSpacing, spacingFloor, coarsen);
|
|
56080
|
+
var sB = spacingFromGap(gB, maxSpacing, spacingFloor, coarsen);
|
|
56081
|
+
var s = Math.min(sA, sB);
|
|
56082
|
+
if (isFinite(s) || !ctx) return s;
|
|
56083
|
+
// both endpoints open: only a long segment can hide a contested middle
|
|
56084
|
+
// (shorter than the reach, either endpoint would have seen it)
|
|
56085
|
+
var a = path.points[k], b = path.points[k + 1];
|
|
56086
|
+
var dx = b[0] - a[0], dy = b[1] - a[1];
|
|
56087
|
+
if (dx * dx + dy * dy <= 4 * maxSpacing * maxSpacing) return s;
|
|
56088
|
+
var midGap = gapAtPoint(ctx, (a[0] + b[0]) / 2, (a[1] + b[1]) / 2,
|
|
56089
|
+
path.owner, maxSpacing);
|
|
56090
|
+
return spacingFromGap(midGap, maxSpacing, spacingFloor, coarsen);
|
|
53385
56091
|
}
|
|
53386
56092
|
|
|
53387
56093
|
// Re-sample every candidate path: emit each original vertex (tagged with its
|
|
53388
56094
|
// vid) plus interior points spaced by the local gap-derived spacing (see
|
|
53389
56095
|
// segmentSpacing). Paths are open, so the last vertex has no following segment.
|
|
53390
|
-
//
|
|
53391
|
-
//
|
|
53392
|
-
//
|
|
53393
|
-
|
|
56096
|
+
// Open-coast segments (both endpoints out of reach of every other feature) are
|
|
56097
|
+
// not densified -- keptSites would discard those interior points, and densifying
|
|
56098
|
+
// them at spacing = buffer distance is what used to emit tens of millions of
|
|
56099
|
+
// sites along a nationwide coastline. A segment with one contested endpoint
|
|
56100
|
+
// still densifies at that endpoint's rate, so a channel that pinches shut is
|
|
56101
|
+
// sampled into its mouth. Long open-coast edges are midpoint-probed (see
|
|
56102
|
+
// segmentSpacing) so a contested channel whose bounding edges lack a vertex
|
|
56103
|
+
// within reach is still sampled.
|
|
56104
|
+
function densifyVertices(verts, gaps, coordDistances, spacingFloor, coarsen, ctx) {
|
|
53394
56105
|
var coords = [];
|
|
53395
56106
|
var owner = [];
|
|
53396
56107
|
var origin = []; // vid for original vertices, -1 for interpolated points
|
|
@@ -53404,10 +56115,11 @@ ${svg}
|
|
|
53404
56115
|
origin.push(path.vids[k]);
|
|
53405
56116
|
if (k + 1 >= m) continue; // open path: no segment past the last vertex
|
|
53406
56117
|
var b = path.points[k + 1];
|
|
53407
|
-
var s = segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen);
|
|
56118
|
+
var s = segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen, ctx);
|
|
53408
56119
|
var dx = b[0] - a[0], dy = b[1] - a[1];
|
|
53409
56120
|
var len = Math.sqrt(dx * dx + dy * dy);
|
|
53410
|
-
|
|
56121
|
+
// s == Infinity for open-coast segments (see spacingFromGap): skip
|
|
56122
|
+
if (s > 0 && isFinite(s) && len > s) {
|
|
53411
56123
|
var steps = Math.floor(len / s);
|
|
53412
56124
|
for (var t = 1; t <= steps; t++) {
|
|
53413
56125
|
var f = t / (steps + 1);
|
|
@@ -53421,8 +56133,15 @@ ${svg}
|
|
|
53421
56133
|
return {coords: coords, owner: owner, origin: origin};
|
|
53422
56134
|
}
|
|
53423
56135
|
|
|
56136
|
+
// Spacing used to densify a segment endpoint. Open coast (no other feature
|
|
56137
|
+
// within reach) returns Infinity so densifyVertices emits no interior points --
|
|
56138
|
+
// those sites cannot shape the medial and used to dominate the site count on
|
|
56139
|
+
// large inputs. Touching/coincident borders keep the coarse buffer-distance
|
|
56140
|
+
// spacing (the source boundary already partitions them; fine sampling would only
|
|
56141
|
+
// flood the triangulation with collinear sites). Real gaps densify at a fraction
|
|
56142
|
+
// of the local width, floored and capped.
|
|
53424
56143
|
function spacingFromGap(gap, maxSpacing, spacingFloor, coarsen) {
|
|
53425
|
-
if (!isFinite(gap)) return
|
|
56144
|
+
if (!isFinite(gap)) return Infinity;
|
|
53426
56145
|
// A gap at or below the buffer's positional tolerance means the two features
|
|
53427
56146
|
// effectively touch: there is no contested channel to run a medial down, and
|
|
53428
56147
|
// the shared source boundary already partitions the overlap. Densifying it
|
|
@@ -53440,18 +56159,18 @@ ${svg}
|
|
|
53440
56159
|
// interior points per segment). Pure counting, no Delaunay -- cheap enough to
|
|
53441
56160
|
// binary-search coarsen against. Counts pre-keep sites (the densification work),
|
|
53442
56161
|
// which is what coarsen actually bounds.
|
|
53443
|
-
function predictSiteCount(verts, gaps, coordDistances, spacingFloor, coarsen) {
|
|
56162
|
+
function predictSiteCount(verts, gaps, coordDistances, spacingFloor, coarsen, ctx) {
|
|
53444
56163
|
var total = verts.count; // every original vertex is emitted
|
|
53445
56164
|
verts.paths.forEach(function(path) {
|
|
53446
56165
|
var maxSpacing = coordDistances[path.owner];
|
|
53447
56166
|
var m = path.vids.length;
|
|
53448
56167
|
for (var k = 0; k + 1 < m; k++) {
|
|
53449
|
-
var s = segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen);
|
|
56168
|
+
var s = segmentSpacing(path, k, gaps, maxSpacing, spacingFloor, coarsen, ctx);
|
|
53450
56169
|
var a = path.points[k];
|
|
53451
56170
|
var b = path.points[k + 1];
|
|
53452
56171
|
var dx = b[0] - a[0], dy = b[1] - a[1];
|
|
53453
56172
|
var len = Math.sqrt(dx * dx + dy * dy);
|
|
53454
|
-
if (s > 0 && len > s) total += Math.floor(len / s);
|
|
56173
|
+
if (s > 0 && isFinite(s) && len > s) total += Math.floor(len / s);
|
|
53455
56174
|
}
|
|
53456
56175
|
});
|
|
53457
56176
|
return total;
|
|
@@ -53461,17 +56180,17 @@ ${svg}
|
|
|
53461
56180
|
// count decreases monotonically as coarsen grows (spacing widens), so binary
|
|
53462
56181
|
// search converges; capped because near-coincident gaps (gap ~ 0) can't be
|
|
53463
56182
|
// 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) {
|
|
56183
|
+
function fitCoarsen(verts, gaps, coordDistances, spacingFloor, ctx) {
|
|
56184
|
+
if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, 1, ctx) <= SITE_BUDGET) {
|
|
53466
56185
|
return 1;
|
|
53467
56186
|
}
|
|
53468
56187
|
var lo = 1, hi = 1024;
|
|
53469
|
-
if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, hi) > SITE_BUDGET) {
|
|
56188
|
+
if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, hi, ctx) > SITE_BUDGET) {
|
|
53470
56189
|
return hi; // even fully coarsened we can't fit; accept the floor-bounded count
|
|
53471
56190
|
}
|
|
53472
56191
|
for (var i = 0; i < 20; i++) {
|
|
53473
56192
|
var mid = (lo + hi) / 2;
|
|
53474
|
-
if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, mid) > SITE_BUDGET) {
|
|
56193
|
+
if (predictSiteCount(verts, gaps, coordDistances, spacingFloor, mid, ctx) > SITE_BUDGET) {
|
|
53475
56194
|
lo = mid;
|
|
53476
56195
|
} else {
|
|
53477
56196
|
hi = mid;
|
|
@@ -54586,6 +57305,10 @@ ${svg}
|
|
|
54586
57305
|
getArcClassifier: getArcClassifier
|
|
54587
57306
|
});
|
|
54588
57307
|
|
|
57308
|
+
// Buffer internals need a reliable interior probe, not a visually centered
|
|
57309
|
+
// label point. The bounded pole search avoids centroid2's second pass.
|
|
57310
|
+
var BUFFER_INNER_POINT_OPTS = {method: 'pole'};
|
|
57311
|
+
|
|
54589
57312
|
function makePolygonBuffer(lyr, dataset, opts) {
|
|
54590
57313
|
var spherical = isLatLngCRS(getDatasetCRS(dataset));
|
|
54591
57314
|
if (spherical && sourceHasCollapsingBandEdge(lyr, dataset)) {
|
|
@@ -54692,6 +57415,15 @@ ${svg}
|
|
|
54692
57415
|
}
|
|
54693
57416
|
var output = buildPolygonBufferOutput(lyr, dataset, opts);
|
|
54694
57417
|
var dataset2 = importGeoJSON(output.geojson, {type: 'polygon'});
|
|
57418
|
+
// A buffer is in the same CRS as its input, and the GeoJSON round trip above
|
|
57419
|
+
// does not carry one. Say so, because a caller that buffers this result again
|
|
57420
|
+
// has to parse a distance against it -- fill-gaps does exactly that when it
|
|
57421
|
+
// erodes the union of the dilated features. Without this, such a distance can
|
|
57422
|
+
// only be resolved for a lat-long input, where getDatasetCrsInfo guesses
|
|
57423
|
+
// wgs84 from the coordinate bounds; on projected input there is nothing to
|
|
57424
|
+
// guess from and the buffer failed with 'Unable to convert kilometers to
|
|
57425
|
+
// unknown coordinates'.
|
|
57426
|
+
setDatasetCrsInfo(dataset2, getDatasetCrsInfo(dataset));
|
|
54695
57427
|
if (spherical) {
|
|
54696
57428
|
splitAntimeridianBufferDataset(dataset2);
|
|
54697
57429
|
if (output.dissolveAfterSplit) {
|
|
@@ -54703,7 +57435,21 @@ ${svg}
|
|
|
54703
57435
|
lyr, dataset, opts, spherical, {
|
|
54704
57436
|
skipShape: function(shape, arcs) {
|
|
54705
57437
|
return shapeHasFillInsideHole(shape, arcs);
|
|
54706
|
-
}
|
|
57438
|
+
},
|
|
57439
|
+
// Under the topological path a hole is often a tile withheld from this
|
|
57440
|
+
// feature because it is a neighbour's territory (see
|
|
57441
|
+
// getTopologicalBufferTileIds). The distance test cannot tell those from
|
|
57442
|
+
// dissolve artifacts -- both lie inside this feature's own buffer -- so
|
|
57443
|
+
// it deleted them, giving this feature ground another feature also
|
|
57444
|
+
// claims. It got worse as the radius grew, since a bigger buffer leaves
|
|
57445
|
+
// a shallower ring behind: across 3108 US counties it double-covered
|
|
57446
|
+
// 7.6km2 at R=500m and 507km2 at R=5km, mostly Virginia's independent
|
|
57447
|
+
// cities inside the counties enclosing them, and on 01_thin_gap_detail2
|
|
57448
|
+
// at 5km it consumed one of the two features outright. Holes that no
|
|
57449
|
+
// other feature covers are still the filter's to judge.
|
|
57450
|
+
coverageIndex: opts.topological ?
|
|
57451
|
+
new PathIndex(dataset2.layers[0].shapes.filter(Boolean), dataset2.arcs) :
|
|
57452
|
+
null
|
|
54707
57453
|
});
|
|
54708
57454
|
}
|
|
54709
57455
|
cullSubTolerancePolygonArtifacts(dataset2, lyr, dataset, opts);
|
|
@@ -54878,17 +57624,30 @@ ${svg}
|
|
|
54878
57624
|
// Mouth-gating mask: closing of the land by the mouth radius r (dilate by r,
|
|
54879
57625
|
// union, erode by r). Built separately from the fill dilation because the
|
|
54880
57626
|
// mouth threshold stays at the mouth size while the fill reaches further.
|
|
57627
|
+
// Two settings keep the dilation from falling short at the tips of a hairline
|
|
57628
|
+
// gap, which is where the mouth of the fill ends up: whatever the dilation fails
|
|
57629
|
+
// to bridge there is left as a residual indentation at the mouth, in proportion
|
|
57630
|
+
// to r, and no later stage can recover it.
|
|
57631
|
+
// - circumscribe_joins: the round joins are otherwise inscribed in the true
|
|
57632
|
+
// circular offset and fall a join sagitta short of it (~0.5% of r at the
|
|
57633
|
+
// default 8 segments/quadrant). See makeInscribedRoundJoin: the correction
|
|
57634
|
+
// costs no vertices, in contrast to raising quad-segs, whose cost is linear
|
|
57635
|
+
// in vertices through every stage below (dilate, union, erode, mask).
|
|
57636
|
+
// - tolerance 0: the default pre-simplification budget is 1% of the radius,
|
|
57637
|
+
// several times a hairline gap's width, so it smooths away the very tips
|
|
57638
|
+
// being bridged (and undoes the join correction with them). Skipping it
|
|
57639
|
+
// costs only a few percent here, since the joins dominate the vertex count.
|
|
54881
57640
|
var dilatedAtR = makePolygonBuffer(lyr, dataset,
|
|
54882
|
-
Object.assign({}, baseOpts, {radius: mouthRadius, topological: false
|
|
57641
|
+
Object.assign({}, baseOpts, {radius: mouthRadius, topological: false,
|
|
57642
|
+
circumscribe_joins: true, tolerance: 0}));
|
|
54883
57643
|
var union = unionBufferDataset(dilatedAtR, baseOpts);
|
|
54884
57644
|
if (!union || !union.arcs) return dilated;
|
|
54885
57645
|
// tolerance: 0 disables the buffer's Douglas-Peucker pre-simplification for the
|
|
54886
57646
|
// erosion. Pre-simplifying before an INWARD offset can push a simplified concave
|
|
54887
57647
|
// vertex past its neighbours and self-intersect the eroded ring; on a large,
|
|
54888
57648
|
// 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.
|
|
57649
|
+
// wrong side and the ring collapses, wiping the gap-fill extent. (The dilation
|
|
57650
|
+
// skips pre-simplification for its own reason -- see above.)
|
|
54892
57651
|
var closing = makePolygonBuffer(union.layers[0], union,
|
|
54893
57652
|
Object.assign({}, baseOpts, {radius: '-' + mouthRadius, topological: false,
|
|
54894
57653
|
tolerance: 0}));
|
|
@@ -54946,7 +57705,7 @@ ${svg}
|
|
|
54946
57705
|
}
|
|
54947
57706
|
// hole: keep only if wide enough to hold a disk of radius keepRadius
|
|
54948
57707
|
var holeShape = [reversePath(path.ids.concat())];
|
|
54949
|
-
var anchor = findAnchorPoint(holeShape, arcs);
|
|
57708
|
+
var anchor = findAnchorPoint(holeShape, arcs, BUFFER_INNER_POINT_OPTS);
|
|
54950
57709
|
var radius = anchor ?
|
|
54951
57710
|
getPointToShapeDistance(anchor.x, anchor.y, holeShape, arcs) : 0;
|
|
54952
57711
|
if (radius >= keepRadius) {
|
|
@@ -54976,8 +57735,12 @@ ${svg}
|
|
|
54976
57735
|
// clears the arc/mouth tortuosity clause despite little depth. Requiring the patch
|
|
54977
57736
|
// depth to reach this fraction of the mouth radius drops both while keeping real
|
|
54978
57737
|
// 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
|
-
|
|
57738
|
+
// gaps reach depth/r >= 1.3, the shallow scallops only ~0.2). The margin matters,
|
|
57739
|
+
// because a coast-hugging strip only a few dozen metres thick can still measure a
|
|
57740
|
+
// large depth: depth is taken from the mouth CHORD, and that chord cuts straight
|
|
57741
|
+
// across whatever bay the strip follows. Those strips land around depth/r ~0.3, so
|
|
57742
|
+
// the floor sits above them rather than on them.
|
|
57743
|
+
var INLET_MIN_ABS_DEPTH = 0.4; // min patch depth as a fraction of the mouth radius
|
|
54981
57744
|
|
|
54982
57745
|
// Island-bridge classification of a fill that joins >= 2 source parts, one small
|
|
54983
57746
|
// (see bridgesSmallIsland). Two independent signatures mark an island bridge:
|
|
@@ -55135,9 +57898,14 @@ ${svg}
|
|
|
55135
57898
|
var islandDataset = importGeoJSON(
|
|
55136
57899
|
{type: 'GeometryCollection', geometries: islandGeoms}, {type: 'polygon'});
|
|
55137
57900
|
setDatasetCrsInfo(islandDataset, crsInfo);
|
|
57901
|
+
// circumscribe_joins matches the closing's dilation (see makeGapFillPolygonBuffer):
|
|
57902
|
+
// this buffer has to cancel the island's mouth-radius reach, so if it grew a join
|
|
57903
|
+
// sagitta less than that dilation did, a hairline of bridge would survive the
|
|
57904
|
+
// erase and reconnect the island.
|
|
55138
57905
|
var islandBuf = makePolygonBuffer(islandDataset.layers[0], islandDataset,
|
|
55139
57906
|
Object.assign({}, opts, {fill_gaps: false, max_widening: null,
|
|
55140
|
-
radius: mouthRadiusStr, topological: false, no_replace: true
|
|
57907
|
+
radius: mouthRadiusStr, topological: false, no_replace: true,
|
|
57908
|
+
circumscribe_joins: true}));
|
|
55141
57909
|
if (!islandBuf || !islandBuf.arcs) return;
|
|
55142
57910
|
var bridgeDataset = importGeoJSON(
|
|
55143
57911
|
{type: 'GeometryCollection', geometries: bridgeGeoms}, {type: 'polygon'});
|
|
@@ -55413,6 +58181,10 @@ ${svg}
|
|
|
55413
58181
|
if (coords.length === 0) return null;
|
|
55414
58182
|
var unionDataset = getBufferDataset(coords);
|
|
55415
58183
|
if (!unionDataset.arcs) return null;
|
|
58184
|
+
// getBufferDataset builds from bare coordinates, so carry the CRS across:
|
|
58185
|
+
// fill-gaps buffers this union again to erode it, and that needs a CRS to
|
|
58186
|
+
// resolve a distance given in real-world units.
|
|
58187
|
+
setDatasetCrsInfo(unionDataset, getDatasetCrsInfo(dataset));
|
|
55416
58188
|
dissolveBufferDataset2(unionDataset, Object.assign({}, opts, {winding_fill: false}));
|
|
55417
58189
|
return unionDataset;
|
|
55418
58190
|
}
|
|
@@ -56193,22 +58965,22 @@ ${svg}
|
|
|
56193
58965
|
var sourceIdIndex = getIdLookup(sourceIds);
|
|
56194
58966
|
var bufferIdIndex = getIdToFeatureIdLookup(bufferIds);
|
|
56195
58967
|
var sourceAreas = getSourceShapeAreas(shapes, sourceArcs);
|
|
56196
|
-
var sourceInteriorPoints = getSourceInteriorPoints(shapes, sourceArcs);
|
|
56197
58968
|
var ownerCtx = createTileOwnerContext(shapes, sourceArcs, sourceAreas,
|
|
56198
58969
|
mosaicIndex.nodes.arcs);
|
|
56199
58970
|
profileStart('topo:assignTiles');
|
|
56200
58971
|
var result = shapes.map(function(shape, i) {
|
|
56201
58972
|
var distance = distances[i];
|
|
56202
|
-
var tileIds
|
|
58973
|
+
var tileIds;
|
|
56203
58974
|
if (!distance || !shape) return null;
|
|
56204
58975
|
tileIds = getTopologicalBufferTileIds(sourceIds[i], bufferIds[i],
|
|
56205
58976
|
i, mosaicIndex, sourceIdIndex, bufferIdIndex, ownerCtx);
|
|
56206
|
-
|
|
56207
|
-
//
|
|
56208
|
-
//
|
|
56209
|
-
// the
|
|
56210
|
-
|
|
56211
|
-
|
|
58977
|
+
// No artifact-hole filtering here. Every hole in this geometry is a tile
|
|
58978
|
+
// withheld from this feature on purpose (see getTopologicalBufferTileIds),
|
|
58979
|
+
// so there is nothing for a single-shape heuristic to clean up and it can
|
|
58980
|
+
// only do harm: the interior-point territory test it used to run missed the
|
|
58981
|
+
// holes it could not sample, and filling those handed a neighbour's ground
|
|
58982
|
+
// to this feature -- 18.6 km2 of it across 3108 US counties at R=5km.
|
|
58983
|
+
return getTileIdsGeometry(tileIds, mosaicIndex, pathfind);
|
|
56212
58984
|
});
|
|
56213
58985
|
profileEnd('topo:assignTiles');
|
|
56214
58986
|
return result;
|
|
@@ -56404,7 +59176,7 @@ ${svg}
|
|
|
56404
59176
|
function getTileAnchorPoint(tileId, mosaicIndex, ownerCtx) {
|
|
56405
59177
|
if (tileId in ownerCtx.anchorCache) return ownerCtx.anchorCache[tileId];
|
|
56406
59178
|
var tile = mosaicIndex.mosaic[tileId];
|
|
56407
|
-
var p = findAnchorPoint(tile, ownerCtx.mosaicArcs) ||
|
|
59179
|
+
var p = findAnchorPoint(tile, ownerCtx.mosaicArcs, BUFFER_INNER_POINT_OPTS) ||
|
|
56408
59180
|
getPathCentroid(tile[0], ownerCtx.mosaicArcs) || null;
|
|
56409
59181
|
ownerCtx.anchorCache[tileId] = p;
|
|
56410
59182
|
return p;
|
|
@@ -56427,22 +59199,6 @@ ${svg}
|
|
|
56427
59199
|
});
|
|
56428
59200
|
}
|
|
56429
59201
|
|
|
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
59202
|
function getIdLookup(ids) {
|
|
56447
59203
|
var index = [];
|
|
56448
59204
|
ids.forEach(function(id) {
|
|
@@ -56654,11 +59410,7 @@ ${svg}
|
|
|
56654
59410
|
return false;
|
|
56655
59411
|
}
|
|
56656
59412
|
|
|
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) {
|
|
59413
|
+
function removePositiveBufferArtifactHoles(geom, shape, arcs, distance) {
|
|
56662
59414
|
if (!geom) return null;
|
|
56663
59415
|
// Nothing to filter unless the result actually has interior rings. Skip the
|
|
56664
59416
|
// index build (filterArtifactHoles is itself a no-op on hole-free polygons,
|
|
@@ -56682,9 +59434,7 @@ ${svg}
|
|
|
56682
59434
|
shapeIndex: buildShapeSegmentIndex(shape, arcs),
|
|
56683
59435
|
pathIndex: shape && shape.length > 0 ? new PathIndex([shape], arcs) : null,
|
|
56684
59436
|
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
|
|
59437
|
+
buildShapeSegmentIndex(sourceHoles.map(function(h) {return h[0];}), arcs) : null
|
|
56688
59438
|
};
|
|
56689
59439
|
if (geom.type == 'Polygon') {
|
|
56690
59440
|
geom.coordinates = filterArtifactHoles(geom.coordinates, minHoleArea, ctx);
|
|
@@ -56794,63 +59544,11 @@ ${svg}
|
|
|
56794
59544
|
function filterArtifactHoles(polygon, minHoleArea, ctx) {
|
|
56795
59545
|
if (polygon.length < 2) return polygon;
|
|
56796
59546
|
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
59547
|
return Math.abs(getGeoJSONRingArea(ring)) > minHoleArea &&
|
|
56801
59548
|
!positiveBufferHoleIsArtifact(ring, ctx);
|
|
56802
59549
|
}));
|
|
56803
59550
|
}
|
|
56804
59551
|
|
|
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
59552
|
function positiveBufferHoleIsArtifact(ring, ctx) {
|
|
56855
59553
|
var n = ring.length - 1; // skip duplicate endpoint
|
|
56856
59554
|
var step = Math.max(1, Math.floor(n / 20));
|
|
@@ -57753,7 +60451,7 @@ ${svg}
|
|
|
57753
60451
|
debug('Short ring', coords);
|
|
57754
60452
|
return;
|
|
57755
60453
|
}
|
|
57756
|
-
if (!samePoint(coords[0], lastEl(coords))) {
|
|
60454
|
+
if (!samePoint$1(coords[0], lastEl(coords))) {
|
|
57757
60455
|
error('Open polygon ring');
|
|
57758
60456
|
}
|
|
57759
60457
|
rings.push(coords); // accumulate rings
|
|
@@ -64030,6 +66728,312 @@ ${svg}
|
|
|
64030
66728
|
// message('[comment]', opts.message);
|
|
64031
66729
|
}; // no-op, so -comment doesn't trigger a parsing error
|
|
64032
66730
|
|
|
66731
|
+
// Close the existing directed contour paths against a fixed raster/nodata
|
|
66732
|
+
// boundary, polygonize the resulting network and dissolve its tiles by range.
|
|
66733
|
+
// This deliberately uses the isoline tracer's conservative validity rule: a
|
|
66734
|
+
// lattice cell is excluded when any of its four sample corners is invalid.
|
|
66735
|
+
function buildClosedContourDataset(contourDataset, grid, contours) {
|
|
66736
|
+
var domain = getContourCellDomain(grid, contours.band);
|
|
66737
|
+
var breaks = getClosedContourBreaks(contours.levels, contours.range);
|
|
66738
|
+
var boundaryDataset, dataset, contourLyr, nodes, arcInfo, mosaic;
|
|
66739
|
+
var tileLyr, outputLyr;
|
|
66740
|
+
if (breaks.length === 0 || domain.activeCount === 0) {
|
|
66741
|
+
return createEmptyClosedContourDataset(contourDataset.info);
|
|
66742
|
+
}
|
|
66743
|
+
boundaryDataset = importGeoJSON(getBoundaryGeoJSON(traceContourDomainRings(domain)), {});
|
|
66744
|
+
boundaryDataset.info = contourDataset.info;
|
|
66745
|
+
dataset = mergeDatasets([contourDataset, boundaryDataset]);
|
|
66746
|
+
contourLyr = dataset.layers[0];
|
|
66747
|
+
nodes = addIntersectionCuts(dataset, {no_snap: true});
|
|
66748
|
+
arcInfo = indexContourArcs(contourLyr);
|
|
66749
|
+
mosaic = buildPolygonMosaic(nodes).mosaic;
|
|
66750
|
+
tileLyr = createBandTileLayer(mosaic, dataset.arcs, arcInfo, domain, grid,
|
|
66751
|
+
contours.band, breaks);
|
|
66752
|
+
dataset.layers = [tileLyr];
|
|
66753
|
+
if (tileLyr.shapes.length === 0) {
|
|
66754
|
+
cleanArcReferences(dataset);
|
|
66755
|
+
return dataset;
|
|
66756
|
+
}
|
|
66757
|
+
outputLyr = dissolvePolygonLayer2(tileLyr, dataset, {
|
|
66758
|
+
fields: ['lower', 'upper'],
|
|
66759
|
+
quiet: true
|
|
66760
|
+
});
|
|
66761
|
+
dataset.layers = [outputLyr];
|
|
66762
|
+
cleanArcReferences(dataset);
|
|
66763
|
+
return dataset;
|
|
66764
|
+
}
|
|
66765
|
+
|
|
66766
|
+
function getClosedContourBreaks(levels, range) {
|
|
66767
|
+
if (!range || !isFinite(range.min) || !isFinite(range.max)) return [];
|
|
66768
|
+
if (range.min === range.max) return [range.min, range.max];
|
|
66769
|
+
return [range.min].concat(levels.filter(function(level) {
|
|
66770
|
+
return level > range.min && level < range.max;
|
|
66771
|
+
}), [range.max]);
|
|
66772
|
+
}
|
|
66773
|
+
|
|
66774
|
+
function getContourCellDomain(grid, band) {
|
|
66775
|
+
var W = grid.width;
|
|
66776
|
+
var H = grid.height;
|
|
66777
|
+
var cellW = W - 1;
|
|
66778
|
+
var cellH = H - 1;
|
|
66779
|
+
var samples = grid.samples;
|
|
66780
|
+
var bands = grid.bands;
|
|
66781
|
+
var valid = getRasterValidityMask(grid);
|
|
66782
|
+
var rowStride = W * bands;
|
|
66783
|
+
var active = new Uint8Array(Math.max(0, cellW * cellH));
|
|
66784
|
+
var activeCount = 0;
|
|
66785
|
+
var cx, cy, pixelId, off;
|
|
66786
|
+
for (cy = 0; cy < cellH; cy++) {
|
|
66787
|
+
for (cx = 0; cx < cellW; cx++) {
|
|
66788
|
+
pixelId = cy * W + cx;
|
|
66789
|
+
if (valid && (!valid[pixelId] || !valid[pixelId + 1] ||
|
|
66790
|
+
!valid[pixelId + W] || !valid[pixelId + W + 1])) continue;
|
|
66791
|
+
off = pixelId * bands + band;
|
|
66792
|
+
if (samples[off] !== samples[off] ||
|
|
66793
|
+
samples[off + bands] !== samples[off + bands] ||
|
|
66794
|
+
samples[off + rowStride] !== samples[off + rowStride] ||
|
|
66795
|
+
samples[off + rowStride + bands] !==
|
|
66796
|
+
samples[off + rowStride + bands]) continue;
|
|
66797
|
+
active[cy * cellW + cx] = 1;
|
|
66798
|
+
activeCount++;
|
|
66799
|
+
}
|
|
66800
|
+
}
|
|
66801
|
+
return {
|
|
66802
|
+
active: active,
|
|
66803
|
+
activeCount: activeCount,
|
|
66804
|
+
width: cellW,
|
|
66805
|
+
height: cellH,
|
|
66806
|
+
gridWidth: W,
|
|
66807
|
+
gridHeight: H,
|
|
66808
|
+
bbox: grid.bbox
|
|
66809
|
+
};
|
|
66810
|
+
}
|
|
66811
|
+
|
|
66812
|
+
function pointIsInContourDomain(x, y, domain) {
|
|
66813
|
+
var bbox = domain.bbox;
|
|
66814
|
+
var dx, dy, cx, cy;
|
|
66815
|
+
if (x < bbox[0] || x > bbox[2] || y < bbox[1] || y > bbox[3] ||
|
|
66816
|
+
domain.width < 1 || domain.height < 1) return false;
|
|
66817
|
+
dx = (bbox[2] - bbox[0]) / domain.gridWidth;
|
|
66818
|
+
dy = (bbox[3] - bbox[1]) / domain.gridHeight;
|
|
66819
|
+
cx = Math.floor((x - bbox[0]) / dx - 0.5);
|
|
66820
|
+
cy = Math.floor((bbox[3] - y) / dy - 0.5);
|
|
66821
|
+
if (cx < 0) cx = 0;
|
|
66822
|
+
if (cy < 0) cy = 0;
|
|
66823
|
+
if (cx >= domain.width) cx = domain.width - 1;
|
|
66824
|
+
if (cy >= domain.height) cy = domain.height - 1;
|
|
66825
|
+
return domain.active[cy * domain.width + cx] === 1;
|
|
66826
|
+
}
|
|
66827
|
+
|
|
66828
|
+
// Trace the union boundary of active contour cells. Segments are directed with
|
|
66829
|
+
// the active domain on their right, matching Mapshaper's clockwise outer-ring
|
|
66830
|
+
// convention. At a point-touch between diagonal components, taking the
|
|
66831
|
+
// rightmost continuation keeps the two rings separate.
|
|
66832
|
+
function traceContourDomainRings(domain) {
|
|
66833
|
+
var W = domain.width;
|
|
66834
|
+
var H = domain.height;
|
|
66835
|
+
var segments = [];
|
|
66836
|
+
var outgoing = new Map();
|
|
66837
|
+
var rings = [];
|
|
66838
|
+
var cx, cy, id, segment;
|
|
66839
|
+
function isActive(x, y) {
|
|
66840
|
+
return x >= 0 && x < W && y >= 0 && y < H &&
|
|
66841
|
+
domain.active[y * W + x] === 1;
|
|
66842
|
+
}
|
|
66843
|
+
function nodeId(x, y) {
|
|
66844
|
+
return y * (W + 1) + x;
|
|
66845
|
+
}
|
|
66846
|
+
function addSegment(x1, y1, x2, y2, dir) {
|
|
66847
|
+
var seg = {
|
|
66848
|
+
from: nodeId(x1, y1),
|
|
66849
|
+
to: nodeId(x2, y2),
|
|
66850
|
+
dir: dir,
|
|
66851
|
+
visited: false
|
|
66852
|
+
};
|
|
66853
|
+
segments.push(seg);
|
|
66854
|
+
if (!outgoing.has(seg.from)) outgoing.set(seg.from, []);
|
|
66855
|
+
outgoing.get(seg.from).push(seg);
|
|
66856
|
+
}
|
|
66857
|
+
for (cy = 0; cy < H; cy++) {
|
|
66858
|
+
for (cx = 0; cx < W; cx++) {
|
|
66859
|
+
if (!isActive(cx, cy)) continue;
|
|
66860
|
+
if (!isActive(cx, cy - 1)) addSegment(cx, cy, cx + 1, cy, 0);
|
|
66861
|
+
if (!isActive(cx + 1, cy)) addSegment(cx + 1, cy, cx + 1, cy + 1, 1);
|
|
66862
|
+
if (!isActive(cx, cy + 1)) addSegment(cx + 1, cy + 1, cx, cy + 1, 2);
|
|
66863
|
+
if (!isActive(cx - 1, cy)) addSegment(cx, cy + 1, cx, cy, 3);
|
|
66864
|
+
}
|
|
66865
|
+
}
|
|
66866
|
+
for (id = 0; id < segments.length; id++) {
|
|
66867
|
+
segment = segments[id];
|
|
66868
|
+
if (!segment.visited) rings.push(followBoundary(segment, outgoing, domain));
|
|
66869
|
+
}
|
|
66870
|
+
return rings;
|
|
66871
|
+
}
|
|
66872
|
+
|
|
66873
|
+
function followBoundary(first, outgoing, domain) {
|
|
66874
|
+
var ring = [getDomainNodeCoords(first.from, domain)];
|
|
66875
|
+
var current = first;
|
|
66876
|
+
var candidates, next;
|
|
66877
|
+
while (current && !current.visited) {
|
|
66878
|
+
current.visited = true;
|
|
66879
|
+
ring.push(getDomainNodeCoords(current.to, domain));
|
|
66880
|
+
candidates = outgoing.get(current.to) || [];
|
|
66881
|
+
next = chooseBoundaryContinuation(current, candidates);
|
|
66882
|
+
current = next;
|
|
66883
|
+
}
|
|
66884
|
+
return ring;
|
|
66885
|
+
}
|
|
66886
|
+
|
|
66887
|
+
function chooseBoundaryContinuation(current, candidates) {
|
|
66888
|
+
var priorities = [1, 0, 3, 2];
|
|
66889
|
+
var i, j, candidate, turn;
|
|
66890
|
+
for (i = 0; i < priorities.length; i++) {
|
|
66891
|
+
for (j = 0; j < candidates.length; j++) {
|
|
66892
|
+
candidate = candidates[j];
|
|
66893
|
+
if (candidate.visited) continue;
|
|
66894
|
+
turn = (candidate.dir - current.dir + 4) % 4;
|
|
66895
|
+
if (turn === priorities[i]) return candidate;
|
|
66896
|
+
}
|
|
66897
|
+
}
|
|
66898
|
+
return null;
|
|
66899
|
+
}
|
|
66900
|
+
|
|
66901
|
+
function getDomainNodeCoords(id, domain) {
|
|
66902
|
+
var W = domain.width;
|
|
66903
|
+
var xId = id % (W + 1);
|
|
66904
|
+
var yId = Math.floor(id / (W + 1));
|
|
66905
|
+
var bbox = domain.bbox;
|
|
66906
|
+
var dx = (bbox[2] - bbox[0]) / domain.gridWidth;
|
|
66907
|
+
var dy = (bbox[3] - bbox[1]) / domain.gridHeight;
|
|
66908
|
+
var x = xId === 0 ? bbox[0] :
|
|
66909
|
+
xId === W ? bbox[2] : bbox[0] + (xId + 0.5) * dx;
|
|
66910
|
+
var y = yId === 0 ? bbox[3] :
|
|
66911
|
+
yId === domain.height ? bbox[1] : bbox[3] - (yId + 0.5) * dy;
|
|
66912
|
+
return [x, y];
|
|
66913
|
+
}
|
|
66914
|
+
|
|
66915
|
+
function getBoundaryGeoJSON(rings) {
|
|
66916
|
+
return {
|
|
66917
|
+
type: 'FeatureCollection',
|
|
66918
|
+
features: rings.map(function(coords) {
|
|
66919
|
+
return {
|
|
66920
|
+
type: 'Feature',
|
|
66921
|
+
properties: {},
|
|
66922
|
+
geometry: {type: 'LineString', coordinates: coords}
|
|
66923
|
+
};
|
|
66924
|
+
})
|
|
66925
|
+
};
|
|
66926
|
+
}
|
|
66927
|
+
|
|
66928
|
+
function indexContourArcs(lyr) {
|
|
66929
|
+
var records = lyr.data ? lyr.data.getRecords() : [];
|
|
66930
|
+
var index = new Map();
|
|
66931
|
+
lyr.shapes.forEach(function(shape, shapeId) {
|
|
66932
|
+
var level = records[shapeId] && records[shapeId].value;
|
|
66933
|
+
if (!shape || typeof level != 'number') return;
|
|
66934
|
+
shape.forEach(function(path) {
|
|
66935
|
+
path.forEach(function(arcId) {
|
|
66936
|
+
var absId = arcId < 0 ? ~arcId : arcId;
|
|
66937
|
+
index.set(absId, {
|
|
66938
|
+
level: level,
|
|
66939
|
+
direction: arcId < 0 ? -1 : 1
|
|
66940
|
+
});
|
|
66941
|
+
});
|
|
66942
|
+
});
|
|
66943
|
+
});
|
|
66944
|
+
return index;
|
|
66945
|
+
}
|
|
66946
|
+
|
|
66947
|
+
function createBandTileLayer(mosaic, arcs, arcInfo, domain, grid, band, breaks) {
|
|
66948
|
+
var shapes = [];
|
|
66949
|
+
var records = [];
|
|
66950
|
+
mosaic.forEach(function(tile) {
|
|
66951
|
+
var anchor = findAnchorPoint(tile, arcs, {});
|
|
66952
|
+
var bounds;
|
|
66953
|
+
if (!anchor || !pointIsInContourDomain(anchor.x, anchor.y, domain)) return;
|
|
66954
|
+
bounds = getTileBandBounds(tile, arcInfo, breaks, grid, band, anchor);
|
|
66955
|
+
if (!bounds) return;
|
|
66956
|
+
shapes.push(tile);
|
|
66957
|
+
records.push(bounds);
|
|
66958
|
+
});
|
|
66959
|
+
return {
|
|
66960
|
+
geometry_type: 'polygon',
|
|
66961
|
+
shapes: shapes,
|
|
66962
|
+
data: new DataTable(records)
|
|
66963
|
+
};
|
|
66964
|
+
}
|
|
66965
|
+
|
|
66966
|
+
function getTileBandBounds(tile, arcInfo, breaks, grid, band, anchor) {
|
|
66967
|
+
var lower = breaks[0];
|
|
66968
|
+
var upper = breaks[breaks.length - 1];
|
|
66969
|
+
var sawLower = false;
|
|
66970
|
+
var sawUpper = false;
|
|
66971
|
+
tile.forEach(function(path) {
|
|
66972
|
+
path.forEach(function(arcId) {
|
|
66973
|
+
var absId = arcId < 0 ? ~arcId : arcId;
|
|
66974
|
+
var info = arcInfo.get(absId);
|
|
66975
|
+
var direction;
|
|
66976
|
+
if (!info) return;
|
|
66977
|
+
direction = arcId < 0 ? -1 : 1;
|
|
66978
|
+
// Contour paths are directed with the above-level side on their left.
|
|
66979
|
+
// Polygon rings keep their interior on the right, so following a contour
|
|
66980
|
+
// arc in its original direction means this tile is below that level.
|
|
66981
|
+
if (direction === info.direction) {
|
|
66982
|
+
if (!sawUpper || info.level < upper) upper = info.level;
|
|
66983
|
+
sawUpper = true;
|
|
66984
|
+
} else {
|
|
66985
|
+
if (!sawLower || info.level > lower) lower = info.level;
|
|
66986
|
+
sawLower = true;
|
|
66987
|
+
}
|
|
66988
|
+
});
|
|
66989
|
+
});
|
|
66990
|
+
if (areAdjacentBreaks(lower, upper, breaks)) {
|
|
66991
|
+
return {lower: lower, upper: upper};
|
|
66992
|
+
}
|
|
66993
|
+
return getSampleBandBounds(grid, band, anchor.x, anchor.y, breaks);
|
|
66994
|
+
}
|
|
66995
|
+
|
|
66996
|
+
function areAdjacentBreaks(lower, upper, breaks) {
|
|
66997
|
+
var i = breaks.indexOf(lower);
|
|
66998
|
+
return i > -1 && i + 1 < breaks.length && breaks[i + 1] === upper;
|
|
66999
|
+
}
|
|
67000
|
+
|
|
67001
|
+
function getSampleBandBounds(grid, band, x, y, breaks) {
|
|
67002
|
+
var bbox = grid.bbox;
|
|
67003
|
+
var col = Math.floor((x - bbox[0]) / (bbox[2] - bbox[0]) * grid.width);
|
|
67004
|
+
var row = Math.floor((bbox[3] - y) / (bbox[3] - bbox[1]) * grid.height);
|
|
67005
|
+
var value, i;
|
|
67006
|
+
if (col < 0) col = 0;
|
|
67007
|
+
if (row < 0) row = 0;
|
|
67008
|
+
if (col >= grid.width) col = grid.width - 1;
|
|
67009
|
+
if (row >= grid.height) row = grid.height - 1;
|
|
67010
|
+
value = grid.samples[(row * grid.width + col) * grid.bands + band];
|
|
67011
|
+
if (value !== value) return null;
|
|
67012
|
+
i = 0;
|
|
67013
|
+
while (i + 2 < breaks.length && value >= breaks[i + 1]) i++;
|
|
67014
|
+
return {lower: breaks[i], upper: breaks[i + 1]};
|
|
67015
|
+
}
|
|
67016
|
+
|
|
67017
|
+
function createEmptyClosedContourDataset(info) {
|
|
67018
|
+
return {
|
|
67019
|
+
info: info || {},
|
|
67020
|
+
layers: [{
|
|
67021
|
+
geometry_type: 'polygon',
|
|
67022
|
+
shapes: [],
|
|
67023
|
+
data: new DataTable([])
|
|
67024
|
+
}]
|
|
67025
|
+
};
|
|
67026
|
+
}
|
|
67027
|
+
|
|
67028
|
+
var RasterContourBands = /*#__PURE__*/Object.freeze({
|
|
67029
|
+
__proto__: null,
|
|
67030
|
+
buildClosedContourDataset: buildClosedContourDataset,
|
|
67031
|
+
getClosedContourBreaks: getClosedContourBreaks,
|
|
67032
|
+
getContourCellDomain: getContourCellDomain,
|
|
67033
|
+
pointIsInContourDomain: pointIsInContourDomain,
|
|
67034
|
+
traceContourDomainRings: traceContourDomainRings
|
|
67035
|
+
});
|
|
67036
|
+
|
|
64033
67037
|
// Trace contour lines (isolines) from a raster layer's working samples using
|
|
64034
67038
|
// marching squares.
|
|
64035
67039
|
//
|
|
@@ -64122,13 +67126,17 @@ ${svg}
|
|
|
64122
67126
|
|
|
64123
67127
|
function getRasterContourLines(raster, opts) {
|
|
64124
67128
|
var grid = getRasterGrid(raster);
|
|
64125
|
-
var band, levels;
|
|
67129
|
+
var band, levels, range;
|
|
64126
67130
|
validateRasterGridForContours(grid);
|
|
64127
67131
|
band = getContourBand(grid, opts);
|
|
64128
67132
|
levels = getContourLevels(grid, band, opts);
|
|
67133
|
+
range = getRasterSampleRange(grid, band);
|
|
64129
67134
|
return {
|
|
67135
|
+
band: band,
|
|
67136
|
+
range: range,
|
|
64130
67137
|
levels: levels,
|
|
64131
|
-
lines: traceRasterContours(grid, band, levels, getCornerClearance(opts)
|
|
67138
|
+
lines: traceRasterContours(grid, band, levels, getCornerClearance(opts),
|
|
67139
|
+
{extend_to_bbox: opts && opts.closed})
|
|
64132
67140
|
};
|
|
64133
67141
|
}
|
|
64134
67142
|
|
|
@@ -64267,7 +67275,7 @@ ${svg}
|
|
|
64267
67275
|
}
|
|
64268
67276
|
|
|
64269
67277
|
// clearance: see CORNER_CLEARANCE; 0 places crossings exactly.
|
|
64270
|
-
function traceRasterContours(grid, band, levels, clearance) {
|
|
67278
|
+
function traceRasterContours(grid, band, levels, clearance, opts) {
|
|
64271
67279
|
var segments = collectContourSegments(grid, band, levels);
|
|
64272
67280
|
var toMapXY = getLatticeToMapTransform(grid);
|
|
64273
67281
|
var hCount = (grid.width - 1) * grid.height;
|
|
@@ -64277,6 +67285,9 @@ ${svg}
|
|
|
64277
67285
|
stitchContourSegments(levelSegments).forEach(function(path) {
|
|
64278
67286
|
var coords = getPathCoords(path, grid, band, levels[i], hCount, toMapXY,
|
|
64279
67287
|
clearance);
|
|
67288
|
+
if (opts && opts.extend_to_bbox) {
|
|
67289
|
+
coords = extendContourPathToBBox(coords, path, grid, hCount);
|
|
67290
|
+
}
|
|
64280
67291
|
if (coords.length > 1) {
|
|
64281
67292
|
lines.push({value: levels[i], coords: coords});
|
|
64282
67293
|
}
|
|
@@ -64285,6 +67296,39 @@ ${svg}
|
|
|
64285
67296
|
return lines;
|
|
64286
67297
|
}
|
|
64287
67298
|
|
|
67299
|
+
// Closed contour bands use the raster bbox as their outer frame. Open contour
|
|
67300
|
+
// paths that reach the outer sample-center lattice are extended through the
|
|
67301
|
+
// half-pixel margin to that frame. Endpoints created by skipped nodata cells
|
|
67302
|
+
// lie on interior lattice edges and are intentionally left in place.
|
|
67303
|
+
function extendContourPathToBBox(coords, path, grid, hCount) {
|
|
67304
|
+
if (path.length < 2 || path[0] === path[path.length - 1]) return coords;
|
|
67305
|
+
var first = getOuterEdgePoint(coords[0], path[0], grid, hCount);
|
|
67306
|
+
var last = getOuterEdgePoint(coords[coords.length - 1],
|
|
67307
|
+
path[path.length - 1], grid, hCount);
|
|
67308
|
+
if (first) coords.unshift(first);
|
|
67309
|
+
if (last) coords.push(last);
|
|
67310
|
+
return coords;
|
|
67311
|
+
}
|
|
67312
|
+
|
|
67313
|
+
function getOuterEdgePoint(xy, edgeId, grid, hCount) {
|
|
67314
|
+
var W = grid.width;
|
|
67315
|
+
var H = grid.height;
|
|
67316
|
+
var bbox = grid.bbox;
|
|
67317
|
+
var x, y;
|
|
67318
|
+
if (edgeId < hCount) {
|
|
67319
|
+
y = Math.floor(edgeId / (W - 1));
|
|
67320
|
+
if (y === 0) return [xy[0], bbox[3]];
|
|
67321
|
+
if (y === H - 1) return [xy[0], bbox[1]];
|
|
67322
|
+
return null;
|
|
67323
|
+
}
|
|
67324
|
+
edgeId -= hCount;
|
|
67325
|
+
y = Math.floor(edgeId / W);
|
|
67326
|
+
x = edgeId - y * W;
|
|
67327
|
+
if (x === 0) return [bbox[0], xy[1]];
|
|
67328
|
+
if (x === W - 1) return [bbox[2], xy[1]];
|
|
67329
|
+
return null;
|
|
67330
|
+
}
|
|
67331
|
+
|
|
64288
67332
|
// Visit each cell once and march it for every level that falls inside its
|
|
64289
67333
|
// value range, rather than scanning the whole grid once per level.
|
|
64290
67334
|
function collectContourSegments(grid, band, levels) {
|
|
@@ -64505,6 +67549,21 @@ ${svg}
|
|
|
64505
67549
|
// Contours come from the layer's working samples, so they reflect any
|
|
64506
67550
|
// earlier edits (e.g. -blur, -clip) rather than the original source pixels.
|
|
64507
67551
|
contours = getRasterContourLines(targetLyr.raster, opts);
|
|
67552
|
+
if (opts.closed) {
|
|
67553
|
+
dataset = contours.lines.length > 0 ?
|
|
67554
|
+
importGeoJSON(getContoursGeoJSON(contours.lines, DEFAULT_CONTOUR_FIELD), {}) :
|
|
67555
|
+
createEmptyContourDataset();
|
|
67556
|
+
setDatasetCrsInfo(dataset, getDatasetCrsInfo(targetDataset));
|
|
67557
|
+
if (contours.lines.length > 0 && !opts.no_smoothing) {
|
|
67558
|
+
smoothContourDataset(dataset, getRasterGrid(targetLyr.raster));
|
|
67559
|
+
}
|
|
67560
|
+
dataset = buildClosedContourDataset(dataset, getRasterGrid(targetLyr.raster),
|
|
67561
|
+
contours);
|
|
67562
|
+
outputLayers = mergeDatasetsIntoDataset(targetDataset, [dataset]);
|
|
67563
|
+
setOutputLayerName(outputLayers[0], targetLyr, 'contours', opts);
|
|
67564
|
+
message(getClosedContoursMessage(outputLayers[0], contours));
|
|
67565
|
+
return outputLayers;
|
|
67566
|
+
}
|
|
64508
67567
|
message(getContoursMessage(contours));
|
|
64509
67568
|
if (contours.lines.length === 0) {
|
|
64510
67569
|
return [createEmptyContourLayer(targetLyr, opts)];
|
|
@@ -64588,6 +67647,17 @@ ${svg}
|
|
|
64588
67647
|
return lyr;
|
|
64589
67648
|
}
|
|
64590
67649
|
|
|
67650
|
+
function createEmptyContourDataset() {
|
|
67651
|
+
return {
|
|
67652
|
+
info: {},
|
|
67653
|
+
layers: [{
|
|
67654
|
+
geometry_type: 'polyline',
|
|
67655
|
+
shapes: [],
|
|
67656
|
+
data: new DataTable([])
|
|
67657
|
+
}]
|
|
67658
|
+
};
|
|
67659
|
+
}
|
|
67660
|
+
|
|
64591
67661
|
function getContoursMessage(contours) {
|
|
64592
67662
|
var levels = contours.levels;
|
|
64593
67663
|
if (levels.length === 0) {
|
|
@@ -64599,6 +67669,16 @@ ${svg}
|
|
|
64599
67669
|
' (' + levels[0] + ' to ' + levels[levels.length - 1] + ')';
|
|
64600
67670
|
}
|
|
64601
67671
|
|
|
67672
|
+
function getClosedContoursMessage(lyr, contours) {
|
|
67673
|
+
var count = lyr.shapes.length;
|
|
67674
|
+
if (count === 0) {
|
|
67675
|
+
return 'No closed contour bands were generated';
|
|
67676
|
+
}
|
|
67677
|
+
return 'Created ' + count + ' closed contour ' +
|
|
67678
|
+
(count === 1 ? 'band' : 'bands') + ' from ' + contours.levels.length +
|
|
67679
|
+
(contours.levels.length === 1 ? ' level' : ' levels');
|
|
67680
|
+
}
|
|
67681
|
+
|
|
64602
67682
|
cmd.dashlines = function(lyr, dataset, opts) {
|
|
64603
67683
|
var crs = getDatasetCRS(dataset);
|
|
64604
67684
|
var defs = getStashedVar('defs');
|
|
@@ -66130,7 +69210,7 @@ ${svg}
|
|
|
66130
69210
|
|
|
66131
69211
|
// Options that require the topology-repair algorithm and are not supported
|
|
66132
69212
|
// by the no-repair fast path.
|
|
66133
|
-
var REPAIR_REQUIRED_OPTS = ['gap_fill_area', 'sliver_control', 'allow_overlaps'];
|
|
69213
|
+
var REPAIR_REQUIRED_OPTS = ['gap_width', 'gap_fill_area', 'sliver_control', 'allow_overlaps'];
|
|
66134
69214
|
|
|
66135
69215
|
// Sample size used to detect intersections in no-repair mode. Detection stops
|
|
66136
69216
|
// after this many hits, so the warning message can include sample locations
|
|
@@ -70215,13 +73295,33 @@ ${svg}
|
|
|
70215
73295
|
}
|
|
70216
73296
|
|
|
70217
73297
|
function pointsFromPolygons(lyr, arcs, opts) {
|
|
70218
|
-
var
|
|
73298
|
+
var innerOpts = opts.inner ? getInnerPointOptions(opts) : null;
|
|
70219
73299
|
return lyr.shapes.map(function(shp) {
|
|
70220
|
-
var p =
|
|
73300
|
+
var p = innerOpts ?
|
|
73301
|
+
findAnchorPoint(shp, arcs, innerOpts) : geom.getShapeCentroid(shp, arcs);
|
|
70221
73302
|
return p ? [[p.x, p.y]] : null;
|
|
70222
73303
|
});
|
|
70223
73304
|
}
|
|
70224
73305
|
|
|
73306
|
+
function getInnerPointOptions(opts) {
|
|
73307
|
+
var method = opts.inner_method || 'centroid2';
|
|
73308
|
+
var methods = ['legacy', 'pole', 'centroid', 'centroid2', 'weighted'];
|
|
73309
|
+
if (methods.indexOf(method) == -1) {
|
|
73310
|
+
stop$1('Unsupported inner point method:', method);
|
|
73311
|
+
}
|
|
73312
|
+
var tolerance = opts.inner_tolerance === undefined ?
|
|
73313
|
+
0.1 : parsePercent(opts.inner_tolerance);
|
|
73314
|
+
var weight = opts.inner_weight === undefined ? 0.6 : opts.inner_weight;
|
|
73315
|
+
if (!(weight >= 0)) {
|
|
73316
|
+
stop$1('Expected inner-weight to be a non-negative number');
|
|
73317
|
+
}
|
|
73318
|
+
return {
|
|
73319
|
+
method: method,
|
|
73320
|
+
tolerance: tolerance,
|
|
73321
|
+
weight: weight
|
|
73322
|
+
};
|
|
73323
|
+
}
|
|
73324
|
+
|
|
70225
73325
|
function coordinateFromValue(val) {
|
|
70226
73326
|
var tmp;
|
|
70227
73327
|
if (utils.isFiniteNumber(val)) {
|
|
@@ -70355,10 +73455,14 @@ ${svg}
|
|
|
70355
73455
|
}
|
|
70356
73456
|
|
|
70357
73457
|
function pointsFromPolygonsForJoin(lyr, dataset) {
|
|
70358
|
-
// TODO use faster method to get inner points
|
|
70359
73458
|
return {
|
|
70360
73459
|
geometry_type: 'point',
|
|
70361
|
-
|
|
73460
|
+
// Joins only require a reliable interior point; centroid bias adds a
|
|
73461
|
+
// second search without improving the containment test.
|
|
73462
|
+
shapes: pointsFromPolygons(lyr, dataset.arcs, {
|
|
73463
|
+
inner: true,
|
|
73464
|
+
inner_method: 'pole'
|
|
73465
|
+
}),
|
|
70362
73466
|
data: lyr.data // TODO copy if needed
|
|
70363
73467
|
};
|
|
70364
73468
|
}
|
|
@@ -70403,85 +73507,6 @@ ${svg}
|
|
|
70403
73507
|
return retn;
|
|
70404
73508
|
}
|
|
70405
73509
|
|
|
70406
|
-
class TinyQueue {
|
|
70407
|
-
constructor(data = [], compare = defaultCompare) {
|
|
70408
|
-
this.data = data;
|
|
70409
|
-
this.length = this.data.length;
|
|
70410
|
-
this.compare = compare;
|
|
70411
|
-
|
|
70412
|
-
if (this.length > 0) {
|
|
70413
|
-
for (let i = (this.length >> 1) - 1; i >= 0; i--) this._down(i);
|
|
70414
|
-
}
|
|
70415
|
-
}
|
|
70416
|
-
|
|
70417
|
-
push(item) {
|
|
70418
|
-
this.data.push(item);
|
|
70419
|
-
this.length++;
|
|
70420
|
-
this._up(this.length - 1);
|
|
70421
|
-
}
|
|
70422
|
-
|
|
70423
|
-
pop() {
|
|
70424
|
-
if (this.length === 0) return undefined;
|
|
70425
|
-
|
|
70426
|
-
const top = this.data[0];
|
|
70427
|
-
const bottom = this.data.pop();
|
|
70428
|
-
this.length--;
|
|
70429
|
-
|
|
70430
|
-
if (this.length > 0) {
|
|
70431
|
-
this.data[0] = bottom;
|
|
70432
|
-
this._down(0);
|
|
70433
|
-
}
|
|
70434
|
-
|
|
70435
|
-
return top;
|
|
70436
|
-
}
|
|
70437
|
-
|
|
70438
|
-
peek() {
|
|
70439
|
-
return this.data[0];
|
|
70440
|
-
}
|
|
70441
|
-
|
|
70442
|
-
_up(pos) {
|
|
70443
|
-
const {data, compare} = this;
|
|
70444
|
-
const item = data[pos];
|
|
70445
|
-
|
|
70446
|
-
while (pos > 0) {
|
|
70447
|
-
const parent = (pos - 1) >> 1;
|
|
70448
|
-
const current = data[parent];
|
|
70449
|
-
if (compare(item, current) >= 0) break;
|
|
70450
|
-
data[pos] = current;
|
|
70451
|
-
pos = parent;
|
|
70452
|
-
}
|
|
70453
|
-
|
|
70454
|
-
data[pos] = item;
|
|
70455
|
-
}
|
|
70456
|
-
|
|
70457
|
-
_down(pos) {
|
|
70458
|
-
const {data, compare} = this;
|
|
70459
|
-
const halfLength = this.length >> 1;
|
|
70460
|
-
const item = data[pos];
|
|
70461
|
-
|
|
70462
|
-
while (pos < halfLength) {
|
|
70463
|
-
let left = (pos << 1) + 1;
|
|
70464
|
-
let best = data[left];
|
|
70465
|
-
const right = left + 1;
|
|
70466
|
-
|
|
70467
|
-
if (right < this.length && compare(data[right], best) < 0) {
|
|
70468
|
-
left = right;
|
|
70469
|
-
best = data[right];
|
|
70470
|
-
}
|
|
70471
|
-
if (compare(best, item) >= 0) break;
|
|
70472
|
-
|
|
70473
|
-
data[pos] = best;
|
|
70474
|
-
pos = left;
|
|
70475
|
-
}
|
|
70476
|
-
|
|
70477
|
-
data[pos] = item;
|
|
70478
|
-
}
|
|
70479
|
-
}
|
|
70480
|
-
|
|
70481
|
-
function defaultCompare(a, b) {
|
|
70482
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
70483
|
-
}
|
|
70484
|
-
|
|
70485
73510
|
/*
|
|
70486
73511
|
ISC License
|
|
70487
73512
|
|
|
@@ -74126,6 +77151,16 @@ ${svg}
|
|
|
74126
77151
|
stopJob(job);
|
|
74127
77152
|
};
|
|
74128
77153
|
|
|
77154
|
+
// The icon names accepted by the icon= property of the -style command.
|
|
77155
|
+
// Kept in a module of its own so that both the renderer (svg-symbols.mjs) and
|
|
77156
|
+
// the -style command can use it without forming an import cycle.
|
|
77157
|
+
|
|
77158
|
+
var iconNames = ['circle', 'square', 'ring', 'star'];
|
|
77159
|
+
|
|
77160
|
+
function isSupportedIconName(name) {
|
|
77161
|
+
return iconNames.indexOf(name) > -1;
|
|
77162
|
+
}
|
|
77163
|
+
|
|
74129
77164
|
cmd.svgStyle = function(lyr, dataset, opts) {
|
|
74130
77165
|
var filterFn, table, fields, hasNewFields;
|
|
74131
77166
|
if (getFeatureCount(lyr) === 0) {
|
|
@@ -74162,12 +77197,16 @@ ${svg}
|
|
|
74162
77197
|
}
|
|
74163
77198
|
var strVal = opts[optName].trim();
|
|
74164
77199
|
var accessor = getSymbolPropertyAccessor(strVal, svgName, lyr);
|
|
77200
|
+
var badIcons = svgName == 'icon' ? [] : null;
|
|
74165
77201
|
table.getRecords().forEach(function(rec, i) {
|
|
74166
77202
|
if (filterFn && !filterFn(i)) {
|
|
74167
77203
|
// make sure field exists if record is excluded by filter
|
|
74168
77204
|
setUndefinedFields(rec, svgName == 'label-pos' ? labelPositionFields : [svgName]);
|
|
74169
77205
|
} else {
|
|
74170
77206
|
rec[svgName] = accessor(i);
|
|
77207
|
+
if (badIcons) {
|
|
77208
|
+
addUnsupportedIconName(badIcons, rec.icon);
|
|
77209
|
+
}
|
|
74171
77210
|
if (svgName == 'label-pos') {
|
|
74172
77211
|
if (!setLabelPositionStyle(rec, rec['label-pos'])) {
|
|
74173
77212
|
stop$1('Unexpected value for label-pos:', rec['label-pos']);
|
|
@@ -74175,6 +77214,9 @@ ${svg}
|
|
|
74175
77214
|
}
|
|
74176
77215
|
}
|
|
74177
77216
|
});
|
|
77217
|
+
if (badIcons && badIcons.length > 0) {
|
|
77218
|
+
warn(formatUnsupportedIconMessage(badIcons));
|
|
77219
|
+
}
|
|
74178
77220
|
});
|
|
74179
77221
|
if (fields.length > 0) {
|
|
74180
77222
|
if (hasNewFields) {
|
|
@@ -74206,6 +77248,32 @@ ${svg}
|
|
|
74206
77248
|
}
|
|
74207
77249
|
}
|
|
74208
77250
|
|
|
77251
|
+
// Icon names can not be validated when options are parsed, because an icon=
|
|
77252
|
+
// value may be a field name or an expression. The assigned values are checked
|
|
77253
|
+
// instead, so that computed names are covered too. Unsupported names are a
|
|
77254
|
+
// warning, not an error -- features with an unsupported name render without an
|
|
77255
|
+
// icon.
|
|
77256
|
+
var maxReportedIconNames = 4;
|
|
77257
|
+
|
|
77258
|
+
function addUnsupportedIconName(names, val) {
|
|
77259
|
+
var name;
|
|
77260
|
+
if (!val) return; // a blank value removes the icon
|
|
77261
|
+
name = String(val);
|
|
77262
|
+
if (isSupportedIconName(name) || names.indexOf(name) > -1) return;
|
|
77263
|
+
names.push(name);
|
|
77264
|
+
}
|
|
77265
|
+
|
|
77266
|
+
function formatUnsupportedIconMessage(names) {
|
|
77267
|
+
var extra = names.length - maxReportedIconNames;
|
|
77268
|
+
var listed = extra > 0 ? names.slice(0, maxReportedIconNames) : names;
|
|
77269
|
+
var str = 'Unsupported icon ' + (names.length > 1 ? 'names' : 'name') + ': ' +
|
|
77270
|
+
listed.join(', ');
|
|
77271
|
+
if (extra > 0) {
|
|
77272
|
+
str += ' (and ' + extra + ' more)';
|
|
77273
|
+
}
|
|
77274
|
+
return str + '. Expected one of: ' + iconNames.join(', ');
|
|
77275
|
+
}
|
|
77276
|
+
|
|
74209
77277
|
function setUndefinedFields(rec, fields) {
|
|
74210
77278
|
fields.forEach(function(field) {
|
|
74211
77279
|
if (field in rec === false) {
|
|
@@ -75897,7 +78965,7 @@ ${svg}
|
|
|
75897
78965
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
75898
78966
|
}
|
|
75899
78967
|
|
|
75900
|
-
var version = "0.7.
|
|
78968
|
+
var version = "0.7.54";
|
|
75901
78969
|
|
|
75902
78970
|
// Parse command line args into commands and run them
|
|
75903
78971
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|
|
@@ -78048,6 +81116,7 @@ ${svg}
|
|
|
78048
81116
|
Logging,
|
|
78049
81117
|
Profile,
|
|
78050
81118
|
RasterBlur,
|
|
81119
|
+
RasterContourBands,
|
|
78051
81120
|
RasterContours,
|
|
78052
81121
|
RasterGrid,
|
|
78053
81122
|
RasterReprojection,
|