mapshaper 0.7.54 → 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/README.md +2 -2
- package/mapshaper.js +624 -23
- package/package.json +1 -1
- package/www/index.html +1 -2
- package/www/mapshaper-gui.js +1 -1
- package/www/mapshaper.js +624 -23
- package/www/sponsor.html +0 -3
package/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,
|
|
@@ -34003,8 +34027,6 @@ ${svg}
|
|
|
34003
34027
|
}); });
|
|
34004
34028
|
}
|
|
34005
34029
|
|
|
34006
|
-
function buildHeader(t,r=0){let n,o=new Builder,i=0;t.columns&&(i=Header.createColumnsVector(o,t.columns.map(e=>{let t;return t=o.createString(e.name),Column.startColumn(o),Column.addName(o,t),Column.addType(o,e.type),Column.endColumn(o)})));let f=o.createString("L1");r&&(Crs.startCrs(o),Crs.addCode(o,r),n=Crs.endCrs(o)),Header.startHeader(o),n&&Header.addCrs(o,n),Header.addFeaturesCount(o,BigInt(t.featuresCount)),Header.addGeometryType(o,t.geometryType),Header.addIndexNodeSize(o,0),i&&Header.addColumns(o,i),Header.addName(o,f);let u=Header.endHeader(o);return o.finishSizePrefixed(u),o.asUint8Array()}
|
|
34007
|
-
|
|
34008
34030
|
// bytes: Uint8Array
|
|
34009
34031
|
function getHeaderMeta(bytes) {
|
|
34010
34032
|
if (!bytes.subarray(0, 3).every((v, i) => magicbytes[i] === v)) {
|
|
@@ -34012,7 +34034,11 @@ ${svg}
|
|
|
34012
34034
|
}
|
|
34013
34035
|
var bb = new ByteBuffer(bytes);
|
|
34014
34036
|
bb.setPosition(magicbytes.length + SIZE_PREFIX_LEN);
|
|
34015
|
-
|
|
34037
|
+
var header = Header.getRootAsHeader(bb);
|
|
34038
|
+
var meta = fromByteBuffer(bb);
|
|
34039
|
+
// The library's header-meta helper omits the FlatGeobuf dataset name.
|
|
34040
|
+
meta.name = header.name();
|
|
34041
|
+
return meta;
|
|
34016
34042
|
}
|
|
34017
34043
|
|
|
34018
34044
|
// bytes: Uint8Array
|
|
@@ -34086,7 +34112,7 @@ ${svg}
|
|
|
34086
34112
|
// source: a feature cursor, {length, getFeature(i)}. Features are pulled and
|
|
34087
34113
|
// encoded one at a time rather than as a collection, because the GeoJSON form
|
|
34088
34114
|
// of a layer is several times larger than the FlatGeobuf it encodes into.
|
|
34089
|
-
function serializeWithColumns(source, columns) {
|
|
34115
|
+
function serializeWithColumns(source, columns, name) {
|
|
34090
34116
|
if (source.length === 0) {
|
|
34091
34117
|
throw new Error('Could not infer geometry type for collection of features.');
|
|
34092
34118
|
}
|
|
@@ -34094,13 +34120,13 @@ ${svg}
|
|
|
34094
34120
|
// type, but that type isn't known until every feature has been seen. Assume
|
|
34095
34121
|
// the layer is homogeneous, which nearly all are, and encode again as
|
|
34096
34122
|
// Unknown in the rare case that it isn't.
|
|
34097
|
-
return encodeCollection(source, columns, null) ||
|
|
34098
|
-
encodeCollection(source, columns, GeometryType.Unknown);
|
|
34123
|
+
return encodeCollection(source, columns, name, null) ||
|
|
34124
|
+
encodeCollection(source, columns, name, GeometryType.Unknown);
|
|
34099
34125
|
}
|
|
34100
34126
|
|
|
34101
34127
|
// Returns null if forcedType is null and the features turn out to have more
|
|
34102
34128
|
// than one geometry type.
|
|
34103
|
-
function encodeCollection(source, columns, forcedType) {
|
|
34129
|
+
function encodeCollection(source, columns, name, forcedType) {
|
|
34104
34130
|
var headerMeta = null;
|
|
34105
34131
|
var sink = null;
|
|
34106
34132
|
// Null-geometry features encountered before the collection's geometry type is
|
|
@@ -34126,7 +34152,8 @@ ${svg}
|
|
|
34126
34152
|
}
|
|
34127
34153
|
var type = GeometryType[feature.geometry.type] || GeometryType.Unknown;
|
|
34128
34154
|
if (!headerMeta) {
|
|
34129
|
-
headerMeta = getEncodedHeaderMeta(
|
|
34155
|
+
headerMeta = getEncodedHeaderMeta(
|
|
34156
|
+
forcedType === null ? type : forcedType, columns, source.length, name);
|
|
34130
34157
|
sink = initSink(source, headerMeta, deferred);
|
|
34131
34158
|
} else if (forcedType === null && type != headerMeta.geometryType) {
|
|
34132
34159
|
return null;
|
|
@@ -34139,15 +34166,16 @@ ${svg}
|
|
|
34139
34166
|
}
|
|
34140
34167
|
if (!sink) {
|
|
34141
34168
|
// No feature had a geometry, so the collection has no geometry type.
|
|
34142
|
-
headerMeta = getEncodedHeaderMeta(
|
|
34143
|
-
columns, source.length);
|
|
34169
|
+
headerMeta = getEncodedHeaderMeta(
|
|
34170
|
+
forcedType === null ? GeometryType.Unknown : forcedType, columns, source.length, name);
|
|
34144
34171
|
sink = initSink(source, headerMeta, deferred);
|
|
34145
34172
|
}
|
|
34146
34173
|
return sink.toBytes();
|
|
34147
34174
|
}
|
|
34148
34175
|
|
|
34149
|
-
function getEncodedHeaderMeta(geometryType, columns, featuresCount) {
|
|
34176
|
+
function getEncodedHeaderMeta(geometryType, columns, featuresCount, name) {
|
|
34150
34177
|
return {
|
|
34178
|
+
name: name,
|
|
34151
34179
|
geometryType: geometryType,
|
|
34152
34180
|
columns: columns,
|
|
34153
34181
|
envelope: null,
|
|
@@ -34163,7 +34191,7 @@ ${svg}
|
|
|
34163
34191
|
function initSink(source, headerMeta, deferredFeatures) {
|
|
34164
34192
|
var sink = new ByteSink(magicbytes.length + estimateEncodedBytes(source));
|
|
34165
34193
|
sink.append(magicbytes);
|
|
34166
|
-
sink.append(
|
|
34194
|
+
sink.append(buildHeaderWithCRS(headerMeta, null));
|
|
34167
34195
|
for (var i = 0; i < deferredFeatures.length; i++) {
|
|
34168
34196
|
sink.append(deferredFeatures[i]);
|
|
34169
34197
|
}
|
|
@@ -34287,13 +34315,14 @@ ${svg}
|
|
|
34287
34315
|
}
|
|
34288
34316
|
var crsMeta = resolveOutputCRS(dataset);
|
|
34289
34317
|
return dataset.layers.map(function(lyr) {
|
|
34318
|
+
var name = lyr.name || 'layer';
|
|
34290
34319
|
var cursor = getFeatureCursor(lyr, dataset, opts, true);
|
|
34291
34320
|
if (cursor.length === 0) {
|
|
34292
34321
|
stop$1('FlatGeobuf export does not support empty layers');
|
|
34293
34322
|
}
|
|
34294
34323
|
var columns = getFlatGeobufColumns(lyr);
|
|
34295
|
-
var content = serializeWithColumns(cursor, columns);
|
|
34296
|
-
var filename =
|
|
34324
|
+
var content = serializeWithColumns(cursor, columns, name);
|
|
34325
|
+
var filename = name + '.' + extension;
|
|
34297
34326
|
if (crsMeta) {
|
|
34298
34327
|
content = rewriteHeaderWithCRS(content, crsMeta);
|
|
34299
34328
|
} else {
|
|
@@ -40998,6 +41027,34 @@ ${svg}
|
|
|
40998
41027
|
})
|
|
40999
41028
|
.option('target', targetOpt);
|
|
41000
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
|
+
|
|
41001
41058
|
parser.command('symbols')
|
|
41002
41059
|
.describe('symbolize points as arrows, circles, stars, polygons, etc.')
|
|
41003
41060
|
.option('type', {
|
|
@@ -74979,7 +75036,7 @@ ${svg}
|
|
|
74979
75036
|
|
|
74980
75037
|
var datasets = [targetDataset];
|
|
74981
75038
|
var outputLayers = targetLayers.map(function(pointLyr) {
|
|
74982
|
-
if (countMultiPartFeatures(pointLyr) > 0) {
|
|
75039
|
+
if (countMultiPartFeatures(pointLyr.shapes) > 0) {
|
|
74983
75040
|
stop$1('This command requires single points');
|
|
74984
75041
|
}
|
|
74985
75042
|
var dataset = getPolygonDataset(pointLyr, bbox, opts);
|
|
@@ -75295,6 +75352,531 @@ ${svg}
|
|
|
75295
75352
|
}, {});
|
|
75296
75353
|
}
|
|
75297
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
|
+
|
|
75298
75880
|
// Support for evaluating expressions embedded in curly-brace templates
|
|
75299
75881
|
|
|
75300
75882
|
// Returns: a string (e.g. a command string used by the -run command)
|
|
@@ -77714,11 +78296,16 @@ ${svg}
|
|
|
77714
78296
|
|
|
77715
78297
|
// TODO: refactor to remove duplication in mapshaper-svg-style.js
|
|
77716
78298
|
cmd.symbols = function(inputLyr, dataset, opts) {
|
|
77717
|
-
|
|
78299
|
+
requirePointLayer(inputLyr);
|
|
77718
78300
|
var lyr = opts.no_replace ? copyLayer(inputLyr) : inputLyr;
|
|
77719
78301
|
var shapeMode = !!opts.geographic;
|
|
77720
78302
|
var metersPerPx;
|
|
77721
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);
|
|
77722
78309
|
requireProjectedDataset(dataset);
|
|
77723
78310
|
metersPerPx = opts.pixel_scale || getMetersPerPixel(lyr);
|
|
77724
78311
|
}
|
|
@@ -78308,7 +78895,7 @@ ${svg}
|
|
|
78308
78895
|
'filter-islands2', 'filter-points', 'filter-slivers', 'grid', 'grid2',
|
|
78309
78896
|
'fuzzy-join', 'ignore', 'inlay', 'innerlines', 'inspect', 'join',
|
|
78310
78897
|
'lines', 'mosaic', 'points', 'polygons', 'rectangles', 'rename-fields',
|
|
78311
|
-
'shapes', 'simplify', 'slice', 'smooth', 'sort', 'split',
|
|
78898
|
+
'repel', 'shapes', 'simplify', 'slice', 'smooth', 'sort', 'split',
|
|
78312
78899
|
'split-on-grid', 'stitch', 'style', 'subdivide', 'symbols', 'uniq'
|
|
78313
78900
|
].indexOf(name) > -1;
|
|
78314
78901
|
}
|
|
@@ -78668,6 +79255,10 @@ ${svg}
|
|
|
78668
79255
|
} else if (name == 'rename-layers') {
|
|
78669
79256
|
cmd.renameLayers(targetLayers, opts.names);
|
|
78670
79257
|
|
|
79258
|
+
} else if (name == 'repel') {
|
|
79259
|
+
cmd.repel(targetLayers, targetDataset, job.catalog,
|
|
79260
|
+
await findRepelPolygons(opts, job.catalog, targets), opts);
|
|
79261
|
+
|
|
78671
79262
|
} else if (name == 'require') {
|
|
78672
79263
|
await cmd.require(opts);
|
|
78673
79264
|
|
|
@@ -78860,6 +79451,15 @@ ${svg}
|
|
|
78860
79451
|
return job;
|
|
78861
79452
|
}
|
|
78862
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
|
+
|
|
78863
79463
|
async function resolveSourceForTargets(command, job, targets) {
|
|
78864
79464
|
job.startCommand(command);
|
|
78865
79465
|
try {
|
|
@@ -78965,7 +79565,7 @@ ${svg}
|
|
|
78965
79565
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
78966
79566
|
}
|
|
78967
79567
|
|
|
78968
|
-
var version = "0.7.
|
|
79568
|
+
var version = "0.7.56";
|
|
78969
79569
|
|
|
78970
79570
|
// Parse command line args into commands and run them
|
|
78971
79571
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|
|
@@ -81170,6 +81770,7 @@ ${svg}
|
|
|
81170
81770
|
Simplify,
|
|
81171
81771
|
SimplifyFast,
|
|
81172
81772
|
SimplifyPct,
|
|
81773
|
+
SymbolCollisions,
|
|
81173
81774
|
Slivers,
|
|
81174
81775
|
Snapping,
|
|
81175
81776
|
SourceUtils,
|