mapshaper 0.5.80 → 0.5.84

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,18 @@
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
+ '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-ratio': 'number',
13703
13723
  'arrow-scaling': 'number',
13704
13724
  effect: null // e.g. "fade"
13705
13725
  }, stylePropertyTypes);
@@ -13735,6 +13755,7 @@
13735
13755
  function getSymbolDataAccessor(lyr, opts) {
13736
13756
  var functions = {};
13737
13757
  var properties = [];
13758
+ var fields = lyr.data ? lyr.data.getFields() : [];
13738
13759
 
13739
13760
  Object.keys(opts).forEach(function(optName) {
13740
13761
  var svgName = optName.replace(/_/g, '-');
@@ -13746,6 +13767,8 @@
13746
13767
  properties.push(svgName);
13747
13768
  });
13748
13769
 
13770
+ // TODO: consider applying values of existing fields with names of symbol properties
13771
+
13749
13772
  return function(id) {
13750
13773
  var d = {}, name;
13751
13774
  for (var i=0; i<properties.length; i++) {
@@ -13761,8 +13784,9 @@
13761
13784
  // * ???
13762
13785
  //
13763
13786
  function mightBeExpression(str, fields) {
13787
+ fields = fields || [];
13764
13788
  if (fields.indexOf(str.trim()) > -1) return true;
13765
- return /[(){}.+-/*?:&|=\[]/.test(str);
13789
+ return /[(){}./*?:&|=\[+-]/.test(str);
13766
13790
  }
13767
13791
 
13768
13792
  function getSymbolPropertyAccessor(val, svgName, lyr) {
@@ -13783,6 +13807,7 @@
13783
13807
  // treating the string as a literal value
13784
13808
  literalVal = strVal;
13785
13809
  }
13810
+ // console.log("literalVal:", mightBeExpression(strVal, fields), strVal, fields)
13786
13811
  if (accessor) return accessor;
13787
13812
  if (literalVal !== null) return function(id) {return literalVal;};
13788
13813
  stop('Unexpected value for', svgName + ':', strVal);
@@ -13816,6 +13841,8 @@
13816
13841
  val = isDashArray(strVal) ? strVal : null;
13817
13842
  } else if (type == 'pattern') {
13818
13843
  val = isPattern(strVal) ? strVal : null;
13844
+ } else if (type == 'boolean') {
13845
+ val = parseBoolean(strVal);
13819
13846
  }
13820
13847
  // else {
13821
13848
  // // unknown type -- assume literal value
@@ -13836,10 +13863,17 @@
13836
13863
  return /^( ?[_a-z][-_a-z0-9]*\b)+$/i.test(str);
13837
13864
  }
13838
13865
 
13866
+
13839
13867
  function isSvgNumber(o) {
13840
13868
  return utils.isFiniteNumber(o) || utils.isString(o) && /^-?[.0-9]+$/.test(o);
13841
13869
  }
13842
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
+
13843
13877
  function isSvgMeasure(o) {
13844
13878
  return utils.isFiniteNumber(o) || utils.isString(o) && /^-?[.0-9]+[a-z]*$/.test(o);
13845
13879
  }
@@ -13863,6 +13897,7 @@
13863
13897
  getSymbolPropertyAccessor: getSymbolPropertyAccessor,
13864
13898
  isSvgClassName: isSvgClassName,
13865
13899
  isSvgNumber: isSvgNumber,
13900
+ parseBoolean: parseBoolean,
13866
13901
  isSvgMeasure: isSvgMeasure,
13867
13902
  parseSvgMeasure: parseSvgMeasure,
13868
13903
  isSvgColor: isSvgColor
@@ -14081,6 +14116,9 @@
14081
14116
  o = importLineString(coords[i]);
14082
14117
  o.properties.d = d + o.properties.d + ' Z';
14083
14118
  }
14119
+ if (coords.length > 1) {
14120
+ o.properties['fill-rule'] = 'evenodd'; // support polygons with holes
14121
+ }
14084
14122
  return o;
14085
14123
  }
14086
14124
 
@@ -17997,6 +18035,10 @@ ${svg}
17997
18035
  return chunks;
17998
18036
  }
17999
18037
 
18038
+ function parseNumberList(token) {
18039
+ return token.split(',').map(parseFloat);
18040
+ }
18041
+
18000
18042
  // Split comma-delimited list, trim quotes from entire list and
18001
18043
  // individual members
18002
18044
  function parseStringList(token) {
@@ -18032,9 +18074,9 @@ ${svg}
18032
18074
  // Updated: don't trim space from tokens like [delimeter= ]
18033
18075
  argv = argv.map(function(s) {
18034
18076
  if (!/= $/.test(s)) {
18035
- s = s.trimEnd();
18077
+ s = utils.rtrim(s);
18036
18078
  }
18037
- s = s.trimStart();
18079
+ s = utils.ltrim(s);
18038
18080
  return s;
18039
18081
  });
18040
18082
  argv = argv.filter(function(s) {return s !== '';}); // remove empty tokens
@@ -18086,6 +18128,7 @@ ${svg}
18086
18128
  var OptionParsingUtils = /*#__PURE__*/Object.freeze({
18087
18129
  __proto__: null,
18088
18130
  splitShellTokens: splitShellTokens,
18131
+ parseNumberList: parseNumberList,
18089
18132
  parseStringList: parseStringList,
18090
18133
  parseColorList: parseColorList,
18091
18134
  cleanArgv: cleanArgv,
@@ -18277,7 +18320,7 @@ ${svg}
18277
18320
  } else if (type == 'strings') {
18278
18321
  val = parseStringList(token);
18279
18322
  } else if (type == 'bbox' || type == 'numbers') {
18280
- val = token.split(',').map(parseFloat);
18323
+ val = parseNumberList(token);
18281
18324
  } else if (type == 'percent') {
18282
18325
  // val = utils.parsePercent(token);
18283
18326
  val = token; // string value is parsed by command function
@@ -20071,42 +20114,73 @@ ${svg}
20071
20114
  describe: 'sides of a polygon or star symbol',
20072
20115
  type: 'number'
20073
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
+ })
20074
20128
  .option('rotation', {
20075
20129
  describe: 'rotation of symbol in degrees'
20076
20130
  })
20077
- .option('orientation', {
20078
- describe: 'use orientation=b for a rotated or flipped orientation'
20079
- })
20080
20131
  .option('length', {
20081
20132
  // alias for arrow-length
20082
20133
  })
20083
- .option('star-ratio', {
20084
- describe: 'ratio of major to minor radius of star',
20134
+ .option('point-ratio', {
20135
+ old_alias: 'star-ratio',
20136
+ describe: '(star) ratio of minor to major radius of star',
20085
20137
  type: 'number'
20086
20138
  })
20087
- .option('arrow-length', {
20088
- describe: 'length of arrows in pixels (use with type=arrow)'
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'
20089
20157
  })
20090
- .option('arrow-direction', {
20091
- describe: 'angle off of vertical (-90 = left-pointing arrow)'
20158
+ .option('head-length', {
20159
+ old_alias: 'arrow-head-width',
20160
+ describe: '(arrow) length of head (alternative to head-angle)'
20092
20161
  })
20093
- .option('arrow-head-angle', {
20094
- describe: 'angle of tip of arrow (default is 40 degrees)'
20162
+ .option('head-shape', {
20163
+ // describe: 'options: a b c'
20095
20164
  })
20096
- .option('arrow-head-width', {
20097
- describe: 'size of arrow head from side to side'
20165
+ .option('stem-width', {
20166
+ old_alias: 'arrow-stem-width',
20167
+ describe: '(arrow) width of stem at its widest point'
20098
20168
  })
20099
- .option('arrow-stem-width', {
20100
- describe: 'width of stem at its widest point'
20169
+ .option('stem-length', {
20170
+ old_alias: 'arrow-stem-length',
20171
+ describe: '(arrow) alternative to length'
20101
20172
  })
20102
- .option('arrow-stem-taper', {
20103
- describe: 'factor for tapering the width of the stem'
20173
+ .option('stem-taper', {
20174
+ old_alias: 'arrow-stem-taper',
20175
+ describe: '(arrow) factor for tapering the width of the stem (0-1)'
20104
20176
  })
20105
- .option('arrow-stem-curve', {
20106
- describe: 'curvature in degrees (arrows are straight by default)'
20177
+ .option('stem-curve', {
20178
+ old_alias: 'arrow-stem-curve',
20179
+ describe: '(arrow) curvature in degrees (default is 0)'
20107
20180
  })
20108
- .option('arrow-min-stem', {
20109
- describe: 'min ratio of stem to total length (for small arrows)',
20181
+ .option('min-stem-ratio', {
20182
+ old_alias: 'arrow-min-stem',
20183
+ describe: '(arrow) min ratio of stem to total length',
20110
20184
  type: 'number'
20111
20185
  })
20112
20186
  .option('stroke', {})
@@ -21643,7 +21717,7 @@ ${svg}
21643
21717
  collectionType = 'mixed';
21644
21718
  }
21645
21719
  } else if (currType != t) {
21646
- stop("Unable to import mixed-geometry GeoJSON features");
21720
+ stop("Unable to import mixed-geometry features");
21647
21721
  }
21648
21722
  }
21649
21723
 
@@ -21754,6 +21828,33 @@ ${svg}
21754
21828
  importShp: importShp
21755
21829
  });
21756
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
+
21757
21858
  function GeoJSONParser(opts) {
21758
21859
  var idField = opts.id_field || GeoJSON.ID_FIELD,
21759
21860
  importer = new PathImporter(opts),
@@ -21775,8 +21876,11 @@ ${svg}
21775
21876
  geom = o;
21776
21877
  }
21777
21878
  // 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);
21879
+ if (geom && geom.type == 'GeometryCollection') {
21880
+ GeoJSON.importComplexFeature(importer, geom, rec, opts);
21881
+ } else {
21882
+ GeoJSON.importSimpleFeature(importer, geom, rec, opts);
21883
+ }
21780
21884
  };
21781
21885
 
21782
21886
  this.done = function() {
@@ -21784,50 +21888,58 @@ ${svg}
21784
21888
  };
21785
21889
  }
21786
21890
 
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;
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;
21807
21896
  }
21808
- (srcCollection.features || srcCollection.geometries || []).forEach(importer.parseObject);
21809
- dataset = importer.done();
21810
- importCRS(dataset, srcObj); // TODO: remove this
21811
- 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);
21812
21920
  }
21813
21921
 
21814
- GeoJSON.importGeometry = function(geom, importer, opts) {
21815
- var type = geom.type;
21816
- 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) {
21817
21932
  if (opts.geometry_type && opts.geometry_type != GeoJSON.translateGeoJSONType(type)) {
21818
21933
  // kludge to filter out all but one type of geometry
21819
21934
  return;
21820
21935
  }
21821
21936
  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
21937
  } else {
21827
- verbose("GeoJSON.importGeometry() Unsupported geometry type:", geom.type);
21938
+ verbose("Unsupported geometry type:", geom.type);
21828
21939
  }
21829
21940
  };
21830
21941
 
21942
+
21831
21943
  // Functions for importing geometry coordinates using a PathImporter
21832
21944
  //
21833
21945
  GeoJSON.pathImporters = {
@@ -21866,8 +21978,8 @@ ${svg}
21866
21978
 
21867
21979
  var GeojsonImport = /*#__PURE__*/Object.freeze({
21868
21980
  __proto__: null,
21869
- GeoJSONParser: GeoJSONParser,
21870
21981
  importGeoJSON: importGeoJSON,
21982
+ GeoJSONParser: GeoJSONParser,
21871
21983
  importCRS: importCRS
21872
21984
  });
21873
21985
 
@@ -37929,91 +38041,131 @@ ${svg}
37929
38041
  points.push(p2);
37930
38042
  }
37931
38043
 
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
-
37973
- var headDx = headWidth / 2,
37974
- stemDx = stemWidth / 2,
37975
- baseDx = stemDx * (1 - stemTaper);
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)];
37976
38060
 
37977
- var coords;
38061
+ // rotateCoords(stem, d.rotation);
38062
+ // rotateCoords(head, d.rotation);
38063
+ // return [stem, head];
38064
+ // }
38065
+
38066
+ // function getStickArrowTip(totalLen, curve) {
38067
+ // // curve/2 intersects the arrowhead at 90deg (trigonometry)
38068
+ // var theta = Math.abs(curve/2) / 180 * Math.PI;
38069
+ // var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1);
38070
+ // var dy = totalLen * Math.cos(theta);
38071
+ // return [dx, dy];
38072
+ // }
38073
+
38074
+ // function addPoints(a, b) {
38075
+ // return [a[0] + b[0], a[1] + b[1]];
38076
+ // }
38077
+
38078
+ function getFilledArrowCoords(d) {
38079
+ var direction = d.rotation || d.direction || 0,
38080
+ stemTaper = d['stem-taper'] || 0,
38081
+ stemCurve = d['stem-curve'] || 0,
38082
+ size = calcArrowSize(d);
38083
+
38084
+ if (!size) return null;
38085
+
38086
+ var headDx = size.headWidth / 2,
38087
+ stemDx = size.stemWidth / 2,
38088
+ baseDx = stemDx * (1 - stemTaper),
38089
+ coords;
37978
38090
 
37979
38091
  if (!stemCurve || Math.abs(stemCurve) > 90) {
37980
- coords = [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
37981
- [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]];
38092
+ coords = calcStraightArrowCoords(size.stemLen, size.headLen, stemDx, headDx, baseDx);
37982
38093
  } else {
37983
38094
  if (direction > 0) stemCurve = -stemCurve;
37984
- coords = getCurvedArrowCoords(stemLen, headLen, stemCurve, stemDx, headDx, baseDx);
38095
+ coords = getCurvedArrowCoords(size.stemLen, size.headLen, stemCurve, stemDx, headDx, baseDx);
37985
38096
  }
37986
38097
 
37987
38098
  rotateCoords(coords, direction);
38099
+ if (d.flipped) {
38100
+ flipY(coords);
38101
+ }
37988
38102
  return [coords];
37989
38103
  }
37990
38104
 
38105
+ function calcStraightArrowCoords(stemLen, headLen, stemDx, headDx, baseDx) {
38106
+ return [[baseDx, 0], [stemDx, stemLen], [headDx, stemLen], [0, stemLen + headLen],
38107
+ [-headDx, stemLen], [-stemDx, stemLen], [-baseDx, 0], [baseDx, 0]];
38108
+ }
37991
38109
 
37992
- function getScale(totalLen, headLen, minStemRatio) {
38110
+ function calcArrowSize(d) {
38111
+ var totalLen = d.radius || d.length || d.r || 0,
38112
+ scale = 1,
38113
+ o = initArrowSize(d); // calc several parameters
38114
+
38115
+ if (totalLen > 0) {
38116
+ scale = calcScale(totalLen, o.headLen, d);
38117
+ o.stemWidth *= scale;
38118
+ o.headWidth *= scale;
38119
+ o.headLen *= scale;
38120
+ o.stemLen = totalLen - o.headLen;
38121
+ }
38122
+
38123
+ if (o.headWidth < o.stemWidth) {
38124
+ stop('Arrow head must be at least as wide as the stem.');
38125
+ }
38126
+ return o;
38127
+ }
38128
+
38129
+ function calcScale(totalLen, headLen, d) {
38130
+ var minStemRatio = d['min-stem-ratio'] >= 0 ? d['min-stem-ratio'] : 0;
38131
+ var stemLen = d['stem-length'] || 0;
37993
38132
  var maxHeadPct = 1 - minStemRatio;
37994
38133
  var headPct = headLen / totalLen;
38134
+ var scale = 1;
38135
+
37995
38136
  if (headPct > maxHeadPct) {
37996
- return maxHeadPct / headPct;
38137
+ scale = maxHeadPct / headPct;
38138
+ } else if (stemLen + headLen > totalLen) {
38139
+ scale = totalLen / (stemLen + headLen);
37997
38140
  }
37998
- return 1;
38141
+ return scale;
37999
38142
  }
38000
38143
 
38001
- function getStickArrowTip(totalLen, curve) {
38002
- // curve/2 intersects the arrowhead at 90deg (trigonometry)
38003
- var theta = Math.abs(curve/2) / 180 * Math.PI;
38004
- var dx = totalLen * Math.sin(theta) * (curve > 0 ? -1 : 1);
38005
- var dy = totalLen * Math.cos(theta);
38006
- return [dx, dy];
38007
- }
38008
-
38009
- function addPoints(a, b) {
38010
- return [a[0] + b[0], a[1] + b[1]];
38144
+ function initArrowSize(d) {
38145
+ var sizeRatio = getHeadSizeRatio(d['head-angle'] || 40); // length to width
38146
+ var o = {
38147
+ stemWidth: d['stem-width'] || 2,
38148
+ stemLen: d['stem-length'] || 0,
38149
+ headWidth: d['head-width'],
38150
+ headLen: d['head-length']
38151
+ };
38152
+ if (!o.headWidth) {
38153
+ if (o.headLen) {
38154
+ o.headWidth = o.headLen / sizeRatio;
38155
+ } else {
38156
+ o.headWidth = o.stemWidth * 3; // assumes stemWidth has been set
38157
+ }
38158
+ }
38159
+ if (!o.headLen) {
38160
+ o.headLen = o.headWidth * sizeRatio;
38161
+ }
38162
+ return o;
38011
38163
  }
38012
38164
 
38013
38165
 
38014
- function getHeadLength(headWidth, headAngle) {
38015
- var headRatio = 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2; // length-to-width head ratio
38016
- return headWidth * headRatio;
38166
+ // Returns ratio of head length to head width
38167
+ function getHeadSizeRatio(headAngle) {
38168
+ return 1 / Math.tan(Math.PI * headAngle / 180 / 2) / 2;
38017
38169
  }
38018
38170
 
38019
38171
  function getCurvedArrowCoords(stemLen, headLen, curvature, stemDx, headDx, baseDx) {
@@ -38028,15 +38180,18 @@ ${svg}
38028
38180
  var ay = baseDx * Math.sin(theta) * -sign;
38029
38181
  var leftStem = getCurvedStemCoords(-ax, -ay, -stemDx + dx, dy, theta);
38030
38182
  var rightStem = getCurvedStemCoords(ax, ay, stemDx + dx, dy, theta);
38031
- // if (stemTaper == 1) leftStem.pop();
38032
38183
  var stem = leftStem.concat(rightStem.reverse());
38033
- stem.pop();
38184
+ // stem.pop();
38034
38185
  return stem.concat(head);
38035
38186
  }
38036
38187
 
38037
38188
  // ax, ay: point on the base
38038
38189
  // bx, by: point on the stem
38039
38190
  function getCurvedStemCoords(ax, ay, bx, by, theta0) {
38191
+ // case: curved side intrudes into head (because stem is too short)
38192
+ if (ay > by) {
38193
+ return [[ax * by / ay, by]];
38194
+ }
38040
38195
  var dx = bx - ax,
38041
38196
  dy = by - ay,
38042
38197
  dy1 = (dy * dy - dx * dx) / (2 * dy),
@@ -38048,10 +38203,9 @@ ${svg}
38048
38203
  leftBend = bx > ax,
38049
38204
  sign = leftBend ? 1 : -1,
38050
38205
  points = Math.round(degrees / 5) + 2,
38051
- // points = theta > 2 && 7 || theta > 1 && 6 || 5,
38052
- increment = theta / (points + 1);
38206
+ increment = theta / (points + 1),
38207
+ coords = [[bx, by]];
38053
38208
 
38054
- var coords = [[bx, by]];
38055
38209
  for (var i=1; i<= points; i++) {
38056
38210
  var phi = i * increment / 2;
38057
38211
  var sinPhi = Math.sin(phi);
@@ -38068,9 +38222,11 @@ ${svg}
38068
38222
  // sides: e.g. 5-pointed star has 10 sides
38069
38223
  // radius: distance from center to point
38070
38224
  //
38071
- function getPolygonCoords(radius, opts) {
38072
- var type = opts.type;
38073
- var sides = +opts.sides || getDefaultSides(type);
38225
+ function getPolygonCoords(d) {
38226
+ var radius = d.radius || d.length || d.r;
38227
+ if (radius > 0 === false) return null;
38228
+ var type = d.type;
38229
+ var sides = +d.sides || getDefaultSides(type);
38074
38230
  var isStar = type == 'star';
38075
38231
  if (isStar && (sides < 6 || sides % 2 !== 0)) {
38076
38232
  stop(`Invalid number of sides for a star (${sides})`);
@@ -38081,14 +38237,14 @@ ${svg}
38081
38237
  angle = 360 / sides,
38082
38238
  b = isStar ? 1 : 0.5,
38083
38239
  theta, even, len;
38084
- if (opts.orientation == 'b') {
38240
+ if (d.orientation == 'b' || d.flipped || d.rotated) {
38085
38241
  b = 0;
38086
38242
  }
38087
38243
  for (var i=0; i<sides; i++) {
38088
38244
  even = i % 2 == 0;
38089
38245
  len = radius;
38090
38246
  if (isStar && even) {
38091
- len *= (opts.star_ratio || 0.5);
38247
+ len *= (d.star_ratio || 0.5);
38092
38248
  }
38093
38249
  theta = (i + b) * angle % 360;
38094
38250
  coords.push(getPlanarSegmentEndpoint(0, 0, theta, len));
@@ -38112,6 +38268,37 @@ ${svg}
38112
38268
  }[type] || 4;
38113
38269
  }
38114
38270
 
38271
+ // Returns GeoJSON MultiPolygon coords
38272
+ function getRingCoords(d) {
38273
+ var radii = parseRings(d.radii || '2');
38274
+ var coords = [];
38275
+ var solidCenter = utils.isOdd(radii.length);
38276
+ var ring, hole;
38277
+ for (var i=0; i<radii.length; i++) {
38278
+ ring = getPolygonCoords({
38279
+ type: 'circle',
38280
+ radius: radii[i]
38281
+ });
38282
+ if (!solidCenter || i > 0) {
38283
+ i++;
38284
+ hole = ring;
38285
+ ring = getPolygonCoords({
38286
+ type: 'circle',
38287
+ radius: radii[i]
38288
+ });
38289
+ ring.push(hole[0]);
38290
+ }
38291
+ coords.push(ring);
38292
+ }
38293
+ return coords;
38294
+ }
38295
+
38296
+ function parseRings(arg) {
38297
+ var arr = Array.isArray(arg) ? arg : parseNumberList(arg);
38298
+ utils.genericSort(arr, true);
38299
+ return utils.uniq(arr);
38300
+ }
38301
+
38115
38302
  // TODO: refactor to remove duplication in mapshaper-svg-style.js
38116
38303
  cmd.symbols = function(inputLyr, dataset, opts) {
38117
38304
  requireSinglePointLayer(inputLyr);
@@ -38128,16 +38315,17 @@ ${svg}
38128
38315
  if (!shp) return null;
38129
38316
  var d = getSymbolData(i);
38130
38317
  var rec = records[i] || {};
38131
- var coords, size, constructor;
38318
+ var geojsonType = 'Polygon';
38319
+ var coords;
38132
38320
  if (d.type == 'arrow') {
38133
- size = d.radius || d.length || d['arrow-length'] || d.r;
38134
- constructor = getFilledArrowCoords;
38321
+ coords = getFilledArrowCoords(d);
38322
+ } else if (d.type == 'ring') {
38323
+ coords = getRingCoords(d);
38324
+ geojsonType = 'MultiPolygon';
38135
38325
  } else {
38136
- size = d.radius || d.length || d.r;
38137
- constructor = getPolygonCoords;
38326
+ coords = getPolygonCoords(d);
38138
38327
  }
38139
- if (size > 0 === false) return null;
38140
- coords = constructor(size, d, opts);
38328
+ if (!coords) return null;
38141
38329
  rotateCoords(coords, +d.rotation || 0);
38142
38330
  if (!polygonMode) {
38143
38331
  flipY(coords);
@@ -38148,9 +38336,9 @@ ${svg}
38148
38336
  if (polygonMode) {
38149
38337
  scaleAndShiftCoords(coords, metersPerPx, shp[0]);
38150
38338
  if (d.tfill) rec.fill = d.fill;
38151
- return createGeometry(coords);
38339
+ return createGeometry(coords, geojsonType);
38152
38340
  } else {
38153
- rec['svg-symbol'] = makeSvgSymbol(coords, d);
38341
+ rec['svg-symbol'] = makeSvgPolygonSymbol(coords, d, geojsonType);
38154
38342
  }
38155
38343
  });
38156
38344
 
@@ -38181,9 +38369,9 @@ ${svg}
38181
38369
  return importGeoJSON(geojson);
38182
38370
  }
38183
38371
 
38184
- function createGeometry(coords) {
38372
+ function createGeometry(coords, type) {
38185
38373
  return {
38186
- type: 'Polygon',
38374
+ type: type,
38187
38375
  coordinates: coords
38188
38376
  };
38189
38377
  }
@@ -38196,7 +38384,12 @@ ${svg}
38196
38384
  }
38197
38385
 
38198
38386
  // Returns an svg-symbol data object for one symbol
38199
- function makeSvgSymbol(coords, properties) {
38387
+ function makeSvgPolygonSymbol(coords, properties, geojsonType) {
38388
+ if (geojsonType == 'MultiPolygon') {
38389
+ coords = convertMultiPolygonCoords(coords);
38390
+ } else if (geojsonType != 'Polygon') {
38391
+ error('Unsupported type:', geojsonType);
38392
+ }
38200
38393
  roundCoordsForSVG(coords);
38201
38394
  return {
38202
38395
  type: 'polygon',
@@ -38205,9 +38398,14 @@ ${svg}
38205
38398
  };
38206
38399
  }
38207
38400
 
38401
+ function convertMultiPolygonCoords(coords) {
38402
+ return coords.reduce(function(memo, poly) {
38403
+ return memo.concat(poly);
38404
+ }, []);
38405
+ }
38406
+
38208
38407
  var Symbols = /*#__PURE__*/Object.freeze({
38209
- __proto__: null,
38210
- makeSvgSymbol: makeSvgSymbol
38408
+ __proto__: null
38211
38409
  });
38212
38410
 
38213
38411
  cmd.target = function(catalog, opts) {