mapshaper 0.6.121 → 0.7.0
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 +1004 -238
- package/package.json +1 -1
- package/www/mapshaper.js +1004 -238
package/mapshaper.js
CHANGED
|
@@ -11960,6 +11960,29 @@
|
|
|
11960
11960
|
return file.endsWith('.' + PACKAGE_EXT);
|
|
11961
11961
|
}
|
|
11962
11962
|
|
|
11963
|
+
// Returns true if @file has an extension that may identify a mapshaper
|
|
11964
|
+
// command file (e.g. "commands.txt"). Detection still requires a content
|
|
11965
|
+
// sniff via stringLooksLikeCommandFile().
|
|
11966
|
+
function isPotentialCommandFile(file) {
|
|
11967
|
+
var ext = getFileExtension(file || '').toLowerCase();
|
|
11968
|
+
return ext === 'txt';
|
|
11969
|
+
}
|
|
11970
|
+
|
|
11971
|
+
// True if @str looks like the content of a mapshaper command file: the first
|
|
11972
|
+
// non-blank, non-comment line begins with the magic word "mapshaper".
|
|
11973
|
+
function stringLooksLikeCommandFile(str) {
|
|
11974
|
+
str = String(str || '');
|
|
11975
|
+
// Skip a leading BOM
|
|
11976
|
+
if (str.charCodeAt(0) === 0xFEFF) str = str.slice(1);
|
|
11977
|
+
var lines = str.split(/\r?\n/);
|
|
11978
|
+
for (var i = 0; i < lines.length; i++) {
|
|
11979
|
+
var line = lines[i].trim();
|
|
11980
|
+
if (!line || line.charAt(0) === '#') continue;
|
|
11981
|
+
return /^mapshaper(\s|$)/.test(line);
|
|
11982
|
+
}
|
|
11983
|
+
return false;
|
|
11984
|
+
}
|
|
11985
|
+
|
|
11963
11986
|
function isZipFile(file) {
|
|
11964
11987
|
return /\.zip$/i.test(file);
|
|
11965
11988
|
}
|
|
@@ -12004,10 +12027,12 @@
|
|
|
12004
12027
|
isImportableAsBinary: isImportableAsBinary,
|
|
12005
12028
|
isKmzFile: isKmzFile,
|
|
12006
12029
|
isPackageFile: isPackageFile,
|
|
12030
|
+
isPotentialCommandFile: isPotentialCommandFile,
|
|
12007
12031
|
isSupportedBinaryInputType: isSupportedBinaryInputType,
|
|
12008
12032
|
isZipFile: isZipFile,
|
|
12009
12033
|
looksLikeContentFile: looksLikeContentFile,
|
|
12010
12034
|
looksLikeImportableFile: looksLikeImportableFile,
|
|
12035
|
+
stringLooksLikeCommandFile: stringLooksLikeCommandFile,
|
|
12011
12036
|
stringLooksLikeJSON: stringLooksLikeJSON,
|
|
12012
12037
|
stringLooksLikeKML: stringLooksLikeKML,
|
|
12013
12038
|
stringLooksLikeSVG: stringLooksLikeSVG
|
|
@@ -13805,12 +13830,17 @@
|
|
|
13805
13830
|
profileStart('intersectSegments');
|
|
13806
13831
|
var raw = arcs.getVertexData(),
|
|
13807
13832
|
intersections = [],
|
|
13833
|
+
// opts.limit (optional): stop searching once this many intersections have
|
|
13834
|
+
// been found. Used for cheap "is this dataset clean?" checks where we
|
|
13835
|
+
// only need a small sample.
|
|
13836
|
+
limit = opts.limit > 0 ? opts.limit : 0,
|
|
13808
13837
|
arr;
|
|
13809
13838
|
for (i=0; i<stripeCount; i++) {
|
|
13810
13839
|
arr = intersectSegments(stripes[i], raw.xx, raw.yy, opts);
|
|
13811
13840
|
for (j=0; j<arr.length; j++) {
|
|
13812
13841
|
intersections.push(arr[j]);
|
|
13813
13842
|
}
|
|
13843
|
+
if (limit > 0 && intersections.length >= limit) break;
|
|
13814
13844
|
}
|
|
13815
13845
|
profileEnd('intersectSegments');
|
|
13816
13846
|
profileStart('dedupIntersections');
|
|
@@ -16106,7 +16136,7 @@
|
|
|
16106
16136
|
|
|
16107
16137
|
// Get a copy of a layer containing a subset of the layer's features,
|
|
16108
16138
|
// given a "where" expression in the options object
|
|
16109
|
-
function getLayerSelection(lyr, arcs, opts) {
|
|
16139
|
+
function getLayerSelection$1(lyr, arcs, opts) {
|
|
16110
16140
|
var lyr2 = utils.extend({}, lyr);
|
|
16111
16141
|
var filterOpts = {
|
|
16112
16142
|
expression: opts.where,
|
|
@@ -16123,11 +16153,11 @@
|
|
|
16123
16153
|
if (!opts || !opts.where) {
|
|
16124
16154
|
error('Missing required "where" parameter');
|
|
16125
16155
|
}
|
|
16126
|
-
var subsetLyr = getLayerSelection(lyr, arcs, opts);
|
|
16156
|
+
var subsetLyr = getLayerSelection$1(lyr, arcs, opts);
|
|
16127
16157
|
var cmdOpts = utils.defaults({where: null}, opts); // prevent infinite recursion
|
|
16128
16158
|
var outputLyr = commandFunc(subsetLyr, arcs, cmdOpts);
|
|
16129
16159
|
var filterOpts = utils.defaults({invert: true}, opts);
|
|
16130
|
-
var filteredLyr = getLayerSelection(lyr, arcs, filterOpts);
|
|
16160
|
+
var filteredLyr = getLayerSelection$1(lyr, arcs, filterOpts);
|
|
16131
16161
|
var merged = cmd.mergeLayers([filteredLyr, outputLyr], {verbose: false, force: true});
|
|
16132
16162
|
return merged[0];
|
|
16133
16163
|
}
|
|
@@ -16156,7 +16186,7 @@
|
|
|
16156
16186
|
result, compiled, defs, d;
|
|
16157
16187
|
if (opts.where) {
|
|
16158
16188
|
// TODO: implement no_replace option for filter() instead of this
|
|
16159
|
-
lyr = getLayerSelection(lyr, arcs, opts);
|
|
16189
|
+
lyr = getLayerSelection$1(lyr, arcs, opts);
|
|
16160
16190
|
msg += ' where ' + opts.where;
|
|
16161
16191
|
}
|
|
16162
16192
|
// Save any assigned variables to the defs object, so they will be available
|
|
@@ -16878,208 +16908,6 @@
|
|
|
16878
16908
|
dissolvePolygonGeometry: dissolvePolygonGeometry
|
|
16879
16909
|
});
|
|
16880
16910
|
|
|
16881
|
-
// Generate a dissolved layer
|
|
16882
|
-
// @opts.fields (optional) names of data fields (dissolves all if falsy)
|
|
16883
|
-
// @opts.sum-fields (Array) (optional)
|
|
16884
|
-
// @opts.copy-fields (Array) (optional)
|
|
16885
|
-
//
|
|
16886
|
-
cmd.dissolve = function(lyr, arcs, opts) {
|
|
16887
|
-
var dissolveShapes, getGroupId;
|
|
16888
|
-
opts = utils.extend({}, opts);
|
|
16889
|
-
if (opts.where) {
|
|
16890
|
-
return applyCommandToLayerSelection(cmd.dissolve, lyr, arcs, opts);
|
|
16891
|
-
}
|
|
16892
|
-
if (opts.field) opts.fields = [opts.field]; // support old "field" parameter
|
|
16893
|
-
getGroupId = getCategoryClassifier(opts.fields, lyr.data);
|
|
16894
|
-
if (opts.multipart || opts.group_points) {
|
|
16895
|
-
dissolveShapes = makeMultipartShapes(lyr, getGroupId);
|
|
16896
|
-
} else if (lyr.geometry_type == 'polygon') {
|
|
16897
|
-
dissolveShapes = dissolvePolygonGeometry(lyr.shapes, getGroupId);
|
|
16898
|
-
} else if (lyr.geometry_type == 'polyline') {
|
|
16899
|
-
dissolveShapes = dissolvePolylineGeometry(lyr, getGroupId, arcs);
|
|
16900
|
-
} else if (lyr.geometry_type == 'point') {
|
|
16901
|
-
dissolveShapes = dissolvePointGeometry(lyr, getGroupId, opts);
|
|
16902
|
-
}
|
|
16903
|
-
return composeDissolveLayer(lyr, dissolveShapes, getGroupId, opts);
|
|
16904
|
-
};
|
|
16905
|
-
|
|
16906
|
-
function makeMultipartShapes(lyr, getGroupId) {
|
|
16907
|
-
if (!lyr.shapes || !lyr.geometry_type) {
|
|
16908
|
-
stop$1('Layer is missing geometry');
|
|
16909
|
-
}
|
|
16910
|
-
cloneShapes(lyr.shapes);
|
|
16911
|
-
var shapes2 = [];
|
|
16912
|
-
lyr.shapes.forEach(function(shp, i) {
|
|
16913
|
-
var groupId = getGroupId(i);
|
|
16914
|
-
if (!shp) return;
|
|
16915
|
-
if (!shapes2[groupId]) {
|
|
16916
|
-
shapes2[groupId] = shp;
|
|
16917
|
-
} else {
|
|
16918
|
-
shapes2[groupId].push.apply(shapes2[groupId], shp);
|
|
16919
|
-
}
|
|
16920
|
-
});
|
|
16921
|
-
return shapes2;
|
|
16922
|
-
}
|
|
16923
|
-
|
|
16924
|
-
// @lyr: original undissolved layer
|
|
16925
|
-
// @shapes: dissolved shapes
|
|
16926
|
-
function composeDissolveLayer(lyr, shapes, getGroupId, opts) {
|
|
16927
|
-
var records = null;
|
|
16928
|
-
var lyr2;
|
|
16929
|
-
if (lyr.data) {
|
|
16930
|
-
records = aggregateDataRecords(lyr.data.getRecords(), getGroupId, opts);
|
|
16931
|
-
// replace missing shapes with nulls
|
|
16932
|
-
for (var i=0, n=records.length; i<n; i++) {
|
|
16933
|
-
if (shapes && !shapes[i]) {
|
|
16934
|
-
shapes[i] = null;
|
|
16935
|
-
}
|
|
16936
|
-
}
|
|
16937
|
-
}
|
|
16938
|
-
lyr2 = {
|
|
16939
|
-
name: opts.no_replace ? null : lyr.name,
|
|
16940
|
-
shapes: shapes,
|
|
16941
|
-
data: records ? new DataTable(records) : null,
|
|
16942
|
-
geometry_type: lyr.geometry_type
|
|
16943
|
-
};
|
|
16944
|
-
if (!opts.silent) {
|
|
16945
|
-
printDissolveMessage(lyr, lyr2);
|
|
16946
|
-
}
|
|
16947
|
-
return lyr2;
|
|
16948
|
-
}
|
|
16949
|
-
|
|
16950
|
-
function printDissolveMessage(pre, post) {
|
|
16951
|
-
var n1 = getFeatureCount(pre),
|
|
16952
|
-
n2 = getFeatureCount(post),
|
|
16953
|
-
msg = utils.format('Dissolved %,d feature%s into %,d feature%s',
|
|
16954
|
-
n1, utils.pluralSuffix(n1), n2,
|
|
16955
|
-
utils.pluralSuffix(n2));
|
|
16956
|
-
message(msg);
|
|
16957
|
-
}
|
|
16958
|
-
|
|
16959
|
-
// Maps tile ids to shape ids (both are non-negative integers). Supports
|
|
16960
|
-
// one-to-many mapping (a tile may belong to multiple shapes)
|
|
16961
|
-
// Also maps shape ids to tile ids. A shape may contain multiple tiles
|
|
16962
|
-
// Also supports 'flattening' -- removing one-to-many tile-shape mappings by
|
|
16963
|
-
// removing all but one shape from a tile.
|
|
16964
|
-
// Supports one-to-many mapping
|
|
16965
|
-
function TileShapeIndex(mosaic, opts) {
|
|
16966
|
-
// indexes for mapping tile ids to shape ids
|
|
16967
|
-
var singleIndex = new Int32Array(mosaic.length);
|
|
16968
|
-
utils.initializeArray(singleIndex, -1);
|
|
16969
|
-
var multipleIndex = [];
|
|
16970
|
-
// index that maps shape ids to tile ids
|
|
16971
|
-
var shapeIndex = [];
|
|
16972
|
-
|
|
16973
|
-
this.getTileIdsByShapeId = function(shapeId) {
|
|
16974
|
-
var ids = shapeIndex[shapeId];
|
|
16975
|
-
// need to filter out tile ids that have been set to -1 (indicating removal)
|
|
16976
|
-
return ids ? ids.filter(function(id) {return id >= 0;}) : [];
|
|
16977
|
-
};
|
|
16978
|
-
|
|
16979
|
-
// assumes index has been flattened
|
|
16980
|
-
this.getShapeIdByTileId = function(id) {
|
|
16981
|
-
var shapeId = singleIndex[id];
|
|
16982
|
-
return shapeId >= 0 ? shapeId : -1;
|
|
16983
|
-
};
|
|
16984
|
-
|
|
16985
|
-
// return ids of all shapes that include a tile
|
|
16986
|
-
this.getShapeIdsByTileId = function(id) {
|
|
16987
|
-
var singleId = singleIndex[id];
|
|
16988
|
-
if (singleId >= 0) {
|
|
16989
|
-
return [singleId];
|
|
16990
|
-
}
|
|
16991
|
-
if (singleId == -1) {
|
|
16992
|
-
return [];
|
|
16993
|
-
}
|
|
16994
|
-
return multipleIndex[id];
|
|
16995
|
-
};
|
|
16996
|
-
|
|
16997
|
-
this.indexTileIdsByShapeId = function(shapeId, tileIds, weightFunction) {
|
|
16998
|
-
shapeIndex[shapeId] = [];
|
|
16999
|
-
for (var i=0; i<tileIds.length; i++) {
|
|
17000
|
-
indexShapeIdByTileId(shapeId, tileIds[i], weightFunction);
|
|
17001
|
-
}
|
|
17002
|
-
};
|
|
17003
|
-
|
|
17004
|
-
// remove many-to-one tile=>shape mappings
|
|
17005
|
-
this.flatten = function() {
|
|
17006
|
-
multipleIndex.forEach(function(shapeIds, tileId) {
|
|
17007
|
-
flattenStackedTile(tileId);
|
|
17008
|
-
});
|
|
17009
|
-
multipleIndex = [];
|
|
17010
|
-
};
|
|
17011
|
-
|
|
17012
|
-
this.getUnusedTileIds = function() {
|
|
17013
|
-
var ids = [];
|
|
17014
|
-
for (var i=0, n=singleIndex.length; i<n; i++) {
|
|
17015
|
-
if (singleIndex[i] == -1) ids.push(i);
|
|
17016
|
-
}
|
|
17017
|
-
return ids;
|
|
17018
|
-
};
|
|
17019
|
-
|
|
17020
|
-
// used by gap fill; assumes that flatten() has been called
|
|
17021
|
-
this.addTileToShape = function(shapeId, tileId) {
|
|
17022
|
-
if (shapeId in shapeIndex === false || singleIndex[tileId] != -1) {
|
|
17023
|
-
error('Internal error');
|
|
17024
|
-
}
|
|
17025
|
-
singleIndex[tileId] = shapeId;
|
|
17026
|
-
shapeIndex[shapeId].push(tileId);
|
|
17027
|
-
};
|
|
17028
|
-
|
|
17029
|
-
// add a shape id to a tile
|
|
17030
|
-
function indexShapeIdByTileId(shapeId, tileId, weightFunction) {
|
|
17031
|
-
var singleId = singleIndex[tileId];
|
|
17032
|
-
if (singleId != -1 && opts.flat) {
|
|
17033
|
-
// pick the best shape if we have a weight function
|
|
17034
|
-
if (weightFunction && weightFunction(shapeId) > weightFunction(singleId)) {
|
|
17035
|
-
// replace existing shape reference
|
|
17036
|
-
removeTileFromShape(tileId, singleId); // bottleneck when overlaps are many
|
|
17037
|
-
singleIndex[tileId] = singleId;
|
|
17038
|
-
singleId = -1;
|
|
17039
|
-
} else {
|
|
17040
|
-
// keep existing shape reference
|
|
17041
|
-
return;
|
|
17042
|
-
}
|
|
17043
|
-
}
|
|
17044
|
-
if (singleId == -1) {
|
|
17045
|
-
singleIndex[tileId] = shapeId;
|
|
17046
|
-
} else if (singleId == -2) {
|
|
17047
|
-
multipleIndex[tileId].push(shapeId);
|
|
17048
|
-
} else {
|
|
17049
|
-
multipleIndex[tileId] = [singleId, shapeId];
|
|
17050
|
-
singleIndex[tileId] = -2;
|
|
17051
|
-
}
|
|
17052
|
-
shapeIndex[shapeId].push(tileId);
|
|
17053
|
-
}
|
|
17054
|
-
|
|
17055
|
-
|
|
17056
|
-
function flattenStackedTile(tileId) {
|
|
17057
|
-
// TODO: select the best shape (using some metric)
|
|
17058
|
-
var shapeIds = multipleIndex[tileId];
|
|
17059
|
-
// if (!shapeIds || shapeIds.length > 1 === false) error('flattening error');
|
|
17060
|
-
var selectedId = shapeIds[0];
|
|
17061
|
-
var shapeId;
|
|
17062
|
-
singleIndex[tileId] = selectedId; // add shape to single index
|
|
17063
|
-
// remove tile from other stacked shapes
|
|
17064
|
-
for (var i=0; i<shapeIds.length; i++) {
|
|
17065
|
-
shapeId = shapeIds[i];
|
|
17066
|
-
if (shapeId != selectedId) {
|
|
17067
|
-
removeTileFromShape(tileId, shapeId);
|
|
17068
|
-
}
|
|
17069
|
-
}
|
|
17070
|
-
}
|
|
17071
|
-
|
|
17072
|
-
function removeTileFromShape(tileId, shapeId) {
|
|
17073
|
-
var tileIds = shapeIndex[shapeId];
|
|
17074
|
-
for (var i=0; i<tileIds.length; i++) {
|
|
17075
|
-
if (tileIds[i] === tileId) {
|
|
17076
|
-
tileIds[i] = -1;
|
|
17077
|
-
break;
|
|
17078
|
-
}
|
|
17079
|
-
}
|
|
17080
|
-
}
|
|
17081
|
-
}
|
|
17082
|
-
|
|
17083
16911
|
// Clean polygon or polyline shapes (in-place)
|
|
17084
16912
|
//
|
|
17085
16913
|
function cleanShapes(shapes, arcs, type) {
|
|
@@ -17696,6 +17524,336 @@
|
|
|
17696
17524
|
sortCutPoints: sortCutPoints
|
|
17697
17525
|
});
|
|
17698
17526
|
|
|
17527
|
+
// Options that require the topology-repair algorithm and are not supported
|
|
17528
|
+
// by the no-repair fast path.
|
|
17529
|
+
var REPAIR_REQUIRED_OPTS = ['gap_fill_area', 'sliver_control', 'allow_overlaps'];
|
|
17530
|
+
|
|
17531
|
+
// Sample size used to detect intersections in no-repair mode. Detection stops
|
|
17532
|
+
// after this many hits, so the warning message can include sample locations
|
|
17533
|
+
// without paying for an exhaustive scan on badly-formed input.
|
|
17534
|
+
var INTERSECTION_SAMPLE_LIMIT = 10;
|
|
17535
|
+
|
|
17536
|
+
// cmd.dissolve accepts two signatures:
|
|
17537
|
+
// (layers, dataset, opts) — multi-layer entry used by the CLI dispatcher.
|
|
17538
|
+
// Polygon layers go through the topology-repairing algorithm by default,
|
|
17539
|
+
// or through the legacy fast algorithm when opts.no_repair is set.
|
|
17540
|
+
// (lyr, arcs, opts) — legacy single-layer entry, retained for backward
|
|
17541
|
+
// compatibility with internal callers and existing tests. Always uses the
|
|
17542
|
+
// legacy fast algorithm; does not perform topology repair.
|
|
17543
|
+
//
|
|
17544
|
+
cmd.dissolve = function(arg1, arg2, opts) {
|
|
17545
|
+
if (Array.isArray(arg1)) {
|
|
17546
|
+
return dissolveLayers(arg1, arg2, opts);
|
|
17547
|
+
}
|
|
17548
|
+
return dissolveSingleLayer(arg1, arg2, opts);
|
|
17549
|
+
};
|
|
17550
|
+
|
|
17551
|
+
function dissolveLayers(layers, dataset, optsArg) {
|
|
17552
|
+
var opts = utils.extend({}, optsArg);
|
|
17553
|
+
if (opts.field) opts.fields = [opts.field]; // support old "field" parameter
|
|
17554
|
+
|
|
17555
|
+
if (opts.no_repair) {
|
|
17556
|
+
var conflicting = REPAIR_REQUIRED_OPTS.filter(function(k) { return opts[k]; });
|
|
17557
|
+
if (conflicting.length > 0) {
|
|
17558
|
+
stop$1('The no-repair option is incompatible with',
|
|
17559
|
+
conflicting.map(function(k) { return k.replace(/_/g, '-'); }).join(', '));
|
|
17560
|
+
}
|
|
17561
|
+
}
|
|
17562
|
+
|
|
17563
|
+
var anyPolygon = layers.some(function(lyr) {
|
|
17564
|
+
return lyr.geometry_type == 'polygon' && layerHasPaths(lyr);
|
|
17565
|
+
});
|
|
17566
|
+
|
|
17567
|
+
if (anyPolygon) {
|
|
17568
|
+
if (opts.no_repair) {
|
|
17569
|
+
detectAndWarnIntersections(dataset, opts);
|
|
17570
|
+
} else {
|
|
17571
|
+
addIntersectionCuts(dataset, opts);
|
|
17572
|
+
}
|
|
17573
|
+
}
|
|
17574
|
+
|
|
17575
|
+
return layers.map(function(lyr) {
|
|
17576
|
+
return dissolveOneLayer(lyr, dataset, opts);
|
|
17577
|
+
});
|
|
17578
|
+
}
|
|
17579
|
+
|
|
17580
|
+
function dissolveOneLayer(lyr, dataset, opts) {
|
|
17581
|
+
if (opts.where) {
|
|
17582
|
+
return dissolveLayerWithWhereClause(lyr, dataset, opts);
|
|
17583
|
+
}
|
|
17584
|
+
if (opts.multipart || opts.group_points) {
|
|
17585
|
+
var classifier = getCategoryClassifier(opts.fields, lyr.data);
|
|
17586
|
+
return composeDissolveLayer(lyr, makeMultipartShapes(lyr, classifier), classifier, opts);
|
|
17587
|
+
}
|
|
17588
|
+
if (lyr.geometry_type == 'polygon') {
|
|
17589
|
+
return dissolvePolygonInLayer(lyr, dataset, opts);
|
|
17590
|
+
}
|
|
17591
|
+
if (lyr.geometry_type == 'polyline') {
|
|
17592
|
+
var polylineClassifier = getCategoryClassifier(opts.fields, lyr.data);
|
|
17593
|
+
var polylineShapes = dissolvePolylineGeometry(lyr, polylineClassifier, dataset.arcs);
|
|
17594
|
+
return composeDissolveLayer(lyr, polylineShapes, polylineClassifier, opts);
|
|
17595
|
+
}
|
|
17596
|
+
if (lyr.geometry_type == 'point') {
|
|
17597
|
+
var pointClassifier = getCategoryClassifier(opts.fields, lyr.data);
|
|
17598
|
+
var pointShapes = dissolvePointGeometry(lyr, pointClassifier, opts);
|
|
17599
|
+
return composeDissolveLayer(lyr, pointShapes, pointClassifier, opts);
|
|
17600
|
+
}
|
|
17601
|
+
// tabular (no geometry): aggregate records only
|
|
17602
|
+
var nullClassifier = getCategoryClassifier(opts.fields, lyr.data);
|
|
17603
|
+
return composeDissolveLayer(lyr, undefined, nullClassifier, opts);
|
|
17604
|
+
}
|
|
17605
|
+
|
|
17606
|
+
function dissolvePolygonInLayer(lyr, dataset, opts) {
|
|
17607
|
+
if (!layerHasPaths(lyr)) return lyr;
|
|
17608
|
+
if (opts.no_repair) {
|
|
17609
|
+
var classifier = getCategoryClassifier(opts.fields, lyr.data);
|
|
17610
|
+
var shapes = dissolvePolygonGeometry(lyr.shapes, classifier);
|
|
17611
|
+
return composeDissolveLayer(lyr, shapes, classifier, opts);
|
|
17612
|
+
}
|
|
17613
|
+
return dissolvePolygonLayer2(lyr, dataset, opts);
|
|
17614
|
+
}
|
|
17615
|
+
|
|
17616
|
+
function dissolveLayerWithWhereClause(lyr, dataset, opts) {
|
|
17617
|
+
// Run dissolve on a subset of features defined by opts.where, then merge the
|
|
17618
|
+
// dissolved subset back together with the unselected features.
|
|
17619
|
+
// Topology repair (if needed) was already performed at the dataset level by
|
|
17620
|
+
// dissolveLayers, so the recursive call uses no_repair=true to avoid doing
|
|
17621
|
+
// the work a second time on a subset of the same arcs.
|
|
17622
|
+
var arcs = dataset.arcs;
|
|
17623
|
+
var subsetLyr = getLayerSelection(lyr, arcs, opts);
|
|
17624
|
+
var cmdOpts = utils.defaults({where: null, no_repair: true}, opts);
|
|
17625
|
+
var dissolved = dissolveOneLayer(subsetLyr, dataset, cmdOpts);
|
|
17626
|
+
var filteredLyr = getLayerSelection(lyr, arcs, utils.defaults({invert: true}, opts));
|
|
17627
|
+
var merged = cmd.mergeLayers([filteredLyr, dissolved], {verbose: false, force: true});
|
|
17628
|
+
return merged[0];
|
|
17629
|
+
}
|
|
17630
|
+
|
|
17631
|
+
function getLayerSelection(lyr, arcs, opts) {
|
|
17632
|
+
var lyr2 = utils.extend({}, lyr);
|
|
17633
|
+
var filterOpts = {
|
|
17634
|
+
expression: opts.where,
|
|
17635
|
+
invert: !!opts.invert,
|
|
17636
|
+
verbose: false,
|
|
17637
|
+
no_replace: opts.no_replace
|
|
17638
|
+
};
|
|
17639
|
+
return cmd.filterFeatures(lyr2, arcs, filterOpts);
|
|
17640
|
+
}
|
|
17641
|
+
|
|
17642
|
+
// Detect a small sample of segment intersections; print a warning if any are
|
|
17643
|
+
// found. Used by the no-repair fast path to alert users that their input has
|
|
17644
|
+
// topology problems. Detection stops after INTERSECTION_SAMPLE_LIMIT hits, so
|
|
17645
|
+
// the cost is bounded for badly-formed input.
|
|
17646
|
+
function detectAndWarnIntersections(dataset, opts) {
|
|
17647
|
+
if (opts.quiet || opts.silent) return;
|
|
17648
|
+
if (!dataset.arcs || dataset.arcs.size() === 0) return;
|
|
17649
|
+
var sample = findSegmentIntersections(dataset.arcs, {limit: INTERSECTION_SAMPLE_LIMIT});
|
|
17650
|
+
if (sample.length === 0) return;
|
|
17651
|
+
var atLeast = sample.length >= INTERSECTION_SAMPLE_LIMIT ? 'at least ' : '';
|
|
17652
|
+
message('Warning: found ' + atLeast + sample.length +
|
|
17653
|
+
' segment intersection' + (sample.length == 1 ? '' : 's') +
|
|
17654
|
+
'. The no-repair option assumes clean topology; output may be incorrect.');
|
|
17655
|
+
}
|
|
17656
|
+
|
|
17657
|
+
// Backward-compat: the legacy per-layer entry, still used by internal callers
|
|
17658
|
+
// and by tests that exercise the original fast algorithm directly. Retains
|
|
17659
|
+
// the original behavior (no topology repair, no multi-layer prep).
|
|
17660
|
+
function dissolveSingleLayer(lyr, arcs, opts) {
|
|
17661
|
+
var dissolveShapes, classifier;
|
|
17662
|
+
opts = utils.extend({}, opts);
|
|
17663
|
+
if (opts.where) {
|
|
17664
|
+
return applyCommandToLayerSelection(dissolveSingleLayer, lyr, arcs, opts);
|
|
17665
|
+
}
|
|
17666
|
+
if (opts.field) opts.fields = [opts.field];
|
|
17667
|
+
classifier = getCategoryClassifier(opts.fields, lyr.data);
|
|
17668
|
+
if (opts.multipart || opts.group_points) {
|
|
17669
|
+
dissolveShapes = makeMultipartShapes(lyr, classifier);
|
|
17670
|
+
} else if (lyr.geometry_type == 'polygon') {
|
|
17671
|
+
dissolveShapes = dissolvePolygonGeometry(lyr.shapes, classifier);
|
|
17672
|
+
} else if (lyr.geometry_type == 'polyline') {
|
|
17673
|
+
dissolveShapes = dissolvePolylineGeometry(lyr, classifier, arcs);
|
|
17674
|
+
} else if (lyr.geometry_type == 'point') {
|
|
17675
|
+
dissolveShapes = dissolvePointGeometry(lyr, classifier, opts);
|
|
17676
|
+
}
|
|
17677
|
+
return composeDissolveLayer(lyr, dissolveShapes, classifier, opts);
|
|
17678
|
+
}
|
|
17679
|
+
|
|
17680
|
+
function makeMultipartShapes(lyr, getGroupId) {
|
|
17681
|
+
if (!lyr.shapes || !lyr.geometry_type) {
|
|
17682
|
+
stop$1('Layer is missing geometry');
|
|
17683
|
+
}
|
|
17684
|
+
cloneShapes(lyr.shapes);
|
|
17685
|
+
var shapes2 = [];
|
|
17686
|
+
lyr.shapes.forEach(function(shp, i) {
|
|
17687
|
+
var groupId = getGroupId(i);
|
|
17688
|
+
if (!shp) return;
|
|
17689
|
+
if (!shapes2[groupId]) {
|
|
17690
|
+
shapes2[groupId] = shp;
|
|
17691
|
+
} else {
|
|
17692
|
+
shapes2[groupId].push.apply(shapes2[groupId], shp);
|
|
17693
|
+
}
|
|
17694
|
+
});
|
|
17695
|
+
return shapes2;
|
|
17696
|
+
}
|
|
17697
|
+
|
|
17698
|
+
// @lyr: original undissolved layer
|
|
17699
|
+
// @shapes: dissolved shapes
|
|
17700
|
+
function composeDissolveLayer(lyr, shapes, getGroupId, opts) {
|
|
17701
|
+
var records = null;
|
|
17702
|
+
var lyr2;
|
|
17703
|
+
if (lyr.data) {
|
|
17704
|
+
records = aggregateDataRecords(lyr.data.getRecords(), getGroupId, opts);
|
|
17705
|
+
// replace missing shapes with nulls
|
|
17706
|
+
for (var i=0, n=records.length; i<n; i++) {
|
|
17707
|
+
if (shapes && !shapes[i]) {
|
|
17708
|
+
shapes[i] = null;
|
|
17709
|
+
}
|
|
17710
|
+
}
|
|
17711
|
+
}
|
|
17712
|
+
lyr2 = {
|
|
17713
|
+
name: opts.no_replace ? null : lyr.name,
|
|
17714
|
+
shapes: shapes,
|
|
17715
|
+
data: records ? new DataTable(records) : null,
|
|
17716
|
+
geometry_type: lyr.geometry_type
|
|
17717
|
+
};
|
|
17718
|
+
if (!opts.silent) {
|
|
17719
|
+
printDissolveMessage(lyr, lyr2);
|
|
17720
|
+
}
|
|
17721
|
+
return lyr2;
|
|
17722
|
+
}
|
|
17723
|
+
|
|
17724
|
+
function printDissolveMessage(pre, post) {
|
|
17725
|
+
var n1 = getFeatureCount(pre),
|
|
17726
|
+
n2 = getFeatureCount(post),
|
|
17727
|
+
msg = utils.format('Dissolved %,d feature%s into %,d feature%s',
|
|
17728
|
+
n1, utils.pluralSuffix(n1), n2,
|
|
17729
|
+
utils.pluralSuffix(n2));
|
|
17730
|
+
message(msg);
|
|
17731
|
+
}
|
|
17732
|
+
|
|
17733
|
+
// Maps tile ids to shape ids (both are non-negative integers). Supports
|
|
17734
|
+
// one-to-many mapping (a tile may belong to multiple shapes)
|
|
17735
|
+
// Also maps shape ids to tile ids. A shape may contain multiple tiles
|
|
17736
|
+
// Also supports 'flattening' -- removing one-to-many tile-shape mappings by
|
|
17737
|
+
// removing all but one shape from a tile.
|
|
17738
|
+
// Supports one-to-many mapping
|
|
17739
|
+
function TileShapeIndex(mosaic, opts) {
|
|
17740
|
+
// indexes for mapping tile ids to shape ids
|
|
17741
|
+
var singleIndex = new Int32Array(mosaic.length);
|
|
17742
|
+
utils.initializeArray(singleIndex, -1);
|
|
17743
|
+
var multipleIndex = [];
|
|
17744
|
+
// index that maps shape ids to tile ids
|
|
17745
|
+
var shapeIndex = [];
|
|
17746
|
+
|
|
17747
|
+
this.getTileIdsByShapeId = function(shapeId) {
|
|
17748
|
+
var ids = shapeIndex[shapeId];
|
|
17749
|
+
// need to filter out tile ids that have been set to -1 (indicating removal)
|
|
17750
|
+
return ids ? ids.filter(function(id) {return id >= 0;}) : [];
|
|
17751
|
+
};
|
|
17752
|
+
|
|
17753
|
+
// assumes index has been flattened
|
|
17754
|
+
this.getShapeIdByTileId = function(id) {
|
|
17755
|
+
var shapeId = singleIndex[id];
|
|
17756
|
+
return shapeId >= 0 ? shapeId : -1;
|
|
17757
|
+
};
|
|
17758
|
+
|
|
17759
|
+
// return ids of all shapes that include a tile
|
|
17760
|
+
this.getShapeIdsByTileId = function(id) {
|
|
17761
|
+
var singleId = singleIndex[id];
|
|
17762
|
+
if (singleId >= 0) {
|
|
17763
|
+
return [singleId];
|
|
17764
|
+
}
|
|
17765
|
+
if (singleId == -1) {
|
|
17766
|
+
return [];
|
|
17767
|
+
}
|
|
17768
|
+
return multipleIndex[id];
|
|
17769
|
+
};
|
|
17770
|
+
|
|
17771
|
+
this.indexTileIdsByShapeId = function(shapeId, tileIds, weightFunction) {
|
|
17772
|
+
shapeIndex[shapeId] = [];
|
|
17773
|
+
for (var i=0; i<tileIds.length; i++) {
|
|
17774
|
+
indexShapeIdByTileId(shapeId, tileIds[i], weightFunction);
|
|
17775
|
+
}
|
|
17776
|
+
};
|
|
17777
|
+
|
|
17778
|
+
// remove many-to-one tile=>shape mappings
|
|
17779
|
+
this.flatten = function() {
|
|
17780
|
+
multipleIndex.forEach(function(shapeIds, tileId) {
|
|
17781
|
+
flattenStackedTile(tileId);
|
|
17782
|
+
});
|
|
17783
|
+
multipleIndex = [];
|
|
17784
|
+
};
|
|
17785
|
+
|
|
17786
|
+
this.getUnusedTileIds = function() {
|
|
17787
|
+
var ids = [];
|
|
17788
|
+
for (var i=0, n=singleIndex.length; i<n; i++) {
|
|
17789
|
+
if (singleIndex[i] == -1) ids.push(i);
|
|
17790
|
+
}
|
|
17791
|
+
return ids;
|
|
17792
|
+
};
|
|
17793
|
+
|
|
17794
|
+
// used by gap fill; assumes that flatten() has been called
|
|
17795
|
+
this.addTileToShape = function(shapeId, tileId) {
|
|
17796
|
+
if (shapeId in shapeIndex === false || singleIndex[tileId] != -1) {
|
|
17797
|
+
error('Internal error');
|
|
17798
|
+
}
|
|
17799
|
+
singleIndex[tileId] = shapeId;
|
|
17800
|
+
shapeIndex[shapeId].push(tileId);
|
|
17801
|
+
};
|
|
17802
|
+
|
|
17803
|
+
// add a shape id to a tile
|
|
17804
|
+
function indexShapeIdByTileId(shapeId, tileId, weightFunction) {
|
|
17805
|
+
var singleId = singleIndex[tileId];
|
|
17806
|
+
if (singleId != -1 && opts.flat) {
|
|
17807
|
+
// pick the best shape if we have a weight function
|
|
17808
|
+
if (weightFunction && weightFunction(shapeId) > weightFunction(singleId)) {
|
|
17809
|
+
// replace existing shape reference
|
|
17810
|
+
removeTileFromShape(tileId, singleId); // bottleneck when overlaps are many
|
|
17811
|
+
singleIndex[tileId] = singleId;
|
|
17812
|
+
singleId = -1;
|
|
17813
|
+
} else {
|
|
17814
|
+
// keep existing shape reference
|
|
17815
|
+
return;
|
|
17816
|
+
}
|
|
17817
|
+
}
|
|
17818
|
+
if (singleId == -1) {
|
|
17819
|
+
singleIndex[tileId] = shapeId;
|
|
17820
|
+
} else if (singleId == -2) {
|
|
17821
|
+
multipleIndex[tileId].push(shapeId);
|
|
17822
|
+
} else {
|
|
17823
|
+
multipleIndex[tileId] = [singleId, shapeId];
|
|
17824
|
+
singleIndex[tileId] = -2;
|
|
17825
|
+
}
|
|
17826
|
+
shapeIndex[shapeId].push(tileId);
|
|
17827
|
+
}
|
|
17828
|
+
|
|
17829
|
+
|
|
17830
|
+
function flattenStackedTile(tileId) {
|
|
17831
|
+
// TODO: select the best shape (using some metric)
|
|
17832
|
+
var shapeIds = multipleIndex[tileId];
|
|
17833
|
+
// if (!shapeIds || shapeIds.length > 1 === false) error('flattening error');
|
|
17834
|
+
var selectedId = shapeIds[0];
|
|
17835
|
+
var shapeId;
|
|
17836
|
+
singleIndex[tileId] = selectedId; // add shape to single index
|
|
17837
|
+
// remove tile from other stacked shapes
|
|
17838
|
+
for (var i=0; i<shapeIds.length; i++) {
|
|
17839
|
+
shapeId = shapeIds[i];
|
|
17840
|
+
if (shapeId != selectedId) {
|
|
17841
|
+
removeTileFromShape(tileId, shapeId);
|
|
17842
|
+
}
|
|
17843
|
+
}
|
|
17844
|
+
}
|
|
17845
|
+
|
|
17846
|
+
function removeTileFromShape(tileId, shapeId) {
|
|
17847
|
+
var tileIds = shapeIndex[shapeId];
|
|
17848
|
+
for (var i=0; i<tileIds.length; i++) {
|
|
17849
|
+
if (tileIds[i] === tileId) {
|
|
17850
|
+
tileIds[i] = -1;
|
|
17851
|
+
break;
|
|
17852
|
+
}
|
|
17853
|
+
}
|
|
17854
|
+
}
|
|
17855
|
+
}
|
|
17856
|
+
|
|
17699
17857
|
// Support for timing using T.start() and T.stop()
|
|
17700
17858
|
var T$1 = {
|
|
17701
17859
|
stack: [],
|
|
@@ -26874,26 +27032,71 @@ ${svg}
|
|
|
26874
27032
|
// show help if only a command name is given
|
|
26875
27033
|
argv.unshift('-help'); // kludge (assumes -help <command> syntax)
|
|
26876
27034
|
} else if (argv.length > 0 && !tokenLooksLikeCommand(argv[0]) && _default) {
|
|
26877
|
-
// if there are arguments before the first explicit command, use the default
|
|
26878
|
-
|
|
27035
|
+
// if there are arguments before the first explicit command, use the default
|
|
27036
|
+
// command. If _default is a function, let it inspect/mutate argv directly
|
|
27037
|
+
// (this lets callers route by file type, e.g. .txt -> -run).
|
|
27038
|
+
if (typeof _default == 'function') {
|
|
27039
|
+
_default(argv);
|
|
27040
|
+
} else {
|
|
27041
|
+
argv.unshift('-' + _default);
|
|
27042
|
+
}
|
|
26879
27043
|
}
|
|
26880
27044
|
|
|
27045
|
+
// snapshot the argv so we can record the source tokens consumed by each
|
|
27046
|
+
// command (used by the late-binding {{...}} interpolator)
|
|
27047
|
+
var argvSnapshot = argv.slice();
|
|
27048
|
+
var totalLen = argvSnapshot.length;
|
|
27049
|
+
|
|
26881
27050
|
while (argv.length > 0) {
|
|
27051
|
+
var consumedBefore = totalLen - argv.length;
|
|
26882
27052
|
cmdName = readCommandName(argv);
|
|
26883
27053
|
if (!cmdName) {
|
|
26884
27054
|
stop$1("Invalid command:", argv[0]);
|
|
26885
27055
|
}
|
|
26886
27056
|
cmdDef = findCommandDefn(cmdName, commandDefs) || null;
|
|
27057
|
+
// Look ahead at the option tokens this command will consume. If any
|
|
27058
|
+
// contains a {{...}} placeholder, the late-binding interpolator (in
|
|
27059
|
+
// mapshaper-run-commands.mjs) will re-parse this command after
|
|
27060
|
+
// substitution, so we skip validation here to avoid premature errors
|
|
27061
|
+
// on un-interpolated tokens.
|
|
27062
|
+
var lookaheadTokens = peekCommandTokens(argv);
|
|
27063
|
+
var deferValidate = lookaheadTokens.some(tokenContainsPlaceholder);
|
|
26887
27064
|
if (!cmdDef) {
|
|
26888
27065
|
cmd = parseUnknownCommandOptions(argv, cmdName);
|
|
26889
27066
|
} else {
|
|
26890
|
-
cmd = parseCommandOptions(argv, cmdDef);
|
|
26891
|
-
}
|
|
27067
|
+
cmd = parseCommandOptions(argv, cmdDef, deferValidate);
|
|
27068
|
+
}
|
|
27069
|
+
var consumedAfter = totalLen - argv.length;
|
|
27070
|
+
// Stash the source tokens for the late-binding {{...}} interpolator.
|
|
27071
|
+
// Defined as non-enumerable so existing tests that deep-equal parsed
|
|
27072
|
+
// commands aren't affected.
|
|
27073
|
+
Object.defineProperty(cmd, '_tokens', {
|
|
27074
|
+
value: argvSnapshot.slice(consumedBefore, consumedAfter),
|
|
27075
|
+
enumerable: false,
|
|
27076
|
+
writable: true,
|
|
27077
|
+
configurable: true
|
|
27078
|
+
});
|
|
26892
27079
|
commands.push(cmd);
|
|
26893
27080
|
}
|
|
26894
27081
|
return commands;
|
|
26895
27082
|
|
|
26896
|
-
|
|
27083
|
+
// Return the tokens (without removing them) that the next command would
|
|
27084
|
+
// consume. Boundary is the next command name, matching the rule in
|
|
27085
|
+
// parseCommandOptions.
|
|
27086
|
+
function peekCommandTokens(argv) {
|
|
27087
|
+
var out = [];
|
|
27088
|
+
for (var i = 0; i < argv.length; i++) {
|
|
27089
|
+
if (tokenLooksLikeCommand(argv[i])) break;
|
|
27090
|
+
out.push(argv[i]);
|
|
27091
|
+
}
|
|
27092
|
+
return out;
|
|
27093
|
+
}
|
|
27094
|
+
|
|
27095
|
+
function tokenContainsPlaceholder(s) {
|
|
27096
|
+
return typeof s == 'string' && s.indexOf('{{') !== -1;
|
|
27097
|
+
}
|
|
27098
|
+
|
|
27099
|
+
function parseCommandOptions(argv, cmdDef, deferValidate) {
|
|
26897
27100
|
var cmd = {
|
|
26898
27101
|
name: cmdDef.name,
|
|
26899
27102
|
options: {},
|
|
@@ -26905,10 +27108,10 @@ ${svg}
|
|
|
26905
27108
|
}
|
|
26906
27109
|
|
|
26907
27110
|
try {
|
|
26908
|
-
if (cmd._.length > 0) {
|
|
27111
|
+
if (cmd._.length > 0 && !deferValidate) {
|
|
26909
27112
|
readDefaultOptionValue(cmd, cmdDef);
|
|
26910
27113
|
}
|
|
26911
|
-
if (cmdDef.validate) {
|
|
27114
|
+
if (cmdDef.validate && !deferValidate) {
|
|
26912
27115
|
cmdDef.validate(cmd);
|
|
26913
27116
|
}
|
|
26914
27117
|
delete cmd.options._; // kludge to remove -o placeholder option
|
|
@@ -27385,7 +27588,16 @@ ${svg}
|
|
|
27385
27588
|
|
|
27386
27589
|
parser.section('I/O commands');
|
|
27387
27590
|
|
|
27388
|
-
|
|
27591
|
+
// When the command line begins with a non-command argument, route .txt files
|
|
27592
|
+
// to "-run <path>" (command files) and everything else to "-i <path>" (data
|
|
27593
|
+
// files). The actual file-or-expression check happens inside cmd.run.
|
|
27594
|
+
parser.default(function(argv) {
|
|
27595
|
+
if (isPotentialCommandFile(argv[0])) {
|
|
27596
|
+
argv.unshift('-run');
|
|
27597
|
+
} else {
|
|
27598
|
+
argv.unshift('-i');
|
|
27599
|
+
}
|
|
27600
|
+
});
|
|
27389
27601
|
|
|
27390
27602
|
parser.command('i')
|
|
27391
27603
|
.describe('input one or more files')
|
|
@@ -27402,6 +27614,10 @@ ${svg}
|
|
|
27402
27614
|
describe: 'import files to separate layers with shared topology',
|
|
27403
27615
|
type: 'flag'
|
|
27404
27616
|
})
|
|
27617
|
+
.option('batch-mode', {
|
|
27618
|
+
describe: 'apply subsequent commands separately to each input file',
|
|
27619
|
+
type: 'flag'
|
|
27620
|
+
})
|
|
27405
27621
|
.option('merge-files', {
|
|
27406
27622
|
// describe: 'merge features from compatible files into the same layer',
|
|
27407
27623
|
type: 'flag'
|
|
@@ -28033,7 +28249,7 @@ ${svg}
|
|
|
28033
28249
|
});
|
|
28034
28250
|
|
|
28035
28251
|
parser.command('dissolve')
|
|
28036
|
-
.describe('merge features within a layer')
|
|
28252
|
+
.describe('merge features within a layer (repairs polygon topology)')
|
|
28037
28253
|
.example('Dissolve all polygons in a feature layer into a single polygon\n' +
|
|
28038
28254
|
'$ mapshaper states.shp -dissolve -o country.shp')
|
|
28039
28255
|
.example('Generate state-level polygons by dissolving a layer of counties\n' +
|
|
@@ -28060,19 +28276,33 @@ ${svg}
|
|
|
28060
28276
|
type: 'flag',
|
|
28061
28277
|
describe: '[points] use 2D math to find centroids of latlong points'
|
|
28062
28278
|
})
|
|
28279
|
+
.option('gap-fill-area', {
|
|
28280
|
+
describe: '[polygons] threshold for filling gaps, e.g. 1.5km2',
|
|
28281
|
+
type: 'area'
|
|
28282
|
+
})
|
|
28283
|
+
.option('sliver-control', sliverControlOpt)
|
|
28284
|
+
.option('allow-overlaps', {
|
|
28285
|
+
describe: '[polygons] allow output polygons to overlap (disables gap fill)',
|
|
28286
|
+
type: 'flag'
|
|
28287
|
+
})
|
|
28288
|
+
.option('no-repair', {
|
|
28289
|
+
describe: '[polygons] skip topology repair (faster; assumes clean input)',
|
|
28290
|
+
type: 'flag'
|
|
28291
|
+
})
|
|
28292
|
+
.option('snap-interval', snapIntervalOpt)
|
|
28293
|
+
.option('no-snap', noSnapOpt)
|
|
28063
28294
|
.option('name', nameOpt)
|
|
28064
28295
|
.option('target', targetOpt)
|
|
28065
28296
|
.option('no-replace', noReplaceOpt);
|
|
28066
28297
|
|
|
28067
28298
|
|
|
28299
|
+
// -dissolve2 is now an alias for -dissolve (the topology repair behavior of
|
|
28300
|
+
// -dissolve2 is now the default behavior of -dissolve). Kept for backward
|
|
28301
|
+
// compatibility; prints a deprecation notice when used.
|
|
28068
28302
|
parser.command('dissolve2')
|
|
28069
|
-
.describe('
|
|
28303
|
+
.describe('alias for -dissolve (deprecated)')
|
|
28070
28304
|
.option('field', {}) // old arg handled by dissolve function
|
|
28071
28305
|
.option('fields', dissolveFieldsOpt)
|
|
28072
|
-
// UPDATE: Use -mosaic command for debugging
|
|
28073
|
-
//.option('mosaic', {type: 'flag'}) // debugging option
|
|
28074
|
-
//.option('arcs', {type: 'flag'}) // debugging option
|
|
28075
|
-
//.option('tiles', {type: 'flag'}) // debugging option
|
|
28076
28306
|
.option('calc', calcOpt)
|
|
28077
28307
|
.option('sum-fields', sumFieldsOpt)
|
|
28078
28308
|
.option('copy-fields', copyFieldsOpt)
|
|
@@ -29352,10 +29582,11 @@ ${svg}
|
|
|
29352
29582
|
});
|
|
29353
29583
|
|
|
29354
29584
|
parser.command('run')
|
|
29355
|
-
.describe('
|
|
29585
|
+
.describe('run commands from a command file or JS expression')
|
|
29356
29586
|
.option('expression', {
|
|
29357
29587
|
DEFAULT: true,
|
|
29358
|
-
|
|
29588
|
+
label: '<file|expression>',
|
|
29589
|
+
describe: 'path to a .txt command file, or a JS expression/template'
|
|
29359
29590
|
})
|
|
29360
29591
|
// deprecated
|
|
29361
29592
|
.option('commands', {alias_to: 'expression'})
|
|
@@ -29527,6 +29758,14 @@ ${svg}
|
|
|
29527
29758
|
}
|
|
29528
29759
|
});
|
|
29529
29760
|
|
|
29761
|
+
parser.command('defaults')
|
|
29762
|
+
.describe('set {{VAR}} interpolation variables only if not already set')
|
|
29763
|
+
.option('values', {
|
|
29764
|
+
DEFAULT: {
|
|
29765
|
+
multi_arg: true
|
|
29766
|
+
}
|
|
29767
|
+
});
|
|
29768
|
+
|
|
29530
29769
|
parser.command('encodings')
|
|
29531
29770
|
.describe('print list of supported text encodings (for .dbf import)');
|
|
29532
29771
|
|
|
@@ -29571,6 +29810,14 @@ ${svg}
|
|
|
29571
29810
|
parser.command('quiet')
|
|
29572
29811
|
.describe('inhibit console messages');
|
|
29573
29812
|
|
|
29813
|
+
parser.command('vars')
|
|
29814
|
+
.describe('define variables for {{VAR}} interpolation (overwrites)')
|
|
29815
|
+
.option('values', {
|
|
29816
|
+
DEFAULT: {
|
|
29817
|
+
multi_arg: true
|
|
29818
|
+
}
|
|
29819
|
+
});
|
|
29820
|
+
|
|
29574
29821
|
parser.command('verbose')
|
|
29575
29822
|
.describe('print verbose processing messages');
|
|
29576
29823
|
|
|
@@ -41741,16 +41988,207 @@ ${svg}
|
|
|
41741
41988
|
compiled(null, defs);
|
|
41742
41989
|
};
|
|
41743
41990
|
|
|
41744
|
-
//
|
|
41745
|
-
|
|
41746
|
-
|
|
41747
|
-
|
|
41748
|
-
|
|
41749
|
-
|
|
41750
|
-
|
|
41991
|
+
// Variable name pattern. Matches simple identifiers: must start with a letter
|
|
41992
|
+
// or underscore, followed by letters, digits or underscores.
|
|
41993
|
+
var VAR_NAME_RXP = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
41994
|
+
|
|
41995
|
+
// Pattern that matches a {{...}} placeholder in command text. The optional
|
|
41996
|
+
// leading character is a backslash (escape) which keeps the placeholder
|
|
41997
|
+
// literal. The braces themselves cannot appear inside a placeholder.
|
|
41998
|
+
//
|
|
41999
|
+
// Group 1: the leading escape (if present)
|
|
42000
|
+
// Group 2: the contents between {{ and }}
|
|
42001
|
+
//
|
|
42002
|
+
var PLACEHOLDER_RXP = /(\\?)\{\{([^{}]+?)\}\}/g;
|
|
42003
|
+
|
|
42004
|
+
// Pattern matching a "KEY=value" inline -vars argument.
|
|
42005
|
+
var ASSIGNMENT_RXP = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
|
|
42006
|
+
|
|
42007
|
+
// Returns true if @s is a valid mapshaper variable name.
|
|
42008
|
+
function isValidVarName(s) {
|
|
42009
|
+
return typeof s == 'string' && VAR_NAME_RXP.test(s);
|
|
42010
|
+
}
|
|
42011
|
+
|
|
42012
|
+
// Returns true if @str might contain a {{...}} placeholder. This is a cheap
|
|
42013
|
+
// fast-path test used to skip re-parsing for commands that have no
|
|
42014
|
+
// placeholders. False positives (e.g. literal "{{" inside a quoted JS
|
|
42015
|
+
// expression) just trigger an interpolation pass that finds nothing to do.
|
|
42016
|
+
function containsPlaceholder(str) {
|
|
42017
|
+
return typeof str == 'string' && str.indexOf('{{') !== -1;
|
|
42018
|
+
}
|
|
42019
|
+
|
|
42020
|
+
// Validate the parsed contents of a -vars JSON file. The file must contain a
|
|
42021
|
+
// flat object whose values are primitive (string, number, boolean, null).
|
|
42022
|
+
// Throws on invalid input. Returns the same object.
|
|
42023
|
+
function validateVarsObject(obj, source) {
|
|
42024
|
+
source = source || '<vars>';
|
|
42025
|
+
if (!utils.isObject(obj) || Array.isArray(obj)) {
|
|
42026
|
+
stop$1('Invalid vars file (' + source + '): expected an object at the top level');
|
|
42027
|
+
}
|
|
42028
|
+
Object.keys(obj).forEach(function(key) {
|
|
42029
|
+
if (!isValidVarName(key)) {
|
|
42030
|
+
stop$1('Invalid var name in ' + source + ': ' + JSON.stringify(key));
|
|
42031
|
+
}
|
|
42032
|
+
var v = obj[key];
|
|
42033
|
+
if (v !== null && typeof v != 'string' && typeof v != 'number' &&
|
|
42034
|
+
typeof v != 'boolean') {
|
|
42035
|
+
stop$1('Invalid value for var "' + key + '" in ' + source +
|
|
42036
|
+
': expected a string, number, boolean or null');
|
|
42037
|
+
}
|
|
42038
|
+
});
|
|
42039
|
+
return obj;
|
|
42040
|
+
}
|
|
42041
|
+
|
|
42042
|
+
// Resolve a single -vars argument. Each argument is either:
|
|
42043
|
+
// * an inline assignment "KEY=value" (key must be a valid var name)
|
|
42044
|
+
// * a path to a JSON file containing a flat object of vars
|
|
42045
|
+
//
|
|
42046
|
+
// @arg: the raw argument string
|
|
42047
|
+
// @cache: optional input cache (passed to cli.readFile so files dropped into
|
|
42048
|
+
// the GUI can be resolved by name)
|
|
42049
|
+
// @merge: target object that receives the resolved entries
|
|
42050
|
+
//
|
|
42051
|
+
function resolveVarsArg(arg, cache, merge) {
|
|
42052
|
+
var assignment = ASSIGNMENT_RXP.exec(arg);
|
|
42053
|
+
if (assignment) {
|
|
42054
|
+
merge[assignment[1]] = assignment[2];
|
|
42055
|
+
return;
|
|
42056
|
+
}
|
|
42057
|
+
// Treat as a JSON file path
|
|
42058
|
+
cli.checkFileExists(arg, cache);
|
|
42059
|
+
var content = cli.readFile(arg, 'utf8', cache);
|
|
42060
|
+
var obj;
|
|
42061
|
+
try {
|
|
42062
|
+
obj = JSON.parse(content);
|
|
42063
|
+
} catch(e) {
|
|
42064
|
+
stop$1('Failed to parse vars file (' + arg + '): ' + e.message);
|
|
42065
|
+
}
|
|
42066
|
+
validateVarsObject(obj, arg);
|
|
42067
|
+
Object.keys(obj).forEach(function(key) {
|
|
42068
|
+
merge[key] = obj[key];
|
|
42069
|
+
});
|
|
42070
|
+
}
|
|
42071
|
+
|
|
42072
|
+
// Resolve an array of -vars arguments into a flat scope object. Later
|
|
42073
|
+
// arguments override earlier ones.
|
|
42074
|
+
function parseVarsArgs(args, cache) {
|
|
42075
|
+
var scope = {};
|
|
42076
|
+
if (!Array.isArray(args)) return scope;
|
|
42077
|
+
args.forEach(function(arg) {
|
|
42078
|
+
resolveVarsArg(arg, cache, scope);
|
|
42079
|
+
});
|
|
42080
|
+
return scope;
|
|
42081
|
+
}
|
|
42082
|
+
|
|
42083
|
+
// Look up env.* variables. Throws in environments without process.env.
|
|
42084
|
+
function lookupEnvVar(name) {
|
|
42085
|
+
if (typeof process == 'undefined' || !process.env) {
|
|
42086
|
+
stop$1('Environment variables are not available in this context');
|
|
42087
|
+
}
|
|
42088
|
+
return process.env[name];
|
|
42089
|
+
}
|
|
42090
|
+
|
|
42091
|
+
// Resolve a single placeholder expression to a string. Recognised forms:
|
|
42092
|
+
// VAR -> defs[VAR]
|
|
42093
|
+
// env.VAR -> process.env[VAR]
|
|
42094
|
+
//
|
|
42095
|
+
// Throws on undefined names, invalid syntax, or non-primitive values.
|
|
42096
|
+
//
|
|
42097
|
+
function resolvePlaceholder(expr, defs) {
|
|
42098
|
+
expr = expr.trim();
|
|
42099
|
+
var envMatch = /^env\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(expr);
|
|
42100
|
+
var val;
|
|
42101
|
+
if (envMatch) {
|
|
42102
|
+
val = lookupEnvVar(envMatch[1]);
|
|
42103
|
+
if (val === undefined || val === null) {
|
|
42104
|
+
stop$1('Undefined environment variable: ' + envMatch[1]);
|
|
42105
|
+
}
|
|
42106
|
+
return String(val);
|
|
42107
|
+
}
|
|
42108
|
+
if (!isValidVarName(expr)) {
|
|
42109
|
+
stop$1('Invalid variable reference: {{' + expr + '}}');
|
|
42110
|
+
}
|
|
42111
|
+
if (!defs || !(expr in defs)) {
|
|
42112
|
+
stop$1('Undefined variable: ' + expr);
|
|
42113
|
+
}
|
|
42114
|
+
val = defs[expr];
|
|
42115
|
+
if (val === null || val === undefined) {
|
|
42116
|
+
stop$1('Undefined variable: ' + expr);
|
|
42117
|
+
}
|
|
42118
|
+
if (typeof val != 'string' && typeof val != 'number' &&
|
|
42119
|
+
typeof val != 'boolean') {
|
|
42120
|
+
stop$1('Variable {{' + expr + '}} is not a primitive value (got ' +
|
|
42121
|
+
(typeof val) + ')');
|
|
42122
|
+
}
|
|
42123
|
+
return String(val);
|
|
42124
|
+
}
|
|
42125
|
+
|
|
42126
|
+
// Substitute {{...}} placeholders in @str using @defs. Placeholders that
|
|
42127
|
+
// are preceded by a backslash are left literal (with the backslash removed).
|
|
42128
|
+
// Substitution is single-pass (no recursion) so that values containing
|
|
42129
|
+
// "{{...}}" do not trigger further interpolation.
|
|
42130
|
+
//
|
|
42131
|
+
function interpolateString(str, defs) {
|
|
42132
|
+
if (typeof str != 'string') return str;
|
|
42133
|
+
return str.replace(PLACEHOLDER_RXP, function(match, escape, expr) {
|
|
42134
|
+
if (escape === '\\') return '{{' + expr + '}}';
|
|
42135
|
+
return resolvePlaceholder(expr, defs);
|
|
42136
|
+
});
|
|
42137
|
+
}
|
|
42138
|
+
|
|
42139
|
+
// -vars KEY=value [KEY=value ...] inline assignments
|
|
42140
|
+
// -vars file.json [more ...] load primitives from a flat JSON object
|
|
42141
|
+
// Mixed forms allowed; later args override earlier ones.
|
|
42142
|
+
//
|
|
42143
|
+
// Writes into job.defs (the same object read by {{X}} interpolation,
|
|
42144
|
+
// -define, -calc and -include).
|
|
42145
|
+
cmd.vars = function(job, opts) {
|
|
42146
|
+
var values = (opts && opts.values) || [];
|
|
42147
|
+
if (!values.length) {
|
|
42148
|
+
stop$1('-vars requires one or more KEY=value or file.json arguments');
|
|
42149
|
+
}
|
|
42150
|
+
var parsed = parseVarsArgs(values, opts && opts.input);
|
|
42151
|
+
if (!job.defs) job.defs = {};
|
|
42152
|
+
Object.keys(parsed).forEach(function(key) {
|
|
42153
|
+
job.defs[key] = parsed[key];
|
|
42154
|
+
});
|
|
42155
|
+
};
|
|
42156
|
+
|
|
42157
|
+
// -defaults KEY=value [KEY=value ...] set-if-unset
|
|
42158
|
+
// Same syntax as -vars, but a key is only assigned if it is not already
|
|
42159
|
+
// present in job.defs. Lets a command file declare overridable defaults
|
|
42160
|
+
// that a CLI -vars can pre-empt.
|
|
42161
|
+
cmd.defaults = function(job, opts) {
|
|
42162
|
+
var values = (opts && opts.values) || [];
|
|
42163
|
+
if (!values.length) {
|
|
42164
|
+
stop$1('-defaults requires one or more KEY=value or file.json arguments');
|
|
42165
|
+
}
|
|
42166
|
+
var parsed = parseVarsArgs(values, opts && opts.input);
|
|
42167
|
+
if (!job.defs) job.defs = {};
|
|
42168
|
+
Object.keys(parsed).forEach(function(key) {
|
|
42169
|
+
if (!(key in job.defs)) {
|
|
42170
|
+
job.defs[key] = parsed[key];
|
|
42171
|
+
}
|
|
41751
42172
|
});
|
|
41752
42173
|
};
|
|
41753
42174
|
|
|
42175
|
+
// -dissolve2 is now an alias for -dissolve. The repair-on-by-default behavior
|
|
42176
|
+
// of -dissolve2 has been promoted to be the default behavior of -dissolve;
|
|
42177
|
+
// the legacy fast-dissolve algorithm is available via -dissolve no-repair.
|
|
42178
|
+
//
|
|
42179
|
+
// This alias prints a deprecation notice and forwards to cmd.dissolve.
|
|
42180
|
+
//
|
|
42181
|
+
var deprecationWarned = false;
|
|
42182
|
+
|
|
42183
|
+
cmd.dissolve2 = function(layers, dataset, opts) {
|
|
42184
|
+
if (!deprecationWarned && !(opts && opts.quiet)) {
|
|
42185
|
+
message('This command has been merged into -dissolve and is deprecated. ' +
|
|
42186
|
+
'Use -dissolve (or -dissolve no-repair for the legacy fast algorithm).');
|
|
42187
|
+
deprecationWarned = true;
|
|
42188
|
+
}
|
|
42189
|
+
return cmd.dissolve(layers, dataset, opts);
|
|
42190
|
+
};
|
|
42191
|
+
|
|
41754
42192
|
// Returns a function for filtering multiple source-table records
|
|
41755
42193
|
// (used by -join command)
|
|
41756
42194
|
function getJoinFilter(data, exp) {
|
|
@@ -48414,8 +48852,138 @@ ${svg}
|
|
|
48414
48852
|
return parsed;
|
|
48415
48853
|
}
|
|
48416
48854
|
|
|
48855
|
+
// Parse the text content of a mapshaper command file (e.g. "commands.txt")
|
|
48856
|
+
// into a single normalized command string suitable for parseCommands().
|
|
48857
|
+
//
|
|
48858
|
+
// Command file syntax (a superset of the equivalent shell command line):
|
|
48859
|
+
// - Optional leading "mapshaper" magic word (used by the file-type sniffer).
|
|
48860
|
+
// - "#" begins a comment that runs to the end of the line. Comments are
|
|
48861
|
+
// ignored unless the "#" appears inside a quoted string.
|
|
48862
|
+
// - Newlines are command separators (unless they fall inside a quoted
|
|
48863
|
+
// string). A trailing backslash on a line is stripped, so shell-style
|
|
48864
|
+
// "\" line continuations are accepted but not required.
|
|
48865
|
+
// - Commands must begin with "-" (e.g. "-i", "-target"). Lines that do
|
|
48866
|
+
// not start with "-" are treated as continuations of the previous
|
|
48867
|
+
// command.
|
|
48868
|
+
// - As on the CLI, an initial command is implied for the first bare token
|
|
48869
|
+
// after the optional "mapshaper" word: a .txt file routes to
|
|
48870
|
+
// "-run <path>" (command file), and any other bare token routes to
|
|
48871
|
+
// "-i <token>" (data file).
|
|
48872
|
+
//
|
|
48873
|
+
// "{{VAR}}" placeholders are substituted at execution time, against the
|
|
48874
|
+
// live job.defs object. See mapshaper-vars-utils.mjs and the late-binding
|
|
48875
|
+
// hook in mapshaper-run-commands.mjs.
|
|
48876
|
+
//
|
|
48877
|
+
function parseCommandFileContent(content) {
|
|
48878
|
+
if (typeof content != 'string') {
|
|
48879
|
+
content = String(content || '');
|
|
48880
|
+
}
|
|
48881
|
+
// Strip BOM if present
|
|
48882
|
+
if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1);
|
|
48883
|
+
var commands = groupCommandFileLines(extractLogicalLines(content));
|
|
48884
|
+
return commands.join(' ');
|
|
48885
|
+
}
|
|
48886
|
+
|
|
48887
|
+
// Walk command file content into logical lines, respecting quoted strings and
|
|
48888
|
+
// stripping "#" comments. Quoted-string contents (including embedded
|
|
48889
|
+
// newlines) are preserved verbatim.
|
|
48890
|
+
function extractLogicalLines(content) {
|
|
48891
|
+
var lines = [];
|
|
48892
|
+
var current = '';
|
|
48893
|
+
var quote = null; // null, "'", or '"'
|
|
48894
|
+
var inComment = false;
|
|
48895
|
+
|
|
48896
|
+
for (var i = 0; i < content.length; i++) {
|
|
48897
|
+
var c = content.charAt(i);
|
|
48898
|
+
if (inComment) {
|
|
48899
|
+
if (c === '\n') {
|
|
48900
|
+
inComment = false;
|
|
48901
|
+
lines.push(current);
|
|
48902
|
+
current = '';
|
|
48903
|
+
}
|
|
48904
|
+
continue;
|
|
48905
|
+
}
|
|
48906
|
+
if (quote) {
|
|
48907
|
+
current += c;
|
|
48908
|
+
if (c === quote) {
|
|
48909
|
+
var bs = 0;
|
|
48910
|
+
for (var j = i - 1; j >= 0 && content.charAt(j) === '\\'; j--) bs++;
|
|
48911
|
+
if (bs % 2 === 0) quote = null;
|
|
48912
|
+
}
|
|
48913
|
+
continue;
|
|
48914
|
+
}
|
|
48915
|
+
if (c === '#') {
|
|
48916
|
+
inComment = true;
|
|
48917
|
+
} else if (c === "'" || c === '"') {
|
|
48918
|
+
quote = c;
|
|
48919
|
+
current += c;
|
|
48920
|
+
} else if (c === '\n') {
|
|
48921
|
+
lines.push(current);
|
|
48922
|
+
current = '';
|
|
48923
|
+
} else {
|
|
48924
|
+
current += c;
|
|
48925
|
+
}
|
|
48926
|
+
}
|
|
48927
|
+
if (current.length > 0) lines.push(current);
|
|
48928
|
+
if (quote) {
|
|
48929
|
+
stop$1('Unterminated quoted string in command file');
|
|
48930
|
+
}
|
|
48931
|
+
return lines;
|
|
48932
|
+
}
|
|
48933
|
+
|
|
48934
|
+
// Group an array of logical lines into command strings:
|
|
48935
|
+
// - Strip trailing-backslash continuations.
|
|
48936
|
+
// - Strip a leading "mapshaper" magic word from the first non-blank line.
|
|
48937
|
+
// - Lines starting with "-" begin a new command.
|
|
48938
|
+
// - Other lines are continuations of the previous command.
|
|
48939
|
+
// - The very first bare token is treated as an implicit -i (data file)
|
|
48940
|
+
// or -run (command file), matching CLI behavior.
|
|
48941
|
+
function groupCommandFileLines(lines) {
|
|
48942
|
+
var commands = [];
|
|
48943
|
+
var cur = '';
|
|
48944
|
+
var sawMagicWord = false;
|
|
48945
|
+
for (var k = 0; k < lines.length; k++) {
|
|
48946
|
+
var line = lines[k];
|
|
48947
|
+
line = line.replace(/\s*\\\s*$/, '').trim();
|
|
48948
|
+
if (!line) continue;
|
|
48949
|
+
|
|
48950
|
+
if (!sawMagicWord && commands.length === 0 && cur === '' &&
|
|
48951
|
+
/^mapshaper(\s|$)/.test(line)) {
|
|
48952
|
+
sawMagicWord = true;
|
|
48953
|
+
line = line.replace(/^mapshaper\s*/, '');
|
|
48954
|
+
if (!line) continue;
|
|
48955
|
+
}
|
|
48956
|
+
|
|
48957
|
+
if (line.charAt(0) === '-') {
|
|
48958
|
+
if (cur) commands.push(cur);
|
|
48959
|
+
cur = line;
|
|
48960
|
+
} else if (!cur && commands.length === 0) {
|
|
48961
|
+
cur = implicitFirstCommand(line);
|
|
48962
|
+
} else {
|
|
48963
|
+
cur += ' ' + line;
|
|
48964
|
+
}
|
|
48965
|
+
}
|
|
48966
|
+
if (cur) commands.push(cur);
|
|
48967
|
+
return commands;
|
|
48968
|
+
}
|
|
48969
|
+
|
|
48970
|
+
// Build the implicit command for a leading bare token:
|
|
48971
|
+
// foo.txt -> -run foo.txt
|
|
48972
|
+
// anything else -> -i <line>
|
|
48973
|
+
// Only the first whitespace-separated token of @line is sniffed for the .txt
|
|
48974
|
+
// extension; any trailing tokens (rare but possible after line joining) are
|
|
48975
|
+
// passed through unchanged.
|
|
48976
|
+
function implicitFirstCommand(line) {
|
|
48977
|
+
var firstTok = line.split(/\s+/)[0];
|
|
48978
|
+
if (isPotentialCommandFile(firstTok)) {
|
|
48979
|
+
return '-run ' + line;
|
|
48980
|
+
}
|
|
48981
|
+
return '-i ' + line;
|
|
48982
|
+
}
|
|
48983
|
+
|
|
48417
48984
|
var ParseCommands = /*#__PURE__*/Object.freeze({
|
|
48418
48985
|
__proto__: null,
|
|
48986
|
+
parseCommandFileContent: parseCommandFileContent,
|
|
48419
48987
|
parseCommands: parseCommands,
|
|
48420
48988
|
parseConsoleCommands: parseConsoleCommands,
|
|
48421
48989
|
standardizeConsoleCommands: standardizeConsoleCommands
|
|
@@ -48438,7 +49006,9 @@ ${svg}
|
|
|
48438
49006
|
}
|
|
48439
49007
|
|
|
48440
49008
|
function commandTakesFileInput(name) {
|
|
48441
|
-
return (name == 'i' || name == 'join' || name == 'erase' || name == 'clip' ||
|
|
49009
|
+
return (name == 'i' || name == 'join' || name == 'erase' || name == 'clip' ||
|
|
49010
|
+
name == 'include' || name == 'vars' || name == 'defaults' ||
|
|
49011
|
+
name == 'run');
|
|
48442
49012
|
}
|
|
48443
49013
|
|
|
48444
49014
|
// TODO: implement these and other functions
|
|
@@ -48464,11 +49034,116 @@ ${svg}
|
|
|
48464
49034
|
// flags.mosaic || flags.snap;
|
|
48465
49035
|
// }
|
|
48466
49036
|
|
|
49037
|
+
// Maximum nesting depth for command files that load other command files
|
|
49038
|
+
var MAX_RUN_DEPTH = 10;
|
|
49039
|
+
|
|
49040
|
+
// Returns the text content of a command file, or null if @file does not look
|
|
49041
|
+
// like a mapshaper command file (wrong extension or missing magic word).
|
|
49042
|
+
// On match, the file content is left in the cache so a subsequent reader
|
|
49043
|
+
// can reuse it.
|
|
49044
|
+
function readCommandFile(file, cache) {
|
|
49045
|
+
if (!isPotentialCommandFile(file)) return null;
|
|
49046
|
+
cli.checkFileExists(file, cache);
|
|
49047
|
+
// cli.readFile(... cache) deletes the entry from the cache after reading,
|
|
49048
|
+
// so we put it back in case downstream code expects to find it there.
|
|
49049
|
+
var content = cli.readFile(file, 'utf8', cache);
|
|
49050
|
+
if (!stringLooksLikeCommandFile(content)) {
|
|
49051
|
+
if (cache) cache[file] = content;
|
|
49052
|
+
return null;
|
|
49053
|
+
}
|
|
49054
|
+
if (cache) cache[file] = content;
|
|
49055
|
+
return content;
|
|
49056
|
+
}
|
|
49057
|
+
|
|
49058
|
+
// Parse and execute the commands in a mapshaper command file within the
|
|
49059
|
+
// given job. Invoked by the -run command when its argument is a .txt file.
|
|
49060
|
+
//
|
|
49061
|
+
// @file: command file path
|
|
49062
|
+
// @content: command file content (string)
|
|
49063
|
+
// @job: parent Job object (commands run in this job)
|
|
49064
|
+
// @opts: options object from the parent -run command (used for input cache and
|
|
49065
|
+
// recursion-depth tracking)
|
|
49066
|
+
//
|
|
49067
|
+
async function runCommandFile(file, content, job, opts) {
|
|
49068
|
+
var depth = (opts && opts._run_depth || 0) + 1;
|
|
49069
|
+
if (depth > MAX_RUN_DEPTH) {
|
|
49070
|
+
stop$1('Command file nesting limit exceeded (' + MAX_RUN_DEPTH + ') at: ' + file);
|
|
49071
|
+
}
|
|
49072
|
+
|
|
49073
|
+
var cache = opts && opts.input || null;
|
|
49074
|
+
var commandStr;
|
|
49075
|
+
try {
|
|
49076
|
+
commandStr = parseCommandFileContent(content);
|
|
49077
|
+
} catch(e) {
|
|
49078
|
+
e.message = 'Error in command file ' + file + ': ' + e.message;
|
|
49079
|
+
throw e;
|
|
49080
|
+
}
|
|
49081
|
+
|
|
49082
|
+
if (!commandStr) {
|
|
49083
|
+
message('Command file contains no commands:', file);
|
|
49084
|
+
return;
|
|
49085
|
+
}
|
|
49086
|
+
|
|
49087
|
+
verbose('Running command file:', file);
|
|
49088
|
+
|
|
49089
|
+
var commands;
|
|
49090
|
+
try {
|
|
49091
|
+
commands = parseCommands(commandStr);
|
|
49092
|
+
} catch(e) {
|
|
49093
|
+
e.message = 'Error in command file ' + file + ': ' + e.message;
|
|
49094
|
+
throw e;
|
|
49095
|
+
}
|
|
49096
|
+
|
|
49097
|
+
// Forward the input cache, output array and depth tracker to nested
|
|
49098
|
+
// commands. This lets a command file's -i find sibling files in the same
|
|
49099
|
+
// cache, lets nested -o commands write to the same output collector (e.g.
|
|
49100
|
+
// when running under applyCommands), and lets nested -run commands respect
|
|
49101
|
+
// the recursion limit.
|
|
49102
|
+
var outputArr = opts && opts.output || null;
|
|
49103
|
+
commands.forEach(function(c) {
|
|
49104
|
+
if (commandTakesFileInput(c.name) && cache) {
|
|
49105
|
+
c.options.input = cache;
|
|
49106
|
+
}
|
|
49107
|
+
if (outputArr && (c.name == 'o' || c.name == 'i' || c.name == 'run' ||
|
|
49108
|
+
c.name == 'info' && c.options.save_to)) {
|
|
49109
|
+
c.options.output = outputArr;
|
|
49110
|
+
}
|
|
49111
|
+
if (c.name == 'run') {
|
|
49112
|
+
c.options._run_depth = depth;
|
|
49113
|
+
}
|
|
49114
|
+
});
|
|
49115
|
+
|
|
49116
|
+
await utils.promisify(runParsedCommands)(commands, job);
|
|
49117
|
+
}
|
|
49118
|
+
|
|
48467
49119
|
cmd.run = async function(job, targets, opts) {
|
|
48468
|
-
var
|
|
48469
|
-
if (!
|
|
48470
|
-
stop$1(
|
|
49120
|
+
var arg = opts.expression;
|
|
49121
|
+
if (!arg) {
|
|
49122
|
+
stop$1('-run requires a command file path or a JS expression');
|
|
49123
|
+
}
|
|
49124
|
+
// Auto-detect a leading argument that looks like a command file (.txt).
|
|
49125
|
+
// If detection succeeds, treat as a command file; otherwise the argument
|
|
49126
|
+
// is a JS expression (the original -run behavior).
|
|
49127
|
+
if (isPotentialCommandFile(arg)) {
|
|
49128
|
+
if (opts.target) {
|
|
49129
|
+
stop$1('-run does not accept a target= option for command files');
|
|
49130
|
+
}
|
|
49131
|
+
await runFromFile(job, arg, opts);
|
|
49132
|
+
} else {
|
|
49133
|
+
await runFromExpression(job, targets, opts);
|
|
49134
|
+
}
|
|
49135
|
+
};
|
|
49136
|
+
|
|
49137
|
+
async function runFromFile(job, file, opts) {
|
|
49138
|
+
var content = readCommandFile(file, opts.input);
|
|
49139
|
+
if (content === null) {
|
|
49140
|
+
stop$1('Not a mapshaper command file (missing "mapshaper" magic word):', file);
|
|
48471
49141
|
}
|
|
49142
|
+
await runCommandFile(file, content, job, opts);
|
|
49143
|
+
}
|
|
49144
|
+
|
|
49145
|
+
async function runFromExpression(job, targets, opts) {
|
|
49146
|
+
var tmp, commands, ctx;
|
|
48472
49147
|
ctx = getBaseContext();
|
|
48473
49148
|
// io proxy adds ability to add datasets dynamically in a required function
|
|
48474
49149
|
ctx.io = getIOProxy();
|
|
@@ -48482,15 +49157,23 @@ ${svg}
|
|
|
48482
49157
|
commands = parseCommands(tmp);
|
|
48483
49158
|
|
|
48484
49159
|
// TODO: remove duplication with mapshaper-run-commands.mjs
|
|
49160
|
+
var outputArr = opts && opts.output || null;
|
|
48485
49161
|
commands.forEach(function(cmd) {
|
|
48486
49162
|
if (commandTakesFileInput(cmd.name)) {
|
|
48487
49163
|
cmd.options.input = ctx.io._cache;
|
|
48488
49164
|
}
|
|
49165
|
+
// Forward the output collector to commands that produce output, so a
|
|
49166
|
+
// generated -o (or info save_to=, or nested -run) writes into the same
|
|
49167
|
+
// output object (e.g. when running under applyCommands).
|
|
49168
|
+
if (outputArr && (cmd.name == 'o' || cmd.name == 'run' ||
|
|
49169
|
+
cmd.name == 'info' && cmd.options.save_to)) {
|
|
49170
|
+
cmd.options.output = outputArr;
|
|
49171
|
+
}
|
|
48489
49172
|
});
|
|
48490
49173
|
|
|
48491
49174
|
await utils.promisify(runParsedCommands)(commands, job);
|
|
48492
49175
|
}
|
|
48493
|
-
}
|
|
49176
|
+
}
|
|
48494
49177
|
|
|
48495
49178
|
cmd.shape = function(targetDataset, opts) {
|
|
48496
49179
|
var geojson, dataset;
|
|
@@ -50685,7 +51368,7 @@ ${svg}
|
|
|
50685
51368
|
name == 'require' || name == 'run' || name == 'define' ||
|
|
50686
51369
|
name == 'include' || name == 'print' || name == 'comment' || name == 'if' || name == 'elif' ||
|
|
50687
51370
|
name == 'else' || name == 'endif' || name == 'stop' || name == 'add-shape' ||
|
|
50688
|
-
name == 'scalebar';
|
|
51371
|
+
name == 'scalebar' || name == 'vars' || name == 'defaults';
|
|
50689
51372
|
}
|
|
50690
51373
|
|
|
50691
51374
|
async function runCommand(command, job) {
|
|
@@ -50820,8 +51503,14 @@ ${svg}
|
|
|
50820
51503
|
} else if (name == 'define') {
|
|
50821
51504
|
cmd.define(job.catalog, opts);
|
|
50822
51505
|
|
|
51506
|
+
} else if (name == 'vars') {
|
|
51507
|
+
cmd.vars(job, opts);
|
|
51508
|
+
|
|
51509
|
+
} else if (name == 'defaults') {
|
|
51510
|
+
cmd.defaults(job, opts);
|
|
51511
|
+
|
|
50823
51512
|
} else if (name == 'dissolve') {
|
|
50824
|
-
outputLayers =
|
|
51513
|
+
outputLayers = cmd.dissolve(targetLayers, targetDataset, opts);
|
|
50825
51514
|
|
|
50826
51515
|
} else if (name == 'dissolve2') {
|
|
50827
51516
|
outputLayers = cmd.dissolve2(targetLayers, targetDataset, opts);
|
|
@@ -51139,7 +51828,7 @@ ${svg}
|
|
|
51139
51828
|
});
|
|
51140
51829
|
}
|
|
51141
51830
|
|
|
51142
|
-
var version = "0.
|
|
51831
|
+
var version = "0.7.0";
|
|
51143
51832
|
|
|
51144
51833
|
// Parse command line args into commands and run them
|
|
51145
51834
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|
|
@@ -51276,6 +51965,11 @@ ${svg}
|
|
|
51276
51965
|
if (outputArr && (cmd.name == 'o' || cmd.name == 'info' && cmd.options.save_to)) {
|
|
51277
51966
|
cmd.options.output = outputArr;
|
|
51278
51967
|
}
|
|
51968
|
+
// -run may load and execute a command file; propagate the output array
|
|
51969
|
+
// so that -o commands nested inside it can write to it too.
|
|
51970
|
+
if (outputArr && cmd.name == 'run') {
|
|
51971
|
+
cmd.options.output = outputArr;
|
|
51972
|
+
}
|
|
51279
51973
|
});
|
|
51280
51974
|
|
|
51281
51975
|
var lastCmd = commands[commands.length - 1];
|
|
@@ -51368,7 +52062,13 @@ ${svg}
|
|
|
51368
52062
|
utils.reduceAsync(commands, job, nextCommand, done);
|
|
51369
52063
|
|
|
51370
52064
|
function nextCommand(job, cmd, next) {
|
|
51371
|
-
|
|
52065
|
+
var resolved;
|
|
52066
|
+
try {
|
|
52067
|
+
resolved = maybeInterpolateCommand(cmd, job);
|
|
52068
|
+
} catch(e) {
|
|
52069
|
+
return next(e);
|
|
52070
|
+
}
|
|
52071
|
+
runCommand(resolved, job).then(function(result) {
|
|
51372
52072
|
next(null, result);
|
|
51373
52073
|
}).catch(function(e) {
|
|
51374
52074
|
next(e);
|
|
@@ -51376,6 +52076,60 @@ ${svg}
|
|
|
51376
52076
|
}
|
|
51377
52077
|
}
|
|
51378
52078
|
|
|
52079
|
+
// Late-binding interpolation: just before each command runs, replace any
|
|
52080
|
+
// {{X}} placeholders in its source tokens against the live job.defs object,
|
|
52081
|
+
// then re-parse to get fresh option values.
|
|
52082
|
+
//
|
|
52083
|
+
// Returns either the original cmd (no placeholders, no _tokens, or the
|
|
52084
|
+
// command will be skipped) or a fresh cmd object with re-parsed options.
|
|
52085
|
+
// The original cmd is never mutated, so commands shared across batches
|
|
52086
|
+
// (see divideImportCommand) still see their un-interpolated tokens.
|
|
52087
|
+
function maybeInterpolateCommand(cmd, job) {
|
|
52088
|
+
var tokens = cmd._tokens;
|
|
52089
|
+
if (!tokens || tokens.length === 0) return cmd;
|
|
52090
|
+
if (!tokens.some(containsPlaceholder)) return cmd;
|
|
52091
|
+
// If the command would be skipped (inactive -if branch, stopped job, etc.),
|
|
52092
|
+
// don't try to interpolate -- the command body never runs and unset
|
|
52093
|
+
// variables shouldn't error here.
|
|
52094
|
+
if (skipCommand(cmd.name, job)) return cmd;
|
|
52095
|
+
|
|
52096
|
+
var defs = job.defs || {};
|
|
52097
|
+
var interpolated;
|
|
52098
|
+
try {
|
|
52099
|
+
interpolated = tokens.map(function(tok) {
|
|
52100
|
+
return interpolateString(tok, defs);
|
|
52101
|
+
});
|
|
52102
|
+
} catch(e) {
|
|
52103
|
+
e.message = '[' + cmd.name + '] ' + e.message;
|
|
52104
|
+
throw e;
|
|
52105
|
+
}
|
|
52106
|
+
|
|
52107
|
+
var reparsed;
|
|
52108
|
+
try {
|
|
52109
|
+
reparsed = parseCommands(interpolated);
|
|
52110
|
+
} catch(e) {
|
|
52111
|
+
e.message = '[' + cmd.name + '] ' + e.message;
|
|
52112
|
+
throw e;
|
|
52113
|
+
}
|
|
52114
|
+
if (reparsed.length !== 1) {
|
|
52115
|
+
stop$1('[' + cmd.name + '] Internal error: token re-parse produced ' +
|
|
52116
|
+
reparsed.length + ' commands');
|
|
52117
|
+
}
|
|
52118
|
+
var newCmd = reparsed[0];
|
|
52119
|
+
// Preserve externally-injected options (input cache, output array,
|
|
52120
|
+
// _run_depth, final flag, replace flag, etc.) that aren't reproduced
|
|
52121
|
+
// by re-parsing the source tokens.
|
|
52122
|
+
Object.keys(cmd.options).forEach(function(k) {
|
|
52123
|
+
if (!(k in newCmd.options)) {
|
|
52124
|
+
newCmd.options[k] = cmd.options[k];
|
|
52125
|
+
}
|
|
52126
|
+
});
|
|
52127
|
+
// Keep the original tokens around so re-runs (e.g. divided import batches)
|
|
52128
|
+
// see the un-interpolated source.
|
|
52129
|
+
newCmd._tokens = tokens;
|
|
52130
|
+
return newCmd;
|
|
52131
|
+
}
|
|
52132
|
+
|
|
51379
52133
|
function handleNonFatalError(err) {
|
|
51380
52134
|
if (err && err.name == 'NonFatalError') {
|
|
51381
52135
|
printError(err);
|
|
@@ -51399,6 +52153,18 @@ ${svg}
|
|
|
51399
52153
|
return [commands];
|
|
51400
52154
|
}
|
|
51401
52155
|
|
|
52156
|
+
// Multiple files trigger batch mode by default. This is a long-standing
|
|
52157
|
+
// wart: most multi-file CLI tools combine inputs by default, and silently
|
|
52158
|
+
// splitting into per-file pipelines is an easy way for users to get wrong
|
|
52159
|
+
// output without noticing. Print a one-time deprecation warning when batch
|
|
52160
|
+
// mode is implicit so existing scripts can migrate before the default flips
|
|
52161
|
+
// in a future major release.
|
|
52162
|
+
if (!opts.batch_mode) {
|
|
52163
|
+
message('Note: implicit batch processing is deprecated. Add `batch-mode` ' +
|
|
52164
|
+
'to keep this behavior, or `combine-files` to import the files as a ' +
|
|
52165
|
+
'group of layers. The default will change in a future release.');
|
|
52166
|
+
}
|
|
52167
|
+
|
|
51402
52168
|
return opts.files.map(function(file) {
|
|
51403
52169
|
var group = [{
|
|
51404
52170
|
name: 'i',
|