mapshaper 0.5.75 → 0.5.76

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/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ v0.5.76
2
+ * Fixed bug in mapshaper-gui -q.
3
+ * Added undocumented -split-lines command.
4
+
1
5
  v0.5.75
2
6
  * Added support for importing data by copy-pasting files onto the web UI (works in Chrome and Safari but not Firefox).
3
7
 
package/mapshaper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.5.74";
3
+ var VERSION = "0.5.75";
4
4
 
5
5
 
6
6
  var utils = /*#__PURE__*/Object.freeze({
@@ -2967,12 +2967,33 @@
2967
2967
  orient2D(cx, cy, dx, dy, bx, by) <= 0;
2968
2968
  }
2969
2969
 
2970
+ // Useful for determining if a segment that intersects another segment is
2971
+ // entering or leaving an enclosed buffer area
2972
+ // returns -1 if angle of p1p2 -> p3p4 is counter-clockwise (left turn)
2973
+ // returns 1 if angle is clockwise
2974
+ // return 0 if segments are collinear
2975
+ function segmentTurn(p1, p2, p3, p4) {
2976
+ var ax = p1[0],
2977
+ ay = p1[1],
2978
+ bx = p2[0],
2979
+ by = p2[1],
2980
+ // shift p3p4 segment to start at p2
2981
+ dx = bx - p3[0],
2982
+ dy = by - p3[1],
2983
+ cx = p4[0] + dx,
2984
+ cy = p4[1] + dy,
2985
+ orientation = orient2D(ax, ay, bx, by, cx, cy);
2986
+ if (!orientation) return 0;
2987
+ return orientation < 0 ? 1 : -1;
2988
+ }
2989
+
2970
2990
  var SegmentGeom = /*#__PURE__*/Object.freeze({
2971
2991
  __proto__: null,
2972
2992
  segmentIntersection: segmentIntersection,
2973
2993
  findClosestPointOnSeg: findClosestPointOnSeg,
2974
2994
  orient2D: orient2D,
2975
- segmentHit: segmentHit
2995
+ segmentHit: segmentHit,
2996
+ segmentTurn: segmentTurn
2976
2997
  });
2977
2998
 
2978
2999
  var geom = Object.assign({}, Geom, PolygonGeom, PathGeom, SegmentGeom, PolygonCentroid);
@@ -8136,10 +8157,14 @@
8136
8157
  }
8137
8158
 
8138
8159
  function roundToDigits(n, d) {
8139
- return +n.toFixed(d);
8160
+ return +n.toFixed(d); // string conversion makes this slow
8161
+ }
8162
+
8163
+ function roundToTenths(n) {
8164
+ return (Math.round(n * 10)) / 10;
8140
8165
  }
8141
8166
 
8142
- // inc: Rounding incrememnt (e.g. 0.001 rounds to thousandths)
8167
+ // inc: Rounding increment (e.g. 0.001 rounds to thousandths)
8143
8168
  function getRoundingFunction(inc) {
8144
8169
  if (!utils.isNumber(inc) || inc === 0) {
8145
8170
  error("Rounding increment must be a non-zero number.");
@@ -8209,6 +8234,7 @@
8209
8234
  __proto__: null,
8210
8235
  roundToSignificantDigits: roundToSignificantDigits,
8211
8236
  roundToDigits: roundToDigits,
8237
+ roundToTenths: roundToTenths,
8212
8238
  getRoundingFunction: getRoundingFunction,
8213
8239
  getBoundsPrecisionForDisplay: getBoundsPrecisionForDisplay,
8214
8240
  getRoundedCoordString: getRoundedCoordString,
@@ -13811,7 +13837,7 @@
13811
13837
  var symbolRenderers = {};
13812
13838
 
13813
13839
  function getTransform(xy, scale) {
13814
- var str = 'translate(' + xy[0] + ' ' + xy[1] + ')';
13840
+ var str = 'translate(' + roundToTenths(xy[0]) + ' ' + roundToTenths(xy[1]) + ')';
13815
13841
  if (scale && scale != 1) {
13816
13842
  str += ' scale(' + scale + ')';
13817
13843
  }
@@ -19718,6 +19744,23 @@ ${svg}
19718
19744
  .option('target', targetOpt)
19719
19745
  .option('no-replace', noReplaceOpt);
19720
19746
 
19747
+ parser.command('split-lines')
19748
+ // .describe('divide lines into sections')
19749
+ .option('dash-length', {
19750
+ type: 'distance',
19751
+ describe: 'length of split-apart lines'
19752
+ })
19753
+ .option('gap-length', {
19754
+ type: 'distance',
19755
+ describe: 'length of gap between segments'
19756
+ })
19757
+ .option('planar', {
19758
+ type: 'flag',
19759
+ describe: 'use planar geometry'
19760
+ })
19761
+ .option('where', whereOpt)
19762
+ .option('target', targetOpt);
19763
+
19721
19764
  parser.command('split-on-grid')
19722
19765
  .describe('split features into separate layers using a grid')
19723
19766
  .validate(validateGridOpts)
@@ -26263,97 +26306,6 @@ ${svg}
26263
26306
  geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmax, bb.ymin, bb.xmin, bb.ymin));
26264
26307
  }
26265
26308
 
26266
- function getGeodesic(P) {
26267
- if (!isLatLngCRS(P)) error('Expected an unprojected CRS');
26268
- var f = P.es / (1 + Math.sqrt(P.one_es));
26269
- var GeographicLib = require('mproj').internal.GeographicLib;
26270
- return new GeographicLib.Geodesic.Geodesic(P.a, f);
26271
- }
26272
-
26273
- function getPlanarSegmentEndpoint(x, y, bearing, meterDist) {
26274
- var rad = bearing / 180 * Math.PI;
26275
- var dx = Math.sin(rad) * meterDist;
26276
- var dy = Math.cos(rad) * meterDist;
26277
- return [x + dx, y + dy];
26278
- }
26279
-
26280
- // source: https://github.com/mapbox/cheap-ruler/blob/master/index.js
26281
- function fastGeodeticSegmentFunction(lng, lat, bearing, meterDist) {
26282
- var D2R = Math.PI / 180;
26283
- var cos = Math.cos(lat * D2R);
26284
- var cos2 = 2 * cos * cos - 1;
26285
- var cos3 = 2 * cos * cos2 - cos;
26286
- var cos4 = 2 * cos * cos3 - cos2;
26287
- var cos5 = 2 * cos * cos4 - cos3;
26288
- var kx = (111.41513 * cos - 0.09455 * cos3 + 0.00012 * cos5) * 1000;
26289
- var ky = (111.13209 - 0.56605 * cos2 + 0.0012 * cos4) * 1000;
26290
- var bearingRad = bearing * D2R;
26291
- var lat2 = lat + Math.cos(bearingRad) * meterDist / ky;
26292
- var lng2 = lng + Math.sin(bearingRad) * meterDist / kx;
26293
- return [lng2, lat2];
26294
- }
26295
-
26296
- function getGeodeticSegmentFunction(P) {
26297
- if (!isLatLngCRS(P)) {
26298
- return getPlanarSegmentEndpoint;
26299
- }
26300
- var g = getGeodesic(P);
26301
- return function(lng, lat, bearing, meterDist) {
26302
- var o = g.Direct(lat, lng, bearing, meterDist);
26303
- var p = [o.lon2, o.lat2];
26304
- return p;
26305
- };
26306
- }
26307
-
26308
- function getFastGeodeticSegmentFunction(P) {
26309
- // CAREFUL: this function has higher error at very large distances and at the poles
26310
- // also, it wouldn't work for other planets than Earth
26311
- return isLatLngCRS(P) ? fastGeodeticSegmentFunction : getPlanarSegmentEndpoint;
26312
- }
26313
-
26314
- // Useful for determining if a segment that intersects another segment is
26315
- // entering or leaving an enclosed buffer area
26316
- // returns -1 if angle of p1p2 -> p3p4 is counter-clockwise (left turn)
26317
- // returns 1 if angle is clockwise
26318
- // return 0 if segments are collinear
26319
- function segmentTurn(p1, p2, p3, p4) {
26320
- var ax = p1[0],
26321
- ay = p1[1],
26322
- bx = p2[0],
26323
- by = p2[1],
26324
- // shift p3p4 segment to start at p2
26325
- dx = bx - p3[0],
26326
- dy = by - p3[1],
26327
- cx = p4[0] + dx,
26328
- cy = p4[1] + dy,
26329
- orientation = geom.orient2D(ax, ay, bx, by, cx, cy);
26330
- if (!orientation) return 0;
26331
- return orientation < 0 ? 1 : -1;
26332
- }
26333
-
26334
- function bearingDegrees(a, b, c, d) {
26335
- return geom.bearing(a, b, c, d) * 180 / Math.PI;
26336
- }
26337
-
26338
- function bearingDegrees2D(a, b, c, d) {
26339
- return geom.bearing2D(a, b, c, d) * 180 / Math.PI;
26340
- }
26341
-
26342
- // return function to calculate bearing of a segment in degrees
26343
- function getBearingFunction(dataset) {
26344
- var P = getDatasetCRS(dataset);
26345
- return isLatLngCRS(P) ? bearingDegrees : bearingDegrees2D;
26346
- }
26347
-
26348
- var Geodesic = /*#__PURE__*/Object.freeze({
26349
- __proto__: null,
26350
- getPlanarSegmentEndpoint: getPlanarSegmentEndpoint,
26351
- getGeodeticSegmentFunction: getGeodeticSegmentFunction,
26352
- getFastGeodeticSegmentFunction: getFastGeodeticSegmentFunction,
26353
- segmentTurn: segmentTurn,
26354
- getBearingFunction: getBearingFunction
26355
- });
26356
-
26357
26309
  function getPolylineBufferMaker2(arcs, geod, getBearing, opts) {
26358
26310
  var makeLeftBuffer = getPathBufferMaker2(arcs, geod, getBearing, opts);
26359
26311
  var geomType = opts.geometry_type;
@@ -26647,6 +26599,101 @@ ${svg}
26647
26599
  };
26648
26600
  }
26649
26601
 
26602
+ // GeographicLib docs: https://geographiclib.sourceforge.io/html/js/
26603
+ // https://geographiclib.sourceforge.io/html/js/module-GeographicLib_Geodesic.Geodesic.html
26604
+ // https://geographiclib.sourceforge.io/html/js/tutorial-2-interface.html
26605
+ function getGeodesic(P) {
26606
+ if (!isLatLngCRS(P)) error('Expected an unprojected CRS');
26607
+ var f = P.es / (1 + Math.sqrt(P.one_es));
26608
+ var GeographicLib = require('mproj').internal.GeographicLib;
26609
+ return new GeographicLib.Geodesic.Geodesic(P.a, f);
26610
+ }
26611
+
26612
+ function interpolatePoint2D(ax, ay, bx, by, k) {
26613
+ var j = 1 - k;
26614
+ return [ax * j + bx * k, ay * j + by * k];
26615
+ }
26616
+
26617
+ function getInterpolationFunction(P) {
26618
+ var spherical = P && isLatLngCRS(P);
26619
+ if (!spherical) return interpolatePoint2D;
26620
+ var geod = getGeodesic(P);
26621
+ return function(lng, lat, lng2, lat2, k) {
26622
+ var r = geod.Inverse(lat, lng, lat2, lng2);
26623
+ var dist = r.s12 * k;
26624
+ var r2 = geod.Direct(lat, lng, r.azi1, dist);
26625
+ return [r2.lon2, r2.lat2];
26626
+ };
26627
+ }
26628
+
26629
+ function getPlanarSegmentEndpoint(x, y, bearing, meterDist) {
26630
+ var rad = bearing / 180 * Math.PI;
26631
+ var dx = Math.sin(rad) * meterDist;
26632
+ var dy = Math.cos(rad) * meterDist;
26633
+ return [x + dx, y + dy];
26634
+ }
26635
+
26636
+ // source: https://github.com/mapbox/cheap-ruler/blob/master/index.js
26637
+ function fastGeodeticSegmentFunction(lng, lat, bearing, meterDist) {
26638
+ var D2R = Math.PI / 180;
26639
+ var cos = Math.cos(lat * D2R);
26640
+ var cos2 = 2 * cos * cos - 1;
26641
+ var cos3 = 2 * cos * cos2 - cos;
26642
+ var cos4 = 2 * cos * cos3 - cos2;
26643
+ var cos5 = 2 * cos * cos4 - cos3;
26644
+ var kx = (111.41513 * cos - 0.09455 * cos3 + 0.00012 * cos5) * 1000;
26645
+ var ky = (111.13209 - 0.56605 * cos2 + 0.0012 * cos4) * 1000;
26646
+ var bearingRad = bearing * D2R;
26647
+ var lat2 = lat + Math.cos(bearingRad) * meterDist / ky;
26648
+ var lng2 = lng + Math.sin(bearingRad) * meterDist / kx;
26649
+ return [lng2, lat2];
26650
+ }
26651
+
26652
+ function getGeodeticSegmentFunction(P) {
26653
+ if (!isLatLngCRS(P)) {
26654
+ return getPlanarSegmentEndpoint;
26655
+ }
26656
+ var g = getGeodesic(P);
26657
+ return function(lng, lat, bearing, meterDist) {
26658
+ var o = g.Direct(lat, lng, bearing, meterDist);
26659
+ var p = [o.lon2, o.lat2];
26660
+ return p;
26661
+ };
26662
+ }
26663
+
26664
+ function getFastGeodeticSegmentFunction(P) {
26665
+ // CAREFUL: this function has higher error at very large distances and at the poles
26666
+ // also, it wouldn't work for other planets than Earth
26667
+ return isLatLngCRS(P) ? fastGeodeticSegmentFunction : getPlanarSegmentEndpoint;
26668
+ }
26669
+
26670
+
26671
+ function bearingDegrees(a, b, c, d) {
26672
+ return geom.bearing(a, b, c, d) * 180 / Math.PI;
26673
+ }
26674
+
26675
+ function bearingDegrees2D(a, b, c, d) {
26676
+ return geom.bearing2D(a, b, c, d) * 180 / Math.PI;
26677
+ }
26678
+
26679
+ // return function to calculate bearing of a segment in degrees
26680
+ function getBearingFunction(dataset) {
26681
+ var P = getDatasetCRS(dataset);
26682
+ return isLatLngCRS(P) ? bearingDegrees : bearingDegrees2D;
26683
+ }
26684
+
26685
+ var Geodesic = /*#__PURE__*/Object.freeze({
26686
+ __proto__: null,
26687
+ interpolatePoint2D: interpolatePoint2D,
26688
+ getInterpolationFunction: getInterpolationFunction,
26689
+ getPlanarSegmentEndpoint: getPlanarSegmentEndpoint,
26690
+ getGeodeticSegmentFunction: getGeodeticSegmentFunction,
26691
+ getFastGeodeticSegmentFunction: getFastGeodeticSegmentFunction,
26692
+ bearingDegrees: bearingDegrees,
26693
+ bearingDegrees2D: bearingDegrees2D,
26694
+ getBearingFunction: getBearingFunction
26695
+ });
26696
+
26650
26697
  function makePolylineBuffer(lyr, dataset, opts) {
26651
26698
  var geojson = makeShapeBufferGeoJSON(lyr, dataset, opts);
26652
26699
  var dataset2 = importGeoJSON(geojson, {});
@@ -31237,6 +31284,7 @@ ${svg}
31237
31284
  type: 'FeatureCollection',
31238
31285
  features: features
31239
31286
  };
31287
+
31240
31288
  // console.log(JSON.stringify(geojson, null, 2))
31241
31289
  return importGeoJSON(geojson);
31242
31290
  };
@@ -34403,11 +34451,6 @@ ${svg}
34403
34451
  });
34404
34452
  }
34405
34453
 
34406
- function interpolatePoint2D(ax, ay, bx, by, k) {
34407
- var j = 1 - k;
34408
- return [ax * j + bx * k, ay * j + by * k];
34409
- }
34410
-
34411
34454
  function interpolatePointsAlongArc(ids, arcs, interval) {
34412
34455
  var iter = arcs.getShapeIter(ids);
34413
34456
  var distance = arcs.isPlanar() ? geom.distance2D : geom.greatCircleDistance;
@@ -37267,6 +37310,111 @@ ${svg}
37267
37310
  getSplitNameFunction: getSplitNameFunction
37268
37311
  });
37269
37312
 
37313
+ cmd.splitLines = function(lyr, dataset, opts) {
37314
+ var crs = getDatasetCRS(dataset);
37315
+ requirePolylineLayer(lyr);
37316
+ var splitFeature = getSplitFeatureFunction(crs, opts);
37317
+
37318
+ // TODO: remove duplication with mapshaper-each.js
37319
+ var editor = getFeatureEditor(lyr, dataset);
37320
+ var exprOpts = {
37321
+ geojson_editor: editor,
37322
+ context: {splitFeature}
37323
+ };
37324
+ var exp = `this.geojson = splitFeature(this.geojson)`;
37325
+
37326
+ var compiled = compileFeatureExpression(exp, lyr, dataset.arcs, exprOpts);
37327
+ var n = getFeatureCount(lyr);
37328
+ var filter;
37329
+ if (opts && opts.where) {
37330
+ filter = compileValueExpression(opts.where, lyr, dataset.arcs);
37331
+ }
37332
+ for (var i=0; i<n; i++) {
37333
+ if (!filter || filter(i)) {
37334
+ compiled(i);
37335
+ }
37336
+ }
37337
+ replaceLayerContents(lyr, dataset, editor.done());
37338
+ };
37339
+
37340
+ function getSplitFeatureFunction(crs, opts) {
37341
+ var dashLen = opts.dash_length ? convertDistanceParam(opts.dash_length, crs) : 0;
37342
+ var gapLen = opts.gap_length ? convertDistanceParam(opts.gap_length, crs) : 0;
37343
+ if (dashLen > 0 === false) {
37344
+ stop('Missing required segment-length parameter');
37345
+ }
37346
+ if (gapLen >= 0 == false) {
37347
+ stop('Invalid gap-length option');
37348
+ }
37349
+ var splitLine = getSplitLineFunction(crs, dashLen, gapLen, !!opts.planar);
37350
+ return function(feat) {
37351
+ var geom = feat.geometry;
37352
+ if (!geom) return feat;
37353
+ if (geom.type == 'LineString') {
37354
+ geom.type = 'MultiLineString';
37355
+ geom.coordinates = [geom.coordinates];
37356
+ }
37357
+ if (geom.type != 'MultiLineString') {
37358
+ error('Unexpected geometry:', geom.type);
37359
+ }
37360
+ geom.coordinates = geom.coordinates.reduce(function(memo, coords) {
37361
+ try {
37362
+ var parts = splitLine(coords);
37363
+ memo = memo.concat(parts);
37364
+ } catch(e) {
37365
+ console.error(e);
37366
+ throw e;
37367
+ }
37368
+ return memo;
37369
+ }, []);
37370
+
37371
+ return feat;
37372
+ };
37373
+ }
37374
+
37375
+ function getSplitLineFunction(crs, dashLen, gapLen, planar) {
37376
+ var interpolate = getInterpolationFunction(planar ? null : crs);
37377
+ var distance = isLatLngCRS(crs) ? greatCircleDistance : distance2D;
37378
+ var inDash, parts2, interval;
37379
+ function addPart(coords) {
37380
+ if (inDash) parts2.push(coords);
37381
+ if (gapLen > 0) {
37382
+ inDash = !inDash;
37383
+ interval = inDash ? dashLen : gapLen;
37384
+ }
37385
+ }
37386
+ return function splitLineString(coords) {
37387
+ var elapsedDist = 0;
37388
+ var p = coords[0];
37389
+ var coords2 = [p];
37390
+ var segLen, k, prev;
37391
+ // init this LineString
37392
+ inDash = true;
37393
+ parts2 = [];
37394
+ interval = gapLen;
37395
+ for (var i=1, n=coords.length; i<n; i++) {
37396
+ prev = p;
37397
+ p = coords[i];
37398
+ segLen = distance(prev[0], prev[1], p[0], p[1]);
37399
+ while (elapsedDist + segLen >= interval) {
37400
+ k = (interval - elapsedDist) / segLen;
37401
+ prev = interpolate(prev[0], prev[1], p[0], p[1], k);
37402
+ elapsedDist = 0;
37403
+ coords2.push(prev);
37404
+ addPart(coords2);
37405
+ coords2 = [prev];
37406
+ segLen = distance(prev[0], prev[1], p[0], p[1]);
37407
+ }
37408
+ coords2.push(p);
37409
+ elapsedDist += segLen;
37410
+ }
37411
+ if (elapsedDist > 0 && coords2.length > 1) {
37412
+ addPart(coords2);
37413
+ }
37414
+ return parts2;
37415
+ };
37416
+ }
37417
+
37270
37418
  cmd.svgStyle = function(lyr, dataset, opts) {
37271
37419
  var filter;
37272
37420
  if (!lyr.data) {
@@ -38228,6 +38376,9 @@ ${svg}
38228
38376
  } else if (name == 'split') {
38229
38377
  outputLayers = applyCommandToEachLayer(cmd.splitLayer, targetLayers, opts.expression, opts);
38230
38378
 
38379
+ } else if (name == 'split-lines') {
38380
+ applyCommandToEachLayer(cmd.splitLines, targetLayers, targetDataset, opts);
38381
+
38231
38382
  } else if (name == 'split-on-grid') {
38232
38383
  outputLayers = applyCommandToEachLayer(cmd.splitLayerOnGrid, targetLayers, arcs, opts);
38233
38384
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.5.75",
3
+ "version": "0.5.76",
4
4
  "description": "A tool for editing vector datasets for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
@@ -485,6 +485,7 @@
485
485
  }
486
486
 
487
487
  function addCSS(el, css) {
488
+ // console.error(css);
488
489
  el.style.cssText = mergeCSS(el.style.cssText, css);
489
490
  }
490
491
 
@@ -1254,7 +1255,7 @@
1254
1255
 
1255
1256
  function receiveFiles(files) {
1256
1257
  var prevSize = queuedFiles.length;
1257
- useQuickView = overQuickView;
1258
+ useQuickView = useQuickView || overQuickView;
1258
1259
  files = handleZipFiles(utils.toArray(files));
1259
1260
  addFilesToQueue(files);
1260
1261
  if (queuedFiles.length === 0) return;
package/www/mapshaper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.5.74";
3
+ var VERSION = "0.5.75";
4
4
 
5
5
 
6
6
  var utils = /*#__PURE__*/Object.freeze({
@@ -2967,12 +2967,33 @@
2967
2967
  orient2D(cx, cy, dx, dy, bx, by) <= 0;
2968
2968
  }
2969
2969
 
2970
+ // Useful for determining if a segment that intersects another segment is
2971
+ // entering or leaving an enclosed buffer area
2972
+ // returns -1 if angle of p1p2 -> p3p4 is counter-clockwise (left turn)
2973
+ // returns 1 if angle is clockwise
2974
+ // return 0 if segments are collinear
2975
+ function segmentTurn(p1, p2, p3, p4) {
2976
+ var ax = p1[0],
2977
+ ay = p1[1],
2978
+ bx = p2[0],
2979
+ by = p2[1],
2980
+ // shift p3p4 segment to start at p2
2981
+ dx = bx - p3[0],
2982
+ dy = by - p3[1],
2983
+ cx = p4[0] + dx,
2984
+ cy = p4[1] + dy,
2985
+ orientation = orient2D(ax, ay, bx, by, cx, cy);
2986
+ if (!orientation) return 0;
2987
+ return orientation < 0 ? 1 : -1;
2988
+ }
2989
+
2970
2990
  var SegmentGeom = /*#__PURE__*/Object.freeze({
2971
2991
  __proto__: null,
2972
2992
  segmentIntersection: segmentIntersection,
2973
2993
  findClosestPointOnSeg: findClosestPointOnSeg,
2974
2994
  orient2D: orient2D,
2975
- segmentHit: segmentHit
2995
+ segmentHit: segmentHit,
2996
+ segmentTurn: segmentTurn
2976
2997
  });
2977
2998
 
2978
2999
  var geom = Object.assign({}, Geom, PolygonGeom, PathGeom, SegmentGeom, PolygonCentroid);
@@ -8136,10 +8157,14 @@
8136
8157
  }
8137
8158
 
8138
8159
  function roundToDigits(n, d) {
8139
- return +n.toFixed(d);
8160
+ return +n.toFixed(d); // string conversion makes this slow
8161
+ }
8162
+
8163
+ function roundToTenths(n) {
8164
+ return (Math.round(n * 10)) / 10;
8140
8165
  }
8141
8166
 
8142
- // inc: Rounding incrememnt (e.g. 0.001 rounds to thousandths)
8167
+ // inc: Rounding increment (e.g. 0.001 rounds to thousandths)
8143
8168
  function getRoundingFunction(inc) {
8144
8169
  if (!utils.isNumber(inc) || inc === 0) {
8145
8170
  error("Rounding increment must be a non-zero number.");
@@ -8209,6 +8234,7 @@
8209
8234
  __proto__: null,
8210
8235
  roundToSignificantDigits: roundToSignificantDigits,
8211
8236
  roundToDigits: roundToDigits,
8237
+ roundToTenths: roundToTenths,
8212
8238
  getRoundingFunction: getRoundingFunction,
8213
8239
  getBoundsPrecisionForDisplay: getBoundsPrecisionForDisplay,
8214
8240
  getRoundedCoordString: getRoundedCoordString,
@@ -13811,7 +13837,7 @@
13811
13837
  var symbolRenderers = {};
13812
13838
 
13813
13839
  function getTransform(xy, scale) {
13814
- var str = 'translate(' + xy[0] + ' ' + xy[1] + ')';
13840
+ var str = 'translate(' + roundToTenths(xy[0]) + ' ' + roundToTenths(xy[1]) + ')';
13815
13841
  if (scale && scale != 1) {
13816
13842
  str += ' scale(' + scale + ')';
13817
13843
  }
@@ -19718,6 +19744,23 @@ ${svg}
19718
19744
  .option('target', targetOpt)
19719
19745
  .option('no-replace', noReplaceOpt);
19720
19746
 
19747
+ parser.command('split-lines')
19748
+ // .describe('divide lines into sections')
19749
+ .option('dash-length', {
19750
+ type: 'distance',
19751
+ describe: 'length of split-apart lines'
19752
+ })
19753
+ .option('gap-length', {
19754
+ type: 'distance',
19755
+ describe: 'length of gap between segments'
19756
+ })
19757
+ .option('planar', {
19758
+ type: 'flag',
19759
+ describe: 'use planar geometry'
19760
+ })
19761
+ .option('where', whereOpt)
19762
+ .option('target', targetOpt);
19763
+
19721
19764
  parser.command('split-on-grid')
19722
19765
  .describe('split features into separate layers using a grid')
19723
19766
  .validate(validateGridOpts)
@@ -26263,97 +26306,6 @@ ${svg}
26263
26306
  geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmax, bb.ymin, bb.xmin, bb.ymin));
26264
26307
  }
26265
26308
 
26266
- function getGeodesic(P) {
26267
- if (!isLatLngCRS(P)) error('Expected an unprojected CRS');
26268
- var f = P.es / (1 + Math.sqrt(P.one_es));
26269
- var GeographicLib = require('mproj').internal.GeographicLib;
26270
- return new GeographicLib.Geodesic.Geodesic(P.a, f);
26271
- }
26272
-
26273
- function getPlanarSegmentEndpoint(x, y, bearing, meterDist) {
26274
- var rad = bearing / 180 * Math.PI;
26275
- var dx = Math.sin(rad) * meterDist;
26276
- var dy = Math.cos(rad) * meterDist;
26277
- return [x + dx, y + dy];
26278
- }
26279
-
26280
- // source: https://github.com/mapbox/cheap-ruler/blob/master/index.js
26281
- function fastGeodeticSegmentFunction(lng, lat, bearing, meterDist) {
26282
- var D2R = Math.PI / 180;
26283
- var cos = Math.cos(lat * D2R);
26284
- var cos2 = 2 * cos * cos - 1;
26285
- var cos3 = 2 * cos * cos2 - cos;
26286
- var cos4 = 2 * cos * cos3 - cos2;
26287
- var cos5 = 2 * cos * cos4 - cos3;
26288
- var kx = (111.41513 * cos - 0.09455 * cos3 + 0.00012 * cos5) * 1000;
26289
- var ky = (111.13209 - 0.56605 * cos2 + 0.0012 * cos4) * 1000;
26290
- var bearingRad = bearing * D2R;
26291
- var lat2 = lat + Math.cos(bearingRad) * meterDist / ky;
26292
- var lng2 = lng + Math.sin(bearingRad) * meterDist / kx;
26293
- return [lng2, lat2];
26294
- }
26295
-
26296
- function getGeodeticSegmentFunction(P) {
26297
- if (!isLatLngCRS(P)) {
26298
- return getPlanarSegmentEndpoint;
26299
- }
26300
- var g = getGeodesic(P);
26301
- return function(lng, lat, bearing, meterDist) {
26302
- var o = g.Direct(lat, lng, bearing, meterDist);
26303
- var p = [o.lon2, o.lat2];
26304
- return p;
26305
- };
26306
- }
26307
-
26308
- function getFastGeodeticSegmentFunction(P) {
26309
- // CAREFUL: this function has higher error at very large distances and at the poles
26310
- // also, it wouldn't work for other planets than Earth
26311
- return isLatLngCRS(P) ? fastGeodeticSegmentFunction : getPlanarSegmentEndpoint;
26312
- }
26313
-
26314
- // Useful for determining if a segment that intersects another segment is
26315
- // entering or leaving an enclosed buffer area
26316
- // returns -1 if angle of p1p2 -> p3p4 is counter-clockwise (left turn)
26317
- // returns 1 if angle is clockwise
26318
- // return 0 if segments are collinear
26319
- function segmentTurn(p1, p2, p3, p4) {
26320
- var ax = p1[0],
26321
- ay = p1[1],
26322
- bx = p2[0],
26323
- by = p2[1],
26324
- // shift p3p4 segment to start at p2
26325
- dx = bx - p3[0],
26326
- dy = by - p3[1],
26327
- cx = p4[0] + dx,
26328
- cy = p4[1] + dy,
26329
- orientation = geom.orient2D(ax, ay, bx, by, cx, cy);
26330
- if (!orientation) return 0;
26331
- return orientation < 0 ? 1 : -1;
26332
- }
26333
-
26334
- function bearingDegrees(a, b, c, d) {
26335
- return geom.bearing(a, b, c, d) * 180 / Math.PI;
26336
- }
26337
-
26338
- function bearingDegrees2D(a, b, c, d) {
26339
- return geom.bearing2D(a, b, c, d) * 180 / Math.PI;
26340
- }
26341
-
26342
- // return function to calculate bearing of a segment in degrees
26343
- function getBearingFunction(dataset) {
26344
- var P = getDatasetCRS(dataset);
26345
- return isLatLngCRS(P) ? bearingDegrees : bearingDegrees2D;
26346
- }
26347
-
26348
- var Geodesic = /*#__PURE__*/Object.freeze({
26349
- __proto__: null,
26350
- getPlanarSegmentEndpoint: getPlanarSegmentEndpoint,
26351
- getGeodeticSegmentFunction: getGeodeticSegmentFunction,
26352
- getFastGeodeticSegmentFunction: getFastGeodeticSegmentFunction,
26353
- segmentTurn: segmentTurn,
26354
- getBearingFunction: getBearingFunction
26355
- });
26356
-
26357
26309
  function getPolylineBufferMaker2(arcs, geod, getBearing, opts) {
26358
26310
  var makeLeftBuffer = getPathBufferMaker2(arcs, geod, getBearing, opts);
26359
26311
  var geomType = opts.geometry_type;
@@ -26647,6 +26599,101 @@ ${svg}
26647
26599
  };
26648
26600
  }
26649
26601
 
26602
+ // GeographicLib docs: https://geographiclib.sourceforge.io/html/js/
26603
+ // https://geographiclib.sourceforge.io/html/js/module-GeographicLib_Geodesic.Geodesic.html
26604
+ // https://geographiclib.sourceforge.io/html/js/tutorial-2-interface.html
26605
+ function getGeodesic(P) {
26606
+ if (!isLatLngCRS(P)) error('Expected an unprojected CRS');
26607
+ var f = P.es / (1 + Math.sqrt(P.one_es));
26608
+ var GeographicLib = require('mproj').internal.GeographicLib;
26609
+ return new GeographicLib.Geodesic.Geodesic(P.a, f);
26610
+ }
26611
+
26612
+ function interpolatePoint2D(ax, ay, bx, by, k) {
26613
+ var j = 1 - k;
26614
+ return [ax * j + bx * k, ay * j + by * k];
26615
+ }
26616
+
26617
+ function getInterpolationFunction(P) {
26618
+ var spherical = P && isLatLngCRS(P);
26619
+ if (!spherical) return interpolatePoint2D;
26620
+ var geod = getGeodesic(P);
26621
+ return function(lng, lat, lng2, lat2, k) {
26622
+ var r = geod.Inverse(lat, lng, lat2, lng2);
26623
+ var dist = r.s12 * k;
26624
+ var r2 = geod.Direct(lat, lng, r.azi1, dist);
26625
+ return [r2.lon2, r2.lat2];
26626
+ };
26627
+ }
26628
+
26629
+ function getPlanarSegmentEndpoint(x, y, bearing, meterDist) {
26630
+ var rad = bearing / 180 * Math.PI;
26631
+ var dx = Math.sin(rad) * meterDist;
26632
+ var dy = Math.cos(rad) * meterDist;
26633
+ return [x + dx, y + dy];
26634
+ }
26635
+
26636
+ // source: https://github.com/mapbox/cheap-ruler/blob/master/index.js
26637
+ function fastGeodeticSegmentFunction(lng, lat, bearing, meterDist) {
26638
+ var D2R = Math.PI / 180;
26639
+ var cos = Math.cos(lat * D2R);
26640
+ var cos2 = 2 * cos * cos - 1;
26641
+ var cos3 = 2 * cos * cos2 - cos;
26642
+ var cos4 = 2 * cos * cos3 - cos2;
26643
+ var cos5 = 2 * cos * cos4 - cos3;
26644
+ var kx = (111.41513 * cos - 0.09455 * cos3 + 0.00012 * cos5) * 1000;
26645
+ var ky = (111.13209 - 0.56605 * cos2 + 0.0012 * cos4) * 1000;
26646
+ var bearingRad = bearing * D2R;
26647
+ var lat2 = lat + Math.cos(bearingRad) * meterDist / ky;
26648
+ var lng2 = lng + Math.sin(bearingRad) * meterDist / kx;
26649
+ return [lng2, lat2];
26650
+ }
26651
+
26652
+ function getGeodeticSegmentFunction(P) {
26653
+ if (!isLatLngCRS(P)) {
26654
+ return getPlanarSegmentEndpoint;
26655
+ }
26656
+ var g = getGeodesic(P);
26657
+ return function(lng, lat, bearing, meterDist) {
26658
+ var o = g.Direct(lat, lng, bearing, meterDist);
26659
+ var p = [o.lon2, o.lat2];
26660
+ return p;
26661
+ };
26662
+ }
26663
+
26664
+ function getFastGeodeticSegmentFunction(P) {
26665
+ // CAREFUL: this function has higher error at very large distances and at the poles
26666
+ // also, it wouldn't work for other planets than Earth
26667
+ return isLatLngCRS(P) ? fastGeodeticSegmentFunction : getPlanarSegmentEndpoint;
26668
+ }
26669
+
26670
+
26671
+ function bearingDegrees(a, b, c, d) {
26672
+ return geom.bearing(a, b, c, d) * 180 / Math.PI;
26673
+ }
26674
+
26675
+ function bearingDegrees2D(a, b, c, d) {
26676
+ return geom.bearing2D(a, b, c, d) * 180 / Math.PI;
26677
+ }
26678
+
26679
+ // return function to calculate bearing of a segment in degrees
26680
+ function getBearingFunction(dataset) {
26681
+ var P = getDatasetCRS(dataset);
26682
+ return isLatLngCRS(P) ? bearingDegrees : bearingDegrees2D;
26683
+ }
26684
+
26685
+ var Geodesic = /*#__PURE__*/Object.freeze({
26686
+ __proto__: null,
26687
+ interpolatePoint2D: interpolatePoint2D,
26688
+ getInterpolationFunction: getInterpolationFunction,
26689
+ getPlanarSegmentEndpoint: getPlanarSegmentEndpoint,
26690
+ getGeodeticSegmentFunction: getGeodeticSegmentFunction,
26691
+ getFastGeodeticSegmentFunction: getFastGeodeticSegmentFunction,
26692
+ bearingDegrees: bearingDegrees,
26693
+ bearingDegrees2D: bearingDegrees2D,
26694
+ getBearingFunction: getBearingFunction
26695
+ });
26696
+
26650
26697
  function makePolylineBuffer(lyr, dataset, opts) {
26651
26698
  var geojson = makeShapeBufferGeoJSON(lyr, dataset, opts);
26652
26699
  var dataset2 = importGeoJSON(geojson, {});
@@ -31237,6 +31284,7 @@ ${svg}
31237
31284
  type: 'FeatureCollection',
31238
31285
  features: features
31239
31286
  };
31287
+
31240
31288
  // console.log(JSON.stringify(geojson, null, 2))
31241
31289
  return importGeoJSON(geojson);
31242
31290
  };
@@ -34403,11 +34451,6 @@ ${svg}
34403
34451
  });
34404
34452
  }
34405
34453
 
34406
- function interpolatePoint2D(ax, ay, bx, by, k) {
34407
- var j = 1 - k;
34408
- return [ax * j + bx * k, ay * j + by * k];
34409
- }
34410
-
34411
34454
  function interpolatePointsAlongArc(ids, arcs, interval) {
34412
34455
  var iter = arcs.getShapeIter(ids);
34413
34456
  var distance = arcs.isPlanar() ? geom.distance2D : geom.greatCircleDistance;
@@ -37267,6 +37310,111 @@ ${svg}
37267
37310
  getSplitNameFunction: getSplitNameFunction
37268
37311
  });
37269
37312
 
37313
+ cmd.splitLines = function(lyr, dataset, opts) {
37314
+ var crs = getDatasetCRS(dataset);
37315
+ requirePolylineLayer(lyr);
37316
+ var splitFeature = getSplitFeatureFunction(crs, opts);
37317
+
37318
+ // TODO: remove duplication with mapshaper-each.js
37319
+ var editor = getFeatureEditor(lyr, dataset);
37320
+ var exprOpts = {
37321
+ geojson_editor: editor,
37322
+ context: {splitFeature}
37323
+ };
37324
+ var exp = `this.geojson = splitFeature(this.geojson)`;
37325
+
37326
+ var compiled = compileFeatureExpression(exp, lyr, dataset.arcs, exprOpts);
37327
+ var n = getFeatureCount(lyr);
37328
+ var filter;
37329
+ if (opts && opts.where) {
37330
+ filter = compileValueExpression(opts.where, lyr, dataset.arcs);
37331
+ }
37332
+ for (var i=0; i<n; i++) {
37333
+ if (!filter || filter(i)) {
37334
+ compiled(i);
37335
+ }
37336
+ }
37337
+ replaceLayerContents(lyr, dataset, editor.done());
37338
+ };
37339
+
37340
+ function getSplitFeatureFunction(crs, opts) {
37341
+ var dashLen = opts.dash_length ? convertDistanceParam(opts.dash_length, crs) : 0;
37342
+ var gapLen = opts.gap_length ? convertDistanceParam(opts.gap_length, crs) : 0;
37343
+ if (dashLen > 0 === false) {
37344
+ stop('Missing required segment-length parameter');
37345
+ }
37346
+ if (gapLen >= 0 == false) {
37347
+ stop('Invalid gap-length option');
37348
+ }
37349
+ var splitLine = getSplitLineFunction(crs, dashLen, gapLen, !!opts.planar);
37350
+ return function(feat) {
37351
+ var geom = feat.geometry;
37352
+ if (!geom) return feat;
37353
+ if (geom.type == 'LineString') {
37354
+ geom.type = 'MultiLineString';
37355
+ geom.coordinates = [geom.coordinates];
37356
+ }
37357
+ if (geom.type != 'MultiLineString') {
37358
+ error('Unexpected geometry:', geom.type);
37359
+ }
37360
+ geom.coordinates = geom.coordinates.reduce(function(memo, coords) {
37361
+ try {
37362
+ var parts = splitLine(coords);
37363
+ memo = memo.concat(parts);
37364
+ } catch(e) {
37365
+ console.error(e);
37366
+ throw e;
37367
+ }
37368
+ return memo;
37369
+ }, []);
37370
+
37371
+ return feat;
37372
+ };
37373
+ }
37374
+
37375
+ function getSplitLineFunction(crs, dashLen, gapLen, planar) {
37376
+ var interpolate = getInterpolationFunction(planar ? null : crs);
37377
+ var distance = isLatLngCRS(crs) ? greatCircleDistance : distance2D;
37378
+ var inDash, parts2, interval;
37379
+ function addPart(coords) {
37380
+ if (inDash) parts2.push(coords);
37381
+ if (gapLen > 0) {
37382
+ inDash = !inDash;
37383
+ interval = inDash ? dashLen : gapLen;
37384
+ }
37385
+ }
37386
+ return function splitLineString(coords) {
37387
+ var elapsedDist = 0;
37388
+ var p = coords[0];
37389
+ var coords2 = [p];
37390
+ var segLen, k, prev;
37391
+ // init this LineString
37392
+ inDash = true;
37393
+ parts2 = [];
37394
+ interval = gapLen;
37395
+ for (var i=1, n=coords.length; i<n; i++) {
37396
+ prev = p;
37397
+ p = coords[i];
37398
+ segLen = distance(prev[0], prev[1], p[0], p[1]);
37399
+ while (elapsedDist + segLen >= interval) {
37400
+ k = (interval - elapsedDist) / segLen;
37401
+ prev = interpolate(prev[0], prev[1], p[0], p[1], k);
37402
+ elapsedDist = 0;
37403
+ coords2.push(prev);
37404
+ addPart(coords2);
37405
+ coords2 = [prev];
37406
+ segLen = distance(prev[0], prev[1], p[0], p[1]);
37407
+ }
37408
+ coords2.push(p);
37409
+ elapsedDist += segLen;
37410
+ }
37411
+ if (elapsedDist > 0 && coords2.length > 1) {
37412
+ addPart(coords2);
37413
+ }
37414
+ return parts2;
37415
+ };
37416
+ }
37417
+
37270
37418
  cmd.svgStyle = function(lyr, dataset, opts) {
37271
37419
  var filter;
37272
37420
  if (!lyr.data) {
@@ -38228,6 +38376,9 @@ ${svg}
38228
38376
  } else if (name == 'split') {
38229
38377
  outputLayers = applyCommandToEachLayer(cmd.splitLayer, targetLayers, opts.expression, opts);
38230
38378
 
38379
+ } else if (name == 'split-lines') {
38380
+ applyCommandToEachLayer(cmd.splitLines, targetLayers, targetDataset, opts);
38381
+
38231
38382
  } else if (name == 'split-on-grid') {
38232
38383
  outputLayers = applyCommandToEachLayer(cmd.splitLayerOnGrid, targetLayers, arcs, opts);
38233
38384