mapshaper 0.7.0 → 0.7.2

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.
Files changed (89) hide show
  1. package/README.md +5 -3
  2. package/mapshaper.js +363 -62
  3. package/package.json +6 -2
  4. package/www/.nojekyll +0 -0
  5. package/www/assets/jetbrains-mono-regular.woff2 +0 -0
  6. package/www/assets/static-page.css +179 -0
  7. package/www/docs/_assets/cmd-search.js +213 -0
  8. package/www/docs/_assets/docs.css +722 -0
  9. package/www/docs/_assets/docs.js +75 -0
  10. package/www/docs/_assets/highlight.css +10 -0
  11. package/www/docs/essentials/command-line.html +127 -0
  12. package/www/docs/essentials/command-line.html.md +112 -0
  13. package/www/docs/essentials/web-app.html +138 -0
  14. package/www/docs/essentials/web-app.html.md +106 -0
  15. package/www/docs/examples/basics.html +275 -0
  16. package/www/docs/examples/basics.html.md +370 -0
  17. package/www/docs/examples/data/Makefile +31 -0
  18. package/www/docs/examples/data/globe.msx +0 -0
  19. package/www/docs/examples/data/globe.svg +616 -0
  20. package/www/docs/examples/data/globe.txt +21 -0
  21. package/www/docs/examples/data/globe.zip +0 -0
  22. package/www/docs/examples/data/ne_50m_admin_0_countries.geojson +1 -0
  23. package/www/docs/examples/data/ne_50m_admin_1_states_provinces_lakes.geojson +1 -0
  24. package/www/docs/examples/data/us-states.msx +0 -0
  25. package/www/docs/examples/data/us-states.svg +56 -0
  26. package/www/docs/examples/data/us-states.txt +6 -0
  27. package/www/docs/examples/data/us-states.zip +0 -0
  28. package/www/docs/examples/globe.html +108 -0
  29. package/www/docs/examples/globe.html.md +64 -0
  30. package/www/docs/examples/us-states.html +88 -0
  31. package/www/docs/examples/us-states.html.md +44 -0
  32. package/www/docs/formats/csv.html +127 -0
  33. package/www/docs/formats/csv.html.md +97 -0
  34. package/www/docs/formats/dbf.html +87 -0
  35. package/www/docs/formats/dbf.html.md +39 -0
  36. package/www/docs/formats/flatgeobuf.html +85 -0
  37. package/www/docs/formats/flatgeobuf.html.md +41 -0
  38. package/www/docs/formats/geojson.html +107 -0
  39. package/www/docs/formats/geojson.html.md +65 -0
  40. package/www/docs/formats/geopackage.html +87 -0
  41. package/www/docs/formats/geopackage.html.md +42 -0
  42. package/www/docs/formats/json.html +83 -0
  43. package/www/docs/formats/json.html.md +35 -0
  44. package/www/docs/formats/kml.html +82 -0
  45. package/www/docs/formats/kml.html.md +39 -0
  46. package/www/docs/formats/overview.html +191 -0
  47. package/www/docs/formats/overview.html.md +34 -0
  48. package/www/docs/formats/shapefile.html +123 -0
  49. package/www/docs/formats/shapefile.html.md +84 -0
  50. package/www/docs/formats/snapshot.html +87 -0
  51. package/www/docs/formats/snapshot.html.md +39 -0
  52. package/www/docs/formats/svg.html +99 -0
  53. package/www/docs/formats/svg.html.md +51 -0
  54. package/www/docs/formats/topojson.html +102 -0
  55. package/www/docs/formats/topojson.html.md +54 -0
  56. package/www/docs/gallery/index.html +80 -0
  57. package/www/docs/gallery/index.html.md +29 -0
  58. package/www/docs/guides/combining-layers.html +109 -0
  59. package/www/docs/guides/combining-layers.html.md +89 -0
  60. package/www/docs/guides/expressions.html +600 -0
  61. package/www/docs/guides/expressions.html.md +376 -0
  62. package/www/docs/guides/programmatic.html +117 -0
  63. package/www/docs/guides/programmatic.html.md +91 -0
  64. package/www/docs/guides/projections.html +158 -0
  65. package/www/docs/guides/projections.html.md +118 -0
  66. package/www/docs/guides/simplification.html +110 -0
  67. package/www/docs/guides/simplification.html.md +96 -0
  68. package/www/docs/guides/topology.html +90 -0
  69. package/www/docs/guides/topology.html.md +63 -0
  70. package/www/docs/images/cal-counties.png +0 -0
  71. package/www/docs/images/simplification-detail.png +0 -0
  72. package/www/docs/images/simplification-dp.png +0 -0
  73. package/www/docs/images/simplification-mod2.png +0 -0
  74. package/www/docs/images/tiger-counties.png +0 -0
  75. package/www/docs/index.html +101 -0
  76. package/www/docs/index.html.md +59 -0
  77. package/www/docs/reference.html +1302 -0
  78. package/www/docs/reference.html.md +1817 -0
  79. package/www/docs/whats-new.html +77 -0
  80. package/www/docs/whats-new.html.md +59 -0
  81. package/www/index.html +47 -4
  82. package/www/llms-full.txt +4053 -0
  83. package/www/llms.txt +55 -0
  84. package/www/mapshaper-gui.js +299 -5
  85. package/www/mapshaper.js +363 -62
  86. package/www/page.css +189 -11
  87. package/www/privacy.html +1 -112
  88. package/www/sponsor.html +4 -164
  89. 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/');
@@ -12032,10 +12124,13 @@
12032
12124
  isZipFile: isZipFile,
12033
12125
  looksLikeContentFile: looksLikeContentFile,
12034
12126
  looksLikeImportableFile: looksLikeImportableFile,
12127
+ stringHasInlineCsvNewline: stringHasInlineCsvNewline,
12035
12128
  stringLooksLikeCommandFile: stringLooksLikeCommandFile,
12129
+ stringLooksLikeCsv: stringLooksLikeCsv,
12036
12130
  stringLooksLikeJSON: stringLooksLikeJSON,
12037
12131
  stringLooksLikeKML: stringLooksLikeKML,
12038
- stringLooksLikeSVG: stringLooksLikeSVG
12132
+ stringLooksLikeSVG: stringLooksLikeSVG,
12133
+ unescapeInlineCsv: unescapeInlineCsv
12039
12134
  });
12040
12135
 
12041
12136
  // input: A file path or a buffer
@@ -12283,10 +12378,14 @@
12283
12378
  return files;
12284
12379
  };
12285
12380
 
12286
- // 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.
12287
12384
  cli.expandInputFiles = function(files) {
12288
12385
  return files.reduce(function(memo, name) {
12289
- if (name.indexOf('*') > -1) {
12386
+ if (stringLooksLikeJSON(name) || stringLooksLikeCsv(name)) {
12387
+ memo.push(name);
12388
+ } else if (name.indexOf('*') > -1) {
12290
12389
  memo = memo.concat(cli.expandFileName(name));
12291
12390
  } else {
12292
12391
  memo.push(name);
@@ -15623,7 +15722,7 @@
15623
15722
  if (utils.isObject(obj)) {
15624
15723
  _records[_id] = obj;
15625
15724
  } else {
15626
- stop$1("Can't assign non-object to $.properties");
15725
+ stop$1("Can't assign non-object to this.properties");
15627
15726
  }
15628
15727
  }, get: function() {
15629
15728
  var rec = _records[_id];
@@ -15756,7 +15855,7 @@
15756
15855
  if (!obj || utils.isArray(obj)) {
15757
15856
  lyr.shapes[_id] = obj || null;
15758
15857
  } else {
15759
- stop$1("Can't assign non-array to $.coordinates");
15858
+ stop$1("Can't assign non-array to this.coordinates");
15760
15859
  }
15761
15860
  }, get: function() {
15762
15861
  return lyr.shapes[_id] || null;
@@ -16177,7 +16276,7 @@
16177
16276
  // Calculate an expression across a group of features, print and return the result
16178
16277
  // Supported functions include sum(), average(), max(), min(), median(), count()
16179
16278
  // Functions receive an expression to be applied to each feature (like the -each command)
16180
- // Examples: 'sum($.area)' 'min(income)'
16279
+ // Examples: 'sum(this.area)' 'min(income)'
16181
16280
  // opts.expression Expression to evaluate
16182
16281
  // opts.where Optional filter expression (see -filter command)
16183
16282
  //
@@ -17219,6 +17318,29 @@
17219
17318
  profileStart('snapAndCut');
17220
17319
  var arcs = dataset.arcs;
17221
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
+
17222
17344
  var coordsHaveChanged = false;
17223
17345
  var snapCount = 0, dupeCount, cutCount;
17224
17346
  var maxLoops = 4, loopCount = 0;
@@ -21408,18 +21530,18 @@ ${svg}
21408
21530
  validateSvgDataFields: validateSvgDataFields
21409
21531
  });
21410
21532
 
21411
- // import { isKmzFile } from '../io/mapshaper-file-types';
21412
-
21413
21533
  function exportKML(dataset, opts) {
21414
21534
  var toKML = require$1("@placemarkio/tokml").toKML;
21415
21535
  var geojsonOpts = Object.assign({combine_layers: true, geojson_type: 'FeatureCollection'}, opts);
21416
21536
  var geojson = exportDatasetAsGeoJSON(dataset, geojsonOpts);
21417
21537
  var kml = toKML(geojson);
21418
- // TODO: add KMZ output
21419
- // var useKmz = opts.file && isKmzFile(opts.file);
21420
- var ofile = opts.file || getOutputFileBase(dataset) + '.kml';
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;
21421
21543
  return [{
21422
- content: kml,
21544
+ content: content,
21423
21545
  filename: ofile
21424
21546
  }];
21425
21547
  }
@@ -24977,13 +25099,19 @@ ${svg}
24977
25099
  // Keep behavior consistent with other exporters that honor explicit output filename.
24978
25100
  extension = getFileExtension(opts.file) || extension;
24979
25101
  }
25102
+ var crsMeta = resolveOutputCRS(dataset);
24980
25103
  return dataset.layers.map(function(lyr) {
24981
25104
  var geojson = getFeatureCollection(lyr, dataset, opts);
24982
25105
  var content = serialize(geojson);
24983
- content = setOutputCRS(content, dataset.info && dataset.info.flatgeobuf_crs);
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
+ }
24984
25112
  return {
24985
25113
  content: content,
24986
- filename: lyr.name + '.' + extension
25114
+ filename: filename
24987
25115
  };
24988
25116
  });
24989
25117
  }
@@ -24999,10 +25127,55 @@ ${svg}
24999
25127
  };
25000
25128
  }
25001
25129
 
25002
- function setOutputCRS(content, crs) {
25003
- var crsMeta = normalizeCRS(crs);
25004
- if (!crsMeta) return content;
25005
- return rewriteHeaderWithCRS(content, crsMeta);
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;
25006
25179
  }
25007
25180
 
25008
25181
  function normalizeCRS(crs) {
@@ -25645,10 +25818,22 @@ ${svg}
25645
25818
  var bounds = getLayerBounds(target.layer, target.dataset.arcs);
25646
25819
  opts = Object.assign({svg_bbox: bounds.toArray()}, opts);
25647
25820
  }
25648
- // convert target fmt to dataset fmt
25649
- var datasets = targets.map(function(target) {
25650
- return utils.defaults({layers: target.layers}, target.dataset);
25651
- });
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
+ }
25652
25837
  return exportDatasets(datasets, opts);
25653
25838
  }
25654
25839
 
@@ -25895,6 +26080,62 @@ ${svg}
25895
26080
  }
25896
26081
  }
25897
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
+
25898
26139
  var Export = /*#__PURE__*/Object.freeze({
25899
26140
  __proto__: null,
25900
26141
  assignUniqueFileNames: assignUniqueFileNames,
@@ -28405,7 +28646,7 @@ ${svg}
28405
28646
  parser.command('each')
28406
28647
  .describe('create/update/delete data fields using a JS expression')
28407
28648
  .example('Add two calculated data fields to a layer of U.S. counties\n' +
28408
- '$ mapshaper counties.shp -each \'STATE_FIPS=CNTY_FIPS.substr(0, 2), AREA=$.area\'')
28649
+ '$ mapshaper counties.shp -each \'STATE_FIPS=CNTY_FIPS.substr(0, 2), AREA=this.area\'')
28409
28650
  .option('expression', {
28410
28651
  DEFAULT: true,
28411
28652
  describe: 'JS expression to apply to each target feature'
@@ -28560,7 +28801,8 @@ ${svg}
28560
28801
  describe: 'create a polygon to match the outline of the graticule',
28561
28802
  type: 'flag'
28562
28803
  })
28563
- .option('name', nameOpt);
28804
+ .option('name', nameOpt)
28805
+ .option('target', targetOpt);
28564
28806
 
28565
28807
 
28566
28808
  // for testing grid update
@@ -28970,7 +29212,7 @@ ${svg}
28970
29212
  })
28971
29213
  .option('interval', {
28972
29214
  // alias: 'i',
28973
- describe: 'output resolution as a distance (e.g. 100)',
29215
+ describe: 'output resolution as a distance (e.g. 100m)',
28974
29216
  type: 'distance'
28975
29217
  })
28976
29218
  /*
@@ -29733,7 +29975,7 @@ ${svg}
29733
29975
  parser.command('calc')
29734
29976
  .describe('calculate statistics about the features in a layer')
29735
29977
  .example('Calculate the total area of a polygon layer\n' +
29736
- '$ mapshaper polygons.shp -calc \'sum($.area)\'')
29978
+ '$ mapshaper polygons.shp -calc \'sum(this.area)\'')
29737
29979
  .example('Count census blocks in NY with zero population\n' +
29738
29980
  '$ mapshaper ny-census-blocks.shp -calc \'count()\' where=\'POPULATION == 0\'')
29739
29981
  .validate(validateExpressionOpt)
@@ -33665,7 +33907,12 @@ ${svg}
33665
33907
  // load external files (e.g. epsg definitions) if needed in GUI
33666
33908
  await initProjLibrary({crs: o.crs_string});
33667
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);
33668
33914
  } else if (o.prj) {
33915
+ // legacy field name; older snapshots may have stored the .prj content here
33669
33916
  o.crs = parsePrj(o.prj);
33670
33917
  }
33671
33918
  return o;
@@ -33753,22 +34000,39 @@ ${svg}
33753
34000
  return target;
33754
34001
  };
33755
34002
 
33756
- // replace any JSON data objects with filenames and cache the data
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.
33757
34006
  function convertDataObjects(files, cache) {
33758
- var names = files.map(str => stringLooksLikeJSON(str) ? 'layer.json' : null).filter(Boolean);
33759
- if (names.length === 0) return;
33760
- if (names.length > 1) {
33761
- // make unique names if importing multiple objects
33762
- names = utils.uniqifyNames(names, formatVersionedFileName);
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
+ });
33763
34018
  }
33764
- files.forEach((str, i) => {
33765
- if (!stringLooksLikeJSON(str)) return;
33766
- var name = names.shift();
33767
- cache[name] = str;
33768
- files[i] = name;
34019
+ slots.forEach(function(slot, i) {
34020
+ if (!slot) return;
34021
+ cache[slot.filename] = slot.content;
34022
+ files[i] = slot.filename;
33769
34023
  });
33770
34024
  }
33771
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
+
33772
34036
  async function importMshpFile(file, catalog, opts) {
33773
34037
  var buf = cli.readFile(file, null, opts.input);
33774
34038
  var obj = await unpackSessionData(buf);
@@ -34347,6 +34611,7 @@ ${svg}
34347
34611
  var job = {
34348
34612
  catalog: catalog || new Catalog(),
34349
34613
  defs: {},
34614
+ vars: {},
34350
34615
  settings: {},
34351
34616
  input_files: []
34352
34617
  };
@@ -34382,6 +34647,7 @@ ${svg}
34382
34647
  stashVar('VERBOSE', job.settings.VERBOSE || cmd.verbose);
34383
34648
  stashVar('QUIET', job.settings.QUIET || cmd.quiet);
34384
34649
  stashVar('defs', job.defs);
34650
+ stashVar('vars', job.vars);
34385
34651
  stashVar('input_files', job.input_files);
34386
34652
  }
34387
34653
 
@@ -42089,15 +42355,22 @@ ${svg}
42089
42355
  }
42090
42356
 
42091
42357
  // Resolve a single placeholder expression to a string. Recognised forms:
42092
- // VAR -> defs[VAR]
42358
+ // VAR -> vars[VAR] if present, else defs[VAR]
42093
42359
  // env.VAR -> process.env[VAR]
42094
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
+ //
42095
42368
  // Throws on undefined names, invalid syntax, or non-primitive values.
42096
42369
  //
42097
- function resolvePlaceholder(expr, defs) {
42370
+ function resolvePlaceholder(expr, vars, defs) {
42098
42371
  expr = expr.trim();
42099
42372
  var envMatch = /^env\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(expr);
42100
- var val;
42373
+ var val, source;
42101
42374
  if (envMatch) {
42102
42375
  val = lookupEnvVar(envMatch[1]);
42103
42376
  if (val === undefined || val === null) {
@@ -42108,10 +42381,14 @@ ${svg}
42108
42381
  if (!isValidVarName(expr)) {
42109
42382
  stop$1('Invalid variable reference: {{' + expr + '}}');
42110
42383
  }
42111
- if (!defs || !(expr in defs)) {
42384
+ if (vars && expr in vars) {
42385
+ source = vars;
42386
+ } else if (defs && expr in defs) {
42387
+ source = defs;
42388
+ } else {
42112
42389
  stop$1('Undefined variable: ' + expr);
42113
42390
  }
42114
- val = defs[expr];
42391
+ val = source[expr];
42115
42392
  if (val === null || val === undefined) {
42116
42393
  stop$1('Undefined variable: ' + expr);
42117
42394
  }
@@ -42123,16 +42400,33 @@ ${svg}
42123
42400
  return String(val);
42124
42401
  }
42125
42402
 
42126
- // Substitute {{...}} placeholders in @str using @defs. Placeholders that
42127
- // are preceded by a backslash are left literal (with the backslash removed).
42128
- // Substitution is single-pass (no recursion) so that values containing
42129
- // "{{...}}" do not trigger further interpolation.
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.
42130
42412
  //
42131
- function interpolateString(str, defs) {
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) {
42132
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
+ }
42133
42427
  return str.replace(PLACEHOLDER_RXP, function(match, escape, expr) {
42134
42428
  if (escape === '\\') return '{{' + expr + '}}';
42135
- return resolvePlaceholder(expr, defs);
42429
+ return resolvePlaceholder(expr, vars, defs);
42136
42430
  });
42137
42431
  }
42138
42432
 
@@ -42140,23 +42434,27 @@ ${svg}
42140
42434
  // -vars file.json [more ...] load primitives from a flat JSON object
42141
42435
  // Mixed forms allowed; later args override earlier ones.
42142
42436
  //
42143
- // Writes into job.defs (the same object read by {{X}} interpolation,
42144
- // -define, -calc and -include).
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}}.
42145
42443
  cmd.vars = function(job, opts) {
42146
42444
  var values = (opts && opts.values) || [];
42147
42445
  if (!values.length) {
42148
42446
  stop$1('-vars requires one or more KEY=value or file.json arguments');
42149
42447
  }
42150
42448
  var parsed = parseVarsArgs(values, opts && opts.input);
42151
- if (!job.defs) job.defs = {};
42449
+ if (!job.vars) job.vars = {};
42152
42450
  Object.keys(parsed).forEach(function(key) {
42153
- job.defs[key] = parsed[key];
42451
+ job.vars[key] = parsed[key];
42154
42452
  });
42155
42453
  };
42156
42454
 
42157
42455
  // -defaults KEY=value [KEY=value ...] set-if-unset
42158
42456
  // Same syntax as -vars, but a key is only assigned if it is not already
42159
- // present in job.defs. Lets a command file declare overridable defaults
42457
+ // present in job.vars. Lets a command file declare overridable defaults
42160
42458
  // that a CLI -vars can pre-empt.
42161
42459
  cmd.defaults = function(job, opts) {
42162
42460
  var values = (opts && opts.values) || [];
@@ -42164,10 +42462,10 @@ ${svg}
42164
42462
  stop$1('-defaults requires one or more KEY=value or file.json arguments');
42165
42463
  }
42166
42464
  var parsed = parseVarsArgs(values, opts && opts.input);
42167
- if (!job.defs) job.defs = {};
42465
+ if (!job.vars) job.vars = {};
42168
42466
  Object.keys(parsed).forEach(function(key) {
42169
- if (!(key in job.defs)) {
42170
- job.defs[key] = parsed[key];
42467
+ if (!(key in job.vars)) {
42468
+ job.vars[key] = parsed[key];
42171
42469
  }
42172
42470
  });
42173
42471
  };
@@ -48871,8 +49169,9 @@ ${svg}
48871
49169
  // "-i <token>" (data file).
48872
49170
  //
48873
49171
  // "{{VAR}}" placeholders are substituted at execution time, against the
48874
- // live job.defs object. See mapshaper-vars-utils.mjs and the late-binding
48875
- // hook in mapshaper-run-commands.mjs.
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.
48876
49175
  //
48877
49176
  function parseCommandFileContent(content) {
48878
49177
  if (typeof content != 'string') {
@@ -51828,7 +52127,7 @@ ${svg}
51828
52127
  });
51829
52128
  }
51830
52129
 
51831
- var version = "0.7.0";
52130
+ var version = "0.7.2";
51832
52131
 
51833
52132
  // Parse command line args into commands and run them
51834
52133
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -52077,8 +52376,9 @@ ${svg}
52077
52376
  }
52078
52377
 
52079
52378
  // Late-binding interpolation: just before each command runs, replace any
52080
- // {{X}} placeholders in its source tokens against the live job.defs object,
52081
- // then re-parse to get fresh option values.
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.
52082
52382
  //
52083
52383
  // Returns either the original cmd (no placeholders, no _tokens, or the
52084
52384
  // command will be skipped) or a fresh cmd object with re-parsed options.
@@ -52093,11 +52393,12 @@ ${svg}
52093
52393
  // variables shouldn't error here.
52094
52394
  if (skipCommand(cmd.name, job)) return cmd;
52095
52395
 
52396
+ var vars = job.vars || {};
52096
52397
  var defs = job.defs || {};
52097
52398
  var interpolated;
52098
52399
  try {
52099
52400
  interpolated = tokens.map(function(tok) {
52100
- return interpolateString(tok, defs);
52401
+ return interpolateString(tok, vars, defs);
52101
52402
  });
52102
52403
  } catch(e) {
52103
52404
  e.message = '[' + cmd.name + '] ' + e.message;