mapshaper 0.6.24 → 0.6.26

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.24";
3
+ var VERSION = "0.6.26";
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
@@ -13704,6 +13728,28 @@
13704
13728
  }});
13705
13729
  }
13706
13730
 
13731
+ if (hasPaths || hasPoints) {
13732
+ addGetters(ctx, {
13733
+ // TODO: count hole/s + containing ring as one part
13734
+ partCount: function() {
13735
+ var shp = lyr.shapes[_id];
13736
+ return shp ? shp.length : 0;
13737
+ },
13738
+ isNull: function() {
13739
+ return ctx.partCount === 0;
13740
+ },
13741
+ bounds: function() {
13742
+ return shapeBounds().toArray();
13743
+ },
13744
+ height: function() {
13745
+ return shapeBounds().height();
13746
+ },
13747
+ width: function() {
13748
+ return shapeBounds().width();
13749
+ }
13750
+ });
13751
+ }
13752
+
13707
13753
  if (hasPaths) {
13708
13754
 
13709
13755
  ctx.bboxContainsPoint = function(x, y) {
@@ -13729,24 +13775,10 @@
13729
13775
  return rect.contains(bbox);
13730
13776
  };
13731
13777
 
13732
- addGetters(ctx, {
13733
- // TODO: count hole/s + containing ring as one part
13734
- partCount: function() {
13735
- return _ids ? _ids.length : 0;
13736
- },
13737
- isNull: function() {
13738
- return ctx.partCount === 0;
13739
- },
13740
- bounds: function() {
13741
- return shapeBounds().toArray();
13742
- },
13743
- height: function() {
13744
- return shapeBounds().height();
13745
- },
13746
- width: function() {
13747
- return shapeBounds().width();
13748
- }
13749
- });
13778
+ // TODO
13779
+ // ctx.intersectsRectangle = function(a, b, c, d) {}; // paths... points too?
13780
+ // ctx.containsPoint = function(x, y) {}; // polygon only
13781
+ // ctx.containedByRectangle(a, b, c, d); // paths and points... how do multipart points work?
13750
13782
 
13751
13783
  if (lyr.geometry_type == 'polyline') {
13752
13784
  addGetters(ctx, {
@@ -13809,7 +13841,7 @@
13809
13841
  }
13810
13842
 
13811
13843
  } else if (hasPoints) {
13812
- // TODO: add functions like bounds, isNull, pointCount
13844
+
13813
13845
  Object.defineProperty(ctx, 'coordinates',
13814
13846
  {set: function(obj) {
13815
13847
  if (!obj || utils.isArray(obj)) {
@@ -13848,8 +13880,13 @@
13848
13880
  }
13849
13881
 
13850
13882
  function shapeBounds() {
13851
- if (!_bounds) {
13883
+ if (_bounds) return _bounds;
13884
+ if (hasPaths) {
13852
13885
  _bounds = arcs.getMultiShapeBounds(_ids);
13886
+ } else if (hasPoints) {
13887
+ _bounds = getPointFeatureBounds(lyr.shapes[_id]);
13888
+ } else {
13889
+ _bounds = new Bounds();
13853
13890
  }
13854
13891
  return _bounds;
13855
13892
  }
@@ -13857,8 +13894,8 @@
13857
13894
  return function(id) {
13858
13895
  _id = id;
13859
13896
  // reset stored values
13897
+ _bounds = null;
13860
13898
  if (hasPaths) {
13861
- _bounds = null;
13862
13899
  _centroid = null;
13863
13900
  _innerXY = null;
13864
13901
  _ids = lyr.shapes[id];
@@ -14263,9 +14300,9 @@
14263
14300
  }
14264
14301
 
14265
14302
  function compileCalcExpression(lyr, arcs, exp) {
14266
- var rowNo = 0, colNo = 0, cols = [];
14303
+ var rowNo = 0, colNo = 0, recId = -1, cols = [];
14267
14304
  var ctx1 = { // context for first phase (capturing values for each feature)
14268
- count: assign,
14305
+ count: assign, // dummy function - first pass does nothing
14269
14306
  sum: captureNum,
14270
14307
  sums: capture,
14271
14308
  average: captureNum,
@@ -14280,6 +14317,7 @@
14280
14317
  max: captureNum,
14281
14318
  mode: capture,
14282
14319
  collect: capture,
14320
+ collectIds: captureId,
14283
14321
  first: assignOnce,
14284
14322
  every: every,
14285
14323
  some: some,
@@ -14303,6 +14341,7 @@
14303
14341
  mean: wrap(utils.mean),
14304
14342
  mode: wrap(getMode),
14305
14343
  collect: wrap(pass),
14344
+ collectIds: wrap(pass),
14306
14345
  first: wrap(pass),
14307
14346
  every: wrap(pass, false),
14308
14347
  some: wrap(pass, false),
@@ -14389,6 +14428,7 @@
14389
14428
 
14390
14429
  function procRecord(i) {
14391
14430
  if (i < 0 || i >= len) error("Invalid record index");
14431
+ recId = i;
14392
14432
  calc1(i);
14393
14433
  rowNo++;
14394
14434
  colNo = 0;
@@ -14397,6 +14437,7 @@
14397
14437
  function noop() {}
14398
14438
 
14399
14439
  function reset() {
14440
+ recId = -1;
14400
14441
  rowNo = 0;
14401
14442
  colNo = 0;
14402
14443
  cols = [];
@@ -14438,6 +14479,10 @@
14438
14479
  }
14439
14480
  */
14440
14481
 
14482
+ function captureId() {
14483
+ capture(recId);
14484
+ }
14485
+
14441
14486
  function capture(val) {
14442
14487
  var col;
14443
14488
  if (rowNo === 0) {
@@ -24602,6 +24647,21 @@ ${svg}
24602
24647
  // Experimental commands
24603
24648
  parser.section('Experimental commands (may give unexpected results)');
24604
24649
 
24650
+ parser.command('add-shape')
24651
+ .describe('')
24652
+ .option('geojson', {
24653
+
24654
+ })
24655
+ .option('coordinates', {
24656
+
24657
+ })
24658
+ .option('properties', {
24659
+
24660
+ })
24661
+ .option('name', nameOpt)
24662
+ .option('target', targetOpt)
24663
+ .option('no-replace', noReplaceOpt);
24664
+
24605
24665
  parser.command('alpha-shapes')
24606
24666
  // .describe('convert points to alpha shapes (aka concave hulls)')
24607
24667
  .option('interval', {
@@ -28076,6 +28136,136 @@ ${svg}
28076
28136
  stashVar('input_files', job.input_files);
28077
28137
  }
28078
28138
 
28139
+ cmd.addShape = addShape;
28140
+
28141
+ function addShape(targetLayers, targetDataset, opts) {
28142
+ if (targetLayers.length > 1) {
28143
+ stop('Command expects a single target layer');
28144
+ }
28145
+ var targetLyr = targetLayers[0]; // may be undefined
28146
+ var targetType = !opts.no_replace && targetLyr && targetLyr.geometry_type || null;
28147
+ var dataset = importGeoJSON(toFeature(opts, targetType));
28148
+ var outputLyr = mergeDatasetsIntoDataset(targetDataset, [dataset])[0];
28149
+ if (opts.no_replace || !targetLyr) {
28150
+ // create new layer
28151
+ setOutputLayerName(outputLyr, targetLyr && targetLyr.name, null, opts);
28152
+ return [outputLyr];
28153
+ }
28154
+ // merge into target layer
28155
+ return cmd.mergeLayers([targetLyr, outputLyr], {force: true});
28156
+ }
28157
+
28158
+ function toFeature(opts, geomType) {
28159
+ if (opts.geojson) {
28160
+ return parseArg(opts.geojson);
28161
+ }
28162
+
28163
+ var geom = opts.coordinates && parseCoordsAsGeometry(opts.coordinates) || null;
28164
+
28165
+ if (!geom) {
28166
+ stop('Missing required shape coordinates');
28167
+ }
28168
+
28169
+ if (geomType == 'point' && geom.type != 'Point') {
28170
+ stop('Expected point coordinates, received', geom.type);
28171
+ }
28172
+
28173
+ if (geomType == 'polygon' && geom.type != 'Polygon') {
28174
+ stop('Expected polygon coordinates, received', geom.type);
28175
+ }
28176
+
28177
+ if (geomType == 'polyline') {
28178
+ if (geom.type == 'Polygon') {
28179
+ geom.coordinates = geom.coordinates[0];
28180
+ geom.type = 'LineString';
28181
+ } else {
28182
+ stop('Expected polyline coordinates, received', geom.type);
28183
+ }
28184
+ }
28185
+
28186
+ return {
28187
+ type: 'Feature',
28188
+ properties: parseProperties(opts.properties),
28189
+ geometry: geom
28190
+ };
28191
+ }
28192
+
28193
+ function parseArg(obj) {
28194
+ return typeof obj == 'string' ? JSON.parse(obj) : obj;
28195
+ }
28196
+
28197
+ function parseProperties(arg) {
28198
+ if (!arg) return null;
28199
+ return parseArg(arg);
28200
+ }
28201
+
28202
+ function isArrayOfNumbers(arr) {
28203
+ return arr.length >= 2 && arr.every(utils.isNumber);
28204
+ }
28205
+
28206
+ function isClosedPath$1(arr) {
28207
+ return isArrayOfPoints(arr) && arr.length > 3 && samePoint$1(arr[0], arr[arr.length - 1]);
28208
+ }
28209
+
28210
+ function samePoint$1(a, b) {
28211
+ return a[0] == b[0] && a[1] == b[1];
28212
+ }
28213
+
28214
+ function isArrayOfPoints(arr) {
28215
+ return arr.every(isPoint);
28216
+ }
28217
+
28218
+ function isPoint(arr) {
28219
+ return arr && arr.length == 2 && isArrayOfNumbers(arr);
28220
+ }
28221
+
28222
+ function transposeCoords(arr) {
28223
+ var coords = [];
28224
+ for (var i=0; i<arr.length; i+=2) {
28225
+ coords.push([arr[i], arr[i+1]]);
28226
+ }
28227
+ if (!isArrayOfPoints(coords)) {
28228
+ stop('Unable to parse x,y,x,y... coordinates');
28229
+ }
28230
+ return coords;
28231
+ }
28232
+
28233
+ function parseCoordsAsGeometry(arg) {
28234
+ if (typeof arg == 'string') {
28235
+ arg = arg.trim();
28236
+ if (!arg.startsWith('[') && !arg.endsWith(']')) {
28237
+ arg = '[' + arg + ']';
28238
+ }
28239
+ }
28240
+ var arr = parseArg(arg);
28241
+ if (isPoint(arr)) {
28242
+ return {
28243
+ type: 'Point',
28244
+ coordinates: arr
28245
+ };
28246
+ }
28247
+
28248
+ if (isArrayOfNumbers(arr)) {
28249
+ arr = transposeCoords(arr);
28250
+ }
28251
+
28252
+ if (isClosedPath$1(arr)) {
28253
+ return {
28254
+ type: 'Polygon',
28255
+ coordinates: [arr]
28256
+ };
28257
+ }
28258
+
28259
+ if (isArrayOfPoints(arr)) {
28260
+ return {
28261
+ type: 'LineString',
28262
+ coordinates: arr
28263
+ };
28264
+ }
28265
+
28266
+ stop('Unable to import coordinates');
28267
+ }
28268
+
28079
28269
  const epsilon = 1.1102230246251565e-16;
28080
28270
  const splitter = 134217729;
28081
28271
  const resulterrbound = (3 + 8 * epsilon) * epsilon;
@@ -29264,17 +29454,6 @@ ${svg}
29264
29454
  bufferIntersection: bufferIntersection
29265
29455
  });
29266
29456
 
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
29457
  function getPolylineBufferMaker2(arcs, geod, getBearing, opts) {
29279
29458
  var makeLeftBuffer = getPathBufferMaker2(arcs, geod, getBearing, opts);
29280
29459
  var geomType = opts.geometry_type;
@@ -32733,6 +32912,8 @@ ${svg}
32733
32912
  var colorArg = opts.colors && opts.colors.length == 1 ? opts.colors[0] : null;
32734
32913
  var colorScheme;
32735
32914
 
32915
+ if (method == 'blacki') return [];
32916
+
32736
32917
  if (colorArg == 'random') {
32737
32918
  if (categorical) {
32738
32919
  return getRandomColors(n);
@@ -32802,7 +32983,7 @@ ${svg}
32802
32983
  }
32803
32984
 
32804
32985
  var sequential = ['quantile', 'nice', 'equal-interval', 'hybrid', 'breaks'];
32805
- var all = ['non-adjacent', 'indexed', 'categorical'].concat(sequential);
32986
+ var all = ['non-adjacent', 'indexed', 'categorical', 'blacki'].concat(sequential);
32806
32987
 
32807
32988
  function getClassifyMethod(opts, dataType) {
32808
32989
  var method;
@@ -32836,6 +33017,79 @@ ${svg}
32836
33017
  return method;
32837
33018
  }
32838
33019
 
33020
+ function getBlackiClassifier(lyr, dataField) {
33021
+ var records = lyr.data.getRecords();
33022
+ // classes are integers, dataIds can be any scalar
33023
+ var dataToClassIndex = {};
33024
+ var classToDataIndex = [];
33025
+ var classCount = 0;
33026
+
33027
+ records.forEach(function(rec, recId) {
33028
+ var dataIds = rec[dataField];
33029
+ var thisClass = -1;
33030
+ var thatClass, dataId;
33031
+
33032
+ if (!Array.isArray(dataIds)) return;
33033
+
33034
+ for (var i=0; i<dataIds.length; i++) {
33035
+ dataId = dataIds[i];
33036
+ thatClass = dataId in dataToClassIndex ? dataToClassIndex[dataId] : -1;
33037
+ if (thatClass == -1) {
33038
+ if (thisClass == -1) {
33039
+ thisClass = classCount++;
33040
+ classToDataIndex[thisClass] = [dataId];
33041
+ }
33042
+ dataToClassIndex[dataId] = thisClass;
33043
+
33044
+ } else {
33045
+
33046
+ if (thisClass == -1) {
33047
+ thisClass = thatClass;
33048
+ } else if (thisClass > thatClass) {
33049
+ mergeClassIntoClass(thisClass, thatClass);
33050
+ thisClass = thatClass;
33051
+ } else if (thisClass < thatClass) {
33052
+ mergeClassIntoClass(thatClass, thisClass);
33053
+ }
33054
+ }
33055
+ }
33056
+ });
33057
+
33058
+ var remapTable = compressClassIds();
33059
+ var classIds = records.map(function(rec) {
33060
+ var ids = rec[dataField];
33061
+ if (!Array.isArray(ids) || ids.length === 0) return -1;
33062
+ // TODO: assert that ids all belong to the same class
33063
+ if (ids[0] in dataToClassIndex === false) {
33064
+ error('Internal error');
33065
+ }
33066
+ return remapTable[dataToClassIndex[ids[0]]];
33067
+ });
33068
+
33069
+ classToDataIndex = null;
33070
+ dataToClassIndex = null;
33071
+
33072
+ return function(recId) {
33073
+ return classIds[recId];
33074
+ };
33075
+
33076
+ function compressClassIds() {
33077
+ var newId = -1;
33078
+ return classToDataIndex.map(function(d, oldId) {
33079
+ if (d !== null) newId++;
33080
+ return newId;
33081
+ });
33082
+ }
33083
+
33084
+ function mergeClassIntoClass(fromId, toId) {
33085
+ classToDataIndex[fromId].forEach(function(dataId) {
33086
+ dataToClassIndex[dataId] = toId;
33087
+ });
33088
+ classToDataIndex[toId] = utils.uniq(classToDataIndex[toId].concat(classToDataIndex[fromId]));
33089
+ classToDataIndex[fromId] = null;
33090
+ }
33091
+ }
33092
+
32839
33093
  cmd.classify = function(lyr, dataset, optsArg) {
32840
33094
  if (!lyr.data) {
32841
33095
  initDataTable(lyr);
@@ -32898,6 +33152,8 @@ ${svg}
32898
33152
  stop('Invalid number of classes:', opts.classes, '(expected a value greater than 1)');
32899
33153
  }
32900
33154
  numClasses = opts.classes;
33155
+ } else if (method == 'blacki') {
33156
+ numClasses = 999; // dummy value
32901
33157
  } else if (method == 'indexed' && dataField) {
32902
33158
  numClasses = getIndexedClassCount(records, dataField);
32903
33159
  } else if (opts.breaks) {
@@ -32935,6 +33191,8 @@ ${svg}
32935
33191
  if (fieldType === null) {
32936
33192
  // no valid data -- always return null value
32937
33193
  classifyByRecordId = function() {return nullValue;};
33194
+ } else if (method == 'blacki') {
33195
+ classifyByRecordId = getBlackiClassifier(lyr, dataField);
32938
33196
  } else if (method == 'non-adjacent') {
32939
33197
  classifyByRecordId = getNonAdjacentClassifier(lyr, dataset, values);
32940
33198
  } else if (method == 'indexed') {
@@ -37095,7 +37353,7 @@ ${svg}
37095
37353
  editor.done();
37096
37354
  if (!opts.debug) {
37097
37355
  buildTopology(dataset);
37098
- cleanProjectedLayers(dataset);
37356
+ cleanProjectedPathLayers(dataset);
37099
37357
  }
37100
37358
  }
37101
37359
 
@@ -37606,15 +37864,41 @@ ${svg}
37606
37864
  } else {
37607
37865
  clipData = getClippingDataset(src, dest, opts);
37608
37866
  }
37867
+ // clip data to projection limits (some projections), if content exceeds the limit
37868
+ //
37609
37869
  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;
37870
+ clipped = clipLayersIfNeeded(dataset, clipData);
37614
37871
  }
37615
37872
  return cut || clipped;
37616
37873
  }
37617
37874
 
37875
+ function clipLayersIfNeeded(dataset, clipData) {
37876
+ // Avoid clipping layers that are fully enclosed within the projectable
37877
+ // coordinate space (represented by a dataset containing a single
37878
+ // polygon layer, @clipData). This avoids performing unnecessary intersection
37879
+ // tests on each line segment.
37880
+ var layers = dataset.layers.filter(function(lyr) {
37881
+ return !layerIsFullyEnclosed(lyr, dataset, clipData);
37882
+ });
37883
+ if (layers.length > 0) {
37884
+ clipLayersInPlace(layers, clipData, dataset, 'clip');
37885
+ return true;
37886
+ }
37887
+ return false;
37888
+ }
37889
+
37890
+ // @clipData: a dataset containing a polygon layer
37891
+ function layerIsFullyEnclosed(lyr, dataset, clipData) {
37892
+ // This test uses the layer's bounding box to represent the extent of the
37893
+ // layer, and can produce false negatives.
37894
+ var dataBounds = getLayerBounds(lyr, dataset.arcs);
37895
+ var enclosed = false;
37896
+ clipData.layers[0].shapes.forEach(function(shp, i) {
37897
+ enclosed = enclosed || testBoundsInPolygon(dataBounds, shp, clipData.arcs);
37898
+ });
37899
+ return enclosed;
37900
+ }
37901
+
37618
37902
 
37619
37903
  function insertPreProjectionCuts(dataset, src, dest) {
37620
37904
  var antimeridian = getAntimeridian(dest.lam0 * 180 / Math.PI);
@@ -37809,7 +38093,6 @@ ${svg}
37809
38093
  var badArcs = 0;
37810
38094
  var badPoints = 0;
37811
38095
  var clipped = preProjectionClip(dataset, src, dest, opts);
37812
-
37813
38096
  dataset.layers.forEach(function(lyr) {
37814
38097
  if (layerHasPoints(lyr)) {
37815
38098
  badPoints += projectPointLayer(lyr, proj); // v2 compatible (invalid points are removed)
@@ -37826,7 +38109,7 @@ ${svg}
37826
38109
  if (clipped) {
37827
38110
  // TODO: could more selective in cleaning clipped layers
37828
38111
  // (probably only needed when clipped area crosses the antimeridian or includes a pole)
37829
- cleanProjectedLayers(dataset);
38112
+ cleanProjectedPathLayers(dataset);
37830
38113
  }
37831
38114
 
37832
38115
  if (badArcs > 0 && !opts.quiet) {
@@ -37842,7 +38125,7 @@ ${svg}
37842
38125
  // * Removes line intersections
37843
38126
  // * TODO: what if a layer contains polygons with desired overlaps? should
37844
38127
  // we ignore overlaps between different features?
37845
- function cleanProjectedLayers(dataset) {
38128
+ function cleanProjectedPathLayers(dataset) {
37846
38129
  // TODO: only clean affected polygons (cleaning all polygons can be slow)
37847
38130
  var polygonLayers = dataset.layers.filter(lyr => lyr.geometry_type == 'polygon');
37848
38131
  // clean options: force a topology update (by default, this only happens when
@@ -37910,7 +38193,7 @@ ${svg}
37910
38193
  __proto__: null,
37911
38194
  fetchCrsInfo: fetchCrsInfo,
37912
38195
  projectDataset: projectDataset,
37913
- cleanProjectedLayers: cleanProjectedLayers,
38196
+ cleanProjectedPathLayers: cleanProjectedPathLayers,
37914
38197
  projectPointLayer: projectPointLayer,
37915
38198
  projectArcs: projectArcs,
37916
38199
  projectArcs2: projectArcs2
@@ -38884,12 +39167,12 @@ ${svg}
38884
39167
  function polylineToMidpoints(shp, arcs, opts) {
38885
39168
  if (!shp) return null;
38886
39169
  var points = shp.map(function(path) {
38887
- return findPathMidpoint(path, arcs);
39170
+ return findPathMidpoint(path, arcs, false);
38888
39171
  });
38889
39172
  return points;
38890
39173
  }
38891
39174
 
38892
- function findPathMidpoint(path, arcs) {
39175
+ function findPathMidpoint(path, arcs, useNearestVertex) {
38893
39176
  var halfLen = calcPathLen(path, arcs, false) / 2;
38894
39177
  var partialLen = 0;
38895
39178
  var p;
@@ -38906,7 +39189,11 @@ ${svg}
38906
39189
  var k;
38907
39190
  if (partialLen + segLen >= halfLen) {
38908
39191
  k = (halfLen - partialLen) / segLen;
38909
- p = [a + k * (c - a), b + k * (d - b)];
39192
+ if (useNearestVertex) {
39193
+ k = k < 0.5 ? 0 : 1;
39194
+ }
39195
+ // p = [a + k * (c - a), b + k * (d - b)];
39196
+ p = [(1 - k) * a + k * c, (1 - k) * b + k * d];
38910
39197
  }
38911
39198
  partialLen += segLen;
38912
39199
  });
@@ -42786,7 +43073,7 @@ ${svg}
42786
43073
  name == 'point-grid' || name == 'shape' || name == 'rectangle' ||
42787
43074
  name == 'require' || name == 'run' || name == 'define' ||
42788
43075
  name == 'include' || name == 'print' || name == 'comment' || name == 'if' || name == 'elif' ||
42789
- name == 'else' || name == 'endif' || name == 'stop';
43076
+ name == 'else' || name == 'endif' || name == 'stop' || name == 'add-shape';
42790
43077
  }
42791
43078
 
42792
43079
  async function runCommand(command, job) {
@@ -42860,7 +43147,14 @@ ${svg}
42860
43147
  source = findCommandSource(convertSourceName(opts.source, targets), job.catalog, opts);
42861
43148
  }
42862
43149
 
42863
- if (name == 'affine') {
43150
+ if (name == 'add-shape') {
43151
+ if (!targetDataset) {
43152
+ targetDataset = {info: {}, layers: []};
43153
+ targetLayers = targetDataset.layers;
43154
+ job.catalog.addDataset(targetDataset);
43155
+ }
43156
+ outputLayers = cmd.addShape(targetLayers, targetDataset, opts);
43157
+ } else if (name == 'affine') {
42864
43158
  cmd.affine(targetLayers, targetDataset, opts);
42865
43159
 
42866
43160
  } else if (name == 'alpha-shapes') {