mapshaper 0.7.70 → 0.7.72

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
@@ -7549,19 +7549,19 @@
7549
7549
  pointN = [CK.lengthMG, CK.lengthMG * Math.tan(30 * D2R$5)];
7550
7550
  CK.pointA = [CK.lengthMA, 0];
7551
7551
  CK.pointB = lineIntersection(CK.pointM, 30, CK.pointA, 45);
7552
- CK.lengthAG = distance$1(CK.pointA, CK.pointG);
7553
- CK.lengthAB = distance$1(CK.pointA, CK.pointB);
7554
- lengthMB = distance$1(CK.pointM, CK.pointB);
7555
- lengthMN = distance$1(CK.pointM, pointN);
7556
- lengthNG = distance$1(pointN, CK.pointG);
7552
+ CK.lengthAG = distance$2(CK.pointA, CK.pointG);
7553
+ CK.lengthAB = distance$2(CK.pointA, CK.pointB);
7554
+ lengthMB = distance$2(CK.pointM, CK.pointB);
7555
+ lengthMN = distance$2(CK.pointM, pointN);
7556
+ lengthNG = distance$2(pointN, CK.pointG);
7557
7557
  CK.pointD = interpolate(lengthMB, lengthMN, pointN, CK.pointM);
7558
7558
  CK.pointF = [CK.lengthMG, lengthNG - lengthMB];
7559
7559
  CK.pointE = [
7560
7560
  pointN[0] - CK.lengthMA * Math.sin(30 * D2R$5),
7561
7561
  pointN[1] - CK.lengthMA * Math.cos(30 * D2R$5)
7562
7562
  ];
7563
- CK.lengthGF = distance$1(CK.pointG, CK.pointF);
7564
- CK.lengthBD = distance$1(CK.pointB, CK.pointD);
7563
+ CK.lengthGF = distance$2(CK.pointG, CK.pointF);
7564
+ CK.lengthBD = distance$2(CK.pointB, CK.pointD);
7565
7565
  CK.lengthGFE = CK.lengthGF + CK.lengthAB;
7566
7566
  CK.deltaMEq = CK.lengthGFE / 45;
7567
7567
  CK.lengthAP75 = 15 * CK.lengthParallel73to90At0;
@@ -7588,7 +7588,7 @@
7588
7588
  k * CK.pointD[0] - CK.pointD[1])
7589
7589
  );
7590
7590
  CK.pointC[0] = k * CK.pointC[1];
7591
- CK.radius = distance$1(CK.pointC, CK.pointD);
7591
+ CK.radius = distance$2(CK.pointC, CK.pointD);
7592
7592
  }
7593
7593
 
7594
7594
  function ll2mp(lon, lat) {
@@ -7619,12 +7619,12 @@
7619
7619
  );
7620
7620
  var l15;
7621
7621
  if (hit[0]) {
7622
- l15 = lT + distance$1(jointT(m), hit[1]);
7622
+ l15 = lT + distance$2(jointT(m), hit[1]);
7623
7623
  } else {
7624
7624
  hit = circleLineIntersection(
7625
7625
  CK.pointC, CK.radius, jointE(m), jointT(m)
7626
7626
  );
7627
- l15 = lT - distance$1(jointT(m), hit[1]);
7627
+ l15 = lT - distance$2(jointT(m), hit[1]);
7628
7628
  }
7629
7629
  return p <= 15 ? zoneK(m, p, l15) : zoneL(m, p, l15);
7630
7630
  }
@@ -7660,8 +7660,8 @@
7660
7660
  function zoneH(m, p) {
7661
7661
  var p75 = parallel75(45);
7662
7662
  var p73 = parallel73(m).parallel73;
7663
- var lF = distance$1(CK.pointT, CK.pointB);
7664
- var lF75 = distance$1(CK.pointB, p75);
7663
+ var lF = distance$2(CK.pointT, CK.pointB);
7664
+ var lF75 = distance$2(CK.pointB, p75);
7665
7665
  var l = (75 - p) * (lF75 + lF) / 2;
7666
7666
  return l <= lF75 ?
7667
7667
  interpolate(l, lF75, p75, CK.pointB) :
@@ -7688,7 +7688,7 @@
7688
7688
  function zoneJ(m, p) {
7689
7689
  var p75 = parallel75(m);
7690
7690
  var p73a = parallel73(m);
7691
- var lF75 = distance$1(jointF(m), p75);
7691
+ var lF75 = distance$2(jointF(m), p75);
7692
7692
  var l = (75 - p) * (lF75 - p73a.lengthParallel73) / 2;
7693
7693
  return l <= lF75 ?
7694
7694
  interpolate(l, lF75, p75, jointF(m)) :
@@ -7745,11 +7745,11 @@
7745
7745
  }
7746
7746
 
7747
7747
  function lengthTorridSegment(m) {
7748
- return distance$1(jointE(m), jointT(m));
7748
+ return distance$2(jointE(m), jointT(m));
7749
7749
  }
7750
7750
 
7751
7751
  function lengthMiddleSegment(m) {
7752
- return distance$1(jointT(m), jointF(m));
7752
+ return distance$2(jointT(m), jointF(m));
7753
7753
  }
7754
7754
 
7755
7755
  function parallel73(m) {
@@ -7757,15 +7757,15 @@
7757
7757
  var jF = jointF(m);
7758
7758
  if (m <= 30) {
7759
7759
  p73 = radialPoint(CK.pointA, CK.lengthAP73, m);
7760
- lF = distance$1(jF, p73);
7760
+ lF = distance$2(jF, p73);
7761
7761
  } else {
7762
7762
  p73 = lineIntersection(CK.pointT, -60, jF, m);
7763
- lF = distance$1(jF, p73);
7763
+ lF = distance$2(jF, p73);
7764
7764
  if (m > 44) {
7765
7765
  xy = lineIntersection(CK.pointT, -60, jF, 2 * m / 3);
7766
7766
  if (xy[0] > p73[0]) {
7767
7767
  p73 = xy;
7768
- lF = -distance$1(jF, p73);
7768
+ lF = -distance$2(jF, p73);
7769
7769
  }
7770
7770
  }
7771
7771
  }
@@ -7838,7 +7838,7 @@
7838
7838
  return createCahillKeyesTransform(mg, true);
7839
7839
  }
7840
7840
 
7841
- function distance$1(a, b) {
7841
+ function distance$2(a, b) {
7842
7842
  return Math.hypot(a[0] - b[0], a[1] - b[1]);
7843
7843
  }
7844
7844
 
@@ -26365,10 +26365,116 @@
26365
26365
  return String(str).trim().split(splitRxp);
26366
26366
  }
26367
26367
 
26368
+ // The id of the <pattern> element a fill-pattern code is exported as. It is
26369
+ // also how the code is recovered when an exported SVG is imported again (see
26370
+ // decodePatternId()), so a change here has to keep decoding the ids of files
26371
+ // that are already out there.
26368
26372
  function getHashId(str) {
26369
26373
  return ('hash_' + str).replace(/[()# ,_]+/g, '_'); // replace some chars that occur in colors
26370
26374
  }
26371
26375
 
26376
+ // The fill-pattern code a <pattern> id was made from, or null if the id is not
26377
+ // one of ours. The id keeps the tokens of the code but drops the # of a hex
26378
+ // colour and the parentheses and commas of rgb() and hsl(), so those are put
26379
+ // back by where the colours fall in each pattern type's syntax. A hex colour
26380
+ // has to have its # back even where the parser would not need it: without it,
26381
+ // a colour of digits reads as a number.
26382
+ function decodePatternId(id) {
26383
+ var tokens, code;
26384
+ if (!/^hash_./.test(id || '')) return null;
26385
+ tokens = mergeColorFunctions(id.substr(5).split('_').filter(Boolean));
26386
+ tokens = restoreHexColors(tokens);
26387
+ if (!tokens) return null;
26388
+ code = tokens.join(' ');
26389
+ return parsePattern(code) ? code : null;
26390
+ }
26391
+
26392
+ function mergeColorFunctions(tokens) {
26393
+ var out = [];
26394
+ var tok, argc, args;
26395
+ for (var i=0; i<tokens.length; i++) {
26396
+ tok = tokens[i];
26397
+ if (/^(rgb|hsl)a?$/i.test(tok)) {
26398
+ argc = /a$/i.test(tok) ? 4 : 3;
26399
+ args = tokens.slice(i + 1, i + 1 + argc);
26400
+ i += args.length;
26401
+ if (tokens[i + 1] == '/' && i + 2 < tokens.length) {
26402
+ // space-separated form with an alpha: rgb(0 0 0 / 50%)
26403
+ out.push(tok + '(' + args.join(' ') + ' / ' + tokens[i + 2] + ')');
26404
+ i += 2;
26405
+ } else {
26406
+ out.push(tok + '(' + args.join(',') + ')');
26407
+ }
26408
+ } else {
26409
+ out.push(tok);
26410
+ }
26411
+ }
26412
+ return out;
26413
+ }
26414
+
26415
+ // Marks each token as a colour or not, by pattern type, and gives the colours
26416
+ // that look like hex values their # back. Returns null for an unknown type.
26417
+ function restoreHexColors(tokens) {
26418
+ var type = tokens[0] || '';
26419
+ var colorAt;
26420
+ if (type.startsWith('dot') || type.startsWith('square')) {
26421
+ colorAt = getDotColorPositions(tokens);
26422
+ } else if (type.startsWith('dash')) {
26423
+ colorAt = getDashColorPositions(tokens);
26424
+ } else if (type.startsWith('hatch') || isNumberToken(type)) {
26425
+ colorAt = getHatchColorPositions(tokens);
26426
+ } else {
26427
+ return null;
26428
+ }
26429
+ return tokens.map(function(tok, i) {
26430
+ return colorAt[i] && /^[0-9a-f]+$/i.test(tok) && [3, 4, 6, 8].includes(tok.length) ?
26431
+ '#' + tok : tok;
26432
+ });
26433
+ }
26434
+
26435
+ // [hatches] [rotation] width1 color1 [width2 color2 ...]
26436
+ function getHatchColorPositions(tokens) {
26437
+ var start = isNumberToken(tokens[0]) ? 0 : 1;
26438
+ var rest = tokens.length - start;
26439
+ var first = start + (rest % 2 == 1 ? 1 : 0); // odd count: a rotation first
26440
+ return tokens.map(function(tok, i) {
26441
+ return i >= first && (i - first) % 2 == 1;
26442
+ });
26443
+ }
26444
+
26445
+ // dots|squares [rotation] size color1 [color2 ...] spacing background
26446
+ function getDotColorPositions(tokens) {
26447
+ var last = tokens.length - 1;
26448
+ // A rotation is there if the second number is also a size -- but a colour
26449
+ // of digits looks like a number too, so a bare one counts as a colour
26450
+ // unless a unit or a decimal point says otherwise.
26451
+ var rotated = /deg$/.test(tokens[1] || '') ||
26452
+ last >= 5 && isNumberToken(tokens[2]) && !mightBeHexDigits(tokens[2]);
26453
+ var firstColor = rotated ? 3 : 2;
26454
+ return tokens.map(function(tok, i) {
26455
+ return i == last || i >= firstColor && i < last - 1;
26456
+ });
26457
+ }
26458
+
26459
+ // dashes [rotation] dash-length space-length width color1 [color2 ...] spacing background
26460
+ function getDashColorPositions(tokens) {
26461
+ var last = tokens.length - 1;
26462
+ var rotated = /deg$/.test(tokens[1] || '') ||
26463
+ last >= 7 && isNumberToken(tokens[4]) && !mightBeHexDigits(tokens[4]);
26464
+ var firstColor = rotated ? 5 : 4;
26465
+ return tokens.map(function(tok, i) {
26466
+ return i == last || i >= firstColor && i < last - 1;
26467
+ });
26468
+ }
26469
+
26470
+ function isNumberToken(tok) {
26471
+ return /^-?(\d+\.?\d*|\.\d+)(px|deg)?$/.test(tok || '');
26472
+ }
26473
+
26474
+ function mightBeHexDigits(tok) {
26475
+ return /^\d+$/.test(tok) && [3, 4, 6, 8].includes(tok.length);
26476
+ }
26477
+
26372
26478
  // properties: properties object of a path data object (prior to conversion to SVG)
26373
26479
  // defs: array of definition objects
26374
26480
  //
@@ -26382,7 +26488,10 @@
26382
26488
  if (!hash) return;
26383
26489
  defs.push(hash);
26384
26490
  }
26385
- properties.fill = hash.href;
26491
+ // The feature's own fill is kept as the paint's fallback colour, which SVG
26492
+ // draws only if the pattern cannot be. It is also how the fill survives
26493
+ // being exported and imported again.
26494
+ properties.fill = properties.fill ? hash.href + ' ' + properties.fill : hash.href;
26386
26495
  }
26387
26496
 
26388
26497
  function makeSVGPatternFill(str, id) {
@@ -26462,6 +26571,8 @@
26462
26571
  var SvgHatch = /*#__PURE__*/Object.freeze({
26463
26572
  __proto__: null,
26464
26573
  convertFillPattern: convertFillPattern,
26574
+ decodePatternId: decodePatternId,
26575
+ getHashId: getHashId,
26465
26576
  parseDashes: parseDashes,
26466
26577
  parseDots: parseDots,
26467
26578
  parseHatches: parseHatches,
@@ -26719,6 +26830,14 @@
26719
26830
  fill: 'color',
26720
26831
  'fill-pattern': 'pattern',
26721
26832
  'fill-effect': null, // todo: validate effect names
26833
+ // soft glows along a polygon's edges -- see svg-glow.mjs. Drawn with a
26834
+ // filter, not written as attributes, so not in propertiesBySymbolType.
26835
+ 'outer-glow-color': 'color',
26836
+ 'outer-glow-opacity': 'number',
26837
+ 'outer-glow-width': 'number',
26838
+ 'inner-glow-color': 'color',
26839
+ 'inner-glow-opacity': 'number',
26840
+ 'inner-glow-width': 'number',
26722
26841
  'font-family': null,
26723
26842
  'font-size': null,
26724
26843
  'font-style': null,
@@ -26759,6 +26878,11 @@
26759
26878
  'callout-color': 'color',
26760
26879
  'callout-width': 'number',
26761
26880
  'callout-opacity': 'number',
26881
+ // arrowheads at the ends of a line -- see svg-line-arrows.mjs. Drawn as
26882
+ // shapes, not written as attributes, so not in propertiesBySymbolType.
26883
+ 'line-start': 'lineend',
26884
+ 'line-end': 'lineend',
26885
+ 'line-end-size': 'number',
26762
26886
  'letter-spacing': 'measure',
26763
26887
  'line-height': 'measure',
26764
26888
  opacity: 'number',
@@ -27070,6 +27194,8 @@
27070
27194
  val = parseCalloutType(strVal);
27071
27195
  } else if (type == 'calloutend') {
27072
27196
  val = parseCalloutEnd(strVal);
27197
+ } else if (type == 'lineend') {
27198
+ val = parseLineEnd(strVal);
27073
27199
  } else if (type == 'pointpair') {
27074
27200
  val = parsePointPair(strVal);
27075
27201
  val = val ? formatPointPair(val) : null;
@@ -27118,7 +27244,13 @@
27118
27244
 
27119
27245
  function parseCalloutEnd(str) {
27120
27246
  var end = String(str).trim().toLowerCase();
27121
- return /^(arrow|open-arrow|none)$/.test(end) ? end : null;
27247
+ return /^(arrow|open-arrow|ring|none)$/.test(end) ? end : null;
27248
+ }
27249
+
27250
+ // A line's ends take a callout's arrowheads, and a dot
27251
+ function parseLineEnd(str) {
27252
+ var end = String(str).trim().toLowerCase();
27253
+ return /^(arrow|open-arrow|dot|none)$/.test(end) ? end : null;
27122
27254
  }
27123
27255
 
27124
27256
  // "x,y" -> [x, y], or null. Stored as a string so that a pair is set and
@@ -27240,6 +27372,7 @@
27240
27372
  parseCalloutEnd: parseCalloutEnd,
27241
27373
  parseCalloutType: parseCalloutType,
27242
27374
  parseLabelPosition: parseLabelPosition,
27375
+ parseLineEnd: parseLineEnd,
27243
27376
  parsePointPair: parsePointPair,
27244
27377
  parseStyleLiteral: parseStyleLiteral,
27245
27378
  parseSvgMeasure: parseSvgMeasure,
@@ -27742,6 +27875,85 @@
27742
27875
  toPixels: toPixels
27743
27876
  });
27744
27877
 
27878
+ // Arrowhead geometry shared by label callouts (svg-label-callout.mjs) and the
27879
+ // ends of line features (svg-line-arrows.mjs), so that the two draw the same
27880
+ // heads. Everything is in px, y down.
27881
+
27882
+ // The angle at an arrowhead's point, degrees. The open one is wider, since a
27883
+ // narrow chevron drawn with a line reads as a thickened line more than as an
27884
+ // arrow.
27885
+ var ARROW_ANGLE = 35;
27886
+ var OPEN_ARROW_ANGLE = 70;
27887
+ // How far into a solid arrowhead the line reaches, as a fraction of the
27888
+ // head's length: far enough that its cap is hidden, short of the tip.
27889
+ var ARROW_LINE_OVERLAP = 0.7;
27890
+
27891
+ var DEFAULT_LINE_WIDTH$2 = 1;
27892
+
27893
+ // An arrowhead's size when nothing says, in px: the length of its sides. 10
27894
+ // for a 1px line, growing with the line, so that a heavier line does not end
27895
+ // in a head too small to read.
27896
+ function getDefaultArrowSize(lineWidth) {
27897
+ var w = lineWidth > 0 ? lineWidth : DEFAULT_LINE_WIDTH$2;
27898
+ return 7 + 3 * w;
27899
+ }
27900
+
27901
+ // [tip, wing, wing], for a head with sides @side long meeting at @angle degrees,
27902
+ // pointing at @tip from direction @dir (a unit vector from the tip back along
27903
+ // the line)
27904
+ function getArrowHead(tip, dir, side, angle) {
27905
+ var len = getArrowHeadLength(side, angle);
27906
+ var half = side * Math.sin(angle / 2 * Math.PI / 180);
27907
+ var bx = tip[0] + dir[0] * len;
27908
+ var by = tip[1] + dir[1] * len;
27909
+ return [tip, [bx - dir[1] * half, by + dir[0] * half], [bx + dir[1] * half, by - dir[0] * half]];
27910
+ }
27911
+
27912
+ // How far along the line a head with sides @side long reaches
27913
+ function getArrowHeadLength(side, angle) {
27914
+ return side * Math.cos(angle / 2 * Math.PI / 180);
27915
+ }
27916
+
27917
+ // The part of polyline @coords after it first leaves the circle of radius @r
27918
+ // around its first point, starting on the circle; or null if it never does.
27919
+ function trimPolyline$1(coords, r) {
27920
+ var p0 = coords[0];
27921
+ for (var i = 1; i < coords.length; i++) {
27922
+ if (distance$1(p0, coords[i]) > r) {
27923
+ return [getCircleExit(p0, r, coords[i - 1], coords[i])].concat(coords.slice(i));
27924
+ }
27925
+ }
27926
+ return null;
27927
+ }
27928
+
27929
+ // The point where segment @p-@q, which starts inside the circle and ends
27930
+ // outside it, crosses it.
27931
+ function getCircleExit(c, r, p, q) {
27932
+ var dx = q[0] - p[0];
27933
+ var dy = q[1] - p[1];
27934
+ var ex = p[0] - c[0];
27935
+ var ey = p[1] - c[1];
27936
+ var a = dx * dx + dy * dy;
27937
+ var b = 2 * (dx * ex + dy * ey);
27938
+ var k = ex * ex + ey * ey - r * r;
27939
+ var s = (-b + Math.sqrt(Math.max(0, b * b - 4 * a * k))) / (2 * a);
27940
+ s = Math.max(0, Math.min(1, s));
27941
+ return [p[0] + s * dx, p[1] + s * dy];
27942
+ }
27943
+
27944
+ function getUnitVector(p, q) {
27945
+ var dx = q[0] - p[0];
27946
+ var dy = q[1] - p[1];
27947
+ var len = Math.sqrt(dx * dx + dy * dy);
27948
+ return len > 0 ? [dx / len, dy / len] : null;
27949
+ }
27950
+
27951
+ function distance$1(p, q) {
27952
+ var dx = q[0] - p[0];
27953
+ var dy = q[1] - p[1];
27954
+ return Math.sqrt(dx * dx + dy * dy);
27955
+ }
27956
+
27745
27957
  // A callout: a line from a label's anchor to its text, straight, elbowed or
27746
27958
  // curved, optionally ending in an arrowhead at the anchor. A dot at the anchor
27747
27959
  // is the label's icon, not a callout marker.
@@ -27754,7 +27966,7 @@
27754
27966
  // See docs/development/text-annotation-design.md.
27755
27967
 
27756
27968
  var DEFAULT_PADDING$1 = 3;
27757
- var DEFAULT_LINE_WIDTH = 1;
27969
+ var DEFAULT_LINE_WIDTH$1 = 1;
27758
27970
  // Space left between the anchor's symbol and the end of the line
27759
27971
  var SYMBOL_CLEARANCE = 2;
27760
27972
  // How far an automatic curve bows out, as a fraction of its chord
@@ -27762,14 +27974,6 @@
27762
27974
  // The shortest first leg an automatic elbow into the top or bottom of the text
27763
27975
  // is drawn with, px
27764
27976
  var MIN_LEG = 8;
27765
- // The angle at an arrowhead's point, degrees. The open one is wider, since a
27766
- // narrow chevron drawn with a line reads as a thickened line more than as an
27767
- // arrow.
27768
- var ARROW_ANGLE = 44;
27769
- var OPEN_ARROW_ANGLE = 70;
27770
- // How far into a solid arrowhead the line reaches, as a fraction of the
27771
- // head's length: far enough that its cap is hidden, short of the tip.
27772
- var ARROW_LINE_OVERLAP = 0.7;
27773
27977
  // Where glyphs sit relative to their baseline, in ems, for estimating the
27774
27978
  // height of a block of text that is measured only for its width
27775
27979
  var ASCENT = 0.8;
@@ -27812,13 +28016,21 @@
27812
28016
 
27813
28017
  // An arrowhead's size when callout-end-size does not say, in px: 10 for the
27814
28018
  // default line, and growing with the line, so that a heavier line does not end
27815
- // in a head too small to read.
28019
+ // in a head too small to read. A ring is twice that across, which is room to
28020
+ // circle a point and its symbol.
27816
28021
  function getDefaultCalloutEndSize(end, lineWidth) {
27817
- var w = lineWidth > 0 ? lineWidth : DEFAULT_LINE_WIDTH;
27818
- return 7 + 3 * w;
28022
+ var size = getDefaultArrowSize(lineWidth);
28023
+ return end == 'ring' ? size * 2 : size;
28024
+ }
28025
+
28026
+ // A ring's size is its diameter, stroke and all, as an icon ring's is; this
28027
+ // is the radius of the circle its stroke is centred on.
28028
+ function getRingRadius(size, lineWidth) {
28029
+ var w = lineWidth > 0 ? lineWidth : DEFAULT_LINE_WIDTH$1;
28030
+ return Math.max(size - w, w) / 2;
27819
28031
  }
27820
28032
 
27821
- // 'arrow', 'open-arrow' or 'none'
28033
+ // 'arrow', 'open-arrow', 'ring' or 'none'
27822
28034
  function getCalloutEndType(rec) {
27823
28035
  return rec && parseCalloutEnd(rec['callout-end'] || '') || 'none';
27824
28036
  }
@@ -27883,7 +28095,8 @@
27883
28095
  // Where a line meets a callout's text and how it gets there from the anchor.
27884
28096
  //
27885
28097
  // o.type 'line', 'elbow' or 'curve'
27886
- // o.end 'arrow' (filled), 'open-arrow' (stroked) or 'none'
28098
+ // o.end 'arrow' (filled), 'open-arrow' (stroked), 'ring' (a hollow circle
28099
+ // around the anchor, in the line's width) or 'none'
27887
28100
  // o.box the text box, from getLabelTextBox()
27888
28101
  // o.via [x, y] the elbow's corner or a point the curve passes through, or null
27889
28102
  // o.attach [fx, fy] where the line meets the text, as fractions of the padded
@@ -27895,21 +28108,22 @@
27895
28108
  // the line width. The size is the length of the head's sides, as
27896
28109
  // drawn, stroke and all: the two styles are different shapes, and
27897
28110
  // what makes one look the size of the other is sides the same
27898
- // length, not the same length along the line.
28111
+ // length, not the same length along the line. A ring's size is its
28112
+ // diameter.
27899
28113
  //
27900
- // Returns {kind, coords, head, openHead, box, attach, via, tip} with coords
28114
+ // Returns {kind, coords, head, openHead, ring, box, attach, via, tip} with coords
27901
28115
  // from the anchor end to the text end -- a polyline's vertices, or a quadratic
27902
28116
  // Bezier's three points -- or null when there is nothing to draw: the anchor
27903
28117
  // inside the padded box, or a gap that leaves no line. head is a filled
27904
28118
  // triangle, openHead the three points of a stroked chevron, wing to tip to
27905
- // wing. The rest is what the shape was worked out from: the padded box, the
28119
+ // wing, ring {center, radius} a stroked circle. The rest is what the shape was worked out from: the padded box, the
27906
28120
  // point where the line meets it, the elbow's corner or the curve's midpoint
27907
28121
  // (null for a straight line), and where the line stops short of the anchor.
27908
28122
  function getCalloutShape(o) {
27909
28123
  var box = padBox$1(o.box, o.padding || 0);
27910
28124
  var a = [0, 0];
27911
28125
  var size = o.endSize > 0 ? o.endSize : getDefaultCalloutEndSize(o.end, o.width);
27912
- var t, v, path, tip, dir, out, lineWidth, headLen, curved;
28126
+ var t, v, path, tip, dir, out, lineWidth, headLen, curved, ringRadius;
27913
28127
  if (boxContains(box, a)) return null;
27914
28128
  t = o.attach ? getBoxPoint(box, o.attach) :
27915
28129
  getAutoAttachment(o.type, box, o.via || null);
@@ -27923,7 +28137,7 @@
27923
28137
  v = o.via || getAutoElbow(a, t, box);
27924
28138
  // A corner on either end is no corner -- straight under the text, say --
27925
28139
  // and a leg of no length has no direction for an arrowhead to take.
27926
- path = {kind: 'polyline', coords: distance(a, v) > 0 && distance(v, t) > 0 ?
28140
+ path = {kind: 'polyline', coords: distance$1(a, v) > 0 && distance$1(v, t) > 0 ?
27927
28141
  [a, v, t] : [a, t]};
27928
28142
  } else if (o.type == 'curve') {
27929
28143
  v = o.via || getAutoBend(a, t);
@@ -27931,10 +28145,14 @@
27931
28145
  } else {
27932
28146
  path = {kind: 'polyline', coords: [a, t]};
27933
28147
  }
27934
- path = trimPath(path, o.gap || 0);
28148
+ // A ring is centred on the anchor, and the line starts at its stroke, or
28149
+ // at the gap if that is further out -- round a symbol bigger than the ring.
28150
+ ringRadius = o.end == 'ring' ? getRingRadius(size, o.width) : 0;
28151
+ path = trimPath(path, Math.max(o.gap || 0, ringRadius));
27935
28152
  if (!path) return null;
27936
28153
  tip = path.coords[0];
27937
28154
  out = {kind: path.kind, coords: path.coords, head: null, openHead: null,
28155
+ ring: ringRadius > 0 ? {center: a, radius: ringRadius} : null,
27938
28156
  box: box, attach: t, via: v || null, tip: tip};
27939
28157
  if (o.end == 'arrow') {
27940
28158
  headLen = getArrowHeadLength(size, ARROW_ANGLE);
@@ -27951,7 +28169,7 @@
27951
28169
  // the chevron's point: the point is set back by that much, so that the
27952
28170
  // arrow ends where a filled one would. The join and the round cap at the
27953
28171
  // far end each add half a line width to an arm, which is taken off it.
27954
- lineWidth = o.width > 0 ? o.width : DEFAULT_LINE_WIDTH;
28172
+ lineWidth = o.width > 0 ? o.width : DEFAULT_LINE_WIDTH$1;
27955
28173
  path = trimPath(path, lineWidth / 2) || path;
27956
28174
  dir = getStartDirection(path);
27957
28175
  if (!dir) return null;
@@ -28001,6 +28219,19 @@
28001
28219
  }
28002
28220
  });
28003
28221
  }
28222
+ if (shape.ring) {
28223
+ children.push({
28224
+ tag: 'circle',
28225
+ properties: {
28226
+ cx: shape.ring.center[0],
28227
+ cy: shape.ring.center[1],
28228
+ r: Math.round(shape.ring.radius * 100) / 100,
28229
+ fill: 'none',
28230
+ stroke: color,
28231
+ 'stroke-width': getLineWidth(rec)
28232
+ }
28233
+ });
28234
+ }
28004
28235
  if (opacity < 1) props.opacity = opacity;
28005
28236
  return {tag: 'g', properties: props, children: children};
28006
28237
  }
@@ -28128,44 +28359,21 @@
28128
28359
  function hasLength(path) {
28129
28360
  var c = path.coords;
28130
28361
  for (var i = 1; i < c.length; i++) {
28131
- if (distance(c[0], c[i]) > 0) return true;
28362
+ if (distance$1(c[0], c[i]) > 0) return true;
28132
28363
  }
28133
28364
  return false;
28134
28365
  }
28135
28366
 
28136
28367
  function trimPolyline(coords, r) {
28137
- var p0 = coords[0];
28138
- for (var i = 1; i < coords.length; i++) {
28139
- if (distance(p0, coords[i]) > r) {
28140
- return {
28141
- kind: 'polyline',
28142
- coords: [getCircleExit(p0, r, coords[i - 1], coords[i])].concat(coords.slice(i))
28143
- };
28144
- }
28145
- }
28146
- return null;
28147
- }
28148
-
28149
- // The point where segment @p-@q, which starts inside the circle and ends
28150
- // outside it, crosses it.
28151
- function getCircleExit(c, r, p, q) {
28152
- var dx = q[0] - p[0];
28153
- var dy = q[1] - p[1];
28154
- var ex = p[0] - c[0];
28155
- var ey = p[1] - c[1];
28156
- var a = dx * dx + dy * dy;
28157
- var b = 2 * (dx * ex + dy * ey);
28158
- var k = ex * ex + ey * ey - r * r;
28159
- var s = (-b + Math.sqrt(Math.max(0, b * b - 4 * a * k))) / (2 * a);
28160
- s = Math.max(0, Math.min(1, s));
28161
- return [p[0] + s * dx, p[1] + s * dy];
28368
+ var trimmed = trimPolyline$1(coords, r);
28369
+ return trimmed ? {kind: 'polyline', coords: trimmed} : null;
28162
28370
  }
28163
28371
 
28164
28372
  function trimBezier(c, r) {
28165
28373
  var steps = 64;
28166
28374
  var lo = 0, hi = -1, mid, i;
28167
28375
  for (i = 1; i <= steps; i++) {
28168
- if (distance(c[0], getBezierPoint(c, i / steps)) > r) {
28376
+ if (distance$1(c[0], getBezierPoint(c, i / steps)) > r) {
28169
28377
  hi = i / steps;
28170
28378
  lo = (i - 1) / steps;
28171
28379
  break;
@@ -28174,7 +28382,7 @@
28174
28382
  if (hi < 0) return null;
28175
28383
  for (i = 0; i < 24; i++) {
28176
28384
  mid = (lo + hi) / 2;
28177
- if (distance(c[0], getBezierPoint(c, mid)) > r) hi = mid;
28385
+ if (distance$1(c[0], getBezierPoint(c, mid)) > r) hi = mid;
28178
28386
  else lo = mid;
28179
28387
  }
28180
28388
  return {kind: 'bezier', coords: splitBezier(c, hi)};
@@ -28200,7 +28408,7 @@
28200
28408
  function getStartDirection(path) {
28201
28409
  var c = path.coords;
28202
28410
  var next = c[1];
28203
- if (path.kind == 'bezier' && distance(c[0], c[1]) === 0) next = c[2];
28411
+ if (path.kind == 'bezier' && distance$1(c[0], c[1]) === 0) next = c[2];
28204
28412
  return getUnitVector(c[0], next);
28205
28413
  }
28206
28414
 
@@ -28232,8 +28440,8 @@
28232
28440
  ctrl = intersectRays(start, dir, end, getUnitVector(end, rest.coords[1]));
28233
28441
  // A curve that doubles back, or runs straight on: a control point on the
28234
28442
  // head's axis still takes the line out of the head straight.
28235
- if (!ctrl) ctrl = [start[0] + dir[0] * distance(start, end) / 2,
28236
- start[1] + dir[1] * distance(start, end) / 2];
28443
+ if (!ctrl) ctrl = [start[0] + dir[0] * distance$1(start, end) / 2,
28444
+ start[1] + dir[1] * distance$1(start, end) / 2];
28237
28445
  return {dir: dir, path: {kind: 'bezier', coords: [start, ctrl, end]}};
28238
28446
  }
28239
28447
 
@@ -28251,20 +28459,6 @@
28251
28459
  return s > 0 && u > 0 ? [p[0] + d[0] * s, p[1] + d[1] * s] : null;
28252
28460
  }
28253
28461
 
28254
- // [tip, wing, wing], for a head with sides @side long meeting at @angle degrees
28255
- function getArrowHead(tip, dir, side, angle) {
28256
- var len = getArrowHeadLength(side, angle);
28257
- var half = side * Math.sin(angle / 2 * Math.PI / 180);
28258
- var bx = tip[0] + dir[0] * len;
28259
- var by = tip[1] + dir[1] * len;
28260
- return [tip, [bx - dir[1] * half, by + dir[0] * half], [bx + dir[1] * half, by - dir[0] * half]];
28261
- }
28262
-
28263
- // How far along the line a head with sides @side long reaches
28264
- function getArrowHeadLength(side, angle) {
28265
- return side * Math.cos(angle / 2 * Math.PI / 180);
28266
- }
28267
-
28268
28462
  // Utilities
28269
28463
 
28270
28464
  function padBox$1(box, pad) {
@@ -28281,19 +28475,6 @@
28281
28475
  return p[0] > box.xmin && p[0] < box.xmax && p[1] > box.ymin && p[1] < box.ymax;
28282
28476
  }
28283
28477
 
28284
- function getUnitVector(p, q) {
28285
- var dx = q[0] - p[0];
28286
- var dy = q[1] - p[1];
28287
- var len = Math.sqrt(dx * dx + dy * dy);
28288
- return len > 0 ? [dx / len, dy / len] : null;
28289
- }
28290
-
28291
- function distance(p, q) {
28292
- var dx = q[0] - p[0];
28293
- var dy = q[1] - p[1];
28294
- return Math.sqrt(dx * dx + dy * dy);
28295
- }
28296
-
28297
28478
  function lerp(p, q, t) {
28298
28479
  return [p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t];
28299
28480
  }
@@ -28303,8 +28484,8 @@
28303
28484
  }
28304
28485
 
28305
28486
  function getLineWidth(rec) {
28306
- var w = getNumber(rec['callout-width'], DEFAULT_LINE_WIDTH);
28307
- return w > 0 ? w : DEFAULT_LINE_WIDTH;
28487
+ var w = getNumber(rec['callout-width'], DEFAULT_LINE_WIDTH$1);
28488
+ return w > 0 ? w : DEFAULT_LINE_WIDTH$1;
28308
28489
  }
28309
28490
 
28310
28491
  function getFontSizeInPx$1(rec) {
@@ -28357,6 +28538,9 @@
28357
28538
  return str;
28358
28539
  }
28359
28540
 
28541
+ // A ring icon's stroke, unless the record sets one; 1px read as too faint.
28542
+ var RING_STROKE_WIDTH = 1.5;
28543
+
28360
28544
  var symbolRenderers = {
28361
28545
  line: line,
28362
28546
  polygon: polygon,
@@ -28429,6 +28613,7 @@
28429
28613
  if (type == 'square') return square(getIconStyleData(d, r), 0, 0);
28430
28614
  if (type == 'ring') return ring(getIconStyleData(d, r), 0, 0);
28431
28615
  if (type == 'star') return star(getIconStyleData(d, r));
28616
+ if (type == 'nyt-star') return nytStar(getIconStyleData(d, r));
28432
28617
  return empty();
28433
28618
  }
28434
28619
 
@@ -28438,8 +28623,11 @@
28438
28623
  return d.r > 0 ? d.r : 5;
28439
28624
  }
28440
28625
  size = d['icon-size'];
28441
- if (type == 'circle' || type == 'ring') {
28626
+ if (type == 'circle') {
28442
28627
  size -= 1;
28628
+ } else if (type == 'ring') {
28629
+ // the stroke's outer half is inside icon-size
28630
+ size -= RING_STROKE_WIDTH;
28443
28631
  } else if (type == 'star') {
28444
28632
  size += 1;
28445
28633
  }
@@ -28546,7 +28734,7 @@
28546
28734
  o.properties.fill = 'none';
28547
28735
  o.properties.stroke = d.fill;
28548
28736
  if (!o.properties['stroke-width']) {
28549
- o.properties['stroke-width'] = 1;
28737
+ o.properties['stroke-width'] = RING_STROKE_WIDTH;
28550
28738
  }
28551
28739
  return o;
28552
28740
  }
@@ -28558,6 +28746,43 @@
28558
28746
  return o;
28559
28747
  }
28560
28748
 
28749
+ // A white nine-pointed star with a heavy outline in the icon's colour, drawn
28750
+ // from the source artwork's 40-unit box. The box includes the outline and its
28751
+ // mitred points, so icon-size is the width of the whole symbol.
28752
+ var NYT_STAR_BOX = 40;
28753
+ var NYT_STAR_STROKE = 5;
28754
+ var NYT_STAR_POINTS = [19.9843, 5.7196, 23.1494, 11.7629, 29.4528, 9.1512,
28755
+ 27.9924, 15.8152, 34.5, 17.8645, 29.0961, 22.0321, 32.7631, 27.7841,
28756
+ 25.9457, 27.504, 25.0572, 34.2675, 20.0149, 29.6714, 14.9847, 34.2804,
28757
+ 14.0769, 27.5192, 7.2619, 27.8167, 10.9166, 22.0553, 5.5, 17.9016,
28758
+ 12.0024, 15.8357, 10.527, 9.1754, 16.835, 11.771];
28759
+
28760
+ function nytStar(d) {
28761
+ var k = d.r * 2 / NYT_STAR_BOX;
28762
+ var o = importPolygon([getNytStarCoords(k)]);
28763
+ applyStyleAttributes(o, 'point', d, nonCirclePointFilter);
28764
+ o.properties.stroke = o.properties.fill;
28765
+ o.properties.fill = '#fff';
28766
+ o.properties['stroke-width'] = roundToHundredths(NYT_STAR_STROKE * k);
28767
+ o.properties['stroke-miterlimit'] = 10;
28768
+ return o;
28769
+ }
28770
+
28771
+ function getNytStarCoords(k) {
28772
+ var c = NYT_STAR_BOX / 2;
28773
+ var coords = [];
28774
+ for (var i=0; i<NYT_STAR_POINTS.length; i+=2) {
28775
+ coords.push([roundToHundredths((NYT_STAR_POINTS[i] - c) * k),
28776
+ roundToHundredths((NYT_STAR_POINTS[i + 1] - c) * k)]);
28777
+ }
28778
+ coords.push(coords[0].concat());
28779
+ return coords;
28780
+ }
28781
+
28782
+ function roundToHundredths(n) {
28783
+ return Math.round(n * 100) / 100;
28784
+ }
28785
+
28561
28786
  function nonCirclePointFilter(k) {
28562
28787
  return k != 'r';
28563
28788
  }
@@ -28637,6 +28862,7 @@
28637
28862
 
28638
28863
  var SvgSymbols = /*#__PURE__*/Object.freeze({
28639
28864
  __proto__: null,
28865
+ RING_STROKE_WIDTH: RING_STROKE_WIDTH,
28640
28866
  featureHasIcon: featureHasIcon,
28641
28867
  getAnchorSymbolRadius: getAnchorSymbolRadius,
28642
28868
  getIconRadius: getIconRadius,
@@ -29949,11 +30175,302 @@
29949
30175
  getSphereEffectParams: getSphereEffectParams
29950
30176
  });
29951
30177
 
30178
+ // Soft glows along the edges of polygons: an outer glow outside the shape and
30179
+ // an inner glow inside it, each with a color, an opacity and a width.
30180
+ //
30181
+ // outer-glow-color a glow is drawn when its color is set, and not
30182
+ // inner-glow-color otherwise (or if the color is "none")
30183
+ // outer-glow-width how far the glow reaches from the edge, in px;
30184
+ // inner-glow-width default 10, and 0 draws no glow
30185
+ // outer-glow-opacity 0-1, default 1
30186
+ // inner-glow-opacity
30187
+ //
30188
+ // A glow is a Gaussian blur of the shape's coverage, with a standard deviation
30189
+ // of half the width: it has faded to about 2% of its opacity at that distance.
30190
+ // The canvas renders the same blur as a shadow, whose shadowBlur is defined as
30191
+ // twice the standard deviation -- so shadowBlur is the width itself.
30192
+ //
30193
+ // A glow is computed from everything the shape paints, its fill and its
30194
+ // stroke, faded by its opacity -- which is what an SVG filter on a group sees.
30195
+ //
30196
+ // Inner glows belong to each shape. An outer glow belongs to the layer as a
30197
+ // whole when every shape in the layer has the same one: it then follows the
30198
+ // outline of all of them together, like a drop shadow, instead of spilling
30199
+ // over each neighbor's edge. See getLayerOuterGlow().
30200
+ //
30201
+ // Illustrator drops a filter that is applied to a <path> with more than one
30202
+ // ring (it imports one as a compound path), and keeps one that is applied to a
30203
+ // group. A glowing shape is therefore always wrapped in a <g> that carries the
30204
+ // filter.
30205
+
30206
+ var glowFields = [
30207
+ 'outer-glow-color', 'outer-glow-opacity', 'outer-glow-width',
30208
+ 'inner-glow-color', 'inner-glow-opacity', 'inner-glow-width'
30209
+ ];
30210
+
30211
+ var DEFAULT_GLOW_WIDTH = 10;
30212
+
30213
+ // How far a glow is drawn past the edge, as a multiple of its width: three
30214
+ // standard deviations, where the blur is gone for all practical purposes.
30215
+ var GLOW_REACH = 1.5;
30216
+
30217
+ // Filter regions are given as a percentage of the bounding box of what they
30218
+ // filter, and a small shape needs a larger percentage than a big one to leave
30219
+ // room for the same glow. Rounding up to one of these keeps the number of
30220
+ // distinct filters small; Illustrator lists every one of them in its menu.
30221
+ var regionMargins = [10, 25, 50, 100, 200, 500, 1000, 2500, 5000, 10000];
30222
+
30223
+ // {color, opacity, width} for one of a record's glows, or null if it has none.
30224
+ // @type: 'outer' or 'inner'
30225
+ function getPolygonGlow(rec, type) {
30226
+ var prefix = type + '-glow-';
30227
+ var width, color, opacity;
30228
+ if (!rec) return null;
30229
+ color = rec[prefix + 'color'];
30230
+ if (isBlank(color) || String(color).trim().toLowerCase() == 'none') return null;
30231
+ width = rec[prefix + 'width'];
30232
+ if (isBlank(width)) width = DEFAULT_GLOW_WIDTH;
30233
+ if (!isSvgNumber(width) || !(Number(width) > 0)) return null;
30234
+ opacity = rec[prefix + 'opacity'];
30235
+ opacity = isSvgNumber(opacity) ? Math.min(Math.max(Number(opacity), 0), 1) : 1;
30236
+ if (opacity === 0) return null;
30237
+ return {color: String(color).trim(), opacity: opacity, width: Number(width)};
30238
+ }
30239
+
30240
+ function isBlank(val) {
30241
+ return val === undefined || val === null || String(val).trim() === '';
30242
+ }
30243
+
30244
+ function polygonHasGlow(rec) {
30245
+ return !!(getPolygonGlow(rec, 'outer') || getPolygonGlow(rec, 'inner'));
30246
+ }
30247
+
30248
+ function getGlowKey(glow) {
30249
+ return glow ? glow.color + ' ' + glow.opacity + ' ' + glow.width : '';
30250
+ }
30251
+
30252
+ // The outer glow of a polygon layer as a whole, or null. A layer has one when
30253
+ // every feature with a shape has the same outer glow; features without a shape
30254
+ // are left out, since they draw nothing. Decided from the data rather than
30255
+ // from what is drawn, so that it does not change with the view.
30256
+ function getLayerOuterGlow(lyr) {
30257
+ var records, shapes, glow, key, g, k;
30258
+ if (!lyr || lyr.geometry_type != 'polygon' || !lyr.data || !lyr.shapes) return null;
30259
+ records = lyr.data.getRecords();
30260
+ shapes = lyr.shapes;
30261
+ glow = null;
30262
+ for (var i=0, n=shapes.length; i<n; i++) {
30263
+ if (!shapes[i] || shapes[i].length === 0) continue;
30264
+ g = getPolygonGlow(records[i], 'outer');
30265
+ if (!g) return null;
30266
+ k = getGlowKey(g);
30267
+ if (!glow) {
30268
+ glow = g;
30269
+ key = k;
30270
+ } else if (k != key) {
30271
+ return null;
30272
+ }
30273
+ }
30274
+ return glow;
30275
+ }
30276
+
30277
+ // The widest glow of any feature in a layer, in px, or 0.
30278
+ function getMaxGlowWidth(records) {
30279
+ var max = 0, g;
30280
+ for (var i=0, n=records ? records.length : 0; i<n; i++) {
30281
+ g = getPolygonGlow(records[i], 'outer');
30282
+ if (g && g.width > max) max = g.width;
30283
+ g = getPolygonGlow(records[i], 'inner');
30284
+ if (g && g.width > max) max = g.width;
30285
+ }
30286
+ return max;
30287
+ }
30288
+
30289
+ // What a <g> needs for a glow filter: the glows and the size of what it
30290
+ // wraps, so that the filter region can be made large enough.
30291
+ // @bbox: [xmin, ymin, xmax, ymax] of the wrapped content, in px
30292
+ // @strokeWidth: the widest stroke in it, which reaches past the bbox
30293
+ function getGlowFilterSpec(outer, inner, bbox, strokeWidth) {
30294
+ var width = Math.max(outer ? outer.width : 0, inner ? inner.width : 0);
30295
+ var reach = width * GLOW_REACH + (strokeWidth > 0 ? strokeWidth / 2 : 0) + 1;
30296
+ return {
30297
+ outer: outer || null,
30298
+ inner: inner || null,
30299
+ marginX: getRegionMargin(reach, bbox[2] - bbox[0]),
30300
+ marginY: getRegionMargin(reach, bbox[3] - bbox[1])
30301
+ };
30302
+ }
30303
+
30304
+ function getRegionMargin(reach, size) {
30305
+ var pct = size > 0 ? reach / size * 100 : Infinity;
30306
+ for (var i=0; i<regionMargins.length; i++) {
30307
+ if (regionMargins[i] >= pct) return regionMargins[i];
30308
+ }
30309
+ return regionMargins[regionMargins.length - 1];
30310
+ }
30311
+
30312
+ function getGlowFilterKey(spec) {
30313
+ return getGlowKey(spec.outer) + '|' + getGlowKey(spec.inner) + '|' +
30314
+ spec.marginX + ' ' + spec.marginY;
30315
+ }
30316
+
30317
+ // The <filter> element for @spec. The outer glow is the blurred coverage of
30318
+ // the source outside the source; the inner glow is the blurred coverage of
30319
+ // everything outside the source, inside the source. The inversion is made
30320
+ // by cutting the source out of a flood rather than with feComponentTransfer,
30321
+ // which Safari fades toward the corners of the source's bounding box.
30322
+ function renderGlowFilter(id, spec) {
30323
+ var mx = spec.marginX, my = spec.marginY;
30324
+ var parts = [];
30325
+ var merge = [];
30326
+ if (spec.outer) {
30327
+ parts.push(
30328
+ `<feGaussianBlur in="SourceAlpha" stdDeviation="${getGlowDeviation(spec.outer)}" result="outer-blur"/>`,
30329
+ `<feFlood flood-color="${escapeAttr(spec.outer.color)}" flood-opacity="${spec.outer.opacity}"/>`,
30330
+ '<feComposite in2="outer-blur" operator="in"/>',
30331
+ '<feComposite in2="SourceAlpha" operator="out" result="outer"/>');
30332
+ merge.push('outer');
30333
+ }
30334
+ merge.push('SourceGraphic');
30335
+ if (spec.inner) {
30336
+ parts.push(
30337
+ '<feFlood flood-color="#000000" flood-opacity="1"/>',
30338
+ '<feComposite in2="SourceAlpha" operator="out"/>',
30339
+ `<feGaussianBlur stdDeviation="${getGlowDeviation(spec.inner)}" result="inner-blur"/>`,
30340
+ `<feFlood flood-color="${escapeAttr(spec.inner.color)}" flood-opacity="${spec.inner.opacity}"/>`,
30341
+ '<feComposite in2="inner-blur" operator="in"/>',
30342
+ '<feComposite in2="SourceAlpha" operator="in" result="inner"/>');
30343
+ merge.push('inner');
30344
+ }
30345
+ return `<filter id="${id}" x="${-mx}%" y="${-my}%" width="${100 + 2 * mx}%" height="${100 + 2 * my}%" color-interpolation-filters="sRGB">\n` +
30346
+ parts.join('\n') + '\n<feMerge>' +
30347
+ merge.map(function(name) { return `<feMergeNode in="${name}"/>`; }).join('') +
30348
+ '</feMerge>\n</filter>\n';
30349
+ }
30350
+
30351
+ function getGlowDeviation(glow) {
30352
+ return Math.round(glow.width / 2 * 1000) / 1000;
30353
+ }
30354
+
30355
+ function escapeAttr(str) {
30356
+ return String(str).replace(/[&<>"]/g, '');
30357
+ }
30358
+
30359
+ // Wraps each glowing shape in a <g> that will carry its filter, and marks the
30360
+ // layer's own <g> if the layer has an outer glow of its own. The filters
30361
+ // themselves are added to <defs> later, by convertGlowFilter().
30362
+ // @children: the SVG objects for @features, one each, in order
30363
+ function applyPolygonGlows(layerObj, children, features, lyr) {
30364
+ var layerGlow = getLayerOuterGlow(lyr);
30365
+ var layerBBox = null;
30366
+ var maxStroke = 0;
30367
+ children.forEach(function(child, i) {
30368
+ var feature = features[i];
30369
+ var geom = feature && (feature.type == 'Feature' ? feature.geometry : feature);
30370
+ var rec = feature && feature.properties || {};
30371
+ var bbox, outer, inner, strokeWidth, wrapper;
30372
+ if (!child || !child.properties || !geom || !geom.coordinates) return;
30373
+ bbox = getCoordinatesBBox(geom.coordinates);
30374
+ if (!bbox) return;
30375
+ strokeWidth = getStrokeWidth(rec);
30376
+ if (strokeWidth > maxStroke) maxStroke = strokeWidth;
30377
+ layerBBox = layerBBox ? mergeBBoxes(layerBBox, bbox) : bbox;
30378
+ outer = layerGlow ? null : getPolygonGlow(rec, 'outer');
30379
+ inner = getPolygonGlow(rec, 'inner');
30380
+ if (!outer && !inner) return;
30381
+ wrapper = {tag: 'g', properties: {}, children: [child],
30382
+ glowFilter: getGlowFilterSpec(outer, inner, bbox, strokeWidth)};
30383
+ // The group is the feature now, so its id goes with it (and the data
30384
+ // attributes of svg-data=, which are added to the layer's children).
30385
+ if ('id' in child.properties) {
30386
+ wrapper.properties.id = child.properties.id;
30387
+ delete child.properties.id;
30388
+ }
30389
+ children[i] = wrapper;
30390
+ });
30391
+ if (layerGlow && layerBBox) {
30392
+ layerObj.glowFilter = getGlowFilterSpec(layerGlow, null, layerBBox, maxStroke);
30393
+ }
30394
+ }
30395
+
30396
+ // Replaces the glow filter an object was marked with by a reference to a
30397
+ // <filter> in @defs, which shapes with the same glows share.
30398
+ function convertGlowFilter(obj, defs) {
30399
+ var spec = obj.glowFilter;
30400
+ var key = getGlowFilterKey(spec);
30401
+ var item = defs.find(function(o) { return o.glowFilterKey === key; });
30402
+ var count;
30403
+ if (!item) {
30404
+ count = defs.filter(function(o) { return !!o.glowFilterKey; }).length;
30405
+ item = {glowFilterKey: key, id: 'glow-' + (count + 1)};
30406
+ item.svg = renderGlowFilter(item.id, spec);
30407
+ defs.push(item);
30408
+ }
30409
+ delete obj.glowFilter;
30410
+ if (!obj.properties) obj.properties = {};
30411
+ obj.properties.filter = 'url(#' + item.id + ')';
30412
+ }
30413
+
30414
+ function getStrokeWidth(rec) {
30415
+ var w = rec['stroke-width'];
30416
+ if (!rec.stroke || rec.stroke == 'none') return 0;
30417
+ return isSvgNumber(w) ? Number(w) : 1;
30418
+ }
30419
+
30420
+ function getCoordinatesBBox(coords) {
30421
+ var bbox = [Infinity, Infinity, -Infinity, -Infinity];
30422
+ extendBBox(bbox, coords);
30423
+ return bbox[0] <= bbox[2] ? bbox : null;
30424
+ }
30425
+
30426
+ function extendBBox(bbox, coords) {
30427
+ var x, y;
30428
+ if (!coords || coords.length === 0) return;
30429
+ if (typeof coords[0] == 'number') {
30430
+ x = coords[0];
30431
+ y = coords[1];
30432
+ if (x < bbox[0]) bbox[0] = x;
30433
+ if (y < bbox[1]) bbox[1] = y;
30434
+ if (x > bbox[2]) bbox[2] = x;
30435
+ if (y > bbox[3]) bbox[3] = y;
30436
+ return;
30437
+ }
30438
+ for (var i=0; i<coords.length; i++) {
30439
+ extendBBox(bbox, coords[i]);
30440
+ }
30441
+ }
30442
+
30443
+ function mergeBBoxes(a, b) {
30444
+ return [Math.min(a[0], b[0]), Math.min(a[1], b[1]),
30445
+ Math.max(a[2], b[2]), Math.max(a[3], b[3])];
30446
+ }
30447
+
30448
+ var SvgGlow = /*#__PURE__*/Object.freeze({
30449
+ __proto__: null,
30450
+ DEFAULT_GLOW_WIDTH: DEFAULT_GLOW_WIDTH,
30451
+ GLOW_REACH: GLOW_REACH,
30452
+ applyPolygonGlows: applyPolygonGlows,
30453
+ convertGlowFilter: convertGlowFilter,
30454
+ getGlowDeviation: getGlowDeviation,
30455
+ getGlowFilterKey: getGlowFilterKey,
30456
+ getGlowFilterSpec: getGlowFilterSpec,
30457
+ getGlowKey: getGlowKey,
30458
+ getLayerOuterGlow: getLayerOuterGlow,
30459
+ getMaxGlowWidth: getMaxGlowWidth,
30460
+ getPolygonGlow: getPolygonGlow,
30461
+ glowFields: glowFields,
30462
+ polygonHasGlow: polygonHasGlow,
30463
+ renderGlowFilter: renderGlowFilter
30464
+ });
30465
+
29952
30466
  // convert object properties to definitions for images and hatch fills
29953
30467
  function convertPropertiesToDefinitions(obj, defs) {
29954
30468
  procNode(obj);
29955
30469
 
29956
30470
  function procNode(obj) {
30471
+ if (obj.glowFilter) {
30472
+ convertGlowFilter(obj, defs);
30473
+ }
29957
30474
  if (obj.tag == 'textPath' && obj.properties && obj.properties[LABEL_PATH_PROPERTY]) {
29958
30475
  convertLabelPath(obj.properties, defs);
29959
30476
  }
@@ -30079,6 +30596,198 @@
30079
30596
  }
30080
30597
  */
30081
30598
 
30599
+ // Markers at the ends of line features: line-start= and line-end= take
30600
+ // arrow (a filled triangle), open-arrow (a stroked chevron), dot (a filled
30601
+ // circle centred on the end) or none, and line-end-size= is the length of a
30602
+ // head's sides in px, for both ends, or a dot's diameter. A dot with no size
30603
+ // of its own is DOT_SIZE_RATIO of an arrowhead's default size.
30604
+ //
30605
+ // The heads are drawn as shapes rather than as SVG <marker>s, so that the
30606
+ // canvas in the GUI and the exported SVG draw the same thing, and so that a
30607
+ // solid head can have the line stop inside it, where its cap is hidden. Every
30608
+ // part of a multi-part line gets its own heads: part order is not something
30609
+ // the commands that make multi-part lines preserve.
30610
+ //
30611
+ // Coordinates are px, y down: the SVG export's space, or the canvas's.
30612
+
30613
+ var DEFAULT_LINE_WIDTH = 1;
30614
+ // 6px across at the default size for a 1px line
30615
+ var DOT_SIZE_RATIO = 0.6;
30616
+
30617
+ // 'arrow', 'open-arrow', 'dot' or 'none'
30618
+ function getLineEndType(rec, field) {
30619
+ return rec && rec[field] && parseLineEnd(rec[field]) || 'none';
30620
+ }
30621
+
30622
+ function lineHasArrows(rec) {
30623
+ return getLineEndType(rec, 'line-start') != 'none' ||
30624
+ getLineEndType(rec, 'line-end') != 'none';
30625
+ }
30626
+
30627
+ // What getLineArrowShape() needs from a record, at scale @scale (the factor
30628
+ // the canvas multiplies line widths by; 1 for SVG).
30629
+ function getLineArrowOpts(rec, scale) {
30630
+ return makeLineArrowOpts(rec['line-start'], rec['line-end'], rec['line-end-size'],
30631
+ rec['stroke-width'], scale);
30632
+ }
30633
+
30634
+ // The same, from the values themselves, for the canvas, which has them in a
30635
+ // style object rather than a record.
30636
+ function makeLineArrowOpts(start, end, size, strokeWidth, scale) {
30637
+ var k = scale > 0 ? scale : 1;
30638
+ var w = isSvgNumber(strokeWidth) && Number(strokeWidth) >= 0 ?
30639
+ Number(strokeWidth) : DEFAULT_LINE_WIDTH;
30640
+ var hasSize = isSvgNumber(size) && Number(size) > 0;
30641
+ var side = hasSize ? Number(size) : getDefaultArrowSize(w);
30642
+ return {
30643
+ start: start && parseLineEnd(start) || 'none',
30644
+ end: end && parseLineEnd(end) || 'none',
30645
+ size: side * k,
30646
+ dotSize: (hasSize ? side : side * DOT_SIZE_RATIO) * k,
30647
+ width: w * k
30648
+ };
30649
+ }
30650
+
30651
+ // Field names, for a layer that may have them
30652
+ var lineArrowFields = ['line-start', 'line-end', 'line-end-size'];
30653
+
30654
+ // One part of a line, with its heads: {coords, heads}, where coords is the
30655
+ // line to stroke -- cut back into a solid head, so that its cap does not show
30656
+ // past the tip -- and heads is a list of {type, points}: a solid head's
30657
+ // triangle, tip first, or an open head's chevron, wing to tip to wing. A dot
30658
+ // is {type, center, radius}.
30659
+ //
30660
+ // A head points along the chord from its tip to where the line first reaches
30661
+ // the head's length from it, rather than along the last segment, which on
30662
+ // detailed or noisy data can be a jog of a fraction of a pixel pointing
30663
+ // anywhere. The line is then straightened to meet the head along its axis. An
30664
+ // end whose part is too short to hold its head gets none.
30665
+ function getLineArrowShape(coords, opts) {
30666
+ var out = {coords: coords, heads: []};
30667
+ var end;
30668
+ if (opts.start != 'none') {
30669
+ addHead(out, opts.start, opts);
30670
+ }
30671
+ if (opts.end != 'none') {
30672
+ end = {coords: out.coords.slice().reverse(), heads: out.heads};
30673
+ addHead(end, opts.end, opts);
30674
+ out.coords = end.coords.reverse();
30675
+ }
30676
+ return out;
30677
+ }
30678
+
30679
+ // Puts a head of @type at the first point of @shape.coords
30680
+ function addHead(shape, type, opts) {
30681
+ var coords = shape.coords;
30682
+ var width = opts.width > 0 ? opts.width : DEFAULT_LINE_WIDTH;
30683
+ var tip, side, angle, len, rest, dir, head;
30684
+ if (!coords || coords.length < 2) return;
30685
+ if (type == 'dot') {
30686
+ addDot(shape, (opts.dotSize || opts.size * DOT_SIZE_RATIO) / 2);
30687
+ return;
30688
+ }
30689
+ if (type == 'open-arrow') {
30690
+ // Stroked with a round join, which reaches half a line width past the
30691
+ // chevron's point: the point is set back by that much, so that the arrow
30692
+ // ends where a solid one would, and the arms lose half a line width each
30693
+ // to the join and the cap.
30694
+ coords = trimPolyline$1(coords, width / 2);
30695
+ if (!coords) return;
30696
+ side = Math.max(opts.size - width, opts.size / 2);
30697
+ angle = OPEN_ARROW_ANGLE;
30698
+ } else {
30699
+ side = opts.size;
30700
+ angle = ARROW_ANGLE;
30701
+ }
30702
+ tip = coords[0];
30703
+ len = getArrowHeadLength(side, angle);
30704
+ rest = trimPolyline$1(coords, len);
30705
+ dir = rest ? getUnitVector(tip, rest[0]) : null;
30706
+ if (!dir) return;
30707
+ head = getArrowHead(tip, dir, side, angle);
30708
+ if (type == 'open-arrow') {
30709
+ shape.heads.push({type: type, points: [head[1], head[0], head[2]]});
30710
+ shape.coords = [tip].concat(rest);
30711
+ } else {
30712
+ shape.heads.push({type: type, points: head});
30713
+ shape.coords = [[tip[0] + dir[0] * len * ARROW_LINE_OVERLAP,
30714
+ tip[1] + dir[1] * len * ARROW_LINE_OVERLAP]].concat(rest);
30715
+ }
30716
+ }
30717
+
30718
+ // The line stops halfway to the dot's edge, which hides its cap in the dot
30719
+ // (for lines narrower than the dot's radius) while overlapping it no more than
30720
+ // a solid head does. A part too short to be cut back keeps its dot.
30721
+ function addDot(shape, radius) {
30722
+ var center = shape.coords[0];
30723
+ var rest = trimPolyline$1(shape.coords, radius / 2);
30724
+ shape.heads.push({type: 'dot', center: center, radius: radius});
30725
+ if (rest) shape.coords = rest;
30726
+ }
30727
+
30728
+ // A line feature with arrowheads, as SVG: a group holding the line and its
30729
+ // heads, for the feature's style attributes to go on. @parts is a list of
30730
+ // polylines (a LineString's coordinates are one).
30731
+ function renderArrowLine(parts, rec) {
30732
+ var opts = getLineArrowOpts(rec, 1);
30733
+ var color = rec.stroke || 'black';
30734
+ var lines = [];
30735
+ var children = [];
30736
+ parts.forEach(function(part) {
30737
+ var shape = getLineArrowShape(part, opts);
30738
+ lines.push(shape.coords.map(roundPoint));
30739
+ shape.heads.forEach(function(head) {
30740
+ children.push(renderHead(head, color, rec));
30741
+ });
30742
+ });
30743
+ children.unshift(importMultiLineString(lines));
30744
+ return {tag: 'g', properties: {}, children: children};
30745
+ }
30746
+
30747
+ function renderHead(head, color, rec) {
30748
+ var points, center;
30749
+ if (head.type == 'dot') {
30750
+ center = roundPoint(head.center);
30751
+ return {tag: 'circle', properties: addFillOpacity({cx: center[0], cy: center[1],
30752
+ r: Math.round(head.radius * 100) / 100, fill: color, stroke: 'none'}, rec)};
30753
+ }
30754
+ points = head.points.map(roundPoint);
30755
+ var d = 'M ' + points.map(function(p) { return p[0] + ' ' + p[1]; }).join(' L ');
30756
+ var props;
30757
+ if (head.type == 'arrow') {
30758
+ props = addFillOpacity({d: d + ' Z', fill: color, stroke: 'none'}, rec);
30759
+ } else {
30760
+ // The line's own dashes would break the chevron up, and its cap may be
30761
+ // square or butt; the chevron is always drawn whole and rounded.
30762
+ props = {d: d, fill: 'none', 'stroke-dasharray': 'none',
30763
+ 'stroke-linecap': 'round', 'stroke-linejoin': 'round'};
30764
+ }
30765
+ return {tag: 'path', properties: props};
30766
+ }
30767
+
30768
+ // A filled marker is as transparent as the line it is drawn in
30769
+ function addFillOpacity(props, rec) {
30770
+ if (isSvgNumber(rec['stroke-opacity'])) {
30771
+ props['fill-opacity'] = Number(rec['stroke-opacity']);
30772
+ }
30773
+ return props;
30774
+ }
30775
+
30776
+ function roundPoint(p) {
30777
+ return [Math.round(p[0] * 100) / 100, Math.round(p[1] * 100) / 100];
30778
+ }
30779
+
30780
+ var SvgLineArrows = /*#__PURE__*/Object.freeze({
30781
+ __proto__: null,
30782
+ getLineArrowOpts: getLineArrowOpts,
30783
+ getLineArrowShape: getLineArrowShape,
30784
+ getLineEndType: getLineEndType,
30785
+ lineArrowFields: lineArrowFields,
30786
+ lineHasArrows: lineHasArrows,
30787
+ makeLineArrowOpts: makeLineArrowOpts,
30788
+ renderArrowLine: renderArrowLine
30789
+ });
30790
+
30082
30791
  var geojsonImporters = {
30083
30792
  Point: importPoint,
30084
30793
  Polygon: importPolygon,
@@ -30101,6 +30810,9 @@
30101
30810
  // rather than several labels at several points
30102
30811
  svgObj = renderPathLabel(d, geom.coordinates,
30103
30812
  {report: opts.path_label_report, id: featureId});
30813
+ } else if (msType == 'polyline' && geom.coordinates && lineHasArrows(d)) {
30814
+ svgObj = renderArrowLine(geomType == 'LineString' ?
30815
+ [geom.coordinates] : geom.coordinates, d);
30104
30816
  } else if (geomType && geom.coordinates) {
30105
30817
  svgObj = geojsonImporters[geomType](geom.coordinates, d);
30106
30818
  }
@@ -30428,7 +31140,7 @@ ${svg}
30428
31140
  lyr = copyLayer(lyr);
30429
31141
  adjustRectangleStyle(lyr);
30430
31142
  }
30431
- layerObj.children = exportSymbolsForSVG(lyr, dataset, opts);
31143
+ layerObj.children = exportSymbolsForSVG(lyr, dataset, opts, layerObj);
30432
31144
  return layerObj;
30433
31145
  }
30434
31146
 
@@ -30606,7 +31318,7 @@ ${svg}
30606
31318
  return dataUrl.split(',')[1];
30607
31319
  }
30608
31320
 
30609
- function exportSymbolsForSVG(lyr, dataset, opts) {
31321
+ function exportSymbolsForSVG(lyr, dataset, opts, layerObj) {
30610
31322
  // TODO: convert geojson features one at a time
30611
31323
  var d = utils.defaults({layers: [lyr]}, dataset);
30612
31324
  var geojson = exportDatasetAsGeoJSON(d, opts);
@@ -30618,6 +31330,9 @@ ${svg}
30618
31330
  var children = importGeoJSONFeatures(features,
30619
31331
  utils.defaults({path_label_report: pathLabelReport}, opts));
30620
31332
  reportPathLabels(pathLabelReport, lyr);
31333
+ if (lyr.geometry_type == 'polygon') {
31334
+ applyPolygonGlows(layerObj, children, features, lyr);
31335
+ }
30621
31336
  // Drop empty placeholder <g/> elements (features whose geometry was null in the
30622
31337
  // source data, collapsed during simplification, or otherwise produced no
30623
31338
  // visible output). Keep the layer's records in lockstep so that data-*
@@ -41904,8 +42619,8 @@ ${svg}
41904
42619
  'halo-opacity': {describe: 'halo opacity, 0-1'},
41905
42620
  'label-width': {describe: 'width of a fixed-width text block in px (lines are wrapped in the web UI)'},
41906
42621
  callout: {describe: 'line from the anchor to the text: line, elbow or curve'},
41907
- 'callout-end': {describe: 'arrowhead at the anchor end: arrow, open-arrow or none'},
41908
- 'callout-end-size': {describe: 'length of an arrowhead\'s sides in px (default is 10)'},
42622
+ 'callout-end': {describe: 'marker at the anchor end: arrow, open-arrow, ring or none'},
42623
+ 'callout-end-size': {describe: 'length of an arrowhead\'s sides, or a ring\'s diameter, in px'},
41909
42624
  'callout-via': {describe: 'x,y of an elbow\'s corner or a point on a curve, in px'},
41910
42625
  'callout-attach': {describe: 'where the callout meets the text, as x,y fractions'},
41911
42626
  'callout-gap': {describe: 'px between the callout and the anchor'},
@@ -43943,6 +44658,24 @@ ${svg}
43943
44658
  .option('fill-hatch', {
43944
44659
  alias_to: 'fill-pattern'
43945
44660
  })
44661
+ .option('outer-glow-color', {
44662
+ describe: 'color of a glow outside each polygon (adds the glow)'
44663
+ })
44664
+ .option('outer-glow-width', {
44665
+ describe: 'how far the outer glow reaches, in px (default is 10)'
44666
+ })
44667
+ .option('outer-glow-opacity', {
44668
+ describe: 'opacity of the outer glow, 0-1'
44669
+ })
44670
+ .option('inner-glow-color', {
44671
+ describe: 'color of a glow inside each polygon (adds the glow)'
44672
+ })
44673
+ .option('inner-glow-width', {
44674
+ describe: 'how far the inner glow reaches, in px (default is 10)'
44675
+ })
44676
+ .option('inner-glow-opacity', {
44677
+ describe: 'opacity of the inner glow, 0-1'
44678
+ })
43946
44679
  .option('stroke', {
43947
44680
  describe: 'stroke color'
43948
44681
  })
@@ -43955,6 +44688,15 @@ ${svg}
43955
44688
  .option('stroke-opacity', {
43956
44689
  describe: 'stroke opacity'
43957
44690
  })
44691
+ .option('line-start', {
44692
+ describe: 'marker at the start of each line: arrow, open-arrow, dot or none'
44693
+ })
44694
+ .option('line-end', {
44695
+ describe: 'marker at the end of each line: arrow, open-arrow, dot or none'
44696
+ })
44697
+ .option('line-end-size', {
44698
+ describe: 'length of an arrowhead\'s sides in px (default grows with stroke-width)'
44699
+ })
43958
44700
  .option('opacity', {
43959
44701
  describe: 'opacity; example: 0.5'
43960
44702
  })
@@ -49567,9 +50309,28 @@ ${svg}
49567
50309
  props[key] = val;
49568
50310
  }
49569
50311
  }
50312
+ importPatternFill(props);
49570
50313
  return props;
49571
50314
  }
49572
50315
 
50316
+ // A fill of url(#id), with or without a fallback colour after it, is a
50317
+ // reference to a paint server in <defs>. The ones mapshaper exports are
50318
+ // pattern fills whose id encodes the fill-pattern code, and the fallback is
50319
+ // the feature's own fill. A reference that is not one of those is dropped:
50320
+ // nothing can draw a url() stored as a colour. Its fallback, if any, is kept.
50321
+ function importPatternFill(props) {
50322
+ var match = /^url\(\s*['"]?#([^'")]+)['"]?\s*\)\s*(.*)$/.exec(String(props.fill || '').trim());
50323
+ var code;
50324
+ if (!match) return;
50325
+ code = decodePatternId(match[1]);
50326
+ if (code) props['fill-pattern'] = code;
50327
+ if (match[2]) {
50328
+ props.fill = match[2];
50329
+ } else {
50330
+ delete props.fill;
50331
+ }
50332
+ }
50333
+
49573
50334
  function getNodeStyles(node) {
49574
50335
  var styles = {};
49575
50336
  var attrs = node.attributes || [];
@@ -51877,8 +52638,13 @@ ${svg}
51877
52638
 
51878
52639
  var iconNames = ['circle', 'square', 'ring', 'star'];
51879
52640
 
52641
+ // Icons that render everywhere but are not advertised: the GUI offers them only
52642
+ // to some users (see gui-nyt.mjs), and a map made with one has to draw the same
52643
+ // for anyone who opens it.
52644
+ var unlistedIconNames = ['nyt-star'];
52645
+
51880
52646
  function isSupportedIconName(name) {
51881
- return iconNames.indexOf(name) > -1;
52647
+ return iconNames.indexOf(name) > -1 || unlistedIconNames.indexOf(name) > -1;
51882
52648
  }
51883
52649
 
51884
52650
  cmd.svgStyle = function(lyr, dataset, opts) {
@@ -53038,13 +53804,17 @@ ${svg}
53038
53804
  var sw;
53039
53805
  if (!(r > 0)) return null;
53040
53806
  if (type == 'ring') {
53041
- // drawn as a stroke, one px wide unless stroke-width says otherwise
53807
+ // drawn as a stroke, RING_STROKE_WIDTH wide unless stroke-width says otherwise
53042
53808
  sw = +d['stroke-width'];
53043
- return getSquareBox(r + (sw > 0 ? sw : 1) / 2, 0, 0);
53809
+ return getSquareBox(r + (sw > 0 ? sw : RING_STROKE_WIDTH) / 2, 0, 0);
53044
53810
  }
53045
53811
  if (type == 'circle' || type == 'square' || type == 'star') {
53046
53812
  return getSquareBox(r + getStrokeOutset(d), 0, 0);
53047
53813
  }
53814
+ if (type == 'nyt-star') {
53815
+ // the outline is part of the symbol's size (see nytStar())
53816
+ return getSquareBox(r, 0, 0);
53817
+ }
53048
53818
  return null; // unsupported icons are not drawn
53049
53819
  }
53050
53820
 
@@ -54004,10 +54774,10 @@ ${svg}
54004
54774
  }
54005
54775
 
54006
54776
  function isClosedPath$1(arr) {
54007
- return isArrayOfPoints(arr) && arr.length > 3 && samePoint$2(arr[0], arr[arr.length - 1]);
54777
+ return isArrayOfPoints(arr) && arr.length > 3 && samePoint$3(arr[0], arr[arr.length - 1]);
54008
54778
  }
54009
54779
 
54010
- function samePoint$2(a, b) {
54780
+ function samePoint$3(a, b) {
54011
54781
  return a[0] == b[0] && a[1] == b[1];
54012
54782
  }
54013
54783
 
@@ -56194,12 +56964,12 @@ ${svg}
56194
56964
  return arr[arr.length - 1];
56195
56965
  }
56196
56966
 
56197
- function samePoint$1(a, b) {
56967
+ function samePoint$2(a, b) {
56198
56968
  return a && b && a[0] === b[0] && a[1] === b[1];
56199
56969
  }
56200
56970
 
56201
56971
  function isClosedPath(arr) {
56202
- return samePoint$1(arr[0], lastEl(arr));
56972
+ return samePoint$2(arr[0], lastEl(arr));
56203
56973
  }
56204
56974
 
56205
56975
  // remove likely rounding errors
@@ -56360,7 +57130,7 @@ ${svg}
56360
57130
  if (!nextPart) {
56361
57131
  return null;
56362
57132
  }
56363
- if (samePoint$1(ring[0], nextPart[0])) {
57133
+ if (samePoint$2(ring[0], nextPart[0])) {
56364
57134
  // done!
56365
57135
  ring.push(ring[0]); // close the ring
56366
57136
  return ring;
@@ -56409,7 +57179,7 @@ ${svg}
56409
57179
 
56410
57180
  function findPartStartingAt(parts, firstPoint) {
56411
57181
  for (var i=0; i<parts.length; i++) {
56412
- if (samePoint$1(parts[i][0], firstPoint)) {
57182
+ if (samePoint$2(parts[i][0], firstPoint)) {
56413
57183
  return parts[i];
56414
57184
  }
56415
57185
  }
@@ -56433,7 +57203,7 @@ ${svg}
56433
57203
  var part = [];
56434
57204
  var firstPoint = path[0];
56435
57205
  var lastPoint = lastEl(path);
56436
- var closed = samePoint$1(firstPoint, lastPoint);
57206
+ var closed = samePoint$2(firstPoint, lastPoint);
56437
57207
  var p, pp, y;
56438
57208
  for (var i=0, n=path.length; i<n; i++) {
56439
57209
  p = path[i];
@@ -61099,10 +61869,10 @@ ${svg}
61099
61869
  var p = recenteredVertex(ctx, points[i]);
61100
61870
  // Successive off-center vertices can re-center onto the same neck point;
61101
61871
  // keep one copy so the chain has no zero-length segments.
61102
- if (!samePoint(p, out[out.length - 1])) out.push(p);
61872
+ if (!samePoint$1(p, out[out.length - 1])) out.push(p);
61103
61873
  }
61104
61874
  var last = points[points.length - 1];
61105
- if (!samePoint(last, out[out.length - 1])) out.push(last);
61875
+ if (!samePoint$1(last, out[out.length - 1])) out.push(last);
61106
61876
  return out.length >= 2 ? out : points;
61107
61877
  }
61108
61878
 
@@ -61205,7 +61975,7 @@ ${svg}
61205
61975
  out[1] = ay + t * dy;
61206
61976
  }
61207
61977
 
61208
- function samePoint(a, b) {
61978
+ function samePoint$1(a, b) {
61209
61979
  return a[0] === b[0] && a[1] === b[1];
61210
61980
  }
61211
61981
 
@@ -65565,7 +66335,7 @@ ${svg}
65565
66335
  debug('Short ring', coords);
65566
66336
  return;
65567
66337
  }
65568
- if (!samePoint$1(coords[0], lastEl(coords))) {
66338
+ if (!samePoint$2(coords[0], lastEl(coords))) {
65569
66339
  error('Open polygon ring');
65570
66340
  }
65571
66341
  rings.push(coords); // accumulate rings
@@ -84575,7 +85345,7 @@ ${svg}
84575
85345
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
84576
85346
  }
84577
85347
 
84578
- var version = "0.7.70";
85348
+ var version = "0.7.72";
84579
85349
 
84580
85350
  // Parse command line args into commands and run them
84581
85351
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -87183,6 +87953,257 @@ ${svg}
87183
87953
  restoreCapturedUnits: restoreCapturedUnits
87184
87954
  });
87185
87955
 
87956
+ // Smooths a freehand ("pencil") stroke with a Gaussian kernel while it is
87957
+ // being drawn, placing vertices one at a time, a little behind the pointer.
87958
+ // Vertices never move once they are placed.
87959
+ //
87960
+ // The trace of the pointer samples is densified to even spacing and smoothed
87961
+ // with the Gaussian kernel that -smooth uses, as a plain weighted average in
87962
+ // arc length. (-smooth adds a quadratic correction that keeps bends from being
87963
+ // flattened, but it lets through far more of the jitter of a slow stroke.) A
87964
+ // smoothed point depends only on the trace within a window around it, so it
87965
+ // is final as soon as the trace extends a window radius beyond it. At the
87966
+ // start of the stroke the trace is extended by an odd reflection about its
87967
+ // first point, which keeps the smoothed curve starting exactly there, and the
87968
+ // end is treated the same way when the stroke is finished.
87969
+ //
87970
+ // Vertices are kept from the smoothed points where the curve has turned by a
87971
+ // given angle since the last vertex, or where the chord from the last vertex
87972
+ // would stray too far from a gentle curve -- the rule -smooth uses to space
87973
+ // its output. So vertices are close together at bends and far apart on
87974
+ // straight stretches, and they appear one at a time.
87975
+ //
87976
+ // Distances are in pixels. For samples in other units, the pixelSize option
87977
+ // gives the size of a pixel in those units.
87978
+
87979
+ // Standard deviation of the smoothing kernel, in arc length. Below about 5px,
87980
+ // the jitter of a slow stroke comes through; larger values round off bends a
87981
+ // few pixels across.
87982
+ var SIGMA = 8;
87983
+
87984
+ // Half-width of the smoothing window, in multiples of SIGMA. This is also how
87985
+ // far the pointer has to be ahead of a smoothed point for it to be final.
87986
+ var WINDOW_RADIUS = 3;
87987
+
87988
+ // Spacing of the densified trace, and of the smoothed points that are tested
87989
+ // as vertices. Small enough that a bend of radius SIGMA turns by only a few
87990
+ // degrees per step.
87991
+ var SOURCE_STEP = 0.5;
87992
+ var OUTPUT_STEP = 0.5;
87993
+
87994
+ // Greatest turn between consecutive segments of the output, and greatest
87995
+ // distance of the output from the smoothed curve along gentle bends.
87996
+ var BEND_ANGLE = 12;
87997
+ var MAX_DEVIATION = 0.75;
87998
+
87999
+ // Spacing of the points returned by getPreview(), and the standard deviation
88000
+ // of the kernel that smooths the preview at the pointer. The preview is
88001
+ // smoothed more heavily further from the pointer, up to SIGMA where it meets
88002
+ // the placed vertices, so that it runs into them without a step. A small
88003
+ // kernel at the pointer removes pixel jitter without lagging behind.
88004
+ var PREVIEW_SPACING = 2;
88005
+ var PREVIEW_SIGMA = 2;
88006
+
88007
+ // start: [x, y] first sample
88008
+ // opts: (optional) {sigma, previewSigma, bendAngle (degrees), maxDeviation, pixelSize}
88009
+ function GaussianStrokeFitter(start, opts) {
88010
+ var px = opts && opts.pixelSize > 0 ? opts.pixelSize : 1;
88011
+ var sigma = getOpt(opts, 'sigma', SIGMA) * px;
88012
+ var radius = sigma * WINDOW_RADIUS;
88013
+ var previewSigma = Math.min(getOpt(opts, 'previewSigma', PREVIEW_SIGMA) * px, sigma);
88014
+ var step = SOURCE_STEP * px;
88015
+ var outputStep = OUTPUT_STEP * px;
88016
+ var decimator = new BendDecimator(start, getOpt(opts, 'bendAngle', BEND_ANGLE) * Math.PI / 180,
88017
+ getOpt(opts, 'maxDeviation', MAX_DEVIATION) * px);
88018
+ var samples = [[start[0], start[1]]];
88019
+ // the densified trace: arc length and coordinates
88020
+ var tt = [0], xx = [start[0]], yy = [start[1]];
88021
+ var traceLen = 0; // length of the densified part of the trace
88022
+ var nextT = step; // arc length of the next densified point
88023
+ var phi = outputStep; // arc length of the next smoothed point
88024
+
88025
+ // Returns the vertices placed by the new sample, which may be none.
88026
+ this.addSample = function(p) {
88027
+ var n = samples.length;
88028
+ if (!isSample(p) || samePoint(p, samples[n - 1])) return [];
88029
+ samples.push([p[0], p[1]]);
88030
+ densifySegment(n - 1);
88031
+ return placeSmoothedPoints(traceLen - radius, false);
88032
+ };
88033
+
88034
+ // Returns the vertices that complete the stroke, ending at the last sample.
88035
+ this.finish = function() {
88036
+ var n = samples.length, end, placed, p;
88037
+ if (n < 2) return [];
88038
+ end = samples[n - 1];
88039
+ if (tt[tt.length - 1] < traceLen) {
88040
+ tt.push(traceLen);
88041
+ xx.push(end[0]);
88042
+ yy.push(end[1]);
88043
+ }
88044
+ placed = placeSmoothedPoints(traceLen - outputStep / 2, true);
88045
+ p = decimator.push(end);
88046
+ if (p) placed.push(p);
88047
+ placed.push([end[0], end[1]]);
88048
+ return placed;
88049
+ };
88050
+
88051
+ // Returns points to show between the last placed vertex and the pointer
88052
+ // (not including either): the latest smoothed point, followed by the rest of
88053
+ // the trace, smoothed less and less towards the pointer (see PREVIEW_SIGMA).
88054
+ // These points are not final.
88055
+ this.getPreview = function() {
88056
+ var cand = decimator.getCandidate();
88057
+ var t0 = cand ? phi - outputStep : 0; // arc length of the candidate
88058
+ var spacing = PREVIEW_SPACING * px;
88059
+ var out = cand ? [cand] : [];
88060
+ var t, s;
88061
+ for (t = t0 + spacing; t < traceLen - spacing / 2; t += spacing) {
88062
+ s = previewSigma + (sigma - previewSigma) * (traceLen - t) / (traceLen - t0);
88063
+ out.push(smoothAt(t, true, s));
88064
+ }
88065
+ return out;
88066
+ };
88067
+
88068
+ // Smooths the trace up to arc length @maxT and returns the vertices placed
88069
+ // from the smoothed points.
88070
+ function placeSmoothedPoints(maxT, atEnd) {
88071
+ var placed = [], p;
88072
+ for (; phi <= maxT; phi += outputStep) {
88073
+ p = decimator.push(smoothAt(phi, atEnd));
88074
+ if (p) placed.push(p);
88075
+ }
88076
+ return placed;
88077
+ }
88078
+
88079
+ // atEnd: treat the last sample as the end of the stroke
88080
+ // s: (optional) standard deviation of the kernel, if not sigma
88081
+ function smoothAt(t, atEnd, s) {
88082
+ var r = s ? s * WINDOW_RADIUS : radius,
88083
+ n = tt.length,
88084
+ lo = firstIndexAbove(t - r),
88085
+ hi = firstIndexAbove(t + r),
88086
+ end = samples[samples.length - 1],
88087
+ wt = [], wx = [], wy = [], i;
88088
+ // odd reflection of the start of the trace
88089
+ for (i = Math.min(firstIndexAbove(r - t), n) - 1; i > 0; i--) {
88090
+ wt.push(-tt[i]);
88091
+ wx.push(2 * xx[0] - xx[i]);
88092
+ wy.push(2 * yy[0] - yy[i]);
88093
+ }
88094
+ for (i = lo; i < hi; i++) {
88095
+ wt.push(tt[i]);
88096
+ wx.push(xx[i]);
88097
+ wy.push(yy[i]);
88098
+ }
88099
+ if (atEnd) {
88100
+ // the trace stops short of the last sample until finish() adds it
88101
+ if (tt[n - 1] < traceLen) {
88102
+ wt.push(traceLen);
88103
+ wx.push(end[0]);
88104
+ wy.push(end[1]);
88105
+ }
88106
+ // odd reflection of the end of the trace
88107
+ for (i = n - 1; i >= 0 && 2 * traceLen - tt[i] < t + r; i--) {
88108
+ if (tt[i] >= traceLen) continue;
88109
+ wt.push(2 * traceLen - tt[i]);
88110
+ wx.push(2 * end[0] - xx[i]);
88111
+ wy.push(2 * end[1] - yy[i]);
88112
+ }
88113
+ }
88114
+ return smoothPoint(wt, [wx, wy], 0, wt.length, t, 'gaussian', s || sigma, r, 0);
88115
+ }
88116
+
88117
+ // first index of the trace with arc length > @t
88118
+ function firstIndexAbove(t) {
88119
+ var lo = 0, hi = tt.length, mid;
88120
+ while (lo < hi) {
88121
+ mid = (lo + hi) >> 1;
88122
+ if (tt[mid] <= t) lo = mid + 1;
88123
+ else hi = mid;
88124
+ }
88125
+ return lo;
88126
+ }
88127
+
88128
+ // Adds points to the trace at even spacing from sample @i to the next one.
88129
+ function densifySegment(i) {
88130
+ var a = samples[i],
88131
+ b = samples[i + 1],
88132
+ len = distance(a, b),
88133
+ f;
88134
+ for (; nextT <= traceLen + len; nextT += step) {
88135
+ f = (nextT - traceLen) / len;
88136
+ tt.push(nextT);
88137
+ xx.push(a[0] + (b[0] - a[0]) * f);
88138
+ yy.push(a[1] + (b[1] - a[1]) * f);
88139
+ }
88140
+ traceLen += len;
88141
+ }
88142
+ }
88143
+
88144
+ // Picks vertices from a stream of closely spaced points along a smooth curve,
88145
+ // keeping a point once the curve has turned by @bendAngle (radians) since the
88146
+ // last kept point, or once the chord from the last kept point would bow more
88147
+ // than @maxDeviation from the curve (estimated as for a circular arc). This is
88148
+ // the rule -smooth uses (decimateByBend() in mapshaper-smooth-algos.mjs),
88149
+ // applied one point at a time.
88150
+ function BendDecimator(start, bendAngle, maxDeviation) {
88151
+ var kept = start, prev = start, candidate = null, turn = 0;
88152
+
88153
+ // Returns the point before @p if it is kept, or null.
88154
+ this.push = function(p) {
88155
+ var out = null;
88156
+ if (candidate) {
88157
+ turn += getTurn(prev, candidate, p);
88158
+ if (turn >= bendAngle || distance(kept, p) * turn / 8 >= maxDeviation) {
88159
+ out = kept = candidate;
88160
+ turn = 0;
88161
+ }
88162
+ prev = candidate;
88163
+ }
88164
+ candidate = p;
88165
+ return out;
88166
+ };
88167
+
88168
+ // The latest point, which has not been kept yet
88169
+ this.getCandidate = function() {
88170
+ return candidate;
88171
+ };
88172
+ }
88173
+
88174
+ function getTurn(a, b, c) {
88175
+ var ux = b[0] - a[0], uy = b[1] - a[1],
88176
+ vx = c[0] - b[0], vy = c[1] - b[1],
88177
+ len = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy)),
88178
+ cos;
88179
+ if (!(len > 0)) return 0;
88180
+ cos = (ux * vx + uy * vy) / len;
88181
+ return Math.acos(Math.min(1, Math.max(-1, cos)));
88182
+ }
88183
+
88184
+ function getOpt(opts, name, defaultVal) {
88185
+ return opts && opts[name] > 0 ? opts[name] : defaultVal;
88186
+ }
88187
+
88188
+ function isSample(p) {
88189
+ return !!p && isFinite(p[0]) && isFinite(p[1]);
88190
+ }
88191
+
88192
+ function samePoint(a, b) {
88193
+ return a[0] == b[0] && a[1] == b[1];
88194
+ }
88195
+
88196
+ function distance(a, b) {
88197
+ var dx = b[0] - a[0], dy = b[1] - a[1];
88198
+ return Math.sqrt(dx * dx + dy * dy);
88199
+ }
88200
+
88201
+ var PencilGaussian = /*#__PURE__*/Object.freeze({
88202
+ __proto__: null,
88203
+ BendDecimator: BendDecimator,
88204
+ GaussianStrokeFitter: GaussianStrokeFitter
88205
+ });
88206
+
87186
88207
  // Attach functions exported by modules to the "internal" object,
87187
88208
  // so they can be run by tests and by the GUI.
87188
88209
  // TODO: rewrite tests to import functions directly from modules,
@@ -87192,7 +88213,8 @@ ${svg}
87192
88213
  internal.svg = Object.assign({}, SvgStringify, SvgPathUtils, GeojsonToSvg,
87193
88214
  SvgFeatureUtils,
87194
88215
  SvgLabels, SvgSymbols, SvgLabelPaths, SvgLabelFit, SvgLabelAlign,
87195
- SvgLabelMetrics, SvgLabelHalo, SvgLabelCallout, SvgLabelMarkup);
88216
+ SvgLabelMetrics, SvgLabelHalo, SvgLabelCallout, SvgLabelMarkup, SvgLineArrows,
88217
+ SvgGlow);
87196
88218
 
87197
88219
  // Reached through the bundle rather than imported from source, unlike most of
87198
88220
  // what tests use, because these modules load fs and fontkit through the
@@ -87317,6 +88339,7 @@ ${svg}
87317
88339
  PathExport,
87318
88340
  Pathfinder,
87319
88341
  PathfinderUtils,
88342
+ PencilGaussian,
87320
88343
  PathImport,
87321
88344
  PathRepair,
87322
88345
  PathUtils,