mapshaper 0.7.37 → 0.7.38
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 +295 -34
- package/package.json +1 -1
- package/www/mapshaper.js +295 -34
package/mapshaper.js
CHANGED
|
@@ -33076,15 +33076,18 @@ ${svg}
|
|
|
33076
33076
|
type: 'flag'
|
|
33077
33077
|
})
|
|
33078
33078
|
.option('corner-bias', {
|
|
33079
|
-
// Sensitivity of corner detection
|
|
33080
|
-
//
|
|
33081
|
-
//
|
|
33082
|
-
//
|
|
33083
|
-
//
|
|
33084
|
-
//
|
|
33085
|
-
//
|
|
33086
|
-
//
|
|
33087
|
-
|
|
33079
|
+
// Sensitivity of corner detection, RELATIVE to an automatic baseline
|
|
33080
|
+
// (default 0 = the automatic setting). Detection is auto-coarsened on
|
|
33081
|
+
// geometry that is sparse relative to the smoothing distance (long segments
|
|
33082
|
+
// read ordinary coarse bends as corners); this option adds to that base.
|
|
33083
|
+
// Positive keeps more corners, negative fewer; it scales only the distance-
|
|
33084
|
+
// proportional detection parameters (not angles), detecting corners as if
|
|
33085
|
+
// the distance were divided by k, where k = bias+1 for bias >= 0 and
|
|
33086
|
+
// 1/(1-bias) for bias < 0, and the smoothing kernel keeps using the real
|
|
33087
|
+
// distance. So on fine data (no auto adjustment) corner-bias=-1 finds the
|
|
33088
|
+
// corners a 2x distance would; on coarse data a positive value counteracts
|
|
33089
|
+
// the automatic coarsening. Use no-corners to turn preservation off entirely.
|
|
33090
|
+
describe: 'corner-detection sensitivity relative to auto (default 0; + more, - fewer)',
|
|
33088
33091
|
type: 'number'
|
|
33089
33092
|
})
|
|
33090
33093
|
.option('prefilter-gate', {
|
|
@@ -63609,6 +63612,14 @@ ${svg}
|
|
|
63609
63612
|
// -- whether a tight coastline or a gentle, hundreds-of-km graticule arc --
|
|
63610
63613
|
// never qualifies; only a localized kink, where the inner turn approaches
|
|
63611
63614
|
// the full turn, is a corner.
|
|
63615
|
+
// 1b. (open paths) Also flag the end of a long straight run that turns sharply
|
|
63616
|
+
// over a single segment but only gently over the window -- a small jog where
|
|
63617
|
+
// a surveyed border meets a curve, which step 1's window would dilute below
|
|
63618
|
+
// the corner angle. Keyed off the raw segment turn, but the pinned vertex is
|
|
63619
|
+
// snapped to the nearby end of the straight run (stable under tiny vertex
|
|
63620
|
+
// moves) and only where that run's line is actually left (so an incidental
|
|
63621
|
+
// notch after which the run resumes on the same line is not flagged). See
|
|
63622
|
+
// straightRunEndNear.
|
|
63612
63623
|
// 2. Between flagged corners, classify each span as "structural" if it is long
|
|
63613
63624
|
// relative to the tolerance and its curvature stays low (so a straight or
|
|
63614
63625
|
// slowly-curving graticule line counts, but sub-tolerance wiggle does not).
|
|
@@ -63651,14 +63662,18 @@ ${svg}
|
|
|
63651
63662
|
// to count, which matches intuition (a 28 km stretch bending at radius 4 km is
|
|
63652
63663
|
// obviously not straight). Genuinely curving coastline bows far from its chord
|
|
63653
63664
|
// and is still rejected, so spurious corners inside wiggly stretches keep getting
|
|
63654
|
-
// culled.
|
|
63655
|
-
|
|
63665
|
+
// culled. Tightened from 0.03 to 0.02 (a run may curve ~9 deg over its length, not
|
|
63666
|
+
// ~14) after a coastline island pinned corners at both ends of a ~10 deg-curving
|
|
63667
|
+
// stretch that read as "straight" only under the looser corridor: an acute corner
|
|
63668
|
+
// takes the full corridor regardless of the angle coupling below, so only the base
|
|
63669
|
+
// factor governs whether such a run can anchor a sharp corner.
|
|
63670
|
+
var STRAIGHT_DEV_FACTOR = 0.02;
|
|
63656
63671
|
|
|
63657
63672
|
// Angle coupling for corner retention: how much sharper the corner must turn than
|
|
63658
63673
|
// the run it borders already curves. A run that passes the chord test may still
|
|
63659
63674
|
// bend gently within the STRAIGHT_DEV_FACTOR corridor -- for a circular arc the
|
|
63660
|
-
// chord-deviation ratio is ~ (the run's total turn)/8, so the base 0.
|
|
63661
|
-
// run that curves ~
|
|
63675
|
+
// chord-deviation ratio is ~ (the run's total turn)/8, so the base 0.02 admits a
|
|
63676
|
+
// run that curves ~9 deg over its length. Pinning a *gentle* bend at the end of
|
|
63662
63677
|
// such a run is unsafe: the "corner" is barely sharper than the run's own
|
|
63663
63678
|
// curving, so it is really a point on a smooth bend, not a junction. (This is the
|
|
63664
63679
|
// failure mode on coarsely sampled / already-simplified coastlines, where a
|
|
@@ -63669,7 +63684,7 @@ ${svg}
|
|
|
63669
63684
|
// turn >= PIN_TURN_RATIO * (8 * dev) <=> dev <= turn / (8 * PIN_TURN_RATIO).
|
|
63670
63685
|
// retentionDevLimit() returns the smaller of STRAIGHT_DEV_FACTOR and
|
|
63671
63686
|
// turn/(8*PIN_TURN_RATIO), so the coupling only bites for gentle corners (below
|
|
63672
|
-
// ~2*STRAIGHT_DEV_FACTOR*PIN_TURN_RATIO ~
|
|
63687
|
+
// ~2*STRAIGHT_DEV_FACTOR*PIN_TURN_RATIO ~ 46 deg); sharp corners (surveyed-border
|
|
63673
63688
|
// right angles, spits, hairpins) keep the full base tolerance, unchanged.
|
|
63674
63689
|
var PIN_TURN_RATIO = 5;
|
|
63675
63690
|
|
|
@@ -63693,6 +63708,15 @@ ${svg}
|
|
|
63693
63708
|
// old 1*tol behaviour for users who want shorter runs pinned.
|
|
63694
63709
|
var MIN_PIN_RUN_LEN_FACTOR = 2.0;
|
|
63695
63710
|
|
|
63711
|
+
// When a long straight run's end turns sharply into a curve with a small jog, the
|
|
63712
|
+
// sharpest single-segment turn can land a few vertices past the run's actual end.
|
|
63713
|
+
// straightRunEndNear searches back this many source vertices (in addition to the
|
|
63714
|
+
// distance-scaled tangent window) to snap to the stable run end, so detection
|
|
63715
|
+
// does not depend on the smoothing distance being large enough for the window to
|
|
63716
|
+
// span the jog. A jog is a handful of vertices; this is deliberately generous
|
|
63717
|
+
// because the search returns the nearest genuine run end (or nothing).
|
|
63718
|
+
var MAX_JOG_VERTICES = 6;
|
|
63719
|
+
|
|
63696
63720
|
// Convert the user-facing corner-bias (0 = neutral) into the positive multiplier
|
|
63697
63721
|
// k applied to corner-detection resolution (ctol = tol / k). The mapping is
|
|
63698
63722
|
// symmetric about zero -- k(+b) * k(-b) = 1 -- and smooth there (both branches
|
|
@@ -63707,6 +63731,39 @@ ${svg}
|
|
|
63707
63731
|
return b >= 0 ? b + 1 : 1 / (1 - b);
|
|
63708
63732
|
}
|
|
63709
63733
|
|
|
63734
|
+
// Ratio of (typical segment length / smoothing distance) at or below which corner
|
|
63735
|
+
// detection sees several segments per tangent window and behaves normally, so no
|
|
63736
|
+
// automatic coarsening is applied. Above it the window shrinks toward a single
|
|
63737
|
+
// segment and ordinary coarse-data bends start reading as corners.
|
|
63738
|
+
var AUTO_BIAS_RATIO = 0.15;
|
|
63739
|
+
// Most-negative automatic bias. Caps how far detection is coarsened on very sparse
|
|
63740
|
+
// geometry (e.g. lo-res contours), where the ratio can be many times AUTO_BIAS_RATIO
|
|
63741
|
+
// but a handful of doublings already merges the whole neighbourhood.
|
|
63742
|
+
var AUTO_BIAS_FLOOR = -4;
|
|
63743
|
+
|
|
63744
|
+
// Automatic corner-bias from the geometry's coarseness relative to the smoothing
|
|
63745
|
+
// distance. @medianSeg is a robust (median) segment length; @dist is the raw
|
|
63746
|
+
// smoothing distance, both in the same ground units. Corner detection keys off a
|
|
63747
|
+
// tangent window ~0.3*dist wide; when the typical segment is an appreciable
|
|
63748
|
+
// fraction of the distance (ratio r = medianSeg/dist above AUTO_BIAS_RATIO) that
|
|
63749
|
+
// window spans too few segments and gentle-but-coarse bends read as corners. We
|
|
63750
|
+
// return a negative bias that coarsens detection (as if the distance were larger)
|
|
63751
|
+
// enough to push the effective ratio back down: each halving of the effective
|
|
63752
|
+
// resolution costs one bias step, so bias = -log2(r / AUTO_BIAS_RATIO), floored.
|
|
63753
|
+
// Returns 0 (no adjustment) when the geometry is fine relative to the distance,
|
|
63754
|
+
// which is the normal case for detailed datasets smoothed at a real distance.
|
|
63755
|
+
// Note it only ever coarsens: a straight run that genuinely anchors a sharp corner
|
|
63756
|
+
// survives this (the corner is retained by bordering a long straight run, which
|
|
63757
|
+
// coarsening does not remove until it exceeds the run's length), while weakly
|
|
63758
|
+
// supported bends on coarse coastlines/contours fall below the corner threshold.
|
|
63759
|
+
function autoCornerBias(medianSeg, dist) {
|
|
63760
|
+
if (!(medianSeg > 0) || !(dist > 0)) return 0;
|
|
63761
|
+
var r = medianSeg / dist;
|
|
63762
|
+
if (r <= AUTO_BIAS_RATIO) return 0;
|
|
63763
|
+
var b = -Math.log2(r / AUTO_BIAS_RATIO);
|
|
63764
|
+
return b < AUTO_BIAS_FLOOR ? AUTO_BIAS_FLOOR : b;
|
|
63765
|
+
}
|
|
63766
|
+
|
|
63710
63767
|
// @cornerBias (optional, default 0 = neutral) scales only the distance-
|
|
63711
63768
|
// proportional corner parameters, by dividing the tolerance they key off
|
|
63712
63769
|
// (ctol = tol / k, k = cornerBiasScale(bias)). The dimensionless thresholds are
|
|
@@ -63763,9 +63820,107 @@ ${svg}
|
|
|
63763
63820
|
if (inner[j] < params.concentration * turns[j]) continue;
|
|
63764
63821
|
if (isLocalMaxTurn(t, turns, j, W, L, m, lo, hi, cyclic)) corners.push(j);
|
|
63765
63822
|
}
|
|
63823
|
+
// Open paths: also flag the terminal vertex of a long straight run that bends
|
|
63824
|
+
// sharply over a single segment but only gently over the tangent window -- e.g.
|
|
63825
|
+
// where a surveyed border meets a coastline with a small jog. The windowed test
|
|
63826
|
+
// above misses these because the wide window dilutes the sharp segment turn into
|
|
63827
|
+
// a sub-threshold bend, so the straight run's end gets rounded into the adjacent
|
|
63828
|
+
// curve. Here we key off the raw segment turn instead, but confine this rescue
|
|
63829
|
+
// to a genuine straight-run end with two gates (plus straightRunEndNear):
|
|
63830
|
+
// 1. the turn is fully concentrated in the inner window: inner-window turn >=
|
|
63831
|
+
// full-window turn. This is a stricter form of the windowed path's
|
|
63832
|
+
// concentration test (>= concentration * full) and is the crux of the
|
|
63833
|
+
// distinction. At a straight-run end the approaches carry ~no turning, so
|
|
63834
|
+
// the whole window's turn sits in the inner window (ratio >= 1); on a
|
|
63835
|
+
// steadily-curving coastline the turning is spread across the window (inner
|
|
63836
|
+
// < full), so a lone sharp segment there is rejected. The wide window
|
|
63837
|
+
// dilutes the end's departure below the corner threshold (which is why the
|
|
63838
|
+
// windowed path misses it), but the concentration ratio stays high.
|
|
63839
|
+
// 2. (via straightRunEndNear) a bordering straight run's line is actually left,
|
|
63840
|
+
// so an incidental notch on an otherwise-continuing run is not pinned.
|
|
63841
|
+
// Rings keep the windowed-only detection for now.
|
|
63842
|
+
if (!cyclic) {
|
|
63843
|
+
var raw = new Float64Array(m);
|
|
63844
|
+
for (i = lo; i < hi; i++) raw[i] = vertexTurn(channels, K, i);
|
|
63845
|
+
for (j = lo; j < hi; j++) {
|
|
63846
|
+
if (raw[j] < params.cornerAngle) continue;
|
|
63847
|
+
if (inner[j] < turns[j]) continue;
|
|
63848
|
+
if (!isLocalMaxTurn(t, raw, j, W, L, m, lo, hi, cyclic)) continue;
|
|
63849
|
+
var e = straightRunEndNear(t, channels, n, j, W, params);
|
|
63850
|
+
if (e >= 0 && corners.indexOf(e) === -1) corners.push(e);
|
|
63851
|
+
}
|
|
63852
|
+
corners.sort(function (a, b) { return a - b; });
|
|
63853
|
+
}
|
|
63766
63854
|
return corners;
|
|
63767
63855
|
}
|
|
63768
63856
|
|
|
63857
|
+
// Given a sharp single-segment turn at open-path vertex @j, find the end of a
|
|
63858
|
+
// long straight run that the path leaves, within arc length @W of @j, and return
|
|
63859
|
+
// that run-end vertex to pin (or -1). We search a neighbourhood rather than
|
|
63860
|
+
// requiring the run's last vertex to be exactly where the segment turn peaks: a
|
|
63861
|
+
// jog into a curve can place its sharpest vertex a step or two past the run's
|
|
63862
|
+
// end, and exactly which vertex is sharpest is sensitive to tiny differences in
|
|
63863
|
+
// vertex placement -- but the straight run's end itself is stable. Snapping to it
|
|
63864
|
+
// keeps detection from flickering on and off with sub-tolerance vertex moves. The
|
|
63865
|
+
// straightRunLeaves test only succeeds at a genuine run end whose continuation
|
|
63866
|
+
// departs, so an incidental jog after which the run resumes is still not pinned.
|
|
63867
|
+
function straightRunEndNear(t, channels, n, j, W, params) {
|
|
63868
|
+
var e, k;
|
|
63869
|
+
// The jog between the sharp turn and the run's end is a fixed handful of source
|
|
63870
|
+
// vertices, independent of the smoothing distance -- so the search extends by a
|
|
63871
|
+
// vertex count as well as the distance-scaled window W. Without the vertex floor
|
|
63872
|
+
// the window shrinks with the distance and, at small distances, can no longer
|
|
63873
|
+
// reach back across the jog to the run end (fewer, not more, corners pinned as
|
|
63874
|
+
// the distance drops -- the opposite of what a long run warrants). We return the
|
|
63875
|
+
// first (nearest) run end found, so a generous reach only costs a few extra
|
|
63876
|
+
// straightRunLeaves checks when there is nothing to pin.
|
|
63877
|
+
for (e = j, k = 0; e >= 1; e--, k++) {
|
|
63878
|
+
if (t[j] - t[e] > W && k > MAX_JOG_VERTICES) break;
|
|
63879
|
+
if (straightRunLeaves(t, channels, n, e, -1, params) ||
|
|
63880
|
+
straightRunLeaves(t, channels, n, e, 1, params)) return e;
|
|
63881
|
+
}
|
|
63882
|
+
for (e = j + 1, k = 1; e < n - 1; e++, k++) {
|
|
63883
|
+
if (t[e] - t[j] > W && k > MAX_JOG_VERTICES) break;
|
|
63884
|
+
if (straightRunLeaves(t, channels, n, e, -1, params) ||
|
|
63885
|
+
straightRunLeaves(t, channels, n, e, 1, params)) return e;
|
|
63886
|
+
}
|
|
63887
|
+
return -1;
|
|
63888
|
+
}
|
|
63889
|
+
|
|
63890
|
+
// Is the near side of @e (walking direction @dir: -1 = run precedes e, +1 = run
|
|
63891
|
+
// follows e) a long straight run, AND does the far side leave that run's line --
|
|
63892
|
+
// i.e. a point a full run-length along the far side sits outside the run's
|
|
63893
|
+
// straightness corridor? A jog that rejoins the run returns to ~0 perpendicular
|
|
63894
|
+
// offset and fails this, so it is not flagged. (This also implies @e is the run's
|
|
63895
|
+
// end: if the run continued straight past @e, the far point would stay on its
|
|
63896
|
+
// line.)
|
|
63897
|
+
function straightRunLeaves(t, channels, n, e, dir, params) {
|
|
63898
|
+
var K = channels.length, L = t[n - 1];
|
|
63899
|
+
var nearEnd = reach(t, n, n, L, e, dir, params.minPinRunLen, false);
|
|
63900
|
+
if (nearEnd === e) return false;
|
|
63901
|
+
var a = dir < 0 ? nearEnd : e;
|
|
63902
|
+
var b = dir < 0 ? e : nearEnd;
|
|
63903
|
+
// Require the near run to be straight to the SAME angle-coupled tolerance
|
|
63904
|
+
// retention will demand of a pin at @e (retentionDevLimit of the turn at @e),
|
|
63905
|
+
// not the looser default corridor. This makes straightRunEndNear's backward
|
|
63906
|
+
// scan stop at the run's true end -- the last vertex actually on the run's line
|
|
63907
|
+
// -- rather than a vertex a little past it that squeaks inside the loose 3%
|
|
63908
|
+
// corridor but would then be culled by retention (leaving nothing pinned).
|
|
63909
|
+
var devLim = retentionDevLimit(cornerTurn(t, channels, n, e, false, params));
|
|
63910
|
+
if (!isStraightRun(t, channels, a, b, params, devLim)) return false;
|
|
63911
|
+
var farEnd = reach(t, n, n, L, e, -dir, params.minPinRunLen, false);
|
|
63912
|
+
if (farEnd === e) return false;
|
|
63913
|
+
var pe = getPt(channels, K, e);
|
|
63914
|
+
var u = subv(getPt(channels, K, nearEnd), pe, K); // direction along the near run
|
|
63915
|
+
var uu = dot(u, u, K);
|
|
63916
|
+
if (!(uu > 0)) return false;
|
|
63917
|
+
var af = subv(getPt(channels, K, farEnd), pe, K);
|
|
63918
|
+
var along = Math.abs(dot(af, u, K)) / Math.sqrt(uu);
|
|
63919
|
+
var perp = Math.sqrt(perpDistSq(channels, K, farEnd, pe, u, uu));
|
|
63920
|
+
// outside the straight corridor extended from the run -> the path has left it
|
|
63921
|
+
return perp > STRAIGHT_DEV_FACTOR * along;
|
|
63922
|
+
}
|
|
63923
|
+
|
|
63769
63924
|
// Is span [a, b] (inclusive vertex indices, a < b, open frame) a structural run:
|
|
63770
63925
|
// long relative to the tolerance and low-curvature throughout?
|
|
63771
63926
|
function isStructuralRun(t, channels, a, b, params) {
|
|
@@ -64139,6 +64294,20 @@ ${svg}
|
|
|
64139
64294
|
// segments); user-overridable via max-bend-angle
|
|
64140
64295
|
var DEVIATION_FACTOR = 0.1; // sagitta guard: also cut a gentle bend that bows
|
|
64141
64296
|
// more than tolerance * this from its chord
|
|
64297
|
+
// Preserved structural runs (long straight / low-curvature spans between pinned
|
|
64298
|
+
// corners) are not smoothed, but their ORIGINAL vertices are resampled with the
|
|
64299
|
+
// same bend-angle decimation so the whole output has adaptive vertex spacing
|
|
64300
|
+
// (no abrupt density seam at a run boundary). The decimation runs in the
|
|
64301
|
+
// smoothing channels, so for unprojected data a long line -- which curves in the
|
|
64302
|
+
// geocentric x,y,z space even when it is "straight" in lng/lat -- keeps enough
|
|
64303
|
+
// interior vertices to approximate that curve on reprojection, scaling with the
|
|
64304
|
+
// line's length automatically. Because it only ever keeps a SUBSET of the
|
|
64305
|
+
// original vertices (never interpolates new ones), it can't distort a rhumb or
|
|
64306
|
+
// geodesic edge -- every output vertex still lies exactly where the source drew
|
|
64307
|
+
// it. Structural runs use a fraction of the bend angle (sampled finer than
|
|
64308
|
+
// smoothed spans) as a conservative bias toward preservation / reprojection
|
|
64309
|
+
// headroom.
|
|
64310
|
+
var STRUCTURAL_BEND_FACTOR = 0.5; // structural-run bend angle = max-bend-angle * this
|
|
64142
64311
|
|
|
64143
64312
|
// Smooth a single arc's coordinates.
|
|
64144
64313
|
// @xx, @yy: coordinate arrays (may be typed-array subarrays) for one arc.
|
|
@@ -64147,8 +64316,9 @@ ${svg}
|
|
|
64147
64316
|
// arcs are preserved exactly (so shared topology nodes stay put); closed arcs
|
|
64148
64317
|
// are smoothed cyclically and returned closed (first point repeated at the end).
|
|
64149
64318
|
// With keepCorners, structural corners (where long straight/low-curvature runs
|
|
64150
|
-
// meet) are detected and pinned
|
|
64151
|
-
//
|
|
64319
|
+
// meet) are detected and pinned; the runs themselves are not smoothed but are
|
|
64320
|
+
// resampled (a subset of their original vertices, at adaptive spacing), and only
|
|
64321
|
+
// the spans between corners are smoothed.
|
|
64152
64322
|
// Resolve the curvature-correction gain (default 1 = fully corrected). gain=0
|
|
64153
64323
|
// leaves the plain weighted moving average; negative values are clamped to 0.
|
|
64154
64324
|
function resolveGain(opts) {
|
|
@@ -64198,7 +64368,7 @@ ${svg}
|
|
|
64198
64368
|
var origY = toArray(yy);
|
|
64199
64369
|
var tol = opts.tolerance * KERNEL_FROM_DISTANCE;
|
|
64200
64370
|
if (n < 3 || !(opts.tolerance > 0)) {
|
|
64201
|
-
return {xx: origX, yy: origY};
|
|
64371
|
+
return {xx: origX, yy: origY, corners: 0};
|
|
64202
64372
|
}
|
|
64203
64373
|
var method = opts.method == 'gaussian' ? 'gaussian' : 'paek';
|
|
64204
64374
|
var closed = !!opts.closed;
|
|
@@ -64210,7 +64380,7 @@ ${svg}
|
|
|
64210
64380
|
// kernel scale stays in true distance regardless of coordinate representation.
|
|
64211
64381
|
var t = arcLengths(origX, origY, n, spherical);
|
|
64212
64382
|
if (!(t[n - 1] > 0)) {
|
|
64213
|
-
return {xx: origX, yy: origY}; // degenerate (coincident points)
|
|
64383
|
+
return {xx: origX, yy: origY, corners: 0}; // degenerate (coincident points)
|
|
64214
64384
|
}
|
|
64215
64385
|
// The low-pass kernel scale is the raw distance scale (tol) times the baked-in
|
|
64216
64386
|
// KERNEL_STRENGTH calibration and the user's `strength` multiplier (default 1).
|
|
@@ -64260,7 +64430,9 @@ ${svg}
|
|
|
64260
64430
|
corners = filterRingCornersByStructure(t, channels, n, corners, ringParams);
|
|
64261
64431
|
}
|
|
64262
64432
|
if (corners.length === 0) {
|
|
64263
|
-
|
|
64433
|
+
var cyc = smoothClosedCyclic(t, channels, n, ctx);
|
|
64434
|
+
cyc.corners = 0;
|
|
64435
|
+
return cyc;
|
|
64264
64436
|
}
|
|
64265
64437
|
// A ring with corners is processed as an open path: rotate it to start (and
|
|
64266
64438
|
// end) at one corner, with the remaining corners as interior breakpoints.
|
|
@@ -64271,7 +64443,11 @@ ${svg}
|
|
|
64271
64443
|
t = arcLengths(origX, origY, n, spherical);
|
|
64272
64444
|
channels = spherical ? lngLatToXYZChannels(origX, origY, n) : [origX, origY];
|
|
64273
64445
|
var breaks = mapRotatedCorners(corners, rot.shift, rot.m);
|
|
64274
|
-
|
|
64446
|
+
var ring = smoothOpenSpans(origX, origY, t, channels, n, breaks, ctx);
|
|
64447
|
+
// The ring seam (corners[0], pinned as the rotated start/end) is itself a
|
|
64448
|
+
// preserved corner, on top of the interior breakpoints smoothOpenSpans kept.
|
|
64449
|
+
ring.corners += 1;
|
|
64450
|
+
return ring;
|
|
64275
64451
|
}
|
|
64276
64452
|
|
|
64277
64453
|
var openBreaks = keepCorners ?
|
|
@@ -64304,11 +64480,12 @@ ${svg}
|
|
|
64304
64480
|
var lo = bounds[s], hi = bounds[s + 1];
|
|
64305
64481
|
var preserve = !!params && isStructuralRun(t, channels, lo, hi, params);
|
|
64306
64482
|
var span = preserve ?
|
|
64307
|
-
|
|
64483
|
+
resampleStructuralRun(origX, origY, channels, lo, hi, ctx) :
|
|
64308
64484
|
smoothSpanOpen(origX, origY, t, channels, lo, hi, ctx);
|
|
64309
64485
|
appendSpan(xx, yy, span, s === 0);
|
|
64310
64486
|
}
|
|
64311
|
-
|
|
64487
|
+
// Interior breakpoints that survived refinement are the pinned corners.
|
|
64488
|
+
return {xx: xx, yy: yy, corners: bounds.length - 2};
|
|
64312
64489
|
}
|
|
64313
64490
|
|
|
64314
64491
|
// Drop interior breakpoints that don't border any pinnable straight run (e.g.
|
|
@@ -64534,6 +64711,34 @@ ${svg}
|
|
|
64534
64711
|
return {xx: xx, yy: yy};
|
|
64535
64712
|
}
|
|
64536
64713
|
|
|
64714
|
+
// Resample a preserved structural run [lo, hi] (a long straight / low-curvature
|
|
64715
|
+
// span between pinned corners). The run is NOT smoothed: its shape is kept by
|
|
64716
|
+
// emitting a SUBSET of its original vertices, decimated with the shared
|
|
64717
|
+
// bend-angle filter in the smoothing channels (so a long line that curves in the
|
|
64718
|
+
// geocentric space keeps interior vertices scaling with its length -- see
|
|
64719
|
+
// STRUCTURAL_BEND_FACTOR). Both endpoints are always kept, at their exact
|
|
64720
|
+
// original coordinates, so pinned corners and shared topology nodes are
|
|
64721
|
+
// unchanged. A run too short to decimate is copied verbatim.
|
|
64722
|
+
function resampleStructuralRun(origX, origY, channels, lo, hi, ctx) {
|
|
64723
|
+
var nSub = hi - lo + 1;
|
|
64724
|
+
if (nSub < 3) return copySpan(origX, origY, lo, hi);
|
|
64725
|
+
var K = channels.length;
|
|
64726
|
+
var P = new Array(nSub);
|
|
64727
|
+
for (var i = 0; i < nSub; i++) {
|
|
64728
|
+
var p = new Array(K);
|
|
64729
|
+
for (var c = 0; c < K; c++) p[c] = channels[c][lo + i];
|
|
64730
|
+
P[i] = p;
|
|
64731
|
+
}
|
|
64732
|
+
var keep = decimateByBend(P, K, ctx.bendAngle * STRUCTURAL_BEND_FACTOR, ctx.tol * DEVIATION_FACTOR);
|
|
64733
|
+
var xx = [], yy = [];
|
|
64734
|
+
for (var ki = 0; ki < keep.length; ki++) {
|
|
64735
|
+
var idx = lo + keep[ki];
|
|
64736
|
+
xx.push(origX[idx]); // exact original coordinate, never interpolated
|
|
64737
|
+
yy.push(origY[idx]);
|
|
64738
|
+
}
|
|
64739
|
+
return {xx: xx, yy: yy};
|
|
64740
|
+
}
|
|
64741
|
+
|
|
64537
64742
|
function appendSpan(xx, yy, span, isFirst) {
|
|
64538
64743
|
for (var i = isFirst ? 0 : 1; i < span.xx.length; i++) {
|
|
64539
64744
|
xx.push(span.xx[i]);
|
|
@@ -64658,26 +64863,38 @@ ${svg}
|
|
|
64658
64863
|
}
|
|
64659
64864
|
|
|
64660
64865
|
// 2. one-pass bend-angle filter
|
|
64661
|
-
var
|
|
64662
|
-
var epsDev = ctx.tol * DEVIATION_FACTOR;
|
|
64866
|
+
var keep = decimateByBend(P, K, ctx.bendAngle, ctx.tol * DEVIATION_FACTOR);
|
|
64663
64867
|
var out = [];
|
|
64664
64868
|
for (var c = 0; c < K; c++) out.push([]);
|
|
64665
|
-
appendPoint(out, P[
|
|
64869
|
+
for (var ki = 0; ki < keep.length; ki++) appendPoint(out, P[keep[ki]], K);
|
|
64870
|
+
return out;
|
|
64871
|
+
}
|
|
64872
|
+
|
|
64873
|
+
// One-pass forward decimation of a K-channel point list @P: keep the two
|
|
64874
|
+
// endpoints plus every interior point where the turn accumulated since the last
|
|
64875
|
+
// kept point reaches @theta, or where the estimated sagitta of the skipped
|
|
64876
|
+
// stretch (chord * accumulated turn / 8, the bow of a circular arc) reaches
|
|
64877
|
+
// @epsDev. Bounds the angle between consecutive kept segments by construction, so
|
|
64878
|
+
// joins stay smooth. Returns the kept indices into @P (always including 0 and the
|
|
64879
|
+
// last index). Shared by the smoothed-curve resampler and the structural-run
|
|
64880
|
+
// resampler (see resampleStructuralRun).
|
|
64881
|
+
function decimateByBend(P, K, theta, epsDev) {
|
|
64882
|
+
var n = P.length;
|
|
64883
|
+
var keep = [0];
|
|
64884
|
+
if (n < 2) return keep;
|
|
64666
64885
|
var anchor = 0; // last kept vertex
|
|
64667
64886
|
var accTurn = 0; // absolute turning accumulated since the anchor
|
|
64668
|
-
for (var j = 1; j <
|
|
64887
|
+
for (var j = 1; j < n - 1; j++) {
|
|
64669
64888
|
accTurn += vecAngle(P[j - 1], P[j], P[j], P[j + 1], K);
|
|
64670
|
-
// sagitta of a circular arc of chord c and total turn a is ~ c*a/8; cut a
|
|
64671
|
-
// long gentle bend before it bows more than epsDev from its chord
|
|
64672
64889
|
var sagitta = chordLen(P[anchor], P[j + 1], K) * accTurn * 0.125;
|
|
64673
64890
|
if (accTurn >= theta || sagitta >= epsDev) {
|
|
64674
|
-
|
|
64891
|
+
keep.push(j);
|
|
64675
64892
|
anchor = j;
|
|
64676
64893
|
accTurn = 0;
|
|
64677
64894
|
}
|
|
64678
64895
|
}
|
|
64679
|
-
|
|
64680
|
-
return
|
|
64896
|
+
keep.push(n - 1);
|
|
64897
|
+
return keep;
|
|
64681
64898
|
}
|
|
64682
64899
|
|
|
64683
64900
|
// Angle (radians) between vectors (b - a) and (d - c) over K channels.
|
|
@@ -64998,16 +65215,32 @@ ${svg}
|
|
|
64998
65215
|
}
|
|
64999
65216
|
}
|
|
65000
65217
|
|
|
65001
|
-
|
|
65218
|
+
// Corner detection is automatically coarsened on geometry that is sparse
|
|
65219
|
+
// relative to the smoothing distance (long segments -> few segments per
|
|
65220
|
+
// detection window -> ordinary coarse bends misread as corners; see
|
|
65221
|
+
// autoCornerBias). The user's corner-bias is relative to this automatic base:
|
|
65222
|
+
// it is added on top, so corner-bias=0 (the default) is "whatever the geometry
|
|
65223
|
+
// warrants", a positive value finds more corners than the auto baseline, a
|
|
65224
|
+
// negative value fewer.
|
|
65225
|
+
var autoBias = 0;
|
|
65226
|
+
if (keepCorners) {
|
|
65227
|
+
autoBias = autoCornerBias(medianSegmentLength(arcs, spherical), tolerance);
|
|
65228
|
+
}
|
|
65229
|
+
var effectiveCornerBias = autoBias + (opts.corner_bias || 0);
|
|
65230
|
+
|
|
65231
|
+
var corners = smoothPaths(arcs, {
|
|
65002
65232
|
tolerance: tolerance,
|
|
65003
65233
|
method: method,
|
|
65004
65234
|
spherical: spherical,
|
|
65005
65235
|
keepCorners: keepCorners,
|
|
65006
|
-
cornerBias:
|
|
65236
|
+
cornerBias: effectiveCornerBias,
|
|
65007
65237
|
gain: opts.gain,
|
|
65008
65238
|
strength: opts.strength,
|
|
65009
65239
|
maxBendAngle: opts.max_bend_angle
|
|
65010
65240
|
});
|
|
65241
|
+
if (keepCorners && corners > 0) {
|
|
65242
|
+
message('Pinned ' + corners + ' corner' + utils.pluralSuffix(corners));
|
|
65243
|
+
}
|
|
65011
65244
|
|
|
65012
65245
|
if (implicitlySmoothedNames.length > 0) {
|
|
65013
65246
|
message(
|
|
@@ -65021,10 +65254,12 @@ ${svg}
|
|
|
65021
65254
|
// untouched, shared polygon boundaries stay coincident and topology is
|
|
65022
65255
|
// preserved; updateVertexData() also handles undo capture and resets stale
|
|
65023
65256
|
// simplification thresholds.
|
|
65257
|
+
// Returns the total number of structural corners preserved across all arcs.
|
|
65024
65258
|
function smoothPaths(arcs, opts) {
|
|
65025
65259
|
var nn = [];
|
|
65026
65260
|
var xx = [];
|
|
65027
65261
|
var yy = [];
|
|
65262
|
+
var corners = 0;
|
|
65028
65263
|
var i, k, res;
|
|
65029
65264
|
arcs.forEach3(function(axx, ayy, azz, arcId) {
|
|
65030
65265
|
res = smoothArcCoords(axx, ayy, {
|
|
@@ -65038,6 +65273,7 @@ ${svg}
|
|
|
65038
65273
|
maxBendAngle: opts.maxBendAngle,
|
|
65039
65274
|
closed: arcs.arcIsClosed(arcId)
|
|
65040
65275
|
});
|
|
65276
|
+
corners += res.corners || 0;
|
|
65041
65277
|
nn.push(res.xx.length);
|
|
65042
65278
|
for (i = 0, k = res.xx.length; i < k; i++) {
|
|
65043
65279
|
xx.push(res.xx[i]);
|
|
@@ -65045,6 +65281,31 @@ ${svg}
|
|
|
65045
65281
|
}
|
|
65046
65282
|
});
|
|
65047
65283
|
arcs.updateVertexData(nn, xx, yy);
|
|
65284
|
+
return corners;
|
|
65285
|
+
}
|
|
65286
|
+
|
|
65287
|
+
// Median segment length across all arcs, in ground units (meters for spherical
|
|
65288
|
+
// data), used to gauge how coarse the geometry is relative to the smoothing
|
|
65289
|
+
// distance (see autoCornerBias). The median is robust to a few very long straight
|
|
65290
|
+
// segments (which are not a spurious-corner risk) and to dense sub-scale detail.
|
|
65291
|
+
// Very large datasets are sampled with a stride so the cost stays bounded.
|
|
65292
|
+
function medianSegmentLength(arcs, spherical) {
|
|
65293
|
+
var totalSegs = arcs.getPointCount() - arcs.size();
|
|
65294
|
+
if (totalSegs < 1) return 0;
|
|
65295
|
+
var MAX_SAMPLES = 100000;
|
|
65296
|
+
var stride = Math.ceil(totalSegs / MAX_SAMPLES);
|
|
65297
|
+
var distFn = spherical ? greatCircleDistance : distance2D;
|
|
65298
|
+
var lens = [];
|
|
65299
|
+
var counter = 0;
|
|
65300
|
+
arcs.forEach3(function(xx, yy) {
|
|
65301
|
+
for (var i = 1, n = xx.length; i < n; i++) {
|
|
65302
|
+
if (counter++ % stride === 0) {
|
|
65303
|
+
lens.push(distFn(xx[i - 1], yy[i - 1], xx[i], yy[i]));
|
|
65304
|
+
}
|
|
65305
|
+
}
|
|
65306
|
+
});
|
|
65307
|
+
if (lens.length === 0) return 0;
|
|
65308
|
+
return utils.findMedian(lens);
|
|
65048
65309
|
}
|
|
65049
65310
|
|
|
65050
65311
|
function getSmoothMethod(opts) {
|
|
@@ -66852,7 +67113,7 @@ ${svg}
|
|
|
66852
67113
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
66853
67114
|
}
|
|
66854
67115
|
|
|
66855
|
-
var version = "0.7.
|
|
67116
|
+
var version = "0.7.38";
|
|
66856
67117
|
|
|
66857
67118
|
// Parse command line args into commands and run them
|
|
66858
67119
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|
package/package.json
CHANGED
package/www/mapshaper.js
CHANGED
|
@@ -33076,15 +33076,18 @@ ${svg}
|
|
|
33076
33076
|
type: 'flag'
|
|
33077
33077
|
})
|
|
33078
33078
|
.option('corner-bias', {
|
|
33079
|
-
// Sensitivity of corner detection
|
|
33080
|
-
//
|
|
33081
|
-
//
|
|
33082
|
-
//
|
|
33083
|
-
//
|
|
33084
|
-
//
|
|
33085
|
-
//
|
|
33086
|
-
//
|
|
33087
|
-
|
|
33079
|
+
// Sensitivity of corner detection, RELATIVE to an automatic baseline
|
|
33080
|
+
// (default 0 = the automatic setting). Detection is auto-coarsened on
|
|
33081
|
+
// geometry that is sparse relative to the smoothing distance (long segments
|
|
33082
|
+
// read ordinary coarse bends as corners); this option adds to that base.
|
|
33083
|
+
// Positive keeps more corners, negative fewer; it scales only the distance-
|
|
33084
|
+
// proportional detection parameters (not angles), detecting corners as if
|
|
33085
|
+
// the distance were divided by k, where k = bias+1 for bias >= 0 and
|
|
33086
|
+
// 1/(1-bias) for bias < 0, and the smoothing kernel keeps using the real
|
|
33087
|
+
// distance. So on fine data (no auto adjustment) corner-bias=-1 finds the
|
|
33088
|
+
// corners a 2x distance would; on coarse data a positive value counteracts
|
|
33089
|
+
// the automatic coarsening. Use no-corners to turn preservation off entirely.
|
|
33090
|
+
describe: 'corner-detection sensitivity relative to auto (default 0; + more, - fewer)',
|
|
33088
33091
|
type: 'number'
|
|
33089
33092
|
})
|
|
33090
33093
|
.option('prefilter-gate', {
|
|
@@ -63609,6 +63612,14 @@ ${svg}
|
|
|
63609
63612
|
// -- whether a tight coastline or a gentle, hundreds-of-km graticule arc --
|
|
63610
63613
|
// never qualifies; only a localized kink, where the inner turn approaches
|
|
63611
63614
|
// the full turn, is a corner.
|
|
63615
|
+
// 1b. (open paths) Also flag the end of a long straight run that turns sharply
|
|
63616
|
+
// over a single segment but only gently over the window -- a small jog where
|
|
63617
|
+
// a surveyed border meets a curve, which step 1's window would dilute below
|
|
63618
|
+
// the corner angle. Keyed off the raw segment turn, but the pinned vertex is
|
|
63619
|
+
// snapped to the nearby end of the straight run (stable under tiny vertex
|
|
63620
|
+
// moves) and only where that run's line is actually left (so an incidental
|
|
63621
|
+
// notch after which the run resumes on the same line is not flagged). See
|
|
63622
|
+
// straightRunEndNear.
|
|
63612
63623
|
// 2. Between flagged corners, classify each span as "structural" if it is long
|
|
63613
63624
|
// relative to the tolerance and its curvature stays low (so a straight or
|
|
63614
63625
|
// slowly-curving graticule line counts, but sub-tolerance wiggle does not).
|
|
@@ -63651,14 +63662,18 @@ ${svg}
|
|
|
63651
63662
|
// to count, which matches intuition (a 28 km stretch bending at radius 4 km is
|
|
63652
63663
|
// obviously not straight). Genuinely curving coastline bows far from its chord
|
|
63653
63664
|
// and is still rejected, so spurious corners inside wiggly stretches keep getting
|
|
63654
|
-
// culled.
|
|
63655
|
-
|
|
63665
|
+
// culled. Tightened from 0.03 to 0.02 (a run may curve ~9 deg over its length, not
|
|
63666
|
+
// ~14) after a coastline island pinned corners at both ends of a ~10 deg-curving
|
|
63667
|
+
// stretch that read as "straight" only under the looser corridor: an acute corner
|
|
63668
|
+
// takes the full corridor regardless of the angle coupling below, so only the base
|
|
63669
|
+
// factor governs whether such a run can anchor a sharp corner.
|
|
63670
|
+
var STRAIGHT_DEV_FACTOR = 0.02;
|
|
63656
63671
|
|
|
63657
63672
|
// Angle coupling for corner retention: how much sharper the corner must turn than
|
|
63658
63673
|
// the run it borders already curves. A run that passes the chord test may still
|
|
63659
63674
|
// bend gently within the STRAIGHT_DEV_FACTOR corridor -- for a circular arc the
|
|
63660
|
-
// chord-deviation ratio is ~ (the run's total turn)/8, so the base 0.
|
|
63661
|
-
// run that curves ~
|
|
63675
|
+
// chord-deviation ratio is ~ (the run's total turn)/8, so the base 0.02 admits a
|
|
63676
|
+
// run that curves ~9 deg over its length. Pinning a *gentle* bend at the end of
|
|
63662
63677
|
// such a run is unsafe: the "corner" is barely sharper than the run's own
|
|
63663
63678
|
// curving, so it is really a point on a smooth bend, not a junction. (This is the
|
|
63664
63679
|
// failure mode on coarsely sampled / already-simplified coastlines, where a
|
|
@@ -63669,7 +63684,7 @@ ${svg}
|
|
|
63669
63684
|
// turn >= PIN_TURN_RATIO * (8 * dev) <=> dev <= turn / (8 * PIN_TURN_RATIO).
|
|
63670
63685
|
// retentionDevLimit() returns the smaller of STRAIGHT_DEV_FACTOR and
|
|
63671
63686
|
// turn/(8*PIN_TURN_RATIO), so the coupling only bites for gentle corners (below
|
|
63672
|
-
// ~2*STRAIGHT_DEV_FACTOR*PIN_TURN_RATIO ~
|
|
63687
|
+
// ~2*STRAIGHT_DEV_FACTOR*PIN_TURN_RATIO ~ 46 deg); sharp corners (surveyed-border
|
|
63673
63688
|
// right angles, spits, hairpins) keep the full base tolerance, unchanged.
|
|
63674
63689
|
var PIN_TURN_RATIO = 5;
|
|
63675
63690
|
|
|
@@ -63693,6 +63708,15 @@ ${svg}
|
|
|
63693
63708
|
// old 1*tol behaviour for users who want shorter runs pinned.
|
|
63694
63709
|
var MIN_PIN_RUN_LEN_FACTOR = 2.0;
|
|
63695
63710
|
|
|
63711
|
+
// When a long straight run's end turns sharply into a curve with a small jog, the
|
|
63712
|
+
// sharpest single-segment turn can land a few vertices past the run's actual end.
|
|
63713
|
+
// straightRunEndNear searches back this many source vertices (in addition to the
|
|
63714
|
+
// distance-scaled tangent window) to snap to the stable run end, so detection
|
|
63715
|
+
// does not depend on the smoothing distance being large enough for the window to
|
|
63716
|
+
// span the jog. A jog is a handful of vertices; this is deliberately generous
|
|
63717
|
+
// because the search returns the nearest genuine run end (or nothing).
|
|
63718
|
+
var MAX_JOG_VERTICES = 6;
|
|
63719
|
+
|
|
63696
63720
|
// Convert the user-facing corner-bias (0 = neutral) into the positive multiplier
|
|
63697
63721
|
// k applied to corner-detection resolution (ctol = tol / k). The mapping is
|
|
63698
63722
|
// symmetric about zero -- k(+b) * k(-b) = 1 -- and smooth there (both branches
|
|
@@ -63707,6 +63731,39 @@ ${svg}
|
|
|
63707
63731
|
return b >= 0 ? b + 1 : 1 / (1 - b);
|
|
63708
63732
|
}
|
|
63709
63733
|
|
|
63734
|
+
// Ratio of (typical segment length / smoothing distance) at or below which corner
|
|
63735
|
+
// detection sees several segments per tangent window and behaves normally, so no
|
|
63736
|
+
// automatic coarsening is applied. Above it the window shrinks toward a single
|
|
63737
|
+
// segment and ordinary coarse-data bends start reading as corners.
|
|
63738
|
+
var AUTO_BIAS_RATIO = 0.15;
|
|
63739
|
+
// Most-negative automatic bias. Caps how far detection is coarsened on very sparse
|
|
63740
|
+
// geometry (e.g. lo-res contours), where the ratio can be many times AUTO_BIAS_RATIO
|
|
63741
|
+
// but a handful of doublings already merges the whole neighbourhood.
|
|
63742
|
+
var AUTO_BIAS_FLOOR = -4;
|
|
63743
|
+
|
|
63744
|
+
// Automatic corner-bias from the geometry's coarseness relative to the smoothing
|
|
63745
|
+
// distance. @medianSeg is a robust (median) segment length; @dist is the raw
|
|
63746
|
+
// smoothing distance, both in the same ground units. Corner detection keys off a
|
|
63747
|
+
// tangent window ~0.3*dist wide; when the typical segment is an appreciable
|
|
63748
|
+
// fraction of the distance (ratio r = medianSeg/dist above AUTO_BIAS_RATIO) that
|
|
63749
|
+
// window spans too few segments and gentle-but-coarse bends read as corners. We
|
|
63750
|
+
// return a negative bias that coarsens detection (as if the distance were larger)
|
|
63751
|
+
// enough to push the effective ratio back down: each halving of the effective
|
|
63752
|
+
// resolution costs one bias step, so bias = -log2(r / AUTO_BIAS_RATIO), floored.
|
|
63753
|
+
// Returns 0 (no adjustment) when the geometry is fine relative to the distance,
|
|
63754
|
+
// which is the normal case for detailed datasets smoothed at a real distance.
|
|
63755
|
+
// Note it only ever coarsens: a straight run that genuinely anchors a sharp corner
|
|
63756
|
+
// survives this (the corner is retained by bordering a long straight run, which
|
|
63757
|
+
// coarsening does not remove until it exceeds the run's length), while weakly
|
|
63758
|
+
// supported bends on coarse coastlines/contours fall below the corner threshold.
|
|
63759
|
+
function autoCornerBias(medianSeg, dist) {
|
|
63760
|
+
if (!(medianSeg > 0) || !(dist > 0)) return 0;
|
|
63761
|
+
var r = medianSeg / dist;
|
|
63762
|
+
if (r <= AUTO_BIAS_RATIO) return 0;
|
|
63763
|
+
var b = -Math.log2(r / AUTO_BIAS_RATIO);
|
|
63764
|
+
return b < AUTO_BIAS_FLOOR ? AUTO_BIAS_FLOOR : b;
|
|
63765
|
+
}
|
|
63766
|
+
|
|
63710
63767
|
// @cornerBias (optional, default 0 = neutral) scales only the distance-
|
|
63711
63768
|
// proportional corner parameters, by dividing the tolerance they key off
|
|
63712
63769
|
// (ctol = tol / k, k = cornerBiasScale(bias)). The dimensionless thresholds are
|
|
@@ -63763,9 +63820,107 @@ ${svg}
|
|
|
63763
63820
|
if (inner[j] < params.concentration * turns[j]) continue;
|
|
63764
63821
|
if (isLocalMaxTurn(t, turns, j, W, L, m, lo, hi, cyclic)) corners.push(j);
|
|
63765
63822
|
}
|
|
63823
|
+
// Open paths: also flag the terminal vertex of a long straight run that bends
|
|
63824
|
+
// sharply over a single segment but only gently over the tangent window -- e.g.
|
|
63825
|
+
// where a surveyed border meets a coastline with a small jog. The windowed test
|
|
63826
|
+
// above misses these because the wide window dilutes the sharp segment turn into
|
|
63827
|
+
// a sub-threshold bend, so the straight run's end gets rounded into the adjacent
|
|
63828
|
+
// curve. Here we key off the raw segment turn instead, but confine this rescue
|
|
63829
|
+
// to a genuine straight-run end with two gates (plus straightRunEndNear):
|
|
63830
|
+
// 1. the turn is fully concentrated in the inner window: inner-window turn >=
|
|
63831
|
+
// full-window turn. This is a stricter form of the windowed path's
|
|
63832
|
+
// concentration test (>= concentration * full) and is the crux of the
|
|
63833
|
+
// distinction. At a straight-run end the approaches carry ~no turning, so
|
|
63834
|
+
// the whole window's turn sits in the inner window (ratio >= 1); on a
|
|
63835
|
+
// steadily-curving coastline the turning is spread across the window (inner
|
|
63836
|
+
// < full), so a lone sharp segment there is rejected. The wide window
|
|
63837
|
+
// dilutes the end's departure below the corner threshold (which is why the
|
|
63838
|
+
// windowed path misses it), but the concentration ratio stays high.
|
|
63839
|
+
// 2. (via straightRunEndNear) a bordering straight run's line is actually left,
|
|
63840
|
+
// so an incidental notch on an otherwise-continuing run is not pinned.
|
|
63841
|
+
// Rings keep the windowed-only detection for now.
|
|
63842
|
+
if (!cyclic) {
|
|
63843
|
+
var raw = new Float64Array(m);
|
|
63844
|
+
for (i = lo; i < hi; i++) raw[i] = vertexTurn(channels, K, i);
|
|
63845
|
+
for (j = lo; j < hi; j++) {
|
|
63846
|
+
if (raw[j] < params.cornerAngle) continue;
|
|
63847
|
+
if (inner[j] < turns[j]) continue;
|
|
63848
|
+
if (!isLocalMaxTurn(t, raw, j, W, L, m, lo, hi, cyclic)) continue;
|
|
63849
|
+
var e = straightRunEndNear(t, channels, n, j, W, params);
|
|
63850
|
+
if (e >= 0 && corners.indexOf(e) === -1) corners.push(e);
|
|
63851
|
+
}
|
|
63852
|
+
corners.sort(function (a, b) { return a - b; });
|
|
63853
|
+
}
|
|
63766
63854
|
return corners;
|
|
63767
63855
|
}
|
|
63768
63856
|
|
|
63857
|
+
// Given a sharp single-segment turn at open-path vertex @j, find the end of a
|
|
63858
|
+
// long straight run that the path leaves, within arc length @W of @j, and return
|
|
63859
|
+
// that run-end vertex to pin (or -1). We search a neighbourhood rather than
|
|
63860
|
+
// requiring the run's last vertex to be exactly where the segment turn peaks: a
|
|
63861
|
+
// jog into a curve can place its sharpest vertex a step or two past the run's
|
|
63862
|
+
// end, and exactly which vertex is sharpest is sensitive to tiny differences in
|
|
63863
|
+
// vertex placement -- but the straight run's end itself is stable. Snapping to it
|
|
63864
|
+
// keeps detection from flickering on and off with sub-tolerance vertex moves. The
|
|
63865
|
+
// straightRunLeaves test only succeeds at a genuine run end whose continuation
|
|
63866
|
+
// departs, so an incidental jog after which the run resumes is still not pinned.
|
|
63867
|
+
function straightRunEndNear(t, channels, n, j, W, params) {
|
|
63868
|
+
var e, k;
|
|
63869
|
+
// The jog between the sharp turn and the run's end is a fixed handful of source
|
|
63870
|
+
// vertices, independent of the smoothing distance -- so the search extends by a
|
|
63871
|
+
// vertex count as well as the distance-scaled window W. Without the vertex floor
|
|
63872
|
+
// the window shrinks with the distance and, at small distances, can no longer
|
|
63873
|
+
// reach back across the jog to the run end (fewer, not more, corners pinned as
|
|
63874
|
+
// the distance drops -- the opposite of what a long run warrants). We return the
|
|
63875
|
+
// first (nearest) run end found, so a generous reach only costs a few extra
|
|
63876
|
+
// straightRunLeaves checks when there is nothing to pin.
|
|
63877
|
+
for (e = j, k = 0; e >= 1; e--, k++) {
|
|
63878
|
+
if (t[j] - t[e] > W && k > MAX_JOG_VERTICES) break;
|
|
63879
|
+
if (straightRunLeaves(t, channels, n, e, -1, params) ||
|
|
63880
|
+
straightRunLeaves(t, channels, n, e, 1, params)) return e;
|
|
63881
|
+
}
|
|
63882
|
+
for (e = j + 1, k = 1; e < n - 1; e++, k++) {
|
|
63883
|
+
if (t[e] - t[j] > W && k > MAX_JOG_VERTICES) break;
|
|
63884
|
+
if (straightRunLeaves(t, channels, n, e, -1, params) ||
|
|
63885
|
+
straightRunLeaves(t, channels, n, e, 1, params)) return e;
|
|
63886
|
+
}
|
|
63887
|
+
return -1;
|
|
63888
|
+
}
|
|
63889
|
+
|
|
63890
|
+
// Is the near side of @e (walking direction @dir: -1 = run precedes e, +1 = run
|
|
63891
|
+
// follows e) a long straight run, AND does the far side leave that run's line --
|
|
63892
|
+
// i.e. a point a full run-length along the far side sits outside the run's
|
|
63893
|
+
// straightness corridor? A jog that rejoins the run returns to ~0 perpendicular
|
|
63894
|
+
// offset and fails this, so it is not flagged. (This also implies @e is the run's
|
|
63895
|
+
// end: if the run continued straight past @e, the far point would stay on its
|
|
63896
|
+
// line.)
|
|
63897
|
+
function straightRunLeaves(t, channels, n, e, dir, params) {
|
|
63898
|
+
var K = channels.length, L = t[n - 1];
|
|
63899
|
+
var nearEnd = reach(t, n, n, L, e, dir, params.minPinRunLen, false);
|
|
63900
|
+
if (nearEnd === e) return false;
|
|
63901
|
+
var a = dir < 0 ? nearEnd : e;
|
|
63902
|
+
var b = dir < 0 ? e : nearEnd;
|
|
63903
|
+
// Require the near run to be straight to the SAME angle-coupled tolerance
|
|
63904
|
+
// retention will demand of a pin at @e (retentionDevLimit of the turn at @e),
|
|
63905
|
+
// not the looser default corridor. This makes straightRunEndNear's backward
|
|
63906
|
+
// scan stop at the run's true end -- the last vertex actually on the run's line
|
|
63907
|
+
// -- rather than a vertex a little past it that squeaks inside the loose 3%
|
|
63908
|
+
// corridor but would then be culled by retention (leaving nothing pinned).
|
|
63909
|
+
var devLim = retentionDevLimit(cornerTurn(t, channels, n, e, false, params));
|
|
63910
|
+
if (!isStraightRun(t, channels, a, b, params, devLim)) return false;
|
|
63911
|
+
var farEnd = reach(t, n, n, L, e, -dir, params.minPinRunLen, false);
|
|
63912
|
+
if (farEnd === e) return false;
|
|
63913
|
+
var pe = getPt(channels, K, e);
|
|
63914
|
+
var u = subv(getPt(channels, K, nearEnd), pe, K); // direction along the near run
|
|
63915
|
+
var uu = dot(u, u, K);
|
|
63916
|
+
if (!(uu > 0)) return false;
|
|
63917
|
+
var af = subv(getPt(channels, K, farEnd), pe, K);
|
|
63918
|
+
var along = Math.abs(dot(af, u, K)) / Math.sqrt(uu);
|
|
63919
|
+
var perp = Math.sqrt(perpDistSq(channels, K, farEnd, pe, u, uu));
|
|
63920
|
+
// outside the straight corridor extended from the run -> the path has left it
|
|
63921
|
+
return perp > STRAIGHT_DEV_FACTOR * along;
|
|
63922
|
+
}
|
|
63923
|
+
|
|
63769
63924
|
// Is span [a, b] (inclusive vertex indices, a < b, open frame) a structural run:
|
|
63770
63925
|
// long relative to the tolerance and low-curvature throughout?
|
|
63771
63926
|
function isStructuralRun(t, channels, a, b, params) {
|
|
@@ -64139,6 +64294,20 @@ ${svg}
|
|
|
64139
64294
|
// segments); user-overridable via max-bend-angle
|
|
64140
64295
|
var DEVIATION_FACTOR = 0.1; // sagitta guard: also cut a gentle bend that bows
|
|
64141
64296
|
// more than tolerance * this from its chord
|
|
64297
|
+
// Preserved structural runs (long straight / low-curvature spans between pinned
|
|
64298
|
+
// corners) are not smoothed, but their ORIGINAL vertices are resampled with the
|
|
64299
|
+
// same bend-angle decimation so the whole output has adaptive vertex spacing
|
|
64300
|
+
// (no abrupt density seam at a run boundary). The decimation runs in the
|
|
64301
|
+
// smoothing channels, so for unprojected data a long line -- which curves in the
|
|
64302
|
+
// geocentric x,y,z space even when it is "straight" in lng/lat -- keeps enough
|
|
64303
|
+
// interior vertices to approximate that curve on reprojection, scaling with the
|
|
64304
|
+
// line's length automatically. Because it only ever keeps a SUBSET of the
|
|
64305
|
+
// original vertices (never interpolates new ones), it can't distort a rhumb or
|
|
64306
|
+
// geodesic edge -- every output vertex still lies exactly where the source drew
|
|
64307
|
+
// it. Structural runs use a fraction of the bend angle (sampled finer than
|
|
64308
|
+
// smoothed spans) as a conservative bias toward preservation / reprojection
|
|
64309
|
+
// headroom.
|
|
64310
|
+
var STRUCTURAL_BEND_FACTOR = 0.5; // structural-run bend angle = max-bend-angle * this
|
|
64142
64311
|
|
|
64143
64312
|
// Smooth a single arc's coordinates.
|
|
64144
64313
|
// @xx, @yy: coordinate arrays (may be typed-array subarrays) for one arc.
|
|
@@ -64147,8 +64316,9 @@ ${svg}
|
|
|
64147
64316
|
// arcs are preserved exactly (so shared topology nodes stay put); closed arcs
|
|
64148
64317
|
// are smoothed cyclically and returned closed (first point repeated at the end).
|
|
64149
64318
|
// With keepCorners, structural corners (where long straight/low-curvature runs
|
|
64150
|
-
// meet) are detected and pinned
|
|
64151
|
-
//
|
|
64319
|
+
// meet) are detected and pinned; the runs themselves are not smoothed but are
|
|
64320
|
+
// resampled (a subset of their original vertices, at adaptive spacing), and only
|
|
64321
|
+
// the spans between corners are smoothed.
|
|
64152
64322
|
// Resolve the curvature-correction gain (default 1 = fully corrected). gain=0
|
|
64153
64323
|
// leaves the plain weighted moving average; negative values are clamped to 0.
|
|
64154
64324
|
function resolveGain(opts) {
|
|
@@ -64198,7 +64368,7 @@ ${svg}
|
|
|
64198
64368
|
var origY = toArray(yy);
|
|
64199
64369
|
var tol = opts.tolerance * KERNEL_FROM_DISTANCE;
|
|
64200
64370
|
if (n < 3 || !(opts.tolerance > 0)) {
|
|
64201
|
-
return {xx: origX, yy: origY};
|
|
64371
|
+
return {xx: origX, yy: origY, corners: 0};
|
|
64202
64372
|
}
|
|
64203
64373
|
var method = opts.method == 'gaussian' ? 'gaussian' : 'paek';
|
|
64204
64374
|
var closed = !!opts.closed;
|
|
@@ -64210,7 +64380,7 @@ ${svg}
|
|
|
64210
64380
|
// kernel scale stays in true distance regardless of coordinate representation.
|
|
64211
64381
|
var t = arcLengths(origX, origY, n, spherical);
|
|
64212
64382
|
if (!(t[n - 1] > 0)) {
|
|
64213
|
-
return {xx: origX, yy: origY}; // degenerate (coincident points)
|
|
64383
|
+
return {xx: origX, yy: origY, corners: 0}; // degenerate (coincident points)
|
|
64214
64384
|
}
|
|
64215
64385
|
// The low-pass kernel scale is the raw distance scale (tol) times the baked-in
|
|
64216
64386
|
// KERNEL_STRENGTH calibration and the user's `strength` multiplier (default 1).
|
|
@@ -64260,7 +64430,9 @@ ${svg}
|
|
|
64260
64430
|
corners = filterRingCornersByStructure(t, channels, n, corners, ringParams);
|
|
64261
64431
|
}
|
|
64262
64432
|
if (corners.length === 0) {
|
|
64263
|
-
|
|
64433
|
+
var cyc = smoothClosedCyclic(t, channels, n, ctx);
|
|
64434
|
+
cyc.corners = 0;
|
|
64435
|
+
return cyc;
|
|
64264
64436
|
}
|
|
64265
64437
|
// A ring with corners is processed as an open path: rotate it to start (and
|
|
64266
64438
|
// end) at one corner, with the remaining corners as interior breakpoints.
|
|
@@ -64271,7 +64443,11 @@ ${svg}
|
|
|
64271
64443
|
t = arcLengths(origX, origY, n, spherical);
|
|
64272
64444
|
channels = spherical ? lngLatToXYZChannels(origX, origY, n) : [origX, origY];
|
|
64273
64445
|
var breaks = mapRotatedCorners(corners, rot.shift, rot.m);
|
|
64274
|
-
|
|
64446
|
+
var ring = smoothOpenSpans(origX, origY, t, channels, n, breaks, ctx);
|
|
64447
|
+
// The ring seam (corners[0], pinned as the rotated start/end) is itself a
|
|
64448
|
+
// preserved corner, on top of the interior breakpoints smoothOpenSpans kept.
|
|
64449
|
+
ring.corners += 1;
|
|
64450
|
+
return ring;
|
|
64275
64451
|
}
|
|
64276
64452
|
|
|
64277
64453
|
var openBreaks = keepCorners ?
|
|
@@ -64304,11 +64480,12 @@ ${svg}
|
|
|
64304
64480
|
var lo = bounds[s], hi = bounds[s + 1];
|
|
64305
64481
|
var preserve = !!params && isStructuralRun(t, channels, lo, hi, params);
|
|
64306
64482
|
var span = preserve ?
|
|
64307
|
-
|
|
64483
|
+
resampleStructuralRun(origX, origY, channels, lo, hi, ctx) :
|
|
64308
64484
|
smoothSpanOpen(origX, origY, t, channels, lo, hi, ctx);
|
|
64309
64485
|
appendSpan(xx, yy, span, s === 0);
|
|
64310
64486
|
}
|
|
64311
|
-
|
|
64487
|
+
// Interior breakpoints that survived refinement are the pinned corners.
|
|
64488
|
+
return {xx: xx, yy: yy, corners: bounds.length - 2};
|
|
64312
64489
|
}
|
|
64313
64490
|
|
|
64314
64491
|
// Drop interior breakpoints that don't border any pinnable straight run (e.g.
|
|
@@ -64534,6 +64711,34 @@ ${svg}
|
|
|
64534
64711
|
return {xx: xx, yy: yy};
|
|
64535
64712
|
}
|
|
64536
64713
|
|
|
64714
|
+
// Resample a preserved structural run [lo, hi] (a long straight / low-curvature
|
|
64715
|
+
// span between pinned corners). The run is NOT smoothed: its shape is kept by
|
|
64716
|
+
// emitting a SUBSET of its original vertices, decimated with the shared
|
|
64717
|
+
// bend-angle filter in the smoothing channels (so a long line that curves in the
|
|
64718
|
+
// geocentric space keeps interior vertices scaling with its length -- see
|
|
64719
|
+
// STRUCTURAL_BEND_FACTOR). Both endpoints are always kept, at their exact
|
|
64720
|
+
// original coordinates, so pinned corners and shared topology nodes are
|
|
64721
|
+
// unchanged. A run too short to decimate is copied verbatim.
|
|
64722
|
+
function resampleStructuralRun(origX, origY, channels, lo, hi, ctx) {
|
|
64723
|
+
var nSub = hi - lo + 1;
|
|
64724
|
+
if (nSub < 3) return copySpan(origX, origY, lo, hi);
|
|
64725
|
+
var K = channels.length;
|
|
64726
|
+
var P = new Array(nSub);
|
|
64727
|
+
for (var i = 0; i < nSub; i++) {
|
|
64728
|
+
var p = new Array(K);
|
|
64729
|
+
for (var c = 0; c < K; c++) p[c] = channels[c][lo + i];
|
|
64730
|
+
P[i] = p;
|
|
64731
|
+
}
|
|
64732
|
+
var keep = decimateByBend(P, K, ctx.bendAngle * STRUCTURAL_BEND_FACTOR, ctx.tol * DEVIATION_FACTOR);
|
|
64733
|
+
var xx = [], yy = [];
|
|
64734
|
+
for (var ki = 0; ki < keep.length; ki++) {
|
|
64735
|
+
var idx = lo + keep[ki];
|
|
64736
|
+
xx.push(origX[idx]); // exact original coordinate, never interpolated
|
|
64737
|
+
yy.push(origY[idx]);
|
|
64738
|
+
}
|
|
64739
|
+
return {xx: xx, yy: yy};
|
|
64740
|
+
}
|
|
64741
|
+
|
|
64537
64742
|
function appendSpan(xx, yy, span, isFirst) {
|
|
64538
64743
|
for (var i = isFirst ? 0 : 1; i < span.xx.length; i++) {
|
|
64539
64744
|
xx.push(span.xx[i]);
|
|
@@ -64658,26 +64863,38 @@ ${svg}
|
|
|
64658
64863
|
}
|
|
64659
64864
|
|
|
64660
64865
|
// 2. one-pass bend-angle filter
|
|
64661
|
-
var
|
|
64662
|
-
var epsDev = ctx.tol * DEVIATION_FACTOR;
|
|
64866
|
+
var keep = decimateByBend(P, K, ctx.bendAngle, ctx.tol * DEVIATION_FACTOR);
|
|
64663
64867
|
var out = [];
|
|
64664
64868
|
for (var c = 0; c < K; c++) out.push([]);
|
|
64665
|
-
appendPoint(out, P[
|
|
64869
|
+
for (var ki = 0; ki < keep.length; ki++) appendPoint(out, P[keep[ki]], K);
|
|
64870
|
+
return out;
|
|
64871
|
+
}
|
|
64872
|
+
|
|
64873
|
+
// One-pass forward decimation of a K-channel point list @P: keep the two
|
|
64874
|
+
// endpoints plus every interior point where the turn accumulated since the last
|
|
64875
|
+
// kept point reaches @theta, or where the estimated sagitta of the skipped
|
|
64876
|
+
// stretch (chord * accumulated turn / 8, the bow of a circular arc) reaches
|
|
64877
|
+
// @epsDev. Bounds the angle between consecutive kept segments by construction, so
|
|
64878
|
+
// joins stay smooth. Returns the kept indices into @P (always including 0 and the
|
|
64879
|
+
// last index). Shared by the smoothed-curve resampler and the structural-run
|
|
64880
|
+
// resampler (see resampleStructuralRun).
|
|
64881
|
+
function decimateByBend(P, K, theta, epsDev) {
|
|
64882
|
+
var n = P.length;
|
|
64883
|
+
var keep = [0];
|
|
64884
|
+
if (n < 2) return keep;
|
|
64666
64885
|
var anchor = 0; // last kept vertex
|
|
64667
64886
|
var accTurn = 0; // absolute turning accumulated since the anchor
|
|
64668
|
-
for (var j = 1; j <
|
|
64887
|
+
for (var j = 1; j < n - 1; j++) {
|
|
64669
64888
|
accTurn += vecAngle(P[j - 1], P[j], P[j], P[j + 1], K);
|
|
64670
|
-
// sagitta of a circular arc of chord c and total turn a is ~ c*a/8; cut a
|
|
64671
|
-
// long gentle bend before it bows more than epsDev from its chord
|
|
64672
64889
|
var sagitta = chordLen(P[anchor], P[j + 1], K) * accTurn * 0.125;
|
|
64673
64890
|
if (accTurn >= theta || sagitta >= epsDev) {
|
|
64674
|
-
|
|
64891
|
+
keep.push(j);
|
|
64675
64892
|
anchor = j;
|
|
64676
64893
|
accTurn = 0;
|
|
64677
64894
|
}
|
|
64678
64895
|
}
|
|
64679
|
-
|
|
64680
|
-
return
|
|
64896
|
+
keep.push(n - 1);
|
|
64897
|
+
return keep;
|
|
64681
64898
|
}
|
|
64682
64899
|
|
|
64683
64900
|
// Angle (radians) between vectors (b - a) and (d - c) over K channels.
|
|
@@ -64998,16 +65215,32 @@ ${svg}
|
|
|
64998
65215
|
}
|
|
64999
65216
|
}
|
|
65000
65217
|
|
|
65001
|
-
|
|
65218
|
+
// Corner detection is automatically coarsened on geometry that is sparse
|
|
65219
|
+
// relative to the smoothing distance (long segments -> few segments per
|
|
65220
|
+
// detection window -> ordinary coarse bends misread as corners; see
|
|
65221
|
+
// autoCornerBias). The user's corner-bias is relative to this automatic base:
|
|
65222
|
+
// it is added on top, so corner-bias=0 (the default) is "whatever the geometry
|
|
65223
|
+
// warrants", a positive value finds more corners than the auto baseline, a
|
|
65224
|
+
// negative value fewer.
|
|
65225
|
+
var autoBias = 0;
|
|
65226
|
+
if (keepCorners) {
|
|
65227
|
+
autoBias = autoCornerBias(medianSegmentLength(arcs, spherical), tolerance);
|
|
65228
|
+
}
|
|
65229
|
+
var effectiveCornerBias = autoBias + (opts.corner_bias || 0);
|
|
65230
|
+
|
|
65231
|
+
var corners = smoothPaths(arcs, {
|
|
65002
65232
|
tolerance: tolerance,
|
|
65003
65233
|
method: method,
|
|
65004
65234
|
spherical: spherical,
|
|
65005
65235
|
keepCorners: keepCorners,
|
|
65006
|
-
cornerBias:
|
|
65236
|
+
cornerBias: effectiveCornerBias,
|
|
65007
65237
|
gain: opts.gain,
|
|
65008
65238
|
strength: opts.strength,
|
|
65009
65239
|
maxBendAngle: opts.max_bend_angle
|
|
65010
65240
|
});
|
|
65241
|
+
if (keepCorners && corners > 0) {
|
|
65242
|
+
message('Pinned ' + corners + ' corner' + utils.pluralSuffix(corners));
|
|
65243
|
+
}
|
|
65011
65244
|
|
|
65012
65245
|
if (implicitlySmoothedNames.length > 0) {
|
|
65013
65246
|
message(
|
|
@@ -65021,10 +65254,12 @@ ${svg}
|
|
|
65021
65254
|
// untouched, shared polygon boundaries stay coincident and topology is
|
|
65022
65255
|
// preserved; updateVertexData() also handles undo capture and resets stale
|
|
65023
65256
|
// simplification thresholds.
|
|
65257
|
+
// Returns the total number of structural corners preserved across all arcs.
|
|
65024
65258
|
function smoothPaths(arcs, opts) {
|
|
65025
65259
|
var nn = [];
|
|
65026
65260
|
var xx = [];
|
|
65027
65261
|
var yy = [];
|
|
65262
|
+
var corners = 0;
|
|
65028
65263
|
var i, k, res;
|
|
65029
65264
|
arcs.forEach3(function(axx, ayy, azz, arcId) {
|
|
65030
65265
|
res = smoothArcCoords(axx, ayy, {
|
|
@@ -65038,6 +65273,7 @@ ${svg}
|
|
|
65038
65273
|
maxBendAngle: opts.maxBendAngle,
|
|
65039
65274
|
closed: arcs.arcIsClosed(arcId)
|
|
65040
65275
|
});
|
|
65276
|
+
corners += res.corners || 0;
|
|
65041
65277
|
nn.push(res.xx.length);
|
|
65042
65278
|
for (i = 0, k = res.xx.length; i < k; i++) {
|
|
65043
65279
|
xx.push(res.xx[i]);
|
|
@@ -65045,6 +65281,31 @@ ${svg}
|
|
|
65045
65281
|
}
|
|
65046
65282
|
});
|
|
65047
65283
|
arcs.updateVertexData(nn, xx, yy);
|
|
65284
|
+
return corners;
|
|
65285
|
+
}
|
|
65286
|
+
|
|
65287
|
+
// Median segment length across all arcs, in ground units (meters for spherical
|
|
65288
|
+
// data), used to gauge how coarse the geometry is relative to the smoothing
|
|
65289
|
+
// distance (see autoCornerBias). The median is robust to a few very long straight
|
|
65290
|
+
// segments (which are not a spurious-corner risk) and to dense sub-scale detail.
|
|
65291
|
+
// Very large datasets are sampled with a stride so the cost stays bounded.
|
|
65292
|
+
function medianSegmentLength(arcs, spherical) {
|
|
65293
|
+
var totalSegs = arcs.getPointCount() - arcs.size();
|
|
65294
|
+
if (totalSegs < 1) return 0;
|
|
65295
|
+
var MAX_SAMPLES = 100000;
|
|
65296
|
+
var stride = Math.ceil(totalSegs / MAX_SAMPLES);
|
|
65297
|
+
var distFn = spherical ? greatCircleDistance : distance2D;
|
|
65298
|
+
var lens = [];
|
|
65299
|
+
var counter = 0;
|
|
65300
|
+
arcs.forEach3(function(xx, yy) {
|
|
65301
|
+
for (var i = 1, n = xx.length; i < n; i++) {
|
|
65302
|
+
if (counter++ % stride === 0) {
|
|
65303
|
+
lens.push(distFn(xx[i - 1], yy[i - 1], xx[i], yy[i]));
|
|
65304
|
+
}
|
|
65305
|
+
}
|
|
65306
|
+
});
|
|
65307
|
+
if (lens.length === 0) return 0;
|
|
65308
|
+
return utils.findMedian(lens);
|
|
65048
65309
|
}
|
|
65049
65310
|
|
|
65050
65311
|
function getSmoothMethod(opts) {
|
|
@@ -66852,7 +67113,7 @@ ${svg}
|
|
|
66852
67113
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
66853
67114
|
}
|
|
66854
67115
|
|
|
66855
|
-
var version = "0.7.
|
|
67116
|
+
var version = "0.7.38";
|
|
66856
67117
|
|
|
66857
67118
|
// Parse command line args into commands and run them
|
|
66858
67119
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|