mapshaper 0.6.39 → 0.6.41

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/bin/mapshaper-gui CHANGED
@@ -9,7 +9,7 @@ var defaultPort = 5555,
9
9
  .option('-q, --quick-view', 'load files with default options, bypassing import dialog')
10
10
  .option('-s, --direct-save', 'save files outside the browser\'s download folder')
11
11
  .option('-f, --force-save', 'allow overwriting input files with output files')
12
- .option('-a, --display-all', 'turn on visibility of all layers')
12
+ .option('-a, --display-all', 'turn on initial visibility of all layers')
13
13
  .option('-n, --name <name(s)>', 'rename input layer or layers')
14
14
  .option('-c, --commands <string>', 'console commands to run initially')
15
15
  .option('-t, --target <name>', 'name of layer to select initially')
package/mapshaper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.6.39";
3
+ var VERSION = "0.6.41";
4
4
 
5
5
 
6
6
  var utils = /*#__PURE__*/Object.freeze({
@@ -10877,7 +10877,8 @@
10877
10877
  pack: pack,
10878
10878
  exportDatasetsToPack: exportDatasetsToPack,
10879
10879
  applyCompression: applyCompression,
10880
- exportDataset: exportDataset
10880
+ exportDataset: exportDataset,
10881
+ exportInfo: exportInfo
10881
10882
  });
10882
10883
 
10883
10884
  // Guess the type of a data file from file extension, or return null if not sure
@@ -13142,17 +13143,23 @@
13142
13143
  var rxp = fmt;
13143
13144
  rxp = rxp.replace('[-]', '(?<prefix>-)?'); // optional -
13144
13145
  rxp = rxp.replace(/\[[NSEW, +-]{2,}\]/, '(?<prefix>$&)');
13146
+ // TODO: validate that if there are degree decimals, there are no M or S codes
13147
+ rxp = rxp.replace(/D+(\.D+)?/, (m, g1) => {
13148
+ var s = '[0-9]+';
13149
+ if (g1) s += `\\.[0-9]+`;
13150
+ return `(?<d>${s})`;
13151
+ });
13152
+ // TODO: validate that if there are minutes decimals, there are no S codes
13145
13153
  rxp = rxp.replace(/(MM?)(\.M+)?/, (m, g1, g2) => {
13146
13154
  var s = g1.length == 1 ? '[0-9]+' : '[0-9][0-9]';
13147
- if (g2) s += `\\.[0-9]+`;
13155
+ if (g2) s += '\\.[0-9]+';
13148
13156
  return `(?<m>${s})`;
13149
13157
  });
13150
13158
  rxp = rxp.replace(/(SS?)(\.S+)?/, (m, g1, g2) => {
13151
13159
  var s = g1.length == 1 ? '[0-9]+' : '[0-9][0-9]';
13152
- if (g2) s += `\\.[0-9]+`;
13160
+ if (g2) s += '\\.[0-9]+';
13153
13161
  return `(?<s>${s})`;
13154
13162
  });
13155
- rxp = rxp.replace(/D+/, '(?<d>[0-9]+)');
13156
13163
  rxp = '^' + rxp + '$';
13157
13164
  try {
13158
13165
  // TODO: make sure all DMS codes have been matched
@@ -13173,43 +13180,42 @@
13173
13180
  return str;
13174
13181
  }
13175
13182
 
13176
- function getDecimals(fmt) {
13177
- var match = /S\.(S+)/.exec(fmt) || /M\.(M+)/.exec(fmt) || null;
13178
- return match ? match[1].length : 0;
13179
- }
13180
-
13181
- // TODO: support DD.DDDDD
13182
13183
  function formatDMS(coord, fmt) {
13183
13184
  if (!fmt) fmt = '[-]D°M\'S.SSS';
13184
13185
  var str = fmt;
13185
13186
  var dstr, mstr, sstr;
13186
- var match = /(D+)[^M]*(M+)[^S[\]]*(S+)?/.exec(fmt);
13187
- if (!match) {
13187
+ var match = /(D+)[^M]*(M+)?[^S[\]]*(S+)?/.exec(fmt);
13188
+ var gotSeconds = match && !!match[3];
13189
+ var gotMinutes = match && !!match[2];
13190
+ if (!match || gotSeconds && !gotMinutes) {
13188
13191
  stop('Invalid DMS format string:', fmt);
13189
13192
  }
13190
- var gotSeconds = !!match[3];
13191
- var decimals = getDecimals(fmt);
13192
- var integers = gotSeconds ? match[3].length : match[2].length;
13193
- var RES = Math.pow(10, decimals);
13194
- var CONV = gotSeconds ? 3600 * RES : 60 * RES;
13195
- var r = Math.floor(Math.abs(coord) * CONV + 0.5);
13196
- var lastPart = formatNumber((r / RES) % 60, integers, decimals);
13197
- if (gotSeconds) {
13198
- r = Math.floor(r / (RES * 60));
13199
- sstr = lastPart;
13200
- mstr = String(r % 60).padStart(match[2].length, '0');
13193
+ var integers = gotSeconds && match[3].length || gotMinutes && match[2].length || match[1].length;
13194
+ var decimalRxp = gotSeconds && /S\.(S+)/ || gotMinutes && /M\.(M+)/ || /D\.(D+)/;
13195
+ var decimals = decimalRxp.test(fmt) ? decimalRxp.exec(fmt)[1].length : 0;
13196
+ if (gotMinutes) {
13197
+ var RES = Math.pow(10, decimals);
13198
+ var CONV = gotSeconds ? 3600 * RES : 60 * RES;
13199
+ var r = Math.floor(Math.abs(coord) * CONV + 0.5);
13200
+ var lastPart = formatNumber((r / RES) % 60, integers, decimals);
13201
+ if (gotSeconds) {
13202
+ r = Math.floor(r / (RES * 60));
13203
+ sstr = lastPart;
13204
+ mstr = String(r % 60).padStart(match[2].length, '0');
13205
+ } else {
13206
+ r = Math.floor(r / RES);
13207
+ mstr = lastPart;
13208
+ }
13209
+ dstr = String(Math.floor(r / 60)).padStart(match[1].length, '0');
13201
13210
  } else {
13202
- r = Math.floor(r / RES);
13203
- mstr = lastPart;
13204
- sstr = '';
13211
+ dstr = Math.abs(coord).toFixed(decimals);
13205
13212
  }
13206
- dstr = String(Math.floor(r / 60)).padStart(match[1].length, '0');
13207
13213
  str = str.replace(/\[-\]/, s => coord < 0 ? '-' : '');
13208
13214
  str = str.replace(/\[[+-]+\]/, s => coord < 0 ? '-' : '+');
13209
13215
  str = str.replace(/\[[NS, ]+\]/, s => coord < 0 ? 'S' : 'N');
13210
13216
  str = str.replace(/\[[EW, ]+\]/, s => coord < 0 ? 'W' : 'E');
13211
- str = str.replace(/D+/, dstr);
13212
- str = str.replace(/M+(\.M+)?/, mstr);
13217
+ str = str.replace(/D+(\.D+)?/, dstr);
13218
+ if (gotMinutes) str = str.replace(/M+(\.M+)?/, mstr);
13213
13219
  if (gotSeconds) str = str.replace(/S+(\.S+)?/, sstr);
13214
13220
  return str;
13215
13221
  }
@@ -17699,12 +17705,18 @@
17699
17705
  }
17700
17706
 
17701
17707
  function calcFrameData(dataset, opts) {
17702
- var bounds = getDatasetBounds(dataset);
17703
- var bounds2 = calcOutputSizeInPixels(bounds, opts);
17708
+ var bounds;
17709
+ if (opts.svg_bbox) {
17710
+ bounds = new Bounds(opts.svg_bbox);
17711
+ opts = Object.assign({margin: 0}, opts); // prevent default pixel margin around content
17712
+ } else {
17713
+ bounds = getDatasetBounds(dataset);
17714
+ }
17715
+ var pixBounds = calcOutputSizeInPixels(bounds, opts);
17704
17716
  return {
17705
17717
  bbox: bounds.toArray(),
17706
- width: Math.round(bounds2.width()),
17707
- height: Math.round(bounds2.height()) || 1,
17718
+ width: Math.round(pixBounds.width()),
17719
+ height: Math.round(pixBounds.height()) || 1,
17708
17720
  type: 'frame'
17709
17721
  };
17710
17722
  }
@@ -23375,6 +23387,10 @@ ${svg}
23375
23387
  describe: '[TopoJSON] export coordinates without quantization',
23376
23388
  type: 'flag'
23377
23389
  })
23390
+ .option('metadata', {
23391
+ // describe: '[TopoJSON] Add a metadata object containing CRS information',
23392
+ type: 'flag'
23393
+ })
23378
23394
  .option('no-point-quantization', {
23379
23395
  // describe: '[TopoJSON] export point coordinates without quantization',
23380
23396
  type: 'flag'
@@ -23439,6 +23455,10 @@ ${svg}
23439
23455
  describe: '[SVG] source units per pixel (alternative to width= option)',
23440
23456
  type: 'number'
23441
23457
  })
23458
+ .option('svg-bbox', {
23459
+ describe: '[SVG] bounding box of SVG map in projected map units',
23460
+ type: 'bbox'
23461
+ })
23442
23462
  .option('point-symbol', {
23443
23463
  describe: '[SVG] circle or square (default is circle)'
23444
23464
  })
@@ -27547,6 +27567,7 @@ ${svg}
27547
27567
  var separator = path.indexOf('/') > 0 ? '/' : '.';
27548
27568
  var parts = path.split(separator);
27549
27569
  var subpath, array, match;
27570
+
27550
27571
  while (parts.length > 0) {
27551
27572
  subpath = parts.shift();
27552
27573
  match = arrayRxp.exec(subpath);
@@ -36229,31 +36250,6 @@ ${svg}
36229
36250
 
36230
36251
  var externalCommands = {};
36231
36252
 
36232
- cmd.external = function(opts) {
36233
- // TODO: remove duplication with -require command
36234
- var _module, moduleFile, moduleName;
36235
- if (!opts.module) {
36236
- stop('Missing required "module" parameter');
36237
- }
36238
- if (cli.isFile(opts.module)) {
36239
- moduleFile = opts.module;
36240
- } else if (cli.isFile(opts.module + '.js')) {
36241
- moduleFile = opts.module + '.js';
36242
- } else {
36243
- moduleName = opts.module;
36244
- }
36245
- if (moduleFile) {
36246
- moduleFile = require$1('path').join(process.cwd(), moduleFile);
36247
- }
36248
- try {
36249
- _module = require$1(moduleFile || moduleName);
36250
- _module(coreAPI);
36251
- } catch(e) {
36252
- // stop(e);
36253
- stop('Unable to load external module:', e.message);
36254
- }
36255
- };
36256
-
36257
36253
  cmd.registerCommand = function(name, params) {
36258
36254
  var defn = {name: name, options: params.options || []};
36259
36255
  // Add definitions of options common to all commands (TODO: remove duplication)
@@ -36263,13 +36259,25 @@ ${svg}
36263
36259
  externalCommands[name] = defn;
36264
36260
  };
36265
36261
 
36262
+ function isValidExternalCommand(defn) {
36263
+ try {
36264
+ validateExternalCommand(defn);
36265
+ return true;
36266
+ } catch(e) {}
36267
+ return false;
36268
+ }
36269
+
36266
36270
  function validateExternalCommand(defn) {
36271
+ var targetTypes = ['layer', 'layers'];
36267
36272
  if (typeof defn.command != 'function') {
36268
36273
  stop('Expected "command" parameter function');
36269
36274
  }
36270
36275
  if (!defn.target) {
36271
36276
  stop('Missing required "target" parameter');
36272
36277
  }
36278
+ if (!targetTypes.includes(defn.target)) {
36279
+ stop('Unrecognized command target type:', defn.target);
36280
+ }
36273
36281
  }
36274
36282
 
36275
36283
  cmd.runExternalCommand = function(cmdOpts, catalog) {
@@ -40609,6 +40617,7 @@ ${svg}
40609
40617
  return outputLayers;
40610
40618
  };
40611
40619
 
40620
+
40612
40621
  function getPolygonDataset(pointLyr, gridBBox, opts) {
40613
40622
  var points = getPointsInLayer(pointLyr);
40614
40623
  var cellSize = opts.interval;
@@ -40714,6 +40723,8 @@ ${svg}
40714
40723
  return getPointBufferCoordinates(center, radius, 20, getPlanarSegmentEndpoint);
40715
40724
  }
40716
40725
 
40726
+ // Returns a function that receives a cell index and returns indices of points
40727
+ // within a given distance of the cell.
40717
40728
  function getPointIndex(points, grid, radius) {
40718
40729
  var Flatbush = require$1('flatbush');
40719
40730
  var gridIndex = new IdTestIndex(grid.cells());
@@ -40845,12 +40856,26 @@ ${svg}
40845
40856
  xmin + (c + 1) * interval, ymin + (r + 1) * interval
40846
40857
  ];
40847
40858
  }
40859
+
40848
40860
  return {
40849
40861
  size, cells, pointToCol, pointToRow, colRowToIdx, pointToIdx,
40850
40862
  idxToCol, idxToRow, idxToBBox, idxToPoint
40851
40863
  };
40852
40864
  }
40853
40865
 
40866
+ var PointToGrid = /*#__PURE__*/Object.freeze({
40867
+ __proto__: null,
40868
+ getPointCircleRadius: getPointCircleRadius,
40869
+ calcCellProperties: calcCellProperties,
40870
+ calcWeights: calcWeights,
40871
+ twoCircleIntersection: twoCircleIntersection,
40872
+ makeCellPolygon: makeCellPolygon,
40873
+ getPointIndex: getPointIndex,
40874
+ getAlignedGridBounds: getAlignedGridBounds,
40875
+ getCenteredGridBounds: getCenteredGridBounds,
40876
+ getGridData: getGridData
40877
+ });
40878
+
40854
40879
  function closeUndershoots(lyr, dataset, opts) {
40855
40880
  var maxGapLen = opts.gap_tolerance ? convertIntervalParam(opts.gap_tolerance, getDatasetCRS(dataset)) : 0;
40856
40881
  var arcs = dataset.arcs;
@@ -41165,8 +41190,16 @@ ${svg}
41165
41190
  }
41166
41191
  try {
41167
41192
  mod = require$1(moduleFile || moduleName);
41193
+ if (typeof mod == 'function') {
41194
+ // -require now includes the functionality of the old -external command
41195
+ var retn = mod(api);
41196
+ if (retn && isValidExternalCommand(retn)) {
41197
+ cmd.registerCommand(retn.name, retn);
41198
+ }
41199
+ }
41168
41200
  } catch(e) {
41169
- stop(e);
41201
+ // stop(e);
41202
+ stop('Unable to load external module:', e.message);
41170
41203
  }
41171
41204
  if (moduleName || opts.alias) {
41172
41205
  defs[opts.alias || moduleName] = mod;
@@ -43332,7 +43365,7 @@ ${svg}
43332
43365
  }
43333
43366
 
43334
43367
  function commandAcceptsMultipleTargetDatasets(name) {
43335
- return name == 'rotate' || name == 'info' || name == 'proj' ||
43368
+ return name == 'rotate' || name == 'info' || name == 'proj' || name == 'require' ||
43336
43369
  name == 'drop' || name == 'target' || name == 'if' || name == 'elif' ||
43337
43370
  name == 'else' || name == 'endif' || name == 'run' || name == 'i';
43338
43371
  }
@@ -43490,7 +43523,8 @@ ${svg}
43490
43523
  outputLayers = applyCommandToEachLayer(cmd.explodeFeatures, targetLayers, arcs, opts);
43491
43524
 
43492
43525
  } else if (name == 'external') {
43493
- cmd.external(opts);
43526
+ // -require now incorporates -external
43527
+ cmd.require(targets, opts);
43494
43528
 
43495
43529
  } else if (name == 'filter') {
43496
43530
  outputLayers = applyCommandToEachLayer(cmd.filterFeatures, targetLayers, arcs, opts);
@@ -44115,14 +44149,6 @@ ${svg}
44115
44149
  runAndRemoveInfoCommands: runAndRemoveInfoCommands
44116
44150
  });
44117
44151
 
44118
- // the mapshaper public api only has 4 functions
44119
- var coreAPI = {
44120
- runCommands,
44121
- applyCommands,
44122
- runCommandsXL,
44123
- enableLogging
44124
- };
44125
-
44126
44152
  // Return an array containing points from a path iterator, clipped to a bounding box
44127
44153
  // Currently using this function for clipping styled polygons in the GUI to speed up layer rendering.
44128
44154
  // Artifacts along the edges make this unsuitable for clipping datasets
@@ -44519,6 +44545,7 @@ ${svg}
44519
44545
  PixelTransform,
44520
44546
  PointPolygonJoin,
44521
44547
  Points,
44548
+ PointToGrid,
44522
44549
  PointUtils,
44523
44550
  PolygonDissolve,
44524
44551
  PolygonDissolve2,
@@ -44566,16 +44593,26 @@ ${svg}
44566
44593
  Zip
44567
44594
  );
44568
44595
 
44569
- // The entry point for the core mapshaper module
44596
+ // the mapshaper public api only has 4 functions
44597
+ var api = {
44598
+ runCommands,
44599
+ applyCommands,
44600
+ runCommandsXL,
44601
+ enableLogging
44602
+ };
44570
44603
 
44571
- var moduleAPI = Object.assign({
44604
+ // Add some namespaces, for easier testability and
44605
+ // to expose internal functions to the web UI
44606
+ Object.assign(api, {
44572
44607
  cli, cmd, geom, utils, internal,
44573
- }, coreAPI);
44608
+ });
44609
+
44610
+ // The entry point for the core mapshaper module
44574
44611
 
44575
44612
  if (typeof module === "object" && module.exports) {
44576
- module.exports = moduleAPI;
44613
+ module.exports = api;
44577
44614
  } else if (typeof window === "object" && window) {
44578
- window.mapshaper = moduleAPI;
44615
+ window.mapshaper = api;
44579
44616
  }
44580
44617
 
44581
44618
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.6.39",
3
+ "version": "0.6.41",
4
4
  "description": "A tool for editing vector datasets for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
package/www/index.html CHANGED
@@ -149,19 +149,27 @@
149
149
  </div>
150
150
  <div class="export-zip-option option-menu">
151
151
 
152
+ <div style="height:10px"></div>
153
+
152
154
  </div>
153
155
  <h4>File format</h4>
154
156
  <div class="export-formats option-menu">
155
157
  </div>
156
158
 
157
- <div class="option-menu"><input type="text" class="advanced-options" placeholder="command line options" />
159
+ <div style="height:10px"></div>
160
+
161
+ <div class="option-menu"><input type="text" class="text-input advanced-options" placeholder="command line options" />
158
162
  <a href="https://github.com/mbloch/mapshaper/wiki/Command-Reference#-o-output" target="_mapshaper_output_docs">
159
163
  <div class="tip-button">?<div class="tip-anchor">
160
164
  <div class="tip">Enter options from the command line interface for
161
165
  the -o command. Examples: bbox no-quantization
162
166
  precision=0.001. Click to see all options.</div></div></div></a>
163
-
164
167
  </div>
168
+
169
+ <!-- <div class="option-menu">
170
+ <input id="ofile-name" class="text-input" type="text" placeholder="output file name" />
171
+ </div> -->
172
+
165
173
  <!-- <div class="cancel-btn btn dialog-btn">Cancel</div> -->
166
174
  <div class="save-btn btn dialog-btn">Export</div>
167
175
  <span id="save-preference"><input type="checkbox"/>choose directory</span>
@@ -288,7 +296,7 @@ apply to TopoJSON files.</div></div></div></div> -->
288
296
 
289
297
  </div>
290
298
 
291
- <div><input type="text" class="advanced-options" placeholder="import options" />
299
+ <div><input type="text" class="text-input advanced-options" placeholder="import options" />
292
300
  <a href="https://github.com/mbloch/mapshaper/wiki/Command-Reference#-i-input" target="_mapshaper_import_docs">
293
301
  <div class="tip-button">?<div class="tip-anchor">
294
302
  <div class="tip">Enter options from the command line
@@ -1265,7 +1265,6 @@
1265
1265
  .onCancel(done);
1266
1266
  }
1267
1267
 
1268
-
1269
1268
  async function saveBlobToSelectedFile(filename, blob, done) {
1270
1269
  // see: https://developer.chrome.com/articles/file-system-access/
1271
1270
  // note: saving multiple files to a directory using showDirectoryPicker()
@@ -1427,7 +1426,6 @@
1427
1426
  // }
1428
1427
 
1429
1428
  snapshots.forEach(function(item, i) {
1430
- var id = i + 1;
1431
1429
  var line = El('div').appendTo(menu).addClass('save-menu-item');
1432
1430
  El('span').appendTo(line).html(`<span class="save-item-label">#${item.number}</span> `);
1433
1431
  // show snapshot size
@@ -1440,7 +1438,10 @@
1440
1438
  var obj = await idb.get(item.id);
1441
1439
  await internal.applyCompression(obj, {consume: true});
1442
1440
  var buf = internal.pack(obj);
1443
- saveBlobToLocalFile('mapshaper_snapshot.msx', new Blob([buf]));
1441
+ // choose output filename and directory every time
1442
+ // saveBlobToLocalFile('mapshaper_snapshot.msx', new Blob([buf]));
1443
+ var fileName = `snapshot-${String(item.number).padStart(2, '0')}.msx`;
1444
+ saveBlobToSelectedFile(fileName, new Blob([buf]), function() {});
1444
1445
  }).text('export');
1445
1446
  El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
1446
1447
  await removeSnapshotById(item.id);
@@ -1978,7 +1979,7 @@
1978
1979
  var names = getFileNames(files);
1979
1980
  var expanded = [];
1980
1981
  if (files.length === 0) return;
1981
- useQuickView = importCount === 0 && (opts.quick_view || overQuickView);
1982
+ useQuickView = importTotal === 0 && (opts.quick_view || overQuickView);
1982
1983
  try {
1983
1984
  expanded = await expandFiles(files);
1984
1985
  } catch(e) {
@@ -3644,6 +3645,7 @@
3644
3645
  var layersArr = [];
3645
3646
  var toggleBtn = null; // checkbox <input> for toggling layer selection
3646
3647
  var exportBtn = gui.container.findChild('.export-btn');
3648
+ var ofileName = gui.container.findChild('#ofile-name');
3647
3649
  new SimpleButton(menu.findChild('.close2-btn')).on('click', gui.clearMode);
3648
3650
 
3649
3651
  if (!GUI.exportIsSupported()) {
@@ -3729,7 +3731,7 @@
3729
3731
  // done: function(string|Error|null)
3730
3732
  async function exportMenuSelection(layers) {
3731
3733
  var opts = getExportOpts();
3732
- // note: command line "target" option gets ignored
3734
+ // note: command line "target" option gets ignored
3733
3735
  var files = await internal.exportTargetLayers(layers, opts);
3734
3736
  gui.session.layersExported(getTargetLayerIds(), getExportOptsAsString());
3735
3737
  await utils$1.promisify(internal.writeFiles)(files, opts);
@@ -9169,6 +9171,10 @@
9169
9171
  return calcBounds(_cx, _cy, _scale / (k || 1));
9170
9172
  };
9171
9173
 
9174
+ this.getFullBounds = function() {
9175
+ return _fullBounds;
9176
+ };
9177
+
9172
9178
  // Update the extent of 'full' zoom without navigating the current view
9173
9179
  //
9174
9180
  this.setFullBounds = function(fullBounds, strictBounds) {
@@ -9504,6 +9510,7 @@
9504
9510
  canv.drawVertices(layer.shapes, arcs, style, filter);
9505
9511
  }
9506
9512
  }
9513
+ canv.clearStyles();
9507
9514
  }
9508
9515
 
9509
9516
 
@@ -9564,6 +9571,11 @@
9564
9571
  _pixelColor = getPixelColorFunction(),
9565
9572
  _ext;
9566
9573
 
9574
+ _self.clearStyles = function() {
9575
+ _ctx.fillStyle = null;
9576
+ _ctx.strokeStyle = null;
9577
+ };
9578
+
9567
9579
  _self.prep = function(extent) {
9568
9580
  var w = extent.width(),
9569
9581
  h = extent.height(),
@@ -10549,25 +10561,11 @@
10549
10561
 
10550
10562
 
10551
10563
  function getDisplayBounds(lyr, arcs) {
10552
- var arcBounds = arcs ? arcs.getBounds() : new Bounds(),
10553
- bounds = arcBounds, // default display extent: all arcs in the dataset
10554
- lyrBounds;
10555
-
10556
- if (lyr.geometry_type == 'point') {
10557
- lyrBounds = internal.getLayerBounds(lyr);
10558
- if (lyrBounds && lyrBounds.hasBounds()) {
10559
- if (lyrBounds.area() > 0 || !arcBounds.hasBounds()) {
10560
- bounds = lyrBounds;
10561
- } else {
10562
- // if a point layer has no extent (e.g. contains only a single point),
10563
- // then merge with arc bounds, to place the point in context.
10564
- bounds = arcBounds.mergeBounds(lyrBounds);
10565
- }
10566
- }
10567
- }
10568
-
10569
- if (!bounds || !bounds.hasBounds()) { // empty layer
10570
- bounds = new Bounds();
10564
+ var bounds = internal.getLayerBounds(lyr, arcs) || new Bounds();
10565
+ if (lyr.geometry_type == 'point' && arcs && bounds.hasBounds() && bounds.area() > 0 === false) {
10566
+ // if a point layer has no extent (e.g. contains only a single point),
10567
+ // then merge with arc bounds, to place the point in context.
10568
+ bounds = bounds.mergeBounds(arcs.getBounds());
10571
10569
  }
10572
10570
  return bounds;
10573
10571
  }
@@ -10841,7 +10839,6 @@
10841
10839
  _ext = new MapExtent(position),
10842
10840
  _nav = new MapNav(gui, _ext, _mouse),
10843
10841
  _visibleLayers = [], // cached visible map layers
10844
- _fullBounds = null,
10845
10842
  _hit,
10846
10843
  _basemap,
10847
10844
  _intersectionLyr, _activeLyr, _overlayLyr,
@@ -10949,8 +10946,8 @@
10949
10946
  updateLayerStyles(getDrawableContentLayers()); // kludge to make sure all layers have styles
10950
10947
 
10951
10948
  // Update map extent (also triggers redraw)
10952
- projectMapExtent(_ext, oldCRS, this.getDisplayCRS(), getFullBounds());
10953
- _fullBounds = getFullBounds(); // update this so map extent doesn't get reset after next update
10949
+ projectMapExtent(_ext, oldCRS, this.getDisplayCRS(), calcFullBounds());
10950
+ updateFullBounds();
10954
10951
  };
10955
10952
 
10956
10953
  // Refresh map display in response to data changes, layer selection, etc.
@@ -10993,28 +10990,27 @@
10993
10990
  gui.dispatchEvent('popup-needs-refresh');
10994
10991
  } else if (_hit) {
10995
10992
  _hit.clearSelection();
10996
- _hit.setLayer(_activeLyr);
10997
10993
  }
10994
+ _hit.setLayer(_activeLyr); // need this every time, to support dynamic reprojection
10998
10995
 
10999
10996
  updateVisibleMapLayers();
11000
- fullBounds = getFullBounds();
10997
+ fullBounds = calcFullBounds();
11001
10998
 
11002
- if (!prevLyr || !_fullBounds || prevLyr.tabular || _activeLyr.tabular || isFrameView()) {
10999
+ if (!prevLyr || prevLyr.tabular || _activeLyr.tabular || isFrameView()) {
11003
11000
  needReset = true;
11004
11001
  } else {
11005
- needReset = mapNeedsReset(fullBounds, _fullBounds, _ext.getBounds(), e.flags);
11002
+ needReset = mapNeedsReset(fullBounds, _ext.getFullBounds(), _ext.getBounds(), e.flags);
11006
11003
  }
11007
11004
 
11008
11005
  if (isFrameView()) {
11009
11006
  _nav.setZoomFactor(0.05); // slow zooming way down to allow fine-tuning frame placement // 0.03
11010
- _ext.setFrame(getFullBounds()); // TODO: remove redundancy with drawLayers()
11007
+ _ext.setFrame(calcFullBounds()); // TODO: remove redundancy with drawLayers()
11011
11008
  needReset = true; // snap to frame extent
11012
11009
  } else {
11013
11010
  _nav.setZoomFactor(1);
11014
11011
  }
11015
11012
  _ext.setFullBounds(fullBounds, getStrictBounds()); // update 'home' button extent
11016
11013
 
11017
- _fullBounds = fullBounds;
11018
11014
  if (needReset) {
11019
11015
  _ext.reset();
11020
11016
  }
@@ -11115,12 +11111,17 @@
11115
11111
  return null;
11116
11112
  }
11117
11113
 
11118
- function getFullBounds() {
11114
+ function updateFullBounds() {
11115
+ _ext.setFullBounds(calcFullBounds(), getStrictBounds());
11116
+ }
11117
+
11118
+ function calcFullBounds() {
11119
11119
  if (isPreviewView()) {
11120
11120
  return internal.getFrameLayerBounds(internal.findFrameLayer(model));
11121
11121
  }
11122
11122
  var b = new Bounds();
11123
- getDrawableContentLayers().forEach(function(lyr) {
11123
+ var layers = getDrawableContentLayers();
11124
+ layers.forEach(function(lyr) {
11124
11125
  b.mergeBounds(lyr.bounds);
11125
11126
  });
11126
11127
 
@@ -11265,6 +11266,7 @@
11265
11266
  // (default) anything could have changed
11266
11267
  function drawLayers2(action) {
11267
11268
  var layersMayHaveChanged = !action;
11269
+ var fullBounds;
11268
11270
  var contentLayers = getDrawableContentLayers();
11269
11271
  var furnitureLayers = getDrawableFurnitureLayers();
11270
11272
  if (!(_ext.width() > 0 && _ext.height() > 0)) {
@@ -11275,7 +11277,7 @@
11275
11277
  if (layersMayHaveChanged) {
11276
11278
  // kludge to handle layer visibility toggling
11277
11279
  _ext.setFrame(isPreviewView() ? getFrameData() : null);
11278
- _ext.setFullBounds(getFullBounds(), getStrictBounds());
11280
+ updateFullBounds();
11279
11281
  updateLayerStyles(contentLayers);
11280
11282
  updateLayerStackOrder(model.getLayers());// update menu_order property of all layers
11281
11283
  }
package/www/mapshaper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.6.39";
3
+ var VERSION = "0.6.41";
4
4
 
5
5
 
6
6
  var utils = /*#__PURE__*/Object.freeze({
@@ -10877,7 +10877,8 @@
10877
10877
  pack: pack,
10878
10878
  exportDatasetsToPack: exportDatasetsToPack,
10879
10879
  applyCompression: applyCompression,
10880
- exportDataset: exportDataset
10880
+ exportDataset: exportDataset,
10881
+ exportInfo: exportInfo
10881
10882
  });
10882
10883
 
10883
10884
  // Guess the type of a data file from file extension, or return null if not sure
@@ -13142,17 +13143,23 @@
13142
13143
  var rxp = fmt;
13143
13144
  rxp = rxp.replace('[-]', '(?<prefix>-)?'); // optional -
13144
13145
  rxp = rxp.replace(/\[[NSEW, +-]{2,}\]/, '(?<prefix>$&)');
13146
+ // TODO: validate that if there are degree decimals, there are no M or S codes
13147
+ rxp = rxp.replace(/D+(\.D+)?/, (m, g1) => {
13148
+ var s = '[0-9]+';
13149
+ if (g1) s += `\\.[0-9]+`;
13150
+ return `(?<d>${s})`;
13151
+ });
13152
+ // TODO: validate that if there are minutes decimals, there are no S codes
13145
13153
  rxp = rxp.replace(/(MM?)(\.M+)?/, (m, g1, g2) => {
13146
13154
  var s = g1.length == 1 ? '[0-9]+' : '[0-9][0-9]';
13147
- if (g2) s += `\\.[0-9]+`;
13155
+ if (g2) s += '\\.[0-9]+';
13148
13156
  return `(?<m>${s})`;
13149
13157
  });
13150
13158
  rxp = rxp.replace(/(SS?)(\.S+)?/, (m, g1, g2) => {
13151
13159
  var s = g1.length == 1 ? '[0-9]+' : '[0-9][0-9]';
13152
- if (g2) s += `\\.[0-9]+`;
13160
+ if (g2) s += '\\.[0-9]+';
13153
13161
  return `(?<s>${s})`;
13154
13162
  });
13155
- rxp = rxp.replace(/D+/, '(?<d>[0-9]+)');
13156
13163
  rxp = '^' + rxp + '$';
13157
13164
  try {
13158
13165
  // TODO: make sure all DMS codes have been matched
@@ -13173,43 +13180,42 @@
13173
13180
  return str;
13174
13181
  }
13175
13182
 
13176
- function getDecimals(fmt) {
13177
- var match = /S\.(S+)/.exec(fmt) || /M\.(M+)/.exec(fmt) || null;
13178
- return match ? match[1].length : 0;
13179
- }
13180
-
13181
- // TODO: support DD.DDDDD
13182
13183
  function formatDMS(coord, fmt) {
13183
13184
  if (!fmt) fmt = '[-]D°M\'S.SSS';
13184
13185
  var str = fmt;
13185
13186
  var dstr, mstr, sstr;
13186
- var match = /(D+)[^M]*(M+)[^S[\]]*(S+)?/.exec(fmt);
13187
- if (!match) {
13187
+ var match = /(D+)[^M]*(M+)?[^S[\]]*(S+)?/.exec(fmt);
13188
+ var gotSeconds = match && !!match[3];
13189
+ var gotMinutes = match && !!match[2];
13190
+ if (!match || gotSeconds && !gotMinutes) {
13188
13191
  stop('Invalid DMS format string:', fmt);
13189
13192
  }
13190
- var gotSeconds = !!match[3];
13191
- var decimals = getDecimals(fmt);
13192
- var integers = gotSeconds ? match[3].length : match[2].length;
13193
- var RES = Math.pow(10, decimals);
13194
- var CONV = gotSeconds ? 3600 * RES : 60 * RES;
13195
- var r = Math.floor(Math.abs(coord) * CONV + 0.5);
13196
- var lastPart = formatNumber((r / RES) % 60, integers, decimals);
13197
- if (gotSeconds) {
13198
- r = Math.floor(r / (RES * 60));
13199
- sstr = lastPart;
13200
- mstr = String(r % 60).padStart(match[2].length, '0');
13193
+ var integers = gotSeconds && match[3].length || gotMinutes && match[2].length || match[1].length;
13194
+ var decimalRxp = gotSeconds && /S\.(S+)/ || gotMinutes && /M\.(M+)/ || /D\.(D+)/;
13195
+ var decimals = decimalRxp.test(fmt) ? decimalRxp.exec(fmt)[1].length : 0;
13196
+ if (gotMinutes) {
13197
+ var RES = Math.pow(10, decimals);
13198
+ var CONV = gotSeconds ? 3600 * RES : 60 * RES;
13199
+ var r = Math.floor(Math.abs(coord) * CONV + 0.5);
13200
+ var lastPart = formatNumber((r / RES) % 60, integers, decimals);
13201
+ if (gotSeconds) {
13202
+ r = Math.floor(r / (RES * 60));
13203
+ sstr = lastPart;
13204
+ mstr = String(r % 60).padStart(match[2].length, '0');
13205
+ } else {
13206
+ r = Math.floor(r / RES);
13207
+ mstr = lastPart;
13208
+ }
13209
+ dstr = String(Math.floor(r / 60)).padStart(match[1].length, '0');
13201
13210
  } else {
13202
- r = Math.floor(r / RES);
13203
- mstr = lastPart;
13204
- sstr = '';
13211
+ dstr = Math.abs(coord).toFixed(decimals);
13205
13212
  }
13206
- dstr = String(Math.floor(r / 60)).padStart(match[1].length, '0');
13207
13213
  str = str.replace(/\[-\]/, s => coord < 0 ? '-' : '');
13208
13214
  str = str.replace(/\[[+-]+\]/, s => coord < 0 ? '-' : '+');
13209
13215
  str = str.replace(/\[[NS, ]+\]/, s => coord < 0 ? 'S' : 'N');
13210
13216
  str = str.replace(/\[[EW, ]+\]/, s => coord < 0 ? 'W' : 'E');
13211
- str = str.replace(/D+/, dstr);
13212
- str = str.replace(/M+(\.M+)?/, mstr);
13217
+ str = str.replace(/D+(\.D+)?/, dstr);
13218
+ if (gotMinutes) str = str.replace(/M+(\.M+)?/, mstr);
13213
13219
  if (gotSeconds) str = str.replace(/S+(\.S+)?/, sstr);
13214
13220
  return str;
13215
13221
  }
@@ -17699,12 +17705,18 @@
17699
17705
  }
17700
17706
 
17701
17707
  function calcFrameData(dataset, opts) {
17702
- var bounds = getDatasetBounds(dataset);
17703
- var bounds2 = calcOutputSizeInPixels(bounds, opts);
17708
+ var bounds;
17709
+ if (opts.svg_bbox) {
17710
+ bounds = new Bounds(opts.svg_bbox);
17711
+ opts = Object.assign({margin: 0}, opts); // prevent default pixel margin around content
17712
+ } else {
17713
+ bounds = getDatasetBounds(dataset);
17714
+ }
17715
+ var pixBounds = calcOutputSizeInPixels(bounds, opts);
17704
17716
  return {
17705
17717
  bbox: bounds.toArray(),
17706
- width: Math.round(bounds2.width()),
17707
- height: Math.round(bounds2.height()) || 1,
17718
+ width: Math.round(pixBounds.width()),
17719
+ height: Math.round(pixBounds.height()) || 1,
17708
17720
  type: 'frame'
17709
17721
  };
17710
17722
  }
@@ -23375,6 +23387,10 @@ ${svg}
23375
23387
  describe: '[TopoJSON] export coordinates without quantization',
23376
23388
  type: 'flag'
23377
23389
  })
23390
+ .option('metadata', {
23391
+ // describe: '[TopoJSON] Add a metadata object containing CRS information',
23392
+ type: 'flag'
23393
+ })
23378
23394
  .option('no-point-quantization', {
23379
23395
  // describe: '[TopoJSON] export point coordinates without quantization',
23380
23396
  type: 'flag'
@@ -23439,6 +23455,10 @@ ${svg}
23439
23455
  describe: '[SVG] source units per pixel (alternative to width= option)',
23440
23456
  type: 'number'
23441
23457
  })
23458
+ .option('svg-bbox', {
23459
+ describe: '[SVG] bounding box of SVG map in projected map units',
23460
+ type: 'bbox'
23461
+ })
23442
23462
  .option('point-symbol', {
23443
23463
  describe: '[SVG] circle or square (default is circle)'
23444
23464
  })
@@ -27547,6 +27567,7 @@ ${svg}
27547
27567
  var separator = path.indexOf('/') > 0 ? '/' : '.';
27548
27568
  var parts = path.split(separator);
27549
27569
  var subpath, array, match;
27570
+
27550
27571
  while (parts.length > 0) {
27551
27572
  subpath = parts.shift();
27552
27573
  match = arrayRxp.exec(subpath);
@@ -36229,31 +36250,6 @@ ${svg}
36229
36250
 
36230
36251
  var externalCommands = {};
36231
36252
 
36232
- cmd.external = function(opts) {
36233
- // TODO: remove duplication with -require command
36234
- var _module, moduleFile, moduleName;
36235
- if (!opts.module) {
36236
- stop('Missing required "module" parameter');
36237
- }
36238
- if (cli.isFile(opts.module)) {
36239
- moduleFile = opts.module;
36240
- } else if (cli.isFile(opts.module + '.js')) {
36241
- moduleFile = opts.module + '.js';
36242
- } else {
36243
- moduleName = opts.module;
36244
- }
36245
- if (moduleFile) {
36246
- moduleFile = require$1('path').join(process.cwd(), moduleFile);
36247
- }
36248
- try {
36249
- _module = require$1(moduleFile || moduleName);
36250
- _module(coreAPI);
36251
- } catch(e) {
36252
- // stop(e);
36253
- stop('Unable to load external module:', e.message);
36254
- }
36255
- };
36256
-
36257
36253
  cmd.registerCommand = function(name, params) {
36258
36254
  var defn = {name: name, options: params.options || []};
36259
36255
  // Add definitions of options common to all commands (TODO: remove duplication)
@@ -36263,13 +36259,25 @@ ${svg}
36263
36259
  externalCommands[name] = defn;
36264
36260
  };
36265
36261
 
36262
+ function isValidExternalCommand(defn) {
36263
+ try {
36264
+ validateExternalCommand(defn);
36265
+ return true;
36266
+ } catch(e) {}
36267
+ return false;
36268
+ }
36269
+
36266
36270
  function validateExternalCommand(defn) {
36271
+ var targetTypes = ['layer', 'layers'];
36267
36272
  if (typeof defn.command != 'function') {
36268
36273
  stop('Expected "command" parameter function');
36269
36274
  }
36270
36275
  if (!defn.target) {
36271
36276
  stop('Missing required "target" parameter');
36272
36277
  }
36278
+ if (!targetTypes.includes(defn.target)) {
36279
+ stop('Unrecognized command target type:', defn.target);
36280
+ }
36273
36281
  }
36274
36282
 
36275
36283
  cmd.runExternalCommand = function(cmdOpts, catalog) {
@@ -40609,6 +40617,7 @@ ${svg}
40609
40617
  return outputLayers;
40610
40618
  };
40611
40619
 
40620
+
40612
40621
  function getPolygonDataset(pointLyr, gridBBox, opts) {
40613
40622
  var points = getPointsInLayer(pointLyr);
40614
40623
  var cellSize = opts.interval;
@@ -40714,6 +40723,8 @@ ${svg}
40714
40723
  return getPointBufferCoordinates(center, radius, 20, getPlanarSegmentEndpoint);
40715
40724
  }
40716
40725
 
40726
+ // Returns a function that receives a cell index and returns indices of points
40727
+ // within a given distance of the cell.
40717
40728
  function getPointIndex(points, grid, radius) {
40718
40729
  var Flatbush = require$1('flatbush');
40719
40730
  var gridIndex = new IdTestIndex(grid.cells());
@@ -40845,12 +40856,26 @@ ${svg}
40845
40856
  xmin + (c + 1) * interval, ymin + (r + 1) * interval
40846
40857
  ];
40847
40858
  }
40859
+
40848
40860
  return {
40849
40861
  size, cells, pointToCol, pointToRow, colRowToIdx, pointToIdx,
40850
40862
  idxToCol, idxToRow, idxToBBox, idxToPoint
40851
40863
  };
40852
40864
  }
40853
40865
 
40866
+ var PointToGrid = /*#__PURE__*/Object.freeze({
40867
+ __proto__: null,
40868
+ getPointCircleRadius: getPointCircleRadius,
40869
+ calcCellProperties: calcCellProperties,
40870
+ calcWeights: calcWeights,
40871
+ twoCircleIntersection: twoCircleIntersection,
40872
+ makeCellPolygon: makeCellPolygon,
40873
+ getPointIndex: getPointIndex,
40874
+ getAlignedGridBounds: getAlignedGridBounds,
40875
+ getCenteredGridBounds: getCenteredGridBounds,
40876
+ getGridData: getGridData
40877
+ });
40878
+
40854
40879
  function closeUndershoots(lyr, dataset, opts) {
40855
40880
  var maxGapLen = opts.gap_tolerance ? convertIntervalParam(opts.gap_tolerance, getDatasetCRS(dataset)) : 0;
40856
40881
  var arcs = dataset.arcs;
@@ -41165,8 +41190,16 @@ ${svg}
41165
41190
  }
41166
41191
  try {
41167
41192
  mod = require$1(moduleFile || moduleName);
41193
+ if (typeof mod == 'function') {
41194
+ // -require now includes the functionality of the old -external command
41195
+ var retn = mod(api);
41196
+ if (retn && isValidExternalCommand(retn)) {
41197
+ cmd.registerCommand(retn.name, retn);
41198
+ }
41199
+ }
41168
41200
  } catch(e) {
41169
- stop(e);
41201
+ // stop(e);
41202
+ stop('Unable to load external module:', e.message);
41170
41203
  }
41171
41204
  if (moduleName || opts.alias) {
41172
41205
  defs[opts.alias || moduleName] = mod;
@@ -43332,7 +43365,7 @@ ${svg}
43332
43365
  }
43333
43366
 
43334
43367
  function commandAcceptsMultipleTargetDatasets(name) {
43335
- return name == 'rotate' || name == 'info' || name == 'proj' ||
43368
+ return name == 'rotate' || name == 'info' || name == 'proj' || name == 'require' ||
43336
43369
  name == 'drop' || name == 'target' || name == 'if' || name == 'elif' ||
43337
43370
  name == 'else' || name == 'endif' || name == 'run' || name == 'i';
43338
43371
  }
@@ -43490,7 +43523,8 @@ ${svg}
43490
43523
  outputLayers = applyCommandToEachLayer(cmd.explodeFeatures, targetLayers, arcs, opts);
43491
43524
 
43492
43525
  } else if (name == 'external') {
43493
- cmd.external(opts);
43526
+ // -require now incorporates -external
43527
+ cmd.require(targets, opts);
43494
43528
 
43495
43529
  } else if (name == 'filter') {
43496
43530
  outputLayers = applyCommandToEachLayer(cmd.filterFeatures, targetLayers, arcs, opts);
@@ -44115,14 +44149,6 @@ ${svg}
44115
44149
  runAndRemoveInfoCommands: runAndRemoveInfoCommands
44116
44150
  });
44117
44151
 
44118
- // the mapshaper public api only has 4 functions
44119
- var coreAPI = {
44120
- runCommands,
44121
- applyCommands,
44122
- runCommandsXL,
44123
- enableLogging
44124
- };
44125
-
44126
44152
  // Return an array containing points from a path iterator, clipped to a bounding box
44127
44153
  // Currently using this function for clipping styled polygons in the GUI to speed up layer rendering.
44128
44154
  // Artifacts along the edges make this unsuitable for clipping datasets
@@ -44519,6 +44545,7 @@ ${svg}
44519
44545
  PixelTransform,
44520
44546
  PointPolygonJoin,
44521
44547
  Points,
44548
+ PointToGrid,
44522
44549
  PointUtils,
44523
44550
  PolygonDissolve,
44524
44551
  PolygonDissolve2,
@@ -44566,16 +44593,26 @@ ${svg}
44566
44593
  Zip
44567
44594
  );
44568
44595
 
44569
- // The entry point for the core mapshaper module
44596
+ // the mapshaper public api only has 4 functions
44597
+ var api = {
44598
+ runCommands,
44599
+ applyCommands,
44600
+ runCommandsXL,
44601
+ enableLogging
44602
+ };
44570
44603
 
44571
- var moduleAPI = Object.assign({
44604
+ // Add some namespaces, for easier testability and
44605
+ // to expose internal functions to the web UI
44606
+ Object.assign(api, {
44572
44607
  cli, cmd, geom, utils, internal,
44573
- }, coreAPI);
44608
+ });
44609
+
44610
+ // The entry point for the core mapshaper module
44574
44611
 
44575
44612
  if (typeof module === "object" && module.exports) {
44576
- module.exports = moduleAPI;
44613
+ module.exports = api;
44577
44614
  } else if (typeof window === "object" && window) {
44578
- window.mapshaper = moduleAPI;
44615
+ window.mapshaper = api;
44579
44616
  }
44580
44617
 
44581
44618
  })();
package/www/page.css CHANGED
@@ -242,10 +242,6 @@ body {
242
242
  white-space: nowrap;
243
243
  }
244
244
 
245
- .export-options .option-menu .advanced-options {
246
- width: 195px;
247
- }
248
-
249
245
  /* --- Main area ------------- */
250
246
 
251
247
  .main-area {
@@ -450,14 +446,14 @@ body.dragover #import-options-drop-area .drop-area {
450
446
 
451
447
  /* --- Import screen -------------- */
452
448
 
453
-
454
- .option-menu .advanced-options {
455
- width: 250px;
449
+ .option-menu .text-input {
450
+ width: 230px;
456
451
  background: rgba(255, 255, 255, 0.4);
457
452
  border: 1px solid #999;
458
453
  padding: 0 3px;
459
454
  height: 18px;
460
455
  font-size: 13px;
456
+ margin: 0 0 5px 0;
461
457
  }
462
458
 
463
459
  #mshp-not-supported {
@@ -548,10 +544,6 @@ body.dragover #import-options-drop-area .drop-area {
548
544
  font-size: 90%;
549
545
  }
550
546
 
551
- .option-menu {
552
- padding: 0 0 9px 0;
553
- }
554
-
555
547
  /*.option-menu input[type="radio"],
556
548
  .option-menu input[type="checkbox"] */
557
549
  .info-box input[type="radio"],