mapshaper 0.7.53 → 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.
Files changed (3) hide show
  1. package/mapshaper.js +1121 -104
  2. package/package.json +1 -1
  3. package/www/mapshaper.js +1121 -104
package/mapshaper.js CHANGED
@@ -8447,7 +8447,7 @@
8447
8447
  var D2R$2 = Math.PI / 180;
8448
8448
  var R2D$3 = 180 / Math.PI;
8449
8449
  var HALF_PI = Math.PI / 2;
8450
- var SQRT2$1 = Math.sqrt(2);
8450
+ var SQRT2$2 = Math.sqrt(2);
8451
8451
  var SQRT3$1 = Math.sqrt(3);
8452
8452
  var ASIN_ONE_THIRD$1 = Math.asin(1 / 3);
8453
8453
  var EDGE_SCALE = Math.acos(-1 / 3) / 2;
@@ -9018,7 +9018,7 @@
9018
9018
  function narukawaFaceForward(lam, phi) {
9019
9019
  var a = lam - Math.asin(Math.sin(lam) / SQRT3$1);
9020
9020
  var theta = Math.atan(2 * SQRT3$1 / Math.PI * a);
9021
- var denominator = 2 + SQRT2$1 * Math.tan(phi);
9021
+ var denominator = 2 + SQRT2$2 * Math.tan(phi);
9022
9022
  var q = denominator > 0 ? (2 + Math.cos(lam)) / denominator : 0;
9023
9023
  var r = q * SQRT3$1 / Math.cos(theta);
9024
9024
  return [r, theta];
@@ -9038,7 +9038,7 @@
9038
9038
  lam = (lo + hi) / 2;
9039
9039
  var q = r * Math.cos(theta) / SQRT3$1;
9040
9040
  var phi = q < EPS$1 ? HALF_PI :
9041
- Math.atan(((2 + Math.cos(lam)) / q - 2) / SQRT2$1);
9041
+ Math.atan(((2 + Math.cos(lam)) / q - 2) / SQRT2$2);
9042
9042
  return [phi, lam];
9043
9043
  }
9044
9044
 
@@ -9478,7 +9478,7 @@
9478
9478
 
9479
9479
  var D2R$1 = Math.PI / 180;
9480
9480
  var R2D$2 = 180 / Math.PI;
9481
- var SQRT2 = Math.sqrt(2);
9481
+ var SQRT2$1 = Math.sqrt(2);
9482
9482
  var SQRT3 = Math.sqrt(3);
9483
9483
  var ASIN_ONE_THIRD = Math.asin(1 / 3);
9484
9484
  var MARKLEY_LATITUDE = Math.acos(1 / 3) * 0.5 * R2D$2;
@@ -10062,7 +10062,7 @@
10062
10062
 
10063
10063
  function leeRaw(lam, phi) {
10064
10064
  var s = stereographicRaw(lam, phi);
10065
- var z = [s[0] * SQRT2, s[1] * SQRT2];
10065
+ var z = [s[0] * SQRT2$1, s[1] * SQRT2$1];
10066
10066
  var i, j;
10067
10067
  // Pick the sector whose rotation maximizes the real part of rot * z.
10068
10068
  var sector = 0;
@@ -18826,6 +18826,602 @@
18826
18826
  });
18827
18827
  }
18828
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
+
18829
19425
  function simplifyArcsFast(arcs, dist) {
18830
19426
  var xx = [],
18831
19427
  yy = [],
@@ -18897,8 +19493,20 @@
18897
19493
  simplifyPolygonFast: simplifyPolygonFast
18898
19494
  });
18899
19495
 
18900
- // Find a point inside a polygon and located away from the polygon edge
18901
- // Method:
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:
18902
19510
  // - get the largest ring of the polygon
18903
19511
  // - get an array of x-values distributed along the horizontal extent of the ring
18904
19512
  // - for each x:
@@ -18909,8 +19517,7 @@
18909
19517
  // - return the adjusted point having the maximum weighted distance from the edge
18910
19518
  //
18911
19519
  // (distance is weighted to slightly favor points near centroid)
18912
- //
18913
- function findAnchorPoint(shp, arcs) {
19520
+ function findLegacyAnchorPoint(shp, arcs) {
18914
19521
  var maxPath = shp && geom.getMaxPath(shp, arcs),
18915
19522
  pathBounds = maxPath && arcs.getSimpleShapeBounds(maxPath),
18916
19523
  thresh, simple;
@@ -19152,7 +19759,8 @@
19152
19759
 
19153
19760
  var AnchorPoints = /*#__PURE__*/Object.freeze({
19154
19761
  __proto__: null,
19155
- findAnchorPoint: findAnchorPoint
19762
+ findAnchorPoint: findAnchorPoint,
19763
+ findLegacyAnchorPoint: findLegacyAnchorPoint
19156
19764
  });
19157
19765
 
19158
19766
  // Returns a function for calculating the percentage of a shape's perimeter by length that
@@ -19403,6 +20011,8 @@
19403
20011
  return bbox;
19404
20012
  }
19405
20013
 
20014
+ var EXPRESSION_INNER_POINT_OPTS = {method: 'centroid2', tolerance: 0.1};
20015
+
19406
20016
  // Returns a function to return a feature proxy by id
19407
20017
  // (the proxy appears as "this" or "$" in a feature expression)
19408
20018
  function initFeatureProxy(lyr, arcs, optsArg) {
@@ -19618,7 +20228,8 @@
19618
20228
  }
19619
20229
 
19620
20230
  function innerXY() {
19621
- _innerXY = _innerXY || findAnchorPoint(_ids, arcs);
20231
+ _innerXY = _innerXY ||
20232
+ findAnchorPoint(_ids, arcs, EXPRESSION_INNER_POINT_OPTS);
19622
20233
  return _innerXY;
19623
20234
  }
19624
20235
 
@@ -25791,6 +26402,10 @@
25791
26402
  return !!(d && (d.icon || d['icon-size'] || (d['icon-color'] && d.r > 0)));
25792
26403
  }
25793
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.
25794
26409
  function renderIcon(d) {
25795
26410
  var type = d.icon || 'circle';
25796
26411
  var r = getIconRadius(d, type);
@@ -25799,7 +26414,6 @@
25799
26414
  if (type == 'square') return square(getIconStyleData(d, r), 0, 0);
25800
26415
  if (type == 'ring') return ring(getIconStyleData(d, r), 0, 0);
25801
26416
  if (type == 'star') return star(getIconStyleData(d, r));
25802
- message('Unknown icon type: ' + type);
25803
26417
  return empty();
25804
26418
  }
25805
26419
 
@@ -33920,6 +34534,8 @@ ${svg}
33920
34534
  var featureIndex = 0;
33921
34535
  for (var i = 0; i < state.count; i++) {
33922
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.
33923
34539
  if (!feat || !feat.geometry) continue;
33924
34540
  var normalized = normalizeFeature(feat, fields);
33925
34541
  try {
@@ -36017,7 +36633,8 @@ ${svg}
36017
36633
  datasets = [mergeDatasetsForExport(datasets)];
36018
36634
  }
36019
36635
  datasets.forEach(sortExportLayers);
36020
- files = await exportGeoPackage(datasets[0], opts);
36636
+ // GeoPackage bypasses exportFileContent(), where precision is normally applied.
36637
+ files = await exportGeoPackage(applyExportPrecision(datasets[0], opts), opts);
36021
36638
  } else if (format == 'geoparquet') {
36022
36639
  var layerCount = datasets.reduce(function(sum, d) {
36023
36640
  return sum + d.layers.length;
@@ -36026,7 +36643,8 @@ ${svg}
36026
36643
  files = [];
36027
36644
  for (var i = 0; i < datasets.length; i++) {
36028
36645
  sortExportLayers(datasets[i]);
36029
- files = files.concat(await exportGeoParquet(datasets[i], opts, singleFileName));
36646
+ // GeoParquet bypasses exportFileContent(), where precision is normally applied.
36647
+ files = files.concat(await exportGeoParquet(applyExportPrecision(datasets[i], opts), opts, singleFileName));
36030
36648
  }
36031
36649
  } else {
36032
36650
  files = datasets.reduce(function(memo, dataset) {
@@ -36080,6 +36698,15 @@ ${svg}
36080
36698
  });
36081
36699
  }
36082
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
+
36083
36710
  // Return an array of objects with 'filename' and 'content' members.
36084
36711
  //
36085
36712
  function exportFileContent(dataset, opts) {
@@ -39192,7 +39819,7 @@ ${svg}
39192
39819
  });
39193
39820
 
39194
39821
  parser.command('contours')
39195
- .describe('convert a raster layer to contour lines')
39822
+ .describe('convert a raster layer to contour lines or closed bands')
39196
39823
  .option('interval', {
39197
39824
  describe: 'spacing between contour levels (default is a round interval)',
39198
39825
  type: 'number'
@@ -39212,6 +39839,10 @@ ${svg}
39212
39839
  .option('field', {
39213
39840
  describe: 'name of field to hold contour values (default is "value")'
39214
39841
  })
39842
+ .option('closed', {
39843
+ describe: 'create closed contour bands with lower and upper fields',
39844
+ type: 'flag'
39845
+ })
39215
39846
  .option('no-smoothing', {
39216
39847
  describe: 'skip smoothing away the one-pixel contour staircase',
39217
39848
  type: 'flag'
@@ -39855,6 +40486,17 @@ ${svg}
39855
40486
  describe: 'create an interior point for each polygon\'s largest ring',
39856
40487
  type: 'flag'
39857
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
+ })
39858
40500
  .option('centroid', {
39859
40501
  describe: 'create a centroid point for each polygon\'s largest ring',
39860
40502
  type: 'flag'
@@ -56663,6 +57305,10 @@ ${svg}
56663
57305
  getArcClassifier: getArcClassifier
56664
57306
  });
56665
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
+
56666
57312
  function makePolygonBuffer(lyr, dataset, opts) {
56667
57313
  var spherical = isLatLngCRS(getDatasetCRS(dataset));
56668
57314
  if (spherical && sourceHasCollapsingBandEdge(lyr, dataset)) {
@@ -57059,7 +57705,7 @@ ${svg}
57059
57705
  }
57060
57706
  // hole: keep only if wide enough to hold a disk of radius keepRadius
57061
57707
  var holeShape = [reversePath(path.ids.concat())];
57062
- var anchor = findAnchorPoint(holeShape, arcs);
57708
+ var anchor = findAnchorPoint(holeShape, arcs, BUFFER_INNER_POINT_OPTS);
57063
57709
  var radius = anchor ?
57064
57710
  getPointToShapeDistance(anchor.x, anchor.y, holeShape, arcs) : 0;
57065
57711
  if (radius >= keepRadius) {
@@ -58530,7 +59176,7 @@ ${svg}
58530
59176
  function getTileAnchorPoint(tileId, mosaicIndex, ownerCtx) {
58531
59177
  if (tileId in ownerCtx.anchorCache) return ownerCtx.anchorCache[tileId];
58532
59178
  var tile = mosaicIndex.mosaic[tileId];
58533
- var p = findAnchorPoint(tile, ownerCtx.mosaicArcs) ||
59179
+ var p = findAnchorPoint(tile, ownerCtx.mosaicArcs, BUFFER_INNER_POINT_OPTS) ||
58534
59180
  getPathCentroid(tile[0], ownerCtx.mosaicArcs) || null;
58535
59181
  ownerCtx.anchorCache[tileId] = p;
58536
59182
  return p;
@@ -66082,6 +66728,312 @@ ${svg}
66082
66728
  // message('[comment]', opts.message);
66083
66729
  }; // no-op, so -comment doesn't trigger a parsing error
66084
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
+
66085
67037
  // Trace contour lines (isolines) from a raster layer's working samples using
66086
67038
  // marching squares.
66087
67039
  //
@@ -66174,13 +67126,17 @@ ${svg}
66174
67126
 
66175
67127
  function getRasterContourLines(raster, opts) {
66176
67128
  var grid = getRasterGrid(raster);
66177
- var band, levels;
67129
+ var band, levels, range;
66178
67130
  validateRasterGridForContours(grid);
66179
67131
  band = getContourBand(grid, opts);
66180
67132
  levels = getContourLevels(grid, band, opts);
67133
+ range = getRasterSampleRange(grid, band);
66181
67134
  return {
67135
+ band: band,
67136
+ range: range,
66182
67137
  levels: levels,
66183
- lines: traceRasterContours(grid, band, levels, getCornerClearance(opts))
67138
+ lines: traceRasterContours(grid, band, levels, getCornerClearance(opts),
67139
+ {extend_to_bbox: opts && opts.closed})
66184
67140
  };
66185
67141
  }
66186
67142
 
@@ -66319,7 +67275,7 @@ ${svg}
66319
67275
  }
66320
67276
 
66321
67277
  // clearance: see CORNER_CLEARANCE; 0 places crossings exactly.
66322
- function traceRasterContours(grid, band, levels, clearance) {
67278
+ function traceRasterContours(grid, band, levels, clearance, opts) {
66323
67279
  var segments = collectContourSegments(grid, band, levels);
66324
67280
  var toMapXY = getLatticeToMapTransform(grid);
66325
67281
  var hCount = (grid.width - 1) * grid.height;
@@ -66329,6 +67285,9 @@ ${svg}
66329
67285
  stitchContourSegments(levelSegments).forEach(function(path) {
66330
67286
  var coords = getPathCoords(path, grid, band, levels[i], hCount, toMapXY,
66331
67287
  clearance);
67288
+ if (opts && opts.extend_to_bbox) {
67289
+ coords = extendContourPathToBBox(coords, path, grid, hCount);
67290
+ }
66332
67291
  if (coords.length > 1) {
66333
67292
  lines.push({value: levels[i], coords: coords});
66334
67293
  }
@@ -66337,6 +67296,39 @@ ${svg}
66337
67296
  return lines;
66338
67297
  }
66339
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
+
66340
67332
  // Visit each cell once and march it for every level that falls inside its
66341
67333
  // value range, rather than scanning the whole grid once per level.
66342
67334
  function collectContourSegments(grid, band, levels) {
@@ -66557,6 +67549,21 @@ ${svg}
66557
67549
  // Contours come from the layer's working samples, so they reflect any
66558
67550
  // earlier edits (e.g. -blur, -clip) rather than the original source pixels.
66559
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
+ }
66560
67567
  message(getContoursMessage(contours));
66561
67568
  if (contours.lines.length === 0) {
66562
67569
  return [createEmptyContourLayer(targetLyr, opts)];
@@ -66640,6 +67647,17 @@ ${svg}
66640
67647
  return lyr;
66641
67648
  }
66642
67649
 
67650
+ function createEmptyContourDataset() {
67651
+ return {
67652
+ info: {},
67653
+ layers: [{
67654
+ geometry_type: 'polyline',
67655
+ shapes: [],
67656
+ data: new DataTable([])
67657
+ }]
67658
+ };
67659
+ }
67660
+
66643
67661
  function getContoursMessage(contours) {
66644
67662
  var levels = contours.levels;
66645
67663
  if (levels.length === 0) {
@@ -66651,6 +67669,16 @@ ${svg}
66651
67669
  ' (' + levels[0] + ' to ' + levels[levels.length - 1] + ')';
66652
67670
  }
66653
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
+
66654
67682
  cmd.dashlines = function(lyr, dataset, opts) {
66655
67683
  var crs = getDatasetCRS(dataset);
66656
67684
  var defs = getStashedVar('defs');
@@ -72267,13 +73295,33 @@ ${svg}
72267
73295
  }
72268
73296
 
72269
73297
  function pointsFromPolygons(lyr, arcs, opts) {
72270
- var func = opts.inner ? findAnchorPoint : geom.getShapeCentroid;
73298
+ var innerOpts = opts.inner ? getInnerPointOptions(opts) : null;
72271
73299
  return lyr.shapes.map(function(shp) {
72272
- var p = func(shp, arcs);
73300
+ var p = innerOpts ?
73301
+ findAnchorPoint(shp, arcs, innerOpts) : geom.getShapeCentroid(shp, arcs);
72273
73302
  return p ? [[p.x, p.y]] : null;
72274
73303
  });
72275
73304
  }
72276
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
+
72277
73325
  function coordinateFromValue(val) {
72278
73326
  var tmp;
72279
73327
  if (utils.isFiniteNumber(val)) {
@@ -72407,10 +73455,14 @@ ${svg}
72407
73455
  }
72408
73456
 
72409
73457
  function pointsFromPolygonsForJoin(lyr, dataset) {
72410
- // TODO use faster method to get inner points
72411
73458
  return {
72412
73459
  geometry_type: 'point',
72413
- shapes: pointsFromPolygons(lyr, dataset.arcs, {inner: true}),
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
+ }),
72414
73466
  data: lyr.data // TODO copy if needed
72415
73467
  };
72416
73468
  }
@@ -72455,85 +73507,6 @@ ${svg}
72455
73507
  return retn;
72456
73508
  }
72457
73509
 
72458
- class TinyQueue {
72459
- constructor(data = [], compare = defaultCompare) {
72460
- this.data = data;
72461
- this.length = this.data.length;
72462
- this.compare = compare;
72463
-
72464
- if (this.length > 0) {
72465
- for (let i = (this.length >> 1) - 1; i >= 0; i--) this._down(i);
72466
- }
72467
- }
72468
-
72469
- push(item) {
72470
- this.data.push(item);
72471
- this.length++;
72472
- this._up(this.length - 1);
72473
- }
72474
-
72475
- pop() {
72476
- if (this.length === 0) return undefined;
72477
-
72478
- const top = this.data[0];
72479
- const bottom = this.data.pop();
72480
- this.length--;
72481
-
72482
- if (this.length > 0) {
72483
- this.data[0] = bottom;
72484
- this._down(0);
72485
- }
72486
-
72487
- return top;
72488
- }
72489
-
72490
- peek() {
72491
- return this.data[0];
72492
- }
72493
-
72494
- _up(pos) {
72495
- const {data, compare} = this;
72496
- const item = data[pos];
72497
-
72498
- while (pos > 0) {
72499
- const parent = (pos - 1) >> 1;
72500
- const current = data[parent];
72501
- if (compare(item, current) >= 0) break;
72502
- data[pos] = current;
72503
- pos = parent;
72504
- }
72505
-
72506
- data[pos] = item;
72507
- }
72508
-
72509
- _down(pos) {
72510
- const {data, compare} = this;
72511
- const halfLength = this.length >> 1;
72512
- const item = data[pos];
72513
-
72514
- while (pos < halfLength) {
72515
- let left = (pos << 1) + 1;
72516
- let best = data[left];
72517
- const right = left + 1;
72518
-
72519
- if (right < this.length && compare(data[right], best) < 0) {
72520
- left = right;
72521
- best = data[right];
72522
- }
72523
- if (compare(best, item) >= 0) break;
72524
-
72525
- data[pos] = best;
72526
- pos = left;
72527
- }
72528
-
72529
- data[pos] = item;
72530
- }
72531
- }
72532
-
72533
- function defaultCompare(a, b) {
72534
- return a < b ? -1 : a > b ? 1 : 0;
72535
- }
72536
-
72537
73510
  /*
72538
73511
  ISC License
72539
73512
 
@@ -76178,6 +77151,16 @@ ${svg}
76178
77151
  stopJob(job);
76179
77152
  };
76180
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
+
76181
77164
  cmd.svgStyle = function(lyr, dataset, opts) {
76182
77165
  var filterFn, table, fields, hasNewFields;
76183
77166
  if (getFeatureCount(lyr) === 0) {
@@ -76214,12 +77197,16 @@ ${svg}
76214
77197
  }
76215
77198
  var strVal = opts[optName].trim();
76216
77199
  var accessor = getSymbolPropertyAccessor(strVal, svgName, lyr);
77200
+ var badIcons = svgName == 'icon' ? [] : null;
76217
77201
  table.getRecords().forEach(function(rec, i) {
76218
77202
  if (filterFn && !filterFn(i)) {
76219
77203
  // make sure field exists if record is excluded by filter
76220
77204
  setUndefinedFields(rec, svgName == 'label-pos' ? labelPositionFields : [svgName]);
76221
77205
  } else {
76222
77206
  rec[svgName] = accessor(i);
77207
+ if (badIcons) {
77208
+ addUnsupportedIconName(badIcons, rec.icon);
77209
+ }
76223
77210
  if (svgName == 'label-pos') {
76224
77211
  if (!setLabelPositionStyle(rec, rec['label-pos'])) {
76225
77212
  stop$1('Unexpected value for label-pos:', rec['label-pos']);
@@ -76227,6 +77214,9 @@ ${svg}
76227
77214
  }
76228
77215
  }
76229
77216
  });
77217
+ if (badIcons && badIcons.length > 0) {
77218
+ warn(formatUnsupportedIconMessage(badIcons));
77219
+ }
76230
77220
  });
76231
77221
  if (fields.length > 0) {
76232
77222
  if (hasNewFields) {
@@ -76258,6 +77248,32 @@ ${svg}
76258
77248
  }
76259
77249
  }
76260
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
+
76261
77277
  function setUndefinedFields(rec, fields) {
76262
77278
  fields.forEach(function(field) {
76263
77279
  if (field in rec === false) {
@@ -77949,7 +78965,7 @@ ${svg}
77949
78965
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
77950
78966
  }
77951
78967
 
77952
- var version = "0.7.53";
78968
+ var version = "0.7.54";
77953
78969
 
77954
78970
  // Parse command line args into commands and run them
77955
78971
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -80100,6 +81116,7 @@ ${svg}
80100
81116
  Logging,
80101
81117
  Profile,
80102
81118
  RasterBlur,
81119
+ RasterContourBands,
80103
81120
  RasterContours,
80104
81121
  RasterGrid,
80105
81122
  RasterReprojection,