mapshaper 0.7.69 → 0.7.71

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
@@ -5882,13 +5882,13 @@
5882
5882
  return msg;
5883
5883
  }
5884
5884
 
5885
- function requirePointLayer$1(lyr, msg) {
5885
+ function requirePointLayer$2(lyr, msg) {
5886
5886
  if (!lyr || lyr.geometry_type !== 'point')
5887
5887
  stop$1(layerTypeMessage(lyr, "Expected a point layer", msg));
5888
5888
  }
5889
5889
 
5890
5890
  function requireSinglePointLayer(lyr, msg) {
5891
- requirePointLayer$1(lyr);
5891
+ requirePointLayer$2(lyr);
5892
5892
  if (countMultiPartFeatures(lyr.shapes) > 0) {
5893
5893
  stop$1(msg || 'This command requires single points; layer contains multi-point features.');
5894
5894
  }
@@ -6128,7 +6128,7 @@
6128
6128
  requireDataFields: requireDataFields,
6129
6129
  requireNotRasterLayer: requireNotRasterLayer,
6130
6130
  requirePathLayer: requirePathLayer,
6131
- requirePointLayer: requirePointLayer$1,
6131
+ requirePointLayer: requirePointLayer$2,
6132
6132
  requirePolygonLayer: requirePolygonLayer,
6133
6133
  requirePolylineLayer: requirePolylineLayer,
6134
6134
  requireSinglePointLayer: requireSinglePointLayer,
@@ -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
 
@@ -26245,10 +26245,10 @@
26245
26245
  parts.unshift('hatches');
26246
26246
  obj = parseHatches(parts); // hatches is the default, name can be omitted
26247
26247
  }
26248
- if (!obj) {
26249
- // consider
26250
- message('Invalid pattern, ignoring:', str);
26251
- }
26248
+ // Silent: this is also how a value is tested for being a pattern at all
26249
+ // (-style tries every value as a literal before trying it as an
26250
+ // expression), so a failure here is not necessarily a mistake. The SVG
26251
+ // exporter reports the patterns it has to drop.
26252
26252
  return obj;
26253
26253
  }
26254
26254
 
@@ -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,13 +26488,19 @@
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) {
26389
26498
  var o = parsePattern(str);
26390
26499
  var svg;
26391
- if (!o) return null;
26500
+ if (!o) {
26501
+ message('Invalid pattern, ignoring:', str);
26502
+ return null;
26503
+ }
26392
26504
  if (o.type == 'hatches') {
26393
26505
  svg = makeHatchPatternSVG(o);
26394
26506
  } else if (o.type == 'dots' || o.type == 'squares') {
@@ -26459,6 +26571,8 @@
26459
26571
  var SvgHatch = /*#__PURE__*/Object.freeze({
26460
26572
  __proto__: null,
26461
26573
  convertFillPattern: convertFillPattern,
26574
+ decodePatternId: decodePatternId,
26575
+ getHashId: getHashId,
26462
26576
  parseDashes: parseDashes,
26463
26577
  parseDots: parseDots,
26464
26578
  parseHatches: parseHatches,
@@ -26756,6 +26870,11 @@
26756
26870
  'callout-color': 'color',
26757
26871
  'callout-width': 'number',
26758
26872
  'callout-opacity': 'number',
26873
+ // arrowheads at the ends of a line -- see svg-line-arrows.mjs. Drawn as
26874
+ // shapes, not written as attributes, so not in propertiesBySymbolType.
26875
+ 'line-start': 'lineend',
26876
+ 'line-end': 'lineend',
26877
+ 'line-end-size': 'number',
26759
26878
  'letter-spacing': 'measure',
26760
26879
  'line-height': 'measure',
26761
26880
  opacity: 'number',
@@ -27067,6 +27186,8 @@
27067
27186
  val = parseCalloutType(strVal);
27068
27187
  } else if (type == 'calloutend') {
27069
27188
  val = parseCalloutEnd(strVal);
27189
+ } else if (type == 'lineend') {
27190
+ val = parseLineEnd(strVal);
27070
27191
  } else if (type == 'pointpair') {
27071
27192
  val = parsePointPair(strVal);
27072
27193
  val = val ? formatPointPair(val) : null;
@@ -27118,6 +27239,12 @@
27118
27239
  return /^(arrow|open-arrow|none)$/.test(end) ? end : null;
27119
27240
  }
27120
27241
 
27242
+ // A line's ends take a callout's markers, and a dot
27243
+ function parseLineEnd(str) {
27244
+ var end = String(str).trim().toLowerCase();
27245
+ return end == 'dot' ? end : parseCalloutEnd(end);
27246
+ }
27247
+
27121
27248
  // "x,y" -> [x, y], or null. Stored as a string so that a pair is set and
27122
27249
  // cleared as one value and reads plainly in a table.
27123
27250
  function parsePointPair(str) {
@@ -27237,6 +27364,7 @@
27237
27364
  parseCalloutEnd: parseCalloutEnd,
27238
27365
  parseCalloutType: parseCalloutType,
27239
27366
  parseLabelPosition: parseLabelPosition,
27367
+ parseLineEnd: parseLineEnd,
27240
27368
  parsePointPair: parsePointPair,
27241
27369
  parseStyleLiteral: parseStyleLiteral,
27242
27370
  parseSvgMeasure: parseSvgMeasure,
@@ -27383,6 +27511,126 @@
27383
27511
  splitLabelHalos: splitLabelHalos
27384
27512
  });
27385
27513
 
27514
+ // Bold words inside a label: label-text='Israel <b>restricted access</b> here'.
27515
+ //
27516
+ // The markup is part of the text rather than a column beside it, so that the
27517
+ // words and their weight cannot drift apart: an expression, a join or a -labels
27518
+ // text= that rewrites the text rewrites the bold with it. The GUI editor never
27519
+ // shows the tags -- it reads them into ranges and writes them back.
27520
+ //
27521
+ // Only a matched <b>...</b> pair with no other b tag inside it is markup.
27522
+ // Anything else -- an unclosed <b>, a stray </b>, "a < b" -- is text, and is
27523
+ // drawn as typed.
27524
+ //
27525
+ // See docs/development/label-tool-design.md.
27526
+
27527
+ var BOLD_PAIR_RXP = /<b>((?:(?!<\/?b>)[\s\S])*?)<\/b>/gi;
27528
+ var BOLD_TAG_RXP = /<\/?b>/i;
27529
+
27530
+ // The font-weight a bold run is drawn with: the keyword, which a browser and
27531
+ // the Node text measurer both resolve to the family's own bold face.
27532
+ var LABEL_BOLD_WEIGHT = 'bold';
27533
+
27534
+ // @str -> {text, bold}: the text with its bold pairs removed, and the bold
27535
+ // stretches as ascending, non-overlapping [start, end) offsets into it.
27536
+ function parseBoldMarkup(str) {
27537
+ var s = str === null || str === undefined ? '' : String(str);
27538
+ var text = '', bold = [], last = 0, m;
27539
+ if (!BOLD_TAG_RXP.test(s)) return {text: s, bold: bold};
27540
+ BOLD_PAIR_RXP.lastIndex = 0;
27541
+ while ((m = BOLD_PAIR_RXP.exec(s))) {
27542
+ text += s.substring(last, m.index);
27543
+ if (m[1]) {
27544
+ bold.push([text.length, text.length + m[1].length]);
27545
+ text += m[1];
27546
+ }
27547
+ last = m.index + m[0].length;
27548
+ }
27549
+ text += s.substring(last);
27550
+ return {text: text, bold: normalizeBoldRanges(bold)};
27551
+ }
27552
+
27553
+ function stripBoldMarkup(str) {
27554
+ return parseBoldMarkup(str).text;
27555
+ }
27556
+
27557
+ // Sorted, clipped to non-empty, with touching and overlapping ranges merged.
27558
+ // Returns a new array.
27559
+ function normalizeBoldRanges(ranges) {
27560
+ var sorted = (ranges || []).filter(function(r) {
27561
+ return r && r[1] > r[0];
27562
+ }).map(function(r) {
27563
+ return [r[0], r[1]];
27564
+ }).sort(function(a, b) {
27565
+ return a[0] - b[0];
27566
+ });
27567
+ var out = [];
27568
+ sorted.forEach(function(r) {
27569
+ var prev = out[out.length - 1];
27570
+ if (prev && r[0] <= prev[1]) {
27571
+ prev[1] = Math.max(prev[1], r[1]);
27572
+ } else {
27573
+ out.push(r);
27574
+ }
27575
+ });
27576
+ return out;
27577
+ }
27578
+
27579
+ // The characters [start, end) of @text as runs of one weight:
27580
+ // [{text, bold}, ...], with nothing for an empty stretch.
27581
+ function getBoldRuns(text, bold, start, end) {
27582
+ var runs = [], pos = start, i, r, a, b;
27583
+ for (i = 0; i < bold.length && pos < end; i++) {
27584
+ r = bold[i];
27585
+ a = Math.max(r[0], pos);
27586
+ b = Math.min(r[1], end);
27587
+ if (b <= a) continue;
27588
+ if (a > pos) runs.push({text: text.substring(pos, a), bold: false});
27589
+ runs.push({text: text.substring(a, b), bold: true});
27590
+ pos = b;
27591
+ }
27592
+ if (pos < end) runs.push({text: text.substring(pos, end), bold: false});
27593
+ return runs;
27594
+ }
27595
+
27596
+ // @runs -> @obj with the first plain run as its value and the rest as <tspan>s,
27597
+ // which is the shape the stringifier writes as mixed content. A label with no
27598
+ // bold in it comes out exactly as it did before bold existed: one value and no
27599
+ // children. Returns @obj.
27600
+ function applyRunsToSvgObject(obj, runs) {
27601
+ var i = 0;
27602
+ var children = [];
27603
+ if (runs.length > 0 && !runs[0].bold) {
27604
+ obj.value = runs[0].text;
27605
+ i = 1;
27606
+ } else {
27607
+ obj.value = '';
27608
+ }
27609
+ for (; i < runs.length; i++) {
27610
+ children.push(renderRun(runs[i]));
27611
+ }
27612
+ if (children.length > 0) {
27613
+ obj.children = children.concat(obj.children || []);
27614
+ }
27615
+ return obj;
27616
+ }
27617
+
27618
+ function renderRun(run) {
27619
+ var o = {tag: 'tspan', value: run.text};
27620
+ if (run.bold) o.properties = {'font-weight': LABEL_BOLD_WEIGHT};
27621
+ return o;
27622
+ }
27623
+
27624
+ var SvgLabelMarkup = /*#__PURE__*/Object.freeze({
27625
+ __proto__: null,
27626
+ LABEL_BOLD_WEIGHT: LABEL_BOLD_WEIGHT,
27627
+ applyRunsToSvgObject: applyRunsToSvgObject,
27628
+ getBoldRuns: getBoldRuns,
27629
+ normalizeBoldRanges: normalizeBoldRanges,
27630
+ parseBoldMarkup: parseBoldMarkup,
27631
+ stripBoldMarkup: stripBoldMarkup
27632
+ });
27633
+
27386
27634
  // Accepting \n (two chars) as an alternative to the newline character
27387
27635
  // (sometimes, '\n' is not converted to newline, e.g. in a Makefile)
27388
27636
  // Also accepting <br>
@@ -27399,21 +27647,45 @@
27399
27647
  var anyLabelBreakRxp = /(\n|\\n|<br>|<wbr>)/i;
27400
27648
  var softBreakRxp = /^<wbr>$/i;
27401
27649
 
27402
- // The lines a label's text is drawn as. Whitespace before a soft break is
27403
- // dropped: a trailing space would move an end- or middle-justified line by a
27404
- // space's width.
27650
+ // The lines a label's text is drawn as, without its bold markup.
27405
27651
  function splitLabelLines(text) {
27406
- var parts = String(text).split(anyLabelBreakRxp);
27407
- var lines = [parts[0]];
27652
+ return splitLabelLineRuns(text).map(function(runs) {
27653
+ return runs.map(function(run) { return run.text; }).join('');
27654
+ });
27655
+ }
27656
+
27657
+ // The lines a label's text is drawn as, each one a list of runs of one weight:
27658
+ // [[{text, bold}, ...], ...]. A bold stretch can cross a line break, and is
27659
+ // then a run on each line.
27660
+ //
27661
+ // Whitespace before a soft break is dropped: a trailing space would move an
27662
+ // end- or middle-justified line by a space's width.
27663
+ function splitLabelLineRuns(text) {
27664
+ var o = parseBoldMarkup(String(text));
27665
+ var parts = o.text.split(anyLabelBreakRxp);
27666
+ var pos = parts[0].length;
27667
+ var lines = [getBoldRuns(o.text, o.bold, 0, pos)];
27408
27668
  for (var i = 1; i < parts.length; i += 2) {
27409
27669
  if (softBreakRxp.test(parts[i])) {
27410
- lines[lines.length - 1] = lines[lines.length - 1].replace(/\s+$/, '');
27670
+ trimTrailingSpace(lines[lines.length - 1]);
27411
27671
  }
27412
- lines.push(parts[i + 1]);
27672
+ pos += parts[i].length;
27673
+ lines.push(getBoldRuns(o.text, o.bold, pos, pos + parts[i + 1].length));
27674
+ pos += parts[i + 1].length;
27413
27675
  }
27414
27676
  return lines;
27415
27677
  }
27416
27678
 
27679
+ function trimTrailingSpace(runs) {
27680
+ var last;
27681
+ while (runs.length > 0) {
27682
+ last = runs[runs.length - 1];
27683
+ last.text = last.text.replace(/\s+$/, '');
27684
+ if (last.text) return;
27685
+ runs.pop();
27686
+ }
27687
+ }
27688
+
27417
27689
  function removeSoftBreaks(text) {
27418
27690
  return String(text).replace(/<wbr>/gi, '');
27419
27691
  }
@@ -27464,7 +27736,7 @@
27464
27736
  // already been through it, so calling it here as well costs nothing and means
27465
27737
  // every way into the renderer draws a label in the position it is stored in.
27466
27738
  var rec = resolveLabelPosition(recArg);
27467
- var morelines = splitLabelLines(toLabelString(rec['label-text']));
27739
+ var morelines = splitLabelLineRuns(toLabelString(rec['label-text']));
27468
27740
  var line = morelines.shift();
27469
27741
  var obj;
27470
27742
  var dx = applyAlignmentShift(rec);
@@ -27475,23 +27747,21 @@
27475
27747
  y: dy,
27476
27748
  x: dx
27477
27749
  };
27478
- obj = {
27750
+ obj = applyRunsToSvgObject({
27479
27751
  tag: 'text',
27480
- value: line,
27481
27752
  properties: properties
27482
- };
27753
+ }, line);
27483
27754
  if (morelines.length > 0) {
27484
27755
  // multiline label
27485
- obj.children = [];
27756
+ obj.children = obj.children || [];
27486
27757
  morelines.forEach(function(line) {
27487
- var tspan = {
27758
+ var tspan = applyRunsToSvgObject({
27488
27759
  tag: 'tspan',
27489
- value: line,
27490
27760
  properties: {
27491
27761
  x: dx,
27492
27762
  dy: getLineHeightDy(rec['line-height'])
27493
27763
  }
27494
- };
27764
+ }, line);
27495
27765
  obj.children.push(tspan);
27496
27766
  });
27497
27767
  }
@@ -27591,11 +27861,91 @@
27591
27861
  removeSoftBreaks: removeSoftBreaks,
27592
27862
  renderLabel: renderLabel$1,
27593
27863
  renderStyledLabel: renderStyledLabel,
27864
+ splitLabelLineRuns: splitLabelLineRuns,
27594
27865
  splitLabelLines: splitLabelLines,
27595
27866
  toLabelString: toLabelString,
27596
27867
  toPixels: toPixels
27597
27868
  });
27598
27869
 
27870
+ // Arrowhead geometry shared by label callouts (svg-label-callout.mjs) and the
27871
+ // ends of line features (svg-line-arrows.mjs), so that the two draw the same
27872
+ // heads. Everything is in px, y down.
27873
+
27874
+ // The angle at an arrowhead's point, degrees. The open one is wider, since a
27875
+ // narrow chevron drawn with a line reads as a thickened line more than as an
27876
+ // arrow.
27877
+ var ARROW_ANGLE = 35;
27878
+ var OPEN_ARROW_ANGLE = 70;
27879
+ // How far into a solid arrowhead the line reaches, as a fraction of the
27880
+ // head's length: far enough that its cap is hidden, short of the tip.
27881
+ var ARROW_LINE_OVERLAP = 0.7;
27882
+
27883
+ var DEFAULT_LINE_WIDTH$2 = 1;
27884
+
27885
+ // An arrowhead's size when nothing says, in px: the length of its sides. 10
27886
+ // for a 1px line, growing with the line, so that a heavier line does not end
27887
+ // in a head too small to read.
27888
+ function getDefaultArrowSize(lineWidth) {
27889
+ var w = lineWidth > 0 ? lineWidth : DEFAULT_LINE_WIDTH$2;
27890
+ return 7 + 3 * w;
27891
+ }
27892
+
27893
+ // [tip, wing, wing], for a head with sides @side long meeting at @angle degrees,
27894
+ // pointing at @tip from direction @dir (a unit vector from the tip back along
27895
+ // the line)
27896
+ function getArrowHead(tip, dir, side, angle) {
27897
+ var len = getArrowHeadLength(side, angle);
27898
+ var half = side * Math.sin(angle / 2 * Math.PI / 180);
27899
+ var bx = tip[0] + dir[0] * len;
27900
+ var by = tip[1] + dir[1] * len;
27901
+ return [tip, [bx - dir[1] * half, by + dir[0] * half], [bx + dir[1] * half, by - dir[0] * half]];
27902
+ }
27903
+
27904
+ // How far along the line a head with sides @side long reaches
27905
+ function getArrowHeadLength(side, angle) {
27906
+ return side * Math.cos(angle / 2 * Math.PI / 180);
27907
+ }
27908
+
27909
+ // The part of polyline @coords after it first leaves the circle of radius @r
27910
+ // around its first point, starting on the circle; or null if it never does.
27911
+ function trimPolyline$1(coords, r) {
27912
+ var p0 = coords[0];
27913
+ for (var i = 1; i < coords.length; i++) {
27914
+ if (distance$1(p0, coords[i]) > r) {
27915
+ return [getCircleExit(p0, r, coords[i - 1], coords[i])].concat(coords.slice(i));
27916
+ }
27917
+ }
27918
+ return null;
27919
+ }
27920
+
27921
+ // The point where segment @p-@q, which starts inside the circle and ends
27922
+ // outside it, crosses it.
27923
+ function getCircleExit(c, r, p, q) {
27924
+ var dx = q[0] - p[0];
27925
+ var dy = q[1] - p[1];
27926
+ var ex = p[0] - c[0];
27927
+ var ey = p[1] - c[1];
27928
+ var a = dx * dx + dy * dy;
27929
+ var b = 2 * (dx * ex + dy * ey);
27930
+ var k = ex * ex + ey * ey - r * r;
27931
+ var s = (-b + Math.sqrt(Math.max(0, b * b - 4 * a * k))) / (2 * a);
27932
+ s = Math.max(0, Math.min(1, s));
27933
+ return [p[0] + s * dx, p[1] + s * dy];
27934
+ }
27935
+
27936
+ function getUnitVector(p, q) {
27937
+ var dx = q[0] - p[0];
27938
+ var dy = q[1] - p[1];
27939
+ var len = Math.sqrt(dx * dx + dy * dy);
27940
+ return len > 0 ? [dx / len, dy / len] : null;
27941
+ }
27942
+
27943
+ function distance$1(p, q) {
27944
+ var dx = q[0] - p[0];
27945
+ var dy = q[1] - p[1];
27946
+ return Math.sqrt(dx * dx + dy * dy);
27947
+ }
27948
+
27599
27949
  // A callout: a line from a label's anchor to its text, straight, elbowed or
27600
27950
  // curved, optionally ending in an arrowhead at the anchor. A dot at the anchor
27601
27951
  // is the label's icon, not a callout marker.
@@ -27608,7 +27958,7 @@
27608
27958
  // See docs/development/text-annotation-design.md.
27609
27959
 
27610
27960
  var DEFAULT_PADDING$1 = 3;
27611
- var DEFAULT_LINE_WIDTH = 1;
27961
+ var DEFAULT_LINE_WIDTH$1 = 1;
27612
27962
  // Space left between the anchor's symbol and the end of the line
27613
27963
  var SYMBOL_CLEARANCE = 2;
27614
27964
  // How far an automatic curve bows out, as a fraction of its chord
@@ -27616,14 +27966,6 @@
27616
27966
  // The shortest first leg an automatic elbow into the top or bottom of the text
27617
27967
  // is drawn with, px
27618
27968
  var MIN_LEG = 8;
27619
- // The angle at an arrowhead's point, degrees. The open one is wider, since a
27620
- // narrow chevron drawn with a line reads as a thickened line more than as an
27621
- // arrow.
27622
- var ARROW_ANGLE = 44;
27623
- var OPEN_ARROW_ANGLE = 70;
27624
- // How far into a solid arrowhead the line reaches, as a fraction of the
27625
- // head's length: far enough that its cap is hidden, short of the tip.
27626
- var ARROW_LINE_OVERLAP = 0.7;
27627
27969
  // Where glyphs sit relative to their baseline, in ems, for estimating the
27628
27970
  // height of a block of text that is measured only for its width
27629
27971
  var ASCENT = 0.8;
@@ -27668,8 +28010,7 @@
27668
28010
  // default line, and growing with the line, so that a heavier line does not end
27669
28011
  // in a head too small to read.
27670
28012
  function getDefaultCalloutEndSize(end, lineWidth) {
27671
- var w = lineWidth > 0 ? lineWidth : DEFAULT_LINE_WIDTH;
27672
- return 7 + 3 * w;
28013
+ return getDefaultArrowSize(lineWidth);
27673
28014
  }
27674
28015
 
27675
28016
  // 'arrow', 'open-arrow' or 'none'
@@ -27777,7 +28118,7 @@
27777
28118
  v = o.via || getAutoElbow(a, t, box);
27778
28119
  // A corner on either end is no corner -- straight under the text, say --
27779
28120
  // and a leg of no length has no direction for an arrowhead to take.
27780
- path = {kind: 'polyline', coords: distance(a, v) > 0 && distance(v, t) > 0 ?
28121
+ path = {kind: 'polyline', coords: distance$1(a, v) > 0 && distance$1(v, t) > 0 ?
27781
28122
  [a, v, t] : [a, t]};
27782
28123
  } else if (o.type == 'curve') {
27783
28124
  v = o.via || getAutoBend(a, t);
@@ -27805,7 +28146,7 @@
27805
28146
  // the chevron's point: the point is set back by that much, so that the
27806
28147
  // arrow ends where a filled one would. The join and the round cap at the
27807
28148
  // far end each add half a line width to an arm, which is taken off it.
27808
- lineWidth = o.width > 0 ? o.width : DEFAULT_LINE_WIDTH;
28149
+ lineWidth = o.width > 0 ? o.width : DEFAULT_LINE_WIDTH$1;
27809
28150
  path = trimPath(path, lineWidth / 2) || path;
27810
28151
  dir = getStartDirection(path);
27811
28152
  if (!dir) return null;
@@ -27982,44 +28323,21 @@
27982
28323
  function hasLength(path) {
27983
28324
  var c = path.coords;
27984
28325
  for (var i = 1; i < c.length; i++) {
27985
- if (distance(c[0], c[i]) > 0) return true;
28326
+ if (distance$1(c[0], c[i]) > 0) return true;
27986
28327
  }
27987
28328
  return false;
27988
28329
  }
27989
28330
 
27990
28331
  function trimPolyline(coords, r) {
27991
- var p0 = coords[0];
27992
- for (var i = 1; i < coords.length; i++) {
27993
- if (distance(p0, coords[i]) > r) {
27994
- return {
27995
- kind: 'polyline',
27996
- coords: [getCircleExit(p0, r, coords[i - 1], coords[i])].concat(coords.slice(i))
27997
- };
27998
- }
27999
- }
28000
- return null;
28001
- }
28002
-
28003
- // The point where segment @p-@q, which starts inside the circle and ends
28004
- // outside it, crosses it.
28005
- function getCircleExit(c, r, p, q) {
28006
- var dx = q[0] - p[0];
28007
- var dy = q[1] - p[1];
28008
- var ex = p[0] - c[0];
28009
- var ey = p[1] - c[1];
28010
- var a = dx * dx + dy * dy;
28011
- var b = 2 * (dx * ex + dy * ey);
28012
- var k = ex * ex + ey * ey - r * r;
28013
- var s = (-b + Math.sqrt(Math.max(0, b * b - 4 * a * k))) / (2 * a);
28014
- s = Math.max(0, Math.min(1, s));
28015
- return [p[0] + s * dx, p[1] + s * dy];
28332
+ var trimmed = trimPolyline$1(coords, r);
28333
+ return trimmed ? {kind: 'polyline', coords: trimmed} : null;
28016
28334
  }
28017
28335
 
28018
28336
  function trimBezier(c, r) {
28019
28337
  var steps = 64;
28020
28338
  var lo = 0, hi = -1, mid, i;
28021
28339
  for (i = 1; i <= steps; i++) {
28022
- if (distance(c[0], getBezierPoint(c, i / steps)) > r) {
28340
+ if (distance$1(c[0], getBezierPoint(c, i / steps)) > r) {
28023
28341
  hi = i / steps;
28024
28342
  lo = (i - 1) / steps;
28025
28343
  break;
@@ -28028,7 +28346,7 @@
28028
28346
  if (hi < 0) return null;
28029
28347
  for (i = 0; i < 24; i++) {
28030
28348
  mid = (lo + hi) / 2;
28031
- if (distance(c[0], getBezierPoint(c, mid)) > r) hi = mid;
28349
+ if (distance$1(c[0], getBezierPoint(c, mid)) > r) hi = mid;
28032
28350
  else lo = mid;
28033
28351
  }
28034
28352
  return {kind: 'bezier', coords: splitBezier(c, hi)};
@@ -28054,7 +28372,7 @@
28054
28372
  function getStartDirection(path) {
28055
28373
  var c = path.coords;
28056
28374
  var next = c[1];
28057
- if (path.kind == 'bezier' && distance(c[0], c[1]) === 0) next = c[2];
28375
+ if (path.kind == 'bezier' && distance$1(c[0], c[1]) === 0) next = c[2];
28058
28376
  return getUnitVector(c[0], next);
28059
28377
  }
28060
28378
 
@@ -28086,8 +28404,8 @@
28086
28404
  ctrl = intersectRays(start, dir, end, getUnitVector(end, rest.coords[1]));
28087
28405
  // A curve that doubles back, or runs straight on: a control point on the
28088
28406
  // head's axis still takes the line out of the head straight.
28089
- if (!ctrl) ctrl = [start[0] + dir[0] * distance(start, end) / 2,
28090
- start[1] + dir[1] * distance(start, end) / 2];
28407
+ if (!ctrl) ctrl = [start[0] + dir[0] * distance$1(start, end) / 2,
28408
+ start[1] + dir[1] * distance$1(start, end) / 2];
28091
28409
  return {dir: dir, path: {kind: 'bezier', coords: [start, ctrl, end]}};
28092
28410
  }
28093
28411
 
@@ -28105,20 +28423,6 @@
28105
28423
  return s > 0 && u > 0 ? [p[0] + d[0] * s, p[1] + d[1] * s] : null;
28106
28424
  }
28107
28425
 
28108
- // [tip, wing, wing], for a head with sides @side long meeting at @angle degrees
28109
- function getArrowHead(tip, dir, side, angle) {
28110
- var len = getArrowHeadLength(side, angle);
28111
- var half = side * Math.sin(angle / 2 * Math.PI / 180);
28112
- var bx = tip[0] + dir[0] * len;
28113
- var by = tip[1] + dir[1] * len;
28114
- return [tip, [bx - dir[1] * half, by + dir[0] * half], [bx + dir[1] * half, by - dir[0] * half]];
28115
- }
28116
-
28117
- // How far along the line a head with sides @side long reaches
28118
- function getArrowHeadLength(side, angle) {
28119
- return side * Math.cos(angle / 2 * Math.PI / 180);
28120
- }
28121
-
28122
28426
  // Utilities
28123
28427
 
28124
28428
  function padBox$1(box, pad) {
@@ -28135,19 +28439,6 @@
28135
28439
  return p[0] > box.xmin && p[0] < box.xmax && p[1] > box.ymin && p[1] < box.ymax;
28136
28440
  }
28137
28441
 
28138
- function getUnitVector(p, q) {
28139
- var dx = q[0] - p[0];
28140
- var dy = q[1] - p[1];
28141
- var len = Math.sqrt(dx * dx + dy * dy);
28142
- return len > 0 ? [dx / len, dy / len] : null;
28143
- }
28144
-
28145
- function distance(p, q) {
28146
- var dx = q[0] - p[0];
28147
- var dy = q[1] - p[1];
28148
- return Math.sqrt(dx * dx + dy * dy);
28149
- }
28150
-
28151
28442
  function lerp(p, q, t) {
28152
28443
  return [p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t];
28153
28444
  }
@@ -28157,8 +28448,8 @@
28157
28448
  }
28158
28449
 
28159
28450
  function getLineWidth(rec) {
28160
- var w = getNumber(rec['callout-width'], DEFAULT_LINE_WIDTH);
28161
- return w > 0 ? w : DEFAULT_LINE_WIDTH;
28451
+ var w = getNumber(rec['callout-width'], DEFAULT_LINE_WIDTH$1);
28452
+ return w > 0 ? w : DEFAULT_LINE_WIDTH$1;
28162
28453
  }
28163
28454
 
28164
28455
  function getFontSizeInPx$1(rec) {
@@ -28211,6 +28502,9 @@
28211
28502
  return str;
28212
28503
  }
28213
28504
 
28505
+ // A ring icon's stroke, unless the record sets one; 1px read as too faint.
28506
+ var RING_STROKE_WIDTH = 1.5;
28507
+
28214
28508
  var symbolRenderers = {
28215
28509
  line: line,
28216
28510
  polygon: polygon,
@@ -28283,6 +28577,7 @@
28283
28577
  if (type == 'square') return square(getIconStyleData(d, r), 0, 0);
28284
28578
  if (type == 'ring') return ring(getIconStyleData(d, r), 0, 0);
28285
28579
  if (type == 'star') return star(getIconStyleData(d, r));
28580
+ if (type == 'nyt-star') return nytStar(getIconStyleData(d, r));
28286
28581
  return empty();
28287
28582
  }
28288
28583
 
@@ -28292,8 +28587,11 @@
28292
28587
  return d.r > 0 ? d.r : 5;
28293
28588
  }
28294
28589
  size = d['icon-size'];
28295
- if (type == 'circle' || type == 'ring') {
28590
+ if (type == 'circle') {
28296
28591
  size -= 1;
28592
+ } else if (type == 'ring') {
28593
+ // the stroke's outer half is inside icon-size
28594
+ size -= RING_STROKE_WIDTH;
28297
28595
  } else if (type == 'star') {
28298
28596
  size += 1;
28299
28597
  }
@@ -28400,7 +28698,7 @@
28400
28698
  o.properties.fill = 'none';
28401
28699
  o.properties.stroke = d.fill;
28402
28700
  if (!o.properties['stroke-width']) {
28403
- o.properties['stroke-width'] = 1;
28701
+ o.properties['stroke-width'] = RING_STROKE_WIDTH;
28404
28702
  }
28405
28703
  return o;
28406
28704
  }
@@ -28412,6 +28710,43 @@
28412
28710
  return o;
28413
28711
  }
28414
28712
 
28713
+ // A white nine-pointed star with a heavy outline in the icon's colour, drawn
28714
+ // from the source artwork's 40-unit box. The box includes the outline and its
28715
+ // mitred points, so icon-size is the width of the whole symbol.
28716
+ var NYT_STAR_BOX = 40;
28717
+ var NYT_STAR_STROKE = 5;
28718
+ var NYT_STAR_POINTS = [19.9843, 5.7196, 23.1494, 11.7629, 29.4528, 9.1512,
28719
+ 27.9924, 15.8152, 34.5, 17.8645, 29.0961, 22.0321, 32.7631, 27.7841,
28720
+ 25.9457, 27.504, 25.0572, 34.2675, 20.0149, 29.6714, 14.9847, 34.2804,
28721
+ 14.0769, 27.5192, 7.2619, 27.8167, 10.9166, 22.0553, 5.5, 17.9016,
28722
+ 12.0024, 15.8357, 10.527, 9.1754, 16.835, 11.771];
28723
+
28724
+ function nytStar(d) {
28725
+ var k = d.r * 2 / NYT_STAR_BOX;
28726
+ var o = importPolygon([getNytStarCoords(k)]);
28727
+ applyStyleAttributes(o, 'point', d, nonCirclePointFilter);
28728
+ o.properties.stroke = o.properties.fill;
28729
+ o.properties.fill = '#fff';
28730
+ o.properties['stroke-width'] = roundToHundredths(NYT_STAR_STROKE * k);
28731
+ o.properties['stroke-miterlimit'] = 10;
28732
+ return o;
28733
+ }
28734
+
28735
+ function getNytStarCoords(k) {
28736
+ var c = NYT_STAR_BOX / 2;
28737
+ var coords = [];
28738
+ for (var i=0; i<NYT_STAR_POINTS.length; i+=2) {
28739
+ coords.push([roundToHundredths((NYT_STAR_POINTS[i] - c) * k),
28740
+ roundToHundredths((NYT_STAR_POINTS[i + 1] - c) * k)]);
28741
+ }
28742
+ coords.push(coords[0].concat());
28743
+ return coords;
28744
+ }
28745
+
28746
+ function roundToHundredths(n) {
28747
+ return Math.round(n * 100) / 100;
28748
+ }
28749
+
28415
28750
  function nonCirclePointFilter(k) {
28416
28751
  return k != 'r';
28417
28752
  }
@@ -28491,6 +28826,7 @@
28491
28826
 
28492
28827
  var SvgSymbols = /*#__PURE__*/Object.freeze({
28493
28828
  __proto__: null,
28829
+ RING_STROKE_WIDTH: RING_STROKE_WIDTH,
28494
28830
  featureHasIcon: featureHasIcon,
28495
28831
  getAnchorSymbolRadius: getAnchorSymbolRadius,
28496
28832
  getIconRadius: getIconRadius,
@@ -29627,16 +29963,14 @@
29627
29963
  // a <tspan> inside a <textPath> advances along the path instead of
29628
29964
  // stacking below it, so the lines are joined rather than dropping
29629
29965
  // everything after the first one
29630
- text = text.split(labelNewlineRxp).join(' ');
29631
29966
  addToReport(report, 'joined', id);
29632
29967
  }
29633
- textPath = {
29968
+ textPath = applyRunsToSvgObject({
29634
29969
  tag: 'textPath',
29635
- value: text,
29636
29970
  properties: {
29637
29971
  startOffset: rec['label-start-offset'] || getDefaultStartOffset(rec)
29638
29972
  }
29639
- };
29973
+ }, joinLineRuns(splitLabelLineRuns(text)));
29640
29974
  // the caller replaces this with a reference to a <defs> entry
29641
29975
  textPath.properties[LABEL_PATH_PROPERTY] = d;
29642
29976
  if (rec['label-side']) {
@@ -29700,6 +30034,28 @@
29700
30034
  return '50%';
29701
30035
  }
29702
30036
 
30037
+ // A path label's lines as one line of runs, with a space for each break. The
30038
+ // space takes the weight of the text before it, and runs of one weight that
30039
+ // the join brings together are merged, so that text with no bold in it is a
30040
+ // single run.
30041
+ function joinLineRuns(lines) {
30042
+ var out = [];
30043
+ lines.forEach(function(runs, i) {
30044
+ if (i > 0) appendRun(out, {text: ' ', bold: out.length > 0 && out[out.length - 1].bold});
30045
+ runs.forEach(function(run) { appendRun(out, run); });
30046
+ });
30047
+ return out;
30048
+ }
30049
+
30050
+ function appendRun(runs, run) {
30051
+ var last = runs[runs.length - 1];
30052
+ if (last && last.bold == run.bold) {
30053
+ last.text += run.text;
30054
+ } else {
30055
+ runs.push({text: run.text, bold: run.bold});
30056
+ }
30057
+ }
30058
+
29703
30059
  function addToReport(report, key, id) {
29704
30060
  if (report && report[key]) report[key].push(id);
29705
30061
  }
@@ -29913,6 +30269,198 @@
29913
30269
  }
29914
30270
  */
29915
30271
 
30272
+ // Markers at the ends of line features: line-start= and line-end= take
30273
+ // arrow (a filled triangle), open-arrow (a stroked chevron), dot (a filled
30274
+ // circle centred on the end) or none, and line-end-size= is the length of a
30275
+ // head's sides in px, for both ends, or a dot's diameter. A dot with no size
30276
+ // of its own is DOT_SIZE_RATIO of an arrowhead's default size.
30277
+ //
30278
+ // The heads are drawn as shapes rather than as SVG <marker>s, so that the
30279
+ // canvas in the GUI and the exported SVG draw the same thing, and so that a
30280
+ // solid head can have the line stop inside it, where its cap is hidden. Every
30281
+ // part of a multi-part line gets its own heads: part order is not something
30282
+ // the commands that make multi-part lines preserve.
30283
+ //
30284
+ // Coordinates are px, y down: the SVG export's space, or the canvas's.
30285
+
30286
+ var DEFAULT_LINE_WIDTH = 1;
30287
+ // 6px across at the default size for a 1px line
30288
+ var DOT_SIZE_RATIO = 0.6;
30289
+
30290
+ // 'arrow', 'open-arrow', 'dot' or 'none'
30291
+ function getLineEndType(rec, field) {
30292
+ return rec && rec[field] && parseLineEnd(rec[field]) || 'none';
30293
+ }
30294
+
30295
+ function lineHasArrows(rec) {
30296
+ return getLineEndType(rec, 'line-start') != 'none' ||
30297
+ getLineEndType(rec, 'line-end') != 'none';
30298
+ }
30299
+
30300
+ // What getLineArrowShape() needs from a record, at scale @scale (the factor
30301
+ // the canvas multiplies line widths by; 1 for SVG).
30302
+ function getLineArrowOpts(rec, scale) {
30303
+ return makeLineArrowOpts(rec['line-start'], rec['line-end'], rec['line-end-size'],
30304
+ rec['stroke-width'], scale);
30305
+ }
30306
+
30307
+ // The same, from the values themselves, for the canvas, which has them in a
30308
+ // style object rather than a record.
30309
+ function makeLineArrowOpts(start, end, size, strokeWidth, scale) {
30310
+ var k = scale > 0 ? scale : 1;
30311
+ var w = isSvgNumber(strokeWidth) && Number(strokeWidth) >= 0 ?
30312
+ Number(strokeWidth) : DEFAULT_LINE_WIDTH;
30313
+ var hasSize = isSvgNumber(size) && Number(size) > 0;
30314
+ var side = hasSize ? Number(size) : getDefaultArrowSize(w);
30315
+ return {
30316
+ start: start && parseLineEnd(start) || 'none',
30317
+ end: end && parseLineEnd(end) || 'none',
30318
+ size: side * k,
30319
+ dotSize: (hasSize ? side : side * DOT_SIZE_RATIO) * k,
30320
+ width: w * k
30321
+ };
30322
+ }
30323
+
30324
+ // Field names, for a layer that may have them
30325
+ var lineArrowFields = ['line-start', 'line-end', 'line-end-size'];
30326
+
30327
+ // One part of a line, with its heads: {coords, heads}, where coords is the
30328
+ // line to stroke -- cut back into a solid head, so that its cap does not show
30329
+ // past the tip -- and heads is a list of {type, points}: a solid head's
30330
+ // triangle, tip first, or an open head's chevron, wing to tip to wing. A dot
30331
+ // is {type, center, radius}.
30332
+ //
30333
+ // A head points along the chord from its tip to where the line first reaches
30334
+ // the head's length from it, rather than along the last segment, which on
30335
+ // detailed or noisy data can be a jog of a fraction of a pixel pointing
30336
+ // anywhere. The line is then straightened to meet the head along its axis. An
30337
+ // end whose part is too short to hold its head gets none.
30338
+ function getLineArrowShape(coords, opts) {
30339
+ var out = {coords: coords, heads: []};
30340
+ var end;
30341
+ if (opts.start != 'none') {
30342
+ addHead(out, opts.start, opts);
30343
+ }
30344
+ if (opts.end != 'none') {
30345
+ end = {coords: out.coords.slice().reverse(), heads: out.heads};
30346
+ addHead(end, opts.end, opts);
30347
+ out.coords = end.coords.reverse();
30348
+ }
30349
+ return out;
30350
+ }
30351
+
30352
+ // Puts a head of @type at the first point of @shape.coords
30353
+ function addHead(shape, type, opts) {
30354
+ var coords = shape.coords;
30355
+ var width = opts.width > 0 ? opts.width : DEFAULT_LINE_WIDTH;
30356
+ var tip, side, angle, len, rest, dir, head;
30357
+ if (!coords || coords.length < 2) return;
30358
+ if (type == 'dot') {
30359
+ addDot(shape, (opts.dotSize || opts.size * DOT_SIZE_RATIO) / 2);
30360
+ return;
30361
+ }
30362
+ if (type == 'open-arrow') {
30363
+ // Stroked with a round join, which reaches half a line width past the
30364
+ // chevron's point: the point is set back by that much, so that the arrow
30365
+ // ends where a solid one would, and the arms lose half a line width each
30366
+ // to the join and the cap.
30367
+ coords = trimPolyline$1(coords, width / 2);
30368
+ if (!coords) return;
30369
+ side = Math.max(opts.size - width, opts.size / 2);
30370
+ angle = OPEN_ARROW_ANGLE;
30371
+ } else {
30372
+ side = opts.size;
30373
+ angle = ARROW_ANGLE;
30374
+ }
30375
+ tip = coords[0];
30376
+ len = getArrowHeadLength(side, angle);
30377
+ rest = trimPolyline$1(coords, len);
30378
+ dir = rest ? getUnitVector(tip, rest[0]) : null;
30379
+ if (!dir) return;
30380
+ head = getArrowHead(tip, dir, side, angle);
30381
+ if (type == 'open-arrow') {
30382
+ shape.heads.push({type: type, points: [head[1], head[0], head[2]]});
30383
+ shape.coords = [tip].concat(rest);
30384
+ } else {
30385
+ shape.heads.push({type: type, points: head});
30386
+ shape.coords = [[tip[0] + dir[0] * len * ARROW_LINE_OVERLAP,
30387
+ tip[1] + dir[1] * len * ARROW_LINE_OVERLAP]].concat(rest);
30388
+ }
30389
+ }
30390
+
30391
+ // The line stops halfway to the dot's edge, which hides its cap in the dot
30392
+ // (for lines narrower than the dot's radius) while overlapping it no more than
30393
+ // a solid head does. A part too short to be cut back keeps its dot.
30394
+ function addDot(shape, radius) {
30395
+ var center = shape.coords[0];
30396
+ var rest = trimPolyline$1(shape.coords, radius / 2);
30397
+ shape.heads.push({type: 'dot', center: center, radius: radius});
30398
+ if (rest) shape.coords = rest;
30399
+ }
30400
+
30401
+ // A line feature with arrowheads, as SVG: a group holding the line and its
30402
+ // heads, for the feature's style attributes to go on. @parts is a list of
30403
+ // polylines (a LineString's coordinates are one).
30404
+ function renderArrowLine(parts, rec) {
30405
+ var opts = getLineArrowOpts(rec, 1);
30406
+ var color = rec.stroke || 'black';
30407
+ var lines = [];
30408
+ var children = [];
30409
+ parts.forEach(function(part) {
30410
+ var shape = getLineArrowShape(part, opts);
30411
+ lines.push(shape.coords.map(roundPoint));
30412
+ shape.heads.forEach(function(head) {
30413
+ children.push(renderHead(head, color, rec));
30414
+ });
30415
+ });
30416
+ children.unshift(importMultiLineString(lines));
30417
+ return {tag: 'g', properties: {}, children: children};
30418
+ }
30419
+
30420
+ function renderHead(head, color, rec) {
30421
+ var points, center;
30422
+ if (head.type == 'dot') {
30423
+ center = roundPoint(head.center);
30424
+ return {tag: 'circle', properties: addFillOpacity({cx: center[0], cy: center[1],
30425
+ r: Math.round(head.radius * 100) / 100, fill: color, stroke: 'none'}, rec)};
30426
+ }
30427
+ points = head.points.map(roundPoint);
30428
+ var d = 'M ' + points.map(function(p) { return p[0] + ' ' + p[1]; }).join(' L ');
30429
+ var props;
30430
+ if (head.type == 'arrow') {
30431
+ props = addFillOpacity({d: d + ' Z', fill: color, stroke: 'none'}, rec);
30432
+ } else {
30433
+ // The line's own dashes would break the chevron up, and its cap may be
30434
+ // square or butt; the chevron is always drawn whole and rounded.
30435
+ props = {d: d, fill: 'none', 'stroke-dasharray': 'none',
30436
+ 'stroke-linecap': 'round', 'stroke-linejoin': 'round'};
30437
+ }
30438
+ return {tag: 'path', properties: props};
30439
+ }
30440
+
30441
+ // A filled marker is as transparent as the line it is drawn in
30442
+ function addFillOpacity(props, rec) {
30443
+ if (isSvgNumber(rec['stroke-opacity'])) {
30444
+ props['fill-opacity'] = Number(rec['stroke-opacity']);
30445
+ }
30446
+ return props;
30447
+ }
30448
+
30449
+ function roundPoint(p) {
30450
+ return [Math.round(p[0] * 100) / 100, Math.round(p[1] * 100) / 100];
30451
+ }
30452
+
30453
+ var SvgLineArrows = /*#__PURE__*/Object.freeze({
30454
+ __proto__: null,
30455
+ getLineArrowOpts: getLineArrowOpts,
30456
+ getLineArrowShape: getLineArrowShape,
30457
+ getLineEndType: getLineEndType,
30458
+ lineArrowFields: lineArrowFields,
30459
+ lineHasArrows: lineHasArrows,
30460
+ makeLineArrowOpts: makeLineArrowOpts,
30461
+ renderArrowLine: renderArrowLine
30462
+ });
30463
+
29916
30464
  var geojsonImporters = {
29917
30465
  Point: importPoint,
29918
30466
  Polygon: importPolygon,
@@ -29935,6 +30483,9 @@
29935
30483
  // rather than several labels at several points
29936
30484
  svgObj = renderPathLabel(d, geom.coordinates,
29937
30485
  {report: opts.path_label_report, id: featureId});
30486
+ } else if (msType == 'polyline' && geom.coordinates && lineHasArrows(d)) {
30487
+ svgObj = renderArrowLine(geomType == 'LineString' ?
30488
+ [geom.coordinates] : geom.coordinates, d);
29938
30489
  } else if (geomType && geom.coordinates) {
29939
30490
  svgObj = geojsonImporters[geomType](geom.coordinates, d);
29940
30491
  }
@@ -40716,6 +41267,22 @@ ${svg}
40716
41267
  }
40717
41268
  }
40718
41269
 
41270
+ // -labels adds a label when given coordinates=, and styles the target's labels
41271
+ // otherwise. Options that belong to one of these are refused in the other,
41272
+ // rather than ignored.
41273
+ function validateLabelsOpts(cmd) {
41274
+ var o = cmd.options;
41275
+ if ('coordinates' in o) {
41276
+ if (o.where || o.ids) {
41277
+ stop$1('where= and ids= select labels to style, and can not be used with coordinates=');
41278
+ }
41279
+ } else if (o.properties) {
41280
+ stop$1('properties= requires coordinates=');
41281
+ } else if ('name' in o && !o.no_replace) {
41282
+ stop$1('name= requires coordinates= or +');
41283
+ }
41284
+ }
41285
+
40719
41286
  function validateExpressionOpt(cmd) {
40720
41287
  if (!cmd.options.expression) {
40721
41288
  error('Command requires a JavaScript expression');
@@ -41696,17 +42263,17 @@ ${svg}
41696
42263
  nameOpt2 = { // for -calc and -info
41697
42264
  describe: 'name the output layer'
41698
42265
  },
41699
- // label style properties accepted by -add-label, so that a label can be
41700
- // created and styled in one command. These are a subset of the
41701
- // properties -style accepts -- the ones that apply to text.
42266
+ // label style properties accepted by -labels and -add-label. -style
42267
+ // accepts these too, for backward compatibility, but leaves them out of
42268
+ // its help.
41702
42269
  labelStyleOpts = {
41703
- 'font-family': {describe: 'label font, e.g. Georgia'},
41704
- 'font-size': {describe: 'label font size, e.g. 14'},
42270
+ 'font-family': {describe: 'CSS font family, e.g. Georgia (default is sans-serif)'},
42271
+ 'font-size': {describe: 'font size (default is 12)'},
41705
42272
  'font-style': {describe: 'normal or italic'},
41706
42273
  'font-weight': {describe: 'normal, bold or a numeric weight'},
41707
42274
  'font-stretch': {describe: 'e.g. condensed'},
41708
42275
  'letter-spacing': {describe: 'extra space between letters'},
41709
- 'line-height': {describe: 'spacing between lines of a multi-line label; a bare number is a multiple of the font size'},
42276
+ 'line-height': {describe: 'spacing between lines of a multi-line label; a bare number is a multiple of the font size (default is 1.1)'},
41710
42277
  'text-anchor': {describe: 'start, middle or end'},
41711
42278
  'label-align': {describe: 'how the lines of a multi-line label align: left, center or right'},
41712
42279
  'dominant-baseline': {describe: 'vertical alignment, e.g. central'},
@@ -41717,23 +42284,30 @@ ${svg}
41717
42284
  dy: {describe: 'vertical offset from the anchor'},
41718
42285
  fill: {describe: 'text color'},
41719
42286
  opacity: {describe: 'text opacity'},
41720
- 'halo-width': {describe: 'width of a halo around the text in px'},
42287
+ 'halo-width': {describe: 'width of a halo around the text in px (default is 0, no halo)'},
41721
42288
  'halo-color': {describe: 'halo color (default is white)'},
41722
42289
  'halo-opacity': {describe: 'halo opacity, 0-1'},
41723
- 'label-width': {describe: 'width of a fixed-width text block in px'},
42290
+ 'label-width': {describe: 'width of a fixed-width text block in px (lines are wrapped in the web UI)'},
41724
42291
  callout: {describe: 'line from the anchor to the text: line, elbow or curve'},
41725
42292
  'callout-end': {describe: 'arrowhead at the anchor end: arrow, open-arrow or none'},
41726
- 'callout-end-size': {describe: 'length of an arrowhead\'s sides in px'},
42293
+ 'callout-end-size': {describe: 'length of an arrowhead\'s sides in px (default is 10)'},
41727
42294
  'callout-via': {describe: 'x,y of an elbow\'s corner or a point on a curve, in px'},
41728
42295
  'callout-attach': {describe: 'where the callout meets the text, as x,y fractions'},
41729
42296
  'callout-gap': {describe: 'px between the callout and the anchor'},
41730
- 'callout-padding': {describe: 'px between the callout and the text'},
42297
+ 'callout-padding': {describe: 'px between the callout and the text (default is 3)'},
41731
42298
  'callout-color': {describe: 'callout color (defaults to the text color)'},
41732
- 'callout-width': {describe: 'callout line width in px'},
42299
+ 'callout-width': {describe: 'callout line width in px (default is 1)'},
41733
42300
  'callout-opacity': {describe: 'callout opacity, 0-1'},
41734
42301
  css: {describe: 'inline css style'},
41735
42302
  class: {describe: 'name of CSS class or classes (space-separated)'}
41736
42303
  },
42304
+ // a label can carry a symbol at its anchor
42305
+ labelIconOpts = {
42306
+ icon: {describe: 'symbol drawn at the label anchor: circle, square, ring, star'},
42307
+ 'icon-size': {describe: 'size of the anchor symbol in px'},
42308
+ 'icon-color': {describe: 'color of the anchor symbol (defaults to the text color)'},
42309
+ 'icon-opacity': {describe: 'opacity of the anchor symbol, 0-1'}
42310
+ },
41737
42311
  noReplaceOpt2 = { // for -calc and -info
41738
42312
  alias: '+',
41739
42313
  type: 'flag',
@@ -41813,6 +42387,18 @@ ${svg}
41813
42387
  type: 'distance'
41814
42388
  };
41815
42389
 
42390
+ // The label options -style used to document, before -labels took them over.
42391
+ // fill, opacity, css and class are left out because -style documents them
42392
+ // for every kind of feature.
42393
+ function getHiddenLabelStyleOpts() {
42394
+ var shared = ['fill', 'opacity', 'css', 'class'];
42395
+ var opts = {'label-text': {}};
42396
+ Object.keys(labelStyleOpts).forEach(function(name) {
42397
+ if (shared.indexOf(name) == -1) opts[name] = {};
42398
+ });
42399
+ return opts;
42400
+ }
42401
+
41816
42402
  var parser = new CommandParser();
41817
42403
  parser.usage('Usage: mapshaper -<command> [options] ...');
41818
42404
 
@@ -43181,6 +43767,32 @@ ${svg}
43181
43767
  .option('encoding', encodingOpt)
43182
43768
  .option('target', targetOpt);
43183
43769
 
43770
+ parser.command('labels')
43771
+ .describe('style labels, convert points to labels or add a label')
43772
+ .validate(validateLabelsOpts)
43773
+ .option('text', {
43774
+ describe: 'label text (a field, JS expression or literal value)'
43775
+ })
43776
+ .option('label-text', {
43777
+ alias_to: 'text'
43778
+ })
43779
+ .option('coordinates', {
43780
+ describe: 'add a label at x,y, or a path label along x,y,x,y,...'
43781
+ })
43782
+ .option('where', whereOpt)
43783
+ .option('ids', {
43784
+ describe: 'comma-sep. list of ids of the labels to style',
43785
+ type: 'numbers'
43786
+ })
43787
+ .options(labelStyleOpts)
43788
+ .options(labelIconOpts)
43789
+ .option('properties', {
43790
+ describe: 'with coordinates=, other attributes as a JSON object'
43791
+ })
43792
+ .option('name', nameOpt)
43793
+ .option('no-replace', noReplaceOpt)
43794
+ .option('target', targetOpt);
43795
+
43184
43796
  parser.command('lines')
43185
43797
  .describe('convert a polygon or point layer to a polyline layer')
43186
43798
  .option('fields', {
@@ -43728,6 +44340,15 @@ ${svg}
43728
44340
  .option('stroke-opacity', {
43729
44341
  describe: 'stroke opacity'
43730
44342
  })
44343
+ .option('line-start', {
44344
+ describe: 'marker at the start of each line: arrow, open-arrow, dot or none'
44345
+ })
44346
+ .option('line-end', {
44347
+ describe: 'marker at the end of each line: arrow, open-arrow, dot or none'
44348
+ })
44349
+ .option('line-end-size', {
44350
+ describe: 'length of an arrowhead\'s sides in px (default grows with stroke-width)'
44351
+ })
43731
44352
  .option('opacity', {
43732
44353
  describe: 'opacity; example: 0.5'
43733
44354
  })
@@ -43744,99 +44365,11 @@ ${svg}
43744
44365
  describe: 'point icon color (defaults to fill color, then black)'
43745
44366
  })
43746
44367
  .option('icon-opacity', {
43747
- describe: 'point icon opacity, 0-1 (defaults to the label\'s opacity)'
43748
- })
43749
- .option('label-text', {
43750
- describe: 'label text (set this to export points as labels)'
43751
- })
43752
- .option('label-pos', {
43753
- describe: 'label position; one of: n, s, e, w, ne, se, nw, sw, c'
43754
- })
43755
- .option('text-anchor', {
43756
- describe: 'label alignment; one of: start, end, middle (default)'
43757
- })
43758
- .option('label-align', {
43759
- describe: 'alignment of the lines of multi-line labels; left, center or right'
43760
- })
43761
- .option('halo-width', {
43762
- describe: 'width of a halo around label text in px (default is 0, no halo)'
43763
- })
43764
- .option('halo-color', {
43765
- describe: 'color of label halos (default is white)'
43766
- })
43767
- .option('halo-opacity', {
43768
- describe: 'opacity of label halos, 0-1'
43769
- })
43770
- .option('dx', {
43771
- describe: 'x offset of labels (default is 0)'
43772
- })
43773
- .option('dy', {
43774
- describe: 'y offset of labels (default is 0/baseline-aligned)'
43775
- })
43776
- .option('font-size', {
43777
- describe: 'size of label text (default is 12)'
43778
- })
43779
- .option('font-family', {
43780
- describe: 'CSS font family of labels (default is sans-serif)'
44368
+ describe: 'point icon opacity, 0-1'
43781
44369
  })
43782
- .option('font-weight', {
43783
- describe: 'CSS font weight property of labels (e.g. bold, 700)'
43784
- })
43785
- .option('font-style', {
43786
- describe: 'CSS font style property of labels (e.g. italic)'
43787
- })
43788
- .option('font-stretch', {
43789
- describe: 'CSS font stretch property of labels (e.g. condensed)'
43790
- })
43791
- .option('letter-spacing', {
43792
- describe: 'CSS letter-spacing property of labels'
43793
- })
43794
- .option('line-height', {
43795
- describe: 'line spacing of multi-line labels; a bare number is a multiple of the font size, as in CSS (default is 1.1)'
43796
- })
43797
- .option('dominant-baseline', {
43798
- describe: 'vertical alignment of labels (e.g. central)'
43799
- })
43800
- .option('label-side', {
43801
- describe: 'which side of its path a label sits on: left or right'
43802
- })
43803
- .option('label-start-offset', {
43804
- describe: 'where label text starts along its path, e.g. 50%'
43805
- })
43806
- .option('label-width', {
43807
- describe: 'width of a fixed-width text block in px (lines are wrapped in the web UI)'
43808
- })
43809
- .option('callout', {
43810
- describe: 'line from a label\'s anchor to its text: line, elbow or curve'
43811
- })
43812
- .option('callout-end', {
43813
- describe: 'arrowhead at the anchor end of a callout: arrow (solid), open-arrow or none'
43814
- })
43815
- .option('callout-end-size', {
43816
- describe: 'length of an arrowhead\'s sides in px (default 10, more for a thicker line)'
43817
- })
43818
- .option('callout-via', {
43819
- describe: 'x,y of an elbow\'s corner or a point on a curve, in px from the anchor'
43820
- })
43821
- .option('callout-attach', {
43822
- describe: 'where a callout meets its text, as x,y fractions of the text box'
43823
- })
43824
- .option('callout-gap', {
43825
- describe: 'px between a callout and its anchor (default: a line meets the icon, an arrow clears it)'
43826
- })
43827
- .option('callout-padding', {
43828
- describe: 'px between a callout and its text (default is 3)'
43829
- })
43830
- .option('callout-color', {
43831
- describe: 'callout color (defaults to the text color)'
43832
- })
43833
- .option('callout-width', {
43834
- describe: 'callout line width in px (default is 1)'
43835
- })
43836
- .option('callout-opacity', {
43837
- describe: 'callout opacity, 0-1'
43838
- })
43839
- .option('target', targetOpt);
44370
+ // deprecated in favor of -labels; accepted, but left out of the help
44371
+ .options(getHiddenLabelStyleOpts())
44372
+ .option('target', targetOpt);
43840
44373
 
43841
44374
  parser.command('repel')
43842
44375
  .describe('move overlapping point symbols apart')
@@ -44082,23 +44615,10 @@ ${svg}
44082
44615
  .option('text', {
44083
44616
  describe: 'label text'
44084
44617
  })
44085
- // label style properties, so that creating and styling a label is one
44086
- // command; anything not listed here can be set with a following -style
44618
+ // superseded by -labels coordinates=; kept so that older session
44619
+ // histories still run
44087
44620
  .options(labelStyleOpts)
44088
- // a label can carry a symbol at its anchor, so these come along too even
44089
- // though they are not text properties
44090
- .option('icon', {
44091
- describe: 'symbol drawn at the label anchor: circle, square, ring, star'
44092
- })
44093
- .option('icon-size', {
44094
- describe: 'size of the anchor symbol in px'
44095
- })
44096
- .option('icon-color', {
44097
- describe: 'color of the anchor symbol (defaults to the text color)'
44098
- })
44099
- .option('icon-opacity', {
44100
- describe: 'opacity of the anchor symbol, 0-1'
44101
- })
44621
+ .options(labelIconOpts)
44102
44622
  .option('properties', {
44103
44623
  describe: 'other attributes, as a JSON object'
44104
44624
  })
@@ -49441,9 +49961,28 @@ ${svg}
49441
49961
  props[key] = val;
49442
49962
  }
49443
49963
  }
49964
+ importPatternFill(props);
49444
49965
  return props;
49445
49966
  }
49446
49967
 
49968
+ // A fill of url(#id), with or without a fallback colour after it, is a
49969
+ // reference to a paint server in <defs>. The ones mapshaper exports are
49970
+ // pattern fills whose id encodes the fill-pattern code, and the fallback is
49971
+ // the feature's own fill. A reference that is not one of those is dropped:
49972
+ // nothing can draw a url() stored as a colour. Its fallback, if any, is kept.
49973
+ function importPatternFill(props) {
49974
+ var match = /^url\(\s*['"]?#([^'")]+)['"]?\s*\)\s*(.*)$/.exec(String(props.fill || '').trim());
49975
+ var code;
49976
+ if (!match) return;
49977
+ code = decodePatternId(match[1]);
49978
+ if (code) props['fill-pattern'] = code;
49979
+ if (match[2]) {
49980
+ props.fill = match[2];
49981
+ } else {
49982
+ delete props.fill;
49983
+ }
49984
+ }
49985
+
49447
49986
  function getNodeStyles(node) {
49448
49987
  var styles = {};
49449
49988
  var attrs = node.attributes || [];
@@ -51395,6 +51934,626 @@ ${svg}
51395
51934
  return d;
51396
51935
  }
51397
51936
 
51937
+ // Insert cutting points in arcs, where bbox intersects other shapes
51938
+ // Return a polygon layer containing the bounding box vectors, divided at cutting points.
51939
+ function divideDatasetByBBox(dataset, bbox) {
51940
+ var arcs = dataset.arcs;
51941
+ var data = findBBoxCutPoints(arcs, bbox);
51942
+ var map = insertCutPoints(data.cutPoints, arcs);
51943
+ arcs.dedupCoords();
51944
+ remapDividedArcs(dataset, map);
51945
+ // merge bbox dataset with target dataset,
51946
+ // so arcs are shared between target layers and bbox layer
51947
+ var clipDataset = bboxPointsToClipDataset(data.bboxPoints);
51948
+ var mergedDataset = mergeDatasets([dataset, clipDataset]);
51949
+ // TODO: detect if we need to rebuild topology (unlikely), like with the full clip command
51950
+ // buildTopology(mergedDataset);
51951
+ var clipLyr = mergedDataset.layers.pop();
51952
+ dataset.arcs = mergedDataset.arcs;
51953
+ dataset.layers = mergedDataset.layers;
51954
+ return clipLyr;
51955
+ }
51956
+
51957
+ function bboxPointsToClipDataset(arr) {
51958
+ var arcs = [];
51959
+ var shape = [];
51960
+ var layer = {geometry_type: 'polygon', shapes: [[shape]]};
51961
+ var p1, p2;
51962
+ for (var i=0, n=arr.length - 1; i<n; i++) {
51963
+ p1 = arr[i];
51964
+ p2 = arr[i+1];
51965
+ arcs.push([[p1.x, p1.y], [p2.x, p2.y]]);
51966
+ shape.push(i);
51967
+ }
51968
+ return {
51969
+ arcs: new ArcCollection(arcs),
51970
+ layers: [layer]
51971
+ };
51972
+ }
51973
+
51974
+ function findBBoxCutPoints(arcs, bbox) {
51975
+ var left = bbox[0],
51976
+ bottom = bbox[1],
51977
+ right = bbox[2],
51978
+ top = bbox[3];
51979
+
51980
+ // arrays of intersection points along each bbox edge
51981
+ var tt = [],
51982
+ rr = [],
51983
+ bb = [],
51984
+ ll = [];
51985
+
51986
+ arcs.forEachSegment(function(i, j, xx, yy) {
51987
+ var ax = xx[i],
51988
+ ay = yy[i],
51989
+ bx = xx[j],
51990
+ by = yy[j];
51991
+ var hit;
51992
+ if (segmentOutsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
51993
+ if (segmentInsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
51994
+
51995
+ hit = geom.segmentIntersection(left, top, right, top, ax, ay, bx, by);
51996
+ if (hit) addHit(tt, hit, i, j, xx, yy);
51997
+
51998
+ hit = geom.segmentIntersection(left, bottom, right, bottom, ax, ay, bx, by);
51999
+ if (hit) addHit(bb, hit, i, j, xx, yy);
52000
+
52001
+ hit = geom.segmentIntersection(left, bottom, left, top, ax, ay, bx, by);
52002
+ if (hit) addHit(ll, hit, i, j, xx, yy);
52003
+
52004
+ hit = geom.segmentIntersection(right, bottom, right, top, ax, ay, bx, by);
52005
+ if (hit) addHit(rr, hit, i, j, xx, yy);
52006
+ });
52007
+
52008
+ return {
52009
+ cutPoints: ll.concat(bb, rr, tt),
52010
+ bboxPoints: getDividedBBoxPoints(bbox, ll, tt, rr, bb)
52011
+ };
52012
+
52013
+ function addHit(arr, hit, i, j, xx, yy) {
52014
+ if (!hit) return;
52015
+ arr.push(formatHit(hit[0], hit[1], i, j, xx, yy));
52016
+ if (hit.length == 4) {
52017
+ arr.push(formatHit(hit[2], hit[3], i, j, xx, yy));
52018
+ }
52019
+ }
52020
+
52021
+ function formatHit(x, y, i, j, xx, yy) {
52022
+ var ids = formatIntersectingSegment(x, y, i, j, xx, yy);
52023
+ return getCutPoint(x, y, ids[0], ids[1]);
52024
+ }
52025
+ }
52026
+
52027
+ function segmentOutsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
52028
+ return ax < xmin && bx < xmin || ax > xmax && bx > xmax ||
52029
+ ay < ymin && by < ymin || ay > ymax && by > ymax;
52030
+ }
52031
+
52032
+ function segmentInsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
52033
+ return ax > xmin && bx > xmin && ax < xmax && bx < xmax &&
52034
+ ay > ymin && by > ymin && ay < ymax && by < ymax;
52035
+ }
52036
+
52037
+ // Returns an array of points representing the vertices in
52038
+ // the bbox with cutting points inserted.
52039
+ function getDividedBBoxPoints(bbox, ll, tt, rr, bb) {
52040
+ var bl = {x: bbox[0], y: bbox[1]},
52041
+ tl = {x: bbox[0], y: bbox[3]},
52042
+ tr = {x: bbox[2], y: bbox[3]},
52043
+ br = {x: bbox[2], y: bbox[1]};
52044
+ ll = utils.sortOn(ll.concat([bl, tl]), 'y', true);
52045
+ tt = utils.sortOn(tt.concat([tl, tr]), 'x', true);
52046
+ rr = utils.sortOn(rr.concat([tr, br]), 'y', false);
52047
+ bb = utils.sortOn(bb.concat([br, bl]), 'x', false);
52048
+ return ll.concat(tt, rr, bb).reduce(function(memo, p2) {
52049
+ var p1 = memo.length > 0 ? memo[memo.length-1] : null;
52050
+ if (p1 === null || p1.x != p2.x || p1.y != p2.y) memo.push(p2);
52051
+ return memo;
52052
+ }, []);
52053
+ }
52054
+
52055
+ var Bbox2Clipping = /*#__PURE__*/Object.freeze({
52056
+ __proto__: null,
52057
+ divideDatasetByBBox: divideDatasetByBBox,
52058
+ segmentInsideBBox: segmentInsideBBox,
52059
+ segmentOutsideBBox: segmentOutsideBBox
52060
+ });
52061
+
52062
+ cmd.filterGeom = function(lyr, arcs, opts) {
52063
+ if (!layerHasGeometry$1(lyr)) {
52064
+ stop$1("Layer is missing geometry");
52065
+ }
52066
+ if (opts.bbox) {
52067
+ filterByBoundsIntersection(lyr, arcs, opts);
52068
+ }
52069
+ cmd.filterFeatures(lyr, arcs, {remove_empty: true, verbose: false});
52070
+ };
52071
+
52072
+ function filterByBoundsIntersection(lyr, arcs, opts) {
52073
+ var filter = getBoundsIntersectionFilter(opts.bbox, lyr, arcs);
52074
+ noteLayerWillChange(lyr, {operation: 'filter-geom', unit: 'shapes'});
52075
+ editShapes(lyr.shapes, filter);
52076
+ markLayerChanged(lyr, {operation: 'filter-geom', unit: 'shapes'});
52077
+ }
52078
+
52079
+ function getBoundsIntersectionFilter(bbox, lyr, arcs) {
52080
+ var bounds = new Bounds(bbox);
52081
+ var filter = lyr.geometry_type == 'point' ?
52082
+ getPointInBoundsTest(bounds) :
52083
+ getPathBoundsIntersectionTest(bounds, arcs);
52084
+ return filter;
52085
+ }
52086
+
52087
+ function getPointInBoundsTest(bounds) {
52088
+ return function(xy) {
52089
+ var contains = bounds.containsPoint(xy[0], xy[1]);
52090
+ return contains ? xy : null;
52091
+ };
52092
+ }
52093
+
52094
+ // V1 too-simple test: bounding-box intersection
52095
+ // internal.getPathBoundsIntersectionTest = function(bounds, arcs) {
52096
+ // return function(path) {
52097
+ // return bounds.intersects(arcs.getSimpleShapeBounds(path)) ? path : null;
52098
+ // };
52099
+ // };
52100
+
52101
+ function getPathBoundsIntersectionTest(bounds, arcs) {
52102
+ var bbox = bounds.toArray(),
52103
+ left = bbox[0],
52104
+ bottom = bbox[1],
52105
+ right = bbox[2],
52106
+ top = bbox[3];
52107
+
52108
+ return function(path) {
52109
+ // case: bounding boxes don't intersect -> the path doesn't intersect the box
52110
+ if (!bounds.intersects(arcs.getSimpleShapeBounds(path))) {
52111
+ return null;
52112
+ }
52113
+ var intersects = false;
52114
+ var ax, ay, bx, by;
52115
+ var iter = arcs.getShapeIter(path);
52116
+
52117
+ if (iter.hasNext()) {
52118
+ ax = iter.x;
52119
+ ay = iter.y;
52120
+ }
52121
+ while (iter.hasNext()) {
52122
+ bx = ax;
52123
+ by = ay;
52124
+ ax = iter.x;
52125
+ ay = iter.y;
52126
+ if (segmentOutsideBBox(ax, ay, bx, by, left, bottom, right, top)) continue;
52127
+ if (segmentInsideBBox(ax, ay, bx, by, left, bottom, right, top)) {
52128
+ intersects = true;
52129
+ break;
52130
+ }
52131
+ if (geom.segmentIntersection(left, top, right, top, ax, ay, bx, by) ||
52132
+ geom.segmentIntersection(left, bottom, right, bottom, ax, ay, bx, by) ||
52133
+ geom.segmentIntersection(left, bottom, left, top, ax, ay, bx, by) ||
52134
+ geom.segmentIntersection(right, bottom, right, top, ax, ay, bx, by)) {
52135
+ intersects = true;
52136
+ break;
52137
+ }
52138
+ }
52139
+
52140
+ // case: bbox is entirely inside this ring
52141
+ if (!intersects && geom.testPointInRing(left, bottom, path, arcs)) {
52142
+ intersects = true;
52143
+ }
52144
+ return intersects ? path : null;
52145
+ };
52146
+ }
52147
+
52148
+ // Return a function for testing if a shape (path or point) intersects a bounding box
52149
+ // TODO: move this function to a different file
52150
+ function getBBoxIntersectionTest(bbox, lyr, arcs) {
52151
+ var filter = getBoundsIntersectionFilter(bbox, lyr, arcs);
52152
+ return function(shapeId) {
52153
+ var shp = lyr.shapes[shapeId];
52154
+ if (!shp) return false;
52155
+ for (var i=0; i<shp.length; i++) {
52156
+ if (filter(shp[i])) return true;
52157
+ }
52158
+ return false;
52159
+ };
52160
+ }
52161
+
52162
+ // return array of shape ids
52163
+ function findShapesIntersectingBBox(bbox, lyr, arcs) {
52164
+ var test = getBBoxIntersectionTest(bbox, lyr, arcs);
52165
+ var ids = [];
52166
+ for (var i=0; i<lyr.shapes.length; i++) {
52167
+ if (test(i)) ids.push(i);
52168
+ }
52169
+ return ids;
52170
+ }
52171
+
52172
+ var FilterGeom = /*#__PURE__*/Object.freeze({
52173
+ __proto__: null,
52174
+ findShapesIntersectingBBox: findShapesIntersectingBBox,
52175
+ getBBoxIntersectionTest: getBBoxIntersectionTest
52176
+ });
52177
+
52178
+ cmd.filterFeatures = function(lyr, arcs, opts) {
52179
+ var records = lyr.data ? lyr.data.getRecords() : null,
52180
+ shapes = lyr.shapes || null,
52181
+ n = getFeatureCount(lyr),
52182
+ filteredShapes = shapes ? [] : null,
52183
+ filteredRecords = records ? [] : null,
52184
+ filteredLyr = getOutputLayer(lyr, opts),
52185
+ invert = !!opts.invert,
52186
+ filter;
52187
+
52188
+ if (opts.expression) {
52189
+ filter = compileFeatureExpression(opts.expression, lyr, arcs);
52190
+ }
52191
+
52192
+ if (opts.ids) {
52193
+ filter = combineFilters(filter, getIdFilter(opts.ids));
52194
+ }
52195
+
52196
+ if (opts.remove_empty) {
52197
+ filter = combineFilters(filter, getNullGeometryFilter(lyr, arcs));
52198
+ }
52199
+
52200
+ if (opts.bbox) {
52201
+ filter = combineFilters(filter, getBBoxIntersectionTest(opts.bbox, lyr, arcs));
52202
+ }
52203
+
52204
+ if (!filter) {
52205
+ stop$1("Missing a filter criterion");
52206
+ }
52207
+
52208
+ utils.repeat(n, function(shapeId) {
52209
+ var result = filter(shapeId);
52210
+ requireBooleanResult(result);
52211
+ if (invert) result = !result;
52212
+ if (result === true) {
52213
+ if (shapes) filteredShapes.push(shapes[shapeId] || null);
52214
+ if (records) filteredRecords.push(records[shapeId] || null);
52215
+ }
52216
+ });
52217
+
52218
+ if (filteredLyr == lyr) {
52219
+ noteLayerWillChange(lyr, {operation: 'filter'});
52220
+ }
52221
+ filteredLyr.shapes = filteredShapes;
52222
+ filteredLyr.data = filteredRecords ? new DataTable(filteredRecords) : null;
52223
+ if (filteredLyr == lyr) {
52224
+ markLayerChanged(lyr, {operation: 'filter'});
52225
+ }
52226
+ if (opts.no_replace) {
52227
+ // if adding a layer, don't share objects between source and filtered layer
52228
+ filteredLyr = copyLayer(filteredLyr);
52229
+ }
52230
+
52231
+ if (opts.verbose !== false && !opts.quiet) {
52232
+ message(utils.format('Retained %,d of %,d features', getFeatureCount(filteredLyr), n));
52233
+ }
52234
+
52235
+ return filteredLyr;
52236
+ };
52237
+
52238
+ // TODO: update filter command to use this function
52239
+ function filterLayerInPlace(lyr, filter, invert) {
52240
+ var records = lyr.data ? lyr.data.getRecords() : null,
52241
+ shapes = lyr.shapes || null,
52242
+ n = getFeatureCount(lyr),
52243
+ filteredShapes = shapes ? [] : null,
52244
+ filteredRecords = records ? [] : null;
52245
+ utils.repeat(n, function(shapeId) {
52246
+ var result = filter(shapeId);
52247
+ requireBooleanResult(result);
52248
+ if (result === true) {
52249
+ if (shapes) filteredShapes.push(shapes[shapeId] || null);
52250
+ if (records) filteredRecords.push(records[shapeId] || null);
52251
+ }
52252
+ });
52253
+ noteLayerWillChange(lyr, {operation: 'filterLayerInPlace'});
52254
+ lyr.shapes = filteredShapes;
52255
+ lyr.data = filteredRecords ? new DataTable(filteredRecords) : null;
52256
+ markLayerChanged(lyr, {operation: 'filterLayerInPlace'});
52257
+ }
52258
+
52259
+ function getIdFilter(ids) {
52260
+ var set = new Set(ids);
52261
+ return function(i) {
52262
+ return set.has(i);
52263
+ };
52264
+ }
52265
+
52266
+ function getNullGeometryFilter(lyr, arcs) {
52267
+ var shapes = lyr.shapes;
52268
+ if (lyr.geometry_type == 'polygon') {
52269
+ return getEmptyPolygonFilter(shapes, arcs);
52270
+ }
52271
+ return function(i) {return !!shapes[i];};
52272
+ }
52273
+
52274
+ function getEmptyPolygonFilter(shapes, arcs) {
52275
+ return function(i) {
52276
+ var shp = shapes[i];
52277
+ return !!shp && geom.getPlanarShapeArea(shapes[i], arcs) > 0;
52278
+ };
52279
+ }
52280
+
52281
+ function combineFilters(a, b) {
52282
+ return a && b ? function(id) {
52283
+ return a(id) && b(id);
52284
+ } : (a || b);
52285
+ }
52286
+
52287
+ // The icon names accepted by the icon= property of the -style command.
52288
+ // Kept in a module of its own so that both the renderer (svg-symbols.mjs) and
52289
+ // the -style command can use it without forming an import cycle.
52290
+
52291
+ var iconNames = ['circle', 'square', 'ring', 'star'];
52292
+
52293
+ // Icons that render everywhere but are not advertised: the GUI offers them only
52294
+ // to some users (see gui-nyt.mjs), and a map made with one has to draw the same
52295
+ // for anyone who opens it.
52296
+ var unlistedIconNames = ['nyt-star'];
52297
+
52298
+ function isSupportedIconName(name) {
52299
+ return iconNames.indexOf(name) > -1 || unlistedIconNames.indexOf(name) > -1;
52300
+ }
52301
+
52302
+ cmd.svgStyle = function(lyr, dataset, opts) {
52303
+ var filterFn, table, fields, hasNewFields, optFields, clearedByPosition, fieldsBefore;
52304
+
52305
+ function hadField(field) {
52306
+ return field in fieldsBefore;
52307
+ }
52308
+
52309
+ if (getFeatureCount(lyr) === 0) {
52310
+ return;
52311
+ }
52312
+ if (!lyr.data) {
52313
+ initDataTable(lyr);
52314
+ }
52315
+ if (opts.where) {
52316
+ filterFn = compileFeatureExpression(opts.where, lyr, dataset.arcs);
52317
+ }
52318
+ if (opts.ids) {
52319
+ filterFn = combineFilters(filterFn, getIdFilter(opts.ids));
52320
+ }
52321
+ if (opts.clear) {
52322
+ lyr.data.getFields().filter(isSupportedSvgStyleProperty).forEach(lyr.data.deleteField, lyr.data);
52323
+ }
52324
+ table = getLayerDataTable(lyr);
52325
+ optFields = getOptionFields(opts);
52326
+ fields = getStyleFields(optFields);
52327
+ // Which of dx/dy/text-anchor a label-pos in this command clears: not the ones
52328
+ // the same command also sets, so that `label-pos=n dx=3` keeps the nudge it
52329
+ // was given instead of clearing it a moment later.
52330
+ //
52331
+ // Clearing at all is what keeps the shorthand usable, since a value on the
52332
+ // record wins over the position: without it, setting a position on a label
52333
+ // that had been dragged would appear to do nothing.
52334
+ clearedByPosition = labelPositionDerivedFields.filter(function(field) {
52335
+ return optFields.indexOf(field) == -1;
52336
+ });
52337
+ hasNewFields = fields.some(function(field) {
52338
+ return !table.fieldExists(field);
52339
+ });
52340
+ // The table's columns as this command found them. Taken once, because the
52341
+ // command adds to them as it runs, and a blanked property must be judged
52342
+ // against what was there before rather than against what it has just made.
52343
+ fieldsBefore = utils.arrayToIndex(table.getFields());
52344
+ if (fields.length > 0) {
52345
+ if (hasNewFields) {
52346
+ table.captureSchemaBefore({operation: 'style', fields: fields});
52347
+ } else {
52348
+ table.captureFieldsBefore(fields, {operation: 'style'});
52349
+ }
52350
+ }
52351
+ Object.keys(opts).forEach(function(optName) {
52352
+ // undo cli parser name conversion; the regex must be global, or a
52353
+ // property with more than one hyphen (e.g. label-start-offset) is silently
52354
+ // skipped rather than applied
52355
+ var svgName = optName.replace(/_/g, '-');
52356
+ if (!isSupportedSvgStyleProperty(svgName)) {
52357
+ return;
52358
+ }
52359
+ var strVal = opts[optName].trim();
52360
+ // An empty value removes the property, rather than being rejected as an
52361
+ // unparseable one. This is how a control gives a property back: a label
52362
+ // dragged off its position clears label-pos, and there is otherwise no
52363
+ // per-property unset -- only -style clear, which clears all of them.
52364
+ var unset = strVal === '' && emptyValueUnsetsProperty(svgName);
52365
+ var accessor = unset ? null : getSymbolPropertyAccessor(strVal, svgName, lyr);
52366
+ var badIcons = svgName == 'icon' ? [] : null;
52367
+ // Removing a position is not setting one, so it neither validates the
52368
+ // value nor clears the offsets the position would have stood for.
52369
+ var posOnPaths = svgName == 'label-pos' && !unset ? [] : null;
52370
+ table.getRecords().forEach(function(rec, i) {
52371
+ if (filterFn && !filterFn(i)) {
52372
+ // make sure field exists if record is excluded by filter
52373
+ setUndefinedFields(rec, [svgName]);
52374
+ if (svgName == 'label-pos') {
52375
+ // ...but a field the position would only have cleared is one this
52376
+ // command is not writing anywhere, so an excluded record has nothing
52377
+ // to stay consistent with
52378
+ setUndefinedFields(rec, labelPositionDerivedFields, {has: hadField});
52379
+ }
52380
+ } else if (unset) {
52381
+ // Nothing to remove, and so nothing to create: removing a property no
52382
+ // record has would otherwise add an empty column for it.
52383
+ if (hadField(svgName) || svgName in rec) rec[svgName] = undefined;
52384
+ } else {
52385
+ rec[svgName] = accessor(i);
52386
+ if (badIcons) {
52387
+ addUnsupportedIconName(badIcons, rec.icon);
52388
+ }
52389
+ if (posOnPaths) {
52390
+ if (!parseLabelPosition(rec['label-pos'])) {
52391
+ stop$1('Unexpected value for label-pos:', rec['label-pos']);
52392
+ }
52393
+ if (shapeIsPathLabel(lyr.shapes && lyr.shapes[i], rec)) {
52394
+ // Not stored, so that ignoring it means ignoring it: a stored
52395
+ // position would show up in the style panel and would start
52396
+ // applying if the label ever lost all but one of its knots. Its
52397
+ // text-anchor is left alone too -- that one does place text along a
52398
+ // path, so a position that had no effect must not clear it.
52399
+ posOnPaths.push(i);
52400
+ rec['label-pos'] = undefined;
52401
+ } else {
52402
+ setUndefinedFields(rec, clearedByPosition, {overwrite: true, has: hadField});
52403
+ }
52404
+ }
52405
+ }
52406
+ });
52407
+ if (badIcons && badIcons.length > 0) {
52408
+ warn(formatUnsupportedIconMessage(badIcons));
52409
+ }
52410
+ if (posOnPaths && posOnPaths.length > 0) {
52411
+ warn(formatPositionOnPathMessage(posOnPaths));
52412
+ }
52413
+ });
52414
+ if (fields.length > 0) {
52415
+ if (hasNewFields) {
52416
+ table.markSchemaChanged({operation: 'style'});
52417
+ } else {
52418
+ table.markFieldsChanged(fields, {operation: 'style'});
52419
+ }
52420
+ }
52421
+ };
52422
+
52423
+ // The style properties this command was given, in SVG spelling.
52424
+ function getOptionFields(opts) {
52425
+ var fields = [];
52426
+ Object.keys(opts).forEach(function(optName) {
52427
+ var svgName = optName.replace(/_/g, '-');
52428
+ if (isSupportedSvgStyleProperty(svgName)) addField(fields, svgName);
52429
+ });
52430
+ return fields;
52431
+ }
52432
+
52433
+ // The fields the command will write, which is what the undo capture covers.
52434
+ // label-pos reaches dx/dy/text-anchor as well -- it no longer stores values in
52435
+ // them, but it does clear them.
52436
+ function getStyleFields(optFields) {
52437
+ var fields = [];
52438
+ optFields.forEach(function(svgName) {
52439
+ addField(fields, svgName);
52440
+ if (svgName == 'label-pos') {
52441
+ labelPositionFields.forEach(function(field) {
52442
+ addField(fields, field);
52443
+ });
52444
+ }
52445
+ });
52446
+ return fields;
52447
+ }
52448
+
52449
+ function addField(fields, field) {
52450
+ if (fields.indexOf(field) == -1) {
52451
+ fields.push(field);
52452
+ }
52453
+ }
52454
+
52455
+ // Icon names can not be validated when options are parsed, because an icon=
52456
+ // value may be a field name or an expression. The assigned values are checked
52457
+ // instead, so that computed names are covered too. Unsupported names are a
52458
+ // warning, not an error -- features with an unsupported name render without an
52459
+ // icon.
52460
+ var maxReportedIconNames = 4;
52461
+
52462
+ function addUnsupportedIconName(names, val) {
52463
+ var name;
52464
+ if (!val) return; // a blank value removes the icon
52465
+ name = String(val);
52466
+ if (isSupportedIconName(name) || names.indexOf(name) > -1) return;
52467
+ names.push(name);
52468
+ }
52469
+
52470
+ function formatUnsupportedIconMessage(names) {
52471
+ var extra = names.length - maxReportedIconNames;
52472
+ var listed = extra > 0 ? names.slice(0, maxReportedIconNames) : names;
52473
+ var str = 'Unsupported icon ' + (names.length > 1 ? 'names' : 'name') + ': ' +
52474
+ listed.join(', ');
52475
+ if (extra > 0) {
52476
+ str += ' (and ' + extra + ' more)';
52477
+ }
52478
+ return str + '. Expected one of: ' + iconNames.join(', ');
52479
+ }
52480
+
52481
+ // Adds @fields to @rec with no value, so that a record the filter excluded
52482
+ // still has the same schema as the ones it kept. With overwrite, also blanks a
52483
+ // value already there -- which is how setting a position takes back the offsets
52484
+ // a label was carrying.
52485
+ //
52486
+ // @has: optional test for whether the layer carries a field at all. A field
52487
+ // nobody has is not created in order to be blanked: -style label-pos=n clears
52488
+ // dx, dy and text-anchor because a value on the record wins over the position,
52489
+ // and there is nothing to win with when the column does not exist. Without
52490
+ // this, clicking a position in the style panel put three empty columns in the
52491
+ // user's table, and three empty columns in their CSV.
52492
+ function setUndefinedFields(rec, fields, opts) {
52493
+ var overwrite = !!(opts && opts.overwrite);
52494
+ var has = opts && opts.has;
52495
+ fields.forEach(function(field) {
52496
+ if (has && !has(field) && field in rec === false) return;
52497
+ if (overwrite || field in rec === false) {
52498
+ rec[field] = undefined;
52499
+ }
52500
+ });
52501
+ }
52502
+
52503
+ // label-pos places text around an anchor point, which a label strung along a
52504
+ // path does not have: its text runs from a start offset in the direction the
52505
+ // path goes. A warning rather than an error, because a layer can hold both
52506
+ // kinds of label and styling all of it at once is reasonable.
52507
+ function formatPositionOnPathMessage(ids) {
52508
+ var extra = ids.length - maxReportedIds;
52509
+ var listed = (extra > 0 ? ids.slice(0, maxReportedIds) : ids).join(', ');
52510
+ return 'Ignoring label-pos on ' + ids.length + ' path ' +
52511
+ (ids.length > 1 ? 'labels' : 'label') + ' (' + listed +
52512
+ (extra > 0 ? ' and ' + extra + ' more' : '') + '). ' +
52513
+ 'Use label-start-offset= and text-anchor= to place text along a path.';
52514
+ }
52515
+
52516
+ var maxReportedIds = 4;
52517
+
52518
+ // -labels styles the labels in a point layer, or turns a point layer into
52519
+ // labels by giving it text=. With coordinates=, it adds one label instead; that
52520
+ // case is cmd.addLabel(), dispatched separately because it accepts an empty
52521
+ // target.
52522
+ //
52523
+ // Styling writes the same properties -style writes, through the same code, so
52524
+ // a label styled by either command is the same label.
52525
+ cmd.labels = function(targetLayers, dataset, opts) {
52526
+ var styleOpts = getStyleOpts(opts);
52527
+ var output = targetLayers.map(function(lyr) {
52528
+ var out;
52529
+ requirePointLayer$1(lyr);
52530
+ out = opts.no_replace ? copyLayer(lyr) : lyr;
52531
+ cmd.svgStyle(out, dataset, styleOpts);
52532
+ return out;
52533
+ });
52534
+ return opts.no_replace ? output : null;
52535
+ };
52536
+
52537
+ // text= is label-text= under the name that reads naturally in this command.
52538
+ function getStyleOpts(opts) {
52539
+ var o = Object.assign({}, opts);
52540
+ if ('text' in o) {
52541
+ o.label_text = o.text;
52542
+ delete o.text;
52543
+ }
52544
+ return o;
52545
+ }
52546
+
52547
+ // Labels are drawn at points. A layer with no geometry is let through, since
52548
+ // it has nothing to conflict with.
52549
+ function requirePointLayer$1(lyr) {
52550
+ if (lyr.geometry_type && lyr.geometry_type != 'point') {
52551
+ stop$1('Labels can only be applied to a point layer; layer "' +
52552
+ (lyr.name || '[unnamed]') + '" contains ' + lyr.geometry_type + 's. ' +
52553
+ 'Use -points to make a point layer to label.');
52554
+ }
52555
+ }
52556
+
51398
52557
  cmd.addLayer = addLayer;
51399
52558
 
51400
52559
  var GEOMETRY_TYPES = ['point', 'polygon', 'polyline'];
@@ -52297,13 +53456,17 @@ ${svg}
52297
53456
  var sw;
52298
53457
  if (!(r > 0)) return null;
52299
53458
  if (type == 'ring') {
52300
- // drawn as a stroke, one px wide unless stroke-width says otherwise
53459
+ // drawn as a stroke, RING_STROKE_WIDTH wide unless stroke-width says otherwise
52301
53460
  sw = +d['stroke-width'];
52302
- return getSquareBox(r + (sw > 0 ? sw : 1) / 2, 0, 0);
53461
+ return getSquareBox(r + (sw > 0 ? sw : RING_STROKE_WIDTH) / 2, 0, 0);
52303
53462
  }
52304
53463
  if (type == 'circle' || type == 'square' || type == 'star') {
52305
53464
  return getSquareBox(r + getStrokeOutset(d), 0, 0);
52306
53465
  }
53466
+ if (type == 'nyt-star') {
53467
+ // the outline is part of the symbol's size (see nytStar())
53468
+ return getSquareBox(r, 0, 0);
53469
+ }
52307
53470
  return null; // unsupported icons are not drawn
52308
53471
  }
52309
53472
 
@@ -53263,10 +54426,10 @@ ${svg}
53263
54426
  }
53264
54427
 
53265
54428
  function isClosedPath$1(arr) {
53266
- return isArrayOfPoints(arr) && arr.length > 3 && samePoint$2(arr[0], arr[arr.length - 1]);
54429
+ return isArrayOfPoints(arr) && arr.length > 3 && samePoint$3(arr[0], arr[arr.length - 1]);
53267
54430
  }
53268
54431
 
53269
- function samePoint$2(a, b) {
54432
+ function samePoint$3(a, b) {
53270
54433
  return a[0] == b[0] && a[1] == b[1];
53271
54434
  }
53272
54435
 
@@ -54072,7 +55235,7 @@ ${svg}
54072
55235
  }
54073
55236
 
54074
55237
  cmd.alphaShapes = function(pointLyr, targetDataset, opts) {
54075
- requirePointLayer$1(pointLyr);
55238
+ requirePointLayer$2(pointLyr);
54076
55239
  if (opts.interval > 0 === false) {
54077
55240
  stop$1('Expected a non-negative interval parameter');
54078
55241
  }
@@ -55453,12 +56616,12 @@ ${svg}
55453
56616
  return arr[arr.length - 1];
55454
56617
  }
55455
56618
 
55456
- function samePoint$1(a, b) {
56619
+ function samePoint$2(a, b) {
55457
56620
  return a && b && a[0] === b[0] && a[1] === b[1];
55458
56621
  }
55459
56622
 
55460
56623
  function isClosedPath(arr) {
55461
- return samePoint$1(arr[0], lastEl(arr));
56624
+ return samePoint$2(arr[0], lastEl(arr));
55462
56625
  }
55463
56626
 
55464
56627
  // remove likely rounding errors
@@ -55619,7 +56782,7 @@ ${svg}
55619
56782
  if (!nextPart) {
55620
56783
  return null;
55621
56784
  }
55622
- if (samePoint$1(ring[0], nextPart[0])) {
56785
+ if (samePoint$2(ring[0], nextPart[0])) {
55623
56786
  // done!
55624
56787
  ring.push(ring[0]); // close the ring
55625
56788
  return ring;
@@ -55668,7 +56831,7 @@ ${svg}
55668
56831
 
55669
56832
  function findPartStartingAt(parts, firstPoint) {
55670
56833
  for (var i=0; i<parts.length; i++) {
55671
- if (samePoint$1(parts[i][0], firstPoint)) {
56834
+ if (samePoint$2(parts[i][0], firstPoint)) {
55672
56835
  return parts[i];
55673
56836
  }
55674
56837
  }
@@ -55692,7 +56855,7 @@ ${svg}
55692
56855
  var part = [];
55693
56856
  var firstPoint = path[0];
55694
56857
  var lastPoint = lastEl(path);
55695
- var closed = samePoint$1(firstPoint, lastPoint);
56858
+ var closed = samePoint$2(firstPoint, lastPoint);
55696
56859
  var p, pp, y;
55697
56860
  for (var i=0, n=path.length; i<n; i++) {
55698
56861
  p = path[i];
@@ -60358,10 +61521,10 @@ ${svg}
60358
61521
  var p = recenteredVertex(ctx, points[i]);
60359
61522
  // Successive off-center vertices can re-center onto the same neck point;
60360
61523
  // keep one copy so the chain has no zero-length segments.
60361
- if (!samePoint(p, out[out.length - 1])) out.push(p);
61524
+ if (!samePoint$1(p, out[out.length - 1])) out.push(p);
60362
61525
  }
60363
61526
  var last = points[points.length - 1];
60364
- if (!samePoint(last, out[out.length - 1])) out.push(last);
61527
+ if (!samePoint$1(last, out[out.length - 1])) out.push(last);
60365
61528
  return out.length >= 2 ? out : points;
60366
61529
  }
60367
61530
 
@@ -60464,7 +61627,7 @@ ${svg}
60464
61627
  out[1] = ay + t * dy;
60465
61628
  }
60466
61629
 
60467
- function samePoint(a, b) {
61630
+ function samePoint$1(a, b) {
60468
61631
  return a[0] === b[0] && a[1] === b[1];
60469
61632
  }
60470
61633
 
@@ -61713,131 +62876,6 @@ ${svg}
61713
62876
  normalizeOverlaySource: normalizeOverlaySource
61714
62877
  });
61715
62878
 
61716
- // Insert cutting points in arcs, where bbox intersects other shapes
61717
- // Return a polygon layer containing the bounding box vectors, divided at cutting points.
61718
- function divideDatasetByBBox(dataset, bbox) {
61719
- var arcs = dataset.arcs;
61720
- var data = findBBoxCutPoints(arcs, bbox);
61721
- var map = insertCutPoints(data.cutPoints, arcs);
61722
- arcs.dedupCoords();
61723
- remapDividedArcs(dataset, map);
61724
- // merge bbox dataset with target dataset,
61725
- // so arcs are shared between target layers and bbox layer
61726
- var clipDataset = bboxPointsToClipDataset(data.bboxPoints);
61727
- var mergedDataset = mergeDatasets([dataset, clipDataset]);
61728
- // TODO: detect if we need to rebuild topology (unlikely), like with the full clip command
61729
- // buildTopology(mergedDataset);
61730
- var clipLyr = mergedDataset.layers.pop();
61731
- dataset.arcs = mergedDataset.arcs;
61732
- dataset.layers = mergedDataset.layers;
61733
- return clipLyr;
61734
- }
61735
-
61736
- function bboxPointsToClipDataset(arr) {
61737
- var arcs = [];
61738
- var shape = [];
61739
- var layer = {geometry_type: 'polygon', shapes: [[shape]]};
61740
- var p1, p2;
61741
- for (var i=0, n=arr.length - 1; i<n; i++) {
61742
- p1 = arr[i];
61743
- p2 = arr[i+1];
61744
- arcs.push([[p1.x, p1.y], [p2.x, p2.y]]);
61745
- shape.push(i);
61746
- }
61747
- return {
61748
- arcs: new ArcCollection(arcs),
61749
- layers: [layer]
61750
- };
61751
- }
61752
-
61753
- function findBBoxCutPoints(arcs, bbox) {
61754
- var left = bbox[0],
61755
- bottom = bbox[1],
61756
- right = bbox[2],
61757
- top = bbox[3];
61758
-
61759
- // arrays of intersection points along each bbox edge
61760
- var tt = [],
61761
- rr = [],
61762
- bb = [],
61763
- ll = [];
61764
-
61765
- arcs.forEachSegment(function(i, j, xx, yy) {
61766
- var ax = xx[i],
61767
- ay = yy[i],
61768
- bx = xx[j],
61769
- by = yy[j];
61770
- var hit;
61771
- if (segmentOutsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
61772
- if (segmentInsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
61773
-
61774
- hit = geom.segmentIntersection(left, top, right, top, ax, ay, bx, by);
61775
- if (hit) addHit(tt, hit, i, j, xx, yy);
61776
-
61777
- hit = geom.segmentIntersection(left, bottom, right, bottom, ax, ay, bx, by);
61778
- if (hit) addHit(bb, hit, i, j, xx, yy);
61779
-
61780
- hit = geom.segmentIntersection(left, bottom, left, top, ax, ay, bx, by);
61781
- if (hit) addHit(ll, hit, i, j, xx, yy);
61782
-
61783
- hit = geom.segmentIntersection(right, bottom, right, top, ax, ay, bx, by);
61784
- if (hit) addHit(rr, hit, i, j, xx, yy);
61785
- });
61786
-
61787
- return {
61788
- cutPoints: ll.concat(bb, rr, tt),
61789
- bboxPoints: getDividedBBoxPoints(bbox, ll, tt, rr, bb)
61790
- };
61791
-
61792
- function addHit(arr, hit, i, j, xx, yy) {
61793
- if (!hit) return;
61794
- arr.push(formatHit(hit[0], hit[1], i, j, xx, yy));
61795
- if (hit.length == 4) {
61796
- arr.push(formatHit(hit[2], hit[3], i, j, xx, yy));
61797
- }
61798
- }
61799
-
61800
- function formatHit(x, y, i, j, xx, yy) {
61801
- var ids = formatIntersectingSegment(x, y, i, j, xx, yy);
61802
- return getCutPoint(x, y, ids[0], ids[1]);
61803
- }
61804
- }
61805
-
61806
- function segmentOutsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
61807
- return ax < xmin && bx < xmin || ax > xmax && bx > xmax ||
61808
- ay < ymin && by < ymin || ay > ymax && by > ymax;
61809
- }
61810
-
61811
- function segmentInsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
61812
- return ax > xmin && bx > xmin && ax < xmax && bx < xmax &&
61813
- ay > ymin && by > ymin && ay < ymax && by < ymax;
61814
- }
61815
-
61816
- // Returns an array of points representing the vertices in
61817
- // the bbox with cutting points inserted.
61818
- function getDividedBBoxPoints(bbox, ll, tt, rr, bb) {
61819
- var bl = {x: bbox[0], y: bbox[1]},
61820
- tl = {x: bbox[0], y: bbox[3]},
61821
- tr = {x: bbox[2], y: bbox[3]},
61822
- br = {x: bbox[2], y: bbox[1]};
61823
- ll = utils.sortOn(ll.concat([bl, tl]), 'y', true);
61824
- tt = utils.sortOn(tt.concat([tl, tr]), 'x', true);
61825
- rr = utils.sortOn(rr.concat([tr, br]), 'y', false);
61826
- bb = utils.sortOn(bb.concat([br, bl]), 'x', false);
61827
- return ll.concat(tt, rr, bb).reduce(function(memo, p2) {
61828
- var p1 = memo.length > 0 ? memo[memo.length-1] : null;
61829
- if (p1 === null || p1.x != p2.x || p1.y != p2.y) memo.push(p2);
61830
- return memo;
61831
- }, []);
61832
- }
61833
-
61834
- var Bbox2Clipping = /*#__PURE__*/Object.freeze({
61835
- __proto__: null,
61836
- divideDatasetByBBox: divideDatasetByBBox,
61837
- segmentInsideBBox: segmentInsideBBox,
61838
- segmentOutsideBBox: segmentOutsideBBox
61839
- });
61840
-
61841
62879
  cmd.clipLayers = function(target, src, dataset, opts) {
61842
62880
  return clipLayers(target, src, dataset, "clip", opts);
61843
62881
  };
@@ -64949,7 +65987,7 @@ ${svg}
64949
65987
  debug('Short ring', coords);
64950
65988
  return;
64951
65989
  }
64952
- if (!samePoint$1(coords[0], lastEl(coords))) {
65990
+ if (!samePoint$2(coords[0], lastEl(coords))) {
64953
65991
  error('Open polygon ring');
64954
65992
  }
64955
65993
  rings.push(coords); // accumulate rings
@@ -64982,7 +66020,7 @@ ${svg}
64982
66020
  cmd.lines = function(lyr, dataset, opts) {
64983
66021
  opts = opts || {};
64984
66022
  if (opts.callouts) {
64985
- requirePointLayer$1(lyr);
66023
+ requirePointLayer$2(lyr);
64986
66024
  return pointsToCallouts(lyr, dataset, opts);
64987
66025
  } else if (lyr.geometry_type == 'point') {
64988
66026
  return pointsToLines(lyr, dataset, opts);
@@ -75469,231 +76507,6 @@ ${svg}
75469
76507
  return api;
75470
76508
  }
75471
76509
 
75472
- cmd.filterGeom = function(lyr, arcs, opts) {
75473
- if (!layerHasGeometry$1(lyr)) {
75474
- stop$1("Layer is missing geometry");
75475
- }
75476
- if (opts.bbox) {
75477
- filterByBoundsIntersection(lyr, arcs, opts);
75478
- }
75479
- cmd.filterFeatures(lyr, arcs, {remove_empty: true, verbose: false});
75480
- };
75481
-
75482
- function filterByBoundsIntersection(lyr, arcs, opts) {
75483
- var filter = getBoundsIntersectionFilter(opts.bbox, lyr, arcs);
75484
- noteLayerWillChange(lyr, {operation: 'filter-geom', unit: 'shapes'});
75485
- editShapes(lyr.shapes, filter);
75486
- markLayerChanged(lyr, {operation: 'filter-geom', unit: 'shapes'});
75487
- }
75488
-
75489
- function getBoundsIntersectionFilter(bbox, lyr, arcs) {
75490
- var bounds = new Bounds(bbox);
75491
- var filter = lyr.geometry_type == 'point' ?
75492
- getPointInBoundsTest(bounds) :
75493
- getPathBoundsIntersectionTest(bounds, arcs);
75494
- return filter;
75495
- }
75496
-
75497
- function getPointInBoundsTest(bounds) {
75498
- return function(xy) {
75499
- var contains = bounds.containsPoint(xy[0], xy[1]);
75500
- return contains ? xy : null;
75501
- };
75502
- }
75503
-
75504
- // V1 too-simple test: bounding-box intersection
75505
- // internal.getPathBoundsIntersectionTest = function(bounds, arcs) {
75506
- // return function(path) {
75507
- // return bounds.intersects(arcs.getSimpleShapeBounds(path)) ? path : null;
75508
- // };
75509
- // };
75510
-
75511
- function getPathBoundsIntersectionTest(bounds, arcs) {
75512
- var bbox = bounds.toArray(),
75513
- left = bbox[0],
75514
- bottom = bbox[1],
75515
- right = bbox[2],
75516
- top = bbox[3];
75517
-
75518
- return function(path) {
75519
- // case: bounding boxes don't intersect -> the path doesn't intersect the box
75520
- if (!bounds.intersects(arcs.getSimpleShapeBounds(path))) {
75521
- return null;
75522
- }
75523
- var intersects = false;
75524
- var ax, ay, bx, by;
75525
- var iter = arcs.getShapeIter(path);
75526
-
75527
- if (iter.hasNext()) {
75528
- ax = iter.x;
75529
- ay = iter.y;
75530
- }
75531
- while (iter.hasNext()) {
75532
- bx = ax;
75533
- by = ay;
75534
- ax = iter.x;
75535
- ay = iter.y;
75536
- if (segmentOutsideBBox(ax, ay, bx, by, left, bottom, right, top)) continue;
75537
- if (segmentInsideBBox(ax, ay, bx, by, left, bottom, right, top)) {
75538
- intersects = true;
75539
- break;
75540
- }
75541
- if (geom.segmentIntersection(left, top, right, top, ax, ay, bx, by) ||
75542
- geom.segmentIntersection(left, bottom, right, bottom, ax, ay, bx, by) ||
75543
- geom.segmentIntersection(left, bottom, left, top, ax, ay, bx, by) ||
75544
- geom.segmentIntersection(right, bottom, right, top, ax, ay, bx, by)) {
75545
- intersects = true;
75546
- break;
75547
- }
75548
- }
75549
-
75550
- // case: bbox is entirely inside this ring
75551
- if (!intersects && geom.testPointInRing(left, bottom, path, arcs)) {
75552
- intersects = true;
75553
- }
75554
- return intersects ? path : null;
75555
- };
75556
- }
75557
-
75558
- // Return a function for testing if a shape (path or point) intersects a bounding box
75559
- // TODO: move this function to a different file
75560
- function getBBoxIntersectionTest(bbox, lyr, arcs) {
75561
- var filter = getBoundsIntersectionFilter(bbox, lyr, arcs);
75562
- return function(shapeId) {
75563
- var shp = lyr.shapes[shapeId];
75564
- if (!shp) return false;
75565
- for (var i=0; i<shp.length; i++) {
75566
- if (filter(shp[i])) return true;
75567
- }
75568
- return false;
75569
- };
75570
- }
75571
-
75572
- // return array of shape ids
75573
- function findShapesIntersectingBBox(bbox, lyr, arcs) {
75574
- var test = getBBoxIntersectionTest(bbox, lyr, arcs);
75575
- var ids = [];
75576
- for (var i=0; i<lyr.shapes.length; i++) {
75577
- if (test(i)) ids.push(i);
75578
- }
75579
- return ids;
75580
- }
75581
-
75582
- var FilterGeom = /*#__PURE__*/Object.freeze({
75583
- __proto__: null,
75584
- findShapesIntersectingBBox: findShapesIntersectingBBox,
75585
- getBBoxIntersectionTest: getBBoxIntersectionTest
75586
- });
75587
-
75588
- cmd.filterFeatures = function(lyr, arcs, opts) {
75589
- var records = lyr.data ? lyr.data.getRecords() : null,
75590
- shapes = lyr.shapes || null,
75591
- n = getFeatureCount(lyr),
75592
- filteredShapes = shapes ? [] : null,
75593
- filteredRecords = records ? [] : null,
75594
- filteredLyr = getOutputLayer(lyr, opts),
75595
- invert = !!opts.invert,
75596
- filter;
75597
-
75598
- if (opts.expression) {
75599
- filter = compileFeatureExpression(opts.expression, lyr, arcs);
75600
- }
75601
-
75602
- if (opts.ids) {
75603
- filter = combineFilters(filter, getIdFilter(opts.ids));
75604
- }
75605
-
75606
- if (opts.remove_empty) {
75607
- filter = combineFilters(filter, getNullGeometryFilter(lyr, arcs));
75608
- }
75609
-
75610
- if (opts.bbox) {
75611
- filter = combineFilters(filter, getBBoxIntersectionTest(opts.bbox, lyr, arcs));
75612
- }
75613
-
75614
- if (!filter) {
75615
- stop$1("Missing a filter criterion");
75616
- }
75617
-
75618
- utils.repeat(n, function(shapeId) {
75619
- var result = filter(shapeId);
75620
- requireBooleanResult(result);
75621
- if (invert) result = !result;
75622
- if (result === true) {
75623
- if (shapes) filteredShapes.push(shapes[shapeId] || null);
75624
- if (records) filteredRecords.push(records[shapeId] || null);
75625
- }
75626
- });
75627
-
75628
- if (filteredLyr == lyr) {
75629
- noteLayerWillChange(lyr, {operation: 'filter'});
75630
- }
75631
- filteredLyr.shapes = filteredShapes;
75632
- filteredLyr.data = filteredRecords ? new DataTable(filteredRecords) : null;
75633
- if (filteredLyr == lyr) {
75634
- markLayerChanged(lyr, {operation: 'filter'});
75635
- }
75636
- if (opts.no_replace) {
75637
- // if adding a layer, don't share objects between source and filtered layer
75638
- filteredLyr = copyLayer(filteredLyr);
75639
- }
75640
-
75641
- if (opts.verbose !== false && !opts.quiet) {
75642
- message(utils.format('Retained %,d of %,d features', getFeatureCount(filteredLyr), n));
75643
- }
75644
-
75645
- return filteredLyr;
75646
- };
75647
-
75648
- // TODO: update filter command to use this function
75649
- function filterLayerInPlace(lyr, filter, invert) {
75650
- var records = lyr.data ? lyr.data.getRecords() : null,
75651
- shapes = lyr.shapes || null,
75652
- n = getFeatureCount(lyr),
75653
- filteredShapes = shapes ? [] : null,
75654
- filteredRecords = records ? [] : null;
75655
- utils.repeat(n, function(shapeId) {
75656
- var result = filter(shapeId);
75657
- requireBooleanResult(result);
75658
- if (result === true) {
75659
- if (shapes) filteredShapes.push(shapes[shapeId] || null);
75660
- if (records) filteredRecords.push(records[shapeId] || null);
75661
- }
75662
- });
75663
- noteLayerWillChange(lyr, {operation: 'filterLayerInPlace'});
75664
- lyr.shapes = filteredShapes;
75665
- lyr.data = filteredRecords ? new DataTable(filteredRecords) : null;
75666
- markLayerChanged(lyr, {operation: 'filterLayerInPlace'});
75667
- }
75668
-
75669
- function getIdFilter(ids) {
75670
- var set = new Set(ids);
75671
- return function(i) {
75672
- return set.has(i);
75673
- };
75674
- }
75675
-
75676
- function getNullGeometryFilter(lyr, arcs) {
75677
- var shapes = lyr.shapes;
75678
- if (lyr.geometry_type == 'polygon') {
75679
- return getEmptyPolygonFilter(shapes, arcs);
75680
- }
75681
- return function(i) {return !!shapes[i];};
75682
- }
75683
-
75684
- function getEmptyPolygonFilter(shapes, arcs) {
75685
- return function(i) {
75686
- var shp = shapes[i];
75687
- return !!shp && geom.getPlanarShapeArea(shapes[i], arcs) > 0;
75688
- };
75689
- }
75690
-
75691
- function combineFilters(a, b) {
75692
- return a && b ? function(id) {
75693
- return a(id) && b(id);
75694
- } : (a || b);
75695
- }
75696
-
75697
76510
  cmd.evaluateEachFeature = function(lyr, dataset, expArg, opts) {
75698
76511
  var n = getFeatureCount(lyr),
75699
76512
  arcs = dataset.arcs,
@@ -79540,7 +80353,7 @@ ${svg}
79540
80353
  }
79541
80354
 
79542
80355
  cmd.pointToGrid = function(targetLayers, targetDataset, opts) {
79543
- targetLayers.forEach(requirePointLayer$1);
80356
+ targetLayers.forEach(requirePointLayer$2);
79544
80357
  if (opts.interval > 0 === false) {
79545
80358
  stop$1('Expected a non-negative interval parameter');
79546
80359
  }
@@ -82325,232 +83138,6 @@ ${svg}
82325
83138
  stopJob(job);
82326
83139
  };
82327
83140
 
82328
- // The icon names accepted by the icon= property of the -style command.
82329
- // Kept in a module of its own so that both the renderer (svg-symbols.mjs) and
82330
- // the -style command can use it without forming an import cycle.
82331
-
82332
- var iconNames = ['circle', 'square', 'ring', 'star'];
82333
-
82334
- function isSupportedIconName(name) {
82335
- return iconNames.indexOf(name) > -1;
82336
- }
82337
-
82338
- cmd.svgStyle = function(lyr, dataset, opts) {
82339
- var filterFn, table, fields, hasNewFields, optFields, clearedByPosition, fieldsBefore;
82340
-
82341
- function hadField(field) {
82342
- return field in fieldsBefore;
82343
- }
82344
-
82345
- if (getFeatureCount(lyr) === 0) {
82346
- return;
82347
- }
82348
- if (!lyr.data) {
82349
- initDataTable(lyr);
82350
- }
82351
- if (opts.where) {
82352
- filterFn = compileFeatureExpression(opts.where, lyr, dataset.arcs);
82353
- }
82354
- if (opts.ids) {
82355
- filterFn = combineFilters(filterFn, getIdFilter(opts.ids));
82356
- }
82357
- if (opts.clear) {
82358
- lyr.data.getFields().filter(isSupportedSvgStyleProperty).forEach(lyr.data.deleteField, lyr.data);
82359
- }
82360
- table = getLayerDataTable(lyr);
82361
- optFields = getOptionFields(opts);
82362
- fields = getStyleFields(optFields);
82363
- // Which of dx/dy/text-anchor a label-pos in this command clears: not the ones
82364
- // the same command also sets, so that `label-pos=n dx=3` keeps the nudge it
82365
- // was given instead of clearing it a moment later.
82366
- //
82367
- // Clearing at all is what keeps the shorthand usable, since a value on the
82368
- // record wins over the position: without it, setting a position on a label
82369
- // that had been dragged would appear to do nothing.
82370
- clearedByPosition = labelPositionDerivedFields.filter(function(field) {
82371
- return optFields.indexOf(field) == -1;
82372
- });
82373
- hasNewFields = fields.some(function(field) {
82374
- return !table.fieldExists(field);
82375
- });
82376
- // The table's columns as this command found them. Taken once, because the
82377
- // command adds to them as it runs, and a blanked property must be judged
82378
- // against what was there before rather than against what it has just made.
82379
- fieldsBefore = utils.arrayToIndex(table.getFields());
82380
- if (fields.length > 0) {
82381
- if (hasNewFields) {
82382
- table.captureSchemaBefore({operation: 'style', fields: fields});
82383
- } else {
82384
- table.captureFieldsBefore(fields, {operation: 'style'});
82385
- }
82386
- }
82387
- Object.keys(opts).forEach(function(optName) {
82388
- // undo cli parser name conversion; the regex must be global, or a
82389
- // property with more than one hyphen (e.g. label-start-offset) is silently
82390
- // skipped rather than applied
82391
- var svgName = optName.replace(/_/g, '-');
82392
- if (!isSupportedSvgStyleProperty(svgName)) {
82393
- return;
82394
- }
82395
- var strVal = opts[optName].trim();
82396
- // An empty value removes the property, rather than being rejected as an
82397
- // unparseable one. This is how a control gives a property back: a label
82398
- // dragged off its position clears label-pos, and there is otherwise no
82399
- // per-property unset -- only -style clear, which clears all of them.
82400
- var unset = strVal === '' && emptyValueUnsetsProperty(svgName);
82401
- var accessor = unset ? null : getSymbolPropertyAccessor(strVal, svgName, lyr);
82402
- var badIcons = svgName == 'icon' ? [] : null;
82403
- // Removing a position is not setting one, so it neither validates the
82404
- // value nor clears the offsets the position would have stood for.
82405
- var posOnPaths = svgName == 'label-pos' && !unset ? [] : null;
82406
- table.getRecords().forEach(function(rec, i) {
82407
- if (filterFn && !filterFn(i)) {
82408
- // make sure field exists if record is excluded by filter
82409
- setUndefinedFields(rec, [svgName]);
82410
- if (svgName == 'label-pos') {
82411
- // ...but a field the position would only have cleared is one this
82412
- // command is not writing anywhere, so an excluded record has nothing
82413
- // to stay consistent with
82414
- setUndefinedFields(rec, labelPositionDerivedFields, {has: hadField});
82415
- }
82416
- } else if (unset) {
82417
- // Nothing to remove, and so nothing to create: removing a property no
82418
- // record has would otherwise add an empty column for it.
82419
- if (hadField(svgName) || svgName in rec) rec[svgName] = undefined;
82420
- } else {
82421
- rec[svgName] = accessor(i);
82422
- if (badIcons) {
82423
- addUnsupportedIconName(badIcons, rec.icon);
82424
- }
82425
- if (posOnPaths) {
82426
- if (!parseLabelPosition(rec['label-pos'])) {
82427
- stop$1('Unexpected value for label-pos:', rec['label-pos']);
82428
- }
82429
- if (shapeIsPathLabel(lyr.shapes && lyr.shapes[i], rec)) {
82430
- // Not stored, so that ignoring it means ignoring it: a stored
82431
- // position would show up in the style panel and would start
82432
- // applying if the label ever lost all but one of its knots. Its
82433
- // text-anchor is left alone too -- that one does place text along a
82434
- // path, so a position that had no effect must not clear it.
82435
- posOnPaths.push(i);
82436
- rec['label-pos'] = undefined;
82437
- } else {
82438
- setUndefinedFields(rec, clearedByPosition, {overwrite: true, has: hadField});
82439
- }
82440
- }
82441
- }
82442
- });
82443
- if (badIcons && badIcons.length > 0) {
82444
- warn(formatUnsupportedIconMessage(badIcons));
82445
- }
82446
- if (posOnPaths && posOnPaths.length > 0) {
82447
- warn(formatPositionOnPathMessage(posOnPaths));
82448
- }
82449
- });
82450
- if (fields.length > 0) {
82451
- if (hasNewFields) {
82452
- table.markSchemaChanged({operation: 'style'});
82453
- } else {
82454
- table.markFieldsChanged(fields, {operation: 'style'});
82455
- }
82456
- }
82457
- };
82458
-
82459
- // The style properties this command was given, in SVG spelling.
82460
- function getOptionFields(opts) {
82461
- var fields = [];
82462
- Object.keys(opts).forEach(function(optName) {
82463
- var svgName = optName.replace(/_/g, '-');
82464
- if (isSupportedSvgStyleProperty(svgName)) addField(fields, svgName);
82465
- });
82466
- return fields;
82467
- }
82468
-
82469
- // The fields the command will write, which is what the undo capture covers.
82470
- // label-pos reaches dx/dy/text-anchor as well -- it no longer stores values in
82471
- // them, but it does clear them.
82472
- function getStyleFields(optFields) {
82473
- var fields = [];
82474
- optFields.forEach(function(svgName) {
82475
- addField(fields, svgName);
82476
- if (svgName == 'label-pos') {
82477
- labelPositionFields.forEach(function(field) {
82478
- addField(fields, field);
82479
- });
82480
- }
82481
- });
82482
- return fields;
82483
- }
82484
-
82485
- function addField(fields, field) {
82486
- if (fields.indexOf(field) == -1) {
82487
- fields.push(field);
82488
- }
82489
- }
82490
-
82491
- // Icon names can not be validated when options are parsed, because an icon=
82492
- // value may be a field name or an expression. The assigned values are checked
82493
- // instead, so that computed names are covered too. Unsupported names are a
82494
- // warning, not an error -- features with an unsupported name render without an
82495
- // icon.
82496
- var maxReportedIconNames = 4;
82497
-
82498
- function addUnsupportedIconName(names, val) {
82499
- var name;
82500
- if (!val) return; // a blank value removes the icon
82501
- name = String(val);
82502
- if (isSupportedIconName(name) || names.indexOf(name) > -1) return;
82503
- names.push(name);
82504
- }
82505
-
82506
- function formatUnsupportedIconMessage(names) {
82507
- var extra = names.length - maxReportedIconNames;
82508
- var listed = extra > 0 ? names.slice(0, maxReportedIconNames) : names;
82509
- var str = 'Unsupported icon ' + (names.length > 1 ? 'names' : 'name') + ': ' +
82510
- listed.join(', ');
82511
- if (extra > 0) {
82512
- str += ' (and ' + extra + ' more)';
82513
- }
82514
- return str + '. Expected one of: ' + iconNames.join(', ');
82515
- }
82516
-
82517
- // Adds @fields to @rec with no value, so that a record the filter excluded
82518
- // still has the same schema as the ones it kept. With overwrite, also blanks a
82519
- // value already there -- which is how setting a position takes back the offsets
82520
- // a label was carrying.
82521
- //
82522
- // @has: optional test for whether the layer carries a field at all. A field
82523
- // nobody has is not created in order to be blanked: -style label-pos=n clears
82524
- // dx, dy and text-anchor because a value on the record wins over the position,
82525
- // and there is nothing to win with when the column does not exist. Without
82526
- // this, clicking a position in the style panel put three empty columns in the
82527
- // user's table, and three empty columns in their CSV.
82528
- function setUndefinedFields(rec, fields, opts) {
82529
- var overwrite = !!(opts && opts.overwrite);
82530
- var has = opts && opts.has;
82531
- fields.forEach(function(field) {
82532
- if (has && !has(field) && field in rec === false) return;
82533
- if (overwrite || field in rec === false) {
82534
- rec[field] = undefined;
82535
- }
82536
- });
82537
- }
82538
-
82539
- // label-pos places text around an anchor point, which a label strung along a
82540
- // path does not have: its text runs from a start offset in the direction the
82541
- // path goes. A warning rather than an error, because a layer can hold both
82542
- // kinds of label and styling all of it at once is reasonable.
82543
- function formatPositionOnPathMessage(ids) {
82544
- var extra = ids.length - maxReportedIds;
82545
- var listed = (extra > 0 ? ids.slice(0, maxReportedIds) : ids).join(', ');
82546
- return 'Ignoring label-pos on ' + ids.length + ' path ' +
82547
- (ids.length > 1 ? 'labels' : 'label') + ' (' + listed +
82548
- (extra > 0 ? ' and ' + extra + ' more' : '') + '). ' +
82549
- 'Use label-start-offset= and text-anchor= to place text along a path.';
82550
- }
82551
-
82552
- var maxReportedIds = 4;
82553
-
82554
83141
  function getStickArrowCoords(d) {
82555
83142
  return getArrowCoords(d, 'stick');
82556
83143
  }
@@ -83082,7 +83669,7 @@ ${svg}
83082
83669
 
83083
83670
  // TODO: refactor to remove duplication in mapshaper-svg-style.js
83084
83671
  cmd.symbols = function(inputLyr, dataset, opts) {
83085
- requirePointLayer$1(inputLyr);
83672
+ requirePointLayer$2(inputLyr);
83086
83673
  var lyr = opts.no_replace ? copyLayer(inputLyr) : inputLyr;
83087
83674
  var shapeMode = !!opts.geographic;
83088
83675
  var metersPerPx;
@@ -83683,6 +84270,9 @@ ${svg}
83683
84270
  if (name == 'rectangle') {
83684
84271
  return !opts.source && !opts.bbox;
83685
84272
  }
84273
+ if (name == 'labels') {
84274
+ return !('coordinates' in opts);
84275
+ }
83686
84276
  return [
83687
84277
  'affine', 'alpha-shapes', 'blur', 'buffer', 'calc', 'check-geometry',
83688
84278
  'classify', 'clean', 'clip', 'cluster', 'contours', 'dashlines', 'data-fill',
@@ -83697,7 +84287,10 @@ ${svg}
83697
84287
  ].indexOf(name) > -1;
83698
84288
  }
83699
84289
 
83700
- function commandAcceptsEmptyTarget(name) {
84290
+ function commandAcceptsEmptyTarget(name, opts) {
84291
+ if (name == 'labels') {
84292
+ return 'coordinates' in opts;
84293
+ }
83701
84294
  return name == 'graticule' || name == 'i' || name == 'help' ||
83702
84295
  name == 'point-grid' || name == 'shape' || name == 'rectangle' || name == 'frame' ||
83703
84296
  name == 'require' || name == 'run' || name == 'define' ||
@@ -83793,7 +84386,7 @@ ${svg}
83793
84386
  stop$1(utils.format('Missing target: %s\nAvailable layers: %s',
83794
84387
  opts.target, getFormattedLayerList(job.catalog)));
83795
84388
  }
83796
- if (!commandAcceptsEmptyTarget(name)) {
84389
+ if (!commandAcceptsEmptyTarget(name, opts)) {
83797
84390
  stop$1("No data is available");
83798
84391
  }
83799
84392
  }
@@ -83816,7 +84409,7 @@ ${svg}
83816
84409
  }
83817
84410
  outputLayers = cmd.addShape(targetLayers, targetDataset, opts);
83818
84411
 
83819
- } else if (name == 'add-label') {
84412
+ } else if (name == 'add-label' || name == 'labels' && 'coordinates' in opts) {
83820
84413
  if (!targetDataset) {
83821
84414
  targetDataset = {info: {}, layers: []};
83822
84415
  targetLayers = targetDataset.layers;
@@ -83824,6 +84417,9 @@ ${svg}
83824
84417
  }
83825
84418
  outputLayers = cmd.addLabel(targetLayers, targetDataset, opts);
83826
84419
 
84420
+ } else if (name == 'labels') {
84421
+ outputLayers = cmd.labels(targetLayers, targetDataset, opts);
84422
+
83827
84423
  } else if (name == 'add-layer') {
83828
84424
  // The new layer arrives in a dataset of its own, so it is added to the
83829
84425
  // catalog here rather than integrated into the target below.
@@ -84401,7 +84997,7 @@ ${svg}
84401
84997
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
84402
84998
  }
84403
84999
 
84404
- var version = "0.7.69";
85000
+ var version = "0.7.71";
84405
85001
 
84406
85002
  // Parse command line args into commands and run them
84407
85003
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -85216,7 +85812,7 @@ ${svg}
85216
85812
  // parse. Null is what every reader already falls back from.
85217
85813
  function measureLabelText(rec) {
85218
85814
  var text = toLabelString(rec && rec['label-text']);
85219
- var font, fontSize, spacing, width;
85815
+ var font, fontSize, spacing, width, failed = false;
85220
85816
  if (!text) return null;
85221
85817
  font = getFontForRecord(rec);
85222
85818
  if (!font) return null;
@@ -85225,10 +85821,22 @@ ${svg}
85225
85821
  if (!(fontSize > 0)) return null;
85226
85822
  // The widest line, which is what the block of a multi-line label is as wide
85227
85823
  // as, and what the browser's getBBox() reports for the same text.
85228
- width = splitLabelLines(text).reduce(function(max, line) {
85229
- var w = measureLine(font, line, fontSize, spacing);
85824
+ //
85825
+ // A line with bold words in it is measured a run at a time, each in its own
85826
+ // face, which loses the kerning between the last letter of one run and the
85827
+ // first of the next -- a fraction of a pixel at a weight change.
85828
+ width = splitLabelLineRuns(text).reduce(function(max, runs) {
85829
+ var w = runs.reduce(function(sum, run) {
85830
+ var face = run.bold ? getFontForRecord(rec, true) : font;
85831
+ if (!face) {
85832
+ failed = true;
85833
+ return sum;
85834
+ }
85835
+ return sum + measureLine(face, run.text, fontSize, spacing);
85836
+ }, 0);
85230
85837
  return w > max ? w : max;
85231
85838
  }, 0);
85839
+ if (failed) return null;
85232
85840
  return width > 0 ? width : null;
85233
85841
  }
85234
85842
 
@@ -85252,9 +85860,12 @@ ${svg}
85252
85860
  // The font a record is drawn in, opened and remembered. A record with no
85253
85861
  // font-family is drawn in the layer group's default, the same one the GUI
85254
85862
  // measures against -- see getLabelTextDefaults().
85255
- function getFontForRecord(rec) {
85863
+ //
85864
+ // @bold: the face its bold runs are drawn in, which is the keyword's 700
85865
+ // whatever the label's own weight is, as it is in CSS.
85866
+ function getFontForRecord(rec, bold) {
85256
85867
  var family = rec['font-family'] || 'sans-serif';
85257
- var weight = getFontWeight(rec);
85868
+ var weight = bold ? WEIGHT_NAMES[LABEL_BOLD_WEIGHT] : getFontWeight(rec);
85258
85869
  var italic = isItalic(rec);
85259
85870
  var stretch = rec['font-stretch'] || '';
85260
85871
  var key = [family, weight, italic ? 'i' : 'n', stretch].join('|');
@@ -86994,6 +87605,257 @@ ${svg}
86994
87605
  restoreCapturedUnits: restoreCapturedUnits
86995
87606
  });
86996
87607
 
87608
+ // Smooths a freehand ("pencil") stroke with a Gaussian kernel while it is
87609
+ // being drawn, placing vertices one at a time, a little behind the pointer.
87610
+ // Vertices never move once they are placed.
87611
+ //
87612
+ // The trace of the pointer samples is densified to even spacing and smoothed
87613
+ // with the Gaussian kernel that -smooth uses, as a plain weighted average in
87614
+ // arc length. (-smooth adds a quadratic correction that keeps bends from being
87615
+ // flattened, but it lets through far more of the jitter of a slow stroke.) A
87616
+ // smoothed point depends only on the trace within a window around it, so it
87617
+ // is final as soon as the trace extends a window radius beyond it. At the
87618
+ // start of the stroke the trace is extended by an odd reflection about its
87619
+ // first point, which keeps the smoothed curve starting exactly there, and the
87620
+ // end is treated the same way when the stroke is finished.
87621
+ //
87622
+ // Vertices are kept from the smoothed points where the curve has turned by a
87623
+ // given angle since the last vertex, or where the chord from the last vertex
87624
+ // would stray too far from a gentle curve -- the rule -smooth uses to space
87625
+ // its output. So vertices are close together at bends and far apart on
87626
+ // straight stretches, and they appear one at a time.
87627
+ //
87628
+ // Distances are in pixels. For samples in other units, the pixelSize option
87629
+ // gives the size of a pixel in those units.
87630
+
87631
+ // Standard deviation of the smoothing kernel, in arc length. Below about 5px,
87632
+ // the jitter of a slow stroke comes through; larger values round off bends a
87633
+ // few pixels across.
87634
+ var SIGMA = 8;
87635
+
87636
+ // Half-width of the smoothing window, in multiples of SIGMA. This is also how
87637
+ // far the pointer has to be ahead of a smoothed point for it to be final.
87638
+ var WINDOW_RADIUS = 3;
87639
+
87640
+ // Spacing of the densified trace, and of the smoothed points that are tested
87641
+ // as vertices. Small enough that a bend of radius SIGMA turns by only a few
87642
+ // degrees per step.
87643
+ var SOURCE_STEP = 0.5;
87644
+ var OUTPUT_STEP = 0.5;
87645
+
87646
+ // Greatest turn between consecutive segments of the output, and greatest
87647
+ // distance of the output from the smoothed curve along gentle bends.
87648
+ var BEND_ANGLE = 12;
87649
+ var MAX_DEVIATION = 0.75;
87650
+
87651
+ // Spacing of the points returned by getPreview(), and the standard deviation
87652
+ // of the kernel that smooths the preview at the pointer. The preview is
87653
+ // smoothed more heavily further from the pointer, up to SIGMA where it meets
87654
+ // the placed vertices, so that it runs into them without a step. A small
87655
+ // kernel at the pointer removes pixel jitter without lagging behind.
87656
+ var PREVIEW_SPACING = 2;
87657
+ var PREVIEW_SIGMA = 2;
87658
+
87659
+ // start: [x, y] first sample
87660
+ // opts: (optional) {sigma, previewSigma, bendAngle (degrees), maxDeviation, pixelSize}
87661
+ function GaussianStrokeFitter(start, opts) {
87662
+ var px = opts && opts.pixelSize > 0 ? opts.pixelSize : 1;
87663
+ var sigma = getOpt(opts, 'sigma', SIGMA) * px;
87664
+ var radius = sigma * WINDOW_RADIUS;
87665
+ var previewSigma = Math.min(getOpt(opts, 'previewSigma', PREVIEW_SIGMA) * px, sigma);
87666
+ var step = SOURCE_STEP * px;
87667
+ var outputStep = OUTPUT_STEP * px;
87668
+ var decimator = new BendDecimator(start, getOpt(opts, 'bendAngle', BEND_ANGLE) * Math.PI / 180,
87669
+ getOpt(opts, 'maxDeviation', MAX_DEVIATION) * px);
87670
+ var samples = [[start[0], start[1]]];
87671
+ // the densified trace: arc length and coordinates
87672
+ var tt = [0], xx = [start[0]], yy = [start[1]];
87673
+ var traceLen = 0; // length of the densified part of the trace
87674
+ var nextT = step; // arc length of the next densified point
87675
+ var phi = outputStep; // arc length of the next smoothed point
87676
+
87677
+ // Returns the vertices placed by the new sample, which may be none.
87678
+ this.addSample = function(p) {
87679
+ var n = samples.length;
87680
+ if (!isSample(p) || samePoint(p, samples[n - 1])) return [];
87681
+ samples.push([p[0], p[1]]);
87682
+ densifySegment(n - 1);
87683
+ return placeSmoothedPoints(traceLen - radius, false);
87684
+ };
87685
+
87686
+ // Returns the vertices that complete the stroke, ending at the last sample.
87687
+ this.finish = function() {
87688
+ var n = samples.length, end, placed, p;
87689
+ if (n < 2) return [];
87690
+ end = samples[n - 1];
87691
+ if (tt[tt.length - 1] < traceLen) {
87692
+ tt.push(traceLen);
87693
+ xx.push(end[0]);
87694
+ yy.push(end[1]);
87695
+ }
87696
+ placed = placeSmoothedPoints(traceLen - outputStep / 2, true);
87697
+ p = decimator.push(end);
87698
+ if (p) placed.push(p);
87699
+ placed.push([end[0], end[1]]);
87700
+ return placed;
87701
+ };
87702
+
87703
+ // Returns points to show between the last placed vertex and the pointer
87704
+ // (not including either): the latest smoothed point, followed by the rest of
87705
+ // the trace, smoothed less and less towards the pointer (see PREVIEW_SIGMA).
87706
+ // These points are not final.
87707
+ this.getPreview = function() {
87708
+ var cand = decimator.getCandidate();
87709
+ var t0 = cand ? phi - outputStep : 0; // arc length of the candidate
87710
+ var spacing = PREVIEW_SPACING * px;
87711
+ var out = cand ? [cand] : [];
87712
+ var t, s;
87713
+ for (t = t0 + spacing; t < traceLen - spacing / 2; t += spacing) {
87714
+ s = previewSigma + (sigma - previewSigma) * (traceLen - t) / (traceLen - t0);
87715
+ out.push(smoothAt(t, true, s));
87716
+ }
87717
+ return out;
87718
+ };
87719
+
87720
+ // Smooths the trace up to arc length @maxT and returns the vertices placed
87721
+ // from the smoothed points.
87722
+ function placeSmoothedPoints(maxT, atEnd) {
87723
+ var placed = [], p;
87724
+ for (; phi <= maxT; phi += outputStep) {
87725
+ p = decimator.push(smoothAt(phi, atEnd));
87726
+ if (p) placed.push(p);
87727
+ }
87728
+ return placed;
87729
+ }
87730
+
87731
+ // atEnd: treat the last sample as the end of the stroke
87732
+ // s: (optional) standard deviation of the kernel, if not sigma
87733
+ function smoothAt(t, atEnd, s) {
87734
+ var r = s ? s * WINDOW_RADIUS : radius,
87735
+ n = tt.length,
87736
+ lo = firstIndexAbove(t - r),
87737
+ hi = firstIndexAbove(t + r),
87738
+ end = samples[samples.length - 1],
87739
+ wt = [], wx = [], wy = [], i;
87740
+ // odd reflection of the start of the trace
87741
+ for (i = Math.min(firstIndexAbove(r - t), n) - 1; i > 0; i--) {
87742
+ wt.push(-tt[i]);
87743
+ wx.push(2 * xx[0] - xx[i]);
87744
+ wy.push(2 * yy[0] - yy[i]);
87745
+ }
87746
+ for (i = lo; i < hi; i++) {
87747
+ wt.push(tt[i]);
87748
+ wx.push(xx[i]);
87749
+ wy.push(yy[i]);
87750
+ }
87751
+ if (atEnd) {
87752
+ // the trace stops short of the last sample until finish() adds it
87753
+ if (tt[n - 1] < traceLen) {
87754
+ wt.push(traceLen);
87755
+ wx.push(end[0]);
87756
+ wy.push(end[1]);
87757
+ }
87758
+ // odd reflection of the end of the trace
87759
+ for (i = n - 1; i >= 0 && 2 * traceLen - tt[i] < t + r; i--) {
87760
+ if (tt[i] >= traceLen) continue;
87761
+ wt.push(2 * traceLen - tt[i]);
87762
+ wx.push(2 * end[0] - xx[i]);
87763
+ wy.push(2 * end[1] - yy[i]);
87764
+ }
87765
+ }
87766
+ return smoothPoint(wt, [wx, wy], 0, wt.length, t, 'gaussian', s || sigma, r, 0);
87767
+ }
87768
+
87769
+ // first index of the trace with arc length > @t
87770
+ function firstIndexAbove(t) {
87771
+ var lo = 0, hi = tt.length, mid;
87772
+ while (lo < hi) {
87773
+ mid = (lo + hi) >> 1;
87774
+ if (tt[mid] <= t) lo = mid + 1;
87775
+ else hi = mid;
87776
+ }
87777
+ return lo;
87778
+ }
87779
+
87780
+ // Adds points to the trace at even spacing from sample @i to the next one.
87781
+ function densifySegment(i) {
87782
+ var a = samples[i],
87783
+ b = samples[i + 1],
87784
+ len = distance(a, b),
87785
+ f;
87786
+ for (; nextT <= traceLen + len; nextT += step) {
87787
+ f = (nextT - traceLen) / len;
87788
+ tt.push(nextT);
87789
+ xx.push(a[0] + (b[0] - a[0]) * f);
87790
+ yy.push(a[1] + (b[1] - a[1]) * f);
87791
+ }
87792
+ traceLen += len;
87793
+ }
87794
+ }
87795
+
87796
+ // Picks vertices from a stream of closely spaced points along a smooth curve,
87797
+ // keeping a point once the curve has turned by @bendAngle (radians) since the
87798
+ // last kept point, or once the chord from the last kept point would bow more
87799
+ // than @maxDeviation from the curve (estimated as for a circular arc). This is
87800
+ // the rule -smooth uses (decimateByBend() in mapshaper-smooth-algos.mjs),
87801
+ // applied one point at a time.
87802
+ function BendDecimator(start, bendAngle, maxDeviation) {
87803
+ var kept = start, prev = start, candidate = null, turn = 0;
87804
+
87805
+ // Returns the point before @p if it is kept, or null.
87806
+ this.push = function(p) {
87807
+ var out = null;
87808
+ if (candidate) {
87809
+ turn += getTurn(prev, candidate, p);
87810
+ if (turn >= bendAngle || distance(kept, p) * turn / 8 >= maxDeviation) {
87811
+ out = kept = candidate;
87812
+ turn = 0;
87813
+ }
87814
+ prev = candidate;
87815
+ }
87816
+ candidate = p;
87817
+ return out;
87818
+ };
87819
+
87820
+ // The latest point, which has not been kept yet
87821
+ this.getCandidate = function() {
87822
+ return candidate;
87823
+ };
87824
+ }
87825
+
87826
+ function getTurn(a, b, c) {
87827
+ var ux = b[0] - a[0], uy = b[1] - a[1],
87828
+ vx = c[0] - b[0], vy = c[1] - b[1],
87829
+ len = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy)),
87830
+ cos;
87831
+ if (!(len > 0)) return 0;
87832
+ cos = (ux * vx + uy * vy) / len;
87833
+ return Math.acos(Math.min(1, Math.max(-1, cos)));
87834
+ }
87835
+
87836
+ function getOpt(opts, name, defaultVal) {
87837
+ return opts && opts[name] > 0 ? opts[name] : defaultVal;
87838
+ }
87839
+
87840
+ function isSample(p) {
87841
+ return !!p && isFinite(p[0]) && isFinite(p[1]);
87842
+ }
87843
+
87844
+ function samePoint(a, b) {
87845
+ return a[0] == b[0] && a[1] == b[1];
87846
+ }
87847
+
87848
+ function distance(a, b) {
87849
+ var dx = b[0] - a[0], dy = b[1] - a[1];
87850
+ return Math.sqrt(dx * dx + dy * dy);
87851
+ }
87852
+
87853
+ var PencilGaussian = /*#__PURE__*/Object.freeze({
87854
+ __proto__: null,
87855
+ BendDecimator: BendDecimator,
87856
+ GaussianStrokeFitter: GaussianStrokeFitter
87857
+ });
87858
+
86997
87859
  // Attach functions exported by modules to the "internal" object,
86998
87860
  // so they can be run by tests and by the GUI.
86999
87861
  // TODO: rewrite tests to import functions directly from modules,
@@ -87003,7 +87865,7 @@ ${svg}
87003
87865
  internal.svg = Object.assign({}, SvgStringify, SvgPathUtils, GeojsonToSvg,
87004
87866
  SvgFeatureUtils,
87005
87867
  SvgLabels, SvgSymbols, SvgLabelPaths, SvgLabelFit, SvgLabelAlign,
87006
- SvgLabelMetrics, SvgLabelHalo, SvgLabelCallout);
87868
+ SvgLabelMetrics, SvgLabelHalo, SvgLabelCallout, SvgLabelMarkup, SvgLineArrows);
87007
87869
 
87008
87870
  // Reached through the bundle rather than imported from source, unlike most of
87009
87871
  // what tests use, because these modules load fs and fontkit through the
@@ -87128,6 +87990,7 @@ ${svg}
87128
87990
  PathExport,
87129
87991
  Pathfinder,
87130
87992
  PathfinderUtils,
87993
+ PencilGaussian,
87131
87994
  PathImport,
87132
87995
  PathRepair,
87133
87996
  PathUtils,