mapshaper 0.7.28 → 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.
- package/mapshaper.js +1310 -14
- package/package.json +1 -1
- package/www/mapshaper.js +1310 -14
package/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', {
|
|
@@ -44900,7 +44967,7 @@ ${svg}
|
|
|
44900
44967
|
message('[medial] sample sites: ' + sites.coords.length);
|
|
44901
44968
|
}
|
|
44902
44969
|
profileStart('medial:computeSegments');
|
|
44903
|
-
var medial = computeMedialSegments(sites, coordDistances);
|
|
44970
|
+
var medial = computeMedialSegments(sites, coordDistances, sites.grid);
|
|
44904
44971
|
profileEnd('medial:computeSegments');
|
|
44905
44972
|
if (medial.segments.length === 0) return null;
|
|
44906
44973
|
// Stitch the individual Voronoi edges (2-point segments that meet at shared
|
|
@@ -45165,7 +45232,11 @@ ${svg}
|
|
|
45165
45232
|
// extrapolated as an outward ray). Dropping the touching interior borders and
|
|
45166
45233
|
// the no-feature coastline shrinks the one remaining Delaunay and avoids
|
|
45167
45234
|
// building a redundant medial where the source boundary already partitions.
|
|
45168
|
-
|
|
45235
|
+
var kept = keptSites(sites, grid, coordDistances);
|
|
45236
|
+
// Keep the segment grid with the sites so computeMedialSegments can re-measure
|
|
45237
|
+
// the true source gap when the sample-pair proximity test is too coarse.
|
|
45238
|
+
kept.grid = grid;
|
|
45239
|
+
return kept;
|
|
45169
45240
|
}
|
|
45170
45241
|
|
|
45171
45242
|
// Bucket every boundary segment into a uniform grid so the nearest cross-feature
|
|
@@ -45280,6 +45351,50 @@ ${svg}
|
|
|
45280
45351
|
return best;
|
|
45281
45352
|
}
|
|
45282
45353
|
|
|
45354
|
+
// True when medial vertex c lies in the buffer overlap of features fp and fq:
|
|
45355
|
+
// within fp's radius of an fp-owned source segment AND within fq's radius of an
|
|
45356
|
+
// fq-owned source segment. Measured against the actual source segments via the
|
|
45357
|
+
// grid, so it is correct regardless of how coarsely the banks were sampled --
|
|
45358
|
+
// unlike the sample-pair distance, which overestimates the gap when the nearest
|
|
45359
|
+
// samples on opposite banks are staggered or far from the true closest approach.
|
|
45360
|
+
// Slack on each reach when rescuing a cross-feature edge whose sample endpoints
|
|
45361
|
+
// fell outside the cheap proximity test. It absorbs the discretization of the
|
|
45362
|
+
// medial graph near a pinch point: the connecting Voronoi edge is bounded by the
|
|
45363
|
+
// site spacing (capped at the buffer distance), so a genuinely contested edge can
|
|
45364
|
+
// run up to ~1.5x reach and its medial vertices can land a similar fraction
|
|
45365
|
+
// outside the overlap. 1.3 covers the worst real case observed (~1.18) with
|
|
45366
|
+
// headroom, while spurious edges between sites contested with *other* features
|
|
45367
|
+
// miss by far more (>=1.5 or have no nearby source segment) and stay pruned.
|
|
45368
|
+
var MEDIAL_OVERLAP_SLACK = 1.3;
|
|
45369
|
+
|
|
45370
|
+
function medialVertexInOverlap(ctx, c, fp, fq, rp, rq) {
|
|
45371
|
+
var sp = rp * MEDIAL_OVERLAP_SLACK, sq = rq * MEDIAL_OVERLAP_SLACK;
|
|
45372
|
+
return pointFeatureDistSq(ctx, c[0], c[1], fp) <= sp * sp &&
|
|
45373
|
+
pointFeatureDistSq(ctx, c[0], c[1], fq) <= sq * sq;
|
|
45374
|
+
}
|
|
45375
|
+
|
|
45376
|
+
// Squared distance from (x, y) to the nearest segment owned by feature @feat,
|
|
45377
|
+
// probing the 3x3 grid-cell neighborhood (cell == max reach, so any segment
|
|
45378
|
+
// within a single feature's radius is in the window). Infinity if none.
|
|
45379
|
+
function pointFeatureDistSq(ctx, x, y, feat) {
|
|
45380
|
+
var seg = ctx.seg, grid = ctx.grid;
|
|
45381
|
+
var cx = ctx.colOf(x), cy = ctx.rowOf(y);
|
|
45382
|
+
var best = Infinity;
|
|
45383
|
+
for (var gx = cx - 1; gx <= cx + 1; gx++) {
|
|
45384
|
+
for (var gy = cy - 1; gy <= cy + 1; gy++) {
|
|
45385
|
+
var bucket = grid.get(ctx.cellKey(gx, gy));
|
|
45386
|
+
if (!bucket) continue;
|
|
45387
|
+
for (var b = 0; b < bucket.length; b++) {
|
|
45388
|
+
var s = bucket[b];
|
|
45389
|
+
if (seg.feat[s] !== feat) continue;
|
|
45390
|
+
var d2 = pointSegDistSq2(x, y, seg.x0[s], seg.y0[s], seg.x1[s], seg.y1[s]);
|
|
45391
|
+
if (d2 < best) best = d2;
|
|
45392
|
+
}
|
|
45393
|
+
}
|
|
45394
|
+
}
|
|
45395
|
+
return best;
|
|
45396
|
+
}
|
|
45397
|
+
|
|
45283
45398
|
// Keep only the sites that border a real gap: a different feature within reach
|
|
45284
45399
|
// (finite gap) but farther than the touching threshold. These are the only sites
|
|
45285
45400
|
// that can shape the medial axis. Touching/coincident interior borders (gap ~ 0,
|
|
@@ -45460,7 +45575,7 @@ ${svg}
|
|
|
45460
45575
|
return Math.floor(e / 3);
|
|
45461
45576
|
}
|
|
45462
45577
|
|
|
45463
|
-
function computeMedialSegments(sites, coordDistances) {
|
|
45578
|
+
function computeMedialSegments(sites, coordDistances, ctx) {
|
|
45464
45579
|
var coords = sites.coords;
|
|
45465
45580
|
var owner = sites.owner;
|
|
45466
45581
|
profileStart('medial:delaunay');
|
|
@@ -45485,18 +45600,28 @@ ${svg}
|
|
|
45485
45600
|
var opp = halfedges[e];
|
|
45486
45601
|
var p = triangles[e];
|
|
45487
45602
|
var q = triangles[nextHalfedge(e)];
|
|
45488
|
-
|
|
45603
|
+
var fp = owner[p], fq = owner[q];
|
|
45604
|
+
if (fp === fq) continue;
|
|
45489
45605
|
var dx = coords[p][0] - coords[q][0];
|
|
45490
45606
|
var dy = coords[p][1] - coords[q][1];
|
|
45491
45607
|
var siteDist = Math.sqrt(dx * dx + dy * dy);
|
|
45492
|
-
var
|
|
45493
|
-
|
|
45494
|
-
// never have overlapping buffers, so their bisector is not a contested edge
|
|
45495
|
-
if (siteDist > reach) continue;
|
|
45608
|
+
var rp = coordDistances[fp], rq = coordDistances[fq];
|
|
45609
|
+
var reach = rp + rq;
|
|
45496
45610
|
var t1 = triangleOfEdge(e);
|
|
45497
45611
|
var c1 = verts[t1];
|
|
45498
45612
|
if (!c1) continue; // degenerate (near-collinear) triangle
|
|
45613
|
+
// Sites within the sum of their radii are accepted directly; this is the
|
|
45614
|
+
// common, cheap case. When they are farther apart, the bisector might still
|
|
45615
|
+
// be contested -- the nearest sample pair overestimates the true source gap
|
|
45616
|
+
// where banks are sampled coarsely or staggered. Re-measure the actual gap
|
|
45617
|
+
// at the medial vertex against the source segments (the grid) and rescue the
|
|
45618
|
+
// edge if it really lies in the buffer overlap. Without the rescue the medial
|
|
45619
|
+
// axis fragments at such spots, leaving the equidistant cut wall open so the
|
|
45620
|
+
// overlap face is never subdivided and a whole contested corridor is assigned
|
|
45621
|
+
// to one feature (a feature wrapping a neighbor's enclosed island).
|
|
45622
|
+
var near = siteDist <= reach;
|
|
45499
45623
|
if (opp === -1) {
|
|
45624
|
+
if (!near && !(ctx && medialVertexInOverlap(ctx, c1, fp, fq, rp, rq))) continue;
|
|
45500
45625
|
// Hull edge: the Voronoi edge here is an unbounded ray (the bisector of
|
|
45501
45626
|
// two sites on the convex hull). Emit it as an outward ray from the
|
|
45502
45627
|
// circumcenter so the medial line reaches and crosses the buffer
|
|
@@ -45517,6 +45642,9 @@ ${svg}
|
|
|
45517
45642
|
var t2 = triangleOfEdge(opp);
|
|
45518
45643
|
var c2 = verts[t2];
|
|
45519
45644
|
if (!c2) continue;
|
|
45645
|
+
if (!near && !(ctx &&
|
|
45646
|
+
(medialVertexInOverlap(ctx, c1, fp, fq, rp, rq) ||
|
|
45647
|
+
medialVertexInOverlap(ctx, c2, fp, fq, rp, rq)))) continue;
|
|
45520
45648
|
var sx = c1[0] - c2[0], sy = c1[1] - c2[1];
|
|
45521
45649
|
var segLen = Math.sqrt(sx * sx + sy * sy);
|
|
45522
45650
|
// a real medial edge inside the overlap is short (on the order of the site
|
|
@@ -57093,6 +57221,241 @@ ${svg}
|
|
|
57093
57221
|
getAspectRatioArg: getAspectRatioArg
|
|
57094
57222
|
});
|
|
57095
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
|
+
|
|
57096
57459
|
cmd.filterIslands = function(lyr, dataset, optsArg) {
|
|
57097
57460
|
var opts = utils.extend({sliver_control: 0}, optsArg); // no sliver control
|
|
57098
57461
|
var arcs = dataset.arcs;
|
|
@@ -61918,6 +62281,933 @@ ${svg}
|
|
|
61918
62281
|
useSphericalSimplify: useSphericalSimplify
|
|
61919
62282
|
});
|
|
61920
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
|
+
|
|
61921
63211
|
cmd.sortFeatures = function(lyr, arcs, opts) {
|
|
61922
63212
|
var n = getFeatureCount(lyr),
|
|
61923
63213
|
ascending = !opts.descending,
|
|
@@ -63407,6 +64697,9 @@ ${svg}
|
|
|
63407
64697
|
} else if (name == 'filter-geom') {
|
|
63408
64698
|
applyCommandToEachLayer(cmd.filterGeom, targetLayers, arcs, opts);
|
|
63409
64699
|
|
|
64700
|
+
} else if (name == 'filter-detail') {
|
|
64701
|
+
cmd.filterDetail(targetDataset, opts, targetLayers);
|
|
64702
|
+
|
|
63410
64703
|
} else if (name == 'filter-islands') {
|
|
63411
64704
|
applyCommandToEachLayer(cmd.filterIslands, targetLayers, targetDataset, opts);
|
|
63412
64705
|
|
|
@@ -63565,6 +64858,9 @@ ${svg}
|
|
|
63565
64858
|
cmd.simplify(targetDataset, opts, targetLayers);
|
|
63566
64859
|
}
|
|
63567
64860
|
|
|
64861
|
+
} else if (name == 'smooth') {
|
|
64862
|
+
cmd.smooth(targetDataset, opts, targetLayers);
|
|
64863
|
+
|
|
63568
64864
|
} else if (name == 'slice') {
|
|
63569
64865
|
outputLayers = cmd.sliceLayers(targetLayers, source, targetDataset, opts);
|
|
63570
64866
|
|
|
@@ -63697,7 +64993,7 @@ ${svg}
|
|
|
63697
64993
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
63698
64994
|
}
|
|
63699
64995
|
|
|
63700
|
-
var version = "0.7.
|
|
64996
|
+
var version = "0.7.30";
|
|
63701
64997
|
|
|
63702
64998
|
// Parse command line args into commands and run them
|
|
63703
64999
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|