mapshaper 0.7.55 → 0.7.58

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
@@ -351,7 +351,7 @@
351
351
  // Calc sum, skip falsy and NaN values
352
352
  // Assumes: no other non-numeric objects in array
353
353
  //
354
- function sum$1(arr, info) {
354
+ function sum$2(arr, info) {
355
355
  if (!isArrayLike(arr)) error$1 ("sum() expects an array, received:", arr);
356
356
  var tot = 0,
357
357
  nan = 0,
@@ -1117,7 +1117,7 @@
1117
1117
  quicksort: quicksort$1, quicksortPartition,
1118
1118
  range, regexEscape, reorderArray: reorderArray$1, repeat: repeat$1, repeatString,
1119
1119
  replaceArray, rpad, rtrim,
1120
- shuffle, some, sortArrayIndex, sortOn, splitLines, sum: sum$1,
1120
+ shuffle, some, sortArrayIndex, sortOn, splitLines, sum: sum$2,
1121
1121
  toArray: toArray$1, toBuffer, trim, trimQuotes,
1122
1122
  uniq, uniqifyNames,
1123
1123
  wildcardToRegExp
@@ -5793,10 +5793,13 @@
5793
5793
 
5794
5794
  function layerOnlyHasRectangles(lyr, arcs) {
5795
5795
  if (!layerHasPaths(lyr)) return false;
5796
- if (countMultiPartFeatures(lyr) > 0) return false;
5797
5796
  return lyr.shapes.every(function(shp) {
5798
5797
  if (!shp) return true;
5799
- return pathIsRectangle(shp[0], arcs);
5798
+ // Every part is tested: a shape whose first ring is rectangular may well
5799
+ // have others that are not.
5800
+ return shp.every(function(path) {
5801
+ return pathIsRectangle(path, arcs);
5802
+ });
5800
5803
  });
5801
5804
  }
5802
5805
 
@@ -5886,7 +5889,7 @@
5886
5889
 
5887
5890
  function requireSinglePointLayer(lyr, msg) {
5888
5891
  requirePointLayer(lyr);
5889
- if (countMultiPartFeatures(lyr) > 0) {
5892
+ if (countMultiPartFeatures(lyr.shapes) > 0) {
5890
5893
  stop$1(msg || 'This command requires single points; layer contains multi-point features.');
5891
5894
  }
5892
5895
  }
@@ -6030,8 +6033,16 @@
6030
6033
  return copy;
6031
6034
  }
6032
6035
 
6036
+ // shapes: one layer's "shapes" array
6037
+ // Guards against being passed a layer instead: several callers made that
6038
+ // mistake, and reading .length off a layer object silently returned 0, which
6039
+ // disabled the multi-part checks that depend on this function.
6033
6040
  function countMultiPartFeatures(shapes) {
6034
6041
  var count = 0;
6042
+ if (!shapes) return 0;
6043
+ if (!Array.isArray(shapes)) {
6044
+ error('countMultiPartFeatures() expects an array of shapes');
6045
+ }
6035
6046
  for (var i=0, n=shapes.length; i<n; i++) {
6036
6047
  if (shapes[i] && shapes[i].length > 1) count++;
6037
6048
  }
@@ -9869,7 +9880,7 @@
9869
9880
  id: piece.id,
9870
9881
  source_region: piece.source_region,
9871
9882
  copy: piece.copy,
9872
- projected_boundary: closeRing$1(piece.boundary)
9883
+ projected_boundary: closeRing$2(piece.boundary)
9873
9884
  };
9874
9885
  }
9875
9886
 
@@ -9877,7 +9888,7 @@
9877
9888
  return {
9878
9889
  id: piece.id,
9879
9890
  source_region: piece.source_region,
9880
- projected_boundary: closeRing$1(piece.boundary)
9891
+ projected_boundary: closeRing$2(piece.boundary)
9881
9892
  };
9882
9893
  }
9883
9894
 
@@ -9959,7 +9970,7 @@
9959
9970
  return paths;
9960
9971
  }
9961
9972
 
9962
- function closeRing$1(ring) {
9973
+ function closeRing$2(ring) {
9963
9974
  var copy = ring.map(function(p) { return p.concat(); });
9964
9975
  if (copy.length && !pointsEqual$2(copy[0], copy[copy.length - 1])) {
9965
9976
  copy.push(copy[0].concat());
@@ -14944,6 +14955,27 @@
14944
14955
  return fflatePostprocess(obj);
14945
14956
  }
14946
14957
 
14958
+ // Return the entry names in a zip archive, without decompressing them.
14959
+ // Used to tell a KMZ from an ordinary zip when there is no filename to go by.
14960
+ // input: A file path or a buffer
14961
+ function listZipEntryNames(input) {
14962
+ if (input instanceof ArrayBuffer) {
14963
+ input = new Uint8Array(input);
14964
+ }
14965
+ if (!runningInBrowser()) {
14966
+ var AdmZip = require('adm-zip');
14967
+ return new AdmZip(input).getEntries().map(function(entry) {
14968
+ return entry.entryName;
14969
+ });
14970
+ }
14971
+ var names = [];
14972
+ unzipSync$1(input, {filter: function(file) {
14973
+ names.push(file.name);
14974
+ return false; // collect names only
14975
+ }});
14976
+ return names;
14977
+ }
14978
+
14947
14979
  function unzipAsync(buf, cb) {
14948
14980
  if (!runningInBrowser()) {
14949
14981
  error('Async unzipping only supported in the browser');
@@ -15038,6 +15070,7 @@
15038
15070
  var Zip = /*#__PURE__*/Object.freeze({
15039
15071
  __proto__: null,
15040
15072
  isImportableZipPath: isImportableZipPath,
15073
+ listZipEntryNames: listZipEntryNames,
15041
15074
  unzipAsync: unzipAsync,
15042
15075
  unzipSync: unzipSync,
15043
15076
  zipAsync: zipAsync,
@@ -18618,14 +18651,15 @@
18618
18651
  function parseHexColor(str) {
18619
18652
  var hex = hexRxp.exec(str)[1];
18620
18653
  if (hex.length == 3 || hex.length == 4) {
18621
- hex = hex.split('').map(function(c) { return c + c; });
18654
+ // expand shorthand, e.g. #f90 -> #ffcc00
18655
+ hex = hex.split('').map(function(c) { return c + c; }).join('');
18622
18656
  }
18623
18657
  if (hex.length != 6 && hex.length != 8) return null;
18624
18658
  return {
18625
18659
  r: parseInt(hex.substr(0, 2), 16),
18626
18660
  g: parseInt(hex.substr(2, 2), 16),
18627
18661
  b: parseInt(hex.substr(4, 2), 16),
18628
- a: hex.length == 8 ? parseInt(hex.substr(7, 2), 16) / 255 : 1
18662
+ a: hex.length == 8 ? parseInt(hex.substr(6, 2), 16) / 255 : 1
18629
18663
  };
18630
18664
  }
18631
18665
 
@@ -25733,6 +25767,173 @@
25733
25767
  return text || text === 0; // accept numerical 0 as label text
25734
25768
  }
25735
25769
 
25770
+ var assignmentRxp = /^([a-z0-9_+-]+)=(?!=)(.*)$/i; // exclude ==
25771
+
25772
+ function splitShellTokens(str) {
25773
+ var BAREWORD = `([^'"\\s])+`;
25774
+ var DOUBLE_QUOTE = `"((\\\\"|[^"])*?)"`;
25775
+ var SINGLE_QUOTE = `'((\\\\'|[^'])*?)'`;
25776
+ var rxp = new RegExp('(' + BAREWORD + '|' + SINGLE_QUOTE + '|' + DOUBLE_QUOTE + ')*', 'g');
25777
+ var matches = str.match(rxp) || [];
25778
+ var chunks = matches.filter(function(chunk) {
25779
+ // single backslashes may be present in multiline commands pasted from a makefile, e.g.
25780
+ return !!chunk && chunk != '\\';
25781
+ }).map(utils.trimQuotes);
25782
+ return chunks;
25783
+ }
25784
+
25785
+ function parsePercent(o) {
25786
+ var str = String(o);
25787
+ var isPct = str.indexOf('%') > 0;
25788
+ var pct;
25789
+ if (isPct) {
25790
+ pct = Number(str.replace('%', '')) / 100;
25791
+ } else {
25792
+ pct = Number(str);
25793
+ }
25794
+ if (!(pct >= 0 && pct <= 1)) {
25795
+ stop$1(utils.format("Invalid percentage: %s", str));
25796
+ }
25797
+ return pct;
25798
+ }
25799
+
25800
+ function parseNumberList(token) {
25801
+ return token.split(',').map(parseFloat);
25802
+ }
25803
+
25804
+ // Split comma-delimited list, trim quotes from entire list and
25805
+ // individual members
25806
+ function parseStringList(token) {
25807
+ var delim = ',';
25808
+ var list = splitOptionList(token, delim);
25809
+ if (list.length == 1) {
25810
+ list = splitOptionList(list[0], delim);
25811
+ }
25812
+ return list;
25813
+ }
25814
+
25815
+ // Accept spaces and/or commas as delimiters
25816
+ function parseColorList(token) {
25817
+ var delim = ', ';
25818
+ // accept rgb(0 0 0) rgb(0,0,0) rgb(0, 0, 0)
25819
+ var token2 = token.replace(/[ ,] *(?=[^(]*\))/g, '~~~'); // kludge: protect rgba() functions from being split apart
25820
+ var list = splitOptionList(token2, delim);
25821
+ if (list.length == 1) {
25822
+ list = splitOptionList(list[0], delim);
25823
+ }
25824
+ list = list.map(function(str) {
25825
+ return str.replace(/~~~/g, ',');
25826
+ });
25827
+ return list;
25828
+ }
25829
+
25830
+ // Splits a comma-delimited list into items, for the list-valued options of the
25831
+ // -symbols command (radii=, values=, fills=). Unlike parseStringList(), empty
25832
+ // items are kept, so that a blank item in one list still lines up with its
25833
+ // counterpart in another. Commas nested inside parentheses, brackets, braces
25834
+ // or quotes are not delimiters, so items like rgba(0,0,0,0.5), Math.max(A,B)
25835
+ // and [R, R * 0.5] survive as single items.
25836
+ function splitListItems(str) {
25837
+ var items = [];
25838
+ var start = 0;
25839
+ var depth = 0;
25840
+ var quote = '';
25841
+ var c;
25842
+ for (var i=0, n=str.length; i<n; i++) {
25843
+ c = str[i];
25844
+ if (quote) {
25845
+ if (c === quote) quote = '';
25846
+ } else if (c === '"' || c === "'") {
25847
+ quote = c;
25848
+ } else if (c === '(' || c === '[' || c === '{') {
25849
+ depth++;
25850
+ } else if (c === ')' || c === ']' || c === '}') {
25851
+ depth--;
25852
+ } else if (c === ',' && depth <= 0) {
25853
+ items.push(str.slice(start, i));
25854
+ start = i + 1;
25855
+ }
25856
+ }
25857
+ items.push(str.slice(start));
25858
+ return items.map(function(item) {
25859
+ return utils.trimQuotes(item.trim());
25860
+ });
25861
+ }
25862
+
25863
+ function cleanArgv(argv) {
25864
+ // Note: original trim caused some quoted spaces to be removed
25865
+ // (e.g. bash shell seems to convert [delimiter=" "] to [delimiter= ],
25866
+ // which then got trimmed to [delimiter=] below)
25867
+ //// argv = argv.map(function(s) {return s.trim();}); // trim whitespace
25868
+
25869
+ // Updated: don't trim space from tokens like [delimeter= ]
25870
+ argv = argv.map(function(s) {
25871
+ if (!/= $/.test(s)) {
25872
+ s = utils.rtrim(s);
25873
+ }
25874
+ s = utils.ltrim(s);
25875
+ return s;
25876
+ });
25877
+ argv = argv.filter(function(s) {return s !== '';}); // remove empty tokens
25878
+ // Note: removing trimQuotes() call... now, strings like 'name="Meg"' will no longer
25879
+ // be parsed the same way as name=Meg and name="Meg"
25880
+ //// argv = argv.map(utils.trimQuotes); // remove one level of single or dbl quotes
25881
+ return argv;
25882
+ }
25883
+
25884
+ function splitOptionList(str, delimChars) {
25885
+ var BAREWORD = '([^' + delimChars + '\'"][^' + delimChars + ']*)'; // TODO: make safer
25886
+ var DOUBLE_QUOTE = '"((\\\\"|[^"])*?)"';
25887
+ var SINGLE_QUOTE = '\'((\\\\\'|[^\'])*?)\'';
25888
+ var rxp = new RegExp('^(' + BAREWORD + '|' + SINGLE_QUOTE + '|' + DOUBLE_QUOTE + ')([' + delimChars + ']+|$)');
25889
+ var chunks = [];
25890
+ var match;
25891
+ while ((match = rxp.exec(str)) !== null) {
25892
+ chunks.push(match[1]);
25893
+ str = str.substr(match[0].length);
25894
+ }
25895
+ return chunks.filter(function(chunk) {
25896
+ return !!chunk && chunk != '\\';
25897
+ }).map(utils.trimQuotes);
25898
+ }
25899
+
25900
+ // Prepare a value to be used as an option value.
25901
+ // Places quotes around strings containing spaces.
25902
+ // e.g. converts Layer 1 -> "Layer 1"
25903
+ // for use in contexts like: name="Layer 1"
25904
+ function formatOptionValue(val) {
25905
+ val = String(val);
25906
+ if (val.indexOf(' ') > -1) {
25907
+ val = JSON.stringify(val); // quote ids with spaces
25908
+ }
25909
+ return val;
25910
+ }
25911
+
25912
+ function isAssignment(token) {
25913
+ return assignmentRxp.test(token);
25914
+ }
25915
+
25916
+ function splitAssignment(token) {
25917
+ var match = assignmentRxp.exec(token),
25918
+ name = match[1],
25919
+ val = utils.trimQuotes(match[2]);
25920
+ return [name, val];
25921
+ }
25922
+
25923
+ var OptionParsingUtils = /*#__PURE__*/Object.freeze({
25924
+ __proto__: null,
25925
+ cleanArgv: cleanArgv,
25926
+ formatOptionValue: formatOptionValue,
25927
+ isAssignment: isAssignment,
25928
+ parseColorList: parseColorList,
25929
+ parseNumberList: parseNumberList,
25930
+ parsePercent: parsePercent,
25931
+ parseStringList: parseStringList,
25932
+ splitAssignment: splitAssignment,
25933
+ splitListItems: splitListItems,
25934
+ splitShellTokens: splitShellTokens
25935
+ });
25936
+
25736
25937
  /* example patterns
25737
25938
  hatches 1px black 1px red 1px white
25738
25939
  1px black 1px red 1px white // same as above (hatches is default)
@@ -26024,7 +26225,10 @@
26024
26225
  length: 'number', // e.g. arrow length
26025
26226
  rotation: 'number',
26026
26227
  radius: 'number',
26027
- radii: null, // string, parsed by function
26228
+ radii: null, // (ring) list, see symbolListProperties
26229
+ hole: 'number', // (pie) radius of the hole in a donut
26230
+ values: null, // (pie) list, see symbolListProperties
26231
+ fills: null, // (pie) list, see symbolListProperties
26028
26232
  flipped: 'boolean',
26029
26233
  rotated: 'boolean',
26030
26234
  direction: 'number',
@@ -26043,6 +26247,18 @@
26043
26247
  effect: null // e.g. "fade"
26044
26248
  }, stylePropertyTypes);
26045
26249
 
26250
+ // Options that take a list of values -- one for each part of a multi-part
26251
+ // symbol -- instead of a single value. Each item in the list is resolved
26252
+ // separately, so an item can be a literal value, a field name or an
26253
+ // expression. The type hint for fills= is null on purpose: an item that isn't
26254
+ // a recognizable color is passed along as a literal string, so that a symbol
26255
+ // can leave that part unfilled instead of the command failing.
26256
+ var symbolListProperties = {
26257
+ radii: 'number',
26258
+ values: 'number',
26259
+ fills: null
26260
+ };
26261
+
26046
26262
  var commonProperties = 'css,class,opacity,stroke,stroke-width,stroke-dasharray,stroke-opacity,fill-opacity,vector-effect'.split(',');
26047
26263
 
26048
26264
  var propertiesBySymbolType = {
@@ -26118,7 +26334,9 @@
26118
26334
  return;
26119
26335
  }
26120
26336
  var val = opts[optName];
26121
- functions[svgName] = getSymbolPropertyAccessor(val, svgName, lyr);
26337
+ functions[svgName] = svgName in symbolListProperties ?
26338
+ getSymbolListAccessor(val, svgName, lyr) :
26339
+ getSymbolPropertyAccessor(val, svgName, lyr);
26122
26340
  properties.push(svgName);
26123
26341
  });
26124
26342
 
@@ -26145,8 +26363,35 @@
26145
26363
  }
26146
26364
 
26147
26365
  function getSymbolPropertyAccessor(val, svgName, lyr) {
26366
+ return getPropertyAccessor(val, symbolPropertyTypes[svgName], lyr, svgName);
26367
+ }
26368
+
26369
+ // Returns a function that maps a feature id to an array of values, for options
26370
+ // like values= and fills= that describe the parts of a multi-part symbol.
26371
+ // The option may be given as an array or as a comma-separated list.
26372
+ function getSymbolListAccessor(val, svgName, lyr) {
26373
+ var typeHint = symbolListProperties[svgName];
26374
+ var items = Array.isArray(val) ? val : splitListItems(String(val));
26375
+ var accessors = items.map(function(item) {
26376
+ return getPropertyAccessor(item, typeHint, lyr, svgName);
26377
+ });
26378
+ return function(id) {
26379
+ return accessors.map(function(accessor) {
26380
+ return accessor(id);
26381
+ });
26382
+ };
26383
+ }
26384
+ // Returns a function that maps a feature id to a property value. The value may
26385
+ // be given as a literal, as the name of a data field, or as a JS expression
26386
+ // evaluated against each feature.
26387
+ // typeHint: a type understood by parseSvgLiteralValue(), or null to accept any
26388
+ // string as a literal value
26389
+ // name: used in error messages
26390
+ // Callers outside the SVG property system pass a type hint directly, rather
26391
+ // than registering an option name in symbolPropertyTypes -- that index also
26392
+ // decides which options the -symbols command treats as symbol properties.
26393
+ function getPropertyAccessor(val, typeHint, lyr, name) {
26148
26394
  var strVal = String(val).trim();
26149
- var typeHint = symbolPropertyTypes[svgName];
26150
26395
  var fields = lyr.data ? lyr.data.getFields() : [];
26151
26396
  var literalVal = null;
26152
26397
  var accessor;
@@ -26164,7 +26409,7 @@
26164
26409
  }
26165
26410
  if (accessor) return accessor;
26166
26411
  if (literalVal !== null) return function(id) {return literalVal;};
26167
- stop$1('Unexpected value for', svgName + ':', strVal);
26412
+ stop$1('Unexpected value for', name + ':', strVal);
26168
26413
  }
26169
26414
 
26170
26415
  function parseStyleExpression(strVal, lyr) {
@@ -26270,7 +26515,9 @@
26270
26515
  applyStyleAttributes: applyStyleAttributes,
26271
26516
  findStylePropertiesBySymbolGeom: findStylePropertiesBySymbolGeom,
26272
26517
  getLabelPositionStyle: getLabelPositionStyle,
26518
+ getPropertyAccessor: getPropertyAccessor,
26273
26519
  getSymbolDataAccessor: getSymbolDataAccessor,
26520
+ getSymbolListAccessor: getSymbolListAccessor,
26274
26521
  getSymbolPropertyAccessor: getSymbolPropertyAccessor,
26275
26522
  isSupportedSvgStyleProperty: isSupportedSvgStyleProperty,
26276
26523
  isSvgClassName: isSvgClassName,
@@ -37954,139 +38201,6 @@ ${svg}
37954
38201
  }
37955
38202
  }
37956
38203
 
37957
- var assignmentRxp = /^([a-z0-9_+-]+)=(?!=)(.*)$/i; // exclude ==
37958
-
37959
- function splitShellTokens(str) {
37960
- var BAREWORD = `([^'"\\s])+`;
37961
- var DOUBLE_QUOTE = `"((\\\\"|[^"])*?)"`;
37962
- var SINGLE_QUOTE = `'((\\\\'|[^'])*?)'`;
37963
- var rxp = new RegExp('(' + BAREWORD + '|' + SINGLE_QUOTE + '|' + DOUBLE_QUOTE + ')*', 'g');
37964
- var matches = str.match(rxp) || [];
37965
- var chunks = matches.filter(function(chunk) {
37966
- // single backslashes may be present in multiline commands pasted from a makefile, e.g.
37967
- return !!chunk && chunk != '\\';
37968
- }).map(utils.trimQuotes);
37969
- return chunks;
37970
- }
37971
-
37972
- function parsePercent(o) {
37973
- var str = String(o);
37974
- var isPct = str.indexOf('%') > 0;
37975
- var pct;
37976
- if (isPct) {
37977
- pct = Number(str.replace('%', '')) / 100;
37978
- } else {
37979
- pct = Number(str);
37980
- }
37981
- if (!(pct >= 0 && pct <= 1)) {
37982
- stop$1(utils.format("Invalid percentage: %s", str));
37983
- }
37984
- return pct;
37985
- }
37986
-
37987
- function parseNumberList(token) {
37988
- return token.split(',').map(parseFloat);
37989
- }
37990
-
37991
- // Split comma-delimited list, trim quotes from entire list and
37992
- // individual members
37993
- function parseStringList(token) {
37994
- var delim = ',';
37995
- var list = splitOptionList(token, delim);
37996
- if (list.length == 1) {
37997
- list = splitOptionList(list[0], delim);
37998
- }
37999
- return list;
38000
- }
38001
-
38002
- // Accept spaces and/or commas as delimiters
38003
- function parseColorList(token) {
38004
- var delim = ', ';
38005
- // accept rgb(0 0 0) rgb(0,0,0) rgb(0, 0, 0)
38006
- var token2 = token.replace(/[ ,] *(?=[^(]*\))/g, '~~~'); // kludge: protect rgba() functions from being split apart
38007
- var list = splitOptionList(token2, delim);
38008
- if (list.length == 1) {
38009
- list = splitOptionList(list[0], delim);
38010
- }
38011
- list = list.map(function(str) {
38012
- return str.replace(/~~~/g, ',');
38013
- });
38014
- return list;
38015
- }
38016
-
38017
- function cleanArgv(argv) {
38018
- // Note: original trim caused some quoted spaces to be removed
38019
- // (e.g. bash shell seems to convert [delimiter=" "] to [delimiter= ],
38020
- // which then got trimmed to [delimiter=] below)
38021
- //// argv = argv.map(function(s) {return s.trim();}); // trim whitespace
38022
-
38023
- // Updated: don't trim space from tokens like [delimeter= ]
38024
- argv = argv.map(function(s) {
38025
- if (!/= $/.test(s)) {
38026
- s = utils.rtrim(s);
38027
- }
38028
- s = utils.ltrim(s);
38029
- return s;
38030
- });
38031
- argv = argv.filter(function(s) {return s !== '';}); // remove empty tokens
38032
- // Note: removing trimQuotes() call... now, strings like 'name="Meg"' will no longer
38033
- // be parsed the same way as name=Meg and name="Meg"
38034
- //// argv = argv.map(utils.trimQuotes); // remove one level of single or dbl quotes
38035
- return argv;
38036
- }
38037
-
38038
- function splitOptionList(str, delimChars) {
38039
- var BAREWORD = '([^' + delimChars + '\'"][^' + delimChars + ']*)'; // TODO: make safer
38040
- var DOUBLE_QUOTE = '"((\\\\"|[^"])*?)"';
38041
- var SINGLE_QUOTE = '\'((\\\\\'|[^\'])*?)\'';
38042
- var rxp = new RegExp('^(' + BAREWORD + '|' + SINGLE_QUOTE + '|' + DOUBLE_QUOTE + ')([' + delimChars + ']+|$)');
38043
- var chunks = [];
38044
- var match;
38045
- while ((match = rxp.exec(str)) !== null) {
38046
- chunks.push(match[1]);
38047
- str = str.substr(match[0].length);
38048
- }
38049
- return chunks.filter(function(chunk) {
38050
- return !!chunk && chunk != '\\';
38051
- }).map(utils.trimQuotes);
38052
- }
38053
-
38054
- // Prepare a value to be used as an option value.
38055
- // Places quotes around strings containing spaces.
38056
- // e.g. converts Layer 1 -> "Layer 1"
38057
- // for use in contexts like: name="Layer 1"
38058
- function formatOptionValue(val) {
38059
- val = String(val);
38060
- if (val.indexOf(' ') > -1) {
38061
- val = JSON.stringify(val); // quote ids with spaces
38062
- }
38063
- return val;
38064
- }
38065
-
38066
- function isAssignment(token) {
38067
- return assignmentRxp.test(token);
38068
- }
38069
-
38070
- function splitAssignment(token) {
38071
- var match = assignmentRxp.exec(token),
38072
- name = match[1],
38073
- val = utils.trimQuotes(match[2]);
38074
- return [name, val];
38075
- }
38076
-
38077
- var OptionParsingUtils = /*#__PURE__*/Object.freeze({
38078
- __proto__: null,
38079
- cleanArgv: cleanArgv,
38080
- formatOptionValue: formatOptionValue,
38081
- isAssignment: isAssignment,
38082
- parseColorList: parseColorList,
38083
- parseNumberList: parseNumberList,
38084
- parsePercent: parsePercent,
38085
- parseStringList: parseStringList,
38086
- splitAssignment: splitAssignment,
38087
- splitShellTokens: splitShellTokens
38088
- });
38089
-
38090
38204
  function CommandParser() {
38091
38205
  var commandRxp = /^--?([a-z][\w-]*)$/i,
38092
38206
  invalidCommandRxp = /^--?[a-z][\w-]*[=]/i, // e.g. -target=A // could be more general
@@ -38266,6 +38380,7 @@ ${svg}
38266
38380
  // Try to read an option for command @cmdDef from @argv
38267
38381
  function readOption(cmd, argv, cmdDef) {
38268
38382
  var token = argv.shift(),
38383
+ assignedValue = null,
38269
38384
  optName, optDef, parts;
38270
38385
 
38271
38386
  if (isAssignment(token)) {
@@ -38275,7 +38390,10 @@ ${svg}
38275
38390
  if (!optDef) ; else if (optDef.type == 'flag' || optDef.assign_to) {
38276
38391
  stop$1("-" + cmdDef.name + " " + parts[0] + " option doesn't take a value");
38277
38392
  } else {
38278
- argv.unshift(parts[1]);
38393
+ // Keep the value out of argv: it was written as part of the token,
38394
+ // so it is a value even when it looks like a command name
38395
+ // (e.g. hole=-size, dx=-OFFSET).
38396
+ assignedValue = parts[1];
38279
38397
  }
38280
38398
  } else {
38281
38399
  // try to parse as a flag option,
@@ -38300,6 +38418,8 @@ ${svg}
38300
38418
  cmd.options[optDef.assign_to] = optDef.name;
38301
38419
  } else if (optDef.type == 'flag') {
38302
38420
  cmd.options[optName] = true;
38421
+ } else if (assignedValue !== null) {
38422
+ cmd.options[optName] = parseOptionValue(assignedValue, optDef);
38303
38423
  } else {
38304
38424
  cmd.options[optName] = readOptionValue(argv, optDef);
38305
38425
  }
@@ -41003,10 +41123,41 @@ ${svg}
41003
41123
  })
41004
41124
  .option('target', targetOpt);
41005
41125
 
41126
+ parser.command('repel')
41127
+ .describe('move overlapping point symbols apart')
41128
+ .option('width', {
41129
+ describe: 'display width of the layer in pixels (required if no frame)',
41130
+ type: 'number'
41131
+ })
41132
+ .option('max-shift', {
41133
+ describe: 'max displacement in pixels; may vary by symbol (default is 20)'
41134
+ })
41135
+ .option('padding', {
41136
+ describe: 'pixels of clearance around each symbol; may vary (default is 0)'
41137
+ })
41138
+ .option('margin', {
41139
+ describe: 'pixels of clearance between two symbols; may vary (default is 0)'
41140
+ })
41141
+ .option('ticks', {
41142
+ describe: 'number of solver passes (default is 100)',
41143
+ type: 'number'
41144
+ })
41145
+ .option('strength', {
41146
+ describe: 'portion of each overlap resolved per pass, 0-1 (default is 0.4)',
41147
+ type: 'number'
41148
+ })
41149
+ .option('radius', {
41150
+ describe: 'field or expression giving symbol radius in pixels',
41151
+ })
41152
+ .option('polygons', {
41153
+ describe: 'layer or file of polygons that symbols must stay inside'
41154
+ })
41155
+ .option('target', targetOpt);
41156
+
41006
41157
  parser.command('symbols')
41007
41158
  .describe('symbolize points as arrows, circles, stars, polygons, etc.')
41008
41159
  .option('type', {
41009
- describe: 'types: arrow, circle, square, star, polygon, ring'
41160
+ describe: 'types: arrow, circle, square, star, polygon, ring, pie'
41010
41161
  })
41011
41162
  .option('stroke', {})
41012
41163
  .option('stroke-width', {})
@@ -41065,6 +41216,15 @@ ${svg}
41065
41216
  .option('radii', {
41066
41217
  describe: '(ring) comma-sep. list of concentric radii, ascending order'
41067
41218
  })
41219
+ .option('hole', {
41220
+ describe: '(pie) radius of the hole in a donut symbol'
41221
+ })
41222
+ .option('values', {
41223
+ describe: '(pie) comma-sep. list of wedge values (numbers or fields)'
41224
+ })
41225
+ .option('fills', {
41226
+ describe: '(pie) comma-sep. list of wedge fill colors'
41227
+ })
41068
41228
  .option('arrow-style', {
41069
41229
  describe: '(arrow) options: stick, standard (default is standard)'
41070
41230
  })
@@ -44316,9 +44476,10 @@ ${svg}
44316
44476
  ({gpkg, tmpPath} = await openGeoPackage(input, geopackage));
44317
44477
  var availableLayers;
44318
44478
  var filterApplied;
44479
+ var unreadableLayers;
44319
44480
  try {
44320
44481
  try {
44321
- ({datasets, availableLayers, filterApplied} = readFeatureTableDatasets(gpkg, opts));
44482
+ ({datasets, availableLayers, filterApplied, unreadableLayers} = readFeatureTableDatasets(gpkg, opts));
44322
44483
  } catch (e) {
44323
44484
  if (!runningInBrowser() && isLocalCsProjError(e)) {
44324
44485
  gpkg.close();
@@ -44331,7 +44492,7 @@ ${svg}
44331
44492
  throw e;
44332
44493
  }
44333
44494
  gpkg = await geopackage.GeoPackageAPI.open(tmpPath);
44334
- ({datasets, availableLayers, filterApplied} = readFeatureTableDatasets(gpkg, opts));
44495
+ ({datasets, availableLayers, filterApplied, unreadableLayers} = readFeatureTableDatasets(gpkg, opts));
44335
44496
  } else {
44336
44497
  throw e;
44337
44498
  }
@@ -44346,6 +44507,7 @@ ${svg}
44346
44507
  layers: [{name: '', data: null}],
44347
44508
  info: {
44348
44509
  _gpkg_available_layers: availableLayers,
44510
+ _gpkg_unreadable_layers: unreadableLayers,
44349
44511
  _gpkg_placeholder: !!filterApplied
44350
44512
  }
44351
44513
  };
@@ -44357,6 +44519,7 @@ ${svg}
44357
44519
  mergedArr.forEach(function(ds) {
44358
44520
  ds.info = ds.info || {};
44359
44521
  ds.info._gpkg_available_layers = availableLayers;
44522
+ ds.info._gpkg_unreadable_layers = unreadableLayers;
44360
44523
  });
44361
44524
  return merged;
44362
44525
  }
@@ -44482,10 +44645,38 @@ ${svg}
44482
44645
  var availableLayers = gpkg.getFeatureTables() || [];
44483
44646
  var tables = filterGeoPackageTables(availableLayers, selected);
44484
44647
  var filterApplied = Array.isArray(selected) && selected.length > 0;
44485
- var datasets = tables.map(function(table) {
44486
- return readFeatureTable(gpkg, table, opts);
44648
+ var unreadableLayers = [];
44649
+ var datasets = [];
44650
+ tables.forEach(function(table) {
44651
+ try {
44652
+ datasets.push(readFeatureTable(gpkg, table, opts));
44653
+ } catch (e) {
44654
+ // The caller recovers from a CRS parsing failure by rewriting the file's
44655
+ // CRS metadata and re-reading, so that error must not be swallowed here.
44656
+ if (isLocalCsProjError(e)) throw e;
44657
+ unreadableLayers.push(table);
44658
+ warn(formatUnreadableTableMessage(table, e));
44659
+ }
44487
44660
  });
44488
- return {datasets: datasets, availableLayers: availableLayers, filterApplied: filterApplied};
44661
+ return {
44662
+ datasets: datasets,
44663
+ availableLayers: availableLayers,
44664
+ filterApplied: filterApplied,
44665
+ unreadableLayers: unreadableLayers
44666
+ };
44667
+ }
44668
+
44669
+ // A GeoPackage can advertise feature tables that aren't readable. USGS National
44670
+ // Map vector products, for instance, list a CLIPPOLY table in gpkg_contents and
44671
+ // gpkg_geometry_columns after dropping the table itself from the file. GDAL
44672
+ // skips such layers with a warning; do the same rather than failing the whole
44673
+ // import over one bad table.
44674
+ function formatUnreadableTableMessage(table, err) {
44675
+ var msg = err && err.message ? String(err.message) : '';
44676
+ var reason = /^Table does not exist:/.test(msg) ?
44677
+ 'the table is listed in the GeoPackage metadata but is missing from the file' :
44678
+ msg || 'unknown error';
44679
+ return 'Skipping GeoPackage layer "' + table + '": ' + reason;
44489
44680
  }
44490
44681
 
44491
44682
  function getSelectedGeoPackageLayers(opts) {
@@ -46458,12 +46649,12 @@ ${svg}
46458
46649
  var coords = path.coords;
46459
46650
  if (coords.length < 2) return;
46460
46651
  if (forcePolylinePaths) {
46461
- lines.push(path.closed || pointsEqual$1(coords[0], coords[coords.length - 1]) ? closeRing(coords) : coords);
46652
+ lines.push(path.closed || pointsEqual$1(coords[0], coords[coords.length - 1]) ? closeRing$1(coords) : coords);
46462
46653
  return;
46463
46654
  }
46464
46655
  if (path.closed || pointsEqual$1(coords[0], coords[coords.length - 1])) {
46465
46656
  if (coords.length >= 3) {
46466
- rings.push(closeRing(coords));
46657
+ rings.push(closeRing$1(coords));
46467
46658
  }
46468
46659
  } else {
46469
46660
  lines.push(coords);
@@ -46767,7 +46958,7 @@ ${svg}
46767
46958
  return /translate\(/i.test(String(str || ''));
46768
46959
  }
46769
46960
 
46770
- function closeRing(coords) {
46961
+ function closeRing$1(coords) {
46771
46962
  var ring = coords.map(function(p) { return [p[0], p[1]]; });
46772
46963
  if (!pointsEqual$1(ring[0], ring[ring.length - 1])) {
46773
46964
  ring.push([ring[0][0], ring[0][1]]);
@@ -47244,15 +47435,12 @@ ${svg}
47244
47435
  var files = opts.files || [];
47245
47436
  var dataset, datasets, target;
47246
47437
 
47438
+ // copy opts, so parameters can be modified within this command
47439
+ opts = Object.assign({}, opts);
47440
+ opts.input = Object.assign({}, opts.input); // make sure we have a cache
47441
+
47247
47442
  if (opts.stdin) {
47248
- datasets = await importDatasetsFromFile('/dev/stdin', opts);
47249
- catalog.addDatasets(datasets);
47250
- if (datasets.length > 1) {
47251
- catalog.setDefaultTargets(datasets.map(function(ds) {
47252
- return {dataset: ds, layers: ds.layers};
47253
- }));
47254
- }
47255
- return normalizeImportedTarget(datasets);
47443
+ files = [readStdinAsFile(opts.input)];
47256
47444
  }
47257
47445
 
47258
47446
  if (files.length > 0 === false) {
@@ -47261,10 +47449,6 @@ ${svg}
47261
47449
 
47262
47450
  verbose("Importing: " + files.join(' '));
47263
47451
 
47264
- // copy opts, so parameters can be modified within this command
47265
- opts = Object.assign({}, opts);
47266
- opts.input = Object.assign({}, opts.input); // make sure we have a cache
47267
-
47268
47452
  convertDataObjects(files, opts.input);
47269
47453
 
47270
47454
  files = expandFiles(files, opts.input);
@@ -47345,6 +47529,41 @@ ${svg}
47345
47529
  return obj.target;
47346
47530
  }
47347
47531
 
47532
+ // Read the stdin stream into @cache and return the filename to import it as.
47533
+ // A zipped dataset is given a .zip or .kmz name, so that it is unpacked by
47534
+ // expandFiles() like an archive passed as a file argument. Stdin carries no
47535
+ // filename to identify its contents, so an archive is detected from the ZIP
47536
+ // magic number, and a KMZ is told from an ordinary zip by its doc.kml entry.
47537
+ function readStdinAsFile(cache) {
47538
+ var path = '/dev/stdin';
47539
+ var content = cli.readFile(path, null, cache);
47540
+ var name = path;
47541
+ if (contentLooksZipped(content)) {
47542
+ name = zipContainsDocKml(content) ? 'stdin.kmz' : 'stdin.zip';
47543
+ }
47544
+ cache[name] = content;
47545
+ return name;
47546
+ }
47547
+
47548
+ // Detect the ZIP local file header signature ("PK\x03\x04")
47549
+ function contentLooksZipped(content) {
47550
+ var bytes = content instanceof ArrayBuffer ? new Uint8Array(content) : content;
47551
+ if (utils.isString(bytes) || !bytes || bytes.length < 4) return false;
47552
+ return bytes[0] == 0x50 && bytes[1] == 0x4b && bytes[2] == 0x03 && bytes[3] == 0x04;
47553
+ }
47554
+
47555
+ function zipContainsDocKml(content) {
47556
+ var names;
47557
+ try {
47558
+ names = listZipEntryNames(content);
47559
+ } catch(e) {
47560
+ return false; // let the unzipping step report a malformed archive
47561
+ }
47562
+ return names.some(function(name) {
47563
+ return parseLocalPath(name).filename.toLowerCase() == 'doc.kml';
47564
+ });
47565
+ }
47566
+
47348
47567
  function expandFiles(files, cache) {
47349
47568
  var files2 = [];
47350
47569
  files.forEach(function(file) {
@@ -47476,6 +47695,7 @@ ${svg}
47476
47695
  content = cli.readFile(path, encoding || 'utf-8', cache);
47477
47696
  fileType = guessInputContentType(content);
47478
47697
  if (fileType == 'text' && content.indexOf('\ufffd') > -1) {
47698
+ // invalidate string data that contains the 'replacement character'
47479
47699
  fileType = null;
47480
47700
  }
47481
47701
  }
@@ -47484,7 +47704,7 @@ ${svg}
47484
47704
  stop$1(getUnsupportedFileMessage(path));
47485
47705
  }
47486
47706
  input[fileType] = {filename: path, content: content};
47487
- content = null;
47707
+ content = null; // for g.c.
47488
47708
  if (fileType == 'shp' || fileType == 'dbf') {
47489
47709
  readShapefileAuxFiles(path, input, cache);
47490
47710
  } else if (isRasterImageInputType(fileType)) {
@@ -47529,12 +47749,14 @@ ${svg}
47529
47749
  function validateAndCleanGpkgSelection(datasets, opts) {
47530
47750
  var availableSet = new Set();
47531
47751
  var importedSet = new Set();
47752
+ var unreadableSet = new Set();
47532
47753
  var sawGpkg = false;
47533
47754
  datasets.forEach(function(ds) {
47534
47755
  var info = ds && ds.info;
47535
47756
  if (!info || !Array.isArray(info._gpkg_available_layers)) return;
47536
47757
  sawGpkg = true;
47537
47758
  info._gpkg_available_layers.forEach(function(name) { availableSet.add(name); });
47759
+ (info._gpkg_unreadable_layers || []).forEach(function(name) { unreadableSet.add(name); });
47538
47760
  if (!info._gpkg_placeholder) {
47539
47761
  (ds.layers || []).forEach(function(lyr) {
47540
47762
  if (lyr && lyr.name) importedSet.add(lyr.name);
@@ -47542,9 +47764,17 @@ ${svg}
47542
47764
  }
47543
47765
  });
47544
47766
  if (sawGpkg && Array.isArray(opts.layers) && opts.layers.length > 0) {
47545
- var missing = opts.layers.filter(function(name) {
47767
+ var absent = opts.layers.filter(function(name) {
47546
47768
  return !importedSet.has(name);
47547
47769
  });
47770
+ // A layer that the file advertises but that can't be parsed is skipped with
47771
+ // a warning during import; report it separately here, because saying it is
47772
+ // missing while also listing it as an existing layer is contradictory.
47773
+ var unreadable = absent.filter(function(name) { return unreadableSet.has(name); });
47774
+ var missing = absent.filter(function(name) { return !unreadableSet.has(name); });
47775
+ if (unreadable.length > 0) {
47776
+ stop$1('Unable to read GeoPackage layer(s): ' + unreadable.join(', '));
47777
+ }
47548
47778
  if (missing.length > 0) {
47549
47779
  stop$1(
47550
47780
  'Missing GeoPackage layer(s): ' + missing.join(', ') + '\n' +
@@ -47558,6 +47788,7 @@ ${svg}
47558
47788
  cleaned.forEach(function(ds) {
47559
47789
  if (ds && ds.info) {
47560
47790
  delete ds.info._gpkg_available_layers;
47791
+ delete ds.info._gpkg_unreadable_layers;
47561
47792
  delete ds.info._gpkg_placeholder;
47562
47793
  }
47563
47794
  });
@@ -48368,7 +48599,7 @@ ${svg}
48368
48599
  const resulterrbound = (3 + 8 * epsilon) * epsilon;
48369
48600
 
48370
48601
  // fast_expansion_sum_zeroelim routine from oritinal code
48371
- function sum(elen, e, flen, f, h) {
48602
+ function sum$1(elen, e, flen, f, h) {
48372
48603
  let Q, Qnew, hh, bvirt;
48373
48604
  let enow = e[0];
48374
48605
  let fnow = f[0];
@@ -48551,7 +48782,7 @@ ${svg}
48551
48782
  bvirt = u3 - _j;
48552
48783
  u[2] = _j - (u3 - bvirt) + (_i - bvirt);
48553
48784
  u[3] = u3;
48554
- const C1len = sum(4, B$2, 4, u, C1);
48785
+ const C1len = sum$1(4, B$2, 4, u, C1);
48555
48786
 
48556
48787
  s1 = acx * bcytail;
48557
48788
  c = splitter * acx;
@@ -48582,7 +48813,7 @@ ${svg}
48582
48813
  bvirt = u3 - _j;
48583
48814
  u[2] = _j - (u3 - bvirt) + (_i - bvirt);
48584
48815
  u[3] = u3;
48585
- const C2len = sum(C1len, C1, 4, u, C2);
48816
+ const C2len = sum$1(C1len, C1, 4, u, C2);
48586
48817
 
48587
48818
  s1 = acxtail * bcytail;
48588
48819
  c = splitter * acxtail;
@@ -48613,7 +48844,7 @@ ${svg}
48613
48844
  bvirt = u3 - _j;
48614
48845
  u[2] = _j - (u3 - bvirt) + (_i - bvirt);
48615
48846
  u[3] = u3;
48616
- const Dlen = sum(C2len, C2, 4, u, D$1);
48847
+ const Dlen = sum$1(C2len, C2, 4, u, D$1);
48617
48848
 
48618
48849
  return D$1[Dlen - 1];
48619
48850
  }
@@ -74984,7 +75215,7 @@ ${svg}
74984
75215
 
74985
75216
  var datasets = [targetDataset];
74986
75217
  var outputLayers = targetLayers.map(function(pointLyr) {
74987
- if (countMultiPartFeatures(pointLyr) > 0) {
75218
+ if (countMultiPartFeatures(pointLyr.shapes) > 0) {
74988
75219
  stop$1('This command requires single points');
74989
75220
  }
74990
75221
  var dataset = getPolygonDataset(pointLyr, bbox, opts);
@@ -75300,6 +75531,761 @@ ${svg}
75300
75531
  }, {});
75301
75532
  }
75302
75533
 
75534
+ // Keeps each symbol's center inside the polygon that contained it to begin
75535
+ // with, so that a symbol representing one area can't drift into a neighboring
75536
+ // one. Used by -repel via its polygons= option.
75537
+ //
75538
+ // Nodes are laid out in pixel space, at a scale of @pixelsPerUnit pixels per
75539
+ // unit of the polygons' coordinate system. That transform is a pure scale (see
75540
+ // how -repel builds its nodes), so dividing a node position by it gives a
75541
+ // position that can be tested against the polygons directly.
75542
+
75543
+ // Bisection steps used to find how far a symbol can travel before it leaves its
75544
+ // polygon. Six steps locate the crossing to within 1/64 of the attempted move;
75545
+ // finer searches were measured and change the result by a fraction of a pixel.
75546
+ var BISECT_STEPS = 6;
75547
+
75548
+ // Assigns each node the id of the polygon containing its anchor, or -1 if the
75549
+ // anchor isn't inside any of them. Returns the number of unassigned nodes,
75550
+ // which are left free to move.
75551
+ function assignContainingPolygons(nodes, lyr, arcs, pixelsPerUnit) {
75552
+ var index = new PathIndex(lyr.shapes, arcs);
75553
+ var unassigned = 0;
75554
+ nodes.forEach(function(node) {
75555
+ node.polygonId = index.findEnclosingShape([node.x0 / pixelsPerUnit, node.y0 / pixelsPerUnit]);
75556
+ node.insideX = node.x0;
75557
+ node.insideY = node.y0;
75558
+ if (node.polygonId == -1) unassigned++;
75559
+ });
75560
+ return unassigned;
75561
+ }
75562
+
75563
+ // Returns a function that the solver runs after each pass: any symbol that has
75564
+ // left its polygon is pulled back along the path it just travelled, to the
75565
+ // furthest point that is still inside.
75566
+ //
75567
+ // This can't violate the max-shift limit. The position it assigns always lies
75568
+ // between two positions the node has already held, both of which the solver
75569
+ // clamped to within max-shift of the anchor, and a disk is convex.
75570
+ function getContainmentConstraint(lyr, arcs, pixelsPerUnit) {
75571
+ var shapes = lyr.shapes;
75572
+
75573
+ function isInside(node, x, y) {
75574
+ return testPointInPolygon(x / pixelsPerUnit, y / pixelsPerUnit,
75575
+ shapes[node.polygonId], arcs);
75576
+ }
75577
+
75578
+ return function(nodes) {
75579
+ var node, lo, hi, mid, x, y, i, j;
75580
+ for (i=0; i<nodes.length; i++) {
75581
+ node = nodes[i];
75582
+ if (node.polygonId == -1) continue;
75583
+ // A node that hasn't moved since it was last checked is still inside,
75584
+ // so most symbols in a sparse layout cost nothing after the early passes
75585
+ if (node.x === node.insideX && node.y === node.insideY) continue;
75586
+ if (isInside(node, node.x, node.y)) {
75587
+ node.insideX = node.x;
75588
+ node.insideY = node.y;
75589
+ continue;
75590
+ }
75591
+ lo = 0; // known inside
75592
+ hi = 1; // known outside
75593
+ for (j=0; j<BISECT_STEPS; j++) {
75594
+ mid = (lo + hi) / 2;
75595
+ x = node.insideX + (node.x - node.insideX) * mid;
75596
+ y = node.insideY + (node.y - node.insideY) * mid;
75597
+ if (isInside(node, x, y)) lo = mid; else hi = mid;
75598
+ }
75599
+ node.x = node.insideX + (node.x - node.insideX) * lo;
75600
+ node.y = node.insideY + (node.y - node.insideY) * lo;
75601
+ node.insideX = node.x;
75602
+ node.insideY = node.y;
75603
+ }
75604
+ };
75605
+ }
75606
+
75607
+ var roundCoord$1 = getRoundingFunction(0.01);
75608
+
75609
+ function getSymbolFillColor(d) {
75610
+ return d.fill || 'magenta';
75611
+ }
75612
+
75613
+ function getSymbolStrokeColor(d) {
75614
+ return d.stroke || d.fill || 'magenta';
75615
+ }
75616
+
75617
+ function applySymbolStyles(sym, d) {
75618
+ if (sym.type == 'polyline') {
75619
+ sym.stroke = getSymbolStrokeColor(d);
75620
+ } else {
75621
+ sym.fill = getSymbolFillColor(d);
75622
+ }
75623
+ if (d.opacity) {
75624
+ sym.opacity = d.opacity;
75625
+ }
75626
+ return sym;
75627
+ }
75628
+
75629
+ function getSymbolRadius(d) {
75630
+ if (d.radius === 0 || d.length === 0 || d.r === 0) return 0;
75631
+ return d.radius || d.length || d.r || 5; // use a default value
75632
+ }
75633
+
75634
+ // Converts the value of a list-valued option into an array of items. The value
75635
+ // arrives either as a whole string (from a direct call, rather than from the
75636
+ // -symbols option accessor) or as an array of items already resolved one by
75637
+ // one. An item that holds an array of its own, as an expression like
75638
+ // [R, R * 0.5] does, is flattened into the list. Items themselves are left
75639
+ // unsplit, because a single item can contain a comma, as rgba() colors do.
75640
+ function toItemList(val) {
75641
+ var items = utils.isString(val) ? splitListItems(val) : [].concat(val);
75642
+ var list = [];
75643
+ for (var i=0; i<items.length; i++) {
75644
+ if (Array.isArray(items[i])) {
75645
+ list = list.concat(items[i]);
75646
+ } else {
75647
+ list.push(items[i]);
75648
+ }
75649
+ }
75650
+ return list;
75651
+ }
75652
+
75653
+ // Like toItemList(), for options that take numbers. An item that is a string
75654
+ // containing commas is split, so that a data field holding a value like "2,4"
75655
+ // works as a list. Values that aren't numbers become NaN, for the caller to
75656
+ // report or ignore.
75657
+ function toNumberList(val) {
75658
+ var list = [];
75659
+ toItemList(val).forEach(function(item) {
75660
+ if (utils.isString(item) && item.indexOf(',') > -1) {
75661
+ list = list.concat(parseNumberList(item));
75662
+ } else {
75663
+ list.push(Number(item));
75664
+ }
75665
+ });
75666
+ return list;
75667
+ }
75668
+
75669
+ // Returns the radius of the smallest circle centered on a symbol's point that
75670
+ // covers the symbol, or null if the symbol contains a part whose reach can't be
75671
+ // measured.
75672
+ //
75673
+ // Only a symbol drawn around its point can be described this way: a circle, or
75674
+ // a group of the circles and polygons that make up ring, pie and donut symbols.
75675
+ // An arrow is drawn from its point outward, so the circle around its point that
75676
+ // covers it says nothing useful about the space it occupies -- arrows are plain
75677
+ // polygon symbols, which reach this function only as group parts, which they
75678
+ // never are.
75679
+ function getSymbolBoundingRadius(sym) {
75680
+ var max = 0;
75681
+ var parts, r, i;
75682
+ if (!sym) return null;
75683
+ if (sym.type == 'circle') {
75684
+ // Half of a stroke lies outside the circle it follows, which is how a ring
75685
+ // symbol paints its bands.
75686
+ return getPositiveNumber(sym.r) + getStrokeOutset(sym);
75687
+ }
75688
+ if (sym.type == 'polygon') {
75689
+ forEachSymbolCoord(sym.coordinates || [], function(p) {
75690
+ var dist = Math.sqrt(p[0] * p[0] + p[1] * p[1]);
75691
+ if (dist > max) max = dist;
75692
+ });
75693
+ return max + getStrokeOutset(sym);
75694
+ }
75695
+ if (sym.type == 'group') {
75696
+ parts = sym.parts || [];
75697
+ for (i=0; i<parts.length; i++) {
75698
+ r = getSymbolBoundingRadius(parts[i]);
75699
+ if (r === null) return null;
75700
+ if (r > max) max = r;
75701
+ }
75702
+ return max;
75703
+ }
75704
+ return null;
75705
+ }
75706
+
75707
+ // A stroke straddles the path it is drawn on, so half of its width extends
75708
+ // beyond the shape.
75709
+ function getStrokeOutset(sym) {
75710
+ var width = sym['stroke-width'];
75711
+ if (!sym.stroke || sym.stroke == 'none') return 0;
75712
+ // an SVG stroke is one pixel wide unless stroke-width says otherwise
75713
+ if (width === undefined || width === null || width === '') return 0.5;
75714
+ return getPositiveNumber(width) / 2;
75715
+ }
75716
+
75717
+ function getPositiveNumber(val) {
75718
+ var num = +val;
75719
+ return num > 0 ? num : 0;
75720
+ }
75721
+
75722
+ function forEachSymbolCoord(coords, cb) {
75723
+ var isPoint = coords && utils.isNumber(coords[0]);
75724
+ var isNested = !isPoint && coords && Array.isArray(coords[0]);
75725
+ if (isPoint) return cb(coords);
75726
+ for (var i=0; i<coords.length; i++) {
75727
+ if (isNested) forEachSymbolCoord(coords[i], cb);
75728
+ }
75729
+ }
75730
+
75731
+ function flipY(coords) {
75732
+ forEachSymbolCoord(coords, function(p) {
75733
+ p[1] = -p[1];
75734
+ });
75735
+ }
75736
+
75737
+ function scaleAndShiftCoords(coords, scale, shift) {
75738
+ forEachSymbolCoord(coords, function(xy) {
75739
+ xy[0] = xy[0] * scale + shift[0];
75740
+ xy[1] = xy[1] * scale + shift[1];
75741
+ });
75742
+ }
75743
+
75744
+ function roundCoordsForSVG(coords) {
75745
+ forEachSymbolCoord(coords, function(p) {
75746
+ p[0] = roundCoord$1(p[0]);
75747
+ p[1] = roundCoord$1(p[1]);
75748
+ });
75749
+ }
75750
+
75751
+ function rotateCoords(coords, rotation) {
75752
+ if (!rotation) return;
75753
+ var f = getAffineTransform(rotation, 1, [0, 0], [0, 0]);
75754
+ forEachSymbolCoord(coords, function(p) {
75755
+ var p2 = f(p[0], p[1]);
75756
+ p[0] = p2[0];
75757
+ p[1] = p2[1];
75758
+ });
75759
+ }
75760
+
75761
+ // Collision reduction for circular symbols, in the spirit of d3-force's
75762
+ // forceCollide. Overlapping symbols are pushed apart, but each one is kept
75763
+ // within a fixed distance of its true position.
75764
+ //
75765
+ // Nodes are laid out in pixel space; converting to and from map coordinates is
75766
+ // the caller's job. A node is {i, x, y, x0, y0, r, maxShift, margin}, where
75767
+ // x0,y0 is the anchor (the symbol's true position), r includes any padding and
75768
+ // maxShift is the furthest this symbol may be displaced. All three of r,
75769
+ // maxShift and margin are per-node, so the caller can vary them from symbol to
75770
+ // symbol; a node with a maxShift of 0 never moves and so acts as a fixed
75771
+ // obstacle.
75772
+ //
75773
+ // A pair of nodes is kept a.r + b.r + max(a.margin, b.margin) apart, so a
75774
+ // margin is a gap that one symbol claims from every neighbor, whatever the
75775
+ // neighbor asks for, and the larger of the two claims wins. Padding is the
75776
+ // alternative already folded into r, where a pair ends up with the sum of the
75777
+ // two symbols' padding between them. margin is optional; treated as 0 if absent.
75778
+ //
75779
+ // A negative margin lets a pair overlap, but only where both nodes permit it:
75780
+ // max() means one node's 0 outranks another's -5.
75781
+
75782
+ // max_shift is not read by the solver (it is a per-node property); it is here
75783
+ // as the default for callers to apply when building nodes.
75784
+ //
75785
+ // strength is the damping factor of what is essentially Gauss-Seidel
75786
+ // relaxation. A symbol in a cluster is corrected once per overlapping neighbor
75787
+ // in a single pass, so the corrections sum to more than its own overlap and
75788
+ // values near 1 overshoot and oscillate. Given the tick budget below, anything
75789
+ // from 0.3 to 1.0 resolves a layout that can be resolved at all; the value only
75790
+ // matters where crowding exceeds what max-shift can fix, and there the lower
75791
+ // end leaves shallower overlaps and holds symbols closer to their true
75792
+ // positions. Below about 0.4 the solver stops reliably breaking up stacks of
75793
+ // coincident symbols.
75794
+ var symbolCollisionDefaults = {
75795
+ ticks: 100,
75796
+ max_shift: 20,
75797
+ strength: 0.4
75798
+ };
75799
+
75800
+ // Mutates the x,y properties of @nodes. Returns the number of nodes that moved.
75801
+ //
75802
+ // opts.constrain is an optional function(nodes) applied after each pass, for
75803
+ // callers that need to restrict where symbols may go (see
75804
+ // mapshaper-symbol-containment.mjs). It must only move a node to a position
75805
+ // between ones the node has already held, or the max-shift limit no longer
75806
+ // holds.
75807
+ function resolveSymbolCollisions(nodes, opts) {
75808
+ var ticks = opts.ticks,
75809
+ strength = opts.strength,
75810
+ constrain = opts.constrain || null,
75811
+ moved = 0,
75812
+ i;
75813
+ if (nodes.length < 2 || !(ticks > 0) || !anyNodeCanMove(nodes)) return 0;
75814
+ for (i=0; i<ticks; i++) {
75815
+ resolveCollisions(nodes, strength);
75816
+ // Limiting the displacement on every pass (instead of only at the end)
75817
+ // avoids pulling symbols back into the overlaps they just escaped.
75818
+ limitDisplacement(nodes);
75819
+ if (constrain) constrain(nodes);
75820
+ }
75821
+ for (i=0; i<nodes.length; i++) {
75822
+ if (nodes[i].x !== nodes[i].x0 || nodes[i].y !== nodes[i].y0) moved++;
75823
+ }
75824
+ return moved;
75825
+ }
75826
+
75827
+ // Collisions shallower than this are not reported. A converged layout doesn't
75828
+ // leave pairs at exactly the separation they need, it leaves a tail of pairs
75829
+ // short of it by a fraction of a pixel -- on a solved 435-symbol layout, 55
75830
+ // pairs fall short by more than a hundredth of a pixel, 6 by more than half a
75831
+ // pixel, and none by more than one. So a threshold of a pixel is where this
75832
+ // solver's convergence residue ends and genuine unresolved crowding begins, as
75833
+ // well as being about the point where the shortfall becomes visible.
75834
+ var VISIBLE_COLLISION = 1; // pixels
75835
+
75836
+ // Number of pairs that fall visibly short of the separation they need, for
75837
+ // reporting how much of the job got done. Deliberately not a count of every
75838
+ // collision: falling short by a 100th of a pixel and by 15 pixels are not the
75839
+ // same result, and treating them alike makes a layout that is visually finished
75840
+ // look like a failure.
75841
+ //
75842
+ // Note this counts collisions, not overlaps, and padding or a margin can put
75843
+ // the two apart: symbols asked to keep 4px between them and left 3px apart are
75844
+ // colliding, in that the layout hasn't done what was asked, but not overlapping.
75845
+ function countSymbolCollisions(nodes) {
75846
+ var count = 0;
75847
+ forEachCollision(nodes, function(a, b, dx, dy, distSq, sum) {
75848
+ if (sum - Math.sqrt(distSq) > VISIBLE_COLLISION) count++;
75849
+ });
75850
+ return count;
75851
+ }
75852
+
75853
+ // Push colliding nodes apart, in proportion to how far short of their required
75854
+ // separation they are. Larger nodes move less than smaller ones. Which of the
75855
+ // two yields is decided by their radii alone, so asking for a wide margin does
75856
+ // not also make a symbol harder to push.
75857
+ function resolveCollisions(nodes, strength) {
75858
+ forEachCollision(nodes, function(a, b, dx, dy, distSq, sum) {
75859
+ separate(a, b, dx, dy, distSq, sum, strength);
75860
+ });
75861
+ }
75862
+
75863
+ // Sweep along the x axis, using the greatest reach still ahead of the current
75864
+ // node to know when to stop looking for neighbors. A node's reach is its radius
75865
+ // plus its margin if that margin is positive: only a positive margin widens the
75866
+ // area a symbol needs, and clamping is what makes the bound below safe.
75867
+ //
75868
+ // Breaking out of the inner loop is valid because the separation a pair needs,
75869
+ // a.r + b.r + max(a.margin, b.margin), never exceeds a.reach + b.reach. With
75870
+ // margins clamped at 0 that holds, since max(ma, mb) <= max(ma, 0) + max(mb, 0).
75871
+ // Without the clamp it fails for two negative margins -- max(-3, -3) is -3, not
75872
+ // -6 -- and the sweep quietly stops short of pairs that are still colliding.
75873
+ //
75874
+ // Margins are collected into an array in the same pass that builds the bound,
75875
+ // which normalizes a missing margin to 0 once per node instead of guarding every
75876
+ // comparison in the loop below.
75877
+ //
75878
+ // Note the sort happens once per pass while the callback moves nodes as it goes,
75879
+ // so the bound is applied to positions that have already shifted and a pair that
75880
+ // drifts into range mid-pass can be missed until the next pass re-sorts. A
75881
+ // consequence is that padding and an equivalent margin, which ask for the same
75882
+ // separation, don't produce identical output: the margin gives a node a longer
75883
+ // reach, which prunes less and catches a few more of those pairs.
75884
+ function forEachCollision(nodes, cb) {
75885
+ var n = nodes.length,
75886
+ maxAhead = new Float64Array(n),
75887
+ margins = new Float64Array(n),
75888
+ max = 0,
75889
+ a, b, dx, dy, sum, distSq, reach, ma, mb, i, j;
75890
+ nodes.sort(compareX);
75891
+ for (i=n-1; i>=0; i--) {
75892
+ ma = nodes[i].margin || 0;
75893
+ margins[i] = ma;
75894
+ reach = nodes[i].r + (ma > 0 ? ma : 0);
75895
+ if (reach > max) max = reach;
75896
+ maxAhead[i] = max;
75897
+ }
75898
+ for (i=0; i<n; i++) {
75899
+ a = nodes[i];
75900
+ ma = margins[i];
75901
+ reach = a.r + (ma > 0 ? ma : 0);
75902
+ for (j=i+1; j<n; j++) {
75903
+ b = nodes[j];
75904
+ dx = b.x - a.x;
75905
+ if (dx > reach + maxAhead[j]) break;
75906
+ dy = b.y - a.y;
75907
+ mb = margins[j];
75908
+ sum = a.r + b.r + (ma > mb ? ma : mb);
75909
+ // Margins can cancel the pair's radii entirely, which separate() would
75910
+ // read as an overlap and answer by pulling the two nodes together.
75911
+ if (sum <= 0) continue;
75912
+ if (dy > sum || dy < -sum) continue;
75913
+ distSq = dx * dx + dy * dy;
75914
+ if (distSq >= sum * sum) continue;
75915
+ cb(a, b, dx, dy, distSq, sum);
75916
+ }
75917
+ }
75918
+ }
75919
+
75920
+ function compareX(a, b) {
75921
+ // Break ties by original index, so the order of equally positioned nodes
75922
+ // doesn't depend on the engine's sort implementation.
75923
+ return a.x - b.x || a.i - b.i;
75924
+ }
75925
+
75926
+ function separate(a, b, dx, dy, distSq, sum, strength) {
75927
+ var ux, uy, overlap, dist, dir, shift, share;
75928
+ if (distSq > 0) {
75929
+ dist = Math.sqrt(distSq);
75930
+ ux = dx / dist;
75931
+ uy = dy / dist;
75932
+ overlap = sum - dist;
75933
+ } else {
75934
+ // Coincident nodes: pick a repeatable arbitrary direction
75935
+ dir = getPseudoDirection(a.i, b.i);
75936
+ ux = dir[0];
75937
+ uy = dir[1];
75938
+ overlap = sum;
75939
+ }
75940
+ shift = overlap * strength;
75941
+ share = b.r * b.r / (a.r * a.r + b.r * b.r); // portion moved by a
75942
+ a.x -= ux * shift * share;
75943
+ a.y -= uy * shift * share;
75944
+ b.x += ux * shift * (1 - share);
75945
+ b.y += uy * shift * (1 - share);
75946
+ }
75947
+
75948
+ // A deterministic stand-in for d3-force's jiggle(), which uses Math.random()
75949
+ function getPseudoDirection(i, j) {
75950
+ var hash = (Math.imul(i + 1, 2654435761) ^ Math.imul(j + 1, 1597334677)) >>> 0,
75951
+ x = (hash & 0xffff) / 0x8000 - 1,
75952
+ y = ((hash >>> 16) & 0xffff) / 0x8000 - 1,
75953
+ dist = Math.sqrt(x * x + y * y);
75954
+ return dist > 0 ? [x / dist, y / dist] : [1, 0];
75955
+ }
75956
+
75957
+ function anyNodeCanMove(nodes) {
75958
+ for (var i=0; i<nodes.length; i++) {
75959
+ if (nodes[i].maxShift > 0) return true;
75960
+ }
75961
+ return false;
75962
+ }
75963
+
75964
+ // Pull each node back onto the circle of radius node.maxShift around its anchor.
75965
+ function limitDisplacement(nodes) {
75966
+ var node, maxShift, dx, dy, distSq, k, i;
75967
+ for (i=0; i<nodes.length; i++) {
75968
+ node = nodes[i];
75969
+ maxShift = node.maxShift;
75970
+ dx = node.x - node.x0;
75971
+ dy = node.y - node.y0;
75972
+ distSq = dx * dx + dy * dy;
75973
+ if (distSq <= maxShift * maxShift) continue;
75974
+ k = maxShift / Math.sqrt(distSq);
75975
+ node.x = node.x0 + dx * k;
75976
+ node.y = node.y0 + dy * k;
75977
+ }
75978
+ }
75979
+
75980
+ var SymbolCollisions = /*#__PURE__*/Object.freeze({
75981
+ __proto__: null,
75982
+ VISIBLE_COLLISION: VISIBLE_COLLISION,
75983
+ countSymbolCollisions: countSymbolCollisions,
75984
+ resolveSymbolCollisions: resolveSymbolCollisions,
75985
+ symbolCollisionDefaults: symbolCollisionDefaults
75986
+ });
75987
+
75988
+ // Move symbols apart to reduce overlaps, keeping each one within a fixed pixel
75989
+ // distance of its true position. Each symbol is laid out as the circle that
75990
+ // covers it, so the command works on the symbol types that are drawn around
75991
+ // their point (see getSymbolLayoutRadius()).
75992
+ //
75993
+ // The layout runs in the pixel space of the map that the symbols will be
75994
+ // rendered in, so the command needs to know the display scale: either from a
75995
+ // frame associated with the target, or from a width= option.
75996
+ //
75997
+ // All targeted layers are laid out in a single simulation, so symbols in
75998
+ // different layers are moved apart from each other as well.
75999
+ //
76000
+ // An optional polygons= layer confines each symbol to the polygon it started
76001
+ // in, so that a symbol standing for one area can't drift into a neighboring
76002
+ // one. Symbols in an area too small to hold them barely move as a result, and
76003
+ // their collisions go unresolved -- that is the point of the option, but it does
76004
+ // mean fewer collisions get fixed.
76005
+ //
76006
+ // Clearance between symbols can be asked for in two ways. padding= surrounds
76007
+ // each symbol, so a pair ends up with the sum of their two paddings between
76008
+ // them; margin= is a gap a symbol claims from every neighbor, so a pair ends up
76009
+ // with the greater of their two margins. Use padding= to make a symbol keep its
76010
+ // distance in proportion to how much of it there is, and margin= to set a gap
76011
+ // that holds however the neighbor is configured.
76012
+ var DEFAULT_PADDING = 0; // pixels
76013
+ var DEFAULT_MARGIN = 0; // pixels
76014
+
76015
+ cmd.repel = function(targetLayers, dataset, catalog, polygonSource, opts) {
76016
+ var solverOpts = {
76017
+ ticks: opts.ticks > 0 ? opts.ticks : symbolCollisionDefaults.ticks,
76018
+ strength: opts.strength > 0 ? opts.strength : symbolCollisionDefaults.strength
76019
+ };
76020
+ var nodes = [];
76021
+
76022
+ // Validated before the display scale is resolved, so that an unusable target
76023
+ // is reported as such instead of as a missing width= option.
76024
+ requireProjectedDataset(dataset);
76025
+ targetLayers.forEach(function(lyr) {
76026
+ requireRepelTarget(lyr, opts);
76027
+ });
76028
+ var pixelsPerUnit = getDisplayScale(targetLayers, dataset, catalog, opts);
76029
+ targetLayers.forEach(function(lyr) {
76030
+ addLayerNodes(nodes, lyr, pixelsPerUnit, opts);
76031
+ });
76032
+ if (nodes.length === 0) {
76033
+ stop$1('Targeted layer(s) contain no symbols to move.');
76034
+ }
76035
+ var unconstrained = addContainment(nodes, dataset, polygonSource, pixelsPerUnit, solverOpts);
76036
+
76037
+ var collisionsBefore = countSymbolCollisions(nodes);
76038
+ var moved = resolveSymbolCollisions(nodes, solverOpts);
76039
+ var collisionsAfter = countSymbolCollisions(nodes);
76040
+ applyDisplacement(nodes, pixelsPerUnit);
76041
+ reportResults(nodes, moved, collisionsBefore, collisionsAfter, !!polygonSource);
76042
+ if (unconstrained > 0) {
76043
+ message(utils.format('%d symbol%s not inside any polygon in %s, and %s left free to move',
76044
+ unconstrained, unconstrained == 1 ? ' is' : 's are', polygonSource.layer.name || 'the polygons= layer',
76045
+ unconstrained == 1 ? 'was' : 'were'));
76046
+ }
76047
+ };
76048
+
76049
+ // Confines symbols to the polygons they start inside. Returns the number of
76050
+ // symbols that no polygon contains, which are left unconstrained.
76051
+ function addContainment(nodes, dataset, polygonSource, pixelsPerUnit, solverOpts) {
76052
+ if (!polygonSource) return 0;
76053
+ var lyr = polygonSource.layer;
76054
+ var arcs = polygonSource.dataset.arcs;
76055
+ requirePolygonLayer(lyr, 'The polygons= option requires a polygon layer.');
76056
+ // Only catches mixing projected with unprojected data; two different
76057
+ // projections are the caller's problem, as with -join
76058
+ requireDatasetsHaveCompatibleCRS([dataset, polygonSource.dataset]);
76059
+ var unconstrained = assignContainingPolygons(nodes, lyr, arcs, pixelsPerUnit);
76060
+ solverOpts.constrain = getContainmentConstraint(lyr, arcs, pixelsPerUnit);
76061
+ return unconstrained;
76062
+ }
76063
+
76064
+ // Symbols are displaced by adding an offset to their original coordinates, so
76065
+ // that symbols that didn't move keep bit-for-bit identical coordinates.
76066
+ // Displaced symbols get a new coordinate array rather than an updated one,
76067
+ // because the GeoJSON importer reuses the arrays it is given and mutating them
76068
+ // would reach back into a caller's input object.
76069
+ function applyDisplacement(nodes, pixelsPerUnit) {
76070
+ var movedNodes = nodes.filter(function(node) {
76071
+ return node.x !== node.x0 || node.y !== node.y0;
76072
+ });
76073
+ var movedLayers = [];
76074
+ movedNodes.forEach(function(node) {
76075
+ if (movedLayers.indexOf(node.lyr) == -1) movedLayers.push(node.lyr);
76076
+ });
76077
+ movedLayers.forEach(function(lyr) {
76078
+ noteLayerWillChange(lyr, {operation: 'repel', unit: 'shapes'});
76079
+ });
76080
+ movedNodes.forEach(function(node) {
76081
+ node.shp[0] = [
76082
+ node.shp[0][0] + (node.x - node.x0) / pixelsPerUnit,
76083
+ node.shp[0][1] + (node.y - node.y0) / pixelsPerUnit
76084
+ ];
76085
+ });
76086
+ movedLayers.forEach(function(lyr) {
76087
+ markLayerChanged(lyr, {operation: 'repel', unit: 'shapes'});
76088
+ });
76089
+ }
76090
+
76091
+ function reportResults(nodes, moved, before, after, contained) {
76092
+ var total = nodes.length;
76093
+ var msg = utils.format('Moved %d of %d symbol%s', moved, total, total == 1 ? '' : 's');
76094
+ // Counts are of visible collisions only, so a layout whose symbols end up at
76095
+ // or a fraction of a pixel short of the separation they need is reported as
76096
+ // finished, and the advice to raise max-shift= is not given when nothing is
76097
+ // left to fix. A collision is not the same as an overlap: two symbols asked to
76098
+ // keep a gap between them collide as soon as the gap closes.
76099
+ if (after > 0) {
76100
+ msg += utils.format('; %d of %d visible collision%s remain (%s)',
76101
+ after, before, before == 1 ? '' : 's',
76102
+ contained ? getMaxShiftAdvice(nodes) + ', or drop polygons=' : getMaxShiftAdvice(nodes));
76103
+ } else if (before > 0) {
76104
+ msg += utils.format('; resolved %d visible collision%s', before, before == 1 ? '' : 's');
76105
+ } else {
76106
+ msg += ' (no visible collisions found)';
76107
+ }
76108
+ message(msg);
76109
+ }
76110
+
76111
+ // max-shift= may vary from symbol to symbol, so report the range in use.
76112
+ function getMaxShiftAdvice(nodes) {
76113
+ var min = Infinity, max = -Infinity;
76114
+ nodes.forEach(function(node) {
76115
+ if (node.maxShift < min) min = node.maxShift;
76116
+ if (node.maxShift > max) max = node.maxShift;
76117
+ });
76118
+ if (min === max) {
76119
+ return utils.format('try a larger max-shift= than %s', min);
76120
+ }
76121
+ return utils.format('try larger max-shift= values, now %s-%s', min, max);
76122
+ }
76123
+
76124
+ function requireRepelTarget(lyr, opts) {
76125
+ requireSinglePointLayer(lyr,
76126
+ '-repel requires single points; layer contains multi-point features.');
76127
+ if (!opts.radius && !layerHasSymbols(lyr)) {
76128
+ stop$1('-repel requires a layer containing symbols ' +
76129
+ '(see the -symbols and -style commands), or a radius= option.');
76130
+ }
76131
+ }
76132
+
76133
+ function layerHasSymbols(lyr) {
76134
+ return !!lyr.data && (lyr.data.fieldExists('svg-symbol') || lyr.data.fieldExists('r'));
76135
+ }
76136
+
76137
+ function addLayerNodes(nodes, lyr, pixelsPerUnit, opts) {
76138
+ var getRadius = getRadiusAccessor(lyr, opts);
76139
+ var getPadding = getPixelValueAccessor(opts.padding, 'padding', DEFAULT_PADDING, lyr);
76140
+ var getMargin = getPixelValueAccessor(opts.margin, 'margin', DEFAULT_MARGIN, lyr, true);
76141
+ var getMaxShift = getPixelValueAccessor(opts.max_shift, 'max-shift',
76142
+ symbolCollisionDefaults.max_shift, lyr);
76143
+ var i, shp, p, r;
76144
+ for (i=0; i<lyr.shapes.length; i++) {
76145
+ shp = lyr.shapes[i];
76146
+ p = shp ? shp[0] : null;
76147
+ if (!p) continue;
76148
+ r = getRadius(i);
76149
+ if (!(r > 0) || !isFinite(p[0]) || !isFinite(p[1])) continue;
76150
+ nodes.push({
76151
+ lyr: lyr,
76152
+ shp: shp,
76153
+ i: nodes.length,
76154
+ x: p[0] * pixelsPerUnit,
76155
+ y: p[1] * pixelsPerUnit,
76156
+ x0: p[0] * pixelsPerUnit,
76157
+ y0: p[1] * pixelsPerUnit,
76158
+ // Padding is added to the radius, so a pair of symbols ends up with
76159
+ // (padding a + padding b) pixels of clearance between them. A margin is
76160
+ // kept separate, because the solver has to compare the two symbols'
76161
+ // margins to find the gap a pair needs.
76162
+ r: r + getPadding(i),
76163
+ margin: getMargin(i),
76164
+ maxShift: getMaxShift(i)
76165
+ });
76166
+ }
76167
+ }
76168
+
76169
+ // padding=, margin= and max-shift= accept a number, a field name or an
76170
+ // expression, and so are resolved for each symbol.
76171
+ //
76172
+ // @allowNegative is for margin=, where a negative value asks that a pair of
76173
+ // symbols be allowed to overlap. Elsewhere a negative value is an error: it
76174
+ // would shrink a radius or a shift limit, neither of which is a coherent
76175
+ // request. A margin still has to be a finite number, so NaN is rejected either
76176
+ // way.
76177
+ function getPixelValueAccessor(optVal, name, defaultVal, lyr, allowNegative) {
76178
+ if (optVal === undefined || optVal === null || optVal === '') {
76179
+ return function(i) {return defaultVal;};
76180
+ }
76181
+ var accessor = getPropertyAccessor(optVal, 'number', lyr, name);
76182
+ return function(i) {
76183
+ var val = +accessor(i);
76184
+ if (allowNegative ? !isFinite(val) : !(val >= 0)) {
76185
+ stop$1(utils.format('Invalid %s= value: %s', name, accessor(i)));
76186
+ }
76187
+ return val;
76188
+ };
76189
+ }
76190
+
76191
+ // Radii come from one of three places: an explicit radius= option, the
76192
+ // svg-symbol field written by -symbols, or the r field written by -style.
76193
+ // Deliberately not using getSymbolRadius(), whose default of 5 would invent
76194
+ // radii for symbols that don't exist.
76195
+ function getRadiusAccessor(lyr, opts) {
76196
+ if (opts.radius) {
76197
+ return getSymbolPropertyAccessor(opts.radius, 'radius', lyr);
76198
+ }
76199
+ var records = lyr.data.getRecords();
76200
+ return function(i) {
76201
+ var rec = records[i];
76202
+ if (!rec) return 0;
76203
+ if (rec['svg-symbol']) return getSymbolLayoutRadius(rec['svg-symbol']);
76204
+ return rec.r > 0 ? +rec.r : 0;
76205
+ };
76206
+ }
76207
+
76208
+ // A symbol is laid out as the circle that covers it, so a symbol type is
76209
+ // supported if it is drawn around its point: a circle, or a group of parts,
76210
+ // which is how ring, pie and donut symbols are made. A symbol drawn from its
76211
+ // point outward, like an arrow, has no meaningful radius and needs radius=.
76212
+ function getSymbolLayoutRadius(sym) {
76213
+ var r;
76214
+ if (utils.isString(sym)) {
76215
+ try {
76216
+ sym = JSON.parse(sym);
76217
+ } catch(e) {
76218
+ return 0;
76219
+ }
76220
+ }
76221
+ if (!sym) return 0;
76222
+ r = sym.type == 'circle' || sym.type == 'group' ?
76223
+ getSymbolBoundingRadius(sym) : null;
76224
+ if (r === null) {
76225
+ stop$1('-repel is unable to size a ' + (sym.type || 'symbol') +
76226
+ ' symbol; use a radius= option to give it a radius.');
76227
+ }
76228
+ return r;
76229
+ }
76230
+
76231
+ // Returns the number of display pixels per map coordinate unit.
76232
+ function getDisplayScale(targetLayers, dataset, catalog, opts) {
76233
+ var frame, bounds, extent;
76234
+ if (opts.width > 0) {
76235
+ bounds = getCombinedBounds(targetLayers, dataset);
76236
+ // A layer of collinear points has no width; fall back to its height so that
76237
+ // width= still gives a usable scale.
76238
+ extent = bounds.width() || bounds.height();
76239
+ if (!(extent > 0)) {
76240
+ stop$1('Unable to calculate a display scale: targeted symbols have no extent.');
76241
+ }
76242
+ return opts.width / extent;
76243
+ }
76244
+ frame = findRepelFrame(dataset, catalog);
76245
+ if (!frame) {
76246
+ stop$1('-repel requires a width= option when the target has no frame.');
76247
+ }
76248
+ warnIfFrameMismatched(frame, targetLayers, dataset);
76249
+ return getFramePixelsPerUnit(frame);
76250
+ }
76251
+
76252
+ function findRepelFrame(dataset, catalog) {
76253
+ var lyr = findFrameLayerInDataset(dataset);
76254
+ var target;
76255
+ if (lyr) return getFrameLayerData(lyr, dataset.arcs);
76256
+ // -frame adds the frame it creates to the catalog as a separate dataset, so a
76257
+ // catalog-wide search is needed to find it (-scalebar does the same).
76258
+ target = catalog ? findFrame(catalog) : null;
76259
+ return target ? getFrameLayerData(target.layer, target.dataset.arcs) : null;
76260
+ }
76261
+
76262
+ // Mirrors the transform applied by fitDatasetToFrame(), so that the layout
76263
+ // matches the pixels that get rendered. A naive width / bbox width differs
76264
+ // whenever the frame bbox and its pixel dimensions have different aspect ratios.
76265
+ function getFramePixelsPerUnit(frameData) {
76266
+ var bounds = new Bounds(frameData.bbox);
76267
+ var bounds2 = frameData.bbox2 ? new Bounds(frameData.bbox2) :
76268
+ new Bounds(0, 0, frameData.width, frameData.height);
76269
+ bounds.fillOut(bounds2.width() / bounds2.height());
76270
+ return bounds.getTransform(bounds2).mx;
76271
+ }
76272
+
76273
+ function warnIfFrameMismatched(frame, targetLayers, dataset) {
76274
+ var bounds = getCombinedBounds(targetLayers, dataset);
76275
+ if (!bounds.hasBounds()) return;
76276
+ if (!new Bounds(frame.bbox).intersects(bounds)) {
76277
+ message('[repel] Warning: the frame does not overlap the targeted symbols. ' +
76278
+ 'If the data was projected after the frame was made, the display scale is wrong.');
76279
+ }
76280
+ }
76281
+
76282
+ function getCombinedBounds(targetLayers, dataset) {
76283
+ return targetLayers.reduce(function(memo, lyr) {
76284
+ var bounds = getLayerBounds(lyr, dataset.arcs);
76285
+ return bounds ? memo.mergeBounds(bounds) : memo;
76286
+ }, new Bounds());
76287
+ }
76288
+
75303
76289
  // Support for evaluating expressions embedded in curly-brace templates
75304
76290
 
75305
76291
  // Returns: a string (e.g. a command string used by the -run command)
@@ -77287,72 +78273,6 @@ ${svg}
77287
78273
  });
77288
78274
  }
77289
78275
 
77290
- var roundCoord$1 = getRoundingFunction(0.01);
77291
-
77292
- function getSymbolFillColor(d) {
77293
- return d.fill || 'magenta';
77294
- }
77295
-
77296
- function getSymbolStrokeColor(d) {
77297
- return d.stroke || d.fill || 'magenta';
77298
- }
77299
-
77300
- function applySymbolStyles(sym, d) {
77301
- if (sym.type == 'polyline') {
77302
- sym.stroke = getSymbolStrokeColor(d);
77303
- } else {
77304
- sym.fill = getSymbolFillColor(d);
77305
- }
77306
- if (d.opacity) {
77307
- sym.opacity = d.opacity;
77308
- }
77309
- return sym;
77310
- }
77311
-
77312
- function getSymbolRadius(d) {
77313
- if (d.radius === 0 || d.length === 0 || d.r === 0) return 0;
77314
- return d.radius || d.length || d.r || 5; // use a default value
77315
- }
77316
-
77317
- function forEachSymbolCoord(coords, cb) {
77318
- var isPoint = coords && utils.isNumber(coords[0]);
77319
- var isNested = !isPoint && coords && Array.isArray(coords[0]);
77320
- if (isPoint) return cb(coords);
77321
- for (var i=0; i<coords.length; i++) {
77322
- if (isNested) forEachSymbolCoord(coords[i], cb);
77323
- }
77324
- }
77325
-
77326
- function flipY(coords) {
77327
- forEachSymbolCoord(coords, function(p) {
77328
- p[1] = -p[1];
77329
- });
77330
- }
77331
-
77332
- function scaleAndShiftCoords(coords, scale, shift) {
77333
- forEachSymbolCoord(coords, function(xy) {
77334
- xy[0] = xy[0] * scale + shift[0];
77335
- xy[1] = xy[1] * scale + shift[1];
77336
- });
77337
- }
77338
-
77339
- function roundCoordsForSVG(coords) {
77340
- forEachSymbolCoord(coords, function(p) {
77341
- p[0] = roundCoord$1(p[0]);
77342
- p[1] = roundCoord$1(p[1]);
77343
- });
77344
- }
77345
-
77346
- function rotateCoords(coords, rotation) {
77347
- if (!rotation) return;
77348
- var f = getAffineTransform(rotation, 1, [0, 0], [0, 0]);
77349
- forEachSymbolCoord(coords, function(p) {
77350
- var p2 = f(p[0], p[1]);
77351
- p[0] = p2[0];
77352
- p[1] = p2[1];
77353
- });
77354
- }
77355
-
77356
78276
  function getStickArrowCoords(d) {
77357
78277
  return getArrowCoords(d, 'stick');
77358
78278
  }
@@ -77690,11 +78610,176 @@ ${svg}
77690
78610
  }
77691
78611
 
77692
78612
  function parseRings(arg) {
77693
- var arr = Array.isArray(arg) ? arg : parseNumberList(arg);
78613
+ var arr = toNumberList(arg);
77694
78614
  utils.genericSort(arr, true);
77695
78615
  return utils.uniq(arr);
77696
78616
  }
77697
78617
 
78618
+ // Matches the vertex density of the 72-sided circle made by getPolygonCoords()
78619
+ var DEGREES_PER_SEGMENT = 5;
78620
+
78621
+ // Returns an svg-symbol object containing one polygon part per wedge, or null
78622
+ // if the symbol has nothing to draw.
78623
+ function makePieSymbol(d, opts) {
78624
+ var wedges = getPieWedges(d, +opts.scale || 1);
78625
+ if (wedges.length === 0) return null;
78626
+ var parts = wedges.map(function(wedge) {
78627
+ flipY(wedge.coordinates); // the SVG y-axis points down
78628
+ roundCoordsForSVG(wedge.coordinates);
78629
+ var part = {
78630
+ type: 'polygon',
78631
+ coordinates: wedge.coordinates,
78632
+ fill: wedge.fill
78633
+ };
78634
+ if (d.stroke) part.stroke = d.stroke;
78635
+ if (d['stroke-width']) part['stroke-width'] = d['stroke-width'];
78636
+ // opacity is applied per-part, not to the group: renderComplexSymbol()
78637
+ // assumes a group symbol has a properties object, which group() only
78638
+ // provides for single-part groups.
78639
+ if (d.opacity) part.opacity = d.opacity;
78640
+ return part;
78641
+ });
78642
+ return {type: 'group', parts: parts};
78643
+ }
78644
+
78645
+ // Returns an array of {fill, coordinates} objects, in the y-up coordinate space
78646
+ // shared by the symbol generators, centered on [0, 0]. Coordinates are an array
78647
+ // of rings, like flattened GeoJSON MultiPolygon coordinates.
78648
+ //
78649
+ // The first wedge starts at the top of the symbol and the rest follow in
78650
+ // clockwise order. A wedge with no value, or without a usable fill color, is
78651
+ // left out -- an unfilled wedge becomes a gap in the pie without shifting the
78652
+ // wedges that follow it.
78653
+ function getPieWedges(d, scale) {
78654
+ var values = getPieValues(d);
78655
+ var fills = getPieFills(d);
78656
+ var radii = getPieRadii(d, scale);
78657
+ var rotation = +d.rotation || 0;
78658
+ var total = sum(values);
78659
+ var wedges = [];
78660
+ var start = rotation;
78661
+ var fill, end;
78662
+ if (total > 0 === false || radii.outer > 0 === false) return wedges;
78663
+ for (var i=0; i<values.length; i++) {
78664
+ end = start + values[i] / total * 360;
78665
+ fill = getWedgeFill(fills[i]);
78666
+ if (fill && end > start) {
78667
+ wedges.push({
78668
+ fill: fill,
78669
+ coordinates: getWedgeCoords(start, end, radii.inner, radii.outer)
78670
+ });
78671
+ }
78672
+ start = end;
78673
+ }
78674
+ return wedges;
78675
+ }
78676
+
78677
+ // Returns an array of rings outlining a wedge spanning two bearings.
78678
+ function getWedgeCoords(startAngle, endAngle, inner, outer) {
78679
+ var ring;
78680
+ if (endAngle - startAngle >= 360) {
78681
+ // A single non-zero value covers the whole symbol: a circle, or a pair of
78682
+ // concentric rings if the symbol is a donut.
78683
+ ring = closeRing(getArcCoords(startAngle, startAngle + 360, outer));
78684
+ if (inner > 0) {
78685
+ // wind the hole in the opposite direction from the outer ring
78686
+ return [ring, closeRing(getArcCoords(startAngle + 360, startAngle, inner))];
78687
+ }
78688
+ return [ring];
78689
+ }
78690
+ ring = getArcCoords(startAngle, endAngle, outer);
78691
+ if (inner > 0) {
78692
+ // A wedge of a donut is a simple polygon, so the hole needs no ring of
78693
+ // its own -- the inner arc doubles back inside the outer one.
78694
+ ring = ring.concat(getArcCoords(endAngle, startAngle, inner));
78695
+ } else {
78696
+ ring.push([0, 0]);
78697
+ }
78698
+ ring.push(ring[0].concat());
78699
+ return [ring];
78700
+ }
78701
+
78702
+ // A 360 degree arc starts and ends at the same angle, but rounding leaves its
78703
+ // endpoints a hair apart, so the ring is closed explicitly.
78704
+ function closeRing(ring) {
78705
+ ring[ring.length - 1] = ring[0].concat();
78706
+ return ring;
78707
+ }
78708
+
78709
+ // Returns points along an arc, including both endpoints. Angles are bearings,
78710
+ // which run clockwise from the top of the symbol -- the same convention as
78711
+ // rotateCoords(), so the rotation= option is just an offset to the first angle.
78712
+ function getArcCoords(startAngle, endAngle, radius) {
78713
+ var sweep = endAngle - startAngle;
78714
+ var segments = Math.max(1, Math.ceil(Math.abs(sweep) / DEGREES_PER_SEGMENT));
78715
+ var coords = [];
78716
+ for (var i=0; i<=segments; i++) {
78717
+ coords.push(getPlanarSegmentEndpoint(0, 0, startAngle + sweep * i / segments, radius));
78718
+ }
78719
+ return coords;
78720
+ }
78721
+
78722
+ // Missing, non-numeric and negative values all count as zero.
78723
+ function getPieValues(d) {
78724
+ if (!d.values) {
78725
+ stop$1('A pie symbol requires a values= option.');
78726
+ }
78727
+ return toNumberList(d.values).map(function(num) {
78728
+ return num > 0 ? num : 0;
78729
+ });
78730
+ }
78731
+
78732
+ function getPieFills(d) {
78733
+ if (!d.fills) {
78734
+ stop$1('A pie symbol requires a fills= option.');
78735
+ }
78736
+ return toItemList(d.fills);
78737
+ }
78738
+
78739
+ // Returns the color to fill a wedge with, or null if the wedge should be left
78740
+ // out of the symbol.
78741
+ function getWedgeFill(val) {
78742
+ var str = val === null || val === undefined ? '' : String(val).trim();
78743
+ var name = str.toLowerCase();
78744
+ var rgb;
78745
+ if (!str || name == 'none' || name == 'transparent') {
78746
+ return null;
78747
+ }
78748
+ rgb = parseColor(str);
78749
+ if (!rgb || rgb.a === 0) return null;
78750
+ return str;
78751
+ }
78752
+
78753
+ // Returns {inner, outer} radii in pixels. The hole= option makes a donut. A
78754
+ // negative hole is measured inward from the outer radius, which gives a band
78755
+ // of wedges of a fixed width without having to repeat a data-driven radius=
78756
+ // expression. A hole as wide as the symbol leaves a plain pie, and a
78757
+ // non-positive outer radius is left for the caller to skip, like radius=0 on
78758
+ // the other symbol types.
78759
+ function getPieRadii(d, scale) {
78760
+ var outer = getSymbolRadius(d);
78761
+ var hole = +d.hole;
78762
+ var inner = 0;
78763
+ if (hole > 0) {
78764
+ inner = hole;
78765
+ } else if (hole < 0) {
78766
+ inner = outer + hole;
78767
+ }
78768
+ if (!(inner > 0) || inner >= outer) inner = 0;
78769
+ return {
78770
+ inner: inner * scale,
78771
+ outer: outer * scale
78772
+ };
78773
+ }
78774
+
78775
+ function sum(arr) {
78776
+ var total = 0;
78777
+ for (var i=0; i<arr.length; i++) {
78778
+ total += arr[i];
78779
+ }
78780
+ return total;
78781
+ }
78782
+
77698
78783
  // Returns an svg-symbol data object for one symbol
77699
78784
  function makePathSymbol(coords, properties, geojsonType) {
77700
78785
  var sym;
@@ -77719,11 +78804,16 @@ ${svg}
77719
78804
 
77720
78805
  // TODO: refactor to remove duplication in mapshaper-svg-style.js
77721
78806
  cmd.symbols = function(inputLyr, dataset, opts) {
77722
- requireSinglePointLayer(inputLyr);
78807
+ requirePointLayer(inputLyr);
77723
78808
  var lyr = opts.no_replace ? copyLayer(inputLyr) : inputLyr;
77724
78809
  var shapeMode = !!opts.geographic;
77725
78810
  var metersPerPx;
77726
78811
  if (shapeMode) {
78812
+ // Only the first point of each feature is used to place a generated shape,
78813
+ // so the other points of a multi-point feature would be dropped. In SVG
78814
+ // mode geometry is untouched and every point gets a symbol, so multi-point
78815
+ // features are fine.
78816
+ requireSinglePointLayer(inputLyr);
77727
78817
  requireProjectedDataset(dataset);
77728
78818
  metersPerPx = opts.pixel_scale || getMetersPerPixel(lyr);
77729
78819
  }
@@ -77746,6 +78836,17 @@ ${svg}
77746
78836
  var d = getSymbolData(i);
77747
78837
  var rec = records[i] || {};
77748
78838
 
78839
+ // A pie has a different fill for each of its wedges, which the single
78840
+ // fill field of a generated shape can't express.
78841
+ if (d.type == 'pie') {
78842
+ if (shapeMode) {
78843
+ stop$1('The pie symbol type does not support the geographic option.');
78844
+ }
78845
+ var pie = makePieSymbol(d, opts);
78846
+ if (pie) rec['svg-symbol'] = pie;
78847
+ return;
78848
+ }
78849
+
77749
78850
  // non-polygon symbols
77750
78851
  if (!shapeMode && d.type == 'circle') {
77751
78852
  rec['svg-symbol'] = makeCircleSymbol(d, opts);
@@ -78313,7 +79414,7 @@ ${svg}
78313
79414
  'filter-islands2', 'filter-points', 'filter-slivers', 'grid', 'grid2',
78314
79415
  'fuzzy-join', 'ignore', 'inlay', 'innerlines', 'inspect', 'join',
78315
79416
  'lines', 'mosaic', 'points', 'polygons', 'rectangles', 'rename-fields',
78316
- 'shapes', 'simplify', 'slice', 'smooth', 'sort', 'split',
79417
+ 'repel', 'shapes', 'simplify', 'slice', 'smooth', 'sort', 'split',
78317
79418
  'split-on-grid', 'stitch', 'style', 'subdivide', 'symbols', 'uniq'
78318
79419
  ].indexOf(name) > -1;
78319
79420
  }
@@ -78673,6 +79774,10 @@ ${svg}
78673
79774
  } else if (name == 'rename-layers') {
78674
79775
  cmd.renameLayers(targetLayers, opts.names);
78675
79776
 
79777
+ } else if (name == 'repel') {
79778
+ cmd.repel(targetLayers, targetDataset, job.catalog,
79779
+ await findRepelPolygons(opts, job.catalog, targets), opts);
79780
+
78676
79781
  } else if (name == 'require') {
78677
79782
  await cmd.require(opts);
78678
79783
 
@@ -78865,6 +79970,15 @@ ${svg}
78865
79970
  return job;
78866
79971
  }
78867
79972
 
79973
+ // -repel's polygons= option names a second layer, like source= elsewhere, but
79974
+ // is resolved here rather than by the shared source= machinery: the layer is
79975
+ // only read (never merged into the target's topology), so it needs neither a
79976
+ // topology merge nor the per-target copies that clip and erase require.
79977
+ async function findRepelPolygons(opts, catalog, targets) {
79978
+ if (!opts.polygons) return null;
79979
+ return await findCommandSourceAsync(convertSourceName(opts.polygons, targets), catalog, opts);
79980
+ }
79981
+
78868
79982
  async function resolveSourceForTargets(command, job, targets) {
78869
79983
  job.startCommand(command);
78870
79984
  try {
@@ -78970,7 +80084,7 @@ ${svg}
78970
80084
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
78971
80085
  }
78972
80086
 
78973
- var version = "0.7.55";
80087
+ var version = "0.7.58";
78974
80088
 
78975
80089
  // Parse command line args into commands and run them
78976
80090
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -81175,6 +82289,7 @@ ${svg}
81175
82289
  Simplify,
81176
82290
  SimplifyFast,
81177
82291
  SimplifyPct,
82292
+ SymbolCollisions,
81178
82293
  Slivers,
81179
82294
  Snapping,
81180
82295
  SourceUtils,