mapshaper 0.5.76 → 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/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ v0.5.77
2
+ * Added -dashlines command (formerly -split-lines).
3
+ * Added support for joining polyline and polyline layers using point-method
4
+
1
5
  v0.5.76
2
6
  * Fixed bug in mapshaper-gui -q.
3
7
  * Added undocumented -split-lines command.
package/bin/mapshaper-gui CHANGED
@@ -83,7 +83,7 @@ function startServer(port) {
83
83
  }
84
84
  serveFile(getAssetFilePath(uri), response);
85
85
  }
86
- }).listen(port, function() {
86
+ }).listen(port, 'localhost', function() {
87
87
  opn("http://localhost:" + port);
88
88
  });
89
89
  }
package/mapshaper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.5.75";
3
+ var VERSION = "0.5.76";
4
4
 
5
5
 
6
6
  var utils = /*#__PURE__*/Object.freeze({
@@ -18973,6 +18973,28 @@ ${svg}
18973
18973
  '$ mapshaper data.json -colorizer name=getColor nodata=#eee breaks=20,40 \\\n' +
18974
18974
  ' colors=#e0f3db,#a8ddb5,#43a2ca -each \'fill = getColor(RATING)\' -o output.json');
18975
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
+
18976
18998
  parser.command('define')
18977
18999
  // .describe('define expression variables')
18978
19000
  .option('expression', {
@@ -19744,23 +19766,6 @@ ${svg}
19744
19766
  .option('target', targetOpt)
19745
19767
  .option('no-replace', noReplaceOpt);
19746
19768
 
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
-
19764
19769
  parser.command('split-on-grid')
19765
19770
  .describe('split features into separate layers using a grid')
19766
19771
  .validate(validateGridOpts)
@@ -29729,6 +29734,178 @@ ${svg}
29729
29734
  getColorizerFunction: getColorizerFunction
29730
29735
  });
29731
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
+
29732
29909
  // This function creates a continuous mosaic of data values in a
29733
29910
  // given field by assigning data from adjacent polygon features to polygons
29734
29911
  // that contain null values.
@@ -31253,44 +31430,6 @@ ${svg}
31253
31430
  }
31254
31431
  };
31255
31432
 
31256
- function expressionUsesGeoJSON(exp) {
31257
- return exp.includes('this.geojson');
31258
- }
31259
-
31260
- function getFeatureEditor(lyr, dataset) {
31261
- var changed = false;
31262
- var api = {};
31263
- // need to copy attribute to avoid circular references if geojson is assigned
31264
- // to a data property.
31265
- var copy = copyLayer(lyr);
31266
- var features = exportLayerAsGeoJSON(copy, dataset, {}, true);
31267
-
31268
- api.get = function(i) {
31269
- return features[i];
31270
- };
31271
-
31272
- api.set = function(feat, i) {
31273
- changed = true;
31274
- if (utils.isString(feat)) {
31275
- feat = JSON.parse(feat);
31276
- }
31277
- features[i] = GeoJSON.toFeature(feat); // TODO: validate
31278
- };
31279
-
31280
- api.done = function() {
31281
- if (!changed) return; // read-only expression
31282
- // TODO: validate number of features, etc.
31283
- var geojson = {
31284
- type: 'FeatureCollection',
31285
- features: features
31286
- };
31287
-
31288
- // console.log(JSON.stringify(geojson, null, 2))
31289
- return importGeoJSON(geojson);
31290
- };
31291
- return api;
31292
- }
31293
-
31294
31433
  cmd.evaluateEachFeature = function(lyr, dataset, exp, opts) {
31295
31434
  var n = getFeatureCount(lyr),
31296
31435
  arcs = dataset.arcs,
@@ -31896,7 +32035,6 @@ ${svg}
31896
32035
  return joinTableToLayer(targetLyr, polygonLyr.data, joinFunction, opts);
31897
32036
  }
31898
32037
 
31899
-
31900
32038
  function prepJoinLayers(targetLyr, srcLyr) {
31901
32039
  if (!targetLyr.data) {
31902
32040
  // create an empty data table if target layer is missing attributes
@@ -34702,6 +34840,38 @@ ${svg}
34702
34840
  }
34703
34841
  }
34704
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
+
34705
34875
  class TinyQueue {
34706
34876
  constructor(data = [], compare = defaultCompare) {
34707
34877
  this.data = data;
@@ -35025,7 +35195,7 @@ ${svg}
35025
35195
 
35026
35196
  cmd.join = function(targetLyr, targetDataset, src, opts) {
35027
35197
  var srcType, targetType, retn;
35028
- if (!src || !src.layer.data || !src.dataset) {
35198
+ if (!src || !src.dataset) {
35029
35199
  stop("Missing a joinable data source");
35030
35200
  }
35031
35201
  if (opts.keys) {
@@ -35033,9 +35203,18 @@ ${svg}
35033
35203
  if (opts.keys.length != 2) {
35034
35204
  stop("Expected two key fields: a target field and a source field");
35035
35205
  }
35206
+ if (!src.layer.data) {
35207
+ stop("Source layer is missing attribute data");
35208
+ }
35036
35209
  retn = joinAttributesToFeatures(targetLyr, src.layer.data, opts);
35037
35210
  } else {
35038
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
+ }
35039
35218
  requireDatasetsHaveCompatibleCRS([targetDataset, src.dataset]);
35040
35219
  srcType = src.layer.geometry_type;
35041
35220
  targetType = targetLyr.geometry_type;
@@ -35047,6 +35226,10 @@ ${svg}
35047
35226
  retn = joinPointsToPoints(targetLyr, src.layer, getDatasetCRS(targetDataset), opts);
35048
35227
  } else if (srcType == 'polygon' && targetType == 'polygon') {
35049
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);
35050
35233
  } else {
35051
35234
  stop(utils.format("Unable to join %s geometry to %s geometry",
35052
35235
  srcType || 'null', targetType || 'null'));
@@ -37310,111 +37493,6 @@ ${svg}
37310
37493
  getSplitNameFunction: getSplitNameFunction
37311
37494
  });
37312
37495
 
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
-
37418
37496
  cmd.svgStyle = function(lyr, dataset, opts) {
37419
37497
  var filter;
37420
37498
  if (!lyr.data) {
@@ -38176,6 +38254,9 @@ ${svg}
38176
38254
  } else if (name == 'colorizer') {
38177
38255
  outputLayers = cmd.colorizer(opts);
38178
38256
 
38257
+ } else if (name == 'dashlines') {
38258
+ applyCommandToEachLayer(cmd.dashlines, targetLayers, targetDataset, opts);
38259
+
38179
38260
  } else if (name == 'define') {
38180
38261
  cmd.define(opts);
38181
38262
 
@@ -38376,9 +38457,6 @@ ${svg}
38376
38457
  } else if (name == 'split') {
38377
38458
  outputLayers = applyCommandToEachLayer(cmd.splitLayer, targetLayers, opts.expression, opts);
38378
38459
 
38379
- } else if (name == 'split-lines') {
38380
- applyCommandToEachLayer(cmd.splitLines, targetLayers, targetDataset, opts);
38381
-
38382
38460
  } else if (name == 'split-on-grid') {
38383
38461
  outputLayers = applyCommandToEachLayer(cmd.splitLayerOnGrid, targetLayers, arcs, opts);
38384
38462
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mapshaper",
3
- "version": "0.5.76",
3
+ "version": "0.5.77",
4
4
  "description": "A tool for editing vector datasets for mapping and GIS.",
5
5
  "keywords": [
6
6
  "shapefile",
@@ -2292,6 +2292,7 @@
2292
2292
  function onPaste(e) {
2293
2293
  // paste plain text (remove any copied HTML tags)
2294
2294
  e.preventDefault();
2295
+ e.stopPropagation(); // don't try to import pasted text as data (see gui-import-control.js)
2295
2296
  var str = (e.originalEvent || e).clipboardData.getData('text/plain');
2296
2297
  document.execCommand("insertHTML", false, str);
2297
2298
  }
package/www/mapshaper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.5.75";
3
+ var VERSION = "0.5.76";
4
4
 
5
5
 
6
6
  var utils = /*#__PURE__*/Object.freeze({
@@ -18973,6 +18973,28 @@ ${svg}
18973
18973
  '$ mapshaper data.json -colorizer name=getColor nodata=#eee breaks=20,40 \\\n' +
18974
18974
  ' colors=#e0f3db,#a8ddb5,#43a2ca -each \'fill = getColor(RATING)\' -o output.json');
18975
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
+
18976
18998
  parser.command('define')
18977
18999
  // .describe('define expression variables')
18978
19000
  .option('expression', {
@@ -19744,23 +19766,6 @@ ${svg}
19744
19766
  .option('target', targetOpt)
19745
19767
  .option('no-replace', noReplaceOpt);
19746
19768
 
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
-
19764
19769
  parser.command('split-on-grid')
19765
19770
  .describe('split features into separate layers using a grid')
19766
19771
  .validate(validateGridOpts)
@@ -29729,6 +29734,178 @@ ${svg}
29729
29734
  getColorizerFunction: getColorizerFunction
29730
29735
  });
29731
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
+
29732
29909
  // This function creates a continuous mosaic of data values in a
29733
29910
  // given field by assigning data from adjacent polygon features to polygons
29734
29911
  // that contain null values.
@@ -31253,44 +31430,6 @@ ${svg}
31253
31430
  }
31254
31431
  };
31255
31432
 
31256
- function expressionUsesGeoJSON(exp) {
31257
- return exp.includes('this.geojson');
31258
- }
31259
-
31260
- function getFeatureEditor(lyr, dataset) {
31261
- var changed = false;
31262
- var api = {};
31263
- // need to copy attribute to avoid circular references if geojson is assigned
31264
- // to a data property.
31265
- var copy = copyLayer(lyr);
31266
- var features = exportLayerAsGeoJSON(copy, dataset, {}, true);
31267
-
31268
- api.get = function(i) {
31269
- return features[i];
31270
- };
31271
-
31272
- api.set = function(feat, i) {
31273
- changed = true;
31274
- if (utils.isString(feat)) {
31275
- feat = JSON.parse(feat);
31276
- }
31277
- features[i] = GeoJSON.toFeature(feat); // TODO: validate
31278
- };
31279
-
31280
- api.done = function() {
31281
- if (!changed) return; // read-only expression
31282
- // TODO: validate number of features, etc.
31283
- var geojson = {
31284
- type: 'FeatureCollection',
31285
- features: features
31286
- };
31287
-
31288
- // console.log(JSON.stringify(geojson, null, 2))
31289
- return importGeoJSON(geojson);
31290
- };
31291
- return api;
31292
- }
31293
-
31294
31433
  cmd.evaluateEachFeature = function(lyr, dataset, exp, opts) {
31295
31434
  var n = getFeatureCount(lyr),
31296
31435
  arcs = dataset.arcs,
@@ -31896,7 +32035,6 @@ ${svg}
31896
32035
  return joinTableToLayer(targetLyr, polygonLyr.data, joinFunction, opts);
31897
32036
  }
31898
32037
 
31899
-
31900
32038
  function prepJoinLayers(targetLyr, srcLyr) {
31901
32039
  if (!targetLyr.data) {
31902
32040
  // create an empty data table if target layer is missing attributes
@@ -34702,6 +34840,38 @@ ${svg}
34702
34840
  }
34703
34841
  }
34704
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
+
34705
34875
  class TinyQueue {
34706
34876
  constructor(data = [], compare = defaultCompare) {
34707
34877
  this.data = data;
@@ -35025,7 +35195,7 @@ ${svg}
35025
35195
 
35026
35196
  cmd.join = function(targetLyr, targetDataset, src, opts) {
35027
35197
  var srcType, targetType, retn;
35028
- if (!src || !src.layer.data || !src.dataset) {
35198
+ if (!src || !src.dataset) {
35029
35199
  stop("Missing a joinable data source");
35030
35200
  }
35031
35201
  if (opts.keys) {
@@ -35033,9 +35203,18 @@ ${svg}
35033
35203
  if (opts.keys.length != 2) {
35034
35204
  stop("Expected two key fields: a target field and a source field");
35035
35205
  }
35206
+ if (!src.layer.data) {
35207
+ stop("Source layer is missing attribute data");
35208
+ }
35036
35209
  retn = joinAttributesToFeatures(targetLyr, src.layer.data, opts);
35037
35210
  } else {
35038
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
+ }
35039
35218
  requireDatasetsHaveCompatibleCRS([targetDataset, src.dataset]);
35040
35219
  srcType = src.layer.geometry_type;
35041
35220
  targetType = targetLyr.geometry_type;
@@ -35047,6 +35226,10 @@ ${svg}
35047
35226
  retn = joinPointsToPoints(targetLyr, src.layer, getDatasetCRS(targetDataset), opts);
35048
35227
  } else if (srcType == 'polygon' && targetType == 'polygon') {
35049
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);
35050
35233
  } else {
35051
35234
  stop(utils.format("Unable to join %s geometry to %s geometry",
35052
35235
  srcType || 'null', targetType || 'null'));
@@ -37310,111 +37493,6 @@ ${svg}
37310
37493
  getSplitNameFunction: getSplitNameFunction
37311
37494
  });
37312
37495
 
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
-
37418
37496
  cmd.svgStyle = function(lyr, dataset, opts) {
37419
37497
  var filter;
37420
37498
  if (!lyr.data) {
@@ -38176,6 +38254,9 @@ ${svg}
38176
38254
  } else if (name == 'colorizer') {
38177
38255
  outputLayers = cmd.colorizer(opts);
38178
38256
 
38257
+ } else if (name == 'dashlines') {
38258
+ applyCommandToEachLayer(cmd.dashlines, targetLayers, targetDataset, opts);
38259
+
38179
38260
  } else if (name == 'define') {
38180
38261
  cmd.define(opts);
38181
38262
 
@@ -38376,9 +38457,6 @@ ${svg}
38376
38457
  } else if (name == 'split') {
38377
38458
  outputLayers = applyCommandToEachLayer(cmd.splitLayer, targetLayers, opts.expression, opts);
38378
38459
 
38379
- } else if (name == 'split-lines') {
38380
- applyCommandToEachLayer(cmd.splitLines, targetLayers, targetDataset, opts);
38381
-
38382
38460
  } else if (name == 'split-on-grid') {
38383
38461
  outputLayers = applyCommandToEachLayer(cmd.splitLayerOnGrid, targetLayers, arcs, opts);
38384
38462