mapshaper 0.7.29 → 0.7.31

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 +1301 -6
  2. package/package.json +1 -1
  3. package/www/mapshaper.js +1301 -6
package/www/mapshaper.js CHANGED
@@ -140,7 +140,7 @@
140
140
  }
141
141
 
142
142
  // Convert an array-like object to an Array, or make a copy if @obj is an Array
143
- function toArray(obj) {
143
+ function toArray$1(obj) {
144
144
  var arr;
145
145
  if (!isArrayLike(obj)) error$1("toArray() requires an array-like object");
146
146
  try {
@@ -249,7 +249,7 @@
249
249
 
250
250
  function promisify(asyncFn) {
251
251
  return function() {
252
- var args = toArray(arguments);
252
+ var args = toArray$1(arguments);
253
253
  return new Promise((resolve, reject) => {
254
254
  var cb = function(err, data) {
255
255
  if (err) reject(err);
@@ -1148,7 +1148,7 @@
1148
1148
  range, reduceAsync, regexEscape, reorderArray: reorderArray$1, repeat, repeatString,
1149
1149
  replaceArray, rpad, rtrim,
1150
1150
  shuffle, some, sortArrayIndex, sortOn, splitLines, sum: sum$1,
1151
- toArray, toBuffer, trim, trimQuotes,
1151
+ toArray: toArray$1, toBuffer, trim, trimQuotes,
1152
1152
  uniq, uniqifyNames,
1153
1153
  wildcardToRegExp
1154
1154
  };
@@ -11825,7 +11825,7 @@
11825
11825
  q.pop();
11826
11826
  const nodeIndex = top >> 1;
11827
11827
  const isLeafLevel = nodeIndex < numItems4;
11828
- const end = Math.min(nodeIndex + nodeSize4, upperBound(nodeIndex, levelBounds));
11828
+ const end = Math.min(nodeIndex + nodeSize4, upperBound$1(nodeIndex, levelBounds));
11829
11829
 
11830
11830
  for (let pos = nodeIndex; pos < end; pos += 4) {
11831
11831
  const childIndex = indices[pos >> 2] | 0;
@@ -11856,7 +11856,7 @@
11856
11856
  * @param {number} value
11857
11857
  * @param {number[]} arr
11858
11858
  */
11859
- function upperBound(value, arr) {
11859
+ function upperBound$1(value, arr) {
11860
11860
  let i = 0;
11861
11861
  let j = arr.length - 1;
11862
11862
  while (i < j) {
@@ -30145,6 +30145,13 @@ ${svg}
30145
30145
  }
30146
30146
  }
30147
30147
 
30148
+ function validateSmoothOpts(cmd) {
30149
+ var o = cmd.options;
30150
+ if (o.distance === undefined || o.distance === null || o.distance === '') {
30151
+ error('Command requires a distance parameter');
30152
+ }
30153
+ }
30154
+
30148
30155
  function validateProjOpts(cmd) {
30149
30156
  var resampling = cmd.options.resampling;
30150
30157
  if (!(cmd.options.crs || cmd.options.match || cmd.options.init)) {
@@ -32378,6 +32385,28 @@ ${svg}
32378
32385
  })
32379
32386
  .option('target', targetOpt);
32380
32387
 
32388
+ parser.command('filter-detail')
32389
+ // undocumented feature
32390
+ // .describe('remove intricate sub-scale detail that smoothing cannot generalize')
32391
+ .option('distance', {
32392
+ DEFAULT: true,
32393
+ type: 'distance',
32394
+ describe: 'detail size threshold as a distance (e.g. 1km)'
32395
+ })
32396
+ .option('tortuosity', {
32397
+ type: 'number',
32398
+ describe: 'min original-length / chord ratio for a run to be cut (default 2); higher only cuts more convoluted detail'
32399
+ })
32400
+ .option('weighting', {
32401
+ type: 'number',
32402
+ describe: 'Visvalingam angle-weight coefficient (default 0.7); higher removes spiky detail more eagerly'
32403
+ })
32404
+ .option('planar', {
32405
+ describe: 'treat decimal degree coords as planar x,y (default is spherical)',
32406
+ type: 'flag'
32407
+ })
32408
+ .option('target', targetOpt);
32409
+
32381
32410
  parser.command('filter-slivers')
32382
32411
  .describe('remove small polygon rings')
32383
32412
  .option('min-area', {
@@ -32881,6 +32910,44 @@ ${svg}
32881
32910
  })
32882
32911
  .option('target', targetOpt);
32883
32912
 
32913
+ parser.command('smooth')
32914
+ .validate(validateSmoothOpts)
32915
+ .describe('scale-aware smoothing of polygon and polyline features')
32916
+ .option('distance', {
32917
+ DEFAULT: true,
32918
+ type: 'distance',
32919
+ describe: 'smoothing tolerance as a distance (e.g. 2km)'
32920
+ })
32921
+ // Gaussian (Savitzky-Golay) is the default and only documented smoother.
32922
+ // 'paek' (exponential kernel) is kept as an undocumented alternative, so
32923
+ // neither flag is given a describe (they stay out of the command help).
32924
+ .option('paek', {
32925
+ assign_to: 'method'
32926
+ })
32927
+ .option('gaussian', {
32928
+ assign_to: 'method'
32929
+ })
32930
+ .option('method', {
32931
+ // hidden option (set by the paek/gaussian flags)
32932
+ })
32933
+ .option('keep-corners', {
32934
+ describe: 'preserve sharp corners where straight-line segments meet',
32935
+ type: 'flag'
32936
+ })
32937
+ .option('gain', {
32938
+ describe: 'shrinkage-correction (default 1; 0 = none, >1 exaggerates bends)',
32939
+ type: 'number'
32940
+ })
32941
+ .option('planar', {
32942
+ // describe: 'smooth decimal degree coords in 2D space (default is spherical)',
32943
+ type: 'flag'
32944
+ })
32945
+ .option('no-prefilter', {
32946
+ describe: 'skip the detail-filtering preprocessing step',
32947
+ type: 'flag'
32948
+ })
32949
+ .option('target', targetOpt);
32950
+
32884
32951
  parser.command('slice')
32885
32952
  // .describe('slice a layer using polygons in another layer')
32886
32953
  .option('source', {
@@ -57154,6 +57221,301 @@ ${svg}
57154
57221
  getAspectRatioArg: getAspectRatioArg
57155
57222
  });
57156
57223
 
57224
+ // Remove intricate sub-scale detail from a single arc so that -smooth has a
57225
+ // clean line to work with, WITHOUT thinning the rest of the line: smooth makes a
57226
+ // better approximation of the original when it has more detail to work with, so
57227
+ // we only cut where there is genuine intricate detail and leave everything else
57228
+ // at full resolution.
57229
+ //
57230
+ // Two phases:
57231
+ //
57232
+ // 1. IDENTIFY candidate chords with a chord-length-gated weighted Visvalingam
57233
+ // peel. Weighted Visvalingam removes the least-significant vertex first
57234
+ // (smallest angle-weighted triangle area), so a thin feature is peeled from
57235
+ // its tip inward and each removal sweeps the smallest possible triangle --
57236
+ // this finds the minimal chord that slices a feature off. The gate (a vertex
57237
+ // is only removable when the chord that would replace it is <= the detail
57238
+ // distance D) segments the line into runs that each fit within the detail
57239
+ // scale and bounds every candidate chord to <= D, so cuts stay local.
57240
+ //
57241
+ // 2. MERGE survivors that hide a slicing chord. A spike with long bare flanks
57242
+ // parks its base vertices as survivors (each flank chord alone exceeds D), so
57243
+ // the short chord that closes the excursion sits between non-adjacent
57244
+ // survivors. Where a near-degenerate closing chord (a needle returning close
57245
+ // to its base: chord <= MERGE_CHORD_FRACTION * D, tortuosity >= threshold)
57246
+ // exists within an arc-length window, widen the run so the next phase can
57247
+ // slice it. Restricting to short closing chords keeps the merge from sweeping
57248
+ // a wide excursion across neighbouring geometry and introducing a crossing.
57249
+ //
57250
+ // 3. COMMIT selectively. For each run of removed vertices between two survivors,
57251
+ // compare the original sub-path length to the chord across it (tortuosity).
57252
+ // Collapse the run to its chord only when it is convoluted (tortuosity >=
57253
+ // threshold) -- a jetty, fjord or crinkle. Otherwise restore the run's
57254
+ // original vertices, so gentle stretches keep full detail for -smooth.
57255
+ //
57256
+ // @xx, @yy coordinate arrays for one arc (may be typed-array subarrays).
57257
+ // @opts: {distance, tortuosity, spherical, weighting}
57258
+ // distance detail size threshold in ground units (meters when spherical): the
57259
+ // longest chord the filter is allowed to create.
57260
+ // tortuosity min original-length / chord ratio for a run to be cut (default 2).
57261
+ // spherical measure area/length on the sphere (lng/lat -> geocentric x,y,z).
57262
+ // weighting Visvalingam angle-weight coefficient (default 0.7), matching
57263
+ // -simplify's weighted_visvalingam.
57264
+ // Returns {xx: [], yy: []}. Arc endpoints are always preserved, so shared
57265
+ // topology nodes stay put and the operation is topology-safe like -simplify.
57266
+ var DEFAULT_WEIGHTING = 0.7;
57267
+ var DEFAULT_TORTUOSITY = 2;
57268
+ // How far (in detail-distances of arc length) the survivor-merge pass looks ahead
57269
+ // for a chord that closes a convoluted excursion. Bounds the pass to O(n) and
57270
+ // caps how long a thin spike it can slice in one merge.
57271
+ var MERGE_WINDOW_FACTOR = 12;
57272
+ // The survivor-merge pass only fires when the closing chord is this fraction of D
57273
+ // or shorter: a near-degenerate needle that returns close to its base can be
57274
+ // sliced safely, but collapsing a wider excursion (chord approaching D) sweeps a
57275
+ // long span across neighbouring geometry and can introduce a crossing. cutRun,
57276
+ // which spans only adjacent survivors, is not constrained this way.
57277
+ var MERGE_CHORD_FRACTION = 0.5;
57278
+
57279
+ function collapseArcDetail(xx, yy, opts) {
57280
+ var n = xx.length;
57281
+ var outX = [], outY = [];
57282
+ var D = opts.distance;
57283
+ if (n < 3 || !(D > 0)) {
57284
+ for (var p = 0; p < n; p++) { outX.push(xx[p]); outY.push(yy[p]); }
57285
+ return {xx: outX, yy: outY};
57286
+ }
57287
+ var spherical = !!opts.spherical;
57288
+ var k = opts.weighting >= 0 ? opts.weighting : DEFAULT_WEIGHTING;
57289
+ var T = opts.tortuosity > 0 ? opts.tortuosity : DEFAULT_TORTUOSITY;
57290
+ var Dsq = D * D;
57291
+
57292
+ // Metric coordinates: geocentric x,y,z on a sphere for lng/lat input, plain
57293
+ // x,y otherwise. Area and chord length are both measured in this space.
57294
+ var mx, my, mz;
57295
+ if (spherical) {
57296
+ mx = new Float64Array(n);
57297
+ my = new Float64Array(n);
57298
+ mz = new Float64Array(n);
57299
+ geom.convLngLatToSph(xx, yy, mx, my, mz);
57300
+ } else {
57301
+ mx = xx;
57302
+ my = yy;
57303
+ }
57304
+
57305
+ function chordSq(a, e) {
57306
+ var dx = mx[a] - mx[e], dy = my[a] - my[e];
57307
+ if (spherical) {
57308
+ var dz = mz[a] - mz[e];
57309
+ return dx * dx + dy * dy + dz * dz;
57310
+ }
57311
+ return dx * dx + dy * dy;
57312
+ }
57313
+
57314
+ function dist(a, e) { return Math.sqrt(chordSq(a, e)); }
57315
+
57316
+ // Weighted effective area of vertex m between a and e, or Infinity ("parked")
57317
+ // when removing m would create a chord longer than the detail distance.
57318
+ function vertexValue(a, m, e) {
57319
+ if (chordSq(a, e) > Dsq) return Infinity;
57320
+ var area, cos;
57321
+ if (spherical) {
57322
+ area = geom.triangleArea3D(mx[a], my[a], mz[a], mx[m], my[m], mz[m], mx[e], my[e], mz[e]);
57323
+ cos = geom.cosine3D(mx[a], my[a], mz[a], mx[m], my[m], mz[m], mx[e], my[e], mz[e]);
57324
+ } else {
57325
+ area = geom.triangleArea(mx[a], my[a], mx[m], my[m], mx[e], my[e]);
57326
+ cos = geom.cosine(mx[a], my[a], mx[m], my[m], mx[e], my[e]);
57327
+ }
57328
+ return (1 - k * cos) * area;
57329
+ }
57330
+
57331
+ var prev = new Int32Array(n);
57332
+ var next = new Int32Array(n);
57333
+ var removed = new Uint8Array(n);
57334
+ var vals = new Float64Array(n);
57335
+ for (var i = 0; i < n; i++) {
57336
+ prev[i] = i - 1;
57337
+ next[i] = i + 1;
57338
+ vals[i] = (i === 0 || i === n - 1) ? Infinity : vertexValue(i - 1, i, i + 1);
57339
+ }
57340
+
57341
+ var heap = new Heap();
57342
+ heap.init(vals);
57343
+ while (heap.size() > 0) {
57344
+ if (heap.peekValue() === Infinity) break; // no eligible vertices remain
57345
+ var c = heap.pop();
57346
+ var b = prev[c], d = next[c];
57347
+ removed[c] = 1;
57348
+ next[b] = d;
57349
+ prev[d] = b;
57350
+ // Only b and d change neighbours, so only their values (and eligibility)
57351
+ // can change; a previously parked neighbour may become removable here.
57352
+ if (b !== 0) heap.updateValue(b, vertexValue(prev[b], b, d));
57353
+ if (d !== n - 1) heap.updateValue(d, vertexValue(b, d, next[d]));
57354
+ }
57355
+
57356
+ // Phase 2: within each run between survivors, cut convoluted spans locally
57357
+ // rather than judging the whole run at once. The peel bounds each run to a
57358
+ // chord <= D (which keeps this search cheap); the cut decision itself is
57359
+ // tortuosity-driven and independent of the run's size, so a small spike is
57360
+ // removed whether it sits alone or embedded in a long gentle stretch.
57361
+ //
57362
+ // Cumulative arc length (metric space) lets us read off the original path
57363
+ // length between any two vertices in O(1).
57364
+ var cumLen = new Float64Array(n);
57365
+ for (var q = 1; q < n; q++) cumLen[q] = cumLen[q - 1] + dist(q - 1, q);
57366
+
57367
+ function cutRun(a, e) {
57368
+ // Emit the kept vertices in (a, e]; a has already been emitted. Walk from a
57369
+ // and, at each step, find the span [i, j] (chord <= D) with the highest
57370
+ // tortuosity and, if it exceeds the threshold, cut it to its chord; otherwise
57371
+ // keep one vertex and advance. Picking the maximum-tortuosity j gives the
57372
+ // tightest return point -- the shortest chord that slices the spike off --
57373
+ // and the test is per span, so a small spike is cut whether it stands alone
57374
+ // or is embedded in a long gentle stretch (no run-size dilution).
57375
+ var i = a;
57376
+ while (i < e) {
57377
+ var bestJ = -1;
57378
+ var bestTort = T;
57379
+ for (var j = i + 2; j <= e; j++) {
57380
+ var c = dist(i, j);
57381
+ if (c > D) continue; // never create a chord longer than the detail scale
57382
+ var tort = c > 0 ? (cumLen[j] - cumLen[i]) / c : Infinity;
57383
+ if (tort > bestTort) {
57384
+ bestTort = tort;
57385
+ bestJ = j;
57386
+ }
57387
+ }
57388
+ if (bestJ >= 0) {
57389
+ outX.push(xx[bestJ]);
57390
+ outY.push(yy[bestJ]);
57391
+ i = bestJ;
57392
+ } else {
57393
+ outX.push(xx[i + 1]);
57394
+ outY.push(yy[i + 1]);
57395
+ i++;
57396
+ }
57397
+ }
57398
+ }
57399
+
57400
+ // Survivor-merge pass. A thin spike with long bare flanks (e.g. a fjord wall or
57401
+ // a dredged channel) forces its base vertices to be parked as survivors -- each
57402
+ // flank chord on its own exceeds D -- so the short chord that actually closes
57403
+ // the excursion is hidden between *non-adjacent* survivors and the per-run
57404
+ // cutRun above never sees it. Walk the survivor chain and, where a later
57405
+ // survivor closes a convoluted excursion with a short chord (<=
57406
+ // MERGE_CHORD_FRACTION * D, tortuosity >= T) within an arc-length window,
57407
+ // splice out the spanned survivors so the run boundary widens and cutRun slices
57408
+ // the spike off. Restricting to short closing chords keeps the merge from
57409
+ // sweeping a wide excursion across neighbouring geometry. This reuses cumLen and the
57410
+ // peel's linked list; it does not re-run the Visvalingam peel. Only non-adjacent
57411
+ // convoluted survivors are merged, so gentle runs (and all existing behaviour)
57412
+ // are untouched.
57413
+ var window = MERGE_WINDOW_FACTOR * D;
57414
+ var mergeChordSq = Dsq * MERGE_CHORD_FRACTION * MERGE_CHORD_FRACTION;
57415
+ var s = 0;
57416
+ while (s !== n - 1) {
57417
+ var mergeJ = -1;
57418
+ var mergeTort = T;
57419
+ var u = next[s];
57420
+ while (cumLen[u] - cumLen[s] <= window) {
57421
+ if (chordSq(s, u) <= mergeChordSq) {
57422
+ var ud = dist(s, u);
57423
+ var utort = ud > 0 ? (cumLen[u] - cumLen[s]) / ud : Infinity;
57424
+ if (utort > mergeTort) {
57425
+ mergeTort = utort;
57426
+ mergeJ = u;
57427
+ }
57428
+ }
57429
+ if (u === n - 1) break;
57430
+ u = next[u];
57431
+ }
57432
+ if (mergeJ > next[s]) {
57433
+ next[s] = mergeJ;
57434
+ prev[mergeJ] = s;
57435
+ s = mergeJ;
57436
+ } else {
57437
+ s = next[s];
57438
+ }
57439
+ }
57440
+
57441
+ outX.push(xx[0]);
57442
+ outY.push(yy[0]);
57443
+ var a = 0;
57444
+ while (a !== n - 1) {
57445
+ var e = next[a];
57446
+ cutRun(a, e);
57447
+ a = e;
57448
+ }
57449
+ return {xx: outX, yy: outY};
57450
+ }
57451
+
57452
+ // Optional preprocessing step before -smooth: collapse intricate sub-scale
57453
+ // detail (jetties, narrow inlets, spikes) that smoothing cannot generalize. Like
57454
+ // -smooth and -simplify this rewrites shared arc vertices in place and preserves
57455
+ // arc endpoints, so polygon topology is left intact.
57456
+ cmd.filterDetail = function(dataset, opts, targetLayers) {
57457
+ var arcs = dataset.arcs;
57458
+ if (!arcs || arcs.size() === 0) return;
57459
+ opts = opts || {};
57460
+ if (opts.distance === undefined || opts.distance === null || opts.distance === '') {
57461
+ stop$1('Missing a detail distance');
57462
+ }
57463
+ var spherical = !opts.planar && !arcs.isPlanar();
57464
+ var distance = convertDetailDistance(opts.distance, dataset, spherical);
57465
+ if (!(distance > 0)) {
57466
+ stop$1('Expected a positive detail distance');
57467
+ }
57468
+ var implicitNames = getImplicitlyTargetedLayerNames(dataset, targetLayers, layerHasPaths);
57469
+
57470
+ // Rewrites coordinates, so lock in any pending simplification first (see -smooth).
57471
+ if (!arcs.isFlat()) {
57472
+ arcs.flatten();
57473
+ }
57474
+
57475
+ var before = arcs.getPointCount();
57476
+ filterDetailPaths(arcs, {
57477
+ distance: distance,
57478
+ tortuosity: opts.tortuosity,
57479
+ weighting: opts.weighting,
57480
+ spherical: spherical
57481
+ });
57482
+ var removed = before - arcs.getPointCount();
57483
+ message('Removed ' + removed + ' of ' + before + ' vertices');
57484
+
57485
+ if (implicitNames.length > 0) {
57486
+ message(
57487
+ 'Also filtered non-target layer' + utils.pluralSuffix(implicitNames.length) +
57488
+ ' from the same dataset: ' + implicitNames.join(', ')
57489
+ );
57490
+ }
57491
+ };
57492
+
57493
+ function filterDetailPaths(arcs, opts) {
57494
+ var nn = [];
57495
+ var xx = [];
57496
+ var yy = [];
57497
+ var i, k, res;
57498
+ arcs.forEach3(function(axx, ayy, azz) {
57499
+ res = collapseArcDetail(axx, ayy, {
57500
+ distance: opts.distance,
57501
+ tortuosity: opts.tortuosity,
57502
+ weighting: opts.weighting,
57503
+ spherical: opts.spherical
57504
+ });
57505
+ nn.push(res.xx.length);
57506
+ for (i = 0, k = res.xx.length; i < k; i++) {
57507
+ xx.push(res.xx[i]);
57508
+ yy.push(res.yy[i]);
57509
+ }
57510
+ });
57511
+ arcs.updateVertexData(nn, xx, yy);
57512
+ }
57513
+
57514
+ function convertDetailDistance(param, dataset, spherical) {
57515
+ var crs = getDatasetCRS(dataset);
57516
+ return spherical ? convertDistanceParam(param, crs) : convertIntervalParam(param, crs);
57517
+ }
57518
+
57157
57519
  cmd.filterIslands = function(lyr, dataset, optsArg) {
57158
57520
  var opts = utils.extend({sliver_control: 0}, optsArg); // no sliver control
57159
57521
  var arcs = dataset.arcs;
@@ -61979,6 +62341,933 @@ ${svg}
61979
62341
  useSphericalSimplify: useSphericalSimplify
61980
62342
  });
61981
62343
 
62344
+ // Structural-corner detection for -smooth's "keep-corners" option.
62345
+ //
62346
+ // Many boundaries alternate between natural, freely-curving stretches (coast,
62347
+ // river centerline) and artificial straight-line segments (state/county
62348
+ // borders). Plain low-pass smoothing rounds the sharp corners where artificial
62349
+ // segments meet. This module finds those corners so the caller can pin them and
62350
+ // smooth each span between them independently, leaving straight runs intact.
62351
+ //
62352
+ // The approach reduces "preserve straight segments and their corners" to
62353
+ // detecting the corners that bound long, low-curvature (straight or gently
62354
+ // curving) runs:
62355
+ // 1. Flag vertices whose direction changes sharply over a tolerance-scaled
62356
+ // window AND where that turn is concentrated near the vertex rather than
62357
+ // spread out. The concentration test compares the turn over a small inner
62358
+ // window to the turn over the full window: for a uniform curve the ratio is
62359
+ // a fixed fraction (the window-length ratio), so a steadily-curving stretch
62360
+ // -- whether a tight coastline or a gentle, hundreds-of-km graticule arc --
62361
+ // never qualifies; only a localized kink, where the inner turn approaches
62362
+ // the full turn, is a corner.
62363
+ // 2. Between flagged corners, classify each span as "structural" if it is long
62364
+ // relative to the tolerance and its curvature stays low (so a straight or
62365
+ // slowly-curving graticule line counts, but sub-tolerance wiggle does not).
62366
+ // 3. Drop corners that don't border any structural span (e.g. spikes inside a
62367
+ // wiggly coastline), merging their spans, until the partition is stable.
62368
+ //
62369
+ // All geometry is done in the caller's smoothing channels (planar x,y or
62370
+ // geocentric x,y,z), so detection is isotropic and matches the smoothing space.
62371
+ // Angles are computed with plain dot products, which work in any dimension.
62372
+
62373
+ var CORNER_ANGLE = 35 * Math.PI / 180; // min concentrated turn to call a corner
62374
+ var TANGENT_WINDOW_FACTOR = 0.25; // tangent-estimation half-window = tol * this
62375
+ var INNER_WINDOW_FACTOR = 0.4; // concentration probe window = tangentWindow * this
62376
+ var CORNER_CONCENTRATION = 0.6; // min ratio of inner-window turn to full-window turn
62377
+ var MIN_RUN_LEN_FACTOR = 1.0; // a structural run must be at least tol * this long
62378
+ var MIN_RUN_RADIUS_FACTOR = 1.0; // and bend no tighter than radius tol * this
62379
+
62380
+ function getCornerParams(tol) {
62381
+ return {
62382
+ tol: tol,
62383
+ cornerAngle: CORNER_ANGLE,
62384
+ tangentWindow: TANGENT_WINDOW_FACTOR * tol,
62385
+ innerWindow: INNER_WINDOW_FACTOR * TANGENT_WINDOW_FACTOR * tol,
62386
+ concentration: CORNER_CONCENTRATION,
62387
+ minRunLen: MIN_RUN_LEN_FACTOR * tol,
62388
+ maxTurnRate: 1 / (MIN_RUN_RADIUS_FACTOR * tol) // radians of turning per ground unit
62389
+ };
62390
+ }
62391
+
62392
+ // Find the interior corner vertices of an arc.
62393
+ // @t: cumulative arc length (length n). @channels: K coordinate arrays.
62394
+ // @cyclic: true for a closed ring (n includes the repeated closing vertex; the
62395
+ // m = n-1 unique vertices are treated cyclically). @params: getCornerParams().
62396
+ // Returns sorted vertex indices: for open arcs in [1, n-2]; for rings in [0, m).
62397
+ function findInteriorCorners(t, channels, n, cyclic, params) {
62398
+ if (n < 3) return [];
62399
+ var K = channels.length;
62400
+ var L = t[n - 1];
62401
+ var m = cyclic ? n - 1 : n;
62402
+ if (cyclic && m < 3) return [];
62403
+ var segLen = cyclic ? ringSegLengths(t, m) : null;
62404
+ var W = params.tangentWindow;
62405
+ var Wi = params.innerWindow;
62406
+ var turns = new Float64Array(m);
62407
+ var inner = new Float64Array(m);
62408
+ var lo = cyclic ? 0 : 1;
62409
+ var hi = cyclic ? m : n - 1; // exclusive
62410
+ for (var i = lo; i < hi; i++) {
62411
+ turns[i] = windowedTurn(t, channels, K, n, L, m, segLen, i, W, cyclic);
62412
+ inner[i] = windowedTurn(t, channels, K, n, L, m, segLen, i, Wi, cyclic);
62413
+ }
62414
+ // candidates above the angle threshold, that are concentrated (a localized
62415
+ // turn, not gradual bending -- see isConcentratedTurn), and that are the
62416
+ // sharpest turn within a tangent-window neighborhood (non-maximum suppression)
62417
+ var corners = [];
62418
+ for (var j = lo; j < hi; j++) {
62419
+ if (turns[j] < params.cornerAngle) continue;
62420
+ if (inner[j] < params.concentration * turns[j]) continue;
62421
+ if (isLocalMaxTurn(t, turns, j, W, L, m, lo, hi, cyclic)) corners.push(j);
62422
+ }
62423
+ return corners;
62424
+ }
62425
+
62426
+ // Is span [a, b] (inclusive vertex indices, a < b, open frame) a structural run:
62427
+ // long relative to the tolerance and low-curvature throughout?
62428
+ function isStructuralRun(t, channels, a, b, params) {
62429
+ var len = t[b] - t[a];
62430
+ if (!(len >= params.minRunLen)) return false;
62431
+ var K = channels.length;
62432
+ var totalTurn = 0;
62433
+ for (var i = a + 1; i < b; i++) {
62434
+ totalTurn += vertexTurn(channels, K, i);
62435
+ if (totalTurn / len > params.maxTurnRate) return false;
62436
+ }
62437
+ return totalTurn / len <= params.maxTurnRate;
62438
+ }
62439
+
62440
+ // --- internals ---
62441
+
62442
+ function ringSegLengths(t, m) {
62443
+ var segLen = new Float64Array(m);
62444
+ for (var i = 0; i < m; i++) segLen[i] = t[i + 1] - t[i];
62445
+ return segLen;
62446
+ }
62447
+
62448
+ // Turn angle at vertex i between the incoming and outgoing directions, each
62449
+ // estimated over an arc-length window W (so the measure is scale-aware and not
62450
+ // dominated by a single short segment).
62451
+ function windowedTurn(t, channels, K, n, L, m, segLen, i, W, cyclic) {
62452
+ var back = reach(t, segLen, n, m, L, i, -1, W, cyclic);
62453
+ var fwd = reach(t, segLen, n, m, L, i, 1, W, cyclic);
62454
+ var pi = getPt(channels, K, i);
62455
+ var pb = getPt(channels, K, back);
62456
+ var pf = getPt(channels, K, fwd);
62457
+ return angleBetween(subv(pi, pb, K), subv(pf, pi, K), K);
62458
+ }
62459
+
62460
+ // Local turn at vertex i using just the adjacent segments.
62461
+ function vertexTurn(channels, K, i) {
62462
+ var pi = getPt(channels, K, i);
62463
+ var pp = getPt(channels, K, i - 1);
62464
+ var pn = getPt(channels, K, i + 1);
62465
+ return angleBetween(subv(pi, pp, K), subv(pn, pi, K), K);
62466
+ }
62467
+
62468
+ // Walk from vertex i in direction dir (+1/-1) until accumulated arc length
62469
+ // reaches W (or a boundary, for open arcs), returning the reached vertex index.
62470
+ function reach(t, segLen, n, m, L, i, dir, W, cyclic) {
62471
+ var j = i, acc = 0;
62472
+ while (acc < W) {
62473
+ if (cyclic) {
62474
+ var k = dir > 0 ? (j + 1) % m : (j - 1 + m) % m;
62475
+ acc += dir > 0 ? segLen[j] : segLen[k];
62476
+ j = k;
62477
+ if (j === i) break; // wrapped the whole ring
62478
+ } else {
62479
+ var nk = j + dir;
62480
+ if (nk < 0 || nk > n - 1) break;
62481
+ acc += Math.abs(t[nk] - t[j]);
62482
+ j = nk;
62483
+ }
62484
+ }
62485
+ return j;
62486
+ }
62487
+
62488
+ function isLocalMaxTurn(t, turns, j, W, L, m, lo, hi, cyclic) {
62489
+ for (var k = lo; k < hi; k++) {
62490
+ if (k === j) continue;
62491
+ var d = Math.abs(t[k] - t[j]);
62492
+ if (cyclic && d > L - d) d = L - d;
62493
+ if (d < W && (turns[k] > turns[j] || (turns[k] === turns[j] && k < j))) {
62494
+ return false;
62495
+ }
62496
+ }
62497
+ return true;
62498
+ }
62499
+
62500
+ function getPt(channels, K, i) {
62501
+ var p = new Array(K);
62502
+ for (var c = 0; c < K; c++) p[c] = channels[c][i];
62503
+ return p;
62504
+ }
62505
+
62506
+ function subv(a, b, K) {
62507
+ var o = new Array(K);
62508
+ for (var c = 0; c < K; c++) o[c] = a[c] - b[c];
62509
+ return o;
62510
+ }
62511
+
62512
+ function angleBetween(u, v, K) {
62513
+ var d = 0, nu = 0, nv = 0;
62514
+ for (var c = 0; c < K; c++) {
62515
+ d += u[c] * v[c];
62516
+ nu += u[c] * u[c];
62517
+ nv += v[c] * v[c];
62518
+ }
62519
+ var den = Math.sqrt(nu * nv);
62520
+ if (!(den > 0)) return 0;
62521
+ var x = d / den;
62522
+ if (x > 1) x = 1;
62523
+ else if (x < -1) x = -1;
62524
+ return Math.acos(x);
62525
+ }
62526
+
62527
+ // Scale-aware line smoothing primitives, shared by the -smooth command.
62528
+ //
62529
+ // The smoother treats a path as coordinate signals parameterized by arc length s
62530
+ // and applies a length-scaled low-pass filter. The user-facing distance is a
62531
+ // resolution: detail with a wavelength around the distance is reduced to roughly
62532
+ // half amplitude (the -6 dB cutoff), finer detail is removed, and features much
62533
+ // larger than the distance pass through nearly unchanged. The distance is not a
62534
+ // deviation bound -- a tall, narrow sub-resolution spike can still be displaced
62535
+ // by an amount comparable to its own amplitude (inherent to convolution
62536
+ // smoothers).
62537
+ //
62538
+ // The filter is a local second-degree polynomial fit whose quadratic term
62539
+ // corrects the inward shrinkage that plain weighted averaging causes on curved
62540
+ // features. The weight kernel selects the method (see smoothPoint):
62541
+ // - 'gaussian' (default, the only documented method): Gaussian kernel
62542
+ // e^(-t^2/2sigma^2), i.e. a Savitzky-Golay smoother.
62543
+ // - 'paek': exponential kernel e^(-|t|/d) (Bodansky et al. 2002, the kernel
62544
+ // ArcGIS's PAEK uses). Kept as an undocumented alternative; with the quadratic
62545
+ // correction it differs only slightly from the gaussian method.
62546
+ //
62547
+ // The smoother works on a list of coordinate "channels": planar data is
62548
+ // smoothed in 2D (x, y); unprojected lng/lat data is converted to geocentric
62549
+ // x, y, z and smoothed in 3D Cartesian on the sphere (then converted back).
62550
+ // Averaging lng/lat directly would shear shapes toward the poles (a degree of
62551
+ // longitude shrinks with cos(lat)); the geocentric representation is isotropic
62552
+ // and handles the antimeridian and poles without special cases, mirroring how
62553
+ // -simplify treats spherical coordinates. The kernel scale stays in true ground
62554
+ // distance because arc length is measured with great-circle distance.
62555
+ //
62556
+ // The user-facing distance is a *resolution*: detail finer than it is removed, a
62557
+ // feature about its size is reduced to roughly half amplitude, and larger
62558
+ // features are largely preserved. KERNEL_FROM_DISTANCE maps that resolution onto
62559
+ // the internal kernel scale, calibrated empirically so the half-amplitude
62560
+ // (-6 dB) wavelength ~= the distance. The remaining calibration constants are
62561
+ // expressed relative to that internal scale and map it onto kernel widths and
62562
+ // output sampling; they are collected here so the mapping can be retuned in one
62563
+ // place. See docs/reference.md.
62564
+ var KERNEL_FROM_DISTANCE = 1.2; // internal kernel scale = distance * this
62565
+ var GAUSSIAN_SIGMA_FACTOR = 0.4; // gaussian sigma = internal scale * this
62566
+ var PAEK_SCALE_FACTOR = 0.4; // exponential kernel scale d = internal scale * this
62567
+ var WINDOW_RADIUS_FACTOR = 1.2; // window half-length = internal scale * this
62568
+ var SOURCE_SPACING_FACTOR = 0.25; // densify source to <= tolerance * this before smoothing
62569
+ var MAX_OUTPUT_FACTOR = 8; // cap output (and source) vertices at inputCount * this
62570
+ var MIN_CLOSED_SEGMENTS = 16; // floor on segments for closed rings (so they resolve)
62571
+
62572
+ // Output resampling. The smoothed curve is a continuous function of arc length;
62573
+ // we sample it densely at a uniform step and then thin that dense polyline with a
62574
+ // single O(n) forward pass that keeps a vertex only where the curve has bent
62575
+ // enough since the last kept vertex. Filtering on accumulated bend angle (rather
62576
+ // than chord deviation, as Douglas-Peucker does) bounds the angle between
62577
+ // consecutive output segments *by construction*, so joins stay smooth with no
62578
+ // separate tangent test. This works cleanly because the filter consumes the
62579
+ // already-smoothed (denoised) curve, where per-vertex turning is small and
62580
+ // well-behaved. Density follows curvature for free: bends accumulate angle
62581
+ // quickly and keep many vertices; straight or gently-curving runs accumulate
62582
+ // slowly and collapse to long segments.
62583
+ //
62584
+ // Accumulated angle alone does not bound *absolute* deviation: a very gentle but
62585
+ // very long bend accumulates angle so slowly that its chord can bow far from the
62586
+ // curve before reaching the angle threshold. A sagitta guard handles that --
62587
+ // chord * accumulatedTurn / 8 estimates the bow of a circular arc, and we also
62588
+ // cut when it exceeds a fraction of the tolerance. Both tests are O(1) per dense
62589
+ // vertex, so the pass stays O(n).
62590
+ var DENSE_STEP_FACTOR = 0.033; // dense sampling step = tolerance * this. Must
62591
+ // resolve the sharpest smoothed feature (radius
62592
+ // ~ the kernel scale) finely enough that the
62593
+ // angle filter can reach BEND_ANGLE joins: one
62594
+ // dense segment turns ~ this/0.4 radians there,
62595
+ // kept well under BEND_ANGLE so the discrete
62596
+ // accumulation barely overshoots the threshold.
62597
+ var BEND_ANGLE = 8 * Math.PI / 180; // keep a vertex after this much accumulated turn
62598
+ var DEVIATION_FACTOR = 0.1; // sagitta guard: also cut a gentle bend that bows
62599
+ // more than tolerance * this from its chord
62600
+
62601
+ // Smooth a single arc's coordinates.
62602
+ // @xx, @yy: coordinate arrays (may be typed-array subarrays) for one arc.
62603
+ // @opts: {tolerance, method, spherical, closed, keepCorners}
62604
+ // Returns {xx: [], yy: []} with the smoothed coordinates. Endpoints of open
62605
+ // arcs are preserved exactly (so shared topology nodes stay put); closed arcs
62606
+ // are smoothed cyclically and returned closed (first point repeated at the end).
62607
+ // With keepCorners, structural corners (where long straight/low-curvature runs
62608
+ // meet) are detected and pinned, and the runs themselves are kept verbatim;
62609
+ // only the spans between corners are smoothed.
62610
+ // Resolve the curvature-correction gain (default 1 = fully corrected). gain=0
62611
+ // leaves the plain weighted moving average; negative values are clamped to 0.
62612
+ function resolveGain(opts) {
62613
+ var g = opts.gain;
62614
+ if (g === undefined || g === null) return 1;
62615
+ return g >= 0 ? g : 0;
62616
+ }
62617
+
62618
+ function smoothArcCoords(xx, yy, opts) {
62619
+ var n = xx.length;
62620
+ var origX = toArray(xx);
62621
+ var origY = toArray(yy);
62622
+ var tol = opts.tolerance * KERNEL_FROM_DISTANCE;
62623
+ if (n < 3 || !(opts.tolerance > 0)) {
62624
+ return {xx: origX, yy: origY};
62625
+ }
62626
+ var method = opts.method == 'gaussian' ? 'gaussian' : 'paek';
62627
+ var closed = !!opts.closed;
62628
+ var spherical = !!opts.spherical;
62629
+ var keepCorners = !!opts.keepCorners;
62630
+ var ctx = {
62631
+ tol: tol,
62632
+ method: method,
62633
+ spherical: spherical,
62634
+ keepCorners: keepCorners,
62635
+ gain: resolveGain(opts),
62636
+ radius: tol * WINDOW_RADIUS_FACTOR,
62637
+ scale: (method == 'gaussian' ? GAUSSIAN_SIGMA_FACTOR : PAEK_SCALE_FACTOR) * tol
62638
+ };
62639
+
62640
+ // Cumulative arc length in ground units (meters for spherical data), so the
62641
+ // kernel scale stays in true distance regardless of coordinate representation.
62642
+ var t = arcLengths(origX, origY, n, spherical);
62643
+ if (!(t[n - 1] > 0)) {
62644
+ return {xx: origX, yy: origY}; // degenerate (coincident points)
62645
+ }
62646
+ var channels = spherical ? lngLatToXYZChannels(origX, origY, n) : [origX, origY];
62647
+
62648
+ if (closed) {
62649
+ var corners = keepCorners ?
62650
+ findInteriorCorners(t, channels, n, true, getCornerParams(tol)) : [];
62651
+ if (corners.length === 0) {
62652
+ return smoothClosedCyclic(t, channels, n, ctx);
62653
+ }
62654
+ // A ring with corners is processed as an open path: rotate it to start (and
62655
+ // end) at one corner, with the remaining corners as interior breakpoints.
62656
+ var rot = rotateRing(origX, origY, n, corners[0]);
62657
+ origX = rot.xx;
62658
+ origY = rot.yy;
62659
+ n = origX.length;
62660
+ t = arcLengths(origX, origY, n, spherical);
62661
+ channels = spherical ? lngLatToXYZChannels(origX, origY, n) : [origX, origY];
62662
+ var breaks = mapRotatedCorners(corners, rot.shift, rot.m);
62663
+ return smoothOpenSpans(origX, origY, t, channels, n, breaks, ctx);
62664
+ }
62665
+
62666
+ var openBreaks = keepCorners ?
62667
+ findInteriorCorners(t, channels, n, false, getCornerParams(tol)) : [];
62668
+ return smoothOpenSpans(origX, origY, t, channels, n, openBreaks, ctx);
62669
+ }
62670
+
62671
+ // Smooth an open path partitioned at @interiorBreaks (sorted interior vertex
62672
+ // indices). Structural runs are copied verbatim; other spans are smoothed with
62673
+ // their endpoints pinned, so every breakpoint (and the two arc endpoints) keeps
62674
+ // its exact original position. Shared breakpoint vertices are emitted once.
62675
+ function smoothOpenSpans(origX, origY, t, channels, n, interiorBreaks, ctx) {
62676
+ var bounds = [0].concat(interiorBreaks);
62677
+ bounds.push(n - 1);
62678
+ if (ctx.keepCorners && bounds.length > 2) {
62679
+ bounds = refineBounds(t, channels, bounds, getCornerParams(ctx.tol));
62680
+ }
62681
+ var params = ctx.keepCorners ? getCornerParams(ctx.tol) : null;
62682
+ var xx = [], yy = [];
62683
+ for (var s = 0; s < bounds.length - 1; s++) {
62684
+ var lo = bounds[s], hi = bounds[s + 1];
62685
+ var preserve = !!params && isStructuralRun(t, channels, lo, hi, params);
62686
+ var span = preserve ?
62687
+ copySpan(origX, origY, lo, hi) :
62688
+ smoothSpanOpen(origX, origY, t, channels, lo, hi, ctx);
62689
+ appendSpan(xx, yy, span, s === 0);
62690
+ }
62691
+ return {xx: xx, yy: yy};
62692
+ }
62693
+
62694
+ // Drop interior breakpoints that don't border any structural run (e.g. spikes
62695
+ // inside a wiggly stretch), merging their spans, until the partition is stable.
62696
+ // Merging can turn two short straight pieces back into one structural run, so
62697
+ // structurality is re-tested each pass.
62698
+ function refineBounds(t, channels, bounds, params) {
62699
+ var changed = true;
62700
+ while (changed && bounds.length > 2) {
62701
+ changed = false;
62702
+ for (var i = 1; i < bounds.length - 1; i++) {
62703
+ var leftStruct = isStructuralRun(t, channels, bounds[i - 1], bounds[i], params);
62704
+ var rightStruct = isStructuralRun(t, channels, bounds[i], bounds[i + 1], params);
62705
+ if (!leftStruct && !rightStruct) {
62706
+ bounds.splice(i, 1);
62707
+ changed = true;
62708
+ break;
62709
+ }
62710
+ }
62711
+ }
62712
+ return bounds;
62713
+ }
62714
+
62715
+ // Smooth a single open span [lo, hi] (inclusive) and pin both ends to their
62716
+ // original coordinates. Reuses the whole-arc smoothing pipeline on the sub-arc.
62717
+ function smoothSpanOpen(origX, origY, t, channels, lo, hi, ctx) {
62718
+ var nSub = hi - lo + 1;
62719
+ if (nSub < 3) return copySpan(origX, origY, lo, hi);
62720
+ var subT = new Float64Array(nSub);
62721
+ for (var k = 0; k < nSub; k++) subT[k] = t[lo + k] - t[lo];
62722
+ var subL = subT[nSub - 1];
62723
+ if (!(subL > 0)) return copySpan(origX, origY, lo, hi);
62724
+ var subCh = [];
62725
+ for (var c = 0; c < channels.length; c++) subCh.push(channels[c].slice(lo, hi + 1));
62726
+ var maxSourcePts = Math.max(nSub, MIN_CLOSED_SEGMENTS) * MAX_OUTPUT_FACTOR + nSub;
62727
+ var maxSpacing = Math.max(ctx.tol * SOURCE_SPACING_FACTOR, subL / maxSourcePts);
62728
+ var dense = densifyChannels(subT, subCh, maxSpacing);
62729
+ var src = buildSource(dense.t, dense.channels, false, ctx.radius, subL);
62730
+ var sm = sampleSmoothedCurve(src, 0, subL, false, ctx, nSub);
62731
+ var out = ctx.spherical ? xyzChannelsToLngLat(sm) : {xx: sm[0], yy: sm[1]};
62732
+ out.xx[0] = origX[lo];
62733
+ out.yy[0] = origY[lo];
62734
+ out.xx[out.xx.length - 1] = origX[hi];
62735
+ out.yy[out.yy.length - 1] = origY[hi];
62736
+ return out;
62737
+ }
62738
+
62739
+ function smoothClosedCyclic(t, channels, n, ctx) {
62740
+ var L = t[n - 1];
62741
+ var maxSourcePts = Math.max(n, MIN_CLOSED_SEGMENTS) * MAX_OUTPUT_FACTOR + n;
62742
+ var maxSpacing = Math.max(ctx.tol * SOURCE_SPACING_FACTOR, L / maxSourcePts);
62743
+ var dense = densifyChannels(t, channels, maxSpacing);
62744
+ var src = buildSource(dense.t, dense.channels, true, ctx.radius, L);
62745
+ var sm = sampleSmoothedCurve(src, 0, L, true, ctx, n);
62746
+ var out = ctx.spherical ? xyzChannelsToLngLat(sm) : {xx: sm[0], yy: sm[1]};
62747
+ // force an exactly closed ring (the periodic endpoints are equal up to fp)
62748
+ out.xx[out.xx.length - 1] = out.xx[0];
62749
+ out.yy[out.yy.length - 1] = out.yy[0];
62750
+ return out;
62751
+ }
62752
+
62753
+ function copySpan(origX, origY, lo, hi) {
62754
+ var xx = [], yy = [];
62755
+ for (var i = lo; i <= hi; i++) {
62756
+ xx.push(origX[i]);
62757
+ yy.push(origY[i]);
62758
+ }
62759
+ return {xx: xx, yy: yy};
62760
+ }
62761
+
62762
+ function appendSpan(xx, yy, span, isFirst) {
62763
+ for (var i = isFirst ? 0 : 1; i < span.xx.length; i++) {
62764
+ xx.push(span.xx[i]);
62765
+ yy.push(span.yy[i]);
62766
+ }
62767
+ }
62768
+
62769
+ // Reorder a closed ring's unique vertices to begin at index @c, re-appending the
62770
+ // start vertex so the result is a closed open-path (first point repeated).
62771
+ function rotateRing(xx, yy, n, c) {
62772
+ var m = n - 1;
62773
+ var ox = [], oy = [];
62774
+ for (var k = 0; k < m; k++) {
62775
+ var idx = (c + k) % m;
62776
+ ox.push(xx[idx]);
62777
+ oy.push(yy[idx]);
62778
+ }
62779
+ ox.push(xx[c]);
62780
+ oy.push(yy[c]);
62781
+ return {xx: ox, yy: oy, shift: c, m: m};
62782
+ }
62783
+
62784
+ // Map ring-frame corner indices to interior positions in the rotated open frame.
62785
+ // The corner the ring was rotated to becomes the (pinned) endpoint, so it is
62786
+ // dropped from the interior list.
62787
+ function mapRotatedCorners(corners, shift, m) {
62788
+ var out = [];
62789
+ for (var i = 0; i < corners.length; i++) {
62790
+ var pos = (corners[i] - shift + m) % m;
62791
+ if (pos > 0) out.push(pos);
62792
+ }
62793
+ out.sort(function(a, b) { return a - b; });
62794
+ return out;
62795
+ }
62796
+
62797
+ function arcLengths(xx, yy, n, spherical) {
62798
+ var t = new Float64Array(n);
62799
+ var distFn = spherical ? geom.greatCircleDistance : geom.distance2D;
62800
+ for (var i = 1; i < n; i++) {
62801
+ t[i] = t[i - 1] + distFn(xx[i - 1], yy[i - 1], xx[i], yy[i]);
62802
+ }
62803
+ return t;
62804
+ }
62805
+
62806
+ function lngLatToXYZChannels(lng, lat, n) {
62807
+ var X = new Float64Array(n), Y = new Float64Array(n), Z = new Float64Array(n), p = [];
62808
+ for (var i = 0; i < n; i++) {
62809
+ geom.lngLatToXYZ(lng[i], lat[i], p);
62810
+ X[i] = p[0];
62811
+ Y[i] = p[1];
62812
+ Z[i] = p[2];
62813
+ }
62814
+ return [X, Y, Z];
62815
+ }
62816
+
62817
+ // Convert smoothed geocentric channels back to lng/lat. xyzToLngLat() projects
62818
+ // each point radially onto the sphere, so the slightly-inside-the-sphere point
62819
+ // produced by averaging maps back to a valid surface coordinate.
62820
+ function xyzChannelsToLngLat(channels) {
62821
+ var X = channels[0], Y = channels[1], Z = channels[2], n = X.length;
62822
+ var xx = [], yy = [], p = [];
62823
+ for (var i = 0; i < n; i++) {
62824
+ geom.xyzToLngLat(X[i], Y[i], Z[i], p);
62825
+ xx.push(p[0]);
62826
+ yy.push(p[1]);
62827
+ }
62828
+ return {xx: xx, yy: yy};
62829
+ }
62830
+
62831
+ // Insert linearly-interpolated samples so no gap between consecutive source
62832
+ // points exceeds @maxSpacing. Interpolating in the smoothing channels keeps
62833
+ // this representation-agnostic (for geocentric input the inserted points sit on
62834
+ // the chord, negligibly below the surface at these spacings, and are
62835
+ // renormalized on the way out). Endpoints (and the closing vertex of a ring)
62836
+ // are preserved exactly.
62837
+ function densifyChannels(t, channels, maxSpacing) {
62838
+ var n = t.length;
62839
+ var K = channels.length;
62840
+ var t2 = [];
62841
+ var c2 = [];
62842
+ for (var c = 0; c < K; c++) c2.push([]);
62843
+ for (var i = 0; i < n - 1; i++) {
62844
+ t2.push(t[i]);
62845
+ for (var a = 0; a < K; a++) c2[a].push(channels[a][i]);
62846
+ var seg = t[i + 1] - t[i];
62847
+ if (seg > maxSpacing) {
62848
+ var steps = Math.ceil(seg / maxSpacing);
62849
+ for (var s = 1; s < steps; s++) {
62850
+ var f = s / steps;
62851
+ t2.push(t[i] + seg * f);
62852
+ for (var b = 0; b < K; b++) {
62853
+ c2[b].push(channels[b][i] + (channels[b][i + 1] - channels[b][i]) * f);
62854
+ }
62855
+ }
62856
+ }
62857
+ }
62858
+ t2.push(t[n - 1]);
62859
+ for (var d = 0; d < K; d++) c2[d].push(channels[d][n - 1]);
62860
+ return {t: new Float64Array(t2), channels: c2, count: t2.length};
62861
+ }
62862
+
62863
+ // Sample the smoothed curve over arc-length [a, b] and thin it (see the
62864
+ // "Output resampling" note above). Step 1 evaluates the smoother at a uniform
62865
+ // dense step; step 2 makes one forward pass keeping the endpoints plus every
62866
+ // interior vertex where the accumulated turn since the last kept vertex reaches
62867
+ // BEND_ANGLE, or where the sagitta guard trips. @inputCount bounds the dense
62868
+ // sampling (and thus the output). Returns one array per channel, ordered by
62869
+ // increasing arc length, including both endpoints.
62870
+ function sampleSmoothedCurve(src, a, b, closed, ctx, inputCount) {
62871
+ var K = src.channels.length;
62872
+ var span = b - a;
62873
+ var maxPoints = Math.max(inputCount, MIN_CLOSED_SEGMENTS) * MAX_OUTPUT_FACTOR;
62874
+
62875
+ // 1. dense uniform sampling of the smoothed curve
62876
+ var nDense = Math.ceil(span / (ctx.tol * DENSE_STEP_FACTOR)) + 1;
62877
+ if (nDense < MIN_CLOSED_SEGMENTS + 1) nDense = MIN_CLOSED_SEGMENTS + 1;
62878
+ if (nDense > maxPoints) nDense = maxPoints;
62879
+ if (nDense < 2) nDense = 2;
62880
+ var P = new Array(nDense);
62881
+ for (var i = 0; i < nDense; i++) {
62882
+ P[i] = smoothAt(src, a + span * (i / (nDense - 1)), ctx);
62883
+ }
62884
+
62885
+ // 2. one-pass bend-angle filter
62886
+ var theta = BEND_ANGLE;
62887
+ var epsDev = ctx.tol * DEVIATION_FACTOR;
62888
+ var out = [];
62889
+ for (var c = 0; c < K; c++) out.push([]);
62890
+ appendPoint(out, P[0], K);
62891
+ var anchor = 0; // last kept vertex
62892
+ var accTurn = 0; // absolute turning accumulated since the anchor
62893
+ for (var j = 1; j < nDense - 1; j++) {
62894
+ accTurn += vecAngle(P[j - 1], P[j], P[j], P[j + 1], K);
62895
+ // sagitta of a circular arc of chord c and total turn a is ~ c*a/8; cut a
62896
+ // long gentle bend before it bows more than epsDev from its chord
62897
+ var sagitta = chordLen(P[anchor], P[j + 1], K) * accTurn * 0.125;
62898
+ if (accTurn >= theta || sagitta >= epsDev) {
62899
+ appendPoint(out, P[j], K);
62900
+ anchor = j;
62901
+ accTurn = 0;
62902
+ }
62903
+ }
62904
+ appendPoint(out, P[nDense - 1], K);
62905
+ return out;
62906
+ }
62907
+
62908
+ // Angle (radians) between vectors (b - a) and (d - c) over K channels.
62909
+ function vecAngle(a, b, c, d, K) {
62910
+ var dot = 0, n1 = 0, n2 = 0;
62911
+ for (var i = 0; i < K; i++) {
62912
+ var u = b[i] - a[i];
62913
+ var v = d[i] - c[i];
62914
+ dot += u * v;
62915
+ n1 += u * u;
62916
+ n2 += v * v;
62917
+ }
62918
+ if (n1 <= 0 || n2 <= 0) return 0;
62919
+ var k = dot / Math.sqrt(n1 * n2);
62920
+ if (k > 1) k = 1;
62921
+ else if (k < -1) k = -1;
62922
+ return Math.acos(k);
62923
+ }
62924
+
62925
+ function chordLen(a, b, K) {
62926
+ var s = 0;
62927
+ for (var c = 0; c < K; c++) {
62928
+ var d = b[c] - a[c];
62929
+ s += d * d;
62930
+ }
62931
+ return Math.sqrt(s);
62932
+ }
62933
+
62934
+ // Evaluate the smoother at a single arc-length position by binary-searching the
62935
+ // window of source samples within +/- radius of phi. Open sources are padded
62936
+ // with odd reflections at each end (see buildSource), so the window is always
62937
+ // full and symmetric -- no special boundary handling is needed here.
62938
+ function smoothAt(src, phi, ctx) {
62939
+ var t = src.t, m = src.count;
62940
+ var lo = lowerBound(t, m, phi - ctx.radius);
62941
+ var hi = upperBound(t, m, phi + ctx.radius);
62942
+ return smoothPoint(t, src.channels, lo, hi, phi, ctx.method, ctx.scale, ctx.radius, ctx.gain);
62943
+ }
62944
+
62945
+ // first index with t[i] >= x
62946
+ function lowerBound(t, n, x) {
62947
+ var lo = 0, hi = n;
62948
+ while (lo < hi) {
62949
+ var mid = (lo + hi) >> 1;
62950
+ if (t[mid] < x) lo = mid + 1;
62951
+ else hi = mid;
62952
+ }
62953
+ return lo;
62954
+ }
62955
+
62956
+ // first index with t[i] > x
62957
+ function upperBound(t, n, x) {
62958
+ var lo = 0, hi = n;
62959
+ while (lo < hi) {
62960
+ var mid = (lo + hi) >> 1;
62961
+ if (t[mid] <= x) lo = mid + 1;
62962
+ else hi = mid;
62963
+ }
62964
+ return lo;
62965
+ }
62966
+
62967
+ function appendPoint(out, p, K) {
62968
+ for (var c = 0; c < K; c++) out[c].push(p[c]);
62969
+ }
62970
+
62971
+ // Assemble the weighting samples for an arc, tagged with cumulative arc length.
62972
+ //
62973
+ // Open arcs are padded with odd (point) reflections of the samples within
62974
+ // `radius` of each end: a sample at offset tau inside the end is mirrored to
62975
+ // -tau with position 2*endpoint - sample. This keeps the smoothing window full
62976
+ // and symmetric at the ends instead of one-sided. A one-sided window biases the
62977
+ // result inward (it averages only interior neighbors), dragging the smoothed
62978
+ // endpoint off a curving end; pinning it back then leaves a long, kinked final
62979
+ // segment. With odd reflection each mirror pair averages back to the endpoint,
62980
+ // so the endpoint is preserved *exactly* while detail right up to it is still
62981
+ // fully smoothed, and the curve leaves the endpoint along a smooth tangent.
62982
+ // (A straight run reflects to its own continuation, so straights stay straight.)
62983
+ //
62984
+ // Closed arcs are instead replicated across enough periods (each shifted by the
62985
+ // perimeter length) that any query window wraps correctly around the ring.
62986
+ function buildSource(t, channels, closed, radius, L) {
62987
+ var n = t.length;
62988
+ var K = channels.length;
62989
+ if (!closed) {
62990
+ return buildOpenSource(t, channels, n, K, radius, L);
62991
+ }
62992
+ var m = n - 1; // drop duplicated closing vertex; period is L
62993
+ var reps = Math.max(1, Math.ceil(radius / L));
62994
+ var et = [];
62995
+ var ec = [];
62996
+ for (var c = 0; c < K; c++) ec.push([]);
62997
+ for (var k = -reps; k <= reps; k++) {
62998
+ var off = k * L;
62999
+ for (var j = 0; j < m; j++) {
63000
+ et.push(t[j] + off);
63001
+ for (var c2 = 0; c2 < K; c2++) ec[c2].push(channels[c2][j]);
63002
+ }
63003
+ }
63004
+ return {t: et, channels: ec, count: et.length, totalLength: L, closed: true};
63005
+ }
63006
+
63007
+ function buildOpenSource(t, channels, n, K, radius, L) {
63008
+ var et = [];
63009
+ var ec = [];
63010
+ for (var c = 0; c < K; c++) ec.push([]);
63011
+ // left odd-reflections (ascending t from ~-radius up to 0): walk interior
63012
+ // samples with t <= radius from the outermost inward so output stays sorted
63013
+ var leftEnd = upperBound(t, n, radius) - 1; // last index with t[i] <= radius
63014
+ for (var i = leftEnd; i >= 1; i--) {
63015
+ et.push(-t[i]);
63016
+ for (var c0 = 0; c0 < K; c0++) ec[c0].push(2 * channels[c0][0] - channels[c0][i]);
63017
+ }
63018
+ // originals
63019
+ for (var j = 0; j < n; j++) {
63020
+ et.push(t[j]);
63021
+ for (var c1 = 0; c1 < K; c1++) ec[c1].push(channels[c1][j]);
63022
+ }
63023
+ // right odd-reflections (ascending t just above L): nearest-to-L first
63024
+ for (var k = n - 2; k >= 0 && t[k] >= L - radius; k--) {
63025
+ et.push(2 * L - t[k]);
63026
+ for (var c2 = 0; c2 < K; c2++) ec[c2].push(2 * channels[c2][n - 1] - channels[c2][k]);
63027
+ }
63028
+ return {t: et, channels: ec, count: et.length, totalLength: L, closed: false};
63029
+ }
63030
+
63031
+ // Both smoothing methods compute the smoothed coordinate as a local weighted
63032
+ // least-squares fit of a second-degree polynomial in the normalized arc-length
63033
+ // offset u = (t - phi)/scale, evaluated at u = 0 (the polynomial's constant
63034
+ // term). The quadratic term lets the fit follow curvature, so the smoothed point
63035
+ // is not pulled toward the chord on a bend the way a plain weighted average is.
63036
+ // That is what keeps either method from shrinking the amplitude of supra-
63037
+ // tolerance features. The methods differ only in the weight kernel (see
63038
+ // kernelWeight): 'paek' is Bodansky et al.'s exponential-weighted quadratic (the
63039
+ // algorithm ArcGIS uses); 'gaussian' is the Gaussian-weighted quadratic, i.e. a
63040
+ // Savitzky-Golay smoother (a sharper frequency cutoff than paek, now with the
63041
+ // same shrinkage correction). Normalizing by `scale` keeps the normal-equation
63042
+ // matrix well-scaled regardless of the absolute tolerance. The fit is linear in
63043
+ // the channel values, so for geocentric input the smoothed point stays in the
63044
+ // plane of nearby vertices (a great-circle arc is preserved up to discretization).
63045
+ // With too few points, or near-singular normal equations, falls back to the
63046
+ // plain weighted average using the same kernel.
63047
+ //
63048
+ // Weights are tapered to reach zero at the window edge (|u| = radius/scale) by
63049
+ // subtracting the edge weight: w = max(0, kernel(u) - kernel(uEdge)). Without
63050
+ // this taper a source sample enters/leaves the moving window carrying a small
63051
+ // but nonzero weight, and that discrete jump -- amplified by the quadratic fit --
63052
+ // shows up as fine-scale jitter rendered as visible kinks. The taper is symmetric
63053
+ // in |u|, so odd-reflection endpoint preservation is unaffected.
63054
+ function smoothPoint(t, channels, lo, hi, phi, method, scale, radius, gain) {
63055
+ var gaussian = method == 'gaussian';
63056
+ // gain scales the quadratic (Savitzky-Golay) curvature correction relative to
63057
+ // the plain weighted moving average m: out = m + gain*(a0 - m). gain=0 leaves
63058
+ // the shrinking moving average, gain=1 is the fully corrected fit, and gain>1
63059
+ // exaggerates the curvature of bends.
63060
+ if (gain === 0 || hi - lo < 3) return weightedAverage(t, channels, lo, hi, phi, scale, radius, gaussian);
63061
+ var edgeW = kernelWeight(radius / scale, gaussian);
63062
+ var s0 = 0, s1 = 0, s2 = 0, s3 = 0, s4 = 0;
63063
+ var K = channels.length;
63064
+ var b0 = new Float64Array(K), b1 = new Float64Array(K), b2 = new Float64Array(K);
63065
+ for (var i = lo; i < hi; i++) {
63066
+ var u = (t[i] - phi) / scale;
63067
+ var w = kernelWeight(u, gaussian) - edgeW;
63068
+ if (w <= 0) continue;
63069
+ var wu = w * u;
63070
+ var wu2 = wu * u;
63071
+ s0 += w;
63072
+ s1 += wu;
63073
+ s2 += wu2;
63074
+ s3 += wu2 * u;
63075
+ s4 += wu2 * u * u;
63076
+ for (var c = 0; c < K; c++) {
63077
+ var v = channels[c][i];
63078
+ b0[c] += w * v;
63079
+ b1[c] += wu * v;
63080
+ b2[c] += wu2 * v;
63081
+ }
63082
+ }
63083
+ // det of the symmetric normal-equation matrix [[s0,s1,s2],[s1,s2,s3],[s2,s3,s4]]
63084
+ var c0 = s2 * s4 - s3 * s3;
63085
+ var c1 = s1 * s4 - s3 * s2;
63086
+ var c2 = s1 * s3 - s2 * s2;
63087
+ var det = s0 * c0 - s1 * c1 + s2 * c2;
63088
+ if (!(Math.abs(det) > 1e-9 * (s0 * s0 * s0 + 1))) {
63089
+ return weightedAverage(t, channels, lo, hi, phi, scale, radius, gaussian);
63090
+ }
63091
+ var out = new Array(K);
63092
+ for (var ch = 0; ch < K; ch++) {
63093
+ var a0 = solveConstantTerm(b0[ch], b1[ch], b2[ch], s1, s2, s3, s4, c0, det);
63094
+ var mean = b0[ch] / s0; // plain weighted moving average for this channel
63095
+ out[ch] = mean + gain * (a0 - mean);
63096
+ }
63097
+ return out;
63098
+ }
63099
+
63100
+ // Smoothing kernel weight at normalized offset u: exponential e^(-|u|) for paek,
63101
+ // Gaussian e^(-u^2/2) for the Savitzky-Golay gaussian method.
63102
+ function kernelWeight(u, gaussian) {
63103
+ return gaussian ? Math.exp(-0.5 * u * u) : Math.exp(-Math.abs(u));
63104
+ }
63105
+
63106
+ // Solve for the constant term a0 of the fitted quadratic via Cramer's rule
63107
+ // (replace the first column of the normal matrix with the RHS [b0,b1,b2]).
63108
+ function solveConstantTerm(b0, b1, b2, s1, s2, s3, s4, c0, det) {
63109
+ var detA = b0 * c0 - s1 * (b1 * s4 - s3 * b2) + s2 * (b1 * s3 - s2 * b2);
63110
+ return detA / det;
63111
+ }
63112
+
63113
+ function weightedAverage(t, channels, lo, hi, phi, scale, radius, gaussian) {
63114
+ var K = channels.length;
63115
+ var edgeW = kernelWeight(radius / scale, gaussian);
63116
+ var wsum = 0;
63117
+ var sums = new Float64Array(K);
63118
+ for (var i = lo; i < hi; i++) {
63119
+ var w = kernelWeight((t[i] - phi) / scale, gaussian) - edgeW;
63120
+ if (w <= 0) continue;
63121
+ wsum += w;
63122
+ for (var c = 0; c < K; c++) sums[c] += w * channels[c][i];
63123
+ }
63124
+ if (!(wsum > 0)) return interpAlongSource(t, channels, lo, hi, phi);
63125
+ return scaleSums(sums, 1 / wsum, K);
63126
+ }
63127
+
63128
+ // Fallback used when a window is empty or weights underflow: linearly
63129
+ // interpolate the position on the source line at arc-length phi (using the
63130
+ // samples bracketing the window). This keeps the output point on the line
63131
+ // instead of snapping to a vertex, so sparse regions degrade gracefully to the
63132
+ // original geometry without staircase artifacts.
63133
+ function interpAlongSource(t, channels, lo, hi, phi) {
63134
+ var K = channels.length;
63135
+ var m = t.length;
63136
+ var i0 = (hi > lo ? lo : lo - 1);
63137
+ if (i0 < 0) i0 = 0;
63138
+ if (i0 > m - 1) i0 = m - 1;
63139
+ var i1 = i0 + 1;
63140
+ if (i1 > m - 1) i1 = m - 1;
63141
+ var out = new Array(K);
63142
+ if (i0 === i1 || t[i1] === t[i0]) {
63143
+ for (var c = 0; c < K; c++) out[c] = channels[c][i0];
63144
+ return out;
63145
+ }
63146
+ var f = (phi - t[i0]) / (t[i1] - t[i0]);
63147
+ if (f < 0) f = 0;
63148
+ else if (f > 1) f = 1;
63149
+ for (var ch = 0; ch < K; ch++) {
63150
+ out[ch] = channels[ch][i0] + (channels[ch][i1] - channels[ch][i0]) * f;
63151
+ }
63152
+ return out;
63153
+ }
63154
+
63155
+ function scaleSums(sums, k, K) {
63156
+ var out = new Array(K);
63157
+ for (var c = 0; c < K; c++) out[c] = sums[c] * k;
63158
+ return out;
63159
+ }
63160
+
63161
+ function toArray(arr) {
63162
+ var out = [];
63163
+ for (var i = 0, n = arr.length; i < n; i++) out.push(arr[i]);
63164
+ return out;
63165
+ }
63166
+
63167
+ cmd.smooth = function(dataset, opts, targetLayers) {
63168
+ var arcs = dataset.arcs;
63169
+ if (!arcs || arcs.size() === 0) return;
63170
+ opts = opts || {};
63171
+ if (opts.distance === undefined || opts.distance === null || opts.distance === '') {
63172
+ stop$1('Missing a smoothing distance');
63173
+ }
63174
+ var spherical = useSphericalSmooth(arcs, opts);
63175
+ var tolerance = convertSmoothTolerance(opts.distance, dataset, spherical);
63176
+ if (!(tolerance > 0)) {
63177
+ stop$1('Expected a positive smoothing distance');
63178
+ }
63179
+ var method = getSmoothMethod(opts);
63180
+ if (opts.gain !== undefined && opts.gain !== null && !(opts.gain >= 0)) {
63181
+ stop$1('Expected gain to be a number >= 0');
63182
+ }
63183
+ var implicitlySmoothedNames = getImplicitlyTargetedLayerNames(dataset, targetLayers, layerHasPaths);
63184
+
63185
+ // Smoothing rewrites coordinates, so lock in any pending (non-destructive)
63186
+ // simplification first -- otherwise we would smooth the original
63187
+ // full-resolution geometry and silently discard the simplification.
63188
+ if (!arcs.isFlat()) {
63189
+ arcs.flatten();
63190
+ }
63191
+
63192
+ // By default, cut intricate sub-scale detail (jetties, narrow inlets, spikes)
63193
+ // that the low-pass smoother cannot generalize cleanly -- left in, these tend
63194
+ // to produce kinks or self-intersections. The detail scale matches the
63195
+ // smoothing distance. Disable with no-prefilter.
63196
+ if (!opts.no_prefilter) {
63197
+ var before = arcs.getPointCount();
63198
+ filterDetailPaths(arcs, {
63199
+ distance: tolerance,
63200
+ spherical: spherical
63201
+ });
63202
+ var removed = before - arcs.getPointCount();
63203
+ if (removed > 0) {
63204
+ message('Prefilter removed ' + removed + ' of ' + before + ' vertices');
63205
+ }
63206
+ }
63207
+
63208
+ smoothPaths(arcs, {
63209
+ tolerance: tolerance,
63210
+ method: method,
63211
+ spherical: spherical,
63212
+ keepCorners: !!opts.keep_corners,
63213
+ gain: opts.gain
63214
+ });
63215
+
63216
+ if (implicitlySmoothedNames.length > 0) {
63217
+ message(
63218
+ 'Also smoothed non-target layer' + utils.pluralSuffix(implicitlySmoothedNames.length) +
63219
+ ' from the same dataset: ' + implicitlySmoothedNames.join(', ')
63220
+ );
63221
+ }
63222
+ };
63223
+
63224
+ // Rewrite every arc's vertices in place. Because the arc-to-shape references are
63225
+ // untouched, shared polygon boundaries stay coincident and topology is
63226
+ // preserved; updateVertexData() also handles undo capture and resets stale
63227
+ // simplification thresholds.
63228
+ function smoothPaths(arcs, opts) {
63229
+ var nn = [];
63230
+ var xx = [];
63231
+ var yy = [];
63232
+ var i, k, res;
63233
+ arcs.forEach3(function(axx, ayy, azz, arcId) {
63234
+ res = smoothArcCoords(axx, ayy, {
63235
+ tolerance: opts.tolerance,
63236
+ method: opts.method,
63237
+ spherical: opts.spherical,
63238
+ keepCorners: opts.keepCorners,
63239
+ gain: opts.gain,
63240
+ closed: arcs.arcIsClosed(arcId)
63241
+ });
63242
+ nn.push(res.xx.length);
63243
+ for (i = 0, k = res.xx.length; i < k; i++) {
63244
+ xx.push(res.xx[i]);
63245
+ yy.push(res.yy[i]);
63246
+ }
63247
+ });
63248
+ arcs.updateVertexData(nn, xx, yy);
63249
+ }
63250
+
63251
+ function getSmoothMethod(opts) {
63252
+ // Gaussian (Savitzky-Golay) is the documented smoother. 'paek' (an exponential
63253
+ // kernel) is kept as an undocumented alternative for backward compatibility.
63254
+ var m = opts.method;
63255
+ if (!m) return 'gaussian';
63256
+ if (m != 'paek' && m != 'gaussian') {
63257
+ stop$1('Unsupported smooth method:', m);
63258
+ }
63259
+ return m;
63260
+ }
63261
+
63262
+ function useSphericalSmooth(arcs, opts) {
63263
+ return !opts.planar && !arcs.isPlanar();
63264
+ }
63265
+
63266
+ function convertSmoothTolerance(param, dataset, spherical) {
63267
+ var crs = getDatasetCRS(dataset);
63268
+ return spherical ? convertDistanceParam(param, crs) : convertIntervalParam(param, crs);
63269
+ }
63270
+
61982
63271
  cmd.sortFeatures = function(lyr, arcs, opts) {
61983
63272
  var n = getFeatureCount(lyr),
61984
63273
  ascending = !opts.descending,
@@ -63468,6 +64757,9 @@ ${svg}
63468
64757
  } else if (name == 'filter-geom') {
63469
64758
  applyCommandToEachLayer(cmd.filterGeom, targetLayers, arcs, opts);
63470
64759
 
64760
+ } else if (name == 'filter-detail') {
64761
+ cmd.filterDetail(targetDataset, opts, targetLayers);
64762
+
63471
64763
  } else if (name == 'filter-islands') {
63472
64764
  applyCommandToEachLayer(cmd.filterIslands, targetLayers, targetDataset, opts);
63473
64765
 
@@ -63626,6 +64918,9 @@ ${svg}
63626
64918
  cmd.simplify(targetDataset, opts, targetLayers);
63627
64919
  }
63628
64920
 
64921
+ } else if (name == 'smooth') {
64922
+ cmd.smooth(targetDataset, opts, targetLayers);
64923
+
63629
64924
  } else if (name == 'slice') {
63630
64925
  outputLayers = cmd.sliceLayers(targetLayers, source, targetDataset, opts);
63631
64926
 
@@ -63758,7 +65053,7 @@ ${svg}
63758
65053
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
63759
65054
  }
63760
65055
 
63761
- var version = "0.7.29";
65056
+ var version = "0.7.31";
63762
65057
 
63763
65058
  // Parse command line args into commands and run them
63764
65059
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.