mapshaper 0.5.82 → 0.5.86

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.85";
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.
@@ -13698,16 +13708,21 @@
13698
13708
  length: 'number', // e.g. arrow length
13699
13709
  rotation: 'number',
13700
13710
  radius: 'number',
13701
- 'arrow-length': 'number',
13702
- 'arrow-direction': 'number',
13703
- 'arrow-head-angle': 'number',
13704
- 'arrow-head-width': 'number',
13705
- 'arrow-stem-width': 'number',
13706
- 'arrow-stem-curve': 'number', // degrees of arc
13707
- 'arrow-stem-taper': 'number',
13708
- 'arrow-stem-length': 'number',
13709
- 'arrow-head-length': 'number',
13710
- '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',
13711
13726
  'arrow-scaling': 'number',
13712
13727
  effect: null // e.g. "fade"
13713
13728
  }, stylePropertyTypes);
@@ -13743,6 +13758,7 @@
13743
13758
  function getSymbolDataAccessor(lyr, opts) {
13744
13759
  var functions = {};
13745
13760
  var properties = [];
13761
+ var fields = lyr.data ? lyr.data.getFields() : [];
13746
13762
 
13747
13763
  Object.keys(opts).forEach(function(optName) {
13748
13764
  var svgName = optName.replace(/_/g, '-');
@@ -13754,6 +13770,8 @@
13754
13770
  properties.push(svgName);
13755
13771
  });
13756
13772
 
13773
+ // TODO: consider applying values of existing fields with names of symbol properties
13774
+
13757
13775
  return function(id) {
13758
13776
  var d = {}, name;
13759
13777
  for (var i=0; i<properties.length; i++) {
@@ -13769,8 +13787,9 @@
13769
13787
  // * ???
13770
13788
  //
13771
13789
  function mightBeExpression(str, fields) {
13790
+ fields = fields || [];
13772
13791
  if (fields.indexOf(str.trim()) > -1) return true;
13773
- return /[(){}.+-/*?:&|=\[]/.test(str);
13792
+ return /[(){}./*?:&|=\[+-]/.test(str);
13774
13793
  }
13775
13794
 
13776
13795
  function getSymbolPropertyAccessor(val, svgName, lyr) {
@@ -13791,6 +13810,7 @@
13791
13810
  // treating the string as a literal value
13792
13811
  literalVal = strVal;
13793
13812
  }
13813
+ // console.log("literalVal:", mightBeExpression(strVal, fields), strVal, fields)
13794
13814
  if (accessor) return accessor;
13795
13815
  if (literalVal !== null) return function(id) {return literalVal;};
13796
13816
  stop('Unexpected value for', svgName + ':', strVal);
@@ -13824,6 +13844,8 @@
13824
13844
  val = isDashArray(strVal) ? strVal : null;
13825
13845
  } else if (type == 'pattern') {
13826
13846
  val = isPattern(strVal) ? strVal : null;
13847
+ } else if (type == 'boolean') {
13848
+ val = parseBoolean(strVal);
13827
13849
  }
13828
13850
  // else {
13829
13851
  // // unknown type -- assume literal value
@@ -13844,10 +13866,17 @@
13844
13866
  return /^( ?[_a-z][-_a-z0-9]*\b)+$/i.test(str);
13845
13867
  }
13846
13868
 
13869
+
13847
13870
  function isSvgNumber(o) {
13848
13871
  return utils.isFiniteNumber(o) || utils.isString(o) && /^-?[.0-9]+$/.test(o);
13849
13872
  }
13850
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
+
13851
13880
  function isSvgMeasure(o) {
13852
13881
  return utils.isFiniteNumber(o) || utils.isString(o) && /^-?[.0-9]+[a-z]*$/.test(o);
13853
13882
  }
@@ -13871,6 +13900,7 @@
13871
13900
  getSymbolPropertyAccessor: getSymbolPropertyAccessor,
13872
13901
  isSvgClassName: isSvgClassName,
13873
13902
  isSvgNumber: isSvgNumber,
13903
+ parseBoolean: parseBoolean,
13874
13904
  isSvgMeasure: isSvgMeasure,
13875
13905
  parseSvgMeasure: parseSvgMeasure,
13876
13906
  isSvgColor: isSvgColor
@@ -14089,6 +14119,9 @@
14089
14119
  o = importLineString(coords[i]);
14090
14120
  o.properties.d = d + o.properties.d + ' Z';
14091
14121
  }
14122
+ if (coords.length > 1) {
14123
+ o.properties['fill-rule'] = 'evenodd'; // support polygons with holes
14124
+ }
14092
14125
  return o;
14093
14126
  }
14094
14127
 
@@ -18005,6 +18038,10 @@ ${svg}
18005
18038
  return chunks;
18006
18039
  }
18007
18040
 
18041
+ function parseNumberList(token) {
18042
+ return token.split(',').map(parseFloat);
18043
+ }
18044
+
18008
18045
  // Split comma-delimited list, trim quotes from entire list and
18009
18046
  // individual members
18010
18047
  function parseStringList(token) {
@@ -18094,6 +18131,7 @@ ${svg}
18094
18131
  var OptionParsingUtils = /*#__PURE__*/Object.freeze({
18095
18132
  __proto__: null,
18096
18133
  splitShellTokens: splitShellTokens,
18134
+ parseNumberList: parseNumberList,
18097
18135
  parseStringList: parseStringList,
18098
18136
  parseColorList: parseColorList,
18099
18137
  cleanArgv: cleanArgv,
@@ -18285,7 +18323,7 @@ ${svg}
18285
18323
  } else if (type == 'strings') {
18286
18324
  val = parseStringList(token);
18287
18325
  } else if (type == 'bbox' || type == 'numbers') {
18288
- val = token.split(',').map(parseFloat);
18326
+ val = parseNumberList(token);
18289
18327
  } else if (type == 'percent') {
18290
18328
  // val = utils.parsePercent(token);
18291
18329
  val = token; // string value is parsed by command function
@@ -20076,56 +20114,82 @@ ${svg}
20076
20114
  type: 'distance'
20077
20115
  })
20078
20116
  .option('sides', {
20079
- describe: 'sides of a polygon or star symbol',
20117
+ describe: 'number of sides of a polygon symbol',
20080
20118
  type: 'number'
20081
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
+ })
20082
20132
  .option('rotation', {
20083
20133
  describe: 'rotation of symbol in degrees'
20084
20134
  })
20085
- .option('orientation', {
20086
- describe: 'use orientation=b for a rotated or flipped orientation'
20087
- })
20088
- .option('length', {
20089
- // alias for arrow-length
20135
+ .option('points', {
20136
+ describe: '(star) number of points'
20090
20137
  })
20091
- .option('star-ratio', {
20092
- describe: 'ratio of major to minor radius of star',
20138
+ .option('point-ratio', {
20139
+ old_alias: 'star-ratio',
20140
+ describe: '(star) ratio of minor to major radius of star',
20093
20141
  type: 'number'
20094
20142
  })
20095
- .option('arrow-length', {
20096
- describe: 'length of arrows in pixels (use with type=arrow)'
20143
+ .option('radii', {
20144
+ describe: '(ring) comma-sep. list of concentric radii, ascending order'
20097
20145
  })
20098
- .option('arrow-direction', {
20099
- describe: 'angle off of vertical (-90 = left-pointing arrow)'
20146
+ .option('length', {
20147
+ old_alias: 'arrow-length',
20148
+ describe: '(arrow) length of arrow in pixels'
20149
+ })
20150
+ .option('direction', {
20151
+ old_alias: 'arrow-direction',
20152
+ describe: '(arrow) angle off vertical (-90 = left-pointing)'
20100
20153
  })
20101
- .option('arrow-head-angle', {
20102
- describe: 'angle of tip of arrow (default is 40 degrees)'
20154
+ .option('head-angle', {
20155
+ old_alias: 'arrow-head-angle',
20156
+ describe: '(arrow) angle of tip of arrow (default is 40 degrees)'
20103
20157
  })
20104
- .option('arrow-head-width', {
20105
- describe: 'size of arrow head from side to side'
20158
+ .option('head-width', {
20159
+ old_alias: 'arrow-head-width',
20160
+ describe: '(arrow) width of arrow head from side to side'
20106
20161
  })
20107
- .option('arrow-head-length', {
20108
- describe: 'length of arrow head (alternative to arrow-head-angle)'
20162
+ .option('head-length', {
20163
+ old_alias: 'arrow-head-width',
20164
+ describe: '(arrow) length of head (alternative to head-angle)'
20109
20165
  })
20110
- .option('arrow-head-shape', {
20166
+ .option('head-shape', {
20111
20167
  // describe: 'options: a b c'
20112
20168
  })
20113
- .option('arrow-stem-width', {
20114
- 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'
20115
20172
  })
20116
- .option('arrow-stem-length', {
20117
- describe: 'alternative to arrow-length'
20173
+ .option('stem-length', {
20174
+ old_alias: 'arrow-stem-length',
20175
+ describe: '(arrow) alternative to length'
20118
20176
  })
20119
- .option('arrow-stem-taper', {
20120
- describe: 'factor for tapering the width of the stem'
20177
+ .option('stem-taper', {
20178
+ old_alias: 'arrow-stem-taper',
20179
+ describe: '(arrow) factor for tapering the width of the stem (0-1)'
20121
20180
  })
20122
- .option('arrow-stem-curve', {
20123
- describe: 'curvature in degrees (arrows are straight by default)'
20181
+ .option('stem-curve', {
20182
+ old_alias: 'arrow-stem-curve',
20183
+ describe: '(arrow) curvature in degrees (default is 0)'
20124
20184
  })
20125
- .option('arrow-min-stem', {
20126
- describe: 'min ratio of stem to total length (for small arrows)',
20185
+ .option('min-stem-ratio', {
20186
+ old_alias: 'arrow-min-stem',
20187
+ describe: '(arrow) min ratio of stem to total length',
20127
20188
  type: 'number'
20128
20189
  })
20190
+ .option('anchor', {
20191
+ describe: '(arrow) takes one of: start, middle, end (default is start)'
20192
+ })
20129
20193
  .option('stroke', {})
20130
20194
  .option('stroke-width', {})
20131
20195
  .option('fill', {
@@ -21660,7 +21724,7 @@ ${svg}
21660
21724
  collectionType = 'mixed';
21661
21725
  }
21662
21726
  } else if (currType != t) {
21663
- stop("Unable to import mixed-geometry GeoJSON features");
21727
+ stop("Unable to import mixed-geometry features");
21664
21728
  }
21665
21729
  }
21666
21730
 
@@ -21771,6 +21835,33 @@ ${svg}
21771
21835
  importShp: importShp
21772
21836
  });
21773
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
+
21774
21865
  function GeoJSONParser(opts) {
21775
21866
  var idField = opts.id_field || GeoJSON.ID_FIELD,
21776
21867
  importer = new PathImporter(opts),
@@ -21792,8 +21883,11 @@ ${svg}
21792
21883
  geom = o;
21793
21884
  }
21794
21885
  // TODO: improve so geometry_type option skips features instead of creating null geometries
21795
- importer.startShape(rec);
21796
- 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
+ }
21797
21891
  };
21798
21892
 
21799
21893
  this.done = function() {
@@ -21801,50 +21895,58 @@ ${svg}
21801
21895
  };
21802
21896
  }
21803
21897
 
21804
- function importGeoJSON(src, optsArg) {
21805
- var opts = optsArg || {};
21806
- var supportedGeometries = Object.keys(GeoJSON.pathImporters),
21807
- srcObj = utils.isString(src) ? JSON.parse(src) : src,
21808
- importer = new GeoJSONParser(opts),
21809
- srcCollection, dataset;
21810
-
21811
- // Convert single feature or geometry into a collection with one member
21812
- if (srcObj.type == 'Feature') {
21813
- srcCollection = {
21814
- type: 'FeatureCollection',
21815
- features: [srcObj]
21816
- };
21817
- } else if (supportedGeometries.includes(srcObj.type)) {
21818
- srcCollection = {
21819
- type: 'GeometryCollection',
21820
- geometries: [srcObj]
21821
- };
21822
- } else {
21823
- 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;
21824
21903
  }
21825
- (srcCollection.features || srcCollection.geometries || []).forEach(importer.parseObject);
21826
- dataset = importer.done();
21827
- importCRS(dataset, srcObj); // TODO: remove this
21828
- 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);
21829
21927
  }
21830
21928
 
21831
- GeoJSON.importGeometry = function(geom, importer, opts) {
21832
- var type = geom.type;
21833
- 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) {
21834
21939
  if (opts.geometry_type && opts.geometry_type != GeoJSON.translateGeoJSONType(type)) {
21835
21940
  // kludge to filter out all but one type of geometry
21836
21941
  return;
21837
21942
  }
21838
21943
  GeoJSON.pathImporters[type](geom.coordinates, importer);
21839
- } else if (type == 'GeometryCollection') {
21840
- geom.geometries.forEach(function(geom) {
21841
- GeoJSON.importGeometry(geom, importer, opts);
21842
- });
21843
21944
  } else {
21844
- verbose("GeoJSON.importGeometry() Unsupported geometry type:", geom.type);
21945
+ verbose("Unsupported geometry type:", geom.type);
21845
21946
  }
21846
21947
  };
21847
21948
 
21949
+
21848
21950
  // Functions for importing geometry coordinates using a PathImporter
21849
21951
  //
21850
21952
  GeoJSON.pathImporters = {
@@ -21883,8 +21985,8 @@ ${svg}
21883
21985
 
21884
21986
  var GeojsonImport = /*#__PURE__*/Object.freeze({
21885
21987
  __proto__: null,
21886
- GeoJSONParser: GeoJSONParser,
21887
21988
  importGeoJSON: importGeoJSON,
21989
+ GeoJSONParser: GeoJSONParser,
21888
21990
  importCRS: importCRS
21889
21991
  });
21890
21992
 
@@ -37968,108 +38070,131 @@ ${svg}
37968
38070
  // return [stem, head];
37969
38071
  // }
37970
38072
 
37971
- function getMinStemRatio(d) {
37972
- return d['arrow-min-stem'] >= 0 ? d['arrow-min-stem'] : 0.4;
37973
- }
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
+ // }
37974
38080
 
37975
- function getFilledArrowCoords(d) {
37976
- var totalLen = d['arrow-length'] || d.radius || d.length || d.r || 0,
37977
- direction = d.rotation || d['arrow-direction'] || 0,
37978
- unscaledStemWidth = d['arrow-stem-width'] || 2,
37979
- unscaledHeadWidth = d['arrow-head-width'] || unscaledStemWidth * 3,
37980
- unscaledHeadLen = getHeadLength(unscaledHeadWidth, d),
37981
- stemTaper = d['arrow-stem-taper'] || 0,
37982
- stemCurve = d['arrow-stem-curve'] || 0;
38081
+ // function addPoints(a, b) {
38082
+ // return [a[0] + b[0], a[1] + b[1]];
38083
+ // }
37983
38084
 
37984
- var headLen, headWidth, stemLen, stemWidth;
38085
+ function calcStraightArrowCoords(stemLen, headLen, stemDx, headDx, baseDx) {
38086
+ return [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
38087
+ [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]];
38088
+ }
37985
38089
 
37986
- var scale = 1;
38090
+ function calcArrowSize(d) {
38091
+ var totalLen = d.radius || d.length || d.r || 0,
38092
+ scale = 1,
38093
+ o = initArrowSize(d); // calc several parameters
37987
38094
 
37988
38095
  if (totalLen > 0) {
37989
- scale = getScale(totalLen, unscaledHeadLen, getMinStemRatio(d));
37990
- headWidth = unscaledHeadWidth * scale;
37991
- headLen = unscaledHeadLen * scale;
37992
- stemWidth = unscaledStemWidth * scale;
37993
- stemLen = totalLen - headLen;
37994
-
37995
- } else {
37996
- headWidth = unscaledHeadWidth;
37997
- headLen = unscaledHeadLen;
37998
- stemWidth = unscaledStemWidth;
37999
- stemLen = d['arrow-stem-length'] || 0;
38000
- totalLen = headLen + stemLen;
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;
38001
38101
  }
38002
38102
 
38003
- if (totalLen > 0 === false) return null;
38004
-
38005
- var coords;
38006
-
38007
- var headDx = headWidth / 2,
38008
- stemDx = stemWidth / 2,
38009
- baseDx = stemDx * (1 - stemTaper);
38010
-
38011
- if (unscaledHeadWidth < unscaledStemWidth) {
38103
+ if (o.headWidth < o.stemWidth) {
38012
38104
  stop('Arrow head must be at least as wide as the stem.');
38013
38105
  }
38014
-
38015
- if (!stemCurve || Math.abs(stemCurve) > 90) {
38016
- coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
38017
- [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]];
38018
- } else {
38019
- if (direction > 0) stemCurve = -stemCurve;
38020
- coords = getCurvedArrowCoords(stemLen, headLen, stemCurve, stemDx, headDx, baseDx);
38021
- }
38022
-
38023
- rotateCoords(coords, direction);
38024
- return [coords];
38106
+ return o;
38025
38107
  }
38026
38108
 
38027
-
38028
- 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;
38029
38112
  var maxHeadPct = 1 - minStemRatio;
38030
38113
  var headPct = headLen / totalLen;
38114
+ var scale = 1;
38115
+
38031
38116
  if (headPct > maxHeadPct) {
38032
- return maxHeadPct / headPct;
38117
+ scale = maxHeadPct / headPct;
38118
+ } else if (stemLen + headLen > totalLen) {
38119
+ scale = totalLen / (stemLen + headLen);
38033
38120
  }
38034
- return 1;
38121
+ return scale;
38035
38122
  }
38036
38123
 
38037
- // function getStickArrowTip(totalLen, curve) {
38038
- // // curve/2 intersects the arrowhead at 90deg (trigonometry)
38039
- // var theta = Math.abs(curve/2) / 180 * Math.PI;
38040
- // var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1);
38041
- // var dy = totalLen * Math.cos(theta);
38042
- // return [dx, dy];
38043
- // }
38044
-
38045
- function addPoints(a, b) {
38046
- return [a[0] + b[0], a[1] + b[1]];
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;
38047
38143
  }
38048
38144
 
38049
38145
 
38050
- function getHeadLength(headWidth, d) {
38051
- var headLength = d['arrow-head-length'];
38052
- if (headLength > 0) return headLength;
38053
- var headAngle = d['arrow-head-angle'] || 40;
38054
- var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio
38055
- return headWidth * headRatio;
38146
+ // Returns ratio of head length to head width
38147
+ function getHeadSizeRatio(headAngle) {
38148
+ return 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2;
38056
38149
  }
38057
38150
 
38058
- function getCurvedArrowCoords(stemLen, headLen, curvature, stemDx, headDx, baseDx) {
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());
38175
+
38176
+ } else {
38177
+ dx = 0;
38178
+ dy = stemLen;
38179
+ stem = [[-baseDx, 0], [baseDx, 0], [baseDx, 0]];
38180
+ }
38181
+
38059
38182
  // coordinates go counter clockwise, starting from the leftmost head coordinate
38060
- var theta = Math.abs(curvature) / 180 * Math.PI;
38061
- var sign = curvature > 0 ? 1 : -1;
38062
- var dx = stemLen * Math.sin(theta / 2) * sign;
38063
- var dy = stemLen * Math.cos(theta / 2);
38064
- var head = [[stemDx + dx, dy], [headDx + dx, dy],
38065
- [dx, headLen + dy], [-headDx + dx, dy], [-stemDx + dx, dy]];
38066
- var ax = baseDx * Math.cos(theta); // rotate arrow base
38067
- var ay = baseDx * Math.sin(theta) * -sign;
38068
- var leftStem = getCurvedStemCoords(-ax, -ay, -stemDx + dx, dy, theta);
38069
- var rightStem = getCurvedStemCoords(ax, ay, stemDx + dx, dy, theta);
38070
- var stem = leftStem.concat(rightStem.reverse());
38071
- // stem.pop();
38072
- 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];
38073
38198
  }
38074
38199
 
38075
38200
  // ax, ay: point on the base
@@ -38106,43 +38231,31 @@ ${svg}
38106
38231
  return coords;
38107
38232
  }
38108
38233
 
38109
- // sides: e.g. 5-pointed star has 10 sides
38110
- // radius: distance from center to point
38111
- //
38112
- function getPolygonCoords(opts) {
38113
- var radius = opts.radius || opts.length || opts.r;
38234
+ function getPolygonCoords(d) {
38235
+ var radius = d.radius || d.length || d.r,
38236
+ sides = +d.sides || getSidesByType(d.type),
38237
+ rotated = sides % 2 == 1,
38238
+ coords = [],
38239
+ angle, b;
38240
+
38114
38241
  if (radius > 0 === false) return null;
38115
- var type = opts.type;
38116
- var sides = +opts.sides || getDefaultSides(type);
38117
- var isStar = type == 'star';
38118
- if (isStar && (sides < 6 || sides % 2 !== 0)) {
38119
- stop(`Invalid number of sides for a star (${sides})`);
38120
- } else if (sides >= 3 === false) {
38242
+ if (sides >= 3 === false) {
38121
38243
  stop(`Invalid number of sides (${sides})`);
38122
38244
  }
38123
- var coords = [],
38124
- angle = 360 / sides,
38125
- b = isStar ? 1 : 0.5,
38126
- theta, even, len;
38127
- if (opts.orientation == 'b') {
38128
- b = 0;
38245
+ if (d.orientation == 'b' || d.flipped || d.rotated) {
38246
+ rotated = !rotated;
38129
38247
  }
38248
+ b = rotated ? 0 : 0.5;
38130
38249
  for (var i=0; i<sides; i++) {
38131
- even = i % 2 == 0;
38132
- len = radius;
38133
- if (isStar && even) {
38134
- len *= (opts.star_ratio || 0.5);
38135
- }
38136
- theta = (i + b) * angle % 360;
38137
- coords.push(getPlanarSegmentEndpoint(0, 0, theta, len));
38250
+ angle = (i + b) / sides * 360;
38251
+ coords.push(getPlanarSegmentEndpoint(0, 0, angle, radius));
38138
38252
  }
38139
38253
  coords.push(coords[0].concat());
38140
38254
  return [coords];
38141
38255
  }
38142
38256
 
38143
- function getDefaultSides(type) {
38257
+ function getSidesByType(type) {
38144
38258
  return {
38145
- star: 10,
38146
38259
  circle: 72,
38147
38260
  triangle: 3,
38148
38261
  square: 4,
@@ -38155,6 +38268,83 @@ ${svg}
38155
38268
  }[type] || 4;
38156
38269
  }
38157
38270
 
38271
+ function getStarCoords(d) {
38272
+ var radius = d.radius || d.length || d.r,
38273
+ points = d.points || d.sides && d.sides / 2 || 5,
38274
+ sides = points * 2,
38275
+ minorRadius = getMinorRadius(points) * radius,
38276
+ b = d.orientation == 'b' || d.flipped || d.rotated ? 0 : 1,
38277
+ coords = [],
38278
+ angle, len;
38279
+
38280
+ if (radius > 0 === false) return null;
38281
+ if (points < 5) {
38282
+ stop(`Invalid number of points for a star (${points})`);
38283
+ }
38284
+ for (var i=0; i<sides; i++) {
38285
+ len = i % 2 == 0 ? minorRadius : radius;
38286
+ angle = (i + b) / sides * 360;
38287
+ coords.push(getPlanarSegmentEndpoint(0, 0, angle, len));
38288
+ }
38289
+ coords.push(coords[0].concat());
38290
+ return [coords];
38291
+ }
38292
+
38293
+ function getMinorRadius(points) {
38294
+ var innerAngle = 360 / points;
38295
+ var pointAngle = getDefaultPointAngle(points);
38296
+ var thetaA = Math.PI / 180 * innerAngle / 2;
38297
+ var thetaB = Math.PI / 180 * pointAngle / 2;
38298
+ var a = Math.tan(thetaB) / (Math.tan(thetaB) + Math.tan(thetaA));
38299
+ var c = a / Math.cos(thetaA);
38300
+ return c;
38301
+ }
38302
+
38303
+ function getDefaultPointAngle(points) {
38304
+ var minSkip = 1;
38305
+ var maxSkip = Math.ceil(points / 2) - 2;
38306
+ var skip = Math.floor((maxSkip + minSkip) / 2);
38307
+ return getPointAngle(points, skip);
38308
+ }
38309
+
38310
+ // skip: number of adjacent points to skip when drawing a segment
38311
+ function getPointAngle(points, skip) {
38312
+ var unitAngle = 360 / points;
38313
+ var centerAngle = unitAngle * (skip + 1);
38314
+ return 180 - centerAngle;
38315
+ }
38316
+
38317
+ // Returns GeoJSON MultiPolygon coords
38318
+ function getRingCoords(d) {
38319
+ var radii = parseRings(d.radii || '2');
38320
+ var coords = [];
38321
+ var solidCenter = utils.isOdd(radii.length);
38322
+ var ring, hole;
38323
+ for (var i=0; i<radii.length; i++) {
38324
+ ring = getPolygonCoords({
38325
+ type: 'circle',
38326
+ radius: radii[i]
38327
+ });
38328
+ if (!solidCenter || i > 0) {
38329
+ i++;
38330
+ hole = ring;
38331
+ ring = getPolygonCoords({
38332
+ type: 'circle',
38333
+ radius: radii[i]
38334
+ });
38335
+ ring.push(hole[0]);
38336
+ }
38337
+ coords.push(ring);
38338
+ }
38339
+ return coords;
38340
+ }
38341
+
38342
+ function parseRings(arg) {
38343
+ var arr = Array.isArray(arg) ? arg : parseNumberList(arg);
38344
+ utils.genericSort(arr, true);
38345
+ return utils.uniq(arr);
38346
+ }
38347
+
38158
38348
  // TODO: refactor to remove duplication in mapshaper-svg-style.js
38159
38349
  cmd.symbols = function(inputLyr, dataset, opts) {
38160
38350
  requireSinglePointLayer(inputLyr);
@@ -38171,9 +38361,15 @@ ${svg}
38171
38361
  if (!shp) return null;
38172
38362
  var d = getSymbolData(i);
38173
38363
  var rec = records[i] || {};
38364
+ var geojsonType = 'Polygon';
38174
38365
  var coords;
38175
38366
  if (d.type == 'arrow') {
38176
38367
  coords = getFilledArrowCoords(d);
38368
+ } else if (d.type == 'ring') {
38369
+ coords = getRingCoords(d);
38370
+ geojsonType = 'MultiPolygon';
38371
+ } else if (d.type == 'star') {
38372
+ coords = getStarCoords(d);
38177
38373
  } else {
38178
38374
  coords = getPolygonCoords(d);
38179
38375
  }
@@ -38188,9 +38384,9 @@ ${svg}
38188
38384
  if (polygonMode) {
38189
38385
  scaleAndShiftCoords(coords, metersPerPx, shp[0]);
38190
38386
  if (d.tfill) rec.fill = d.fill;
38191
- return createGeometry(coords);
38387
+ return createGeometry(coords, geojsonType);
38192
38388
  } else {
38193
- rec['svg-symbol'] = makeSvgSymbol(coords, d);
38389
+ rec['svg-symbol'] = makeSvgPolygonSymbol(coords, d, geojsonType);
38194
38390
  }
38195
38391
  });
38196
38392
 
@@ -38221,9 +38417,9 @@ ${svg}
38221
38417
  return importGeoJSON(geojson);
38222
38418
  }
38223
38419
 
38224
- function createGeometry(coords) {
38420
+ function createGeometry(coords, type) {
38225
38421
  return {
38226
- type: 'Polygon',
38422
+ type: type,
38227
38423
  coordinates: coords
38228
38424
  };
38229
38425
  }
@@ -38236,7 +38432,12 @@ ${svg}
38236
38432
  }
38237
38433
 
38238
38434
  // Returns an svg-symbol data object for one symbol
38239
- function makeSvgSymbol(coords, properties) {
38435
+ function makeSvgPolygonSymbol(coords, properties, geojsonType) {
38436
+ if (geojsonType == 'MultiPolygon') {
38437
+ coords = convertMultiPolygonCoords(coords);
38438
+ } else if (geojsonType != 'Polygon') {
38439
+ error('Unsupported type:', geojsonType);
38440
+ }
38240
38441
  roundCoordsForSVG(coords);
38241
38442
  return {
38242
38443
  type: 'polygon',
@@ -38245,9 +38446,14 @@ ${svg}
38245
38446
  };
38246
38447
  }
38247
38448
 
38449
+ function convertMultiPolygonCoords(coords) {
38450
+ return coords.reduce(function(memo, poly) {
38451
+ return memo.concat(poly);
38452
+ }, []);
38453
+ }
38454
+
38248
38455
  var Symbols = /*#__PURE__*/Object.freeze({
38249
- __proto__: null,
38250
- makeSvgSymbol: makeSvgSymbol
38456
+ __proto__: null
38251
38457
  });
38252
38458
 
38253
38459
  cmd.target = function(catalog, opts) {