mapshaper 0.7.56 → 0.7.59

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/www/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
@@ -9880,7 +9880,7 @@
9880
9880
  id: piece.id,
9881
9881
  source_region: piece.source_region,
9882
9882
  copy: piece.copy,
9883
- projected_boundary: closeRing$1(piece.boundary)
9883
+ projected_boundary: closeRing$2(piece.boundary)
9884
9884
  };
9885
9885
  }
9886
9886
 
@@ -9888,7 +9888,7 @@
9888
9888
  return {
9889
9889
  id: piece.id,
9890
9890
  source_region: piece.source_region,
9891
- projected_boundary: closeRing$1(piece.boundary)
9891
+ projected_boundary: closeRing$2(piece.boundary)
9892
9892
  };
9893
9893
  }
9894
9894
 
@@ -9970,7 +9970,7 @@
9970
9970
  return paths;
9971
9971
  }
9972
9972
 
9973
- function closeRing$1(ring) {
9973
+ function closeRing$2(ring) {
9974
9974
  var copy = ring.map(function(p) { return p.concat(); });
9975
9975
  if (copy.length && !pointsEqual$2(copy[0], copy[copy.length - 1])) {
9976
9976
  copy.push(copy[0].concat());
@@ -14955,6 +14955,27 @@
14955
14955
  return fflatePostprocess(obj);
14956
14956
  }
14957
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
+
14958
14979
  function unzipAsync(buf, cb) {
14959
14980
  if (!runningInBrowser()) {
14960
14981
  error('Async unzipping only supported in the browser');
@@ -15049,6 +15070,7 @@
15049
15070
  var Zip = /*#__PURE__*/Object.freeze({
15050
15071
  __proto__: null,
15051
15072
  isImportableZipPath: isImportableZipPath,
15073
+ listZipEntryNames: listZipEntryNames,
15052
15074
  unzipAsync: unzipAsync,
15053
15075
  unzipSync: unzipSync,
15054
15076
  zipAsync: zipAsync,
@@ -18629,14 +18651,15 @@
18629
18651
  function parseHexColor(str) {
18630
18652
  var hex = hexRxp.exec(str)[1];
18631
18653
  if (hex.length == 3 || hex.length == 4) {
18632
- 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('');
18633
18656
  }
18634
18657
  if (hex.length != 6 && hex.length != 8) return null;
18635
18658
  return {
18636
18659
  r: parseInt(hex.substr(0, 2), 16),
18637
18660
  g: parseInt(hex.substr(2, 2), 16),
18638
18661
  b: parseInt(hex.substr(4, 2), 16),
18639
- 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
18640
18663
  };
18641
18664
  }
18642
18665
 
@@ -25744,6 +25767,173 @@
25744
25767
  return text || text === 0; // accept numerical 0 as label text
25745
25768
  }
25746
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
+
25747
25937
  /* example patterns
25748
25938
  hatches 1px black 1px red 1px white
25749
25939
  1px black 1px red 1px white // same as above (hatches is default)
@@ -26035,7 +26225,10 @@
26035
26225
  length: 'number', // e.g. arrow length
26036
26226
  rotation: 'number',
26037
26227
  radius: 'number',
26038
- 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
26039
26232
  flipped: 'boolean',
26040
26233
  rotated: 'boolean',
26041
26234
  direction: 'number',
@@ -26054,6 +26247,18 @@
26054
26247
  effect: null // e.g. "fade"
26055
26248
  }, stylePropertyTypes);
26056
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
+
26057
26262
  var commonProperties = 'css,class,opacity,stroke,stroke-width,stroke-dasharray,stroke-opacity,fill-opacity,vector-effect'.split(',');
26058
26263
 
26059
26264
  var propertiesBySymbolType = {
@@ -26129,7 +26334,9 @@
26129
26334
  return;
26130
26335
  }
26131
26336
  var val = opts[optName];
26132
- functions[svgName] = getSymbolPropertyAccessor(val, svgName, lyr);
26337
+ functions[svgName] = svgName in symbolListProperties ?
26338
+ getSymbolListAccessor(val, svgName, lyr) :
26339
+ getSymbolPropertyAccessor(val, svgName, lyr);
26133
26340
  properties.push(svgName);
26134
26341
  });
26135
26342
 
@@ -26159,6 +26366,21 @@
26159
26366
  return getPropertyAccessor(val, symbolPropertyTypes[svgName], lyr, svgName);
26160
26367
  }
26161
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
+ }
26162
26384
  // Returns a function that maps a feature id to a property value. The value may
26163
26385
  // be given as a literal, as the name of a data field, or as a JS expression
26164
26386
  // evaluated against each feature.
@@ -26295,6 +26517,7 @@
26295
26517
  getLabelPositionStyle: getLabelPositionStyle,
26296
26518
  getPropertyAccessor: getPropertyAccessor,
26297
26519
  getSymbolDataAccessor: getSymbolDataAccessor,
26520
+ getSymbolListAccessor: getSymbolListAccessor,
26298
26521
  getSymbolPropertyAccessor: getSymbolPropertyAccessor,
26299
26522
  isSupportedSvgStyleProperty: isSupportedSvgStyleProperty,
26300
26523
  isSvgClassName: isSvgClassName,
@@ -37978,139 +38201,6 @@ ${svg}
37978
38201
  }
37979
38202
  }
37980
38203
 
37981
- var assignmentRxp = /^([a-z0-9_+-]+)=(?!=)(.*)$/i; // exclude ==
37982
-
37983
- function splitShellTokens(str) {
37984
- var BAREWORD = `([^'"\\s])+`;
37985
- var DOUBLE_QUOTE = `"((\\\\"|[^"])*?)"`;
37986
- var SINGLE_QUOTE = `'((\\\\'|[^'])*?)'`;
37987
- var rxp = new RegExp('(' + BAREWORD + '|' + SINGLE_QUOTE + '|' + DOUBLE_QUOTE + ')*', 'g');
37988
- var matches = str.match(rxp) || [];
37989
- var chunks = matches.filter(function(chunk) {
37990
- // single backslashes may be present in multiline commands pasted from a makefile, e.g.
37991
- return !!chunk && chunk != '\\';
37992
- }).map(utils.trimQuotes);
37993
- return chunks;
37994
- }
37995
-
37996
- function parsePercent(o) {
37997
- var str = String(o);
37998
- var isPct = str.indexOf('%') > 0;
37999
- var pct;
38000
- if (isPct) {
38001
- pct = Number(str.replace('%', '')) / 100;
38002
- } else {
38003
- pct = Number(str);
38004
- }
38005
- if (!(pct >= 0 && pct <= 1)) {
38006
- stop$1(utils.format("Invalid percentage: %s", str));
38007
- }
38008
- return pct;
38009
- }
38010
-
38011
- function parseNumberList(token) {
38012
- return token.split(',').map(parseFloat);
38013
- }
38014
-
38015
- // Split comma-delimited list, trim quotes from entire list and
38016
- // individual members
38017
- function parseStringList(token) {
38018
- var delim = ',';
38019
- var list = splitOptionList(token, delim);
38020
- if (list.length == 1) {
38021
- list = splitOptionList(list[0], delim);
38022
- }
38023
- return list;
38024
- }
38025
-
38026
- // Accept spaces and/or commas as delimiters
38027
- function parseColorList(token) {
38028
- var delim = ', ';
38029
- // accept rgb(0 0 0) rgb(0,0,0) rgb(0, 0, 0)
38030
- var token2 = token.replace(/[ ,] *(?=[^(]*\))/g, '~~~'); // kludge: protect rgba() functions from being split apart
38031
- var list = splitOptionList(token2, delim);
38032
- if (list.length == 1) {
38033
- list = splitOptionList(list[0], delim);
38034
- }
38035
- list = list.map(function(str) {
38036
- return str.replace(/~~~/g, ',');
38037
- });
38038
- return list;
38039
- }
38040
-
38041
- function cleanArgv(argv) {
38042
- // Note: original trim caused some quoted spaces to be removed
38043
- // (e.g. bash shell seems to convert [delimiter=" "] to [delimiter= ],
38044
- // which then got trimmed to [delimiter=] below)
38045
- //// argv = argv.map(function(s) {return s.trim();}); // trim whitespace
38046
-
38047
- // Updated: don't trim space from tokens like [delimeter= ]
38048
- argv = argv.map(function(s) {
38049
- if (!/= $/.test(s)) {
38050
- s = utils.rtrim(s);
38051
- }
38052
- s = utils.ltrim(s);
38053
- return s;
38054
- });
38055
- argv = argv.filter(function(s) {return s !== '';}); // remove empty tokens
38056
- // Note: removing trimQuotes() call... now, strings like 'name="Meg"' will no longer
38057
- // be parsed the same way as name=Meg and name="Meg"
38058
- //// argv = argv.map(utils.trimQuotes); // remove one level of single or dbl quotes
38059
- return argv;
38060
- }
38061
-
38062
- function splitOptionList(str, delimChars) {
38063
- var BAREWORD = '([^' + delimChars + '\'"][^' + delimChars + ']*)'; // TODO: make safer
38064
- var DOUBLE_QUOTE = '"((\\\\"|[^"])*?)"';
38065
- var SINGLE_QUOTE = '\'((\\\\\'|[^\'])*?)\'';
38066
- var rxp = new RegExp('^(' + BAREWORD + '|' + SINGLE_QUOTE + '|' + DOUBLE_QUOTE + ')([' + delimChars + ']+|$)');
38067
- var chunks = [];
38068
- var match;
38069
- while ((match = rxp.exec(str)) !== null) {
38070
- chunks.push(match[1]);
38071
- str = str.substr(match[0].length);
38072
- }
38073
- return chunks.filter(function(chunk) {
38074
- return !!chunk && chunk != '\\';
38075
- }).map(utils.trimQuotes);
38076
- }
38077
-
38078
- // Prepare a value to be used as an option value.
38079
- // Places quotes around strings containing spaces.
38080
- // e.g. converts Layer 1 -> "Layer 1"
38081
- // for use in contexts like: name="Layer 1"
38082
- function formatOptionValue(val) {
38083
- val = String(val);
38084
- if (val.indexOf(' ') > -1) {
38085
- val = JSON.stringify(val); // quote ids with spaces
38086
- }
38087
- return val;
38088
- }
38089
-
38090
- function isAssignment(token) {
38091
- return assignmentRxp.test(token);
38092
- }
38093
-
38094
- function splitAssignment(token) {
38095
- var match = assignmentRxp.exec(token),
38096
- name = match[1],
38097
- val = utils.trimQuotes(match[2]);
38098
- return [name, val];
38099
- }
38100
-
38101
- var OptionParsingUtils = /*#__PURE__*/Object.freeze({
38102
- __proto__: null,
38103
- cleanArgv: cleanArgv,
38104
- formatOptionValue: formatOptionValue,
38105
- isAssignment: isAssignment,
38106
- parseColorList: parseColorList,
38107
- parseNumberList: parseNumberList,
38108
- parsePercent: parsePercent,
38109
- parseStringList: parseStringList,
38110
- splitAssignment: splitAssignment,
38111
- splitShellTokens: splitShellTokens
38112
- });
38113
-
38114
38204
  function CommandParser() {
38115
38205
  var commandRxp = /^--?([a-z][\w-]*)$/i,
38116
38206
  invalidCommandRxp = /^--?[a-z][\w-]*[=]/i, // e.g. -target=A // could be more general
@@ -38290,6 +38380,7 @@ ${svg}
38290
38380
  // Try to read an option for command @cmdDef from @argv
38291
38381
  function readOption(cmd, argv, cmdDef) {
38292
38382
  var token = argv.shift(),
38383
+ assignedValue = null,
38293
38384
  optName, optDef, parts;
38294
38385
 
38295
38386
  if (isAssignment(token)) {
@@ -38299,7 +38390,10 @@ ${svg}
38299
38390
  if (!optDef) ; else if (optDef.type == 'flag' || optDef.assign_to) {
38300
38391
  stop$1("-" + cmdDef.name + " " + parts[0] + " option doesn't take a value");
38301
38392
  } else {
38302
- 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];
38303
38397
  }
38304
38398
  } else {
38305
38399
  // try to parse as a flag option,
@@ -38324,6 +38418,8 @@ ${svg}
38324
38418
  cmd.options[optDef.assign_to] = optDef.name;
38325
38419
  } else if (optDef.type == 'flag') {
38326
38420
  cmd.options[optName] = true;
38421
+ } else if (assignedValue !== null) {
38422
+ cmd.options[optName] = parseOptionValue(assignedValue, optDef);
38327
38423
  } else {
38328
38424
  cmd.options[optName] = readOptionValue(argv, optDef);
38329
38425
  }
@@ -40287,9 +40383,9 @@ ${svg}
40287
40383
  .option('no-replace', noReplaceOpt);
40288
40384
 
40289
40385
  parser.command('grid')
40290
- .describe('create a grid of square, hexagonal, rhombus or triangle polygons')
40386
+ .describe('create a grid of square, hexagonal, rhombus, triangle or cairo polygons')
40291
40387
  .option('type', {
40292
- describe: 'square, square2, hex, hex2, rhombus, rhombus2, triangle or triangle2 (default is square)'
40388
+ describe: 'square, square2, hex, hex2, rhombus, rhombus2, triangle, triangle2 or cairo (default is square)'
40293
40389
  })
40294
40390
  .option('interval', {
40295
40391
  describe: 'side length (e.g. 500m, 12km)',
@@ -40311,6 +40407,10 @@ ${svg}
40311
40407
  describe: 'scale factor for cells, between 0 and 2',
40312
40408
  type: 'number'
40313
40409
  })
40410
+ .option('rotate', {
40411
+ describe: 'rotation angle in degrees (default is 0)',
40412
+ type: 'number'
40413
+ })
40314
40414
  // .option('bbox', {
40315
40415
  // type: 'bbox',
40316
40416
  // describe: 'xmin,ymin,xmax,ymax (default is bbox of data)'
@@ -41028,7 +41128,7 @@ ${svg}
41028
41128
  .option('target', targetOpt);
41029
41129
 
41030
41130
  parser.command('repel')
41031
- .describe('move overlapping circle symbols apart')
41131
+ .describe('move overlapping point symbols apart')
41032
41132
  .option('width', {
41033
41133
  describe: 'display width of the layer in pixels (required if no frame)',
41034
41134
  type: 'number'
@@ -41039,6 +41139,9 @@ ${svg}
41039
41139
  .option('padding', {
41040
41140
  describe: 'pixels of clearance around each symbol; may vary (default is 0)'
41041
41141
  })
41142
+ .option('margin', {
41143
+ describe: 'pixels of clearance between two symbols; may vary (default is 0)'
41144
+ })
41042
41145
  .option('ticks', {
41043
41146
  describe: 'number of solver passes (default is 100)',
41044
41147
  type: 'number'
@@ -41058,7 +41161,7 @@ ${svg}
41058
41161
  parser.command('symbols')
41059
41162
  .describe('symbolize points as arrows, circles, stars, polygons, etc.')
41060
41163
  .option('type', {
41061
- describe: 'types: arrow, circle, square, star, polygon, ring'
41164
+ describe: 'types: arrow, circle, square, star, polygon, ring, pie'
41062
41165
  })
41063
41166
  .option('stroke', {})
41064
41167
  .option('stroke-width', {})
@@ -41117,6 +41220,15 @@ ${svg}
41117
41220
  .option('radii', {
41118
41221
  describe: '(ring) comma-sep. list of concentric radii, ascending order'
41119
41222
  })
41223
+ .option('hole', {
41224
+ describe: '(pie) radius of the hole in a donut symbol'
41225
+ })
41226
+ .option('values', {
41227
+ describe: '(pie) comma-sep. list of wedge values (numbers or fields)'
41228
+ })
41229
+ .option('fills', {
41230
+ describe: '(pie) comma-sep. list of wedge fill colors'
41231
+ })
41120
41232
  .option('arrow-style', {
41121
41233
  describe: '(arrow) options: stick, standard (default is standard)'
41122
41234
  })
@@ -44368,9 +44480,10 @@ ${svg}
44368
44480
  ({gpkg, tmpPath} = await openGeoPackage(input, geopackage));
44369
44481
  var availableLayers;
44370
44482
  var filterApplied;
44483
+ var unreadableLayers;
44371
44484
  try {
44372
44485
  try {
44373
- ({datasets, availableLayers, filterApplied} = readFeatureTableDatasets(gpkg, opts));
44486
+ ({datasets, availableLayers, filterApplied, unreadableLayers} = readFeatureTableDatasets(gpkg, opts));
44374
44487
  } catch (e) {
44375
44488
  if (!runningInBrowser() && isLocalCsProjError(e)) {
44376
44489
  gpkg.close();
@@ -44383,7 +44496,7 @@ ${svg}
44383
44496
  throw e;
44384
44497
  }
44385
44498
  gpkg = await geopackage.GeoPackageAPI.open(tmpPath);
44386
- ({datasets, availableLayers, filterApplied} = readFeatureTableDatasets(gpkg, opts));
44499
+ ({datasets, availableLayers, filterApplied, unreadableLayers} = readFeatureTableDatasets(gpkg, opts));
44387
44500
  } else {
44388
44501
  throw e;
44389
44502
  }
@@ -44398,6 +44511,7 @@ ${svg}
44398
44511
  layers: [{name: '', data: null}],
44399
44512
  info: {
44400
44513
  _gpkg_available_layers: availableLayers,
44514
+ _gpkg_unreadable_layers: unreadableLayers,
44401
44515
  _gpkg_placeholder: !!filterApplied
44402
44516
  }
44403
44517
  };
@@ -44409,6 +44523,7 @@ ${svg}
44409
44523
  mergedArr.forEach(function(ds) {
44410
44524
  ds.info = ds.info || {};
44411
44525
  ds.info._gpkg_available_layers = availableLayers;
44526
+ ds.info._gpkg_unreadable_layers = unreadableLayers;
44412
44527
  });
44413
44528
  return merged;
44414
44529
  }
@@ -44534,10 +44649,38 @@ ${svg}
44534
44649
  var availableLayers = gpkg.getFeatureTables() || [];
44535
44650
  var tables = filterGeoPackageTables(availableLayers, selected);
44536
44651
  var filterApplied = Array.isArray(selected) && selected.length > 0;
44537
- var datasets = tables.map(function(table) {
44538
- return readFeatureTable(gpkg, table, opts);
44652
+ var unreadableLayers = [];
44653
+ var datasets = [];
44654
+ tables.forEach(function(table) {
44655
+ try {
44656
+ datasets.push(readFeatureTable(gpkg, table, opts));
44657
+ } catch (e) {
44658
+ // The caller recovers from a CRS parsing failure by rewriting the file's
44659
+ // CRS metadata and re-reading, so that error must not be swallowed here.
44660
+ if (isLocalCsProjError(e)) throw e;
44661
+ unreadableLayers.push(table);
44662
+ warn(formatUnreadableTableMessage(table, e));
44663
+ }
44539
44664
  });
44540
- return {datasets: datasets, availableLayers: availableLayers, filterApplied: filterApplied};
44665
+ return {
44666
+ datasets: datasets,
44667
+ availableLayers: availableLayers,
44668
+ filterApplied: filterApplied,
44669
+ unreadableLayers: unreadableLayers
44670
+ };
44671
+ }
44672
+
44673
+ // A GeoPackage can advertise feature tables that aren't readable. USGS National
44674
+ // Map vector products, for instance, list a CLIPPOLY table in gpkg_contents and
44675
+ // gpkg_geometry_columns after dropping the table itself from the file. GDAL
44676
+ // skips such layers with a warning; do the same rather than failing the whole
44677
+ // import over one bad table.
44678
+ function formatUnreadableTableMessage(table, err) {
44679
+ var msg = err && err.message ? String(err.message) : '';
44680
+ var reason = /^Table does not exist:/.test(msg) ?
44681
+ 'the table is listed in the GeoPackage metadata but is missing from the file' :
44682
+ msg || 'unknown error';
44683
+ return 'Skipping GeoPackage layer "' + table + '": ' + reason;
44541
44684
  }
44542
44685
 
44543
44686
  function getSelectedGeoPackageLayers(opts) {
@@ -46510,12 +46653,12 @@ ${svg}
46510
46653
  var coords = path.coords;
46511
46654
  if (coords.length < 2) return;
46512
46655
  if (forcePolylinePaths) {
46513
- lines.push(path.closed || pointsEqual$1(coords[0], coords[coords.length - 1]) ? closeRing(coords) : coords);
46656
+ lines.push(path.closed || pointsEqual$1(coords[0], coords[coords.length - 1]) ? closeRing$1(coords) : coords);
46514
46657
  return;
46515
46658
  }
46516
46659
  if (path.closed || pointsEqual$1(coords[0], coords[coords.length - 1])) {
46517
46660
  if (coords.length >= 3) {
46518
- rings.push(closeRing(coords));
46661
+ rings.push(closeRing$1(coords));
46519
46662
  }
46520
46663
  } else {
46521
46664
  lines.push(coords);
@@ -46819,7 +46962,7 @@ ${svg}
46819
46962
  return /translate\(/i.test(String(str || ''));
46820
46963
  }
46821
46964
 
46822
- function closeRing(coords) {
46965
+ function closeRing$1(coords) {
46823
46966
  var ring = coords.map(function(p) { return [p[0], p[1]]; });
46824
46967
  if (!pointsEqual$1(ring[0], ring[ring.length - 1])) {
46825
46968
  ring.push([ring[0][0], ring[0][1]]);
@@ -47296,15 +47439,12 @@ ${svg}
47296
47439
  var files = opts.files || [];
47297
47440
  var dataset, datasets, target;
47298
47441
 
47442
+ // copy opts, so parameters can be modified within this command
47443
+ opts = Object.assign({}, opts);
47444
+ opts.input = Object.assign({}, opts.input); // make sure we have a cache
47445
+
47299
47446
  if (opts.stdin) {
47300
- datasets = await importDatasetsFromFile('/dev/stdin', opts);
47301
- catalog.addDatasets(datasets);
47302
- if (datasets.length > 1) {
47303
- catalog.setDefaultTargets(datasets.map(function(ds) {
47304
- return {dataset: ds, layers: ds.layers};
47305
- }));
47306
- }
47307
- return normalizeImportedTarget(datasets);
47447
+ files = [readStdinAsFile(opts.input)];
47308
47448
  }
47309
47449
 
47310
47450
  if (files.length > 0 === false) {
@@ -47313,10 +47453,6 @@ ${svg}
47313
47453
 
47314
47454
  verbose("Importing: " + files.join(' '));
47315
47455
 
47316
- // copy opts, so parameters can be modified within this command
47317
- opts = Object.assign({}, opts);
47318
- opts.input = Object.assign({}, opts.input); // make sure we have a cache
47319
-
47320
47456
  convertDataObjects(files, opts.input);
47321
47457
 
47322
47458
  files = expandFiles(files, opts.input);
@@ -47397,6 +47533,41 @@ ${svg}
47397
47533
  return obj.target;
47398
47534
  }
47399
47535
 
47536
+ // Read the stdin stream into @cache and return the filename to import it as.
47537
+ // A zipped dataset is given a .zip or .kmz name, so that it is unpacked by
47538
+ // expandFiles() like an archive passed as a file argument. Stdin carries no
47539
+ // filename to identify its contents, so an archive is detected from the ZIP
47540
+ // magic number, and a KMZ is told from an ordinary zip by its doc.kml entry.
47541
+ function readStdinAsFile(cache) {
47542
+ var path = '/dev/stdin';
47543
+ var content = cli.readFile(path, null, cache);
47544
+ var name = path;
47545
+ if (contentLooksZipped(content)) {
47546
+ name = zipContainsDocKml(content) ? 'stdin.kmz' : 'stdin.zip';
47547
+ }
47548
+ cache[name] = content;
47549
+ return name;
47550
+ }
47551
+
47552
+ // Detect the ZIP local file header signature ("PK\x03\x04")
47553
+ function contentLooksZipped(content) {
47554
+ var bytes = content instanceof ArrayBuffer ? new Uint8Array(content) : content;
47555
+ if (utils.isString(bytes) || !bytes || bytes.length < 4) return false;
47556
+ return bytes[0] == 0x50 && bytes[1] == 0x4b && bytes[2] == 0x03 && bytes[3] == 0x04;
47557
+ }
47558
+
47559
+ function zipContainsDocKml(content) {
47560
+ var names;
47561
+ try {
47562
+ names = listZipEntryNames(content);
47563
+ } catch(e) {
47564
+ return false; // let the unzipping step report a malformed archive
47565
+ }
47566
+ return names.some(function(name) {
47567
+ return parseLocalPath(name).filename.toLowerCase() == 'doc.kml';
47568
+ });
47569
+ }
47570
+
47400
47571
  function expandFiles(files, cache) {
47401
47572
  var files2 = [];
47402
47573
  files.forEach(function(file) {
@@ -47528,6 +47699,7 @@ ${svg}
47528
47699
  content = cli.readFile(path, encoding || 'utf-8', cache);
47529
47700
  fileType = guessInputContentType(content);
47530
47701
  if (fileType == 'text' && content.indexOf('\ufffd') > -1) {
47702
+ // invalidate string data that contains the 'replacement character'
47531
47703
  fileType = null;
47532
47704
  }
47533
47705
  }
@@ -47536,7 +47708,7 @@ ${svg}
47536
47708
  stop$1(getUnsupportedFileMessage(path));
47537
47709
  }
47538
47710
  input[fileType] = {filename: path, content: content};
47539
- content = null;
47711
+ content = null; // for g.c.
47540
47712
  if (fileType == 'shp' || fileType == 'dbf') {
47541
47713
  readShapefileAuxFiles(path, input, cache);
47542
47714
  } else if (isRasterImageInputType(fileType)) {
@@ -47581,12 +47753,14 @@ ${svg}
47581
47753
  function validateAndCleanGpkgSelection(datasets, opts) {
47582
47754
  var availableSet = new Set();
47583
47755
  var importedSet = new Set();
47756
+ var unreadableSet = new Set();
47584
47757
  var sawGpkg = false;
47585
47758
  datasets.forEach(function(ds) {
47586
47759
  var info = ds && ds.info;
47587
47760
  if (!info || !Array.isArray(info._gpkg_available_layers)) return;
47588
47761
  sawGpkg = true;
47589
47762
  info._gpkg_available_layers.forEach(function(name) { availableSet.add(name); });
47763
+ (info._gpkg_unreadable_layers || []).forEach(function(name) { unreadableSet.add(name); });
47590
47764
  if (!info._gpkg_placeholder) {
47591
47765
  (ds.layers || []).forEach(function(lyr) {
47592
47766
  if (lyr && lyr.name) importedSet.add(lyr.name);
@@ -47594,9 +47768,17 @@ ${svg}
47594
47768
  }
47595
47769
  });
47596
47770
  if (sawGpkg && Array.isArray(opts.layers) && opts.layers.length > 0) {
47597
- var missing = opts.layers.filter(function(name) {
47771
+ var absent = opts.layers.filter(function(name) {
47598
47772
  return !importedSet.has(name);
47599
47773
  });
47774
+ // A layer that the file advertises but that can't be parsed is skipped with
47775
+ // a warning during import; report it separately here, because saying it is
47776
+ // missing while also listing it as an existing layer is contradictory.
47777
+ var unreadable = absent.filter(function(name) { return unreadableSet.has(name); });
47778
+ var missing = absent.filter(function(name) { return !unreadableSet.has(name); });
47779
+ if (unreadable.length > 0) {
47780
+ stop$1('Unable to read GeoPackage layer(s): ' + unreadable.join(', '));
47781
+ }
47600
47782
  if (missing.length > 0) {
47601
47783
  stop$1(
47602
47784
  'Missing GeoPackage layer(s): ' + missing.join(', ') + '\n' +
@@ -47610,6 +47792,7 @@ ${svg}
47610
47792
  cleaned.forEach(function(ds) {
47611
47793
  if (ds && ds.info) {
47612
47794
  delete ds.info._gpkg_available_layers;
47795
+ delete ds.info._gpkg_unreadable_layers;
47613
47796
  delete ds.info._gpkg_placeholder;
47614
47797
  }
47615
47798
  });
@@ -48420,7 +48603,7 @@ ${svg}
48420
48603
  const resulterrbound = (3 + 8 * epsilon) * epsilon;
48421
48604
 
48422
48605
  // fast_expansion_sum_zeroelim routine from oritinal code
48423
- function sum(elen, e, flen, f, h) {
48606
+ function sum$1(elen, e, flen, f, h) {
48424
48607
  let Q, Qnew, hh, bvirt;
48425
48608
  let enow = e[0];
48426
48609
  let fnow = f[0];
@@ -48603,7 +48786,7 @@ ${svg}
48603
48786
  bvirt = u3 - _j;
48604
48787
  u[2] = _j - (u3 - bvirt) + (_i - bvirt);
48605
48788
  u[3] = u3;
48606
- const C1len = sum(4, B$2, 4, u, C1);
48789
+ const C1len = sum$1(4, B$2, 4, u, C1);
48607
48790
 
48608
48791
  s1 = acx * bcytail;
48609
48792
  c = splitter * acx;
@@ -48634,7 +48817,7 @@ ${svg}
48634
48817
  bvirt = u3 - _j;
48635
48818
  u[2] = _j - (u3 - bvirt) + (_i - bvirt);
48636
48819
  u[3] = u3;
48637
- const C2len = sum(C1len, C1, 4, u, C2);
48820
+ const C2len = sum$1(C1len, C1, 4, u, C2);
48638
48821
 
48639
48822
  s1 = acxtail * bcytail;
48640
48823
  c = splitter * acxtail;
@@ -48665,7 +48848,7 @@ ${svg}
48665
48848
  bvirt = u3 - _j;
48666
48849
  u[2] = _j - (u3 - bvirt) + (_i - bvirt);
48667
48850
  u[3] = u3;
48668
- const Dlen = sum(C2len, C2, 4, u, D$1);
48851
+ const Dlen = sum$1(C2len, C2, 4, u, D$1);
48669
48852
 
48670
48853
  return D$1[Dlen - 1];
48671
48854
  }
@@ -74376,7 +74559,7 @@ ${svg}
74376
74559
  cmd.polygonGrid = function(targetLayers, targetDataset, opts) {
74377
74560
  requireProjectedDataset(targetDataset);
74378
74561
  var params = getGridParams(targetLayers, targetDataset, opts);
74379
- var gridDataset = makeGridDataset(params); // grid is a new dataset
74562
+ var gridDataset = makeGridDataset(params, opts); // grid is a new dataset
74380
74563
  gridDataset.info = copyDatasetInfo(targetDataset.info);
74381
74564
  setOutputLayerName(gridDataset.layers[0], null, 'grid', opts);
74382
74565
  if (opts.debug) gridDataset.layers.push(cmd.pointGrid2(targetLayers, targetDataset, opts));
@@ -74440,35 +74623,67 @@ ${svg}
74440
74623
 
74441
74624
  function makeGridDataset(params, opts) {
74442
74625
  var geojson, dataset;
74626
+ var rotation = opts.rotate || 0;
74627
+ var genParams = rotation ? getRotatedGridParams(params, rotation) : params;
74443
74628
  if (params.type == 'square') {
74444
- geojson = getSquareGridGeoJSON(getSquareGridCoordinates(params));
74629
+ geojson = getSquareGridGeoJSON(getSquareGridCoordinates(genParams));
74445
74630
  } else if (params.type == 'square2') {
74446
- geojson = getRotatedSquareGridGeoJSON(params);
74631
+ geojson = getRotatedSquareGridGeoJSON(genParams);
74447
74632
  } else if (params.type == 'hex') {
74448
- geojson = getHexGridGeoJSON(getHexGridCoordinates(params));
74633
+ geojson = getHexGridGeoJSON(getHexGridCoordinates(genParams));
74449
74634
  } else if (params.type == 'hex2') {
74450
74635
  // use rotated grid
74451
- geojson = getHexGridGeoJSON(getHexGridCoordinates(swapGridParams(params)));
74636
+ geojson = getHexGridGeoJSON(getHexGridCoordinates(swapGridParams(genParams)));
74452
74637
  swapPolygonCoords(geojson);
74453
74638
  } else if (params.type == 'rhombus') {
74454
- geojson = getRhombusGridGeoJSON(params, false);
74639
+ geojson = getRhombusGridGeoJSON(genParams, false);
74455
74640
  } else if (params.type == 'rhombus2') {
74456
- geojson = getRhombusGridGeoJSON(params, true);
74641
+ geojson = getRhombusGridGeoJSON(genParams, true);
74457
74642
  } else if (params.type == 'triangle') {
74458
- geojson = getTriangleGridGeoJSON(params, false);
74643
+ geojson = getTriangleGridGeoJSON(genParams, false);
74459
74644
  } else if (params.type == 'triangle2') {
74460
- geojson = getTriangleGridGeoJSON(params, true);
74645
+ geojson = getTriangleGridGeoJSON(genParams, true);
74646
+ } else if (params.type == 'cairo') {
74647
+ geojson = getCairoGridGeoJSON(genParams);
74461
74648
  } else {
74462
74649
  stop$1('Unsupported grid type');
74463
74650
  }
74464
74651
  scaleGridCells(geojson, params.cellScale);
74465
74652
  alignGridToBounds(geojson, params.bbox);
74653
+ if (rotation) {
74654
+ rotateGridCells(geojson, rotation, params.bbox);
74655
+ }
74466
74656
  cullGridCells(geojson, params.bbox);
74467
74657
  dataset = importGeoJSON(geojson, {});
74468
74658
  buildTopology(dataset);
74469
74659
  return dataset;
74470
74660
  }
74471
74661
 
74662
+ // Expand the generation rectangle so that, after rotation around the shared
74663
+ // center, the grid still covers the original target bbox.
74664
+ function getRotatedGridParams(params, rotation) {
74665
+ var angle = rotation * Math.PI / 180;
74666
+ var cos = Math.abs(Math.cos(angle));
74667
+ var sin = Math.abs(Math.sin(angle));
74668
+ return utils.defaults({
74669
+ width: params.width * cos + params.height * sin,
74670
+ height: params.width * sin + params.height * cos
74671
+ }, params);
74672
+ }
74673
+
74674
+ function rotateGridCells(geojson, rotation, bbox) {
74675
+ var cx = (bbox[0] + bbox[2]) / 2;
74676
+ var cy = (bbox[1] + bbox[3]) / 2;
74677
+ var transform = getAffineTransform(rotation, 1, [0, 0], [cx, cy]);
74678
+ geojson.geometries.forEach(function(geom) {
74679
+ if (geom.type == 'Polygon') {
74680
+ geom.coordinates[0] = geom.coordinates[0].map(function(xy) {
74681
+ return transform(xy[0], xy[1]);
74682
+ });
74683
+ }
74684
+ });
74685
+ }
74686
+
74472
74687
  function swapGridParams(params) {
74473
74688
  var bbox = params.bbox;
74474
74689
  return utils.defaults({
@@ -74550,6 +74765,7 @@ ${svg}
74550
74765
  if (type == 'hex' || type == 'hex2') return 3 * Math.sqrt(3) / 2;
74551
74766
  if (type == 'rhombus' || type == 'rhombus2') return Math.sqrt(3) / 2;
74552
74767
  if (type == 'triangle' || type == 'triangle2') return Math.sqrt(3) / 4;
74768
+ if (type == 'cairo') return (4 + Math.sqrt(7)) / 4;
74553
74769
  stop$1('Unsupported grid type');
74554
74770
  }
74555
74771
 
@@ -74703,6 +74919,95 @@ ${svg}
74703
74919
  return geojson;
74704
74920
  }
74705
74921
 
74922
+ // Equilateral type-4 Cairo tessellation. Four-valent (90°) vertices sit on a
74923
+ // square lattice of spacing S = interval * (√7+1)/2; each lattice square
74924
+ // contains a central bar of length `interval`, alternating horizontal and
74925
+ // vertical. Two pentagons share each bar.
74926
+ //
74927
+ // Vertices are computed once and reused. Algebraically equal expressions
74928
+ // such as i*S and (i-1)*S + S are not bit-identical, and independently
74929
+ // computed copies of the same 4-way junction become T-intersections.
74930
+ function getCairoGridGeoJSON(params) {
74931
+ var side = params.interval;
74932
+ var spacing = side * (Math.sqrt(7) + 1) / 2;
74933
+ var halfBar = side / 2;
74934
+ var iMin = -1;
74935
+ var jMin = -1;
74936
+ var iMax = Math.ceil(params.width / spacing) + 1;
74937
+ var jMax = Math.ceil(params.height / spacing) + 1;
74938
+ var lattice = [];
74939
+ var bars = [];
74940
+ var geometries = [];
74941
+ var i, j, y, cx, cy;
74942
+
74943
+ for (j = jMin - 1; j <= jMax + 1; j++) {
74944
+ y = j * spacing;
74945
+ lattice[j] = [];
74946
+ for (i = iMin - 1; i <= iMax + 1; i++) {
74947
+ lattice[j][i] = [i * spacing, y];
74948
+ }
74949
+ }
74950
+
74951
+ for (j = jMin - 1; j < jMax + 1; j++) {
74952
+ bars[j] = [];
74953
+ for (i = iMin - 1; i < iMax + 1; i++) {
74954
+ cx = (i + 0.5) * spacing;
74955
+ cy = (j + 0.5) * spacing;
74956
+ bars[j][i] = (i + j) % 2 === 0 ? {
74957
+ a: [cx - halfBar, cy],
74958
+ b: [cx + halfBar, cy]
74959
+ } : {
74960
+ a: [cx, cy - halfBar],
74961
+ b: [cx, cy + halfBar]
74962
+ };
74963
+ }
74964
+ }
74965
+
74966
+ for (j = jMin; j < jMax; j++) {
74967
+ for (i = iMin; i < iMax; i++) {
74968
+ if ((i + j) % 2 === 0) {
74969
+ appendCairoPentagon(geometries, [
74970
+ lattice[j][i],
74971
+ bars[j - 1][i].b,
74972
+ lattice[j][i + 1],
74973
+ bars[j][i].b,
74974
+ bars[j][i].a
74975
+ ]);
74976
+ appendCairoPentagon(geometries, [
74977
+ lattice[j + 1][i],
74978
+ bars[j][i].a,
74979
+ bars[j][i].b,
74980
+ lattice[j + 1][i + 1],
74981
+ bars[j + 1][i].a
74982
+ ]);
74983
+ } else {
74984
+ appendCairoPentagon(geometries, [
74985
+ lattice[j][i],
74986
+ bars[j][i].a,
74987
+ bars[j][i].b,
74988
+ lattice[j + 1][i],
74989
+ bars[j][i - 1].b
74990
+ ]);
74991
+ appendCairoPentagon(geometries, [
74992
+ lattice[j][i + 1],
74993
+ bars[j][i + 1].a,
74994
+ lattice[j + 1][i + 1],
74995
+ bars[j][i].b,
74996
+ bars[j][i].a
74997
+ ]);
74998
+ }
74999
+ }
75000
+ }
75001
+ return {type: 'GeometryCollection', geometries: geometries};
75002
+ }
75003
+
75004
+ function appendCairoPentagon(geometries, verts) {
75005
+ geometries.push({
75006
+ type: 'Polygon',
75007
+ coordinates: [verts.concat([verts[0]])]
75008
+ });
75009
+ }
75010
+
74706
75011
  function subdivideHexagon(coords) {
74707
75012
  var center = getPolygonRingCenter(coords);
74708
75013
  return [
@@ -75425,16 +75730,180 @@ ${svg}
75425
75730
  };
75426
75731
  }
75427
75732
 
75733
+ var roundCoord$1 = getRoundingFunction(0.01);
75734
+
75735
+ function getSymbolFillColor(d) {
75736
+ return d.fill || 'magenta';
75737
+ }
75738
+
75739
+ function getSymbolStrokeColor(d) {
75740
+ return d.stroke || d.fill || 'magenta';
75741
+ }
75742
+
75743
+ function applySymbolStyles(sym, d) {
75744
+ if (sym.type == 'polyline') {
75745
+ sym.stroke = getSymbolStrokeColor(d);
75746
+ } else {
75747
+ sym.fill = getSymbolFillColor(d);
75748
+ }
75749
+ if (d.opacity) {
75750
+ sym.opacity = d.opacity;
75751
+ }
75752
+ return sym;
75753
+ }
75754
+
75755
+ function getSymbolRadius(d) {
75756
+ if (d.radius === 0 || d.length === 0 || d.r === 0) return 0;
75757
+ return d.radius || d.length || d.r || 5; // use a default value
75758
+ }
75759
+
75760
+ // Converts the value of a list-valued option into an array of items. The value
75761
+ // arrives either as a whole string (from a direct call, rather than from the
75762
+ // -symbols option accessor) or as an array of items already resolved one by
75763
+ // one. An item that holds an array of its own, as an expression like
75764
+ // [R, R * 0.5] does, is flattened into the list. Items themselves are left
75765
+ // unsplit, because a single item can contain a comma, as rgba() colors do.
75766
+ function toItemList(val) {
75767
+ var items = utils.isString(val) ? splitListItems(val) : [].concat(val);
75768
+ var list = [];
75769
+ for (var i=0; i<items.length; i++) {
75770
+ if (Array.isArray(items[i])) {
75771
+ list = list.concat(items[i]);
75772
+ } else {
75773
+ list.push(items[i]);
75774
+ }
75775
+ }
75776
+ return list;
75777
+ }
75778
+
75779
+ // Like toItemList(), for options that take numbers. An item that is a string
75780
+ // containing commas is split, so that a data field holding a value like "2,4"
75781
+ // works as a list. Values that aren't numbers become NaN, for the caller to
75782
+ // report or ignore.
75783
+ function toNumberList(val) {
75784
+ var list = [];
75785
+ toItemList(val).forEach(function(item) {
75786
+ if (utils.isString(item) && item.indexOf(',') > -1) {
75787
+ list = list.concat(parseNumberList(item));
75788
+ } else {
75789
+ list.push(Number(item));
75790
+ }
75791
+ });
75792
+ return list;
75793
+ }
75794
+
75795
+ // Returns the radius of the smallest circle centered on a symbol's point that
75796
+ // covers the symbol, or null if the symbol contains a part whose reach can't be
75797
+ // measured.
75798
+ //
75799
+ // Only a symbol drawn around its point can be described this way: a circle, or
75800
+ // a group of the circles and polygons that make up ring, pie and donut symbols.
75801
+ // An arrow is drawn from its point outward, so the circle around its point that
75802
+ // covers it says nothing useful about the space it occupies -- arrows are plain
75803
+ // polygon symbols, which reach this function only as group parts, which they
75804
+ // never are.
75805
+ function getSymbolBoundingRadius(sym) {
75806
+ var max = 0;
75807
+ var parts, r, i;
75808
+ if (!sym) return null;
75809
+ if (sym.type == 'circle') {
75810
+ // Half of a stroke lies outside the circle it follows, which is how a ring
75811
+ // symbol paints its bands.
75812
+ return getPositiveNumber(sym.r) + getStrokeOutset(sym);
75813
+ }
75814
+ if (sym.type == 'polygon') {
75815
+ forEachSymbolCoord(sym.coordinates || [], function(p) {
75816
+ var dist = Math.sqrt(p[0] * p[0] + p[1] * p[1]);
75817
+ if (dist > max) max = dist;
75818
+ });
75819
+ return max + getStrokeOutset(sym);
75820
+ }
75821
+ if (sym.type == 'group') {
75822
+ parts = sym.parts || [];
75823
+ for (i=0; i<parts.length; i++) {
75824
+ r = getSymbolBoundingRadius(parts[i]);
75825
+ if (r === null) return null;
75826
+ if (r > max) max = r;
75827
+ }
75828
+ return max;
75829
+ }
75830
+ return null;
75831
+ }
75832
+
75833
+ // A stroke straddles the path it is drawn on, so half of its width extends
75834
+ // beyond the shape.
75835
+ function getStrokeOutset(sym) {
75836
+ var width = sym['stroke-width'];
75837
+ if (!sym.stroke || sym.stroke == 'none') return 0;
75838
+ // an SVG stroke is one pixel wide unless stroke-width says otherwise
75839
+ if (width === undefined || width === null || width === '') return 0.5;
75840
+ return getPositiveNumber(width) / 2;
75841
+ }
75842
+
75843
+ function getPositiveNumber(val) {
75844
+ var num = +val;
75845
+ return num > 0 ? num : 0;
75846
+ }
75847
+
75848
+ function forEachSymbolCoord(coords, cb) {
75849
+ var isPoint = coords && utils.isNumber(coords[0]);
75850
+ var isNested = !isPoint && coords && Array.isArray(coords[0]);
75851
+ if (isPoint) return cb(coords);
75852
+ for (var i=0; i<coords.length; i++) {
75853
+ if (isNested) forEachSymbolCoord(coords[i], cb);
75854
+ }
75855
+ }
75856
+
75857
+ function flipY(coords) {
75858
+ forEachSymbolCoord(coords, function(p) {
75859
+ p[1] = -p[1];
75860
+ });
75861
+ }
75862
+
75863
+ function scaleAndShiftCoords(coords, scale, shift) {
75864
+ forEachSymbolCoord(coords, function(xy) {
75865
+ xy[0] = xy[0] * scale + shift[0];
75866
+ xy[1] = xy[1] * scale + shift[1];
75867
+ });
75868
+ }
75869
+
75870
+ function roundCoordsForSVG(coords) {
75871
+ forEachSymbolCoord(coords, function(p) {
75872
+ p[0] = roundCoord$1(p[0]);
75873
+ p[1] = roundCoord$1(p[1]);
75874
+ });
75875
+ }
75876
+
75877
+ function rotateCoords(coords, rotation) {
75878
+ if (!rotation) return;
75879
+ var f = getAffineTransform(rotation, 1, [0, 0], [0, 0]);
75880
+ forEachSymbolCoord(coords, function(p) {
75881
+ var p2 = f(p[0], p[1]);
75882
+ p[0] = p2[0];
75883
+ p[1] = p2[1];
75884
+ });
75885
+ }
75886
+
75428
75887
  // Collision reduction for circular symbols, in the spirit of d3-force's
75429
75888
  // forceCollide. Overlapping symbols are pushed apart, but each one is kept
75430
75889
  // within a fixed distance of its true position.
75431
75890
  //
75432
75891
  // Nodes are laid out in pixel space; converting to and from map coordinates is
75433
- // the caller's job. A node is {i, x, y, x0, y0, r, maxShift}, where x0,y0 is
75434
- // the anchor (the symbol's true position), r includes any padding and maxShift
75435
- // is the furthest this symbol may be displaced. Both r and maxShift are
75436
- // per-node, so the caller can vary them from symbol to symbol; a node with a
75437
- // maxShift of 0 never moves and so acts as a fixed obstacle.
75892
+ // the caller's job. A node is {i, x, y, x0, y0, r, maxShift, margin}, where
75893
+ // x0,y0 is the anchor (the symbol's true position), r includes any padding and
75894
+ // maxShift is the furthest this symbol may be displaced. All three of r,
75895
+ // maxShift and margin are per-node, so the caller can vary them from symbol to
75896
+ // symbol; a node with a maxShift of 0 never moves and so acts as a fixed
75897
+ // obstacle.
75898
+ //
75899
+ // A pair of nodes is kept a.r + b.r + max(a.margin, b.margin) apart, so a
75900
+ // margin is a gap that one symbol claims from every neighbor, whatever the
75901
+ // neighbor asks for, and the larger of the two claims wins. Padding is the
75902
+ // alternative already folded into r, where a pair ends up with the sum of the
75903
+ // two symbols' padding between them. margin is optional; treated as 0 if absent.
75904
+ //
75905
+ // A negative margin lets a pair overlap, but only where both nodes permit it:
75906
+ // max() means one node's 0 outranks another's -5.
75438
75907
 
75439
75908
  // max_shift is not read by the solver (it is a per-node property); it is here
75440
75909
  // as the default for callers to apply when building nodes.
@@ -75481,55 +75950,91 @@ ${svg}
75481
75950
  return moved;
75482
75951
  }
75483
75952
 
75484
- // Overlaps shallower than this are not reported. A converged layout doesn't
75485
- // leave symbols exactly touching, it leaves a tail of overlaps a fraction of a
75486
- // pixel deep -- on a solved 435-symbol layout, 55 pairs overlap by more than a
75487
- // hundredth of a pixel, 6 by more than half a pixel, and none by more than one.
75488
- // So a threshold of a pixel is where this solver's convergence residue ends and
75489
- // genuine unresolved crowding begins, as well as being about the point where an
75490
- // overlap becomes visible.
75491
- var VISIBLE_OVERLAP = 1; // pixels
75492
-
75493
- // Number of visibly overlapping pairs, for reporting how much of the job got
75494
- // done. Deliberately not a count of all overlaps: an overlap 100th of a pixel
75495
- // deep and one 15 pixels deep are not the same result, and treating them alike
75496
- // makes a layout that is visually finished look like a failure.
75953
+ // Collisions shallower than this are not reported. A converged layout doesn't
75954
+ // leave pairs at exactly the separation they need, it leaves a tail of pairs
75955
+ // short of it by a fraction of a pixel -- on a solved 435-symbol layout, 55
75956
+ // pairs fall short by more than a hundredth of a pixel, 6 by more than half a
75957
+ // pixel, and none by more than one. So a threshold of a pixel is where this
75958
+ // solver's convergence residue ends and genuine unresolved crowding begins, as
75959
+ // well as being about the point where the shortfall becomes visible.
75960
+ var VISIBLE_COLLISION = 1; // pixels
75961
+
75962
+ // Number of pairs that fall visibly short of the separation they need, for
75963
+ // reporting how much of the job got done. Deliberately not a count of every
75964
+ // collision: falling short by a 100th of a pixel and by 15 pixels are not the
75965
+ // same result, and treating them alike makes a layout that is visually finished
75966
+ // look like a failure.
75967
+ //
75968
+ // Note this counts collisions, not overlaps, and padding or a margin can put
75969
+ // the two apart: symbols asked to keep 4px between them and left 3px apart are
75970
+ // colliding, in that the layout hasn't done what was asked, but not overlapping.
75497
75971
  function countSymbolCollisions(nodes) {
75498
75972
  var count = 0;
75499
75973
  forEachCollision(nodes, function(a, b, dx, dy, distSq, sum) {
75500
- if (sum - Math.sqrt(distSq) > VISIBLE_OVERLAP) count++;
75974
+ if (sum - Math.sqrt(distSq) > VISIBLE_COLLISION) count++;
75501
75975
  });
75502
75976
  return count;
75503
75977
  }
75504
75978
 
75505
- // Push overlapping nodes apart, in proportion to the overlap. Larger nodes move
75506
- // less than smaller ones.
75979
+ // Push colliding nodes apart, in proportion to how far short of their required
75980
+ // separation they are. Larger nodes move less than smaller ones. Which of the
75981
+ // two yields is decided by their radii alone, so asking for a wide margin does
75982
+ // not also make a symbol harder to push.
75507
75983
  function resolveCollisions(nodes, strength) {
75508
75984
  forEachCollision(nodes, function(a, b, dx, dy, distSq, sum) {
75509
75985
  separate(a, b, dx, dy, distSq, sum, strength);
75510
75986
  });
75511
75987
  }
75512
75988
 
75513
- // Sweep along the x axis, using the largest radius still ahead of the current
75514
- // node to know when to stop looking for neighbors.
75989
+ // Sweep along the x axis, using the greatest reach still ahead of the current
75990
+ // node to know when to stop looking for neighbors. A node's reach is its radius
75991
+ // plus its margin if that margin is positive: only a positive margin widens the
75992
+ // area a symbol needs, and clamping is what makes the bound below safe.
75993
+ //
75994
+ // Breaking out of the inner loop is valid because the separation a pair needs,
75995
+ // a.r + b.r + max(a.margin, b.margin), never exceeds a.reach + b.reach. With
75996
+ // margins clamped at 0 that holds, since max(ma, mb) <= max(ma, 0) + max(mb, 0).
75997
+ // Without the clamp it fails for two negative margins -- max(-3, -3) is -3, not
75998
+ // -6 -- and the sweep quietly stops short of pairs that are still colliding.
75999
+ //
76000
+ // Margins are collected into an array in the same pass that builds the bound,
76001
+ // which normalizes a missing margin to 0 once per node instead of guarding every
76002
+ // comparison in the loop below.
76003
+ //
76004
+ // Note the sort happens once per pass while the callback moves nodes as it goes,
76005
+ // so the bound is applied to positions that have already shifted and a pair that
76006
+ // drifts into range mid-pass can be missed until the next pass re-sorts. A
76007
+ // consequence is that padding and an equivalent margin, which ask for the same
76008
+ // separation, don't produce identical output: the margin gives a node a longer
76009
+ // reach, which prunes less and catches a few more of those pairs.
75515
76010
  function forEachCollision(nodes, cb) {
75516
76011
  var n = nodes.length,
75517
76012
  maxAhead = new Float64Array(n),
76013
+ margins = new Float64Array(n),
75518
76014
  max = 0,
75519
- a, b, dx, dy, sum, distSq, i, j;
76015
+ a, b, dx, dy, sum, distSq, reach, ma, mb, i, j;
75520
76016
  nodes.sort(compareX);
75521
76017
  for (i=n-1; i>=0; i--) {
75522
- if (nodes[i].r > max) max = nodes[i].r;
76018
+ ma = nodes[i].margin || 0;
76019
+ margins[i] = ma;
76020
+ reach = nodes[i].r + (ma > 0 ? ma : 0);
76021
+ if (reach > max) max = reach;
75523
76022
  maxAhead[i] = max;
75524
76023
  }
75525
76024
  for (i=0; i<n; i++) {
75526
76025
  a = nodes[i];
76026
+ ma = margins[i];
76027
+ reach = a.r + (ma > 0 ? ma : 0);
75527
76028
  for (j=i+1; j<n; j++) {
75528
76029
  b = nodes[j];
75529
76030
  dx = b.x - a.x;
75530
- if (dx > a.r + maxAhead[j]) break;
76031
+ if (dx > reach + maxAhead[j]) break;
75531
76032
  dy = b.y - a.y;
75532
- sum = a.r + b.r;
76033
+ mb = margins[j];
76034
+ sum = a.r + b.r + (ma > mb ? ma : mb);
76035
+ // Margins can cancel the pair's radii entirely, which separate() would
76036
+ // read as an overlap and answer by pulling the two nodes together.
76037
+ if (sum <= 0) continue;
75533
76038
  if (dy > sum || dy < -sum) continue;
75534
76039
  distSq = dx * dx + dy * dy;
75535
76040
  if (distSq >= sum * sum) continue;
@@ -75600,14 +76105,16 @@ ${svg}
75600
76105
 
75601
76106
  var SymbolCollisions = /*#__PURE__*/Object.freeze({
75602
76107
  __proto__: null,
75603
- VISIBLE_OVERLAP: VISIBLE_OVERLAP,
76108
+ VISIBLE_COLLISION: VISIBLE_COLLISION,
75604
76109
  countSymbolCollisions: countSymbolCollisions,
75605
76110
  resolveSymbolCollisions: resolveSymbolCollisions,
75606
76111
  symbolCollisionDefaults: symbolCollisionDefaults
75607
76112
  });
75608
76113
 
75609
- // Move circular symbols apart to reduce overlaps, keeping each one within a
75610
- // fixed pixel distance of its true position.
76114
+ // Move symbols apart to reduce overlaps, keeping each one within a fixed pixel
76115
+ // distance of its true position. Each symbol is laid out as the circle that
76116
+ // covers it, so the command works on the symbol types that are drawn around
76117
+ // their point (see getSymbolLayoutRadius()).
75611
76118
  //
75612
76119
  // The layout runs in the pixel space of the map that the symbols will be
75613
76120
  // rendered in, so the command needs to know the display scale: either from a
@@ -75619,9 +76126,17 @@ ${svg}
75619
76126
  // An optional polygons= layer confines each symbol to the polygon it started
75620
76127
  // in, so that a symbol standing for one area can't drift into a neighboring
75621
76128
  // one. Symbols in an area too small to hold them barely move as a result, and
75622
- // their overlaps go unresolved -- that is the point of the option, but it does
75623
- // mean fewer overlaps get fixed.
76129
+ // their collisions go unresolved -- that is the point of the option, but it does
76130
+ // mean fewer collisions get fixed.
76131
+ //
76132
+ // Clearance between symbols can be asked for in two ways. padding= surrounds
76133
+ // each symbol, so a pair ends up with the sum of their two paddings between
76134
+ // them; margin= is a gap a symbol claims from every neighbor, so a pair ends up
76135
+ // with the greater of their two margins. Use padding= to make a symbol keep its
76136
+ // distance in proportion to how much of it there is, and margin= to set a gap
76137
+ // that holds however the neighbor is configured.
75624
76138
  var DEFAULT_PADDING = 0; // pixels
76139
+ var DEFAULT_MARGIN = 0; // pixels
75625
76140
 
75626
76141
  cmd.repel = function(targetLayers, dataset, catalog, polygonSource, opts) {
75627
76142
  var solverOpts = {
@@ -75641,7 +76156,7 @@ ${svg}
75641
76156
  addLayerNodes(nodes, lyr, pixelsPerUnit, opts);
75642
76157
  });
75643
76158
  if (nodes.length === 0) {
75644
- stop$1('Targeted layer(s) contain no circle symbols to move.');
76159
+ stop$1('Targeted layer(s) contain no symbols to move.');
75645
76160
  }
75646
76161
  var unconstrained = addContainment(nodes, dataset, polygonSource, pixelsPerUnit, solverOpts);
75647
76162
 
@@ -75702,17 +76217,19 @@ ${svg}
75702
76217
  function reportResults(nodes, moved, before, after, contained) {
75703
76218
  var total = nodes.length;
75704
76219
  var msg = utils.format('Moved %d of %d symbol%s', moved, total, total == 1 ? '' : 's');
75705
- // Counts are of visible overlaps only, so a layout whose symbols end up
75706
- // touching or a fraction of a pixel apart is reported as finished, and the
75707
- // advice to raise max-shift= is not given when nothing is left to fix.
76220
+ // Counts are of visible collisions only, so a layout whose symbols end up at
76221
+ // or a fraction of a pixel short of the separation they need is reported as
76222
+ // finished, and the advice to raise max-shift= is not given when nothing is
76223
+ // left to fix. A collision is not the same as an overlap: two symbols asked to
76224
+ // keep a gap between them collide as soon as the gap closes.
75708
76225
  if (after > 0) {
75709
- msg += utils.format('; %d of %d visible overlap%s remain (%s)',
76226
+ msg += utils.format('; %d of %d visible collision%s remain (%s)',
75710
76227
  after, before, before == 1 ? '' : 's',
75711
76228
  contained ? getMaxShiftAdvice(nodes) + ', or drop polygons=' : getMaxShiftAdvice(nodes));
75712
76229
  } else if (before > 0) {
75713
- msg += utils.format('; resolved %d visible overlap%s', before, before == 1 ? '' : 's');
76230
+ msg += utils.format('; resolved %d visible collision%s', before, before == 1 ? '' : 's');
75714
76231
  } else {
75715
- msg += ' (no visible overlaps found)';
76232
+ msg += ' (no visible collisions found)';
75716
76233
  }
75717
76234
  message(msg);
75718
76235
  }
@@ -75733,19 +76250,20 @@ ${svg}
75733
76250
  function requireRepelTarget(lyr, opts) {
75734
76251
  requireSinglePointLayer(lyr,
75735
76252
  '-repel requires single points; layer contains multi-point features.');
75736
- if (!opts.radius && !layerHasCircleSymbols(lyr)) {
75737
- stop$1('-repel requires a layer containing circle symbols ' +
76253
+ if (!opts.radius && !layerHasSymbols(lyr)) {
76254
+ stop$1('-repel requires a layer containing symbols ' +
75738
76255
  '(see the -symbols and -style commands), or a radius= option.');
75739
76256
  }
75740
76257
  }
75741
76258
 
75742
- function layerHasCircleSymbols(lyr) {
76259
+ function layerHasSymbols(lyr) {
75743
76260
  return !!lyr.data && (lyr.data.fieldExists('svg-symbol') || lyr.data.fieldExists('r'));
75744
76261
  }
75745
76262
 
75746
76263
  function addLayerNodes(nodes, lyr, pixelsPerUnit, opts) {
75747
76264
  var getRadius = getRadiusAccessor(lyr, opts);
75748
76265
  var getPadding = getPixelValueAccessor(opts.padding, 'padding', DEFAULT_PADDING, lyr);
76266
+ var getMargin = getPixelValueAccessor(opts.margin, 'margin', DEFAULT_MARGIN, lyr, true);
75749
76267
  var getMaxShift = getPixelValueAccessor(opts.max_shift, 'max-shift',
75750
76268
  symbolCollisionDefaults.max_shift, lyr);
75751
76269
  var i, shp, p, r;
@@ -75764,23 +76282,32 @@ ${svg}
75764
76282
  x0: p[0] * pixelsPerUnit,
75765
76283
  y0: p[1] * pixelsPerUnit,
75766
76284
  // Padding is added to the radius, so a pair of symbols ends up with
75767
- // (padding a + padding b) pixels of clearance between them.
76285
+ // (padding a + padding b) pixels of clearance between them. A margin is
76286
+ // kept separate, because the solver has to compare the two symbols'
76287
+ // margins to find the gap a pair needs.
75768
76288
  r: r + getPadding(i),
76289
+ margin: getMargin(i),
75769
76290
  maxShift: getMaxShift(i)
75770
76291
  });
75771
76292
  }
75772
76293
  }
75773
76294
 
75774
- // padding= and max-shift= accept a number, a field name or an expression, and
75775
- // so are resolved for each symbol.
75776
- function getPixelValueAccessor(optVal, name, defaultVal, lyr) {
76295
+ // padding=, margin= and max-shift= accept a number, a field name or an
76296
+ // expression, and so are resolved for each symbol.
76297
+ //
76298
+ // @allowNegative is for margin=, where a negative value asks that a pair of
76299
+ // symbols be allowed to overlap. Elsewhere a negative value is an error: it
76300
+ // would shrink a radius or a shift limit, neither of which is a coherent
76301
+ // request. A margin still has to be a finite number, so NaN is rejected either
76302
+ // way.
76303
+ function getPixelValueAccessor(optVal, name, defaultVal, lyr, allowNegative) {
75777
76304
  if (optVal === undefined || optVal === null || optVal === '') {
75778
76305
  return function(i) {return defaultVal;};
75779
76306
  }
75780
76307
  var accessor = getPropertyAccessor(optVal, 'number', lyr, name);
75781
76308
  return function(i) {
75782
76309
  var val = +accessor(i);
75783
- if (!(val >= 0)) {
76310
+ if (allowNegative ? !isFinite(val) : !(val >= 0)) {
75784
76311
  stop$1(utils.format('Invalid %s= value: %s', name, accessor(i)));
75785
76312
  }
75786
76313
  return val;
@@ -75799,12 +76326,17 @@ ${svg}
75799
76326
  return function(i) {
75800
76327
  var rec = records[i];
75801
76328
  if (!rec) return 0;
75802
- if (rec['svg-symbol']) return getCircleSymbolRadius(rec['svg-symbol']);
76329
+ if (rec['svg-symbol']) return getSymbolLayoutRadius(rec['svg-symbol']);
75803
76330
  return rec.r > 0 ? +rec.r : 0;
75804
76331
  };
75805
76332
  }
75806
76333
 
75807
- function getCircleSymbolRadius(sym) {
76334
+ // A symbol is laid out as the circle that covers it, so a symbol type is
76335
+ // supported if it is drawn around its point: a circle, or a group of parts,
76336
+ // which is how ring, pie and donut symbols are made. A symbol drawn from its
76337
+ // point outward, like an arrow, has no meaningful radius and needs radius=.
76338
+ function getSymbolLayoutRadius(sym) {
76339
+ var r;
75808
76340
  if (utils.isString(sym)) {
75809
76341
  try {
75810
76342
  sym = JSON.parse(sym);
@@ -75812,11 +76344,14 @@ ${svg}
75812
76344
  return 0;
75813
76345
  }
75814
76346
  }
75815
- if (!sym || sym.type != 'circle') {
75816
- stop$1('-repel currently supports circle symbols only' +
75817
- (sym && sym.type ? ' (found a ' + sym.type + ' symbol).' : '.'));
76347
+ if (!sym) return 0;
76348
+ r = sym.type == 'circle' || sym.type == 'group' ?
76349
+ getSymbolBoundingRadius(sym) : null;
76350
+ if (r === null) {
76351
+ stop$1('-repel is unable to size a ' + (sym.type || 'symbol') +
76352
+ ' symbol; use a radius= option to give it a radius.');
75818
76353
  }
75819
- return sym.r > 0 ? +sym.r : 0;
76354
+ return r;
75820
76355
  }
75821
76356
 
75822
76357
  // Returns the number of display pixels per map coordinate unit.
@@ -77864,72 +78399,6 @@ ${svg}
77864
78399
  });
77865
78400
  }
77866
78401
 
77867
- var roundCoord$1 = getRoundingFunction(0.01);
77868
-
77869
- function getSymbolFillColor(d) {
77870
- return d.fill || 'magenta';
77871
- }
77872
-
77873
- function getSymbolStrokeColor(d) {
77874
- return d.stroke || d.fill || 'magenta';
77875
- }
77876
-
77877
- function applySymbolStyles(sym, d) {
77878
- if (sym.type == 'polyline') {
77879
- sym.stroke = getSymbolStrokeColor(d);
77880
- } else {
77881
- sym.fill = getSymbolFillColor(d);
77882
- }
77883
- if (d.opacity) {
77884
- sym.opacity = d.opacity;
77885
- }
77886
- return sym;
77887
- }
77888
-
77889
- function getSymbolRadius(d) {
77890
- if (d.radius === 0 || d.length === 0 || d.r === 0) return 0;
77891
- return d.radius || d.length || d.r || 5; // use a default value
77892
- }
77893
-
77894
- function forEachSymbolCoord(coords, cb) {
77895
- var isPoint = coords && utils.isNumber(coords[0]);
77896
- var isNested = !isPoint && coords && Array.isArray(coords[0]);
77897
- if (isPoint) return cb(coords);
77898
- for (var i=0; i<coords.length; i++) {
77899
- if (isNested) forEachSymbolCoord(coords[i], cb);
77900
- }
77901
- }
77902
-
77903
- function flipY(coords) {
77904
- forEachSymbolCoord(coords, function(p) {
77905
- p[1] = -p[1];
77906
- });
77907
- }
77908
-
77909
- function scaleAndShiftCoords(coords, scale, shift) {
77910
- forEachSymbolCoord(coords, function(xy) {
77911
- xy[0] = xy[0] * scale + shift[0];
77912
- xy[1] = xy[1] * scale + shift[1];
77913
- });
77914
- }
77915
-
77916
- function roundCoordsForSVG(coords) {
77917
- forEachSymbolCoord(coords, function(p) {
77918
- p[0] = roundCoord$1(p[0]);
77919
- p[1] = roundCoord$1(p[1]);
77920
- });
77921
- }
77922
-
77923
- function rotateCoords(coords, rotation) {
77924
- if (!rotation) return;
77925
- var f = getAffineTransform(rotation, 1, [0, 0], [0, 0]);
77926
- forEachSymbolCoord(coords, function(p) {
77927
- var p2 = f(p[0], p[1]);
77928
- p[0] = p2[0];
77929
- p[1] = p2[1];
77930
- });
77931
- }
77932
-
77933
78402
  function getStickArrowCoords(d) {
77934
78403
  return getArrowCoords(d, 'stick');
77935
78404
  }
@@ -78267,11 +78736,176 @@ ${svg}
78267
78736
  }
78268
78737
 
78269
78738
  function parseRings(arg) {
78270
- var arr = Array.isArray(arg) ? arg : parseNumberList(arg);
78739
+ var arr = toNumberList(arg);
78271
78740
  utils.genericSort(arr, true);
78272
78741
  return utils.uniq(arr);
78273
78742
  }
78274
78743
 
78744
+ // Matches the vertex density of the 72-sided circle made by getPolygonCoords()
78745
+ var DEGREES_PER_SEGMENT = 5;
78746
+
78747
+ // Returns an svg-symbol object containing one polygon part per wedge, or null
78748
+ // if the symbol has nothing to draw.
78749
+ function makePieSymbol(d, opts) {
78750
+ var wedges = getPieWedges(d, +opts.scale || 1);
78751
+ if (wedges.length === 0) return null;
78752
+ var parts = wedges.map(function(wedge) {
78753
+ flipY(wedge.coordinates); // the SVG y-axis points down
78754
+ roundCoordsForSVG(wedge.coordinates);
78755
+ var part = {
78756
+ type: 'polygon',
78757
+ coordinates: wedge.coordinates,
78758
+ fill: wedge.fill
78759
+ };
78760
+ if (d.stroke) part.stroke = d.stroke;
78761
+ if (d['stroke-width']) part['stroke-width'] = d['stroke-width'];
78762
+ // opacity is applied per-part, not to the group: renderComplexSymbol()
78763
+ // assumes a group symbol has a properties object, which group() only
78764
+ // provides for single-part groups.
78765
+ if (d.opacity) part.opacity = d.opacity;
78766
+ return part;
78767
+ });
78768
+ return {type: 'group', parts: parts};
78769
+ }
78770
+
78771
+ // Returns an array of {fill, coordinates} objects, in the y-up coordinate space
78772
+ // shared by the symbol generators, centered on [0, 0]. Coordinates are an array
78773
+ // of rings, like flattened GeoJSON MultiPolygon coordinates.
78774
+ //
78775
+ // The first wedge starts at the top of the symbol and the rest follow in
78776
+ // clockwise order. A wedge with no value, or without a usable fill color, is
78777
+ // left out -- an unfilled wedge becomes a gap in the pie without shifting the
78778
+ // wedges that follow it.
78779
+ function getPieWedges(d, scale) {
78780
+ var values = getPieValues(d);
78781
+ var fills = getPieFills(d);
78782
+ var radii = getPieRadii(d, scale);
78783
+ var rotation = +d.rotation || 0;
78784
+ var total = sum(values);
78785
+ var wedges = [];
78786
+ var start = rotation;
78787
+ var fill, end;
78788
+ if (total > 0 === false || radii.outer > 0 === false) return wedges;
78789
+ for (var i=0; i<values.length; i++) {
78790
+ end = start + values[i] / total * 360;
78791
+ fill = getWedgeFill(fills[i]);
78792
+ if (fill && end > start) {
78793
+ wedges.push({
78794
+ fill: fill,
78795
+ coordinates: getWedgeCoords(start, end, radii.inner, radii.outer)
78796
+ });
78797
+ }
78798
+ start = end;
78799
+ }
78800
+ return wedges;
78801
+ }
78802
+
78803
+ // Returns an array of rings outlining a wedge spanning two bearings.
78804
+ function getWedgeCoords(startAngle, endAngle, inner, outer) {
78805
+ var ring;
78806
+ if (endAngle - startAngle >= 360) {
78807
+ // A single non-zero value covers the whole symbol: a circle, or a pair of
78808
+ // concentric rings if the symbol is a donut.
78809
+ ring = closeRing(getArcCoords(startAngle, startAngle + 360, outer));
78810
+ if (inner > 0) {
78811
+ // wind the hole in the opposite direction from the outer ring
78812
+ return [ring, closeRing(getArcCoords(startAngle + 360, startAngle, inner))];
78813
+ }
78814
+ return [ring];
78815
+ }
78816
+ ring = getArcCoords(startAngle, endAngle, outer);
78817
+ if (inner > 0) {
78818
+ // A wedge of a donut is a simple polygon, so the hole needs no ring of
78819
+ // its own -- the inner arc doubles back inside the outer one.
78820
+ ring = ring.concat(getArcCoords(endAngle, startAngle, inner));
78821
+ } else {
78822
+ ring.push([0, 0]);
78823
+ }
78824
+ ring.push(ring[0].concat());
78825
+ return [ring];
78826
+ }
78827
+
78828
+ // A 360 degree arc starts and ends at the same angle, but rounding leaves its
78829
+ // endpoints a hair apart, so the ring is closed explicitly.
78830
+ function closeRing(ring) {
78831
+ ring[ring.length - 1] = ring[0].concat();
78832
+ return ring;
78833
+ }
78834
+
78835
+ // Returns points along an arc, including both endpoints. Angles are bearings,
78836
+ // which run clockwise from the top of the symbol -- the same convention as
78837
+ // rotateCoords(), so the rotation= option is just an offset to the first angle.
78838
+ function getArcCoords(startAngle, endAngle, radius) {
78839
+ var sweep = endAngle - startAngle;
78840
+ var segments = Math.max(1, Math.ceil(Math.abs(sweep) / DEGREES_PER_SEGMENT));
78841
+ var coords = [];
78842
+ for (var i=0; i<=segments; i++) {
78843
+ coords.push(getPlanarSegmentEndpoint(0, 0, startAngle + sweep * i / segments, radius));
78844
+ }
78845
+ return coords;
78846
+ }
78847
+
78848
+ // Missing, non-numeric and negative values all count as zero.
78849
+ function getPieValues(d) {
78850
+ if (!d.values) {
78851
+ stop$1('A pie symbol requires a values= option.');
78852
+ }
78853
+ return toNumberList(d.values).map(function(num) {
78854
+ return num > 0 ? num : 0;
78855
+ });
78856
+ }
78857
+
78858
+ function getPieFills(d) {
78859
+ if (!d.fills) {
78860
+ stop$1('A pie symbol requires a fills= option.');
78861
+ }
78862
+ return toItemList(d.fills);
78863
+ }
78864
+
78865
+ // Returns the color to fill a wedge with, or null if the wedge should be left
78866
+ // out of the symbol.
78867
+ function getWedgeFill(val) {
78868
+ var str = val === null || val === undefined ? '' : String(val).trim();
78869
+ var name = str.toLowerCase();
78870
+ var rgb;
78871
+ if (!str || name == 'none' || name == 'transparent') {
78872
+ return null;
78873
+ }
78874
+ rgb = parseColor(str);
78875
+ if (!rgb || rgb.a === 0) return null;
78876
+ return str;
78877
+ }
78878
+
78879
+ // Returns {inner, outer} radii in pixels. The hole= option makes a donut. A
78880
+ // negative hole is measured inward from the outer radius, which gives a band
78881
+ // of wedges of a fixed width without having to repeat a data-driven radius=
78882
+ // expression. A hole as wide as the symbol leaves a plain pie, and a
78883
+ // non-positive outer radius is left for the caller to skip, like radius=0 on
78884
+ // the other symbol types.
78885
+ function getPieRadii(d, scale) {
78886
+ var outer = getSymbolRadius(d);
78887
+ var hole = +d.hole;
78888
+ var inner = 0;
78889
+ if (hole > 0) {
78890
+ inner = hole;
78891
+ } else if (hole < 0) {
78892
+ inner = outer + hole;
78893
+ }
78894
+ if (!(inner > 0) || inner >= outer) inner = 0;
78895
+ return {
78896
+ inner: inner * scale,
78897
+ outer: outer * scale
78898
+ };
78899
+ }
78900
+
78901
+ function sum(arr) {
78902
+ var total = 0;
78903
+ for (var i=0; i<arr.length; i++) {
78904
+ total += arr[i];
78905
+ }
78906
+ return total;
78907
+ }
78908
+
78275
78909
  // Returns an svg-symbol data object for one symbol
78276
78910
  function makePathSymbol(coords, properties, geojsonType) {
78277
78911
  var sym;
@@ -78328,6 +78962,17 @@ ${svg}
78328
78962
  var d = getSymbolData(i);
78329
78963
  var rec = records[i] || {};
78330
78964
 
78965
+ // A pie has a different fill for each of its wedges, which the single
78966
+ // fill field of a generated shape can't express.
78967
+ if (d.type == 'pie') {
78968
+ if (shapeMode) {
78969
+ stop$1('The pie symbol type does not support the geographic option.');
78970
+ }
78971
+ var pie = makePieSymbol(d, opts);
78972
+ if (pie) rec['svg-symbol'] = pie;
78973
+ return;
78974
+ }
78975
+
78331
78976
  // non-polygon symbols
78332
78977
  if (!shapeMode && d.type == 'circle') {
78333
78978
  rec['svg-symbol'] = makeCircleSymbol(d, opts);
@@ -79565,7 +80210,7 @@ ${svg}
79565
80210
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
79566
80211
  }
79567
80212
 
79568
- var version = "0.7.56";
80213
+ var version = "0.7.59";
79569
80214
 
79570
80215
  // Parse command line args into commands and run them
79571
80216
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.