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/README.md +12 -0
- package/mapshaper.js +465 -112
- package/package.json +1 -1
- package/www/donate.html +215 -0
- package/www/index.html +1 -1
- package/www/mapshaper.js +465 -112
- package/www/page.css +5 -0
package/www/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
|
|
12511
|
-
// index
|
|
12512
|
-
//
|
|
12513
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13598
|
-
|
|
13599
|
-
|
|
13600
|
-
var
|
|
13601
|
-
|
|
13602
|
-
|
|
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
|
-
|
|
13605
|
-
|
|
13606
|
-
|
|
13607
|
-
|
|
13608
|
-
|
|
13609
|
-
|
|
13610
|
-
|
|
13611
|
-
|
|
13612
|
-
|
|
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
|
-
|
|
17126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17209
|
-
|
|
17210
|
-
|
|
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
|
-
|
|
17226
|
-
|
|
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.
|
|
17311
|
-
|
|
17312
|
-
|
|
17313
|
-
|
|
17314
|
-
|
|
17315
|
-
|
|
17316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18092
|
+
profileEnd('mi.buildPolygonMosaic');
|
|
18093
|
+
profileStart('mi.ShapeArcIndex');
|
|
17757
18094
|
var arcTileIndex = new ShapeArcIndex(mosaic, nodes.arcs);
|
|
17758
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17993
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40099
|
+
profileStart('cp.dissolveTargetRings');
|
|
39748
40100
|
targetShapes = targetShapes.map(dissolvePolygon);
|
|
40101
|
+
profileEnd('cp.dissolveTargetRings');
|
|
39749
40102
|
}
|
|
39750
40103
|
|
|
39751
|
-
|
|
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);
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|