mapshaper 0.5.79 → 0.5.83

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.78";
3
+ var VERSION = "0.5.79";
4
4
 
5
5
 
6
6
  var utils = /*#__PURE__*/Object.freeze({
@@ -16,6 +16,8 @@
16
16
  get isFiniteNumber () { return isFiniteNumber; },
17
17
  get isNonNegNumber () { return isNonNegNumber; },
18
18
  get isInteger () { return isInteger; },
19
+ get isEven () { return isEven; },
20
+ get isOdd () { return isOdd; },
19
21
  get isString () { return isString; },
20
22
  get isDate () { return isDate; },
21
23
  get isBoolean () { return isBoolean; },
@@ -149,6 +151,14 @@
149
151
  return isNumber(obj) && ((obj | 0) === obj);
150
152
  }
151
153
 
154
+ function isEven(obj) {
155
+ return (obj % 2) === 0;
156
+ }
157
+
158
+ function isOdd(obj) {
159
+ return (obj % 2) === 1;
160
+ }
161
+
152
162
  function isString(obj) {
153
163
  return obj != null && obj.toString === String.prototype.toString;
154
164
  // TODO: replace w/ something better.
@@ -1745,9 +1755,17 @@
1745
1755
  return [xmin, ymin, xmax, ymax];
1746
1756
  }
1747
1757
 
1748
- // TODO: remove this constant, use actual data from dataset CRS
1749
- // also consider using ellipsoidal formulas when appropriate
1750
- var R$1 = 6378137;
1758
+ var WGS84 = {
1759
+ // https://en.wikipedia.org/wiki/Earth_radius
1760
+ SEMIMAJOR_AXIS: 6378137,
1761
+ SEMIMINOR_AXIS: 6356752.3142,
1762
+ AUTHALIC_RADIUS: 6371007.2,
1763
+ VOLUMETRIC_RADIUS: 6371000.8
1764
+ };
1765
+
1766
+ // TODO: remove this constant, use actual data from dataset CRS,
1767
+ // also consider using ellipsoidal formulas where greater accuracy might be important.
1768
+ var R$1 = WGS84.SEMIMAJOR_AXIS;
1751
1769
  var D2R = Math.PI / 180;
1752
1770
  var R2D = 180 / Math.PI;
1753
1771
 
@@ -2342,15 +2360,21 @@
2342
2360
  }, 0);
2343
2361
  }
2344
2362
 
2345
- function getSphericalShapeArea(shp, arcs) {
2363
+ function getSphericalShapeArea(shp, arcs, R) {
2346
2364
  if (arcs.isPlanar()) {
2347
2365
  error("[getSphericalShapeArea()] Function requires decimal degree coordinates");
2348
2366
  }
2349
2367
  return (shp || []).reduce(function(area, ids) {
2350
- return area + getSphericalPathArea(ids, arcs);
2368
+ return area + getSphericalPathArea(ids, arcs, R);
2351
2369
  }, 0);
2352
2370
  }
2353
2371
 
2372
+ // export function getEllipsoidalShapeArea(shp, arcs, crs) {
2373
+ // return (shp || []).reduce(function(area, ids) {
2374
+ // return area + getEllipsoidalPathArea(ids, arcs, crs);
2375
+ // }, 0);
2376
+ // }
2377
+
2354
2378
  // Return true if point is inside or on boundary of a shape
2355
2379
  //
2356
2380
  function testPointInPolygon(x, y, shp, arcs) {
@@ -2464,16 +2488,17 @@
2464
2488
  return (arcs.isPlanar() ? getPlanarPathArea : getSphericalPathArea)(ids, arcs);
2465
2489
  }
2466
2490
 
2467
- function getSphericalPathArea(ids, arcs) {
2491
+ function getSphericalPathArea(ids, arcs, R) {
2468
2492
  var iter = arcs.getShapeIter(ids);
2469
- return getSphericalPathArea2(iter);
2493
+ return getSphericalPathArea2(iter, R);
2470
2494
  }
2471
2495
 
2472
- function getSphericalPathArea2(iter) {
2496
+ function getSphericalPathArea2(iter, R) {
2473
2497
  var sum = 0,
2474
2498
  started = false,
2475
2499
  deg2rad = Math.PI / 180,
2476
2500
  x, y, xp, yp;
2501
+ R = R || WGS84.SEMIMAJOR_AXIS;
2477
2502
  while (iter.hasNext()) {
2478
2503
  x = iter.x * deg2rad;
2479
2504
  y = Math.sin(iter.y * deg2rad);
@@ -2485,7 +2510,7 @@
2485
2510
  xp = x;
2486
2511
  yp = y;
2487
2512
  }
2488
- return sum / 2 * 6378137 * 6378137;
2513
+ return sum / 2 * R * R;
2489
2514
  }
2490
2515
 
2491
2516
  // Get path area from an array of [x, y] points
@@ -3949,7 +3974,7 @@
3949
3974
  function requireSinglePointLayer(lyr, msg) {
3950
3975
  requirePointLayer(lyr);
3951
3976
  if (countMultiPartFeatures(lyr) > 0) {
3952
- stop(msg || 'This command requires single points');
3977
+ stop(msg || 'This command requires single points; layer contains multi-point features.');
3953
3978
  }
3954
3979
  }
3955
3980
 
@@ -4004,6 +4029,7 @@
4004
4029
  return opts && opts.no_replace ? {geometry_type: src.geometry_type} : src;
4005
4030
  }
4006
4031
 
4032
+ //
4007
4033
  function setOutputLayerName(dest, src, defName, opts) {
4008
4034
  opts = opts || {};
4009
4035
  if (opts.name) {
@@ -4080,6 +4106,7 @@
4080
4106
  return counts;
4081
4107
  }
4082
4108
 
4109
+ // Returns a Bounds object
4083
4110
  function getLayerBounds(lyr, arcs) {
4084
4111
  var bounds = null;
4085
4112
  if (lyr.geometry_type == 'point') {
@@ -6151,9 +6178,11 @@
6151
6178
  arcCount += n;
6152
6179
  });
6153
6180
 
6154
- mergedArcs = mergeArcs(arcSources);
6155
- if (mergedArcs.size() != arcCount) {
6156
- error("[mergeDatasets()] Arc indexing error");
6181
+ if (arcSources.length > 0) {
6182
+ mergedArcs = mergeArcs(arcSources);
6183
+ if (mergedArcs.size() != arcCount) {
6184
+ error("[mergeDatasets()] Arc indexing error");
6185
+ }
6157
6186
  }
6158
6187
 
6159
6188
  return {
@@ -6172,6 +6201,8 @@
6172
6201
  }
6173
6202
 
6174
6203
  function mergeArcs(arr) {
6204
+ // Returning the original causes a test to fail
6205
+ // if (arr.length < 2) return arr[0];
6175
6206
  var dataArr = arr.map(function(arcs) {
6176
6207
  if (arcs.getRetainedInterval() > 0) {
6177
6208
  verbose("Baking-in simplification setting.");
@@ -6776,31 +6807,47 @@
6776
6807
  dataset.layers = currLayers;
6777
6808
  }
6778
6809
 
6779
- // Replace a layer in-place with a layer from a second dataset
6810
+ // Replace a layer with a layer from a second dataset
6811
+ // (in-place)
6780
6812
  // (Typically, the second dataset is imported from dynamically generated GeoJSON and contains one layer)
6781
6813
  function replaceLayerContents(lyr, dataset, dataset2) {
6814
+ var lyr2 = mergeOutputLayerIntoDataset(lyr, dataset, dataset2, {});
6815
+ if (layerHasPaths(lyr2)) {
6816
+ buildTopology(dataset);
6817
+ }
6818
+ }
6819
+
6820
+ function mergeOutputLayerIntoDataset(lyr, dataset, dataset2, opts) {
6782
6821
  if (!dataset2 || dataset2.layers.length != 1) {
6783
- error('Invalid replacement layer');
6822
+ error('Invalid source dataset');
6784
6823
  }
6785
6824
  if (dataset.layers.includes(lyr) === false) {
6786
6825
  error('Invalid target layer');
6787
6826
  }
6788
- var lyr2 = dataset2.layers[0];
6789
- if (dataset2.arcs) {
6790
- // this command returns merged layers instead of adding them to target dataset
6791
- mergeDatasetsIntoDataset(dataset, [dataset2]);
6792
- }
6827
+ // this command returns merged layers instead of adding them to target dataset
6828
+ var outputLayers = mergeDatasetsIntoDataset(dataset, [dataset2]);
6829
+ var lyr2 = outputLayers[0];
6793
6830
 
6794
- Object.assign(lyr, {data: null, shapes: null}, lyr2);
6831
+ // TODO: find a more reliable way of knowing when to copy data
6832
+ var copyData = !lyr2.data && lyr.data && getFeatureCount(lyr2) == lyr.data.size();
6795
6833
 
6796
- if (layerHasPaths(lyr2)) {
6797
- buildTopology(dataset);
6834
+ if (copyData) {
6835
+ lyr2.data = opts.no_replace ? lyr.data.clone() : lyr.data;
6798
6836
  }
6799
- if (layerHasPaths(lyr)) {
6800
- // Remove unused arcs from replaced layer
6801
- // TODO: consider using clean insead of this
6802
- dissolveArcs(dataset);
6837
+ if (opts.no_replace) {
6838
+ // dataset.layers.push(lyr2);
6839
+
6840
+ } else {
6841
+ lyr2 = Object.assign(lyr, {data: null, shapes: null}, lyr2);
6842
+ if (layerHasPaths(lyr)) {
6843
+ // Remove unused arcs from replaced layer
6844
+ // TODO: consider using clean insead of this
6845
+ dissolveArcs(dataset);
6846
+ }
6803
6847
  }
6848
+
6849
+ lyr2.name = opts.name || lyr2.name;
6850
+ return lyr2;
6804
6851
  }
6805
6852
 
6806
6853
  // Transform the points in a dataset in-place; don't clean up corrupted shapes
@@ -6829,6 +6876,7 @@
6829
6876
  pruneArcs: pruneArcs,
6830
6877
  replaceLayers: replaceLayers,
6831
6878
  replaceLayerContents: replaceLayerContents,
6879
+ mergeOutputLayerIntoDataset: mergeOutputLayerIntoDataset,
6832
6880
  transformPoints: transformPoints
6833
6881
  });
6834
6882
 
@@ -9412,6 +9460,12 @@
9412
9460
  area: function() {
9413
9461
  return _isPlanar ? ctx.planarArea : geom.getSphericalShapeArea(_ids, arcs);
9414
9462
  },
9463
+ // area2: function() {
9464
+ // return _isPlanar ? ctx.planarArea : geom.getSphericalShapeArea(_ids, arcs, WGS84.SEMIMINOR_RADIUS);
9465
+ // },
9466
+ // area3: function() {
9467
+ // return _isPlanar ? ctx.planarArea : geom.getSphericalShapeArea(_ids, arcs, WGS84.AUTHALIC_RADIUS);
9468
+ // },
9415
9469
  perimeter: function() {
9416
9470
  return geom.getShapePerimeter(_ids, arcs);
9417
9471
  },
@@ -13332,14 +13386,14 @@
13332
13386
  return mergeArcs([arcs, new ArcCollection(nn, xx, yy)]);
13333
13387
  }
13334
13388
 
13335
- var roundCoord$1 = getRoundingFunction(0.01);
13389
+ var roundCoord$2 = getRoundingFunction(0.01);
13336
13390
 
13337
13391
  function stringifyVertex(p) {
13338
- return ' ' + roundCoord$1(p[0]) + ' ' + roundCoord$1(p[1]);
13392
+ return ' ' + roundCoord$2(p[0]) + ' ' + roundCoord$2(p[1]);
13339
13393
  }
13340
13394
 
13341
13395
  function stringifyCP(p) {
13342
- return ' ' + roundCoord$1(p[2]) + ' ' + roundCoord$1(p[3]);
13396
+ return ' ' + roundCoord$2(p[2]) + ' ' + roundCoord$2(p[3]);
13343
13397
  }
13344
13398
 
13345
13399
  function isCubicCtrl(p) {
@@ -13374,40 +13428,9 @@
13374
13428
  stringifyCP(p2) + stringifyVertex(p2);
13375
13429
  }
13376
13430
 
13377
- function findArcCenter(p1, p2, degrees) {
13378
- var p3 = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2], // midpoint betw. p1, p2
13379
- tan = 1 / Math.tan(degrees / 180 * Math.PI / 2),
13380
- cp = getAffineTransform(90, tan, [0, 0], p3)(p2[0], p2[1]);
13381
- return cp;
13382
- }
13383
-
13384
- // export function addBezierArcControlPoints(p1, p2, degrees) {
13385
- function addBezierArcControlPoints(points, degrees) {
13386
- // source: https://stackoverflow.com/questions/734076/how-to-best-approximate-a-geometrical-arc-with-a-bezier-curve
13387
- var p2 = points.pop(),
13388
- p1 = points.pop(),
13389
- cp = findArcCenter(p1, p2, degrees),
13390
- xc = cp[0],
13391
- yc = cp[1],
13392
- ax = p1[0] - xc,
13393
- ay = p1[1] - yc,
13394
- bx = p2[0] - xc,
13395
- by = p2[1] - yc,
13396
- q1 = ax * ax + ay * ay,
13397
- q2 = q1 + ax * bx + ay * by,
13398
- k2 = 4/3 * (Math.sqrt(2 * q1 * q2) - q2) / (ax * by - ay * bx);
13399
-
13400
- points.push(p1);
13401
- points.push([xc + ax - k2 * ay, yc + ay + k2 * ax, 'C']);
13402
- points.push([xc + bx + k2 * by, yc + by - k2 * bx, 'C']);
13403
- points.push(p2);
13404
- }
13405
-
13406
13431
  var SvgPathUtils = /*#__PURE__*/Object.freeze({
13407
13432
  __proto__: null,
13408
- stringifyLineStringCoords: stringifyLineStringCoords,
13409
- findArcCenter: findArcCenter,
13410
- addBezierArcControlPoints: addBezierArcControlPoints
13433
+ stringifyLineStringCoords: stringifyLineStringCoords
13411
13434
  });
13412
13435
 
13413
13436
  /* example patterns
@@ -13684,11 +13707,19 @@
13684
13707
  type: null,
13685
13708
  length: 'number', // e.g. arrow length
13686
13709
  rotation: 'number',
13687
- 'arrow-head-angle': 'number',
13688
- 'arrow-head-width': 'number',
13689
- 'arrow-stem-width': 'number',
13690
- 'arrow-stem-curve': 'number', // degrees of arc
13691
- 'arrow-stem-taper': 'number',
13710
+ radius: 'number',
13711
+ radii: null, // string, parsed by function
13712
+ flipped: 'boolean',
13713
+ rotated: 'boolean',
13714
+ direction: 'number',
13715
+ 'head-angle': 'number',
13716
+ 'head-width': 'number',
13717
+ 'head-length': 'number',
13718
+ 'stem-width': 'number',
13719
+ 'stem-curve': 'number', // degrees of arc
13720
+ 'stem-taper': 'number',
13721
+ 'stem-length': 'number',
13722
+ 'min-stem': 'number',
13692
13723
  'arrow-scaling': 'number',
13693
13724
  effect: null // e.g. "fade"
13694
13725
  }, stylePropertyTypes);
@@ -13724,17 +13755,20 @@
13724
13755
  function getSymbolDataAccessor(lyr, opts) {
13725
13756
  var functions = {};
13726
13757
  var properties = [];
13758
+ var fields = lyr.data ? lyr.data.getFields() : [];
13727
13759
 
13728
13760
  Object.keys(opts).forEach(function(optName) {
13729
13761
  var svgName = optName.replace(/_/g, '-');
13730
13762
  if (!isSupportedSvgSymbolProperty(svgName)) {
13731
13763
  return;
13732
13764
  }
13733
- var strVal = opts[optName].trim();
13734
- functions[svgName] = getSymbolPropertyAccessor(strVal, svgName, lyr);
13765
+ var val = opts[optName];
13766
+ functions[svgName] = getSymbolPropertyAccessor(val, svgName, lyr);
13735
13767
  properties.push(svgName);
13736
13768
  });
13737
13769
 
13770
+ // TODO: consider applying values of existing fields with names of symbol properties
13771
+
13738
13772
  return function(id) {
13739
13773
  var d = {}, name;
13740
13774
  for (var i=0; i<properties.length; i++) {
@@ -13750,11 +13784,13 @@
13750
13784
  // * ???
13751
13785
  //
13752
13786
  function mightBeExpression(str, fields) {
13787
+ fields = fields || [];
13753
13788
  if (fields.indexOf(str.trim()) > -1) return true;
13754
- return /[(){}.+-/*?:&|=\[]/.test(str);
13789
+ return /[(){}./*?:&|=\[+-]/.test(str);
13755
13790
  }
13756
13791
 
13757
- function getSymbolPropertyAccessor(strVal, svgName, lyr) {
13792
+ function getSymbolPropertyAccessor(val, svgName, lyr) {
13793
+ var strVal = String(val).trim();
13758
13794
  var typeHint = symbolPropertyTypes[svgName];
13759
13795
  var fields = lyr.data ? lyr.data.getFields() : [];
13760
13796
  var literalVal = null;
@@ -13771,6 +13807,7 @@
13771
13807
  // treating the string as a literal value
13772
13808
  literalVal = strVal;
13773
13809
  }
13810
+ // console.log("literalVal:", mightBeExpression(strVal, fields), strVal, fields)
13774
13811
  if (accessor) return accessor;
13775
13812
  if (literalVal !== null) return function(id) {return literalVal;};
13776
13813
  stop('Unexpected value for', svgName + ':', strVal);
@@ -13804,6 +13841,8 @@
13804
13841
  val = isDashArray(strVal) ? strVal : null;
13805
13842
  } else if (type == 'pattern') {
13806
13843
  val = isPattern(strVal) ? strVal : null;
13844
+ } else if (type == 'boolean') {
13845
+ val = parseBoolean(strVal);
13807
13846
  }
13808
13847
  // else {
13809
13848
  // // unknown type -- assume literal value
@@ -13824,10 +13863,17 @@
13824
13863
  return /^( ?[_a-z][-_a-z0-9]*\b)+$/i.test(str);
13825
13864
  }
13826
13865
 
13866
+
13827
13867
  function isSvgNumber(o) {
13828
13868
  return utils.isFiniteNumber(o) || utils.isString(o) && /^-?[.0-9]+$/.test(o);
13829
13869
  }
13830
13870
 
13871
+ function parseBoolean(o) {
13872
+ if (o === true || o === 'true') return true;
13873
+ if (o === false || o === 'false') return false;
13874
+ return null;
13875
+ }
13876
+
13831
13877
  function isSvgMeasure(o) {
13832
13878
  return utils.isFiniteNumber(o) || utils.isString(o) && /^-?[.0-9]+[a-z]*$/.test(o);
13833
13879
  }
@@ -13851,12 +13897,12 @@
13851
13897
  getSymbolPropertyAccessor: getSymbolPropertyAccessor,
13852
13898
  isSvgClassName: isSvgClassName,
13853
13899
  isSvgNumber: isSvgNumber,
13900
+ parseBoolean: parseBoolean,
13854
13901
  isSvgMeasure: isSvgMeasure,
13855
13902
  parseSvgMeasure: parseSvgMeasure,
13856
13903
  isSvgColor: isSvgColor
13857
13904
  });
13858
13905
 
13859
- var symbolBuilders = {};
13860
13906
  var symbolRenderers = {};
13861
13907
 
13862
13908
  function getTransform(xy, scale) {
@@ -13869,7 +13915,6 @@
13869
13915
 
13870
13916
  var SvgCommon = /*#__PURE__*/Object.freeze({
13871
13917
  __proto__: null,
13872
- symbolBuilders: symbolBuilders,
13873
13918
  symbolRenderers: symbolRenderers,
13874
13919
  getTransform: getTransform
13875
13920
  });
@@ -14071,6 +14116,9 @@
14071
14116
  o = importLineString(coords[i]);
14072
14117
  o.properties.d = d + o.properties.d + ' Z';
14073
14118
  }
14119
+ if (coords.length > 1) {
14120
+ o.properties['fill-rule'] = 'evenodd'; // support polygons with holes
14121
+ }
14074
14122
  return o;
14075
14123
  }
14076
14124
 
@@ -17987,6 +18035,10 @@ ${svg}
17987
18035
  return chunks;
17988
18036
  }
17989
18037
 
18038
+ function parseNumberList(token) {
18039
+ return token.split(',').map(parseFloat);
18040
+ }
18041
+
17990
18042
  // Split comma-delimited list, trim quotes from entire list and
17991
18043
  // individual members
17992
18044
  function parseStringList(token) {
@@ -18022,9 +18074,9 @@ ${svg}
18022
18074
  // Updated: don't trim space from tokens like [delimeter= ]
18023
18075
  argv = argv.map(function(s) {
18024
18076
  if (!/= $/.test(s)) {
18025
- s = s.trimEnd();
18077
+ s = utils.rtrim(s);
18026
18078
  }
18027
- s = s.trimStart();
18079
+ s = utils.ltrim(s);
18028
18080
  return s;
18029
18081
  });
18030
18082
  argv = argv.filter(function(s) {return s !== '';}); // remove empty tokens
@@ -18076,6 +18128,7 @@ ${svg}
18076
18128
  var OptionParsingUtils = /*#__PURE__*/Object.freeze({
18077
18129
  __proto__: null,
18078
18130
  splitShellTokens: splitShellTokens,
18131
+ parseNumberList: parseNumberList,
18079
18132
  parseStringList: parseStringList,
18080
18133
  parseColorList: parseColorList,
18081
18134
  cleanArgv: cleanArgv,
@@ -18267,7 +18320,7 @@ ${svg}
18267
18320
  } else if (type == 'strings') {
18268
18321
  val = parseStringList(token);
18269
18322
  } else if (type == 'bbox' || type == 'numbers') {
18270
- val = token.split(',').map(parseFloat);
18323
+ val = parseNumberList(token);
18271
18324
  } else if (type == 'percent') {
18272
18325
  // val = utils.parsePercent(token);
18273
18326
  val = token; // string value is parsed by command function
@@ -19479,6 +19532,13 @@ ${svg}
19479
19532
  })
19480
19533
  .option('target', targetOpt);
19481
19534
 
19535
+ parser.command('include')
19536
+ .describe('import JS data and functions for use in JS expressions')
19537
+ .option('file', {
19538
+ DEFAULT: true,
19539
+ describe: 'file containing a JS object with key:value pairs to import'
19540
+ });
19541
+
19482
19542
  parser.command('inlay')
19483
19543
  .describe('inscribe a polygon layer inside another polygon layer')
19484
19544
  .option('source', {
@@ -19791,6 +19851,7 @@ ${svg}
19791
19851
  })
19792
19852
  .option('target', targetOpt);
19793
19853
 
19854
+
19794
19855
  parser.command('simplify')
19795
19856
  .validate(validateSimplifyOpts)
19796
19857
  .example('Retain 10% of removable vertices\n$ mapshaper input.shp -simplify 10%')
@@ -20029,26 +20090,112 @@ ${svg}
20029
20090
  .option('target', targetOpt);
20030
20091
 
20031
20092
  parser.command('symbols')
20032
- // .describe('generate a variety of SVG symbols')
20093
+ // .describe('symbolize points as polygons, circles, stars or arrows')
20033
20094
  .option('type', {
20034
- describe: 'symbol type'
20095
+ describe: 'symbol type (e.g. star, polygon, circle, arrow)'
20096
+ })
20097
+ .option('scale', {
20098
+ describe: 'scale symbols by a factor',
20099
+ type: 'number'
20100
+ })
20101
+ .option('pixel-scale', {
20102
+ describe: 'symbol scale in meters-per-pixel (see polygons option)',
20103
+ type: 'number',
20104
+ })
20105
+ .option('polygons', {
20106
+ describe: 'generate symbols as polygons instead of SVG objects',
20107
+ type: 'flag'
20108
+ })
20109
+ .option('radius', {
20110
+ describe: 'distance from center to farthest point on the symbol',
20111
+ type: 'distance'
20112
+ })
20113
+ .option('sides', {
20114
+ describe: 'sides of a polygon or star symbol',
20115
+ type: 'number'
20116
+ })
20117
+ .option('orientation', {
20118
+ // describe: 'use orientation=b for a rotated or flipped orientation'
20119
+ })
20120
+ .option('flipped', {
20121
+ type: 'flag',
20122
+ describe: 'symbol is vertically flipped'
20123
+ })
20124
+ .option('rotated', {
20125
+ type: 'flag',
20126
+ describe: 'symbol is rotated to a different orientation'
20127
+ })
20128
+ .option('rotation', {
20129
+ describe: 'rotation of symbol in degrees'
20130
+ })
20131
+ .option('length', {
20132
+ // alias for arrow-length
20133
+ })
20134
+ .option('point-ratio', {
20135
+ old_alias: 'star-ratio',
20136
+ describe: '(star) ratio of minor to major radius of star',
20137
+ type: 'number'
20138
+ })
20139
+ .option('radii', {
20140
+ describe: '(ring) comma-sep. list of concentric radii, ascending order'
20141
+ })
20142
+ .option('length', {
20143
+ old_alias: 'arrow-length',
20144
+ describe: '(arrow) length of arrow in pixels'
20145
+ })
20146
+ .option('direction', {
20147
+ old_alias: 'arrow-direction',
20148
+ describe: '(arrow) angle off vertical (-90 = left-pointing)'
20149
+ })
20150
+ .option('head-angle', {
20151
+ old_alias: 'arrow-head-angle',
20152
+ describe: '(arrow) angle of tip of arrow (default is 40 degrees)'
20153
+ })
20154
+ .option('head-width', {
20155
+ old_alias: 'arrow-head-width',
20156
+ describe: '(arrow) width of arrow head from side to side'
20157
+ })
20158
+ .option('head-length', {
20159
+ old_alias: 'arrow-head-width',
20160
+ describe: '(arrow) length of head (alternative to head-angle)'
20161
+ })
20162
+ .option('head-shape', {
20163
+ // describe: 'options: a b c'
20164
+ })
20165
+ .option('stem-width', {
20166
+ old_alias: 'arrow-stem-width',
20167
+ describe: '(arrow) width of stem at its widest point'
20168
+ })
20169
+ .option('stem-length', {
20170
+ old_alias: 'arrow-stem-length',
20171
+ describe: '(arrow) alternative to length'
20172
+ })
20173
+ .option('stem-taper', {
20174
+ old_alias: 'arrow-stem-taper',
20175
+ describe: '(arrow) factor for tapering the width of the stem (0-1)'
20176
+ })
20177
+ .option('stem-curve', {
20178
+ old_alias: 'arrow-stem-curve',
20179
+ describe: '(arrow) curvature in degrees (default is 0)'
20180
+ })
20181
+ .option('min-stem-ratio', {
20182
+ old_alias: 'arrow-min-stem',
20183
+ describe: '(arrow) min ratio of stem to total length',
20184
+ type: 'number'
20035
20185
  })
20036
20186
  .option('stroke', {})
20037
20187
  .option('stroke-width', {})
20038
- .option('fill', {})
20039
- .option('length', {})
20040
- .option('rotation', {})
20188
+ .option('fill', {
20189
+ describe: 'symbol fill color'
20190
+ })
20041
20191
  .option('effect', {})
20042
- .option('arrow-head-angle', {})
20043
- .option('arrow-stem-width', {})
20044
- .option('arrow-head-width', {})
20045
- .option('arrow-stem-curve', {})
20046
- .option('arrow-stem-taper', {})
20047
- .option('arrow-scaling', {})
20048
- .option('where', whereOpt)
20049
- .option('target', targetOpt);
20192
+ // .option('where', whereOpt)
20193
+ .option('name', nameOpt)
20194
+ .option('target', targetOpt)
20195
+ .option('no-replace', noReplaceOpt);
20050
20196
  // .option('name', nameOpt);
20051
20197
 
20198
+
20052
20199
  parser.command('target')
20053
20200
  .describe('set active layer (or layers)')
20054
20201
  .option('target', {
@@ -20199,13 +20346,6 @@ ${svg}
20199
20346
  })
20200
20347
  .option('name', nameOpt);
20201
20348
 
20202
- parser.command('include')
20203
- .describe('import JS data and functions for use in JS expressions')
20204
- .option('file', {
20205
- DEFAULT: true,
20206
- describe: 'file containing a JS object with key:value pairs to import'
20207
- });
20208
-
20209
20349
  parser.command('fuzzy-join')
20210
20350
  .describe('join points to polygons, with data fill and fuzzy match')
20211
20351
  .option('source', {
@@ -20341,16 +20481,6 @@ ${svg}
20341
20481
  })
20342
20482
  .option('name', nameOpt);
20343
20483
 
20344
- // parser.command('shapes')
20345
- // .describe('convert points to shapes')
20346
- // .option('type', {
20347
- // })
20348
- // .option('size', {
20349
- // })
20350
- // .option('rotation', {
20351
- // })
20352
-
20353
-
20354
20484
  parser.command('subdivide')
20355
20485
  .describe('recursively split a layer using a JS expression')
20356
20486
  .validate(validateExpressionOpt)
@@ -21214,12 +21344,13 @@ ${svg}
21214
21344
  if (this instanceof ShpReader === false) {
21215
21345
  return new ShpReader(shpSrc, shxSrc);
21216
21346
  }
21217
-
21218
21347
  var shpFile = utils.isString(shpSrc) ? new FileReader(shpSrc) : new BufferReader(shpSrc);
21219
21348
  var header = parseHeader(shpFile.readToBinArray(0, 100));
21220
- var shpSize = shpFile.size();
21221
- var RecordClass = new ShpRecordClass(header.type);
21222
- var shpOffset, recordCount;
21349
+ var shpType = header.type;
21350
+ var shpOffset = 100; // used when reading .shp without .shx
21351
+ var recordCount = 0;
21352
+ var badRecordNumberCount = 0;
21353
+ var RecordClass = new ShpRecordClass(shpType);
21223
21354
  var shxBin, shxFile;
21224
21355
 
21225
21356
  if (shxSrc) {
@@ -21227,8 +21358,6 @@ ${svg}
21227
21358
  shxBin = shxFile.readToBinArray(0, shxFile.size()).bigEndian();
21228
21359
  }
21229
21360
 
21230
- reset();
21231
-
21232
21361
  this.header = function() {
21233
21362
  return header;
21234
21363
  };
@@ -21246,37 +21375,35 @@ ${svg}
21246
21375
 
21247
21376
  // Iterator interface for reading shape records
21248
21377
  this.nextShape = function() {
21249
- var shape = readNextShape(recordCount);
21250
- if (shape) {
21251
- recordCount++;
21252
- } else {
21253
- shpFile.close();
21254
- reset();
21378
+ var shape;
21379
+ if (!shpFile) {
21380
+ error('Tried to read from a used ShpReader');
21381
+ // return null; // this reader was already used
21255
21382
  }
21383
+ shape = readNextShape(recordCount);
21384
+ if (!shape) {
21385
+ done();
21386
+ return null;
21387
+ }
21388
+ recordCount++;
21256
21389
  return shape;
21257
21390
  };
21258
21391
 
21259
21392
  // Returns a shape record or null if no more shapes can be read
21393
+ // i: Expected 0-based index of the next record
21260
21394
  //
21261
21395
  function readNextShape(i) {
21262
- var expectedId = i + 1; // Shapefile ids are 1-based
21263
- var shape, offset;
21264
- if (shxBin) {
21265
- if (shxFile.size() <= 100 + i * 8) return null; // done
21266
- shxBin.position(100 + i * 8);
21267
- offset = shxBin.readUint32() * 2;
21268
- shape = readIndexedShape(shpFile, offset, expectedId);
21269
- } else {
21270
- // Reading without a .shx file (returns null at end-of-file)
21271
- offset = shpOffset;
21272
- shape = readNonIndexedShape(shpFile, offset, expectedId);
21273
- }
21274
- return shape || null;
21396
+ return shxBin ?
21397
+ readIndexedShape(shpFile, shxBin, i) :
21398
+ readNonIndexedShape(shpFile, shpOffset, i);
21275
21399
  }
21276
21400
 
21277
- function reset() {
21278
- shpOffset = 100;
21279
- recordCount = 0;
21401
+ function done() {
21402
+ shpFile.close();
21403
+ shpFile = shxFile = shxBin = null;
21404
+ if (badRecordNumberCount > 0) {
21405
+ message(`Warning: ${badRecordNumberCount}/${recordCount} features have non-standard record numbers in the .shp file.`);
21406
+ }
21280
21407
  }
21281
21408
 
21282
21409
  function parseHeader(bin) {
@@ -21307,33 +21434,35 @@ ${svg}
21307
21434
 
21308
21435
 
21309
21436
  function readShapeAtOffset(shpFile, offset) {
21310
- var shape = null,
21311
- recordSize, recordType, recordId, goodSize, goodType, bin;
21312
-
21313
- if (offset + 12 <= shpSize) {
21314
- bin = shpFile.readToBinArray(offset, 12);
21315
- recordId = bin.bigEndian().readUint32();
21316
- // record size is bytes in content section + 8 header bytes
21317
- recordSize = bin.readUint32() * 2 + 8;
21318
- recordType = bin.littleEndian().readUint32();
21319
- goodSize = offset + recordSize <= shpSize && recordSize >= 12;
21320
- goodType = recordType === 0 || recordType == header.type;
21321
- if (goodSize && goodType) {
21322
- bin = shpFile.readToBinArray(offset, recordSize);
21323
- shape = new RecordClass(bin, recordSize);
21324
- }
21437
+ var fileSize = shpFile.size();
21438
+ if (offset + 12 > fileSize) return null; // reached end-of-file
21439
+ var bin = shpFile.readToBinArray(offset, 12);
21440
+ var recordId = bin.bigEndian().readUint32();
21441
+ // record size is bytes in content section + 8 header bytes
21442
+ var recordSize = bin.readUint32() * 2 + 8;
21443
+ var recordType = bin.littleEndian().readUint32();
21444
+ var goodSize = offset + recordSize <= fileSize && recordSize >= 12;
21445
+ var goodType = recordType === 0 || recordType == shpType;
21446
+ if (!goodSize || !goodType) {
21447
+ return null;
21325
21448
  }
21326
- return shape;
21449
+ bin = shpFile.readToBinArray(offset, recordSize);
21450
+ return new RecordClass(bin, recordSize);
21327
21451
  }
21328
21452
 
21329
- function readIndexedShape(shpFile, offset, expectedId) {
21453
+ function readIndexedShape(shpFile, shxBin, i) {
21454
+ if (shxBin.size() <= 100 + i * 8) return null; // done
21455
+ shxBin.position(100 + i * 8);
21456
+ var expectedId = i + 1;
21457
+ var offset = shxBin.readUint32() * 2;
21458
+ var recLen = shxBin.readUint32() * 2; // TODO: match this to recLen in .shp
21330
21459
  var shape = readShapeAtOffset(shpFile, offset);
21331
21460
  if (!shape) {
21332
21461
  stop('Index of Shapefile record', expectedId, 'in the .shx file is invalid.');
21333
21462
  }
21334
21463
  if (shape.id != expectedId) {
21335
- // stop("Found a Shapefile record with an out-of-sequence id (" + shape.id + ") -- bailing.");
21336
- message(`Warning: A feature has a different record number in .shx (${expectedId}) and .shp (${shape.id}).`);
21464
+ badRecordNumberCount++;
21465
+ verbose(`Warning: A feature has a different record number in .shx (${expectedId}) and .shp (${shape.id}).`);
21337
21466
  }
21338
21467
  // TODO: consider printing verbose message if a .shp file contains garbage bytes
21339
21468
  // example files:
@@ -21346,11 +21475,12 @@ ${svg}
21346
21475
  // in consecutive sequence in the .shp file. This is a problem when the .shx
21347
21476
  // index file is not present.
21348
21477
  //
21349
- // Here, we try to scan past invalid content to find the next record.
21478
+ // Here, we try to scan past any invalid content to find the next record.
21350
21479
  // Records are required to be in sequential order.
21351
21480
  //
21352
- function readNonIndexedShape(shpFile, start, expectedId) {
21353
- var offset = start,
21481
+ function readNonIndexedShape(shpFile, start, i) {
21482
+ var expectedId = i + 1, // Shapefile ids are 1-based
21483
+ offset = start,
21354
21484
  fileSize = shpFile.size(),
21355
21485
  shape = null,
21356
21486
  bin, recordId, recordType, isValidType;
@@ -21358,7 +21488,7 @@ ${svg}
21358
21488
  bin = shpFile.readToBinArray(offset, 12);
21359
21489
  recordId = bin.bigEndian().readUint32();
21360
21490
  recordType = bin.littleEndian().skipBytes(4).readUint32();
21361
- isValidType = recordType == header.type || recordType === 0;
21491
+ isValidType = recordType == shpType || recordType === 0;
21362
21492
  if (!isValidType || recordId != expectedId && recordType === 0) {
21363
21493
  offset += 4; // keep scanning -- try next integer position
21364
21494
  continue;
@@ -21385,22 +21515,6 @@ ${svg}
21385
21515
  return this.header().type;
21386
21516
  };
21387
21517
 
21388
- ShpReader.prototype.getCounts = function() {
21389
- var counts = {
21390
- nullCount: 0,
21391
- partCount: 0,
21392
- shapeCount: 0,
21393
- pointCount: 0
21394
- };
21395
- this.forEachShape(function(shp) {
21396
- if (shp.isNull) counts.nullCount++;
21397
- counts.pointCount += shp.pointCount;
21398
- counts.partCount += shp.partCount;
21399
- counts.shapeCount++;
21400
- });
21401
- return counts;
21402
- };
21403
-
21404
21518
  // Apply snapping, remove duplicate coords and clean up defective paths in a dataset
21405
21519
  // Assumes that any CRS info has been added to the dataset
21406
21520
  // @opts: import options
@@ -21603,7 +21717,7 @@ ${svg}
21603
21717
  collectionType = 'mixed';
21604
21718
  }
21605
21719
  } else if (currType != t) {
21606
- stop("Unable to import mixed-geometry GeoJSON features");
21720
+ stop("Unable to import mixed-geometry features");
21607
21721
  }
21608
21722
  }
21609
21723
 
@@ -21714,6 +21828,33 @@ ${svg}
21714
21828
  importShp: importShp
21715
21829
  });
21716
21830
 
21831
+ function importGeoJSON(src, optsArg) {
21832
+ var opts = optsArg || {};
21833
+ var supportedGeometries = Object.keys(GeoJSON.pathImporters),
21834
+ srcObj = utils.isString(src) ? JSON.parse(src) : src,
21835
+ importer = new GeoJSONParser(opts),
21836
+ srcCollection, dataset;
21837
+
21838
+ // Convert single feature or geometry into a collection with one member
21839
+ if (srcObj.type == 'Feature') {
21840
+ srcCollection = {
21841
+ type: 'FeatureCollection',
21842
+ features: [srcObj]
21843
+ };
21844
+ } else if (supportedGeometries.includes(srcObj.type)) {
21845
+ srcCollection = {
21846
+ type: 'GeometryCollection',
21847
+ geometries: [srcObj]
21848
+ };
21849
+ } else {
21850
+ srcCollection = srcObj;
21851
+ }
21852
+ (srcCollection.features || srcCollection.geometries || []).forEach(importer.parseObject);
21853
+ dataset = importer.done();
21854
+ importCRS(dataset, srcObj); // TODO: remove this
21855
+ return dataset;
21856
+ }
21857
+
21717
21858
  function GeoJSONParser(opts) {
21718
21859
  var idField = opts.id_field || GeoJSON.ID_FIELD,
21719
21860
  importer = new PathImporter(opts),
@@ -21735,8 +21876,11 @@ ${svg}
21735
21876
  geom = o;
21736
21877
  }
21737
21878
  // TODO: improve so geometry_type option skips features instead of creating null geometries
21738
- importer.startShape(rec);
21739
- if (geom) GeoJSON.importGeometry(geom, importer, opts);
21879
+ if (geom && geom.type == 'GeometryCollection') {
21880
+ GeoJSON.importComplexFeature(importer, geom, rec, opts);
21881
+ } else {
21882
+ GeoJSON.importSimpleFeature(importer, geom, rec, opts);
21883
+ }
21740
21884
  };
21741
21885
 
21742
21886
  this.done = function() {
@@ -21744,50 +21888,58 @@ ${svg}
21744
21888
  };
21745
21889
  }
21746
21890
 
21747
- function importGeoJSON(src, optsArg) {
21748
- var opts = optsArg || {};
21749
- var supportedGeometries = Object.keys(GeoJSON.pathImporters),
21750
- srcObj = utils.isString(src) ? JSON.parse(src) : src,
21751
- importer = new GeoJSONParser(opts),
21752
- srcCollection, dataset;
21753
-
21754
- // Convert single feature or geometry into a collection with one member
21755
- if (srcObj.type == 'Feature') {
21756
- srcCollection = {
21757
- type: 'FeatureCollection',
21758
- features: [srcObj]
21759
- };
21760
- } else if (supportedGeometries.includes(srcObj.type)) {
21761
- srcCollection = {
21762
- type: 'GeometryCollection',
21763
- geometries: [srcObj]
21764
- };
21765
- } else {
21766
- srcCollection = srcObj;
21891
+ GeoJSON.importComplexFeature = function(importer, geom, rec, opts) {
21892
+ var types = divideGeometriesByType(geom.geometries || []);
21893
+ if (types.length === 0) {
21894
+ importer.startShape(rec); // import a feature with null geometry
21895
+ return;
21767
21896
  }
21768
- (srcCollection.features || srcCollection.geometries || []).forEach(importer.parseObject);
21769
- dataset = importer.done();
21770
- importCRS(dataset, srcObj); // TODO: remove this
21771
- return dataset;
21897
+ types.forEach(function(geometries, i) {
21898
+ importer.startShape(copyRecord(rec));
21899
+ geometries.forEach(function(geom) {
21900
+ GeoJSON.importSimpleGeometry(importer, geom, opts);
21901
+ });
21902
+ });
21903
+ };
21904
+
21905
+ function divideGeometriesByType(geometries, index) {
21906
+ index = index || {};
21907
+ geometries.forEach(function(geom) {
21908
+ if (!geom) return;
21909
+ var mtype = GeoJSON.translateGeoJSONType(geom.type);
21910
+ if (mtype) {
21911
+ if (mtype in index === false) {
21912
+ index[mtype] = [];
21913
+ }
21914
+ index[mtype].push(geom);
21915
+ } else if (geom.type == 'GeometryCollection') {
21916
+ divideGeometriesByType(geom.geometries || [], index);
21917
+ }
21918
+ });
21919
+ return Object.values(index);
21772
21920
  }
21773
21921
 
21774
- GeoJSON.importGeometry = function(geom, importer, opts) {
21775
- var type = geom.type;
21776
- if (type in GeoJSON.pathImporters) {
21922
+ GeoJSON.importSimpleFeature = function(importer, geom, rec, opts) {
21923
+ importer.startShape(rec);
21924
+ GeoJSON.importSimpleGeometry(importer, geom, opts);
21925
+ };
21926
+
21927
+ GeoJSON.importSimpleGeometry = function(importer, geom, opts) {
21928
+ var type = geom ? geom.type : null;
21929
+ if (type === null) {
21930
+ // no geometry to import
21931
+ } else if (type in GeoJSON.pathImporters) {
21777
21932
  if (opts.geometry_type && opts.geometry_type != GeoJSON.translateGeoJSONType(type)) {
21778
21933
  // kludge to filter out all but one type of geometry
21779
21934
  return;
21780
21935
  }
21781
21936
  GeoJSON.pathImporters[type](geom.coordinates, importer);
21782
- } else if (type == 'GeometryCollection') {
21783
- geom.geometries.forEach(function(geom) {
21784
- GeoJSON.importGeometry(geom, importer, opts);
21785
- });
21786
21937
  } else {
21787
- verbose("GeoJSON.importGeometry() Unsupported geometry type:", geom.type);
21938
+ verbose("Unsupported geometry type:", geom.type);
21788
21939
  }
21789
21940
  };
21790
21941
 
21942
+
21791
21943
  // Functions for importing geometry coordinates using a PathImporter
21792
21944
  //
21793
21945
  GeoJSON.pathImporters = {
@@ -21826,8 +21978,8 @@ ${svg}
21826
21978
 
21827
21979
  var GeojsonImport = /*#__PURE__*/Object.freeze({
21828
21980
  __proto__: null,
21829
- GeoJSONParser: GeoJSONParser,
21830
21981
  importGeoJSON: importGeoJSON,
21982
+ GeoJSONParser: GeoJSONParser,
21831
21983
  importCRS: importCRS
21832
21984
  });
21833
21985
 
@@ -26209,7 +26361,6 @@ ${svg}
26209
26361
  if (constTol) return constTol;
26210
26362
  return constTol ? constTol : meterDist * pctOfRadius;
26211
26363
  };
26212
-
26213
26364
  }
26214
26365
 
26215
26366
  function getBufferDistanceFunction(lyr, dataset, opts) {
@@ -27382,7 +27533,7 @@ ${svg}
27382
27533
  cmd.buffer = makeBufferLayer;
27383
27534
 
27384
27535
  function makeBufferLayer(lyr, dataset, opts) {
27385
- var dataset2, lyr2;
27536
+ var dataset2;
27386
27537
  if (lyr.geometry_type == 'point') {
27387
27538
  dataset2 = makePointBuffer(lyr, dataset, opts);
27388
27539
  } else if (lyr.geometry_type == 'polyline') {
@@ -27392,13 +27543,9 @@ ${svg}
27392
27543
  } else {
27393
27544
  stop("Unsupported geometry type");
27394
27545
  }
27395
- var outputLayers = mergeDatasetsIntoDataset(dataset, [dataset2]);
27396
- lyr2 = outputLayers[0];
27397
- setOutputLayerName(lyr2, lyr, null, opts);
27398
- if (lyr.data && !lyr2.data) {
27399
- lyr2.data = opts.no_replace ? lyr.data.clone() : lyr.data;
27400
- }
27401
- return outputLayers;
27546
+
27547
+ var lyr2 = mergeOutputLayerIntoDataset(lyr, dataset, dataset2, opts);
27548
+ return [lyr2];
27402
27549
  }
27403
27550
 
27404
27551
  // TODO: support three or more stops
@@ -33983,7 +34130,7 @@ ${svg}
33983
34130
 
33984
34131
  function createMeridianPart(x, ymin, ymax) {
33985
34132
  var coords = densifyPathByInterval([[x, ymin], [x, ymax]], precision);
33986
- meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord(x)}));
34133
+ meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord$1(x)}));
33987
34134
  }
33988
34135
 
33989
34136
  function createParallel(y) {
@@ -33993,7 +34140,7 @@ ${svg}
33993
34140
  }
33994
34141
 
33995
34142
  // remove tiny offsets
33996
- function roundCoord(x) {
34143
+ function roundCoord$1(x) {
33997
34144
  return +x.toFixed(3) || 0;
33998
34145
  }
33999
34146
 
@@ -36644,6 +36791,99 @@ ${svg}
36644
36791
  parseScalebarLabelToKm: parseScalebarLabelToKm
36645
36792
  });
36646
36793
 
36794
+ cmd.shape = function(targetDataset, opts) {
36795
+ var geojson, dataset;
36796
+ if (opts.coordinates) {
36797
+ geojson = makeShapeFromCoords(opts);
36798
+ } else if (opts.type == 'circle') {
36799
+ geojson = makeCircle(opts);
36800
+ } else if (opts.type == 'rectangle' && opts.bbox) {
36801
+ geojson = getRectangleGeoJSON(opts);
36802
+ } else {
36803
+ stop('Missing coordinates parameter');
36804
+ }
36805
+ // TODO: project shape if targetDataset is projected
36806
+ dataset = importGeoJSON(geojson, {});
36807
+ if (opts.rotation) {
36808
+ rotateDatasetCoords(dataset, opts.rotation);
36809
+ }
36810
+ dataset.layers[0].name = opts.name || opts.type || 'shape';
36811
+ return dataset;
36812
+ };
36813
+
36814
+ function getRectangleGeoJSON(opts) {
36815
+ var bbox = opts.bbox,
36816
+ xmin = bbox[0],
36817
+ ymin = bbox[1],
36818
+ xmax = bbox[2],
36819
+ ymax = bbox[3],
36820
+ interval = 0.5,
36821
+ coords = [],
36822
+ type = opts.geometry == 'polyline' ? 'LineString' : 'Polygon';
36823
+ addSide(xmin, ymin, xmin, ymax);
36824
+ addSide(xmin, ymax, xmax, ymax);
36825
+ addSide(xmax, ymax, xmax, ymin);
36826
+ addSide(xmax, ymin, xmin, ymin);
36827
+ coords.push([xmin, ymin]);
36828
+ return {
36829
+ type: type,
36830
+ coordinates: type == 'Polygon' ? [coords] : coords
36831
+ };
36832
+
36833
+ function addSide(x1, y1, x2, y2) {
36834
+ var dx = x2 - x1,
36835
+ dy = y2 - y1,
36836
+ n = Math.ceil(Math.max(Math.abs(dx) / interval, Math.abs(dy) / interval)),
36837
+ xint = dx / n,
36838
+ yint = dy / n;
36839
+ for (var i=0; i<n; i++) {
36840
+ coords.push([x1 + i * xint, y1 + i * yint]);
36841
+ }
36842
+ }
36843
+ }
36844
+
36845
+ function makeCircle(opts) {
36846
+ if (opts.radius > 0 === false && opts.radius_angle > 0 === false) {
36847
+ stop('Missing required radius parameter.');
36848
+ }
36849
+ var cp = opts.center || [0, 0];
36850
+ var radius = opts.radius || getCircleRadiusFromAngle(getCRS('wgs84'), opts.radius_angle);
36851
+ return getCircleGeoJSON(cp, radius, null, {geometry_type : opts.geometry || 'polygon'});
36852
+ }
36853
+
36854
+ function makeShapeFromCoords(opts) {
36855
+ var coordinates = [];
36856
+ var offsets = opts.offsets || [];
36857
+ var coords = opts.coordinates;
36858
+ var type, i, x, y;
36859
+ if (coords.length >= 2 === false) {
36860
+ stop('Invalid coordinates parameter.');
36861
+ }
36862
+ for (i=0; i<coords.length; i+= 2) {
36863
+ x = coords[i];
36864
+ y = coords[i + 1];
36865
+ coordinates.push([x, y]);
36866
+ }
36867
+ for (i=0; i<offsets.length; i+=2) {
36868
+ x += offsets[i];
36869
+ y += offsets[i + 1];
36870
+ coordinates.push([x, y]);
36871
+ }
36872
+ if (GeoJSON.pathIsRing(coordinates)) {
36873
+ type = 'Polygon';
36874
+ } else if (opts.closed && coordinates.length >= 3) {
36875
+ type = 'Polygon';
36876
+ coordinates.push(coordinates[0]);
36877
+ } else {
36878
+ type = 'LineString';
36879
+ }
36880
+ return {
36881
+ type: type,
36882
+ coordinates: type == 'Polygon' ? [coordinates] : coordinates
36883
+ };
36884
+
36885
+ }
36886
+
36647
36887
  function calcSimplifyStats(arcs, use3D) {
36648
36888
  var distSq = use3D ? pointSegGeoDistSq : geom.pointSegDistSq,
36649
36889
  calcAngle = use3D ? geom.signedAngleSph : geom.signedAngle,
@@ -37731,144 +37971,431 @@ ${svg}
37731
37971
  });
37732
37972
  };
37733
37973
 
37734
- symbolBuilders.arrow = function(d) {
37735
- var len = 'length' in d ? d.length : 10;
37736
- var filled = 'fill' in d;
37737
- return filled ? getFilledArrow(d, len) : getStickArrow(d, len);
37738
- };
37974
+ var roundCoord = getRoundingFunction(0.01);
37739
37975
 
37740
- function getStickArrow(d, len) {
37741
- return {
37742
- type: 'polyline',
37743
- coordinates: getStickArrowCoords(d, len),
37744
- stroke: d.stroke || 'magenta',
37745
- 'stroke-width': 'stroke-width' in d ? d['stroke-width'] : 1
37746
- };
37976
+ function forEachSymbolCoord(coords, cb) {
37977
+ var isPoint = coords && utils.isNumber(coords[0]);
37978
+ var isNested = !isPoint && coords && Array.isArray(coords[0]);
37979
+ if (isPoint) return cb(coords);
37980
+ for (var i=0; i<coords.length; i++) {
37981
+ if (isNested) forEachSymbolCoord(coords[i], cb);
37982
+ }
37747
37983
  }
37748
37984
 
37749
- function getFilledArrow(d, totalLen) {
37750
- return {
37751
- type: 'polygon',
37752
- coordinates: getFilledArrowCoords(d, totalLen),
37753
- fill: d.fill || 'magenta'
37754
- };
37985
+ function flipY(coords) {
37986
+ forEachSymbolCoord(coords, function(p) {
37987
+ p[1] = -p[1];
37988
+ });
37755
37989
  }
37756
37990
 
37757
- function getScale(totalLen, headLen) {
37758
- var maxHeadPct = 0.60;
37759
- var headPct = headLen / totalLen;
37760
- if (headPct > maxHeadPct) {
37761
- return maxHeadPct / headPct;
37991
+ function scaleAndShiftCoords(coords, scale, shift) {
37992
+ forEachSymbolCoord(coords, function(xy) {
37993
+ xy[0] = xy[0] * scale + shift[0];
37994
+ xy[1] = xy[1] * scale + shift[1];
37995
+ });
37996
+ }
37997
+
37998
+ function roundCoordsForSVG(coords) {
37999
+ forEachSymbolCoord(coords, function(p) {
38000
+ p[0] = roundCoord(p[0]);
38001
+ p[1] = roundCoord(p[1]);
38002
+ });
38003
+ }
38004
+
38005
+ function rotateCoords(coords, rotation) {
38006
+ if (!rotation) return;
38007
+ var f = getAffineTransform(rotation, 1, [0, 0], [0, 0]);
38008
+ forEachSymbolCoord(coords, function(p) {
38009
+ var p2 = f(p[0], p[1]);
38010
+ p[0] = p2[0];
38011
+ p[1] = p2[1];
38012
+ });
38013
+ }
38014
+
38015
+ function findArcCenter(p1, p2, degrees) {
38016
+ var p3 = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2], // midpoint betw. p1, p2
38017
+ tan = 1 / Math.tan(degrees / 180 * Math.PI / 2),
38018
+ cp = getAffineTransform(90, tan, [0, 0], p3)(p2[0], p2[1]);
38019
+ return cp;
38020
+ }
38021
+
38022
+ // export function addBezierArcControlPoints(p1, p2, degrees) {
38023
+ function addBezierArcControlPoints(points, degrees) {
38024
+ // source: https://stackoverflow.com/questions/734076/how-to-best-approximate-a-geometrical-arc-with-a-bezier-curve
38025
+ var p2 = points.pop(),
38026
+ p1 = points.pop(),
38027
+ cp = findArcCenter(p1, p2, degrees),
38028
+ xc = cp[0],
38029
+ yc = cp[1],
38030
+ ax = p1[0] - xc,
38031
+ ay = p1[1] - yc,
38032
+ bx = p2[0] - xc,
38033
+ by = p2[1] - yc,
38034
+ q1 = ax * ax + ay * ay,
38035
+ q2 = q1 + ax * bx + ay * by,
38036
+ k2 = 4/3 * (Math.sqrt(2 * q1 * q2) - q2) / (ax * by - ay * bx);
38037
+
38038
+ points.push(p1);
38039
+ points.push([xc + ax - k2 * ay, yc + ay + k2 * ax, 'C']);
38040
+ points.push([xc + bx + k2 * by, yc + by - k2 * bx, 'C']);
38041
+ points.push(p2);
38042
+ }
38043
+
38044
+ // export function getStickArrowCoords(d, totalLen) {
38045
+ // var minStemRatio = getMinStemRatio(d);
38046
+ // var headAngle = d['arrow-head-angle'] || 90;
38047
+ // var curve = d['arrow-stem-curve'] || 0;
38048
+ // var unscaledHeadWidth = d['arrow-head-width'] || 9;
38049
+ // var unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle);
38050
+ // var scale = getScale(totalLen, unscaledHeadLen, minStemRatio);
38051
+ // var headWidth = unscaledHeadWidth * scale;
38052
+ // var headLen = unscaledHeadLen * scale;
38053
+ // var tip = getStickArrowTip(totalLen, curve);
38054
+ // var stem = [[0, 0], tip.concat()];
38055
+ // if (curve) {
38056
+ // addBezierArcControlPoints(stem, curve);
38057
+ // }
38058
+ // if (!headLen) return [stem];
38059
+ // var head = [addPoints([-headWidth / 2, -headLen], tip), tip.concat(), addPoints([headWidth / 2, -headLen], tip)];
38060
+
38061
+ // rotateCoords(stem, d.rotation);
38062
+ // rotateCoords(head, d.rotation);
38063
+ // return [stem, head];
38064
+ // }
38065
+
38066
+
38067
+ function getFilledArrowCoords(d) {
38068
+ var direction = d.rotation || d.direction || 0,
38069
+ stemTaper = d['stem-taper'] || 0,
38070
+ stemCurve = d['stem-curve'] || 0,
38071
+ size = calcArrowSize(d);
38072
+
38073
+ if (!size) return null;
38074
+
38075
+ var headDx = size.headWidth / 2,
38076
+ stemDx = size.stemWidth / 2,
38077
+ baseDx = stemDx * (1 - stemTaper),
38078
+ coords;
38079
+
38080
+ if (!stemCurve || Math.abs(stemCurve) > 90) {
38081
+ coords = calcStraightArrowCoords(size.stemLen, size.headLen, stemDx, headDx, baseDx);
38082
+ } else {
38083
+ if (direction > 0) stemCurve = -stemCurve;
38084
+ coords = getCurvedArrowCoords(size.stemLen, size.headLen, size.stemCurve, stemDx, headDx, baseDx);
37762
38085
  }
37763
- return 1;
38086
+
38087
+ rotateCoords(coords, direction);
38088
+ if (d.flipped) {
38089
+ flipY(coords);
38090
+ }
38091
+ return [coords];
37764
38092
  }
37765
38093
 
37766
- function getStickArrowTip(totalLen, curve) {
37767
- // curve/2 intersects the arrowhead at 90deg (trigonometry)
37768
- var theta = Math.abs(curve/2) / 180 * Math.PI;
37769
- var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1);
37770
- var dy = totalLen * Math.cos(theta);
37771
- return [dx, dy];
38094
+ function calcStraightArrowCoords(stemLen, headLen, stemDx, headDx, baseDx) {
38095
+ return [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
38096
+ [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]];
37772
38097
  }
37773
38098
 
37774
- function addPoints(a, b) {
37775
- return [a[0] + b[0], a[1] + b[1]];
38099
+ function calcArrowSize(d) {
38100
+ var totalLen = d.radius || d.length || d.r || 0,
38101
+ unscaledStemWidth = d['stem-width'] || 2,
38102
+ unscaledHeadWidth = d['head-width'] || unscaledStemWidth * 3,
38103
+ unscaledHeadLen = d['head-length'] || calcHeadLength(unscaledHeadWidth, d),
38104
+ scale = 1,
38105
+ o = {};
38106
+
38107
+ if (totalLen > 0) {
38108
+ scale = calcScale(totalLen, unscaledHeadLen, d);
38109
+ o.headWidth = unscaledHeadWidth * scale;
38110
+ o.headLen = unscaledHeadLen * scale;
38111
+ o.stemWidth = unscaledStemWidth * scale;
38112
+ o.stemLen = totalLen - o.headLen;
38113
+
38114
+ } else {
38115
+ o.headWidth = unscaledHeadWidth;
38116
+ o.headLen = unscaledHeadLen;
38117
+ o.stemWidth = unscaledStemWidth;
38118
+ o.stemLen = d['stem-length'] || 0;
38119
+ }
38120
+
38121
+ if (unscaledHeadWidth < unscaledStemWidth) {
38122
+ stop('Arrow head must be at least as wide as the stem.');
38123
+ }
38124
+ return o;
37776
38125
  }
37777
38126
 
37778
- function getStickArrowCoords(d, totalLen) {
37779
- var headAngle = d['arrow-head-angle'] || 90;
37780
- var curve = d['arrow-stem-curve'] || 0;
37781
- var unscaledHeadWidth = d['arrow-head-width'] || 9;
37782
- var unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle);
37783
- var scale = getScale(totalLen, unscaledHeadLen); // scale down small arrows
37784
- var headWidth = unscaledHeadWidth * scale;
37785
- var headLen = unscaledHeadLen * scale;
37786
- var tip = getStickArrowTip(totalLen, curve);
37787
- var stem = [[0, 0], tip.concat()];
37788
- if (curve) {
37789
- addBezierArcControlPoints(stem, curve);
38127
+ function calcScale(totalLen, headLen, d) {
38128
+ var minStemRatio = d['min-stem'] >= 0 ? d['min-stem'] : 0;
38129
+ var stemLen = d['stem-length'] || 0;
38130
+ var maxHeadPct = 1 - minStemRatio;
38131
+ var headPct = headLen / totalLen;
38132
+ var scale = 1;
38133
+
38134
+ if (headPct > maxHeadPct) {
38135
+ scale = maxHeadPct / headPct;
38136
+ } else if (stemLen + headLen > totalLen) {
38137
+ scale = totalLen / (stemLen + headLen);
37790
38138
  }
37791
- if (!headLen) return [stem];
37792
- var head = [addPoints([-headWidth / 2, -headLen], tip), tip.concat(), addPoints([headWidth / 2, -headLen], tip)];
38139
+ return scale;
38140
+ }
37793
38141
 
37794
- rotateSymbolCoords(stem, d.rotation);
37795
- rotateSymbolCoords(head, d.rotation);
37796
- return [stem, head];
38142
+ // function getStickArrowTip(totalLen, curve) {
38143
+ // // curve/2 intersects the arrowhead at 90deg (trigonometry)
38144
+ // var theta = Math.abs(curve/2) / 180 * Math.PI;
38145
+ // var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1);
38146
+ // var dy = totalLen * Math.cos(theta);
38147
+ // return [dx, dy];
38148
+ // }
38149
+
38150
+ function addPoints(a, b) {
38151
+ return [a[0] + b[0], a[1] + b[1]];
37797
38152
  }
37798
38153
 
37799
- function getHeadLength(headWidth, headAngle) {
38154
+
38155
+ function calcHeadLength(headWidth, d) {
38156
+ var headAngle = d['head-angle'] || 40;
37800
38157
  var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio
37801
38158
  return headWidth * headRatio;
37802
38159
  }
37803
38160
 
37804
- function getFilledArrowCoords(d, totalLen) {
37805
- var headAngle = d['arrow-head-angle'] || 40,
37806
- unscaledStemWidth = d['arrow-stem-width'] || 2,
37807
- unscaledHeadWidth = d['arrow-head-width'] || unscaledStemWidth * 3,
37808
- unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle),
37809
- scale = getScale(totalLen, unscaledHeadLen), // scale down small arrows
37810
- headWidth = unscaledHeadWidth * scale,
37811
- headLen = unscaledHeadLen * scale,
37812
- stemWidth = unscaledStemWidth * scale,
37813
- stemTaper = d['arrow-stem-taper'] || 0,
37814
- stemLen = totalLen - headLen;
37815
-
37816
- var headDx = headWidth / 2,
37817
- stemDx = stemWidth / 2,
37818
- baseDx = stemDx * (1 - stemTaper);
37819
-
37820
- var coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
37821
- [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]];
38161
+ function getCurvedArrowCoords(stemLen, headLen, curvature, stemDx, headDx, baseDx) {
38162
+ // coordinates go counter clockwise, starting from the leftmost head coordinate
38163
+ var theta = Math.abs(curvature) / 180 * Math.PI;
38164
+ var sign = curvature > 0 ? 1 : -1;
38165
+ var dx = stemLen * Math.sin(theta / 2) * sign;
38166
+ var dy = stemLen * Math.cos(theta / 2);
38167
+ var head = [[stemDx + dx, dy], [headDx + dx, dy],
38168
+ [dx, headLen + dy], [-headDx + dx, dy], [-stemDx + dx, dy]];
38169
+ var ax = baseDx * Math.cos(theta); // rotate arrow base
38170
+ var ay = baseDx * Math.sin(theta) * -sign;
38171
+ var leftStem = getCurvedStemCoords(-ax, -ay, -stemDx + dx, dy, theta);
38172
+ var rightStem = getCurvedStemCoords(ax, ay, stemDx + dx, dy, theta);
38173
+ var stem = leftStem.concat(rightStem.reverse());
38174
+ // stem.pop();
38175
+ return stem.concat(head);
38176
+ }
38177
+
38178
+ // ax, ay: point on the base
38179
+ // bx, by: point on the stem
38180
+ function getCurvedStemCoords(ax, ay, bx, by, theta0) {
38181
+ // case: curved side intrudes into head (because stem is too short)
38182
+ if (ay > by) {
38183
+ return [[ax * by / ay, by]];
38184
+ }
38185
+ var dx = bx - ax,
38186
+ dy = by - ay,
38187
+ dy1 = (dy * dy - dx * dx) / (2 * dy),
38188
+ dy2 = dy - dy1,
38189
+ dx2 = Math.sqrt(dx * dx + dy * dy) / 2,
38190
+ theta = Math.PI - Math.asin(dx2 / dy2) * 2,
38191
+ degrees = theta * 180 / Math.PI,
38192
+ radius = dy2 / Math.tan(theta / 2),
38193
+ leftBend = bx > ax,
38194
+ sign = leftBend ? 1 : -1,
38195
+ points = Math.round(degrees / 5) + 2,
38196
+ increment = theta / (points + 1),
38197
+ coords = [[bx, by]];
38198
+
38199
+ for (var i=1; i<= points; i++) {
38200
+ var phi = i * increment / 2;
38201
+ var sinPhi = Math.sin(phi);
38202
+ var cosPhi = Math.cos(phi);
38203
+ var c = sinPhi * radius * 2;
38204
+ var a = sinPhi * c;
38205
+ var b = cosPhi * c;
38206
+ coords.push([bx - a * sign, by - b]);
38207
+ }
38208
+ coords.push([ax, ay]);
38209
+ return coords;
38210
+ }
37822
38211
 
37823
- rotateSymbolCoords(coords, d.rotation);
38212
+ // sides: e.g. 5-pointed star has 10 sides
38213
+ // radius: distance from center to point
38214
+ //
38215
+ function getPolygonCoords(d) {
38216
+ var radius = d.radius || d.length || d.r;
38217
+ if (radius > 0 === false) return null;
38218
+ var type = d.type;
38219
+ var sides = +d.sides || getDefaultSides(type);
38220
+ var isStar = type == 'star';
38221
+ if (isStar && (sides < 6 || sides % 2 !== 0)) {
38222
+ stop(`Invalid number of sides for a star (${sides})`);
38223
+ } else if (sides >= 3 === false) {
38224
+ stop(`Invalid number of sides (${sides})`);
38225
+ }
38226
+ var coords = [],
38227
+ angle = 360 / sides,
38228
+ b = isStar ? 1 : 0.5,
38229
+ theta, even, len;
38230
+ if (d.orientation == 'b' || d.flipped || d.rotated) {
38231
+ b = 0;
38232
+ }
38233
+ for (var i=0; i<sides; i++) {
38234
+ even = i % 2 == 0;
38235
+ len = radius;
38236
+ if (isStar && even) {
38237
+ len *= (d.star_ratio || 0.5);
38238
+ }
38239
+ theta = (i + b) * angle % 360;
38240
+ coords.push(getPlanarSegmentEndpoint(0, 0, theta, len));
38241
+ }
38242
+ coords.push(coords[0].concat());
37824
38243
  return [coords];
37825
38244
  }
37826
38245
 
37827
- function rotateSymbolCoords(coords, rotation) {
37828
- // TODO: consider avoiding re-instantiating function on every call
37829
- var f = getAffineTransform(rotation || 0, 1, [0, 0], [0, 0]);
37830
- coords.forEach(function(p) {
37831
- var p2 = f ? f(p[0], p[1]) : p;
37832
- p[0] = p2[0];
37833
- p[1] = -p2[1]; // flip y-axis (to produce display coords)
37834
- });
38246
+ function getDefaultSides(type) {
38247
+ return {
38248
+ star: 10,
38249
+ circle: 72,
38250
+ triangle: 3,
38251
+ square: 4,
38252
+ pentagon: 5,
38253
+ hexagon: 6,
38254
+ heptagon: 7,
38255
+ octagon: 8,
38256
+ nonagon: 9,
38257
+ decagon: 10
38258
+ }[type] || 4;
38259
+ }
38260
+
38261
+ // Returns GeoJSON MultiPolygon coords
38262
+ function getRingCoords(d) {
38263
+ var radii = parseRings(d.radii || '2');
38264
+ var coords = [];
38265
+ var solidCenter = utils.isOdd(radii.length);
38266
+ var ring, hole;
38267
+ for (var i=0; i<radii.length; i++) {
38268
+ ring = getPolygonCoords({
38269
+ type: 'circle',
38270
+ radius: radii[i]
38271
+ });
38272
+ if (!solidCenter || i > 0) {
38273
+ i++;
38274
+ hole = ring;
38275
+ ring = getPolygonCoords({
38276
+ type: 'circle',
38277
+ radius: radii[i]
38278
+ });
38279
+ ring.push(hole[0]);
38280
+ }
38281
+ coords.push(ring);
38282
+ }
38283
+ return coords;
38284
+ }
38285
+
38286
+ function parseRings(arg) {
38287
+ var arr = Array.isArray(arg) ? arg : parseNumberList(arg);
38288
+ utils.genericSort(arr, true);
38289
+ return utils.uniq(arr);
37835
38290
  }
37836
38291
 
37837
38292
  // TODO: refactor to remove duplication in mapshaper-svg-style.js
37838
- cmd.symbols = function(lyr, opts) {
37839
- var f, filter;
37840
- // console.log("-symbols opts", opts)
37841
- requirePointLayer(lyr);
37842
- f = getSymbolDataAccessor(lyr, opts);
37843
- if (opts.where) {
37844
- filter = compileValueExpression(opts.where, lyr, null);
37845
- }
37846
- getLayerDataTable(lyr).getRecords().forEach(function(rec, i) {
37847
- if (filter && filter(i)) {
37848
- if ('svg-symbol' in rec === false) {
37849
- rec['svg-symbol'] = undefined;
37850
- }
38293
+ cmd.symbols = function(inputLyr, dataset, opts) {
38294
+ requireSinglePointLayer(inputLyr);
38295
+ var lyr = opts.no_replace ? copyLayer(inputLyr) : inputLyr;
38296
+ var polygonMode = !!opts.polygons;
38297
+ var metersPerPx;
38298
+ if (polygonMode) {
38299
+ requireProjectedDataset(dataset);
38300
+ metersPerPx = opts.pixel_scale || getMetersPerPixel(lyr, dataset);
38301
+ }
38302
+ var records = getLayerDataTable(lyr).getRecords();
38303
+ var getSymbolData = getSymbolDataAccessor(lyr, opts);
38304
+ var geometries = lyr.shapes.map(function(shp, i) {
38305
+ if (!shp) return null;
38306
+ var d = getSymbolData(i);
38307
+ var rec = records[i] || {};
38308
+ var geojsonType = 'Polygon';
38309
+ var coords;
38310
+ if (d.type == 'arrow') {
38311
+ coords = getFilledArrowCoords(d);
38312
+ } else if (d.type == 'ring') {
38313
+ coords = getRingCoords(d);
38314
+ geojsonType = 'MultiPolygon';
37851
38315
  } else {
37852
- rec['svg-symbol'] = buildSymbol(f(i));
38316
+ coords = getPolygonCoords(d);
38317
+ }
38318
+ if (!coords) return null;
38319
+ rotateCoords(coords, +d.rotation || 0);
38320
+ if (!polygonMode) {
38321
+ flipY(coords);
38322
+ }
38323
+ if (+opts.scale) {
38324
+ scaleAndShiftCoords(coords, +opts.scale, [0, 0]);
38325
+ }
38326
+ if (polygonMode) {
38327
+ scaleAndShiftCoords(coords, metersPerPx, shp[0]);
38328
+ if (d.tfill) rec.fill = d.fill;
38329
+ return createGeometry(coords, geojsonType);
38330
+ } else {
38331
+ rec['svg-symbol'] = makeSvgPolygonSymbol(coords, d, geojsonType);
37853
38332
  }
37854
38333
  });
38334
+
38335
+ var outputLyr, dataset2;
38336
+ if (polygonMode) {
38337
+ dataset2 = importGeometries(geometries, records);
38338
+ outputLyr = mergeOutputLayerIntoDataset(inputLyr, dataset, dataset2, opts);
38339
+ outputLyr.data = lyr.data;
38340
+ } else {
38341
+ outputLyr = lyr;
38342
+ }
38343
+ return [outputLyr];
37855
38344
  };
37856
38345
 
38346
+ function importGeometries(geometries, records) {
38347
+ var features = geometries.map(function(geom, i) {
38348
+ var d = records[i];
38349
+ return {
38350
+ type: 'Feature',
38351
+ properties: records[i] || null,
38352
+ geometry: geom
38353
+ };
38354
+ });
38355
+ var geojson = {
38356
+ type: 'FeatureCollection',
38357
+ features: features
38358
+ };
38359
+ return importGeoJSON(geojson);
38360
+ }
38361
+
38362
+ function createGeometry(coords, type) {
38363
+ return {
38364
+ type: type,
38365
+ coordinates: coords
38366
+ };
38367
+ }
38368
+
38369
+ function getMetersPerPixel(lyr, dataset) {
38370
+
38371
+ // TODO: handle single point, no extent
38372
+ var bounds = getLayerBounds(lyr);
38373
+ return bounds.width() / 800;
38374
+ }
38375
+
37857
38376
  // Returns an svg-symbol data object for one symbol
37858
- function buildSymbol(properties) {
37859
- var type = properties.type;
37860
- var f = symbolBuilders[type];
37861
- if (!type) {
37862
- stop('Missing required "type" parameter');
37863
- } else if (!f) {
37864
- stop('Unknown symbol type:', type);
38377
+ function makeSvgPolygonSymbol(coords, properties, geojsonType) {
38378
+ if (geojsonType == 'MultiPolygon') {
38379
+ coords = convertMultiPolygonCoords(coords);
38380
+ } else if (geojsonType != 'Polygon') {
38381
+ error('Unsupported type:', geojsonType);
37865
38382
  }
37866
- return f(properties);
38383
+ roundCoordsForSVG(coords);
38384
+ return {
38385
+ type: 'polygon',
38386
+ coordinates: coords,
38387
+ fill: properties.fill || 'magenta'
38388
+ };
38389
+ }
38390
+
38391
+ function convertMultiPolygonCoords(coords) {
38392
+ return coords.reduce(function(memo, poly) {
38393
+ return memo.concat(poly);
38394
+ }, []);
37867
38395
  }
37868
38396
 
37869
38397
  var Symbols = /*#__PURE__*/Object.freeze({
37870
- __proto__: null,
37871
- buildSymbol: buildSymbol
38398
+ __proto__: null
37872
38399
  });
37873
38400
 
37874
38401
  cmd.target = function(catalog, opts) {
@@ -38014,99 +38541,6 @@ ${svg}
38014
38541
  }
38015
38542
  };
38016
38543
 
38017
- cmd.shape = function(targetDataset, opts) {
38018
- var geojson, dataset;
38019
- if (opts.coordinates) {
38020
- geojson = makeShapeFromCoords(opts);
38021
- } else if (opts.type == 'circle') {
38022
- geojson = makeCircle(opts);
38023
- } else if (opts.type == 'rectangle' && opts.bbox) {
38024
- geojson = getRectangleGeoJSON(opts);
38025
- } else {
38026
- stop('Missing coordinates parameter');
38027
- }
38028
- // TODO: project shape if targetDataset is projected
38029
- dataset = importGeoJSON(geojson, {});
38030
- if (opts.rotation) {
38031
- rotateDatasetCoords(dataset, opts.rotation);
38032
- }
38033
- dataset.layers[0].name = opts.name || opts.type || 'shape';
38034
- return dataset;
38035
- };
38036
-
38037
- function getRectangleGeoJSON(opts) {
38038
- var bbox = opts.bbox,
38039
- xmin = bbox[0],
38040
- ymin = bbox[1],
38041
- xmax = bbox[2],
38042
- ymax = bbox[3],
38043
- interval = 0.5,
38044
- coords = [],
38045
- type = opts.geometry == 'polyline' ? 'LineString' : 'Polygon';
38046
- addSide(xmin, ymin, xmin, ymax);
38047
- addSide(xmin, ymax, xmax, ymax);
38048
- addSide(xmax, ymax, xmax, ymin);
38049
- addSide(xmax, ymin, xmin, ymin);
38050
- coords.push([xmin, ymin]);
38051
- return {
38052
- type: type,
38053
- coordinates: type == 'Polygon' ? [coords] : coords
38054
- };
38055
-
38056
- function addSide(x1, y1, x2, y2) {
38057
- var dx = x2 - x1,
38058
- dy = y2 - y1,
38059
- n = Math.ceil(Math.max(Math.abs(dx) / interval, Math.abs(dy) / interval)),
38060
- xint = dx / n,
38061
- yint = dy / n;
38062
- for (var i=0; i<n; i++) {
38063
- coords.push([x1 + i * xint, y1 + i * yint]);
38064
- }
38065
- }
38066
- }
38067
-
38068
- function makeCircle(opts) {
38069
- if (opts.radius > 0 === false && opts.radius_angle > 0 === false) {
38070
- stop('Missing required radius parameter.');
38071
- }
38072
- var cp = opts.center || [0, 0];
38073
- var radius = opts.radius || getCircleRadiusFromAngle(getCRS('wgs84'), opts.radius_angle);
38074
- return getCircleGeoJSON(cp, radius, null, {geometry_type : opts.geometry || 'polygon'});
38075
- }
38076
-
38077
- function makeShapeFromCoords(opts) {
38078
- var coordinates = [];
38079
- var offsets = opts.offsets || [];
38080
- var coords = opts.coordinates;
38081
- var type, i, x, y;
38082
- if (coords.length >= 2 === false) {
38083
- stop('Invalid coordinates parameter.');
38084
- }
38085
- for (i=0; i<coords.length; i+= 2) {
38086
- x = coords[i];
38087
- y = coords[i + 1];
38088
- coordinates.push([x, y]);
38089
- }
38090
- for (i=0; i<offsets.length; i+=2) {
38091
- x += offsets[i];
38092
- y += offsets[i + 1];
38093
- coordinates.push([x, y]);
38094
- }
38095
- if (GeoJSON.pathIsRing(coordinates)) {
38096
- type = 'Polygon';
38097
- } else if (opts.closed && coordinates.length >= 3) {
38098
- type = 'Polygon';
38099
- coordinates.push(coordinates[0]);
38100
- } else {
38101
- type = 'LineString';
38102
- }
38103
- return {
38104
- type: type,
38105
- coordinates: type == 'Polygon' ? [coordinates] : coordinates
38106
- };
38107
-
38108
- }
38109
-
38110
38544
  cmd.variableSimplify = function(layers, dataset, opts) {
38111
38545
  var lyr = layers[0];
38112
38546
  var arcs = dataset.arcs;
@@ -38648,6 +39082,9 @@ ${svg}
38648
39082
  } else if (name == 'shape') {
38649
39083
  catalog.addDataset(cmd.shape(targetDataset, opts));
38650
39084
 
39085
+ } else if (name == 'shapes') {
39086
+ outputLayers = applyCommandToEachLayer(cmd.shapes, targetLayers, targetDataset, opts);
39087
+
38651
39088
  } else if (name == 'simplify') {
38652
39089
  if (opts.variable) {
38653
39090
  cmd.variableSimplify(targetLayers, targetDataset, opts);
@@ -38677,7 +39114,7 @@ ${svg}
38677
39114
  applyCommandToEachLayer(cmd.svgStyle, targetLayers, targetDataset, opts);
38678
39115
 
38679
39116
  } else if (name == 'symbols') {
38680
- applyCommandToEachLayer(cmd.symbols, targetLayers, opts);
39117
+ outputLayers = applyCommandToEachLayer(cmd.symbols, targetLayers, targetDataset, opts);
38681
39118
 
38682
39119
  } else if (name == 'subdivide') {
38683
39120
  outputLayers = applyCommandToEachLayer(cmd.subdivideLayer, targetLayers, arcs, opts.expression);