mapshaper 0.7.55 → 0.7.56
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 +605 -9
- package/package.json +1 -1
- package/www/mapshaper.js +605 -9
package/www/mapshaper.js
CHANGED
|
@@ -5793,10 +5793,13 @@
|
|
|
5793
5793
|
|
|
5794
5794
|
function layerOnlyHasRectangles(lyr, arcs) {
|
|
5795
5795
|
if (!layerHasPaths(lyr)) return false;
|
|
5796
|
-
if (countMultiPartFeatures(lyr) > 0) return false;
|
|
5797
5796
|
return lyr.shapes.every(function(shp) {
|
|
5798
5797
|
if (!shp) return true;
|
|
5799
|
-
|
|
5798
|
+
// Every part is tested: a shape whose first ring is rectangular may well
|
|
5799
|
+
// have others that are not.
|
|
5800
|
+
return shp.every(function(path) {
|
|
5801
|
+
return pathIsRectangle(path, arcs);
|
|
5802
|
+
});
|
|
5800
5803
|
});
|
|
5801
5804
|
}
|
|
5802
5805
|
|
|
@@ -5886,7 +5889,7 @@
|
|
|
5886
5889
|
|
|
5887
5890
|
function requireSinglePointLayer(lyr, msg) {
|
|
5888
5891
|
requirePointLayer(lyr);
|
|
5889
|
-
if (countMultiPartFeatures(lyr) > 0) {
|
|
5892
|
+
if (countMultiPartFeatures(lyr.shapes) > 0) {
|
|
5890
5893
|
stop$1(msg || 'This command requires single points; layer contains multi-point features.');
|
|
5891
5894
|
}
|
|
5892
5895
|
}
|
|
@@ -6030,8 +6033,16 @@
|
|
|
6030
6033
|
return copy;
|
|
6031
6034
|
}
|
|
6032
6035
|
|
|
6036
|
+
// shapes: one layer's "shapes" array
|
|
6037
|
+
// Guards against being passed a layer instead: several callers made that
|
|
6038
|
+
// mistake, and reading .length off a layer object silently returned 0, which
|
|
6039
|
+
// disabled the multi-part checks that depend on this function.
|
|
6033
6040
|
function countMultiPartFeatures(shapes) {
|
|
6034
6041
|
var count = 0;
|
|
6042
|
+
if (!shapes) return 0;
|
|
6043
|
+
if (!Array.isArray(shapes)) {
|
|
6044
|
+
error('countMultiPartFeatures() expects an array of shapes');
|
|
6045
|
+
}
|
|
6035
6046
|
for (var i=0, n=shapes.length; i<n; i++) {
|
|
6036
6047
|
if (shapes[i] && shapes[i].length > 1) count++;
|
|
6037
6048
|
}
|
|
@@ -26145,8 +26156,20 @@
|
|
|
26145
26156
|
}
|
|
26146
26157
|
|
|
26147
26158
|
function getSymbolPropertyAccessor(val, svgName, lyr) {
|
|
26159
|
+
return getPropertyAccessor(val, symbolPropertyTypes[svgName], lyr, svgName);
|
|
26160
|
+
}
|
|
26161
|
+
|
|
26162
|
+
// Returns a function that maps a feature id to a property value. The value may
|
|
26163
|
+
// be given as a literal, as the name of a data field, or as a JS expression
|
|
26164
|
+
// evaluated against each feature.
|
|
26165
|
+
// typeHint: a type understood by parseSvgLiteralValue(), or null to accept any
|
|
26166
|
+
// string as a literal value
|
|
26167
|
+
// name: used in error messages
|
|
26168
|
+
// Callers outside the SVG property system pass a type hint directly, rather
|
|
26169
|
+
// than registering an option name in symbolPropertyTypes -- that index also
|
|
26170
|
+
// decides which options the -symbols command treats as symbol properties.
|
|
26171
|
+
function getPropertyAccessor(val, typeHint, lyr, name) {
|
|
26148
26172
|
var strVal = String(val).trim();
|
|
26149
|
-
var typeHint = symbolPropertyTypes[svgName];
|
|
26150
26173
|
var fields = lyr.data ? lyr.data.getFields() : [];
|
|
26151
26174
|
var literalVal = null;
|
|
26152
26175
|
var accessor;
|
|
@@ -26164,7 +26187,7 @@
|
|
|
26164
26187
|
}
|
|
26165
26188
|
if (accessor) return accessor;
|
|
26166
26189
|
if (literalVal !== null) return function(id) {return literalVal;};
|
|
26167
|
-
stop$1('Unexpected value for',
|
|
26190
|
+
stop$1('Unexpected value for', name + ':', strVal);
|
|
26168
26191
|
}
|
|
26169
26192
|
|
|
26170
26193
|
function parseStyleExpression(strVal, lyr) {
|
|
@@ -26270,6 +26293,7 @@
|
|
|
26270
26293
|
applyStyleAttributes: applyStyleAttributes,
|
|
26271
26294
|
findStylePropertiesBySymbolGeom: findStylePropertiesBySymbolGeom,
|
|
26272
26295
|
getLabelPositionStyle: getLabelPositionStyle,
|
|
26296
|
+
getPropertyAccessor: getPropertyAccessor,
|
|
26273
26297
|
getSymbolDataAccessor: getSymbolDataAccessor,
|
|
26274
26298
|
getSymbolPropertyAccessor: getSymbolPropertyAccessor,
|
|
26275
26299
|
isSupportedSvgStyleProperty: isSupportedSvgStyleProperty,
|
|
@@ -41003,6 +41027,34 @@ ${svg}
|
|
|
41003
41027
|
})
|
|
41004
41028
|
.option('target', targetOpt);
|
|
41005
41029
|
|
|
41030
|
+
parser.command('repel')
|
|
41031
|
+
.describe('move overlapping circle symbols apart')
|
|
41032
|
+
.option('width', {
|
|
41033
|
+
describe: 'display width of the layer in pixels (required if no frame)',
|
|
41034
|
+
type: 'number'
|
|
41035
|
+
})
|
|
41036
|
+
.option('max-shift', {
|
|
41037
|
+
describe: 'max displacement in pixels; may vary by symbol (default is 20)'
|
|
41038
|
+
})
|
|
41039
|
+
.option('padding', {
|
|
41040
|
+
describe: 'pixels of clearance around each symbol; may vary (default is 0)'
|
|
41041
|
+
})
|
|
41042
|
+
.option('ticks', {
|
|
41043
|
+
describe: 'number of solver passes (default is 100)',
|
|
41044
|
+
type: 'number'
|
|
41045
|
+
})
|
|
41046
|
+
.option('strength', {
|
|
41047
|
+
describe: 'portion of each overlap resolved per pass, 0-1 (default is 0.4)',
|
|
41048
|
+
type: 'number'
|
|
41049
|
+
})
|
|
41050
|
+
.option('radius', {
|
|
41051
|
+
describe: 'field or expression giving symbol radius in pixels',
|
|
41052
|
+
})
|
|
41053
|
+
.option('polygons', {
|
|
41054
|
+
describe: 'layer or file of polygons that symbols must stay inside'
|
|
41055
|
+
})
|
|
41056
|
+
.option('target', targetOpt);
|
|
41057
|
+
|
|
41006
41058
|
parser.command('symbols')
|
|
41007
41059
|
.describe('symbolize points as arrows, circles, stars, polygons, etc.')
|
|
41008
41060
|
.option('type', {
|
|
@@ -74984,7 +75036,7 @@ ${svg}
|
|
|
74984
75036
|
|
|
74985
75037
|
var datasets = [targetDataset];
|
|
74986
75038
|
var outputLayers = targetLayers.map(function(pointLyr) {
|
|
74987
|
-
if (countMultiPartFeatures(pointLyr) > 0) {
|
|
75039
|
+
if (countMultiPartFeatures(pointLyr.shapes) > 0) {
|
|
74988
75040
|
stop$1('This command requires single points');
|
|
74989
75041
|
}
|
|
74990
75042
|
var dataset = getPolygonDataset(pointLyr, bbox, opts);
|
|
@@ -75300,6 +75352,531 @@ ${svg}
|
|
|
75300
75352
|
}, {});
|
|
75301
75353
|
}
|
|
75302
75354
|
|
|
75355
|
+
// Keeps each symbol's center inside the polygon that contained it to begin
|
|
75356
|
+
// with, so that a symbol representing one area can't drift into a neighboring
|
|
75357
|
+
// one. Used by -repel via its polygons= option.
|
|
75358
|
+
//
|
|
75359
|
+
// Nodes are laid out in pixel space, at a scale of @pixelsPerUnit pixels per
|
|
75360
|
+
// unit of the polygons' coordinate system. That transform is a pure scale (see
|
|
75361
|
+
// how -repel builds its nodes), so dividing a node position by it gives a
|
|
75362
|
+
// position that can be tested against the polygons directly.
|
|
75363
|
+
|
|
75364
|
+
// Bisection steps used to find how far a symbol can travel before it leaves its
|
|
75365
|
+
// polygon. Six steps locate the crossing to within 1/64 of the attempted move;
|
|
75366
|
+
// finer searches were measured and change the result by a fraction of a pixel.
|
|
75367
|
+
var BISECT_STEPS = 6;
|
|
75368
|
+
|
|
75369
|
+
// Assigns each node the id of the polygon containing its anchor, or -1 if the
|
|
75370
|
+
// anchor isn't inside any of them. Returns the number of unassigned nodes,
|
|
75371
|
+
// which are left free to move.
|
|
75372
|
+
function assignContainingPolygons(nodes, lyr, arcs, pixelsPerUnit) {
|
|
75373
|
+
var index = new PathIndex(lyr.shapes, arcs);
|
|
75374
|
+
var unassigned = 0;
|
|
75375
|
+
nodes.forEach(function(node) {
|
|
75376
|
+
node.polygonId = index.findEnclosingShape([node.x0 / pixelsPerUnit, node.y0 / pixelsPerUnit]);
|
|
75377
|
+
node.insideX = node.x0;
|
|
75378
|
+
node.insideY = node.y0;
|
|
75379
|
+
if (node.polygonId == -1) unassigned++;
|
|
75380
|
+
});
|
|
75381
|
+
return unassigned;
|
|
75382
|
+
}
|
|
75383
|
+
|
|
75384
|
+
// Returns a function that the solver runs after each pass: any symbol that has
|
|
75385
|
+
// left its polygon is pulled back along the path it just travelled, to the
|
|
75386
|
+
// furthest point that is still inside.
|
|
75387
|
+
//
|
|
75388
|
+
// This can't violate the max-shift limit. The position it assigns always lies
|
|
75389
|
+
// between two positions the node has already held, both of which the solver
|
|
75390
|
+
// clamped to within max-shift of the anchor, and a disk is convex.
|
|
75391
|
+
function getContainmentConstraint(lyr, arcs, pixelsPerUnit) {
|
|
75392
|
+
var shapes = lyr.shapes;
|
|
75393
|
+
|
|
75394
|
+
function isInside(node, x, y) {
|
|
75395
|
+
return testPointInPolygon(x / pixelsPerUnit, y / pixelsPerUnit,
|
|
75396
|
+
shapes[node.polygonId], arcs);
|
|
75397
|
+
}
|
|
75398
|
+
|
|
75399
|
+
return function(nodes) {
|
|
75400
|
+
var node, lo, hi, mid, x, y, i, j;
|
|
75401
|
+
for (i=0; i<nodes.length; i++) {
|
|
75402
|
+
node = nodes[i];
|
|
75403
|
+
if (node.polygonId == -1) continue;
|
|
75404
|
+
// A node that hasn't moved since it was last checked is still inside,
|
|
75405
|
+
// so most symbols in a sparse layout cost nothing after the early passes
|
|
75406
|
+
if (node.x === node.insideX && node.y === node.insideY) continue;
|
|
75407
|
+
if (isInside(node, node.x, node.y)) {
|
|
75408
|
+
node.insideX = node.x;
|
|
75409
|
+
node.insideY = node.y;
|
|
75410
|
+
continue;
|
|
75411
|
+
}
|
|
75412
|
+
lo = 0; // known inside
|
|
75413
|
+
hi = 1; // known outside
|
|
75414
|
+
for (j=0; j<BISECT_STEPS; j++) {
|
|
75415
|
+
mid = (lo + hi) / 2;
|
|
75416
|
+
x = node.insideX + (node.x - node.insideX) * mid;
|
|
75417
|
+
y = node.insideY + (node.y - node.insideY) * mid;
|
|
75418
|
+
if (isInside(node, x, y)) lo = mid; else hi = mid;
|
|
75419
|
+
}
|
|
75420
|
+
node.x = node.insideX + (node.x - node.insideX) * lo;
|
|
75421
|
+
node.y = node.insideY + (node.y - node.insideY) * lo;
|
|
75422
|
+
node.insideX = node.x;
|
|
75423
|
+
node.insideY = node.y;
|
|
75424
|
+
}
|
|
75425
|
+
};
|
|
75426
|
+
}
|
|
75427
|
+
|
|
75428
|
+
// Collision reduction for circular symbols, in the spirit of d3-force's
|
|
75429
|
+
// forceCollide. Overlapping symbols are pushed apart, but each one is kept
|
|
75430
|
+
// within a fixed distance of its true position.
|
|
75431
|
+
//
|
|
75432
|
+
// Nodes are laid out in pixel space; converting to and from map coordinates is
|
|
75433
|
+
// the caller's job. A node is {i, x, y, x0, y0, r, maxShift}, where x0,y0 is
|
|
75434
|
+
// the anchor (the symbol's true position), r includes any padding and maxShift
|
|
75435
|
+
// is the furthest this symbol may be displaced. Both r and maxShift are
|
|
75436
|
+
// per-node, so the caller can vary them from symbol to symbol; a node with a
|
|
75437
|
+
// maxShift of 0 never moves and so acts as a fixed obstacle.
|
|
75438
|
+
|
|
75439
|
+
// max_shift is not read by the solver (it is a per-node property); it is here
|
|
75440
|
+
// as the default for callers to apply when building nodes.
|
|
75441
|
+
//
|
|
75442
|
+
// strength is the damping factor of what is essentially Gauss-Seidel
|
|
75443
|
+
// relaxation. A symbol in a cluster is corrected once per overlapping neighbor
|
|
75444
|
+
// in a single pass, so the corrections sum to more than its own overlap and
|
|
75445
|
+
// values near 1 overshoot and oscillate. Given the tick budget below, anything
|
|
75446
|
+
// from 0.3 to 1.0 resolves a layout that can be resolved at all; the value only
|
|
75447
|
+
// matters where crowding exceeds what max-shift can fix, and there the lower
|
|
75448
|
+
// end leaves shallower overlaps and holds symbols closer to their true
|
|
75449
|
+
// positions. Below about 0.4 the solver stops reliably breaking up stacks of
|
|
75450
|
+
// coincident symbols.
|
|
75451
|
+
var symbolCollisionDefaults = {
|
|
75452
|
+
ticks: 100,
|
|
75453
|
+
max_shift: 20,
|
|
75454
|
+
strength: 0.4
|
|
75455
|
+
};
|
|
75456
|
+
|
|
75457
|
+
// Mutates the x,y properties of @nodes. Returns the number of nodes that moved.
|
|
75458
|
+
//
|
|
75459
|
+
// opts.constrain is an optional function(nodes) applied after each pass, for
|
|
75460
|
+
// callers that need to restrict where symbols may go (see
|
|
75461
|
+
// mapshaper-symbol-containment.mjs). It must only move a node to a position
|
|
75462
|
+
// between ones the node has already held, or the max-shift limit no longer
|
|
75463
|
+
// holds.
|
|
75464
|
+
function resolveSymbolCollisions(nodes, opts) {
|
|
75465
|
+
var ticks = opts.ticks,
|
|
75466
|
+
strength = opts.strength,
|
|
75467
|
+
constrain = opts.constrain || null,
|
|
75468
|
+
moved = 0,
|
|
75469
|
+
i;
|
|
75470
|
+
if (nodes.length < 2 || !(ticks > 0) || !anyNodeCanMove(nodes)) return 0;
|
|
75471
|
+
for (i=0; i<ticks; i++) {
|
|
75472
|
+
resolveCollisions(nodes, strength);
|
|
75473
|
+
// Limiting the displacement on every pass (instead of only at the end)
|
|
75474
|
+
// avoids pulling symbols back into the overlaps they just escaped.
|
|
75475
|
+
limitDisplacement(nodes);
|
|
75476
|
+
if (constrain) constrain(nodes);
|
|
75477
|
+
}
|
|
75478
|
+
for (i=0; i<nodes.length; i++) {
|
|
75479
|
+
if (nodes[i].x !== nodes[i].x0 || nodes[i].y !== nodes[i].y0) moved++;
|
|
75480
|
+
}
|
|
75481
|
+
return moved;
|
|
75482
|
+
}
|
|
75483
|
+
|
|
75484
|
+
// Overlaps shallower than this are not reported. A converged layout doesn't
|
|
75485
|
+
// leave symbols exactly touching, it leaves a tail of overlaps a fraction of a
|
|
75486
|
+
// pixel deep -- on a solved 435-symbol layout, 55 pairs overlap by more than a
|
|
75487
|
+
// hundredth of a pixel, 6 by more than half a pixel, and none by more than one.
|
|
75488
|
+
// So a threshold of a pixel is where this solver's convergence residue ends and
|
|
75489
|
+
// genuine unresolved crowding begins, as well as being about the point where an
|
|
75490
|
+
// overlap becomes visible.
|
|
75491
|
+
var VISIBLE_OVERLAP = 1; // pixels
|
|
75492
|
+
|
|
75493
|
+
// Number of visibly overlapping pairs, for reporting how much of the job got
|
|
75494
|
+
// done. Deliberately not a count of all overlaps: an overlap 100th of a pixel
|
|
75495
|
+
// deep and one 15 pixels deep are not the same result, and treating them alike
|
|
75496
|
+
// makes a layout that is visually finished look like a failure.
|
|
75497
|
+
function countSymbolCollisions(nodes) {
|
|
75498
|
+
var count = 0;
|
|
75499
|
+
forEachCollision(nodes, function(a, b, dx, dy, distSq, sum) {
|
|
75500
|
+
if (sum - Math.sqrt(distSq) > VISIBLE_OVERLAP) count++;
|
|
75501
|
+
});
|
|
75502
|
+
return count;
|
|
75503
|
+
}
|
|
75504
|
+
|
|
75505
|
+
// Push overlapping nodes apart, in proportion to the overlap. Larger nodes move
|
|
75506
|
+
// less than smaller ones.
|
|
75507
|
+
function resolveCollisions(nodes, strength) {
|
|
75508
|
+
forEachCollision(nodes, function(a, b, dx, dy, distSq, sum) {
|
|
75509
|
+
separate(a, b, dx, dy, distSq, sum, strength);
|
|
75510
|
+
});
|
|
75511
|
+
}
|
|
75512
|
+
|
|
75513
|
+
// Sweep along the x axis, using the largest radius still ahead of the current
|
|
75514
|
+
// node to know when to stop looking for neighbors.
|
|
75515
|
+
function forEachCollision(nodes, cb) {
|
|
75516
|
+
var n = nodes.length,
|
|
75517
|
+
maxAhead = new Float64Array(n),
|
|
75518
|
+
max = 0,
|
|
75519
|
+
a, b, dx, dy, sum, distSq, i, j;
|
|
75520
|
+
nodes.sort(compareX);
|
|
75521
|
+
for (i=n-1; i>=0; i--) {
|
|
75522
|
+
if (nodes[i].r > max) max = nodes[i].r;
|
|
75523
|
+
maxAhead[i] = max;
|
|
75524
|
+
}
|
|
75525
|
+
for (i=0; i<n; i++) {
|
|
75526
|
+
a = nodes[i];
|
|
75527
|
+
for (j=i+1; j<n; j++) {
|
|
75528
|
+
b = nodes[j];
|
|
75529
|
+
dx = b.x - a.x;
|
|
75530
|
+
if (dx > a.r + maxAhead[j]) break;
|
|
75531
|
+
dy = b.y - a.y;
|
|
75532
|
+
sum = a.r + b.r;
|
|
75533
|
+
if (dy > sum || dy < -sum) continue;
|
|
75534
|
+
distSq = dx * dx + dy * dy;
|
|
75535
|
+
if (distSq >= sum * sum) continue;
|
|
75536
|
+
cb(a, b, dx, dy, distSq, sum);
|
|
75537
|
+
}
|
|
75538
|
+
}
|
|
75539
|
+
}
|
|
75540
|
+
|
|
75541
|
+
function compareX(a, b) {
|
|
75542
|
+
// Break ties by original index, so the order of equally positioned nodes
|
|
75543
|
+
// doesn't depend on the engine's sort implementation.
|
|
75544
|
+
return a.x - b.x || a.i - b.i;
|
|
75545
|
+
}
|
|
75546
|
+
|
|
75547
|
+
function separate(a, b, dx, dy, distSq, sum, strength) {
|
|
75548
|
+
var ux, uy, overlap, dist, dir, shift, share;
|
|
75549
|
+
if (distSq > 0) {
|
|
75550
|
+
dist = Math.sqrt(distSq);
|
|
75551
|
+
ux = dx / dist;
|
|
75552
|
+
uy = dy / dist;
|
|
75553
|
+
overlap = sum - dist;
|
|
75554
|
+
} else {
|
|
75555
|
+
// Coincident nodes: pick a repeatable arbitrary direction
|
|
75556
|
+
dir = getPseudoDirection(a.i, b.i);
|
|
75557
|
+
ux = dir[0];
|
|
75558
|
+
uy = dir[1];
|
|
75559
|
+
overlap = sum;
|
|
75560
|
+
}
|
|
75561
|
+
shift = overlap * strength;
|
|
75562
|
+
share = b.r * b.r / (a.r * a.r + b.r * b.r); // portion moved by a
|
|
75563
|
+
a.x -= ux * shift * share;
|
|
75564
|
+
a.y -= uy * shift * share;
|
|
75565
|
+
b.x += ux * shift * (1 - share);
|
|
75566
|
+
b.y += uy * shift * (1 - share);
|
|
75567
|
+
}
|
|
75568
|
+
|
|
75569
|
+
// A deterministic stand-in for d3-force's jiggle(), which uses Math.random()
|
|
75570
|
+
function getPseudoDirection(i, j) {
|
|
75571
|
+
var hash = (Math.imul(i + 1, 2654435761) ^ Math.imul(j + 1, 1597334677)) >>> 0,
|
|
75572
|
+
x = (hash & 0xffff) / 0x8000 - 1,
|
|
75573
|
+
y = ((hash >>> 16) & 0xffff) / 0x8000 - 1,
|
|
75574
|
+
dist = Math.sqrt(x * x + y * y);
|
|
75575
|
+
return dist > 0 ? [x / dist, y / dist] : [1, 0];
|
|
75576
|
+
}
|
|
75577
|
+
|
|
75578
|
+
function anyNodeCanMove(nodes) {
|
|
75579
|
+
for (var i=0; i<nodes.length; i++) {
|
|
75580
|
+
if (nodes[i].maxShift > 0) return true;
|
|
75581
|
+
}
|
|
75582
|
+
return false;
|
|
75583
|
+
}
|
|
75584
|
+
|
|
75585
|
+
// Pull each node back onto the circle of radius node.maxShift around its anchor.
|
|
75586
|
+
function limitDisplacement(nodes) {
|
|
75587
|
+
var node, maxShift, dx, dy, distSq, k, i;
|
|
75588
|
+
for (i=0; i<nodes.length; i++) {
|
|
75589
|
+
node = nodes[i];
|
|
75590
|
+
maxShift = node.maxShift;
|
|
75591
|
+
dx = node.x - node.x0;
|
|
75592
|
+
dy = node.y - node.y0;
|
|
75593
|
+
distSq = dx * dx + dy * dy;
|
|
75594
|
+
if (distSq <= maxShift * maxShift) continue;
|
|
75595
|
+
k = maxShift / Math.sqrt(distSq);
|
|
75596
|
+
node.x = node.x0 + dx * k;
|
|
75597
|
+
node.y = node.y0 + dy * k;
|
|
75598
|
+
}
|
|
75599
|
+
}
|
|
75600
|
+
|
|
75601
|
+
var SymbolCollisions = /*#__PURE__*/Object.freeze({
|
|
75602
|
+
__proto__: null,
|
|
75603
|
+
VISIBLE_OVERLAP: VISIBLE_OVERLAP,
|
|
75604
|
+
countSymbolCollisions: countSymbolCollisions,
|
|
75605
|
+
resolveSymbolCollisions: resolveSymbolCollisions,
|
|
75606
|
+
symbolCollisionDefaults: symbolCollisionDefaults
|
|
75607
|
+
});
|
|
75608
|
+
|
|
75609
|
+
// Move circular symbols apart to reduce overlaps, keeping each one within a
|
|
75610
|
+
// fixed pixel distance of its true position.
|
|
75611
|
+
//
|
|
75612
|
+
// The layout runs in the pixel space of the map that the symbols will be
|
|
75613
|
+
// rendered in, so the command needs to know the display scale: either from a
|
|
75614
|
+
// frame associated with the target, or from a width= option.
|
|
75615
|
+
//
|
|
75616
|
+
// All targeted layers are laid out in a single simulation, so symbols in
|
|
75617
|
+
// different layers are moved apart from each other as well.
|
|
75618
|
+
//
|
|
75619
|
+
// An optional polygons= layer confines each symbol to the polygon it started
|
|
75620
|
+
// in, so that a symbol standing for one area can't drift into a neighboring
|
|
75621
|
+
// one. Symbols in an area too small to hold them barely move as a result, and
|
|
75622
|
+
// their overlaps go unresolved -- that is the point of the option, but it does
|
|
75623
|
+
// mean fewer overlaps get fixed.
|
|
75624
|
+
var DEFAULT_PADDING = 0; // pixels
|
|
75625
|
+
|
|
75626
|
+
cmd.repel = function(targetLayers, dataset, catalog, polygonSource, opts) {
|
|
75627
|
+
var solverOpts = {
|
|
75628
|
+
ticks: opts.ticks > 0 ? opts.ticks : symbolCollisionDefaults.ticks,
|
|
75629
|
+
strength: opts.strength > 0 ? opts.strength : symbolCollisionDefaults.strength
|
|
75630
|
+
};
|
|
75631
|
+
var nodes = [];
|
|
75632
|
+
|
|
75633
|
+
// Validated before the display scale is resolved, so that an unusable target
|
|
75634
|
+
// is reported as such instead of as a missing width= option.
|
|
75635
|
+
requireProjectedDataset(dataset);
|
|
75636
|
+
targetLayers.forEach(function(lyr) {
|
|
75637
|
+
requireRepelTarget(lyr, opts);
|
|
75638
|
+
});
|
|
75639
|
+
var pixelsPerUnit = getDisplayScale(targetLayers, dataset, catalog, opts);
|
|
75640
|
+
targetLayers.forEach(function(lyr) {
|
|
75641
|
+
addLayerNodes(nodes, lyr, pixelsPerUnit, opts);
|
|
75642
|
+
});
|
|
75643
|
+
if (nodes.length === 0) {
|
|
75644
|
+
stop$1('Targeted layer(s) contain no circle symbols to move.');
|
|
75645
|
+
}
|
|
75646
|
+
var unconstrained = addContainment(nodes, dataset, polygonSource, pixelsPerUnit, solverOpts);
|
|
75647
|
+
|
|
75648
|
+
var collisionsBefore = countSymbolCollisions(nodes);
|
|
75649
|
+
var moved = resolveSymbolCollisions(nodes, solverOpts);
|
|
75650
|
+
var collisionsAfter = countSymbolCollisions(nodes);
|
|
75651
|
+
applyDisplacement(nodes, pixelsPerUnit);
|
|
75652
|
+
reportResults(nodes, moved, collisionsBefore, collisionsAfter, !!polygonSource);
|
|
75653
|
+
if (unconstrained > 0) {
|
|
75654
|
+
message(utils.format('%d symbol%s not inside any polygon in %s, and %s left free to move',
|
|
75655
|
+
unconstrained, unconstrained == 1 ? ' is' : 's are', polygonSource.layer.name || 'the polygons= layer',
|
|
75656
|
+
unconstrained == 1 ? 'was' : 'were'));
|
|
75657
|
+
}
|
|
75658
|
+
};
|
|
75659
|
+
|
|
75660
|
+
// Confines symbols to the polygons they start inside. Returns the number of
|
|
75661
|
+
// symbols that no polygon contains, which are left unconstrained.
|
|
75662
|
+
function addContainment(nodes, dataset, polygonSource, pixelsPerUnit, solverOpts) {
|
|
75663
|
+
if (!polygonSource) return 0;
|
|
75664
|
+
var lyr = polygonSource.layer;
|
|
75665
|
+
var arcs = polygonSource.dataset.arcs;
|
|
75666
|
+
requirePolygonLayer(lyr, 'The polygons= option requires a polygon layer.');
|
|
75667
|
+
// Only catches mixing projected with unprojected data; two different
|
|
75668
|
+
// projections are the caller's problem, as with -join
|
|
75669
|
+
requireDatasetsHaveCompatibleCRS([dataset, polygonSource.dataset]);
|
|
75670
|
+
var unconstrained = assignContainingPolygons(nodes, lyr, arcs, pixelsPerUnit);
|
|
75671
|
+
solverOpts.constrain = getContainmentConstraint(lyr, arcs, pixelsPerUnit);
|
|
75672
|
+
return unconstrained;
|
|
75673
|
+
}
|
|
75674
|
+
|
|
75675
|
+
// Symbols are displaced by adding an offset to their original coordinates, so
|
|
75676
|
+
// that symbols that didn't move keep bit-for-bit identical coordinates.
|
|
75677
|
+
// Displaced symbols get a new coordinate array rather than an updated one,
|
|
75678
|
+
// because the GeoJSON importer reuses the arrays it is given and mutating them
|
|
75679
|
+
// would reach back into a caller's input object.
|
|
75680
|
+
function applyDisplacement(nodes, pixelsPerUnit) {
|
|
75681
|
+
var movedNodes = nodes.filter(function(node) {
|
|
75682
|
+
return node.x !== node.x0 || node.y !== node.y0;
|
|
75683
|
+
});
|
|
75684
|
+
var movedLayers = [];
|
|
75685
|
+
movedNodes.forEach(function(node) {
|
|
75686
|
+
if (movedLayers.indexOf(node.lyr) == -1) movedLayers.push(node.lyr);
|
|
75687
|
+
});
|
|
75688
|
+
movedLayers.forEach(function(lyr) {
|
|
75689
|
+
noteLayerWillChange(lyr, {operation: 'repel', unit: 'shapes'});
|
|
75690
|
+
});
|
|
75691
|
+
movedNodes.forEach(function(node) {
|
|
75692
|
+
node.shp[0] = [
|
|
75693
|
+
node.shp[0][0] + (node.x - node.x0) / pixelsPerUnit,
|
|
75694
|
+
node.shp[0][1] + (node.y - node.y0) / pixelsPerUnit
|
|
75695
|
+
];
|
|
75696
|
+
});
|
|
75697
|
+
movedLayers.forEach(function(lyr) {
|
|
75698
|
+
markLayerChanged(lyr, {operation: 'repel', unit: 'shapes'});
|
|
75699
|
+
});
|
|
75700
|
+
}
|
|
75701
|
+
|
|
75702
|
+
function reportResults(nodes, moved, before, after, contained) {
|
|
75703
|
+
var total = nodes.length;
|
|
75704
|
+
var msg = utils.format('Moved %d of %d symbol%s', moved, total, total == 1 ? '' : 's');
|
|
75705
|
+
// Counts are of visible overlaps only, so a layout whose symbols end up
|
|
75706
|
+
// touching or a fraction of a pixel apart is reported as finished, and the
|
|
75707
|
+
// advice to raise max-shift= is not given when nothing is left to fix.
|
|
75708
|
+
if (after > 0) {
|
|
75709
|
+
msg += utils.format('; %d of %d visible overlap%s remain (%s)',
|
|
75710
|
+
after, before, before == 1 ? '' : 's',
|
|
75711
|
+
contained ? getMaxShiftAdvice(nodes) + ', or drop polygons=' : getMaxShiftAdvice(nodes));
|
|
75712
|
+
} else if (before > 0) {
|
|
75713
|
+
msg += utils.format('; resolved %d visible overlap%s', before, before == 1 ? '' : 's');
|
|
75714
|
+
} else {
|
|
75715
|
+
msg += ' (no visible overlaps found)';
|
|
75716
|
+
}
|
|
75717
|
+
message(msg);
|
|
75718
|
+
}
|
|
75719
|
+
|
|
75720
|
+
// max-shift= may vary from symbol to symbol, so report the range in use.
|
|
75721
|
+
function getMaxShiftAdvice(nodes) {
|
|
75722
|
+
var min = Infinity, max = -Infinity;
|
|
75723
|
+
nodes.forEach(function(node) {
|
|
75724
|
+
if (node.maxShift < min) min = node.maxShift;
|
|
75725
|
+
if (node.maxShift > max) max = node.maxShift;
|
|
75726
|
+
});
|
|
75727
|
+
if (min === max) {
|
|
75728
|
+
return utils.format('try a larger max-shift= than %s', min);
|
|
75729
|
+
}
|
|
75730
|
+
return utils.format('try larger max-shift= values, now %s-%s', min, max);
|
|
75731
|
+
}
|
|
75732
|
+
|
|
75733
|
+
function requireRepelTarget(lyr, opts) {
|
|
75734
|
+
requireSinglePointLayer(lyr,
|
|
75735
|
+
'-repel requires single points; layer contains multi-point features.');
|
|
75736
|
+
if (!opts.radius && !layerHasCircleSymbols(lyr)) {
|
|
75737
|
+
stop$1('-repel requires a layer containing circle symbols ' +
|
|
75738
|
+
'(see the -symbols and -style commands), or a radius= option.');
|
|
75739
|
+
}
|
|
75740
|
+
}
|
|
75741
|
+
|
|
75742
|
+
function layerHasCircleSymbols(lyr) {
|
|
75743
|
+
return !!lyr.data && (lyr.data.fieldExists('svg-symbol') || lyr.data.fieldExists('r'));
|
|
75744
|
+
}
|
|
75745
|
+
|
|
75746
|
+
function addLayerNodes(nodes, lyr, pixelsPerUnit, opts) {
|
|
75747
|
+
var getRadius = getRadiusAccessor(lyr, opts);
|
|
75748
|
+
var getPadding = getPixelValueAccessor(opts.padding, 'padding', DEFAULT_PADDING, lyr);
|
|
75749
|
+
var getMaxShift = getPixelValueAccessor(opts.max_shift, 'max-shift',
|
|
75750
|
+
symbolCollisionDefaults.max_shift, lyr);
|
|
75751
|
+
var i, shp, p, r;
|
|
75752
|
+
for (i=0; i<lyr.shapes.length; i++) {
|
|
75753
|
+
shp = lyr.shapes[i];
|
|
75754
|
+
p = shp ? shp[0] : null;
|
|
75755
|
+
if (!p) continue;
|
|
75756
|
+
r = getRadius(i);
|
|
75757
|
+
if (!(r > 0) || !isFinite(p[0]) || !isFinite(p[1])) continue;
|
|
75758
|
+
nodes.push({
|
|
75759
|
+
lyr: lyr,
|
|
75760
|
+
shp: shp,
|
|
75761
|
+
i: nodes.length,
|
|
75762
|
+
x: p[0] * pixelsPerUnit,
|
|
75763
|
+
y: p[1] * pixelsPerUnit,
|
|
75764
|
+
x0: p[0] * pixelsPerUnit,
|
|
75765
|
+
y0: p[1] * pixelsPerUnit,
|
|
75766
|
+
// Padding is added to the radius, so a pair of symbols ends up with
|
|
75767
|
+
// (padding a + padding b) pixels of clearance between them.
|
|
75768
|
+
r: r + getPadding(i),
|
|
75769
|
+
maxShift: getMaxShift(i)
|
|
75770
|
+
});
|
|
75771
|
+
}
|
|
75772
|
+
}
|
|
75773
|
+
|
|
75774
|
+
// padding= and max-shift= accept a number, a field name or an expression, and
|
|
75775
|
+
// so are resolved for each symbol.
|
|
75776
|
+
function getPixelValueAccessor(optVal, name, defaultVal, lyr) {
|
|
75777
|
+
if (optVal === undefined || optVal === null || optVal === '') {
|
|
75778
|
+
return function(i) {return defaultVal;};
|
|
75779
|
+
}
|
|
75780
|
+
var accessor = getPropertyAccessor(optVal, 'number', lyr, name);
|
|
75781
|
+
return function(i) {
|
|
75782
|
+
var val = +accessor(i);
|
|
75783
|
+
if (!(val >= 0)) {
|
|
75784
|
+
stop$1(utils.format('Invalid %s= value: %s', name, accessor(i)));
|
|
75785
|
+
}
|
|
75786
|
+
return val;
|
|
75787
|
+
};
|
|
75788
|
+
}
|
|
75789
|
+
|
|
75790
|
+
// Radii come from one of three places: an explicit radius= option, the
|
|
75791
|
+
// svg-symbol field written by -symbols, or the r field written by -style.
|
|
75792
|
+
// Deliberately not using getSymbolRadius(), whose default of 5 would invent
|
|
75793
|
+
// radii for symbols that don't exist.
|
|
75794
|
+
function getRadiusAccessor(lyr, opts) {
|
|
75795
|
+
if (opts.radius) {
|
|
75796
|
+
return getSymbolPropertyAccessor(opts.radius, 'radius', lyr);
|
|
75797
|
+
}
|
|
75798
|
+
var records = lyr.data.getRecords();
|
|
75799
|
+
return function(i) {
|
|
75800
|
+
var rec = records[i];
|
|
75801
|
+
if (!rec) return 0;
|
|
75802
|
+
if (rec['svg-symbol']) return getCircleSymbolRadius(rec['svg-symbol']);
|
|
75803
|
+
return rec.r > 0 ? +rec.r : 0;
|
|
75804
|
+
};
|
|
75805
|
+
}
|
|
75806
|
+
|
|
75807
|
+
function getCircleSymbolRadius(sym) {
|
|
75808
|
+
if (utils.isString(sym)) {
|
|
75809
|
+
try {
|
|
75810
|
+
sym = JSON.parse(sym);
|
|
75811
|
+
} catch(e) {
|
|
75812
|
+
return 0;
|
|
75813
|
+
}
|
|
75814
|
+
}
|
|
75815
|
+
if (!sym || sym.type != 'circle') {
|
|
75816
|
+
stop$1('-repel currently supports circle symbols only' +
|
|
75817
|
+
(sym && sym.type ? ' (found a ' + sym.type + ' symbol).' : '.'));
|
|
75818
|
+
}
|
|
75819
|
+
return sym.r > 0 ? +sym.r : 0;
|
|
75820
|
+
}
|
|
75821
|
+
|
|
75822
|
+
// Returns the number of display pixels per map coordinate unit.
|
|
75823
|
+
function getDisplayScale(targetLayers, dataset, catalog, opts) {
|
|
75824
|
+
var frame, bounds, extent;
|
|
75825
|
+
if (opts.width > 0) {
|
|
75826
|
+
bounds = getCombinedBounds(targetLayers, dataset);
|
|
75827
|
+
// A layer of collinear points has no width; fall back to its height so that
|
|
75828
|
+
// width= still gives a usable scale.
|
|
75829
|
+
extent = bounds.width() || bounds.height();
|
|
75830
|
+
if (!(extent > 0)) {
|
|
75831
|
+
stop$1('Unable to calculate a display scale: targeted symbols have no extent.');
|
|
75832
|
+
}
|
|
75833
|
+
return opts.width / extent;
|
|
75834
|
+
}
|
|
75835
|
+
frame = findRepelFrame(dataset, catalog);
|
|
75836
|
+
if (!frame) {
|
|
75837
|
+
stop$1('-repel requires a width= option when the target has no frame.');
|
|
75838
|
+
}
|
|
75839
|
+
warnIfFrameMismatched(frame, targetLayers, dataset);
|
|
75840
|
+
return getFramePixelsPerUnit(frame);
|
|
75841
|
+
}
|
|
75842
|
+
|
|
75843
|
+
function findRepelFrame(dataset, catalog) {
|
|
75844
|
+
var lyr = findFrameLayerInDataset(dataset);
|
|
75845
|
+
var target;
|
|
75846
|
+
if (lyr) return getFrameLayerData(lyr, dataset.arcs);
|
|
75847
|
+
// -frame adds the frame it creates to the catalog as a separate dataset, so a
|
|
75848
|
+
// catalog-wide search is needed to find it (-scalebar does the same).
|
|
75849
|
+
target = catalog ? findFrame(catalog) : null;
|
|
75850
|
+
return target ? getFrameLayerData(target.layer, target.dataset.arcs) : null;
|
|
75851
|
+
}
|
|
75852
|
+
|
|
75853
|
+
// Mirrors the transform applied by fitDatasetToFrame(), so that the layout
|
|
75854
|
+
// matches the pixels that get rendered. A naive width / bbox width differs
|
|
75855
|
+
// whenever the frame bbox and its pixel dimensions have different aspect ratios.
|
|
75856
|
+
function getFramePixelsPerUnit(frameData) {
|
|
75857
|
+
var bounds = new Bounds(frameData.bbox);
|
|
75858
|
+
var bounds2 = frameData.bbox2 ? new Bounds(frameData.bbox2) :
|
|
75859
|
+
new Bounds(0, 0, frameData.width, frameData.height);
|
|
75860
|
+
bounds.fillOut(bounds2.width() / bounds2.height());
|
|
75861
|
+
return bounds.getTransform(bounds2).mx;
|
|
75862
|
+
}
|
|
75863
|
+
|
|
75864
|
+
function warnIfFrameMismatched(frame, targetLayers, dataset) {
|
|
75865
|
+
var bounds = getCombinedBounds(targetLayers, dataset);
|
|
75866
|
+
if (!bounds.hasBounds()) return;
|
|
75867
|
+
if (!new Bounds(frame.bbox).intersects(bounds)) {
|
|
75868
|
+
message('[repel] Warning: the frame does not overlap the targeted symbols. ' +
|
|
75869
|
+
'If the data was projected after the frame was made, the display scale is wrong.');
|
|
75870
|
+
}
|
|
75871
|
+
}
|
|
75872
|
+
|
|
75873
|
+
function getCombinedBounds(targetLayers, dataset) {
|
|
75874
|
+
return targetLayers.reduce(function(memo, lyr) {
|
|
75875
|
+
var bounds = getLayerBounds(lyr, dataset.arcs);
|
|
75876
|
+
return bounds ? memo.mergeBounds(bounds) : memo;
|
|
75877
|
+
}, new Bounds());
|
|
75878
|
+
}
|
|
75879
|
+
|
|
75303
75880
|
// Support for evaluating expressions embedded in curly-brace templates
|
|
75304
75881
|
|
|
75305
75882
|
// Returns: a string (e.g. a command string used by the -run command)
|
|
@@ -77719,11 +78296,16 @@ ${svg}
|
|
|
77719
78296
|
|
|
77720
78297
|
// TODO: refactor to remove duplication in mapshaper-svg-style.js
|
|
77721
78298
|
cmd.symbols = function(inputLyr, dataset, opts) {
|
|
77722
|
-
|
|
78299
|
+
requirePointLayer(inputLyr);
|
|
77723
78300
|
var lyr = opts.no_replace ? copyLayer(inputLyr) : inputLyr;
|
|
77724
78301
|
var shapeMode = !!opts.geographic;
|
|
77725
78302
|
var metersPerPx;
|
|
77726
78303
|
if (shapeMode) {
|
|
78304
|
+
// Only the first point of each feature is used to place a generated shape,
|
|
78305
|
+
// so the other points of a multi-point feature would be dropped. In SVG
|
|
78306
|
+
// mode geometry is untouched and every point gets a symbol, so multi-point
|
|
78307
|
+
// features are fine.
|
|
78308
|
+
requireSinglePointLayer(inputLyr);
|
|
77727
78309
|
requireProjectedDataset(dataset);
|
|
77728
78310
|
metersPerPx = opts.pixel_scale || getMetersPerPixel(lyr);
|
|
77729
78311
|
}
|
|
@@ -78313,7 +78895,7 @@ ${svg}
|
|
|
78313
78895
|
'filter-islands2', 'filter-points', 'filter-slivers', 'grid', 'grid2',
|
|
78314
78896
|
'fuzzy-join', 'ignore', 'inlay', 'innerlines', 'inspect', 'join',
|
|
78315
78897
|
'lines', 'mosaic', 'points', 'polygons', 'rectangles', 'rename-fields',
|
|
78316
|
-
'shapes', 'simplify', 'slice', 'smooth', 'sort', 'split',
|
|
78898
|
+
'repel', 'shapes', 'simplify', 'slice', 'smooth', 'sort', 'split',
|
|
78317
78899
|
'split-on-grid', 'stitch', 'style', 'subdivide', 'symbols', 'uniq'
|
|
78318
78900
|
].indexOf(name) > -1;
|
|
78319
78901
|
}
|
|
@@ -78673,6 +79255,10 @@ ${svg}
|
|
|
78673
79255
|
} else if (name == 'rename-layers') {
|
|
78674
79256
|
cmd.renameLayers(targetLayers, opts.names);
|
|
78675
79257
|
|
|
79258
|
+
} else if (name == 'repel') {
|
|
79259
|
+
cmd.repel(targetLayers, targetDataset, job.catalog,
|
|
79260
|
+
await findRepelPolygons(opts, job.catalog, targets), opts);
|
|
79261
|
+
|
|
78676
79262
|
} else if (name == 'require') {
|
|
78677
79263
|
await cmd.require(opts);
|
|
78678
79264
|
|
|
@@ -78865,6 +79451,15 @@ ${svg}
|
|
|
78865
79451
|
return job;
|
|
78866
79452
|
}
|
|
78867
79453
|
|
|
79454
|
+
// -repel's polygons= option names a second layer, like source= elsewhere, but
|
|
79455
|
+
// is resolved here rather than by the shared source= machinery: the layer is
|
|
79456
|
+
// only read (never merged into the target's topology), so it needs neither a
|
|
79457
|
+
// topology merge nor the per-target copies that clip and erase require.
|
|
79458
|
+
async function findRepelPolygons(opts, catalog, targets) {
|
|
79459
|
+
if (!opts.polygons) return null;
|
|
79460
|
+
return await findCommandSourceAsync(convertSourceName(opts.polygons, targets), catalog, opts);
|
|
79461
|
+
}
|
|
79462
|
+
|
|
78868
79463
|
async function resolveSourceForTargets(command, job, targets) {
|
|
78869
79464
|
job.startCommand(command);
|
|
78870
79465
|
try {
|
|
@@ -78970,7 +79565,7 @@ ${svg}
|
|
|
78970
79565
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
78971
79566
|
}
|
|
78972
79567
|
|
|
78973
|
-
var version = "0.7.
|
|
79568
|
+
var version = "0.7.56";
|
|
78974
79569
|
|
|
78975
79570
|
// Parse command line args into commands and run them
|
|
78976
79571
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|
|
@@ -81175,6 +81770,7 @@ ${svg}
|
|
|
81175
81770
|
Simplify,
|
|
81176
81771
|
SimplifyFast,
|
|
81177
81772
|
SimplifyPct,
|
|
81773
|
+
SymbolCollisions,
|
|
81178
81774
|
Slivers,
|
|
81179
81775
|
Snapping,
|
|
81180
81776
|
SourceUtils,
|