mapshaper 0.7.34 → 0.7.36
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 +906 -179
- package/package.json +1 -1
- package/www/index.html +7 -2
- package/www/mapshaper-gui.js +210 -41
- package/www/mapshaper.js +906 -179
package/mapshaper.js
CHANGED
|
@@ -31959,12 +31959,12 @@ ${svg}
|
|
|
31959
31959
|
type: 'flag'
|
|
31960
31960
|
})
|
|
31961
31961
|
.option('coarse-bridge', {
|
|
31962
|
-
// Undocumented:
|
|
31963
|
-
//
|
|
31964
|
-
//
|
|
31965
|
-
//
|
|
31966
|
-
//
|
|
31967
|
-
//
|
|
31962
|
+
// Undocumented: force the low-resolution concave bridge at EVERY deep
|
|
31963
|
+
// concave bend, bypassing the default's exposure gate and the loop
|
|
31964
|
+
// remover's clip budget. The guarded form is the default (2026-07-02);
|
|
31965
|
+
// this unguarded form is faster still but NOT area-safe: an exposed
|
|
31966
|
+
// bridge can dent the buffer or create spurious holes (see the caution
|
|
31967
|
+
// at makeCoarseConcaveJoin and "coarse-bridge" in buffer-line-notes.md).
|
|
31968
31968
|
type: 'flag'
|
|
31969
31969
|
})
|
|
31970
31970
|
.option('no-gap-patch', {
|
|
@@ -32519,6 +32519,14 @@ ${svg}
|
|
|
32519
32519
|
type: 'number',
|
|
32520
32520
|
describe: 'Visvalingam angle-weight coefficient (default 0.7); higher removes spiky detail more eagerly'
|
|
32521
32521
|
})
|
|
32522
|
+
.option('roundness', {
|
|
32523
|
+
type: 'number',
|
|
32524
|
+
describe: 'protect rounded loops: min enclosed-area / loop-perimeter as a fraction of the distance (default 0.2); higher removes more, 0 disables'
|
|
32525
|
+
})
|
|
32526
|
+
.option('min-area', {
|
|
32527
|
+
type: 'number',
|
|
32528
|
+
describe: 'drop a closed ring (island/hole) when the filter leaves less than this fraction of its original area (default 0.6); 0 disables'
|
|
32529
|
+
})
|
|
32522
32530
|
.option('planar', {
|
|
32523
32531
|
describe: 'treat decimal degree coords as planar x,y (default is spherical)',
|
|
32524
32532
|
type: 'flag'
|
|
@@ -33052,6 +33060,13 @@ ${svg}
|
|
|
33052
33060
|
describe: 'shrinkage-correction (default 1; 0 = none, >1 exaggerates bends)',
|
|
33053
33061
|
type: 'number'
|
|
33054
33062
|
})
|
|
33063
|
+
.option('strength', {
|
|
33064
|
+
// undocumented: multiplier on the smoothing kernel scale relative to the
|
|
33065
|
+
// distance (default 1; >1 smooths more strongly with larger divergence from
|
|
33066
|
+
// the original, <1 more gently). Scales only the low-pass kernel -- corner
|
|
33067
|
+
// detection, prefiltering and island dropping stay keyed to the raw distance.
|
|
33068
|
+
type: 'number'
|
|
33069
|
+
})
|
|
33055
33070
|
.option('max-bend-angle', {
|
|
33056
33071
|
describe: 'max bend between output segments in degrees (default is 8)',
|
|
33057
33072
|
type: 'number'
|
|
@@ -33061,10 +33076,15 @@ ${svg}
|
|
|
33061
33076
|
type: 'flag'
|
|
33062
33077
|
})
|
|
33063
33078
|
.option('corner-bias', {
|
|
33064
|
-
//
|
|
33065
|
-
//
|
|
33066
|
-
//
|
|
33067
|
-
//
|
|
33079
|
+
// Sensitivity of corner detection (default 0 = neutral). Positive keeps
|
|
33080
|
+
// more corners, negative fewer; +/- values of equal size are inverses.
|
|
33081
|
+
// Under the hood it scales only the distance-proportional detection
|
|
33082
|
+
// parameters (not angles), detecting corners as if the distance were
|
|
33083
|
+
// divided by k, where k = bias+1 for bias >= 0 and 1/(1-bias) for bias < 0;
|
|
33084
|
+
// the smoothing kernel keeps using the real distance. So corner-bias=-1
|
|
33085
|
+
// finds the corners a 2x distance would, corner-bias=1 those of a 0.5x
|
|
33086
|
+
// distance. Use no-corners to turn corner preservation off entirely.
|
|
33087
|
+
describe: 'corner-detection sensitivity (default 0; + is more sensitive, - is less)',
|
|
33068
33088
|
type: 'number'
|
|
33069
33089
|
})
|
|
33070
33090
|
.option('prefilter-gate', {
|
|
@@ -33072,6 +33092,19 @@ ${svg}
|
|
|
33072
33092
|
// intricate detail (default 4; higher removes less)
|
|
33073
33093
|
type: 'number'
|
|
33074
33094
|
})
|
|
33095
|
+
.option('prefilter-roundness', {
|
|
33096
|
+
// undocumented: prefilter roundness protection -- min enclosed-area /
|
|
33097
|
+
// loop-perimeter (as a fraction of the smoothing distance) for a loop
|
|
33098
|
+
// (e.g. an island) to be protected from the prefilter (default 0.2;
|
|
33099
|
+
// higher removes more, 0 disables and lets rounded islands collapse)
|
|
33100
|
+
type: 'number'
|
|
33101
|
+
})
|
|
33102
|
+
.option('prefilter-min-area', {
|
|
33103
|
+
// undocumented: drop a closed ring (island/hole) when the prefilter would
|
|
33104
|
+
// leave less than this fraction of its original area (default 0.6), rather
|
|
33105
|
+
// than smoothing a distorted remnant; 0 disables the drop
|
|
33106
|
+
type: 'number'
|
|
33107
|
+
})
|
|
33075
33108
|
.option('planar', {
|
|
33076
33109
|
// describe: 'smooth decimal degree coords in 2D space (default is spherical)',
|
|
33077
33110
|
type: 'flag'
|
|
@@ -42059,17 +42092,15 @@ ${svg}
|
|
|
42059
42092
|
return turnPrefix[posHi] - turnPrefix[posLo];
|
|
42060
42093
|
}
|
|
42061
42094
|
|
|
42062
|
-
// Multi-pass collapse
|
|
42063
|
-
//
|
|
42064
|
-
//
|
|
42065
|
-
//
|
|
42066
|
-
//
|
|
42067
|
-
//
|
|
42068
|
-
//
|
|
42069
|
-
//
|
|
42070
|
-
//
|
|
42071
|
-
// turn gate; caps (180-degree arcs) exceed it and so are never collapsed.
|
|
42072
|
-
// @fillFloor (dip+coverage path): max winding-0 area a collapse may swallow; a
|
|
42095
|
+
// Multi-pass collapse. Iterating lets the collapse of tight inner overshoot
|
|
42096
|
+
// loops shorten the spans of the loops that wrap them, so a wrapper whose span
|
|
42097
|
+
// was too long (or too-large-turn, on the gated paths) on one pass can become
|
|
42098
|
+
// collapsible on the next -- the multi-pass "peel simple interior loops first"
|
|
42099
|
+
// idea. With dipTags supplied (clean-outline rings; the tags act as a flag,
|
|
42100
|
+
// see below) each candidate is decided by the exact coverage check plus the
|
|
42101
|
+
// opposite-wound hole guard; otherwise the source-turn or geometric turn gate
|
|
42102
|
+
// applies (maxTurn; caps' 180-degree arcs exceed it and are never collapsed).
|
|
42103
|
+
// @fillFloor (coverage path): max winding-0 area a collapse may swallow; a
|
|
42073
42104
|
// collapse filling a larger hole/notch is refused (see BUFFER_LOOP_FILL_AREA_FRAC).
|
|
42074
42105
|
// Callers pass dist^2 * BUFFER_LOOP_FILL_AREA_FRAC; defaults to the uncovered
|
|
42075
42106
|
// floor when omitted (unit tests without a buffer distance).
|
|
@@ -42078,11 +42109,32 @@ ${svg}
|
|
|
42078
42109
|
if (maxTurn === undefined) maxTurn = BUFFER_LOOP_MAX_TURN;
|
|
42079
42110
|
if (!(maxPasses >= 1)) maxPasses = 12;
|
|
42080
42111
|
if (!(fillFloor >= 0)) fillFloor = BUFFER_LOOP_CHECK_MIN_AREA;
|
|
42081
|
-
|
|
42112
|
+
// dipTags acts purely as a flag here: a ring built by the clean-outline
|
|
42113
|
+
// construction opts into the coverage-checked path. The per-vertex tag
|
|
42114
|
+
// values are not consulted (the tag-excluded turn gate was removed in favor
|
|
42115
|
+
// of the exact coverage check), so the tags are not threaded through passes.
|
|
42116
|
+
var coverage = !!dipTags;
|
|
42117
|
+
// Neighborhood clip budget, shared by all passes over this ring: each
|
|
42118
|
+
// accepted collapse's uncovered (clipped) area accumulates in a coarse
|
|
42119
|
+
// grid, and an accept that would push any touched cell past the per-collapse
|
|
42120
|
+
// floor is refused instead. Individually sub-floor clips are the design's
|
|
42121
|
+
// tolerance, but several of them can land in the SAME neighborhood (dense
|
|
42122
|
+
// fold clusters -- routine with coarse bridge geometry, theoretically
|
|
42123
|
+
// possible with overlapping same-pass collapses) and compound into a
|
|
42124
|
+
// floor-scale dent; the budget bounds the damage per neighborhood to the
|
|
42125
|
+
// same floor that bounds it per collapse.
|
|
42126
|
+
// Cell size scales with the buffer radius (recovered from the disk-relative
|
|
42127
|
+
// fillFloor) so a neighborhood is radius-scale at any latitude/units; clip
|
|
42128
|
+
// attribution is O(1) per accept (center cell), so a dent straddling a cell
|
|
42129
|
+
// edge is bounded by 2x the floor rather than 1x.
|
|
42130
|
+
var budgetCell = Math.max(1000, Math.sqrt(fillFloor / BUFFER_LOOP_FILL_AREA_FRAC));
|
|
42131
|
+
var clipBudget = coverage ? {map: new Map(), cell: budgetCell} : null;
|
|
42132
|
+
var work = ring, workPos = null;
|
|
42082
42133
|
for (var pass = 0; pass < maxPasses; pass++) {
|
|
42083
|
-
var res = collapseRingLoopsPass(work, maxGap, workPos, turnPrefix, maxTurn,
|
|
42134
|
+
var res = collapseRingLoopsPass(work, maxGap, workPos, turnPrefix, maxTurn,
|
|
42135
|
+
coverage, fillFloor, clipBudget);
|
|
42084
42136
|
if (res.ring.length === work.length) return res.ring; // stable
|
|
42085
|
-
work = res.ring; workPos = res.srcPos;
|
|
42137
|
+
work = res.ring; workPos = res.srcPos;
|
|
42086
42138
|
}
|
|
42087
42139
|
return work;
|
|
42088
42140
|
}
|
|
@@ -42090,38 +42142,25 @@ ${svg}
|
|
|
42090
42142
|
// One greedy forward-collapse pass (mirrors removeBufferRingLoops' compaction).
|
|
42091
42143
|
// The span gate is, in order of preference:
|
|
42092
42144
|
// - the reliable source-path turn (getSpanTurn) when srcPos+turnPrefix given;
|
|
42093
|
-
// - else the
|
|
42094
|
-
//
|
|
42095
|
-
//
|
|
42096
|
-
|
|
42097
|
-
function collapseRingLoopsPass(ring, maxGap, srcPos, turnPrefix, maxTurn, dipTags, fillFloor) {
|
|
42145
|
+
// - else the exact coverage check when `coverage` is set (clean-outline rings);
|
|
42146
|
+
// - else the ring's cumulative turn with a geometric cusp threshold.
|
|
42147
|
+
// Returns {ring, srcPos} so the caller can iterate.
|
|
42148
|
+
function collapseRingLoopsPass(ring, maxGap, srcPos, turnPrefix, maxTurn, coverage, fillFloor, clipBudget) {
|
|
42098
42149
|
var gated = !!(srcPos && turnPrefix);
|
|
42099
42150
|
var n = ring.length - 1;
|
|
42100
|
-
// The
|
|
42101
|
-
//
|
|
42151
|
+
// The coverage path defers entirely to the exact coverage check, so it needs
|
|
42152
|
+
// neither a turn gate nor a cumulative-turn prefix; the other two paths
|
|
42102
42153
|
// (source-turn, geometric) have no coverage check and keep the turn gate.
|
|
42103
|
-
var
|
|
42104
|
-
|
|
42105
|
-
//
|
|
42106
|
-
//
|
|
42107
|
-
var covIndex =
|
|
42108
|
-
|
|
42109
|
-
// UNCOVERING area, so it cannot catch a collapse that FILLS a real winding-0
|
|
42110
|
-
// hole (filling adds coverage), and its area pre-filter treats any sub-floor
|
|
42111
|
-
// loop as safe to drop. A dropped sub-loop wound OPPOSITE to the parent ring
|
|
42112
|
-
// bounds such a hole, so refuse the collapse outright -- this keeps real holes
|
|
42113
|
-
// (annulus interiors, self-crossing-line pockets) the coverage check alone
|
|
42114
|
-
// misses, at the cost of only an O(span) signed-area test on the crossings the
|
|
42115
|
-
// collapse actually evaluates (far cheaper than the old O(n*maxGap) per-pass
|
|
42116
|
-
// pre-scan). A same-wound overshoot fold that happens to WRAP a hole is caught
|
|
42117
|
-
// instead by the fill guard in the coverage check (it measures filled area).
|
|
42118
|
-
var parentCCW = coveragePath ? (ringSignedArea(ring) >= 0) : false;
|
|
42154
|
+
var ringTurn = (!gated && !coverage) ? ringAbsTurnPrefix(ring) : null;
|
|
42155
|
+
// The y-band edge index and the parent orientation are built lazily, on the
|
|
42156
|
+
// first candidate that survives to the coverage check: most rings (and most
|
|
42157
|
+
// passes) never get that far, and both are O(ring length) to compute.
|
|
42158
|
+
var covIndex = null;
|
|
42159
|
+
var parentCCW = false, parentKnown = false;
|
|
42119
42160
|
var out = [ring[0]];
|
|
42120
42161
|
var outPos = gated ? [srcPos[0]] : null;
|
|
42121
|
-
var outTags = dipTags ? [dipTags[0]] : null;
|
|
42122
42162
|
var segmentEnd = ring[1];
|
|
42123
42163
|
var segmentEndPos = gated ? srcPos[1] : 0;
|
|
42124
|
-
var segmentEndTag = dipTags ? dipTags[1] : 0;
|
|
42125
42164
|
var nextRingIndex = 2;
|
|
42126
42165
|
while (true) {
|
|
42127
42166
|
var anchor = out[out.length - 1];
|
|
@@ -42133,22 +42172,34 @@ ${svg}
|
|
|
42133
42172
|
var c = ring[s], d = ring[s + 1];
|
|
42134
42173
|
var hit = segHit(ax, ay, bx, by, c[0], c[1], d[0], d[1]);
|
|
42135
42174
|
if (!hit) continue;
|
|
42136
|
-
if (
|
|
42137
|
-
//
|
|
42138
|
-
//
|
|
42139
|
-
//
|
|
42175
|
+
if (coverage) {
|
|
42176
|
+
// Opposite-wound hole protection. The coverage check only guards against
|
|
42177
|
+
// UNCOVERING area, so it cannot catch a collapse that FILLS a real
|
|
42178
|
+
// winding-0 hole (filling adds coverage), and its area pre-filter treats
|
|
42179
|
+
// any sub-floor loop as safe to drop. A dropped sub-loop wound OPPOSITE
|
|
42180
|
+
// to the parent ring bounds such a hole, so refuse the collapse outright
|
|
42181
|
+
// -- this keeps real holes (annulus interiors, self-crossing-line
|
|
42182
|
+
// pockets) the coverage check alone misses, at the cost of only an
|
|
42183
|
+
// O(span) signed-area test on the crossings the collapse actually
|
|
42184
|
+
// evaluates. A same-wound overshoot fold that happens to WRAP a hole is
|
|
42185
|
+
// caught instead by the fill guard in the coverage check.
|
|
42186
|
+
if (!parentKnown) {
|
|
42187
|
+
parentCCW = ringSignedArea(ring) >= 0;
|
|
42188
|
+
parentKnown = true;
|
|
42189
|
+
}
|
|
42140
42190
|
if ((loopAreaSign(hit[0], hit[1], bx, by, ring, nextRingIndex, s) >= 0) !== parentCCW) {
|
|
42141
42191
|
continue;
|
|
42142
42192
|
}
|
|
42143
|
-
// No turn gate on the
|
|
42144
|
-
// It measures how much of the dropped region would become
|
|
42145
|
-
// refuses collapses that would clip a significant lobe
|
|
42146
|
-
// docs/development/buffer-line-notes.md), catching real lobes and
|
|
42147
|
-
// that a cheap turn/area/winding signal cannot separate from
|
|
42148
|
-
// The turn gate was both leaving valid interior loops
|
|
42149
|
-
// hairpins whose source turn exceeds the cap) and
|
|
42150
|
-
// deferring their removal to the dissolve.
|
|
42151
|
-
if (!
|
|
42193
|
+
// No turn gate on the coverage path: the exact coverage check is the
|
|
42194
|
+
// arbiter. It measures how much of the dropped region would become
|
|
42195
|
+
// uncovered and refuses collapses that would clip a significant lobe
|
|
42196
|
+
// (see docs/development/buffer-line-notes.md), catching real lobes and
|
|
42197
|
+
// end caps that a cheap turn/area/winding signal cannot separate from
|
|
42198
|
+
// safe folds. The turn gate was both leaving valid interior loops
|
|
42199
|
+
// uncollapsed (tight hairpins whose source turn exceeds the cap) and
|
|
42200
|
+
// slowing the pipeline by deferring their removal to the dissolve.
|
|
42201
|
+
if (!covIndex) covIndex = buildEdgeYIndex(ring, n);
|
|
42202
|
+
if (!collapseKeepsAreaCovered(ring, n, hit, segmentEnd, nextRingIndex, s, covIndex, fillFloor, clipBudget)) {
|
|
42152
42203
|
continue; // dropping this loop would uncover or fill real area -- leave it
|
|
42153
42204
|
}
|
|
42154
42205
|
} else {
|
|
@@ -42166,24 +42217,20 @@ ${svg}
|
|
|
42166
42217
|
if (crossing) {
|
|
42167
42218
|
segmentEnd = crossing;
|
|
42168
42219
|
if (gated) segmentEndPos = anchorPos;
|
|
42169
|
-
segmentEndTag = 0; // an offset crossing is not a dip vertex
|
|
42170
42220
|
nextRingIndex = crossingEndIndex;
|
|
42171
42221
|
continue;
|
|
42172
42222
|
}
|
|
42173
42223
|
out.push(segmentEnd);
|
|
42174
42224
|
if (gated) outPos.push(segmentEndPos);
|
|
42175
|
-
if (dipTags) outTags.push(segmentEndTag);
|
|
42176
42225
|
if (nextRingIndex > n - 1) break;
|
|
42177
42226
|
segmentEnd = ring[nextRingIndex];
|
|
42178
42227
|
if (gated) segmentEndPos = srcPos[nextRingIndex];
|
|
42179
|
-
if (dipTags) segmentEndTag = dipTags[nextRingIndex];
|
|
42180
42228
|
nextRingIndex++;
|
|
42181
42229
|
}
|
|
42182
|
-
if (out.length < 4) return {ring: ring, srcPos: srcPos
|
|
42230
|
+
if (out.length < 4) return {ring: ring, srcPos: srcPos}; // collapsed away; keep original
|
|
42183
42231
|
out.push(out[0].concat());
|
|
42184
42232
|
if (gated) outPos.push(outPos[0]);
|
|
42185
|
-
|
|
42186
|
-
return {ring: out, srcPos: gated ? outPos : null, dipTags: dipTags ? outTags : null};
|
|
42233
|
+
return {ring: out, srcPos: gated ? outPos : null};
|
|
42187
42234
|
}
|
|
42188
42235
|
|
|
42189
42236
|
// Cumulative absolute turn (degrees) indexed by ring vertex, EXCLUDING fold
|
|
@@ -42191,17 +42238,13 @@ ${svg}
|
|
|
42191
42238
|
// near-180-degree cusps where the offset doubles back on itself (an artifact of
|
|
42192
42239
|
// the self-crossing offset, with no corresponding source bend). A normal offset
|
|
42193
42240
|
// join's turn equals its source bend angle, so the remaining sum reconstructs
|
|
42194
|
-
// the source stretch's cumulative turn -- the
|
|
42195
|
-
//
|
|
42196
|
-
//
|
|
42197
|
-
//
|
|
42198
|
-
//
|
|
42199
|
-
// geometric turn threshold. Tag-gating is the point: a real sharp bend (e.g. a
|
|
42200
|
-
// fjord mouth, emitted as a miter, never tagged) keeps its turn and so is never
|
|
42201
|
-
// mistaken for a fold, which is what makes the geometric-only threshold
|
|
42202
|
-
// over-collapse sharp coastlines.
|
|
42241
|
+
// the source stretch's cumulative turn -- the loop-removal signal -- without
|
|
42242
|
+
// source provenance. Only the provenance-free geometric path uses this (the
|
|
42243
|
+
// clean-outline rings use the exact coverage check instead); the purely
|
|
42244
|
+
// geometric cusp threshold can over-collapse real sharp bends (e.g. a fjord
|
|
42245
|
+
// mouth), which is one reason the coverage check replaced it as the default.
|
|
42203
42246
|
var CUSP_TURN = 135;
|
|
42204
|
-
function ringAbsTurnPrefix(ring
|
|
42247
|
+
function ringAbsTurnPrefix(ring) {
|
|
42205
42248
|
var n = ring.length - 1;
|
|
42206
42249
|
var prefix = new Float64Array(n + 1);
|
|
42207
42250
|
var RAD = 180 / Math.PI;
|
|
@@ -42212,8 +42255,7 @@ ${svg}
|
|
|
42212
42255
|
var cross = e1x * e2y - e1y * e2x;
|
|
42213
42256
|
var dot = e1x * e2x + e1y * e2y;
|
|
42214
42257
|
var ang = Math.abs(Math.atan2(cross, dot)) * RAD;
|
|
42215
|
-
|
|
42216
|
-
prefix[k] = prefix[k - 1] + (isCusp ? 0 : ang);
|
|
42258
|
+
prefix[k] = prefix[k - 1] + (ang > CUSP_TURN ? 0 : ang);
|
|
42217
42259
|
}
|
|
42218
42260
|
prefix[n] = prefix[n - 1];
|
|
42219
42261
|
return prefix;
|
|
@@ -42261,12 +42303,14 @@ ${svg}
|
|
|
42261
42303
|
// The threshold is in ring units; under web Mercator the scale factor is >= 1
|
|
42262
42304
|
// everywhere, so it is an upper bound on the real m^2 area and no genuinely large
|
|
42263
42305
|
// clip is skipped regardless of latitude.
|
|
42264
|
-
function collapseKeepsAreaCovered(ring, n, X, b, nextRingIndex, s, index, fillFloor) {
|
|
42306
|
+
function collapseKeepsAreaCovered(ring, n, X, b, nextRingIndex, s, index, fillFloor, clipBudget) {
|
|
42265
42307
|
// Area pre-filter keeps the scanline off the hot path: a loop can neither clip
|
|
42266
|
-
// nor fill more than its own area, so it is safe to skip
|
|
42267
|
-
//
|
|
42268
|
-
//
|
|
42269
|
-
//
|
|
42308
|
+
// nor fill more than its own (absolute winding) area, so it is safe to skip
|
|
42309
|
+
// whenever the loop is below BOTH thresholds; collapseUncoveredArea guards the
|
|
42310
|
+
// self-crossing case where the net shoelace under-reports that area. (Filling
|
|
42311
|
+
// a winding-0 region never reaches the uncovered floor, so the fill floor must
|
|
42312
|
+
// join the skip test -- otherwise small folds that could fill a small hole
|
|
42313
|
+
// would be skipped, or every collapse would be scanned.)
|
|
42270
42314
|
var floor = fillFloor < BUFFER_LOOP_CHECK_MIN_AREA ? fillFloor : BUFFER_LOOP_CHECK_MIN_AREA;
|
|
42271
42315
|
var u = collapseUncoveredArea(ring, n, X, b, nextRingIndex, s, index, floor);
|
|
42272
42316
|
if (u < 0) return true; // loop below both floors -- too small to clip or fill
|
|
@@ -42275,33 +42319,68 @@ ${svg}
|
|
|
42275
42319
|
// fill floor scales with the buffer disk (dist^2) because a real hole's area is
|
|
42276
42320
|
// a fixed fraction of the disk while a fold sliver is orders of magnitude
|
|
42277
42321
|
// smaller relative to the radius (see BUFFER_LOOP_FILL_AREA_FRAC).
|
|
42278
|
-
|
|
42322
|
+
if (!(u < BUFFER_LOOP_CHECK_MIN_AREA && _lastFillArea < fillFloor)) return false;
|
|
42323
|
+
// Neighborhood budget (see removeBufferRingLoopsIterative): a sub-floor clip
|
|
42324
|
+
// is only accepted while its neighborhood's accumulated clips stay under the
|
|
42325
|
+
// floor. Zero-clip collapses (the common case) skip this entirely.
|
|
42326
|
+
if (u > 0 && clipBudget) {
|
|
42327
|
+
// Key on the clipped region's own centroid (accumulated by the sweep):
|
|
42328
|
+
// overlapping clips from different collapses then share a cell even when
|
|
42329
|
+
// their loops' bboxes differ by kilometers. Two dents straddling a cell
|
|
42330
|
+
// edge can still each spend a budget, so the worst-case neighborhood dent
|
|
42331
|
+
// is 2x the per-collapse floor.
|
|
42332
|
+
var cell = clipBudget.cell;
|
|
42333
|
+
var key = Math.floor(_lastClipX / cell) + ':' + Math.floor(_lastClipY / cell);
|
|
42334
|
+
var spent = clipBudget.map.get(key) || 0;
|
|
42335
|
+
if (spent + u >= BUFFER_LOOP_CHECK_MIN_AREA) return false;
|
|
42336
|
+
clipBudget.map.set(key, spent + u);
|
|
42337
|
+
}
|
|
42338
|
+
return true;
|
|
42279
42339
|
}
|
|
42280
42340
|
|
|
42281
42341
|
// Returns the area of the dropped region a collapse would leave uncovered, or -1
|
|
42282
|
-
// when the loop
|
|
42283
|
-
//
|
|
42342
|
+
// when the loop is provably too small to matter (scanline skipped). See
|
|
42343
|
+
// collapseKeepsAreaCovered for the winding rationale.
|
|
42344
|
+
//
|
|
42345
|
+
// The skip must not trust the net shoelace area alone: a SELF-CROSSING span
|
|
42346
|
+
// (figure-eight) has lobes of opposite winding whose signed areas cancel, so a
|
|
42347
|
+
// near-zero net can hide winding regions far above the floor. |net| bounds the
|
|
42348
|
+
// regions only for a simple loop; the bbox bounds them always. So the scanline
|
|
42349
|
+
// is skipped when the bbox is under the floor, or when the net is under the
|
|
42350
|
+
// floor AND the loop has no self-crossing (O(span^2) pairwise test, span <=
|
|
42351
|
+
// maxGap+2 edges -- run only in the suspicious small-net/large-bbox band).
|
|
42284
42352
|
function collapseUncoveredArea(ring, n, X, b, nextRingIndex, s, index, floor) {
|
|
42285
42353
|
var i, loopLen = s - nextRingIndex + 3; // X, b, ring[next..s]
|
|
42286
42354
|
// Loop area (shoelace over X, b, ring[next..s]) and bounding box.
|
|
42287
42355
|
var area2 = 0, px = X[0], py = X[1];
|
|
42288
42356
|
var minx = X[0], maxx = X[0], miny = X[1], maxy = X[1];
|
|
42289
|
-
|
|
42357
|
+
var qx = b[0], qy = b[1];
|
|
42358
|
+
if (qx < minx) minx = qx; else if (qx > maxx) maxx = qx;
|
|
42359
|
+
if (qy < miny) miny = qy; else if (qy > maxy) maxy = qy;
|
|
42360
|
+
area2 += px * qy - qx * py; px = qx; py = qy;
|
|
42361
|
+
for (i = nextRingIndex; i <= s; i++) {
|
|
42362
|
+
qx = ring[i][0]; qy = ring[i][1];
|
|
42363
|
+
area2 += px * qy - qx * py; px = qx; py = qy;
|
|
42290
42364
|
if (qx < minx) minx = qx; else if (qx > maxx) maxx = qx;
|
|
42291
42365
|
if (qy < miny) miny = qy; else if (qy > maxy) maxy = qy;
|
|
42292
42366
|
}
|
|
42293
|
-
bbox(b[0], b[1]);
|
|
42294
|
-
area2 += px * b[1] - b[0] * py; px = b[0]; py = b[1];
|
|
42295
|
-
for (i = nextRingIndex; i <= s; i++) {
|
|
42296
|
-
area2 += px * ring[i][1] - ring[i][0] * py; px = ring[i][0]; py = ring[i][1];
|
|
42297
|
-
bbox(ring[i][0], ring[i][1]);
|
|
42298
|
-
}
|
|
42299
42367
|
area2 += px * X[1] - X[0] * py;
|
|
42300
|
-
|
|
42301
|
-
|
|
42302
|
-
// that can cross any scanline); reuse module scratch to avoid per-call garbage.
|
|
42368
|
+
var smallNet = Math.abs(area2) / 2 < floor;
|
|
42369
|
+
if (smallNet && (maxx - minx) * (maxy - miny) < floor) return -1;
|
|
42303
42370
|
var scr = coverageScratch(n + loopLen);
|
|
42304
42371
|
var lx0 = scr.lx0, ly0 = scr.ly0, lx1 = scr.lx1, ly1 = scr.ly1;
|
|
42372
|
+
// Loop edges (X->b, b->ring[next..s], ring[s]->X), built before the band
|
|
42373
|
+
// collection so the self-cross test can run first.
|
|
42374
|
+
var lc = 0;
|
|
42375
|
+
lx0[lc] = X[0]; ly0[lc] = X[1]; lx1[lc] = b[0]; ly1[lc] = b[1]; lc++;
|
|
42376
|
+
lx0[lc] = b[0]; ly0[lc] = b[1]; lx1[lc] = ring[nextRingIndex][0]; ly1[lc] = ring[nextRingIndex][1]; lc++;
|
|
42377
|
+
for (i = nextRingIndex; i < s; i++) {
|
|
42378
|
+
lx0[lc] = ring[i][0]; ly0[lc] = ring[i][1]; lx1[lc] = ring[i + 1][0]; ly1[lc] = ring[i + 1][1]; lc++;
|
|
42379
|
+
}
|
|
42380
|
+
lx0[lc] = ring[s][0]; ly0[lc] = ring[s][1]; lx1[lc] = X[0]; ly1[lc] = X[1]; lc++;
|
|
42381
|
+
if (smallNet && !loopEdgesCross(lx0, ly0, lx1, ly1, lc)) return -1; // simple: |net| == area
|
|
42382
|
+
// Collect the ring edges whose y-range meets the loop's band (the only ones
|
|
42383
|
+
// that can cross any scanline); reuse module scratch to avoid per-call garbage.
|
|
42305
42384
|
var band = scr.band, le = 0;
|
|
42306
42385
|
var gen = ++index.gen, stamp = index.stamp, start = index.start, edges = index.edges;
|
|
42307
42386
|
var kb, klo = index.binOf(miny), khi = index.binOf(maxy);
|
|
@@ -42316,17 +42395,23 @@ ${svg}
|
|
|
42316
42395
|
band[le++] = ei;
|
|
42317
42396
|
}
|
|
42318
42397
|
}
|
|
42319
|
-
// Loop edges (X->b, b->ring[next..s], ring[s]->X).
|
|
42320
|
-
var lc = 0;
|
|
42321
|
-
lx0[lc] = X[0]; ly0[lc] = X[1]; lx1[lc] = b[0]; ly1[lc] = b[1]; lc++;
|
|
42322
|
-
lx0[lc] = b[0]; ly0[lc] = b[1]; lx1[lc] = ring[nextRingIndex][0]; ly1[lc] = ring[nextRingIndex][1]; lc++;
|
|
42323
|
-
for (i = nextRingIndex; i < s; i++) {
|
|
42324
|
-
lx0[lc] = ring[i][0]; ly0[lc] = ring[i][1]; lx1[lc] = ring[i + 1][0]; ly1[lc] = ring[i + 1][1]; lc++;
|
|
42325
|
-
}
|
|
42326
|
-
lx0[lc] = ring[s][0]; ly0[lc] = ring[s][1]; lx1[lc] = X[0]; ly1[lc] = X[1]; lc++;
|
|
42327
42398
|
return loopUncoveredArea(ring, band, le, lx0, ly0, lx1, ly1, lc, minx, maxx, miny, maxy, scr);
|
|
42328
42399
|
}
|
|
42329
42400
|
|
|
42401
|
+
// True if any two non-adjacent loop edges cross (strict interior). Adjacent
|
|
42402
|
+
// edges share an endpoint and cannot strictly cross, so they are skipped.
|
|
42403
|
+
function loopEdgesCross(lx0, ly0, lx1, ly1, lc) {
|
|
42404
|
+
for (var i = 0; i < lc - 1; i++) {
|
|
42405
|
+
for (var j = i + 2; j < lc; j++) {
|
|
42406
|
+
if (i === 0 && j === lc - 1) continue; // adjacent via closure at X
|
|
42407
|
+
if (segHit(lx0[i], ly0[i], lx1[i], ly1[i], lx0[j], ly0[j], lx1[j], ly1[j])) {
|
|
42408
|
+
return true;
|
|
42409
|
+
}
|
|
42410
|
+
}
|
|
42411
|
+
}
|
|
42412
|
+
return false;
|
|
42413
|
+
}
|
|
42414
|
+
|
|
42330
42415
|
// y-band index of ring edges: bins the ring's y-range so a scanline query at
|
|
42331
42416
|
// [miny, maxy] returns only edges reaching that band instead of scanning all n.
|
|
42332
42417
|
// start[k]..start[k+1] are indices into `edges` for bin k; `stamp`/`gen` dedup an
|
|
@@ -42383,16 +42468,24 @@ ${svg}
|
|
|
42383
42468
|
// Returns the uncovered area and writes the filled area to _lastFillArea.
|
|
42384
42469
|
// `band` lists the indices of ring edges that can reach the loop's y-band.
|
|
42385
42470
|
var _lastFillArea = 0;
|
|
42471
|
+
var _lastClipX = 0, _lastClipY = 0;
|
|
42386
42472
|
function loopUncoveredArea(ring, band, be, lx0, ly0, lx1, ly1, le, minx, maxx, miny, maxy, scr) {
|
|
42387
42473
|
var h = maxy - miny;
|
|
42388
42474
|
_lastFillArea = 0;
|
|
42389
42475
|
if (h <= 0 || maxx <= minx) return 0;
|
|
42476
|
+
// Known limitation: rows are uniform with a hard cap, so dy scales with the
|
|
42477
|
+
// loop's bbox height and a region thinner than dy in y can fall between row
|
|
42478
|
+
// midpoints unmeasured (only reachable via a loop whose bbox is much taller
|
|
42479
|
+
// than the region -- not observed outside synthetic data). A designed fix
|
|
42480
|
+
// (vertex-guided rows) was implemented, measured at 4-8% of buffer build
|
|
42481
|
+
// time, and reverted; see "Scanline row starvation" in
|
|
42482
|
+
// docs/development/buffer-line-notes.md to re-add it if it is ever needed.
|
|
42390
42483
|
var target = Math.sqrt(BUFFER_LOOP_CHECK_MIN_AREA) / 4;
|
|
42391
42484
|
var rows = Math.round(h / (target > 0 ? target : h));
|
|
42392
42485
|
if (rows < 8) rows = 8; else if (rows > 40) rows = 40;
|
|
42393
42486
|
var dy = h / rows;
|
|
42394
42487
|
var xs = scr.xs, df = scr.df, dl = scr.dl, order = scr.order;
|
|
42395
|
-
var total = 0, fill = 0, r, k, i;
|
|
42488
|
+
var total = 0, fill = 0, momX = 0, momY = 0, r, k, i;
|
|
42396
42489
|
for (r = 0; r < rows; r++) {
|
|
42397
42490
|
var y = miny + (r + 0.5) * dy;
|
|
42398
42491
|
// Winding just left of the loop's x-range (base), plus the crossings that
|
|
@@ -42421,17 +42514,24 @@ ${svg}
|
|
|
42421
42514
|
for (i = 0; i < m; i++) {
|
|
42422
42515
|
var o = order[i], xk = xs[o];
|
|
42423
42516
|
if (wl !== 0 && xk > prevx) {
|
|
42424
|
-
if (wf - wl === 0)
|
|
42517
|
+
if (wf - wl === 0) {
|
|
42518
|
+
total += xk - prevx;
|
|
42519
|
+
momX += (xk + prevx) / 2 * (xk - prevx); momY += y * (xk - prevx);
|
|
42520
|
+
}
|
|
42425
42521
|
else if (wf === 0) fill += xk - prevx;
|
|
42426
42522
|
}
|
|
42427
42523
|
wf += df[o]; wl += dl[o]; prevx = xk;
|
|
42428
42524
|
}
|
|
42429
42525
|
if (wl !== 0 && maxx > prevx) {
|
|
42430
|
-
if (wf - wl === 0)
|
|
42526
|
+
if (wf - wl === 0) {
|
|
42527
|
+
total += maxx - prevx;
|
|
42528
|
+
momX += (maxx + prevx) / 2 * (maxx - prevx); momY += y * (maxx - prevx);
|
|
42529
|
+
}
|
|
42431
42530
|
else if (wf === 0) fill += maxx - prevx;
|
|
42432
42531
|
}
|
|
42433
42532
|
}
|
|
42434
42533
|
_lastFillArea = fill * dy;
|
|
42534
|
+
if (total > 0) { _lastClipX = momX / total; _lastClipY = momY / total; }
|
|
42435
42535
|
return total * dy;
|
|
42436
42536
|
}
|
|
42437
42537
|
|
|
@@ -43151,9 +43251,9 @@ ${svg}
|
|
|
43151
43251
|
var roundJoinSegAngle = 90 / roundJoinSegsPerQuadrant;
|
|
43152
43252
|
// Max arc step (degrees) for the coarse concave bridge (makeCoarseConcaveJoin),
|
|
43153
43253
|
// the optional low-resolution alternative to makeConcaveJoin in
|
|
43154
|
-
// traceCleanOffsetSide. Larger = fewer points = faster dissolve.
|
|
43155
|
-
//
|
|
43156
|
-
//
|
|
43254
|
+
// traceCleanOffsetSide. Larger = fewer points = faster dissolve. NOTE: a
|
|
43255
|
+
// surviving (uncollapsed) bridge CAN become output boundary, where this step
|
|
43256
|
+
// sets the dent depth -- see the caution at makeCoarseConcaveJoin.
|
|
43157
43257
|
var CLEAN_OUTLINE_BRIDGE_STEP = 90;
|
|
43158
43258
|
var capStyle = opts.cap_style || 'round'; // expect 'round' or 'flat'
|
|
43159
43259
|
var pathIter = useMercator ?
|
|
@@ -44159,11 +44259,38 @@ ${svg}
|
|
|
44159
44259
|
} else {
|
|
44160
44260
|
add(p2Prev, segId, 1);
|
|
44161
44261
|
joinPoints = concaveJoin(x1, y1, bearing - 90, -joinAngle, dist);
|
|
44262
|
+
var exposedWedge = false;
|
|
44263
|
+
if (!opts.coarse_bridge) {
|
|
44264
|
+
// Exposure-gated coarse bridge (default): a dip may be emitted
|
|
44265
|
+
// coarsely only when it cannot affect the output.
|
|
44266
|
+
// 1. Exposure gate: probe the full-resolution arc + tips against
|
|
44267
|
+
// the source path (wedgeIsExposed; each probe uses its own
|
|
44268
|
+
// radius with a 0.98 margin, erring toward "exposed" = the
|
|
44269
|
+
// full arc). An uncovered arc point is potential true output
|
|
44270
|
+
// boundary -- an exposed dip that the loop remover refuses to
|
|
44271
|
+
// collapse IS the boundary there, so it must stay at full
|
|
44272
|
+
// resolution (see the caution at makeCoarseConcaveJoin).
|
|
44273
|
+
// Collapsed coarse dips legally clip up to the remover's
|
|
44274
|
+
// per-collapse floor (the chord-to-arc lens is single-covered
|
|
44275
|
+
// where only the fold's own winding spans it); the remover's
|
|
44276
|
+
// neighborhood clip budget keeps clustered dips from
|
|
44277
|
+
// compounding several such clips into one visible dent (a
|
|
44278
|
+
// 102k m^2 dent on the innerlines 2km Sabine River fold
|
|
44279
|
+
// cluster before the budget existed).
|
|
44280
|
+
if (!vertsSegIndex) vertsSegIndex = buildVertsSegmentIndex(verts);
|
|
44281
|
+
exposedWedge = wedgeIsExposed(vertsSegIndex, segId - 1, segId,
|
|
44282
|
+
x1, y1, joinPoints, p2Prev, p1);
|
|
44283
|
+
if (!exposedWedge) {
|
|
44284
|
+
joinPoints = makeCoarseConcaveJoin(x1, y1, bearing - 90, -joinAngle, dist);
|
|
44285
|
+
}
|
|
44286
|
+
}
|
|
44162
44287
|
if (useGapPatch(opts, useMercator) &&
|
|
44163
44288
|
offsetEdgesFanApart(p1Prev, p2Prev, p1, p2)) {
|
|
44164
44289
|
if (!vertsSegIndex) vertsSegIndex = buildVertsSegmentIndex(verts);
|
|
44165
|
-
if (
|
|
44166
|
-
|
|
44290
|
+
if (opts.coarse_bridge ?
|
|
44291
|
+
wedgeIsExposed(vertsSegIndex, segId - 1, segId, x1, y1,
|
|
44292
|
+
joinPoints, p2Prev, p1) :
|
|
44293
|
+
exposedWedge) {
|
|
44167
44294
|
fanApartBends.push(segId);
|
|
44168
44295
|
}
|
|
44169
44296
|
}
|
|
@@ -44367,13 +44494,21 @@ ${svg}
|
|
|
44367
44494
|
return makeRoundJoin(cx, cy, startBearing, arcAngle, dist).reverse();
|
|
44368
44495
|
}
|
|
44369
44496
|
|
|
44370
|
-
// Coarse alternative to makeConcaveJoin
|
|
44371
|
-
//
|
|
44372
|
-
//
|
|
44373
|
-
//
|
|
44374
|
-
//
|
|
44375
|
-
//
|
|
44376
|
-
//
|
|
44497
|
+
// Coarse alternative to makeConcaveJoin: bridges a concave bend with as few
|
|
44498
|
+
// as one reversed arc vertex (CLEAN_OUTLINE_BRIDGE_STEP), producing a
|
|
44499
|
+
// smaller ring for the winding dissolve to chew through. Used by default
|
|
44500
|
+
// only behind the exposure gate above (plus the loop remover's neighborhood
|
|
44501
|
+
// clip budget); opts.coarse_bridge forces it everywhere, unguarded.
|
|
44502
|
+
// CAUTION -- unsound WITHOUT those guards (2026-07-02 eval, see
|
|
44503
|
+
// "coarse-bridge" in docs/development/buffer-line-notes.md): a dip that the
|
|
44504
|
+
// loop remover refuses to collapse can be real output boundary (near-U-turn
|
|
44505
|
+
// bends whose wedge nothing else covers; single-sided polygon grows; hole
|
|
44506
|
+
// boundaries), and there the coarse chords replace the true arc. Chord
|
|
44507
|
+
// vertices stay on the radius-dist circle, so coarse geometry never falls
|
|
44508
|
+
// OUTSIDE the true buffer -- the failures are inward dents (sagitta up to
|
|
44509
|
+
// (1-cos45)*dist ~ 0.29*dist at the 90-degree step) and spurious holes
|
|
44510
|
+
// (chord-triangle slivers whose at-radius vertices defeat the artifact-hole
|
|
44511
|
+
// filter's distance classification).
|
|
44377
44512
|
function makeCoarseConcaveJoin(cx, cy, startBearing, arcAngle, dist) {
|
|
44378
44513
|
var segs = Math.min(roundJoinSegsPerQuadrant,
|
|
44379
44514
|
Math.max(1, Math.ceil(arcAngle / CLEAN_OUTLINE_BRIDGE_STEP)));
|
|
@@ -44385,6 +44520,7 @@ ${svg}
|
|
|
44385
44520
|
return points.reverse();
|
|
44386
44521
|
}
|
|
44387
44522
|
|
|
44523
|
+
|
|
44388
44524
|
// get interior vertices of an interpolated CW arc
|
|
44389
44525
|
function makeRoundJoin(cx, cy, startBearing, arcAngle, dist) {
|
|
44390
44526
|
var points = [];
|
|
@@ -58231,18 +58367,55 @@ ${svg}
|
|
|
58231
58367
|
// threshold) -- a jetty, fjord or crinkle. Otherwise restore the run's
|
|
58232
58368
|
// original vertices, so gentle stretches keep full detail for -smooth.
|
|
58233
58369
|
//
|
|
58370
|
+
// A ROUNDNESS gate protects substantial, rounded loops from both the merge pass
|
|
58371
|
+
// and the commit. Tortuosity (path length / chord) alone cannot tell a thin
|
|
58372
|
+
// needle from a round bulge -- both can be highly tortuous, and a closing chord
|
|
58373
|
+
// of length ~0 makes tortuosity infinite for either. Worst case: a closed ring
|
|
58374
|
+
// stores its start and end vertex at the same coordinate, so the seam chord
|
|
58375
|
+
// (vertex 0 -> vertex n-1) has length 0 and infinite tortuosity, which used to
|
|
58376
|
+
// make the merge pass splice out the entire ring -- destroying every island
|
|
58377
|
+
// whose perimeter fit inside the merge window, regardless of size or roundness.
|
|
58378
|
+
// The gate distinguishes them by the enclosed area: for a candidate section
|
|
58379
|
+
// closed by its chord, protect it when area / loop-perimeter >= roundness * D
|
|
58380
|
+
// (the isoperimetric area-to-perimeter ratio, biased by the detail distance). A
|
|
58381
|
+
// thin needle encloses ~0 area (never protected -> still cut); a round island
|
|
58382
|
+
// encloses a large area (protected). Because area/perimeter equals radius/2 for
|
|
58383
|
+
// a circle, the default gate protects circular features at or above the detail
|
|
58384
|
+
// resolution and preferentially removes smaller or less-round ones.
|
|
58385
|
+
//
|
|
58234
58386
|
// @xx, @yy coordinate arrays for one arc (may be typed-array subarrays).
|
|
58235
|
-
// @opts: {distance, tortuosity, spherical, weighting}
|
|
58387
|
+
// @opts: {distance, tortuosity, spherical, weighting, roundness}
|
|
58236
58388
|
// distance detail size threshold in ground units (meters when spherical): the
|
|
58237
58389
|
// longest chord the filter is allowed to create.
|
|
58238
58390
|
// tortuosity min original-length / chord ratio for a run to be cut (default 2).
|
|
58239
58391
|
// spherical measure area/length on the sphere (lng/lat -> geocentric x,y,z).
|
|
58240
58392
|
// weighting Visvalingam angle-weight coefficient (default 0.7), matching
|
|
58241
58393
|
// -simplify's weighted_visvalingam.
|
|
58394
|
+
// roundness min enclosed-area / loop-perimeter (as a fraction of the detail
|
|
58395
|
+
// distance) for a loop to be protected from removal (default 0.2);
|
|
58396
|
+
// higher removes more, 0 disables the protection.
|
|
58242
58397
|
// Returns {xx: [], yy: []}. Arc endpoints are always preserved, so shared
|
|
58243
58398
|
// topology nodes stay put and the operation is topology-safe like -simplify.
|
|
58244
58399
|
var DEFAULT_WEIGHTING = 0.7;
|
|
58245
58400
|
var DEFAULT_TORTUOSITY = 4;
|
|
58401
|
+
// Protect a candidate loop from collapse when its enclosed-area / loop-perimeter
|
|
58402
|
+
// exceeds this fraction of the detail distance D. For a circle area/perimeter =
|
|
58403
|
+
// radius/2, so 0.2 protects circular features of diameter >= ~D and drops finer
|
|
58404
|
+
// or less-round detail; a thin needle (area ~ 0) is never protected.
|
|
58405
|
+
var DEFAULT_ROUNDNESS = 0.2;
|
|
58406
|
+
// A closed ring (island, lake or hole) is dropped entirely when the filter would
|
|
58407
|
+
// leave less than this fraction of its original enclosed area. The roundness gate
|
|
58408
|
+
// stops a substantial ring from being merged away wholesale, but it is evaluated
|
|
58409
|
+
// per candidate span, so cutRun can still slice off convoluted sub-spans of the
|
|
58410
|
+
// perimeter and shrink a near-scale island to a small, distorted remnant. Once
|
|
58411
|
+
// most of the area is gone the remnant no longer faithfully represents the island
|
|
58412
|
+
// and a clean drop is better than a mangled sliver -- so if the survivors enclose
|
|
58413
|
+
// less than this share of the original area, collapse the ring to its degenerate
|
|
58414
|
+
// seam (like a fully sub-scale ring) and let the pipeline discard it. Only closed
|
|
58415
|
+
// rings are affected; open arcs and shared boundaries are never dropped, so the
|
|
58416
|
+
// operation stays topology-safe. Set to 0 to disable. The default 0.6 keeps a
|
|
58417
|
+
// ring only while the filter removes at most ~40% of its area.
|
|
58418
|
+
var DEFAULT_MIN_RING_AREA = 0.6;
|
|
58246
58419
|
// How far (in detail-distances of arc length) the survivor-merge pass looks ahead
|
|
58247
58420
|
// for a chord that closes a convoluted excursion. Bounds the pass to O(n) and
|
|
58248
58421
|
// caps how long a thin spike it can slice in one merge.
|
|
@@ -58256,7 +58429,7 @@ ${svg}
|
|
|
58256
58429
|
|
|
58257
58430
|
function collapseArcDetail(xx, yy, opts) {
|
|
58258
58431
|
var n = xx.length;
|
|
58259
|
-
var outX = [], outY = [];
|
|
58432
|
+
var outX = [], outY = [], outIdx = [];
|
|
58260
58433
|
var D = opts.distance;
|
|
58261
58434
|
if (n < 3 || !(D > 0)) {
|
|
58262
58435
|
for (var p = 0; p < n; p++) { outX.push(xx[p]); outY.push(yy[p]); }
|
|
@@ -58265,6 +58438,12 @@ ${svg}
|
|
|
58265
58438
|
var spherical = !!opts.spherical;
|
|
58266
58439
|
var k = opts.weighting >= 0 ? opts.weighting : DEFAULT_WEIGHTING;
|
|
58267
58440
|
var T = opts.tortuosity > 0 ? opts.tortuosity : DEFAULT_TORTUOSITY;
|
|
58441
|
+
// roundness >= 0; 0 disables the roundness protection (legacy behavior).
|
|
58442
|
+
var R = opts.roundness >= 0 ? opts.roundness : DEFAULT_ROUNDNESS;
|
|
58443
|
+
// minRingArea >= 0; 0 disables the drop-shredded-ring gate.
|
|
58444
|
+
var minRingArea = opts.minRingArea >= 0 ? opts.minRingArea : DEFAULT_MIN_RING_AREA;
|
|
58445
|
+
// A closed ring stores its start and end vertex at the same coordinate.
|
|
58446
|
+
var isRing = n >= 4 && xx[0] === xx[n - 1] && yy[0] === yy[n - 1];
|
|
58268
58447
|
var Dsq = D * D;
|
|
58269
58448
|
|
|
58270
58449
|
// Metric coordinates: geocentric x,y,z on a sphere for lng/lat input, plain
|
|
@@ -58342,6 +58521,52 @@ ${svg}
|
|
|
58342
58521
|
var cumLen = new Float64Array(n);
|
|
58343
58522
|
for (var q = 1; q < n; q++) cumLen[q] = cumLen[q - 1] + dist(q - 1, q);
|
|
58344
58523
|
|
|
58524
|
+
// Cumulative cross products of successive position vectors (in a frame
|
|
58525
|
+
// translated to vertex 0, to keep magnitudes small and the subtraction
|
|
58526
|
+
// precise) give the enclosed area of any section i..j closed by its chord in
|
|
58527
|
+
// O(1). The vector-area form (0.5 * |sum of edge cross products + closing
|
|
58528
|
+
// term|) is origin-independent for a closed loop, so it works unchanged for
|
|
58529
|
+
// the geocentric 3D coords used in spherical mode and the plain 2D coords
|
|
58530
|
+
// otherwise. Only used by the roundness gate below.
|
|
58531
|
+
var ox = mx[0], oy = my[0], oz = spherical ? mz[0] : 0;
|
|
58532
|
+
var cumCz = new Float64Array(n), cumCx, cumCy;
|
|
58533
|
+
if (spherical) { cumCx = new Float64Array(n); cumCy = new Float64Array(n); }
|
|
58534
|
+
for (var t = 1; t < n; t++) {
|
|
58535
|
+
var ax = mx[t - 1] - ox, ay = my[t - 1] - oy, bx = mx[t] - ox, by = my[t] - oy;
|
|
58536
|
+
if (spherical) {
|
|
58537
|
+
var az = mz[t - 1] - oz, bz = mz[t] - oz;
|
|
58538
|
+
cumCx[t] = cumCx[t - 1] + (ay * bz - az * by);
|
|
58539
|
+
cumCy[t] = cumCy[t - 1] + (az * bx - ax * bz);
|
|
58540
|
+
cumCz[t] = cumCz[t - 1] + (ax * by - ay * bx);
|
|
58541
|
+
} else {
|
|
58542
|
+
cumCz[t] = cumCz[t - 1] + (ax * by - ay * bx);
|
|
58543
|
+
}
|
|
58544
|
+
}
|
|
58545
|
+
|
|
58546
|
+
// Area enclosed by the section vertices i..j closed by the chord j->i.
|
|
58547
|
+
function sectionArea(i, j) {
|
|
58548
|
+
var aix = mx[i] - ox, aiy = my[i] - oy, ajx = mx[j] - ox, ajy = my[j] - oy;
|
|
58549
|
+
if (spherical) {
|
|
58550
|
+
var aiz = mz[i] - oz, ajz = mz[j] - oz;
|
|
58551
|
+
var vx = (cumCx[j] - cumCx[i]) + (ajy * aiz - ajz * aiy);
|
|
58552
|
+
var vy = (cumCy[j] - cumCy[i]) + (ajz * aix - ajx * aiz);
|
|
58553
|
+
var vz = (cumCz[j] - cumCz[i]) + (ajx * aiy - ajy * aix);
|
|
58554
|
+
return 0.5 * Math.sqrt(vx * vx + vy * vy + vz * vz);
|
|
58555
|
+
}
|
|
58556
|
+
return 0.5 * Math.abs((cumCz[j] - cumCz[i]) + (ajx * aiy - ajy * aix));
|
|
58557
|
+
}
|
|
58558
|
+
|
|
58559
|
+
// A candidate section (i..j) is protected from collapse when the loop it forms
|
|
58560
|
+
// with its closing chord is a substantial, rounded feature: enclosed area per
|
|
58561
|
+
// unit loop-perimeter reaches the roundness fraction of the detail distance.
|
|
58562
|
+
// A thin needle encloses ~0 area (area/perimeter ~ 0) and is never protected;
|
|
58563
|
+
// a round bulge or island encloses enough area to clear the gate.
|
|
58564
|
+
function isProtected(i, j) {
|
|
58565
|
+
if (!(R > 0)) return false;
|
|
58566
|
+
var perim = (cumLen[j] - cumLen[i]) + dist(i, j);
|
|
58567
|
+
return perim > 0 && sectionArea(i, j) / perim >= R * D;
|
|
58568
|
+
}
|
|
58569
|
+
|
|
58345
58570
|
function cutRun(a, e) {
|
|
58346
58571
|
// Emit the kept vertices in (a, e]; a has already been emitted. Walk from a
|
|
58347
58572
|
// and, at each step, find the span [i, j] (chord <= D) with the highest
|
|
@@ -58358,7 +58583,8 @@ ${svg}
|
|
|
58358
58583
|
var c = dist(i, j);
|
|
58359
58584
|
if (c > D) continue; // never create a chord longer than the detail scale
|
|
58360
58585
|
var tort = c > 0 ? (cumLen[j] - cumLen[i]) / c : Infinity;
|
|
58361
|
-
|
|
58586
|
+
// roundness gate: keep substantial, rounded bulges at full detail
|
|
58587
|
+
if (tort > bestTort && !isProtected(i, j)) {
|
|
58362
58588
|
bestTort = tort;
|
|
58363
58589
|
bestJ = j;
|
|
58364
58590
|
}
|
|
@@ -58366,10 +58592,12 @@ ${svg}
|
|
|
58366
58592
|
if (bestJ >= 0) {
|
|
58367
58593
|
outX.push(xx[bestJ]);
|
|
58368
58594
|
outY.push(yy[bestJ]);
|
|
58595
|
+
outIdx.push(bestJ);
|
|
58369
58596
|
i = bestJ;
|
|
58370
58597
|
} else {
|
|
58371
58598
|
outX.push(xx[i + 1]);
|
|
58372
58599
|
outY.push(yy[i + 1]);
|
|
58600
|
+
outIdx.push(i + 1);
|
|
58373
58601
|
i++;
|
|
58374
58602
|
}
|
|
58375
58603
|
}
|
|
@@ -58399,7 +58627,9 @@ ${svg}
|
|
|
58399
58627
|
if (chordSq(s, u) <= mergeChordSq) {
|
|
58400
58628
|
var ud = dist(s, u);
|
|
58401
58629
|
var utort = ud > 0 ? (cumLen[u] - cumLen[s]) / ud : Infinity;
|
|
58402
|
-
|
|
58630
|
+
// roundness gate: a rounded loop (e.g. a closed ring closing on its own
|
|
58631
|
+
// zero-length seam) is a real feature, not a needle -- never merge it.
|
|
58632
|
+
if (utort > mergeTort && !isProtected(s, u)) {
|
|
58403
58633
|
mergeTort = utort;
|
|
58404
58634
|
mergeJ = u;
|
|
58405
58635
|
}
|
|
@@ -58418,13 +58648,49 @@ ${svg}
|
|
|
58418
58648
|
|
|
58419
58649
|
outX.push(xx[0]);
|
|
58420
58650
|
outY.push(yy[0]);
|
|
58651
|
+
outIdx.push(0);
|
|
58421
58652
|
var a = 0;
|
|
58422
58653
|
while (a !== n - 1) {
|
|
58423
58654
|
var e = next[a];
|
|
58424
58655
|
cutRun(a, e);
|
|
58425
58656
|
a = e;
|
|
58426
58657
|
}
|
|
58658
|
+
|
|
58659
|
+
// Drop a closed ring that the filter has shredded to a small remnant: if the
|
|
58660
|
+
// survivors enclose less than minRingArea of the original ring's area, collapse
|
|
58661
|
+
// it to its degenerate seam so the pipeline discards it (a clean drop rather
|
|
58662
|
+
// than a distorted sliver). Uses the survivors' metric coordinates for the
|
|
58663
|
+
// filtered area and the whole-ring section area for the original.
|
|
58664
|
+
if (isRing && minRingArea > 0) {
|
|
58665
|
+
var origArea = sectionArea(0, n - 1);
|
|
58666
|
+
if (origArea > 0 && ringAreaByIdx(outIdx) < minRingArea * origArea) {
|
|
58667
|
+
return {xx: [xx[0], xx[n - 1]], yy: [yy[0], yy[n - 1]]};
|
|
58668
|
+
}
|
|
58669
|
+
}
|
|
58427
58670
|
return {xx: outX, yy: outY};
|
|
58671
|
+
|
|
58672
|
+
// Enclosed area of the closed ring formed by the survivor vertices (given as
|
|
58673
|
+
// indices into the original arc), measured in the same metric space as
|
|
58674
|
+
// sectionArea via the origin-independent vector-area form.
|
|
58675
|
+
function ringAreaByIdx(idx) {
|
|
58676
|
+
var m = idx.length;
|
|
58677
|
+
if (m < 4) return 0;
|
|
58678
|
+
var o0x = mx[idx[0]], o0y = my[idx[0]], o0z = spherical ? mz[idx[0]] : 0;
|
|
58679
|
+
var vx = 0, vy = 0, vz = 0;
|
|
58680
|
+
for (var t = 1; t < m; t++) {
|
|
58681
|
+
var p = idx[t - 1], q = idx[t];
|
|
58682
|
+
var ax = mx[p] - o0x, ay = my[p] - o0y, bx = mx[q] - o0x, by = my[q] - o0y;
|
|
58683
|
+
if (spherical) {
|
|
58684
|
+
var az = mz[p] - o0z, bz = mz[q] - o0z;
|
|
58685
|
+
vx += ay * bz - az * by;
|
|
58686
|
+
vy += az * bx - ax * bz;
|
|
58687
|
+
vz += ax * by - ay * bx;
|
|
58688
|
+
} else {
|
|
58689
|
+
vz += ax * by - ay * bx;
|
|
58690
|
+
}
|
|
58691
|
+
}
|
|
58692
|
+
return spherical ? 0.5 * Math.sqrt(vx * vx + vy * vy + vz * vz) : 0.5 * Math.abs(vz);
|
|
58693
|
+
}
|
|
58428
58694
|
}
|
|
58429
58695
|
|
|
58430
58696
|
// Optional preprocessing step before -smooth: collapse intricate sub-scale
|
|
@@ -58455,6 +58721,8 @@ ${svg}
|
|
|
58455
58721
|
distance: distance,
|
|
58456
58722
|
tortuosity: opts.tortuosity,
|
|
58457
58723
|
weighting: opts.weighting,
|
|
58724
|
+
roundness: opts.roundness,
|
|
58725
|
+
minRingArea: opts.min_area,
|
|
58458
58726
|
spherical: spherical
|
|
58459
58727
|
});
|
|
58460
58728
|
var removed = before - arcs.getPointCount();
|
|
@@ -58478,6 +58746,8 @@ ${svg}
|
|
|
58478
58746
|
distance: opts.distance,
|
|
58479
58747
|
tortuosity: opts.tortuosity,
|
|
58480
58748
|
weighting: opts.weighting,
|
|
58749
|
+
roundness: opts.roundness,
|
|
58750
|
+
minRingArea: opts.minRingArea,
|
|
58481
58751
|
spherical: opts.spherical
|
|
58482
58752
|
});
|
|
58483
58753
|
nn.push(res.xx.length);
|
|
@@ -63320,7 +63590,7 @@ ${svg}
|
|
|
63320
63590
|
});
|
|
63321
63591
|
|
|
63322
63592
|
// Structural-corner detection for -smooth's corner preservation (on by default;
|
|
63323
|
-
// disabled with no-corners
|
|
63593
|
+
// disabled with no-corners).
|
|
63324
63594
|
//
|
|
63325
63595
|
// Many boundaries alternate between natural, freely-curving stretches (coast,
|
|
63326
63596
|
// river centerline) and artificial straight-line segments (state/county
|
|
@@ -63354,21 +63624,112 @@ ${svg}
|
|
|
63354
63624
|
var INNER_WINDOW_FACTOR = 0.4; // concentration probe window = tangentWindow * this
|
|
63355
63625
|
var CORNER_CONCENTRATION = 0.6; // min ratio of inner-window turn to full-window turn
|
|
63356
63626
|
var MIN_RUN_LEN_FACTOR = 1.0; // a structural run must be at least tol * this long
|
|
63357
|
-
|
|
63358
|
-
|
|
63359
|
-
//
|
|
63360
|
-
//
|
|
63361
|
-
//
|
|
63627
|
+
// ...and bend no tighter than radius tol * this. This is the curvature gate for
|
|
63628
|
+
// "structural" (straight or slowly-curving, e.g. a surveyed border or graticule
|
|
63629
|
+
// arc). It must be well above 1: at factor 1 a minimal run may turn a full
|
|
63630
|
+
// radian (~57 deg) over its own length, so ordinary coastal arcs qualify and
|
|
63631
|
+
// their end bends get pinned as spurious corners (radius ~1-1.6*tol). At 3 a
|
|
63632
|
+
// minimal run turns <= ~19 deg, excluding natural coastal curvature while still
|
|
63633
|
+
// admitting genuinely straight borders (radius ~infinite) and graticule arcs
|
|
63634
|
+
// (radius >> tol).
|
|
63635
|
+
var MIN_RUN_RADIUS_FACTOR = 3.0;
|
|
63636
|
+
|
|
63637
|
+
// Straightness gate used to decide corner *retention* (whether a detected corner
|
|
63638
|
+
// borders a run worth pinning), as distinct from isStructuralRun's per-vertex
|
|
63639
|
+
// gate that decides whether a span is copied verbatim. A run is "straight at the
|
|
63640
|
+
// smoothing scale" if every vertex stays within a thin corridor around the
|
|
63641
|
+
// straight chord joining the run's endpoints: max perpendicular deviation <=
|
|
63642
|
+
// STRAIGHT_DEV_FACTOR * chord length. Because this measures deviation from the
|
|
63643
|
+
// chord rather than summing raw per-segment turning, it is robust to
|
|
63644
|
+
// sub-tolerance digitizing wiggle: a finely ragged but geometrically straight
|
|
63645
|
+
// border (huge total per-vertex turning, tiny deviation) qualifies, so its
|
|
63646
|
+
// bounding corners are kept -- while isStructuralRun would (correctly, for its
|
|
63647
|
+
// own purpose) reject it as too wiggly to copy verbatim. The ratio behaves like
|
|
63648
|
+
// a minimum-radius-over-length gate: a run bending with radius R over length L
|
|
63649
|
+
// deviates from its chord by ~L/(8R), so the threshold corresponds to
|
|
63650
|
+
// R >~ L/(8*STRAIGHT_DEV_FACTOR) -- longer runs must be proportionally straighter
|
|
63651
|
+
// to count, which matches intuition (a 28 km stretch bending at radius 4 km is
|
|
63652
|
+
// obviously not straight). Genuinely curving coastline bows far from its chord
|
|
63653
|
+
// and is still rejected, so spurious corners inside wiggly stretches keep getting
|
|
63654
|
+
// culled.
|
|
63655
|
+
var STRAIGHT_DEV_FACTOR = 0.03;
|
|
63656
|
+
|
|
63657
|
+
// Angle coupling for corner retention: how much sharper the corner must turn than
|
|
63658
|
+
// the run it borders already curves. A run that passes the chord test may still
|
|
63659
|
+
// 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.03 admits a
|
|
63661
|
+
// run that curves ~14 deg over its length. Pinning a *gentle* bend at the end of
|
|
63662
|
+
// such a run is unsafe: the "corner" is barely sharper than the run's own
|
|
63663
|
+
// curving, so it is really a point on a smooth bend, not a junction. (This is the
|
|
63664
|
+
// failure mode on coarsely sampled / already-simplified coastlines, where a
|
|
63665
|
+
// gently curving stretch is sampled as a few long segments that read as a
|
|
63666
|
+
// borderline-straight run with soft bends at each end.) So the straightness limit
|
|
63667
|
+
// for retention is tightened for gentle corners: a corner is pinnable only if its
|
|
63668
|
+
// turn is at least PIN_TURN_RATIO times the run's own bend, i.e.
|
|
63669
|
+
// turn >= PIN_TURN_RATIO * (8 * dev) <=> dev <= turn / (8 * PIN_TURN_RATIO).
|
|
63670
|
+
// retentionDevLimit() returns the smaller of STRAIGHT_DEV_FACTOR and
|
|
63671
|
+
// turn/(8*PIN_TURN_RATIO), so the coupling only bites for gentle corners (below
|
|
63672
|
+
// ~2*STRAIGHT_DEV_FACTOR*PIN_TURN_RATIO ~ 69 deg); sharp corners (surveyed-border
|
|
63673
|
+
// right angles, spits, hairpins) keep the full base tolerance, unchanged.
|
|
63674
|
+
var PIN_TURN_RATIO = 5;
|
|
63675
|
+
|
|
63676
|
+
// Minimum length (in tol units) a straight run must have to justify *pinning* a
|
|
63677
|
+
// bordering corner. This is deliberately larger than MIN_RUN_LEN_FACTOR (the
|
|
63678
|
+
// floor for calling a span "structural" at all): pinning a corner is a stronger
|
|
63679
|
+
// commitment than copying a clean span, so it demands stronger evidence that the
|
|
63680
|
+
// run is a deliberate straight feature rather than incidental collinearity.
|
|
63681
|
+
//
|
|
63682
|
+
// The failure mode this guards against appears on sparse / already-simplified
|
|
63683
|
+
// data, where a gently curving coastline is sampled as a few long segments. A
|
|
63684
|
+
// short near-collinear stretch only ~1*tol long (often just 1-2 segments) then
|
|
63685
|
+
// passes the chord-deviation test -- with so few interior points there is almost
|
|
63686
|
+
// nothing to deviate -- and gets pinned, kinking an otherwise smooth curve. At
|
|
63687
|
+
// the smoothing scale such a stretch is indistinguishable from a coarsely
|
|
63688
|
+
// sampled bend, so it should not anchor a corner. Requiring the run to be
|
|
63689
|
+
// clearly longer than the smoothing distance (factor 2) drops these stubs while
|
|
63690
|
+
// keeping genuine straight borders (which run many times the distance) and even
|
|
63691
|
+
// coarsely sampled but truly long straight segments (e.g. a 2-3*tol contour
|
|
63692
|
+
// edge). Scales with corner-bias (via ctol), so a positive bias restores the
|
|
63693
|
+
// old 1*tol behaviour for users who want shorter runs pinned.
|
|
63694
|
+
var MIN_PIN_RUN_LEN_FACTOR = 2.0;
|
|
63695
|
+
|
|
63696
|
+
// Convert the user-facing corner-bias (0 = neutral) into the positive multiplier
|
|
63697
|
+
// k applied to corner-detection resolution (ctol = tol / k). The mapping is
|
|
63698
|
+
// symmetric about zero -- k(+b) * k(-b) = 1 -- and smooth there (both branches
|
|
63699
|
+
// have slope 1 at b = 0), so opposite biases of equal magnitude are exact
|
|
63700
|
+
// inverses. A positive bias makes detection finer (k > 1, ctol < tol: more, more
|
|
63701
|
+
// finely supported corners); a negative bias makes it coarser (k < 1, ctol > tol:
|
|
63702
|
+
// fewer corners), each as if the smoothing distance were tol/k. Examples: +1
|
|
63703
|
+
// doubles the resolution (k=2, "as if distance were halved"), -1 halves it
|
|
63704
|
+
// (k=1/2, "as if doubled"); +2 -> k=3, -2 -> k=1/3.
|
|
63705
|
+
function cornerBiasScale(cornerBias) {
|
|
63706
|
+
var b = cornerBias || 0;
|
|
63707
|
+
return b >= 0 ? b + 1 : 1 / (1 - b);
|
|
63708
|
+
}
|
|
63709
|
+
|
|
63710
|
+
// @cornerBias (optional, default 0 = neutral) scales only the distance-
|
|
63711
|
+
// proportional corner parameters, by dividing the tolerance they key off
|
|
63712
|
+
// (ctol = tol / k, k = cornerBiasScale(bias)). The dimensionless thresholds are
|
|
63713
|
+
// left untouched: the corner angle, the concentration ratio, and -- downstream,
|
|
63714
|
+
// inside isStraightRun / retentionDevLimit -- STRAIGHT_DEV_FACTOR and
|
|
63715
|
+
// PIN_TURN_RATIO. So corner-bias detects (and retains) corners exactly as if the
|
|
63716
|
+
// smoothing distance were tol/k, while the smoothing kernel keeps using the real
|
|
63717
|
+
// distance. In particular `-smooth corner-bias=-1 1km` gives the same corner
|
|
63718
|
+
// results as `-smooth 2km` (a negative bias finds fewer, only well-supported
|
|
63719
|
+
// corners; a positive bias finds more), but smooths at 1km. All lengths below are
|
|
63720
|
+
// derived from ctol; only cornerAngle and concentration (both dimensionless) stay
|
|
63721
|
+
// fixed.
|
|
63362
63722
|
function getCornerParams(tol, cornerBias) {
|
|
63363
|
-
var
|
|
63723
|
+
var ctol = tol / cornerBiasScale(cornerBias);
|
|
63364
63724
|
return {
|
|
63365
63725
|
tol: tol,
|
|
63366
63726
|
cornerAngle: CORNER_ANGLE,
|
|
63367
|
-
tangentWindow: TANGENT_WINDOW_FACTOR *
|
|
63368
|
-
innerWindow: INNER_WINDOW_FACTOR * TANGENT_WINDOW_FACTOR *
|
|
63727
|
+
tangentWindow: TANGENT_WINDOW_FACTOR * ctol,
|
|
63728
|
+
innerWindow: INNER_WINDOW_FACTOR * TANGENT_WINDOW_FACTOR * ctol,
|
|
63369
63729
|
concentration: CORNER_CONCENTRATION,
|
|
63370
|
-
minRunLen: MIN_RUN_LEN_FACTOR *
|
|
63371
|
-
|
|
63730
|
+
minRunLen: MIN_RUN_LEN_FACTOR * ctol,
|
|
63731
|
+
minPinRunLen: MIN_PIN_RUN_LEN_FACTOR * ctol,
|
|
63732
|
+
maxTurnRate: 1 / (MIN_RUN_RADIUS_FACTOR * ctol) // radians of turning per ground unit
|
|
63372
63733
|
};
|
|
63373
63734
|
}
|
|
63374
63735
|
|
|
@@ -63420,6 +63781,122 @@ ${svg}
|
|
|
63420
63781
|
return totalTurn / len <= params.maxTurnRate;
|
|
63421
63782
|
}
|
|
63422
63783
|
|
|
63784
|
+
// Cyclic form of isStructuralRun for a closed ring: the span runs forward from
|
|
63785
|
+
// ring vertex @a to ring vertex @b over the m = n-1 unique vertices, wrapping
|
|
63786
|
+
// when b <= a (a == b means the whole ring). Length and turning are measured
|
|
63787
|
+
// cyclically. Used to decide whether a detected ring corner borders a genuine
|
|
63788
|
+
// straight/low-curvature run before it is pinned (see smoothArcCoords).
|
|
63789
|
+
function isStructuralRingSpan(t, channels, n, a, b, params) {
|
|
63790
|
+
var m = n - 1;
|
|
63791
|
+
if (m < 2) return false;
|
|
63792
|
+
var L = t[n - 1];
|
|
63793
|
+
var len = b > a ? t[b] - t[a] : (L - t[a]) + t[b];
|
|
63794
|
+
if (!(len >= params.minRunLen)) return false;
|
|
63795
|
+
var K = channels.length;
|
|
63796
|
+
var totalTurn = 0;
|
|
63797
|
+
var i = a;
|
|
63798
|
+
while (true) {
|
|
63799
|
+
i = (i + 1) % m;
|
|
63800
|
+
if (i === b) break;
|
|
63801
|
+
totalTurn += ringVertexTurn(channels, K, m, i);
|
|
63802
|
+
if (totalTurn / len > params.maxTurnRate) return false;
|
|
63803
|
+
}
|
|
63804
|
+
return totalTurn / len <= params.maxTurnRate;
|
|
63805
|
+
}
|
|
63806
|
+
|
|
63807
|
+
// Is span [a, b] (inclusive vertex indices, a < b, open frame) "straight at the
|
|
63808
|
+
// smoothing scale": clearly longer than the smoothing distance (>= minPinRunLen,
|
|
63809
|
+
// see MIN_PIN_RUN_LEN_FACTOR) AND confined to a thin corridor around its endpoint
|
|
63810
|
+
// chord (see STRAIGHT_DEV_FACTOR)? Used to decide whether a detected corner
|
|
63811
|
+
// borders a straight run worth pinning. Unlike isStructuralRun -- which sums raw
|
|
63812
|
+
// per-segment turning and is therefore defeated by sub-tolerance digitizing
|
|
63813
|
+
// noise -- this measures perpendicular deviation from the chord, so a finely
|
|
63814
|
+
// ragged but geometrically straight border still qualifies. The length floor is
|
|
63815
|
+
// the pinning-specific minPinRunLen (not minRunLen): a run only ~1*tol long has
|
|
63816
|
+
// too few interior points for the chord test to distinguish a true straight
|
|
63817
|
+
// border from a coarsely sampled bend, so it must not anchor a corner. @devLimit
|
|
63818
|
+
// overrides the corridor half-width (default STRAIGHT_DEV_FACTOR); retention
|
|
63819
|
+
// passes a per-corner value tightened for gentle bends (see retentionDevLimit).
|
|
63820
|
+
function isStraightRun(t, channels, a, b, params, devLimit) {
|
|
63821
|
+
var lim = devLimit === undefined ? STRAIGHT_DEV_FACTOR : devLimit;
|
|
63822
|
+
var len = t[b] - t[a];
|
|
63823
|
+
if (!(len >= params.minPinRunLen)) return false;
|
|
63824
|
+
var K = channels.length;
|
|
63825
|
+
var A = getPt(channels, K, a);
|
|
63826
|
+
var AB = subv(getPt(channels, K, b), A, K);
|
|
63827
|
+
var abDot = dot(AB, AB, K);
|
|
63828
|
+
if (!(abDot > 0)) return false;
|
|
63829
|
+
var limit2 = lim * lim * abDot;
|
|
63830
|
+
for (var i = a + 1; i < b; i++) {
|
|
63831
|
+
if (perpDistSq(channels, K, i, A, AB, abDot) > limit2) return false;
|
|
63832
|
+
}
|
|
63833
|
+
return true;
|
|
63834
|
+
}
|
|
63835
|
+
|
|
63836
|
+
// Cyclic form of isStraightRun for a closed ring: the span runs forward from ring
|
|
63837
|
+
// vertex @a to ring vertex @b over the m = n-1 unique vertices, wrapping when
|
|
63838
|
+
// b <= a. A whole-ring span (a == b, the single-corner case) has no meaningful
|
|
63839
|
+
// chord, so it falls back to the turning-rate test (a large low-curvature ring
|
|
63840
|
+
// keeps its one corner). Used by the closed-ring corner cull (see
|
|
63841
|
+
// filterRingCornersByStructure in mapshaper-smooth-algos).
|
|
63842
|
+
function isStraightRingSpan(t, channels, n, a, b, params, devLimit) {
|
|
63843
|
+
var lim = devLimit === undefined ? STRAIGHT_DEV_FACTOR : devLimit;
|
|
63844
|
+
var m = n - 1;
|
|
63845
|
+
if (m < 2) return false;
|
|
63846
|
+
if (a === b) return isStructuralRingSpan(t, channels, n, a, b, params);
|
|
63847
|
+
var L = t[n - 1];
|
|
63848
|
+
var len = b > a ? t[b] - t[a] : (L - t[a]) + t[b];
|
|
63849
|
+
if (!(len >= params.minPinRunLen)) return false;
|
|
63850
|
+
var K = channels.length;
|
|
63851
|
+
var A = getPt(channels, K, a);
|
|
63852
|
+
var AB = subv(getPt(channels, K, b), A, K);
|
|
63853
|
+
var abDot = dot(AB, AB, K);
|
|
63854
|
+
if (!(abDot > 0)) return false;
|
|
63855
|
+
var limit2 = lim * lim * abDot;
|
|
63856
|
+
var i = a;
|
|
63857
|
+
while (true) {
|
|
63858
|
+
i = (i + 1) % m;
|
|
63859
|
+
if (i === b) break;
|
|
63860
|
+
if (perpDistSq(channels, K, i, A, AB, abDot) > limit2) return false;
|
|
63861
|
+
}
|
|
63862
|
+
return true;
|
|
63863
|
+
}
|
|
63864
|
+
|
|
63865
|
+
// Straightness limit for pinning a corner whose windowed turn is @turnRad (see
|
|
63866
|
+
// PIN_TURN_RATIO): min(STRAIGHT_DEV_FACTOR, turnRad / (8 * PIN_TURN_RATIO)).
|
|
63867
|
+
function retentionDevLimit(turnRad) {
|
|
63868
|
+
var lim = turnRad / (8 * PIN_TURN_RATIO);
|
|
63869
|
+
return lim < STRAIGHT_DEV_FACTOR ? lim : STRAIGHT_DEV_FACTOR;
|
|
63870
|
+
}
|
|
63871
|
+
|
|
63872
|
+
// Windowed turn (radians) at vertex @i, over params.tangentWindow each side --
|
|
63873
|
+
// the same measure findInteriorCorners uses to flag the corner. @cyclic selects
|
|
63874
|
+
// the open or ring frame.
|
|
63875
|
+
function cornerTurn(t, channels, n, i, cyclic, params) {
|
|
63876
|
+
var K = channels.length;
|
|
63877
|
+
var L = t[n - 1];
|
|
63878
|
+
var m = cyclic ? n - 1 : n;
|
|
63879
|
+
var segLen = cyclic ? ringSegLengths(t, m) : null;
|
|
63880
|
+
return windowedTurn(t, channels, K, n, L, m, segLen, i, params.tangentWindow, cyclic);
|
|
63881
|
+
}
|
|
63882
|
+
|
|
63883
|
+
// Does the open span [a, b] justify pinning the corner at vertex @corner: is it a
|
|
63884
|
+
// straight run (isStraightRun) whose straightness is enough for the corner's turn
|
|
63885
|
+
// angle (retentionDevLimit)? A gentle bend needs a straighter run than a sharp
|
|
63886
|
+
// one. Used by refineBounds.
|
|
63887
|
+
function bordersStraightRun(t, channels, n, corner, a, b, params) {
|
|
63888
|
+
var lim = retentionDevLimit(cornerTurn(t, channels, n, corner, false, params));
|
|
63889
|
+
return isStraightRun(t, channels, a, b, params, lim);
|
|
63890
|
+
}
|
|
63891
|
+
|
|
63892
|
+
// Ring analogue of bordersStraightRun, for the closed-ring corner cull
|
|
63893
|
+
// (filterRingCornersByStructure). @corner is a ring vertex; the span runs from
|
|
63894
|
+
// ring vertex @a to @b (cyclic when b <= a).
|
|
63895
|
+
function bordersStraightRingSpan(t, channels, n, corner, a, b, params) {
|
|
63896
|
+
var lim = retentionDevLimit(cornerTurn(t, channels, n, corner, true, params));
|
|
63897
|
+
return isStraightRingSpan(t, channels, n, a, b, params, lim);
|
|
63898
|
+
}
|
|
63899
|
+
|
|
63423
63900
|
// --- internals ---
|
|
63424
63901
|
|
|
63425
63902
|
function ringSegLengths(t, m) {
|
|
@@ -63448,6 +63925,14 @@ ${svg}
|
|
|
63448
63925
|
return angleBetween(subv(pi, pp, K), subv(pn, pi, K), K);
|
|
63449
63926
|
}
|
|
63450
63927
|
|
|
63928
|
+
// Local turn at ring vertex i using cyclic neighbours over m unique vertices.
|
|
63929
|
+
function ringVertexTurn(channels, K, m, i) {
|
|
63930
|
+
var pi = getPt(channels, K, i);
|
|
63931
|
+
var pp = getPt(channels, K, (i - 1 + m) % m);
|
|
63932
|
+
var pn = getPt(channels, K, (i + 1) % m);
|
|
63933
|
+
return angleBetween(subv(pi, pp, K), subv(pn, pi, K), K);
|
|
63934
|
+
}
|
|
63935
|
+
|
|
63451
63936
|
// Walk from vertex i in direction dir (+1/-1) until accumulated arc length
|
|
63452
63937
|
// reaches W (or a boundary, for open arcs), returning the reached vertex index.
|
|
63453
63938
|
function reach(t, segLen, n, m, L, i, dir, W, cyclic) {
|
|
@@ -63492,6 +63977,25 @@ ${svg}
|
|
|
63492
63977
|
return o;
|
|
63493
63978
|
}
|
|
63494
63979
|
|
|
63980
|
+
function dot(a, b, K) {
|
|
63981
|
+
var d = 0;
|
|
63982
|
+
for (var c = 0; c < K; c++) d += a[c] * b[c];
|
|
63983
|
+
return d;
|
|
63984
|
+
}
|
|
63985
|
+
|
|
63986
|
+
// Squared perpendicular distance of vertex @i from the line through point @A
|
|
63987
|
+
// with direction @AB (abDot = AB.AB). = |AP|^2 - (AP.AB)^2 / |AB|^2.
|
|
63988
|
+
function perpDistSq(channels, K, i, A, AB, abDot) {
|
|
63989
|
+
var apAp = 0, apAb = 0, d;
|
|
63990
|
+
for (var c = 0; c < K; c++) {
|
|
63991
|
+
d = channels[c][i] - A[c];
|
|
63992
|
+
apAp += d * d;
|
|
63993
|
+
apAb += d * AB[c];
|
|
63994
|
+
}
|
|
63995
|
+
var perp = apAp - apAb * apAb / abDot;
|
|
63996
|
+
return perp > 0 ? perp : 0;
|
|
63997
|
+
}
|
|
63998
|
+
|
|
63495
63999
|
function angleBetween(u, v, K) {
|
|
63496
64000
|
var d = 0, nu = 0, nv = 0;
|
|
63497
64001
|
for (var c = 0; c < K; c++) {
|
|
@@ -63510,13 +64014,15 @@ ${svg}
|
|
|
63510
64014
|
// Scale-aware line smoothing primitives, shared by the -smooth command.
|
|
63511
64015
|
//
|
|
63512
64016
|
// The smoother treats a path as coordinate signals parameterized by arc length s
|
|
63513
|
-
// and applies a length-scaled low-pass filter. The user-facing distance is
|
|
63514
|
-
//
|
|
63515
|
-
//
|
|
63516
|
-
//
|
|
63517
|
-
//
|
|
63518
|
-
//
|
|
63519
|
-
//
|
|
64017
|
+
// and applies a length-scaled low-pass filter. The user-facing distance is
|
|
64018
|
+
// calibrated so that it approximates the maximum displacement of the smoothed
|
|
64019
|
+
// line from the original at high-displacement features (e.g. acute bends): finer
|
|
64020
|
+
// detail is removed and larger features pass through progressively less changed.
|
|
64021
|
+
// In frequency terms the kernel's half-amplitude (-6 dB) wavelength sits at
|
|
64022
|
+
// roughly KERNEL_STRENGTH * distance, so detail a few times finer than the
|
|
64023
|
+
// distance is strongly attenuated. The distance is not a strict deviation
|
|
64024
|
+
// bound -- a tall, narrow sub-resolution spike can still be displaced by an
|
|
64025
|
+
// amount comparable to its own amplitude (inherent to convolution smoothers).
|
|
63520
64026
|
//
|
|
63521
64027
|
// The filter is a local second-degree polynomial fit whose quadratic term
|
|
63522
64028
|
// corrects the inward shrinkage that plain weighted averaging causes on curved
|
|
@@ -63536,21 +64042,44 @@ ${svg}
|
|
|
63536
64042
|
// -simplify treats spherical coordinates. The kernel scale stays in true ground
|
|
63537
64043
|
// distance because arc length is measured with great-circle distance.
|
|
63538
64044
|
//
|
|
63539
|
-
//
|
|
63540
|
-
//
|
|
63541
|
-
//
|
|
63542
|
-
// the
|
|
63543
|
-
//
|
|
63544
|
-
//
|
|
63545
|
-
//
|
|
63546
|
-
//
|
|
64045
|
+
// KERNEL_FROM_DISTANCE maps the user distance onto the internal reference scale
|
|
64046
|
+
// (tol) that keys corner detection, output sampling and densification. The kernel
|
|
64047
|
+
// itself is then widened by KERNEL_STRENGTH (below), so the distance approximates
|
|
64048
|
+
// the maximum displacement at sharp features and the -6 dB wavelength sits near
|
|
64049
|
+
// KERNEL_STRENGTH * distance. The remaining calibration constants are expressed
|
|
64050
|
+
// relative to the internal scale and map it onto kernel widths and output
|
|
64051
|
+
// sampling; they are collected here so the mapping can be retuned in one place.
|
|
64052
|
+
// See docs/reference.md.
|
|
63547
64053
|
var KERNEL_FROM_DISTANCE = 1.2; // internal kernel scale = distance * this
|
|
64054
|
+
// Base smoothing strength baked into the default: the low-pass kernel scale is
|
|
64055
|
+
// this * tol (before the user's `strength` multiplier and the ring cap). It is
|
|
64056
|
+
// calibrated so the distance parameter approximates the maximum displacement of
|
|
64057
|
+
// the smoothed line from the original at high-displacement features (e.g. acute
|
|
64058
|
+
// bends) -- a markedly stronger, more intuitive effect than the raw -6 dB
|
|
64059
|
+
// mapping (which displaced the line far less than the distance). ONLY the kernel
|
|
64060
|
+
// scale is affected; tol -- and therefore corner detection, output sampling, the
|
|
64061
|
+
// prefilter and island dropping -- stays keyed to the raw distance.
|
|
64062
|
+
var KERNEL_STRENGTH = 5;
|
|
63548
64063
|
var GAUSSIAN_SIGMA_FACTOR = 0.4; // gaussian sigma = internal scale * this
|
|
63549
64064
|
var PAEK_SCALE_FACTOR = 0.4; // exponential kernel scale d = internal scale * this
|
|
63550
64065
|
var WINDOW_RADIUS_FACTOR = 1.2; // window half-length = internal scale * this
|
|
63551
64066
|
var SOURCE_SPACING_FACTOR = 0.25; // densify source to <= tolerance * this before smoothing
|
|
63552
64067
|
var MAX_OUTPUT_FACTOR = 8; // cap output (and source) vertices at inputCount * this
|
|
63553
64068
|
var MIN_CLOSED_SEGMENTS = 16; // floor on segments for closed rings (so they resolve)
|
|
64069
|
+
// A closed ring cannot be smoothed at a resolution coarser than the ring itself:
|
|
64070
|
+
// once the kernel window (radius = internal scale * WINDOW_RADIUS_FACTOR)
|
|
64071
|
+
// reaches half the ring's perimeter, every output point averages over the whole
|
|
64072
|
+
// loop and the ring degenerates toward a point (a circle once re-inflated). So
|
|
64073
|
+
// for a closed ring the internal scale is capped just below that threshold,
|
|
64074
|
+
// which is factor 1/(2*WINDOW_RADIUS_FACTOR) ~ 0.42. Up to the threshold the
|
|
64075
|
+
// ring keeps its shape (elongated stays elongated, and detail is rounded as much
|
|
64076
|
+
// as the ring's own size allows); the enclosed area it loses to curve-shortening
|
|
64077
|
+
// on the way is restored afterward by restoreRingArea() (a similarity rescale
|
|
64078
|
+
// about the centroid), so a small island is rounded at close to the full
|
|
64079
|
+
// requested scale without shrinking. The cap only binds when the requested
|
|
64080
|
+
// distance nears the ring's own size; large rings (perimeter >> distance) smooth
|
|
64081
|
+
// gently, lose negligible area and are effectively unaffected by either step.
|
|
64082
|
+
var MAX_RING_SCALE_FACTOR = 0.42;
|
|
63554
64083
|
|
|
63555
64084
|
// Output resampling. The smoothed curve is a continuous function of arc length;
|
|
63556
64085
|
// we sample it densely at a uniform step and then thin that dense polyline with a
|
|
@@ -63613,12 +64142,29 @@ ${svg}
|
|
|
63613
64142
|
return deg * Math.PI / 180;
|
|
63614
64143
|
}
|
|
63615
64144
|
|
|
63616
|
-
// Resolve the
|
|
63617
|
-
//
|
|
63618
|
-
//
|
|
64145
|
+
// Resolve the corner-detection bias (default 0 = neutral). This is the raw
|
|
64146
|
+
// user-facing value; getCornerParams / cornerBiasScale convert it into the
|
|
64147
|
+
// multiplier on detection resolution. A positive bias keeps more corners, a
|
|
64148
|
+
// negative bias fewer. Missing/null falls back to neutral.
|
|
63619
64149
|
function resolveCornerBias(opts) {
|
|
63620
64150
|
var b = opts.cornerBias;
|
|
63621
|
-
return b
|
|
64151
|
+
return (b === undefined || b === null) ? 0 : b;
|
|
64152
|
+
}
|
|
64153
|
+
|
|
64154
|
+
// Resolve the smoothing-strength multiplier (default 1). It scales only the
|
|
64155
|
+
// low-pass kernel (window radius and sigma) relative to the distance, so a value
|
|
64156
|
+
// > 1 smooths more strongly (wider kernel, larger divergence from the original)
|
|
64157
|
+
// and < 1 more gently. Everything else keyed to the distance -- corner detection,
|
|
64158
|
+
// output sampling, the prefilter and island dropping -- is left unchanged.
|
|
64159
|
+
// Non-positive or missing values fall back to 1.
|
|
64160
|
+
//
|
|
64161
|
+
// By design, curve exaggeration (gain > 1) scales WITH strength: gain multiplies
|
|
64162
|
+
// the quadratic curvature correction (a0 - mean) in smoothPoint, and that term is
|
|
64163
|
+
// measured over the strength-scaled kernel window, so a wider kernel amplifies a
|
|
64164
|
+
// given gain. This coupling is intentional -- do not normalize it out.
|
|
64165
|
+
function resolveStrength(opts) {
|
|
64166
|
+
var s = opts.strength;
|
|
64167
|
+
return s > 0 ? s : 1;
|
|
63622
64168
|
}
|
|
63623
64169
|
|
|
63624
64170
|
function smoothArcCoords(xx, yy, opts) {
|
|
@@ -63634,6 +64180,29 @@ ${svg}
|
|
|
63634
64180
|
var spherical = !!opts.spherical;
|
|
63635
64181
|
var keepCorners = !!opts.keepCorners;
|
|
63636
64182
|
var bendAngle = resolveBendAngle(opts);
|
|
64183
|
+
|
|
64184
|
+
// Cumulative arc length in ground units (meters for spherical data), so the
|
|
64185
|
+
// kernel scale stays in true distance regardless of coordinate representation.
|
|
64186
|
+
var t = arcLengths(origX, origY, n, spherical);
|
|
64187
|
+
if (!(t[n - 1] > 0)) {
|
|
64188
|
+
return {xx: origX, yy: origY}; // degenerate (coincident points)
|
|
64189
|
+
}
|
|
64190
|
+
// The low-pass kernel scale is the raw distance scale (tol) times the baked-in
|
|
64191
|
+
// KERNEL_STRENGTH calibration and the user's `strength` multiplier (default 1).
|
|
64192
|
+
// Only the kernel (radius, sigma) uses this scale; tol -- which drives corner
|
|
64193
|
+
// detection, output sampling and densification -- stays keyed to the raw
|
|
64194
|
+
// distance, so those effects are unaffected by either strength factor.
|
|
64195
|
+
var kernelScale = tol * KERNEL_STRENGTH * resolveStrength(opts);
|
|
64196
|
+
// A closed ring smaller than the smoothing resolution would collapse toward its
|
|
64197
|
+
// centroid, so cap both scales at a fraction of the ring's perimeter (see
|
|
64198
|
+
// MAX_RING_SCALE_FACTOR). This only binds when the requested distance (or the
|
|
64199
|
+
// boosted kernel) approaches the ring's own size; otherwise it is a no-op. The
|
|
64200
|
+
// cap on kernelScale also stops a large `strength` from collapsing a ring.
|
|
64201
|
+
if (closed) {
|
|
64202
|
+
var ringCap = MAX_RING_SCALE_FACTOR * t[n - 1];
|
|
64203
|
+
tol = Math.min(tol, ringCap);
|
|
64204
|
+
kernelScale = Math.min(kernelScale, ringCap);
|
|
64205
|
+
}
|
|
63637
64206
|
var ctx = {
|
|
63638
64207
|
tol: tol,
|
|
63639
64208
|
method: method,
|
|
@@ -63646,21 +64215,25 @@ ${svg}
|
|
|
63646
64215
|
// segment still turns well under the threshold; never coarsen it beyond the
|
|
63647
64216
|
// default (the angle filter alone thins the output for larger angles).
|
|
63648
64217
|
denseStep: tol * DENSE_STEP_FACTOR * Math.min(1, bendAngle / DEFAULT_BEND_ANGLE),
|
|
63649
|
-
radius:
|
|
63650
|
-
scale: (method == 'gaussian' ? GAUSSIAN_SIGMA_FACTOR : PAEK_SCALE_FACTOR) *
|
|
64218
|
+
radius: kernelScale * WINDOW_RADIUS_FACTOR,
|
|
64219
|
+
scale: (method == 'gaussian' ? GAUSSIAN_SIGMA_FACTOR : PAEK_SCALE_FACTOR) * kernelScale
|
|
63651
64220
|
};
|
|
63652
|
-
|
|
63653
|
-
// Cumulative arc length in ground units (meters for spherical data), so the
|
|
63654
|
-
// kernel scale stays in true distance regardless of coordinate representation.
|
|
63655
|
-
var t = arcLengths(origX, origY, n, spherical);
|
|
63656
|
-
if (!(t[n - 1] > 0)) {
|
|
63657
|
-
return {xx: origX, yy: origY}; // degenerate (coincident points)
|
|
63658
|
-
}
|
|
63659
64221
|
var channels = spherical ? lngLatToXYZChannels(origX, origY, n) : [origX, origY];
|
|
63660
64222
|
|
|
63661
64223
|
if (closed) {
|
|
64224
|
+
var ringParams = getCornerParams(tol, ctx.cornerBias);
|
|
63662
64225
|
var corners = keepCorners ?
|
|
63663
|
-
findInteriorCorners(t, channels, n, true,
|
|
64226
|
+
findInteriorCorners(t, channels, n, true, ringParams) : [];
|
|
64227
|
+
// findInteriorCorners flags localized bends by angle alone; it does not check
|
|
64228
|
+
// whether a candidate borders a structural (long, low-curvature) run. Keep
|
|
64229
|
+
// only the corners that do -- a natural ring with no straight segments has
|
|
64230
|
+
// none and must smooth cyclically. Otherwise the ring would be rotated to
|
|
64231
|
+
// corners[0] and smoothed as an open path with that vertex pinned as a
|
|
64232
|
+
// spurious cusp (whose location shifts with the tolerance-scaled detection
|
|
64233
|
+
// window), even though refineBounds later drops every interior breakpoint.
|
|
64234
|
+
if (corners.length > 0) {
|
|
64235
|
+
corners = filterRingCornersByStructure(t, channels, n, corners, ringParams);
|
|
64236
|
+
}
|
|
63664
64237
|
if (corners.length === 0) {
|
|
63665
64238
|
return smoothClosedCyclic(t, channels, n, ctx);
|
|
63666
64239
|
}
|
|
@@ -63682,9 +64255,18 @@ ${svg}
|
|
|
63682
64255
|
}
|
|
63683
64256
|
|
|
63684
64257
|
// Smooth an open path partitioned at @interiorBreaks (sorted interior vertex
|
|
63685
|
-
// indices).
|
|
63686
|
-
//
|
|
63687
|
-
// its
|
|
64258
|
+
// indices). Corner retention and verbatim-copy are two separate decisions:
|
|
64259
|
+
// - A breakpoint is kept only if it borders a straight run that is straight
|
|
64260
|
+
// enough for its turn angle (bordersStraightRun -- deviation from the endpoint
|
|
64261
|
+
// chord, robust to sub-tolerance wiggle, and tightened for gentle bends so a
|
|
64262
|
+
// soft bend on a borderline-straight run is not pinned); otherwise
|
|
64263
|
+
// refineBounds drops it.
|
|
64264
|
+
// - A kept span is copied verbatim only if it is clean per-vertex
|
|
64265
|
+
// (isStructuralRun); otherwise it is smoothed with its endpoints pinned. So a
|
|
64266
|
+
// straight-but-noisy border is smoothed into a clean straight line between
|
|
64267
|
+
// its pinned corners, rather than curving into its neighbours.
|
|
64268
|
+
// Every breakpoint (and the two arc endpoints) keeps its exact original position;
|
|
64269
|
+
// shared breakpoint vertices are emitted once.
|
|
63688
64270
|
function smoothOpenSpans(origX, origY, t, channels, n, interiorBreaks, ctx) {
|
|
63689
64271
|
var bounds = [0].concat(interiorBreaks);
|
|
63690
64272
|
bounds.push(n - 1);
|
|
@@ -63704,17 +64286,28 @@ ${svg}
|
|
|
63704
64286
|
return {xx: xx, yy: yy};
|
|
63705
64287
|
}
|
|
63706
64288
|
|
|
63707
|
-
// Drop interior breakpoints that don't border any
|
|
63708
|
-
// inside a wiggly stretch
|
|
63709
|
-
//
|
|
63710
|
-
//
|
|
64289
|
+
// Drop interior breakpoints that don't border any pinnable straight run (e.g.
|
|
64290
|
+
// spikes inside a wiggly stretch, or -- crucially on sparse/simplified data --
|
|
64291
|
+
// points sampled along a gentle curve), merging their spans, until the partition
|
|
64292
|
+
// is stable. Merging can turn two short pieces back into one straight run, so the
|
|
64293
|
+
// test is repeated each pass. A breakpoint is kept only if an adjacent span is
|
|
64294
|
+
// straight at the smoothing scale AND straight enough for the breakpoint's own
|
|
64295
|
+
// turn angle (bordersStraightRun): deviation from the endpoint chord, tightened
|
|
64296
|
+
// for gentle corners so a soft bend on a borderline-straight run is not pinned.
|
|
64297
|
+
// The older per-vertex turning gate (isStructuralRun) is deliberately NOT used
|
|
64298
|
+
// for retention -- it admits any run bending no tighter than radius
|
|
64299
|
+
// MIN_RUN_RADIUS_FACTOR*tol, i.e. gentle curves, which on coarsely-sampled data
|
|
64300
|
+
// produces spurious corners along smooth bends. (isStructuralRun still governs
|
|
64301
|
+
// verbatim-copy of a kept span; see smoothOpenSpans.) The corner for both
|
|
64302
|
+
// adjacent spans is the breakpoint itself, so its turn angle gates each side.
|
|
63711
64303
|
function refineBounds(t, channels, bounds, params) {
|
|
64304
|
+
var n = channels[0].length;
|
|
63712
64305
|
var changed = true;
|
|
63713
64306
|
while (changed && bounds.length > 2) {
|
|
63714
64307
|
changed = false;
|
|
63715
64308
|
for (var i = 1; i < bounds.length - 1; i++) {
|
|
63716
|
-
var leftStruct =
|
|
63717
|
-
var rightStruct =
|
|
64309
|
+
var leftStruct = bordersStraightRun(t, channels, n, bounds[i], bounds[i - 1], bounds[i], params);
|
|
64310
|
+
var rightStruct = bordersStraightRun(t, channels, n, bounds[i], bounds[i], bounds[i + 1], params);
|
|
63718
64311
|
if (!leftStruct && !rightStruct) {
|
|
63719
64312
|
bounds.splice(i, 1);
|
|
63720
64313
|
changed = true;
|
|
@@ -63725,6 +64318,39 @@ ${svg}
|
|
|
63725
64318
|
return bounds;
|
|
63726
64319
|
}
|
|
63727
64320
|
|
|
64321
|
+
// Drop closed-ring corners that don't border a run worth pinning on either side,
|
|
64322
|
+
// merging their (cyclic) spans, until the set is stable -- the cyclic analogue
|
|
64323
|
+
// of refineBounds, applied before the ring is rotated/pinned. A single corner
|
|
64324
|
+
// is tested against the whole-ring span. Uses the same angle-coupled
|
|
64325
|
+
// chord-straightness criterion as refineBounds (see bordersStraightRingSpan).
|
|
64326
|
+
// Returns the surviving corners (a subset of @corners, order preserved); an empty
|
|
64327
|
+
// result means the ring has no qualifying corner and should smooth cyclically.
|
|
64328
|
+
function filterRingCornersByStructure(t, channels, n, corners, params) {
|
|
64329
|
+
var list = corners.slice();
|
|
64330
|
+
var changed = true;
|
|
64331
|
+
while (changed && list.length > 0) {
|
|
64332
|
+
changed = false;
|
|
64333
|
+
for (var i = 0; i < list.length; i++) {
|
|
64334
|
+
var cur = list[i];
|
|
64335
|
+
var leftStruct, rightStruct;
|
|
64336
|
+
if (list.length === 1) {
|
|
64337
|
+
leftStruct = rightStruct = bordersStraightRingSpan(t, channels, n, cur, cur, cur, params);
|
|
64338
|
+
} else {
|
|
64339
|
+
var prev = list[(i - 1 + list.length) % list.length];
|
|
64340
|
+
var next = list[(i + 1) % list.length];
|
|
64341
|
+
leftStruct = bordersStraightRingSpan(t, channels, n, cur, prev, cur, params);
|
|
64342
|
+
rightStruct = bordersStraightRingSpan(t, channels, n, cur, cur, next, params);
|
|
64343
|
+
}
|
|
64344
|
+
if (!leftStruct && !rightStruct) {
|
|
64345
|
+
list.splice(i, 1);
|
|
64346
|
+
changed = true;
|
|
64347
|
+
break;
|
|
64348
|
+
}
|
|
64349
|
+
}
|
|
64350
|
+
}
|
|
64351
|
+
return list;
|
|
64352
|
+
}
|
|
64353
|
+
|
|
63728
64354
|
// Smooth a single open span [lo, hi] (inclusive) and pin both ends to their
|
|
63729
64355
|
// original coordinates. Reuses the whole-arc smoothing pipeline on the sub-arc.
|
|
63730
64356
|
function smoothSpanOpen(origX, origY, t, channels, lo, hi, ctx) {
|
|
@@ -63756,6 +64382,10 @@ ${svg}
|
|
|
63756
64382
|
var dense = densifyChannels(t, channels, maxSpacing);
|
|
63757
64383
|
var src = buildSource(dense.t, dense.channels, true, ctx.radius, L);
|
|
63758
64384
|
var sm = sampleSmoothedCurve(src, 0, L, true, ctx, n);
|
|
64385
|
+
// Smoothing shrinks a closed loop (curve-shortening); restore its original
|
|
64386
|
+
// enclosed area so small rings can be rounded at the full scale without
|
|
64387
|
+
// shrinking. A no-op for large rings (they lose negligible area).
|
|
64388
|
+
restoreRingArea(sm, channels, n, ctx.spherical);
|
|
63759
64389
|
var out = ctx.spherical ? xyzChannelsToLngLat(sm) : {xx: sm[0], yy: sm[1]};
|
|
63760
64390
|
// force an exactly closed ring (the periodic endpoints are equal up to fp)
|
|
63761
64391
|
out.xx[out.xx.length - 1] = out.xx[0];
|
|
@@ -63763,6 +64393,98 @@ ${svg}
|
|
|
63763
64393
|
return out;
|
|
63764
64394
|
}
|
|
63765
64395
|
|
|
64396
|
+
// Rescale a smoothed closed ring about its centroid so it re-encloses the
|
|
64397
|
+
// original ring's area. Because it is a uniform similarity transform, the
|
|
64398
|
+
// smoothed *shape* is unchanged -- only its size -- so the rounding introduced by
|
|
64399
|
+
// smoothing is preserved while the curve-shortening shrinkage is undone.
|
|
64400
|
+
// @sm are the smoothed smoothing channels (plain arrays; [x,y] planar or unit-
|
|
64401
|
+
// sphere [X,Y,Z] spherical, last point == first). @orig are the original channels
|
|
64402
|
+
// (length @n, closed). Silent no-op if either area is non-positive.
|
|
64403
|
+
function restoreRingArea(sm, orig, n, spherical) {
|
|
64404
|
+
var origArea = ringChannelArea(orig, n, spherical);
|
|
64405
|
+
var m = sm[0].length;
|
|
64406
|
+
var smArea = ringChannelArea(sm, m, spherical);
|
|
64407
|
+
if (!(origArea > 0) || !(smArea > 0)) return;
|
|
64408
|
+
var f = Math.sqrt(origArea / smArea);
|
|
64409
|
+
if (spherical) {
|
|
64410
|
+
scaleRingSpherical(sm, m, f);
|
|
64411
|
+
} else {
|
|
64412
|
+
scaleRingPlanar(sm, m, f);
|
|
64413
|
+
}
|
|
64414
|
+
}
|
|
64415
|
+
|
|
64416
|
+
// Enclosed-area proxy of a closed ring (@count points, last == first). Planar:
|
|
64417
|
+
// the shoelace area on (x,y). Spherical: the shoelace area of the ring projected
|
|
64418
|
+
// into the tangent plane at its centroid direction. Only ratios of two such
|
|
64419
|
+
// areas are used, so the (unit-sphere) scale is irrelevant, and the tangent-plane
|
|
64420
|
+
// error is second order in the ring's size -- negligible for the small rings
|
|
64421
|
+
// where this is needed.
|
|
64422
|
+
function ringChannelArea(ch, count, spherical) {
|
|
64423
|
+
if (!spherical) {
|
|
64424
|
+
var x = ch[0], y = ch[1], a = 0;
|
|
64425
|
+
for (var i = 0; i < count - 1; i++) a += x[i] * y[i + 1] - x[i + 1] * y[i];
|
|
64426
|
+
return Math.abs(a / 2);
|
|
64427
|
+
}
|
|
64428
|
+
var basis = tangentBasis(ringCentroidDir(ch, count - 1));
|
|
64429
|
+
var ex = basis.ex, ey = basis.ey;
|
|
64430
|
+
var X = ch[0], Y = ch[1], Z = ch[2], area = 0, px, py, qx, qy;
|
|
64431
|
+
for (var j = 0; j < count - 1; j++) {
|
|
64432
|
+
px = X[j] * ex[0] + Y[j] * ex[1] + Z[j] * ex[2];
|
|
64433
|
+
py = X[j] * ey[0] + Y[j] * ey[1] + Z[j] * ey[2];
|
|
64434
|
+
qx = X[j + 1] * ex[0] + Y[j + 1] * ex[1] + Z[j + 1] * ex[2];
|
|
64435
|
+
qy = X[j + 1] * ey[0] + Y[j + 1] * ey[1] + Z[j + 1] * ey[2];
|
|
64436
|
+
area += px * qy - qx * py;
|
|
64437
|
+
}
|
|
64438
|
+
return Math.abs(area / 2);
|
|
64439
|
+
}
|
|
64440
|
+
|
|
64441
|
+
function scaleRingPlanar(sm, count, f) {
|
|
64442
|
+
var x = sm[0], y = sm[1], cx = 0, cy = 0, i;
|
|
64443
|
+
for (i = 0; i < count - 1; i++) { cx += x[i]; cy += y[i]; }
|
|
64444
|
+
cx /= (count - 1); cy /= (count - 1);
|
|
64445
|
+
for (i = 0; i < count; i++) {
|
|
64446
|
+
x[i] = cx + (x[i] - cx) * f;
|
|
64447
|
+
y[i] = cy + (y[i] - cy) * f;
|
|
64448
|
+
}
|
|
64449
|
+
}
|
|
64450
|
+
|
|
64451
|
+
// Scale each unit-sphere point's angular offset from the centroid direction by
|
|
64452
|
+
// ~f (keeping the radial component, then renormalizing), which scales the
|
|
64453
|
+
// enclosed area by ~f^2 for the small caps where this runs.
|
|
64454
|
+
function scaleRingSpherical(sm, count, f) {
|
|
64455
|
+
var X = sm[0], Y = sm[1], Z = sm[2];
|
|
64456
|
+
var c = ringCentroidDir(sm, count - 1);
|
|
64457
|
+
for (var i = 0; i < count; i++) {
|
|
64458
|
+
var dot = X[i] * c[0] + Y[i] * c[1] + Z[i] * c[2];
|
|
64459
|
+
var tx = X[i] - dot * c[0], ty = Y[i] - dot * c[1], tz = Z[i] - dot * c[2];
|
|
64460
|
+
var vx = dot * c[0] + f * tx, vy = dot * c[1] + f * ty, vz = dot * c[2] + f * tz;
|
|
64461
|
+
var nrm = Math.sqrt(vx * vx + vy * vy + vz * vz) || 1;
|
|
64462
|
+
X[i] = vx / nrm; Y[i] = vy / nrm; Z[i] = vz / nrm;
|
|
64463
|
+
}
|
|
64464
|
+
}
|
|
64465
|
+
|
|
64466
|
+
function ringCentroidDir(ch, m) {
|
|
64467
|
+
var X = ch[0], Y = ch[1], Z = ch[2], cx = 0, cy = 0, cz = 0;
|
|
64468
|
+
for (var i = 0; i < m; i++) { cx += X[i]; cy += Y[i]; cz += Z[i]; }
|
|
64469
|
+
var nrm = Math.sqrt(cx * cx + cy * cy + cz * cz) || 1;
|
|
64470
|
+
return [cx / nrm, cy / nrm, cz / nrm];
|
|
64471
|
+
}
|
|
64472
|
+
|
|
64473
|
+
// Orthonormal tangent basis (ex, ey) at unit direction c on the sphere.
|
|
64474
|
+
function tangentBasis(c) {
|
|
64475
|
+
// pick the world axis least aligned with c to avoid a degenerate cross product
|
|
64476
|
+
var ax = Math.abs(c[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0];
|
|
64477
|
+
var ex = cross(c, ax);
|
|
64478
|
+
var en = Math.sqrt(ex[0] * ex[0] + ex[1] * ex[1] + ex[2] * ex[2]) || 1;
|
|
64479
|
+
ex = [ex[0] / en, ex[1] / en, ex[2] / en];
|
|
64480
|
+
var ey = cross(c, ex);
|
|
64481
|
+
return {c: c, ex: ex, ey: ey};
|
|
64482
|
+
}
|
|
64483
|
+
|
|
64484
|
+
function cross(a, b) {
|
|
64485
|
+
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
64486
|
+
}
|
|
64487
|
+
|
|
63766
64488
|
function copySpan(origX, origY, lo, hi) {
|
|
63767
64489
|
var xx = [], yy = [];
|
|
63768
64490
|
for (var i = lo; i <= hi; i++) {
|
|
@@ -64202,11 +64924,12 @@ ${svg}
|
|
|
64202
64924
|
stop$1('Expected prefilter-gate to be a number > 0');
|
|
64203
64925
|
}
|
|
64204
64926
|
if (opts.corner_bias !== undefined && opts.corner_bias !== null &&
|
|
64205
|
-
|
|
64206
|
-
stop$1('Expected corner-bias to be a number
|
|
64927
|
+
typeof opts.corner_bias != 'number') {
|
|
64928
|
+
stop$1('Expected corner-bias to be a number');
|
|
64207
64929
|
}
|
|
64208
|
-
// Corner preservation is on by default; no-corners
|
|
64209
|
-
|
|
64930
|
+
// Corner preservation is on by default; no-corners turns it off. (corner-bias
|
|
64931
|
+
// only tunes sensitivity: 0 is neutral, not off.)
|
|
64932
|
+
var keepCorners = !opts.no_corners;
|
|
64210
64933
|
var implicitlySmoothedNames = getImplicitlyTargetedLayerNames(dataset, targetLayers, layerHasPaths);
|
|
64211
64934
|
|
|
64212
64935
|
// Smoothing rewrites coordinates, so lock in any pending (non-destructive)
|
|
@@ -64225,6 +64948,8 @@ ${svg}
|
|
|
64225
64948
|
filterDetailPaths(arcs, {
|
|
64226
64949
|
distance: tolerance,
|
|
64227
64950
|
tortuosity: opts.prefilter_gate,
|
|
64951
|
+
roundness: opts.prefilter_roundness,
|
|
64952
|
+
minRingArea: opts.prefilter_min_area,
|
|
64228
64953
|
spherical: spherical
|
|
64229
64954
|
});
|
|
64230
64955
|
var removed = before - arcs.getPointCount();
|
|
@@ -64240,6 +64965,7 @@ ${svg}
|
|
|
64240
64965
|
keepCorners: keepCorners,
|
|
64241
64966
|
cornerBias: opts.corner_bias,
|
|
64242
64967
|
gain: opts.gain,
|
|
64968
|
+
strength: opts.strength,
|
|
64243
64969
|
maxBendAngle: opts.max_bend_angle
|
|
64244
64970
|
});
|
|
64245
64971
|
|
|
@@ -64268,6 +64994,7 @@ ${svg}
|
|
|
64268
64994
|
keepCorners: opts.keepCorners,
|
|
64269
64995
|
cornerBias: opts.cornerBias,
|
|
64270
64996
|
gain: opts.gain,
|
|
64997
|
+
strength: opts.strength,
|
|
64271
64998
|
maxBendAngle: opts.maxBendAngle,
|
|
64272
64999
|
closed: arcs.arcIsClosed(arcId)
|
|
64273
65000
|
});
|
|
@@ -66085,7 +66812,7 @@ ${svg}
|
|
|
66085
66812
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
66086
66813
|
}
|
|
66087
66814
|
|
|
66088
|
-
var version = "0.7.
|
|
66815
|
+
var version = "0.7.36";
|
|
66089
66816
|
|
|
66090
66817
|
// Parse command line args into commands and run them
|
|
66091
66818
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|