mapshaper 0.5.73 → 0.5.77

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.5.72";
3
+ var VERSION = "0.5.76";
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
8140
8161
  }
8141
8162
 
8142
- // inc: Rounding incrememnt (e.g. 0.001 rounds to thousandths)
8163
+ function roundToTenths(n) {
8164
+ return (Math.round(n * 10)) / 10;
8165
+ }
8166
+
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
  }
@@ -18947,6 +18973,28 @@ ${svg}
18947
18973
  '$ mapshaper data.json -colorizer name=getColor nodata=#eee breaks=20,40 \\\n' +
18948
18974
  ' colors=#e0f3db,#a8ddb5,#43a2ca -each \'fill = getColor(RATING)\' -o output.json');
18949
18975
 
18976
+ parser.command('dashlines')
18977
+ .describe('split lines into sections, with or without a gap')
18978
+ .oldAlias('split-lines')
18979
+ .option('dash-length', {
18980
+ type: 'distance',
18981
+ describe: 'length of split-apart lines (e.g. 200km)'
18982
+ })
18983
+ .option('gap-length', {
18984
+ type: 'distance',
18985
+ describe: 'length of gaps between dashes (default is 0)'
18986
+ })
18987
+ .option('scaled', {
18988
+ type: 'flag',
18989
+ describe: 'scale dashes and gaps to prevent partial dashes'
18990
+ })
18991
+ .option('planar', {
18992
+ type: 'flag',
18993
+ describe: 'use planar geometry'
18994
+ })
18995
+ .option('where', whereOpt)
18996
+ .option('target', targetOpt);
18997
+
18950
18998
  parser.command('define')
18951
18999
  // .describe('define expression variables')
18952
19000
  .option('expression', {
@@ -26263,97 +26311,6 @@ ${svg}
26263
26311
  geom.segmentIntersection(a[0], a[1], b[0], b[1], bb.xmax, bb.ymin, bb.xmin, bb.ymin));
26264
26312
  }
26265
26313
 
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
26314
  function getPolylineBufferMaker2(arcs, geod, getBearing, opts) {
26358
26315
  var makeLeftBuffer = getPathBufferMaker2(arcs, geod, getBearing, opts);
26359
26316
  var geomType = opts.geometry_type;
@@ -26647,6 +26604,101 @@ ${svg}
26647
26604
  };
26648
26605
  }
26649
26606
 
26607
+ // GeographicLib docs: https://geographiclib.sourceforge.io/html/js/
26608
+ // https://geographiclib.sourceforge.io/html/js/module-GeographicLib_Geodesic.Geodesic.html
26609
+ // https://geographiclib.sourceforge.io/html/js/tutorial-2-interface.html
26610
+ function getGeodesic(P) {
26611
+ if (!isLatLngCRS(P)) error('Expected an unprojected CRS');
26612
+ var f = P.es / (1 + Math.sqrt(P.one_es));
26613
+ var GeographicLib = require('mproj').internal.GeographicLib;
26614
+ return new GeographicLib.Geodesic.Geodesic(P.a, f);
26615
+ }
26616
+
26617
+ function interpolatePoint2D(ax, ay, bx, by, k) {
26618
+ var j = 1 - k;
26619
+ return [ax * j + bx * k, ay * j + by * k];
26620
+ }
26621
+
26622
+ function getInterpolationFunction(P) {
26623
+ var spherical = P && isLatLngCRS(P);
26624
+ if (!spherical) return interpolatePoint2D;
26625
+ var geod = getGeodesic(P);
26626
+ return function(lng, lat, lng2, lat2, k) {
26627
+ var r = geod.Inverse(lat, lng, lat2, lng2);
26628
+ var dist = r.s12 * k;
26629
+ var r2 = geod.Direct(lat, lng, r.azi1, dist);
26630
+ return [r2.lon2, r2.lat2];
26631
+ };
26632
+ }
26633
+
26634
+ function getPlanarSegmentEndpoint(x, y, bearing, meterDist) {
26635
+ var rad = bearing / 180 * Math.PI;
26636
+ var dx = Math.sin(rad) * meterDist;
26637
+ var dy = Math.cos(rad) * meterDist;
26638
+ return [x + dx, y + dy];
26639
+ }
26640
+
26641
+ // source: https://github.com/mapbox/cheap-ruler/blob/master/index.js
26642
+ function fastGeodeticSegmentFunction(lng, lat, bearing, meterDist) {
26643
+ var D2R = Math.PI / 180;
26644
+ var cos = Math.cos(lat * D2R);
26645
+ var cos2 = 2 * cos * cos - 1;
26646
+ var cos3 = 2 * cos * cos2 - cos;
26647
+ var cos4 = 2 * cos * cos3 - cos2;
26648
+ var cos5 = 2 * cos * cos4 - cos3;
26649
+ var kx = (111.41513 * cos - 0.09455 * cos3 + 0.00012 * cos5) * 1000;
26650
+ var ky = (111.13209 - 0.56605 * cos2 + 0.0012 * cos4) * 1000;
26651
+ var bearingRad = bearing * D2R;
26652
+ var lat2 = lat + Math.cos(bearingRad) * meterDist / ky;
26653
+ var lng2 = lng + Math.sin(bearingRad) * meterDist / kx;
26654
+ return [lng2, lat2];
26655
+ }
26656
+
26657
+ function getGeodeticSegmentFunction(P) {
26658
+ if (!isLatLngCRS(P)) {
26659
+ return getPlanarSegmentEndpoint;
26660
+ }
26661
+ var g = getGeodesic(P);
26662
+ return function(lng, lat, bearing, meterDist) {
26663
+ var o = g.Direct(lat, lng, bearing, meterDist);
26664
+ var p = [o.lon2, o.lat2];
26665
+ return p;
26666
+ };
26667
+ }
26668
+
26669
+ function getFastGeodeticSegmentFunction(P) {
26670
+ // CAREFUL: this function has higher error at very large distances and at the poles
26671
+ // also, it wouldn't work for other planets than Earth
26672
+ return isLatLngCRS(P) ? fastGeodeticSegmentFunction : getPlanarSegmentEndpoint;
26673
+ }
26674
+
26675
+
26676
+ function bearingDegrees(a, b, c, d) {
26677
+ return geom.bearing(a, b, c, d) * 180 / Math.PI;
26678
+ }
26679
+
26680
+ function bearingDegrees2D(a, b, c, d) {
26681
+ return geom.bearing2D(a, b, c, d) * 180 / Math.PI;
26682
+ }
26683
+
26684
+ // return function to calculate bearing of a segment in degrees
26685
+ function getBearingFunction(dataset) {
26686
+ var P = getDatasetCRS(dataset);
26687
+ return isLatLngCRS(P) ? bearingDegrees : bearingDegrees2D;
26688
+ }
26689
+
26690
+ var Geodesic = /*#__PURE__*/Object.freeze({
26691
+ __proto__: null,
26692
+ interpolatePoint2D: interpolatePoint2D,
26693
+ getInterpolationFunction: getInterpolationFunction,
26694
+ getPlanarSegmentEndpoint: getPlanarSegmentEndpoint,
26695
+ getGeodeticSegmentFunction: getGeodeticSegmentFunction,
26696
+ getFastGeodeticSegmentFunction: getFastGeodeticSegmentFunction,
26697
+ bearingDegrees: bearingDegrees,
26698
+ bearingDegrees2D: bearingDegrees2D,
26699
+ getBearingFunction: getBearingFunction
26700
+ });
26701
+
26650
26702
  function makePolylineBuffer(lyr, dataset, opts) {
26651
26703
  var geojson = makeShapeBufferGeoJSON(lyr, dataset, opts);
26652
26704
  var dataset2 = importGeoJSON(geojson, {});
@@ -29682,6 +29734,178 @@ ${svg}
29682
29734
  getColorizerFunction: getColorizerFunction
29683
29735
  });
29684
29736
 
29737
+ function expressionUsesGeoJSON(exp) {
29738
+ return exp.includes('this.geojson');
29739
+ }
29740
+
29741
+ function getFeatureEditor(lyr, dataset) {
29742
+ var changed = false;
29743
+ var api = {};
29744
+ // need to copy attribute to avoid circular references if geojson is assigned
29745
+ // to a data property.
29746
+ var copy = copyLayer(lyr);
29747
+ var features = exportLayerAsGeoJSON(copy, dataset, {}, true);
29748
+
29749
+ api.get = function(i) {
29750
+ return features[i];
29751
+ };
29752
+
29753
+ api.set = function(feat, i) {
29754
+ changed = true;
29755
+ if (utils.isString(feat)) {
29756
+ feat = JSON.parse(feat);
29757
+ }
29758
+ features[i] = GeoJSON.toFeature(feat); // TODO: validate
29759
+ };
29760
+
29761
+ api.done = function() {
29762
+ if (!changed) return; // read-only expression
29763
+ // TODO: validate number of features, etc.
29764
+ var geojson = {
29765
+ type: 'FeatureCollection',
29766
+ features: features
29767
+ };
29768
+
29769
+ // console.log(JSON.stringify(geojson, null, 2))
29770
+ return importGeoJSON(geojson);
29771
+ };
29772
+ return api;
29773
+ }
29774
+
29775
+ cmd.dashlines = function(lyr, dataset, opts) {
29776
+ var crs = getDatasetCRS(dataset);
29777
+ var defs = getStateVar('defs');
29778
+ var exp = `this.geojson = splitFeature(this.geojson)`;
29779
+ requirePolylineLayer(lyr);
29780
+ defs.splitFeature = getSplitFeatureFunction(crs, opts);
29781
+ cmd.evaluateEachFeature(lyr, dataset, exp, opts);
29782
+ delete defs.splitFeature;
29783
+ };
29784
+
29785
+ function getSplitFeatureFunction(crs, opts) {
29786
+ var dashLen = opts.dash_length ? convertDistanceParam(opts.dash_length, crs) : 0;
29787
+ var gapLen = opts.gap_length ? convertDistanceParam(opts.gap_length, crs) : 0;
29788
+ if (dashLen > 0 === false) {
29789
+ stop('Missing required dash-length parameter');
29790
+ }
29791
+ if (gapLen >= 0 == false) {
29792
+ stop('Invalid gap-length option');
29793
+ }
29794
+ var splitLine = getSplitLineFunction(crs, dashLen, gapLen, opts);
29795
+ return function(feat) {
29796
+ var geom = feat.geometry;
29797
+ if (!geom) return feat;
29798
+ if (geom.type == 'LineString') {
29799
+ geom.type = 'MultiLineString';
29800
+ geom.coordinates = [geom.coordinates];
29801
+ }
29802
+ if (geom.type != 'MultiLineString') {
29803
+ error('Unexpected geometry:', geom.type);
29804
+ }
29805
+ geom.coordinates = geom.coordinates.reduce(function(memo, coords) {
29806
+ try {
29807
+ var parts = splitLine(coords);
29808
+ memo = memo.concat(parts);
29809
+ } catch(e) {
29810
+ console.error(e);
29811
+ throw e;
29812
+ }
29813
+ return memo;
29814
+ }, []);
29815
+
29816
+ return feat;
29817
+ };
29818
+ }
29819
+
29820
+ function getSplitLineFunction(crs, dashLen, gapLen, opts) {
29821
+ var planar = !!opts.planar;
29822
+ var interpolate = getInterpolationFunction(planar ? null : crs);
29823
+ var distance = isLatLngCRS(crs) ? greatCircleDistance : distance2D;
29824
+ var inDash, parts2, interval, scale;
29825
+ function addPart(coords) {
29826
+ if (inDash) parts2.push(coords);
29827
+ if (gapLen > 0) {
29828
+ inDash = !inDash;
29829
+ interval = scale * (inDash ? dashLen : gapLen);
29830
+ }
29831
+ }
29832
+
29833
+ return function splitLineString(coords) {
29834
+ var elapsedDist = 0;
29835
+ var p = coords[0];
29836
+ var coords2 = [p];
29837
+ var segLen, pct, prev;
29838
+ if (opts.scaled) {
29839
+ scale = scaleDashes(dashLen, gapLen, getLineLength(coords, distance));
29840
+ } else {
29841
+ scale = 1;
29842
+ }
29843
+ // init this LineString
29844
+ inDash = gapLen > 0 ? false : true;
29845
+ interval = scale * (inDash ? dashLen : gapLen);
29846
+ if (!inDash) {
29847
+ // start gapped lines with a half-gap
29848
+ // (a half-gap or a half-dash is probably better for rings and intersecting lines)
29849
+ interval *= 0.5;
29850
+ }
29851
+ parts2 = [];
29852
+ for (var i=1, n=coords.length; i<n; i++) {
29853
+ prev = p;
29854
+ p = coords[i];
29855
+ segLen = distance(prev[0], prev[1], p[0], p[1]);
29856
+ if (segLen <= 0) continue;
29857
+ while (elapsedDist + segLen >= interval) {
29858
+ // this segment contains a break either within it or at the far endpoint
29859
+ pct = (interval - elapsedDist) / segLen;
29860
+ if (pct > 0.999 && i == n - 1) {
29861
+ // snap to endpoint (so fp rounding errors don't result in a tiny
29862
+ // last segment)
29863
+ pct = 1;
29864
+ }
29865
+ if (pct < 1) {
29866
+ prev = interpolate(prev[0], prev[1], p[0], p[1], pct);
29867
+ } else {
29868
+ prev = p;
29869
+ }
29870
+ coords2.push(prev);
29871
+ addPart(coords2);
29872
+ // start a new part
29873
+ coords2 = pct < 1 ? [prev] : [];
29874
+ elapsedDist = 0;
29875
+ segLen = (1 - pct) * segLen;
29876
+ }
29877
+ coords2.push(p);
29878
+ elapsedDist += segLen;
29879
+ }
29880
+ if (elapsedDist > 0 && coords2.length > 1) {
29881
+ addPart(coords2);
29882
+ }
29883
+ return parts2;
29884
+ };
29885
+ }
29886
+
29887
+ function getLineLength(coords, distance) {
29888
+ var len = 0;
29889
+ for (var i=1, n=coords.length; i<n; i++) {
29890
+ len += distance(coords[i-1][0], coords[i-1][1], coords[i][0], coords[i][1]);
29891
+ }
29892
+ return len;
29893
+ }
29894
+
29895
+ function scaleDashes(dash, gap, len) {
29896
+ var dash2, gap2;
29897
+ var n = len / (dash + gap); // number of dashes
29898
+ var n1 = Math.floor(n);
29899
+ var n2 = Math.ceil(n);
29900
+ var k1 = len / (n1 * (dash + gap)); // scaled-up dashes, >1
29901
+ var k2 = len / (n2 * (dash + gap)); // scaled-down dashes <1
29902
+ var k = k2;
29903
+ if (k1 < 1/k2 && n1 > 0) {
29904
+ k = k1; // pick the smaller of the two scales
29905
+ }
29906
+ return k;
29907
+ }
29908
+
29685
29909
  // This function creates a continuous mosaic of data values in a
29686
29910
  // given field by assigning data from adjacent polygon features to polygons
29687
29911
  // that contain null values.
@@ -31206,43 +31430,6 @@ ${svg}
31206
31430
  }
31207
31431
  };
31208
31432
 
31209
- function expressionUsesGeoJSON(exp) {
31210
- return exp.includes('this.geojson');
31211
- }
31212
-
31213
- function getFeatureEditor(lyr, dataset) {
31214
- var changed = false;
31215
- var api = {};
31216
- // need to copy attribute to avoid circular references if geojson is assigned
31217
- // to a data property.
31218
- var copy = copyLayer(lyr);
31219
- var features = exportLayerAsGeoJSON(copy, dataset, {}, true);
31220
-
31221
- api.get = function(i) {
31222
- return features[i];
31223
- };
31224
-
31225
- api.set = function(feat, i) {
31226
- changed = true;
31227
- if (utils.isString(feat)) {
31228
- feat = JSON.parse(feat);
31229
- }
31230
- features[i] = GeoJSON.toFeature(feat); // TODO: validate
31231
- };
31232
-
31233
- api.done = function() {
31234
- if (!changed) return; // read-only expression
31235
- // TODO: validate number of features, etc.
31236
- var geojson = {
31237
- type: 'FeatureCollection',
31238
- features: features
31239
- };
31240
- // console.log(JSON.stringify(geojson, null, 2))
31241
- return importGeoJSON(geojson);
31242
- };
31243
- return api;
31244
- }
31245
-
31246
31433
  cmd.evaluateEachFeature = function(lyr, dataset, exp, opts) {
31247
31434
  var n = getFeatureCount(lyr),
31248
31435
  arcs = dataset.arcs,
@@ -31848,7 +32035,6 @@ ${svg}
31848
32035
  return joinTableToLayer(targetLyr, polygonLyr.data, joinFunction, opts);
31849
32036
  }
31850
32037
 
31851
-
31852
32038
  function prepJoinLayers(targetLyr, srcLyr) {
31853
32039
  if (!targetLyr.data) {
31854
32040
  // create an empty data table if target layer is missing attributes
@@ -34176,6 +34362,32 @@ ${svg}
34176
34362
  return d;
34177
34363
  }
34178
34364
 
34365
+ function findNearestVertices(p, shp, arcs) {
34366
+ var p2 = findNearestVertex(p[0], p[1], shp, arcs);
34367
+ return findVertexIds(p2.x, p2.y, arcs);
34368
+ }
34369
+
34370
+ // p: point to snap
34371
+ // ids: ids of nearby vertices, possibly including an arc endpoint
34372
+ function snapPointToArcEndpoint(p, ids, arcs) {
34373
+ var p2, p3, dx, dy;
34374
+ ids.forEach(function(idx) {
34375
+ if (vertexIsArcStart(idx, arcs)) {
34376
+ p2 = getVertexCoords(idx + 1, arcs);
34377
+ } else if (vertexIsArcEnd(idx, arcs)) {
34378
+ p2 = getVertexCoords(idx - 1, arcs);
34379
+ }
34380
+ });
34381
+ if (!p2) return;
34382
+ dx = p2[0] - p[0];
34383
+ dy = p2[1] - p[1];
34384
+ if (Math.abs(dx) > Math.abs(dy)) {
34385
+ p[1] = p2[1]; // snap y coord
34386
+ } else {
34387
+ p[0] = p2[0];
34388
+ }
34389
+ }
34390
+
34179
34391
  // Find ids of vertices with identical coordinates to x,y in an ArcCollection
34180
34392
  // Caveat: does not exclude vertices that are not visible at the
34181
34393
  // current level of simplification.
@@ -34238,6 +34450,18 @@ ${svg}
34238
34450
  return minLen < Infinity ? {x: minX, y: minY} : null;
34239
34451
  }
34240
34452
 
34453
+ var VertexUtils = /*#__PURE__*/Object.freeze({
34454
+ __proto__: null,
34455
+ findNearestVertices: findNearestVertices,
34456
+ snapPointToArcEndpoint: snapPointToArcEndpoint,
34457
+ findVertexIds: findVertexIds,
34458
+ getVertexCoords: getVertexCoords,
34459
+ vertexIsArcEnd: vertexIsArcEnd,
34460
+ vertexIsArcStart: vertexIsArcStart,
34461
+ setVertexCoords: setVertexCoords,
34462
+ findNearestVertex: findNearestVertex
34463
+ });
34464
+
34241
34465
  // Returns x,y coordinates of the point that is at the midpoint of each polyline feature
34242
34466
  // Uses 2d cartesian geometry
34243
34467
  // TODO: optionally use spherical geometry
@@ -34365,11 +34589,6 @@ ${svg}
34365
34589
  });
34366
34590
  }
34367
34591
 
34368
- function interpolatePoint2D(ax, ay, bx, by, k) {
34369
- var j = 1 - k;
34370
- return [ax * j + bx * k, ay * j + by * k];
34371
- }
34372
-
34373
34592
  function interpolatePointsAlongArc(ids, arcs, interval) {
34374
34593
  var iter = arcs.getShapeIter(ids);
34375
34594
  var distance = arcs.isPlanar() ? geom.distance2D : geom.greatCircleDistance;
@@ -34621,6 +34840,38 @@ ${svg}
34621
34840
  }
34622
34841
  }
34623
34842
 
34843
+ function pointsFromPolylinesForJoin(lyr, dataset) {
34844
+ var shapes = lyr.shapes.map(function(shp) {
34845
+ return polylineToMidpoints(shp, dataset.arcs);
34846
+ });
34847
+ return {
34848
+ geometry_type: 'point',
34849
+ shapes: shapes,
34850
+ data: lyr.data // TODO copy if needed
34851
+ };
34852
+ }
34853
+
34854
+ function validateOpts(opts) {
34855
+ if (!opts.point_method) {
34856
+ stop('The "point-method" flag is required for polyline-polygon joins');
34857
+ }
34858
+ }
34859
+
34860
+ function joinPolylinesToPolygons(targetLyr, targetDataset, source, opts) {
34861
+ validateOpts(opts);
34862
+ var pointLyr = pointsFromPolylinesForJoin(source.layer, source.dataset);
34863
+ var retn = joinPointsToPolygons(targetLyr, targetDataset.arcs, pointLyr, opts);
34864
+ return retn;
34865
+ }
34866
+
34867
+ function joinPolygonsToPolylines(targetLyr, targetDataset, source, opts) {
34868
+ validateOpts(opts);
34869
+ var pointLyr = pointsFromPolylinesForJoin(targetLyr, targetDataset);
34870
+ var retn = joinPolygonsToPoints(pointLyr, source.layer, source.dataset.arcs, opts);
34871
+ targetLyr.data = pointLyr.data;
34872
+ return retn;
34873
+ }
34874
+
34624
34875
  class TinyQueue {
34625
34876
  constructor(data = [], compare = defaultCompare) {
34626
34877
  this.data = data;
@@ -34944,7 +35195,7 @@ ${svg}
34944
35195
 
34945
35196
  cmd.join = function(targetLyr, targetDataset, src, opts) {
34946
35197
  var srcType, targetType, retn;
34947
- if (!src || !src.layer.data || !src.dataset) {
35198
+ if (!src || !src.dataset) {
34948
35199
  stop("Missing a joinable data source");
34949
35200
  }
34950
35201
  if (opts.keys) {
@@ -34952,9 +35203,18 @@ ${svg}
34952
35203
  if (opts.keys.length != 2) {
34953
35204
  stop("Expected two key fields: a target field and a source field");
34954
35205
  }
35206
+ if (!src.layer.data) {
35207
+ stop("Source layer is missing attribute data");
35208
+ }
34955
35209
  retn = joinAttributesToFeatures(targetLyr, src.layer.data, opts);
34956
35210
  } else {
34957
35211
  // spatial join
35212
+ if (!src.layer.data) {
35213
+ // KLUDGE -- users might want to join a layer without attributes
35214
+ // to test for intersection... the simplest way to support this is
35215
+ // to add an empty data table to the source layer
35216
+ initDataTable(src.layer);
35217
+ }
34958
35218
  requireDatasetsHaveCompatibleCRS([targetDataset, src.dataset]);
34959
35219
  srcType = src.layer.geometry_type;
34960
35220
  targetType = targetLyr.geometry_type;
@@ -34966,6 +35226,10 @@ ${svg}
34966
35226
  retn = joinPointsToPoints(targetLyr, src.layer, getDatasetCRS(targetDataset), opts);
34967
35227
  } else if (srcType == 'polygon' && targetType == 'polygon') {
34968
35228
  retn = joinPolygonsToPolygons(targetLyr, targetDataset, src, opts);
35229
+ } else if (srcType == 'polyline' && targetType == 'polygon') {
35230
+ retn = joinPolylinesToPolygons(targetLyr, targetDataset, src, opts);
35231
+ } else if (srcType == 'polygon' && targetType == 'polyline') {
35232
+ retn = joinPolygonsToPolylines(targetLyr, targetDataset, src, opts);
34969
35233
  } else {
34970
35234
  stop(utils.format("Unable to join %s geometry to %s geometry",
34971
35235
  srcType || 'null', targetType || 'null'));
@@ -37990,6 +38254,9 @@ ${svg}
37990
38254
  } else if (name == 'colorizer') {
37991
38255
  outputLayers = cmd.colorizer(opts);
37992
38256
 
38257
+ } else if (name == 'dashlines') {
38258
+ applyCommandToEachLayer(cmd.dashlines, targetLayers, targetDataset, opts);
38259
+
37993
38260
  } else if (name == 'define') {
37994
38261
  cmd.define(opts);
37995
38262
 
@@ -39097,7 +39364,8 @@ ${svg}
39097
39364
  TopojsonImport,
39098
39365
  Topology,
39099
39366
  Units,
39100
- SvgHatch
39367
+ SvgHatch,
39368
+ VertexUtils
39101
39369
  );
39102
39370
 
39103
39371
  // The entry point for the core mapshaper module
package/www/page.css CHANGED
@@ -32,7 +32,7 @@ html, body {
32
32
  body {
33
33
  overflow: hidden;
34
34
  background-color: #fff;
35
- font: 14px/1.4 'Source Sans Pro', Helvetica, sans-serif;
35
+ font: 14px/1.4 'Source Sans Pro', Arial, sans-serif;
36
36
  color: #444;
37
37
  user-select: none;
38
38
  -webkit-user-select: none;