mapshaper 0.7.29 → 0.7.30

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