mapshaper 0.6.118 → 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/mapshaper.js CHANGED
@@ -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
 
@@ -7320,15 +7343,201 @@
7320
7343
  };
7321
7344
  }
7322
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
+
7323
7530
  // Dissolve arcs that can be merged without affecting topology of layers
7324
7531
  // remove arcs that are not referenced by any layer; remap arc ids
7325
7532
  // in layers. (dataset.arcs is replaced).
7326
7533
  function dissolveArcs(dataset) {
7534
+ profileStart('dissolveArcs.body');
7327
7535
  var arcs = dataset.arcs,
7328
7536
  layers = dataset.layers.filter(layerHasPaths);
7329
7537
 
7330
7538
  if (!arcs || !layers.length) {
7331
7539
  dataset.arcs = null;
7540
+ profileEnd('dissolveArcs.body');
7332
7541
  return;
7333
7542
  }
7334
7543
 
@@ -7338,14 +7547,17 @@
7338
7547
  arcIndex = new Int32Array(arcs.size()), // maps old arc ids to new ids
7339
7548
  arcStatus = new Uint8Array(arcs.size());
7340
7549
  // arcStatus: 0 = unvisited, 1 = dropped, 2 = remapped, 3 = remapped + reversed
7550
+ profileStart('dissolveArcs.translatePaths');
7341
7551
  layers.forEach(function(lyr) {
7342
- // modify copies of the original shapes; original shapes should be unmodified
7343
- // (need to test this)
7344
7552
  lyr.shapes = lyr.shapes.map(function(shape, i) {
7345
7553
  return editShapeParts(shape && shape.concat(), translatePath);
7346
7554
  });
7347
7555
  });
7556
+ profileEnd('dissolveArcs.translatePaths');
7557
+ profileStart('dissolveArcs.dissolveArcCollection');
7348
7558
  dataset.arcs = dissolveArcCollection(arcs, newArcs, totalPoints);
7559
+ profileEnd('dissolveArcs.dissolveArcCollection');
7560
+ profileEnd('dissolveArcs.body');
7349
7561
 
7350
7562
  function translatePath(path) {
7351
7563
  var pointCount = 0;
@@ -12507,14 +12719,22 @@
12507
12719
 
12508
12720
  function testPointInRing(p, cand) {
12509
12721
  if (!cand.bounds.containsPoint(p[0], p[1])) return false;
12510
- if (!cand.index && cand.bounds.area() > totalArea * 0.01) {
12511
- // index larger polygons (because they are slower to test via pointInRing()
12512
- // and they are more likely to be involved in repeated hit tests).
12513
- 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
+ }
12514
12736
  }
12515
- return cand.index ?
12516
- cand.index.pointInPolygon(p[0], p[1]) :
12517
- geom.testPointInRing(p[0], p[1], cand.ids, arcs);
12737
+ return cand.index.pointInPolygon(p[0], p[1]);
12518
12738
  }
12519
12739
 
12520
12740
  //
@@ -13534,6 +13754,7 @@
13534
13754
 
13535
13755
  function singleStripeId(y) {return 0;}
13536
13756
  // Count segments in each stripe
13757
+ profileStart('stripeSetup');
13537
13758
  arcs.forEachSegment(function(id1, id2, xx, yy) {
13538
13759
  var s1 = stripeId(yy[id1]),
13539
13760
  s2 = stripeId(yy[id2]);
@@ -13570,8 +13791,10 @@
13570
13791
  s1 += s2 > s1 ? 1 : -1;
13571
13792
  }
13572
13793
  });
13794
+ profileEnd('stripeSetup');
13573
13795
 
13574
13796
  // Detect intersections among segments in each stripe.
13797
+ profileStart('intersectSegments');
13575
13798
  var raw = arcs.getVertexData(),
13576
13799
  intersections = [],
13577
13800
  arr;
@@ -13581,7 +13804,11 @@
13581
13804
  intersections.push(arr[j]);
13582
13805
  }
13583
13806
  }
13584
- 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;
13585
13812
  }
13586
13813
 
13587
13814
 
@@ -13594,22 +13821,42 @@
13594
13821
 
13595
13822
 
13596
13823
  function dedupIntersections(arr, keyFunction) {
13597
- var index = {};
13598
- var getKey = keyFunction || getIntersectionKey;
13599
- return arr.filter(function(o) {
13600
- var key = getKey(o);
13601
- if (key in index) {
13602
- 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);
13603
13833
  }
13604
- index[key] = true;
13605
- return true;
13606
- });
13607
- }
13608
-
13609
- // Get an indexable key from an intersection object
13610
- // Assumes that vertex ids of o.a and o.b are sorted
13611
- function getIntersectionKey(o) {
13612
- 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;
13613
13860
  }
13614
13861
 
13615
13862
  function getUniqueIntersectionKey(o) {
@@ -17003,16 +17250,26 @@
17003
17250
  // Remove any unused arcs from the dataset's ArcCollection.
17004
17251
  // Return a NodeCollection
17005
17252
  function cleanArcReferences(dataset) {
17253
+ profileStart('NodeCollection#1');
17006
17254
  var nodes = new NodeCollection(dataset.arcs);
17255
+ profileEnd('NodeCollection#1');
17256
+ profileStart('findDuplicateArcs');
17007
17257
  var map = findDuplicateArcs(nodes);
17258
+ profileEnd('findDuplicateArcs');
17008
17259
  var dropCount;
17009
17260
  if (map) {
17261
+ profileStart('replaceIndexedArcIds');
17010
17262
  replaceIndexedArcIds(dataset, map);
17263
+ profileEnd('replaceIndexedArcIds');
17011
17264
  }
17265
+ profileStart('deleteUnusedArcs');
17012
17266
  dropCount = deleteUnusedArcs(dataset);
17267
+ profileEnd('deleteUnusedArcs');
17013
17268
  if (dropCount > 0) {
17014
17269
  // rebuild nodes if arcs have changed
17270
+ profileStart('NodeCollection#2');
17015
17271
  nodes = new NodeCollection(dataset.arcs);
17272
+ profileEnd('NodeCollection#2');
17016
17273
  }
17017
17274
  return nodes;
17018
17275
  }
@@ -17072,11 +17329,13 @@
17072
17329
  // and re-index the paths of all the layers that reference the arc collection.
17073
17330
  // (in-place)
17074
17331
  function addIntersectionCuts(dataset, _opts) {
17332
+ profileStart('addIntersectionCuts');
17075
17333
  var opts = _opts || {};
17076
17334
  var arcs = dataset.arcs;
17077
17335
  var arcBounds = arcs && arcs.getBounds();
17078
17336
  var snapDist, nodes;
17079
17337
  if (!arcBounds || !arcBounds.hasBounds()) {
17338
+ profileEnd('addIntersectionCuts');
17080
17339
  return new NodeCollection([]);
17081
17340
  }
17082
17341
 
@@ -17091,46 +17350,74 @@
17091
17350
 
17092
17351
  // bake-in any simplification (bug fix; before, -simplify followed by dissolve2
17093
17352
  // used to reset simplification)
17353
+ profileStart('flatten');
17094
17354
  arcs.flatten();
17355
+ profileEnd('flatten');
17095
17356
 
17096
17357
  var changed = snapAndCut(dataset, snapDist);
17097
17358
 
17098
17359
  // Detect topology again if coordinates have changed
17099
17360
  if (changed || opts.rebuild_topology) {
17361
+ profileStart('buildTopology');
17100
17362
  buildTopology(dataset);
17363
+ profileEnd('buildTopology');
17101
17364
  }
17102
17365
  // Remove degenerate shapes
17103
17366
  // Without this step, pathfinder function would encounter dead ends.
17367
+ profileStart('cleanShapes');
17104
17368
  dataset.layers.forEach(function(lyr) {
17105
17369
  if (layerHasPaths(lyr)) {
17106
17370
  cleanShapes(lyr.shapes, arcs, lyr.geometry_type);
17107
17371
  }
17108
17372
  });
17373
+ profileEnd('cleanShapes');
17109
17374
  // Further clean-up -- remove duplicate and unused arcs, etc.
17375
+ profileStart('cleanArcReferences');
17110
17376
  nodes = cleanArcReferences(dataset);
17377
+ profileEnd('cleanArcReferences');
17378
+ profileEnd('addIntersectionCuts');
17111
17379
  return nodes;
17112
17380
  }
17113
17381
 
17114
17382
  function snapAndCut(dataset, snapDist) {
17383
+ profileStart('snapAndCut');
17115
17384
  var arcs = dataset.arcs;
17116
17385
  var cutOpts = snapDist > 0 ? {tolerance: snapDist} : {tolerance: 0};
17117
17386
  var coordsHaveChanged = false;
17118
- var snapCount, dupeCount, cutCount;
17387
+ var snapCount = 0, dupeCount, cutCount;
17119
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;
17120
17396
 
17121
- // snap + cut until no more cuts are needed
17122
17397
  do {
17123
17398
  loopCount++;
17124
17399
  cutCount = 0;
17125
- snapCount = snapCoordsByInterval(arcs, snapDist);
17126
- 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
+ }
17127
17412
  if (snapCount > 0 || cutCount > 0 || loopCount == 1) {
17128
- // cut arcs at points where segments intersect
17413
+ profileStart('cutPathsAtIntersections');
17129
17414
  cutCount = cutPathsAtIntersections(dataset, cutOpts);
17415
+ profileEnd('cutPathsAtIntersections');
17130
17416
  }
17131
17417
  coordsHaveChanged |= (snapCount + dupeCount + cutCount) > 0;
17132
- debug("[snapAndCut] pass:", loopCount, "snaps:", snapCount, "dupes:", dupeCount, "cuts:", cutCount);
17418
+ debug("[snapAndCut] pass:", loopCount, "snaps:", snapCount, "dupes:", dupeCount, "cuts:", cutCount, skipFollowupSnap ? "(skipped snap)" : "");
17133
17419
  } while (loopCount < maxLoops && cutCount > 0);
17420
+ profileEnd('snapAndCut');
17134
17421
 
17135
17422
  // should this be added?
17136
17423
  // if (cutCount > 0 && loopCount == maxLoops) {
@@ -17195,9 +17482,13 @@
17195
17482
  // Divides a collection of arcs at points where arc paths cross each other
17196
17483
  // Returns array for remapping arc ids
17197
17484
  function divideArcs(arcs, opts) {
17485
+ profileStart('findClippingPoints');
17198
17486
  var points = findClippingPoints(arcs, opts);
17487
+ profileEnd('findClippingPoints');
17199
17488
  // TODO: avoid the following if no points need to be added
17489
+ profileStart('insertCutPoints');
17200
17490
  var map = insertCutPoints(points, arcs);
17491
+ profileEnd('insertCutPoints');
17201
17492
  // segment-point intersections currently create duplicate points
17202
17493
  // TODO: consider dedup in a later cleanup pass?
17203
17494
  // arcs.dedupCoords();
@@ -17205,9 +17496,14 @@
17205
17496
  }
17206
17497
 
17207
17498
  function findClippingPoints(arcs, opts) {
17208
- var intersections = findSegmentIntersections(arcs, opts),
17209
- data = arcs.getVertexData();
17210
- 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;
17211
17507
  }
17212
17508
 
17213
17509
  // Inserts array of cutting points into an ArcCollection
@@ -17221,9 +17517,24 @@
17221
17517
  i1 = 0,
17222
17518
  nn1 = [],
17223
17519
  srcArcTotal = arcs.size(),
17224
- map = new Uint32Array(srcArcTotal),
17225
- points = filterSortedCutPoints(sortCutPoints(unfilteredPoints, xx0, yy0), arcs),
17226
- 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,
17227
17538
  xx1 = new Float64Array(destPointTotal),
17228
17539
  yy1 = new Float64Array(destPointTotal),
17229
17540
  n0, n1, arcLen, p;
@@ -17265,6 +17576,7 @@
17265
17576
 
17266
17577
  if (i1 != destPointTotal) error("[insertCutPoints()] Counting error");
17267
17578
  arcs.updateVertexData(nn1, xx1, yy1, null);
17579
+ profileEnd('rewriteVertexData');
17268
17580
  return map;
17269
17581
  }
17270
17582
 
@@ -17307,14 +17619,35 @@
17307
17619
  // Insertion order: ascending id of first endpoint of containing segment and
17308
17620
  // ascending distance from same endpoint.
17309
17621
  function sortCutPoints(points, xx, yy) {
17310
- points.sort(function(a, b) {
17311
- if (a.i != b.i) return a.i - b.i;
17312
- 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);
17313
- // The old code below is no longer reliable, now that out-of-range intersection
17314
- // points are allowed.
17315
- // return Math.abs(a.x - xx[a.i]) - Math.abs(b.x - xx[b.i]) ||
17316
- // Math.abs(a.y - yy[a.i]) - Math.abs(b.y - yy[b.i]);
17317
- });
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
+ }
17318
17651
  return points;
17319
17652
  }
17320
17653
 
@@ -17426,31 +17759,32 @@
17426
17759
  //
17427
17760
  function buildPolygonMosaic(nodes) {
17428
17761
  T$1.start();
17429
- // Detach any acyclic paths (spikes) from arc graph (these would interfere with
17430
- // the ring finding operation). This modifies @nodes -- a side effect.
17762
+ profileStart('bpm.detachAcyclicArcs');
17431
17763
  nodes.detachAcyclicArcs();
17764
+ profileEnd('bpm.detachAcyclicArcs');
17765
+ profileStart('bpm.findMosaicRings');
17432
17766
  var data = findMosaicRings(nodes);
17767
+ profileEnd('bpm.findMosaicRings');
17433
17768
 
17434
- // Process CW rings: these are indivisible space-enclosing boundaries of mosaic tiles
17435
17769
  var mosaic = data.cw.map(function(ring) {return [ring];});
17436
17770
  debug('Find mosaic rings', T$1.stop());
17437
17771
  T$1.start();
17438
17772
 
17439
- // Process CCW rings: these are either holes or enclosure
17440
- // TODO: optimize -- testing CCW path of every island is costly
17441
17773
  var enclosures = [];
17442
- 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');
17443
17778
  data.ccw.forEach(function(ring) {
17444
17779
  var id = index.findSmallestEnclosingPolygon(ring);
17445
17780
  if (id > -1) {
17446
- // Enclosed CCW rings are holes in the enclosing mosaic tile
17447
17781
  mosaic[id].push(ring);
17448
17782
  } else {
17449
- // Non-enclosed CCW rings are outer boundaries -- add to enclosures layer
17450
17783
  reversePath(ring);
17451
17784
  enclosures.push([ring]);
17452
17785
  }
17453
17786
  });
17787
+ profileEnd('bpm.findEnclosingForCCW');
17454
17788
  debug(utils.format("Detect holes (holes: %d, enclosures: %d)", data.ccw.length - enclosures.length, enclosures.length), T$1.stop());
17455
17789
 
17456
17790
  return {mosaic: mosaic, enclosures: enclosures, lostArcs: data.lostArcs};
@@ -17750,23 +18084,22 @@
17750
18084
  });
17751
18085
 
17752
18086
  function MosaicIndex(lyr, nodes, optsArg) {
18087
+ profileStart('MosaicIndex.ctor');
17753
18088
  var opts = optsArg || {};
17754
18089
  var shapes = lyr.shapes;
18090
+ profileStart('mi.buildPolygonMosaic');
17755
18091
  var mosaic = buildPolygonMosaic(nodes).mosaic;
17756
- // map arc ids to tile ids
18092
+ profileEnd('mi.buildPolygonMosaic');
18093
+ profileStart('mi.ShapeArcIndex');
17757
18094
  var arcTileIndex = new ShapeArcIndex(mosaic, nodes.arcs);
17758
- // keep track of which tiles have been assigned to shapes
18095
+ profileEnd('mi.ShapeArcIndex');
17759
18096
  var fetchedTileIndex = new IdTestIndex(mosaic.length);
17760
- // bidirection index of tile ids <=> shape ids
17761
18097
  var tileShapeIndex = new TileShapeIndex(mosaic, opts);
17762
- // assign tiles to shapes
18098
+ profileStart('mi.PolygonTiler.ctor');
17763
18099
  var shapeTiler = new PolygonTiler(mosaic, arcTileIndex, nodes, opts);
18100
+ profileEnd('mi.PolygonTiler.ctor');
17764
18101
  var weightFunction = null;
17765
18102
  if (!opts.simple && opts.flat) {
17766
- // opts.simple is an optimization when dissolving everything into one polygon
17767
- // using -dissolve2. In this situation, we don't need a weight function.
17768
- // Otherwise, if polygons are being dissolved into multiple groups,
17769
- // we use a function to assign tiles in overlapping areas to a single shape.
17770
18103
  weightFunction = getOverlapPriorityFunction(lyr.shapes, nodes.arcs, opts.overlap_rule);
17771
18104
  }
17772
18105
  this.mosaic = mosaic;
@@ -17774,16 +18107,19 @@
17774
18107
  this.getSourceIdsByTileId = tileShapeIndex.getShapeIdsByTileId; // expose for -mosaic command
17775
18108
  this.getTileIdsByShapeId = tileShapeIndex.getTileIdsByShapeId;
17776
18109
 
17777
- // Assign shape ids to mosaic tile shapes.
18110
+ profileStart('mi.assignTilesToShapes');
17778
18111
  shapes.forEach(function(shp, shapeId) {
17779
18112
  var tileIds = shapeTiler.getTilesInShape(shp, shapeId);
17780
18113
  tileShapeIndex.indexTileIdsByShapeId(shapeId, tileIds, weightFunction);
17781
18114
  });
18115
+ profileEnd('mi.assignTilesToShapes');
17782
18116
 
17783
- // ensure each tile is assigned to only one shape
17784
18117
  if (opts.flat) {
18118
+ profileStart('mi.tileShapeIndex.flatten');
17785
18119
  tileShapeIndex.flatten();
18120
+ profileEnd('mi.tileShapeIndex.flatten');
17786
18121
  }
18122
+ profileEnd('MosaicIndex.ctor');
17787
18123
 
17788
18124
  // fill gaps
17789
18125
  // (assumes that tiles have been allocated to shapes and mosaic has been flattened)
@@ -17963,22 +18299,30 @@
17963
18299
  }
17964
18300
 
17965
18301
  function dissolvePolygonGroups2(groups, lyr, dataset, opts) {
18302
+ profileStart('dissolvePolygonGroups2');
18303
+ profileStart('dpg2.NodeCollection');
17966
18304
  var arcFilter = getArcPresenceTest(lyr.shapes, dataset.arcs);
17967
18305
  var nodes = new NodeCollection(dataset.arcs, arcFilter);
18306
+ profileEnd('dpg2.NodeCollection');
17968
18307
  var mosaicOpts = {
17969
18308
  flat: !opts.allow_overlaps,
17970
18309
  simple: groups.length == 1,
17971
18310
  overlap_rule: opts.overlap_rule
17972
18311
  };
18312
+ profileStart('dpg2.MosaicIndex');
17973
18313
  var mosaicIndex = new MosaicIndex(lyr, nodes, mosaicOpts);
18314
+ profileEnd('dpg2.MosaicIndex');
17974
18315
  // gap fill doesn't work yet with overlapping shapes
17975
18316
  var fillGaps = !opts.allow_overlaps && (opts.sliver_control || opts.gap_fill_area);
17976
18317
  var cleanupData, filterData;
17977
18318
  if (fillGaps) {
18319
+ profileStart('dpg2.removeGaps');
17978
18320
  var sliverOpts = utils.extend({sliver_control: 1}, opts);
17979
18321
  filterData = getSliverFilter(lyr, dataset, sliverOpts);
17980
18322
  cleanupData = mosaicIndex.removeGaps(filterData.filter);
18323
+ profileEnd('dpg2.removeGaps');
17981
18324
  }
18325
+ profileStart('dpg2.dissolveTiles');
17982
18326
  var pathfind = getRingIntersector(mosaicIndex.nodes);
17983
18327
  var dissolvedShapes = groups.map(function(shapeIds) {
17984
18328
  var tiles = mosaicIndex.getTilesByShapeIds(shapeIds);
@@ -17989,14 +18333,16 @@
17989
18333
  }
17990
18334
  return dissolveTileGroup2(tiles, pathfind);
17991
18335
  });
17992
- // convert self-intersecting rings to outer/inner rings, for OGC
17993
- // Simple Features compliance
18336
+ profileEnd('dpg2.dissolveTiles');
18337
+ profileStart('dpg2.fixTangentHoles');
17994
18338
  dissolvedShapes = fixTangentHoles(dissolvedShapes, pathfind);
18339
+ profileEnd('dpg2.fixTangentHoles');
17995
18340
 
17996
18341
  if (fillGaps && !opts.quiet) {
17997
18342
  var msg = getGapRemovalMessage(cleanupData.removed, cleanupData.remaining, filterData.label);
17998
18343
  if (msg) message(msg);
17999
18344
  }
18345
+ profileEnd('dissolvePolygonGroups2');
18000
18346
  return dissolvedShapes;
18001
18347
  }
18002
18348
 
@@ -18216,40 +18562,52 @@
18216
18562
  cmd.cleanLayers = cleanLayers;
18217
18563
 
18218
18564
  function cleanLayers(layers, dataset, optsArg) {
18565
+ profileStart('cleanLayers');
18219
18566
  var opts = optsArg || {};
18220
18567
  var deepClean = !opts.only_arcs;
18221
18568
  var pathClean = utils.some(layers, layerHasPaths);
18222
18569
  var nodes;
18223
18570
  if (opts.debug) {
18224
18571
  addIntersectionCuts(dataset, opts);
18572
+ profileEnd('cleanLayers');
18225
18573
  return;
18226
18574
  }
18227
18575
  layers.forEach(function(lyr) {
18228
18576
  if (!layerHasGeometry(lyr)) return;
18229
18577
  if (lyr.geometry_type == 'polygon' && opts.rewind) {
18578
+ profileStart('rewindPolygons');
18230
18579
  rewindPolygons(lyr, dataset.arcs);
18580
+ profileEnd('rewindPolygons');
18231
18581
  }
18232
18582
  if (deepClean) {
18233
18583
  if (!nodes) {
18234
18584
  nodes = addIntersectionCuts(dataset, opts);
18235
18585
  }
18236
18586
  if (lyr.geometry_type == 'polygon') {
18587
+ profileStart('cleanPolygonLayerGeometry');
18237
18588
  cleanPolygonLayerGeometry(lyr, dataset, opts);
18589
+ profileEnd('cleanPolygonLayerGeometry');
18238
18590
  } else if (lyr.geometry_type == 'polyline') {
18591
+ profileStart('cleanPolylineLayerGeometry');
18239
18592
  cleanPolylineLayerGeometry(lyr, dataset);
18593
+ profileEnd('cleanPolylineLayerGeometry');
18240
18594
  } else if (lyr.geometry_type == 'point') {
18241
18595
  cleanPointLayerGeometry(lyr);
18242
18596
  }
18243
18597
  }
18244
18598
  if (!opts.allow_empty) {
18599
+ profileStart('filterFeatures');
18245
18600
  cmd.filterFeatures(lyr, dataset.arcs, {remove_empty: true, verbose: opts.verbose});
18601
+ profileEnd('filterFeatures');
18246
18602
  }
18247
18603
  });
18248
18604
 
18249
18605
  if (!opts.no_arc_dissolve && pathClean && dataset.arcs) {
18250
- // remove leftover endpoints within contiguous lines
18606
+ profileStart('dissolveArcs');
18251
18607
  dissolveArcs(dataset);
18608
+ profileEnd('dissolveArcs');
18252
18609
  }
18610
+ profileEnd('cleanLayers');
18253
18611
  }
18254
18612
 
18255
18613
  function cleanPolygonLayerGeometry(lyr, dataset, opts) {
@@ -39726,6 +40084,7 @@ ${svg}
39726
40084
 
39727
40085
  // assumes layers and arcs have been prepared for clipping
39728
40086
  function clipPolygons(targetShapes, clipShapes, nodes, type, optsArg) {
40087
+ profileStart('clipPolygons');
39729
40088
  var arcs = nodes.arcs;
39730
40089
  var opts = optsArg || {};
39731
40090
  var clipFlags = new Uint8Array(arcs.size());
@@ -39736,59 +40095,47 @@ ${svg}
39736
40095
  var findPath = getPathFinder(nodes, useRoute, routeIsActive);
39737
40096
  var dissolvePolygon = getPolygonDissolver(nodes);
39738
40097
 
39739
- // The following cleanup step is a performance bottleneck (it often takes longer than
39740
- // other clipping operations) and is usually not needed. Furthermore, it only
39741
- // eliminates a few kinds of problems, like target polygons with abnormal winding
39742
- // or overlapping rings. TODO: try to optimize or remove it for all cases
39743
-
39744
- // skipping shape cleanup when using the experimental fast bbox clipping option
39745
- // if (!opts.bbox2 && !opts.no_cleanup) {
39746
40098
  if (!opts.bbox2) {
39747
- // clean each target polygon by dissolving its rings
40099
+ profileStart('cp.dissolveTargetRings');
39748
40100
  targetShapes = targetShapes.map(dissolvePolygon);
40101
+ profileEnd('cp.dissolveTargetRings');
39749
40102
  }
39750
40103
 
39751
- // Originally, clip shapes were dissolved here as an optimization, using
39752
- // an unreliable dissolve function.
39753
- // Now, clip shapes are dissolved using a more reliable (but slower)
39754
- // function in mapshaper-clip-erase.js
39755
- // clipShapes = [dissolvePolygon(internal.concatShapes(clipShapes))];
39756
-
39757
- // Open pathways in the clip/erase layer
39758
- // Need to expose clip/erase routes in both directions by setting route
39759
- // in both directions to visible -- this is how cut-out shapes are detected
39760
- // Or-ing with 0x11 makes both directions visible (so reverse paths will block)
40104
+ profileStart('cp.openClipRoutes');
39761
40105
  openArcRoutes(clipShapes, arcs, clipFlags, type == 'clip', type == 'erase', true, 0x11);
40106
+ profileEnd('cp.openClipRoutes');
40107
+ profileStart('cp.PathIndex#1');
39762
40108
  var index = new PathIndex(clipShapes, arcs);
40109
+ profileEnd('cp.PathIndex#1');
40110
+ profileStart('cp.clipShapes');
39763
40111
  var clippedShapes = targetShapes.map(function(shape, i) {
39764
40112
  if (shape) {
39765
40113
  return clipPolygon(shape, type, index);
39766
40114
  }
39767
40115
  return null;
39768
40116
  });
40117
+ profileEnd('cp.clipShapes');
39769
40118
 
39770
- markPathsAsUsed(clippedShapes, routeFlags); // to help us find unused paths later
39771
-
39772
-
39773
- // add clip/erase polygons that are fully contained in a target polygon
39774
- // need to index only non-intersecting clip shapes
39775
- // (Intersecting shapes have one or more arcs that have been scanned)
39776
-
39777
- // first, find shapes that do not intersect the target layer
39778
- // (these could be inside or outside the target polygons)
40119
+ markPathsAsUsed(clippedShapes, routeFlags);
39779
40120
 
40121
+ profileStart('cp.findUndividedClip');
39780
40122
  var undividedClipShapes = findUndividedClipShapes(clipShapes);
40123
+ profileEnd('cp.findUndividedClip');
39781
40124
 
39782
- closeArcRoutes(clipShapes, arcs, routeFlags, true, true); // not needed?
40125
+ closeArcRoutes(clipShapes, arcs, routeFlags, true, true);
40126
+ profileStart('cp.PathIndex#2');
39783
40127
  index = new PathIndex(undividedClipShapes, arcs);
40128
+ profileEnd('cp.PathIndex#2');
40129
+ profileStart('cp.findInteriorPaths');
39784
40130
  targetShapes.forEach(function(shape, shapeId) {
39785
- // find clipping paths that are internal to this target polygon
39786
40131
  var paths = shape ? findInteriorPaths(shape, type, index) : null;
39787
40132
  if (paths) {
39788
40133
  clippedShapes[shapeId] = (clippedShapes[shapeId] || []).concat(paths);
39789
40134
  }
39790
40135
  });
40136
+ profileEnd('cp.findInteriorPaths');
39791
40137
 
40138
+ profileEnd('clipPolygons');
39792
40139
  return clippedShapes;
39793
40140
 
39794
40141
  function clipPolygon(shape, type, index) {
@@ -40216,31 +40563,36 @@ ${svg}
40216
40563
  // @clipSrc: layer in @dataset or filename
40217
40564
  // @type: 'clip' or 'erase'
40218
40565
  function clipLayers(targetLayers, clipSrc, targetDataset, type, opts) {
40566
+ profileStart('clipLayers');
40219
40567
  var usingPathClip = utils.some(targetLayers, layerHasPaths);
40220
- var mergedDataset, clipLyr, nodes;
40568
+ var mergedDataset, clipLyr, nodes, result;
40221
40569
  opts = opts || {no_cleanup: true}; // TODO: update testing functions
40222
40570
  if (opts.bbox2 && usingPathClip) { // assumes target dataset has arcs
40223
- return clipLayersByBBox(targetLayers, targetDataset, opts);
40571
+ result = clipLayersByBBox(targetLayers, targetDataset, opts);
40572
+ profileEnd('clipLayers');
40573
+ return result;
40224
40574
  }
40575
+ profileStart('mergeLayersForOverlay');
40225
40576
  mergedDataset = mergeLayersForOverlay(targetLayers, targetDataset, clipSrc, opts);
40577
+ profileEnd('mergeLayersForOverlay');
40226
40578
  clipLyr = mergedDataset.layers[mergedDataset.layers.length-1];
40227
40579
  if (usingPathClip) {
40228
- // add vertices at all line intersections
40229
- // (generally slower than actual clipping)
40230
40580
  nodes = addIntersectionCuts(mergedDataset, opts);
40231
40581
  targetDataset.arcs = mergedDataset.arcs;
40232
- // dissolve clip layer shapes (to remove overlaps and other topological issues
40233
- // that might confuse the clipping function)
40234
- // use a data-free copy of the clip lyr, so data records are not dissolved
40235
- // (this avoids triggering an unnecessary and expensive DBF read operation in some cases).
40582
+ profileStart('clipDissolvePolygonLayer2');
40236
40583
  clipLyr = utils.defaults({data: null}, clipLyr);
40237
40584
  clipLyr = dissolvePolygonLayer2(clipLyr, mergedDataset, {quiet: true, silent: true});
40585
+ profileEnd('clipDissolvePolygonLayer2');
40238
40586
 
40239
40587
  } else {
40240
40588
  nodes = new NodeCollection(mergedDataset.arcs);
40241
40589
  }
40242
40590
 
40243
- 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;
40244
40596
  }
40245
40597
 
40246
40598
  function clipLayersByBBox(layers, dataset, opts) {
@@ -50779,7 +51131,7 @@ ${svg}
50779
51131
  });
50780
51132
  }
50781
51133
 
50782
- var version = "0.6.118";
51134
+ var version = "0.6.119";
50783
51135
 
50784
51136
  // Parse command line args into commands and run them
50785
51137
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -51477,6 +51829,7 @@ ${svg}
51477
51829
  LayerUtils,
51478
51830
  Lines,
51479
51831
  Logging,
51832
+ Profile,
51480
51833
  Merging,
51481
51834
  MosaicIndex$1,
51482
51835
  OptionParsingUtils,