mapshaper 0.5.76 → 0.5.80

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.75";
3
+ var VERSION = "0.5.79";
4
4
 
5
5
 
6
6
  var utils = /*#__PURE__*/Object.freeze({
@@ -52,6 +52,7 @@
52
52
  get initializeArray () { return initializeArray; },
53
53
  get replaceArray () { return replaceArray; },
54
54
  get repeatString () { return repeatString; },
55
+ get splitLines () { return splitLines; },
55
56
  get pluralSuffix () { return pluralSuffix; },
56
57
  get endsWith () { return endsWith; },
57
58
  get lpad () { return lpad; },
@@ -61,7 +62,9 @@
61
62
  get rtrim () { return rtrim; },
62
63
  get addThousandsSep () { return addThousandsSep; },
63
64
  get numToStr () { return numToStr; },
64
- get formatNumber () { return formatNumber$2; },
65
+ get formatNumber () { return formatNumber; },
66
+ get formatIntlNumber () { return formatIntlNumber; },
67
+ get formatNumberForDisplay () { return formatNumberForDisplay; },
65
68
  get shuffle () { return shuffle; },
66
69
  get sortOn () { return sortOn; },
67
70
  get genericSort () { return genericSort; },
@@ -532,6 +535,10 @@
532
535
  return str;
533
536
  }
534
537
 
538
+ function splitLines(str) {
539
+ return str.split(/\r?\n/);
540
+ }
541
+
535
542
  function pluralSuffix(count) {
536
543
  return count != 1 ? 's' : '';
537
544
  }
@@ -584,7 +591,16 @@
584
591
  return decimals >= 0 ? num.toFixed(decimals) : String(num);
585
592
  }
586
593
 
587
- function formatNumber$2(num, decimals, nullStr, showPos) {
594
+ function formatNumber(val) {
595
+ return val + '';
596
+ }
597
+
598
+ function formatIntlNumber(val) {
599
+ var str = formatNumber(val);
600
+ return '"' + str.replace('.', ',') + '"'; // need to quote if comma-delimited
601
+ }
602
+
603
+ function formatNumberForDisplay(num, decimals, nullStr, showPos) {
588
604
  var fmt;
589
605
  if (isNaN(num)) {
590
606
  fmt = nullStr || '-';
@@ -862,7 +878,8 @@
862
878
  str = str.toUpperCase();
863
879
  }
864
880
  else if (isNumber) {
865
- str = numToStr(val, isInt ? 0 : decimals);
881
+ // str = formatNumberForDisplay(val, isInt ? 0 : decimals);
882
+ str = numToStr(val, decimals);
866
883
  if (str[0] == '-') {
867
884
  isNeg = true;
868
885
  str = str.substr(1);
@@ -1313,9 +1330,10 @@
1313
1330
  }
1314
1331
 
1315
1332
  function logArgs(args) {
1316
- if (LOGGING && !getStateVar('QUIET') && utils.isArrayLike(args)) {
1317
- (!STDOUT && console.error || console.log).call(console, formatLogArgs(args));
1318
- }
1333
+ if (!LOGGING || getStateVar('QUIET') || !utils.isArrayLike(args)) return;
1334
+ var msg = formatLogArgs(args);
1335
+ if (STDOUT) console.log(msg);
1336
+ else console.error(msg);
1319
1337
  }
1320
1338
 
1321
1339
  var Logging = /*#__PURE__*/Object.freeze({
@@ -1727,9 +1745,17 @@
1727
1745
  return [xmin, ymin, xmax, ymax];
1728
1746
  }
1729
1747
 
1730
- // TODO: remove this constant, use actual data from dataset CRS
1731
- // also consider using ellipsoidal formulas when appropriate
1732
- var R$1 = 6378137;
1748
+ var WGS84 = {
1749
+ // https://en.wikipedia.org/wiki/Earth_radius
1750
+ SEMIMAJOR_AXIS: 6378137,
1751
+ SEMIMINOR_AXIS: 6356752.3142,
1752
+ AUTHALIC_RADIUS: 6371007.2,
1753
+ VOLUMETRIC_RADIUS: 6371000.8
1754
+ };
1755
+
1756
+ // TODO: remove this constant, use actual data from dataset CRS,
1757
+ // also consider using ellipsoidal formulas where greater accuracy might be important.
1758
+ var R$1 = WGS84.SEMIMAJOR_AXIS;
1733
1759
  var D2R = Math.PI / 180;
1734
1760
  var R2D = 180 / Math.PI;
1735
1761
 
@@ -2324,12 +2350,12 @@
2324
2350
  }, 0);
2325
2351
  }
2326
2352
 
2327
- function getSphericalShapeArea(shp, arcs) {
2353
+ function getSphericalShapeArea(shp, arcs, R) {
2328
2354
  if (arcs.isPlanar()) {
2329
2355
  error("[getSphericalShapeArea()] Function requires decimal degree coordinates");
2330
2356
  }
2331
2357
  return (shp || []).reduce(function(area, ids) {
2332
- return area + getSphericalPathArea(ids, arcs);
2358
+ return area + getSphericalPathArea(ids, arcs, R);
2333
2359
  }, 0);
2334
2360
  }
2335
2361
 
@@ -2446,16 +2472,17 @@
2446
2472
  return (arcs.isPlanar() ? getPlanarPathArea : getSphericalPathArea)(ids, arcs);
2447
2473
  }
2448
2474
 
2449
- function getSphericalPathArea(ids, arcs) {
2475
+ function getSphericalPathArea(ids, arcs, R) {
2450
2476
  var iter = arcs.getShapeIter(ids);
2451
- return getSphericalPathArea2(iter);
2477
+ return getSphericalPathArea2(iter, R);
2452
2478
  }
2453
2479
 
2454
- function getSphericalPathArea2(iter) {
2480
+ function getSphericalPathArea2(iter, R) {
2455
2481
  var sum = 0,
2456
2482
  started = false,
2457
2483
  deg2rad = Math.PI / 180,
2458
2484
  x, y, xp, yp;
2485
+ R = R || WGS84.SEMIMAJOR_AXIS;
2459
2486
  while (iter.hasNext()) {
2460
2487
  x = iter.x * deg2rad;
2461
2488
  y = Math.sin(iter.y * deg2rad);
@@ -2467,7 +2494,7 @@
2467
2494
  xp = x;
2468
2495
  yp = y;
2469
2496
  }
2470
- return sum / 2 * 6378137 * 6378137;
2497
+ return sum / 2 * R * R;
2471
2498
  }
2472
2499
 
2473
2500
  // Get path area from an array of [x, y] points
@@ -3931,7 +3958,7 @@
3931
3958
  function requireSinglePointLayer(lyr, msg) {
3932
3959
  requirePointLayer(lyr);
3933
3960
  if (countMultiPartFeatures(lyr) > 0) {
3934
- stop(msg || 'This command requires single points');
3961
+ stop(msg || 'This command requires single points; layer contains multi-point features.');
3935
3962
  }
3936
3963
  }
3937
3964
 
@@ -3986,6 +4013,7 @@
3986
4013
  return opts && opts.no_replace ? {geometry_type: src.geometry_type} : src;
3987
4014
  }
3988
4015
 
4016
+ //
3989
4017
  function setOutputLayerName(dest, src, defName, opts) {
3990
4018
  opts = opts || {};
3991
4019
  if (opts.name) {
@@ -4062,6 +4090,7 @@
4062
4090
  return counts;
4063
4091
  }
4064
4092
 
4093
+ // Returns a Bounds object
4065
4094
  function getLayerBounds(lyr, arcs) {
4066
4095
  var bounds = null;
4067
4096
  if (lyr.geometry_type == 'point') {
@@ -6133,9 +6162,11 @@
6133
6162
  arcCount += n;
6134
6163
  });
6135
6164
 
6136
- mergedArcs = mergeArcs(arcSources);
6137
- if (mergedArcs.size() != arcCount) {
6138
- error("[mergeDatasets()] Arc indexing error");
6165
+ if (arcSources.length > 0) {
6166
+ mergedArcs = mergeArcs(arcSources);
6167
+ if (mergedArcs.size() != arcCount) {
6168
+ error("[mergeDatasets()] Arc indexing error");
6169
+ }
6139
6170
  }
6140
6171
 
6141
6172
  return {
@@ -6154,6 +6185,8 @@
6154
6185
  }
6155
6186
 
6156
6187
  function mergeArcs(arr) {
6188
+ // Returning the original causes a test to fail
6189
+ // if (arr.length < 2) return arr[0];
6157
6190
  var dataArr = arr.map(function(arcs) {
6158
6191
  if (arcs.getRetainedInterval() > 0) {
6159
6192
  verbose("Baking-in simplification setting.");
@@ -6758,31 +6791,47 @@
6758
6791
  dataset.layers = currLayers;
6759
6792
  }
6760
6793
 
6761
- // Replace a layer in-place with a layer from a second dataset
6794
+ // Replace a layer with a layer from a second dataset
6795
+ // (in-place)
6762
6796
  // (Typically, the second dataset is imported from dynamically generated GeoJSON and contains one layer)
6763
6797
  function replaceLayerContents(lyr, dataset, dataset2) {
6798
+ var lyr2 = mergeOutputLayerIntoDataset(lyr, dataset, dataset2, {});
6799
+ if (layerHasPaths(lyr2)) {
6800
+ buildTopology(dataset);
6801
+ }
6802
+ }
6803
+
6804
+ function mergeOutputLayerIntoDataset(lyr, dataset, dataset2, opts) {
6764
6805
  if (!dataset2 || dataset2.layers.length != 1) {
6765
- error('Invalid replacement layer');
6806
+ error('Invalid source dataset');
6766
6807
  }
6767
6808
  if (dataset.layers.includes(lyr) === false) {
6768
6809
  error('Invalid target layer');
6769
6810
  }
6770
- var lyr2 = dataset2.layers[0];
6771
- if (dataset2.arcs) {
6772
- // this command returns merged layers instead of adding them to target dataset
6773
- mergeDatasetsIntoDataset(dataset, [dataset2]);
6774
- }
6811
+ // this command returns merged layers instead of adding them to target dataset
6812
+ var outputLayers = mergeDatasetsIntoDataset(dataset, [dataset2]);
6813
+ var lyr2 = outputLayers[0];
6775
6814
 
6776
- Object.assign(lyr, {data: null, shapes: null}, lyr2);
6815
+ // TODO: find a more reliable way of knowing when to copy data
6816
+ var copyData = !lyr2.data && lyr.data && getFeatureCount(lyr2) == lyr.data.size();
6777
6817
 
6778
- if (layerHasPaths(lyr2)) {
6779
- buildTopology(dataset);
6818
+ if (copyData) {
6819
+ lyr2.data = opts.no_replace ? lyr.data.clone() : lyr.data;
6780
6820
  }
6781
- if (layerHasPaths(lyr)) {
6782
- // Remove unused arcs from replaced layer
6783
- // TODO: consider using clean insead of this
6784
- dissolveArcs(dataset);
6821
+ if (opts.no_replace) {
6822
+ // dataset.layers.push(lyr2);
6823
+
6824
+ } else {
6825
+ lyr2 = Object.assign(lyr, {data: null, shapes: null}, lyr2);
6826
+ if (layerHasPaths(lyr)) {
6827
+ // Remove unused arcs from replaced layer
6828
+ // TODO: consider using clean insead of this
6829
+ dissolveArcs(dataset);
6830
+ }
6785
6831
  }
6832
+
6833
+ lyr2.name = opts.name || lyr2.name;
6834
+ return lyr2;
6786
6835
  }
6787
6836
 
6788
6837
  // Transform the points in a dataset in-place; don't clean up corrupted shapes
@@ -6811,6 +6860,7 @@
6811
6860
  pruneArcs: pruneArcs,
6812
6861
  replaceLayers: replaceLayers,
6813
6862
  replaceLayerContents: replaceLayerContents,
6863
+ mergeOutputLayerIntoDataset: mergeOutputLayerIntoDataset,
6814
6864
  transformPoints: transformPoints
6815
6865
  });
6816
6866
 
@@ -9394,6 +9444,12 @@
9394
9444
  area: function() {
9395
9445
  return _isPlanar ? ctx.planarArea : geom.getSphericalShapeArea(_ids, arcs);
9396
9446
  },
9447
+ // area2: function() {
9448
+ // return _isPlanar ? ctx.planarArea : geom.getSphericalShapeArea(_ids, arcs, WGS84.SEMIMINOR_RADIUS);
9449
+ // },
9450
+ // area3: function() {
9451
+ // return _isPlanar ? ctx.planarArea : geom.getSphericalShapeArea(_ids, arcs, WGS84.AUTHALIC_RADIUS);
9452
+ // },
9397
9453
  perimeter: function() {
9398
9454
  return geom.getShapePerimeter(_ids, arcs);
9399
9455
  },
@@ -10296,6 +10352,12 @@
10296
10352
  };
10297
10353
  }
10298
10354
 
10355
+ /*
10356
+
10357
+
10358
+
10359
+ */
10360
+
10299
10361
  // TODO: use polygon pathfinder shared code
10300
10362
  function collectPolylineArcs(ids, nodes, testArc, useArc) {
10301
10363
  var parts = [];
@@ -11865,7 +11927,6 @@
11865
11927
  });
11866
11928
 
11867
11929
  // Map positive or negative integer ids to non-negative integer ids
11868
-
11869
11930
  function IdLookupIndex(n, clearable) {
11870
11931
  var fwdIndex = new Int32Array(n);
11871
11932
  var revIndex = new Int32Array(n);
@@ -13309,14 +13370,14 @@
13309
13370
  return mergeArcs([arcs, new ArcCollection(nn, xx, yy)]);
13310
13371
  }
13311
13372
 
13312
- var roundCoord$1 = getRoundingFunction(0.01);
13373
+ var roundCoord$2 = getRoundingFunction(0.01);
13313
13374
 
13314
13375
  function stringifyVertex(p) {
13315
- return ' ' + roundCoord$1(p[0]) + ' ' + roundCoord$1(p[1]);
13376
+ return ' ' + roundCoord$2(p[0]) + ' ' + roundCoord$2(p[1]);
13316
13377
  }
13317
13378
 
13318
13379
  function stringifyCP(p) {
13319
- return ' ' + roundCoord$1(p[2]) + ' ' + roundCoord$1(p[3]);
13380
+ return ' ' + roundCoord$2(p[2]) + ' ' + roundCoord$2(p[3]);
13320
13381
  }
13321
13382
 
13322
13383
  function isCubicCtrl(p) {
@@ -13351,40 +13412,9 @@
13351
13412
  stringifyCP(p2) + stringifyVertex(p2);
13352
13413
  }
13353
13414
 
13354
- function findArcCenter(p1, p2, degrees) {
13355
- var p3 = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2], // midpoint betw. p1, p2
13356
- tan = 1 / Math.tan(degrees / 180 * Math.PI / 2),
13357
- cp = getAffineTransform(90, tan, [0, 0], p3)(p2[0], p2[1]);
13358
- return cp;
13359
- }
13360
-
13361
- // export function addBezierArcControlPoints(p1, p2, degrees) {
13362
- function addBezierArcControlPoints(points, degrees) {
13363
- // source: https://stackoverflow.com/questions/734076/how-to-best-approximate-a-geometrical-arc-with-a-bezier-curve
13364
- var p2 = points.pop(),
13365
- p1 = points.pop(),
13366
- cp = findArcCenter(p1, p2, degrees),
13367
- xc = cp[0],
13368
- yc = cp[1],
13369
- ax = p1[0] - xc,
13370
- ay = p1[1] - yc,
13371
- bx = p2[0] - xc,
13372
- by = p2[1] - yc,
13373
- q1 = ax * ax + ay * ay,
13374
- q2 = q1 + ax * bx + ay * by,
13375
- k2 = 4/3 * (Math.sqrt(2 * q1 * q2) - q2) / (ax * by - ay * bx);
13376
-
13377
- points.push(p1);
13378
- points.push([xc + ax - k2 * ay, yc + ay + k2 * ax, 'C']);
13379
- points.push([xc + bx + k2 * by, yc + by - k2 * bx, 'C']);
13380
- points.push(p2);
13381
- }
13382
-
13383
13415
  var SvgPathUtils = /*#__PURE__*/Object.freeze({
13384
13416
  __proto__: null,
13385
- stringifyLineStringCoords: stringifyLineStringCoords,
13386
- findArcCenter: findArcCenter,
13387
- addBezierArcControlPoints: addBezierArcControlPoints
13417
+ stringifyLineStringCoords: stringifyLineStringCoords
13388
13418
  });
13389
13419
 
13390
13420
  /* example patterns
@@ -13661,11 +13691,15 @@
13661
13691
  type: null,
13662
13692
  length: 'number', // e.g. arrow length
13663
13693
  rotation: 'number',
13694
+ radius: 'number',
13695
+ 'arrow-length': 'number',
13696
+ 'arrow-direction': 'number',
13664
13697
  'arrow-head-angle': 'number',
13665
13698
  'arrow-head-width': 'number',
13666
13699
  'arrow-stem-width': 'number',
13667
13700
  'arrow-stem-curve': 'number', // degrees of arc
13668
13701
  'arrow-stem-taper': 'number',
13702
+ 'arrow-min-stem': 'number',
13669
13703
  'arrow-scaling': 'number',
13670
13704
  effect: null // e.g. "fade"
13671
13705
  }, stylePropertyTypes);
@@ -13707,8 +13741,8 @@
13707
13741
  if (!isSupportedSvgSymbolProperty(svgName)) {
13708
13742
  return;
13709
13743
  }
13710
- var strVal = opts[optName].trim();
13711
- functions[svgName] = getSymbolPropertyAccessor(strVal, svgName, lyr);
13744
+ var val = opts[optName];
13745
+ functions[svgName] = getSymbolPropertyAccessor(val, svgName, lyr);
13712
13746
  properties.push(svgName);
13713
13747
  });
13714
13748
 
@@ -13731,7 +13765,8 @@
13731
13765
  return /[(){}.+-/*?:&|=\[]/.test(str);
13732
13766
  }
13733
13767
 
13734
- function getSymbolPropertyAccessor(strVal, svgName, lyr) {
13768
+ function getSymbolPropertyAccessor(val, svgName, lyr) {
13769
+ var strVal = String(val).trim();
13735
13770
  var typeHint = symbolPropertyTypes[svgName];
13736
13771
  var fields = lyr.data ? lyr.data.getFields() : [];
13737
13772
  var literalVal = null;
@@ -13833,7 +13868,6 @@
13833
13868
  isSvgColor: isSvgColor
13834
13869
  });
13835
13870
 
13836
- var symbolBuilders = {};
13837
13871
  var symbolRenderers = {};
13838
13872
 
13839
13873
  function getTransform(xy, scale) {
@@ -13846,7 +13880,6 @@
13846
13880
 
13847
13881
  var SvgCommon = /*#__PURE__*/Object.freeze({
13848
13882
  __proto__: null,
13849
- symbolBuilders: symbolBuilders,
13850
13883
  symbolRenderers: symbolRenderers,
13851
13884
  getTransform: getTransform
13852
13885
  });
@@ -15565,6 +15598,139 @@ ${svg}
15565
15598
  }];
15566
15599
  }
15567
15600
 
15601
+ function exportRecordsAsFixedWidthString(fields, records, opts) {
15602
+ var rows = [], col;
15603
+ for (var i=0; i<fields.length; i++) {
15604
+ col = formatFixedWidthColumn(fields[i], records, opts);
15605
+ if (i === 0) {
15606
+ rows = col;
15607
+ } else for (var j=0; j<rows.length; j++) {
15608
+ rows[j] += ' ' + col[j];
15609
+ }
15610
+ }
15611
+ return rows.join('\n');
15612
+ }
15613
+
15614
+ function formatFixedWidthColumn(field, records, opts) {
15615
+ var arr = [],
15616
+ maxLen = field.length,
15617
+ n = records.length,
15618
+ i, val;
15619
+ arr.push(field);
15620
+ for (i=0; i<n; i++) {
15621
+ val = formatFixedWidthValue(records[i][field], opts);
15622
+ maxLen = Math.max(maxLen, val.length);
15623
+ arr.push(val);
15624
+ }
15625
+ for (i=0; i<arr.length; i++) {
15626
+ arr[i] = arr[i].padEnd(maxLen, ' ');
15627
+ }
15628
+ return arr;
15629
+ }
15630
+
15631
+ function formatFixedWidthValue(val, opts) {
15632
+ // TODO: remove duplication with mapshaper-delim-export.js
15633
+ var s;
15634
+ if (val == null) {
15635
+ s = '';
15636
+ } else if (utils.isString(val)) {
15637
+ s = val; // TODO: handle wide characters, newlines etc.
15638
+ } else if (utils.isNumber(val)) {
15639
+ s = opts.decimal_comma ? utils.formatIntlNumber(val) : utils.formatNumber(val);
15640
+ } else if (utils.isObject(val)) {
15641
+ s = JSON.stringify(val);
15642
+ } else {
15643
+ s = val + '';
15644
+ }
15645
+ return s;
15646
+ }
15647
+
15648
+
15649
+ function readFixedWidthRecords(reader, opts) {
15650
+ var str = reader.toString(opts.encoding || 'ascii');
15651
+ return readFixedWidthRecordsFromString(str, opts);
15652
+ }
15653
+
15654
+ function readFixedWidthRecordsFromString(str, ops) {
15655
+ var fields = parseFixedWidthInfo(str.substring(0, 2000));
15656
+ if (!fields) return [];
15657
+ var lines = utils.splitLines(str);
15658
+ if (lines[lines.length - 1] === '') lines.pop(); // handle newline at end of string
15659
+ var records = [];
15660
+ for (var i=1; i<lines.length; i++) {
15661
+ records.push(parseFixedWidthLine(lines[i], fields));
15662
+ }
15663
+ return records;
15664
+ }
15665
+
15666
+ function parseFixedWidthInfo(sample) {
15667
+ var lines = utils.splitLines(sample);
15668
+ if (lines.length > 2) lines.pop(); // remove possible partial line
15669
+ var n = getMaxLineLength(lines);
15670
+ var headerLine = lines[0];
15671
+ var colInfo = [];
15672
+ var colStart = 0;
15673
+ var inContent = false;
15674
+ var inHeader = false;
15675
+ var isContentChar, isHeaderChar, isColStart, colEnd;
15676
+ for (var i=0; i<=n; i++) {
15677
+ isHeaderChar = testContentChar(headerLine, i);
15678
+ isContentChar = !testEmptyCol(lines, i);
15679
+ isColStart = isHeaderChar && !inHeader;
15680
+ if (isColStart && inContent) {
15681
+ // all lines should have a space char in the position right before a header starts
15682
+ return null;
15683
+ }
15684
+ if (i == n || i > 0 && isColStart) {
15685
+ colEnd = i == n ? undefined : i-1;
15686
+ colInfo.push({
15687
+ name: readValue$1(headerLine, colStart, colEnd),
15688
+ end: colEnd,
15689
+ start: colStart
15690
+ });
15691
+ colStart = i;
15692
+ }
15693
+ inContent = isContentChar;
15694
+ inHeader = isHeaderChar;
15695
+ }
15696
+ return colInfo.length > 0 ? colInfo : null;
15697
+ }
15698
+
15699
+ function getMaxLineLength(lines) {
15700
+ var max = 0;
15701
+ for (var i=0; i<lines.length; i++) {
15702
+ max = Math.max(max, lines[i].length);
15703
+ }
15704
+ return max;
15705
+ }
15706
+
15707
+ function readValue$1(line, start, end) {
15708
+ return line.substring(start, end).trim();
15709
+ }
15710
+
15711
+ function parseFixedWidthLine(str, fields) {
15712
+ var obj = {}, field;
15713
+ for (var i=0; i<fields.length; i++) {
15714
+ field = fields[i];
15715
+ obj[field.name] = readValue$1(str, field.start, field.end);
15716
+ }
15717
+ return obj;
15718
+ }
15719
+
15720
+ function testContentChar(str, i) {
15721
+ return i < str.length && str[i] !== ' ';
15722
+ }
15723
+
15724
+ // return true iff all samples are blank at index i
15725
+ function testEmptyCol(samples, i) {
15726
+ var line;
15727
+ for (var j=0; j<samples.length; j++) {
15728
+ line = samples[j];
15729
+ if (testContentChar(line, i)) return false;
15730
+ }
15731
+ return true;
15732
+ }
15733
+
15568
15734
  // Generate output content from a dataset object
15569
15735
  function exportDelim(dataset, opts) {
15570
15736
  var delim = getExportDelimiter(dataset.info, opts),
@@ -15586,6 +15752,9 @@ ${svg}
15586
15752
  var encoding = opts.encoding || 'utf8';
15587
15753
  var records = lyr.data.getRecords();
15588
15754
  var fields = findFieldNames(records, opts.field_order);
15755
+ if (delim == ' ') {
15756
+ return exportRecordsAsFixedWidthString(fields, records, opts);
15757
+ }
15589
15758
  var formatRow = getDelimRowFormatter(fields, delim, opts);
15590
15759
  // exporting utf8 and ascii text as string by default (for now)
15591
15760
  var exportAsString = encodingIsUtf8(encoding) && !opts.to_buffer &&
@@ -15643,15 +15812,6 @@ ${svg}
15643
15812
  };
15644
15813
  }
15645
15814
 
15646
- function formatNumber$1(val) {
15647
- return val + '';
15648
- }
15649
-
15650
- function formatIntlNumber(val) {
15651
- var str = formatNumber$1(val);
15652
- return '"' + str.replace('.', ',') + '"'; // need to quote if comma-delimited
15653
- }
15654
-
15655
15815
  function getDelimValueFormatter(delim, opts) {
15656
15816
  var dquoteRxp = new RegExp('["\n\r' + delim + ']');
15657
15817
  var decimalComma = opts && opts.decimal_comma || false;
@@ -15668,7 +15828,7 @@ ${svg}
15668
15828
  } else if (utils.isString(val)) {
15669
15829
  s = formatString(val);
15670
15830
  } else if (utils.isNumber(val)) {
15671
- s = decimalComma ? formatIntlNumber(val) : formatNumber$1(val);
15831
+ s = decimalComma ? utils.formatIntlNumber(val) : utils.formatNumber(val);
15672
15832
  } else if (utils.isObject(val)) {
15673
15833
  s = formatString(JSON.stringify(val));
15674
15834
  } else {
@@ -15711,8 +15871,6 @@ ${svg}
15711
15871
  __proto__: null,
15712
15872
  exportDelim: exportDelim,
15713
15873
  exportLayerAsDSV: exportLayerAsDSV,
15714
- formatNumber: formatNumber$1,
15715
- formatIntlNumber: formatIntlNumber,
15716
15874
  getDelimValueFormatter: getDelimValueFormatter
15717
15875
  });
15718
15876
 
@@ -17076,8 +17234,9 @@ ${svg}
17076
17234
  //
17077
17235
  // TODO: confirm compatibility with all supported encodings
17078
17236
  function readDelimRecords(reader, delim, optsArg) {
17237
+ var opts = optsArg || {};
17238
+ if (delim == ' ') return readFixedWidthRecords(reader, opts);
17079
17239
  var reader2 = new Reader2(reader),
17080
- opts = optsArg || {},
17081
17240
  headerStr = readLinesAsString(reader2, getDelimHeaderLines(opts), opts.encoding),
17082
17241
  header = parseDelimHeaderSection(headerStr, delim, opts),
17083
17242
  convertRowArr = getRowConverter(header.import_fields),
@@ -17100,6 +17259,7 @@ ${svg}
17100
17259
  // for delimiter characters and newlines. Input size is limited by the maximum
17101
17260
  // string size.
17102
17261
  function readDelimRecordsFromString(str, delim, opts) {
17262
+ if (delim == ' ') return readFixedWidthRecordsFromString(str, opts);
17103
17263
  var header = parseDelimHeaderSection(str, delim, opts);
17104
17264
  if (header.import_fields.length === 0 || !header.remainder) return [];
17105
17265
  var convert = getRowConverter(header.import_fields);
@@ -17503,7 +17663,7 @@ ${svg}
17503
17663
  };
17504
17664
  }
17505
17665
 
17506
- var supportedDelimiters = ['|', '\t', ',', ';'];
17666
+ var supportedDelimiters = ['|', '\t', ',', ';', ' '];
17507
17667
 
17508
17668
  function isSupportedDelimiter(d) {
17509
17669
  return utils.contains(supportedDelimiters, d);
@@ -17864,9 +18024,21 @@ ${svg}
17864
18024
  }
17865
18025
 
17866
18026
  function cleanArgv(argv) {
17867
- argv = argv.map(function(s) {return s.trim();}); // trim whitespace
18027
+ // Note: original trim caused some quoted spaces to be removed
18028
+ // (e.g. bash shell seems to convert [delimiter=" "] to [delimiter= ],
18029
+ // which then got trimmed to [delimiter=] below)
18030
+ //// argv = argv.map(function(s) {return s.trim();}); // trim whitespace
18031
+
18032
+ // Updated: don't trim space from tokens like [delimeter= ]
18033
+ argv = argv.map(function(s) {
18034
+ if (!/= $/.test(s)) {
18035
+ s = s.trimEnd();
18036
+ }
18037
+ s = s.trimStart();
18038
+ return s;
18039
+ });
17868
18040
  argv = argv.filter(function(s) {return s !== '';}); // remove empty tokens
17869
- // removing trimQuotes() call... now, strings like 'name="Meg"' will no longer
18041
+ // Note: removing trimQuotes() call... now, strings like 'name="Meg"' will no longer
17870
18042
  // be parsed the same way as name=Meg and name="Meg"
17871
18043
  //// argv = argv.map(utils.trimQuotes); // remove one level of single or dbl quotes
17872
18044
  return argv;
@@ -18973,6 +19145,28 @@ ${svg}
18973
19145
  '$ mapshaper data.json -colorizer name=getColor nodata=#eee breaks=20,40 \\\n' +
18974
19146
  ' colors=#e0f3db,#a8ddb5,#43a2ca -each \'fill = getColor(RATING)\' -o output.json');
18975
19147
 
19148
+ parser.command('dashlines')
19149
+ .describe('split lines into sections, with or without a gap')
19150
+ .oldAlias('split-lines')
19151
+ .option('dash-length', {
19152
+ type: 'distance',
19153
+ describe: 'length of split-apart lines (e.g. 200km)'
19154
+ })
19155
+ .option('gap-length', {
19156
+ type: 'distance',
19157
+ describe: 'length of gaps between dashes (default is 0)'
19158
+ })
19159
+ .option('scaled', {
19160
+ type: 'flag',
19161
+ describe: 'scale dashes and gaps to prevent partial dashes'
19162
+ })
19163
+ .option('planar', {
19164
+ type: 'flag',
19165
+ describe: 'use planar geometry'
19166
+ })
19167
+ .option('where', whereOpt)
19168
+ .option('target', targetOpt);
19169
+
18976
19170
  parser.command('define')
18977
19171
  // .describe('define expression variables')
18978
19172
  .option('expression', {
@@ -19169,6 +19363,10 @@ ${svg}
19169
19363
  .option('keep-shapes', {
19170
19364
  type: 'flag'
19171
19365
  })
19366
+ .option('ids', {
19367
+ // describe: 'filter on a list of feature ids',
19368
+ type: 'numbers'
19369
+ })
19172
19370
  .option('cleanup', {type: 'flag'}) // TODO: document
19173
19371
  .option('name', nameOpt)
19174
19372
  .option('target', targetOpt)
@@ -19291,6 +19489,13 @@ ${svg}
19291
19489
  })
19292
19490
  .option('target', targetOpt);
19293
19491
 
19492
+ parser.command('include')
19493
+ .describe('import JS data and functions for use in JS expressions')
19494
+ .option('file', {
19495
+ DEFAULT: true,
19496
+ describe: 'file containing a JS object with key:value pairs to import'
19497
+ });
19498
+
19294
19499
  parser.command('inlay')
19295
19500
  .describe('inscribe a polygon layer inside another polygon layer')
19296
19501
  .option('source', {
@@ -19603,6 +19808,7 @@ ${svg}
19603
19808
  })
19604
19809
  .option('target', targetOpt);
19605
19810
 
19811
+
19606
19812
  parser.command('simplify')
19607
19813
  .validate(validateSimplifyOpts)
19608
19814
  .example('Retain 10% of removable vertices\n$ mapshaper input.shp -simplify 10%')
@@ -19737,6 +19943,11 @@ ${svg}
19737
19943
  DEFAULT: true,
19738
19944
  describe: 'expression or field for grouping features and naming split layers'
19739
19945
  })
19946
+ .option('ids', {
19947
+ // used by gui history to split on selected features
19948
+ // describe: 'split on a list of feature ids',
19949
+ type: 'numbers'
19950
+ })
19740
19951
  .option('apart', {
19741
19952
  describe: 'save output layers to independent datasets',
19742
19953
  type: 'flag'
@@ -19744,23 +19955,6 @@ ${svg}
19744
19955
  .option('target', targetOpt)
19745
19956
  .option('no-replace', noReplaceOpt);
19746
19957
 
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
19958
  parser.command('split-on-grid')
19765
19959
  .describe('split features into separate layers using a grid')
19766
19960
  .validate(validateGridOpts)
@@ -19853,26 +20047,81 @@ ${svg}
19853
20047
  .option('target', targetOpt);
19854
20048
 
19855
20049
  parser.command('symbols')
19856
- // .describe('generate a variety of SVG symbols')
20050
+ // .describe('symbolize points as polygons, circles, stars or arrows')
19857
20051
  .option('type', {
19858
- describe: 'symbol type'
20052
+ describe: 'symbol type (e.g. star, polygon, circle, arrow)'
20053
+ })
20054
+ .option('scale', {
20055
+ describe: 'scale symbols by a factor',
20056
+ type: 'number'
20057
+ })
20058
+ .option('pixel-scale', {
20059
+ describe: 'symbol scale in meters-per-pixel (see polygons option)',
20060
+ type: 'number',
20061
+ })
20062
+ .option('polygons', {
20063
+ describe: 'generate symbols as polygons instead of SVG objects',
20064
+ type: 'flag'
20065
+ })
20066
+ .option('radius', {
20067
+ describe: 'distance from center to farthest point on the symbol',
20068
+ type: 'distance'
20069
+ })
20070
+ .option('sides', {
20071
+ describe: 'sides of a polygon or star symbol',
20072
+ type: 'number'
20073
+ })
20074
+ .option('rotation', {
20075
+ describe: 'rotation of symbol in degrees'
20076
+ })
20077
+ .option('orientation', {
20078
+ describe: 'use orientation=b for a rotated or flipped orientation'
20079
+ })
20080
+ .option('length', {
20081
+ // alias for arrow-length
20082
+ })
20083
+ .option('star-ratio', {
20084
+ describe: 'ratio of major to minor radius of star',
20085
+ type: 'number'
20086
+ })
20087
+ .option('arrow-length', {
20088
+ describe: 'length of arrows in pixels (use with type=arrow)'
20089
+ })
20090
+ .option('arrow-direction', {
20091
+ describe: 'angle off of vertical (-90 = left-pointing arrow)'
20092
+ })
20093
+ .option('arrow-head-angle', {
20094
+ describe: 'angle of tip of arrow (default is 40 degrees)'
20095
+ })
20096
+ .option('arrow-head-width', {
20097
+ describe: 'size of arrow head from side to side'
20098
+ })
20099
+ .option('arrow-stem-width', {
20100
+ describe: 'width of stem at its widest point'
20101
+ })
20102
+ .option('arrow-stem-taper', {
20103
+ describe: 'factor for tapering the width of the stem'
20104
+ })
20105
+ .option('arrow-stem-curve', {
20106
+ describe: 'curvature in degrees (arrows are straight by default)'
20107
+ })
20108
+ .option('arrow-min-stem', {
20109
+ describe: 'min ratio of stem to total length (for small arrows)',
20110
+ type: 'number'
19859
20111
  })
19860
20112
  .option('stroke', {})
19861
20113
  .option('stroke-width', {})
19862
- .option('fill', {})
19863
- .option('length', {})
19864
- .option('rotation', {})
20114
+ .option('fill', {
20115
+ describe: 'symbol fill color'
20116
+ })
19865
20117
  .option('effect', {})
19866
- .option('arrow-head-angle', {})
19867
- .option('arrow-stem-width', {})
19868
- .option('arrow-head-width', {})
19869
- .option('arrow-stem-curve', {})
19870
- .option('arrow-stem-taper', {})
19871
- .option('arrow-scaling', {})
19872
- .option('where', whereOpt)
19873
- .option('target', targetOpt);
20118
+ // .option('where', whereOpt)
20119
+ .option('name', nameOpt)
20120
+ .option('target', targetOpt)
20121
+ .option('no-replace', noReplaceOpt);
19874
20122
  // .option('name', nameOpt);
19875
20123
 
20124
+
19876
20125
  parser.command('target')
19877
20126
  .describe('set active layer (or layers)')
19878
20127
  .option('target', {
@@ -20023,13 +20272,6 @@ ${svg}
20023
20272
  })
20024
20273
  .option('name', nameOpt);
20025
20274
 
20026
- parser.command('include')
20027
- .describe('import JS data and functions for use in JS expressions')
20028
- .option('file', {
20029
- DEFAULT: true,
20030
- describe: 'file containing a JS object with key:value pairs to import'
20031
- });
20032
-
20033
20275
  parser.command('fuzzy-join')
20034
20276
  .describe('join points to polygons, with data fill and fuzzy match')
20035
20277
  .option('source', {
@@ -21028,12 +21270,13 @@ ${svg}
21028
21270
  if (this instanceof ShpReader === false) {
21029
21271
  return new ShpReader(shpSrc, shxSrc);
21030
21272
  }
21031
-
21032
21273
  var shpFile = utils.isString(shpSrc) ? new FileReader(shpSrc) : new BufferReader(shpSrc);
21033
21274
  var header = parseHeader(shpFile.readToBinArray(0, 100));
21034
- var shpSize = shpFile.size();
21035
- var RecordClass = new ShpRecordClass(header.type);
21036
- var shpOffset, recordCount, skippedBytes;
21275
+ var shpType = header.type;
21276
+ var shpOffset = 100; // used when reading .shp without .shx
21277
+ var recordCount = 0;
21278
+ var badRecordNumberCount = 0;
21279
+ var RecordClass = new ShpRecordClass(shpType);
21037
21280
  var shxBin, shxFile;
21038
21281
 
21039
21282
  if (shxSrc) {
@@ -21041,8 +21284,6 @@ ${svg}
21041
21284
  shxBin = shxFile.readToBinArray(0, shxFile.size()).bigEndian();
21042
21285
  }
21043
21286
 
21044
- reset();
21045
-
21046
21287
  this.header = function() {
21047
21288
  return header;
21048
21289
  };
@@ -21060,62 +21301,35 @@ ${svg}
21060
21301
 
21061
21302
  // Iterator interface for reading shape records
21062
21303
  this.nextShape = function() {
21063
- var shape = readNextShape();
21304
+ var shape;
21305
+ if (!shpFile) {
21306
+ error('Tried to read from a used ShpReader');
21307
+ // return null; // this reader was already used
21308
+ }
21309
+ shape = readNextShape(recordCount);
21064
21310
  if (!shape) {
21065
- if (skippedBytes > 0) {
21066
- // Encountered in files from natural earth v2.0.0:
21067
- // ne_10m_admin_0_boundary_lines_land.shp
21068
- // ne_110m_admin_0_scale_rank.shp
21069
- verbose("Skipped over " + skippedBytes + " non-data bytes in the .shp file.");
21070
- }
21071
- shpFile.close();
21072
- reset();
21311
+ done();
21312
+ return null;
21073
21313
  }
21314
+ recordCount++;
21074
21315
  return shape;
21075
21316
  };
21076
21317
 
21077
- function readNextShape() {
21078
- var expectedId = recordCount + 1; // Shapefile ids are 1-based
21079
- var shape, offset;
21080
- if (done()) return null;
21081
- if (shxBin) {
21082
- shxBin.position(100 + recordCount * 8);
21083
- offset = shxBin.readUint32() * 2;
21084
- if (offset > shpOffset) {
21085
- skippedBytes += offset - shpOffset;
21086
- }
21087
- } else {
21088
- offset = shpOffset;
21089
- }
21090
- shape = readShapeAtOffset(offset);
21091
- if (!shape) {
21092
- // Some in-the-wild .shp files contain junk bytes between records. This
21093
- // is a problem if the .shx index file is not present.
21094
- // Here, we try to scan past the junk to find the next record.
21095
- shape = huntForNextShape(offset, expectedId);
21096
- }
21097
- if (shape) {
21098
- if (shape.id < expectedId) {
21099
- message("Found a Shapefile record with the same id as a previous record (" + shape.id + ") -- skipping.");
21100
- return readNextShape();
21101
- } else if (shape.id > expectedId) {
21102
- stop("Shapefile contains an out-of-sequence record. Possible data corruption -- bailing.");
21103
- }
21104
- recordCount++;
21105
- }
21106
- return shape || null;
21318
+ // Returns a shape record or null if no more shapes can be read
21319
+ // i: Expected 0-based index of the next record
21320
+ //
21321
+ function readNextShape(i) {
21322
+ return shxBin ?
21323
+ readIndexedShape(shpFile, shxBin, i) :
21324
+ readNonIndexedShape(shpFile, shpOffset, i);
21107
21325
  }
21108
21326
 
21109
21327
  function done() {
21110
- if (shxFile && shxFile.size() <= 100 + recordCount * 8) return true;
21111
- if (shpOffset + 12 > shpSize) return true;
21112
- return false;
21113
- }
21114
-
21115
- function reset() {
21116
- shpOffset = 100;
21117
- skippedBytes = 0;
21118
- recordCount = 0;
21328
+ shpFile.close();
21329
+ shpFile = shxFile = shxBin = null;
21330
+ if (badRecordNumberCount > 0) {
21331
+ message(`Warning: ${badRecordNumberCount}/${recordCount} features have non-standard record numbers in the .shp file.`);
21332
+ }
21119
21333
  }
21120
21334
 
21121
21335
  function parseHeader(bin) {
@@ -21144,47 +21358,81 @@ ${svg}
21144
21358
  return header;
21145
21359
  }
21146
21360
 
21147
- function readShapeAtOffset(offset) {
21148
- var shape = null,
21149
- recordSize, recordType, recordId, goodSize, goodType, bin;
21150
21361
 
21151
- if (offset + 12 <= shpSize) {
21152
- bin = shpFile.readToBinArray(offset, 12);
21153
- recordId = bin.bigEndian().readUint32();
21154
- // record size is bytes in content section + 8 header bytes
21155
- recordSize = bin.readUint32() * 2 + 8;
21156
- recordType = bin.littleEndian().readUint32();
21157
- goodSize = offset + recordSize <= shpSize && recordSize >= 12;
21158
- goodType = recordType === 0 || recordType == header.type;
21159
- if (goodSize && goodType) {
21160
- bin = shpFile.readToBinArray(offset, recordSize);
21161
- shape = new RecordClass(bin, recordSize);
21162
- shpOffset = offset + shape.byteLength; // advance read position
21163
- }
21362
+ function readShapeAtOffset(shpFile, offset) {
21363
+ var fileSize = shpFile.size();
21364
+ if (offset + 12 > fileSize) return null; // reached end-of-file
21365
+ var bin = shpFile.readToBinArray(offset, 12);
21366
+ var recordId = bin.bigEndian().readUint32();
21367
+ // record size is bytes in content section + 8 header bytes
21368
+ var recordSize = bin.readUint32() * 2 + 8;
21369
+ var recordType = bin.littleEndian().readUint32();
21370
+ var goodSize = offset + recordSize <= fileSize && recordSize >= 12;
21371
+ var goodType = recordType === 0 || recordType == shpType;
21372
+ if (!goodSize || !goodType) {
21373
+ return null;
21374
+ }
21375
+ bin = shpFile.readToBinArray(offset, recordSize);
21376
+ return new RecordClass(bin, recordSize);
21377
+ }
21378
+
21379
+ function readIndexedShape(shpFile, shxBin, i) {
21380
+ if (shxBin.size() <= 100 + i * 8) return null; // done
21381
+ shxBin.position(100 + i * 8);
21382
+ var expectedId = i + 1;
21383
+ var offset = shxBin.readUint32() * 2;
21384
+ var recLen = shxBin.readUint32() * 2; // TODO: match this to recLen in .shp
21385
+ var shape = readShapeAtOffset(shpFile, offset);
21386
+ if (!shape) {
21387
+ stop('Index of Shapefile record', expectedId, 'in the .shx file is invalid.');
21164
21388
  }
21389
+ if (shape.id != expectedId) {
21390
+ badRecordNumberCount++;
21391
+ verbose(`Warning: A feature has a different record number in .shx (${expectedId}) and .shp (${shape.id}).`);
21392
+ }
21393
+ // TODO: consider printing verbose message if a .shp file contains garbage bytes
21394
+ // example files:
21395
+ // ne_10m_admin_0_boundary_lines_land.shp
21396
+ // ne_110m_admin_0_scale_rank.shp
21165
21397
  return shape;
21166
21398
  }
21167
21399
 
21168
- // TODO: add tests
21169
- // Try to scan past unreadable content to find next record
21170
- function huntForNextShape(start, id) {
21171
- var offset = start + 4,
21400
+ // The Shapefile specification does not require records to be densely packed or
21401
+ // in consecutive sequence in the .shp file. This is a problem when the .shx
21402
+ // index file is not present.
21403
+ //
21404
+ // Here, we try to scan past any invalid content to find the next record.
21405
+ // Records are required to be in sequential order.
21406
+ //
21407
+ function readNonIndexedShape(shpFile, start, i) {
21408
+ var expectedId = i + 1, // Shapefile ids are 1-based
21409
+ offset = start,
21410
+ fileSize = shpFile.size(),
21172
21411
  shape = null,
21173
- bin, recordId, recordType, count;
21174
- while (offset + 12 <= shpSize) {
21412
+ bin, recordId, recordType, isValidType;
21413
+ while (offset + 12 <= fileSize) {
21175
21414
  bin = shpFile.readToBinArray(offset, 12);
21176
21415
  recordId = bin.bigEndian().readUint32();
21177
21416
  recordType = bin.littleEndian().skipBytes(4).readUint32();
21178
- if (recordId == id && (recordType == header.type || recordType === 0)) {
21179
- // we have a likely position, but may still be unparsable
21180
- shape = readShapeAtOffset(offset);
21181
- break;
21417
+ isValidType = recordType == shpType || recordType === 0;
21418
+ if (!isValidType || recordId != expectedId && recordType === 0) {
21419
+ offset += 4; // keep scanning -- try next integer position
21420
+ continue;
21421
+ }
21422
+ shape = readShapeAtOffset(shpFile, offset);
21423
+ if (!shape) break; // probably ran into end of file
21424
+ shpOffset = offset + shape.byteLength; // update
21425
+ if (recordId == expectedId) break; // found an apparently valid shape
21426
+ if (recordId < expectedId) {
21427
+ message("Found a Shapefile record with the same id as a previous record (" + shape.id + ") -- skipping.");
21428
+ offset += shape.byteLength;
21429
+ } else {
21430
+ stop("Shapefile contains an out-of-sequence record. Possible data corruption -- bailing.");
21182
21431
  }
21183
- offset += 4; // try next integer position
21184
21432
  }
21185
- count = shape ? offset - start : shpSize - start;
21186
- // debug('Skipped', count, 'bytes', shape ? 'before record ' + id : 'at the end of the file');
21187
- skippedBytes += count;
21433
+ if (shape && offset > start) {
21434
+ verbose("Skipped over " + (offset - start) + " non-data bytes in the .shp file.");
21435
+ }
21188
21436
  return shape;
21189
21437
  }
21190
21438
  }
@@ -21193,22 +21441,6 @@ ${svg}
21193
21441
  return this.header().type;
21194
21442
  };
21195
21443
 
21196
- ShpReader.prototype.getCounts = function() {
21197
- var counts = {
21198
- nullCount: 0,
21199
- partCount: 0,
21200
- shapeCount: 0,
21201
- pointCount: 0
21202
- };
21203
- this.forEachShape(function(shp) {
21204
- if (shp.isNull) counts.nullCount++;
21205
- counts.pointCount += shp.pointCount;
21206
- counts.partCount += shp.partCount;
21207
- counts.shapeCount++;
21208
- });
21209
- return counts;
21210
- };
21211
-
21212
21444
  // Apply snapping, remove duplicate coords and clean up defective paths in a dataset
21213
21445
  // Assumes that any CRS info has been added to the dataset
21214
21446
  // @opts: import options
@@ -26017,7 +26249,6 @@ ${svg}
26017
26249
  if (constTol) return constTol;
26018
26250
  return constTol ? constTol : meterDist * pctOfRadius;
26019
26251
  };
26020
-
26021
26252
  }
26022
26253
 
26023
26254
  function getBufferDistanceFunction(lyr, dataset, opts) {
@@ -27190,7 +27421,7 @@ ${svg}
27190
27421
  cmd.buffer = makeBufferLayer;
27191
27422
 
27192
27423
  function makeBufferLayer(lyr, dataset, opts) {
27193
- var dataset2, lyr2;
27424
+ var dataset2;
27194
27425
  if (lyr.geometry_type == 'point') {
27195
27426
  dataset2 = makePointBuffer(lyr, dataset, opts);
27196
27427
  } else if (lyr.geometry_type == 'polyline') {
@@ -27200,13 +27431,9 @@ ${svg}
27200
27431
  } else {
27201
27432
  stop("Unsupported geometry type");
27202
27433
  }
27203
- var outputLayers = mergeDatasetsIntoDataset(dataset, [dataset2]);
27204
- lyr2 = outputLayers[0];
27205
- setOutputLayerName(lyr2, lyr, null, opts);
27206
- if (lyr.data && !lyr2.data) {
27207
- lyr2.data = opts.no_replace ? lyr.data.clone() : lyr.data;
27208
- }
27209
- return outputLayers;
27434
+
27435
+ var lyr2 = mergeOutputLayerIntoDataset(lyr, dataset, dataset2, opts);
27436
+ return [lyr2];
27210
27437
  }
27211
27438
 
27212
27439
  // TODO: support three or more stops
@@ -29729,6 +29956,178 @@ ${svg}
29729
29956
  getColorizerFunction: getColorizerFunction
29730
29957
  });
29731
29958
 
29959
+ function expressionUsesGeoJSON(exp) {
29960
+ return exp.includes('this.geojson');
29961
+ }
29962
+
29963
+ function getFeatureEditor(lyr, dataset) {
29964
+ var changed = false;
29965
+ var api = {};
29966
+ // need to copy attribute to avoid circular references if geojson is assigned
29967
+ // to a data property.
29968
+ var copy = copyLayer(lyr);
29969
+ var features = exportLayerAsGeoJSON(copy, dataset, {}, true);
29970
+
29971
+ api.get = function(i) {
29972
+ return features[i];
29973
+ };
29974
+
29975
+ api.set = function(feat, i) {
29976
+ changed = true;
29977
+ if (utils.isString(feat)) {
29978
+ feat = JSON.parse(feat);
29979
+ }
29980
+ features[i] = GeoJSON.toFeature(feat); // TODO: validate
29981
+ };
29982
+
29983
+ api.done = function() {
29984
+ if (!changed) return; // read-only expression
29985
+ // TODO: validate number of features, etc.
29986
+ var geojson = {
29987
+ type: 'FeatureCollection',
29988
+ features: features
29989
+ };
29990
+
29991
+ // console.log(JSON.stringify(geojson, null, 2))
29992
+ return importGeoJSON(geojson);
29993
+ };
29994
+ return api;
29995
+ }
29996
+
29997
+ cmd.dashlines = function(lyr, dataset, opts) {
29998
+ var crs = getDatasetCRS(dataset);
29999
+ var defs = getStateVar('defs');
30000
+ var exp = `this.geojson = splitFeature(this.geojson)`;
30001
+ requirePolylineLayer(lyr);
30002
+ defs.splitFeature = getSplitFeatureFunction(crs, opts);
30003
+ cmd.evaluateEachFeature(lyr, dataset, exp, opts);
30004
+ delete defs.splitFeature;
30005
+ };
30006
+
30007
+ function getSplitFeatureFunction(crs, opts) {
30008
+ var dashLen = opts.dash_length ? convertDistanceParam(opts.dash_length, crs) : 0;
30009
+ var gapLen = opts.gap_length ? convertDistanceParam(opts.gap_length, crs) : 0;
30010
+ if (dashLen > 0 === false) {
30011
+ stop('Missing required dash-length parameter');
30012
+ }
30013
+ if (gapLen >= 0 == false) {
30014
+ stop('Invalid gap-length option');
30015
+ }
30016
+ var splitLine = getSplitLineFunction(crs, dashLen, gapLen, opts);
30017
+ return function(feat) {
30018
+ var geom = feat.geometry;
30019
+ if (!geom) return feat;
30020
+ if (geom.type == 'LineString') {
30021
+ geom.type = 'MultiLineString';
30022
+ geom.coordinates = [geom.coordinates];
30023
+ }
30024
+ if (geom.type != 'MultiLineString') {
30025
+ error('Unexpected geometry:', geom.type);
30026
+ }
30027
+ geom.coordinates = geom.coordinates.reduce(function(memo, coords) {
30028
+ try {
30029
+ var parts = splitLine(coords);
30030
+ memo = memo.concat(parts);
30031
+ } catch(e) {
30032
+ console.error(e);
30033
+ throw e;
30034
+ }
30035
+ return memo;
30036
+ }, []);
30037
+
30038
+ return feat;
30039
+ };
30040
+ }
30041
+
30042
+ function getSplitLineFunction(crs, dashLen, gapLen, opts) {
30043
+ var planar = !!opts.planar;
30044
+ var interpolate = getInterpolationFunction(planar ? null : crs);
30045
+ var distance = isLatLngCRS(crs) ? greatCircleDistance : distance2D;
30046
+ var inDash, parts2, interval, scale;
30047
+ function addPart(coords) {
30048
+ if (inDash) parts2.push(coords);
30049
+ if (gapLen > 0) {
30050
+ inDash = !inDash;
30051
+ interval = scale * (inDash ? dashLen : gapLen);
30052
+ }
30053
+ }
30054
+
30055
+ return function splitLineString(coords) {
30056
+ var elapsedDist = 0;
30057
+ var p = coords[0];
30058
+ var coords2 = [p];
30059
+ var segLen, pct, prev;
30060
+ if (opts.scaled) {
30061
+ scale = scaleDashes(dashLen, gapLen, getLineLength(coords, distance));
30062
+ } else {
30063
+ scale = 1;
30064
+ }
30065
+ // init this LineString
30066
+ inDash = gapLen > 0 ? false : true;
30067
+ interval = scale * (inDash ? dashLen : gapLen);
30068
+ if (!inDash) {
30069
+ // start gapped lines with a half-gap
30070
+ // (a half-gap or a half-dash is probably better for rings and intersecting lines)
30071
+ interval *= 0.5;
30072
+ }
30073
+ parts2 = [];
30074
+ for (var i=1, n=coords.length; i<n; i++) {
30075
+ prev = p;
30076
+ p = coords[i];
30077
+ segLen = distance(prev[0], prev[1], p[0], p[1]);
30078
+ if (segLen <= 0) continue;
30079
+ while (elapsedDist + segLen >= interval) {
30080
+ // this segment contains a break either within it or at the far endpoint
30081
+ pct = (interval - elapsedDist) / segLen;
30082
+ if (pct > 0.999 && i == n - 1) {
30083
+ // snap to endpoint (so fp rounding errors don't result in a tiny
30084
+ // last segment)
30085
+ pct = 1;
30086
+ }
30087
+ if (pct < 1) {
30088
+ prev = interpolate(prev[0], prev[1], p[0], p[1], pct);
30089
+ } else {
30090
+ prev = p;
30091
+ }
30092
+ coords2.push(prev);
30093
+ addPart(coords2);
30094
+ // start a new part
30095
+ coords2 = pct < 1 ? [prev] : [];
30096
+ elapsedDist = 0;
30097
+ segLen = (1 - pct) * segLen;
30098
+ }
30099
+ coords2.push(p);
30100
+ elapsedDist += segLen;
30101
+ }
30102
+ if (elapsedDist > 0 && coords2.length > 1) {
30103
+ addPart(coords2);
30104
+ }
30105
+ return parts2;
30106
+ };
30107
+ }
30108
+
30109
+ function getLineLength(coords, distance) {
30110
+ var len = 0;
30111
+ for (var i=1, n=coords.length; i<n; i++) {
30112
+ len += distance(coords[i-1][0], coords[i-1][1], coords[i][0], coords[i][1]);
30113
+ }
30114
+ return len;
30115
+ }
30116
+
30117
+ function scaleDashes(dash, gap, len) {
30118
+ var dash2, gap2;
30119
+ var n = len / (dash + gap); // number of dashes
30120
+ var n1 = Math.floor(n);
30121
+ var n2 = Math.ceil(n);
30122
+ var k1 = len / (n1 * (dash + gap)); // scaled-up dashes, >1
30123
+ var k2 = len / (n2 * (dash + gap)); // scaled-down dashes <1
30124
+ var k = k2;
30125
+ if (k1 < 1/k2 && n1 > 0) {
30126
+ k = k1; // pick the smaller of the two scales
30127
+ }
30128
+ return k;
30129
+ }
30130
+
29732
30131
  // This function creates a continuous mosaic of data values in a
29733
30132
  // given field by assigning data from adjacent polygon features to polygons
29734
30133
  // that contain null values.
@@ -31253,44 +31652,6 @@ ${svg}
31253
31652
  }
31254
31653
  };
31255
31654
 
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
31655
  cmd.evaluateEachFeature = function(lyr, dataset, exp, opts) {
31295
31656
  var n = getFeatureCount(lyr),
31296
31657
  arcs = dataset.arcs,
@@ -31533,6 +31894,10 @@ ${svg}
31533
31894
  filter = compileValueExpression(opts.expression, lyr, arcs);
31534
31895
  }
31535
31896
 
31897
+ if (opts.ids) {
31898
+ filter = combineFilters(filter, getIdFilter(opts.ids));
31899
+ }
31900
+
31536
31901
  if (opts.remove_empty) {
31537
31902
  filter = combineFilters(filter, getNullGeometryFilter(lyr, arcs));
31538
31903
  }
@@ -31591,6 +31956,13 @@ ${svg}
31591
31956
  lyr.data = filteredRecords ? new DataTable(filteredRecords) : null;
31592
31957
  }
31593
31958
 
31959
+ function getIdFilter(ids) {
31960
+ var set = new Set(ids);
31961
+ return function(i) {
31962
+ return set.has(i);
31963
+ };
31964
+ }
31965
+
31594
31966
  function getNullGeometryFilter(lyr, arcs) {
31595
31967
  var shapes = lyr.shapes;
31596
31968
  if (lyr.geometry_type == 'polygon') {
@@ -31896,7 +32268,6 @@ ${svg}
31896
32268
  return joinTableToLayer(targetLyr, polygonLyr.data, joinFunction, opts);
31897
32269
  }
31898
32270
 
31899
-
31900
32271
  function prepJoinLayers(targetLyr, srcLyr) {
31901
32272
  if (!targetLyr.data) {
31902
32273
  // create an empty data table if target layer is missing attributes
@@ -33647,7 +34018,7 @@ ${svg}
33647
34018
 
33648
34019
  function createMeridianPart(x, ymin, ymax) {
33649
34020
  var coords = densifyPathByInterval([[x, ymin], [x, ymax]], precision);
33650
- meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord(x)}));
34021
+ meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord$1(x)}));
33651
34022
  }
33652
34023
 
33653
34024
  function createParallel(y) {
@@ -33657,7 +34028,7 @@ ${svg}
33657
34028
  }
33658
34029
 
33659
34030
  // remove tiny offsets
33660
- function roundCoord(x) {
34031
+ function roundCoord$1(x) {
33661
34032
  return +x.toFixed(3) || 0;
33662
34033
  }
33663
34034
 
@@ -33864,10 +34235,6 @@ ${svg}
33864
34235
 
33865
34236
 
33866
34237
 
33867
- function formatNumber(val) {
33868
- return val + '';
33869
- }
33870
-
33871
34238
  function maxChars(arr) {
33872
34239
  return arr.reduce(function(memo, str) {
33873
34240
  var w = stringDisplayWidth(str);
@@ -33891,14 +34258,14 @@ ${svg}
33891
34258
  }
33892
34259
 
33893
34260
  function countIntegralChars(val) {
33894
- return utils.isNumber(val) ? (formatNumber(val) + '.').indexOf('.') : 0;
34261
+ return utils.isNumber(val) ? (utils.formatNumber(val) + '.').indexOf('.') : 0;
33895
34262
  }
33896
34263
 
33897
34264
  function formatTableValue(val, integralChars) {
33898
34265
  var str;
33899
34266
  if (utils.isNumber(val)) {
33900
34267
  str = utils.lpad("", integralChars - countIntegralChars(val), ' ') +
33901
- formatNumber(val);
34268
+ utils.formatNumber(val);
33902
34269
  } else if (utils.isString(val)) {
33903
34270
  str = formatString(val);
33904
34271
  } else if (utils.isDate(val)) {
@@ -34702,6 +35069,38 @@ ${svg}
34702
35069
  }
34703
35070
  }
34704
35071
 
35072
+ function pointsFromPolylinesForJoin(lyr, dataset) {
35073
+ var shapes = lyr.shapes.map(function(shp) {
35074
+ return polylineToMidpoints(shp, dataset.arcs);
35075
+ });
35076
+ return {
35077
+ geometry_type: 'point',
35078
+ shapes: shapes,
35079
+ data: lyr.data // TODO copy if needed
35080
+ };
35081
+ }
35082
+
35083
+ function validateOpts(opts) {
35084
+ if (!opts.point_method) {
35085
+ stop('The "point-method" flag is required for polyline-polygon joins');
35086
+ }
35087
+ }
35088
+
35089
+ function joinPolylinesToPolygons(targetLyr, targetDataset, source, opts) {
35090
+ validateOpts(opts);
35091
+ var pointLyr = pointsFromPolylinesForJoin(source.layer, source.dataset);
35092
+ var retn = joinPointsToPolygons(targetLyr, targetDataset.arcs, pointLyr, opts);
35093
+ return retn;
35094
+ }
35095
+
35096
+ function joinPolygonsToPolylines(targetLyr, targetDataset, source, opts) {
35097
+ validateOpts(opts);
35098
+ var pointLyr = pointsFromPolylinesForJoin(targetLyr, targetDataset);
35099
+ var retn = joinPolygonsToPoints(pointLyr, source.layer, source.dataset.arcs, opts);
35100
+ targetLyr.data = pointLyr.data;
35101
+ return retn;
35102
+ }
35103
+
34705
35104
  class TinyQueue {
34706
35105
  constructor(data = [], compare = defaultCompare) {
34707
35106
  this.data = data;
@@ -35025,7 +35424,7 @@ ${svg}
35025
35424
 
35026
35425
  cmd.join = function(targetLyr, targetDataset, src, opts) {
35027
35426
  var srcType, targetType, retn;
35028
- if (!src || !src.layer.data || !src.dataset) {
35427
+ if (!src || !src.dataset) {
35029
35428
  stop("Missing a joinable data source");
35030
35429
  }
35031
35430
  if (opts.keys) {
@@ -35033,9 +35432,18 @@ ${svg}
35033
35432
  if (opts.keys.length != 2) {
35034
35433
  stop("Expected two key fields: a target field and a source field");
35035
35434
  }
35435
+ if (!src.layer.data) {
35436
+ stop("Source layer is missing attribute data");
35437
+ }
35036
35438
  retn = joinAttributesToFeatures(targetLyr, src.layer.data, opts);
35037
35439
  } else {
35038
35440
  // spatial join
35441
+ if (!src.layer.data) {
35442
+ // KLUDGE -- users might want to join a layer without attributes
35443
+ // to test for intersection... the simplest way to support this is
35444
+ // to add an empty data table to the source layer
35445
+ initDataTable(src.layer);
35446
+ }
35039
35447
  requireDatasetsHaveCompatibleCRS([targetDataset, src.dataset]);
35040
35448
  srcType = src.layer.geometry_type;
35041
35449
  targetType = targetLyr.geometry_type;
@@ -35047,6 +35455,10 @@ ${svg}
35047
35455
  retn = joinPointsToPoints(targetLyr, src.layer, getDatasetCRS(targetDataset), opts);
35048
35456
  } else if (srcType == 'polygon' && targetType == 'polygon') {
35049
35457
  retn = joinPolygonsToPolygons(targetLyr, targetDataset, src, opts);
35458
+ } else if (srcType == 'polyline' && targetType == 'polygon') {
35459
+ retn = joinPolylinesToPolygons(targetLyr, targetDataset, src, opts);
35460
+ } else if (srcType == 'polygon' && targetType == 'polyline') {
35461
+ retn = joinPolygonsToPolylines(targetLyr, targetDataset, src, opts);
35050
35462
  } else {
35051
35463
  stop(utils.format("Unable to join %s geometry to %s geometry",
35052
35464
  srcType || 'null', targetType || 'null'));
@@ -36267,6 +36679,99 @@ ${svg}
36267
36679
  parseScalebarLabelToKm: parseScalebarLabelToKm
36268
36680
  });
36269
36681
 
36682
+ cmd.shape = function(targetDataset, opts) {
36683
+ var geojson, dataset;
36684
+ if (opts.coordinates) {
36685
+ geojson = makeShapeFromCoords(opts);
36686
+ } else if (opts.type == 'circle') {
36687
+ geojson = makeCircle(opts);
36688
+ } else if (opts.type == 'rectangle' && opts.bbox) {
36689
+ geojson = getRectangleGeoJSON(opts);
36690
+ } else {
36691
+ stop('Missing coordinates parameter');
36692
+ }
36693
+ // TODO: project shape if targetDataset is projected
36694
+ dataset = importGeoJSON(geojson, {});
36695
+ if (opts.rotation) {
36696
+ rotateDatasetCoords(dataset, opts.rotation);
36697
+ }
36698
+ dataset.layers[0].name = opts.name || opts.type || 'shape';
36699
+ return dataset;
36700
+ };
36701
+
36702
+ function getRectangleGeoJSON(opts) {
36703
+ var bbox = opts.bbox,
36704
+ xmin = bbox[0],
36705
+ ymin = bbox[1],
36706
+ xmax = bbox[2],
36707
+ ymax = bbox[3],
36708
+ interval = 0.5,
36709
+ coords = [],
36710
+ type = opts.geometry == 'polyline' ? 'LineString' : 'Polygon';
36711
+ addSide(xmin, ymin, xmin, ymax);
36712
+ addSide(xmin, ymax, xmax, ymax);
36713
+ addSide(xmax, ymax, xmax, ymin);
36714
+ addSide(xmax, ymin, xmin, ymin);
36715
+ coords.push([xmin, ymin]);
36716
+ return {
36717
+ type: type,
36718
+ coordinates: type == 'Polygon' ? [coords] : coords
36719
+ };
36720
+
36721
+ function addSide(x1, y1, x2, y2) {
36722
+ var dx = x2 - x1,
36723
+ dy = y2 - y1,
36724
+ n = Math.ceil(Math.max(Math.abs(dx) / interval, Math.abs(dy) / interval)),
36725
+ xint = dx / n,
36726
+ yint = dy / n;
36727
+ for (var i=0; i<n; i++) {
36728
+ coords.push([x1 + i * xint, y1 + i * yint]);
36729
+ }
36730
+ }
36731
+ }
36732
+
36733
+ function makeCircle(opts) {
36734
+ if (opts.radius > 0 === false && opts.radius_angle > 0 === false) {
36735
+ stop('Missing required radius parameter.');
36736
+ }
36737
+ var cp = opts.center || [0, 0];
36738
+ var radius = opts.radius || getCircleRadiusFromAngle(getCRS('wgs84'), opts.radius_angle);
36739
+ return getCircleGeoJSON(cp, radius, null, {geometry_type : opts.geometry || 'polygon'});
36740
+ }
36741
+
36742
+ function makeShapeFromCoords(opts) {
36743
+ var coordinates = [];
36744
+ var offsets = opts.offsets || [];
36745
+ var coords = opts.coordinates;
36746
+ var type, i, x, y;
36747
+ if (coords.length >= 2 === false) {
36748
+ stop('Invalid coordinates parameter.');
36749
+ }
36750
+ for (i=0; i<coords.length; i+= 2) {
36751
+ x = coords[i];
36752
+ y = coords[i + 1];
36753
+ coordinates.push([x, y]);
36754
+ }
36755
+ for (i=0; i<offsets.length; i+=2) {
36756
+ x += offsets[i];
36757
+ y += offsets[i + 1];
36758
+ coordinates.push([x, y]);
36759
+ }
36760
+ if (GeoJSON.pathIsRing(coordinates)) {
36761
+ type = 'Polygon';
36762
+ } else if (opts.closed && coordinates.length >= 3) {
36763
+ type = 'Polygon';
36764
+ coordinates.push(coordinates[0]);
36765
+ } else {
36766
+ type = 'LineString';
36767
+ }
36768
+ return {
36769
+ type: type,
36770
+ coordinates: type == 'Polygon' ? [coordinates] : coordinates
36771
+ };
36772
+
36773
+ }
36774
+
36270
36775
  function calcSimplifyStats(arcs, use3D) {
36271
36776
  var distSq = use3D ? pointSegGeoDistSq : geom.pointSegDistSq,
36272
36777
  calcAngle = use3D ? geom.signedAngleSph : geom.signedAngle,
@@ -37225,13 +37730,20 @@ ${svg}
37225
37730
 
37226
37731
  // @expression: optional field name or expression
37227
37732
  //
37228
- cmd.splitLayer = function(src, expression, opts) {
37229
- var lyr0 = opts && opts.no_replace ? copyLayer(src) : src,
37733
+ cmd.splitLayer = function(src, expression, optsArg) {
37734
+ var opts = optsArg || {},
37735
+ lyr0 = opts.no_replace ? copyLayer(src) : src,
37230
37736
  properties = lyr0.data ? lyr0.data.getRecords() : null,
37231
37737
  shapes = lyr0.shapes,
37232
37738
  index = {},
37233
37739
  splitLayers = [],
37234
- namer = getSplitNameFunction(lyr0, expression);
37740
+ namer;
37741
+
37742
+ if (opts.ids) {
37743
+ namer = getIdSplitFunction(opts.ids);
37744
+ } else {
37745
+ namer = getSplitNameFunction(lyr0, expression);
37746
+ }
37235
37747
 
37236
37748
  // if (splitField) {
37237
37749
  // internal.requireDataField(lyr0, splitField);
@@ -37263,15 +37775,24 @@ ${svg}
37263
37775
  return splitLayers;
37264
37776
  };
37265
37777
 
37778
+ function getIdSplitFunction(ids) {
37779
+ var set = new Set(ids);
37780
+ return function(i) {
37781
+ return set.has(i) ? '1' : '2';
37782
+ };
37783
+ }
37784
+
37785
+ function getDefaultSplitFunction(lyr) {
37786
+ // if not splitting on an expression and layer is unnamed, name split-apart layers
37787
+ // like: split-1, split-2, ...
37788
+ return function(i) {
37789
+ return (lyr && lyr.name || 'split') + '-' + (i + 1);
37790
+ };
37791
+ }
37792
+
37266
37793
  function getSplitNameFunction(lyr, exp) {
37267
37794
  var compiled;
37268
- if (!exp) {
37269
- // if not splitting on an expression and layer is unnamed, name split-apart layers
37270
- // like: split-1, split-2, ...
37271
- return function(i) {
37272
- return (lyr && lyr.name || 'split') + '-' + (i + 1);
37273
- };
37274
- }
37795
+ if (!exp) return getDefaultSplitFunction(lyr);
37275
37796
  lyr = {name: lyr.name, data: lyr.data}; // remove shape info
37276
37797
  compiled = compileValueExpression(exp, lyr, null);
37277
37798
  return function(i) {
@@ -37310,111 +37831,6 @@ ${svg}
37310
37831
  getSplitNameFunction: getSplitNameFunction
37311
37832
  });
37312
37833
 
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
37834
  cmd.svgStyle = function(lyr, dataset, opts) {
37419
37835
  var filter;
37420
37836
  if (!lyr.data) {
@@ -37443,56 +37859,83 @@ ${svg}
37443
37859
  });
37444
37860
  };
37445
37861
 
37446
- symbolBuilders.arrow = function(d) {
37447
- var len = 'length' in d ? d.length : 10;
37448
- var filled = 'fill' in d;
37449
- return filled ? getFilledArrow(d, len) : getStickArrow(d, len);
37450
- };
37862
+ var roundCoord = getRoundingFunction(0.01);
37451
37863
 
37452
- function getStickArrow(d, len) {
37453
- return {
37454
- type: 'polyline',
37455
- coordinates: getStickArrowCoords(d, len),
37456
- stroke: d.stroke || 'magenta',
37457
- 'stroke-width': 'stroke-width' in d ? d['stroke-width'] : 1
37458
- };
37864
+ function forEachSymbolCoord(coords, cb) {
37865
+ var isPoint = coords && utils.isNumber(coords[0]);
37866
+ var isNested = !isPoint && coords && Array.isArray(coords[0]);
37867
+ if (isPoint) return cb(coords);
37868
+ for (var i=0; i<coords.length; i++) {
37869
+ if (isNested) forEachSymbolCoord(coords[i], cb);
37870
+ }
37459
37871
  }
37460
37872
 
37461
- function getFilledArrow(d, totalLen) {
37462
- return {
37463
- type: 'polygon',
37464
- coordinates: getFilledArrowCoords(d, totalLen),
37465
- fill: d.fill || 'magenta'
37466
- };
37873
+ function flipY(coords) {
37874
+ forEachSymbolCoord(coords, function(p) {
37875
+ p[1] = -p[1];
37876
+ });
37467
37877
  }
37468
37878
 
37469
- function getScale(totalLen, headLen) {
37470
- var maxHeadPct = 0.60;
37471
- var headPct = headLen / totalLen;
37472
- if (headPct > maxHeadPct) {
37473
- return maxHeadPct / headPct;
37474
- }
37475
- return 1;
37879
+ function scaleAndShiftCoords(coords, scale, shift) {
37880
+ forEachSymbolCoord(coords, function(xy) {
37881
+ xy[0] = xy[0] * scale + shift[0];
37882
+ xy[1] = xy[1] * scale + shift[1];
37883
+ });
37476
37884
  }
37477
37885
 
37478
- function getStickArrowTip(totalLen, curve) {
37479
- // curve/2 intersects the arrowhead at 90deg (trigonometry)
37480
- var theta = Math.abs(curve/2) / 180 * Math.PI;
37481
- var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1);
37482
- var dy = totalLen * Math.cos(theta);
37483
- return [dx, dy];
37886
+ function roundCoordsForSVG(coords) {
37887
+ forEachSymbolCoord(coords, function(p) {
37888
+ p[0] = roundCoord(p[0]);
37889
+ p[1] = roundCoord(p[1]);
37890
+ });
37484
37891
  }
37485
37892
 
37486
- function addPoints(a, b) {
37487
- return [a[0] + b[0], a[1] + b[1]];
37893
+ function rotateCoords(coords, rotation) {
37894
+ if (!rotation) return;
37895
+ var f = getAffineTransform(rotation, 1, [0, 0], [0, 0]);
37896
+ forEachSymbolCoord(coords, function(p) {
37897
+ var p2 = f(p[0], p[1]);
37898
+ p[0] = p2[0];
37899
+ p[1] = p2[1];
37900
+ });
37901
+ }
37902
+
37903
+ function findArcCenter(p1, p2, degrees) {
37904
+ var p3 = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2], // midpoint betw. p1, p2
37905
+ tan = 1 / Math.tan(degrees / 180 * Math.PI / 2),
37906
+ cp = getAffineTransform(90, tan, [0, 0], p3)(p2[0], p2[1]);
37907
+ return cp;
37908
+ }
37909
+
37910
+ // export function addBezierArcControlPoints(p1, p2, degrees) {
37911
+ function addBezierArcControlPoints(points, degrees) {
37912
+ // source: https://stackoverflow.com/questions/734076/how-to-best-approximate-a-geometrical-arc-with-a-bezier-curve
37913
+ var p2 = points.pop(),
37914
+ p1 = points.pop(),
37915
+ cp = findArcCenter(p1, p2, degrees),
37916
+ xc = cp[0],
37917
+ yc = cp[1],
37918
+ ax = p1[0] - xc,
37919
+ ay = p1[1] - yc,
37920
+ bx = p2[0] - xc,
37921
+ by = p2[1] - yc,
37922
+ q1 = ax * ax + ay * ay,
37923
+ q2 = q1 + ax * bx + ay * by,
37924
+ k2 = 4/3 * (Math.sqrt(2 * q1 * q2) - q2) / (ax * by - ay * bx);
37925
+
37926
+ points.push(p1);
37927
+ points.push([xc + ax - k2 * ay, yc + ay + k2 * ax, 'C']);
37928
+ points.push([xc + bx + k2 * by, yc + by - k2 * bx, 'C']);
37929
+ points.push(p2);
37488
37930
  }
37489
37931
 
37490
37932
  function getStickArrowCoords(d, totalLen) {
37933
+ var minStemRatio = getMinStemRatio(d);
37491
37934
  var headAngle = d['arrow-head-angle'] || 90;
37492
37935
  var curve = d['arrow-stem-curve'] || 0;
37493
37936
  var unscaledHeadWidth = d['arrow-head-width'] || 9;
37494
37937
  var unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle);
37495
- var scale = getScale(totalLen, unscaledHeadLen); // scale down small arrows
37938
+ var scale = getScale(totalLen, unscaledHeadLen, minStemRatio);
37496
37939
  var headWidth = unscaledHeadWidth * scale;
37497
37940
  var headLen = unscaledHeadLen * scale;
37498
37941
  var tip = getStickArrowTip(totalLen, curve);
@@ -37503,84 +37946,268 @@ ${svg}
37503
37946
  if (!headLen) return [stem];
37504
37947
  var head = [addPoints([-headWidth / 2, -headLen], tip), tip.concat(), addPoints([headWidth / 2, -headLen], tip)];
37505
37948
 
37506
- rotateSymbolCoords(stem, d.rotation);
37507
- rotateSymbolCoords(head, d.rotation);
37949
+ rotateCoords(stem, d.rotation);
37950
+ rotateCoords(head, d.rotation);
37508
37951
  return [stem, head];
37509
37952
  }
37510
37953
 
37511
- function getHeadLength(headWidth, headAngle) {
37512
- var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio
37513
- return headWidth * headRatio;
37954
+ function getMinStemRatio(d) {
37955
+ return d['arrow-min-stem'] >= 0 ? d['arrow-min-stem'] : 0.4;
37514
37956
  }
37515
37957
 
37516
- function getFilledArrowCoords(d, totalLen) {
37517
- var headAngle = d['arrow-head-angle'] || 40,
37958
+ function getFilledArrowCoords(totalLen, d) {
37959
+ var minStemRatio = getMinStemRatio(d),
37960
+ headAngle = d['arrow-head-angle'] || 40,
37961
+ direction = d.rotation || d['arrow-direction'] || 0,
37518
37962
  unscaledStemWidth = d['arrow-stem-width'] || 2,
37519
37963
  unscaledHeadWidth = d['arrow-head-width'] || unscaledStemWidth * 3,
37520
37964
  unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle),
37521
- scale = getScale(totalLen, unscaledHeadLen), // scale down small arrows
37965
+ scale = getScale(totalLen, unscaledHeadLen, minStemRatio),
37522
37966
  headWidth = unscaledHeadWidth * scale,
37523
37967
  headLen = unscaledHeadLen * scale,
37524
37968
  stemWidth = unscaledStemWidth * scale,
37525
37969
  stemTaper = d['arrow-stem-taper'] || 0,
37970
+ stemCurve = d['arrow-stem-curve'] || 0,
37526
37971
  stemLen = totalLen - headLen;
37527
37972
 
37528
37973
  var headDx = headWidth / 2,
37529
37974
  stemDx = stemWidth / 2,
37530
37975
  baseDx = stemDx * (1 - stemTaper);
37531
37976
 
37532
- var coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
37977
+ var coords;
37978
+
37979
+ if (!stemCurve || Math.abs(stemCurve) > 90) {
37980
+ coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
37533
37981
  [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]];
37982
+ } else {
37983
+ if (direction > 0) stemCurve = -stemCurve;
37984
+ coords = getCurvedArrowCoords(stemLen, headLen, stemCurve, stemDx, headDx, baseDx);
37985
+ }
37534
37986
 
37535
- rotateSymbolCoords(coords, d.rotation);
37987
+ rotateCoords(coords, direction);
37536
37988
  return [coords];
37537
37989
  }
37538
37990
 
37539
- function rotateSymbolCoords(coords, rotation) {
37540
- // TODO: consider avoiding re-instantiating function on every call
37541
- var f = getAffineTransform(rotation || 0, 1, [0, 0], [0, 0]);
37542
- coords.forEach(function(p) {
37543
- var p2 = f ? f(p[0], p[1]) : p;
37544
- p[0] = p2[0];
37545
- p[1] = -p2[1]; // flip y-axis (to produce display coords)
37546
- });
37991
+
37992
+ function getScale(totalLen, headLen, minStemRatio) {
37993
+ var maxHeadPct = 1 - minStemRatio;
37994
+ var headPct = headLen / totalLen;
37995
+ if (headPct > maxHeadPct) {
37996
+ return maxHeadPct / headPct;
37997
+ }
37998
+ return 1;
37547
37999
  }
37548
38000
 
37549
- // TODO: refactor to remove duplication in mapshaper-svg-style.js
37550
- cmd.symbols = function(lyr, opts) {
37551
- var f, filter;
37552
- // console.log("-symbols opts", opts)
37553
- requirePointLayer(lyr);
37554
- f = getSymbolDataAccessor(lyr, opts);
37555
- if (opts.where) {
37556
- filter = compileValueExpression(opts.where, lyr, null);
38001
+ function getStickArrowTip(totalLen, curve) {
38002
+ // curve/2 intersects the arrowhead at 90deg (trigonometry)
38003
+ var theta = Math.abs(curve/2) / 180 * Math.PI;
38004
+ var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1);
38005
+ var dy = totalLen * Math.cos(theta);
38006
+ return [dx, dy];
38007
+ }
38008
+
38009
+ function addPoints(a, b) {
38010
+ return [a[0] + b[0], a[1] + b[1]];
38011
+ }
38012
+
38013
+
38014
+ function getHeadLength(headWidth, headAngle) {
38015
+ var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio
38016
+ return headWidth * headRatio;
38017
+ }
38018
+
38019
+ function getCurvedArrowCoords(stemLen, headLen, curvature, stemDx, headDx, baseDx) {
38020
+ // coordinates go counter clockwise, starting from the leftmost head coordinate
38021
+ var theta = Math.abs(curvature) / 180 * Math.PI;
38022
+ var sign = curvature > 0 ? 1 : -1;
38023
+ var dx = stemLen * Math.sin(theta / 2) * sign;
38024
+ var dy = stemLen * Math.cos(theta / 2);
38025
+ var head = [[stemDx + dx, dy], [headDx + dx, dy],
38026
+ [dx, headLen + dy], [-headDx + dx, dy], [-stemDx + dx, dy]];
38027
+ var ax = baseDx * Math.cos(theta); // rotate arrow base
38028
+ var ay = baseDx * Math.sin(theta) * -sign;
38029
+ var leftStem = getCurvedStemCoords(-ax, -ay, -stemDx + dx, dy, theta);
38030
+ var rightStem = getCurvedStemCoords(ax, ay, stemDx + dx, dy, theta);
38031
+ // if (stemTaper == 1) leftStem.pop();
38032
+ var stem = leftStem.concat(rightStem.reverse());
38033
+ stem.pop();
38034
+ return stem.concat(head);
38035
+ }
38036
+
38037
+ // ax, ay: point on the base
38038
+ // bx, by: point on the stem
38039
+ function getCurvedStemCoords(ax, ay, bx, by, theta0) {
38040
+ var dx = bx - ax,
38041
+ dy = by - ay,
38042
+ dy1 = (dy * dy - dx * dx) / (2 * dy),
38043
+ dy2 = dy - dy1,
38044
+ dx2 = Math.sqrt(dx * dx + dy * dy) / 2,
38045
+ theta = Math.PI - Math.asin(dx2 / dy2) * 2,
38046
+ degrees = theta * 180 / Math.PI,
38047
+ radius = dy2 / Math.tan(theta / 2),
38048
+ leftBend = bx > ax,
38049
+ sign = leftBend ? 1 : -1,
38050
+ points = Math.round(degrees / 5) + 2,
38051
+ // points = theta > 2 && 7 || theta > 1 && 6 || 5,
38052
+ increment = theta / (points + 1);
38053
+
38054
+ var coords = [[bx, by]];
38055
+ for (var i=1; i<= points; i++) {
38056
+ var phi = i * increment / 2;
38057
+ var sinPhi = Math.sin(phi);
38058
+ var cosPhi = Math.cos(phi);
38059
+ var c = sinPhi * radius * 2;
38060
+ var a = sinPhi * c;
38061
+ var b = cosPhi * c;
38062
+ coords.push([bx - a * sign, by - b]);
38063
+ }
38064
+ coords.push([ax, ay]);
38065
+ return coords;
38066
+ }
38067
+
38068
+ // sides: e.g. 5-pointed star has 10 sides
38069
+ // radius: distance from center to point
38070
+ //
38071
+ function getPolygonCoords(radius, opts) {
38072
+ var type = opts.type;
38073
+ var sides = +opts.sides || getDefaultSides(type);
38074
+ var isStar = type == 'star';
38075
+ if (isStar && (sides < 6 || sides % 2 !== 0)) {
38076
+ stop(`Invalid number of sides for a star (${sides})`);
38077
+ } else if (sides >= 3 === false) {
38078
+ stop(`Invalid number of sides (${sides})`);
37557
38079
  }
37558
- getLayerDataTable(lyr).getRecords().forEach(function(rec, i) {
37559
- if (filter && filter(i)) {
37560
- if ('svg-symbol' in rec === false) {
37561
- rec['svg-symbol'] = undefined;
37562
- }
38080
+ var coords = [],
38081
+ angle = 360 / sides,
38082
+ b = isStar ? 1 : 0.5,
38083
+ theta, even, len;
38084
+ if (opts.orientation == 'b') {
38085
+ b = 0;
38086
+ }
38087
+ for (var i=0; i<sides; i++) {
38088
+ even = i % 2 == 0;
38089
+ len = radius;
38090
+ if (isStar && even) {
38091
+ len *= (opts.star_ratio || 0.5);
38092
+ }
38093
+ theta = (i + b) * angle % 360;
38094
+ coords.push(getPlanarSegmentEndpoint(0, 0, theta, len));
38095
+ }
38096
+ coords.push(coords[0].concat());
38097
+ return [coords];
38098
+ }
38099
+
38100
+ function getDefaultSides(type) {
38101
+ return {
38102
+ star: 10,
38103
+ circle: 72,
38104
+ triangle: 3,
38105
+ square: 4,
38106
+ pentagon: 5,
38107
+ hexagon: 6,
38108
+ heptagon: 7,
38109
+ octagon: 8,
38110
+ nonagon: 9,
38111
+ decagon: 10
38112
+ }[type] || 4;
38113
+ }
38114
+
38115
+ // TODO: refactor to remove duplication in mapshaper-svg-style.js
38116
+ cmd.symbols = function(inputLyr, dataset, opts) {
38117
+ requireSinglePointLayer(inputLyr);
38118
+ var lyr = opts.no_replace ? copyLayer(inputLyr) : inputLyr;
38119
+ var polygonMode = !!opts.polygons;
38120
+ var metersPerPx;
38121
+ if (polygonMode) {
38122
+ requireProjectedDataset(dataset);
38123
+ metersPerPx = opts.pixel_scale || getMetersPerPixel(lyr, dataset);
38124
+ }
38125
+ var records = getLayerDataTable(lyr).getRecords();
38126
+ var getSymbolData = getSymbolDataAccessor(lyr, opts);
38127
+ var geometries = lyr.shapes.map(function(shp, i) {
38128
+ if (!shp) return null;
38129
+ var d = getSymbolData(i);
38130
+ var rec = records[i] || {};
38131
+ var coords, size, constructor;
38132
+ if (d.type == 'arrow') {
38133
+ size = d.radius || d.length || d['arrow-length'] || d.r;
38134
+ constructor = getFilledArrowCoords;
37563
38135
  } else {
37564
- rec['svg-symbol'] = buildSymbol(f(i));
38136
+ size = d.radius || d.length || d.r;
38137
+ constructor = getPolygonCoords;
38138
+ }
38139
+ if (size > 0 === false) return null;
38140
+ coords = constructor(size, d, opts);
38141
+ rotateCoords(coords, +d.rotation || 0);
38142
+ if (!polygonMode) {
38143
+ flipY(coords);
38144
+ }
38145
+ if (+opts.scale) {
38146
+ scaleAndShiftCoords(coords, +opts.scale, [0, 0]);
38147
+ }
38148
+ if (polygonMode) {
38149
+ scaleAndShiftCoords(coords, metersPerPx, shp[0]);
38150
+ if (d.tfill) rec.fill = d.fill;
38151
+ return createGeometry(coords);
38152
+ } else {
38153
+ rec['svg-symbol'] = makeSvgSymbol(coords, d);
37565
38154
  }
37566
38155
  });
38156
+
38157
+ var outputLyr, dataset2;
38158
+ if (polygonMode) {
38159
+ dataset2 = importGeometries(geometries, records);
38160
+ outputLyr = mergeOutputLayerIntoDataset(inputLyr, dataset, dataset2, opts);
38161
+ outputLyr.data = lyr.data;
38162
+ } else {
38163
+ outputLyr = lyr;
38164
+ }
38165
+ return [outputLyr];
37567
38166
  };
37568
38167
 
38168
+ function importGeometries(geometries, records) {
38169
+ var features = geometries.map(function(geom, i) {
38170
+ var d = records[i];
38171
+ return {
38172
+ type: 'Feature',
38173
+ properties: records[i] || null,
38174
+ geometry: geom
38175
+ };
38176
+ });
38177
+ var geojson = {
38178
+ type: 'FeatureCollection',
38179
+ features: features
38180
+ };
38181
+ return importGeoJSON(geojson);
38182
+ }
38183
+
38184
+ function createGeometry(coords) {
38185
+ return {
38186
+ type: 'Polygon',
38187
+ coordinates: coords
38188
+ };
38189
+ }
38190
+
38191
+ function getMetersPerPixel(lyr, dataset) {
38192
+
38193
+ // TODO: handle single point, no extent
38194
+ var bounds = getLayerBounds(lyr);
38195
+ return bounds.width() / 800;
38196
+ }
38197
+
37569
38198
  // Returns an svg-symbol data object for one symbol
37570
- function buildSymbol(properties) {
37571
- var type = properties.type;
37572
- var f = symbolBuilders[type];
37573
- if (!type) {
37574
- stop('Missing required "type" parameter');
37575
- } else if (!f) {
37576
- stop('Unknown symbol type:', type);
37577
- }
37578
- return f(properties);
38199
+ function makeSvgSymbol(coords, properties) {
38200
+ roundCoordsForSVG(coords);
38201
+ return {
38202
+ type: 'polygon',
38203
+ coordinates: coords,
38204
+ fill: properties.fill || 'magenta'
38205
+ };
37579
38206
  }
37580
38207
 
37581
38208
  var Symbols = /*#__PURE__*/Object.freeze({
37582
38209
  __proto__: null,
37583
- buildSymbol: buildSymbol
38210
+ makeSvgSymbol: makeSvgSymbol
37584
38211
  });
37585
38212
 
37586
38213
  cmd.target = function(catalog, opts) {
@@ -37726,99 +38353,6 @@ ${svg}
37726
38353
  }
37727
38354
  };
37728
38355
 
37729
- cmd.shape = function(targetDataset, opts) {
37730
- var geojson, dataset;
37731
- if (opts.coordinates) {
37732
- geojson = makeShapeFromCoords(opts);
37733
- } else if (opts.type == 'circle') {
37734
- geojson = makeCircle(opts);
37735
- } else if (opts.type == 'rectangle' && opts.bbox) {
37736
- geojson = getRectangleGeoJSON(opts);
37737
- } else {
37738
- stop('Missing coordinates parameter');
37739
- }
37740
- // TODO: project shape if targetDataset is projected
37741
- dataset = importGeoJSON(geojson, {});
37742
- if (opts.rotation) {
37743
- rotateDatasetCoords(dataset, opts.rotation);
37744
- }
37745
- dataset.layers[0].name = opts.name || opts.type || 'shape';
37746
- return dataset;
37747
- };
37748
-
37749
- function getRectangleGeoJSON(opts) {
37750
- var bbox = opts.bbox,
37751
- xmin = bbox[0],
37752
- ymin = bbox[1],
37753
- xmax = bbox[2],
37754
- ymax = bbox[3],
37755
- interval = 0.5,
37756
- coords = [],
37757
- type = opts.geometry == 'polyline' ? 'LineString' : 'Polygon';
37758
- addSide(xmin, ymin, xmin, ymax);
37759
- addSide(xmin, ymax, xmax, ymax);
37760
- addSide(xmax, ymax, xmax, ymin);
37761
- addSide(xmax, ymin, xmin, ymin);
37762
- coords.push([xmin, ymin]);
37763
- return {
37764
- type: type,
37765
- coordinates: type == 'Polygon' ? [coords] : coords
37766
- };
37767
-
37768
- function addSide(x1, y1, x2, y2) {
37769
- var dx = x2 - x1,
37770
- dy = y2 - y1,
37771
- n = Math.ceil(Math.max(Math.abs(dx) / interval, Math.abs(dy) / interval)),
37772
- xint = dx / n,
37773
- yint = dy / n;
37774
- for (var i=0; i<n; i++) {
37775
- coords.push([x1 + i * xint, y1 + i * yint]);
37776
- }
37777
- }
37778
- }
37779
-
37780
- function makeCircle(opts) {
37781
- if (opts.radius > 0 === false && opts.radius_angle > 0 === false) {
37782
- stop('Missing required radius parameter.');
37783
- }
37784
- var cp = opts.center || [0, 0];
37785
- var radius = opts.radius || getCircleRadiusFromAngle(getCRS('wgs84'), opts.radius_angle);
37786
- return getCircleGeoJSON(cp, radius, null, {geometry_type : opts.geometry || 'polygon'});
37787
- }
37788
-
37789
- function makeShapeFromCoords(opts) {
37790
- var coordinates = [];
37791
- var offsets = opts.offsets || [];
37792
- var coords = opts.coordinates;
37793
- var type, i, x, y;
37794
- if (coords.length >= 2 === false) {
37795
- stop('Invalid coordinates parameter.');
37796
- }
37797
- for (i=0; i<coords.length; i+= 2) {
37798
- x = coords[i];
37799
- y = coords[i + 1];
37800
- coordinates.push([x, y]);
37801
- }
37802
- for (i=0; i<offsets.length; i+=2) {
37803
- x += offsets[i];
37804
- y += offsets[i + 1];
37805
- coordinates.push([x, y]);
37806
- }
37807
- if (GeoJSON.pathIsRing(coordinates)) {
37808
- type = 'Polygon';
37809
- } else if (opts.closed && coordinates.length >= 3) {
37810
- type = 'Polygon';
37811
- coordinates.push(coordinates[0]);
37812
- } else {
37813
- type = 'LineString';
37814
- }
37815
- return {
37816
- type: type,
37817
- coordinates: type == 'Polygon' ? [coordinates] : coordinates
37818
- };
37819
-
37820
- }
37821
-
37822
38356
  cmd.variableSimplify = function(layers, dataset, opts) {
37823
38357
  var lyr = layers[0];
37824
38358
  var arcs = dataset.arcs;
@@ -38176,6 +38710,9 @@ ${svg}
38176
38710
  } else if (name == 'colorizer') {
38177
38711
  outputLayers = cmd.colorizer(opts);
38178
38712
 
38713
+ } else if (name == 'dashlines') {
38714
+ applyCommandToEachLayer(cmd.dashlines, targetLayers, targetDataset, opts);
38715
+
38179
38716
  } else if (name == 'define') {
38180
38717
  cmd.define(opts);
38181
38718
 
@@ -38357,6 +38894,9 @@ ${svg}
38357
38894
  } else if (name == 'shape') {
38358
38895
  catalog.addDataset(cmd.shape(targetDataset, opts));
38359
38896
 
38897
+ } else if (name == 'shapes') {
38898
+ outputLayers = applyCommandToEachLayer(cmd.shapes, targetLayers, targetDataset, opts);
38899
+
38360
38900
  } else if (name == 'simplify') {
38361
38901
  if (opts.variable) {
38362
38902
  cmd.variableSimplify(targetLayers, targetDataset, opts);
@@ -38376,9 +38916,6 @@ ${svg}
38376
38916
  } else if (name == 'split') {
38377
38917
  outputLayers = applyCommandToEachLayer(cmd.splitLayer, targetLayers, opts.expression, opts);
38378
38918
 
38379
- } else if (name == 'split-lines') {
38380
- applyCommandToEachLayer(cmd.splitLines, targetLayers, targetDataset, opts);
38381
-
38382
38919
  } else if (name == 'split-on-grid') {
38383
38920
  outputLayers = applyCommandToEachLayer(cmd.splitLayerOnGrid, targetLayers, arcs, opts);
38384
38921
 
@@ -38389,7 +38926,7 @@ ${svg}
38389
38926
  applyCommandToEachLayer(cmd.svgStyle, targetLayers, targetDataset, opts);
38390
38927
 
38391
38928
  } else if (name == 'symbols') {
38392
- applyCommandToEachLayer(cmd.symbols, targetLayers, opts);
38929
+ outputLayers = applyCommandToEachLayer(cmd.symbols, targetLayers, targetDataset, opts);
38393
38930
 
38394
38931
  } else if (name == 'subdivide') {
38395
38932
  outputLayers = applyCommandToEachLayer(cmd.subdivideLayer, targetLayers, arcs, opts.expression);