mapshaper 0.6.109 → 0.6.111

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 CHANGED
@@ -1328,13 +1328,21 @@
1328
1328
 
1329
1329
  function verbose() {
1330
1330
  // verbose can be set globally with the -verbose command or separately for each command
1331
- if (getStashedVar('VERBOSE')) {
1331
+ if (useVerbose()) {
1332
1332
  message.apply(null, arguments);
1333
1333
  }
1334
1334
  }
1335
1335
 
1336
+ function useVerbose() {
1337
+ return getStashedVar('VERBOSE');
1338
+ }
1339
+
1340
+ function useDebug() {
1341
+ return getStashedVar('DEBUG');
1342
+ }
1343
+
1336
1344
  function debug() {
1337
- if (getStashedVar('DEBUG')) {
1345
+ if (useDebug()) {
1338
1346
  logArgs(arguments);
1339
1347
  }
1340
1348
  }
@@ -1458,6 +1466,8 @@
1458
1466
  setLoggingFunctions: setLoggingFunctions,
1459
1467
  stop: stop,
1460
1468
  truncateString: truncateString,
1469
+ useDebug: useDebug,
1470
+ useVerbose: useVerbose,
1461
1471
  verbose: verbose,
1462
1472
  warn: warn
1463
1473
  });
@@ -2265,44 +2275,6 @@
2265
2275
  return dist * R$1;
2266
2276
  }
2267
2277
 
2268
- // TODO: make this safe for small angles
2269
- function innerAngle(ax, ay, bx, by, cx, cy) {
2270
- var ab = distance2D(ax, ay, bx, by),
2271
- bc = distance2D(bx, by, cx, cy),
2272
- theta, dotp;
2273
- if (ab === 0 || bc === 0) {
2274
- theta = 0;
2275
- } else {
2276
- dotp = ((ax - bx) * (cx - bx) + (ay - by) * (cy - by)) / (ab * bc);
2277
- if (dotp >= 1 - 1e-14) {
2278
- theta = 0;
2279
- } else if (dotp <= -1 + 1e-14) {
2280
- theta = Math.PI;
2281
- } else {
2282
- theta = Math.acos(dotp); // consider using other formula at small dp
2283
- }
2284
- }
2285
- return theta;
2286
- }
2287
-
2288
- function innerAngle3D(ax, ay, az, bx, by, bz, cx, cy, cz) {
2289
- var ab = distance3D(ax, ay, az, bx, by, bz),
2290
- bc = distance3D(bx, by, bz, cx, cy, cz),
2291
- theta, dotp;
2292
- if (ab === 0 || bc === 0) {
2293
- theta = 0;
2294
- } else {
2295
- dotp = ((ax - bx) * (cx - bx) + (ay - by) * (cy - by) + (az - bz) * (cz - bz)) / (ab * bc);
2296
- if (dotp >= 1) {
2297
- theta = 0;
2298
- } else if (dotp <= -1) {
2299
- theta = Math.PI;
2300
- } else {
2301
- theta = Math.acos(dotp); // consider using other formula at small dp
2302
- }
2303
- }
2304
- return theta;
2305
- }
2306
2278
 
2307
2279
  function triangleArea(ax, ay, bx, by, cx, cy) {
2308
2280
  var area = Math.abs(((ay - cy) * (bx - cx) + (by - cy) * (cx - ax)) / 2);
@@ -2391,36 +2363,10 @@
2391
2363
  return distanceSq(px, py, ax + t * (bx - ax), ay + t * (by - ay));
2392
2364
  }
2393
2365
 
2394
-
2395
- // internal.reversePathCoords = function(arr, start, len) {
2396
- // var i = start,
2397
- // j = start + len - 1,
2398
- // tmp;
2399
- // while (i < j) {
2400
- // tmp = arr[i];
2401
- // arr[i] = arr[j];
2402
- // arr[j] = tmp;
2403
- // i++;
2404
- // j--;
2405
- // }
2406
- // };
2407
-
2408
- // merge B into A
2409
- // function mergeBounds(a, b) {
2410
- // if (b[0] < a[0]) a[0] = b[0];
2411
- // if (b[1] < a[1]) a[1] = b[1];
2412
- // if (b[2] > a[2]) a[2] = b[2];
2413
- // if (b[3] > a[3]) a[3] = b[3];
2414
- // }
2415
-
2416
2366
  function containsBounds(a, b) {
2417
2367
  return a[0] <= b[0] && a[2] >= b[2] && a[1] <= b[1] && a[3] >= b[3];
2418
2368
  }
2419
2369
 
2420
- // function boundsArea(b) {
2421
- // return (b[2] - b[0]) * (b[3] - b[1]);
2422
- // }
2423
-
2424
2370
  var Geom = /*#__PURE__*/Object.freeze({
2425
2371
  __proto__: null,
2426
2372
  D2R: D2R,
@@ -2438,9 +2384,7 @@
2438
2384
  distanceSq: distanceSq,
2439
2385
  distanceSq3D: distanceSq3D,
2440
2386
  greatCircleDistance: greatCircleDistance,
2441
- innerAngle: innerAngle,
2442
2387
  innerAngle2: innerAngle2,
2443
- innerAngle3D: innerAngle3D,
2444
2388
  lngLatToXYZ: lngLatToXYZ,
2445
2389
  pointSegDistSq: pointSegDistSq,
2446
2390
  pointSegDistSq2: pointSegDistSq2,
@@ -2962,7 +2906,22 @@
2962
2906
  // @coords: Array of relevant coordinates (e.g. bbox coordinates of vertex coordinates
2963
2907
  // of two intersecting segments).
2964
2908
  //
2909
+
2910
+ // Updated the original function with a smaller interval... which works
2911
+ // better on a (limited) set of real-world sample data
2912
+ // (less likely to create erroneous output)
2965
2913
  function getHighPrecisionSnapInterval(coords) {
2914
+ var n = Math.max.apply(null, coords.map(Math.abs));
2915
+ var ceil = n <= 1 ? 1 : 2 ** Math.ceil(Math.log2(n));
2916
+ // console.log(ceil < ceil + ceil / 2 ** 51) // true
2917
+ // console.log(ceil < ceil + ceil / 2 ** 52) // true
2918
+ // console.log(ceil < ceil + ceil / 2 ** 53) // false
2919
+ var interval = ceil / 2 ** 51;
2920
+ // console.log('interval:', interval)
2921
+ return interval;
2922
+ }
2923
+
2924
+ function getHighPrecisionSnapInterval_old(coords) {
2966
2925
  var maxCoord = Math.max.apply(null, coords.map(Math.abs));
2967
2926
  return maxCoord * 1e-14;
2968
2927
  }
@@ -3148,11 +3107,77 @@
3148
3107
  getCoordinateIds: getCoordinateIds,
3149
3108
  getEndpointIds: getEndpointIds,
3150
3109
  getHighPrecisionSnapInterval: getHighPrecisionSnapInterval,
3110
+ getHighPrecisionSnapInterval_old: getHighPrecisionSnapInterval_old,
3151
3111
  snapCoords: snapCoords,
3152
3112
  snapCoordsByInterval: snapCoordsByInterval,
3153
3113
  snapEndpointsByInterval: snapEndpointsByInterval
3154
3114
  });
3155
3115
 
3116
+ function toScaledStr(num, k) {
3117
+ var abs = Math.abs(num);
3118
+ var signed = num != abs;
3119
+ // String() matches behavior of big.js
3120
+ // (as opposed to toFixed() or toPrecision())
3121
+ var s = abs < 1e-6 ? abs.toFixed(k) : String(abs);
3122
+ var parts = s.split('.');
3123
+ var integer = parts[0] == '0' ? '' : parts[0];
3124
+ var decimal = parts.length == 2 ? parts[1] : '';
3125
+ if (decimal.length < k) {
3126
+ decimal = decimal.padEnd(k, '0');
3127
+ } else if (decimal.length > k) {
3128
+ decimal = decimal.slice(0, k);
3129
+ }
3130
+ var s2 = integer + decimal;
3131
+ // remove any leading 0s
3132
+ while (s2[0] == 0 && s2.length > 1) {
3133
+ s2 = s2.slice(1);
3134
+ }
3135
+ if (signed) {
3136
+ s2 = '-' + s2;
3137
+ }
3138
+ return s2;
3139
+ }
3140
+
3141
+ // returns Number
3142
+ function fromScaledStr(s, decimals) {
3143
+ var signed = s[0] == '-';
3144
+ var uns = signed ? s.slice(1) : s;
3145
+ var s2, len;
3146
+ len = uns.length;
3147
+ if (len > decimals) {
3148
+ s2 = uns.slice(0, len - decimals) + '.' + uns.slice(-decimals);
3149
+ } else if (len == decimals) {
3150
+ s2 = '0.' + uns;
3151
+ } else {
3152
+ s2 = '0.' + uns.padStart(decimals, '0');
3153
+ }
3154
+ if (signed) {
3155
+ s2 = '-' + s2;
3156
+ }
3157
+ return Number(s2);
3158
+ }
3159
+
3160
+ function findBigIntScaleFactor() {
3161
+ var minVal = Infinity;
3162
+ var intLen = 0, s;
3163
+ for (var i=0, n=arguments.length; i<n; i++) {
3164
+ minVal = Math.min(minVal, Math.abs(arguments[i]));
3165
+ }
3166
+ if (minVal >= 1) {
3167
+ s = minVal.toFixed(1);
3168
+ intLen = s.indexOf('.');
3169
+ } else if (minVal !== 0) {
3170
+ s = minVal.toFixed(10).slice(2); // decimal part, up to _ decimals
3171
+ while (s[0] === '0') {
3172
+ intLen--;
3173
+ s = s.slice(1);
3174
+ }
3175
+ }
3176
+ return 17 - intLen;
3177
+ }
3178
+
3179
+ //import { findCrossIntersection_big } from '../geom/mapshaper-segment-geom-big';
3180
+
3156
3181
  // Find the intersection between two 2D segments
3157
3182
  // Returns 0, 1 or 2 [x, y] locations as null, [x, y], or [x1, y1, x2, y2]
3158
3183
  // Special cases:
@@ -3164,43 +3189,59 @@
3164
3189
  function segmentIntersection(ax, ay, bx, by, cx, cy, dx, dy, epsArg) {
3165
3190
  // Use a small tolerance interval, so collinear segments and T-intersections
3166
3191
  // are detected (floating point rounding often causes exact functions to fail)
3167
- var eps = epsArg >= 0 ? epsArg :
3192
+ var eps = epsArg > 0 ? epsArg :
3168
3193
  getHighPrecisionSnapInterval([ax, ay, bx, by, cx, cy, dx, dy]);
3169
3194
  var epsSq = eps * eps;
3170
3195
  var touches, cross;
3196
+
3171
3197
  // Detect 0, 1 or 2 'touch' intersections, where a vertex of one segment
3172
3198
  // is very close to the other segment's linear portion.
3173
3199
  // One touch indicates either a T-intersection or two overlapping collinear
3174
3200
  // segments that share an endpoint. Two touches indicates overlapping
3175
3201
  // collinear segments that do not share an endpoint.
3176
3202
  touches = findPointSegTouches(epsSq, ax, ay, bx, by, cx, cy, dx, dy);
3177
- // if (touches) return touches;
3178
3203
  // Ignore endpoint-only intersections
3179
3204
  if (!touches && testEndpointHit(epsSq, ax, ay, bx, by, cx, cy, dx, dy)) {
3180
3205
  return null;
3181
3206
  }
3182
3207
  // Detect cross intersection
3183
- cross = findCrossIntersection(ax, ay, bx, by, cx, cy, dx, dy, eps);
3208
+ // (TODO: consider cross intersections that are also endpoint hits)
3209
+ if (!touches) {
3210
+ cross = findCrossIntersection(ax, ay, bx, by, cx, cy, dx, dy, eps);
3211
+ }
3184
3212
  return touches || cross || null;
3185
3213
  }
3186
3214
 
3187
-
3188
- // Find the intersection point of two segments that cross each other,
3189
- // or return null if the segments do not cross.
3190
- // Assumes endpoint intersections have already been detected
3191
3215
  function findCrossIntersection(ax, ay, bx, by, cx, cy, dx, dy, eps) {
3192
- if (!segmentHit(ax, ay, bx, by, cx, cy, dx, dy)) return null;
3193
- var den = determinant2D(bx - ax, by - ay, dx - cx, dy - cy);
3194
- var m = orient2D(cx, cy, dx, dy, ax, ay) / den;
3195
- var p = [ax + m * (bx - ax), ay + m * (by - ay)];
3196
- if (Math.abs(den) < 1e-25) {
3197
- // changed from 1e-18 to 1e-25 (see geom ex1)
3198
- // assume that collinear and near-collinear segment intersections have been
3199
- // accounted for already.
3200
- // TODO: is this a valid assumption?
3216
+ var p;
3217
+ // The normal-precision hit function works for all inputs when eps > 0 because
3218
+ // the geometries that cause the ordinary function fails are detected as
3219
+ // 'touches' or endpoint hits (at least this was true in all the real-world
3220
+ // data samples that were tested).
3221
+ //
3222
+ if (eps > 0 && !segmentHit_fast(ax, ay, bx, by, cx, cy, dx, dy)) {
3223
+ return null;
3224
+ } else if (eps === 0 && !segmentHit_robust(ax, ay, bx, by, cx, cy, dx, dy)) {
3201
3225
  return null;
3202
3226
  }
3203
3227
 
3228
+ // in a typical layer with many intersections along shared polygon boundaries,
3229
+ // robust is preferred in most (>90%) segment intersections in order to keep
3230
+ // the positional error within a small interval (e.g. 50% of eps)
3231
+ //
3232
+ if (useRobustCross(ax, ay, bx, by, cx, cy, dx, dy)) {
3233
+ p = findCrossIntersection_robust(ax, ay, bx, by, cx, cy, dx, dy);
3234
+ // var p2 = findCrossIntersection_big(ax, ay, bx, by, cx, cy, dx, dy);
3235
+ // var dx = p[0] - p2[0];
3236
+ // var dy = p[1] - p2[1];
3237
+ // if (dx != 0 || dy != 0) {
3238
+ // console.log(dx, dy)
3239
+ // }
3240
+ } else {
3241
+ p = findCrossIntersection_fast(ax, ay, bx, by, cx, cy, dx, dy);
3242
+ }
3243
+ if (!p) return null;
3244
+
3204
3245
  // Snap p to a vertex if very close to one
3205
3246
  // This avoids tiny segments caused by T-intersection overshoots and prevents
3206
3247
  // pathfinder errors related to f-p rounding.
@@ -3215,9 +3256,90 @@
3215
3256
  return p;
3216
3257
  }
3217
3258
 
3259
+ function findCrossIntersection_fast(ax, ay, bx, by, cx, cy, dx, dy) {
3260
+ var den = determinant2D(bx - ax, by - ay, dx - cx, dy - cy);
3261
+ var m = orient2D(cx, cy, dx, dy, ax, ay) / den;
3262
+ var p = [ax + m * (bx - ax), ay + m * (by - ay)];
3263
+ if (Math.abs(den) < 1e-25) {
3264
+ // changed from 1e-18 to 1e-25 (see geom ex1)
3265
+ // assume that collinear and near-collinear segment intersections have been
3266
+ // accounted for already.
3267
+ // TODO: is this really true?
3268
+ return null;
3269
+ }
3270
+ return p;
3271
+ }
3272
+
3273
+ // this function, using BigInt, is 3-4x faster than the version using big.js
3274
+ function findCrossIntersection_robust(ax, ay, bx, by, cx, cy, dx, dy) {
3275
+ var d = findBigIntScaleFactor(ax, ay, bx, by, cx, cy, dx, dy);
3276
+ var d2 = 16; // scale numerator of integer division by this many decimal digits
3277
+ var k_bi = 10000000000000000n; // matches d2
3278
+ var ax_bi = BigInt(toScaledStr(ax, d));
3279
+ var ay_bi = BigInt(toScaledStr(ay, d));
3280
+ var bx_bi = BigInt(toScaledStr(bx, d));
3281
+ var by_bi = BigInt(toScaledStr(by, d));
3282
+ var cx_bi = BigInt(toScaledStr(cx, d));
3283
+ var cy_bi = BigInt(toScaledStr(cy, d));
3284
+ var dx_bi = BigInt(toScaledStr(dx, d));
3285
+ var dy_bi = BigInt(toScaledStr(dy, d));
3286
+ var den = determinant2D(bx_bi - ax_bi, by_bi - ay_bi, dx_bi - cx_bi, dy_bi - cy_bi);
3287
+ if (den === 0n) {
3288
+ debug('DIV0 error - should have been identified as collinear "touch" intersection.');
3289
+ return null;
3290
+ }
3291
+ var num = orient2D(cx_bi, cy_bi, dx_bi, dy_bi, ax_bi, ay_bi) * k_bi;
3292
+ var m_bi = num / den;
3293
+ var x_bi = ax_bi * k_bi + m_bi * (bx_bi - ax_bi);
3294
+ var y_bi = ay_bi * k_bi + m_bi * (by_bi - ay_bi);
3295
+ var x = fromScaledStr(x_bi.toString(), d + d2);
3296
+ var y = fromScaledStr(y_bi.toString(), d + d2);
3297
+ return [x, y];
3298
+ }
3299
+
3300
+ function useRobustCross(ax, ay, bx, by, cx, cy, dx, dy) {
3301
+ // angle and seg length ratio thresholds were found by comparing
3302
+ // fast and robust outputs on sample data
3303
+ if (innerAngle(ax, ay, bx, by, cx, cy, dx, dy) < 0.1) return true;
3304
+ var len1 = distance2D(ax, ay, bx, by);
3305
+ var len2 = distance2D(cx, cy, dx, dy);
3306
+ var ratio = len1 < len2 ? len1 / len2 : len2 / len1 || 0;
3307
+ if (ratio < 0.001) return true;
3308
+ return false;
3309
+ }
3310
+
3311
+ // Returns smaller of two angles between two segments (unsigned)
3312
+ function innerAngle(ax, ay, bx, by, cx, cy, dx, dy) {
3313
+ var v1x = bx - ax;
3314
+ var v1y = by - ay;
3315
+ var v2x = dx - cx;
3316
+ var v2y = dy - cy;
3317
+ var dot = v1x * v2x + v1y * v2y;
3318
+ var mag1Sq = v1x * v1x + v1y * v1y;
3319
+ var mag2Sq = v2x * v2x + v2y * v2y;
3320
+ if (mag1Sq === 0 || mag2Sq === 0) {
3321
+ return 0;
3322
+ }
3323
+ var cosTheta = dot / Math.sqrt(mag1Sq * mag2Sq);
3324
+ var theta;
3325
+ if (cosTheta > 1 - 1e-14) {
3326
+ theta = 0;
3327
+ } else if (cosTheta < -1 + 1e-14) {
3328
+ theta = Math.PI;
3329
+ } else {
3330
+ theta = Math.acos(cosTheta);
3331
+ }
3332
+ if (theta >= Math.PI / 2) {
3333
+ theta = Math.PI - theta;
3334
+ }
3335
+ return theta;
3336
+ }
3337
+
3218
3338
  function testEndpointHit(epsSq, ax, ay, bx, by, cx, cy, dx, dy) {
3219
- return distanceSq(ax, ay, cx, cy) <= epsSq || distanceSq(ax, ay, dx, dy) <= epsSq ||
3220
- distanceSq(bx, by, cx, cy) <= epsSq || distanceSq(bx, by, dx, dy) <= epsSq;
3339
+ return distanceSq(ax, ay, cx, cy) <= epsSq ||
3340
+ distanceSq(ax, ay, dx, dy) <= epsSq ||
3341
+ distanceSq(bx, by, cx, cy) <= epsSq ||
3342
+ distanceSq(bx, by, dx, dy) <= epsSq;
3221
3343
  }
3222
3344
 
3223
3345
  function findPointSegTouches(epsSq, ax, ay, bx, by, cx, cy, dx, dy) {
@@ -3228,6 +3350,9 @@
3228
3350
  collectPointSegTouch(touches, epsSq, dx, dy, ax, ay, bx, by);
3229
3351
  if (touches.length === 0) return null;
3230
3352
  if (touches.length > 4) {
3353
+ // console.log('XX', touches.length)
3354
+ // console.log('Seg 1', getSegFeature(ax, ay, bx, by, true))
3355
+ // console.log('Seg 2', getSegFeature(cx, cy, dx, dy, false))
3231
3356
  // Geometrically, more than two touch intersections can not occur.
3232
3357
  // Is it possible that fp rounding or a bug might result in >2 touches?
3233
3358
  debug('Intersection detection error');
@@ -3244,10 +3369,10 @@
3244
3369
  var pa = distanceSq(ax, ay, px, py);
3245
3370
  var pb = distanceSq(bx, by, px, py);
3246
3371
  if (pa <= epsSq || pb <= epsSq) return; // ignore endpoint hits
3372
+ // console.log("Dist:", Math.sqrt(pab), "eps:", Math.sqrt(epsSq), "p:", px, py)
3247
3373
  arr.push(px, py); // T intersection at P and AB
3248
3374
  }
3249
3375
 
3250
-
3251
3376
  // Used by mapshaper-undershoots.js
3252
3377
  // TODO: make more robust, make sure result is compatible with segmentIntersection()
3253
3378
  // (rounding errors currently must be handled downstream)
@@ -3296,16 +3421,43 @@
3296
3421
  // when a segment is vertical or horizontal. This has caused problems when
3297
3422
  // repeatedly applying bbox clipping along the same segment
3298
3423
  var x = p[0],
3299
- y = p[1];
3424
+ y = p[1],
3425
+ s1out = false,
3426
+ s2out = false;
3427
+
3300
3428
  // assumes that segment ranges intersect
3301
- x = clampToCloseRange(x, ax, bx);
3302
- x = clampToCloseRange(x, cx, dx);
3303
- y = clampToCloseRange(y, ay, by);
3304
- y = clampToCloseRange(y, cy, dy);
3429
+ if (outsideRange(x, ax, bx)) {
3430
+ x = clampToClosestEndpoint(x, ax, bx);
3431
+ s1out = true;
3432
+ }
3433
+ if (outsideRange(x, cx, dx)) {
3434
+ x = clampToClosestEndpoint(x, cx, dx);
3435
+ s2out = true;
3436
+ }
3437
+ if (outsideRange(y, ay, by)) {
3438
+ y = clampToClosestEndpoint(y, ay, by);
3439
+ s1out = true;
3440
+ }
3441
+ if (outsideRange(y, cy, dy)) {
3442
+ y = clampToClosestEndpoint(y, cy, dy);
3443
+ s2out = true;
3444
+ }
3445
+ if ((s1out || s2out)) {
3446
+ debug('Clamping a segment intersection point');
3447
+ // console.log("angle:", innerAngle(ax, ay, bx, by, cx, cy, dx, dy))
3448
+ // console.log('Feature 1', getSegFeature(ax, ay, bx, by, s1out));
3449
+ // console.log('Feature 2', getSegFeature(cx, cy, dx, dy, s2out));
3450
+ // console.log('Point:', JSON.stringify({
3451
+ // type: 'Feature',
3452
+ // properties: {fill: 'red'},
3453
+ // geometry: {type: 'Point', coordinates: [p[0], p[1]]}
3454
+ // }));
3455
+ }
3305
3456
  p[0] = x;
3306
3457
  p[1] = y;
3307
3458
  }
3308
3459
 
3460
+
3309
3461
  // a: coordinate of point
3310
3462
  // b: endpoint coordinate of segment
3311
3463
  // c: other endpoint of segment
@@ -3321,16 +3473,13 @@
3321
3473
  return out;
3322
3474
  }
3323
3475
 
3324
- function clampToCloseRange(a, b, c) {
3325
- var lim;
3326
- if (outsideRange(a, b, c)) {
3327
- lim = Math.abs(a - b) < Math.abs(a - c) ? b : c;
3328
- if (Math.abs(a - lim) > 1e-15) {
3329
- debug("[clampToCloseRange()] large clamping interval", a, b, c);
3330
- }
3331
- a = lim;
3476
+ function clampToClosestEndpoint(a, b, c) {
3477
+ var lim = Math.abs(a - b) < Math.abs(a - c) ? b : c;
3478
+ var interval = Math.abs(a - lim);
3479
+ if (interval > 1e-15) {
3480
+ debug("[clampToClosestEndpoint()] large clamping interval:", interval);
3332
3481
  }
3333
- return a;
3482
+ return lim;
3334
3483
  }
3335
3484
 
3336
3485
  // Determinant of matrix
@@ -3348,10 +3497,35 @@
3348
3497
  return determinant2D(ax - cx, ay - cy, bx - cx, by - cy);
3349
3498
  }
3350
3499
 
3500
+ function orient2D_robust(ax, ay, bx, by, cx, cy) {
3501
+ var d = findBigIntScaleFactor(ax, ay, bx, by, cx, cy);
3502
+ var ax_bi = BigInt(toScaledStr(ax, d));
3503
+ var ay_bi = BigInt(toScaledStr(ay, d));
3504
+ var bx_bi = BigInt(toScaledStr(bx, d));
3505
+ var by_bi = BigInt(toScaledStr(by, d));
3506
+ var cx_bi = BigInt(toScaledStr(cx, d));
3507
+ var cy_bi = BigInt(toScaledStr(cy, d));
3508
+ var o2d_bi = orient2D(ax_bi, ay_bi, bx_bi, by_bi, cx_bi, cy_bi);
3509
+ return fromScaledStr(o2d_bi.toString(), d);
3510
+ }
3511
+
3512
+ function segmentHit_robust(ax, ay, bx, by, cx, cy, dx, dy) {
3513
+ var d = findBigIntScaleFactor(ax, ay, bx, by, cx, cy, dx, dy);
3514
+ var ax_bi = BigInt(toScaledStr(ax, d));
3515
+ var ay_bi = BigInt(toScaledStr(ay, d));
3516
+ var bx_bi = BigInt(toScaledStr(bx, d));
3517
+ var by_bi = BigInt(toScaledStr(by, d));
3518
+ var cx_bi = BigInt(toScaledStr(cx, d));
3519
+ var cy_bi = BigInt(toScaledStr(cy, d));
3520
+ var dx_bi = BigInt(toScaledStr(dx, d));
3521
+ var dy_bi = BigInt(toScaledStr(dy, d));
3522
+ return segmentHit_fast(ax_bi, ay_bi, bx_bi, by_bi, cx_bi, cy_bi, dx_bi, dy_bi);
3523
+ }
3524
+
3351
3525
  // Source: Sedgewick, _Algorithms in C_
3352
3526
  // (Other functions were tried that were more sensitive to floating point errors
3353
3527
  // than this function)
3354
- function segmentHit(ax, ay, bx, by, cx, cy, dx, dy) {
3528
+ function segmentHit_fast(ax, ay, bx, by, cx, cy, dx, dy) {
3355
3529
  return orient2D(ax, ay, bx, by, cx, cy) *
3356
3530
  orient2D(ax, ay, bx, by, dx, dy) <= 0 &&
3357
3531
  orient2D(cx, cy, dx, dy, ax, ay) *
@@ -3381,8 +3555,10 @@
3381
3555
  var SegmentGeom = /*#__PURE__*/Object.freeze({
3382
3556
  __proto__: null,
3383
3557
  findClosestPointOnSeg: findClosestPointOnSeg,
3558
+ findCrossIntersection_robust: findCrossIntersection_robust,
3384
3559
  orient2D: orient2D,
3385
- segmentHit: segmentHit,
3560
+ orient2D_robust: orient2D_robust,
3561
+ segmentHit_fast: segmentHit_fast,
3386
3562
  segmentIntersection: segmentIntersection,
3387
3563
  segmentTurn: segmentTurn
3388
3564
  });
@@ -3692,7 +3868,7 @@
3692
3868
  // editPart: callback function
3693
3869
  function editShapes(shapes, editPart) {
3694
3870
  for (var i=0, n=shapes.length; i<n; i++) {
3695
- shapes[i] = editShapeParts(shapes[i], editPart);
3871
+ shapes[i] = editShapeParts(shapes[i], editPart, i);
3696
3872
  }
3697
3873
  }
3698
3874
 
@@ -3700,8 +3876,9 @@
3700
3876
  // @cb: function(part, i, parts)
3701
3877
  // If @cb returns an array, it replaces the existing value
3702
3878
  // If @cb returns null, the path is removed from the feature
3879
+ // @shpId: (optional) id of shape
3703
3880
  //
3704
- function editShapeParts(parts, cb) {
3881
+ function editShapeParts(parts, cb, shpId) {
3705
3882
  if (!parts) return null; // null geometry not edited
3706
3883
  if (!utils.isArray(parts)) error("Expected an array, received:", parts);
3707
3884
  var nulls = 0,
@@ -3709,7 +3886,7 @@
3709
3886
  retn;
3710
3887
 
3711
3888
  for (var i=0; i<n; i++) {
3712
- retn = cb(parts[i], i, parts);
3889
+ retn = cb(parts[i], i, parts, shpId);
3713
3890
  if (retn === null) {
3714
3891
  nulls++;
3715
3892
  parts[i] = null;
@@ -5756,7 +5933,7 @@
5756
5933
  //
5757
5934
  this.forEach2 = function(cb) {
5758
5935
  for (var arcId=0, n=this.size(); arcId<n; arcId++) {
5759
- cb(_ii[arcId], _nn[arcId], _xx, _yy, _zz, arcId);
5936
+ cb(arcId, _ii[arcId], _nn[arcId], _xx, _yy, _zz);
5760
5937
  }
5761
5938
  };
5762
5939
 
@@ -5891,6 +6068,44 @@
5891
6068
  };
5892
6069
 
5893
6070
  this.arcIsDegenerate = function(arcId) {
6071
+ return this.arcHasZeroLength(arcId) || this.arcIsSpike(arcId) || this.arcIsSpike_v1(arcId);
6072
+ };
6073
+
6074
+ // only finds two-segment spikes
6075
+ this.arcIsSpike_v1 = function(arcId) {
6076
+ var iter = this.getArcIter(arcId);
6077
+ var x0, y0;
6078
+ if (iter.hasNext()) {
6079
+ x0 = iter.x;
6080
+ y0 = iter.y;
6081
+ }
6082
+ iter.hasNext(); // ignore second point
6083
+ if (iter.hasNext()) {
6084
+ if (iter.x == x0 && iter.y == y0 && !iter.hasNext()) {
6085
+ // three-vertex arc, first two are the same
6086
+ return true;
6087
+ }
6088
+ }
6089
+ return false;
6090
+ };
6091
+
6092
+ // identifies n-segment spikes
6093
+ this.arcIsSpike = function(arcId) {
6094
+ arcId = absArcId(arcId);
6095
+ var i = _ii[arcId];
6096
+ var j = i + _nn[arcId] - 1;
6097
+ while (j > i) {
6098
+ if (_xx[i] != _xx[j] || _yy[i] != _yy[j]) {
6099
+ return false;
6100
+ }
6101
+ j--;
6102
+ i++;
6103
+ }
6104
+ return true;
6105
+ };
6106
+
6107
+
6108
+ this.arcHasZeroLength = function(arcId) {
5894
6109
  var iter = this.getArcIter(arcId);
5895
6110
  var i = 0,
5896
6111
  x, y;
@@ -6397,10 +6612,10 @@
6397
6612
  // Visit each point in the path, up to but not including the last point
6398
6613
  for (var i = start; i < end; i++) {
6399
6614
  if (pointIsArcEndpoint(i)) {
6400
- if (firstNodeId > -1) {
6401
- arcIds.push(addEdge(arcStartId, i));
6402
- } else {
6615
+ if (firstNodeId == -1) {
6403
6616
  firstNodeId = i;
6617
+ } else {
6618
+ arcIds.push(addEdge(arcStartId, i));
6404
6619
  }
6405
6620
  arcStartId = i;
6406
6621
  }
@@ -6991,7 +7206,7 @@
6991
7206
  yy2 = new Float64Array(n),
6992
7207
  ids2 = new Int32Array(n);
6993
7208
 
6994
- arcs.forEach2(function(i, n, xx, yy, zz, arcId) {
7209
+ arcs.forEach2(function(arcId, i, n, xx, yy, zz) {
6995
7210
  var start = i,
6996
7211
  end = i + n - 1,
6997
7212
  start2 = arcId * 2,
@@ -7042,7 +7257,7 @@
7042
7257
  layers.forEach(function(lyr) {
7043
7258
  // modify copies of the original shapes; original shapes should be unmodified
7044
7259
  // (need to test this)
7045
- lyr.shapes = lyr.shapes.map(function(shape) {
7260
+ lyr.shapes = lyr.shapes.map(function(shape, i) {
7046
7261
  return editShapeParts(shape && shape.concat(), translatePath);
7047
7262
  });
7048
7263
  });
@@ -12584,11 +12799,178 @@
12584
12799
  stringifyAsNDJSON: stringifyAsNDJSON
12585
12800
  });
12586
12801
 
12802
+ function findNearestVertices(p, shp, arcs) {
12803
+ var p2 = findNearestVertex(p[0], p[1], shp, arcs);
12804
+ return findVertexIds(p2.x, p2.y, arcs);
12805
+ }
12806
+
12807
+ function snapVerticesToPoint(ids, p, arcs) {
12808
+ var data = arcs.getVertexData();
12809
+ ids.forEach(function(idx) {
12810
+ setVertexCoords(p[0], p[1], idx, arcs);
12811
+ arcs.updateArcBounds(findArcIdFromVertexId(idx, data.ii));
12812
+ });
12813
+ }
12814
+
12815
+
12816
+ // p: point to snap
12817
+ // ids: ids of nearby vertices, possibly including an arc endpoint
12818
+ function snapPointToArcEndpoint(p, ids, arcs) {
12819
+ var p2, dx, dy;
12820
+ ids.forEach(function(idx) {
12821
+ if (vertexIsArcStart(idx, arcs)) {
12822
+ p2 = getVertexCoords(idx + 1, arcs);
12823
+ } else if (vertexIsArcEnd(idx, arcs)) {
12824
+ p2 = getVertexCoords(idx - 1, arcs);
12825
+ }
12826
+ });
12827
+ if (!p2) return;
12828
+ dx = p2[0] - p[0];
12829
+ dy = p2[1] - p[1];
12830
+ if (Math.abs(dx) > Math.abs(dy)) {
12831
+ p[1] = p2[1]; // snap y coord
12832
+ } else {
12833
+ p[0] = p2[0];
12834
+ }
12835
+ }
12836
+
12837
+ // Find ids of vertices with identical coordinates to x,y in an ArcCollection
12838
+ // Caveat: does not exclude vertices that are not visible at the
12839
+ // current level of simplification.
12840
+ function findVertexIds(x, y, arcs) {
12841
+ var data = arcs.getVertexData(),
12842
+ xx = data.xx,
12843
+ yy = data.yy,
12844
+ ids = [];
12845
+ for (var i=0, n=xx.length; i<n; i++) {
12846
+ if (xx[i] == x && yy[i] == y) ids.push(i);
12847
+ }
12848
+ return ids;
12849
+ }
12850
+
12851
+ function getVertexCoords(i, arcs) {
12852
+ var data = arcs.getVertexData();
12853
+ return [data.xx[i], data.yy[i]];
12854
+ }
12855
+
12856
+ function vertexIsArcEnd(idx, arcs) {
12857
+ // Test whether the vertex at index @idx is the endpoint of an arc
12858
+ var data = arcs.getVertexData(),
12859
+ ii = data.ii,
12860
+ nn = data.nn;
12861
+ for (var j=0, n=ii.length; j<n; j++) {
12862
+ if (idx === ii[j] + nn[j] - 1) return true;
12863
+ }
12864
+ return false;
12865
+ }
12866
+
12867
+ function vertexIsArcEndpoint(idx, arcs) {
12868
+ return vertexIsArcStart(idx, arcs) || vertexIsArcEnd(idx, arcs);
12869
+ }
12870
+
12871
+ function vertexIsArcStart(idx, arcs) {
12872
+ var ii = arcs.getVertexData().ii;
12873
+ for (var j=0, n=ii.length; j<n; j++) {
12874
+ if (idx === ii[j]) return true;
12875
+ }
12876
+ return false;
12877
+ }
12878
+
12879
+ function getArcStartCoords(arcId, arcs) {
12880
+ var coords = getArcEndpointCoords(arcId, arcs);
12881
+ return coords[0];
12882
+ }
12883
+
12884
+ function getArcEndCoords(arcId, arcs) {
12885
+ var coords = getArcEndpointCoords(arcId, arcs);
12886
+ return coords[1];
12887
+ }
12888
+
12889
+ function getArcEndpointCoords(arcId, arcs) {
12890
+ if (arcId < 0) {
12891
+ return getArcEndpointCoords(~arcId, arcs).reverse();
12892
+ }
12893
+ var data = arcs.getVertexData();
12894
+ var i = data.ii[arcId];
12895
+ var n = data.nn[arcId];
12896
+ var a = [data.xx[i], data.yy[i]];
12897
+ var b = [data.xx[i + n - 1], data.yy[i + n - 1]];
12898
+ return [a, b];
12899
+ }
12900
+
12901
+ function setVertexCoords(x, y, i, arcs) {
12902
+ var data = arcs.getVertexData();
12903
+ data.xx[i] = x;
12904
+ data.yy[i] = y;
12905
+ }
12906
+
12907
+ function findNearestVertex(x, y, shp, arcs, spherical) {
12908
+ var calcLen = spherical ? geom.greatCircleDistance : geom.distance2D,
12909
+ minLen = Infinity,
12910
+ minX, minY, dist, iter;
12911
+ for (var i=0; i<shp.length; i++) {
12912
+ iter = arcs.getShapeIter(shp[i]);
12913
+ while (iter.hasNext()) {
12914
+ dist = calcLen(x, y, iter.x, iter.y);
12915
+ if (dist < minLen) {
12916
+ minLen = dist;
12917
+ minX = iter.x;
12918
+ minY = iter.y;
12919
+ }
12920
+ }
12921
+ }
12922
+ return minLen < Infinity ? {x: minX, y: minY} : null;
12923
+ }
12924
+
12925
+ var VertexUtils = /*#__PURE__*/Object.freeze({
12926
+ __proto__: null,
12927
+ findNearestVertex: findNearestVertex,
12928
+ findNearestVertices: findNearestVertices,
12929
+ findVertexIds: findVertexIds,
12930
+ getArcEndCoords: getArcEndCoords,
12931
+ getArcEndpointCoords: getArcEndpointCoords,
12932
+ getArcStartCoords: getArcStartCoords,
12933
+ getVertexCoords: getVertexCoords,
12934
+ setVertexCoords: setVertexCoords,
12935
+ snapPointToArcEndpoint: snapPointToArcEndpoint,
12936
+ snapVerticesToPoint: snapVerticesToPoint,
12937
+ vertexIsArcEnd: vertexIsArcEnd,
12938
+ vertexIsArcEndpoint: vertexIsArcEndpoint,
12939
+ vertexIsArcStart: vertexIsArcStart
12940
+ });
12941
+
12942
+ function debugConnectedArcs(ids, arcs) {
12943
+ var colors = ['orange', 'blue', 'green', 'red', 'magenta', 'grey'];
12944
+ var features = ids.map(function(arcId, i) {
12945
+ return getArcFeature(arcId, arcs, {arcId: arcId, stroke: colors[i] || 'black'});
12946
+ });
12947
+ var geojson = '{"type": "FeatureCollection", "features": [' + features.join(',') + ']}';
12948
+ debug(geojson);
12949
+ }
12950
+
12951
+ function getArcFeature(arcId, arcs, properties) {
12952
+ return JSON.stringify({
12953
+ type: "Feature",
12954
+ properties: properties,
12955
+ geometry: GeoJSON.exportLineGeom([[arcId]], arcs)
12956
+ });
12957
+ }
12958
+
12587
12959
  function isValidArc(arcId, arcs) {
12588
12960
  // check for arcs with no vertices
12589
12961
  // TODO: also check for other kinds of degenerate arcs
12590
12962
  // (e.g. collapsed arcs consisting of identical points)
12591
- return arcs.getArcLength(arcId) > 1;
12963
+ var len = arcs.getArcLength(arcId);
12964
+ if (len >= 2 === false) {
12965
+ return false;
12966
+ }
12967
+ // if (len <=3) {
12968
+ // var endpoints = getArcEndpointCoords(arcId, arcs);
12969
+ // if (pointsAreEqual(endpoints[0], endpoints[1])) {
12970
+ // return false;
12971
+ // }
12972
+ // }
12973
+ return true;
12592
12974
  }
12593
12975
 
12594
12976
  // Return id of rightmost connected arc in relation to @fromArcId
@@ -12620,11 +13002,11 @@
12620
13002
  error("Duplicate point error");
12621
13003
  }*/
12622
13004
 
12623
-
12624
13005
  for (j=0; j<ids.length; j++) {
12625
13006
  candId = ids[j];
12626
13007
  if (!isValidArc(candId, arcs)) {
12627
13008
  // skip empty arcs
13009
+ debug('skipping one arc:', candId, 'out of:', ids.length, ids);
12628
13010
  continue;
12629
13011
  }
12630
13012
  icand = arcs.indexOfVertex(candId, -2);
@@ -12635,6 +13017,14 @@
12635
13017
  continue;
12636
13018
  }
12637
13019
  code = chooseRighthandPath(fromX, fromY, nodeX, nodeY, xx[ito], yy[ito], xx[icand], yy[icand]);
13020
+
13021
+ if (xx[ito] == xx[icand] && yy[ito] == yy[icand]) {
13022
+ debug("Pathfinder warning: duplicate segments: i:", ito, "j:", icand, "ids:", [fromArcId].concat(ids));
13023
+ if (useDebug()) {
13024
+ // debugDuplicatePathfinderSegments(nodeX, nodeY, ito, icand, nodes.arcs);
13025
+ debugConnectedArcs(ids, nodes.arcs);
13026
+ }
13027
+ }
12638
13028
  if (code == 2) {
12639
13029
  ito = icand;
12640
13030
  toArcId = candId;
@@ -12644,26 +13034,22 @@
12644
13034
 
12645
13035
  if (toArcId == fromArcId) {
12646
13036
  // This shouldn't occur, assuming that other arcs are present
12647
- error("Pathfinder error");
13037
+ error("Pathfinder error", toArcId, fromArcId);
12648
13038
  }
12649
13039
  return toArcId;
12650
13040
  }
12651
13041
 
12652
- // TODO: consider using simpler internal.chooseRighthandPath2()
12653
13042
  // Returns 1 if node->a, return 2 if node->b, else return 0
12654
13043
  // TODO: better handling of identical angles (better -- avoid creating them)
12655
13044
  function chooseRighthandPath(fromX, fromY, nodeX, nodeY, ax, ay, bx, by) {
12656
13045
  var angleA = geom.signedAngle(fromX, fromY, nodeX, nodeY, ax, ay);
12657
13046
  var angleB = geom.signedAngle(fromX, fromY, nodeX, nodeY, bx, by);
13047
+ // should use arbitrary precision math to evaluate angles smaller than this
13048
+ // (all observed errors caused by fp rounding occured with smaller angles than this)
13049
+ var smallAngle = 0.001;
12658
13050
  var code;
12659
13051
  if (angleA <= 0 || angleB <= 0) {
12660
13052
  debug("[chooseRighthandPath()] 0 angle(s):", angleA, angleB);
12661
- if (angleA <= 0) {
12662
- debug(' A orient2D:', geom.orient2D(fromX, fromY, nodeX, nodeY, ax, ay));
12663
- }
12664
- if (angleB <= 0) {
12665
- debug(' B orient2D:', geom.orient2D(fromX, fromY, nodeX, nodeY, bx, by));
12666
- }
12667
13053
  // TODO: test against "from" segment
12668
13054
  if (angleA > 0) {
12669
13055
  code = 1;
@@ -12672,23 +13058,23 @@
12672
13058
  } else {
12673
13059
  code = 0;
12674
13060
  }
12675
- } else if (angleA < angleB) {
13061
+ } else if (angleA < angleB - smallAngle) {
12676
13062
  code = 1;
12677
- } else if (angleB < angleA) {
13063
+ } else if (angleB < angleA - smallAngle) {
12678
13064
  code = 2;
12679
13065
  } else if (isNaN(angleA) || isNaN(angleB)) {
12680
13066
  // probably a duplicate point, which should not occur
12681
13067
  error('Invalid node geometry');
12682
13068
  } else {
12683
- // Equal angles: use fallback test that is less sensitive to rounding error
12684
- code = chooseRighthandVector(ax - nodeX, ay - nodeY, bx - nodeX, by - nodeY);
12685
- debug('[chooseRighthandPath()] equal angles:', angleA, 'fallback test:', code);
13069
+ // Close-to-equal or equal angles: use more exact test.
13070
+ code = chooseBetweenClosePaths(nodeX, nodeY, ax, ay, bx, by);
13071
+ // debug('[chooseRighthandPath()] close-to-equal angles:', Math.abs(angleA) - Math.abs(angleB), 'hi-res code:', code);
12686
13072
  }
12687
13073
  return code;
12688
13074
  }
12689
13075
 
12690
- function chooseRighthandVector(ax, ay, bx, by) {
12691
- var orient = geom.orient2D(ax, ay, 0, 0, bx, by);
13076
+ function chooseBetweenClosePaths(nodeX, nodeY, ax, ay, bx, by) {
13077
+ var orient = orient2D_robust(ax, ay, nodeX, nodeY, bx, by);
12692
13078
  var code;
12693
13079
  if (orient > 0) {
12694
13080
  code = 2;
@@ -12702,7 +13088,7 @@
12702
13088
 
12703
13089
  var PathfinderUtils = /*#__PURE__*/Object.freeze({
12704
13090
  __proto__: null,
12705
- chooseRighthandVector: chooseRighthandVector,
13091
+ chooseBetweenClosePaths: chooseBetweenClosePaths,
12706
13092
  getRightmostArc: getRightmostArc
12707
13093
  });
12708
13094
 
@@ -13227,7 +13613,7 @@
13227
13613
  }
13228
13614
 
13229
13615
  // test two candidate segments for intersection
13230
- hit = geom.segmentIntersection(s1p1x, s1p1y, s1p2x, s1p2y,
13616
+ hit = segmentIntersection(s1p1x, s1p1y, s1p2x, s1p2y,
13231
13617
  s2p1x, s2p1y, s2p2x, s2p2y, tolerance);
13232
13618
  if (hit) {
13233
13619
  seg1 = [s1p1, s1p2];
@@ -13285,146 +13671,6 @@
13285
13671
  sortIntersections: sortIntersections
13286
13672
  });
13287
13673
 
13288
- function findNearestVertices(p, shp, arcs) {
13289
- var p2 = findNearestVertex(p[0], p[1], shp, arcs);
13290
- return findVertexIds(p2.x, p2.y, arcs);
13291
- }
13292
-
13293
- function snapVerticesToPoint(ids, p, arcs) {
13294
- var data = arcs.getVertexData();
13295
- ids.forEach(function(idx) {
13296
- setVertexCoords(p[0], p[1], idx, arcs);
13297
- arcs.updateArcBounds(findArcIdFromVertexId(idx, data.ii));
13298
- });
13299
- }
13300
-
13301
-
13302
- // p: point to snap
13303
- // ids: ids of nearby vertices, possibly including an arc endpoint
13304
- function snapPointToArcEndpoint(p, ids, arcs) {
13305
- var p2, dx, dy;
13306
- ids.forEach(function(idx) {
13307
- if (vertexIsArcStart(idx, arcs)) {
13308
- p2 = getVertexCoords(idx + 1, arcs);
13309
- } else if (vertexIsArcEnd(idx, arcs)) {
13310
- p2 = getVertexCoords(idx - 1, arcs);
13311
- }
13312
- });
13313
- if (!p2) return;
13314
- dx = p2[0] - p[0];
13315
- dy = p2[1] - p[1];
13316
- if (Math.abs(dx) > Math.abs(dy)) {
13317
- p[1] = p2[1]; // snap y coord
13318
- } else {
13319
- p[0] = p2[0];
13320
- }
13321
- }
13322
-
13323
- // Find ids of vertices with identical coordinates to x,y in an ArcCollection
13324
- // Caveat: does not exclude vertices that are not visible at the
13325
- // current level of simplification.
13326
- function findVertexIds(x, y, arcs) {
13327
- var data = arcs.getVertexData(),
13328
- xx = data.xx,
13329
- yy = data.yy,
13330
- ids = [];
13331
- for (var i=0, n=xx.length; i<n; i++) {
13332
- if (xx[i] == x && yy[i] == y) ids.push(i);
13333
- }
13334
- return ids;
13335
- }
13336
-
13337
- function getVertexCoords(i, arcs) {
13338
- var data = arcs.getVertexData();
13339
- return [data.xx[i], data.yy[i]];
13340
- }
13341
-
13342
- function vertexIsArcEnd(idx, arcs) {
13343
- // Test whether the vertex at index @idx is the endpoint of an arc
13344
- var data = arcs.getVertexData(),
13345
- ii = data.ii,
13346
- nn = data.nn;
13347
- for (var j=0, n=ii.length; j<n; j++) {
13348
- if (idx === ii[j] + nn[j] - 1) return true;
13349
- }
13350
- return false;
13351
- }
13352
-
13353
- function vertexIsArcEndpoint(idx, arcs) {
13354
- return vertexIsArcStart(idx, arcs) || vertexIsArcEnd(idx, arcs);
13355
- }
13356
-
13357
- function vertexIsArcStart(idx, arcs) {
13358
- var ii = arcs.getVertexData().ii;
13359
- for (var j=0, n=ii.length; j<n; j++) {
13360
- if (idx === ii[j]) return true;
13361
- }
13362
- return false;
13363
- }
13364
-
13365
- function getArcStartCoords(arcId, arcs) {
13366
- var coords = getArcEndpointCoords(arcId, arcs);
13367
- return coords[0];
13368
- }
13369
-
13370
- function getArcEndCoords(arcId, arcs) {
13371
- var coords = getArcEndpointCoords(arcId, arcs);
13372
- return coords[1];
13373
- }
13374
-
13375
- function getArcEndpointCoords(arcId, arcs) {
13376
- if (arcId < 0) {
13377
- return getArcEndpointCoords(~arcId, arcs).reverse();
13378
- }
13379
- var data = arcs.getVertexData();
13380
- var i = data.ii[arcId];
13381
- var n = data.nn[arcId];
13382
- var a = [data.xx[i], data.yy[i]];
13383
- var b = [data.xx[i + n - 1], data.yy[i + n - 1]];
13384
- return [a, b];
13385
- }
13386
-
13387
- function setVertexCoords(x, y, i, arcs) {
13388
- var data = arcs.getVertexData();
13389
- data.xx[i] = x;
13390
- data.yy[i] = y;
13391
- }
13392
-
13393
- function findNearestVertex(x, y, shp, arcs, spherical) {
13394
- var calcLen = spherical ? geom.greatCircleDistance : geom.distance2D,
13395
- minLen = Infinity,
13396
- minX, minY, dist, iter;
13397
- for (var i=0; i<shp.length; i++) {
13398
- iter = arcs.getShapeIter(shp[i]);
13399
- while (iter.hasNext()) {
13400
- dist = calcLen(x, y, iter.x, iter.y);
13401
- if (dist < minLen) {
13402
- minLen = dist;
13403
- minX = iter.x;
13404
- minY = iter.y;
13405
- }
13406
- }
13407
- }
13408
- return minLen < Infinity ? {x: minX, y: minY} : null;
13409
- }
13410
-
13411
- var VertexUtils = /*#__PURE__*/Object.freeze({
13412
- __proto__: null,
13413
- findNearestVertex: findNearestVertex,
13414
- findNearestVertices: findNearestVertices,
13415
- findVertexIds: findVertexIds,
13416
- getArcEndCoords: getArcEndCoords,
13417
- getArcEndpointCoords: getArcEndpointCoords,
13418
- getArcStartCoords: getArcStartCoords,
13419
- getVertexCoords: getVertexCoords,
13420
- setVertexCoords: setVertexCoords,
13421
- snapPointToArcEndpoint: snapPointToArcEndpoint,
13422
- snapVerticesToPoint: snapVerticesToPoint,
13423
- vertexIsArcEnd: vertexIsArcEnd,
13424
- vertexIsArcEndpoint: vertexIsArcEndpoint,
13425
- vertexIsArcStart: vertexIsArcStart
13426
- });
13427
-
13428
13674
  // arcs: ArcCollection containing original coordinates
13429
13675
  function getRepairFunction(arcs) {
13430
13676
  var arcsOrig = arcs.getCopy();
@@ -13856,6 +14102,9 @@
13856
14102
  getSliverTest(dataset.arcs, threshold, sliverControl) :
13857
14103
  getMinAreaTest(threshold, dataset);
13858
14104
  var label = getSliverLabel(getAreaLabel(threshold, crs), sliverControl > 0);
14105
+ if (opts.keep_shapes) {
14106
+ filter = keepShapes(filter);
14107
+ }
13859
14108
  return {
13860
14109
  threshold: threshold,
13861
14110
  filter: filter,
@@ -13863,6 +14112,23 @@
13863
14112
  };
13864
14113
  }
13865
14114
 
14115
+ // wrap path filter in a function that ensures at least one part of every shape
14116
+ // is retained.
14117
+ function keepShapes(filter) {
14118
+ var flags;
14119
+ return function(path, pathId, paths) {
14120
+ // console.log("keepShapes()", path, pathId, paths)
14121
+ if (pathId === 0) {
14122
+ flags = paths.map(path => filter(path));
14123
+ }
14124
+ if (flags.length > 0 && flags.every(Boolean)) {
14125
+ // kludge ... assumes that the first path is a valid ring (e.g. not a)
14126
+ flags[0] = false;
14127
+ }
14128
+ return flags[pathId];
14129
+ };
14130
+ }
14131
+
13866
14132
  function getSliverLabel(areaStr, variable) {
13867
14133
  if (variable) {
13868
14134
  areaStr = areaStr.replace(' ', '+ ') + ' variable';
@@ -16508,6 +16774,7 @@
16508
16774
 
16509
16775
  function cleanPath(path, arcs) {
16510
16776
  var nulls = 0;
16777
+ // console.log("[cleanPath()]", path, path?.[0])
16511
16778
  for (var i=0, n=path.length; i<n; i++) {
16512
16779
  if (arcs.arcIsDegenerate(path[i])) {
16513
16780
  nulls++;
@@ -16796,54 +17063,45 @@
16796
17063
  if (changed || opts.rebuild_topology) {
16797
17064
  buildTopology(dataset);
16798
17065
  }
16799
-
16800
- // Clean shapes by removing collapsed arc references, etc.
16801
- // TODO: consider alternative -- avoid creating degenerate arcs
16802
- // in insertCutPoints()
17066
+ // Remove degenerate shapes
17067
+ // Without this step, pathfinder function would encounter dead ends.
16803
17068
  dataset.layers.forEach(function(lyr) {
16804
17069
  if (layerHasPaths(lyr)) {
16805
17070
  cleanShapes(lyr.shapes, arcs, lyr.geometry_type);
16806
17071
  }
16807
17072
  });
16808
-
16809
- // Further clean-up -- remove duplicate and missing arcs
17073
+ // Further clean-up -- remove duplicate and unused arcs, etc.
16810
17074
  nodes = cleanArcReferences(dataset);
16811
17075
  return nodes;
16812
17076
  }
16813
17077
 
16814
17078
  function snapAndCut(dataset, snapDist) {
16815
17079
  var arcs = dataset.arcs;
16816
- var cutOpts = snapDist > 0 ? {} : {tolerance: 0};
17080
+ var cutOpts = snapDist > 0 ? {tolerance: snapDist} : {tolerance: 0};
16817
17081
  var coordsHaveChanged = false;
16818
17082
  var snapCount, dupeCount, cutCount;
16819
- snapCount = snapCoordsByInterval(arcs, snapDist);
16820
- dupeCount = arcs.dedupCoords();
16821
-
16822
- // why was topology built here previously????
16823
- // if (snapCount > 0 || dupeCount > 0) {
16824
- // // Detect topology again if coordinates have changed
16825
- // internal.buildTopology(dataset);
16826
- // }
17083
+ var maxLoops = 4, loopCount = 0;
16827
17084
 
16828
- // cut arcs at points where segments intersect
16829
- cutCount = cutPathsAtIntersections(dataset, cutOpts);
16830
- if (cutCount > 0 || snapCount > 0 || dupeCount > 0) {
16831
- coordsHaveChanged = true;
16832
- }
16833
-
16834
- // perform a second snap + cut pass if needed
16835
- if (cutCount > 0) {
17085
+ // snap + cut until no more cuts are needed
17086
+ do {
17087
+ loopCount++;
16836
17088
  cutCount = 0;
16837
17089
  snapCount = snapCoordsByInterval(arcs, snapDist);
16838
- arcs.dedupCoords(); // need to do this here?
16839
- if (snapCount > 0) {
17090
+ dupeCount = arcs.dedupCoords();
17091
+ if (snapCount > 0 || cutCount > 0 || loopCount == 1) {
17092
+ // cut arcs at points where segments intersect
16840
17093
  cutCount = cutPathsAtIntersections(dataset, cutOpts);
16841
17094
  }
16842
- if (cutCount > 0) {
16843
- arcs.dedupCoords(); // need to do this here?
16844
- debug('Second-pass vertices added:', cutCount, 'consider third pass?');
16845
- }
16846
- }
17095
+ coordsHaveChanged |= (snapCount + dupeCount + cutCount) > 0;
17096
+ debug("[snapAndCut] pass:", loopCount, "snaps:", snapCount, "dupes:", dupeCount, "cuts:", cutCount);
17097
+ } while (loopCount < maxLoops && cutCount > 0);
17098
+
17099
+ // should this be added?
17100
+ // if (cutCount > 0 && loopCount == maxLoops) {
17101
+ // snapCount = snapCoordsByInterval(arcs, snapDist);
17102
+ // arcs.dedupCoords(); // need to do this here?
17103
+ // }
17104
+
16847
17105
  return coordsHaveChanged;
16848
17106
  }
16849
17107
 
@@ -17028,21 +17286,21 @@
17028
17286
  function filterSortedCutPoints(points, arcs) {
17029
17287
  var filtered = [],
17030
17288
  pointId = 0;
17031
- arcs.forEach2(function(i, n, xx, yy) {
17032
- var j = i + n - 1,
17033
- x0 = xx[i],
17034
- y0 = yy[i],
17035
- xn = xx[j],
17036
- yn = yy[j],
17289
+ arcs.forEach2(function(arcId, i, n, xx, yy) {
17290
+ var j = i + n - 1, // idx of second endpoint
17291
+ x1 = xx[i],
17292
+ y1 = yy[i],
17293
+ x2 = xx[j],
17294
+ y2 = yy[j],
17037
17295
  p, pp;
17038
17296
 
17039
17297
  while (pointId < points.length && points[pointId].i <= j) {
17040
17298
  p = points[pointId];
17299
+ pointId++;
17041
17300
  pp = filtered[filtered.length - 1]; // previous point
17042
- if (p.x == x0 && p.y == y0 || p.x == xn && p.y == yn) ; else if (pp && pp.x == p.x && pp.y == p.y && pp.i == p.i) ; else {
17301
+ if (p.x == x1 && p.y == y1 && p.i == i || p.x == x2 && p.y == y2 && p.i >= j - 1) ; else if (pp && pp.x == p.x && pp.y == p.y && pp.i == p.i) ; else {
17043
17302
  filtered.push(p);
17044
17303
  }
17045
- pointId++;
17046
17304
  }
17047
17305
  });
17048
17306
  return filtered;
@@ -18370,6 +18628,7 @@
18370
18628
  return geom;
18371
18629
  };
18372
18630
 
18631
+ // ids: shape data (array of array of arc ids)
18373
18632
  GeoJSON.exportLineGeom = function(ids, arcs) {
18374
18633
  var obj = exportPathData(ids, arcs, "polyline");
18375
18634
  if (obj.pointCount === 0) return null;
@@ -18385,6 +18644,7 @@
18385
18644
  };
18386
18645
  };
18387
18646
 
18647
+ // ids: shape data (array of array of arc ids)
18388
18648
  GeoJSON.exportPolygonGeom = function(ids, arcs, opts) {
18389
18649
  var obj = exportPathData(ids, arcs, "polygon");
18390
18650
  if (obj.pointCount === 0) return null;
@@ -18777,203 +19037,6 @@
18777
19037
  catalog.getDatasets().push(o);
18778
19038
  }
18779
19039
 
18780
- // Apply rotation, scale and/or shift to some or all of the features in a dataset
18781
- //
18782
- cmd.affine = function(targetLayers, dataset, opts) {
18783
- // Need to separate the targeted shapes from any other shapes that share
18784
- // the same topology. So we duplicate any arcs that are shared by the targeted
18785
- // shapes and their topological neighbors and remap arc references in the
18786
- // neighbors to point to the copies.
18787
- // TODO: explore alternative: if some arcs are shared between transformed and
18788
- // non-transformed shapes, first remove topology, then tranform, then rebuild topology
18789
- //
18790
- var rotateArg = opts.rotate || 0;
18791
- var scaleArg = opts.scale || 1;
18792
- var shiftArg = opts.shift ? convertIntervalPair(opts.shift, getDatasetCRS(dataset)) : [0, 0];
18793
- var arcs = dataset.arcs;
18794
- var targetShapes = [];
18795
- var otherShapes = [];
18796
- var targetPoints = [];
18797
- var targetFlags, otherFlags, transform;
18798
- dataset.layers.filter(layerHasGeometry).forEach(function(lyr) {
18799
- var hits = [],
18800
- misses = [],
18801
- test;
18802
- if (targetLayers.indexOf(lyr) == -1) {
18803
- misses = lyr.shapes;
18804
- } else if (opts.where) {
18805
- test = compileFeatureExpression(opts.where, lyr, dataset.arcs);
18806
- lyr.shapes.forEach(function(shp, i) {
18807
- (test(i) ? hits : misses).push(shp);
18808
- });
18809
- } else {
18810
- hits = lyr.shapes;
18811
- }
18812
- if (lyr.geometry_type == 'point') {
18813
- targetPoints = targetPoints.concat(hits);
18814
- } else {
18815
- targetShapes = targetShapes.concat(hits);
18816
- otherShapes = otherShapes.concat(misses);
18817
- }
18818
- });
18819
- var anchorArg = getAffineAnchor({arcs: dataset.arcs, layers: [{
18820
- geometry_type: 'point', shapes: targetPoints}, {geometry_type: 'polyline',
18821
- shapes: targetShapes}]}, opts);
18822
- transform = getAffineTransform(rotateArg, scaleArg, shiftArg, anchorArg);
18823
- if (opts.fit_bbox) {
18824
- transform = getFitBoxTransform(opts.fit_bbox, targetPoints, targetShapes, arcs);
18825
- }
18826
- if (targetShapes.length > 0) {
18827
- targetFlags = new Uint8Array(arcs.size());
18828
- otherFlags = new Uint8Array(arcs.size());
18829
- countArcsInShapes(targetShapes, targetFlags);
18830
- if (otherShapes.length > 0) {
18831
- countArcsInShapes(otherShapes, otherFlags);
18832
- applyArrayMask(otherFlags, targetFlags);
18833
- dataset.arcs = duplicateSelectedArcs(otherShapes, arcs, otherFlags);
18834
- }
18835
- dataset.arcs.transformPoints(function(x, y, arcId) {
18836
- if (arcId < targetFlags.length && targetFlags[arcId] > 0) {
18837
- return transform(x, y);
18838
- }
18839
- });
18840
- }
18841
- forEachPoint(targetPoints, function(p) {
18842
- var p2 = transform(p[0], p[1]);
18843
- p[0] = p2[0];
18844
- p[1] = p2[1];
18845
- });
18846
- };
18847
-
18848
- function getAffineAnchor(dataset, opts) {
18849
- var anchor, bounds;
18850
- if (opts.anchor) {
18851
- anchor = opts.anchor;
18852
- } else {
18853
- // get bounds of selected shapes to calculate center of rotation/scale
18854
- bounds = getDatasetBounds(dataset);
18855
- anchor = [bounds.centerX(), bounds.centerY()];
18856
- }
18857
- return anchor;
18858
- }
18859
-
18860
- // TODO: handle problems with unprojected datasets
18861
- // option 1: don't allow affine transformation of unprojected data
18862
- // option 2: error if transformed data exceeds valid coordinate range
18863
- // source: http://mathworld.wolfram.com/AffineTransformation.html
18864
- function getAffineTransform(rotation, scale, shift, anchor) {
18865
- var angle = rotation * Math.PI / 180;
18866
- var a = scale * Math.cos(angle);
18867
- var b = -scale * Math.sin(angle);
18868
- return function(x, y) {
18869
- var x2 = a * (x - anchor[0]) - b * (y - anchor[1]) + shift[0] + anchor[0];
18870
- var y2 = b * (x - anchor[0]) + a * (y - anchor[1]) + shift[1] + anchor[1];
18871
- return [x2, y2];
18872
- };
18873
- }
18874
-
18875
- function getFitBoxTransform(bbox, points, shapes, arcs) {
18876
- var dataset = {
18877
- arcs: arcs,
18878
- layers: []
18879
- };
18880
- if (points && points.length) {
18881
- dataset.layers.push({
18882
- geometry_type: 'point',
18883
- shapes: points
18884
- });
18885
- }
18886
- if (shapes && shapes.length) {
18887
- dataset.layers.push({
18888
- geometry_type: 'polyline',
18889
- shapes: shapes
18890
- });
18891
- }
18892
- var frame = calcFrameData(dataset, {fit_bbox: bbox});
18893
- var fromBounds = new Bounds(frame.bbox);
18894
- var toBounds = new Bounds(frame.bbox2);
18895
- var fwd = fromBounds.getTransform(toBounds, false);
18896
- return function(x, y) {
18897
- return fwd.transform(x, y);
18898
- };
18899
- }
18900
-
18901
- function applyArrayMask(destArr, maskArr) {
18902
- for (var i=0, n=destArr.length; i<n; i++) {
18903
- if (maskArr[i] === 0) destArr[i] = 0;
18904
- }
18905
- }
18906
-
18907
- function duplicateSelectedArcs(shapes, arcs, flags) {
18908
- var arcCount = 0;
18909
- var vertexCount = 0;
18910
- var data = arcs.getVertexData();
18911
- var xx = [], yy = [], nn = [], map = [], n;
18912
- for (var i=0, len=flags.length; i<len; i++) {
18913
- if (flags[i] > 0) {
18914
- map[i] = arcs.size() + arcCount;
18915
- n = data.nn[i];
18916
- utils.copyElements(data.xx, data.ii[i], xx, vertexCount, n);
18917
- utils.copyElements(data.yy, data.ii[i], yy, vertexCount, n);
18918
- nn.push(n);
18919
- vertexCount += n;
18920
- arcCount++;
18921
- }
18922
- }
18923
- forEachArcId(shapes, function(id) {
18924
- var absId = absArcId(id);
18925
- if (flags[absId] > 0) {
18926
- return id < 0 ? ~map[absId] : map[absId];
18927
- }
18928
- });
18929
- return mergeArcs([arcs, new ArcCollection(nn, xx, yy)]);
18930
- }
18931
-
18932
- var roundCoord$2 = getRoundingFunction(0.01);
18933
-
18934
- function stringifyVertex(p) {
18935
- return ' ' + roundCoord$2(p[0]) + ' ' + roundCoord$2(p[1]);
18936
- }
18937
-
18938
- function isCubicCtrl(p) {
18939
- return p.length > 2 && p[2] == 'C';
18940
- }
18941
-
18942
- function stringifyPolygonCoords(coords) {
18943
- var parts = [];
18944
- for (var i=0; i<coords.length; i++) {
18945
- parts.push(stringifyLineStringCoords(coords[i]) + ' Z');
18946
- }
18947
- return parts.length > 0 ? parts.join(' ') : '';
18948
- }
18949
-
18950
- function stringifyLineStringCoords(coords) {
18951
- if (coords.length === 0) return '';
18952
- var d = 'M';
18953
- var fromCurve = false;
18954
- var p, i, n;
18955
- for (i=0, n=coords.length; i<n; i++) {
18956
- p = coords[i];
18957
- if (isCubicCtrl(p)) {
18958
- // TODO: add defensive check
18959
- d += ' C' + stringifyVertex(p) + stringifyVertex(coords[++i]) + stringifyVertex(coords[++i]);
18960
- fromCurve = true;
18961
- } else if (fromCurve) {
18962
- d += ' L' + stringifyVertex(p);
18963
- fromCurve = false;
18964
- } else {
18965
- d += stringifyVertex(p);
18966
- }
18967
- }
18968
- return d;
18969
- }
18970
-
18971
- var SvgPathUtils = /*#__PURE__*/Object.freeze({
18972
- __proto__: null,
18973
- stringifyLineStringCoords: stringifyLineStringCoords,
18974
- stringifyPolygonCoords: stringifyPolygonCoords
18975
- });
18976
-
18977
19040
  /* example patterns
18978
19041
  hatches 1px black 1px red 1px white
18979
19042
  1px black 1px red 1px white // same as above (hatches is default)
@@ -19562,7 +19625,7 @@
19562
19625
  }
19563
19626
 
19564
19627
  function importLineString(coords) {
19565
- var d = stringifyLineStringCoords(coords);
19628
+ var d = stringifyLineStringCoords$1(coords);
19566
19629
  return {
19567
19630
  tag: 'path',
19568
19631
  properties: {d: d}
@@ -19570,7 +19633,7 @@
19570
19633
  }
19571
19634
 
19572
19635
  function importMultiLineString(coords) {
19573
- var d = coords.map(stringifyLineStringCoords).join(' ');
19636
+ var d = coords.map(stringifyLineStringCoords$1).join(' ');
19574
19637
  return {
19575
19638
  tag: 'path',
19576
19639
  properties: {d: d}
@@ -19592,7 +19655,7 @@
19592
19655
  var o = {
19593
19656
  tag: 'path',
19594
19657
  properties: {
19595
- d: stringifyPolygonCoords(coords)
19658
+ d: stringifyPolygonCoords$1(coords)
19596
19659
  }
19597
19660
  };
19598
19661
  if (coords.length > 1) {
@@ -19601,6 +19664,23 @@
19601
19664
  return o;
19602
19665
  }
19603
19666
 
19667
+ function stringifyPolygonCoords$1(coords) {
19668
+ var parts = [];
19669
+ for (var i=0; i<coords.length; i++) {
19670
+ parts.push(stringifyLineStringCoords$1(coords[i]) + ' Z');
19671
+ }
19672
+ return parts.length > 0 ? parts.join(' ') : '';
19673
+ }
19674
+
19675
+ function stringifyLineStringCoords$1(coords) {
19676
+ if (coords.length === 0) return '';
19677
+ var d = 'M';
19678
+ for (var i=0, n=coords.length; i<n; i++) {
19679
+ d += ' ' + coords[i][0] + ' ' + coords[i][1];
19680
+ }
19681
+ return d;
19682
+ }
19683
+
19604
19684
  var GeojsonToSvg = /*#__PURE__*/Object.freeze({
19605
19685
  __proto__: null,
19606
19686
  flattenMultiPolygonCoords: flattenMultiPolygonCoords,
@@ -20490,7 +20570,7 @@
20490
20570
  opts = Object.assign({invert_y: true, margin: "1"}, opts);
20491
20571
  frame = getFrameData(dataset, opts);
20492
20572
  fitDatasetToFrame(dataset, frame);
20493
- setCoordinatePrecision(dataset, opts.precision || 0.0001);
20573
+ setCoordinatePrecision(dataset, opts.precision || 0.01);
20494
20574
 
20495
20575
  // error if one or more svg_data fields are not present in any layers
20496
20576
  if (opts.svg_data) validateSvgDataFields(dataset.layers, opts.svg_data);
@@ -22246,7 +22326,7 @@ ${svg}
22246
22326
  if (opts.presimplify) {
22247
22327
  fromZ = getPresimplifyFunction(bounds.width());
22248
22328
  }
22249
- arcs.forEach2(function(i, n, xx, yy, zz) {
22329
+ arcs.forEach2(function(arcId, i, n, xx, yy, zz) {
22250
22330
  var arc = [], p;
22251
22331
  for (var j=i + n; i<j; i++) {
22252
22332
  p = [xx[i], yy[i]];
@@ -24842,6 +24922,11 @@ ${svg}
24842
24922
  .option('sliver-control', sliverControlOpt)
24843
24923
  .option('snap-interval', snapIntervalOpt)
24844
24924
  .option('no-snap', noSnapOpt)
24925
+ // // not useful in -clean -- clean removes gaps, not slivers
24926
+ // .option('keep-shapes', {
24927
+ // type: 'flag',
24928
+ // describe: 'protect sliver polygons from complete removal'
24929
+ // })
24845
24930
  .option('allow-overlaps', {
24846
24931
  describe: 'allow polygons to overlap (disables gap fill)',
24847
24932
  type: 'flag'
@@ -24893,6 +24978,10 @@ ${svg}
24893
24978
  })
24894
24979
  .option('name', nameOpt)
24895
24980
  .option('no-snap', noSnapOpt)
24981
+ .option('no-cleanup', {
24982
+ // for debugging - no documentation
24983
+ type: 'flag'
24984
+ })
24896
24985
  .option('target', targetOpt)
24897
24986
  .option('no-replace', noReplaceOpt);
24898
24987
 
@@ -25128,6 +25217,10 @@ ${svg}
25128
25217
  .option('bbox', bboxOpt)
25129
25218
  .option('name', nameOpt)
25130
25219
  .option('no-snap', noSnapOpt)
25220
+ .option('no-cleanup', {
25221
+ // for debugging - no documentation
25222
+ type: 'flag'
25223
+ })
25131
25224
  .option('target', targetOpt)
25132
25225
  .option('no-replace', noReplaceOpt);
25133
25226
 
@@ -25238,6 +25331,10 @@ ${svg}
25238
25331
  type: 'flag',
25239
25332
  describe: 'delete features with null geometry'
25240
25333
  })
25334
+ .option('keep-shapes', {
25335
+ type: 'flag',
25336
+ describe: 'protect sliver polygons from complete removal'
25337
+ })
25241
25338
  .option('target', targetOpt);
25242
25339
 
25243
25340
  parser.command('graticule')
@@ -25445,9 +25542,8 @@ ${svg}
25445
25542
  .option('calc', calcOpt)
25446
25543
  .option('name', nameOpt)
25447
25544
  .option('target', targetOpt)
25448
- .option('no-snap', { // undocumented, for debugging
25449
- type: 'flag'
25450
- })
25545
+ .option('snap-interval', snapIntervalOpt)
25546
+ .option('no-snap', noSnapOpt)
25451
25547
  .option('no-replace', noReplaceOpt);
25452
25548
 
25453
25549
  parser.command('point-grid')
@@ -29843,6 +29939,158 @@ ${svg}
29843
29939
  stop('Unable to import coordinates');
29844
29940
  }
29845
29941
 
29942
+ // Apply rotation, scale and/or shift to some or all of the features in a dataset
29943
+ //
29944
+ cmd.affine = function(targetLayers, dataset, opts) {
29945
+ // Need to separate the targeted shapes from any other shapes that share
29946
+ // the same topology. So we duplicate any arcs that are shared by the targeted
29947
+ // shapes and their topological neighbors and remap arc references in the
29948
+ // neighbors to point to the copies.
29949
+ // TODO: explore alternative: if some arcs are shared between transformed and
29950
+ // non-transformed shapes, first remove topology, then tranform, then rebuild topology
29951
+ //
29952
+ var rotateArg = opts.rotate || 0;
29953
+ var scaleArg = opts.scale || 1;
29954
+ var shiftArg = opts.shift ? convertIntervalPair(opts.shift, getDatasetCRS(dataset)) : [0, 0];
29955
+ var arcs = dataset.arcs;
29956
+ var targetShapes = [];
29957
+ var otherShapes = [];
29958
+ var targetPoints = [];
29959
+ var targetFlags, otherFlags, transform;
29960
+ dataset.layers.filter(layerHasGeometry).forEach(function(lyr) {
29961
+ var hits = [],
29962
+ misses = [],
29963
+ test;
29964
+ if (targetLayers.indexOf(lyr) == -1) {
29965
+ misses = lyr.shapes;
29966
+ } else if (opts.where) {
29967
+ test = compileFeatureExpression(opts.where, lyr, dataset.arcs);
29968
+ lyr.shapes.forEach(function(shp, i) {
29969
+ (test(i) ? hits : misses).push(shp);
29970
+ });
29971
+ } else {
29972
+ hits = lyr.shapes;
29973
+ }
29974
+ if (lyr.geometry_type == 'point') {
29975
+ targetPoints = targetPoints.concat(hits);
29976
+ } else {
29977
+ targetShapes = targetShapes.concat(hits);
29978
+ otherShapes = otherShapes.concat(misses);
29979
+ }
29980
+ });
29981
+ var anchorArg = getAffineAnchor({arcs: dataset.arcs, layers: [{
29982
+ geometry_type: 'point', shapes: targetPoints}, {geometry_type: 'polyline',
29983
+ shapes: targetShapes}]}, opts);
29984
+ transform = getAffineTransform(rotateArg, scaleArg, shiftArg, anchorArg);
29985
+ if (opts.fit_bbox) {
29986
+ transform = getFitBoxTransform(opts.fit_bbox, targetPoints, targetShapes, arcs);
29987
+ }
29988
+ if (targetShapes.length > 0) {
29989
+ targetFlags = new Uint8Array(arcs.size());
29990
+ otherFlags = new Uint8Array(arcs.size());
29991
+ countArcsInShapes(targetShapes, targetFlags);
29992
+ if (otherShapes.length > 0) {
29993
+ countArcsInShapes(otherShapes, otherFlags);
29994
+ applyArrayMask(otherFlags, targetFlags);
29995
+ dataset.arcs = duplicateSelectedArcs(otherShapes, arcs, otherFlags);
29996
+ }
29997
+ dataset.arcs.transformPoints(function(x, y, arcId) {
29998
+ if (arcId < targetFlags.length && targetFlags[arcId] > 0) {
29999
+ return transform(x, y);
30000
+ }
30001
+ });
30002
+ }
30003
+ forEachPoint(targetPoints, function(p) {
30004
+ var p2 = transform(p[0], p[1]);
30005
+ p[0] = p2[0];
30006
+ p[1] = p2[1];
30007
+ });
30008
+ };
30009
+
30010
+ function getAffineAnchor(dataset, opts) {
30011
+ var anchor, bounds;
30012
+ if (opts.anchor) {
30013
+ anchor = opts.anchor;
30014
+ } else {
30015
+ // get bounds of selected shapes to calculate center of rotation/scale
30016
+ bounds = getDatasetBounds(dataset);
30017
+ anchor = [bounds.centerX(), bounds.centerY()];
30018
+ }
30019
+ return anchor;
30020
+ }
30021
+
30022
+ // TODO: handle problems with unprojected datasets
30023
+ // option 1: don't allow affine transformation of unprojected data
30024
+ // option 2: error if transformed data exceeds valid coordinate range
30025
+ // source: http://mathworld.wolfram.com/AffineTransformation.html
30026
+ function getAffineTransform(rotation, scale, shift, anchor) {
30027
+ var angle = rotation * Math.PI / 180;
30028
+ var a = scale * Math.cos(angle);
30029
+ var b = -scale * Math.sin(angle);
30030
+ return function(x, y) {
30031
+ var x2 = a * (x - anchor[0]) - b * (y - anchor[1]) + shift[0] + anchor[0];
30032
+ var y2 = b * (x - anchor[0]) + a * (y - anchor[1]) + shift[1] + anchor[1];
30033
+ return [x2, y2];
30034
+ };
30035
+ }
30036
+
30037
+ function getFitBoxTransform(bbox, points, shapes, arcs) {
30038
+ var dataset = {
30039
+ arcs: arcs,
30040
+ layers: []
30041
+ };
30042
+ if (points && points.length) {
30043
+ dataset.layers.push({
30044
+ geometry_type: 'point',
30045
+ shapes: points
30046
+ });
30047
+ }
30048
+ if (shapes && shapes.length) {
30049
+ dataset.layers.push({
30050
+ geometry_type: 'polyline',
30051
+ shapes: shapes
30052
+ });
30053
+ }
30054
+ var frame = calcFrameData(dataset, {fit_bbox: bbox});
30055
+ var fromBounds = new Bounds(frame.bbox);
30056
+ var toBounds = new Bounds(frame.bbox2);
30057
+ var fwd = fromBounds.getTransform(toBounds, false);
30058
+ return function(x, y) {
30059
+ return fwd.transform(x, y);
30060
+ };
30061
+ }
30062
+
30063
+ function applyArrayMask(destArr, maskArr) {
30064
+ for (var i=0, n=destArr.length; i<n; i++) {
30065
+ if (maskArr[i] === 0) destArr[i] = 0;
30066
+ }
30067
+ }
30068
+
30069
+ function duplicateSelectedArcs(shapes, arcs, flags) {
30070
+ var arcCount = 0;
30071
+ var vertexCount = 0;
30072
+ var data = arcs.getVertexData();
30073
+ var xx = [], yy = [], nn = [], map = [], n;
30074
+ for (var i=0, len=flags.length; i<len; i++) {
30075
+ if (flags[i] > 0) {
30076
+ map[i] = arcs.size() + arcCount;
30077
+ n = data.nn[i];
30078
+ utils.copyElements(data.xx, data.ii[i], xx, vertexCount, n);
30079
+ utils.copyElements(data.yy, data.ii[i], yy, vertexCount, n);
30080
+ nn.push(n);
30081
+ vertexCount += n;
30082
+ arcCount++;
30083
+ }
30084
+ }
30085
+ forEachArcId(shapes, function(id) {
30086
+ var absId = absArcId(id);
30087
+ if (flags[absId] > 0) {
30088
+ return id < 0 ? ~map[absId] : map[absId];
30089
+ }
30090
+ });
30091
+ return mergeArcs([arcs, new ArcCollection(nn, xx, yy)]);
30092
+ }
30093
+
29846
30094
  const epsilon = 1.1102230246251565e-16;
29847
30095
  const splitter = 134217729;
29848
30096
  const resulterrbound = (3 + 8 * epsilon) * epsilon;
@@ -34876,7 +35124,7 @@ ${svg}
34876
35124
  var ringTest = filterData.filter;
34877
35125
  var removed = 0;
34878
35126
  var pathFilter = function(path, i, paths) {
34879
- if (ringTest(path)) {
35127
+ if (ringTest(path, i, paths)) {
34880
35128
  removed++;
34881
35129
  return null;
34882
35130
  }
@@ -35016,6 +35264,7 @@ ${svg}
35016
35264
  // or overlapping rings. TODO: try to optimize or remove it for all cases
35017
35265
 
35018
35266
  // skipping shape cleanup when using the experimental fast bbox clipping option
35267
+ // if (!opts.bbox2 && !opts.no_cleanup) {
35019
35268
  if (!opts.bbox2) {
35020
35269
  // clean each target polygon by dissolving its rings
35021
35270
  targetShapes = targetShapes.map(dissolvePolygon);
@@ -35512,7 +35761,7 @@ ${svg}
35512
35761
  } else {
35513
35762
  nodes = new NodeCollection(mergedDataset.arcs);
35514
35763
  }
35515
- // clipLyr = mergedDataset.layers.pop();
35764
+
35516
35765
  return clipLayersByLayer(targetLayers, clipLyr, nodes, type, opts);
35517
35766
  }
35518
35767
 
@@ -40504,7 +40753,7 @@ ${svg}
40504
40753
 
40505
40754
  function createMeridianPart(x, ymin, ymax) {
40506
40755
  var coords = densifyPathByInterval([[x, ymin], [x, ymax]], precision);
40507
- meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord$1(x)}));
40756
+ meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord$2(x)}));
40508
40757
  }
40509
40758
 
40510
40759
  function createParallel(y) {
@@ -40514,7 +40763,7 @@ ${svg}
40514
40763
  }
40515
40764
 
40516
40765
  // remove tiny offsets
40517
- function roundCoord$1(x) {
40766
+ function roundCoord$2(x) {
40518
40767
  return +x.toFixed(3) || 0;
40519
40768
  }
40520
40769
 
@@ -44613,7 +44862,7 @@ ${svg}
44613
44862
  });
44614
44863
  };
44615
44864
 
44616
- var roundCoord = getRoundingFunction(0.01);
44865
+ var roundCoord$1 = getRoundingFunction(0.01);
44617
44866
 
44618
44867
  function getSymbolFillColor(d) {
44619
44868
  return d.fill || 'magenta';
@@ -44664,8 +44913,8 @@ ${svg}
44664
44913
 
44665
44914
  function roundCoordsForSVG(coords) {
44666
44915
  forEachSymbolCoord(coords, function(p) {
44667
- p[0] = roundCoord(p[0]);
44668
- p[1] = roundCoord(p[1]);
44916
+ p[0] = roundCoord$1(p[0]);
44917
+ p[1] = roundCoord$1(p[1]);
44669
44918
  });
44670
44919
  }
44671
44920
 
@@ -45390,7 +45639,7 @@ ${svg}
45390
45639
  // Filter arcs based on an array of thresholds
45391
45640
  function applyArcThresholds(arcs, thresholds) {
45392
45641
  arcs.getVertexData().zz;
45393
- arcs.forEach2(function(start, n, xx, yy, zz, arcId) {
45642
+ arcs.forEach2(function(arcId, start, n, xx, yy, zz) {
45394
45643
  var arcZ = thresholds[arcId];
45395
45644
  var z;
45396
45645
  for (var i=1; i<n-1; i++) {
@@ -46044,7 +46293,7 @@ ${svg}
46044
46293
  });
46045
46294
  }
46046
46295
 
46047
- var version = "0.6.109";
46296
+ var version = "0.6.111";
46048
46297
 
46049
46298
  // Parse command line args into commands and run them
46050
46299
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -46376,6 +46625,51 @@ ${svg}
46376
46625
  testCommands: testCommands
46377
46626
  });
46378
46627
 
46628
+ var roundCoord = getRoundingFunction(0.01);
46629
+
46630
+ function stringifyVertex(p) {
46631
+ return ' ' + roundCoord(p[0]) + ' ' + roundCoord(p[1]);
46632
+ }
46633
+
46634
+ function isCubicCtrl(p) {
46635
+ return p.length > 2 && p[2] == 'C';
46636
+ }
46637
+
46638
+ function stringifyPolygonCoords(coords) {
46639
+ var parts = [];
46640
+ for (var i=0; i<coords.length; i++) {
46641
+ parts.push(stringifyLineStringCoords(coords[i]) + ' Z');
46642
+ }
46643
+ return parts.length > 0 ? parts.join(' ') : '';
46644
+ }
46645
+
46646
+ function stringifyLineStringCoords(coords) {
46647
+ if (coords.length === 0) return '';
46648
+ var d = 'M';
46649
+ var fromCurve = false;
46650
+ var p, i, n;
46651
+ for (i=0, n=coords.length; i<n; i++) {
46652
+ p = coords[i];
46653
+ if (isCubicCtrl(p)) {
46654
+ // TODO: add defensive check
46655
+ d += ' C' + stringifyVertex(p) + stringifyVertex(coords[++i]) + stringifyVertex(coords[++i]);
46656
+ fromCurve = true;
46657
+ } else if (fromCurve) {
46658
+ d += ' L' + stringifyVertex(p);
46659
+ fromCurve = false;
46660
+ } else {
46661
+ d += stringifyVertex(p);
46662
+ }
46663
+ }
46664
+ return d;
46665
+ }
46666
+
46667
+ var SvgPathUtils = /*#__PURE__*/Object.freeze({
46668
+ __proto__: null,
46669
+ stringifyLineStringCoords: stringifyLineStringCoords,
46670
+ stringifyPolygonCoords: stringifyPolygonCoords
46671
+ });
46672
+
46379
46673
  // Return an array containing points from a path iterator, clipped to a bounding box
46380
46674
  // Currently using this function for clipping styled polygons in the GUI to speed up layer rendering.
46381
46675
  // Artifacts along the edges make this unsuitable for clipping datasets