mapshaper 0.6.24 → 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/mapshaper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.6.24";
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
@@ -13729,6 +13753,11 @@
13729
13753
  return rect.contains(bbox);
13730
13754
  };
13731
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
+
13732
13761
  addGetters(ctx, {
13733
13762
  // TODO: count hole/s + containing ring as one part
13734
13763
  partCount: function() {
@@ -24602,6 +24631,21 @@ ${svg}
24602
24631
  // Experimental commands
24603
24632
  parser.section('Experimental commands (may give unexpected results)');
24604
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
+
24605
24649
  parser.command('alpha-shapes')
24606
24650
  // .describe('convert points to alpha shapes (aka concave hulls)')
24607
24651
  .option('interval', {
@@ -28076,6 +28120,136 @@ ${svg}
28076
28120
  stashVar('input_files', job.input_files);
28077
28121
  }
28078
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
+
28079
28253
  const epsilon = 1.1102230246251565e-16;
28080
28254
  const splitter = 134217729;
28081
28255
  const resulterrbound = (3 + 8 * epsilon) * epsilon;
@@ -29264,17 +29438,6 @@ ${svg}
29264
29438
  bufferIntersection: bufferIntersection
29265
29439
  });
29266
29440
 
29267
- function testSegmentBoundsIntersection(a, b, bb) {
29268
- if (bb.containsPoint(a[0], a[1])) {
29269
- return true;
29270
- }
29271
- return !!(
29272
- geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmin, bb.ymin, bb.xmin, bb.ymax) ||
29273
- geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmin, bb.ymax, bb.xmax, bb.ymax) ||
29274
- geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmax, bb.ymax, bb.xmax, bb.ymin) ||
29275
- geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmax, bb.ymin, bb.xmin, bb.ymin));
29276
- }
29277
-
29278
29441
  function getPolylineBufferMaker2(arcs, geod, getBearing, opts) {
29279
29442
  var makeLeftBuffer = getPathBufferMaker2(arcs, geod, getBearing, opts);
29280
29443
  var geomType = opts.geometry_type;
@@ -37095,7 +37258,7 @@ ${svg}
37095
37258
  editor.done();
37096
37259
  if (!opts.debug) {
37097
37260
  buildTopology(dataset);
37098
- cleanProjectedLayers(dataset);
37261
+ cleanProjectedPathLayers(dataset);
37099
37262
  }
37100
37263
  }
37101
37264
 
@@ -37606,15 +37769,41 @@ ${svg}
37606
37769
  } else {
37607
37770
  clipData = getClippingDataset(src, dest, opts);
37608
37771
  }
37772
+ // clip data to projection limits (some projections), if content exceeds the limit
37773
+ //
37609
37774
  if (clipData) {
37610
- // TODO: don't bother to clip content that is fully within
37611
- // the clipping shape. But how to tell?
37612
- clipLayersInPlace(dataset.layers, clipData, dataset, 'clip');
37613
- clipped = true;
37775
+ clipped = clipLayersIfNeeded(dataset, clipData);
37614
37776
  }
37615
37777
  return cut || clipped;
37616
37778
  }
37617
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
+
37618
37807
 
37619
37808
  function insertPreProjectionCuts(dataset, src, dest) {
37620
37809
  var antimeridian = getAntimeridian(dest.lam0 * 180 / Math.PI);
@@ -37809,7 +37998,6 @@ ${svg}
37809
37998
  var badArcs = 0;
37810
37999
  var badPoints = 0;
37811
38000
  var clipped = preProjectionClip(dataset, src, dest, opts);
37812
-
37813
38001
  dataset.layers.forEach(function(lyr) {
37814
38002
  if (layerHasPoints(lyr)) {
37815
38003
  badPoints += projectPointLayer(lyr, proj); // v2 compatible (invalid points are removed)
@@ -37826,7 +38014,7 @@ ${svg}
37826
38014
  if (clipped) {
37827
38015
  // TODO: could more selective in cleaning clipped layers
37828
38016
  // (probably only needed when clipped area crosses the antimeridian or includes a pole)
37829
- cleanProjectedLayers(dataset);
38017
+ cleanProjectedPathLayers(dataset);
37830
38018
  }
37831
38019
 
37832
38020
  if (badArcs > 0 && !opts.quiet) {
@@ -37842,7 +38030,7 @@ ${svg}
37842
38030
  // * Removes line intersections
37843
38031
  // * TODO: what if a layer contains polygons with desired overlaps? should
37844
38032
  // we ignore overlaps between different features?
37845
- function cleanProjectedLayers(dataset) {
38033
+ function cleanProjectedPathLayers(dataset) {
37846
38034
  // TODO: only clean affected polygons (cleaning all polygons can be slow)
37847
38035
  var polygonLayers = dataset.layers.filter(lyr => lyr.geometry_type == 'polygon');
37848
38036
  // clean options: force a topology update (by default, this only happens when
@@ -37910,7 +38098,7 @@ ${svg}
37910
38098
  __proto__: null,
37911
38099
  fetchCrsInfo: fetchCrsInfo,
37912
38100
  projectDataset: projectDataset,
37913
- cleanProjectedLayers: cleanProjectedLayers,
38101
+ cleanProjectedPathLayers: cleanProjectedPathLayers,
37914
38102
  projectPointLayer: projectPointLayer,
37915
38103
  projectArcs: projectArcs,
37916
38104
  projectArcs2: projectArcs2
@@ -38884,12 +39072,12 @@ ${svg}
38884
39072
  function polylineToMidpoints(shp, arcs, opts) {
38885
39073
  if (!shp) return null;
38886
39074
  var points = shp.map(function(path) {
38887
- return findPathMidpoint(path, arcs);
39075
+ return findPathMidpoint(path, arcs, false);
38888
39076
  });
38889
39077
  return points;
38890
39078
  }
38891
39079
 
38892
- function findPathMidpoint(path, arcs) {
39080
+ function findPathMidpoint(path, arcs, useNearestVertex) {
38893
39081
  var halfLen = calcPathLen(path, arcs, false) / 2;
38894
39082
  var partialLen = 0;
38895
39083
  var p;
@@ -38906,7 +39094,11 @@ ${svg}
38906
39094
  var k;
38907
39095
  if (partialLen + segLen >= halfLen) {
38908
39096
  k = (halfLen - partialLen) / segLen;
38909
- 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];
38910
39102
  }
38911
39103
  partialLen += segLen;
38912
39104
  });
@@ -42786,7 +42978,7 @@ ${svg}
42786
42978
  name == 'point-grid' || name == 'shape' || name == 'rectangle' ||
42787
42979
  name == 'require' || name == 'run' || name == 'define' ||
42788
42980
  name == 'include' || name == 'print' || name == 'comment' || name == 'if' || name == 'elif' ||
42789
- name == 'else' || name == 'endif' || name == 'stop';
42981
+ name == 'else' || name == 'endif' || name == 'stop' || name == 'add-shape';
42790
42982
  }
42791
42983
 
42792
42984
  async function runCommand(command, job) {
@@ -42860,7 +43052,14 @@ ${svg}
42860
43052
  source = findCommandSource(convertSourceName(opts.source, targets), job.catalog, opts);
42861
43053
  }
42862
43054
 
42863
- 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') {
42864
43063
  cmd.affine(targetLayers, targetDataset, opts);
42865
43064
 
42866
43065
  } else if (name == 'alpha-shapes') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.6.24",
3
+ "version": "0.6.25",
4
4
  "description": "A tool for editing vector datasets for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
@@ -44,7 +44,6 @@
44
44
  "@tmcw/togeojson": "^5.6.0",
45
45
  "@xmldom/xmldom": "^0.8.6",
46
46
  "adm-zip": "^0.5.9",
47
- "bson": "^4.7.0",
48
47
  "commander": "7.0.0",
49
48
  "cookies": "^0.8.0",
50
49
  "d3-color": "3.1.0",
@@ -58,7 +57,7 @@
58
57
  "idb-keyval": "^6.2.0",
59
58
  "kdbush": "^3.0.0",
60
59
  "mproj": "0.0.35",
61
- "msgpackr": "^1.8.1",
60
+ "msgpackr": "^1.8.5",
62
61
  "opn": "^5.3.0",
63
62
  "rw": "~1.3.3",
64
63
  "sync-request": "5.0.0",
package/www/index.html CHANGED
@@ -118,6 +118,7 @@
118
118
  <div>
119
119
  <div class="select-btn btn dialog-btn">Select</div>
120
120
  <div class="clip-btn btn dialog-btn">Clip</div>
121
+ <div class="erase-btn btn dialog-btn">Erase</div>
121
122
  <div class="info-btn btn dialog-btn">Info</div>
122
123
  <!-- <div class="zoom-btn btn dialog-btn">Zoom In</div>
123
124
  <div class="rectangle-btn btn dialog-btn">Rectangle</div> -->
@@ -3050,7 +3050,9 @@
3050
3050
 
3051
3051
  function saveHistory() {
3052
3052
  history = history.filter(Boolean); // TODO: fix condition that leaves a blank line on the history
3053
- GUI.setSavedValue('console_history', history.slice(-100));
3053
+ if (history.length > 0) {
3054
+ GUI.setSavedValue('console_history', history.slice(-100));
3055
+ }
3054
3056
  }
3055
3057
 
3056
3058
  function toLog(str, cname) {
@@ -3679,7 +3681,7 @@
3679
3681
  .on('change', function() {
3680
3682
  GUI.setSavedValue('choose-save-dir', this.checked);
3681
3683
  })
3682
- .attr('checked', GUI.getSavedValue('choose-save-dir') || false);
3684
+ .attr('checked', GUI.getSavedValue('choose-save-dir') || null);
3683
3685
  }
3684
3686
 
3685
3687
  function turnOn() {
@@ -8053,8 +8055,14 @@
8053
8055
  }
8054
8056
  });
8055
8057
 
8058
+ // add new field form
8059
+ if (editable) {
8060
+ renderNewRow(tableEl, rec, table);
8061
+ }
8062
+
8063
+ tableEl.appendTo(el);
8056
8064
  if (rows > 0) {
8057
- tableEl.appendTo(el);
8065
+ // tableEl.appendTo(el);
8058
8066
 
8059
8067
  tableEl.on('copy', function(e) {
8060
8068
  // remove leading or trailing tabs that sometimes get copied when
@@ -8074,15 +8082,26 @@
8074
8082
  el.html(utils$1.format('<div class="note">This %s is missing attribute data.</div>',
8075
8083
  table && table.getFields().length > 0 ? 'feature': 'layer'));
8076
8084
  }
8085
+
8086
+ if (editable) {
8087
+ var line = El('div').appendTo(el);
8088
+ El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
8089
+
8090
+ }).text('add field');
8091
+ }
8092
+ }
8093
+
8094
+ function renderNewRow(el, rec, table) {
8095
+
8077
8096
  }
8078
8097
 
8079
8098
  function renderRow(table, rec, key, type, editable) {
8080
- var rowHtml = '<td class="field-name">%s</td><td><span class="value">%s</span> </td>';
8081
8099
  var val = rec[key];
8082
8100
  var str = formatInspectorValue(val, type);
8101
+ var rowHtml = `<td class="field-name">${key}</td><td><span class="value">${utils$1.htmlEscape(str)}</span> </td>`;
8083
8102
  var cell = El('tr')
8084
8103
  .appendTo(table)
8085
- .html(utils$1.format(rowHtml, key, utils$1.htmlEscape(str)))
8104
+ .html(rowHtml)
8086
8105
  .findChild('.value');
8087
8106
  setFieldClass(cell, val, type);
8088
8107
  if (editable) {
@@ -10193,6 +10212,10 @@
10193
10212
  runCommand('-clip bbox=' + box.getDataCoords().join(','));
10194
10213
  });
10195
10214
 
10215
+ new SimpleButton(popup.findChild('.erase-btn')).on('click', function() {
10216
+ runCommand('-erase bbox=' + box.getDataCoords().join(','));
10217
+ });
10218
+
10196
10219
  gui.addMode('box_tool', turnOn, turnOff);
10197
10220
 
10198
10221
  gui.on('interaction_mode_change', function(e) {
package/www/mapshaper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.6.24";
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
@@ -13729,6 +13753,11 @@
13729
13753
  return rect.contains(bbox);
13730
13754
  };
13731
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
+
13732
13761
  addGetters(ctx, {
13733
13762
  // TODO: count hole/s + containing ring as one part
13734
13763
  partCount: function() {
@@ -24602,6 +24631,21 @@ ${svg}
24602
24631
  // Experimental commands
24603
24632
  parser.section('Experimental commands (may give unexpected results)');
24604
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
+
24605
24649
  parser.command('alpha-shapes')
24606
24650
  // .describe('convert points to alpha shapes (aka concave hulls)')
24607
24651
  .option('interval', {
@@ -28076,6 +28120,136 @@ ${svg}
28076
28120
  stashVar('input_files', job.input_files);
28077
28121
  }
28078
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
+
28079
28253
  const epsilon = 1.1102230246251565e-16;
28080
28254
  const splitter = 134217729;
28081
28255
  const resulterrbound = (3 + 8 * epsilon) * epsilon;
@@ -29264,17 +29438,6 @@ ${svg}
29264
29438
  bufferIntersection: bufferIntersection
29265
29439
  });
29266
29440
 
29267
- function testSegmentBoundsIntersection(a, b, bb) {
29268
- if (bb.containsPoint(a[0], a[1])) {
29269
- return true;
29270
- }
29271
- return !!(
29272
- geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmin, bb.ymin, bb.xmin, bb.ymax) ||
29273
- geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmin, bb.ymax, bb.xmax, bb.ymax) ||
29274
- geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmax, bb.ymax, bb.xmax, bb.ymin) ||
29275
- geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmax, bb.ymin, bb.xmin, bb.ymin));
29276
- }
29277
-
29278
29441
  function getPolylineBufferMaker2(arcs, geod, getBearing, opts) {
29279
29442
  var makeLeftBuffer = getPathBufferMaker2(arcs, geod, getBearing, opts);
29280
29443
  var geomType = opts.geometry_type;
@@ -37095,7 +37258,7 @@ ${svg}
37095
37258
  editor.done();
37096
37259
  if (!opts.debug) {
37097
37260
  buildTopology(dataset);
37098
- cleanProjectedLayers(dataset);
37261
+ cleanProjectedPathLayers(dataset);
37099
37262
  }
37100
37263
  }
37101
37264
 
@@ -37606,15 +37769,41 @@ ${svg}
37606
37769
  } else {
37607
37770
  clipData = getClippingDataset(src, dest, opts);
37608
37771
  }
37772
+ // clip data to projection limits (some projections), if content exceeds the limit
37773
+ //
37609
37774
  if (clipData) {
37610
- // TODO: don't bother to clip content that is fully within
37611
- // the clipping shape. But how to tell?
37612
- clipLayersInPlace(dataset.layers, clipData, dataset, 'clip');
37613
- clipped = true;
37775
+ clipped = clipLayersIfNeeded(dataset, clipData);
37614
37776
  }
37615
37777
  return cut || clipped;
37616
37778
  }
37617
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
+
37618
37807
 
37619
37808
  function insertPreProjectionCuts(dataset, src, dest) {
37620
37809
  var antimeridian = getAntimeridian(dest.lam0 * 180 / Math.PI);
@@ -37809,7 +37998,6 @@ ${svg}
37809
37998
  var badArcs = 0;
37810
37999
  var badPoints = 0;
37811
38000
  var clipped = preProjectionClip(dataset, src, dest, opts);
37812
-
37813
38001
  dataset.layers.forEach(function(lyr) {
37814
38002
  if (layerHasPoints(lyr)) {
37815
38003
  badPoints += projectPointLayer(lyr, proj); // v2 compatible (invalid points are removed)
@@ -37826,7 +38014,7 @@ ${svg}
37826
38014
  if (clipped) {
37827
38015
  // TODO: could more selective in cleaning clipped layers
37828
38016
  // (probably only needed when clipped area crosses the antimeridian or includes a pole)
37829
- cleanProjectedLayers(dataset);
38017
+ cleanProjectedPathLayers(dataset);
37830
38018
  }
37831
38019
 
37832
38020
  if (badArcs > 0 && !opts.quiet) {
@@ -37842,7 +38030,7 @@ ${svg}
37842
38030
  // * Removes line intersections
37843
38031
  // * TODO: what if a layer contains polygons with desired overlaps? should
37844
38032
  // we ignore overlaps between different features?
37845
- function cleanProjectedLayers(dataset) {
38033
+ function cleanProjectedPathLayers(dataset) {
37846
38034
  // TODO: only clean affected polygons (cleaning all polygons can be slow)
37847
38035
  var polygonLayers = dataset.layers.filter(lyr => lyr.geometry_type == 'polygon');
37848
38036
  // clean options: force a topology update (by default, this only happens when
@@ -37910,7 +38098,7 @@ ${svg}
37910
38098
  __proto__: null,
37911
38099
  fetchCrsInfo: fetchCrsInfo,
37912
38100
  projectDataset: projectDataset,
37913
- cleanProjectedLayers: cleanProjectedLayers,
38101
+ cleanProjectedPathLayers: cleanProjectedPathLayers,
37914
38102
  projectPointLayer: projectPointLayer,
37915
38103
  projectArcs: projectArcs,
37916
38104
  projectArcs2: projectArcs2
@@ -38884,12 +39072,12 @@ ${svg}
38884
39072
  function polylineToMidpoints(shp, arcs, opts) {
38885
39073
  if (!shp) return null;
38886
39074
  var points = shp.map(function(path) {
38887
- return findPathMidpoint(path, arcs);
39075
+ return findPathMidpoint(path, arcs, false);
38888
39076
  });
38889
39077
  return points;
38890
39078
  }
38891
39079
 
38892
- function findPathMidpoint(path, arcs) {
39080
+ function findPathMidpoint(path, arcs, useNearestVertex) {
38893
39081
  var halfLen = calcPathLen(path, arcs, false) / 2;
38894
39082
  var partialLen = 0;
38895
39083
  var p;
@@ -38906,7 +39094,11 @@ ${svg}
38906
39094
  var k;
38907
39095
  if (partialLen + segLen >= halfLen) {
38908
39096
  k = (halfLen - partialLen) / segLen;
38909
- 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];
38910
39102
  }
38911
39103
  partialLen += segLen;
38912
39104
  });
@@ -42786,7 +42978,7 @@ ${svg}
42786
42978
  name == 'point-grid' || name == 'shape' || name == 'rectangle' ||
42787
42979
  name == 'require' || name == 'run' || name == 'define' ||
42788
42980
  name == 'include' || name == 'print' || name == 'comment' || name == 'if' || name == 'elif' ||
42789
- name == 'else' || name == 'endif' || name == 'stop';
42981
+ name == 'else' || name == 'endif' || name == 'stop' || name == 'add-shape';
42790
42982
  }
42791
42983
 
42792
42984
  async function runCommand(command, job) {
@@ -42860,7 +43052,14 @@ ${svg}
42860
43052
  source = findCommandSource(convertSourceName(opts.source, targets), job.catalog, opts);
42861
43053
  }
42862
43054
 
42863
- 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') {
42864
43063
  cmd.affine(targetLayers, targetDataset, opts);
42865
43064
 
42866
43065
  } else if (name == 'alpha-shapes') {