mapshaper 0.6.117 → 0.6.119

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/www/mapshaper.js CHANGED
@@ -19,7 +19,7 @@
19
19
  get expandoBuffer () { return expandoBuffer; },
20
20
  get extend () { return extend$1; },
21
21
  get extendBuffer () { return extendBuffer; },
22
- get find () { return find; },
22
+ get find () { return find$1; },
23
23
  get findMedian () { return findMedian; },
24
24
  get findQuantile () { return findQuantile; },
25
25
  get findRankByValue () { return findRankByValue; },
@@ -442,7 +442,7 @@
442
442
  }, true);
443
443
  }
444
444
 
445
- function find(arr, test, ctx) {
445
+ function find$1(arr, test, ctx) {
446
446
  var matches = arr.filter(test, ctx);
447
447
  return matches.length === 0 ? null : matches[0];
448
448
  }
@@ -3208,6 +3208,25 @@
3208
3208
 
3209
3209
  //import { findCrossIntersection_big } from '../geom/mapshaper-segment-geom-big';
3210
3210
 
3211
+ // Ad-hoc counters (reset by segmentIntersectionStatsReset, inspected by
3212
+ // segmentIntersectionStats). Cheap when unused, so safe to leave in.
3213
+ var STATS = {
3214
+ calls: 0, // segmentIntersection() invocations
3215
+ touches: 0, // pairs that returned a T-touch result
3216
+ endpointHits: 0, // pairs that returned null via testEndpointHit
3217
+ crossCandidates: 0, // pairs that reached findCrossIntersection()
3218
+ crossRejectedFast: 0,// rejected by segmentHit_fast early-out
3219
+ crossRobust: 0, // useRobustCross() === true, ran BigInt math
3220
+ crossFast: 0, // useRobustCross() === false, ran fp math
3221
+ crossNull: 0 // findCrossIntersection returned null (e.g. collinear)
3222
+ };
3223
+ function segmentIntersectionStatsReset() {
3224
+ for (var k in STATS) STATS[k] = 0;
3225
+ }
3226
+ function segmentIntersectionStats() {
3227
+ return Object.assign({}, STATS);
3228
+ }
3229
+
3211
3230
  // Find the intersection between two 2D segments
3212
3231
  // Returns 0, 1 or 2 [x, y] locations as null, [x, y], or [x1, y1, x2, y2]
3213
3232
  // Special cases:
@@ -3217,6 +3236,7 @@
3217
3236
  // is counted as an intersection (there will be either one or two)
3218
3237
  //
3219
3238
  function segmentIntersection(ax, ay, bx, by, cx, cy, dx, dy, epsArg) {
3239
+ STATS.calls++;
3220
3240
  // Use a small tolerance interval, so collinear segments and T-intersections
3221
3241
  // are detected (floating point rounding often causes exact functions to fail)
3222
3242
  var eps = epsArg > 0 ? epsArg :
@@ -3230,8 +3250,10 @@
3230
3250
  // segments that share an endpoint. Two touches indicates overlapping
3231
3251
  // collinear segments that do not share an endpoint.
3232
3252
  touches = findPointSegTouches(epsSq, ax, ay, bx, by, cx, cy, dx, dy);
3253
+ if (touches) STATS.touches++;
3233
3254
  // Ignore endpoint-only intersections
3234
3255
  if (!touches && testEndpointHit(epsSq, ax, ay, bx, by, cx, cy, dx, dy)) {
3256
+ STATS.endpointHits++;
3235
3257
  return null;
3236
3258
  }
3237
3259
  // Detect cross intersection
@@ -3243,6 +3265,7 @@
3243
3265
  }
3244
3266
 
3245
3267
  function findCrossIntersection(ax, ay, bx, by, cx, cy, dx, dy, eps) {
3268
+ STATS.crossCandidates++;
3246
3269
  var p;
3247
3270
  // The normal-precision hit function works for all inputs when eps > 0 because
3248
3271
  // the geometries that cause the ordinary function fails are detected as
@@ -3250,8 +3273,10 @@
3250
3273
  // data samples that were tested).
3251
3274
  //
3252
3275
  if (eps > 0 && !segmentHit_fast(ax, ay, bx, by, cx, cy, dx, dy)) {
3276
+ STATS.crossRejectedFast++;
3253
3277
  return null;
3254
3278
  } else if (eps === 0 && !segmentHit_robust(ax, ay, bx, by, cx, cy, dx, dy)) {
3279
+ STATS.crossRejectedFast++;
3255
3280
  return null;
3256
3281
  }
3257
3282
 
@@ -3260,17 +3285,13 @@
3260
3285
  // the positional error within a small interval (e.g. 50% of eps)
3261
3286
  //
3262
3287
  if (useRobustCross(ax, ay, bx, by, cx, cy, dx, dy)) {
3288
+ STATS.crossRobust++;
3263
3289
  p = findCrossIntersection_robust(ax, ay, bx, by, cx, cy, dx, dy);
3264
- // var p2 = findCrossIntersection_big(ax, ay, bx, by, cx, cy, dx, dy);
3265
- // var dx = p[0] - p2[0];
3266
- // var dy = p[1] - p2[1];
3267
- // if (dx != 0 || dy != 0) {
3268
- // console.log(dx, dy)
3269
- // }
3270
3290
  } else {
3291
+ STATS.crossFast++;
3271
3292
  p = findCrossIntersection_fast(ax, ay, bx, by, cx, cy, dx, dy);
3272
3293
  }
3273
- if (!p) return null;
3294
+ if (!p) { STATS.crossNull++; return null; }
3274
3295
 
3275
3296
  // Snap p to a vertex if very close to one
3276
3297
  // This avoids tiny segments caused by T-intersection overshoots and prevents
@@ -3590,6 +3611,8 @@
3590
3611
  orient2D_robust: orient2D_robust,
3591
3612
  segmentHit_fast: segmentHit_fast,
3592
3613
  segmentIntersection: segmentIntersection,
3614
+ segmentIntersectionStats: segmentIntersectionStats,
3615
+ segmentIntersectionStatsReset: segmentIntersectionStatsReset,
3593
3616
  segmentTurn: segmentTurn
3594
3617
  });
3595
3618
 
@@ -6399,41 +6422,52 @@
6399
6422
  };
6400
6423
  }
6401
6424
 
6402
- // Used for building topology
6425
+ // Used for building topology.
6426
+ //
6427
+ // Every arc is stored as a reference into some source coordinate arrays
6428
+ // (srcXX/srcYY) plus an inclusive [start..end] index range. The caller
6429
+ // decides which buffer to hand in: the shared xx/yy for normal arcs, or
6430
+ // a freshly-allocated merged buffer for split/wrap-around arcs. ArcIndex
6431
+ // doesn't care which — no sentinels, no special cases in its own code.
6403
6432
  //
6404
6433
  function ArcIndex(pointCount) {
6405
6434
  var hashTableSize = Math.floor(pointCount * 0.25 + 1),
6406
6435
  hash = getXYHash(hashTableSize),
6407
6436
  hashTable = new Int32Array(hashTableSize),
6408
6437
  chainIds = [],
6409
- arcs = [],
6438
+ arcSrcXX = [],
6439
+ arcSrcYY = [],
6440
+ arcStart = [],
6441
+ arcEnd = [],
6410
6442
  arcPoints = 0;
6411
6443
 
6412
- utils.initializeArray(hashTable, -1);
6444
+ initializeArray(hashTable, -1);
6413
6445
 
6414
- this.addArc = function(xx, yy) {
6415
- var end = xx.length - 1,
6416
- key = hash(xx[end], yy[end]),
6417
- chainId = hashTable[key],
6418
- arcId = arcs.length;
6446
+ // Register an arc whose coordinates are `srcXX[start..end]`, `srcYY[start..end]`
6447
+ // (inclusive). Returns the new arc id.
6448
+ this.addArc = function(srcXX, srcYY, start, end) {
6449
+ var arcId = arcSrcXX.length,
6450
+ key = hash(srcXX[end], srcYY[end]);
6451
+ chainIds.push(hashTable[key]);
6419
6452
  hashTable[key] = arcId;
6420
- arcs.push([xx, yy]);
6421
- arcPoints += xx.length;
6422
- chainIds.push(chainId);
6453
+ arcSrcXX.push(srcXX);
6454
+ arcSrcYY.push(srcYY);
6455
+ arcStart.push(start);
6456
+ arcEnd.push(end);
6457
+ arcPoints += end - start + 1;
6423
6458
  return arcId;
6424
6459
  };
6425
6460
 
6426
- // Look for a previously generated arc with the same sequence of coords, but in the
6427
- // opposite direction. (This program uses the convention of CW for space-enclosing rings, CCW for holes,
6428
- // so coincident boundaries should contain the same points in reverse sequence).
6461
+ // Look for a previously generated arc with the same sequence of coords, but
6462
+ // in the opposite direction. (This program uses the convention of CW for
6463
+ // space-enclosing rings, CCW for holes, so coincident boundaries should
6464
+ // contain the same points in reverse sequence.)
6429
6465
  //
6430
6466
  this.findDuplicateArc = function(xx, yy, start, end, getNext, getPrev) {
6431
- // First, look for a reverse match
6432
6467
  var arcId = findArcNeighbor(xx, yy, start, end, getNext);
6433
6468
  if (arcId === null) {
6434
- // Look for forward match
6435
- // (Abnormal topology, but we're accepting it because in-the-wild
6436
- // Shapefiles sometimes have duplicate paths)
6469
+ // Look for forward match (abnormal topology, but we accept it because
6470
+ // in-the-wild Shapefiles sometimes have duplicate paths).
6437
6471
  arcId = findArcNeighbor(xx, yy, end, start, getPrev);
6438
6472
  } else {
6439
6473
  arcId = ~arcId;
@@ -6445,17 +6479,14 @@
6445
6479
  var next = getNext(start),
6446
6480
  key = hash(xx[start], yy[start]),
6447
6481
  arcId = hashTable[key],
6448
- arcX, arcY, len;
6449
-
6482
+ sx, sy, s, e;
6450
6483
  while (arcId != -1) {
6451
- // check endpoints and one segment...
6452
- // it would be more rigorous but slower to identify a match
6453
- // by comparing all segments in the coordinate sequence
6454
- arcX = arcs[arcId][0];
6455
- arcY = arcs[arcId][1];
6456
- len = arcX.length;
6457
- if (arcX[0] === xx[end] && arcX[len-1] === xx[start] && arcX[len-2] === xx[next] &&
6458
- arcY[0] === yy[end] && arcY[len-1] === yy[start] && arcY[len-2] === yy[next]) {
6484
+ sx = arcSrcXX[arcId]; sy = arcSrcYY[arcId];
6485
+ s = arcStart[arcId]; e = arcEnd[arcId];
6486
+ // Check endpoints and one segment. A more rigorous match would compare
6487
+ // every segment, but that's slower and this is sufficient in practice.
6488
+ if (sx[s] === xx[end] && sx[e] === xx[start] && sx[e - 1] === xx[next] &&
6489
+ sy[s] === yy[end] && sy[e] === yy[start] && sy[e - 1] === yy[next]) {
6459
6490
  return arcId;
6460
6491
  }
6461
6492
  arcId = chainIds[arcId];
@@ -6464,22 +6495,31 @@
6464
6495
  }
6465
6496
 
6466
6497
  this.getVertexData = function() {
6467
- var xx = new Float64Array(arcPoints),
6468
- yy = new Float64Array(arcPoints),
6469
- nn = new Uint32Array(arcs.length),
6498
+ var arcCount = arcSrcXX.length,
6499
+ destXX = new Float64Array(arcPoints),
6500
+ destYY = new Float64Array(arcPoints),
6501
+ nn = new Uint32Array(arcCount),
6470
6502
  copied = 0,
6471
- arc, len;
6472
- for (var i=0, n=arcs.length; i<n; i++) {
6473
- arc = arcs[i];
6474
- len = arc[0].length;
6475
- utils.copyElements(arc[0], 0, xx, copied, len);
6476
- utils.copyElements(arc[1], 0, yy, copied, len);
6503
+ sx, sy, s, e, len, i;
6504
+ for (i = 0; i < arcCount; i++) {
6505
+ sx = arcSrcXX[i]; sy = arcSrcYY[i];
6506
+ s = arcStart[i]; e = arcEnd[i];
6507
+ len = e - s + 1;
6508
+ if (sx.subarray) {
6509
+ destXX.set(sx.subarray(s, e + 1), copied);
6510
+ destYY.set(sy.subarray(s, e + 1), copied);
6511
+ } else {
6512
+ for (var k = 0; k < len; k++) {
6513
+ destXX[copied + k] = sx[s + k];
6514
+ destYY[copied + k] = sy[s + k];
6515
+ }
6516
+ }
6477
6517
  nn[i] = len;
6478
6518
  copied += len;
6479
6519
  }
6480
6520
  return {
6481
- xx: xx,
6482
- yy: yy,
6521
+ xx: destXX,
6522
+ yy: destYY,
6483
6523
  nn: nn
6484
6524
  };
6485
6525
  };
@@ -6578,18 +6618,15 @@
6578
6618
  var pointCount = xx.length,
6579
6619
  chainIds = initPointChains(xx, yy),
6580
6620
  pathIds = initPathIds(pointCount, nn),
6621
+ pathIsRing = initPathIsRing(nn, xx, yy),
6622
+ isNode = computeIsNode(nn, xx, yy, chainIds, pathIds, pathIsRing),
6581
6623
  index = new ArcIndex(pointCount),
6582
- slice = usingTypedArrays() ? xx.subarray : Array.prototype.slice,
6583
6624
  paths, retn;
6584
6625
  paths = convertPaths(nn);
6585
6626
  retn = index.getVertexData();
6586
6627
  retn.paths = paths;
6587
6628
  return retn;
6588
6629
 
6589
- function usingTypedArrays() {
6590
- return !!(xx.subarray && yy.subarray);
6591
- }
6592
-
6593
6630
  function convertPaths(nn) {
6594
6631
  var paths = [],
6595
6632
  pointId = 0,
@@ -6602,28 +6639,28 @@
6602
6639
  return paths;
6603
6640
  }
6604
6641
 
6642
+ // Fast neighbour lookups using the precomputed pathIsRing cache.
6643
+ // Used by findDuplicateArc (via addEdge/addSplitEdge) and by addRing.
6605
6644
  function nextPoint(id) {
6606
6645
  var partId = pathIds[id],
6607
6646
  nextId = id + 1;
6608
6647
  if (nextId < pointCount && pathIds[nextId] === partId) {
6609
- return id + 1;
6648
+ return nextId;
6610
6649
  }
6611
- var len = nn[partId];
6612
- return sameXY(id, id - len + 1) ? id - len + 2 : -1;
6650
+ return pathIsRing[partId] ? id - nn[partId] + 2 : -1;
6613
6651
  }
6614
6652
 
6615
6653
  function prevPoint(id) {
6616
6654
  var partId = pathIds[id],
6617
6655
  prevId = id - 1;
6618
6656
  if (prevId >= 0 && pathIds[prevId] === partId) {
6619
- return id - 1;
6657
+ return prevId;
6620
6658
  }
6621
- var len = nn[partId];
6622
- return sameXY(id, id + len - 1) ? id + len - 2 : -1;
6659
+ return pathIsRing[partId] ? id + nn[partId] - 2 : -1;
6623
6660
  }
6624
6661
 
6625
- function sameXY(a, b) {
6626
- return xx[a] == xx[b] && yy[a] == yy[b];
6662
+ function pointIsArcEndpoint(id) {
6663
+ return isNode[id] === 1;
6627
6664
  }
6628
6665
 
6629
6666
  // Convert a non-topological path to one or more topological arcs
@@ -6666,49 +6703,9 @@
6666
6703
  return arcIds;
6667
6704
  }
6668
6705
 
6669
- // Test if a point @id is an endpoint of a topological path
6670
- function pointIsArcEndpoint(id) {
6671
- var id2 = chainIds[id],
6672
- prev = prevPoint(id),
6673
- next = nextPoint(id),
6674
- prev2, next2;
6675
- if (prev == -1 || next == -1) {
6676
- // @id is an endpoint if it is the start or end of an open path
6677
- return true;
6678
- }
6679
- while (id != id2) {
6680
- prev2 = prevPoint(id2);
6681
- next2 = nextPoint(id2);
6682
- if (prev2 == -1 || next2 == -1 || brokenEdge(prev, next, prev2, next2)) {
6683
- // there is a discontinuity at @id -- point is arc endpoint
6684
- return true;
6685
- }
6686
- id2 = chainIds[id2];
6687
- }
6688
- return false;
6689
- }
6690
-
6691
- // a and b are two vertices with the same x, y coordinates
6692
- // test if the segments on either side of them are also identical
6693
- function brokenEdge(aprev, anext, bprev, bnext) {
6694
- var apx = xx[aprev],
6695
- anx = xx[anext],
6696
- bpx = xx[bprev],
6697
- bnx = xx[bnext],
6698
- apy = yy[aprev],
6699
- any = yy[anext],
6700
- bpy = yy[bprev],
6701
- bny = yy[bnext];
6702
- if (apx == bnx && anx == bpx && apy == bny && any == bpy ||
6703
- apx == bpx && anx == bnx && apy == bpy && any == bny) {
6704
- return false;
6705
- }
6706
- return true;
6707
- }
6708
-
6709
6706
  function mergeArcParts(src, startId, endId, startId2, endId2) {
6710
6707
  var len = endId - startId + endId2 - startId2 + 2,
6711
- ArrayClass = usingTypedArrays() ? Float64Array : Array,
6708
+ ArrayClass = src.subarray ? Float64Array : Array,
6712
6709
  dest = new ArrayClass(len),
6713
6710
  j = 0, i;
6714
6711
  for (i=startId; i <= endId; i++) {
@@ -6723,8 +6720,12 @@
6723
6720
  function addSplitEdge(start1, end1, start2, end2) {
6724
6721
  var arcId = index.findDuplicateArc(xx, yy, start1, end2, nextPoint, prevPoint);
6725
6722
  if (arcId === null) {
6726
- arcId = index.addArc(mergeArcParts(xx, start1, end1, start2, end2),
6727
- mergeArcParts(yy, start1, end1, start2, end2));
6723
+ // Coordinates for a split (wrap-around) edge don't form a contiguous
6724
+ // slice of xx/yy, so we build a standalone buffer and hand it to the
6725
+ // index as the arc's source.
6726
+ var mx = mergeArcParts(xx, start1, end1, start2, end2),
6727
+ my = mergeArcParts(yy, start1, end1, start2, end2);
6728
+ arcId = index.addArc(mx, my, 0, mx.length - 1);
6728
6729
  }
6729
6730
  return arcId;
6730
6731
  }
@@ -6733,8 +6734,7 @@
6733
6734
  // search for a matching edge that has already been generated
6734
6735
  var arcId = index.findDuplicateArc(xx, yy, start, end, nextPoint, prevPoint);
6735
6736
  if (arcId === null) {
6736
- arcId = index.addArc(slice.call(xx, start, end + 1),
6737
- slice.call(yy, start, end + 1));
6737
+ arcId = index.addArc(xx, yy, start, end);
6738
6738
  }
6739
6739
  return arcId;
6740
6740
  }
@@ -6777,6 +6777,87 @@
6777
6777
  return pathIds;
6778
6778
  }
6779
6779
 
6780
+ // Per-path flag: 1 if the path is a closed ring (first vertex coincides with
6781
+ // last vertex), else 0. Computed once so that prevPoint()/nextPoint() don't
6782
+ // need to call sameXY() at path boundaries.
6783
+ //
6784
+ function initPathIsRing(nn, xx, yy) {
6785
+ var pathCount = nn.length,
6786
+ pathIsRing = new Uint8Array(pathCount),
6787
+ pstart = 0, len;
6788
+ for (var p = 0; p < pathCount; p++) {
6789
+ len = nn[p];
6790
+ if (len > 1 &&
6791
+ xx[pstart] === xx[pstart + len - 1] &&
6792
+ yy[pstart] === yy[pstart + len - 1]) {
6793
+ pathIsRing[p] = 1;
6794
+ }
6795
+ pstart += len;
6796
+ }
6797
+ return pathIsRing;
6798
+ }
6799
+
6800
+ // Decide, for every point, whether it is a topological node (an arc
6801
+ // endpoint). Being a node is a property of the entire coincident-point
6802
+ // chain: all points sharing a location agree on the answer. So we walk
6803
+ // each chain at most once — O(n) total — instead of doing the walk
6804
+ // independently at every point (O(n * K), quadratic per chain).
6805
+ //
6806
+ // A chain's points are nodes iff:
6807
+ // - any member has a missing neighbour (open-path endpoint), or
6808
+ // - two members disagree on the unordered pair of neighbour coords.
6809
+ //
6810
+ function computeIsNode(nn, xx, yy, chainIds, pathIds, pathIsRing) {
6811
+ var n = xx.length,
6812
+ isNode = new Uint8Array(n),
6813
+ done = new Uint8Array(n);
6814
+
6815
+ function nextPoint(id) {
6816
+ var part = pathIds[id], nid = id + 1;
6817
+ if (nid < n && pathIds[nid] === part) return nid;
6818
+ return pathIsRing[part] ? id - nn[part] + 2 : -1;
6819
+ }
6820
+
6821
+ function prevPoint(id) {
6822
+ var part = pathIds[id], pid = id - 1;
6823
+ if (pid >= 0 && pathIds[pid] === part) return pid;
6824
+ return pathIsRing[part] ? id + nn[part] - 2 : -1;
6825
+ }
6826
+
6827
+ for (var i = 0; i < n; i++) {
6828
+ if (done[i]) continue;
6829
+ var result = chainIsBroken(i);
6830
+ var id = i;
6831
+ do {
6832
+ isNode[id] = result;
6833
+ done[id] = 1;
6834
+ id = chainIds[id];
6835
+ } while (id !== i);
6836
+ }
6837
+ return isNode;
6838
+
6839
+ // Returns 1 if the chain containing `start` has any broken neighbour
6840
+ // signature (i.e. all members are nodes), else 0.
6841
+ function chainIsBroken(start) {
6842
+ var prev = prevPoint(start),
6843
+ next = nextPoint(start);
6844
+ if (prev === -1 || next === -1) return 1;
6845
+ var refPX = xx[prev], refPY = yy[prev],
6846
+ refNX = xx[next], refNY = yy[next];
6847
+ var id = chainIds[start];
6848
+ while (id !== start) {
6849
+ var p = prevPoint(id), q = nextPoint(id);
6850
+ if (p === -1 || q === -1) return 1;
6851
+ var px = xx[p], py = yy[p], qx = xx[q], qy = yy[q];
6852
+ var fwd = px === refPX && py === refPY && qx === refNX && qy === refNY;
6853
+ var rev = px === refNX && py === refNY && qx === refPX && qy === refPY;
6854
+ if (!fwd && !rev) return 1;
6855
+ id = chainIds[id];
6856
+ }
6857
+ return 0;
6858
+ }
6859
+ }
6860
+
6780
6861
  function replaceArcIds(src, replacements) {
6781
6862
  return src.map(function(shape) {
6782
6863
  return replaceArcsInShape(shape, replacements);
@@ -7262,15 +7343,201 @@
7262
7343
  };
7263
7344
  }
7264
7345
 
7346
+ // Lightweight hierarchical profiler.
7347
+ // Intended for ad-hoc performance work on hot pipelines (e.g. addIntersectionCuts).
7348
+ //
7349
+ // Usage:
7350
+ // import { profileStart, profileEnd, profileWrap, ... } from './utils/mapshaper-profile';
7351
+ // profileStart('phase'); doWork(); profileEnd('phase');
7352
+ // var result = profileWrap('phase', () => doWork());
7353
+ //
7354
+ // When disabled (the default) every call short-circuits in ~one comparison; safe
7355
+ // to leave in hot code paths. Enable from the CLI with the `-profile` command,
7356
+ // from JS with enableProfiling(), or by setting the MAPSHAPER_PROFILE env var.
7357
+ //
7358
+ // The profiler tracks a stack of currently-open phases so calls can nest. Each
7359
+ // unique stack path accumulates ms-elapsed and a call count. profileReport()
7360
+ // returns a flat array; formatProfileReport() pretty-prints a tree.
7361
+
7362
+ var ENABLED = false;
7363
+ var ROOT = makeNode('<root>');
7364
+ var STACK = [ROOT];
7365
+ var WALL_START = 0;
7366
+
7367
+ function makeNode(label) {
7368
+ return {
7369
+ label: label,
7370
+ totalMs: 0,
7371
+ selfMs: 0,
7372
+ calls: 0,
7373
+ childMsAccum: 0,
7374
+ children: Object.create(null)
7375
+ };
7376
+ }
7377
+
7378
+ function nowMs() {
7379
+ if (typeof process !== 'undefined' && process.hrtime && process.hrtime.bigint) {
7380
+ // bigint -> number division: precision down to 1 microsecond is fine for our needs
7381
+ return Number(process.hrtime.bigint()) / 1e6;
7382
+ }
7383
+ if (typeof performance !== 'undefined' && performance.now) {
7384
+ return performance.now();
7385
+ }
7386
+ return Date.now();
7387
+ }
7388
+
7389
+ function enableProfiling() {
7390
+ ENABLED = true;
7391
+ WALL_START = nowMs();
7392
+ }
7393
+
7394
+ function disableProfiling() {
7395
+ ENABLED = false;
7396
+ }
7397
+
7398
+ function profileEnabled() {
7399
+ return ENABLED;
7400
+ }
7401
+
7402
+ function profileReset() {
7403
+ ROOT = makeNode('<root>');
7404
+ STACK = [ROOT];
7405
+ WALL_START = ENABLED ? nowMs() : 0;
7406
+ }
7407
+
7408
+ // Open a new phase. Cheap (~one branch) when disabled.
7409
+ function profileStart(label) {
7410
+ if (!ENABLED) return;
7411
+ var parent = STACK[STACK.length - 1];
7412
+ var node = parent.children[label];
7413
+ if (!node) {
7414
+ node = makeNode(label);
7415
+ parent.children[label] = node;
7416
+ }
7417
+ node._t0 = nowMs();
7418
+ STACK.push(node);
7419
+ }
7420
+
7421
+ function profileEnd(label) {
7422
+ if (!ENABLED) return;
7423
+ var node = STACK[STACK.length - 1];
7424
+ if (label && node.label !== label) {
7425
+ // Mismatched labels usually mean an early return forgot to call profileEnd().
7426
+ // Walk up the stack until we find a match, closing the intervening frames.
7427
+ while (STACK.length > 1 && STACK[STACK.length - 1].label !== label) {
7428
+ profileEnd(STACK[STACK.length - 1].label);
7429
+ }
7430
+ node = STACK[STACK.length - 1];
7431
+ if (node.label !== label) return; // give up rather than throw
7432
+ }
7433
+ var elapsed = nowMs() - node._t0;
7434
+ node._t0 = 0;
7435
+ node.totalMs += elapsed;
7436
+ node.calls += 1;
7437
+ STACK.pop();
7438
+ var parent = STACK[STACK.length - 1];
7439
+ parent.childMsAccum += elapsed;
7440
+ }
7441
+
7442
+ // Wrap a function call. Re-throws if the callback throws but still closes the
7443
+ // phase so the stack stays consistent.
7444
+ function profileWrap(label, fn) {
7445
+ if (!ENABLED) return fn();
7446
+ profileStart(label);
7447
+ try {
7448
+ return fn();
7449
+ } finally {
7450
+ profileEnd(label);
7451
+ }
7452
+ }
7453
+
7454
+ // Build a flat tree report: array of {depth, label, totalMs, selfMs, calls}.
7455
+ function profileReport() {
7456
+ var rows = [];
7457
+ function visit(node, depth) {
7458
+ if (depth > 0) {
7459
+ rows.push({
7460
+ depth: depth - 1,
7461
+ label: node.label,
7462
+ totalMs: node.totalMs,
7463
+ selfMs: Math.max(0, node.totalMs - node.childMsAccum),
7464
+ calls: node.calls
7465
+ });
7466
+ }
7467
+ var keys = Object.keys(node.children);
7468
+ keys.sort(function(a, b) {
7469
+ return node.children[b].totalMs - node.children[a].totalMs;
7470
+ });
7471
+ for (var i = 0; i < keys.length; i++) {
7472
+ visit(node.children[keys[i]], depth + 1);
7473
+ }
7474
+ }
7475
+ visit(ROOT, 0);
7476
+ return rows;
7477
+ }
7478
+
7479
+ function profileWallElapsedMs() {
7480
+ return ENABLED && WALL_START ? nowMs() - WALL_START : 0;
7481
+ }
7482
+
7483
+ // Pretty-print the current report as a column-aligned tree.
7484
+ function formatProfileReport(opts) {
7485
+ var rows = profileReport();
7486
+ if (rows.length === 0) return '(profile is empty)';
7487
+ opts = opts || {};
7488
+ var indent = ' ';
7489
+ var lines = [];
7490
+ // header
7491
+ lines.push(['phase', 'total ms', 'self ms', 'calls'].join('\t'));
7492
+ for (var i = 0; i < rows.length; i++) {
7493
+ var r = rows[i];
7494
+ var label = '';
7495
+ for (var j = 0; j < r.depth; j++) label += indent;
7496
+ label += r.label;
7497
+ lines.push([
7498
+ label,
7499
+ r.totalMs.toFixed(2),
7500
+ r.selfMs.toFixed(2),
7501
+ String(r.calls)
7502
+ ].join('\t'));
7503
+ }
7504
+ if (opts.includeWall) {
7505
+ lines.push('');
7506
+ lines.push('wall elapsed ms: ' + profileWallElapsedMs().toFixed(2));
7507
+ }
7508
+ return lines.join('\n');
7509
+ }
7510
+
7511
+ // Honour an env var so users (and CI harnesses) can opt in without code changes.
7512
+ if (typeof process !== 'undefined' && process.env && process.env.MAPSHAPER_PROFILE) {
7513
+ enableProfiling();
7514
+ }
7515
+
7516
+ var Profile = /*#__PURE__*/Object.freeze({
7517
+ __proto__: null,
7518
+ disableProfiling: disableProfiling,
7519
+ enableProfiling: enableProfiling,
7520
+ formatProfileReport: formatProfileReport,
7521
+ profileEnabled: profileEnabled,
7522
+ profileEnd: profileEnd,
7523
+ profileReport: profileReport,
7524
+ profileReset: profileReset,
7525
+ profileStart: profileStart,
7526
+ profileWallElapsedMs: profileWallElapsedMs,
7527
+ profileWrap: profileWrap
7528
+ });
7529
+
7265
7530
  // Dissolve arcs that can be merged without affecting topology of layers
7266
7531
  // remove arcs that are not referenced by any layer; remap arc ids
7267
7532
  // in layers. (dataset.arcs is replaced).
7268
7533
  function dissolveArcs(dataset) {
7534
+ profileStart('dissolveArcs.body');
7269
7535
  var arcs = dataset.arcs,
7270
7536
  layers = dataset.layers.filter(layerHasPaths);
7271
7537
 
7272
7538
  if (!arcs || !layers.length) {
7273
7539
  dataset.arcs = null;
7540
+ profileEnd('dissolveArcs.body');
7274
7541
  return;
7275
7542
  }
7276
7543
 
@@ -7280,14 +7547,17 @@
7280
7547
  arcIndex = new Int32Array(arcs.size()), // maps old arc ids to new ids
7281
7548
  arcStatus = new Uint8Array(arcs.size());
7282
7549
  // arcStatus: 0 = unvisited, 1 = dropped, 2 = remapped, 3 = remapped + reversed
7550
+ profileStart('dissolveArcs.translatePaths');
7283
7551
  layers.forEach(function(lyr) {
7284
- // modify copies of the original shapes; original shapes should be unmodified
7285
- // (need to test this)
7286
7552
  lyr.shapes = lyr.shapes.map(function(shape, i) {
7287
7553
  return editShapeParts(shape && shape.concat(), translatePath);
7288
7554
  });
7289
7555
  });
7556
+ profileEnd('dissolveArcs.translatePaths');
7557
+ profileStart('dissolveArcs.dissolveArcCollection');
7290
7558
  dataset.arcs = dissolveArcCollection(arcs, newArcs, totalPoints);
7559
+ profileEnd('dissolveArcs.dissolveArcCollection');
7560
+ profileEnd('dissolveArcs.body');
7291
7561
 
7292
7562
  function translatePath(path) {
7293
7563
  var pointCount = 0;
@@ -12449,14 +12719,22 @@
12449
12719
 
12450
12720
  function testPointInRing(p, cand) {
12451
12721
  if (!cand.bounds.containsPoint(p[0], p[1])) return false;
12452
- if (!cand.index && cand.bounds.area() > totalArea * 0.01) {
12453
- // index larger polygons (because they are slower to test via pointInRing()
12454
- // and they are more likely to be involved in repeated hit tests).
12455
- cand.index = new PolygonIndex([cand.ids], arcs);
12722
+ if (!cand.index) {
12723
+ // Build a per-ring scanline index when (a) the ring is large relative to
12724
+ // the dataset (likely involved in many hit tests anyway) or (b) the same
12725
+ // ring has already been tested at least once, indicating it lies in a
12726
+ // hot spot for repeated point-in-polygon queries (e.g. coastal counties
12727
+ // tested by every offshore-island CCW ring during mosaic enclosure
12728
+ // detection). Building on the second hit avoids paying the index build
12729
+ // cost for rings that are tested only once.
12730
+ if (cand._tested || cand.bounds.area() > totalArea * 0.01) {
12731
+ cand.index = new PolygonIndex([cand.ids], arcs);
12732
+ } else {
12733
+ cand._tested = true;
12734
+ return geom.testPointInRing(p[0], p[1], cand.ids, arcs);
12735
+ }
12456
12736
  }
12457
- return cand.index ?
12458
- cand.index.pointInPolygon(p[0], p[1]) :
12459
- geom.testPointInRing(p[0], p[1], cand.ids, arcs);
12737
+ return cand.index.pointInPolygon(p[0], p[1]);
12460
12738
  }
12461
12739
 
12462
12740
  //
@@ -13476,6 +13754,7 @@
13476
13754
 
13477
13755
  function singleStripeId(y) {return 0;}
13478
13756
  // Count segments in each stripe
13757
+ profileStart('stripeSetup');
13479
13758
  arcs.forEachSegment(function(id1, id2, xx, yy) {
13480
13759
  var s1 = stripeId(yy[id1]),
13481
13760
  s2 = stripeId(yy[id2]);
@@ -13512,8 +13791,10 @@
13512
13791
  s1 += s2 > s1 ? 1 : -1;
13513
13792
  }
13514
13793
  });
13794
+ profileEnd('stripeSetup');
13515
13795
 
13516
13796
  // Detect intersections among segments in each stripe.
13797
+ profileStart('intersectSegments');
13517
13798
  var raw = arcs.getVertexData(),
13518
13799
  intersections = [],
13519
13800
  arr;
@@ -13523,7 +13804,11 @@
13523
13804
  intersections.push(arr[j]);
13524
13805
  }
13525
13806
  }
13526
- return dedupIntersections(intersections, opts.unique ? getUniqueIntersectionKey : null);
13807
+ profileEnd('intersectSegments');
13808
+ profileStart('dedupIntersections');
13809
+ var deduped = dedupIntersections(intersections, opts.unique ? getUniqueIntersectionKey : null);
13810
+ profileEnd('dedupIntersections');
13811
+ return deduped;
13527
13812
  }
13528
13813
 
13529
13814
 
@@ -13536,22 +13821,42 @@
13536
13821
 
13537
13822
 
13538
13823
  function dedupIntersections(arr, keyFunction) {
13539
- var index = {};
13540
- var getKey = keyFunction || getIntersectionKey;
13541
- return arr.filter(function(o) {
13542
- var key = getKey(o);
13543
- if (key in index) {
13544
- return false;
13824
+ if (keyFunction) {
13825
+ var index = new Map();
13826
+ var out = [];
13827
+ for (var i = 0, n = arr.length; i < n; i++) {
13828
+ var o = arr[i];
13829
+ var k = keyFunction(o);
13830
+ if (index.has(k)) continue;
13831
+ index.set(k, true);
13832
+ out.push(o);
13545
13833
  }
13546
- index[key] = true;
13547
- return true;
13548
- });
13549
- }
13550
-
13551
- // Get an indexable key from an intersection object
13552
- // Assumes that vertex ids of o.a and o.b are sorted
13553
- function getIntersectionKey(o) {
13554
- return o.a.join(',') + ';' + o.b.join(',');
13834
+ return out;
13835
+ }
13836
+ // Default key is the pair of segments (a, b). Each segment is a vertex id
13837
+ // pair where the second id is either equal to or one greater than the first
13838
+ // (see formatIntersectingSegment). Pack each segment into a single number
13839
+ // (id * 2 + isMidSegment) and use a two-level Map keyed by integers, which
13840
+ // avoids the per-call string allocation of the previous Array#join key.
13841
+ var outer = new Map();
13842
+ var result = [];
13843
+ for (var ii = 0, nn = arr.length; ii < nn; ii++) {
13844
+ var o2 = arr[ii];
13845
+ var a = o2.a, b = o2.b;
13846
+ var ak = a[0] * 2 + (a[0] === a[1] ? 0 : 1);
13847
+ var bk = b[0] * 2 + (b[0] === b[1] ? 0 : 1);
13848
+ var inner = outer.get(ak);
13849
+ if (inner) {
13850
+ if (inner.has(bk)) continue;
13851
+ inner.add(bk);
13852
+ } else {
13853
+ inner = new Set();
13854
+ inner.add(bk);
13855
+ outer.set(ak, inner);
13856
+ }
13857
+ result.push(o2);
13858
+ }
13859
+ return result;
13555
13860
  }
13556
13861
 
13557
13862
  function getUniqueIntersectionKey(o) {
@@ -16945,16 +17250,26 @@
16945
17250
  // Remove any unused arcs from the dataset's ArcCollection.
16946
17251
  // Return a NodeCollection
16947
17252
  function cleanArcReferences(dataset) {
17253
+ profileStart('NodeCollection#1');
16948
17254
  var nodes = new NodeCollection(dataset.arcs);
17255
+ profileEnd('NodeCollection#1');
17256
+ profileStart('findDuplicateArcs');
16949
17257
  var map = findDuplicateArcs(nodes);
17258
+ profileEnd('findDuplicateArcs');
16950
17259
  var dropCount;
16951
17260
  if (map) {
17261
+ profileStart('replaceIndexedArcIds');
16952
17262
  replaceIndexedArcIds(dataset, map);
17263
+ profileEnd('replaceIndexedArcIds');
16953
17264
  }
17265
+ profileStart('deleteUnusedArcs');
16954
17266
  dropCount = deleteUnusedArcs(dataset);
17267
+ profileEnd('deleteUnusedArcs');
16955
17268
  if (dropCount > 0) {
16956
17269
  // rebuild nodes if arcs have changed
17270
+ profileStart('NodeCollection#2');
16957
17271
  nodes = new NodeCollection(dataset.arcs);
17272
+ profileEnd('NodeCollection#2');
16958
17273
  }
16959
17274
  return nodes;
16960
17275
  }
@@ -17014,11 +17329,13 @@
17014
17329
  // and re-index the paths of all the layers that reference the arc collection.
17015
17330
  // (in-place)
17016
17331
  function addIntersectionCuts(dataset, _opts) {
17332
+ profileStart('addIntersectionCuts');
17017
17333
  var opts = _opts || {};
17018
17334
  var arcs = dataset.arcs;
17019
17335
  var arcBounds = arcs && arcs.getBounds();
17020
17336
  var snapDist, nodes;
17021
17337
  if (!arcBounds || !arcBounds.hasBounds()) {
17338
+ profileEnd('addIntersectionCuts');
17022
17339
  return new NodeCollection([]);
17023
17340
  }
17024
17341
 
@@ -17033,46 +17350,74 @@
17033
17350
 
17034
17351
  // bake-in any simplification (bug fix; before, -simplify followed by dissolve2
17035
17352
  // used to reset simplification)
17353
+ profileStart('flatten');
17036
17354
  arcs.flatten();
17355
+ profileEnd('flatten');
17037
17356
 
17038
17357
  var changed = snapAndCut(dataset, snapDist);
17039
17358
 
17040
17359
  // Detect topology again if coordinates have changed
17041
17360
  if (changed || opts.rebuild_topology) {
17361
+ profileStart('buildTopology');
17042
17362
  buildTopology(dataset);
17363
+ profileEnd('buildTopology');
17043
17364
  }
17044
17365
  // Remove degenerate shapes
17045
17366
  // Without this step, pathfinder function would encounter dead ends.
17367
+ profileStart('cleanShapes');
17046
17368
  dataset.layers.forEach(function(lyr) {
17047
17369
  if (layerHasPaths(lyr)) {
17048
17370
  cleanShapes(lyr.shapes, arcs, lyr.geometry_type);
17049
17371
  }
17050
17372
  });
17373
+ profileEnd('cleanShapes');
17051
17374
  // Further clean-up -- remove duplicate and unused arcs, etc.
17375
+ profileStart('cleanArcReferences');
17052
17376
  nodes = cleanArcReferences(dataset);
17377
+ profileEnd('cleanArcReferences');
17378
+ profileEnd('addIntersectionCuts');
17053
17379
  return nodes;
17054
17380
  }
17055
17381
 
17056
17382
  function snapAndCut(dataset, snapDist) {
17383
+ profileStart('snapAndCut');
17057
17384
  var arcs = dataset.arcs;
17058
17385
  var cutOpts = snapDist > 0 ? {tolerance: snapDist} : {tolerance: 0};
17059
17386
  var coordsHaveChanged = false;
17060
- var snapCount, dupeCount, cutCount;
17387
+ var snapCount = 0, dupeCount, cutCount;
17061
17388
  var maxLoops = 4, loopCount = 0;
17389
+ // After pass 1, snap & dedup can be skipped IF pass 1 snap moved nothing:
17390
+ // cuts inserted exact intersection coordinates that match either an existing
17391
+ // vertex (handled by formatIntersectingSegment) or a fresh point. The only
17392
+ // way a fresh cut point could need re-snapping is if it falls within snap
17393
+ // interval of a third (unrelated) vertex by coincidence; in clean data that
17394
+ // didn't happen on pass 1 either, so we skip the redundant O(V log V) sort.
17395
+ var skipFollowupSnap;
17062
17396
 
17063
- // snap + cut until no more cuts are needed
17064
17397
  do {
17065
17398
  loopCount++;
17066
17399
  cutCount = 0;
17067
- snapCount = snapCoordsByInterval(arcs, snapDist);
17068
- dupeCount = arcs.dedupCoords();
17400
+ skipFollowupSnap = loopCount > 1 && snapCount === 0;
17401
+ if (!skipFollowupSnap) {
17402
+ profileStart('snap');
17403
+ snapCount = snapCoordsByInterval(arcs, snapDist);
17404
+ profileEnd('snap');
17405
+ profileStart('dedupCoords');
17406
+ dupeCount = arcs.dedupCoords();
17407
+ profileEnd('dedupCoords');
17408
+ } else {
17409
+ snapCount = 0;
17410
+ dupeCount = 0;
17411
+ }
17069
17412
  if (snapCount > 0 || cutCount > 0 || loopCount == 1) {
17070
- // cut arcs at points where segments intersect
17413
+ profileStart('cutPathsAtIntersections');
17071
17414
  cutCount = cutPathsAtIntersections(dataset, cutOpts);
17415
+ profileEnd('cutPathsAtIntersections');
17072
17416
  }
17073
17417
  coordsHaveChanged |= (snapCount + dupeCount + cutCount) > 0;
17074
- debug("[snapAndCut] pass:", loopCount, "snaps:", snapCount, "dupes:", dupeCount, "cuts:", cutCount);
17418
+ debug("[snapAndCut] pass:", loopCount, "snaps:", snapCount, "dupes:", dupeCount, "cuts:", cutCount, skipFollowupSnap ? "(skipped snap)" : "");
17075
17419
  } while (loopCount < maxLoops && cutCount > 0);
17420
+ profileEnd('snapAndCut');
17076
17421
 
17077
17422
  // should this be added?
17078
17423
  // if (cutCount > 0 && loopCount == maxLoops) {
@@ -17137,9 +17482,13 @@
17137
17482
  // Divides a collection of arcs at points where arc paths cross each other
17138
17483
  // Returns array for remapping arc ids
17139
17484
  function divideArcs(arcs, opts) {
17485
+ profileStart('findClippingPoints');
17140
17486
  var points = findClippingPoints(arcs, opts);
17487
+ profileEnd('findClippingPoints');
17141
17488
  // TODO: avoid the following if no points need to be added
17489
+ profileStart('insertCutPoints');
17142
17490
  var map = insertCutPoints(points, arcs);
17491
+ profileEnd('insertCutPoints');
17143
17492
  // segment-point intersections currently create duplicate points
17144
17493
  // TODO: consider dedup in a later cleanup pass?
17145
17494
  // arcs.dedupCoords();
@@ -17147,9 +17496,14 @@
17147
17496
  }
17148
17497
 
17149
17498
  function findClippingPoints(arcs, opts) {
17150
- var intersections = findSegmentIntersections(arcs, opts),
17151
- data = arcs.getVertexData();
17152
- return convertIntersectionsToCutPoints(intersections, data.xx, data.yy);
17499
+ profileStart('findSegmentIntersections');
17500
+ var intersections = findSegmentIntersections(arcs, opts);
17501
+ profileEnd('findSegmentIntersections');
17502
+ profileStart('convertIntersectionsToCutPoints');
17503
+ var data = arcs.getVertexData();
17504
+ var points = convertIntersectionsToCutPoints(intersections, data.xx, data.yy);
17505
+ profileEnd('convertIntersectionsToCutPoints');
17506
+ return points;
17153
17507
  }
17154
17508
 
17155
17509
  // Inserts array of cutting points into an ArcCollection
@@ -17163,9 +17517,24 @@
17163
17517
  i1 = 0,
17164
17518
  nn1 = [],
17165
17519
  srcArcTotal = arcs.size(),
17166
- map = new Uint32Array(srcArcTotal),
17167
- points = filterSortedCutPoints(sortCutPoints(unfilteredPoints, xx0, yy0), arcs),
17168
- destPointTotal = arcs.getPointCount() + points.length * 2,
17520
+ map = new Uint32Array(srcArcTotal);
17521
+ profileStart('sortCutPoints');
17522
+ var sorted = sortCutPoints(unfilteredPoints, xx0, yy0);
17523
+ profileEnd('sortCutPoints');
17524
+ profileStart('filterSortedCutPoints');
17525
+ var points = filterSortedCutPoints(sorted, arcs);
17526
+ profileEnd('filterSortedCutPoints');
17527
+ // Skip the full xx/yy rewrite when nothing actually needs cutting.
17528
+ // map[k] stores the destination arc-id at which source arc k begins; with no
17529
+ // cuts, source arc k becomes destination arc k (map is the identity).
17530
+ if (points.length === 0) {
17531
+ for (var k = 0; k < srcArcTotal; k++) {
17532
+ map[k] = k;
17533
+ }
17534
+ return map;
17535
+ }
17536
+ profileStart('rewriteVertexData');
17537
+ var destPointTotal = arcs.getPointCount() + points.length * 2,
17169
17538
  xx1 = new Float64Array(destPointTotal),
17170
17539
  yy1 = new Float64Array(destPointTotal),
17171
17540
  n0, n1, arcLen, p;
@@ -17207,6 +17576,7 @@
17207
17576
 
17208
17577
  if (i1 != destPointTotal) error("[insertCutPoints()] Counting error");
17209
17578
  arcs.updateVertexData(nn1, xx1, yy1, null);
17579
+ profileEnd('rewriteVertexData');
17210
17580
  return map;
17211
17581
  }
17212
17582
 
@@ -17249,14 +17619,35 @@
17249
17619
  // Insertion order: ascending id of first endpoint of containing segment and
17250
17620
  // ascending distance from same endpoint.
17251
17621
  function sortCutPoints(points, xx, yy) {
17252
- points.sort(function(a, b) {
17253
- if (a.i != b.i) return a.i - b.i;
17254
- return geom.distanceSq(xx[a.i], yy[a.i], a.x, a.y) - geom.distanceSq(xx[b.i], yy[b.i], b.x, b.y);
17255
- // The old code below is no longer reliable, now that out-of-range intersection
17256
- // points are allowed.
17257
- // return Math.abs(a.x - xx[a.i]) - Math.abs(b.x - xx[b.i]) ||
17258
- // Math.abs(a.y - yy[a.i]) - Math.abs(b.y - yy[b.i]);
17259
- });
17622
+ var len = points.length;
17623
+ // Precompute the sort key once per point. The comparator below would
17624
+ // otherwise do 2*log(n) distanceSq evaluations per element. Distances are
17625
+ // kept in a parallel array so we don't mutate the caller's point objects.
17626
+ var dists = xx ? new Float64Array(len) : null;
17627
+ if (dists) {
17628
+ for (var k = 0; k < len; k++) {
17629
+ var p = points[k];
17630
+ var dx = p.x - xx[p.i];
17631
+ var dy = p.y - yy[p.i];
17632
+ dists[k] = dx * dx + dy * dy;
17633
+ }
17634
+ // Decorate-sort-undecorate: attach (point, dist) tuples, sort by (i, dist),
17635
+ // then write back. Avoids closing over `dists` lookup-by-identity.
17636
+ var tagged = new Array(len);
17637
+ for (var t = 0; t < len; t++) {
17638
+ tagged[t] = [points[t], dists[t]];
17639
+ }
17640
+ tagged.sort(function(a, b) {
17641
+ return a[0].i - b[0].i || a[1] - b[1];
17642
+ });
17643
+ for (var u = 0; u < len; u++) {
17644
+ points[u] = tagged[u][0];
17645
+ }
17646
+ } else {
17647
+ points.sort(function(a, b) {
17648
+ return a.i - b.i;
17649
+ });
17650
+ }
17260
17651
  return points;
17261
17652
  }
17262
17653
 
@@ -17368,31 +17759,32 @@
17368
17759
  //
17369
17760
  function buildPolygonMosaic(nodes) {
17370
17761
  T$1.start();
17371
- // Detach any acyclic paths (spikes) from arc graph (these would interfere with
17372
- // the ring finding operation). This modifies @nodes -- a side effect.
17762
+ profileStart('bpm.detachAcyclicArcs');
17373
17763
  nodes.detachAcyclicArcs();
17764
+ profileEnd('bpm.detachAcyclicArcs');
17765
+ profileStart('bpm.findMosaicRings');
17374
17766
  var data = findMosaicRings(nodes);
17767
+ profileEnd('bpm.findMosaicRings');
17375
17768
 
17376
- // Process CW rings: these are indivisible space-enclosing boundaries of mosaic tiles
17377
17769
  var mosaic = data.cw.map(function(ring) {return [ring];});
17378
17770
  debug('Find mosaic rings', T$1.stop());
17379
17771
  T$1.start();
17380
17772
 
17381
- // Process CCW rings: these are either holes or enclosure
17382
- // TODO: optimize -- testing CCW path of every island is costly
17383
17773
  var enclosures = [];
17384
- var index = new PathIndex(mosaic, nodes.arcs); // index CW rings to help identify holes
17774
+ profileStart('bpm.PathIndex');
17775
+ var index = new PathIndex(mosaic, nodes.arcs);
17776
+ profileEnd('bpm.PathIndex');
17777
+ profileStart('bpm.findEnclosingForCCW');
17385
17778
  data.ccw.forEach(function(ring) {
17386
17779
  var id = index.findSmallestEnclosingPolygon(ring);
17387
17780
  if (id > -1) {
17388
- // Enclosed CCW rings are holes in the enclosing mosaic tile
17389
17781
  mosaic[id].push(ring);
17390
17782
  } else {
17391
- // Non-enclosed CCW rings are outer boundaries -- add to enclosures layer
17392
17783
  reversePath(ring);
17393
17784
  enclosures.push([ring]);
17394
17785
  }
17395
17786
  });
17787
+ profileEnd('bpm.findEnclosingForCCW');
17396
17788
  debug(utils.format("Detect holes (holes: %d, enclosures: %d)", data.ccw.length - enclosures.length, enclosures.length), T$1.stop());
17397
17789
 
17398
17790
  return {mosaic: mosaic, enclosures: enclosures, lostArcs: data.lostArcs};
@@ -17692,23 +18084,22 @@
17692
18084
  });
17693
18085
 
17694
18086
  function MosaicIndex(lyr, nodes, optsArg) {
18087
+ profileStart('MosaicIndex.ctor');
17695
18088
  var opts = optsArg || {};
17696
18089
  var shapes = lyr.shapes;
18090
+ profileStart('mi.buildPolygonMosaic');
17697
18091
  var mosaic = buildPolygonMosaic(nodes).mosaic;
17698
- // map arc ids to tile ids
18092
+ profileEnd('mi.buildPolygonMosaic');
18093
+ profileStart('mi.ShapeArcIndex');
17699
18094
  var arcTileIndex = new ShapeArcIndex(mosaic, nodes.arcs);
17700
- // keep track of which tiles have been assigned to shapes
18095
+ profileEnd('mi.ShapeArcIndex');
17701
18096
  var fetchedTileIndex = new IdTestIndex(mosaic.length);
17702
- // bidirection index of tile ids <=> shape ids
17703
18097
  var tileShapeIndex = new TileShapeIndex(mosaic, opts);
17704
- // assign tiles to shapes
18098
+ profileStart('mi.PolygonTiler.ctor');
17705
18099
  var shapeTiler = new PolygonTiler(mosaic, arcTileIndex, nodes, opts);
18100
+ profileEnd('mi.PolygonTiler.ctor');
17706
18101
  var weightFunction = null;
17707
18102
  if (!opts.simple && opts.flat) {
17708
- // opts.simple is an optimization when dissolving everything into one polygon
17709
- // using -dissolve2. In this situation, we don't need a weight function.
17710
- // Otherwise, if polygons are being dissolved into multiple groups,
17711
- // we use a function to assign tiles in overlapping areas to a single shape.
17712
18103
  weightFunction = getOverlapPriorityFunction(lyr.shapes, nodes.arcs, opts.overlap_rule);
17713
18104
  }
17714
18105
  this.mosaic = mosaic;
@@ -17716,16 +18107,19 @@
17716
18107
  this.getSourceIdsByTileId = tileShapeIndex.getShapeIdsByTileId; // expose for -mosaic command
17717
18108
  this.getTileIdsByShapeId = tileShapeIndex.getTileIdsByShapeId;
17718
18109
 
17719
- // Assign shape ids to mosaic tile shapes.
18110
+ profileStart('mi.assignTilesToShapes');
17720
18111
  shapes.forEach(function(shp, shapeId) {
17721
18112
  var tileIds = shapeTiler.getTilesInShape(shp, shapeId);
17722
18113
  tileShapeIndex.indexTileIdsByShapeId(shapeId, tileIds, weightFunction);
17723
18114
  });
18115
+ profileEnd('mi.assignTilesToShapes');
17724
18116
 
17725
- // ensure each tile is assigned to only one shape
17726
18117
  if (opts.flat) {
18118
+ profileStart('mi.tileShapeIndex.flatten');
17727
18119
  tileShapeIndex.flatten();
18120
+ profileEnd('mi.tileShapeIndex.flatten');
17728
18121
  }
18122
+ profileEnd('MosaicIndex.ctor');
17729
18123
 
17730
18124
  // fill gaps
17731
18125
  // (assumes that tiles have been allocated to shapes and mosaic has been flattened)
@@ -17905,22 +18299,30 @@
17905
18299
  }
17906
18300
 
17907
18301
  function dissolvePolygonGroups2(groups, lyr, dataset, opts) {
18302
+ profileStart('dissolvePolygonGroups2');
18303
+ profileStart('dpg2.NodeCollection');
17908
18304
  var arcFilter = getArcPresenceTest(lyr.shapes, dataset.arcs);
17909
18305
  var nodes = new NodeCollection(dataset.arcs, arcFilter);
18306
+ profileEnd('dpg2.NodeCollection');
17910
18307
  var mosaicOpts = {
17911
18308
  flat: !opts.allow_overlaps,
17912
18309
  simple: groups.length == 1,
17913
18310
  overlap_rule: opts.overlap_rule
17914
18311
  };
18312
+ profileStart('dpg2.MosaicIndex');
17915
18313
  var mosaicIndex = new MosaicIndex(lyr, nodes, mosaicOpts);
18314
+ profileEnd('dpg2.MosaicIndex');
17916
18315
  // gap fill doesn't work yet with overlapping shapes
17917
18316
  var fillGaps = !opts.allow_overlaps && (opts.sliver_control || opts.gap_fill_area);
17918
18317
  var cleanupData, filterData;
17919
18318
  if (fillGaps) {
18319
+ profileStart('dpg2.removeGaps');
17920
18320
  var sliverOpts = utils.extend({sliver_control: 1}, opts);
17921
18321
  filterData = getSliverFilter(lyr, dataset, sliverOpts);
17922
18322
  cleanupData = mosaicIndex.removeGaps(filterData.filter);
18323
+ profileEnd('dpg2.removeGaps');
17923
18324
  }
18325
+ profileStart('dpg2.dissolveTiles');
17924
18326
  var pathfind = getRingIntersector(mosaicIndex.nodes);
17925
18327
  var dissolvedShapes = groups.map(function(shapeIds) {
17926
18328
  var tiles = mosaicIndex.getTilesByShapeIds(shapeIds);
@@ -17931,14 +18333,16 @@
17931
18333
  }
17932
18334
  return dissolveTileGroup2(tiles, pathfind);
17933
18335
  });
17934
- // convert self-intersecting rings to outer/inner rings, for OGC
17935
- // Simple Features compliance
18336
+ profileEnd('dpg2.dissolveTiles');
18337
+ profileStart('dpg2.fixTangentHoles');
17936
18338
  dissolvedShapes = fixTangentHoles(dissolvedShapes, pathfind);
18339
+ profileEnd('dpg2.fixTangentHoles');
17937
18340
 
17938
18341
  if (fillGaps && !opts.quiet) {
17939
18342
  var msg = getGapRemovalMessage(cleanupData.removed, cleanupData.remaining, filterData.label);
17940
18343
  if (msg) message(msg);
17941
18344
  }
18345
+ profileEnd('dissolvePolygonGroups2');
17942
18346
  return dissolvedShapes;
17943
18347
  }
17944
18348
 
@@ -18158,40 +18562,52 @@
18158
18562
  cmd.cleanLayers = cleanLayers;
18159
18563
 
18160
18564
  function cleanLayers(layers, dataset, optsArg) {
18565
+ profileStart('cleanLayers');
18161
18566
  var opts = optsArg || {};
18162
18567
  var deepClean = !opts.only_arcs;
18163
18568
  var pathClean = utils.some(layers, layerHasPaths);
18164
18569
  var nodes;
18165
18570
  if (opts.debug) {
18166
18571
  addIntersectionCuts(dataset, opts);
18572
+ profileEnd('cleanLayers');
18167
18573
  return;
18168
18574
  }
18169
18575
  layers.forEach(function(lyr) {
18170
18576
  if (!layerHasGeometry(lyr)) return;
18171
18577
  if (lyr.geometry_type == 'polygon' && opts.rewind) {
18578
+ profileStart('rewindPolygons');
18172
18579
  rewindPolygons(lyr, dataset.arcs);
18580
+ profileEnd('rewindPolygons');
18173
18581
  }
18174
18582
  if (deepClean) {
18175
18583
  if (!nodes) {
18176
18584
  nodes = addIntersectionCuts(dataset, opts);
18177
18585
  }
18178
18586
  if (lyr.geometry_type == 'polygon') {
18587
+ profileStart('cleanPolygonLayerGeometry');
18179
18588
  cleanPolygonLayerGeometry(lyr, dataset, opts);
18589
+ profileEnd('cleanPolygonLayerGeometry');
18180
18590
  } else if (lyr.geometry_type == 'polyline') {
18591
+ profileStart('cleanPolylineLayerGeometry');
18181
18592
  cleanPolylineLayerGeometry(lyr, dataset);
18593
+ profileEnd('cleanPolylineLayerGeometry');
18182
18594
  } else if (lyr.geometry_type == 'point') {
18183
18595
  cleanPointLayerGeometry(lyr);
18184
18596
  }
18185
18597
  }
18186
18598
  if (!opts.allow_empty) {
18599
+ profileStart('filterFeatures');
18187
18600
  cmd.filterFeatures(lyr, dataset.arcs, {remove_empty: true, verbose: opts.verbose});
18601
+ profileEnd('filterFeatures');
18188
18602
  }
18189
18603
  });
18190
18604
 
18191
18605
  if (!opts.no_arc_dissolve && pathClean && dataset.arcs) {
18192
- // remove leftover endpoints within contiguous lines
18606
+ profileStart('dissolveArcs');
18193
18607
  dissolveArcs(dataset);
18608
+ profileEnd('dissolveArcs');
18194
18609
  }
18610
+ profileEnd('cleanLayers');
18195
18611
  }
18196
18612
 
18197
18613
  function cleanPolygonLayerGeometry(lyr, dataset, opts) {
@@ -18724,13 +19140,13 @@
18724
19140
  // default is true iff layers contain attributes
18725
19141
  return utils.some(layers, function(lyr) {
18726
19142
  var fields = lyr.data ? lyr.data.getFields() : [];
18727
- var haveData = useFeatureProperties(fields, opts);
19143
+ var haveData = useFeatureProperties$1(fields, opts);
18728
19144
  var haveId = !!getIdField(fields, opts);
18729
19145
  return haveData || haveId;
18730
19146
  });
18731
19147
  }
18732
19148
 
18733
- function useFeatureProperties(fields, opts) {
19149
+ function useFeatureProperties$1(fields, opts) {
18734
19150
  return !(opts.drop_table || opts.cut_table || fields.length === 0 ||
18735
19151
  fields.length == 1 && fields[0] == GeoJSON.ID_FIELD);
18736
19152
  }
@@ -18739,7 +19155,7 @@
18739
19155
  var fields = table ? table.getFields() : [],
18740
19156
  idField = getIdField(fields, opts),
18741
19157
  properties, records;
18742
- if (!useFeatureProperties(fields, opts)) {
19158
+ if (!useFeatureProperties$1(fields, opts)) {
18743
19159
  return null;
18744
19160
  }
18745
19161
  records = table.getRecords();
@@ -24500,12 +24916,10 @@ ${svg}
24500
24916
  }
24501
24917
 
24502
24918
  async function exportLayerToGeoPackage(lyr, dataset, gpkg, opts) {
24503
- var features, fields, columns, tableName, targetSrs;
24919
+ var fields, columns, tableName, targetSrs, state;
24504
24920
  if (!lyr.geometry_type) return;
24505
- features = exportLayerAsGeoJSON(lyr, dataset, opts, true, null)
24506
- .filter(feat => !!(feat && feat.geometry));
24507
- if (features.length === 0) return;
24508
- fields = inferFieldTypes(features);
24921
+ fields = inferFieldTypesFromLayer(lyr, dataset, opts);
24922
+ if (!fields) return;
24509
24923
  tableName = getTableName(lyr.name);
24510
24924
  columns = fields.map(function(field) {
24511
24925
  return {
@@ -24522,8 +24936,11 @@ ${svg}
24522
24936
  // GeoJSON is EPSG:4326 and reprojects unless given EPSG:4326 metadata.
24523
24937
  // Mapshaper output coords are already in target CRS, so suppress reprojection.
24524
24938
  var srs = getSourceSrsWithoutReprojection(featureDao && featureDao.srs, targetSrs);
24525
- for (var i = 0; i < features.length; i++) {
24526
- var feat = features[i];
24939
+ state = initLayerFeatureExportState(lyr, dataset, opts);
24940
+ var featureIndex = 0;
24941
+ for (var i = 0; i < state.count; i++) {
24942
+ var feat = exportFeatureAtIndex(state, i);
24943
+ if (!feat || !feat.geometry) continue;
24527
24944
  var normalized = normalizeFeature(feat, fields);
24528
24945
  try {
24529
24946
  await gpkg.addGeoJSONFeatureToGeoPackageWithFeatureDaoAndSrs(
@@ -24533,8 +24950,9 @@ ${svg}
24533
24950
  );
24534
24951
  } catch (e) {
24535
24952
  throw Error('GeoPackage insert failed at layer "' + tableName +
24536
- '", feature ' + i + ': ' + e.message);
24953
+ '", feature ' + featureIndex + ': ' + e.message);
24537
24954
  }
24955
+ featureIndex++;
24538
24956
  }
24539
24957
  }
24540
24958
 
@@ -24561,9 +24979,11 @@ ${svg}
24561
24979
  stop$1('Unable to export GeoPackage bytes');
24562
24980
  }
24563
24981
 
24564
- function inferFieldTypes(features) {
24982
+ function inferFieldTypesFromLayer(lyr, dataset, opts) {
24983
+ var sawFeature = false;
24565
24984
  var index = {};
24566
- features.forEach(function(feat) {
24985
+ forEachLayerExportFeature(lyr, dataset, opts, function(feat) {
24986
+ sawFeature = true;
24567
24987
  var props = feat.properties || {};
24568
24988
  Object.keys(props).forEach(function(name) {
24569
24989
  if (!isSupportedPropertyName(name)) return;
@@ -24577,11 +24997,79 @@ ${svg}
24577
24997
  }
24578
24998
  });
24579
24999
  });
25000
+ if (!sawFeature) return null;
24580
25001
  return Object.keys(index).map(function(name) {
24581
25002
  return {name: name, type: index[name]};
24582
25003
  });
24583
25004
  }
24584
25005
 
25006
+ function forEachLayerExportFeature(lyr, dataset, opts, cb) {
25007
+ var state = initLayerFeatureExportState(lyr, dataset, opts);
25008
+ var featureIndex = 0;
25009
+ for (var i = 0; i < state.count; i++) {
25010
+ var feature = exportFeatureAtIndex(state, i);
25011
+ if (!feature || !feature.geometry) continue;
25012
+ cb(feature, featureIndex++, i);
25013
+ }
25014
+ }
25015
+
25016
+ function initLayerFeatureExportState(lyr, dataset, opts) {
25017
+ var records = lyr.data ? lyr.data.getRecords() : null;
25018
+ var fields = lyr.data ? lyr.data.getFields() : [];
25019
+ var shapes = lyr.shapes || null;
25020
+ if (records && shapes && records.length !== shapes.length) {
25021
+ stop$1('Mismatch between number of properties and number of shapes');
25022
+ }
25023
+ return {
25024
+ lyr: lyr,
25025
+ dataset: dataset,
25026
+ opts: opts || {},
25027
+ fields: fields,
25028
+ records: records,
25029
+ shapes: shapes,
25030
+ count: Math.max(records ? records.length : 0, shapes ? shapes.length : 0),
25031
+ idField: getIdField(fields, opts || {}),
25032
+ useProps: useFeatureProperties(fields, opts || {})
25033
+ };
25034
+ }
25035
+
25036
+ function exportFeatureAtIndex(state, i) {
25037
+ var shape = state.shapes ? state.shapes[i] : null;
25038
+ var geom = shape ? GeoJSON.exporters[state.lyr.geometry_type](shape, state.dataset.arcs, state.opts) : null;
25039
+ var rec = state.records ? state.records[i] : null;
25040
+ var props = null;
25041
+ if (state.useProps && rec) {
25042
+ if (state.idField == GeoJSON.ID_FIELD) {
25043
+ props = Object.assign({}, rec);
25044
+ delete props[state.idField];
25045
+ } else {
25046
+ props = rec;
25047
+ }
25048
+ }
25049
+ var feat = GeoJSON.toFeature(geom, props);
25050
+ if (state.idField && rec) {
25051
+ feat.id = state.idField in rec ? rec[state.idField] : null;
25052
+ }
25053
+ if (state.opts.no_null_props && !feat.properties) {
25054
+ feat.properties = {};
25055
+ }
25056
+ if (Array.isArray(state.opts.hoist) && feat.properties) {
25057
+ feat.properties = Object.assign({}, feat.properties);
25058
+ state.opts.hoist.forEach(function(field) {
25059
+ if (Object.prototype.hasOwnProperty.call(feat.properties, field)) {
25060
+ feat[field] = feat.properties[field];
25061
+ delete feat.properties[field];
25062
+ }
25063
+ });
25064
+ }
25065
+ return feat;
25066
+ }
25067
+
25068
+ function useFeatureProperties(fields, opts) {
25069
+ return !(opts.drop_table || opts.cut_table || fields.length === 0 ||
25070
+ fields.length == 1 && fields[0] == GeoJSON.ID_FIELD);
25071
+ }
25072
+
24585
25073
  function inferValueType(value) {
24586
25074
  if (value === null || value === undefined) return null;
24587
25075
  if (value instanceof Date) return 'DATETIME';
@@ -24600,10 +25088,6 @@ ${svg}
24600
25088
  }
24601
25089
 
24602
25090
  function normalizeFeature(feature, fields) {
24603
- fields.reduce(function(memo, field) {
24604
- memo[field.name] = field.type;
24605
- return memo;
24606
- }, {});
24607
25091
  var props = {};
24608
25092
  fields.forEach(function(field) {
24609
25093
  var value = feature.properties && Object.prototype.hasOwnProperty.call(feature.properties, field.name) ?
@@ -24622,11 +25106,8 @@ ${svg}
24622
25106
 
24623
25107
  function getExportSrs(dataset) {
24624
25108
  var info = dataset.info || {};
24625
- var gpkgCrs = normalizeSrsForInsert(info.geopackage_crs);
25109
+ var gpkgCrs = resolveExportSrs(normalizeSrsForInsert(info.geopackage_crs), info.wkt1);
24626
25110
  if (gpkgCrs) {
24627
- if (!gpkgCrs.definition && info.wkt1) {
24628
- gpkgCrs.definition = info.wkt1;
24629
- }
24630
25111
  return gpkgCrs;
24631
25112
  }
24632
25113
  var parsed = parseCrsString(info.crs_string);
@@ -24642,13 +25123,83 @@ ${svg}
24642
25123
  if (info.wkt1) {
24643
25124
  return buildCustomSrsFromWkt(info.wkt1);
24644
25125
  }
25126
+ // If the dataset has an in-memory CRS object (e.g. set by -proj) but no
25127
+ // metadata source, derive a WKT1 definition from it so a projected CRS
25128
+ // isn't lost on round-trip.
25129
+ var derivedWkt = getWktFromDatasetCrs(dataset);
25130
+ if (derivedWkt) {
25131
+ return buildCustomSrsFromWkt(derivedWkt);
25132
+ }
25133
+ // Unknown CRS fallback:
25134
+ // - probable decimal-degree coordinates => allow WGS84 assumption
25135
+ // - otherwise use undefined cartesian CRS (null/unknown projection)
25136
+ if (probablyDecimalDegreeBounds(getDatasetBounds(dataset))) {
25137
+ return {
25138
+ srs_id: 4326,
25139
+ organization: 'EPSG',
25140
+ organization_coordsys_id: 4326
25141
+ };
25142
+ }
24645
25143
  return {
24646
- srs_id: 4326,
24647
- organization: 'EPSG',
24648
- organization_coordsys_id: 4326
25144
+ srs_id: -1,
25145
+ srs_name: 'Undefined Cartesian SRS',
25146
+ organization: 'NONE',
25147
+ organization_coordsys_id: -1
24649
25148
  };
24650
25149
  }
24651
25150
 
25151
+ function getWktFromDatasetCrs(dataset) {
25152
+ // Only derive from an explicitly-set CRS object (e.g. from -proj).
25153
+ // This avoids building a custom SRS for lat/lon datasets that were
25154
+ // auto-defaulted to WGS84 by getDatasetCRS().
25155
+ var info = dataset && dataset.info;
25156
+ if (!info || !info.crs) return null;
25157
+ try {
25158
+ return crsToPrj(info.crs) || null;
25159
+ } catch (e) {
25160
+ return null;
25161
+ }
25162
+ }
25163
+
25164
+ function resolveExportSrs(srs, fallbackWkt) {
25165
+ if (!srs) return null;
25166
+ var out = Object.assign({}, srs);
25167
+ if (!out.definition && fallbackWkt) {
25168
+ out.definition = fallbackWkt;
25169
+ }
25170
+ if (out.srs_id == null && out.organization_coordsys_id != null) {
25171
+ out.srs_id = out.organization_coordsys_id;
25172
+ }
25173
+ if (out.srs_id == null) {
25174
+ var fromWkt = parseWktAuthority(out.definition);
25175
+ if (fromWkt) {
25176
+ out.srs_id = fromWkt.srs_id;
25177
+ if (!out.organization) out.organization = fromWkt.organization;
25178
+ if (out.organization_coordsys_id == null) {
25179
+ out.organization_coordsys_id = fromWkt.organization_coordsys_id;
25180
+ }
25181
+ }
25182
+ }
25183
+ if (out.srs_id == null && out.definition) {
25184
+ var custom = buildCustomSrsFromWkt(out.definition);
25185
+ out.srs_id = custom.srs_id;
25186
+ if (!out.srs_name) out.srs_name = custom.srs_name;
25187
+ if (!out.organization) out.organization = custom.organization;
25188
+ if (out.organization_coordsys_id == null) {
25189
+ out.organization_coordsys_id = custom.organization_coordsys_id;
25190
+ }
25191
+ if (!out.description) out.description = custom.description;
25192
+ }
25193
+ if (out.srs_id == null) return null;
25194
+ if (out.organization_coordsys_id == null) {
25195
+ out.organization_coordsys_id = out.srs_id;
25196
+ }
25197
+ if (!out.organization) {
25198
+ out.organization = 'NONE';
25199
+ }
25200
+ return out;
25201
+ }
25202
+
24652
25203
  function parseCrsString(str) {
24653
25204
  if (!str || typeof str != 'string') return null;
24654
25205
  var match = str.match(/^([a-z]+):(\d+)$/i);
@@ -24740,9 +25291,10 @@ ${svg}
24740
25291
  var geopackage = require$1('@ngageoint/geopackage');
24741
25292
  var spatialReferenceSystem = new geopackage.SpatialReferenceSystem();
24742
25293
  spatialReferenceSystem.srs_id = srs.srs_id;
24743
- spatialReferenceSystem.srs_name = srs.srs_name || (srs.organization + ':' + srs.organization_coordsys_id);
24744
- spatialReferenceSystem.organization = srs.organization;
24745
- spatialReferenceSystem.organization_coordsys_id = srs.organization_coordsys_id;
25294
+ spatialReferenceSystem.srs_name = srs.srs_name || (String(srs.organization || 'NONE') + ':' + srs.organization_coordsys_id);
25295
+ spatialReferenceSystem.organization = srs.organization || 'NONE';
25296
+ spatialReferenceSystem.organization_coordsys_id = srs.organization_coordsys_id != null ?
25297
+ srs.organization_coordsys_id : srs.srs_id;
24746
25298
  spatialReferenceSystem.definition = srs.definition;
24747
25299
  spatialReferenceSystem.description = srs.description || null;
24748
25300
  gpkg.createSpatialReferenceSystem(spatialReferenceSystem);
@@ -24751,7 +25303,9 @@ ${svg}
24751
25303
  function getSourceSrsWithoutReprojection(tableSrs, fallbackSrs) {
24752
25304
  var target = normalizeSrsForInsert(tableSrs) || normalizeSrsForInsert(fallbackSrs);
24753
25305
  return {
24754
- srs_id: target.srs_id,
25306
+ // geopackage writer rejects negative srs_id for source metadata;
25307
+ // use 0 (undefined geographic) when target is undefined cartesian (-1).
25308
+ srs_id: target.srs_id < 0 ? 0 : target.srs_id,
24755
25309
  organization: 'EPSG',
24756
25310
  organization_coordsys_id: 4326
24757
25311
  };
@@ -24828,18 +25382,21 @@ ${svg}
24828
25382
  if (normalized.organization_coordsys_id == null && normalized.organizationCoordsysId != null) {
24829
25383
  normalized.organization_coordsys_id = normalized.organizationCoordsysId;
24830
25384
  }
24831
- if (normalized.srs_id == null) {
24832
- normalized.srs_id = normalized.organization_coordsys_id || 4326;
25385
+ if (normalized.srs_id != null) {
25386
+ normalized.srs_id = +normalized.srs_id;
25387
+ if (!Number.isFinite(normalized.srs_id)) {
25388
+ normalized.srs_id = null;
25389
+ }
24833
25390
  }
24834
- normalized.srs_id = +normalized.srs_id || 4326;
24835
- if (!normalized.organization) {
24836
- normalized.organization = 'EPSG';
25391
+ if (normalized.organization) {
25392
+ normalized.organization = String(normalized.organization).toUpperCase();
24837
25393
  }
24838
- normalized.organization = String(normalized.organization).toUpperCase();
24839
- if (normalized.organization_coordsys_id == null) {
24840
- normalized.organization_coordsys_id = normalized.srs_id || 4326;
25394
+ if (normalized.organization_coordsys_id != null) {
25395
+ normalized.organization_coordsys_id = +normalized.organization_coordsys_id;
25396
+ if (!Number.isFinite(normalized.organization_coordsys_id)) {
25397
+ normalized.organization_coordsys_id = null;
25398
+ }
24841
25399
  }
24842
- normalized.organization_coordsys_id = +normalized.organization_coordsys_id || 4326;
24843
25400
  return normalized;
24844
25401
  }
24845
25402
 
@@ -26869,6 +27426,10 @@ ${svg}
26869
27426
  .option('name', {
26870
27427
  describe: 'rename the imported layer(s)'
26871
27428
  })
27429
+ .option('layers', {
27430
+ type: 'strings',
27431
+ describe: '[GPKG] comma-sep. list of layers to import'
27432
+ })
26872
27433
  .option('geometry-type', {
26873
27434
  // undocumented; GeoJSON import rejects all but one kind of geometry
26874
27435
  // describe: '[GeoJSON] Import one kind of geometry (point|polygon|polyline)'
@@ -31463,24 +32024,35 @@ ${svg}
31463
32024
  var geopackage = require$1('@ngageoint/geopackage');
31464
32025
  var gpkg;
31465
32026
  var datasets;
31466
- var source;
31467
32027
  var tmpPath = null;
31468
32028
 
31469
32029
  if (!geopackage || !geopackage.GeoPackageAPI) {
31470
32030
  stop$1('GeoPackage library is not loaded');
31471
32031
  }
31472
32032
 
31473
- if (utils.isString(content)) {
31474
- source = content;
31475
- } else if (!runningInBrowser()) {
31476
- tmpPath = writeGeoPackageTempFile(content);
31477
- source = tmpPath;
31478
- } else {
31479
- source = new Uint8Array(content);
31480
- }
31481
- gpkg = await geopackage.GeoPackageAPI.open(source);
32033
+ ({gpkg, tmpPath} = await openGeoPackage(content, geopackage));
32034
+ var availableLayers;
32035
+ var filterApplied;
31482
32036
  try {
31483
- datasets = readFeatureTableDatasets(gpkg, opts);
32037
+ try {
32038
+ ({datasets, availableLayers, filterApplied} = readFeatureTableDatasets(gpkg, opts));
32039
+ } catch (e) {
32040
+ if (!runningInBrowser() && isLocalCsProjError(e)) {
32041
+ gpkg.close();
32042
+ if (tmpPath) {
32043
+ sanitizeGeoPackageCrsMetadata(tmpPath);
32044
+ } else if (utils.isString(content)) {
32045
+ tmpPath = copyGeoPackageTempFile(content);
32046
+ sanitizeGeoPackageCrsMetadata(tmpPath);
32047
+ } else {
32048
+ throw e;
32049
+ }
32050
+ gpkg = await geopackage.GeoPackageAPI.open(tmpPath);
32051
+ ({datasets, availableLayers, filterApplied} = readFeatureTableDatasets(gpkg, opts));
32052
+ } else {
32053
+ throw e;
32054
+ }
32055
+ }
31484
32056
  } finally {
31485
32057
  gpkg.close();
31486
32058
  removeTempGeoPackageFile(tmpPath);
@@ -31489,13 +32061,72 @@ ${svg}
31489
32061
  if (datasets.length === 0) {
31490
32062
  return {
31491
32063
  layers: [{name: '', data: null}],
31492
- info: {}
32064
+ info: {
32065
+ _gpkg_available_layers: availableLayers,
32066
+ _gpkg_placeholder: !!filterApplied
32067
+ }
31493
32068
  };
31494
32069
  }
31495
32070
 
31496
32071
  await initProjLib(datasets);
32072
+ var merged = mergeGeoPackageDatasets(datasets);
32073
+ var mergedArr = Array.isArray(merged) ? merged : [merged];
32074
+ mergedArr.forEach(function(ds) {
32075
+ ds.info = ds.info || {};
32076
+ ds.info._gpkg_available_layers = availableLayers;
32077
+ });
32078
+ return merged;
32079
+ }
31497
32080
 
31498
- return mergeDatasets(datasets);
32081
+ async function openGeoPackage(content, geopackage) {
32082
+ var source;
32083
+ var tmpPath = null;
32084
+ if (utils.isString(content)) {
32085
+ if (!runningInBrowser()) {
32086
+ try {
32087
+ return {
32088
+ gpkg: await geopackage.GeoPackageAPI.open(content),
32089
+ tmpPath: null
32090
+ };
32091
+ } catch (e) {
32092
+ if (!isLocalCsProjError(e)) throw e;
32093
+ tmpPath = copyGeoPackageTempFile(content);
32094
+ sanitizeGeoPackageCrsMetadata(tmpPath);
32095
+ return {
32096
+ gpkg: await geopackage.GeoPackageAPI.open(tmpPath),
32097
+ tmpPath: tmpPath
32098
+ };
32099
+ }
32100
+ }
32101
+ source = content;
32102
+ } else if (!runningInBrowser()) {
32103
+ tmpPath = writeGeoPackageTempFile(content);
32104
+ try {
32105
+ return {
32106
+ gpkg: await geopackage.GeoPackageAPI.open(tmpPath),
32107
+ tmpPath: tmpPath
32108
+ };
32109
+ } catch (e) {
32110
+ if (!isLocalCsProjError(e)) throw e;
32111
+ sanitizeGeoPackageCrsMetadata(tmpPath);
32112
+ return {
32113
+ gpkg: await geopackage.GeoPackageAPI.open(tmpPath),
32114
+ tmpPath: tmpPath
32115
+ };
32116
+ }
32117
+ } else {
32118
+ source = new Uint8Array(content);
32119
+ }
32120
+ return {
32121
+ gpkg: await geopackage.GeoPackageAPI.open(source),
32122
+ tmpPath: null
32123
+ };
32124
+ }
32125
+
32126
+ function isLocalCsProjError(err) {
32127
+ var msg = err && err.message ? String(err.message) : '';
32128
+ return msg.includes("havn't handled \"_\" in keyword yet") ||
32129
+ msg.includes('LOCAL_CS');
31499
32130
  }
31500
32131
 
31501
32132
  function writeGeoPackageTempFile(content) {
@@ -31504,10 +32135,40 @@ ${svg}
31504
32135
  var path = require$1('path');
31505
32136
  var unique = Date.now() + '-' + process.pid + '-' + Math.random().toString(36).slice(2);
31506
32137
  var tmpPath = path.join(os.tmpdir(), 'mapshaper-gpkg-import-' + unique + '.gpkg');
31507
- fs.writeFileSync(tmpPath, Buffer.from(new Uint8Array(content)));
32138
+ if (Buffer.isBuffer(content)) {
32139
+ fs.writeFileSync(tmpPath, content);
32140
+ } else if (content instanceof Uint8Array) {
32141
+ fs.writeFileSync(tmpPath, Buffer.from(content.buffer, content.byteOffset, content.byteLength));
32142
+ } else if (content instanceof ArrayBuffer) {
32143
+ fs.writeFileSync(tmpPath, Buffer.from(content));
32144
+ } else {
32145
+ fs.writeFileSync(tmpPath, Buffer.from(new Uint8Array(content)));
32146
+ }
31508
32147
  return tmpPath;
31509
32148
  }
31510
32149
 
32150
+ function copyGeoPackageTempFile(filepath) {
32151
+ var fs = require$1('fs');
32152
+ var os = require$1('os');
32153
+ var path = require$1('path');
32154
+ var unique = Date.now() + '-' + process.pid + '-' + Math.random().toString(36).slice(2);
32155
+ var tmpPath = path.join(os.tmpdir(), 'mapshaper-gpkg-import-' + unique + '.gpkg');
32156
+ fs.copyFileSync(filepath, tmpPath);
32157
+ return tmpPath;
32158
+ }
32159
+
32160
+ function sanitizeGeoPackageCrsMetadata(filepath) {
32161
+ var Database = require$1('better-sqlite3');
32162
+ var db = new Database(filepath);
32163
+ try {
32164
+ // GDAL may write LOCAL_CS definitions that proj4 can't parse.
32165
+ // Normalize to 'undefined' so GeoPackageAPI.open() won't throw.
32166
+ db.prepare("UPDATE gpkg_spatial_ref_sys SET definition = 'undefined' WHERE definition LIKE 'LOCAL_CS[%'").run();
32167
+ } finally {
32168
+ db.close();
32169
+ }
32170
+ }
32171
+
31511
32172
  function removeTempGeoPackageFile(filepath) {
31512
32173
  if (!filepath) return;
31513
32174
  var fs = require$1('fs');
@@ -31526,13 +32187,207 @@ ${svg}
31526
32187
  }
31527
32188
 
31528
32189
  function readFeatureTableDatasets(gpkg, opts) {
31529
- var tables = gpkg.getFeatureTables() || [];
31530
- return tables.map(function(table) {
32190
+ var selected = getSelectedGeoPackageLayers(opts);
32191
+ var availableLayers = gpkg.getFeatureTables() || [];
32192
+ var tables = filterGeoPackageTables(availableLayers, selected);
32193
+ var filterApplied = Array.isArray(selected) && selected.length > 0;
32194
+ var datasets = tables.map(function(table) {
31531
32195
  return readFeatureTable(gpkg, table, opts);
31532
32196
  });
32197
+ return {datasets: datasets, availableLayers: availableLayers, filterApplied: filterApplied};
32198
+ }
32199
+
32200
+ function getSelectedGeoPackageLayers(opts) {
32201
+ if (!opts) return null;
32202
+ return opts.gpkg_layers || opts.layers || null;
32203
+ }
32204
+
32205
+ function filterGeoPackageTables(tables, selected) {
32206
+ if (!Array.isArray(selected) || selected.length === 0) return tables;
32207
+ var index = selected.reduce(function(memo, name) {
32208
+ memo[name] = true;
32209
+ return memo;
32210
+ }, {});
32211
+ return tables.filter(function(name) {
32212
+ return !!index[name];
32213
+ });
32214
+ }
32215
+
32216
+ function mergeGeoPackageDatasets(datasets) {
32217
+ var groups = groupGeoPackageDatasets(datasets);
32218
+ var merged = groups.reduce(function(memo, group) {
32219
+ return memo.concat(mergeGeoPackageDatasetGroup(group));
32220
+ }, []);
32221
+ return merged.length == 1 ? merged[0] : merged;
31533
32222
  }
31534
32223
 
31535
- function convertOrgToProj4(crs) {
32224
+ function mergeGeoPackageDatasetGroup(group) {
32225
+ var pathDatasets = [];
32226
+ var pointDatasets = [];
32227
+ var dataDatasets = [];
32228
+
32229
+ group.forEach(function(dataset) {
32230
+ var kind = getDatasetGeometryKind(dataset);
32231
+ if (kind == 'path') {
32232
+ pathDatasets.push(dataset);
32233
+ } else if (kind == 'point') {
32234
+ pointDatasets.push(dataset);
32235
+ } else {
32236
+ dataDatasets.push(dataset);
32237
+ }
32238
+ });
32239
+
32240
+ var output = [];
32241
+ if (dataDatasets.length > 0) {
32242
+ output.push(dataDatasets.length == 1 ? dataDatasets[0] : mergeDatasets(dataDatasets));
32243
+ }
32244
+ if (pointDatasets.length > 0) {
32245
+ output.push(pointDatasets.length == 1 ? pointDatasets[0] : mergeDatasets(pointDatasets));
32246
+ }
32247
+ if (pathDatasets.length > 0) {
32248
+ var groupedPaths = groupPathDatasetsBySharedArcs(pathDatasets);
32249
+ if (groupedPaths.mergedDataset) {
32250
+ output.push(groupedPaths.mergedDataset);
32251
+ } else {
32252
+ groupedPaths.components.forEach(function(component) {
32253
+ output.push(component.length == 1 ? component[0] : mergeDatasets(component));
32254
+ });
32255
+ }
32256
+ }
32257
+ return output;
32258
+ }
32259
+
32260
+ function groupGeoPackageDatasets(datasets) {
32261
+ var index = {};
32262
+ datasets.forEach(function(dataset) {
32263
+ var key = getGeoPackageDatasetGroupKey(dataset);
32264
+ if (!index[key]) index[key] = [];
32265
+ index[key].push(dataset);
32266
+ });
32267
+ return Object.keys(index).map(function(key) {
32268
+ return index[key];
32269
+ });
32270
+ }
32271
+
32272
+ function getGeoPackageDatasetGroupKey(dataset) {
32273
+ var info = dataset.info || {};
32274
+ var crs = info.geopackage_crs || null;
32275
+ if (isDataOnlyDataset(dataset)) {
32276
+ return 'data-only';
32277
+ }
32278
+ if (crs) {
32279
+ var org = normalizeCrsOrg(crs.organization);
32280
+ var code = normalizeNumericCode(crs.organization_coordsys_id);
32281
+ if (isTrustedCrsAuthority(org) && code !== null && !(org == 'NONE' && code <= 0)) {
32282
+ return 'crs:' + org + ':' + code;
32283
+ }
32284
+ if (isUsableWkt1Definition(crs.definition)) {
32285
+ return 'wkt:' + crs.definition;
32286
+ }
32287
+ }
32288
+ return probablyDecimalDegreeBounds(getDatasetBounds(dataset)) ?
32289
+ 'unknown:unprojected' :
32290
+ 'unknown:projected';
32291
+ }
32292
+
32293
+ function isDataOnlyDataset(dataset) {
32294
+ return (dataset.layers || []).every(function(lyr) {
32295
+ return !lyr.geometry_type;
32296
+ });
32297
+ }
32298
+
32299
+ function getDatasetGeometryKind(dataset) {
32300
+ if (isDataOnlyDataset(dataset)) return 'data';
32301
+ if (datasetHasPaths(dataset)) return 'path';
32302
+ var hasPoints = (dataset.layers || []).some(function(lyr) {
32303
+ return layerHasPoints(lyr);
32304
+ });
32305
+ return hasPoints ? 'point' : 'data';
32306
+ }
32307
+
32308
+ function groupPathDatasetsBySharedArcs(datasets) {
32309
+ if (datasets.length < 2) {
32310
+ return {
32311
+ mergedDataset: datasets[0] || null,
32312
+ components: []
32313
+ };
32314
+ }
32315
+ var copy = datasets.map(copyDatasetForExport);
32316
+ var merged = mergeDatasets(copy);
32317
+ if (!merged.arcs || merged.arcs.size() === 0) {
32318
+ return {
32319
+ mergedDataset: null,
32320
+ components: datasets.map(function(dataset) { return [dataset]; })
32321
+ };
32322
+ }
32323
+ buildTopology(merged);
32324
+ var parent = datasets.map(function(_, i) { return i; });
32325
+ var arcOwner = new Map();
32326
+ merged.layers.forEach(function(layer, layerIdx) {
32327
+ var seen = new Set();
32328
+ forEachArcId(layer.shapes || [], function(arcId) {
32329
+ seen.add(arcId < 0 ? ~arcId : arcId);
32330
+ });
32331
+ seen.forEach(function(absId) {
32332
+ if (!arcOwner.has(absId)) {
32333
+ arcOwner.set(absId, layerIdx);
32334
+ } else {
32335
+ union(parent, layerIdx, arcOwner.get(absId));
32336
+ }
32337
+ });
32338
+ });
32339
+ var components = {};
32340
+ datasets.forEach(function(dataset, i) {
32341
+ var root = find(parent, i);
32342
+ if (!components[root]) components[root] = [];
32343
+ components[root].push(dataset);
32344
+ });
32345
+ var grouped = Object.keys(components).map(function(key) {
32346
+ return components[key];
32347
+ });
32348
+ if (grouped.length == 1) {
32349
+ return {
32350
+ mergedDataset: merged,
32351
+ components: []
32352
+ };
32353
+ }
32354
+ return {
32355
+ mergedDataset: null,
32356
+ components: grouped
32357
+ };
32358
+ }
32359
+
32360
+ function find(parent, i) {
32361
+ var p = parent[i];
32362
+ if (p !== i) {
32363
+ parent[i] = find(parent, p);
32364
+ }
32365
+ return parent[i];
32366
+ }
32367
+
32368
+ function union(parent, a, b) {
32369
+ var ra = find(parent, a);
32370
+ var rb = find(parent, b);
32371
+ if (ra !== rb) {
32372
+ parent[rb] = ra;
32373
+ }
32374
+ }
32375
+
32376
+ function normalizeNumericCode(val) {
32377
+ var n = +val;
32378
+ return Number.isFinite(n) ? n : null;
32379
+ }
32380
+
32381
+ function normalizeCrsOrg(org) {
32382
+ if (!org || org == 'undefined') return null;
32383
+ return String(org).toUpperCase();
32384
+ }
32385
+
32386
+ function isTrustedCrsAuthority(org) {
32387
+ return org == 'EPSG' || org == 'ESRI' || org == 'NONE';
32388
+ }
32389
+
32390
+ function convertOrgToProjString(crs) {
31536
32391
  var org = crs.organization.toLowerCase();
31537
32392
  if (org == 'epsg' || org == 'esri') {
31538
32393
  return org + ':' + crs.organization_coordsys_id;
@@ -31560,14 +32415,25 @@ ${svg}
31560
32415
  });
31561
32416
  dataset.info = dataset.info || {};
31562
32417
  dataset.info.geopackage_crs = crs;
31563
- if (crs?.definition && crs.definition !== 'undefined') {
32418
+ if (isUsableWkt1Definition(crs?.definition)) {
31564
32419
  dataset.info.wkt1 = crs.definition;
31565
32420
  } else if (crs?.organization && crs.organization !== 'undefined') {
31566
- dataset.info.crs_string = convertOrgToProj4(crs);
32421
+ var crsString = convertOrgToProjString(crs);
32422
+ if (crsString) {
32423
+ dataset.info.crs_string = crsString;
32424
+ }
31567
32425
  }
31568
32426
  return dataset;
31569
32427
  }
31570
32428
 
32429
+ function isUsableWkt1Definition(defn) {
32430
+ if (!defn || defn === 'undefined') return false;
32431
+ // GDAL may emit LOCAL_CS["Undefined SRS", ...] for null CRS;
32432
+ // this is not a usable projected/geographic CRS for mapshaper.
32433
+ if (/^\s*LOCAL_CS\[/i.test(defn)) return false;
32434
+ return true;
32435
+ }
32436
+
31571
32437
  function convertFeatureRow(featureRow) {
31572
32438
  var feature = {
31573
32439
  type: 'Feature',
@@ -31583,8 +32449,7 @@ ${svg}
31583
32449
  // skip feature if geometry can't be parsed
31584
32450
  return null;
31585
32451
  }
31586
- if (!feature.geometry) return null;
31587
- var geomColName = featureRow.geometryColumn.name;
32452
+ var geomColName = featureRow.geometryColumn && featureRow.geometryColumn.name;
31588
32453
  for (var key in featureRow.values) {
31589
32454
  if (Object.prototype.hasOwnProperty.call(featureRow.values, key) &&
31590
32455
  key !== geomColName) {
@@ -32363,6 +33228,11 @@ ${svg}
32363
33228
  } else {
32364
33229
  return importContent(obj, opts);
32365
33230
  }
33231
+ if (Array.isArray(dataset)) {
33232
+ return dataset.map(function(ds) {
33233
+ return finalizeImportedDataset(ds, dataFmt, data, opts);
33234
+ });
33235
+ }
32366
33236
  return finalizeImportedDataset(dataset, dataFmt, data, opts);
32367
33237
  }
32368
33238
 
@@ -32564,12 +33434,18 @@ ${svg}
32564
33434
 
32565
33435
  cmd.importFiles = async function(catalog, opts) {
32566
33436
  var files = opts.files || [];
32567
- var dataset;
33437
+ var dataset, datasets, target;
32568
33438
 
32569
33439
  if (opts.stdin) {
32570
33440
  dataset = await importFileAsync('/dev/stdin', opts);
32571
- catalog.addDataset(dataset);
32572
- return dataset;
33441
+ datasets = normalizeImportedDatasets(dataset);
33442
+ catalog.addDatasets(datasets);
33443
+ if (datasets.length > 1) {
33444
+ catalog.setDefaultTargets(datasets.map(function(ds) {
33445
+ return {dataset: ds, layers: ds.layers};
33446
+ }));
33447
+ }
33448
+ return normalizeImportedTarget(datasets);
32573
33449
  }
32574
33450
 
32575
33451
  if (files.length > 0 === false) {
@@ -32604,14 +33480,22 @@ ${svg}
32604
33480
  } else {
32605
33481
  dataset = await importFilesTogetherAsync(files, opts);
32606
33482
  }
33483
+ datasets = normalizeImportedDatasets(dataset);
33484
+ datasets = validateAndCleanGpkgSelection(datasets, opts);
32607
33485
 
32608
33486
  if (opts.merge_files && files.length > 1) {
32609
33487
  // TODO: deprecate and remove this option (use -merge-layers cmd instead)
32610
- dataset.layers = cmd.mergeLayers(dataset.layers);
33488
+ datasets[0].layers = cmd.mergeLayers(datasets[0].layers);
32611
33489
  }
32612
33490
 
32613
- catalog.addDataset(dataset);
32614
- return dataset;
33491
+ catalog.addDatasets(datasets);
33492
+ if (datasets.length > 1) {
33493
+ catalog.setDefaultTargets(datasets.map(function(ds) {
33494
+ return {dataset: ds, layers: ds.layers};
33495
+ }));
33496
+ }
33497
+ target = normalizeImportedTarget(datasets);
33498
+ return target;
32615
33499
  };
32616
33500
 
32617
33501
  // replace any JSON data objects with filenames and cache the data
@@ -32712,7 +33596,8 @@ ${svg}
32712
33596
 
32713
33597
  cli.checkFileExists(path, cache);
32714
33598
 
32715
- if ((fileType == 'shp' || fileType == 'json' || fileType == 'text' || fileType == 'dbf') && !cached) {
33599
+ if ((fileType == 'shp' || fileType == 'json' || fileType == 'text' || fileType == 'dbf' ||
33600
+ fileType == 'gpkg') && !cached) {
32716
33601
  // these file types are read incrementally
32717
33602
  content = null;
32718
33603
 
@@ -32815,17 +33700,20 @@ ${svg}
32815
33700
  // Import multiple files to a single dataset
32816
33701
  function importFilesTogether(files, opts) {
32817
33702
  var unbuiltTopology = false;
32818
- var datasets = files.map(function(fname) {
33703
+ var datasets = files.reduce(function(memo, fname) {
32819
33704
  // import without topology or snapping
32820
33705
  var importOpts = utils.defaults({no_topology: true, snap: false, snap_interval: null, files: [fname]}, opts);
32821
- var dataset = importFile(fname, importOpts);
33706
+ var imported = normalizeImportedDatasets(importFile(fname, importOpts));
32822
33707
  // check if dataset contains non-topological paths
32823
33708
  // TODO: may also need to rebuild topology if multiple topojson files are merged
32824
- if (dataset.arcs && dataset.arcs.size() > 0 && dataset.info.input_formats[0] != 'topojson') {
32825
- unbuiltTopology = true;
32826
- }
32827
- return dataset;
32828
- });
33709
+ imported.forEach(function(dataset) {
33710
+ if (dataset.arcs && dataset.arcs.size() > 0 && dataset.info.input_formats[0] != 'topojson') {
33711
+ unbuiltTopology = true;
33712
+ }
33713
+ memo.push(dataset);
33714
+ });
33715
+ return memo;
33716
+ }, []);
32829
33717
  var combined = mergeDatasets(datasets);
32830
33718
  // Build topology, if needed
32831
33719
  // TODO: consider updating topology of TopoJSON files instead of concatenating arcs
@@ -32843,12 +33731,15 @@ ${svg}
32843
33731
  for (var fname of files) {
32844
33732
  // import without topology or snapping
32845
33733
  var importOpts = utils.defaults({no_topology: true, snap: false, snap_interval: null, files: [fname]}, opts);
32846
- var dataset = await importFileAsync(fname, importOpts);
32847
- if (dataset.arcs && dataset.arcs.size() > 0 && dataset.info.input_formats[0] != 'topojson') {
32848
- unbuiltTopology = true;
32849
- }
32850
- datasets.push(dataset);
33734
+ var imported = normalizeImportedDatasets(await importFileAsync(fname, importOpts));
33735
+ imported.forEach(function(dataset) {
33736
+ if (dataset.arcs && dataset.arcs.size() > 0 && dataset.info.input_formats[0] != 'topojson') {
33737
+ unbuiltTopology = true;
33738
+ }
33739
+ datasets.push(dataset);
33740
+ });
32851
33741
  }
33742
+ datasets = validateAndCleanGpkgSelection(datasets, opts);
32852
33743
  var combined = mergeDatasets(datasets);
32853
33744
  if (unbuiltTopology && !opts.no_topology) {
32854
33745
  cleanPathsAfterImport(combined, opts);
@@ -32857,6 +33748,60 @@ ${svg}
32857
33748
  return combined;
32858
33749
  }
32859
33750
 
33751
+ // Validate the GeoPackage layers= selection across all imported datasets,
33752
+ // remove any placeholder datasets produced by filter misses, and strip the
33753
+ // bookkeeping metadata attached by the GeoPackage importer.
33754
+ function validateAndCleanGpkgSelection(datasets, opts) {
33755
+ var availableSet = new Set();
33756
+ var importedSet = new Set();
33757
+ var sawGpkg = false;
33758
+ datasets.forEach(function(ds) {
33759
+ var info = ds && ds.info;
33760
+ if (!info || !Array.isArray(info._gpkg_available_layers)) return;
33761
+ sawGpkg = true;
33762
+ info._gpkg_available_layers.forEach(function(name) { availableSet.add(name); });
33763
+ if (!info._gpkg_placeholder) {
33764
+ (ds.layers || []).forEach(function(lyr) {
33765
+ if (lyr && lyr.name) importedSet.add(lyr.name);
33766
+ });
33767
+ }
33768
+ });
33769
+ if (sawGpkg && Array.isArray(opts.layers) && opts.layers.length > 0) {
33770
+ var missing = opts.layers.filter(function(name) {
33771
+ return !importedSet.has(name);
33772
+ });
33773
+ if (missing.length > 0) {
33774
+ stop$1(
33775
+ 'Missing GeoPackage layer(s): ' + missing.join(', ') + '\n' +
33776
+ 'Existing layers: ' + Array.from(availableSet).join(' ')
33777
+ );
33778
+ }
33779
+ }
33780
+ var cleaned = datasets.filter(function(ds) {
33781
+ return !(ds && ds.info && ds.info._gpkg_placeholder);
33782
+ });
33783
+ cleaned.forEach(function(ds) {
33784
+ if (ds && ds.info) {
33785
+ delete ds.info._gpkg_available_layers;
33786
+ delete ds.info._gpkg_placeholder;
33787
+ }
33788
+ });
33789
+ return cleaned;
33790
+ }
33791
+
33792
+ function normalizeImportedDatasets(datasetOrArray) {
33793
+ return Array.isArray(datasetOrArray) ? datasetOrArray : [datasetOrArray];
33794
+ }
33795
+
33796
+ function normalizeImportedTarget(datasets) {
33797
+ if (datasets.length == 1) return datasets[0];
33798
+ return {
33799
+ layers: datasets.reduce(function(memo, dataset) {
33800
+ return memo.concat(dataset.layers);
33801
+ }, [])
33802
+ };
33803
+ }
33804
+
32860
33805
  function getUnsupportedFileMessage(path) {
32861
33806
  var ext = getFileExtension(path);
32862
33807
  var msg = 'Unable to import ' + path;
@@ -39139,6 +40084,7 @@ ${svg}
39139
40084
 
39140
40085
  // assumes layers and arcs have been prepared for clipping
39141
40086
  function clipPolygons(targetShapes, clipShapes, nodes, type, optsArg) {
40087
+ profileStart('clipPolygons');
39142
40088
  var arcs = nodes.arcs;
39143
40089
  var opts = optsArg || {};
39144
40090
  var clipFlags = new Uint8Array(arcs.size());
@@ -39149,59 +40095,47 @@ ${svg}
39149
40095
  var findPath = getPathFinder(nodes, useRoute, routeIsActive);
39150
40096
  var dissolvePolygon = getPolygonDissolver(nodes);
39151
40097
 
39152
- // The following cleanup step is a performance bottleneck (it often takes longer than
39153
- // other clipping operations) and is usually not needed. Furthermore, it only
39154
- // eliminates a few kinds of problems, like target polygons with abnormal winding
39155
- // or overlapping rings. TODO: try to optimize or remove it for all cases
39156
-
39157
- // skipping shape cleanup when using the experimental fast bbox clipping option
39158
- // if (!opts.bbox2 && !opts.no_cleanup) {
39159
40098
  if (!opts.bbox2) {
39160
- // clean each target polygon by dissolving its rings
40099
+ profileStart('cp.dissolveTargetRings');
39161
40100
  targetShapes = targetShapes.map(dissolvePolygon);
40101
+ profileEnd('cp.dissolveTargetRings');
39162
40102
  }
39163
40103
 
39164
- // Originally, clip shapes were dissolved here as an optimization, using
39165
- // an unreliable dissolve function.
39166
- // Now, clip shapes are dissolved using a more reliable (but slower)
39167
- // function in mapshaper-clip-erase.js
39168
- // clipShapes = [dissolvePolygon(internal.concatShapes(clipShapes))];
39169
-
39170
- // Open pathways in the clip/erase layer
39171
- // Need to expose clip/erase routes in both directions by setting route
39172
- // in both directions to visible -- this is how cut-out shapes are detected
39173
- // Or-ing with 0x11 makes both directions visible (so reverse paths will block)
40104
+ profileStart('cp.openClipRoutes');
39174
40105
  openArcRoutes(clipShapes, arcs, clipFlags, type == 'clip', type == 'erase', true, 0x11);
40106
+ profileEnd('cp.openClipRoutes');
40107
+ profileStart('cp.PathIndex#1');
39175
40108
  var index = new PathIndex(clipShapes, arcs);
40109
+ profileEnd('cp.PathIndex#1');
40110
+ profileStart('cp.clipShapes');
39176
40111
  var clippedShapes = targetShapes.map(function(shape, i) {
39177
40112
  if (shape) {
39178
40113
  return clipPolygon(shape, type, index);
39179
40114
  }
39180
40115
  return null;
39181
40116
  });
40117
+ profileEnd('cp.clipShapes');
39182
40118
 
39183
- markPathsAsUsed(clippedShapes, routeFlags); // to help us find unused paths later
39184
-
39185
-
39186
- // add clip/erase polygons that are fully contained in a target polygon
39187
- // need to index only non-intersecting clip shapes
39188
- // (Intersecting shapes have one or more arcs that have been scanned)
39189
-
39190
- // first, find shapes that do not intersect the target layer
39191
- // (these could be inside or outside the target polygons)
40119
+ markPathsAsUsed(clippedShapes, routeFlags);
39192
40120
 
40121
+ profileStart('cp.findUndividedClip');
39193
40122
  var undividedClipShapes = findUndividedClipShapes(clipShapes);
40123
+ profileEnd('cp.findUndividedClip');
39194
40124
 
39195
- closeArcRoutes(clipShapes, arcs, routeFlags, true, true); // not needed?
40125
+ closeArcRoutes(clipShapes, arcs, routeFlags, true, true);
40126
+ profileStart('cp.PathIndex#2');
39196
40127
  index = new PathIndex(undividedClipShapes, arcs);
40128
+ profileEnd('cp.PathIndex#2');
40129
+ profileStart('cp.findInteriorPaths');
39197
40130
  targetShapes.forEach(function(shape, shapeId) {
39198
- // find clipping paths that are internal to this target polygon
39199
40131
  var paths = shape ? findInteriorPaths(shape, type, index) : null;
39200
40132
  if (paths) {
39201
40133
  clippedShapes[shapeId] = (clippedShapes[shapeId] || []).concat(paths);
39202
40134
  }
39203
40135
  });
40136
+ profileEnd('cp.findInteriorPaths');
39204
40137
 
40138
+ profileEnd('clipPolygons');
39205
40139
  return clippedShapes;
39206
40140
 
39207
40141
  function clipPolygon(shape, type, index) {
@@ -39629,31 +40563,36 @@ ${svg}
39629
40563
  // @clipSrc: layer in @dataset or filename
39630
40564
  // @type: 'clip' or 'erase'
39631
40565
  function clipLayers(targetLayers, clipSrc, targetDataset, type, opts) {
40566
+ profileStart('clipLayers');
39632
40567
  var usingPathClip = utils.some(targetLayers, layerHasPaths);
39633
- var mergedDataset, clipLyr, nodes;
40568
+ var mergedDataset, clipLyr, nodes, result;
39634
40569
  opts = opts || {no_cleanup: true}; // TODO: update testing functions
39635
40570
  if (opts.bbox2 && usingPathClip) { // assumes target dataset has arcs
39636
- return clipLayersByBBox(targetLayers, targetDataset, opts);
40571
+ result = clipLayersByBBox(targetLayers, targetDataset, opts);
40572
+ profileEnd('clipLayers');
40573
+ return result;
39637
40574
  }
40575
+ profileStart('mergeLayersForOverlay');
39638
40576
  mergedDataset = mergeLayersForOverlay(targetLayers, targetDataset, clipSrc, opts);
40577
+ profileEnd('mergeLayersForOverlay');
39639
40578
  clipLyr = mergedDataset.layers[mergedDataset.layers.length-1];
39640
40579
  if (usingPathClip) {
39641
- // add vertices at all line intersections
39642
- // (generally slower than actual clipping)
39643
40580
  nodes = addIntersectionCuts(mergedDataset, opts);
39644
40581
  targetDataset.arcs = mergedDataset.arcs;
39645
- // dissolve clip layer shapes (to remove overlaps and other topological issues
39646
- // that might confuse the clipping function)
39647
- // use a data-free copy of the clip lyr, so data records are not dissolved
39648
- // (this avoids triggering an unnecessary and expensive DBF read operation in some cases).
40582
+ profileStart('clipDissolvePolygonLayer2');
39649
40583
  clipLyr = utils.defaults({data: null}, clipLyr);
39650
40584
  clipLyr = dissolvePolygonLayer2(clipLyr, mergedDataset, {quiet: true, silent: true});
40585
+ profileEnd('clipDissolvePolygonLayer2');
39651
40586
 
39652
40587
  } else {
39653
40588
  nodes = new NodeCollection(mergedDataset.arcs);
39654
40589
  }
39655
40590
 
39656
- return clipLayersByLayer(targetLayers, clipLyr, nodes, type, opts);
40591
+ profileStart('clipLayersByLayer');
40592
+ result = clipLayersByLayer(targetLayers, clipLyr, nodes, type, opts);
40593
+ profileEnd('clipLayersByLayer');
40594
+ profileEnd('clipLayers');
40595
+ return result;
39657
40596
  }
39658
40597
 
39659
40598
  function clipLayersByBBox(layers, dataset, opts) {
@@ -50192,7 +51131,7 @@ ${svg}
50192
51131
  });
50193
51132
  }
50194
51133
 
50195
- var version = "0.6.117";
51134
+ var version = "0.6.119";
50196
51135
 
50197
51136
  // Parse command line args into commands and run them
50198
51137
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -50890,6 +51829,7 @@ ${svg}
50890
51829
  LayerUtils,
50891
51830
  Lines,
50892
51831
  Logging,
51832
+ Profile,
50893
51833
  Merging,
50894
51834
  MosaicIndex$1,
50895
51835
  OptionParsingUtils,