mapshaper 0.6.120 → 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 +1013 -239
- package/package.json +1 -1
- package/www/CNAME +1 -0
- package/www/index.html +1 -1
- package/www/mapshaper-gui.js +108 -9
- package/www/mapshaper.js +1013 -239
- package/www/privacy.html +203 -0
- package/www/sponsor.html +8 -2
- package/www/terms.html +188 -0
package/www/mapshaper.js
CHANGED
|
@@ -11723,7 +11723,11 @@
|
|
|
11723
11723
|
version: 1,
|
|
11724
11724
|
created: 'YYYY-MM-DDTHH:mm:ss.sssZ', // ISO string
|
|
11725
11725
|
datasets: [],
|
|
11726
|
-
gui: {} // see gui-session-snapshot-control.mjs
|
|
11726
|
+
gui: {}, // see gui-session-snapshot-control.mjs
|
|
11727
|
+
history: { // optional; only present in snapshots created by the GUI
|
|
11728
|
+
commands: ['-i foo.shp', '-simplify 10%', ...],
|
|
11729
|
+
savedAtIndex: 0 // index of the first command after the last save boundary
|
|
11730
|
+
}
|
|
11727
11731
|
}
|
|
11728
11732
|
*/
|
|
11729
11733
|
|
|
@@ -11747,12 +11751,16 @@
|
|
|
11747
11751
|
// exporting from command line: { compact: true, file: 'tmp.msx', final: true }
|
|
11748
11752
|
// exporting from gui export menu: {compact: true, format: 'msx'}
|
|
11749
11753
|
// saving gui temp snapshot: {compact: false}
|
|
11754
|
+
// opts.history: optional GUI session history captured by SessionHistory#getHistorySnapshot
|
|
11750
11755
|
async function exportDatasetsToPack(datasets, opts) {
|
|
11751
11756
|
var obj = {
|
|
11752
11757
|
version: 1,
|
|
11753
11758
|
created: (new Date).toISOString(),
|
|
11754
11759
|
datasets: await Promise.all(datasets.map(dataset => exportDataset(dataset, opts)))
|
|
11755
11760
|
};
|
|
11761
|
+
if (opts.history) {
|
|
11762
|
+
obj.history = opts.history;
|
|
11763
|
+
}
|
|
11756
11764
|
return obj;
|
|
11757
11765
|
}
|
|
11758
11766
|
|
|
@@ -11952,6 +11960,29 @@
|
|
|
11952
11960
|
return file.endsWith('.' + PACKAGE_EXT);
|
|
11953
11961
|
}
|
|
11954
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
|
+
|
|
11955
11986
|
function isZipFile(file) {
|
|
11956
11987
|
return /\.zip$/i.test(file);
|
|
11957
11988
|
}
|
|
@@ -11996,10 +12027,12 @@
|
|
|
11996
12027
|
isImportableAsBinary: isImportableAsBinary,
|
|
11997
12028
|
isKmzFile: isKmzFile,
|
|
11998
12029
|
isPackageFile: isPackageFile,
|
|
12030
|
+
isPotentialCommandFile: isPotentialCommandFile,
|
|
11999
12031
|
isSupportedBinaryInputType: isSupportedBinaryInputType,
|
|
12000
12032
|
isZipFile: isZipFile,
|
|
12001
12033
|
looksLikeContentFile: looksLikeContentFile,
|
|
12002
12034
|
looksLikeImportableFile: looksLikeImportableFile,
|
|
12035
|
+
stringLooksLikeCommandFile: stringLooksLikeCommandFile,
|
|
12003
12036
|
stringLooksLikeJSON: stringLooksLikeJSON,
|
|
12004
12037
|
stringLooksLikeKML: stringLooksLikeKML,
|
|
12005
12038
|
stringLooksLikeSVG: stringLooksLikeSVG
|
|
@@ -13797,12 +13830,17 @@
|
|
|
13797
13830
|
profileStart('intersectSegments');
|
|
13798
13831
|
var raw = arcs.getVertexData(),
|
|
13799
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,
|
|
13800
13837
|
arr;
|
|
13801
13838
|
for (i=0; i<stripeCount; i++) {
|
|
13802
13839
|
arr = intersectSegments(stripes[i], raw.xx, raw.yy, opts);
|
|
13803
13840
|
for (j=0; j<arr.length; j++) {
|
|
13804
13841
|
intersections.push(arr[j]);
|
|
13805
13842
|
}
|
|
13843
|
+
if (limit > 0 && intersections.length >= limit) break;
|
|
13806
13844
|
}
|
|
13807
13845
|
profileEnd('intersectSegments');
|
|
13808
13846
|
profileStart('dedupIntersections');
|
|
@@ -16098,7 +16136,7 @@
|
|
|
16098
16136
|
|
|
16099
16137
|
// Get a copy of a layer containing a subset of the layer's features,
|
|
16100
16138
|
// given a "where" expression in the options object
|
|
16101
|
-
function getLayerSelection(lyr, arcs, opts) {
|
|
16139
|
+
function getLayerSelection$1(lyr, arcs, opts) {
|
|
16102
16140
|
var lyr2 = utils.extend({}, lyr);
|
|
16103
16141
|
var filterOpts = {
|
|
16104
16142
|
expression: opts.where,
|
|
@@ -16115,11 +16153,11 @@
|
|
|
16115
16153
|
if (!opts || !opts.where) {
|
|
16116
16154
|
error('Missing required "where" parameter');
|
|
16117
16155
|
}
|
|
16118
|
-
var subsetLyr = getLayerSelection(lyr, arcs, opts);
|
|
16156
|
+
var subsetLyr = getLayerSelection$1(lyr, arcs, opts);
|
|
16119
16157
|
var cmdOpts = utils.defaults({where: null}, opts); // prevent infinite recursion
|
|
16120
16158
|
var outputLyr = commandFunc(subsetLyr, arcs, cmdOpts);
|
|
16121
16159
|
var filterOpts = utils.defaults({invert: true}, opts);
|
|
16122
|
-
var filteredLyr = getLayerSelection(lyr, arcs, filterOpts);
|
|
16160
|
+
var filteredLyr = getLayerSelection$1(lyr, arcs, filterOpts);
|
|
16123
16161
|
var merged = cmd.mergeLayers([filteredLyr, outputLyr], {verbose: false, force: true});
|
|
16124
16162
|
return merged[0];
|
|
16125
16163
|
}
|
|
@@ -16148,7 +16186,7 @@
|
|
|
16148
16186
|
result, compiled, defs, d;
|
|
16149
16187
|
if (opts.where) {
|
|
16150
16188
|
// TODO: implement no_replace option for filter() instead of this
|
|
16151
|
-
lyr = getLayerSelection(lyr, arcs, opts);
|
|
16189
|
+
lyr = getLayerSelection$1(lyr, arcs, opts);
|
|
16152
16190
|
msg += ' where ' + opts.where;
|
|
16153
16191
|
}
|
|
16154
16192
|
// Save any assigned variables to the defs object, so they will be available
|
|
@@ -16870,208 +16908,6 @@
|
|
|
16870
16908
|
dissolvePolygonGeometry: dissolvePolygonGeometry
|
|
16871
16909
|
});
|
|
16872
16910
|
|
|
16873
|
-
// Generate a dissolved layer
|
|
16874
|
-
// @opts.fields (optional) names of data fields (dissolves all if falsy)
|
|
16875
|
-
// @opts.sum-fields (Array) (optional)
|
|
16876
|
-
// @opts.copy-fields (Array) (optional)
|
|
16877
|
-
//
|
|
16878
|
-
cmd.dissolve = function(lyr, arcs, opts) {
|
|
16879
|
-
var dissolveShapes, getGroupId;
|
|
16880
|
-
opts = utils.extend({}, opts);
|
|
16881
|
-
if (opts.where) {
|
|
16882
|
-
return applyCommandToLayerSelection(cmd.dissolve, lyr, arcs, opts);
|
|
16883
|
-
}
|
|
16884
|
-
if (opts.field) opts.fields = [opts.field]; // support old "field" parameter
|
|
16885
|
-
getGroupId = getCategoryClassifier(opts.fields, lyr.data);
|
|
16886
|
-
if (opts.multipart || opts.group_points) {
|
|
16887
|
-
dissolveShapes = makeMultipartShapes(lyr, getGroupId);
|
|
16888
|
-
} else if (lyr.geometry_type == 'polygon') {
|
|
16889
|
-
dissolveShapes = dissolvePolygonGeometry(lyr.shapes, getGroupId);
|
|
16890
|
-
} else if (lyr.geometry_type == 'polyline') {
|
|
16891
|
-
dissolveShapes = dissolvePolylineGeometry(lyr, getGroupId, arcs);
|
|
16892
|
-
} else if (lyr.geometry_type == 'point') {
|
|
16893
|
-
dissolveShapes = dissolvePointGeometry(lyr, getGroupId, opts);
|
|
16894
|
-
}
|
|
16895
|
-
return composeDissolveLayer(lyr, dissolveShapes, getGroupId, opts);
|
|
16896
|
-
};
|
|
16897
|
-
|
|
16898
|
-
function makeMultipartShapes(lyr, getGroupId) {
|
|
16899
|
-
if (!lyr.shapes || !lyr.geometry_type) {
|
|
16900
|
-
stop$1('Layer is missing geometry');
|
|
16901
|
-
}
|
|
16902
|
-
cloneShapes(lyr.shapes);
|
|
16903
|
-
var shapes2 = [];
|
|
16904
|
-
lyr.shapes.forEach(function(shp, i) {
|
|
16905
|
-
var groupId = getGroupId(i);
|
|
16906
|
-
if (!shp) return;
|
|
16907
|
-
if (!shapes2[groupId]) {
|
|
16908
|
-
shapes2[groupId] = shp;
|
|
16909
|
-
} else {
|
|
16910
|
-
shapes2[groupId].push.apply(shapes2[groupId], shp);
|
|
16911
|
-
}
|
|
16912
|
-
});
|
|
16913
|
-
return shapes2;
|
|
16914
|
-
}
|
|
16915
|
-
|
|
16916
|
-
// @lyr: original undissolved layer
|
|
16917
|
-
// @shapes: dissolved shapes
|
|
16918
|
-
function composeDissolveLayer(lyr, shapes, getGroupId, opts) {
|
|
16919
|
-
var records = null;
|
|
16920
|
-
var lyr2;
|
|
16921
|
-
if (lyr.data) {
|
|
16922
|
-
records = aggregateDataRecords(lyr.data.getRecords(), getGroupId, opts);
|
|
16923
|
-
// replace missing shapes with nulls
|
|
16924
|
-
for (var i=0, n=records.length; i<n; i++) {
|
|
16925
|
-
if (shapes && !shapes[i]) {
|
|
16926
|
-
shapes[i] = null;
|
|
16927
|
-
}
|
|
16928
|
-
}
|
|
16929
|
-
}
|
|
16930
|
-
lyr2 = {
|
|
16931
|
-
name: opts.no_replace ? null : lyr.name,
|
|
16932
|
-
shapes: shapes,
|
|
16933
|
-
data: records ? new DataTable(records) : null,
|
|
16934
|
-
geometry_type: lyr.geometry_type
|
|
16935
|
-
};
|
|
16936
|
-
if (!opts.silent) {
|
|
16937
|
-
printDissolveMessage(lyr, lyr2);
|
|
16938
|
-
}
|
|
16939
|
-
return lyr2;
|
|
16940
|
-
}
|
|
16941
|
-
|
|
16942
|
-
function printDissolveMessage(pre, post) {
|
|
16943
|
-
var n1 = getFeatureCount(pre),
|
|
16944
|
-
n2 = getFeatureCount(post),
|
|
16945
|
-
msg = utils.format('Dissolved %,d feature%s into %,d feature%s',
|
|
16946
|
-
n1, utils.pluralSuffix(n1), n2,
|
|
16947
|
-
utils.pluralSuffix(n2));
|
|
16948
|
-
message(msg);
|
|
16949
|
-
}
|
|
16950
|
-
|
|
16951
|
-
// Maps tile ids to shape ids (both are non-negative integers). Supports
|
|
16952
|
-
// one-to-many mapping (a tile may belong to multiple shapes)
|
|
16953
|
-
// Also maps shape ids to tile ids. A shape may contain multiple tiles
|
|
16954
|
-
// Also supports 'flattening' -- removing one-to-many tile-shape mappings by
|
|
16955
|
-
// removing all but one shape from a tile.
|
|
16956
|
-
// Supports one-to-many mapping
|
|
16957
|
-
function TileShapeIndex(mosaic, opts) {
|
|
16958
|
-
// indexes for mapping tile ids to shape ids
|
|
16959
|
-
var singleIndex = new Int32Array(mosaic.length);
|
|
16960
|
-
utils.initializeArray(singleIndex, -1);
|
|
16961
|
-
var multipleIndex = [];
|
|
16962
|
-
// index that maps shape ids to tile ids
|
|
16963
|
-
var shapeIndex = [];
|
|
16964
|
-
|
|
16965
|
-
this.getTileIdsByShapeId = function(shapeId) {
|
|
16966
|
-
var ids = shapeIndex[shapeId];
|
|
16967
|
-
// need to filter out tile ids that have been set to -1 (indicating removal)
|
|
16968
|
-
return ids ? ids.filter(function(id) {return id >= 0;}) : [];
|
|
16969
|
-
};
|
|
16970
|
-
|
|
16971
|
-
// assumes index has been flattened
|
|
16972
|
-
this.getShapeIdByTileId = function(id) {
|
|
16973
|
-
var shapeId = singleIndex[id];
|
|
16974
|
-
return shapeId >= 0 ? shapeId : -1;
|
|
16975
|
-
};
|
|
16976
|
-
|
|
16977
|
-
// return ids of all shapes that include a tile
|
|
16978
|
-
this.getShapeIdsByTileId = function(id) {
|
|
16979
|
-
var singleId = singleIndex[id];
|
|
16980
|
-
if (singleId >= 0) {
|
|
16981
|
-
return [singleId];
|
|
16982
|
-
}
|
|
16983
|
-
if (singleId == -1) {
|
|
16984
|
-
return [];
|
|
16985
|
-
}
|
|
16986
|
-
return multipleIndex[id];
|
|
16987
|
-
};
|
|
16988
|
-
|
|
16989
|
-
this.indexTileIdsByShapeId = function(shapeId, tileIds, weightFunction) {
|
|
16990
|
-
shapeIndex[shapeId] = [];
|
|
16991
|
-
for (var i=0; i<tileIds.length; i++) {
|
|
16992
|
-
indexShapeIdByTileId(shapeId, tileIds[i], weightFunction);
|
|
16993
|
-
}
|
|
16994
|
-
};
|
|
16995
|
-
|
|
16996
|
-
// remove many-to-one tile=>shape mappings
|
|
16997
|
-
this.flatten = function() {
|
|
16998
|
-
multipleIndex.forEach(function(shapeIds, tileId) {
|
|
16999
|
-
flattenStackedTile(tileId);
|
|
17000
|
-
});
|
|
17001
|
-
multipleIndex = [];
|
|
17002
|
-
};
|
|
17003
|
-
|
|
17004
|
-
this.getUnusedTileIds = function() {
|
|
17005
|
-
var ids = [];
|
|
17006
|
-
for (var i=0, n=singleIndex.length; i<n; i++) {
|
|
17007
|
-
if (singleIndex[i] == -1) ids.push(i);
|
|
17008
|
-
}
|
|
17009
|
-
return ids;
|
|
17010
|
-
};
|
|
17011
|
-
|
|
17012
|
-
// used by gap fill; assumes that flatten() has been called
|
|
17013
|
-
this.addTileToShape = function(shapeId, tileId) {
|
|
17014
|
-
if (shapeId in shapeIndex === false || singleIndex[tileId] != -1) {
|
|
17015
|
-
error('Internal error');
|
|
17016
|
-
}
|
|
17017
|
-
singleIndex[tileId] = shapeId;
|
|
17018
|
-
shapeIndex[shapeId].push(tileId);
|
|
17019
|
-
};
|
|
17020
|
-
|
|
17021
|
-
// add a shape id to a tile
|
|
17022
|
-
function indexShapeIdByTileId(shapeId, tileId, weightFunction) {
|
|
17023
|
-
var singleId = singleIndex[tileId];
|
|
17024
|
-
if (singleId != -1 && opts.flat) {
|
|
17025
|
-
// pick the best shape if we have a weight function
|
|
17026
|
-
if (weightFunction && weightFunction(shapeId) > weightFunction(singleId)) {
|
|
17027
|
-
// replace existing shape reference
|
|
17028
|
-
removeTileFromShape(tileId, singleId); // bottleneck when overlaps are many
|
|
17029
|
-
singleIndex[tileId] = singleId;
|
|
17030
|
-
singleId = -1;
|
|
17031
|
-
} else {
|
|
17032
|
-
// keep existing shape reference
|
|
17033
|
-
return;
|
|
17034
|
-
}
|
|
17035
|
-
}
|
|
17036
|
-
if (singleId == -1) {
|
|
17037
|
-
singleIndex[tileId] = shapeId;
|
|
17038
|
-
} else if (singleId == -2) {
|
|
17039
|
-
multipleIndex[tileId].push(shapeId);
|
|
17040
|
-
} else {
|
|
17041
|
-
multipleIndex[tileId] = [singleId, shapeId];
|
|
17042
|
-
singleIndex[tileId] = -2;
|
|
17043
|
-
}
|
|
17044
|
-
shapeIndex[shapeId].push(tileId);
|
|
17045
|
-
}
|
|
17046
|
-
|
|
17047
|
-
|
|
17048
|
-
function flattenStackedTile(tileId) {
|
|
17049
|
-
// TODO: select the best shape (using some metric)
|
|
17050
|
-
var shapeIds = multipleIndex[tileId];
|
|
17051
|
-
// if (!shapeIds || shapeIds.length > 1 === false) error('flattening error');
|
|
17052
|
-
var selectedId = shapeIds[0];
|
|
17053
|
-
var shapeId;
|
|
17054
|
-
singleIndex[tileId] = selectedId; // add shape to single index
|
|
17055
|
-
// remove tile from other stacked shapes
|
|
17056
|
-
for (var i=0; i<shapeIds.length; i++) {
|
|
17057
|
-
shapeId = shapeIds[i];
|
|
17058
|
-
if (shapeId != selectedId) {
|
|
17059
|
-
removeTileFromShape(tileId, shapeId);
|
|
17060
|
-
}
|
|
17061
|
-
}
|
|
17062
|
-
}
|
|
17063
|
-
|
|
17064
|
-
function removeTileFromShape(tileId, shapeId) {
|
|
17065
|
-
var tileIds = shapeIndex[shapeId];
|
|
17066
|
-
for (var i=0; i<tileIds.length; i++) {
|
|
17067
|
-
if (tileIds[i] === tileId) {
|
|
17068
|
-
tileIds[i] = -1;
|
|
17069
|
-
break;
|
|
17070
|
-
}
|
|
17071
|
-
}
|
|
17072
|
-
}
|
|
17073
|
-
}
|
|
17074
|
-
|
|
17075
16911
|
// Clean polygon or polyline shapes (in-place)
|
|
17076
16912
|
//
|
|
17077
16913
|
function cleanShapes(shapes, arcs, type) {
|
|
@@ -17688,6 +17524,336 @@
|
|
|
17688
17524
|
sortCutPoints: sortCutPoints
|
|
17689
17525
|
});
|
|
17690
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
|
+
|
|
17691
17857
|
// Support for timing using T.start() and T.stop()
|
|
17692
17858
|
var T$1 = {
|
|
17693
17859
|
stack: [],
|
|
@@ -26866,26 +27032,71 @@ ${svg}
|
|
|
26866
27032
|
// show help if only a command name is given
|
|
26867
27033
|
argv.unshift('-help'); // kludge (assumes -help <command> syntax)
|
|
26868
27034
|
} else if (argv.length > 0 && !tokenLooksLikeCommand(argv[0]) && _default) {
|
|
26869
|
-
// if there are arguments before the first explicit command, use the default
|
|
26870
|
-
|
|
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
|
+
}
|
|
26871
27043
|
}
|
|
26872
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
|
+
|
|
26873
27050
|
while (argv.length > 0) {
|
|
27051
|
+
var consumedBefore = totalLen - argv.length;
|
|
26874
27052
|
cmdName = readCommandName(argv);
|
|
26875
27053
|
if (!cmdName) {
|
|
26876
27054
|
stop$1("Invalid command:", argv[0]);
|
|
26877
27055
|
}
|
|
26878
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);
|
|
26879
27064
|
if (!cmdDef) {
|
|
26880
27065
|
cmd = parseUnknownCommandOptions(argv, cmdName);
|
|
26881
27066
|
} else {
|
|
26882
|
-
cmd = parseCommandOptions(argv, cmdDef);
|
|
26883
|
-
}
|
|
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
|
+
});
|
|
26884
27079
|
commands.push(cmd);
|
|
26885
27080
|
}
|
|
26886
27081
|
return commands;
|
|
26887
27082
|
|
|
26888
|
-
|
|
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) {
|
|
26889
27100
|
var cmd = {
|
|
26890
27101
|
name: cmdDef.name,
|
|
26891
27102
|
options: {},
|
|
@@ -26897,10 +27108,10 @@ ${svg}
|
|
|
26897
27108
|
}
|
|
26898
27109
|
|
|
26899
27110
|
try {
|
|
26900
|
-
if (cmd._.length > 0) {
|
|
27111
|
+
if (cmd._.length > 0 && !deferValidate) {
|
|
26901
27112
|
readDefaultOptionValue(cmd, cmdDef);
|
|
26902
27113
|
}
|
|
26903
|
-
if (cmdDef.validate) {
|
|
27114
|
+
if (cmdDef.validate && !deferValidate) {
|
|
26904
27115
|
cmdDef.validate(cmd);
|
|
26905
27116
|
}
|
|
26906
27117
|
delete cmd.options._; // kludge to remove -o placeholder option
|
|
@@ -27377,7 +27588,16 @@ ${svg}
|
|
|
27377
27588
|
|
|
27378
27589
|
parser.section('I/O commands');
|
|
27379
27590
|
|
|
27380
|
-
|
|
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
|
+
});
|
|
27381
27601
|
|
|
27382
27602
|
parser.command('i')
|
|
27383
27603
|
.describe('input one or more files')
|
|
@@ -27394,6 +27614,10 @@ ${svg}
|
|
|
27394
27614
|
describe: 'import files to separate layers with shared topology',
|
|
27395
27615
|
type: 'flag'
|
|
27396
27616
|
})
|
|
27617
|
+
.option('batch-mode', {
|
|
27618
|
+
describe: 'apply subsequent commands separately to each input file',
|
|
27619
|
+
type: 'flag'
|
|
27620
|
+
})
|
|
27397
27621
|
.option('merge-files', {
|
|
27398
27622
|
// describe: 'merge features from compatible files into the same layer',
|
|
27399
27623
|
type: 'flag'
|
|
@@ -28025,7 +28249,7 @@ ${svg}
|
|
|
28025
28249
|
});
|
|
28026
28250
|
|
|
28027
28251
|
parser.command('dissolve')
|
|
28028
|
-
.describe('merge features within a layer')
|
|
28252
|
+
.describe('merge features within a layer (repairs polygon topology)')
|
|
28029
28253
|
.example('Dissolve all polygons in a feature layer into a single polygon\n' +
|
|
28030
28254
|
'$ mapshaper states.shp -dissolve -o country.shp')
|
|
28031
28255
|
.example('Generate state-level polygons by dissolving a layer of counties\n' +
|
|
@@ -28052,19 +28276,33 @@ ${svg}
|
|
|
28052
28276
|
type: 'flag',
|
|
28053
28277
|
describe: '[points] use 2D math to find centroids of latlong points'
|
|
28054
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)
|
|
28055
28294
|
.option('name', nameOpt)
|
|
28056
28295
|
.option('target', targetOpt)
|
|
28057
28296
|
.option('no-replace', noReplaceOpt);
|
|
28058
28297
|
|
|
28059
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.
|
|
28060
28302
|
parser.command('dissolve2')
|
|
28061
|
-
.describe('
|
|
28303
|
+
.describe('alias for -dissolve (deprecated)')
|
|
28062
28304
|
.option('field', {}) // old arg handled by dissolve function
|
|
28063
28305
|
.option('fields', dissolveFieldsOpt)
|
|
28064
|
-
// UPDATE: Use -mosaic command for debugging
|
|
28065
|
-
//.option('mosaic', {type: 'flag'}) // debugging option
|
|
28066
|
-
//.option('arcs', {type: 'flag'}) // debugging option
|
|
28067
|
-
//.option('tiles', {type: 'flag'}) // debugging option
|
|
28068
28306
|
.option('calc', calcOpt)
|
|
28069
28307
|
.option('sum-fields', sumFieldsOpt)
|
|
28070
28308
|
.option('copy-fields', copyFieldsOpt)
|
|
@@ -29344,10 +29582,11 @@ ${svg}
|
|
|
29344
29582
|
});
|
|
29345
29583
|
|
|
29346
29584
|
parser.command('run')
|
|
29347
|
-
.describe('
|
|
29585
|
+
.describe('run commands from a command file or JS expression')
|
|
29348
29586
|
.option('expression', {
|
|
29349
29587
|
DEFAULT: true,
|
|
29350
|
-
|
|
29588
|
+
label: '<file|expression>',
|
|
29589
|
+
describe: 'path to a .txt command file, or a JS expression/template'
|
|
29351
29590
|
})
|
|
29352
29591
|
// deprecated
|
|
29353
29592
|
.option('commands', {alias_to: 'expression'})
|
|
@@ -29519,6 +29758,14 @@ ${svg}
|
|
|
29519
29758
|
}
|
|
29520
29759
|
});
|
|
29521
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
|
+
|
|
29522
29769
|
parser.command('encodings')
|
|
29523
29770
|
.describe('print list of supported text encodings (for .dbf import)');
|
|
29524
29771
|
|
|
@@ -29563,6 +29810,14 @@ ${svg}
|
|
|
29563
29810
|
parser.command('quiet')
|
|
29564
29811
|
.describe('inhibit console messages');
|
|
29565
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
|
+
|
|
29566
29821
|
parser.command('verbose')
|
|
29567
29822
|
.describe('print verbose processing messages');
|
|
29568
29823
|
|
|
@@ -41733,16 +41988,207 @@ ${svg}
|
|
|
41733
41988
|
compiled(null, defs);
|
|
41734
41989
|
};
|
|
41735
41990
|
|
|
41736
|
-
//
|
|
41737
|
-
|
|
41738
|
-
|
|
41739
|
-
|
|
41740
|
-
|
|
41741
|
-
|
|
41742
|
-
|
|
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
|
+
}
|
|
41743
42172
|
});
|
|
41744
42173
|
};
|
|
41745
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
|
+
|
|
41746
42192
|
// Returns a function for filtering multiple source-table records
|
|
41747
42193
|
// (used by -join command)
|
|
41748
42194
|
function getJoinFilter(data, exp) {
|
|
@@ -48406,8 +48852,138 @@ ${svg}
|
|
|
48406
48852
|
return parsed;
|
|
48407
48853
|
}
|
|
48408
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
|
+
|
|
48409
48984
|
var ParseCommands = /*#__PURE__*/Object.freeze({
|
|
48410
48985
|
__proto__: null,
|
|
48986
|
+
parseCommandFileContent: parseCommandFileContent,
|
|
48411
48987
|
parseCommands: parseCommands,
|
|
48412
48988
|
parseConsoleCommands: parseConsoleCommands,
|
|
48413
48989
|
standardizeConsoleCommands: standardizeConsoleCommands
|
|
@@ -48430,7 +49006,9 @@ ${svg}
|
|
|
48430
49006
|
}
|
|
48431
49007
|
|
|
48432
49008
|
function commandTakesFileInput(name) {
|
|
48433
|
-
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');
|
|
48434
49012
|
}
|
|
48435
49013
|
|
|
48436
49014
|
// TODO: implement these and other functions
|
|
@@ -48456,11 +49034,116 @@ ${svg}
|
|
|
48456
49034
|
// flags.mosaic || flags.snap;
|
|
48457
49035
|
// }
|
|
48458
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
|
+
|
|
48459
49119
|
cmd.run = async function(job, targets, opts) {
|
|
48460
|
-
var
|
|
48461
|
-
if (!
|
|
48462
|
-
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);
|
|
48463
49141
|
}
|
|
49142
|
+
await runCommandFile(file, content, job, opts);
|
|
49143
|
+
}
|
|
49144
|
+
|
|
49145
|
+
async function runFromExpression(job, targets, opts) {
|
|
49146
|
+
var tmp, commands, ctx;
|
|
48464
49147
|
ctx = getBaseContext();
|
|
48465
49148
|
// io proxy adds ability to add datasets dynamically in a required function
|
|
48466
49149
|
ctx.io = getIOProxy();
|
|
@@ -48474,15 +49157,23 @@ ${svg}
|
|
|
48474
49157
|
commands = parseCommands(tmp);
|
|
48475
49158
|
|
|
48476
49159
|
// TODO: remove duplication with mapshaper-run-commands.mjs
|
|
49160
|
+
var outputArr = opts && opts.output || null;
|
|
48477
49161
|
commands.forEach(function(cmd) {
|
|
48478
49162
|
if (commandTakesFileInput(cmd.name)) {
|
|
48479
49163
|
cmd.options.input = ctx.io._cache;
|
|
48480
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
|
+
}
|
|
48481
49172
|
});
|
|
48482
49173
|
|
|
48483
49174
|
await utils.promisify(runParsedCommands)(commands, job);
|
|
48484
49175
|
}
|
|
48485
|
-
}
|
|
49176
|
+
}
|
|
48486
49177
|
|
|
48487
49178
|
cmd.shape = function(targetDataset, opts) {
|
|
48488
49179
|
var geojson, dataset;
|
|
@@ -50677,7 +51368,7 @@ ${svg}
|
|
|
50677
51368
|
name == 'require' || name == 'run' || name == 'define' ||
|
|
50678
51369
|
name == 'include' || name == 'print' || name == 'comment' || name == 'if' || name == 'elif' ||
|
|
50679
51370
|
name == 'else' || name == 'endif' || name == 'stop' || name == 'add-shape' ||
|
|
50680
|
-
name == 'scalebar';
|
|
51371
|
+
name == 'scalebar' || name == 'vars' || name == 'defaults';
|
|
50681
51372
|
}
|
|
50682
51373
|
|
|
50683
51374
|
async function runCommand(command, job) {
|
|
@@ -50812,8 +51503,14 @@ ${svg}
|
|
|
50812
51503
|
} else if (name == 'define') {
|
|
50813
51504
|
cmd.define(job.catalog, opts);
|
|
50814
51505
|
|
|
51506
|
+
} else if (name == 'vars') {
|
|
51507
|
+
cmd.vars(job, opts);
|
|
51508
|
+
|
|
51509
|
+
} else if (name == 'defaults') {
|
|
51510
|
+
cmd.defaults(job, opts);
|
|
51511
|
+
|
|
50815
51512
|
} else if (name == 'dissolve') {
|
|
50816
|
-
outputLayers =
|
|
51513
|
+
outputLayers = cmd.dissolve(targetLayers, targetDataset, opts);
|
|
50817
51514
|
|
|
50818
51515
|
} else if (name == 'dissolve2') {
|
|
50819
51516
|
outputLayers = cmd.dissolve2(targetLayers, targetDataset, opts);
|
|
@@ -51131,7 +51828,7 @@ ${svg}
|
|
|
51131
51828
|
});
|
|
51132
51829
|
}
|
|
51133
51830
|
|
|
51134
|
-
var version = "0.
|
|
51831
|
+
var version = "0.7.0";
|
|
51135
51832
|
|
|
51136
51833
|
// Parse command line args into commands and run them
|
|
51137
51834
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|
|
@@ -51268,6 +51965,11 @@ ${svg}
|
|
|
51268
51965
|
if (outputArr && (cmd.name == 'o' || cmd.name == 'info' && cmd.options.save_to)) {
|
|
51269
51966
|
cmd.options.output = outputArr;
|
|
51270
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
|
+
}
|
|
51271
51973
|
});
|
|
51272
51974
|
|
|
51273
51975
|
var lastCmd = commands[commands.length - 1];
|
|
@@ -51360,7 +52062,13 @@ ${svg}
|
|
|
51360
52062
|
utils.reduceAsync(commands, job, nextCommand, done);
|
|
51361
52063
|
|
|
51362
52064
|
function nextCommand(job, cmd, next) {
|
|
51363
|
-
|
|
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) {
|
|
51364
52072
|
next(null, result);
|
|
51365
52073
|
}).catch(function(e) {
|
|
51366
52074
|
next(e);
|
|
@@ -51368,6 +52076,60 @@ ${svg}
|
|
|
51368
52076
|
}
|
|
51369
52077
|
}
|
|
51370
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
|
+
|
|
51371
52133
|
function handleNonFatalError(err) {
|
|
51372
52134
|
if (err && err.name == 'NonFatalError') {
|
|
51373
52135
|
printError(err);
|
|
@@ -51391,6 +52153,18 @@ ${svg}
|
|
|
51391
52153
|
return [commands];
|
|
51392
52154
|
}
|
|
51393
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
|
+
|
|
51394
52168
|
return opts.files.map(function(file) {
|
|
51395
52169
|
var group = [{
|
|
51396
52170
|
name: 'i',
|