mapshaper 0.6.23 → 0.6.25

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/www/mapshaper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.6.23";
3
+ var VERSION = "0.6.25";
4
4
 
5
5
 
6
6
  var utils = /*#__PURE__*/Object.freeze({
@@ -2449,6 +2449,17 @@
2449
2449
  getPathCentroid: getPathCentroid
2450
2450
  });
2451
2451
 
2452
+ function testSegmentBoundsIntersection(a, b, bb) {
2453
+ if (bb.containsPoint(a[0], a[1])) {
2454
+ return true;
2455
+ }
2456
+ return !!(
2457
+ geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmin, bb.ymin, bb.xmin, bb.ymax) ||
2458
+ geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmin, bb.ymax, bb.xmax, bb.ymax) ||
2459
+ geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmax, bb.ymax, bb.xmax, bb.ymin) ||
2460
+ geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmax, bb.ymin, bb.xmin, bb.ymin));
2461
+ }
2462
+
2452
2463
  // A compactness measure designed for testing electoral districts for gerrymandering.
2453
2464
  // Returns value in [0-1] range. 1 = perfect circle, 0 = collapsed polygon
2454
2465
  function calcPolsbyPopperCompactness(area, perimeter) {
@@ -2496,21 +2507,32 @@
2496
2507
  // }, 0);
2497
2508
  // }
2498
2509
 
2510
+ // test if a rectangle is completely enclosed in a planar polygon
2511
+ function testBoundsInPolygon(bounds, shp, arcs) {
2512
+ if (!shp || !testPointInPolygon(bounds.xmin, bounds.ymin, shp, arcs)) return false;
2513
+ var isIn = true;
2514
+ shp.forEach(function(ids) {
2515
+ forEachSegmentInPath(ids, arcs, function(a, b, xx, yy) {
2516
+ isIn = isIn && !testSegmentBoundsIntersection([xx[a], yy[a]], [xx[b], yy[b]], bounds);
2517
+ });
2518
+ });
2519
+ return isIn;
2520
+ }
2521
+
2499
2522
  // Return true if point is inside or on boundary of a shape
2500
2523
  //
2501
2524
  function testPointInPolygon(x, y, shp, arcs) {
2502
2525
  var isIn = false,
2503
2526
  isOn = false;
2504
- if (shp) {
2505
- shp.forEach(function(ids) {
2506
- var inRing = testPointInRing(x, y, ids, arcs);
2507
- if (inRing == 1) {
2508
- isIn = !isIn;
2509
- } else if (inRing == -1) {
2510
- isOn = true;
2511
- }
2512
- });
2513
- }
2527
+ if (!shp) return false;
2528
+ shp.forEach(function(ids) {
2529
+ var inRing = testPointInRing(x, y, ids, arcs);
2530
+ if (inRing == 1) {
2531
+ isIn = !isIn;
2532
+ } else if (inRing == -1) {
2533
+ isOn = true;
2534
+ }
2535
+ });
2514
2536
  return isOn || isIn;
2515
2537
  }
2516
2538
 
@@ -2518,6 +2540,8 @@
2518
2540
  return ay + (x - ax) * (by - ay) / (bx - ax);
2519
2541
  }
2520
2542
 
2543
+
2544
+
2521
2545
  // Test if point (x, y) is inside, outside or on the boundary of a polygon ring
2522
2546
  // Return 0: outside; 1: inside; -1: on boundary
2523
2547
  //
@@ -2708,6 +2732,7 @@
2708
2732
  getShapeArea: getShapeArea,
2709
2733
  getPlanarShapeArea: getPlanarShapeArea,
2710
2734
  getSphericalShapeArea: getSphericalShapeArea,
2735
+ testBoundsInPolygon: testBoundsInPolygon,
2711
2736
  testPointInPolygon: testPointInPolygon,
2712
2737
  testPointInRing: testPointInRing,
2713
2738
  testRayIntersection: testRayIntersection,
@@ -7251,7 +7276,7 @@
7251
7276
  let size = source.length;
7252
7277
  let value = this ? this.unpack(source, size) : defaultUnpackr.unpack(source, size);
7253
7278
  if (forEach) {
7254
- forEach(value);
7279
+ if (forEach(value) === false) return;
7255
7280
  while(position$1 < size) {
7256
7281
  lastPosition = position$1;
7257
7282
  if (forEach(checkedRead()) === false) {
@@ -8193,7 +8218,7 @@
8193
8218
  let safeEnd;
8194
8219
  let bundledStrings = null;
8195
8220
  let writeStructSlots;
8196
- const MAX_BUNDLE_SIZE = 0xf000;
8221
+ const MAX_BUNDLE_SIZE = 0x5500; // maximum characters such that the encoded bytes fits in 16 bits.
8197
8222
  const hasNonLatin = /[\u0080-\uFFFF]/;
8198
8223
  const RECORD_SYMBOL = Symbol('record-id');
8199
8224
  class Packr extends Unpackr {
@@ -8222,7 +8247,7 @@
8222
8247
  if (maxSharedStructures > 8160)
8223
8248
  throw new Error('Maximum maxSharedStructure is 8160')
8224
8249
  if (options.structuredClone && options.moreTypes == undefined) {
8225
- options.moreTypes = true;
8250
+ this.moreTypes = true;
8226
8251
  }
8227
8252
  let maxOwnStructures = options.maxOwnStructures;
8228
8253
  if (maxOwnStructures == null)
@@ -8917,12 +8942,11 @@
8917
8942
  if (notifySharedUpdate)
8918
8943
  return hasSharedUpdate = true;
8919
8944
  position = newPosition;
8920
- if (start > 0) {
8921
- pack(value);
8922
- if (start == 0)
8923
- return { position, targetView, target }; // indicate the buffer was re-allocated
8924
- } else
8925
- pack(value);
8945
+ let startTarget = target;
8946
+ pack(value);
8947
+ if (startTarget !== target) {
8948
+ return { position, targetView, target }; // indicate the buffer was re-allocated
8949
+ }
8926
8950
  return position;
8927
8951
  }, this);
8928
8952
  if (newPosition === 0) // bail and go to a msgpack object
@@ -10620,7 +10644,8 @@
10620
10644
  buf = new Uint8Array(buf);
10621
10645
  }
10622
10646
  opts = opts || {};
10623
- var out = await utils.promisify(gunzip)(buf, opts);
10647
+ var gunzip$1 = runningInBrowser() ? utils.promisify(gunzip) : utils.promisify(require('zlib').gunzip);
10648
+ var out = await gunzip$1(buf, opts);
10624
10649
  if (opts.filename && !isImportableAsBinary(opts.filename)) {
10625
10650
  out = strFromU8(out);
10626
10651
  }
@@ -10628,8 +10653,6 @@
10628
10653
  }
10629
10654
 
10630
10655
  function gunzipSync(buf, filename) {
10631
- // TODO: use native module in Node
10632
- // require('zlib').
10633
10656
  if (buf instanceof ArrayBuffer) {
10634
10657
  buf = new Uint8Array(buf);
10635
10658
  }
@@ -10768,16 +10791,39 @@
10768
10791
  // gui: (optional) gui instance
10769
10792
  //
10770
10793
  async function exportDatasetsToPack(datasets, opts) {
10771
- return {
10794
+ var obj = {
10772
10795
  version: 1,
10773
10796
  created: (new Date).toISOString(),
10774
- datasets: await Promise.all(datasets.map(d => exportDataset(d, opts || {})))
10797
+ datasets: await Promise.all(datasets.map(exportDataset))
10775
10798
  };
10799
+ if (opts.compact) {
10800
+ await applyCompression(obj);
10801
+ }
10802
+ return obj;
10803
+ }
10804
+
10805
+ async function applyCompression(obj, opts) {
10806
+ var promises = [];
10807
+ obj.datasets.forEach(d => {
10808
+ if (d.arcs) promises.push(compressArcs(d.arcs, opts));
10809
+ });
10810
+ await Promise.all(promises);
10811
+ }
10812
+
10813
+ async function compressArcs(obj, opts) {
10814
+ var gzipOpts = Object.assign({level: 1, consume: false}, opts);
10815
+ var promises = [gzipAsync(obj.nn, gzipOpts), gzipAsync(obj.xx, gzipOpts), gzipAsync(obj.yy, gzipOpts)];
10816
+ if (obj.zz) promises.push(gzipAsync(obj.zz, gzipOpts));
10817
+ var results = await Promise.all(promises);
10818
+ obj.nn = results.shift();
10819
+ obj.xx = results.shift();
10820
+ obj.yy = results.shift();
10821
+ if (obj.zz) obj.zz = results.shift();
10776
10822
  }
10777
10823
 
10778
10824
  async function exportDataset(dataset, opts) {
10779
10825
  return {
10780
- arcs: dataset.arcs ? await exportArcs(dataset.arcs, opts) : null,
10826
+ arcs: dataset.arcs ? exportArcs(dataset.arcs) : null,
10781
10827
  info: dataset.info ? exportInfo(dataset.info) : null,
10782
10828
  layers: await Promise.all((dataset.layers || []).map(exportLayer))
10783
10829
  };
@@ -10787,38 +10833,22 @@
10787
10833
  return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
10788
10834
  }
10789
10835
 
10790
- async function exportArcs(arcs, opts) {
10836
+ function exportArcs(arcs) {
10791
10837
  var data = arcs.getVertexData();
10792
- var obj = {
10838
+ return {
10793
10839
  nn: typedArrayToBuffer(data.nn),
10794
10840
  xx: typedArrayToBuffer(data.xx),
10795
10841
  yy: typedArrayToBuffer(data.yy),
10796
10842
  zz: data.zz ? typedArrayToBuffer(data.zz) : null,
10797
10843
  zlimit: arcs.getRetainedInterval()
10798
10844
  };
10799
-
10800
- // gzipping typically sees about 70% compression on unrounded coordinates
10801
- // -- possibly not worth the time
10802
- if (opts.compact) {
10803
- var gzipOpts = {level: 1, consume: false};
10804
- var promises = [gzipAsync(obj.nn, gzipOpts), gzipAsync(obj.xx, gzipOpts), gzipAsync(obj.yy, gzipOpts)];
10805
- if (obj.zz) promises.push(gzipAsync(obj.zz, gzipOpts));
10806
- var results = await Promise.all(promises);
10807
- obj.nn = results.shift();
10808
- obj.xx = results.shift();
10809
- obj.yy = results.shift();
10810
- if (obj.zz) obj.zz = results.shift();
10811
- }
10812
- return obj;
10813
10845
  }
10814
10846
 
10815
10847
  async function exportLayer(lyr) {
10816
- // console.time('table')
10817
10848
  var data = null;
10818
10849
  if (lyr.data) {
10819
10850
  data = await exportTable2(lyr.data);
10820
10851
  }
10821
- // console.timeEnd('table')
10822
10852
  return {
10823
10853
  name: lyr.name || null,
10824
10854
  geometry_type: lyr.geometry_type || null,
@@ -10845,6 +10875,7 @@
10845
10875
  exportPackedDatasets: exportPackedDatasets,
10846
10876
  pack: pack,
10847
10877
  exportDatasetsToPack: exportDatasetsToPack,
10878
+ applyCompression: applyCompression,
10848
10879
  exportDataset: exportDataset
10849
10880
  });
10850
10881
 
@@ -13722,6 +13753,11 @@
13722
13753
  return rect.contains(bbox);
13723
13754
  };
13724
13755
 
13756
+ // TODO
13757
+ // ctx.intersectsRectangle = function(a, b, c, d) {}; // paths... points too?
13758
+ // ctx.containsPoint = function(x, y) {}; // polygon only
13759
+ // ctx.containedByRectangle(a, b, c, d); // paths and points... how do multipart points work?
13760
+
13725
13761
  addGetters(ctx, {
13726
13762
  // TODO: count hole/s + containing ring as one part
13727
13763
  partCount: function() {
@@ -24595,6 +24631,21 @@ ${svg}
24595
24631
  // Experimental commands
24596
24632
  parser.section('Experimental commands (may give unexpected results)');
24597
24633
 
24634
+ parser.command('add-shape')
24635
+ .describe('')
24636
+ .option('geojson', {
24637
+
24638
+ })
24639
+ .option('coordinates', {
24640
+
24641
+ })
24642
+ .option('properties', {
24643
+
24644
+ })
24645
+ .option('name', nameOpt)
24646
+ .option('target', targetOpt)
24647
+ .option('no-replace', noReplaceOpt);
24648
+
24598
24649
  parser.command('alpha-shapes')
24599
24650
  // .describe('convert points to alpha shapes (aka concave hulls)')
24600
24651
  .option('interval', {
@@ -24922,6 +24973,9 @@ ${svg}
24922
24973
 
24923
24974
  parser.command('info')
24924
24975
  .describe('print information about data layers')
24976
+ .option('save-to', {
24977
+ describe: 'name of file to save info in JSON format'
24978
+ })
24925
24979
  .option('target', targetOpt);
24926
24980
 
24927
24981
  parser.command('inspect')
@@ -27457,12 +27511,14 @@ ${svg}
27457
27511
  // Import datasets contained in a BSON blob
27458
27512
  // Return command target as a dataset
27459
27513
  //
27460
- async function unpackSession(buf) {
27461
- var obj = unpack(buf, {});
27514
+ async function unpackSessionData(buf) {
27515
+ return restoreSessionData(unpack(buf, {}));
27516
+ }
27517
+
27518
+ async function restoreSessionData(obj) {
27462
27519
  if (!isValidSession(obj)) {
27463
27520
  stop('Invalid mapshaper session data object');
27464
27521
  }
27465
-
27466
27522
  var datasets = await Promise.all(obj.datasets.map(importDataset));
27467
27523
  return Object.assign(obj, {datasets: datasets});
27468
27524
  }
@@ -27539,7 +27595,8 @@ ${svg}
27539
27595
 
27540
27596
  var Unpack = /*#__PURE__*/Object.freeze({
27541
27597
  __proto__: null,
27542
- unpackSession: unpackSession
27598
+ unpackSessionData: unpackSessionData,
27599
+ restoreSessionData: restoreSessionData
27543
27600
  });
27544
27601
 
27545
27602
  cmd.importFiles = async function(catalog, opts) {
@@ -27593,7 +27650,7 @@ ${svg}
27593
27650
 
27594
27651
  async function importMshpFile(file, catalog, opts) {
27595
27652
  var buf = cli.readFile(file, null, opts.input);
27596
- var obj = await unpackSession(buf);
27653
+ var obj = await unpackSessionData(buf);
27597
27654
  obj.datasets.forEach(catalog.addDataset, catalog);
27598
27655
  return obj.target;
27599
27656
  }
@@ -28063,6 +28120,136 @@ ${svg}
28063
28120
  stashVar('input_files', job.input_files);
28064
28121
  }
28065
28122
 
28123
+ cmd.addShape = addShape;
28124
+
28125
+ function addShape(targetLayers, targetDataset, opts) {
28126
+ if (targetLayers.length > 1) {
28127
+ stop('Command expects a single target layer');
28128
+ }
28129
+ var targetLyr = targetLayers[0]; // may be undefined
28130
+ var targetType = !opts.no_replace && targetLyr && targetLyr.geometry_type || null;
28131
+ var dataset = importGeoJSON(toFeature(opts, targetType));
28132
+ var outputLyr = mergeDatasetsIntoDataset(targetDataset, [dataset])[0];
28133
+ if (opts.no_replace || !targetLyr) {
28134
+ // create new layer
28135
+ setOutputLayerName(outputLyr, targetLyr && targetLyr.name, null, opts);
28136
+ return [outputLyr];
28137
+ }
28138
+ // merge into target layer
28139
+ return cmd.mergeLayers([targetLyr, outputLyr], {force: true});
28140
+ }
28141
+
28142
+ function toFeature(opts, geomType) {
28143
+ if (opts.geojson) {
28144
+ return parseArg(opts.geojson);
28145
+ }
28146
+
28147
+ var geom = opts.coordinates && parseCoordsAsGeometry(opts.coordinates) || null;
28148
+
28149
+ if (!geom) {
28150
+ stop('Missing required shape coordinates');
28151
+ }
28152
+
28153
+ if (geomType == 'point' && geom.type != 'Point') {
28154
+ stop('Expected point coordinates, received', geom.type);
28155
+ }
28156
+
28157
+ if (geomType == 'polygon' && geom.type != 'Polygon') {
28158
+ stop('Expected polygon coordinates, received', geom.type);
28159
+ }
28160
+
28161
+ if (geomType == 'polyline') {
28162
+ if (geom.type == 'Polygon') {
28163
+ geom.coordinates = geom.coordinates[0];
28164
+ geom.type = 'LineString';
28165
+ } else {
28166
+ stop('Expected polyline coordinates, received', geom.type);
28167
+ }
28168
+ }
28169
+
28170
+ return {
28171
+ type: 'Feature',
28172
+ properties: parseProperties(opts.properties),
28173
+ geometry: geom
28174
+ };
28175
+ }
28176
+
28177
+ function parseArg(obj) {
28178
+ return typeof obj == 'string' ? JSON.parse(obj) : obj;
28179
+ }
28180
+
28181
+ function parseProperties(arg) {
28182
+ if (!arg) return null;
28183
+ return parseArg(arg);
28184
+ }
28185
+
28186
+ function isArrayOfNumbers(arr) {
28187
+ return arr.length >= 2 && arr.every(utils.isNumber);
28188
+ }
28189
+
28190
+ function isClosedPath$1(arr) {
28191
+ return isArrayOfPoints(arr) && arr.length > 3 && samePoint$1(arr[0], arr[arr.length - 1]);
28192
+ }
28193
+
28194
+ function samePoint$1(a, b) {
28195
+ return a[0] == b[0] && a[1] == b[1];
28196
+ }
28197
+
28198
+ function isArrayOfPoints(arr) {
28199
+ return arr.every(isPoint);
28200
+ }
28201
+
28202
+ function isPoint(arr) {
28203
+ return arr && arr.length == 2 && isArrayOfNumbers(arr);
28204
+ }
28205
+
28206
+ function transposeCoords(arr) {
28207
+ var coords = [];
28208
+ for (var i=0; i<arr.length; i+=2) {
28209
+ coords.push([arr[i], arr[i+1]]);
28210
+ }
28211
+ if (!isArrayOfPoints(coords)) {
28212
+ stop('Unable to parse x,y,x,y... coordinates');
28213
+ }
28214
+ return coords;
28215
+ }
28216
+
28217
+ function parseCoordsAsGeometry(arg) {
28218
+ if (typeof arg == 'string') {
28219
+ arg = arg.trim();
28220
+ if (!arg.startsWith('[') && !arg.endsWith(']')) {
28221
+ arg = '[' + arg + ']';
28222
+ }
28223
+ }
28224
+ var arr = parseArg(arg);
28225
+ if (isPoint(arr)) {
28226
+ return {
28227
+ type: 'Point',
28228
+ coordinates: arr
28229
+ };
28230
+ }
28231
+
28232
+ if (isArrayOfNumbers(arr)) {
28233
+ arr = transposeCoords(arr);
28234
+ }
28235
+
28236
+ if (isClosedPath$1(arr)) {
28237
+ return {
28238
+ type: 'Polygon',
28239
+ coordinates: [arr]
28240
+ };
28241
+ }
28242
+
28243
+ if (isArrayOfPoints(arr)) {
28244
+ return {
28245
+ type: 'LineString',
28246
+ coordinates: arr
28247
+ };
28248
+ }
28249
+
28250
+ stop('Unable to import coordinates');
28251
+ }
28252
+
28066
28253
  const epsilon = 1.1102230246251565e-16;
28067
28254
  const splitter = 134217729;
28068
28255
  const resulterrbound = (3 + 8 * epsilon) * epsilon;
@@ -29251,17 +29438,6 @@ ${svg}
29251
29438
  bufferIntersection: bufferIntersection
29252
29439
  });
29253
29440
 
29254
- function testSegmentBoundsIntersection(a, b, bb) {
29255
- if (bb.containsPoint(a[0], a[1])) {
29256
- return true;
29257
- }
29258
- return !!(
29259
- geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmin, bb.ymin, bb.xmin, bb.ymax) ||
29260
- geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmin, bb.ymax, bb.xmax, bb.ymax) ||
29261
- geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmax, bb.ymax, bb.xmax, bb.ymin) ||
29262
- geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmax, bb.ymin, bb.xmin, bb.ymin));
29263
- }
29264
-
29265
29441
  function getPolylineBufferMaker2(arcs, geod, getBearing, opts) {
29266
29442
  var makeLeftBuffer = getPathBufferMaker2(arcs, geod, getBearing, opts);
29267
29443
  var geomType = opts.geometry_type;
@@ -37082,7 +37258,7 @@ ${svg}
37082
37258
  editor.done();
37083
37259
  if (!opts.debug) {
37084
37260
  buildTopology(dataset);
37085
- cleanProjectedLayers(dataset);
37261
+ cleanProjectedPathLayers(dataset);
37086
37262
  }
37087
37263
  }
37088
37264
 
@@ -37593,15 +37769,41 @@ ${svg}
37593
37769
  } else {
37594
37770
  clipData = getClippingDataset(src, dest, opts);
37595
37771
  }
37772
+ // clip data to projection limits (some projections), if content exceeds the limit
37773
+ //
37596
37774
  if (clipData) {
37597
- // TODO: don't bother to clip content that is fully within
37598
- // the clipping shape. But how to tell?
37599
- clipLayersInPlace(dataset.layers, clipData, dataset, 'clip');
37600
- clipped = true;
37775
+ clipped = clipLayersIfNeeded(dataset, clipData);
37601
37776
  }
37602
37777
  return cut || clipped;
37603
37778
  }
37604
37779
 
37780
+ function clipLayersIfNeeded(dataset, clipData) {
37781
+ // Avoid clipping layers that are fully enclosed within the projectable
37782
+ // coordinate space (represented by a dataset containing a single
37783
+ // polygon layer, @clipData). This avoids performing unnecessary intersection
37784
+ // tests on each line segment.
37785
+ var layers = dataset.layers.filter(function(lyr) {
37786
+ return !layerIsFullyEnclosed(lyr, dataset, clipData);
37787
+ });
37788
+ if (layers.length > 0) {
37789
+ clipLayersInPlace(layers, clipData, dataset, 'clip');
37790
+ return true;
37791
+ }
37792
+ return false;
37793
+ }
37794
+
37795
+ // @clipData: a dataset containing a polygon layer
37796
+ function layerIsFullyEnclosed(lyr, dataset, clipData) {
37797
+ // This test uses the layer's bounding box to represent the extent of the
37798
+ // layer, and can produce false negatives.
37799
+ var dataBounds = getLayerBounds(lyr, dataset.arcs);
37800
+ var enclosed = false;
37801
+ clipData.layers[0].shapes.forEach(function(shp, i) {
37802
+ enclosed = enclosed || testBoundsInPolygon(dataBounds, shp, clipData.arcs);
37803
+ });
37804
+ return enclosed;
37805
+ }
37806
+
37605
37807
 
37606
37808
  function insertPreProjectionCuts(dataset, src, dest) {
37607
37809
  var antimeridian = getAntimeridian(dest.lam0 * 180 / Math.PI);
@@ -37796,7 +37998,6 @@ ${svg}
37796
37998
  var badArcs = 0;
37797
37999
  var badPoints = 0;
37798
38000
  var clipped = preProjectionClip(dataset, src, dest, opts);
37799
-
37800
38001
  dataset.layers.forEach(function(lyr) {
37801
38002
  if (layerHasPoints(lyr)) {
37802
38003
  badPoints += projectPointLayer(lyr, proj); // v2 compatible (invalid points are removed)
@@ -37813,7 +38014,7 @@ ${svg}
37813
38014
  if (clipped) {
37814
38015
  // TODO: could more selective in cleaning clipped layers
37815
38016
  // (probably only needed when clipped area crosses the antimeridian or includes a pole)
37816
- cleanProjectedLayers(dataset);
38017
+ cleanProjectedPathLayers(dataset);
37817
38018
  }
37818
38019
 
37819
38020
  if (badArcs > 0 && !opts.quiet) {
@@ -37829,7 +38030,7 @@ ${svg}
37829
38030
  // * Removes line intersections
37830
38031
  // * TODO: what if a layer contains polygons with desired overlaps? should
37831
38032
  // we ignore overlaps between different features?
37832
- function cleanProjectedLayers(dataset) {
38033
+ function cleanProjectedPathLayers(dataset) {
37833
38034
  // TODO: only clean affected polygons (cleaning all polygons can be slow)
37834
38035
  var polygonLayers = dataset.layers.filter(lyr => lyr.geometry_type == 'polygon');
37835
38036
  // clean options: force a topology update (by default, this only happens when
@@ -37897,7 +38098,7 @@ ${svg}
37897
38098
  __proto__: null,
37898
38099
  fetchCrsInfo: fetchCrsInfo,
37899
38100
  projectDataset: projectDataset,
37900
- cleanProjectedLayers: cleanProjectedLayers,
38101
+ cleanProjectedPathLayers: cleanProjectedPathLayers,
37901
38102
  projectPointLayer: projectPointLayer,
37902
38103
  projectArcs: projectArcs,
37903
38104
  projectArcs2: projectArcs2
@@ -38240,55 +38441,27 @@ ${svg}
38240
38441
 
38241
38442
  var MAX_RULE_LEN = 50;
38242
38443
 
38243
- cmd.printInfo = function(layers) {
38244
- var str = '';
38245
- layers.forEach(function(o, i) {
38246
- var title = 'Layer: ' + (o.layer.name || '[unnamed layer]');
38247
- var tableStr = getAttributeTableInfo(o.layer);
38248
- var tableWidth = measureLongestLine(tableStr);
38249
- var ruleLen = Math.min(Math.max(title.length, tableWidth), MAX_RULE_LEN);
38250
- str += '\n';
38251
- str += utils.lpad('', ruleLen, '=') + '\n';
38252
- str += title + '\n';
38253
- str += utils.lpad('', ruleLen, '-') + '\n';
38254
- str += getLayerInfo(o.layer, o.dataset);
38255
- str += tableStr;
38444
+ cmd.info = function(targets, opts) {
38445
+ var layers = expandCommandTargets(targets);
38446
+ var arr = layers.map(function(o) {
38447
+ return getLayerInfo(o.layer, o.dataset);
38256
38448
  });
38257
- message(str);
38449
+ message(formatInfo(arr));
38450
+ if (opts.save_to) {
38451
+ var output = [{
38452
+ filename: opts.save_to + (opts.save_to.endsWith('.json') ? '' : '.json'),
38453
+ content: JSON.stringify(arr, null, 2)
38454
+ }];
38455
+ writeFiles(output, opts);
38456
+ }
38258
38457
  };
38259
38458
 
38260
- function measureLongestLine(str) {
38261
- return Math.max.apply(null, str.split('\n').map(function(line) {return stringDisplayWidth(line);}));
38262
- }
38459
+ cmd.printInfo = cmd.info; // old name
38263
38460
 
38264
- function stringDisplayWidth(str) {
38265
- var w = 0;
38266
- for (var i = 0, n=str.length; i < n; i++) {
38267
- w += charDisplayWidth(str.charCodeAt(i));
38268
- }
38269
- return w;
38270
- }
38271
-
38272
- // see https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
38273
- // this is a simplified version, focusing on double-width CJK chars and ignoring nonprinting etc chars
38274
- function charDisplayWidth(c) {
38275
- if (c >= 0x1100 &&
38276
- (c <= 0x115f || c == 0x2329 || c == 0x232a ||
38277
- (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f) || /* CJK ... Yi */
38278
- (c >= 0xac00 && c <= 0xd7a3) || /* Hangul Syllables */
38279
- (c >= 0xf900 && c <= 0xfaff) || /* CJK Compatibility Ideographs */
38280
- (c >= 0xfe10 && c <= 0xfe19) || /* Vertical forms */
38281
- (c >= 0xfe30 && c <= 0xfe6f) || /* CJK Compatibility Forms */
38282
- (c >= 0xff00 && c <= 0xff60) || /* Fullwidth Forms */
38283
- (c >= 0xffe0 && c <= 0xffe6) ||
38284
- (c >= 0x20000 && c <= 0x2fffd) ||
38285
- (c >= 0x30000 && c <= 0x3fffd))) return 2;
38286
- return 1;
38287
- }
38288
-
38289
- function getLayerData(lyr, dataset) {
38461
+ function getLayerInfo(lyr, dataset) {
38290
38462
  var n = getFeatureCount(lyr);
38291
38463
  var o = {
38464
+ layer_name: lyr.name,
38292
38465
  geometry_type: lyr.geometry_type,
38293
38466
  feature_count: n,
38294
38467
  null_shape_count: 0,
@@ -38296,31 +38469,47 @@ ${svg}
38296
38469
  };
38297
38470
  if (lyr.shapes && lyr.shapes.length > 0) {
38298
38471
  o.null_shape_count = countNullShapes(lyr.shapes);
38299
- o.bbox =getLayerBounds(lyr, dataset.arcs).toArray();
38472
+ o.bbox = getLayerBounds(lyr, dataset.arcs).toArray();
38300
38473
  o.proj4 = getProjInfo(dataset);
38301
38474
  }
38475
+ o.source_file = getLayerSourceFile(lyr, dataset) || null;
38476
+ o.attribute_data = getAttributeTableInfo(lyr);
38302
38477
  return o;
38303
38478
  }
38304
38479
 
38305
- // TODO: consider polygons with zero area or other invalid geometries
38306
- function countNullShapes(shapes) {
38307
- var count = 0;
38308
- for (var i=0; i<shapes.length; i++) {
38309
- if (!shapes[i] || shapes[i].length === 0) count++;
38480
+ // i: (optional) record index
38481
+ function getAttributeTableInfo(lyr, i) {
38482
+ if (!lyr.data || lyr.data.size() === 0 || lyr.data.getFields().length === 0) {
38483
+ return null;
38310
38484
  }
38311
- return count;
38485
+ var fields = applyFieldOrder(lyr.data.getFields(), 'ascending');
38486
+ var valueName = i === undefined ? 'first_value' : 'value';
38487
+ return fields.map(function(fname) {
38488
+ return {
38489
+ field: fname,
38490
+ [valueName]: lyr.data.getReadOnlyRecordAt(i || 0)[fname]
38491
+ };
38492
+ });
38312
38493
  }
38313
38494
 
38314
- function countNullRecords(records) {
38315
- var count = 0;
38316
- for (var i=0; i<records.length; i++) {
38317
- if (!records[i]) count++;
38318
- }
38319
- return count;
38495
+ function formatInfo(arr) {
38496
+ var str = '';
38497
+ arr.forEach(function(info, i) {
38498
+ var title = 'Layer: ' + (info.layer_name || '[unnamed layer]');
38499
+ var tableStr = formatAttributeTableInfo(info.attribute_data);
38500
+ var tableWidth = measureLongestLine(tableStr);
38501
+ var ruleLen = Math.min(Math.max(title.length, tableWidth), MAX_RULE_LEN);
38502
+ str += '\n';
38503
+ str += utils.lpad('', ruleLen, '=') + '\n';
38504
+ str += title + '\n';
38505
+ str += utils.lpad('', ruleLen, '-') + '\n';
38506
+ str += formatLayerInfo(info);
38507
+ str += tableStr;
38508
+ });
38509
+ return str;
38320
38510
  }
38321
38511
 
38322
- function getLayerInfo(lyr, dataset) {
38323
- var data = getLayerData(lyr, dataset);
38512
+ function formatLayerInfo(data) {
38324
38513
  var str = '';
38325
38514
  str += "Type: " + (data.geometry_type || "tabular data") + "\n";
38326
38515
  str += utils.format("Records: %,d\n",data.feature_count);
@@ -38331,21 +38520,19 @@ ${svg}
38331
38520
  str += "Bounds: " + data.bbox.join(',') + "\n";
38332
38521
  str += "CRS: " + data.proj4 + "\n";
38333
38522
  }
38334
- str += "Source: " + (getLayerSourceFile(lyr, dataset) || 'n/a') + "\n";
38523
+ str += "Source: " + (data.source_file || 'n/a') + "\n";
38335
38524
  return str;
38336
38525
  }
38337
38526
 
38338
- function getAttributeTableInfo(lyr, i) {
38339
- if (!lyr.data || lyr.data.size() === 0 || lyr.data.getFields().length === 0) {
38340
- return "Attribute data: [none]\n";
38341
- }
38342
- return "\nAttribute data\n" + formatAttributeTable(lyr.data, i);
38343
- }
38344
-
38345
- function formatAttributeTable(data, i) {
38346
- var fields = applyFieldOrder(data.getFields(), 'ascending');
38347
- var vals = fields.map(function(fname) {
38348
- return data.getReadOnlyRecordAt(i || 0)[fname];
38527
+ function formatAttributeTableInfo(arr) {
38528
+ if (!arr) return "Attribute data: [none]\n";
38529
+ var header = "\nAttribute data\n";
38530
+ var valKey = 'first_value' in arr[0] ? 'first_value' : 'value';
38531
+ var vals = [];
38532
+ var fields = [];
38533
+ arr.forEach(function(o) {
38534
+ fields.push(o.field);
38535
+ vals.push(o[valKey]);
38349
38536
  });
38350
38537
  var maxIntegralChars = vals.reduce(function(max, val) {
38351
38538
  if (utils.isNumber(val)) {
@@ -38357,20 +38544,66 @@ ${svg}
38357
38544
  var col2Arr = vals.reduce(function(memo, val) {
38358
38545
  memo.push(formatTableValue(val, maxIntegralChars));
38359
38546
  return memo;
38360
- }, [i >= 0 ? 'Value' : 'First value']);
38547
+ }, [valKey == 'first_value' ? 'First value' : 'Value']);
38361
38548
  var col1Chars = maxChars(col1Arr);
38362
38549
  var col2Chars = maxChars(col2Arr);
38363
38550
  var sepStr = (utils.rpad('', col1Chars + 2, '-') + '+' +
38364
38551
  utils.rpad('', col2Chars + 2, '-')).substr(0, MAX_RULE_LEN);
38365
38552
  var sepLine = sepStr + '\n';
38366
- var table = sepLine;
38553
+ var table = '';
38367
38554
  col1Arr.forEach(function(col1, i) {
38368
38555
  var w = stringDisplayWidth(col1);
38369
38556
  table += ' ' + col1 + utils.rpad('', col1Chars - w, ' ') + ' | ' +
38370
38557
  col2Arr[i] + '\n';
38371
38558
  if (i === 0) table += sepLine; // separator after first line
38372
38559
  });
38373
- return table + sepLine;
38560
+ return header + sepLine + table + sepLine;
38561
+ }
38562
+
38563
+ function measureLongestLine(str) {
38564
+ return Math.max.apply(null, str.split('\n').map(function(line) {return stringDisplayWidth(line);}));
38565
+ }
38566
+
38567
+ function stringDisplayWidth(str) {
38568
+ var w = 0;
38569
+ for (var i = 0, n=str.length; i < n; i++) {
38570
+ w += charDisplayWidth(str.charCodeAt(i));
38571
+ }
38572
+ return w;
38573
+ }
38574
+
38575
+ // see https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
38576
+ // this is a simplified version, focusing on double-width CJK chars and ignoring nonprinting etc chars
38577
+ function charDisplayWidth(c) {
38578
+ if (c >= 0x1100 &&
38579
+ (c <= 0x115f || c == 0x2329 || c == 0x232a ||
38580
+ (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f) || /* CJK ... Yi */
38581
+ (c >= 0xac00 && c <= 0xd7a3) || /* Hangul Syllables */
38582
+ (c >= 0xf900 && c <= 0xfaff) || /* CJK Compatibility Ideographs */
38583
+ (c >= 0xfe10 && c <= 0xfe19) || /* Vertical forms */
38584
+ (c >= 0xfe30 && c <= 0xfe6f) || /* CJK Compatibility Forms */
38585
+ (c >= 0xff00 && c <= 0xff60) || /* Fullwidth Forms */
38586
+ (c >= 0xffe0 && c <= 0xffe6) ||
38587
+ (c >= 0x20000 && c <= 0x2fffd) ||
38588
+ (c >= 0x30000 && c <= 0x3fffd))) return 2;
38589
+ return 1;
38590
+ }
38591
+
38592
+ // TODO: consider polygons with zero area or other invalid geometries
38593
+ function countNullShapes(shapes) {
38594
+ var count = 0;
38595
+ for (var i=0; i<shapes.length; i++) {
38596
+ if (!shapes[i] || shapes[i].length === 0) count++;
38597
+ }
38598
+ return count;
38599
+ }
38600
+
38601
+ function countNullRecords(records) {
38602
+ var count = 0;
38603
+ for (var i=0; i<records.length; i++) {
38604
+ if (!records[i]) count++;
38605
+ }
38606
+ return count;
38374
38607
  }
38375
38608
 
38376
38609
  function maxChars(arr) {
@@ -38424,8 +38657,9 @@ ${svg}
38424
38657
 
38425
38658
  var Info = /*#__PURE__*/Object.freeze({
38426
38659
  __proto__: null,
38427
- getLayerData: getLayerData,
38660
+ getLayerInfo: getLayerInfo,
38428
38661
  getAttributeTableInfo: getAttributeTableInfo,
38662
+ formatAttributeTableInfo: formatAttributeTableInfo,
38429
38663
  formatTableValue: formatTableValue
38430
38664
  });
38431
38665
 
@@ -38477,7 +38711,7 @@ ${svg}
38477
38711
  function getFeatureInfo(id, lyr, arcs) {
38478
38712
  var msg = "Feature " + id + '\n';
38479
38713
  msg += getShapeInfo(id, lyr, arcs);
38480
- msg += getAttributeTableInfo(lyr, id);
38714
+ msg += formatAttributeTableInfo(getAttributeTableInfo(lyr, id));
38481
38715
  return msg;
38482
38716
  }
38483
38717
 
@@ -38838,12 +39072,12 @@ ${svg}
38838
39072
  function polylineToMidpoints(shp, arcs, opts) {
38839
39073
  if (!shp) return null;
38840
39074
  var points = shp.map(function(path) {
38841
- return findPathMidpoint(path, arcs);
39075
+ return findPathMidpoint(path, arcs, false);
38842
39076
  });
38843
39077
  return points;
38844
39078
  }
38845
39079
 
38846
- function findPathMidpoint(path, arcs) {
39080
+ function findPathMidpoint(path, arcs, useNearestVertex) {
38847
39081
  var halfLen = calcPathLen(path, arcs, false) / 2;
38848
39082
  var partialLen = 0;
38849
39083
  var p;
@@ -38860,7 +39094,11 @@ ${svg}
38860
39094
  var k;
38861
39095
  if (partialLen + segLen >= halfLen) {
38862
39096
  k = (halfLen - partialLen) / segLen;
38863
- p = [a + k * (c - a), b + k * (d - b)];
39097
+ if (useNearestVertex) {
39098
+ k = k < 0.5 ? 0 : 1;
39099
+ }
39100
+ // p = [a + k * (c - a), b + k * (d - b)];
39101
+ p = [(1 - k) * a + k * c, (1 - k) * b + k * d];
38864
39102
  }
38865
39103
  partialLen += segLen;
38866
39104
  });
@@ -40589,7 +40827,7 @@ ${svg}
40589
40827
 
40590
40828
  function getRunCommandData(target) {
40591
40829
  var lyr = target.layers[0];
40592
- var data = getLayerData(lyr, target.dataset);
40830
+ var data = getLayerInfo(lyr, target.dataset);
40593
40831
  data.layer = lyr;
40594
40832
  data.dataset = target.dataset;
40595
40833
  return data;
@@ -42740,7 +42978,7 @@ ${svg}
42740
42978
  name == 'point-grid' || name == 'shape' || name == 'rectangle' ||
42741
42979
  name == 'require' || name == 'run' || name == 'define' ||
42742
42980
  name == 'include' || name == 'print' || name == 'comment' || name == 'if' || name == 'elif' ||
42743
- name == 'else' || name == 'endif' || name == 'stop';
42981
+ name == 'else' || name == 'endif' || name == 'stop' || name == 'add-shape';
42744
42982
  }
42745
42983
 
42746
42984
  async function runCommand(command, job) {
@@ -42814,7 +43052,14 @@ ${svg}
42814
43052
  source = findCommandSource(convertSourceName(opts.source, targets), job.catalog, opts);
42815
43053
  }
42816
43054
 
42817
- if (name == 'affine') {
43055
+ if (name == 'add-shape') {
43056
+ if (!targetDataset) {
43057
+ targetDataset = {info: {}, layers: []};
43058
+ targetLayers = targetDataset.layers;
43059
+ job.catalog.addDataset(targetDataset);
43060
+ }
43061
+ outputLayers = cmd.addShape(targetLayers, targetDataset, opts);
43062
+ } else if (name == 'affine') {
42818
43063
  cmd.affine(targetLayers, targetDataset, opts);
42819
43064
 
42820
43065
  } else if (name == 'alpha-shapes') {
@@ -42939,7 +43184,7 @@ ${svg}
42939
43184
  cmd.include(opts);
42940
43185
 
42941
43186
  } else if (name == 'info') {
42942
- cmd.printInfo(expandCommandTargets(targets));
43187
+ cmd.info(targets, opts);
42943
43188
 
42944
43189
  } else if (name == 'inlay') {
42945
43190
  outputLayers = cmd.inlay(targetLayers, source, targetDataset, opts);
@@ -43305,13 +43550,13 @@ ${svg}
43305
43550
  commands = runAndRemoveInfoCommands(commands);
43306
43551
  if (commands.length === 0) return done(null);
43307
43552
 
43308
- // add options to -i -o -join -clip -erase commands to bypass file i/o
43553
+ // add options to -i -o -join -clip -erase etc. commands to bypass file i/o
43309
43554
  // TODO: find a less kludgy solution
43310
43555
  commands.forEach(function(cmd) {
43311
43556
  if (commandTakesFileInput(cmd.name) && inputObj) {
43312
43557
  cmd.options.input = inputObj;
43313
43558
  }
43314
- if (cmd.name == 'o' && outputArr) {
43559
+ if (outputArr && (cmd.name == 'o' || cmd.name == 'info' && cmd.options.save_to)) {
43315
43560
  cmd.options.output = outputArr;
43316
43561
  }
43317
43562
  });