mapshaper 0.6.121 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/mapshaper.js +1342 -275
- package/package.json +5 -2
- package/www/assets/jetbrains-mono-regular.woff2 +0 -0
- package/www/assets/static-page.css +179 -0
- package/www/docs/_assets/cmd-search.js +213 -0
- package/www/docs/_assets/docs.css +712 -0
- package/www/docs/_assets/docs.js +75 -0
- package/www/docs/_assets/highlight.css +10 -0
- package/www/docs/essentials/command-line.html +127 -0
- package/www/docs/essentials/command-line.html.md +112 -0
- package/www/docs/essentials/web-app.html +138 -0
- package/www/docs/essentials/web-app.html.md +106 -0
- package/www/docs/examples/basics.html +276 -0
- package/www/docs/examples/basics.html.md +371 -0
- package/www/docs/examples/data/Makefile +31 -0
- package/www/docs/examples/data/globe.msx +0 -0
- package/www/docs/examples/data/globe.svg +616 -0
- package/www/docs/examples/data/globe.txt +21 -0
- package/www/docs/examples/data/globe.zip +0 -0
- package/www/docs/examples/data/ne_50m_admin_0_countries.geojson +1 -0
- package/www/docs/examples/data/ne_50m_admin_1_states_provinces_lakes.geojson +1 -0
- package/www/docs/examples/data/us-states.msx +0 -0
- package/www/docs/examples/data/us-states.svg +56 -0
- package/www/docs/examples/data/us-states.txt +6 -0
- package/www/docs/examples/data/us-states.zip +0 -0
- package/www/docs/examples/globe.html +108 -0
- package/www/docs/examples/globe.html.md +64 -0
- package/www/docs/examples/us-states.html +88 -0
- package/www/docs/examples/us-states.html.md +44 -0
- package/www/docs/formats/csv.html +127 -0
- package/www/docs/formats/csv.html.md +97 -0
- package/www/docs/formats/dbf.html +87 -0
- package/www/docs/formats/dbf.html.md +39 -0
- package/www/docs/formats/flatgeobuf.html +86 -0
- package/www/docs/formats/flatgeobuf.html.md +42 -0
- package/www/docs/formats/geojson.html +107 -0
- package/www/docs/formats/geojson.html.md +65 -0
- package/www/docs/formats/geopackage.html +87 -0
- package/www/docs/formats/geopackage.html.md +42 -0
- package/www/docs/formats/json.html +83 -0
- package/www/docs/formats/json.html.md +35 -0
- package/www/docs/formats/kml.html +82 -0
- package/www/docs/formats/kml.html.md +39 -0
- package/www/docs/formats/overview.html +192 -0
- package/www/docs/formats/overview.html.md +35 -0
- package/www/docs/formats/shapefile.html +123 -0
- package/www/docs/formats/shapefile.html.md +84 -0
- package/www/docs/formats/snapshot.html +87 -0
- package/www/docs/formats/snapshot.html.md +39 -0
- package/www/docs/formats/svg.html +99 -0
- package/www/docs/formats/svg.html.md +51 -0
- package/www/docs/formats/topojson.html +102 -0
- package/www/docs/formats/topojson.html.md +54 -0
- package/www/docs/gallery/index.html +80 -0
- package/www/docs/gallery/index.html.md +29 -0
- package/www/docs/guides/combining-layers.html +105 -0
- package/www/docs/guides/combining-layers.html.md +81 -0
- package/www/docs/guides/expressions.html +600 -0
- package/www/docs/guides/expressions.html.md +376 -0
- package/www/docs/guides/programmatic.html +117 -0
- package/www/docs/guides/programmatic.html.md +91 -0
- package/www/docs/guides/projections.html +158 -0
- package/www/docs/guides/projections.html.md +118 -0
- package/www/docs/guides/simplification.html +110 -0
- package/www/docs/guides/simplification.html.md +94 -0
- package/www/docs/guides/topology.html +90 -0
- package/www/docs/guides/topology.html.md +63 -0
- package/www/docs/images/simplification-detail.png +0 -0
- package/www/docs/images/simplification-dp.png +0 -0
- package/www/docs/images/simplification-mod2.png +0 -0
- package/www/docs/index.html +101 -0
- package/www/docs/index.html.md +59 -0
- package/www/docs/reference.html +1302 -0
- package/www/docs/reference.html.md +1817 -0
- package/www/docs/whats-new.html +76 -0
- package/www/docs/whats-new.html.md +53 -0
- package/www/index.html +30 -3
- package/www/llms-full.txt +4040 -0
- package/www/llms.txt +55 -0
- package/www/mapshaper-gui.js +7 -1
- package/www/mapshaper.js +1342 -275
- package/www/page.css +1 -1
- package/www/privacy.html +1 -112
- package/www/sponsor.html +4 -164
- package/www/terms.html +1 -112
package/www/mapshaper.js
CHANGED
|
@@ -1269,6 +1269,10 @@
|
|
|
1269
1269
|
LOGGING = true;
|
|
1270
1270
|
}
|
|
1271
1271
|
|
|
1272
|
+
function disableLogging() {
|
|
1273
|
+
LOGGING = false;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1272
1276
|
function loggingEnabled() {
|
|
1273
1277
|
return !!LOGGING;
|
|
1274
1278
|
}
|
|
@@ -1475,6 +1479,7 @@
|
|
|
1475
1479
|
NonFatalError: NonFatalError,
|
|
1476
1480
|
UserError: UserError,
|
|
1477
1481
|
debug: debug,
|
|
1482
|
+
disableLogging: disableLogging,
|
|
1478
1483
|
enableLogging: enableLogging,
|
|
1479
1484
|
error: error,
|
|
1480
1485
|
formatColumns: formatColumns,
|
|
@@ -5499,6 +5504,58 @@
|
|
|
5499
5504
|
return parseCrsString$1(wkt1ToProj(str));
|
|
5500
5505
|
}
|
|
5501
5506
|
|
|
5507
|
+
// Extract an EPSG (or other authority) code from a short string like
|
|
5508
|
+
// "epsg:4326" or "ESRI:54030". Returns {org, code} or null.
|
|
5509
|
+
function parseAuthorityCodeString(str) {
|
|
5510
|
+
if (!str || typeof str != 'string') return null;
|
|
5511
|
+
var match = str.match(/^([a-z]+):(\d+)$/i);
|
|
5512
|
+
if (!match) return null;
|
|
5513
|
+
var code = +match[2];
|
|
5514
|
+
if (!code) return null;
|
|
5515
|
+
return {
|
|
5516
|
+
org: match[1].toUpperCase(),
|
|
5517
|
+
code: code
|
|
5518
|
+
};
|
|
5519
|
+
}
|
|
5520
|
+
|
|
5521
|
+
// Extract the top-level AUTHORITY clause from a WKT1 .prj string.
|
|
5522
|
+
// Returns {org, code} or null. Skips nested AUTHORITY clauses on inner
|
|
5523
|
+
// elements like the datum or unit, which would otherwise produce the
|
|
5524
|
+
// wrong code for projected CRSes.
|
|
5525
|
+
function parseAuthorityCodeFromWkt(wkt) {
|
|
5526
|
+
if (!wkt || typeof wkt != 'string') return null;
|
|
5527
|
+
var depth = 0;
|
|
5528
|
+
var inQuote = false;
|
|
5529
|
+
for (var i = 0; i < wkt.length; i++) {
|
|
5530
|
+
var c = wkt.charAt(i);
|
|
5531
|
+
if (c == '"') {
|
|
5532
|
+
inQuote = !inQuote;
|
|
5533
|
+
continue;
|
|
5534
|
+
}
|
|
5535
|
+
if (inQuote) continue;
|
|
5536
|
+
if (c == '[' || c == '(') {
|
|
5537
|
+
depth++;
|
|
5538
|
+
continue;
|
|
5539
|
+
}
|
|
5540
|
+
if (c == ']' || c == ')') {
|
|
5541
|
+
depth--;
|
|
5542
|
+
continue;
|
|
5543
|
+
}
|
|
5544
|
+
if (depth != 1) continue;
|
|
5545
|
+
var slice = wkt.slice(i, i + 10).toUpperCase();
|
|
5546
|
+
if (slice == 'AUTHORITY[' || slice.slice(0, 10) == 'AUTHORITY(') {
|
|
5547
|
+
var match = wkt.slice(i).match(/^AUTHORITY\s*[[(]\s*"([^"]+)"\s*,\s*"?(\d+)"?\s*[\])]/i);
|
|
5548
|
+
if (match) {
|
|
5549
|
+
return {
|
|
5550
|
+
org: String(match[1]).toUpperCase(),
|
|
5551
|
+
code: +match[2]
|
|
5552
|
+
};
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
}
|
|
5556
|
+
return null;
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5502
5559
|
var Projections = /*#__PURE__*/Object.freeze({
|
|
5503
5560
|
__proto__: null,
|
|
5504
5561
|
crsAreEqual: crsAreEqual,
|
|
@@ -5522,6 +5579,8 @@
|
|
|
5522
5579
|
isWGS84: isWGS84,
|
|
5523
5580
|
isWebMercator: isWebMercator,
|
|
5524
5581
|
looksLikeProj4String: looksLikeProj4String,
|
|
5582
|
+
parseAuthorityCodeFromWkt: parseAuthorityCodeFromWkt,
|
|
5583
|
+
parseAuthorityCodeString: parseAuthorityCodeString,
|
|
5525
5584
|
parseCrsString: parseCrsString$1,
|
|
5526
5585
|
parsePrj: parsePrj,
|
|
5527
5586
|
printProjections: printProjections,
|
|
@@ -11928,6 +11987,39 @@
|
|
|
11928
11987
|
return /^\s*[{[]/.test(String(str));
|
|
11929
11988
|
}
|
|
11930
11989
|
|
|
11990
|
+
// Heuristic: detect inline comma-delimited data passed as an -i argument.
|
|
11991
|
+
// Required signals (intentionally strict to avoid false positives on filenames):
|
|
11992
|
+
// 1. Contains a real newline OR the literal escape sequence "\n"
|
|
11993
|
+
// 2. The first and second non-empty lines each contain at least one comma
|
|
11994
|
+
// Multi-character delimiters (tab, semicolon, pipe) are not detected here;
|
|
11995
|
+
// only comma-delimited input is supported as inline data for now.
|
|
11996
|
+
function stringLooksLikeCsv(str) {
|
|
11997
|
+
if (typeof str !== 'string' || str.length === 0) return false;
|
|
11998
|
+
if (!stringHasInlineCsvNewline(str)) return false;
|
|
11999
|
+
var normalized = unescapeInlineCsv(str);
|
|
12000
|
+
var lines = normalized.split(/\r?\n/).filter(function(line) {
|
|
12001
|
+
return line.length > 0;
|
|
12002
|
+
});
|
|
12003
|
+
if (lines.length < 2) return false;
|
|
12004
|
+
return lines[0].indexOf(',') > -1 && lines[1].indexOf(',') > -1;
|
|
12005
|
+
}
|
|
12006
|
+
|
|
12007
|
+
// True if @str contains either a real newline or the literal two-character
|
|
12008
|
+
// escape sequence "\n" (backslash + n) anywhere in the string.
|
|
12009
|
+
function stringHasInlineCsvNewline(str) {
|
|
12010
|
+
return str.indexOf('\n') > -1 || /\\n/.test(str);
|
|
12011
|
+
}
|
|
12012
|
+
|
|
12013
|
+
// Convert literal "\n" / "\r\n" escape sequences in an inline CSV string
|
|
12014
|
+
// into real newline characters. If the input already contains a real newline,
|
|
12015
|
+
// it is returned unchanged so that backslash-n sequences inside quoted cells
|
|
12016
|
+
// are preserved verbatim.
|
|
12017
|
+
function unescapeInlineCsv(str) {
|
|
12018
|
+
if (typeof str !== 'string') return str;
|
|
12019
|
+
if (str.indexOf('\n') > -1) return str;
|
|
12020
|
+
return str.replace(/\\r\\n/g, '\n').replace(/\\n/g, '\n');
|
|
12021
|
+
}
|
|
12022
|
+
|
|
11931
12023
|
function stringLooksLikeKML(str) {
|
|
11932
12024
|
str = String(str);
|
|
11933
12025
|
return str.includes('<kml ') && str.includes('xmlns="http://www.opengis.net/kml/');
|
|
@@ -11960,6 +12052,29 @@
|
|
|
11960
12052
|
return file.endsWith('.' + PACKAGE_EXT);
|
|
11961
12053
|
}
|
|
11962
12054
|
|
|
12055
|
+
// Returns true if @file has an extension that may identify a mapshaper
|
|
12056
|
+
// command file (e.g. "commands.txt"). Detection still requires a content
|
|
12057
|
+
// sniff via stringLooksLikeCommandFile().
|
|
12058
|
+
function isPotentialCommandFile(file) {
|
|
12059
|
+
var ext = getFileExtension(file || '').toLowerCase();
|
|
12060
|
+
return ext === 'txt';
|
|
12061
|
+
}
|
|
12062
|
+
|
|
12063
|
+
// True if @str looks like the content of a mapshaper command file: the first
|
|
12064
|
+
// non-blank, non-comment line begins with the magic word "mapshaper".
|
|
12065
|
+
function stringLooksLikeCommandFile(str) {
|
|
12066
|
+
str = String(str || '');
|
|
12067
|
+
// Skip a leading BOM
|
|
12068
|
+
if (str.charCodeAt(0) === 0xFEFF) str = str.slice(1);
|
|
12069
|
+
var lines = str.split(/\r?\n/);
|
|
12070
|
+
for (var i = 0; i < lines.length; i++) {
|
|
12071
|
+
var line = lines[i].trim();
|
|
12072
|
+
if (!line || line.charAt(0) === '#') continue;
|
|
12073
|
+
return /^mapshaper(\s|$)/.test(line);
|
|
12074
|
+
}
|
|
12075
|
+
return false;
|
|
12076
|
+
}
|
|
12077
|
+
|
|
11963
12078
|
function isZipFile(file) {
|
|
11964
12079
|
return /\.zip$/i.test(file);
|
|
11965
12080
|
}
|
|
@@ -12004,13 +12119,18 @@
|
|
|
12004
12119
|
isImportableAsBinary: isImportableAsBinary,
|
|
12005
12120
|
isKmzFile: isKmzFile,
|
|
12006
12121
|
isPackageFile: isPackageFile,
|
|
12122
|
+
isPotentialCommandFile: isPotentialCommandFile,
|
|
12007
12123
|
isSupportedBinaryInputType: isSupportedBinaryInputType,
|
|
12008
12124
|
isZipFile: isZipFile,
|
|
12009
12125
|
looksLikeContentFile: looksLikeContentFile,
|
|
12010
12126
|
looksLikeImportableFile: looksLikeImportableFile,
|
|
12127
|
+
stringHasInlineCsvNewline: stringHasInlineCsvNewline,
|
|
12128
|
+
stringLooksLikeCommandFile: stringLooksLikeCommandFile,
|
|
12129
|
+
stringLooksLikeCsv: stringLooksLikeCsv,
|
|
12011
12130
|
stringLooksLikeJSON: stringLooksLikeJSON,
|
|
12012
12131
|
stringLooksLikeKML: stringLooksLikeKML,
|
|
12013
|
-
stringLooksLikeSVG: stringLooksLikeSVG
|
|
12132
|
+
stringLooksLikeSVG: stringLooksLikeSVG,
|
|
12133
|
+
unescapeInlineCsv: unescapeInlineCsv
|
|
12014
12134
|
});
|
|
12015
12135
|
|
|
12016
12136
|
// input: A file path or a buffer
|
|
@@ -12258,10 +12378,14 @@
|
|
|
12258
12378
|
return files;
|
|
12259
12379
|
};
|
|
12260
12380
|
|
|
12261
|
-
// Expand any wildcards.
|
|
12381
|
+
// Expand any wildcards. Inline data strings (JSON / CSV passed directly on
|
|
12382
|
+
// the command line) are passed through unchanged so that "*" appearing
|
|
12383
|
+
// inside the data isn't interpreted as a glob.
|
|
12262
12384
|
cli.expandInputFiles = function(files) {
|
|
12263
12385
|
return files.reduce(function(memo, name) {
|
|
12264
|
-
if (name
|
|
12386
|
+
if (stringLooksLikeJSON(name) || stringLooksLikeCsv(name)) {
|
|
12387
|
+
memo.push(name);
|
|
12388
|
+
} else if (name.indexOf('*') > -1) {
|
|
12265
12389
|
memo = memo.concat(cli.expandFileName(name));
|
|
12266
12390
|
} else {
|
|
12267
12391
|
memo.push(name);
|
|
@@ -13805,12 +13929,17 @@
|
|
|
13805
13929
|
profileStart('intersectSegments');
|
|
13806
13930
|
var raw = arcs.getVertexData(),
|
|
13807
13931
|
intersections = [],
|
|
13932
|
+
// opts.limit (optional): stop searching once this many intersections have
|
|
13933
|
+
// been found. Used for cheap "is this dataset clean?" checks where we
|
|
13934
|
+
// only need a small sample.
|
|
13935
|
+
limit = opts.limit > 0 ? opts.limit : 0,
|
|
13808
13936
|
arr;
|
|
13809
13937
|
for (i=0; i<stripeCount; i++) {
|
|
13810
13938
|
arr = intersectSegments(stripes[i], raw.xx, raw.yy, opts);
|
|
13811
13939
|
for (j=0; j<arr.length; j++) {
|
|
13812
13940
|
intersections.push(arr[j]);
|
|
13813
13941
|
}
|
|
13942
|
+
if (limit > 0 && intersections.length >= limit) break;
|
|
13814
13943
|
}
|
|
13815
13944
|
profileEnd('intersectSegments');
|
|
13816
13945
|
profileStart('dedupIntersections');
|
|
@@ -15593,7 +15722,7 @@
|
|
|
15593
15722
|
if (utils.isObject(obj)) {
|
|
15594
15723
|
_records[_id] = obj;
|
|
15595
15724
|
} else {
|
|
15596
|
-
stop$1("Can't assign non-object to
|
|
15725
|
+
stop$1("Can't assign non-object to this.properties");
|
|
15597
15726
|
}
|
|
15598
15727
|
}, get: function() {
|
|
15599
15728
|
var rec = _records[_id];
|
|
@@ -15726,7 +15855,7 @@
|
|
|
15726
15855
|
if (!obj || utils.isArray(obj)) {
|
|
15727
15856
|
lyr.shapes[_id] = obj || null;
|
|
15728
15857
|
} else {
|
|
15729
|
-
stop$1("Can't assign non-array to
|
|
15858
|
+
stop$1("Can't assign non-array to this.coordinates");
|
|
15730
15859
|
}
|
|
15731
15860
|
}, get: function() {
|
|
15732
15861
|
return lyr.shapes[_id] || null;
|
|
@@ -16106,7 +16235,7 @@
|
|
|
16106
16235
|
|
|
16107
16236
|
// Get a copy of a layer containing a subset of the layer's features,
|
|
16108
16237
|
// given a "where" expression in the options object
|
|
16109
|
-
function getLayerSelection(lyr, arcs, opts) {
|
|
16238
|
+
function getLayerSelection$1(lyr, arcs, opts) {
|
|
16110
16239
|
var lyr2 = utils.extend({}, lyr);
|
|
16111
16240
|
var filterOpts = {
|
|
16112
16241
|
expression: opts.where,
|
|
@@ -16123,11 +16252,11 @@
|
|
|
16123
16252
|
if (!opts || !opts.where) {
|
|
16124
16253
|
error('Missing required "where" parameter');
|
|
16125
16254
|
}
|
|
16126
|
-
var subsetLyr = getLayerSelection(lyr, arcs, opts);
|
|
16255
|
+
var subsetLyr = getLayerSelection$1(lyr, arcs, opts);
|
|
16127
16256
|
var cmdOpts = utils.defaults({where: null}, opts); // prevent infinite recursion
|
|
16128
16257
|
var outputLyr = commandFunc(subsetLyr, arcs, cmdOpts);
|
|
16129
16258
|
var filterOpts = utils.defaults({invert: true}, opts);
|
|
16130
|
-
var filteredLyr = getLayerSelection(lyr, arcs, filterOpts);
|
|
16259
|
+
var filteredLyr = getLayerSelection$1(lyr, arcs, filterOpts);
|
|
16131
16260
|
var merged = cmd.mergeLayers([filteredLyr, outputLyr], {verbose: false, force: true});
|
|
16132
16261
|
return merged[0];
|
|
16133
16262
|
}
|
|
@@ -16147,7 +16276,7 @@
|
|
|
16147
16276
|
// Calculate an expression across a group of features, print and return the result
|
|
16148
16277
|
// Supported functions include sum(), average(), max(), min(), median(), count()
|
|
16149
16278
|
// Functions receive an expression to be applied to each feature (like the -each command)
|
|
16150
|
-
// Examples: 'sum(
|
|
16279
|
+
// Examples: 'sum(this.area)' 'min(income)'
|
|
16151
16280
|
// opts.expression Expression to evaluate
|
|
16152
16281
|
// opts.where Optional filter expression (see -filter command)
|
|
16153
16282
|
//
|
|
@@ -16156,7 +16285,7 @@
|
|
|
16156
16285
|
result, compiled, defs, d;
|
|
16157
16286
|
if (opts.where) {
|
|
16158
16287
|
// TODO: implement no_replace option for filter() instead of this
|
|
16159
|
-
lyr = getLayerSelection(lyr, arcs, opts);
|
|
16288
|
+
lyr = getLayerSelection$1(lyr, arcs, opts);
|
|
16160
16289
|
msg += ' where ' + opts.where;
|
|
16161
16290
|
}
|
|
16162
16291
|
// Save any assigned variables to the defs object, so they will be available
|
|
@@ -16878,208 +17007,6 @@
|
|
|
16878
17007
|
dissolvePolygonGeometry: dissolvePolygonGeometry
|
|
16879
17008
|
});
|
|
16880
17009
|
|
|
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
17010
|
// Clean polygon or polyline shapes (in-place)
|
|
17084
17011
|
//
|
|
17085
17012
|
function cleanShapes(shapes, arcs, type) {
|
|
@@ -17391,6 +17318,29 @@
|
|
|
17391
17318
|
profileStart('snapAndCut');
|
|
17392
17319
|
var arcs = dataset.arcs;
|
|
17393
17320
|
var cutOpts = snapDist > 0 ? {tolerance: snapDist} : {tolerance: 0};
|
|
17321
|
+
|
|
17322
|
+
// Probe for intersections before any modification. If the input has none,
|
|
17323
|
+
// every pass of the loop below is provably a no-op: snap() can only
|
|
17324
|
+
// consolidate FP noise around intersection points, cutPathsAtIntersections
|
|
17325
|
+
// inserts cut points only where segments cross, and buildTopology is gated
|
|
17326
|
+
// on coordsHaveChanged. So we can return early and skip a ~O(V log V) snap
|
|
17327
|
+
// that would snap zero points, which is the dominant cost of robust
|
|
17328
|
+
// dissolve on already-clean polygon input.
|
|
17329
|
+
//
|
|
17330
|
+
// limit=1 makes the probe cheap on dirty input too — it stops the stripe
|
|
17331
|
+
// scan after the first hit, so the probe's full cost is only paid on
|
|
17332
|
+
// truly clean input (where it replaces the equivalent scan that
|
|
17333
|
+
// cutPathsAtIntersections would otherwise do on pass 1).
|
|
17334
|
+
var probeOpts = {tolerance: cutOpts.tolerance, limit: 1};
|
|
17335
|
+
profileStart('probeIntersections');
|
|
17336
|
+
var probe = findSegmentIntersections(arcs, probeOpts);
|
|
17337
|
+
profileEnd('probeIntersections');
|
|
17338
|
+
if (probe.length === 0) {
|
|
17339
|
+
debug('[snapAndCut] skipped (no intersections)');
|
|
17340
|
+
profileEnd('snapAndCut');
|
|
17341
|
+
return false;
|
|
17342
|
+
}
|
|
17343
|
+
|
|
17394
17344
|
var coordsHaveChanged = false;
|
|
17395
17345
|
var snapCount = 0, dupeCount, cutCount;
|
|
17396
17346
|
var maxLoops = 4, loopCount = 0;
|
|
@@ -17696,6 +17646,336 @@
|
|
|
17696
17646
|
sortCutPoints: sortCutPoints
|
|
17697
17647
|
});
|
|
17698
17648
|
|
|
17649
|
+
// Options that require the topology-repair algorithm and are not supported
|
|
17650
|
+
// by the no-repair fast path.
|
|
17651
|
+
var REPAIR_REQUIRED_OPTS = ['gap_fill_area', 'sliver_control', 'allow_overlaps'];
|
|
17652
|
+
|
|
17653
|
+
// Sample size used to detect intersections in no-repair mode. Detection stops
|
|
17654
|
+
// after this many hits, so the warning message can include sample locations
|
|
17655
|
+
// without paying for an exhaustive scan on badly-formed input.
|
|
17656
|
+
var INTERSECTION_SAMPLE_LIMIT = 10;
|
|
17657
|
+
|
|
17658
|
+
// cmd.dissolve accepts two signatures:
|
|
17659
|
+
// (layers, dataset, opts) — multi-layer entry used by the CLI dispatcher.
|
|
17660
|
+
// Polygon layers go through the topology-repairing algorithm by default,
|
|
17661
|
+
// or through the legacy fast algorithm when opts.no_repair is set.
|
|
17662
|
+
// (lyr, arcs, opts) — legacy single-layer entry, retained for backward
|
|
17663
|
+
// compatibility with internal callers and existing tests. Always uses the
|
|
17664
|
+
// legacy fast algorithm; does not perform topology repair.
|
|
17665
|
+
//
|
|
17666
|
+
cmd.dissolve = function(arg1, arg2, opts) {
|
|
17667
|
+
if (Array.isArray(arg1)) {
|
|
17668
|
+
return dissolveLayers(arg1, arg2, opts);
|
|
17669
|
+
}
|
|
17670
|
+
return dissolveSingleLayer(arg1, arg2, opts);
|
|
17671
|
+
};
|
|
17672
|
+
|
|
17673
|
+
function dissolveLayers(layers, dataset, optsArg) {
|
|
17674
|
+
var opts = utils.extend({}, optsArg);
|
|
17675
|
+
if (opts.field) opts.fields = [opts.field]; // support old "field" parameter
|
|
17676
|
+
|
|
17677
|
+
if (opts.no_repair) {
|
|
17678
|
+
var conflicting = REPAIR_REQUIRED_OPTS.filter(function(k) { return opts[k]; });
|
|
17679
|
+
if (conflicting.length > 0) {
|
|
17680
|
+
stop$1('The no-repair option is incompatible with',
|
|
17681
|
+
conflicting.map(function(k) { return k.replace(/_/g, '-'); }).join(', '));
|
|
17682
|
+
}
|
|
17683
|
+
}
|
|
17684
|
+
|
|
17685
|
+
var anyPolygon = layers.some(function(lyr) {
|
|
17686
|
+
return lyr.geometry_type == 'polygon' && layerHasPaths(lyr);
|
|
17687
|
+
});
|
|
17688
|
+
|
|
17689
|
+
if (anyPolygon) {
|
|
17690
|
+
if (opts.no_repair) {
|
|
17691
|
+
detectAndWarnIntersections(dataset, opts);
|
|
17692
|
+
} else {
|
|
17693
|
+
addIntersectionCuts(dataset, opts);
|
|
17694
|
+
}
|
|
17695
|
+
}
|
|
17696
|
+
|
|
17697
|
+
return layers.map(function(lyr) {
|
|
17698
|
+
return dissolveOneLayer(lyr, dataset, opts);
|
|
17699
|
+
});
|
|
17700
|
+
}
|
|
17701
|
+
|
|
17702
|
+
function dissolveOneLayer(lyr, dataset, opts) {
|
|
17703
|
+
if (opts.where) {
|
|
17704
|
+
return dissolveLayerWithWhereClause(lyr, dataset, opts);
|
|
17705
|
+
}
|
|
17706
|
+
if (opts.multipart || opts.group_points) {
|
|
17707
|
+
var classifier = getCategoryClassifier(opts.fields, lyr.data);
|
|
17708
|
+
return composeDissolveLayer(lyr, makeMultipartShapes(lyr, classifier), classifier, opts);
|
|
17709
|
+
}
|
|
17710
|
+
if (lyr.geometry_type == 'polygon') {
|
|
17711
|
+
return dissolvePolygonInLayer(lyr, dataset, opts);
|
|
17712
|
+
}
|
|
17713
|
+
if (lyr.geometry_type == 'polyline') {
|
|
17714
|
+
var polylineClassifier = getCategoryClassifier(opts.fields, lyr.data);
|
|
17715
|
+
var polylineShapes = dissolvePolylineGeometry(lyr, polylineClassifier, dataset.arcs);
|
|
17716
|
+
return composeDissolveLayer(lyr, polylineShapes, polylineClassifier, opts);
|
|
17717
|
+
}
|
|
17718
|
+
if (lyr.geometry_type == 'point') {
|
|
17719
|
+
var pointClassifier = getCategoryClassifier(opts.fields, lyr.data);
|
|
17720
|
+
var pointShapes = dissolvePointGeometry(lyr, pointClassifier, opts);
|
|
17721
|
+
return composeDissolveLayer(lyr, pointShapes, pointClassifier, opts);
|
|
17722
|
+
}
|
|
17723
|
+
// tabular (no geometry): aggregate records only
|
|
17724
|
+
var nullClassifier = getCategoryClassifier(opts.fields, lyr.data);
|
|
17725
|
+
return composeDissolveLayer(lyr, undefined, nullClassifier, opts);
|
|
17726
|
+
}
|
|
17727
|
+
|
|
17728
|
+
function dissolvePolygonInLayer(lyr, dataset, opts) {
|
|
17729
|
+
if (!layerHasPaths(lyr)) return lyr;
|
|
17730
|
+
if (opts.no_repair) {
|
|
17731
|
+
var classifier = getCategoryClassifier(opts.fields, lyr.data);
|
|
17732
|
+
var shapes = dissolvePolygonGeometry(lyr.shapes, classifier);
|
|
17733
|
+
return composeDissolveLayer(lyr, shapes, classifier, opts);
|
|
17734
|
+
}
|
|
17735
|
+
return dissolvePolygonLayer2(lyr, dataset, opts);
|
|
17736
|
+
}
|
|
17737
|
+
|
|
17738
|
+
function dissolveLayerWithWhereClause(lyr, dataset, opts) {
|
|
17739
|
+
// Run dissolve on a subset of features defined by opts.where, then merge the
|
|
17740
|
+
// dissolved subset back together with the unselected features.
|
|
17741
|
+
// Topology repair (if needed) was already performed at the dataset level by
|
|
17742
|
+
// dissolveLayers, so the recursive call uses no_repair=true to avoid doing
|
|
17743
|
+
// the work a second time on a subset of the same arcs.
|
|
17744
|
+
var arcs = dataset.arcs;
|
|
17745
|
+
var subsetLyr = getLayerSelection(lyr, arcs, opts);
|
|
17746
|
+
var cmdOpts = utils.defaults({where: null, no_repair: true}, opts);
|
|
17747
|
+
var dissolved = dissolveOneLayer(subsetLyr, dataset, cmdOpts);
|
|
17748
|
+
var filteredLyr = getLayerSelection(lyr, arcs, utils.defaults({invert: true}, opts));
|
|
17749
|
+
var merged = cmd.mergeLayers([filteredLyr, dissolved], {verbose: false, force: true});
|
|
17750
|
+
return merged[0];
|
|
17751
|
+
}
|
|
17752
|
+
|
|
17753
|
+
function getLayerSelection(lyr, arcs, opts) {
|
|
17754
|
+
var lyr2 = utils.extend({}, lyr);
|
|
17755
|
+
var filterOpts = {
|
|
17756
|
+
expression: opts.where,
|
|
17757
|
+
invert: !!opts.invert,
|
|
17758
|
+
verbose: false,
|
|
17759
|
+
no_replace: opts.no_replace
|
|
17760
|
+
};
|
|
17761
|
+
return cmd.filterFeatures(lyr2, arcs, filterOpts);
|
|
17762
|
+
}
|
|
17763
|
+
|
|
17764
|
+
// Detect a small sample of segment intersections; print a warning if any are
|
|
17765
|
+
// found. Used by the no-repair fast path to alert users that their input has
|
|
17766
|
+
// topology problems. Detection stops after INTERSECTION_SAMPLE_LIMIT hits, so
|
|
17767
|
+
// the cost is bounded for badly-formed input.
|
|
17768
|
+
function detectAndWarnIntersections(dataset, opts) {
|
|
17769
|
+
if (opts.quiet || opts.silent) return;
|
|
17770
|
+
if (!dataset.arcs || dataset.arcs.size() === 0) return;
|
|
17771
|
+
var sample = findSegmentIntersections(dataset.arcs, {limit: INTERSECTION_SAMPLE_LIMIT});
|
|
17772
|
+
if (sample.length === 0) return;
|
|
17773
|
+
var atLeast = sample.length >= INTERSECTION_SAMPLE_LIMIT ? 'at least ' : '';
|
|
17774
|
+
message('Warning: found ' + atLeast + sample.length +
|
|
17775
|
+
' segment intersection' + (sample.length == 1 ? '' : 's') +
|
|
17776
|
+
'. The no-repair option assumes clean topology; output may be incorrect.');
|
|
17777
|
+
}
|
|
17778
|
+
|
|
17779
|
+
// Backward-compat: the legacy per-layer entry, still used by internal callers
|
|
17780
|
+
// and by tests that exercise the original fast algorithm directly. Retains
|
|
17781
|
+
// the original behavior (no topology repair, no multi-layer prep).
|
|
17782
|
+
function dissolveSingleLayer(lyr, arcs, opts) {
|
|
17783
|
+
var dissolveShapes, classifier;
|
|
17784
|
+
opts = utils.extend({}, opts);
|
|
17785
|
+
if (opts.where) {
|
|
17786
|
+
return applyCommandToLayerSelection(dissolveSingleLayer, lyr, arcs, opts);
|
|
17787
|
+
}
|
|
17788
|
+
if (opts.field) opts.fields = [opts.field];
|
|
17789
|
+
classifier = getCategoryClassifier(opts.fields, lyr.data);
|
|
17790
|
+
if (opts.multipart || opts.group_points) {
|
|
17791
|
+
dissolveShapes = makeMultipartShapes(lyr, classifier);
|
|
17792
|
+
} else if (lyr.geometry_type == 'polygon') {
|
|
17793
|
+
dissolveShapes = dissolvePolygonGeometry(lyr.shapes, classifier);
|
|
17794
|
+
} else if (lyr.geometry_type == 'polyline') {
|
|
17795
|
+
dissolveShapes = dissolvePolylineGeometry(lyr, classifier, arcs);
|
|
17796
|
+
} else if (lyr.geometry_type == 'point') {
|
|
17797
|
+
dissolveShapes = dissolvePointGeometry(lyr, classifier, opts);
|
|
17798
|
+
}
|
|
17799
|
+
return composeDissolveLayer(lyr, dissolveShapes, classifier, opts);
|
|
17800
|
+
}
|
|
17801
|
+
|
|
17802
|
+
function makeMultipartShapes(lyr, getGroupId) {
|
|
17803
|
+
if (!lyr.shapes || !lyr.geometry_type) {
|
|
17804
|
+
stop$1('Layer is missing geometry');
|
|
17805
|
+
}
|
|
17806
|
+
cloneShapes(lyr.shapes);
|
|
17807
|
+
var shapes2 = [];
|
|
17808
|
+
lyr.shapes.forEach(function(shp, i) {
|
|
17809
|
+
var groupId = getGroupId(i);
|
|
17810
|
+
if (!shp) return;
|
|
17811
|
+
if (!shapes2[groupId]) {
|
|
17812
|
+
shapes2[groupId] = shp;
|
|
17813
|
+
} else {
|
|
17814
|
+
shapes2[groupId].push.apply(shapes2[groupId], shp);
|
|
17815
|
+
}
|
|
17816
|
+
});
|
|
17817
|
+
return shapes2;
|
|
17818
|
+
}
|
|
17819
|
+
|
|
17820
|
+
// @lyr: original undissolved layer
|
|
17821
|
+
// @shapes: dissolved shapes
|
|
17822
|
+
function composeDissolveLayer(lyr, shapes, getGroupId, opts) {
|
|
17823
|
+
var records = null;
|
|
17824
|
+
var lyr2;
|
|
17825
|
+
if (lyr.data) {
|
|
17826
|
+
records = aggregateDataRecords(lyr.data.getRecords(), getGroupId, opts);
|
|
17827
|
+
// replace missing shapes with nulls
|
|
17828
|
+
for (var i=0, n=records.length; i<n; i++) {
|
|
17829
|
+
if (shapes && !shapes[i]) {
|
|
17830
|
+
shapes[i] = null;
|
|
17831
|
+
}
|
|
17832
|
+
}
|
|
17833
|
+
}
|
|
17834
|
+
lyr2 = {
|
|
17835
|
+
name: opts.no_replace ? null : lyr.name,
|
|
17836
|
+
shapes: shapes,
|
|
17837
|
+
data: records ? new DataTable(records) : null,
|
|
17838
|
+
geometry_type: lyr.geometry_type
|
|
17839
|
+
};
|
|
17840
|
+
if (!opts.silent) {
|
|
17841
|
+
printDissolveMessage(lyr, lyr2);
|
|
17842
|
+
}
|
|
17843
|
+
return lyr2;
|
|
17844
|
+
}
|
|
17845
|
+
|
|
17846
|
+
function printDissolveMessage(pre, post) {
|
|
17847
|
+
var n1 = getFeatureCount(pre),
|
|
17848
|
+
n2 = getFeatureCount(post),
|
|
17849
|
+
msg = utils.format('Dissolved %,d feature%s into %,d feature%s',
|
|
17850
|
+
n1, utils.pluralSuffix(n1), n2,
|
|
17851
|
+
utils.pluralSuffix(n2));
|
|
17852
|
+
message(msg);
|
|
17853
|
+
}
|
|
17854
|
+
|
|
17855
|
+
// Maps tile ids to shape ids (both are non-negative integers). Supports
|
|
17856
|
+
// one-to-many mapping (a tile may belong to multiple shapes)
|
|
17857
|
+
// Also maps shape ids to tile ids. A shape may contain multiple tiles
|
|
17858
|
+
// Also supports 'flattening' -- removing one-to-many tile-shape mappings by
|
|
17859
|
+
// removing all but one shape from a tile.
|
|
17860
|
+
// Supports one-to-many mapping
|
|
17861
|
+
function TileShapeIndex(mosaic, opts) {
|
|
17862
|
+
// indexes for mapping tile ids to shape ids
|
|
17863
|
+
var singleIndex = new Int32Array(mosaic.length);
|
|
17864
|
+
utils.initializeArray(singleIndex, -1);
|
|
17865
|
+
var multipleIndex = [];
|
|
17866
|
+
// index that maps shape ids to tile ids
|
|
17867
|
+
var shapeIndex = [];
|
|
17868
|
+
|
|
17869
|
+
this.getTileIdsByShapeId = function(shapeId) {
|
|
17870
|
+
var ids = shapeIndex[shapeId];
|
|
17871
|
+
// need to filter out tile ids that have been set to -1 (indicating removal)
|
|
17872
|
+
return ids ? ids.filter(function(id) {return id >= 0;}) : [];
|
|
17873
|
+
};
|
|
17874
|
+
|
|
17875
|
+
// assumes index has been flattened
|
|
17876
|
+
this.getShapeIdByTileId = function(id) {
|
|
17877
|
+
var shapeId = singleIndex[id];
|
|
17878
|
+
return shapeId >= 0 ? shapeId : -1;
|
|
17879
|
+
};
|
|
17880
|
+
|
|
17881
|
+
// return ids of all shapes that include a tile
|
|
17882
|
+
this.getShapeIdsByTileId = function(id) {
|
|
17883
|
+
var singleId = singleIndex[id];
|
|
17884
|
+
if (singleId >= 0) {
|
|
17885
|
+
return [singleId];
|
|
17886
|
+
}
|
|
17887
|
+
if (singleId == -1) {
|
|
17888
|
+
return [];
|
|
17889
|
+
}
|
|
17890
|
+
return multipleIndex[id];
|
|
17891
|
+
};
|
|
17892
|
+
|
|
17893
|
+
this.indexTileIdsByShapeId = function(shapeId, tileIds, weightFunction) {
|
|
17894
|
+
shapeIndex[shapeId] = [];
|
|
17895
|
+
for (var i=0; i<tileIds.length; i++) {
|
|
17896
|
+
indexShapeIdByTileId(shapeId, tileIds[i], weightFunction);
|
|
17897
|
+
}
|
|
17898
|
+
};
|
|
17899
|
+
|
|
17900
|
+
// remove many-to-one tile=>shape mappings
|
|
17901
|
+
this.flatten = function() {
|
|
17902
|
+
multipleIndex.forEach(function(shapeIds, tileId) {
|
|
17903
|
+
flattenStackedTile(tileId);
|
|
17904
|
+
});
|
|
17905
|
+
multipleIndex = [];
|
|
17906
|
+
};
|
|
17907
|
+
|
|
17908
|
+
this.getUnusedTileIds = function() {
|
|
17909
|
+
var ids = [];
|
|
17910
|
+
for (var i=0, n=singleIndex.length; i<n; i++) {
|
|
17911
|
+
if (singleIndex[i] == -1) ids.push(i);
|
|
17912
|
+
}
|
|
17913
|
+
return ids;
|
|
17914
|
+
};
|
|
17915
|
+
|
|
17916
|
+
// used by gap fill; assumes that flatten() has been called
|
|
17917
|
+
this.addTileToShape = function(shapeId, tileId) {
|
|
17918
|
+
if (shapeId in shapeIndex === false || singleIndex[tileId] != -1) {
|
|
17919
|
+
error('Internal error');
|
|
17920
|
+
}
|
|
17921
|
+
singleIndex[tileId] = shapeId;
|
|
17922
|
+
shapeIndex[shapeId].push(tileId);
|
|
17923
|
+
};
|
|
17924
|
+
|
|
17925
|
+
// add a shape id to a tile
|
|
17926
|
+
function indexShapeIdByTileId(shapeId, tileId, weightFunction) {
|
|
17927
|
+
var singleId = singleIndex[tileId];
|
|
17928
|
+
if (singleId != -1 && opts.flat) {
|
|
17929
|
+
// pick the best shape if we have a weight function
|
|
17930
|
+
if (weightFunction && weightFunction(shapeId) > weightFunction(singleId)) {
|
|
17931
|
+
// replace existing shape reference
|
|
17932
|
+
removeTileFromShape(tileId, singleId); // bottleneck when overlaps are many
|
|
17933
|
+
singleIndex[tileId] = singleId;
|
|
17934
|
+
singleId = -1;
|
|
17935
|
+
} else {
|
|
17936
|
+
// keep existing shape reference
|
|
17937
|
+
return;
|
|
17938
|
+
}
|
|
17939
|
+
}
|
|
17940
|
+
if (singleId == -1) {
|
|
17941
|
+
singleIndex[tileId] = shapeId;
|
|
17942
|
+
} else if (singleId == -2) {
|
|
17943
|
+
multipleIndex[tileId].push(shapeId);
|
|
17944
|
+
} else {
|
|
17945
|
+
multipleIndex[tileId] = [singleId, shapeId];
|
|
17946
|
+
singleIndex[tileId] = -2;
|
|
17947
|
+
}
|
|
17948
|
+
shapeIndex[shapeId].push(tileId);
|
|
17949
|
+
}
|
|
17950
|
+
|
|
17951
|
+
|
|
17952
|
+
function flattenStackedTile(tileId) {
|
|
17953
|
+
// TODO: select the best shape (using some metric)
|
|
17954
|
+
var shapeIds = multipleIndex[tileId];
|
|
17955
|
+
// if (!shapeIds || shapeIds.length > 1 === false) error('flattening error');
|
|
17956
|
+
var selectedId = shapeIds[0];
|
|
17957
|
+
var shapeId;
|
|
17958
|
+
singleIndex[tileId] = selectedId; // add shape to single index
|
|
17959
|
+
// remove tile from other stacked shapes
|
|
17960
|
+
for (var i=0; i<shapeIds.length; i++) {
|
|
17961
|
+
shapeId = shapeIds[i];
|
|
17962
|
+
if (shapeId != selectedId) {
|
|
17963
|
+
removeTileFromShape(tileId, shapeId);
|
|
17964
|
+
}
|
|
17965
|
+
}
|
|
17966
|
+
}
|
|
17967
|
+
|
|
17968
|
+
function removeTileFromShape(tileId, shapeId) {
|
|
17969
|
+
var tileIds = shapeIndex[shapeId];
|
|
17970
|
+
for (var i=0; i<tileIds.length; i++) {
|
|
17971
|
+
if (tileIds[i] === tileId) {
|
|
17972
|
+
tileIds[i] = -1;
|
|
17973
|
+
break;
|
|
17974
|
+
}
|
|
17975
|
+
}
|
|
17976
|
+
}
|
|
17977
|
+
}
|
|
17978
|
+
|
|
17699
17979
|
// Support for timing using T.start() and T.stop()
|
|
17700
17980
|
var T$1 = {
|
|
17701
17981
|
stack: [],
|
|
@@ -21250,18 +21530,18 @@ ${svg}
|
|
|
21250
21530
|
validateSvgDataFields: validateSvgDataFields
|
|
21251
21531
|
});
|
|
21252
21532
|
|
|
21253
|
-
// import { isKmzFile } from '../io/mapshaper-file-types';
|
|
21254
|
-
|
|
21255
21533
|
function exportKML(dataset, opts) {
|
|
21256
21534
|
var toKML = require$1("@placemarkio/tokml").toKML;
|
|
21257
21535
|
var geojsonOpts = Object.assign({combine_layers: true, geojson_type: 'FeatureCollection'}, opts);
|
|
21258
21536
|
var geojson = exportDatasetAsGeoJSON(dataset, geojsonOpts);
|
|
21259
21537
|
var kml = toKML(geojson);
|
|
21260
|
-
|
|
21261
|
-
|
|
21262
|
-
var
|
|
21538
|
+
var useKmz = !!(opts.file && isKmzFile(opts.file));
|
|
21539
|
+
var ofile = opts.file || getOutputFileBase(dataset) + (useKmz ? '.kmz' : '.kml');
|
|
21540
|
+
var content = useKmz
|
|
21541
|
+
? zipSync([{ filename: 'doc.kml', content: kml }])
|
|
21542
|
+
: kml;
|
|
21263
21543
|
return [{
|
|
21264
|
-
content:
|
|
21544
|
+
content: content,
|
|
21265
21545
|
filename: ofile
|
|
21266
21546
|
}];
|
|
21267
21547
|
}
|
|
@@ -24819,13 +25099,19 @@ ${svg}
|
|
|
24819
25099
|
// Keep behavior consistent with other exporters that honor explicit output filename.
|
|
24820
25100
|
extension = getFileExtension(opts.file) || extension;
|
|
24821
25101
|
}
|
|
25102
|
+
var crsMeta = resolveOutputCRS(dataset);
|
|
24822
25103
|
return dataset.layers.map(function(lyr) {
|
|
24823
25104
|
var geojson = getFeatureCollection(lyr, dataset, opts);
|
|
24824
25105
|
var content = serialize(geojson);
|
|
24825
|
-
|
|
25106
|
+
var filename = lyr.name + '.' + extension;
|
|
25107
|
+
if (crsMeta) {
|
|
25108
|
+
content = rewriteHeaderWithCRS(content, crsMeta);
|
|
25109
|
+
} else {
|
|
25110
|
+
message('Wrote', filename, 'without a CRS in the FlatGeobuf header (mapshaper could not derive an EPSG code for this dataset). Downstream tools may misinterpret the coordinates or refuse to load the file.');
|
|
25111
|
+
}
|
|
24826
25112
|
return {
|
|
24827
25113
|
content: content,
|
|
24828
|
-
filename:
|
|
25114
|
+
filename: filename
|
|
24829
25115
|
};
|
|
24830
25116
|
});
|
|
24831
25117
|
}
|
|
@@ -24841,10 +25127,55 @@ ${svg}
|
|
|
24841
25127
|
};
|
|
24842
25128
|
}
|
|
24843
25129
|
|
|
24844
|
-
|
|
24845
|
-
|
|
24846
|
-
|
|
24847
|
-
|
|
25130
|
+
// Try several strategies to derive an EPSG code for the dataset. Returns
|
|
25131
|
+
// a CRS-meta object suitable for buildHeaderWithCRS(), or null if no
|
|
25132
|
+
// EPSG code could be found. We don't write WKT-only CRSes -- mapshaper
|
|
25133
|
+
// can't reliably round-trip them and many readers ignore the WKT field.
|
|
25134
|
+
function resolveOutputCRS(dataset) {
|
|
25135
|
+
var info = (dataset && dataset.info) || {};
|
|
25136
|
+
var meta;
|
|
25137
|
+
|
|
25138
|
+
// 1. Round-tripped from another FlatGeobuf
|
|
25139
|
+
meta = normalizeCRS(info.flatgeobuf_crs);
|
|
25140
|
+
if (meta) return meta;
|
|
25141
|
+
|
|
25142
|
+
// 2. Round-tripped from a GeoPackage with an EPSG-coded SRS
|
|
25143
|
+
if (info.geopackage_crs &&
|
|
25144
|
+
String(info.geopackage_crs.organization || '').toUpperCase() === 'EPSG') {
|
|
25145
|
+
meta = normalizeCRS({
|
|
25146
|
+
org: 'EPSG',
|
|
25147
|
+
code: info.geopackage_crs.organization_coordsys_id || info.geopackage_crs.srs_id
|
|
25148
|
+
});
|
|
25149
|
+
if (meta) return meta;
|
|
25150
|
+
}
|
|
25151
|
+
|
|
25152
|
+
// 3. Explicit "epsg:NNNN" / "esri:NNNN" string set by -proj or alike
|
|
25153
|
+
meta = normalizeCRS(parseAuthorityCodeString(info.crs_string));
|
|
25154
|
+
if (meta) return meta;
|
|
25155
|
+
|
|
25156
|
+
// 4. AUTHORITY["EPSG", N] in a .prj/WKT1 string (typically from a Shapefile)
|
|
25157
|
+
meta = normalizeCRS(parseAuthorityCodeFromWkt(info.wkt1));
|
|
25158
|
+
if (meta) return meta;
|
|
25159
|
+
|
|
25160
|
+
// 5. Recognized CRS object: WGS-84 (any encoding) or Web Mercator.
|
|
25161
|
+
// getDatasetCrsInfo() also auto-detects WGS-84 from lat/lng-like bounds,
|
|
25162
|
+
// which is how GeoJSON sources end up with a usable CRS object.
|
|
25163
|
+
var crsInfo;
|
|
25164
|
+
try {
|
|
25165
|
+
crsInfo = getDatasetCrsInfo(dataset);
|
|
25166
|
+
} catch (e) {
|
|
25167
|
+
crsInfo = null;
|
|
25168
|
+
}
|
|
25169
|
+
if (crsInfo && crsInfo.crs) {
|
|
25170
|
+
if (isWGS84(crsInfo.crs)) {
|
|
25171
|
+
return normalizeCRS({org: 'EPSG', code: 4326});
|
|
25172
|
+
}
|
|
25173
|
+
if (isWebMercator(crsInfo.crs)) {
|
|
25174
|
+
return normalizeCRS({org: 'EPSG', code: 3857});
|
|
25175
|
+
}
|
|
25176
|
+
}
|
|
25177
|
+
|
|
25178
|
+
return null;
|
|
24848
25179
|
}
|
|
24849
25180
|
|
|
24850
25181
|
function normalizeCRS(crs) {
|
|
@@ -25487,10 +25818,22 @@ ${svg}
|
|
|
25487
25818
|
var bounds = getLayerBounds(target.layer, target.dataset.arcs);
|
|
25488
25819
|
opts = Object.assign({svg_bbox: bounds.toArray()}, opts);
|
|
25489
25820
|
}
|
|
25490
|
-
|
|
25491
|
-
var datasets
|
|
25492
|
-
|
|
25493
|
-
|
|
25821
|
+
var format = getOutputFormat(targets[0].dataset, opts);
|
|
25822
|
+
var datasets;
|
|
25823
|
+
if (format == PACKAGE_EXT && !runningInBrowser()) {
|
|
25824
|
+
// CLI .msx export captures the whole session: every dataset/layer in the
|
|
25825
|
+
// catalog ships in the snapshot, not just the -target subset. Targeted
|
|
25826
|
+
// layers come back visible (pinned) and stacked in the order matched by
|
|
25827
|
+
// -target; untargeted layers come along for the ride, hidden and parked
|
|
25828
|
+
// at the bottom of the GUI stack. This matches GUI snapshot semantics
|
|
25829
|
+
// and lets `mapshaper a.shp b.shp -target a -o foo.msx` produce a
|
|
25830
|
+
// shareable bundle without losing b.shp.
|
|
25831
|
+
datasets = prepareCatalogForCliPackExport(catalog, targets);
|
|
25832
|
+
} else {
|
|
25833
|
+
datasets = targets.map(function(target) {
|
|
25834
|
+
return utils.defaults({layers: target.layers}, target.dataset);
|
|
25835
|
+
});
|
|
25836
|
+
}
|
|
25494
25837
|
return exportDatasets(datasets, opts);
|
|
25495
25838
|
}
|
|
25496
25839
|
|
|
@@ -25737,6 +26080,62 @@ ${svg}
|
|
|
25737
26080
|
}
|
|
25738
26081
|
}
|
|
25739
26082
|
|
|
26083
|
+
// Prepare the full catalog for a CLI `-o foo.msx` export. Returns a
|
|
26084
|
+
// shallow-copied datasets/layers tree (the live model is left alone) where:
|
|
26085
|
+
//
|
|
26086
|
+
// - every dataset and every layer in the catalog is present, not just the
|
|
26087
|
+
// -target subset, so .msx round-trips the entire working session;
|
|
26088
|
+
// - layers matched by -target are marked `pinned: true` so the GUI shows
|
|
26089
|
+
// them on load (no `&display-all` URL flag needed);
|
|
26090
|
+
// - those targeted layers get menu_order values that follow the linear
|
|
26091
|
+
// -target list (first targeted = bottom of the stack, last = top),
|
|
26092
|
+
// matching the SVG draw order from the same -target line;
|
|
26093
|
+
// - untargeted layers get menu_order values below every targeted layer,
|
|
26094
|
+
// so they sit at the bottom of the GUI panel out of the way (they're
|
|
26095
|
+
// hidden, but if the user pins one later it doesn't pop above the
|
|
26096
|
+
// intended stack).
|
|
26097
|
+
//
|
|
26098
|
+
// Intra-dataset array order is preserved so re-importing with `mapshaper
|
|
26099
|
+
// foo.msx -target * -o bar.svg` still iterates layers in the order they
|
|
26100
|
+
// appeared during the original run.
|
|
26101
|
+
function prepareCatalogForCliPackExport(catalog, targets) {
|
|
26102
|
+
var targeted = new Set();
|
|
26103
|
+
targets.forEach(function(t) {
|
|
26104
|
+
t.layers.forEach(function(lyr) { targeted.add(lyr); });
|
|
26105
|
+
});
|
|
26106
|
+
var datasets = catalog.getDatasets();
|
|
26107
|
+
// Count untargeted layers globally so we can offset targeted layers'
|
|
26108
|
+
// menu_order to sit above them in a single consecutive range.
|
|
26109
|
+
var untargetedCount = 0;
|
|
26110
|
+
datasets.forEach(function(d) {
|
|
26111
|
+
d.layers.forEach(function(lyr) {
|
|
26112
|
+
if (!targeted.has(lyr)) untargetedCount++;
|
|
26113
|
+
});
|
|
26114
|
+
});
|
|
26115
|
+
var untargetedSeq = 0;
|
|
26116
|
+
return datasets.map(function(dataset) {
|
|
26117
|
+
var layers = dataset.layers.map(function(lyr) {
|
|
26118
|
+
var isTargeted = targeted.has(lyr);
|
|
26119
|
+
var menuOrder;
|
|
26120
|
+
if (isTargeted && lyr.target_id != null && lyr.target_id >= 0) {
|
|
26121
|
+
// target_id is 0-based across the whole -target list; offset past
|
|
26122
|
+
// the untargeted block so targeted layers occupy [untargetedCount+1
|
|
26123
|
+
// .. untargetedCount+N].
|
|
26124
|
+
menuOrder = untargetedCount + lyr.target_id + 1;
|
|
26125
|
+
} else {
|
|
26126
|
+
// Untargeted (or untargeted-shaped target_id): pack into the bottom
|
|
26127
|
+
// of the global stack, in catalog walk order.
|
|
26128
|
+
menuOrder = ++untargetedSeq;
|
|
26129
|
+
}
|
|
26130
|
+
return utils.defaults({
|
|
26131
|
+
pinned: isTargeted,
|
|
26132
|
+
menu_order: menuOrder
|
|
26133
|
+
}, lyr);
|
|
26134
|
+
});
|
|
26135
|
+
return utils.defaults({layers: layers}, dataset);
|
|
26136
|
+
});
|
|
26137
|
+
}
|
|
26138
|
+
|
|
25740
26139
|
var Export = /*#__PURE__*/Object.freeze({
|
|
25741
26140
|
__proto__: null,
|
|
25742
26141
|
assignUniqueFileNames: assignUniqueFileNames,
|
|
@@ -26874,26 +27273,71 @@ ${svg}
|
|
|
26874
27273
|
// show help if only a command name is given
|
|
26875
27274
|
argv.unshift('-help'); // kludge (assumes -help <command> syntax)
|
|
26876
27275
|
} else if (argv.length > 0 && !tokenLooksLikeCommand(argv[0]) && _default) {
|
|
26877
|
-
// if there are arguments before the first explicit command, use the default
|
|
26878
|
-
|
|
27276
|
+
// if there are arguments before the first explicit command, use the default
|
|
27277
|
+
// command. If _default is a function, let it inspect/mutate argv directly
|
|
27278
|
+
// (this lets callers route by file type, e.g. .txt -> -run).
|
|
27279
|
+
if (typeof _default == 'function') {
|
|
27280
|
+
_default(argv);
|
|
27281
|
+
} else {
|
|
27282
|
+
argv.unshift('-' + _default);
|
|
27283
|
+
}
|
|
26879
27284
|
}
|
|
26880
27285
|
|
|
27286
|
+
// snapshot the argv so we can record the source tokens consumed by each
|
|
27287
|
+
// command (used by the late-binding {{...}} interpolator)
|
|
27288
|
+
var argvSnapshot = argv.slice();
|
|
27289
|
+
var totalLen = argvSnapshot.length;
|
|
27290
|
+
|
|
26881
27291
|
while (argv.length > 0) {
|
|
27292
|
+
var consumedBefore = totalLen - argv.length;
|
|
26882
27293
|
cmdName = readCommandName(argv);
|
|
26883
27294
|
if (!cmdName) {
|
|
26884
27295
|
stop$1("Invalid command:", argv[0]);
|
|
26885
27296
|
}
|
|
26886
27297
|
cmdDef = findCommandDefn(cmdName, commandDefs) || null;
|
|
27298
|
+
// Look ahead at the option tokens this command will consume. If any
|
|
27299
|
+
// contains a {{...}} placeholder, the late-binding interpolator (in
|
|
27300
|
+
// mapshaper-run-commands.mjs) will re-parse this command after
|
|
27301
|
+
// substitution, so we skip validation here to avoid premature errors
|
|
27302
|
+
// on un-interpolated tokens.
|
|
27303
|
+
var lookaheadTokens = peekCommandTokens(argv);
|
|
27304
|
+
var deferValidate = lookaheadTokens.some(tokenContainsPlaceholder);
|
|
26887
27305
|
if (!cmdDef) {
|
|
26888
27306
|
cmd = parseUnknownCommandOptions(argv, cmdName);
|
|
26889
27307
|
} else {
|
|
26890
|
-
cmd = parseCommandOptions(argv, cmdDef);
|
|
26891
|
-
}
|
|
27308
|
+
cmd = parseCommandOptions(argv, cmdDef, deferValidate);
|
|
27309
|
+
}
|
|
27310
|
+
var consumedAfter = totalLen - argv.length;
|
|
27311
|
+
// Stash the source tokens for the late-binding {{...}} interpolator.
|
|
27312
|
+
// Defined as non-enumerable so existing tests that deep-equal parsed
|
|
27313
|
+
// commands aren't affected.
|
|
27314
|
+
Object.defineProperty(cmd, '_tokens', {
|
|
27315
|
+
value: argvSnapshot.slice(consumedBefore, consumedAfter),
|
|
27316
|
+
enumerable: false,
|
|
27317
|
+
writable: true,
|
|
27318
|
+
configurable: true
|
|
27319
|
+
});
|
|
26892
27320
|
commands.push(cmd);
|
|
26893
27321
|
}
|
|
26894
27322
|
return commands;
|
|
26895
27323
|
|
|
26896
|
-
|
|
27324
|
+
// Return the tokens (without removing them) that the next command would
|
|
27325
|
+
// consume. Boundary is the next command name, matching the rule in
|
|
27326
|
+
// parseCommandOptions.
|
|
27327
|
+
function peekCommandTokens(argv) {
|
|
27328
|
+
var out = [];
|
|
27329
|
+
for (var i = 0; i < argv.length; i++) {
|
|
27330
|
+
if (tokenLooksLikeCommand(argv[i])) break;
|
|
27331
|
+
out.push(argv[i]);
|
|
27332
|
+
}
|
|
27333
|
+
return out;
|
|
27334
|
+
}
|
|
27335
|
+
|
|
27336
|
+
function tokenContainsPlaceholder(s) {
|
|
27337
|
+
return typeof s == 'string' && s.indexOf('{{') !== -1;
|
|
27338
|
+
}
|
|
27339
|
+
|
|
27340
|
+
function parseCommandOptions(argv, cmdDef, deferValidate) {
|
|
26897
27341
|
var cmd = {
|
|
26898
27342
|
name: cmdDef.name,
|
|
26899
27343
|
options: {},
|
|
@@ -26905,10 +27349,10 @@ ${svg}
|
|
|
26905
27349
|
}
|
|
26906
27350
|
|
|
26907
27351
|
try {
|
|
26908
|
-
if (cmd._.length > 0) {
|
|
27352
|
+
if (cmd._.length > 0 && !deferValidate) {
|
|
26909
27353
|
readDefaultOptionValue(cmd, cmdDef);
|
|
26910
27354
|
}
|
|
26911
|
-
if (cmdDef.validate) {
|
|
27355
|
+
if (cmdDef.validate && !deferValidate) {
|
|
26912
27356
|
cmdDef.validate(cmd);
|
|
26913
27357
|
}
|
|
26914
27358
|
delete cmd.options._; // kludge to remove -o placeholder option
|
|
@@ -27385,7 +27829,16 @@ ${svg}
|
|
|
27385
27829
|
|
|
27386
27830
|
parser.section('I/O commands');
|
|
27387
27831
|
|
|
27388
|
-
|
|
27832
|
+
// When the command line begins with a non-command argument, route .txt files
|
|
27833
|
+
// to "-run <path>" (command files) and everything else to "-i <path>" (data
|
|
27834
|
+
// files). The actual file-or-expression check happens inside cmd.run.
|
|
27835
|
+
parser.default(function(argv) {
|
|
27836
|
+
if (isPotentialCommandFile(argv[0])) {
|
|
27837
|
+
argv.unshift('-run');
|
|
27838
|
+
} else {
|
|
27839
|
+
argv.unshift('-i');
|
|
27840
|
+
}
|
|
27841
|
+
});
|
|
27389
27842
|
|
|
27390
27843
|
parser.command('i')
|
|
27391
27844
|
.describe('input one or more files')
|
|
@@ -27402,6 +27855,10 @@ ${svg}
|
|
|
27402
27855
|
describe: 'import files to separate layers with shared topology',
|
|
27403
27856
|
type: 'flag'
|
|
27404
27857
|
})
|
|
27858
|
+
.option('batch-mode', {
|
|
27859
|
+
describe: 'apply subsequent commands separately to each input file',
|
|
27860
|
+
type: 'flag'
|
|
27861
|
+
})
|
|
27405
27862
|
.option('merge-files', {
|
|
27406
27863
|
// describe: 'merge features from compatible files into the same layer',
|
|
27407
27864
|
type: 'flag'
|
|
@@ -28033,7 +28490,7 @@ ${svg}
|
|
|
28033
28490
|
});
|
|
28034
28491
|
|
|
28035
28492
|
parser.command('dissolve')
|
|
28036
|
-
.describe('merge features within a layer')
|
|
28493
|
+
.describe('merge features within a layer (repairs polygon topology)')
|
|
28037
28494
|
.example('Dissolve all polygons in a feature layer into a single polygon\n' +
|
|
28038
28495
|
'$ mapshaper states.shp -dissolve -o country.shp')
|
|
28039
28496
|
.example('Generate state-level polygons by dissolving a layer of counties\n' +
|
|
@@ -28060,19 +28517,33 @@ ${svg}
|
|
|
28060
28517
|
type: 'flag',
|
|
28061
28518
|
describe: '[points] use 2D math to find centroids of latlong points'
|
|
28062
28519
|
})
|
|
28520
|
+
.option('gap-fill-area', {
|
|
28521
|
+
describe: '[polygons] threshold for filling gaps, e.g. 1.5km2',
|
|
28522
|
+
type: 'area'
|
|
28523
|
+
})
|
|
28524
|
+
.option('sliver-control', sliverControlOpt)
|
|
28525
|
+
.option('allow-overlaps', {
|
|
28526
|
+
describe: '[polygons] allow output polygons to overlap (disables gap fill)',
|
|
28527
|
+
type: 'flag'
|
|
28528
|
+
})
|
|
28529
|
+
.option('no-repair', {
|
|
28530
|
+
describe: '[polygons] skip topology repair (faster; assumes clean input)',
|
|
28531
|
+
type: 'flag'
|
|
28532
|
+
})
|
|
28533
|
+
.option('snap-interval', snapIntervalOpt)
|
|
28534
|
+
.option('no-snap', noSnapOpt)
|
|
28063
28535
|
.option('name', nameOpt)
|
|
28064
28536
|
.option('target', targetOpt)
|
|
28065
28537
|
.option('no-replace', noReplaceOpt);
|
|
28066
28538
|
|
|
28067
28539
|
|
|
28540
|
+
// -dissolve2 is now an alias for -dissolve (the topology repair behavior of
|
|
28541
|
+
// -dissolve2 is now the default behavior of -dissolve). Kept for backward
|
|
28542
|
+
// compatibility; prints a deprecation notice when used.
|
|
28068
28543
|
parser.command('dissolve2')
|
|
28069
|
-
.describe('
|
|
28544
|
+
.describe('alias for -dissolve (deprecated)')
|
|
28070
28545
|
.option('field', {}) // old arg handled by dissolve function
|
|
28071
28546
|
.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
28547
|
.option('calc', calcOpt)
|
|
28077
28548
|
.option('sum-fields', sumFieldsOpt)
|
|
28078
28549
|
.option('copy-fields', copyFieldsOpt)
|
|
@@ -28175,7 +28646,7 @@ ${svg}
|
|
|
28175
28646
|
parser.command('each')
|
|
28176
28647
|
.describe('create/update/delete data fields using a JS expression')
|
|
28177
28648
|
.example('Add two calculated data fields to a layer of U.S. counties\n' +
|
|
28178
|
-
'$ mapshaper counties.shp -each \'STATE_FIPS=CNTY_FIPS.substr(0, 2), AREA
|
|
28649
|
+
'$ mapshaper counties.shp -each \'STATE_FIPS=CNTY_FIPS.substr(0, 2), AREA=this.area\'')
|
|
28179
28650
|
.option('expression', {
|
|
28180
28651
|
DEFAULT: true,
|
|
28181
28652
|
describe: 'JS expression to apply to each target feature'
|
|
@@ -28330,7 +28801,8 @@ ${svg}
|
|
|
28330
28801
|
describe: 'create a polygon to match the outline of the graticule',
|
|
28331
28802
|
type: 'flag'
|
|
28332
28803
|
})
|
|
28333
|
-
.option('name', nameOpt)
|
|
28804
|
+
.option('name', nameOpt)
|
|
28805
|
+
.option('target', targetOpt);
|
|
28334
28806
|
|
|
28335
28807
|
|
|
28336
28808
|
// for testing grid update
|
|
@@ -28740,7 +29212,7 @@ ${svg}
|
|
|
28740
29212
|
})
|
|
28741
29213
|
.option('interval', {
|
|
28742
29214
|
// alias: 'i',
|
|
28743
|
-
describe: 'output resolution as a distance (e.g.
|
|
29215
|
+
describe: 'output resolution as a distance (e.g. 100m)',
|
|
28744
29216
|
type: 'distance'
|
|
28745
29217
|
})
|
|
28746
29218
|
/*
|
|
@@ -29352,10 +29824,11 @@ ${svg}
|
|
|
29352
29824
|
});
|
|
29353
29825
|
|
|
29354
29826
|
parser.command('run')
|
|
29355
|
-
.describe('
|
|
29827
|
+
.describe('run commands from a command file or JS expression')
|
|
29356
29828
|
.option('expression', {
|
|
29357
29829
|
DEFAULT: true,
|
|
29358
|
-
|
|
29830
|
+
label: '<file|expression>',
|
|
29831
|
+
describe: 'path to a .txt command file, or a JS expression/template'
|
|
29359
29832
|
})
|
|
29360
29833
|
// deprecated
|
|
29361
29834
|
.option('commands', {alias_to: 'expression'})
|
|
@@ -29502,7 +29975,7 @@ ${svg}
|
|
|
29502
29975
|
parser.command('calc')
|
|
29503
29976
|
.describe('calculate statistics about the features in a layer')
|
|
29504
29977
|
.example('Calculate the total area of a polygon layer\n' +
|
|
29505
|
-
'$ mapshaper polygons.shp -calc \'sum(
|
|
29978
|
+
'$ mapshaper polygons.shp -calc \'sum(this.area)\'')
|
|
29506
29979
|
.example('Count census blocks in NY with zero population\n' +
|
|
29507
29980
|
'$ mapshaper ny-census-blocks.shp -calc \'count()\' where=\'POPULATION == 0\'')
|
|
29508
29981
|
.validate(validateExpressionOpt)
|
|
@@ -29527,6 +30000,14 @@ ${svg}
|
|
|
29527
30000
|
}
|
|
29528
30001
|
});
|
|
29529
30002
|
|
|
30003
|
+
parser.command('defaults')
|
|
30004
|
+
.describe('set {{VAR}} interpolation variables only if not already set')
|
|
30005
|
+
.option('values', {
|
|
30006
|
+
DEFAULT: {
|
|
30007
|
+
multi_arg: true
|
|
30008
|
+
}
|
|
30009
|
+
});
|
|
30010
|
+
|
|
29530
30011
|
parser.command('encodings')
|
|
29531
30012
|
.describe('print list of supported text encodings (for .dbf import)');
|
|
29532
30013
|
|
|
@@ -29571,6 +30052,14 @@ ${svg}
|
|
|
29571
30052
|
parser.command('quiet')
|
|
29572
30053
|
.describe('inhibit console messages');
|
|
29573
30054
|
|
|
30055
|
+
parser.command('vars')
|
|
30056
|
+
.describe('define variables for {{VAR}} interpolation (overwrites)')
|
|
30057
|
+
.option('values', {
|
|
30058
|
+
DEFAULT: {
|
|
30059
|
+
multi_arg: true
|
|
30060
|
+
}
|
|
30061
|
+
});
|
|
30062
|
+
|
|
29574
30063
|
parser.command('verbose')
|
|
29575
30064
|
.describe('print verbose processing messages');
|
|
29576
30065
|
|
|
@@ -33418,7 +33907,12 @@ ${svg}
|
|
|
33418
33907
|
// load external files (e.g. epsg definitions) if needed in GUI
|
|
33419
33908
|
await initProjLibrary({crs: o.crs_string});
|
|
33420
33909
|
o.crs = parseCrsString$1(o.crs_string);
|
|
33910
|
+
} else if (o.wkt1) {
|
|
33911
|
+
// Shapefile-sourced snapshots typically carry wkt1 but no crs_string;
|
|
33912
|
+
// reconstitute the proj object from it so direct readers of info.crs work.
|
|
33913
|
+
o.crs = parsePrj(o.wkt1);
|
|
33421
33914
|
} else if (o.prj) {
|
|
33915
|
+
// legacy field name; older snapshots may have stored the .prj content here
|
|
33422
33916
|
o.crs = parsePrj(o.prj);
|
|
33423
33917
|
}
|
|
33424
33918
|
return o;
|
|
@@ -33506,22 +34000,39 @@ ${svg}
|
|
|
33506
34000
|
return target;
|
|
33507
34001
|
};
|
|
33508
34002
|
|
|
33509
|
-
//
|
|
34003
|
+
// Replace any inline data strings (JSON objects/arrays or comma-delimited
|
|
34004
|
+
// text) with synthetic filenames and stash the content in @cache so the
|
|
34005
|
+
// downstream importer can read it as if it had come from a file.
|
|
33510
34006
|
function convertDataObjects(files, cache) {
|
|
33511
|
-
var
|
|
33512
|
-
|
|
33513
|
-
if (
|
|
33514
|
-
|
|
33515
|
-
|
|
34007
|
+
var slots = files.map(classifyInlineData);
|
|
34008
|
+
var inlineCount = slots.filter(Boolean).length;
|
|
34009
|
+
if (inlineCount === 0) return;
|
|
34010
|
+
if (inlineCount > 1) {
|
|
34011
|
+
// ensure unique filenames when multiple inline strings are passed together
|
|
34012
|
+
var names = slots.filter(Boolean).map(function(s) { return s.filename; });
|
|
34013
|
+
var unique = utils.uniqifyNames(names, formatVersionedFileName);
|
|
34014
|
+
var idx = 0;
|
|
34015
|
+
slots.forEach(function(slot) {
|
|
34016
|
+
if (slot) slot.filename = unique[idx++];
|
|
34017
|
+
});
|
|
33516
34018
|
}
|
|
33517
|
-
|
|
33518
|
-
if (!
|
|
33519
|
-
|
|
33520
|
-
|
|
33521
|
-
files[i] = name;
|
|
34019
|
+
slots.forEach(function(slot, i) {
|
|
34020
|
+
if (!slot) return;
|
|
34021
|
+
cache[slot.filename] = slot.content;
|
|
34022
|
+
files[i] = slot.filename;
|
|
33522
34023
|
});
|
|
33523
34024
|
}
|
|
33524
34025
|
|
|
34026
|
+
function classifyInlineData(str) {
|
|
34027
|
+
if (stringLooksLikeJSON(str)) {
|
|
34028
|
+
return {filename: 'layer.json', content: str};
|
|
34029
|
+
}
|
|
34030
|
+
if (stringLooksLikeCsv(str)) {
|
|
34031
|
+
return {filename: 'layer.csv', content: unescapeInlineCsv(str)};
|
|
34032
|
+
}
|
|
34033
|
+
return null;
|
|
34034
|
+
}
|
|
34035
|
+
|
|
33525
34036
|
async function importMshpFile(file, catalog, opts) {
|
|
33526
34037
|
var buf = cli.readFile(file, null, opts.input);
|
|
33527
34038
|
var obj = await unpackSessionData(buf);
|
|
@@ -34100,6 +34611,7 @@ ${svg}
|
|
|
34100
34611
|
var job = {
|
|
34101
34612
|
catalog: catalog || new Catalog(),
|
|
34102
34613
|
defs: {},
|
|
34614
|
+
vars: {},
|
|
34103
34615
|
settings: {},
|
|
34104
34616
|
input_files: []
|
|
34105
34617
|
};
|
|
@@ -34135,6 +34647,7 @@ ${svg}
|
|
|
34135
34647
|
stashVar('VERBOSE', job.settings.VERBOSE || cmd.verbose);
|
|
34136
34648
|
stashVar('QUIET', job.settings.QUIET || cmd.quiet);
|
|
34137
34649
|
stashVar('defs', job.defs);
|
|
34650
|
+
stashVar('vars', job.vars);
|
|
34138
34651
|
stashVar('input_files', job.input_files);
|
|
34139
34652
|
}
|
|
34140
34653
|
|
|
@@ -41741,16 +42254,239 @@ ${svg}
|
|
|
41741
42254
|
compiled(null, defs);
|
|
41742
42255
|
};
|
|
41743
42256
|
|
|
41744
|
-
//
|
|
41745
|
-
|
|
41746
|
-
|
|
41747
|
-
|
|
41748
|
-
|
|
41749
|
-
|
|
41750
|
-
|
|
42257
|
+
// Variable name pattern. Matches simple identifiers: must start with a letter
|
|
42258
|
+
// or underscore, followed by letters, digits or underscores.
|
|
42259
|
+
var VAR_NAME_RXP = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
42260
|
+
|
|
42261
|
+
// Pattern that matches a {{...}} placeholder in command text. The optional
|
|
42262
|
+
// leading character is a backslash (escape) which keeps the placeholder
|
|
42263
|
+
// literal. The braces themselves cannot appear inside a placeholder.
|
|
42264
|
+
//
|
|
42265
|
+
// Group 1: the leading escape (if present)
|
|
42266
|
+
// Group 2: the contents between {{ and }}
|
|
42267
|
+
//
|
|
42268
|
+
var PLACEHOLDER_RXP = /(\\?)\{\{([^{}]+?)\}\}/g;
|
|
42269
|
+
|
|
42270
|
+
// Pattern matching a "KEY=value" inline -vars argument.
|
|
42271
|
+
var ASSIGNMENT_RXP = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
|
|
42272
|
+
|
|
42273
|
+
// Returns true if @s is a valid mapshaper variable name.
|
|
42274
|
+
function isValidVarName(s) {
|
|
42275
|
+
return typeof s == 'string' && VAR_NAME_RXP.test(s);
|
|
42276
|
+
}
|
|
42277
|
+
|
|
42278
|
+
// Returns true if @str might contain a {{...}} placeholder. This is a cheap
|
|
42279
|
+
// fast-path test used to skip re-parsing for commands that have no
|
|
42280
|
+
// placeholders. False positives (e.g. literal "{{" inside a quoted JS
|
|
42281
|
+
// expression) just trigger an interpolation pass that finds nothing to do.
|
|
42282
|
+
function containsPlaceholder(str) {
|
|
42283
|
+
return typeof str == 'string' && str.indexOf('{{') !== -1;
|
|
42284
|
+
}
|
|
42285
|
+
|
|
42286
|
+
// Validate the parsed contents of a -vars JSON file. The file must contain a
|
|
42287
|
+
// flat object whose values are primitive (string, number, boolean, null).
|
|
42288
|
+
// Throws on invalid input. Returns the same object.
|
|
42289
|
+
function validateVarsObject(obj, source) {
|
|
42290
|
+
source = source || '<vars>';
|
|
42291
|
+
if (!utils.isObject(obj) || Array.isArray(obj)) {
|
|
42292
|
+
stop$1('Invalid vars file (' + source + '): expected an object at the top level');
|
|
42293
|
+
}
|
|
42294
|
+
Object.keys(obj).forEach(function(key) {
|
|
42295
|
+
if (!isValidVarName(key)) {
|
|
42296
|
+
stop$1('Invalid var name in ' + source + ': ' + JSON.stringify(key));
|
|
42297
|
+
}
|
|
42298
|
+
var v = obj[key];
|
|
42299
|
+
if (v !== null && typeof v != 'string' && typeof v != 'number' &&
|
|
42300
|
+
typeof v != 'boolean') {
|
|
42301
|
+
stop$1('Invalid value for var "' + key + '" in ' + source +
|
|
42302
|
+
': expected a string, number, boolean or null');
|
|
42303
|
+
}
|
|
42304
|
+
});
|
|
42305
|
+
return obj;
|
|
42306
|
+
}
|
|
42307
|
+
|
|
42308
|
+
// Resolve a single -vars argument. Each argument is either:
|
|
42309
|
+
// * an inline assignment "KEY=value" (key must be a valid var name)
|
|
42310
|
+
// * a path to a JSON file containing a flat object of vars
|
|
42311
|
+
//
|
|
42312
|
+
// @arg: the raw argument string
|
|
42313
|
+
// @cache: optional input cache (passed to cli.readFile so files dropped into
|
|
42314
|
+
// the GUI can be resolved by name)
|
|
42315
|
+
// @merge: target object that receives the resolved entries
|
|
42316
|
+
//
|
|
42317
|
+
function resolveVarsArg(arg, cache, merge) {
|
|
42318
|
+
var assignment = ASSIGNMENT_RXP.exec(arg);
|
|
42319
|
+
if (assignment) {
|
|
42320
|
+
merge[assignment[1]] = assignment[2];
|
|
42321
|
+
return;
|
|
42322
|
+
}
|
|
42323
|
+
// Treat as a JSON file path
|
|
42324
|
+
cli.checkFileExists(arg, cache);
|
|
42325
|
+
var content = cli.readFile(arg, 'utf8', cache);
|
|
42326
|
+
var obj;
|
|
42327
|
+
try {
|
|
42328
|
+
obj = JSON.parse(content);
|
|
42329
|
+
} catch(e) {
|
|
42330
|
+
stop$1('Failed to parse vars file (' + arg + '): ' + e.message);
|
|
42331
|
+
}
|
|
42332
|
+
validateVarsObject(obj, arg);
|
|
42333
|
+
Object.keys(obj).forEach(function(key) {
|
|
42334
|
+
merge[key] = obj[key];
|
|
42335
|
+
});
|
|
42336
|
+
}
|
|
42337
|
+
|
|
42338
|
+
// Resolve an array of -vars arguments into a flat scope object. Later
|
|
42339
|
+
// arguments override earlier ones.
|
|
42340
|
+
function parseVarsArgs(args, cache) {
|
|
42341
|
+
var scope = {};
|
|
42342
|
+
if (!Array.isArray(args)) return scope;
|
|
42343
|
+
args.forEach(function(arg) {
|
|
42344
|
+
resolveVarsArg(arg, cache, scope);
|
|
42345
|
+
});
|
|
42346
|
+
return scope;
|
|
42347
|
+
}
|
|
42348
|
+
|
|
42349
|
+
// Look up env.* variables. Throws in environments without process.env.
|
|
42350
|
+
function lookupEnvVar(name) {
|
|
42351
|
+
if (typeof process == 'undefined' || !process.env) {
|
|
42352
|
+
stop$1('Environment variables are not available in this context');
|
|
42353
|
+
}
|
|
42354
|
+
return process.env[name];
|
|
42355
|
+
}
|
|
42356
|
+
|
|
42357
|
+
// Resolve a single placeholder expression to a string. Recognised forms:
|
|
42358
|
+
// VAR -> vars[VAR] if present, else defs[VAR]
|
|
42359
|
+
// env.VAR -> process.env[VAR]
|
|
42360
|
+
//
|
|
42361
|
+
// vars is the templating-scope object (-vars / -defaults writes).
|
|
42362
|
+
// defs is the expression-scope object (-define / -calc / -include /
|
|
42363
|
+
// -require / -colorizer writes). The fallback exists so that
|
|
42364
|
+
// "-define base = 'out'" -> "-o {{base}}.geojson" and
|
|
42365
|
+
// "-calc 'N = count()'" -> "-if '{{N}} > 100'" keep working without
|
|
42366
|
+
// the user having to know which scope a value lives in.
|
|
42367
|
+
//
|
|
42368
|
+
// Throws on undefined names, invalid syntax, or non-primitive values.
|
|
42369
|
+
//
|
|
42370
|
+
function resolvePlaceholder(expr, vars, defs) {
|
|
42371
|
+
expr = expr.trim();
|
|
42372
|
+
var envMatch = /^env\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(expr);
|
|
42373
|
+
var val, source;
|
|
42374
|
+
if (envMatch) {
|
|
42375
|
+
val = lookupEnvVar(envMatch[1]);
|
|
42376
|
+
if (val === undefined || val === null) {
|
|
42377
|
+
stop$1('Undefined environment variable: ' + envMatch[1]);
|
|
42378
|
+
}
|
|
42379
|
+
return String(val);
|
|
42380
|
+
}
|
|
42381
|
+
if (!isValidVarName(expr)) {
|
|
42382
|
+
stop$1('Invalid variable reference: {{' + expr + '}}');
|
|
42383
|
+
}
|
|
42384
|
+
if (vars && expr in vars) {
|
|
42385
|
+
source = vars;
|
|
42386
|
+
} else if (defs && expr in defs) {
|
|
42387
|
+
source = defs;
|
|
42388
|
+
} else {
|
|
42389
|
+
stop$1('Undefined variable: ' + expr);
|
|
42390
|
+
}
|
|
42391
|
+
val = source[expr];
|
|
42392
|
+
if (val === null || val === undefined) {
|
|
42393
|
+
stop$1('Undefined variable: ' + expr);
|
|
42394
|
+
}
|
|
42395
|
+
if (typeof val != 'string' && typeof val != 'number' &&
|
|
42396
|
+
typeof val != 'boolean') {
|
|
42397
|
+
stop$1('Variable {{' + expr + '}} is not a primitive value (got ' +
|
|
42398
|
+
(typeof val) + ')');
|
|
42399
|
+
}
|
|
42400
|
+
return String(val);
|
|
42401
|
+
}
|
|
42402
|
+
|
|
42403
|
+
// Substitute {{...}} placeholders in @str.
|
|
42404
|
+
//
|
|
42405
|
+
// Two call signatures, kept for backward compatibility:
|
|
42406
|
+
// interpolateString(str, vars, defs) -- preferred, two-store form
|
|
42407
|
+
// interpolateString(str, defs) -- legacy, single-store form
|
|
42408
|
+
//
|
|
42409
|
+
// In the legacy form, the second argument is treated as the expression
|
|
42410
|
+
// scope (defs); there is no template scope. New callers should use the
|
|
42411
|
+
// two-store form.
|
|
42412
|
+
//
|
|
42413
|
+
// Placeholders preceded by a backslash are left literal (with the
|
|
42414
|
+
// backslash removed). Substitution is single-pass (no recursion) so
|
|
42415
|
+
// values containing "{{...}}" do not trigger further interpolation.
|
|
42416
|
+
//
|
|
42417
|
+
function interpolateString(str, varsOrDefs, defsArg) {
|
|
42418
|
+
if (typeof str != 'string') return str;
|
|
42419
|
+
var vars, defs;
|
|
42420
|
+
if (arguments.length >= 3) {
|
|
42421
|
+
vars = varsOrDefs;
|
|
42422
|
+
defs = defsArg;
|
|
42423
|
+
} else {
|
|
42424
|
+
vars = null;
|
|
42425
|
+
defs = varsOrDefs;
|
|
42426
|
+
}
|
|
42427
|
+
return str.replace(PLACEHOLDER_RXP, function(match, escape, expr) {
|
|
42428
|
+
if (escape === '\\') return '{{' + expr + '}}';
|
|
42429
|
+
return resolvePlaceholder(expr, vars, defs);
|
|
42430
|
+
});
|
|
42431
|
+
}
|
|
42432
|
+
|
|
42433
|
+
// -vars KEY=value [KEY=value ...] inline assignments
|
|
42434
|
+
// -vars file.json [more ...] load primitives from a flat JSON object
|
|
42435
|
+
// Mixed forms allowed; later args override earlier ones.
|
|
42436
|
+
//
|
|
42437
|
+
// Writes into job.vars, the templating-scope object read by {{X}}
|
|
42438
|
+
// interpolation. Values written here are NOT visible by bare name in JS
|
|
42439
|
+
// expressions (-each, -filter, -define, etc.); use -define for that.
|
|
42440
|
+
// {{X}} substitution falls back to job.defs if a name is missing from
|
|
42441
|
+
// vars, so values set by -define / -calc / -include are still
|
|
42442
|
+
// referenceable from {{X}}.
|
|
42443
|
+
cmd.vars = function(job, opts) {
|
|
42444
|
+
var values = (opts && opts.values) || [];
|
|
42445
|
+
if (!values.length) {
|
|
42446
|
+
stop$1('-vars requires one or more KEY=value or file.json arguments');
|
|
42447
|
+
}
|
|
42448
|
+
var parsed = parseVarsArgs(values, opts && opts.input);
|
|
42449
|
+
if (!job.vars) job.vars = {};
|
|
42450
|
+
Object.keys(parsed).forEach(function(key) {
|
|
42451
|
+
job.vars[key] = parsed[key];
|
|
42452
|
+
});
|
|
42453
|
+
};
|
|
42454
|
+
|
|
42455
|
+
// -defaults KEY=value [KEY=value ...] set-if-unset
|
|
42456
|
+
// Same syntax as -vars, but a key is only assigned if it is not already
|
|
42457
|
+
// present in job.vars. Lets a command file declare overridable defaults
|
|
42458
|
+
// that a CLI -vars can pre-empt.
|
|
42459
|
+
cmd.defaults = function(job, opts) {
|
|
42460
|
+
var values = (opts && opts.values) || [];
|
|
42461
|
+
if (!values.length) {
|
|
42462
|
+
stop$1('-defaults requires one or more KEY=value or file.json arguments');
|
|
42463
|
+
}
|
|
42464
|
+
var parsed = parseVarsArgs(values, opts && opts.input);
|
|
42465
|
+
if (!job.vars) job.vars = {};
|
|
42466
|
+
Object.keys(parsed).forEach(function(key) {
|
|
42467
|
+
if (!(key in job.vars)) {
|
|
42468
|
+
job.vars[key] = parsed[key];
|
|
42469
|
+
}
|
|
41751
42470
|
});
|
|
41752
42471
|
};
|
|
41753
42472
|
|
|
42473
|
+
// -dissolve2 is now an alias for -dissolve. The repair-on-by-default behavior
|
|
42474
|
+
// of -dissolve2 has been promoted to be the default behavior of -dissolve;
|
|
42475
|
+
// the legacy fast-dissolve algorithm is available via -dissolve no-repair.
|
|
42476
|
+
//
|
|
42477
|
+
// This alias prints a deprecation notice and forwards to cmd.dissolve.
|
|
42478
|
+
//
|
|
42479
|
+
var deprecationWarned = false;
|
|
42480
|
+
|
|
42481
|
+
cmd.dissolve2 = function(layers, dataset, opts) {
|
|
42482
|
+
if (!deprecationWarned && !(opts && opts.quiet)) {
|
|
42483
|
+
message('This command has been merged into -dissolve and is deprecated. ' +
|
|
42484
|
+
'Use -dissolve (or -dissolve no-repair for the legacy fast algorithm).');
|
|
42485
|
+
deprecationWarned = true;
|
|
42486
|
+
}
|
|
42487
|
+
return cmd.dissolve(layers, dataset, opts);
|
|
42488
|
+
};
|
|
42489
|
+
|
|
41754
42490
|
// Returns a function for filtering multiple source-table records
|
|
41755
42491
|
// (used by -join command)
|
|
41756
42492
|
function getJoinFilter(data, exp) {
|
|
@@ -48414,8 +49150,139 @@ ${svg}
|
|
|
48414
49150
|
return parsed;
|
|
48415
49151
|
}
|
|
48416
49152
|
|
|
49153
|
+
// Parse the text content of a mapshaper command file (e.g. "commands.txt")
|
|
49154
|
+
// into a single normalized command string suitable for parseCommands().
|
|
49155
|
+
//
|
|
49156
|
+
// Command file syntax (a superset of the equivalent shell command line):
|
|
49157
|
+
// - Optional leading "mapshaper" magic word (used by the file-type sniffer).
|
|
49158
|
+
// - "#" begins a comment that runs to the end of the line. Comments are
|
|
49159
|
+
// ignored unless the "#" appears inside a quoted string.
|
|
49160
|
+
// - Newlines are command separators (unless they fall inside a quoted
|
|
49161
|
+
// string). A trailing backslash on a line is stripped, so shell-style
|
|
49162
|
+
// "\" line continuations are accepted but not required.
|
|
49163
|
+
// - Commands must begin with "-" (e.g. "-i", "-target"). Lines that do
|
|
49164
|
+
// not start with "-" are treated as continuations of the previous
|
|
49165
|
+
// command.
|
|
49166
|
+
// - As on the CLI, an initial command is implied for the first bare token
|
|
49167
|
+
// after the optional "mapshaper" word: a .txt file routes to
|
|
49168
|
+
// "-run <path>" (command file), and any other bare token routes to
|
|
49169
|
+
// "-i <token>" (data file).
|
|
49170
|
+
//
|
|
49171
|
+
// "{{VAR}}" placeholders are substituted at execution time, against the
|
|
49172
|
+
// live job.vars object (with job.defs as a fallback for values written by
|
|
49173
|
+
// -define / -calc / -include). See mapshaper-vars-utils.mjs and the
|
|
49174
|
+
// late-binding hook in mapshaper-run-commands.mjs.
|
|
49175
|
+
//
|
|
49176
|
+
function parseCommandFileContent(content) {
|
|
49177
|
+
if (typeof content != 'string') {
|
|
49178
|
+
content = String(content || '');
|
|
49179
|
+
}
|
|
49180
|
+
// Strip BOM if present
|
|
49181
|
+
if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1);
|
|
49182
|
+
var commands = groupCommandFileLines(extractLogicalLines(content));
|
|
49183
|
+
return commands.join(' ');
|
|
49184
|
+
}
|
|
49185
|
+
|
|
49186
|
+
// Walk command file content into logical lines, respecting quoted strings and
|
|
49187
|
+
// stripping "#" comments. Quoted-string contents (including embedded
|
|
49188
|
+
// newlines) are preserved verbatim.
|
|
49189
|
+
function extractLogicalLines(content) {
|
|
49190
|
+
var lines = [];
|
|
49191
|
+
var current = '';
|
|
49192
|
+
var quote = null; // null, "'", or '"'
|
|
49193
|
+
var inComment = false;
|
|
49194
|
+
|
|
49195
|
+
for (var i = 0; i < content.length; i++) {
|
|
49196
|
+
var c = content.charAt(i);
|
|
49197
|
+
if (inComment) {
|
|
49198
|
+
if (c === '\n') {
|
|
49199
|
+
inComment = false;
|
|
49200
|
+
lines.push(current);
|
|
49201
|
+
current = '';
|
|
49202
|
+
}
|
|
49203
|
+
continue;
|
|
49204
|
+
}
|
|
49205
|
+
if (quote) {
|
|
49206
|
+
current += c;
|
|
49207
|
+
if (c === quote) {
|
|
49208
|
+
var bs = 0;
|
|
49209
|
+
for (var j = i - 1; j >= 0 && content.charAt(j) === '\\'; j--) bs++;
|
|
49210
|
+
if (bs % 2 === 0) quote = null;
|
|
49211
|
+
}
|
|
49212
|
+
continue;
|
|
49213
|
+
}
|
|
49214
|
+
if (c === '#') {
|
|
49215
|
+
inComment = true;
|
|
49216
|
+
} else if (c === "'" || c === '"') {
|
|
49217
|
+
quote = c;
|
|
49218
|
+
current += c;
|
|
49219
|
+
} else if (c === '\n') {
|
|
49220
|
+
lines.push(current);
|
|
49221
|
+
current = '';
|
|
49222
|
+
} else {
|
|
49223
|
+
current += c;
|
|
49224
|
+
}
|
|
49225
|
+
}
|
|
49226
|
+
if (current.length > 0) lines.push(current);
|
|
49227
|
+
if (quote) {
|
|
49228
|
+
stop$1('Unterminated quoted string in command file');
|
|
49229
|
+
}
|
|
49230
|
+
return lines;
|
|
49231
|
+
}
|
|
49232
|
+
|
|
49233
|
+
// Group an array of logical lines into command strings:
|
|
49234
|
+
// - Strip trailing-backslash continuations.
|
|
49235
|
+
// - Strip a leading "mapshaper" magic word from the first non-blank line.
|
|
49236
|
+
// - Lines starting with "-" begin a new command.
|
|
49237
|
+
// - Other lines are continuations of the previous command.
|
|
49238
|
+
// - The very first bare token is treated as an implicit -i (data file)
|
|
49239
|
+
// or -run (command file), matching CLI behavior.
|
|
49240
|
+
function groupCommandFileLines(lines) {
|
|
49241
|
+
var commands = [];
|
|
49242
|
+
var cur = '';
|
|
49243
|
+
var sawMagicWord = false;
|
|
49244
|
+
for (var k = 0; k < lines.length; k++) {
|
|
49245
|
+
var line = lines[k];
|
|
49246
|
+
line = line.replace(/\s*\\\s*$/, '').trim();
|
|
49247
|
+
if (!line) continue;
|
|
49248
|
+
|
|
49249
|
+
if (!sawMagicWord && commands.length === 0 && cur === '' &&
|
|
49250
|
+
/^mapshaper(\s|$)/.test(line)) {
|
|
49251
|
+
sawMagicWord = true;
|
|
49252
|
+
line = line.replace(/^mapshaper\s*/, '');
|
|
49253
|
+
if (!line) continue;
|
|
49254
|
+
}
|
|
49255
|
+
|
|
49256
|
+
if (line.charAt(0) === '-') {
|
|
49257
|
+
if (cur) commands.push(cur);
|
|
49258
|
+
cur = line;
|
|
49259
|
+
} else if (!cur && commands.length === 0) {
|
|
49260
|
+
cur = implicitFirstCommand(line);
|
|
49261
|
+
} else {
|
|
49262
|
+
cur += ' ' + line;
|
|
49263
|
+
}
|
|
49264
|
+
}
|
|
49265
|
+
if (cur) commands.push(cur);
|
|
49266
|
+
return commands;
|
|
49267
|
+
}
|
|
49268
|
+
|
|
49269
|
+
// Build the implicit command for a leading bare token:
|
|
49270
|
+
// foo.txt -> -run foo.txt
|
|
49271
|
+
// anything else -> -i <line>
|
|
49272
|
+
// Only the first whitespace-separated token of @line is sniffed for the .txt
|
|
49273
|
+
// extension; any trailing tokens (rare but possible after line joining) are
|
|
49274
|
+
// passed through unchanged.
|
|
49275
|
+
function implicitFirstCommand(line) {
|
|
49276
|
+
var firstTok = line.split(/\s+/)[0];
|
|
49277
|
+
if (isPotentialCommandFile(firstTok)) {
|
|
49278
|
+
return '-run ' + line;
|
|
49279
|
+
}
|
|
49280
|
+
return '-i ' + line;
|
|
49281
|
+
}
|
|
49282
|
+
|
|
48417
49283
|
var ParseCommands = /*#__PURE__*/Object.freeze({
|
|
48418
49284
|
__proto__: null,
|
|
49285
|
+
parseCommandFileContent: parseCommandFileContent,
|
|
48419
49286
|
parseCommands: parseCommands,
|
|
48420
49287
|
parseConsoleCommands: parseConsoleCommands,
|
|
48421
49288
|
standardizeConsoleCommands: standardizeConsoleCommands
|
|
@@ -48438,7 +49305,9 @@ ${svg}
|
|
|
48438
49305
|
}
|
|
48439
49306
|
|
|
48440
49307
|
function commandTakesFileInput(name) {
|
|
48441
|
-
return (name == 'i' || name == 'join' || name == 'erase' || name == 'clip' ||
|
|
49308
|
+
return (name == 'i' || name == 'join' || name == 'erase' || name == 'clip' ||
|
|
49309
|
+
name == 'include' || name == 'vars' || name == 'defaults' ||
|
|
49310
|
+
name == 'run');
|
|
48442
49311
|
}
|
|
48443
49312
|
|
|
48444
49313
|
// TODO: implement these and other functions
|
|
@@ -48464,11 +49333,116 @@ ${svg}
|
|
|
48464
49333
|
// flags.mosaic || flags.snap;
|
|
48465
49334
|
// }
|
|
48466
49335
|
|
|
49336
|
+
// Maximum nesting depth for command files that load other command files
|
|
49337
|
+
var MAX_RUN_DEPTH = 10;
|
|
49338
|
+
|
|
49339
|
+
// Returns the text content of a command file, or null if @file does not look
|
|
49340
|
+
// like a mapshaper command file (wrong extension or missing magic word).
|
|
49341
|
+
// On match, the file content is left in the cache so a subsequent reader
|
|
49342
|
+
// can reuse it.
|
|
49343
|
+
function readCommandFile(file, cache) {
|
|
49344
|
+
if (!isPotentialCommandFile(file)) return null;
|
|
49345
|
+
cli.checkFileExists(file, cache);
|
|
49346
|
+
// cli.readFile(... cache) deletes the entry from the cache after reading,
|
|
49347
|
+
// so we put it back in case downstream code expects to find it there.
|
|
49348
|
+
var content = cli.readFile(file, 'utf8', cache);
|
|
49349
|
+
if (!stringLooksLikeCommandFile(content)) {
|
|
49350
|
+
if (cache) cache[file] = content;
|
|
49351
|
+
return null;
|
|
49352
|
+
}
|
|
49353
|
+
if (cache) cache[file] = content;
|
|
49354
|
+
return content;
|
|
49355
|
+
}
|
|
49356
|
+
|
|
49357
|
+
// Parse and execute the commands in a mapshaper command file within the
|
|
49358
|
+
// given job. Invoked by the -run command when its argument is a .txt file.
|
|
49359
|
+
//
|
|
49360
|
+
// @file: command file path
|
|
49361
|
+
// @content: command file content (string)
|
|
49362
|
+
// @job: parent Job object (commands run in this job)
|
|
49363
|
+
// @opts: options object from the parent -run command (used for input cache and
|
|
49364
|
+
// recursion-depth tracking)
|
|
49365
|
+
//
|
|
49366
|
+
async function runCommandFile(file, content, job, opts) {
|
|
49367
|
+
var depth = (opts && opts._run_depth || 0) + 1;
|
|
49368
|
+
if (depth > MAX_RUN_DEPTH) {
|
|
49369
|
+
stop$1('Command file nesting limit exceeded (' + MAX_RUN_DEPTH + ') at: ' + file);
|
|
49370
|
+
}
|
|
49371
|
+
|
|
49372
|
+
var cache = opts && opts.input || null;
|
|
49373
|
+
var commandStr;
|
|
49374
|
+
try {
|
|
49375
|
+
commandStr = parseCommandFileContent(content);
|
|
49376
|
+
} catch(e) {
|
|
49377
|
+
e.message = 'Error in command file ' + file + ': ' + e.message;
|
|
49378
|
+
throw e;
|
|
49379
|
+
}
|
|
49380
|
+
|
|
49381
|
+
if (!commandStr) {
|
|
49382
|
+
message('Command file contains no commands:', file);
|
|
49383
|
+
return;
|
|
49384
|
+
}
|
|
49385
|
+
|
|
49386
|
+
verbose('Running command file:', file);
|
|
49387
|
+
|
|
49388
|
+
var commands;
|
|
49389
|
+
try {
|
|
49390
|
+
commands = parseCommands(commandStr);
|
|
49391
|
+
} catch(e) {
|
|
49392
|
+
e.message = 'Error in command file ' + file + ': ' + e.message;
|
|
49393
|
+
throw e;
|
|
49394
|
+
}
|
|
49395
|
+
|
|
49396
|
+
// Forward the input cache, output array and depth tracker to nested
|
|
49397
|
+
// commands. This lets a command file's -i find sibling files in the same
|
|
49398
|
+
// cache, lets nested -o commands write to the same output collector (e.g.
|
|
49399
|
+
// when running under applyCommands), and lets nested -run commands respect
|
|
49400
|
+
// the recursion limit.
|
|
49401
|
+
var outputArr = opts && opts.output || null;
|
|
49402
|
+
commands.forEach(function(c) {
|
|
49403
|
+
if (commandTakesFileInput(c.name) && cache) {
|
|
49404
|
+
c.options.input = cache;
|
|
49405
|
+
}
|
|
49406
|
+
if (outputArr && (c.name == 'o' || c.name == 'i' || c.name == 'run' ||
|
|
49407
|
+
c.name == 'info' && c.options.save_to)) {
|
|
49408
|
+
c.options.output = outputArr;
|
|
49409
|
+
}
|
|
49410
|
+
if (c.name == 'run') {
|
|
49411
|
+
c.options._run_depth = depth;
|
|
49412
|
+
}
|
|
49413
|
+
});
|
|
49414
|
+
|
|
49415
|
+
await utils.promisify(runParsedCommands)(commands, job);
|
|
49416
|
+
}
|
|
49417
|
+
|
|
48467
49418
|
cmd.run = async function(job, targets, opts) {
|
|
48468
|
-
var
|
|
48469
|
-
if (!
|
|
48470
|
-
stop$1(
|
|
49419
|
+
var arg = opts.expression;
|
|
49420
|
+
if (!arg) {
|
|
49421
|
+
stop$1('-run requires a command file path or a JS expression');
|
|
48471
49422
|
}
|
|
49423
|
+
// Auto-detect a leading argument that looks like a command file (.txt).
|
|
49424
|
+
// If detection succeeds, treat as a command file; otherwise the argument
|
|
49425
|
+
// is a JS expression (the original -run behavior).
|
|
49426
|
+
if (isPotentialCommandFile(arg)) {
|
|
49427
|
+
if (opts.target) {
|
|
49428
|
+
stop$1('-run does not accept a target= option for command files');
|
|
49429
|
+
}
|
|
49430
|
+
await runFromFile(job, arg, opts);
|
|
49431
|
+
} else {
|
|
49432
|
+
await runFromExpression(job, targets, opts);
|
|
49433
|
+
}
|
|
49434
|
+
};
|
|
49435
|
+
|
|
49436
|
+
async function runFromFile(job, file, opts) {
|
|
49437
|
+
var content = readCommandFile(file, opts.input);
|
|
49438
|
+
if (content === null) {
|
|
49439
|
+
stop$1('Not a mapshaper command file (missing "mapshaper" magic word):', file);
|
|
49440
|
+
}
|
|
49441
|
+
await runCommandFile(file, content, job, opts);
|
|
49442
|
+
}
|
|
49443
|
+
|
|
49444
|
+
async function runFromExpression(job, targets, opts) {
|
|
49445
|
+
var tmp, commands, ctx;
|
|
48472
49446
|
ctx = getBaseContext();
|
|
48473
49447
|
// io proxy adds ability to add datasets dynamically in a required function
|
|
48474
49448
|
ctx.io = getIOProxy();
|
|
@@ -48482,15 +49456,23 @@ ${svg}
|
|
|
48482
49456
|
commands = parseCommands(tmp);
|
|
48483
49457
|
|
|
48484
49458
|
// TODO: remove duplication with mapshaper-run-commands.mjs
|
|
49459
|
+
var outputArr = opts && opts.output || null;
|
|
48485
49460
|
commands.forEach(function(cmd) {
|
|
48486
49461
|
if (commandTakesFileInput(cmd.name)) {
|
|
48487
49462
|
cmd.options.input = ctx.io._cache;
|
|
48488
49463
|
}
|
|
49464
|
+
// Forward the output collector to commands that produce output, so a
|
|
49465
|
+
// generated -o (or info save_to=, or nested -run) writes into the same
|
|
49466
|
+
// output object (e.g. when running under applyCommands).
|
|
49467
|
+
if (outputArr && (cmd.name == 'o' || cmd.name == 'run' ||
|
|
49468
|
+
cmd.name == 'info' && cmd.options.save_to)) {
|
|
49469
|
+
cmd.options.output = outputArr;
|
|
49470
|
+
}
|
|
48489
49471
|
});
|
|
48490
49472
|
|
|
48491
49473
|
await utils.promisify(runParsedCommands)(commands, job);
|
|
48492
49474
|
}
|
|
48493
|
-
}
|
|
49475
|
+
}
|
|
48494
49476
|
|
|
48495
49477
|
cmd.shape = function(targetDataset, opts) {
|
|
48496
49478
|
var geojson, dataset;
|
|
@@ -50685,7 +51667,7 @@ ${svg}
|
|
|
50685
51667
|
name == 'require' || name == 'run' || name == 'define' ||
|
|
50686
51668
|
name == 'include' || name == 'print' || name == 'comment' || name == 'if' || name == 'elif' ||
|
|
50687
51669
|
name == 'else' || name == 'endif' || name == 'stop' || name == 'add-shape' ||
|
|
50688
|
-
name == 'scalebar';
|
|
51670
|
+
name == 'scalebar' || name == 'vars' || name == 'defaults';
|
|
50689
51671
|
}
|
|
50690
51672
|
|
|
50691
51673
|
async function runCommand(command, job) {
|
|
@@ -50820,8 +51802,14 @@ ${svg}
|
|
|
50820
51802
|
} else if (name == 'define') {
|
|
50821
51803
|
cmd.define(job.catalog, opts);
|
|
50822
51804
|
|
|
51805
|
+
} else if (name == 'vars') {
|
|
51806
|
+
cmd.vars(job, opts);
|
|
51807
|
+
|
|
51808
|
+
} else if (name == 'defaults') {
|
|
51809
|
+
cmd.defaults(job, opts);
|
|
51810
|
+
|
|
50823
51811
|
} else if (name == 'dissolve') {
|
|
50824
|
-
outputLayers =
|
|
51812
|
+
outputLayers = cmd.dissolve(targetLayers, targetDataset, opts);
|
|
50825
51813
|
|
|
50826
51814
|
} else if (name == 'dissolve2') {
|
|
50827
51815
|
outputLayers = cmd.dissolve2(targetLayers, targetDataset, opts);
|
|
@@ -51139,7 +52127,7 @@ ${svg}
|
|
|
51139
52127
|
});
|
|
51140
52128
|
}
|
|
51141
52129
|
|
|
51142
|
-
var version = "0.
|
|
52130
|
+
var version = "0.7.1";
|
|
51143
52131
|
|
|
51144
52132
|
// Parse command line args into commands and run them
|
|
51145
52133
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|
|
@@ -51276,6 +52264,11 @@ ${svg}
|
|
|
51276
52264
|
if (outputArr && (cmd.name == 'o' || cmd.name == 'info' && cmd.options.save_to)) {
|
|
51277
52265
|
cmd.options.output = outputArr;
|
|
51278
52266
|
}
|
|
52267
|
+
// -run may load and execute a command file; propagate the output array
|
|
52268
|
+
// so that -o commands nested inside it can write to it too.
|
|
52269
|
+
if (outputArr && cmd.name == 'run') {
|
|
52270
|
+
cmd.options.output = outputArr;
|
|
52271
|
+
}
|
|
51279
52272
|
});
|
|
51280
52273
|
|
|
51281
52274
|
var lastCmd = commands[commands.length - 1];
|
|
@@ -51368,7 +52361,13 @@ ${svg}
|
|
|
51368
52361
|
utils.reduceAsync(commands, job, nextCommand, done);
|
|
51369
52362
|
|
|
51370
52363
|
function nextCommand(job, cmd, next) {
|
|
51371
|
-
|
|
52364
|
+
var resolved;
|
|
52365
|
+
try {
|
|
52366
|
+
resolved = maybeInterpolateCommand(cmd, job);
|
|
52367
|
+
} catch(e) {
|
|
52368
|
+
return next(e);
|
|
52369
|
+
}
|
|
52370
|
+
runCommand(resolved, job).then(function(result) {
|
|
51372
52371
|
next(null, result);
|
|
51373
52372
|
}).catch(function(e) {
|
|
51374
52373
|
next(e);
|
|
@@ -51376,6 +52375,62 @@ ${svg}
|
|
|
51376
52375
|
}
|
|
51377
52376
|
}
|
|
51378
52377
|
|
|
52378
|
+
// Late-binding interpolation: just before each command runs, replace any
|
|
52379
|
+
// {{X}} placeholders in its source tokens against the live job.vars and
|
|
52380
|
+
// job.defs objects (vars first, defs as fallback), then re-parse to get
|
|
52381
|
+
// fresh option values.
|
|
52382
|
+
//
|
|
52383
|
+
// Returns either the original cmd (no placeholders, no _tokens, or the
|
|
52384
|
+
// command will be skipped) or a fresh cmd object with re-parsed options.
|
|
52385
|
+
// The original cmd is never mutated, so commands shared across batches
|
|
52386
|
+
// (see divideImportCommand) still see their un-interpolated tokens.
|
|
52387
|
+
function maybeInterpolateCommand(cmd, job) {
|
|
52388
|
+
var tokens = cmd._tokens;
|
|
52389
|
+
if (!tokens || tokens.length === 0) return cmd;
|
|
52390
|
+
if (!tokens.some(containsPlaceholder)) return cmd;
|
|
52391
|
+
// If the command would be skipped (inactive -if branch, stopped job, etc.),
|
|
52392
|
+
// don't try to interpolate -- the command body never runs and unset
|
|
52393
|
+
// variables shouldn't error here.
|
|
52394
|
+
if (skipCommand(cmd.name, job)) return cmd;
|
|
52395
|
+
|
|
52396
|
+
var vars = job.vars || {};
|
|
52397
|
+
var defs = job.defs || {};
|
|
52398
|
+
var interpolated;
|
|
52399
|
+
try {
|
|
52400
|
+
interpolated = tokens.map(function(tok) {
|
|
52401
|
+
return interpolateString(tok, vars, defs);
|
|
52402
|
+
});
|
|
52403
|
+
} catch(e) {
|
|
52404
|
+
e.message = '[' + cmd.name + '] ' + e.message;
|
|
52405
|
+
throw e;
|
|
52406
|
+
}
|
|
52407
|
+
|
|
52408
|
+
var reparsed;
|
|
52409
|
+
try {
|
|
52410
|
+
reparsed = parseCommands(interpolated);
|
|
52411
|
+
} catch(e) {
|
|
52412
|
+
e.message = '[' + cmd.name + '] ' + e.message;
|
|
52413
|
+
throw e;
|
|
52414
|
+
}
|
|
52415
|
+
if (reparsed.length !== 1) {
|
|
52416
|
+
stop$1('[' + cmd.name + '] Internal error: token re-parse produced ' +
|
|
52417
|
+
reparsed.length + ' commands');
|
|
52418
|
+
}
|
|
52419
|
+
var newCmd = reparsed[0];
|
|
52420
|
+
// Preserve externally-injected options (input cache, output array,
|
|
52421
|
+
// _run_depth, final flag, replace flag, etc.) that aren't reproduced
|
|
52422
|
+
// by re-parsing the source tokens.
|
|
52423
|
+
Object.keys(cmd.options).forEach(function(k) {
|
|
52424
|
+
if (!(k in newCmd.options)) {
|
|
52425
|
+
newCmd.options[k] = cmd.options[k];
|
|
52426
|
+
}
|
|
52427
|
+
});
|
|
52428
|
+
// Keep the original tokens around so re-runs (e.g. divided import batches)
|
|
52429
|
+
// see the un-interpolated source.
|
|
52430
|
+
newCmd._tokens = tokens;
|
|
52431
|
+
return newCmd;
|
|
52432
|
+
}
|
|
52433
|
+
|
|
51379
52434
|
function handleNonFatalError(err) {
|
|
51380
52435
|
if (err && err.name == 'NonFatalError') {
|
|
51381
52436
|
printError(err);
|
|
@@ -51399,6 +52454,18 @@ ${svg}
|
|
|
51399
52454
|
return [commands];
|
|
51400
52455
|
}
|
|
51401
52456
|
|
|
52457
|
+
// Multiple files trigger batch mode by default. This is a long-standing
|
|
52458
|
+
// wart: most multi-file CLI tools combine inputs by default, and silently
|
|
52459
|
+
// splitting into per-file pipelines is an easy way for users to get wrong
|
|
52460
|
+
// output without noticing. Print a one-time deprecation warning when batch
|
|
52461
|
+
// mode is implicit so existing scripts can migrate before the default flips
|
|
52462
|
+
// in a future major release.
|
|
52463
|
+
if (!opts.batch_mode) {
|
|
52464
|
+
message('Note: implicit batch processing is deprecated. Add `batch-mode` ' +
|
|
52465
|
+
'to keep this behavior, or `combine-files` to import the files as a ' +
|
|
52466
|
+
'group of layers. The default will change in a future release.');
|
|
52467
|
+
}
|
|
52468
|
+
|
|
51402
52469
|
return opts.files.map(function(file) {
|
|
51403
52470
|
var group = [{
|
|
51404
52471
|
name: 'i',
|