mapshaper 0.5.81 → 0.5.85

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.79";
3
+ var VERSION = "0.5.83";
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.
@@ -2359,6 +2369,12 @@
2359
2369
  }, 0);
2360
2370
  }
2361
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
+
2362
2378
  // Return true if point is inside or on boundary of a shape
2363
2379
  //
2364
2380
  function testPointInPolygon(x, y, shp, arcs) {
@@ -13692,14 +13708,21 @@
13692
13708
  length: 'number', // e.g. arrow length
13693
13709
  rotation: 'number',
13694
13710
  radius: 'number',
13695
- 'arrow-length': 'number',
13696
- 'arrow-direction': 'number',
13697
- 'arrow-head-angle': 'number',
13698
- 'arrow-head-width': 'number',
13699
- 'arrow-stem-width': 'number',
13700
- 'arrow-stem-curve': 'number', // degrees of arc
13701
- 'arrow-stem-taper': 'number',
13702
- 'arrow-min-stem': 'number',
13711
+ radii: null, // string, parsed by function
13712
+ flipped: 'boolean',
13713
+ rotated: 'boolean',
13714
+ direction: 'number',
13715
+ sides: 'number', // polygons and stars
13716
+ points: 'number', // polygons and stars
13717
+ anchor: null, // arrows; takes start, middle, end
13718
+ 'head-angle': 'number',
13719
+ 'head-width': 'number',
13720
+ 'head-length': 'number',
13721
+ 'stem-width': 'number',
13722
+ 'stem-curve': 'number', // degrees of arc
13723
+ 'stem-taper': 'number',
13724
+ 'stem-length': 'number',
13725
+ 'min-stem-ratio': 'number',
13703
13726
  'arrow-scaling': 'number',
13704
13727
  effect: null // e.g. "fade"
13705
13728
  }, stylePropertyTypes);
@@ -13735,6 +13758,7 @@
13735
13758
  function getSymbolDataAccessor(lyr, opts) {
13736
13759
  var functions = {};
13737
13760
  var properties = [];
13761
+ var fields = lyr.data ? lyr.data.getFields() : [];
13738
13762
 
13739
13763
  Object.keys(opts).forEach(function(optName) {
13740
13764
  var svgName = optName.replace(/_/g, '-');
@@ -13746,6 +13770,8 @@
13746
13770
  properties.push(svgName);
13747
13771
  });
13748
13772
 
13773
+ // TODO: consider applying values of existing fields with names of symbol properties
13774
+
13749
13775
  return function(id) {
13750
13776
  var d = {}, name;
13751
13777
  for (var i=0; i<properties.length; i++) {
@@ -13761,8 +13787,9 @@
13761
13787
  // * ???
13762
13788
  //
13763
13789
  function mightBeExpression(str, fields) {
13790
+ fields = fields || [];
13764
13791
  if (fields.indexOf(str.trim()) > -1) return true;
13765
- return /[(){}.+-/*?:&|=\[]/.test(str);
13792
+ return /[(){}./*?:&|=\[+-]/.test(str);
13766
13793
  }
13767
13794
 
13768
13795
  function getSymbolPropertyAccessor(val, svgName, lyr) {
@@ -13783,6 +13810,7 @@
13783
13810
  // treating the string as a literal value
13784
13811
  literalVal = strVal;
13785
13812
  }
13813
+ // console.log("literalVal:", mightBeExpression(strVal, fields), strVal, fields)
13786
13814
  if (accessor) return accessor;
13787
13815
  if (literalVal !== null) return function(id) {return literalVal;};
13788
13816
  stop('Unexpected value for', svgName + ':', strVal);
@@ -13816,6 +13844,8 @@
13816
13844
  val = isDashArray(strVal) ? strVal : null;
13817
13845
  } else if (type == 'pattern') {
13818
13846
  val = isPattern(strVal) ? strVal : null;
13847
+ } else if (type == 'boolean') {
13848
+ val = parseBoolean(strVal);
13819
13849
  }
13820
13850
  // else {
13821
13851
  // // unknown type -- assume literal value
@@ -13836,10 +13866,17 @@
13836
13866
  return /^( ?[_a-z][-_a-z0-9]*\b)+$/i.test(str);
13837
13867
  }
13838
13868
 
13869
+
13839
13870
  function isSvgNumber(o) {
13840
13871
  return utils.isFiniteNumber(o) || utils.isString(o) && /^-?[.0-9]+$/.test(o);
13841
13872
  }
13842
13873
 
13874
+ function parseBoolean(o) {
13875
+ if (o === true || o === 'true') return true;
13876
+ if (o === false || o === 'false') return false;
13877
+ return null;
13878
+ }
13879
+
13843
13880
  function isSvgMeasure(o) {
13844
13881
  return utils.isFiniteNumber(o) || utils.isString(o) && /^-?[.0-9]+[a-z]*$/.test(o);
13845
13882
  }
@@ -13863,6 +13900,7 @@
13863
13900
  getSymbolPropertyAccessor: getSymbolPropertyAccessor,
13864
13901
  isSvgClassName: isSvgClassName,
13865
13902
  isSvgNumber: isSvgNumber,
13903
+ parseBoolean: parseBoolean,
13866
13904
  isSvgMeasure: isSvgMeasure,
13867
13905
  parseSvgMeasure: parseSvgMeasure,
13868
13906
  isSvgColor: isSvgColor
@@ -14081,6 +14119,9 @@
14081
14119
  o = importLineString(coords[i]);
14082
14120
  o.properties.d = d + o.properties.d + ' Z';
14083
14121
  }
14122
+ if (coords.length > 1) {
14123
+ o.properties['fill-rule'] = 'evenodd'; // support polygons with holes
14124
+ }
14084
14125
  return o;
14085
14126
  }
14086
14127
 
@@ -17997,6 +18038,10 @@ ${svg}
17997
18038
  return chunks;
17998
18039
  }
17999
18040
 
18041
+ function parseNumberList(token) {
18042
+ return token.split(',').map(parseFloat);
18043
+ }
18044
+
18000
18045
  // Split comma-delimited list, trim quotes from entire list and
18001
18046
  // individual members
18002
18047
  function parseStringList(token) {
@@ -18086,6 +18131,7 @@ ${svg}
18086
18131
  var OptionParsingUtils = /*#__PURE__*/Object.freeze({
18087
18132
  __proto__: null,
18088
18133
  splitShellTokens: splitShellTokens,
18134
+ parseNumberList: parseNumberList,
18089
18135
  parseStringList: parseStringList,
18090
18136
  parseColorList: parseColorList,
18091
18137
  cleanArgv: cleanArgv,
@@ -18277,7 +18323,7 @@ ${svg}
18277
18323
  } else if (type == 'strings') {
18278
18324
  val = parseStringList(token);
18279
18325
  } else if (type == 'bbox' || type == 'numbers') {
18280
- val = token.split(',').map(parseFloat);
18326
+ val = parseNumberList(token);
18281
18327
  } else if (type == 'percent') {
18282
18328
  // val = utils.parsePercent(token);
18283
18329
  val = token; // string value is parsed by command function
@@ -20068,47 +20114,82 @@ ${svg}
20068
20114
  type: 'distance'
20069
20115
  })
20070
20116
  .option('sides', {
20071
- describe: 'sides of a polygon or star symbol',
20117
+ describe: 'number of sides of a polygon symbol',
20072
20118
  type: 'number'
20073
20119
  })
20120
+ .option('orientation', {
20121
+ // TODO: removed (replaced by flipped and rotated)
20122
+ // describe: 'use orientation=b for a rotated or flipped orientation'
20123
+ })
20124
+ .option('flipped', {
20125
+ type: 'flag',
20126
+ describe: 'symbol is vertically flipped'
20127
+ })
20128
+ .option('rotated', {
20129
+ type: 'flag',
20130
+ describe: 'symbol is rotated to a different orientation'
20131
+ })
20074
20132
  .option('rotation', {
20075
20133
  describe: 'rotation of symbol in degrees'
20076
20134
  })
20077
- .option('orientation', {
20078
- describe: 'use orientation=b for a rotated or flipped orientation'
20135
+ .option('points', {
20136
+ describe: '(star) number of points'
20137
+ })
20138
+ .option('point-ratio', {
20139
+ old_alias: 'star-ratio',
20140
+ describe: '(star) ratio of minor to major radius of star',
20141
+ type: 'number'
20142
+ })
20143
+ .option('radii', {
20144
+ describe: '(ring) comma-sep. list of concentric radii, ascending order'
20079
20145
  })
20080
20146
  .option('length', {
20081
- // alias for arrow-length
20147
+ old_alias: 'arrow-length',
20148
+ describe: '(arrow) length of arrow in pixels'
20082
20149
  })
20083
- .option('star-ratio', {
20084
- describe: 'ratio of major to minor radius of star',
20085
- type: 'number'
20150
+ .option('direction', {
20151
+ old_alias: 'arrow-direction',
20152
+ describe: '(arrow) angle off vertical (-90 = left-pointing)'
20086
20153
  })
20087
- .option('arrow-length', {
20088
- describe: 'length of arrows in pixels (use with type=arrow)'
20154
+ .option('head-angle', {
20155
+ old_alias: 'arrow-head-angle',
20156
+ describe: '(arrow) angle of tip of arrow (default is 40 degrees)'
20089
20157
  })
20090
- .option('arrow-direction', {
20091
- describe: 'angle off of vertical (-90 = left-pointing arrow)'
20158
+ .option('head-width', {
20159
+ old_alias: 'arrow-head-width',
20160
+ describe: '(arrow) width of arrow head from side to side'
20092
20161
  })
20093
- .option('arrow-head-angle', {
20094
- describe: 'angle of tip of arrow (default is 40 degrees)'
20162
+ .option('head-length', {
20163
+ old_alias: 'arrow-head-width',
20164
+ describe: '(arrow) length of head (alternative to head-angle)'
20095
20165
  })
20096
- .option('arrow-head-width', {
20097
- describe: 'size of arrow head from side to side'
20166
+ .option('head-shape', {
20167
+ // describe: 'options: a b c'
20098
20168
  })
20099
- .option('arrow-stem-width', {
20100
- describe: 'width of stem at its widest point'
20169
+ .option('stem-width', {
20170
+ old_alias: 'arrow-stem-width',
20171
+ describe: '(arrow) width of stem at its widest point'
20101
20172
  })
20102
- .option('arrow-stem-taper', {
20103
- describe: 'factor for tapering the width of the stem'
20173
+ .option('stem-length', {
20174
+ old_alias: 'arrow-stem-length',
20175
+ describe: '(arrow) alternative to length'
20104
20176
  })
20105
- .option('arrow-stem-curve', {
20106
- describe: 'curvature in degrees (arrows are straight by default)'
20177
+ .option('stem-taper', {
20178
+ old_alias: 'arrow-stem-taper',
20179
+ describe: '(arrow) factor for tapering the width of the stem (0-1)'
20107
20180
  })
20108
- .option('arrow-min-stem', {
20109
- describe: 'min ratio of stem to total length (for small arrows)',
20181
+ .option('stem-curve', {
20182
+ old_alias: 'arrow-stem-curve',
20183
+ describe: '(arrow) curvature in degrees (default is 0)'
20184
+ })
20185
+ .option('min-stem-ratio', {
20186
+ old_alias: 'arrow-min-stem',
20187
+ describe: '(arrow) min ratio of stem to total length',
20110
20188
  type: 'number'
20111
20189
  })
20190
+ .option('anchor', {
20191
+ describe: '(arrow) takes one of: start, middle, end (default is start)'
20192
+ })
20112
20193
  .option('stroke', {})
20113
20194
  .option('stroke-width', {})
20114
20195
  .option('fill', {
@@ -21643,7 +21724,7 @@ ${svg}
21643
21724
  collectionType = 'mixed';
21644
21725
  }
21645
21726
  } else if (currType != t) {
21646
- stop("Unable to import mixed-geometry GeoJSON features");
21727
+ stop("Unable to import mixed-geometry features");
21647
21728
  }
21648
21729
  }
21649
21730
 
@@ -21754,6 +21835,33 @@ ${svg}
21754
21835
  importShp: importShp
21755
21836
  });
21756
21837
 
21838
+ function importGeoJSON(src, optsArg) {
21839
+ var opts = optsArg || {};
21840
+ var supportedGeometries = Object.keys(GeoJSON.pathImporters),
21841
+ srcObj = utils.isString(src) ? JSON.parse(src) : src,
21842
+ importer = new GeoJSONParser(opts),
21843
+ srcCollection, dataset;
21844
+
21845
+ // Convert single feature or geometry into a collection with one member
21846
+ if (srcObj.type == 'Feature') {
21847
+ srcCollection = {
21848
+ type: 'FeatureCollection',
21849
+ features: [srcObj]
21850
+ };
21851
+ } else if (supportedGeometries.includes(srcObj.type)) {
21852
+ srcCollection = {
21853
+ type: 'GeometryCollection',
21854
+ geometries: [srcObj]
21855
+ };
21856
+ } else {
21857
+ srcCollection = srcObj;
21858
+ }
21859
+ (srcCollection.features || srcCollection.geometries || []).forEach(importer.parseObject);
21860
+ dataset = importer.done();
21861
+ importCRS(dataset, srcObj); // TODO: remove this
21862
+ return dataset;
21863
+ }
21864
+
21757
21865
  function GeoJSONParser(opts) {
21758
21866
  var idField = opts.id_field || GeoJSON.ID_FIELD,
21759
21867
  importer = new PathImporter(opts),
@@ -21775,8 +21883,11 @@ ${svg}
21775
21883
  geom = o;
21776
21884
  }
21777
21885
  // TODO: improve so geometry_type option skips features instead of creating null geometries
21778
- importer.startShape(rec);
21779
- if (geom) GeoJSON.importGeometry(geom, importer, opts);
21886
+ if (geom && geom.type == 'GeometryCollection') {
21887
+ GeoJSON.importComplexFeature(importer, geom, rec, opts);
21888
+ } else {
21889
+ GeoJSON.importSimpleFeature(importer, geom, rec, opts);
21890
+ }
21780
21891
  };
21781
21892
 
21782
21893
  this.done = function() {
@@ -21784,50 +21895,58 @@ ${svg}
21784
21895
  };
21785
21896
  }
21786
21897
 
21787
- function importGeoJSON(src, optsArg) {
21788
- var opts = optsArg || {};
21789
- var supportedGeometries = Object.keys(GeoJSON.pathImporters),
21790
- srcObj = utils.isString(src) ? JSON.parse(src) : src,
21791
- importer = new GeoJSONParser(opts),
21792
- srcCollection, dataset;
21793
-
21794
- // Convert single feature or geometry into a collection with one member
21795
- if (srcObj.type == 'Feature') {
21796
- srcCollection = {
21797
- type: 'FeatureCollection',
21798
- features: [srcObj]
21799
- };
21800
- } else if (supportedGeometries.includes(srcObj.type)) {
21801
- srcCollection = {
21802
- type: 'GeometryCollection',
21803
- geometries: [srcObj]
21804
- };
21805
- } else {
21806
- srcCollection = srcObj;
21898
+ GeoJSON.importComplexFeature = function(importer, geom, rec, opts) {
21899
+ var types = divideGeometriesByType(geom.geometries || []);
21900
+ if (types.length === 0) {
21901
+ importer.startShape(rec); // import a feature with null geometry
21902
+ return;
21807
21903
  }
21808
- (srcCollection.features || srcCollection.geometries || []).forEach(importer.parseObject);
21809
- dataset = importer.done();
21810
- importCRS(dataset, srcObj); // TODO: remove this
21811
- return dataset;
21904
+ types.forEach(function(geometries, i) {
21905
+ importer.startShape(copyRecord(rec));
21906
+ geometries.forEach(function(geom) {
21907
+ GeoJSON.importSimpleGeometry(importer, geom, opts);
21908
+ });
21909
+ });
21910
+ };
21911
+
21912
+ function divideGeometriesByType(geometries, index) {
21913
+ index = index || {};
21914
+ geometries.forEach(function(geom) {
21915
+ if (!geom) return;
21916
+ var mtype = GeoJSON.translateGeoJSONType(geom.type);
21917
+ if (mtype) {
21918
+ if (mtype in index === false) {
21919
+ index[mtype] = [];
21920
+ }
21921
+ index[mtype].push(geom);
21922
+ } else if (geom.type == 'GeometryCollection') {
21923
+ divideGeometriesByType(geom.geometries || [], index);
21924
+ }
21925
+ });
21926
+ return Object.values(index);
21812
21927
  }
21813
21928
 
21814
- GeoJSON.importGeometry = function(geom, importer, opts) {
21815
- var type = geom.type;
21816
- if (type in GeoJSON.pathImporters) {
21929
+ GeoJSON.importSimpleFeature = function(importer, geom, rec, opts) {
21930
+ importer.startShape(rec);
21931
+ GeoJSON.importSimpleGeometry(importer, geom, opts);
21932
+ };
21933
+
21934
+ GeoJSON.importSimpleGeometry = function(importer, geom, opts) {
21935
+ var type = geom ? geom.type : null;
21936
+ if (type === null) {
21937
+ // no geometry to import
21938
+ } else if (type in GeoJSON.pathImporters) {
21817
21939
  if (opts.geometry_type && opts.geometry_type != GeoJSON.translateGeoJSONType(type)) {
21818
21940
  // kludge to filter out all but one type of geometry
21819
21941
  return;
21820
21942
  }
21821
21943
  GeoJSON.pathImporters[type](geom.coordinates, importer);
21822
- } else if (type == 'GeometryCollection') {
21823
- geom.geometries.forEach(function(geom) {
21824
- GeoJSON.importGeometry(geom, importer, opts);
21825
- });
21826
21944
  } else {
21827
- verbose("GeoJSON.importGeometry() Unsupported geometry type:", geom.type);
21945
+ verbose("Unsupported geometry type:", geom.type);
21828
21946
  }
21829
21947
  };
21830
21948
 
21949
+
21831
21950
  // Functions for importing geometry coordinates using a PathImporter
21832
21951
  //
21833
21952
  GeoJSON.pathImporters = {
@@ -21866,8 +21985,8 @@ ${svg}
21866
21985
 
21867
21986
  var GeojsonImport = /*#__PURE__*/Object.freeze({
21868
21987
  __proto__: null,
21869
- GeoJSONParser: GeoJSONParser,
21870
21988
  importGeoJSON: importGeoJSON,
21989
+ GeoJSONParser: GeoJSONParser,
21871
21990
  importCRS: importCRS
21872
21991
  });
21873
21992
 
@@ -37929,111 +38048,153 @@ ${svg}
37929
38048
  points.push(p2);
37930
38049
  }
37931
38050
 
37932
- function getStickArrowCoords(d, totalLen) {
37933
- var minStemRatio = getMinStemRatio(d);
37934
- var headAngle = d['arrow-head-angle'] || 90;
37935
- var curve = d['arrow-stem-curve'] || 0;
37936
- var unscaledHeadWidth = d['arrow-head-width'] || 9;
37937
- var unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle);
37938
- var scale = getScale(totalLen, unscaledHeadLen, minStemRatio);
37939
- var headWidth = unscaledHeadWidth * scale;
37940
- var headLen = unscaledHeadLen * scale;
37941
- var tip = getStickArrowTip(totalLen, curve);
37942
- var stem = [[0, 0], tip.concat()];
37943
- if (curve) {
37944
- addBezierArcControlPoints(stem, curve);
37945
- }
37946
- if (!headLen) return [stem];
37947
- var head = [addPoints([-headWidth / 2, -headLen], tip), tip.concat(), addPoints([headWidth / 2, -headLen], tip)];
37948
-
37949
- rotateCoords(stem, d.rotation);
37950
- rotateCoords(head, d.rotation);
37951
- return [stem, head];
37952
- }
37953
-
37954
- function getMinStemRatio(d) {
37955
- return d['arrow-min-stem'] >= 0 ? d['arrow-min-stem'] : 0.4;
37956
- }
37957
-
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,
37962
- unscaledStemWidth = d['arrow-stem-width'] || 2,
37963
- unscaledHeadWidth = d['arrow-head-width'] || unscaledStemWidth * 3,
37964
- unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle),
37965
- scale = getScale(totalLen, unscaledHeadLen, minStemRatio),
37966
- headWidth = unscaledHeadWidth * scale,
37967
- headLen = unscaledHeadLen * scale,
37968
- stemWidth = unscaledStemWidth * scale,
37969
- stemTaper = d['arrow-stem-taper'] || 0,
37970
- stemCurve = d['arrow-stem-curve'] || 0,
37971
- stemLen = totalLen - headLen,
37972
- coords;
37973
-
37974
- var headDx = headWidth / 2,
37975
- stemDx = stemWidth / 2,
37976
- baseDx = stemDx * (1 - stemTaper);
37977
-
37978
- if (unscaledHeadWidth < unscaledStemWidth) {
37979
- stop('Arrow head must be at least as wide as the stem.');
37980
- }
38051
+ // export function getStickArrowCoords(d, totalLen) {
38052
+ // var minStemRatio = getMinStemRatio(d);
38053
+ // var headAngle = d['arrow-head-angle'] || 90;
38054
+ // var curve = d['arrow-stem-curve'] || 0;
38055
+ // var unscaledHeadWidth = d['arrow-head-width'] || 9;
38056
+ // var unscaledHeadLen = getHeadLength(unscaledHeadWidth, headAngle);
38057
+ // var scale = getScale(totalLen, unscaledHeadLen, minStemRatio);
38058
+ // var headWidth = unscaledHeadWidth * scale;
38059
+ // var headLen = unscaledHeadLen * scale;
38060
+ // var tip = getStickArrowTip(totalLen, curve);
38061
+ // var stem = [[0, 0], tip.concat()];
38062
+ // if (curve) {
38063
+ // addBezierArcControlPoints(stem, curve);
38064
+ // }
38065
+ // if (!headLen) return [stem];
38066
+ // var head = [addPoints([-headWidth / 2, -headLen], tip), tip.concat(), addPoints([headWidth / 2, -headLen], tip)];
38067
+
38068
+ // rotateCoords(stem, d.rotation);
38069
+ // rotateCoords(head, d.rotation);
38070
+ // return [stem, head];
38071
+ // }
38072
+
38073
+ // function getStickArrowTip(totalLen, curve) {
38074
+ // // curve/2 intersects the arrowhead at 90deg (trigonometry)
38075
+ // var theta = Math.abs(curve/2) / 180 * Math.PI;
38076
+ // var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1);
38077
+ // var dy = totalLen * Math.cos(theta);
38078
+ // return [dx, dy];
38079
+ // }
38080
+
38081
+ // function addPoints(a, b) {
38082
+ // return [a[0] + b[0], a[1] + b[1]];
38083
+ // }
37981
38084
 
37982
- if (!stemCurve || Math.abs(stemCurve) > 90) {
37983
- coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
38085
+ function calcStraightArrowCoords(stemLen, headLen, stemDx, headDx, baseDx) {
38086
+ return [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
37984
38087
  [-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);
38088
+ }
38089
+
38090
+ function calcArrowSize(d) {
38091
+ var totalLen = d.radius || d.length || d.r || 0,
38092
+ scale = 1,
38093
+ o = initArrowSize(d); // calc several parameters
38094
+
38095
+ if (totalLen > 0) {
38096
+ scale = calcScale(totalLen, o.headLen, d);
38097
+ o.stemWidth *= scale;
38098
+ o.headWidth *= scale;
38099
+ o.headLen *= scale;
38100
+ o.stemLen = totalLen - o.headLen;
37988
38101
  }
37989
38102
 
37990
- rotateCoords(coords, direction);
37991
- return [coords];
38103
+ if (o.headWidth < o.stemWidth) {
38104
+ stop('Arrow head must be at least as wide as the stem.');
38105
+ }
38106
+ return o;
37992
38107
  }
37993
38108
 
37994
-
37995
- function getScale(totalLen, headLen, minStemRatio) {
38109
+ function calcScale(totalLen, headLen, d) {
38110
+ var minStemRatio = d['min-stem-ratio'] >= 0 ? d['min-stem-ratio'] : 0;
38111
+ var stemLen = d['stem-length'] || 0;
37996
38112
  var maxHeadPct = 1 - minStemRatio;
37997
38113
  var headPct = headLen / totalLen;
38114
+ var scale = 1;
38115
+
37998
38116
  if (headPct > maxHeadPct) {
37999
- return maxHeadPct / headPct;
38117
+ scale = maxHeadPct / headPct;
38118
+ } else if (stemLen + headLen > totalLen) {
38119
+ scale = totalLen / (stemLen + headLen);
38000
38120
  }
38001
- return 1;
38121
+ return scale;
38002
38122
  }
38003
38123
 
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];
38124
+ function initArrowSize(d) {
38125
+ var sizeRatio = getHeadSizeRatio(d['head-angle'] || 40); // length to width
38126
+ var o = {
38127
+ stemWidth: d['stem-width'] || 2,
38128
+ stemLen: d['stem-length'] || 0,
38129
+ headWidth: d['head-width'],
38130
+ headLen: d['head-length']
38131
+ };
38132
+ if (!o.headWidth) {
38133
+ if (o.headLen) {
38134
+ o.headWidth = o.headLen / sizeRatio;
38135
+ } else {
38136
+ o.headWidth = o.stemWidth * 3; // assumes stemWidth has been set
38137
+ }
38138
+ }
38139
+ if (!o.headLen) {
38140
+ o.headLen = o.headWidth * sizeRatio;
38141
+ }
38142
+ return o;
38010
38143
  }
38011
38144
 
38012
- function addPoints(a, b) {
38013
- return [a[0] + b[0], a[1] + b[1]];
38014
- }
38015
38145
 
38146
+ // Returns ratio of head length to head width
38147
+ function getHeadSizeRatio(headAngle) {
38148
+ return 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2;
38149
+ }
38150
+
38151
+ function getFilledArrowCoords(d) {
38152
+ var direction = d.rotation || d.direction || 0,
38153
+ stemTaper = d['stem-taper'] || 0,
38154
+ curvature = d['stem-curve'] || 0,
38155
+ size = calcArrowSize(d);
38156
+ if (!size) return null;
38157
+ var stemLen = size.stemLen,
38158
+ headLen = size.headLen,
38159
+ headDx = size.headWidth / 2,
38160
+ stemDx = size.stemWidth / 2,
38161
+ baseDx = stemDx * (1 - stemTaper),
38162
+ head, stem, coords, dx, dy;
38163
+
38164
+ if (curvature) {
38165
+ if (direction > 0) curvature = -curvature;
38166
+ var theta = Math.abs(curvature) / 180 * Math.PI;
38167
+ var sign = curvature > 0 ? 1 : -1;
38168
+ var ax = baseDx * Math.cos(theta); // rotate arrow base
38169
+ var ay = baseDx * Math.sin(theta) * -sign;
38170
+ dx = stemLen * Math.sin(theta / 2) * sign;
38171
+ dy = stemLen * Math.cos(theta / 2);
38172
+ var leftStem = getCurvedStemCoords(-ax, -ay, -stemDx + dx, dy, theta);
38173
+ var rightStem = getCurvedStemCoords(ax, ay, stemDx + dx, dy, theta);
38174
+ stem = leftStem.concat(rightStem.reverse());
38016
38175
 
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
- }
38176
+ } else {
38177
+ dx = 0;
38178
+ dy = stemLen;
38179
+ stem = [[-baseDx, 0], [baseDx, 0], [baseDx, 0]];
38180
+ }
38021
38181
 
38022
- function getCurvedArrowCoords(stemLen, headLen, curvature, stemDx, headDx, baseDx) {
38023
38182
  // 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);
38183
+ head = [[stemDx + dx, dy], [headDx + dx, dy],
38184
+ [dx, headLen + dy], [-headDx + dx, dy], [-stemDx + dx, dy]];
38185
+
38186
+ coords = stem.concat(head);
38187
+ if (d.anchor == 'end') {
38188
+ scaleAndShiftCoords(coords, 1, [-dx, -dy - headLen]);
38189
+ } else if (d.anchor == 'middle') {
38190
+ scaleAndShiftCoords(coords, 1, [-dx/2, (-dy - headLen)/2]);
38191
+ }
38192
+
38193
+ rotateCoords(coords, direction);
38194
+ if (d.flipped) {
38195
+ flipY(coords);
38196
+ }
38197
+ return [coords];
38037
38198
  }
38038
38199
 
38039
38200
  // ax, ay: point on the base
@@ -38070,15 +38231,45 @@ ${svg}
38070
38231
  return coords;
38071
38232
  }
38072
38233
 
38234
+ function getMinorRadius(points) {
38235
+ var innerAngle = 360 / points;
38236
+ var pointAngle = getDefaultPointAngle(points);
38237
+ var thetaA = Math.PI / 180 * innerAngle / 2;
38238
+ var thetaB = Math.PI / 180 * pointAngle / 2;
38239
+ var a = Math.tan(thetaB) / (Math.tan(thetaB) + Math.tan(thetaA));
38240
+ var c = a / Math.cos(thetaA);
38241
+ return c;
38242
+ }
38243
+
38244
+
38245
+ function getPointAngle(points, skip) {
38246
+ var unitAngle = 360 / points;
38247
+ var centerAngle = unitAngle * (skip + 1);
38248
+ return 180 - centerAngle;
38249
+ }
38250
+
38251
+ function getDefaultPointAngle(points) {
38252
+ var minSkip = 1;
38253
+ var maxSkip = Math.ceil(points / 2) - 2;
38254
+ var skip = Math.floor((maxSkip + minSkip) / 2);
38255
+ return getPointAngle(points, skip);
38256
+ }
38257
+
38073
38258
  // sides: e.g. 5-pointed star has 10 sides
38074
38259
  // radius: distance from center to point
38075
38260
  //
38076
- function getPolygonCoords(radius, opts) {
38077
- var type = opts.type;
38078
- var sides = +opts.sides || getDefaultSides(type);
38261
+ function getPolygonCoords(d) {
38262
+ var radius = d.radius || d.length || d.r;
38263
+ if (radius > 0 === false) return null;
38264
+ var type = d.type;
38265
+ var sides = +d.sides || getDefaultSides(type);
38079
38266
  var isStar = type == 'star';
38080
- if (isStar && (sides < 6 || sides % 2 !== 0)) {
38081
- stop(`Invalid number of sides for a star (${sides})`);
38267
+ if (isStar && d.points > 0) {
38268
+ sides = d.points * 2;
38269
+ }
38270
+ var starRatio = isStar ? d.star_ratio || getMinorRadius(sides / 2) : 0;
38271
+ if (isStar && (sides < 10 || sides % 2 !== 0)) {
38272
+ stop(`Invalid number of points for a star (${sides / 2})`);
38082
38273
  } else if (sides >= 3 === false) {
38083
38274
  stop(`Invalid number of sides (${sides})`);
38084
38275
  }
@@ -38086,14 +38277,14 @@ ${svg}
38086
38277
  angle = 360 / sides,
38087
38278
  b = isStar ? 1 : 0.5,
38088
38279
  theta, even, len;
38089
- if (opts.orientation == 'b') {
38280
+ if (d.orientation == 'b' || d.flipped || d.rotated) {
38090
38281
  b = 0;
38091
38282
  }
38092
38283
  for (var i=0; i<sides; i++) {
38093
38284
  even = i % 2 == 0;
38094
38285
  len = radius;
38095
38286
  if (isStar && even) {
38096
- len *= (opts.star_ratio || 0.5);
38287
+ len *= starRatio;
38097
38288
  }
38098
38289
  theta = (i + b) * angle % 360;
38099
38290
  coords.push(getPlanarSegmentEndpoint(0, 0, theta, len));
@@ -38117,6 +38308,37 @@ ${svg}
38117
38308
  }[type] || 4;
38118
38309
  }
38119
38310
 
38311
+ // Returns GeoJSON MultiPolygon coords
38312
+ function getRingCoords(d) {
38313
+ var radii = parseRings(d.radii || '2');
38314
+ var coords = [];
38315
+ var solidCenter = utils.isOdd(radii.length);
38316
+ var ring, hole;
38317
+ for (var i=0; i<radii.length; i++) {
38318
+ ring = getPolygonCoords({
38319
+ type: 'circle',
38320
+ radius: radii[i]
38321
+ });
38322
+ if (!solidCenter || i > 0) {
38323
+ i++;
38324
+ hole = ring;
38325
+ ring = getPolygonCoords({
38326
+ type: 'circle',
38327
+ radius: radii[i]
38328
+ });
38329
+ ring.push(hole[0]);
38330
+ }
38331
+ coords.push(ring);
38332
+ }
38333
+ return coords;
38334
+ }
38335
+
38336
+ function parseRings(arg) {
38337
+ var arr = Array.isArray(arg) ? arg : parseNumberList(arg);
38338
+ utils.genericSort(arr, true);
38339
+ return utils.uniq(arr);
38340
+ }
38341
+
38120
38342
  // TODO: refactor to remove duplication in mapshaper-svg-style.js
38121
38343
  cmd.symbols = function(inputLyr, dataset, opts) {
38122
38344
  requireSinglePointLayer(inputLyr);
@@ -38133,16 +38355,17 @@ ${svg}
38133
38355
  if (!shp) return null;
38134
38356
  var d = getSymbolData(i);
38135
38357
  var rec = records[i] || {};
38136
- var coords, size, constructor;
38358
+ var geojsonType = 'Polygon';
38359
+ var coords;
38137
38360
  if (d.type == 'arrow') {
38138
- size = d.radius || d.length || d['arrow-length'] || d.r;
38139
- constructor = getFilledArrowCoords;
38361
+ coords = getFilledArrowCoords(d);
38362
+ } else if (d.type == 'ring') {
38363
+ coords = getRingCoords(d);
38364
+ geojsonType = 'MultiPolygon';
38140
38365
  } else {
38141
- size = d.radius || d.length || d.r;
38142
- constructor = getPolygonCoords;
38366
+ coords = getPolygonCoords(d);
38143
38367
  }
38144
- if (size > 0 === false) return null;
38145
- coords = constructor(size, d, opts);
38368
+ if (!coords) return null;
38146
38369
  rotateCoords(coords, +d.rotation || 0);
38147
38370
  if (!polygonMode) {
38148
38371
  flipY(coords);
@@ -38153,9 +38376,9 @@ ${svg}
38153
38376
  if (polygonMode) {
38154
38377
  scaleAndShiftCoords(coords, metersPerPx, shp[0]);
38155
38378
  if (d.tfill) rec.fill = d.fill;
38156
- return createGeometry(coords);
38379
+ return createGeometry(coords, geojsonType);
38157
38380
  } else {
38158
- rec['svg-symbol'] = makeSvgSymbol(coords, d);
38381
+ rec['svg-symbol'] = makeSvgPolygonSymbol(coords, d, geojsonType);
38159
38382
  }
38160
38383
  });
38161
38384
 
@@ -38186,9 +38409,9 @@ ${svg}
38186
38409
  return importGeoJSON(geojson);
38187
38410
  }
38188
38411
 
38189
- function createGeometry(coords) {
38412
+ function createGeometry(coords, type) {
38190
38413
  return {
38191
- type: 'Polygon',
38414
+ type: type,
38192
38415
  coordinates: coords
38193
38416
  };
38194
38417
  }
@@ -38201,7 +38424,12 @@ ${svg}
38201
38424
  }
38202
38425
 
38203
38426
  // Returns an svg-symbol data object for one symbol
38204
- function makeSvgSymbol(coords, properties) {
38427
+ function makeSvgPolygonSymbol(coords, properties, geojsonType) {
38428
+ if (geojsonType == 'MultiPolygon') {
38429
+ coords = convertMultiPolygonCoords(coords);
38430
+ } else if (geojsonType != 'Polygon') {
38431
+ error('Unsupported type:', geojsonType);
38432
+ }
38205
38433
  roundCoordsForSVG(coords);
38206
38434
  return {
38207
38435
  type: 'polygon',
@@ -38210,9 +38438,14 @@ ${svg}
38210
38438
  };
38211
38439
  }
38212
38440
 
38441
+ function convertMultiPolygonCoords(coords) {
38442
+ return coords.reduce(function(memo, poly) {
38443
+ return memo.concat(poly);
38444
+ }, []);
38445
+ }
38446
+
38213
38447
  var Symbols = /*#__PURE__*/Object.freeze({
38214
- __proto__: null,
38215
- makeSvgSymbol: makeSvgSymbol
38448
+ __proto__: null
38216
38449
  });
38217
38450
 
38218
38451
  cmd.target = function(catalog, opts) {