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/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
- return pathIsRectangle(shp[0], arcs);
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', svgName + ':', strVal);
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
- requireSinglePointLayer(inputLyr);
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.55";
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,