mapshaper 0.5.77 → 0.5.81

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/mapshaper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  (function () {
2
2
 
3
- var VERSION = "0.5.76";
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 = utils.rtrim(s);
18036
+ }
18037
+ s = utils.ltrim(s);
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;
@@ -19191,6 +19363,10 @@ ${svg}
19191
19363
  .option('keep-shapes', {
19192
19364
  type: 'flag'
19193
19365
  })
19366
+ .option('ids', {
19367
+ // describe: 'filter on a list of feature ids',
19368
+ type: 'numbers'
19369
+ })
19194
19370
  .option('cleanup', {type: 'flag'}) // TODO: document
19195
19371
  .option('name', nameOpt)
19196
19372
  .option('target', targetOpt)
@@ -19313,6 +19489,13 @@ ${svg}
19313
19489
  })
19314
19490
  .option('target', targetOpt);
19315
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
+
19316
19499
  parser.command('inlay')
19317
19500
  .describe('inscribe a polygon layer inside another polygon layer')
19318
19501
  .option('source', {
@@ -19625,6 +19808,7 @@ ${svg}
19625
19808
  })
19626
19809
  .option('target', targetOpt);
19627
19810
 
19811
+
19628
19812
  parser.command('simplify')
19629
19813
  .validate(validateSimplifyOpts)
19630
19814
  .example('Retain 10% of removable vertices\n$ mapshaper input.shp -simplify 10%')
@@ -19759,6 +19943,11 @@ ${svg}
19759
19943
  DEFAULT: true,
19760
19944
  describe: 'expression or field for grouping features and naming split layers'
19761
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
+ })
19762
19951
  .option('apart', {
19763
19952
  describe: 'save output layers to independent datasets',
19764
19953
  type: 'flag'
@@ -19858,26 +20047,81 @@ ${svg}
19858
20047
  .option('target', targetOpt);
19859
20048
 
19860
20049
  parser.command('symbols')
19861
- // .describe('generate a variety of SVG symbols')
20050
+ // .describe('symbolize points as polygons, circles, stars or arrows')
19862
20051
  .option('type', {
19863
- 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'
19864
20111
  })
19865
20112
  .option('stroke', {})
19866
20113
  .option('stroke-width', {})
19867
- .option('fill', {})
19868
- .option('length', {})
19869
- .option('rotation', {})
20114
+ .option('fill', {
20115
+ describe: 'symbol fill color'
20116
+ })
19870
20117
  .option('effect', {})
19871
- .option('arrow-head-angle', {})
19872
- .option('arrow-stem-width', {})
19873
- .option('arrow-head-width', {})
19874
- .option('arrow-stem-curve', {})
19875
- .option('arrow-stem-taper', {})
19876
- .option('arrow-scaling', {})
19877
- .option('where', whereOpt)
19878
- .option('target', targetOpt);
20118
+ // .option('where', whereOpt)
20119
+ .option('name', nameOpt)
20120
+ .option('target', targetOpt)
20121
+ .option('no-replace', noReplaceOpt);
19879
20122
  // .option('name', nameOpt);
19880
20123
 
20124
+
19881
20125
  parser.command('target')
19882
20126
  .describe('set active layer (or layers)')
19883
20127
  .option('target', {
@@ -20028,13 +20272,6 @@ ${svg}
20028
20272
  })
20029
20273
  .option('name', nameOpt);
20030
20274
 
20031
- parser.command('include')
20032
- .describe('import JS data and functions for use in JS expressions')
20033
- .option('file', {
20034
- DEFAULT: true,
20035
- describe: 'file containing a JS object with key:value pairs to import'
20036
- });
20037
-
20038
20275
  parser.command('fuzzy-join')
20039
20276
  .describe('join points to polygons, with data fill and fuzzy match')
20040
20277
  .option('source', {
@@ -21033,12 +21270,13 @@ ${svg}
21033
21270
  if (this instanceof ShpReader === false) {
21034
21271
  return new ShpReader(shpSrc, shxSrc);
21035
21272
  }
21036
-
21037
21273
  var shpFile = utils.isString(shpSrc) ? new FileReader(shpSrc) : new BufferReader(shpSrc);
21038
21274
  var header = parseHeader(shpFile.readToBinArray(0, 100));
21039
- var shpSize = shpFile.size();
21040
- var RecordClass = new ShpRecordClass(header.type);
21041
- 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);
21042
21280
  var shxBin, shxFile;
21043
21281
 
21044
21282
  if (shxSrc) {
@@ -21046,8 +21284,6 @@ ${svg}
21046
21284
  shxBin = shxFile.readToBinArray(0, shxFile.size()).bigEndian();
21047
21285
  }
21048
21286
 
21049
- reset();
21050
-
21051
21287
  this.header = function() {
21052
21288
  return header;
21053
21289
  };
@@ -21065,62 +21301,35 @@ ${svg}
21065
21301
 
21066
21302
  // Iterator interface for reading shape records
21067
21303
  this.nextShape = function() {
21068
- 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);
21069
21310
  if (!shape) {
21070
- if (skippedBytes > 0) {
21071
- // Encountered in files from natural earth v2.0.0:
21072
- // ne_10m_admin_0_boundary_lines_land.shp
21073
- // ne_110m_admin_0_scale_rank.shp
21074
- verbose("Skipped over " + skippedBytes + " non-data bytes in the .shp file.");
21075
- }
21076
- shpFile.close();
21077
- reset();
21311
+ done();
21312
+ return null;
21078
21313
  }
21314
+ recordCount++;
21079
21315
  return shape;
21080
21316
  };
21081
21317
 
21082
- function readNextShape() {
21083
- var expectedId = recordCount + 1; // Shapefile ids are 1-based
21084
- var shape, offset;
21085
- if (done()) return null;
21086
- if (shxBin) {
21087
- shxBin.position(100 + recordCount * 8);
21088
- offset = shxBin.readUint32() * 2;
21089
- if (offset > shpOffset) {
21090
- skippedBytes += offset - shpOffset;
21091
- }
21092
- } else {
21093
- offset = shpOffset;
21094
- }
21095
- shape = readShapeAtOffset(offset);
21096
- if (!shape) {
21097
- // Some in-the-wild .shp files contain junk bytes between records. This
21098
- // is a problem if the .shx index file is not present.
21099
- // Here, we try to scan past the junk to find the next record.
21100
- shape = huntForNextShape(offset, expectedId);
21101
- }
21102
- if (shape) {
21103
- if (shape.id < expectedId) {
21104
- message("Found a Shapefile record with the same id as a previous record (" + shape.id + ") -- skipping.");
21105
- return readNextShape();
21106
- } else if (shape.id > expectedId) {
21107
- stop("Shapefile contains an out-of-sequence record. Possible data corruption -- bailing.");
21108
- }
21109
- recordCount++;
21110
- }
21111
- 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);
21112
21325
  }
21113
21326
 
21114
21327
  function done() {
21115
- if (shxFile && shxFile.size() <= 100 + recordCount * 8) return true;
21116
- if (shpOffset + 12 > shpSize) return true;
21117
- return false;
21118
- }
21119
-
21120
- function reset() {
21121
- shpOffset = 100;
21122
- skippedBytes = 0;
21123
- 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
+ }
21124
21333
  }
21125
21334
 
21126
21335
  function parseHeader(bin) {
@@ -21149,47 +21358,81 @@ ${svg}
21149
21358
  return header;
21150
21359
  }
21151
21360
 
21152
- function readShapeAtOffset(offset) {
21153
- var shape = null,
21154
- recordSize, recordType, recordId, goodSize, goodType, bin;
21155
21361
 
21156
- if (offset + 12 <= shpSize) {
21157
- bin = shpFile.readToBinArray(offset, 12);
21158
- recordId = bin.bigEndian().readUint32();
21159
- // record size is bytes in content section + 8 header bytes
21160
- recordSize = bin.readUint32() * 2 + 8;
21161
- recordType = bin.littleEndian().readUint32();
21162
- goodSize = offset + recordSize <= shpSize && recordSize >= 12;
21163
- goodType = recordType === 0 || recordType == header.type;
21164
- if (goodSize && goodType) {
21165
- bin = shpFile.readToBinArray(offset, recordSize);
21166
- shape = new RecordClass(bin, recordSize);
21167
- shpOffset = offset + shape.byteLength; // advance read position
21168
- }
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.');
21169
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
21170
21397
  return shape;
21171
21398
  }
21172
21399
 
21173
- // TODO: add tests
21174
- // Try to scan past unreadable content to find next record
21175
- function huntForNextShape(start, id) {
21176
- 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(),
21177
21411
  shape = null,
21178
- bin, recordId, recordType, count;
21179
- while (offset + 12 <= shpSize) {
21412
+ bin, recordId, recordType, isValidType;
21413
+ while (offset + 12 <= fileSize) {
21180
21414
  bin = shpFile.readToBinArray(offset, 12);
21181
21415
  recordId = bin.bigEndian().readUint32();
21182
21416
  recordType = bin.littleEndian().skipBytes(4).readUint32();
21183
- if (recordId == id && (recordType == header.type || recordType === 0)) {
21184
- // we have a likely position, but may still be unparsable
21185
- shape = readShapeAtOffset(offset);
21186
- 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.");
21187
21431
  }
21188
- offset += 4; // try next integer position
21189
21432
  }
21190
- count = shape ? offset - start : shpSize - start;
21191
- // debug('Skipped', count, 'bytes', shape ? 'before record ' + id : 'at the end of the file');
21192
- skippedBytes += count;
21433
+ if (shape && offset > start) {
21434
+ verbose("Skipped over " + (offset - start) + " non-data bytes in the .shp file.");
21435
+ }
21193
21436
  return shape;
21194
21437
  }
21195
21438
  }
@@ -21198,22 +21441,6 @@ ${svg}
21198
21441
  return this.header().type;
21199
21442
  };
21200
21443
 
21201
- ShpReader.prototype.getCounts = function() {
21202
- var counts = {
21203
- nullCount: 0,
21204
- partCount: 0,
21205
- shapeCount: 0,
21206
- pointCount: 0
21207
- };
21208
- this.forEachShape(function(shp) {
21209
- if (shp.isNull) counts.nullCount++;
21210
- counts.pointCount += shp.pointCount;
21211
- counts.partCount += shp.partCount;
21212
- counts.shapeCount++;
21213
- });
21214
- return counts;
21215
- };
21216
-
21217
21444
  // Apply snapping, remove duplicate coords and clean up defective paths in a dataset
21218
21445
  // Assumes that any CRS info has been added to the dataset
21219
21446
  // @opts: import options
@@ -26022,7 +26249,6 @@ ${svg}
26022
26249
  if (constTol) return constTol;
26023
26250
  return constTol ? constTol : meterDist * pctOfRadius;
26024
26251
  };
26025
-
26026
26252
  }
26027
26253
 
26028
26254
  function getBufferDistanceFunction(lyr, dataset, opts) {
@@ -27195,7 +27421,7 @@ ${svg}
27195
27421
  cmd.buffer = makeBufferLayer;
27196
27422
 
27197
27423
  function makeBufferLayer(lyr, dataset, opts) {
27198
- var dataset2, lyr2;
27424
+ var dataset2;
27199
27425
  if (lyr.geometry_type == 'point') {
27200
27426
  dataset2 = makePointBuffer(lyr, dataset, opts);
27201
27427
  } else if (lyr.geometry_type == 'polyline') {
@@ -27205,13 +27431,9 @@ ${svg}
27205
27431
  } else {
27206
27432
  stop("Unsupported geometry type");
27207
27433
  }
27208
- var outputLayers = mergeDatasetsIntoDataset(dataset, [dataset2]);
27209
- lyr2 = outputLayers[0];
27210
- setOutputLayerName(lyr2, lyr, null, opts);
27211
- if (lyr.data && !lyr2.data) {
27212
- lyr2.data = opts.no_replace ? lyr.data.clone() : lyr.data;
27213
- }
27214
- return outputLayers;
27434
+
27435
+ var lyr2 = mergeOutputLayerIntoDataset(lyr, dataset, dataset2, opts);
27436
+ return [lyr2];
27215
27437
  }
27216
27438
 
27217
27439
  // TODO: support three or more stops
@@ -31672,6 +31894,10 @@ ${svg}
31672
31894
  filter = compileValueExpression(opts.expression, lyr, arcs);
31673
31895
  }
31674
31896
 
31897
+ if (opts.ids) {
31898
+ filter = combineFilters(filter, getIdFilter(opts.ids));
31899
+ }
31900
+
31675
31901
  if (opts.remove_empty) {
31676
31902
  filter = combineFilters(filter, getNullGeometryFilter(lyr, arcs));
31677
31903
  }
@@ -31730,6 +31956,13 @@ ${svg}
31730
31956
  lyr.data = filteredRecords ? new DataTable(filteredRecords) : null;
31731
31957
  }
31732
31958
 
31959
+ function getIdFilter(ids) {
31960
+ var set = new Set(ids);
31961
+ return function(i) {
31962
+ return set.has(i);
31963
+ };
31964
+ }
31965
+
31733
31966
  function getNullGeometryFilter(lyr, arcs) {
31734
31967
  var shapes = lyr.shapes;
31735
31968
  if (lyr.geometry_type == 'polygon') {
@@ -33785,7 +34018,7 @@ ${svg}
33785
34018
 
33786
34019
  function createMeridianPart(x, ymin, ymax) {
33787
34020
  var coords = densifyPathByInterval([[x, ymin], [x, ymax]], precision);
33788
- meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord(x)}));
34021
+ meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord$1(x)}));
33789
34022
  }
33790
34023
 
33791
34024
  function createParallel(y) {
@@ -33795,7 +34028,7 @@ ${svg}
33795
34028
  }
33796
34029
 
33797
34030
  // remove tiny offsets
33798
- function roundCoord(x) {
34031
+ function roundCoord$1(x) {
33799
34032
  return +x.toFixed(3) || 0;
33800
34033
  }
33801
34034
 
@@ -34002,10 +34235,6 @@ ${svg}
34002
34235
 
34003
34236
 
34004
34237
 
34005
- function formatNumber(val) {
34006
- return val + '';
34007
- }
34008
-
34009
34238
  function maxChars(arr) {
34010
34239
  return arr.reduce(function(memo, str) {
34011
34240
  var w = stringDisplayWidth(str);
@@ -34029,14 +34258,14 @@ ${svg}
34029
34258
  }
34030
34259
 
34031
34260
  function countIntegralChars(val) {
34032
- return utils.isNumber(val) ? (formatNumber(val) + '.').indexOf('.') : 0;
34261
+ return utils.isNumber(val) ? (utils.formatNumber(val) + '.').indexOf('.') : 0;
34033
34262
  }
34034
34263
 
34035
34264
  function formatTableValue(val, integralChars) {
34036
34265
  var str;
34037
34266
  if (utils.isNumber(val)) {
34038
34267
  str = utils.lpad("", integralChars - countIntegralChars(val), ' ') +
34039
- formatNumber(val);
34268
+ utils.formatNumber(val);
34040
34269
  } else if (utils.isString(val)) {
34041
34270
  str = formatString(val);
34042
34271
  } else if (utils.isDate(val)) {
@@ -36450,6 +36679,99 @@ ${svg}
36450
36679
  parseScalebarLabelToKm: parseScalebarLabelToKm
36451
36680
  });
36452
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
+
36453
36775
  function calcSimplifyStats(arcs, use3D) {
36454
36776
  var distSq = use3D ? pointSegGeoDistSq : geom.pointSegDistSq,
36455
36777
  calcAngle = use3D ? geom.signedAngleSph : geom.signedAngle,
@@ -37408,13 +37730,20 @@ ${svg}
37408
37730
 
37409
37731
  // @expression: optional field name or expression
37410
37732
  //
37411
- cmd.splitLayer = function(src, expression, opts) {
37412
- 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,
37413
37736
  properties = lyr0.data ? lyr0.data.getRecords() : null,
37414
37737
  shapes = lyr0.shapes,
37415
37738
  index = {},
37416
37739
  splitLayers = [],
37417
- namer = getSplitNameFunction(lyr0, expression);
37740
+ namer;
37741
+
37742
+ if (opts.ids) {
37743
+ namer = getIdSplitFunction(opts.ids);
37744
+ } else {
37745
+ namer = getSplitNameFunction(lyr0, expression);
37746
+ }
37418
37747
 
37419
37748
  // if (splitField) {
37420
37749
  // internal.requireDataField(lyr0, splitField);
@@ -37446,15 +37775,24 @@ ${svg}
37446
37775
  return splitLayers;
37447
37776
  };
37448
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
+
37449
37793
  function getSplitNameFunction(lyr, exp) {
37450
37794
  var compiled;
37451
- if (!exp) {
37452
- // if not splitting on an expression and layer is unnamed, name split-apart layers
37453
- // like: split-1, split-2, ...
37454
- return function(i) {
37455
- return (lyr && lyr.name || 'split') + '-' + (i + 1);
37456
- };
37457
- }
37795
+ if (!exp) return getDefaultSplitFunction(lyr);
37458
37796
  lyr = {name: lyr.name, data: lyr.data}; // remove shape info
37459
37797
  compiled = compileValueExpression(exp, lyr, null);
37460
37798
  return function(i) {
@@ -37521,56 +37859,83 @@ ${svg}
37521
37859
  });
37522
37860
  };
37523
37861
 
37524
- symbolBuilders.arrow = function(d) {
37525
- var len = 'length' in d ? d.length : 10;
37526
- var filled = 'fill' in d;
37527
- return filled ? getFilledArrow(d, len) : getStickArrow(d, len);
37528
- };
37862
+ var roundCoord = getRoundingFunction(0.01);
37529
37863
 
37530
- function getStickArrow(d, len) {
37531
- return {
37532
- type: 'polyline',
37533
- coordinates: getStickArrowCoords(d, len),
37534
- stroke: d.stroke || 'magenta',
37535
- 'stroke-width': 'stroke-width' in d ? d['stroke-width'] : 1
37536
- };
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
+ }
37537
37871
  }
37538
37872
 
37539
- function getFilledArrow(d, totalLen) {
37540
- return {
37541
- type: 'polygon',
37542
- coordinates: getFilledArrowCoords(d, totalLen),
37543
- fill: d.fill || 'magenta'
37544
- };
37873
+ function flipY(coords) {
37874
+ forEachSymbolCoord(coords, function(p) {
37875
+ p[1] = -p[1];
37876
+ });
37545
37877
  }
37546
37878
 
37547
- function getScale(totalLen, headLen) {
37548
- var maxHeadPct = 0.60;
37549
- var headPct = headLen / totalLen;
37550
- if (headPct > maxHeadPct) {
37551
- return maxHeadPct / headPct;
37552
- }
37553
- 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
+ });
37554
37884
  }
37555
37885
 
37556
- function getStickArrowTip(totalLen, curve) {
37557
- // curve/2 intersects the arrowhead at 90deg (trigonometry)
37558
- var theta = Math.abs(curve/2) / 180 * Math.PI;
37559
- var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1);
37560
- var dy = totalLen * Math.cos(theta);
37561
- 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
+ });
37562
37891
  }
37563
37892
 
37564
- function addPoints(a, b) {
37565
- 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);
37566
37930
  }
37567
37931
 
37568
37932
  function getStickArrowCoords(d, totalLen) {
37933
+ var minStemRatio = getMinStemRatio(d);
37569
37934
  var headAngle = d['arrow-head-angle'] || 90;
37570
37935
  var curve = d['arrow-stem-curve'] || 0;
37571
37936
  var unscaledHeadWidth = d['arrow-head-width'] || 9;
37572
37937
  var unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle);
37573
- var scale = getScale(totalLen, unscaledHeadLen); // scale down small arrows
37938
+ var scale = getScale(totalLen, unscaledHeadLen, minStemRatio);
37574
37939
  var headWidth = unscaledHeadWidth * scale;
37575
37940
  var headLen = unscaledHeadLen * scale;
37576
37941
  var tip = getStickArrowTip(totalLen, curve);
@@ -37581,84 +37946,273 @@ ${svg}
37581
37946
  if (!headLen) return [stem];
37582
37947
  var head = [addPoints([-headWidth / 2, -headLen], tip), tip.concat(), addPoints([headWidth / 2, -headLen], tip)];
37583
37948
 
37584
- rotateSymbolCoords(stem, d.rotation);
37585
- rotateSymbolCoords(head, d.rotation);
37949
+ rotateCoords(stem, d.rotation);
37950
+ rotateCoords(head, d.rotation);
37586
37951
  return [stem, head];
37587
37952
  }
37588
37953
 
37589
- function getHeadLength(headWidth, headAngle) {
37590
- var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio
37591
- return headWidth * headRatio;
37954
+ function getMinStemRatio(d) {
37955
+ return d['arrow-min-stem'] >= 0 ? d['arrow-min-stem'] : 0.4;
37592
37956
  }
37593
37957
 
37594
- function getFilledArrowCoords(d, totalLen) {
37595
- 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,
37596
37962
  unscaledStemWidth = d['arrow-stem-width'] || 2,
37597
37963
  unscaledHeadWidth = d['arrow-head-width'] || unscaledStemWidth * 3,
37598
37964
  unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle),
37599
- scale = getScale(totalLen, unscaledHeadLen), // scale down small arrows
37965
+ scale = getScale(totalLen, unscaledHeadLen, minStemRatio),
37600
37966
  headWidth = unscaledHeadWidth * scale,
37601
37967
  headLen = unscaledHeadLen * scale,
37602
37968
  stemWidth = unscaledStemWidth * scale,
37603
37969
  stemTaper = d['arrow-stem-taper'] || 0,
37604
- stemLen = totalLen - headLen;
37970
+ stemCurve = d['arrow-stem-curve'] || 0,
37971
+ stemLen = totalLen - headLen,
37972
+ coords;
37605
37973
 
37606
37974
  var headDx = headWidth / 2,
37607
37975
  stemDx = stemWidth / 2,
37608
37976
  baseDx = stemDx * (1 - stemTaper);
37609
37977
 
37610
- var coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
37978
+ if (unscaledHeadWidth < unscaledStemWidth) {
37979
+ stop('Arrow head must be at least as wide as the stem.');
37980
+ }
37981
+
37982
+ if (!stemCurve || Math.abs(stemCurve) > 90) {
37983
+ coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
37611
37984
  [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]];
37985
+ } else {
37986
+ if (direction > 0) stemCurve = -stemCurve;
37987
+ coords = getCurvedArrowCoords(stemLen, headLen, stemCurve, stemDx, headDx, baseDx);
37988
+ }
37612
37989
 
37613
- rotateSymbolCoords(coords, d.rotation);
37990
+ rotateCoords(coords, direction);
37614
37991
  return [coords];
37615
37992
  }
37616
37993
 
37617
- function rotateSymbolCoords(coords, rotation) {
37618
- // TODO: consider avoiding re-instantiating function on every call
37619
- var f = getAffineTransform(rotation || 0, 1, [0, 0], [0, 0]);
37620
- coords.forEach(function(p) {
37621
- var p2 = f ? f(p[0], p[1]) : p;
37622
- p[0] = p2[0];
37623
- p[1] = -p2[1]; // flip y-axis (to produce display coords)
37624
- });
37994
+
37995
+ function getScale(totalLen, headLen, minStemRatio) {
37996
+ var maxHeadPct = 1 - minStemRatio;
37997
+ var headPct = headLen / totalLen;
37998
+ if (headPct > maxHeadPct) {
37999
+ return maxHeadPct / headPct;
38000
+ }
38001
+ return 1;
37625
38002
  }
37626
38003
 
37627
- // TODO: refactor to remove duplication in mapshaper-svg-style.js
37628
- cmd.symbols = function(lyr, opts) {
37629
- var f, filter;
37630
- // console.log("-symbols opts", opts)
37631
- requirePointLayer(lyr);
37632
- f = getSymbolDataAccessor(lyr, opts);
37633
- if (opts.where) {
37634
- filter = compileValueExpression(opts.where, lyr, null);
38004
+ function getStickArrowTip(totalLen, curve) {
38005
+ // curve/2 intersects the arrowhead at 90deg (trigonometry)
38006
+ var theta = Math.abs(curve/2) / 180 * Math.PI;
38007
+ var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1);
38008
+ var dy = totalLen * Math.cos(theta);
38009
+ return [dx, dy];
38010
+ }
38011
+
38012
+ function addPoints(a, b) {
38013
+ return [a[0] + b[0], a[1] + b[1]];
38014
+ }
38015
+
38016
+
38017
+ function getHeadLength(headWidth, headAngle) {
38018
+ var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio
38019
+ return headWidth * headRatio;
38020
+ }
38021
+
38022
+ function getCurvedArrowCoords(stemLen, headLen, curvature, stemDx, headDx, baseDx) {
38023
+ // coordinates go counter clockwise, starting from the leftmost head coordinate
38024
+ var theta = Math.abs(curvature) / 180 * Math.PI;
38025
+ var sign = curvature > 0 ? 1 : -1;
38026
+ var dx = stemLen * Math.sin(theta / 2) * sign;
38027
+ var dy = stemLen * Math.cos(theta / 2);
38028
+ var head = [[stemDx + dx, dy], [headDx + dx, dy],
38029
+ [dx, headLen + dy], [-headDx + dx, dy], [-stemDx + dx, dy]];
38030
+ var ax = baseDx * Math.cos(theta); // rotate arrow base
38031
+ var ay = baseDx * Math.sin(theta) * -sign;
38032
+ var leftStem = getCurvedStemCoords(-ax, -ay, -stemDx + dx, dy, theta);
38033
+ var rightStem = getCurvedStemCoords(ax, ay, stemDx + dx, dy, theta);
38034
+ var stem = leftStem.concat(rightStem.reverse());
38035
+ // stem.pop();
38036
+ return stem.concat(head);
38037
+ }
38038
+
38039
+ // ax, ay: point on the base
38040
+ // bx, by: point on the stem
38041
+ function getCurvedStemCoords(ax, ay, bx, by, theta0) {
38042
+ // case: curved side intrudes into head (because stem is too short)
38043
+ if (ay > by) {
38044
+ return [[ax * by / ay, by]];
37635
38045
  }
37636
- getLayerDataTable(lyr).getRecords().forEach(function(rec, i) {
37637
- if (filter && filter(i)) {
37638
- if ('svg-symbol' in rec === false) {
37639
- rec['svg-symbol'] = undefined;
37640
- }
38046
+ var dx = bx - ax,
38047
+ dy = by - ay,
38048
+ dy1 = (dy * dy - dx * dx) / (2 * dy),
38049
+ dy2 = dy - dy1,
38050
+ dx2 = Math.sqrt(dx * dx + dy * dy) / 2,
38051
+ theta = Math.PI - Math.asin(dx2 / dy2) * 2,
38052
+ degrees = theta * 180 / Math.PI,
38053
+ radius = dy2 / Math.tan(theta / 2),
38054
+ leftBend = bx > ax,
38055
+ sign = leftBend ? 1 : -1,
38056
+ points = Math.round(degrees / 5) + 2,
38057
+ increment = theta / (points + 1),
38058
+ coords = [[bx, by]];
38059
+
38060
+ for (var i=1; i<= points; i++) {
38061
+ var phi = i * increment / 2;
38062
+ var sinPhi = Math.sin(phi);
38063
+ var cosPhi = Math.cos(phi);
38064
+ var c = sinPhi * radius * 2;
38065
+ var a = sinPhi * c;
38066
+ var b = cosPhi * c;
38067
+ coords.push([bx - a * sign, by - b]);
38068
+ }
38069
+ coords.push([ax, ay]);
38070
+ return coords;
38071
+ }
38072
+
38073
+ // sides: e.g. 5-pointed star has 10 sides
38074
+ // radius: distance from center to point
38075
+ //
38076
+ function getPolygonCoords(radius, opts) {
38077
+ var type = opts.type;
38078
+ var sides = +opts.sides || getDefaultSides(type);
38079
+ var isStar = type == 'star';
38080
+ if (isStar && (sides < 6 || sides % 2 !== 0)) {
38081
+ stop(`Invalid number of sides for a star (${sides})`);
38082
+ } else if (sides >= 3 === false) {
38083
+ stop(`Invalid number of sides (${sides})`);
38084
+ }
38085
+ var coords = [],
38086
+ angle = 360 / sides,
38087
+ b = isStar ? 1 : 0.5,
38088
+ theta, even, len;
38089
+ if (opts.orientation == 'b') {
38090
+ b = 0;
38091
+ }
38092
+ for (var i=0; i<sides; i++) {
38093
+ even = i % 2 == 0;
38094
+ len = radius;
38095
+ if (isStar && even) {
38096
+ len *= (opts.star_ratio || 0.5);
38097
+ }
38098
+ theta = (i + b) * angle % 360;
38099
+ coords.push(getPlanarSegmentEndpoint(0, 0, theta, len));
38100
+ }
38101
+ coords.push(coords[0].concat());
38102
+ return [coords];
38103
+ }
38104
+
38105
+ function getDefaultSides(type) {
38106
+ return {
38107
+ star: 10,
38108
+ circle: 72,
38109
+ triangle: 3,
38110
+ square: 4,
38111
+ pentagon: 5,
38112
+ hexagon: 6,
38113
+ heptagon: 7,
38114
+ octagon: 8,
38115
+ nonagon: 9,
38116
+ decagon: 10
38117
+ }[type] || 4;
38118
+ }
38119
+
38120
+ // TODO: refactor to remove duplication in mapshaper-svg-style.js
38121
+ cmd.symbols = function(inputLyr, dataset, opts) {
38122
+ requireSinglePointLayer(inputLyr);
38123
+ var lyr = opts.no_replace ? copyLayer(inputLyr) : inputLyr;
38124
+ var polygonMode = !!opts.polygons;
38125
+ var metersPerPx;
38126
+ if (polygonMode) {
38127
+ requireProjectedDataset(dataset);
38128
+ metersPerPx = opts.pixel_scale || getMetersPerPixel(lyr, dataset);
38129
+ }
38130
+ var records = getLayerDataTable(lyr).getRecords();
38131
+ var getSymbolData = getSymbolDataAccessor(lyr, opts);
38132
+ var geometries = lyr.shapes.map(function(shp, i) {
38133
+ if (!shp) return null;
38134
+ var d = getSymbolData(i);
38135
+ var rec = records[i] || {};
38136
+ var coords, size, constructor;
38137
+ if (d.type == 'arrow') {
38138
+ size = d.radius || d.length || d['arrow-length'] || d.r;
38139
+ constructor = getFilledArrowCoords;
38140
+ } else {
38141
+ size = d.radius || d.length || d.r;
38142
+ constructor = getPolygonCoords;
38143
+ }
38144
+ if (size > 0 === false) return null;
38145
+ coords = constructor(size, d, opts);
38146
+ rotateCoords(coords, +d.rotation || 0);
38147
+ if (!polygonMode) {
38148
+ flipY(coords);
38149
+ }
38150
+ if (+opts.scale) {
38151
+ scaleAndShiftCoords(coords, +opts.scale, [0, 0]);
38152
+ }
38153
+ if (polygonMode) {
38154
+ scaleAndShiftCoords(coords, metersPerPx, shp[0]);
38155
+ if (d.tfill) rec.fill = d.fill;
38156
+ return createGeometry(coords);
37641
38157
  } else {
37642
- rec['svg-symbol'] = buildSymbol(f(i));
38158
+ rec['svg-symbol'] = makeSvgSymbol(coords, d);
37643
38159
  }
37644
38160
  });
38161
+
38162
+ var outputLyr, dataset2;
38163
+ if (polygonMode) {
38164
+ dataset2 = importGeometries(geometries, records);
38165
+ outputLyr = mergeOutputLayerIntoDataset(inputLyr, dataset, dataset2, opts);
38166
+ outputLyr.data = lyr.data;
38167
+ } else {
38168
+ outputLyr = lyr;
38169
+ }
38170
+ return [outputLyr];
37645
38171
  };
37646
38172
 
38173
+ function importGeometries(geometries, records) {
38174
+ var features = geometries.map(function(geom, i) {
38175
+ var d = records[i];
38176
+ return {
38177
+ type: 'Feature',
38178
+ properties: records[i] || null,
38179
+ geometry: geom
38180
+ };
38181
+ });
38182
+ var geojson = {
38183
+ type: 'FeatureCollection',
38184
+ features: features
38185
+ };
38186
+ return importGeoJSON(geojson);
38187
+ }
38188
+
38189
+ function createGeometry(coords) {
38190
+ return {
38191
+ type: 'Polygon',
38192
+ coordinates: coords
38193
+ };
38194
+ }
38195
+
38196
+ function getMetersPerPixel(lyr, dataset) {
38197
+
38198
+ // TODO: handle single point, no extent
38199
+ var bounds = getLayerBounds(lyr);
38200
+ return bounds.width() / 800;
38201
+ }
38202
+
37647
38203
  // Returns an svg-symbol data object for one symbol
37648
- function buildSymbol(properties) {
37649
- var type = properties.type;
37650
- var f = symbolBuilders[type];
37651
- if (!type) {
37652
- stop('Missing required "type" parameter');
37653
- } else if (!f) {
37654
- stop('Unknown symbol type:', type);
37655
- }
37656
- return f(properties);
38204
+ function makeSvgSymbol(coords, properties) {
38205
+ roundCoordsForSVG(coords);
38206
+ return {
38207
+ type: 'polygon',
38208
+ coordinates: coords,
38209
+ fill: properties.fill || 'magenta'
38210
+ };
37657
38211
  }
37658
38212
 
37659
38213
  var Symbols = /*#__PURE__*/Object.freeze({
37660
38214
  __proto__: null,
37661
- buildSymbol: buildSymbol
38215
+ makeSvgSymbol: makeSvgSymbol
37662
38216
  });
37663
38217
 
37664
38218
  cmd.target = function(catalog, opts) {
@@ -37804,99 +38358,6 @@ ${svg}
37804
38358
  }
37805
38359
  };
37806
38360
 
37807
- cmd.shape = function(targetDataset, opts) {
37808
- var geojson, dataset;
37809
- if (opts.coordinates) {
37810
- geojson = makeShapeFromCoords(opts);
37811
- } else if (opts.type == 'circle') {
37812
- geojson = makeCircle(opts);
37813
- } else if (opts.type == 'rectangle' && opts.bbox) {
37814
- geojson = getRectangleGeoJSON(opts);
37815
- } else {
37816
- stop('Missing coordinates parameter');
37817
- }
37818
- // TODO: project shape if targetDataset is projected
37819
- dataset = importGeoJSON(geojson, {});
37820
- if (opts.rotation) {
37821
- rotateDatasetCoords(dataset, opts.rotation);
37822
- }
37823
- dataset.layers[0].name = opts.name || opts.type || 'shape';
37824
- return dataset;
37825
- };
37826
-
37827
- function getRectangleGeoJSON(opts) {
37828
- var bbox = opts.bbox,
37829
- xmin = bbox[0],
37830
- ymin = bbox[1],
37831
- xmax = bbox[2],
37832
- ymax = bbox[3],
37833
- interval = 0.5,
37834
- coords = [],
37835
- type = opts.geometry == 'polyline' ? 'LineString' : 'Polygon';
37836
- addSide(xmin, ymin, xmin, ymax);
37837
- addSide(xmin, ymax, xmax, ymax);
37838
- addSide(xmax, ymax, xmax, ymin);
37839
- addSide(xmax, ymin, xmin, ymin);
37840
- coords.push([xmin, ymin]);
37841
- return {
37842
- type: type,
37843
- coordinates: type == 'Polygon' ? [coords] : coords
37844
- };
37845
-
37846
- function addSide(x1, y1, x2, y2) {
37847
- var dx = x2 - x1,
37848
- dy = y2 - y1,
37849
- n = Math.ceil(Math.max(Math.abs(dx) / interval, Math.abs(dy) / interval)),
37850
- xint = dx / n,
37851
- yint = dy / n;
37852
- for (var i=0; i<n; i++) {
37853
- coords.push([x1 + i * xint, y1 + i * yint]);
37854
- }
37855
- }
37856
- }
37857
-
37858
- function makeCircle(opts) {
37859
- if (opts.radius > 0 === false && opts.radius_angle > 0 === false) {
37860
- stop('Missing required radius parameter.');
37861
- }
37862
- var cp = opts.center || [0, 0];
37863
- var radius = opts.radius || getCircleRadiusFromAngle(getCRS('wgs84'), opts.radius_angle);
37864
- return getCircleGeoJSON(cp, radius, null, {geometry_type : opts.geometry || 'polygon'});
37865
- }
37866
-
37867
- function makeShapeFromCoords(opts) {
37868
- var coordinates = [];
37869
- var offsets = opts.offsets || [];
37870
- var coords = opts.coordinates;
37871
- var type, i, x, y;
37872
- if (coords.length >= 2 === false) {
37873
- stop('Invalid coordinates parameter.');
37874
- }
37875
- for (i=0; i<coords.length; i+= 2) {
37876
- x = coords[i];
37877
- y = coords[i + 1];
37878
- coordinates.push([x, y]);
37879
- }
37880
- for (i=0; i<offsets.length; i+=2) {
37881
- x += offsets[i];
37882
- y += offsets[i + 1];
37883
- coordinates.push([x, y]);
37884
- }
37885
- if (GeoJSON.pathIsRing(coordinates)) {
37886
- type = 'Polygon';
37887
- } else if (opts.closed && coordinates.length >= 3) {
37888
- type = 'Polygon';
37889
- coordinates.push(coordinates[0]);
37890
- } else {
37891
- type = 'LineString';
37892
- }
37893
- return {
37894
- type: type,
37895
- coordinates: type == 'Polygon' ? [coordinates] : coordinates
37896
- };
37897
-
37898
- }
37899
-
37900
38361
  cmd.variableSimplify = function(layers, dataset, opts) {
37901
38362
  var lyr = layers[0];
37902
38363
  var arcs = dataset.arcs;
@@ -38438,6 +38899,9 @@ ${svg}
38438
38899
  } else if (name == 'shape') {
38439
38900
  catalog.addDataset(cmd.shape(targetDataset, opts));
38440
38901
 
38902
+ } else if (name == 'shapes') {
38903
+ outputLayers = applyCommandToEachLayer(cmd.shapes, targetLayers, targetDataset, opts);
38904
+
38441
38905
  } else if (name == 'simplify') {
38442
38906
  if (opts.variable) {
38443
38907
  cmd.variableSimplify(targetLayers, targetDataset, opts);
@@ -38467,7 +38931,7 @@ ${svg}
38467
38931
  applyCommandToEachLayer(cmd.svgStyle, targetLayers, targetDataset, opts);
38468
38932
 
38469
38933
  } else if (name == 'symbols') {
38470
- applyCommandToEachLayer(cmd.symbols, targetLayers, opts);
38934
+ outputLayers = applyCommandToEachLayer(cmd.symbols, targetLayers, targetDataset, opts);
38471
38935
 
38472
38936
  } else if (name == 'subdivide') {
38473
38937
  outputLayers = applyCommandToEachLayer(cmd.subdivideLayer, targetLayers, arcs, opts.expression);