mapshaper 0.7.69 → 0.7.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mapshaper.js CHANGED
@@ -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,
@@ -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
 
@@ -26388,7 +26388,10 @@
26388
26388
  function makeSVGPatternFill(str, id) {
26389
26389
  var o = parsePattern(str);
26390
26390
  var svg;
26391
- if (!o) return null;
26391
+ if (!o) {
26392
+ message('Invalid pattern, ignoring:', str);
26393
+ return null;
26394
+ }
26392
26395
  if (o.type == 'hatches') {
26393
26396
  svg = makeHatchPatternSVG(o);
26394
26397
  } else if (o.type == 'dots' || o.type == 'squares') {
@@ -27383,6 +27386,126 @@
27383
27386
  splitLabelHalos: splitLabelHalos
27384
27387
  });
27385
27388
 
27389
+ // Bold words inside a label: label-text='Israel <b>restricted access</b> here'.
27390
+ //
27391
+ // The markup is part of the text rather than a column beside it, so that the
27392
+ // words and their weight cannot drift apart: an expression, a join or a -labels
27393
+ // text= that rewrites the text rewrites the bold with it. The GUI editor never
27394
+ // shows the tags -- it reads them into ranges and writes them back.
27395
+ //
27396
+ // Only a matched <b>...</b> pair with no other b tag inside it is markup.
27397
+ // Anything else -- an unclosed <b>, a stray </b>, "a < b" -- is text, and is
27398
+ // drawn as typed.
27399
+ //
27400
+ // See docs/development/label-tool-design.md.
27401
+
27402
+ var BOLD_PAIR_RXP = /<b>((?:(?!<\/?b>)[\s\S])*?)<\/b>/gi;
27403
+ var BOLD_TAG_RXP = /<\/?b>/i;
27404
+
27405
+ // The font-weight a bold run is drawn with: the keyword, which a browser and
27406
+ // the Node text measurer both resolve to the family's own bold face.
27407
+ var LABEL_BOLD_WEIGHT = 'bold';
27408
+
27409
+ // @str -> {text, bold}: the text with its bold pairs removed, and the bold
27410
+ // stretches as ascending, non-overlapping [start, end) offsets into it.
27411
+ function parseBoldMarkup(str) {
27412
+ var s = str === null || str === undefined ? '' : String(str);
27413
+ var text = '', bold = [], last = 0, m;
27414
+ if (!BOLD_TAG_RXP.test(s)) return {text: s, bold: bold};
27415
+ BOLD_PAIR_RXP.lastIndex = 0;
27416
+ while ((m = BOLD_PAIR_RXP.exec(s))) {
27417
+ text += s.substring(last, m.index);
27418
+ if (m[1]) {
27419
+ bold.push([text.length, text.length + m[1].length]);
27420
+ text += m[1];
27421
+ }
27422
+ last = m.index + m[0].length;
27423
+ }
27424
+ text += s.substring(last);
27425
+ return {text: text, bold: normalizeBoldRanges(bold)};
27426
+ }
27427
+
27428
+ function stripBoldMarkup(str) {
27429
+ return parseBoldMarkup(str).text;
27430
+ }
27431
+
27432
+ // Sorted, clipped to non-empty, with touching and overlapping ranges merged.
27433
+ // Returns a new array.
27434
+ function normalizeBoldRanges(ranges) {
27435
+ var sorted = (ranges || []).filter(function(r) {
27436
+ return r && r[1] > r[0];
27437
+ }).map(function(r) {
27438
+ return [r[0], r[1]];
27439
+ }).sort(function(a, b) {
27440
+ return a[0] - b[0];
27441
+ });
27442
+ var out = [];
27443
+ sorted.forEach(function(r) {
27444
+ var prev = out[out.length - 1];
27445
+ if (prev && r[0] <= prev[1]) {
27446
+ prev[1] = Math.max(prev[1], r[1]);
27447
+ } else {
27448
+ out.push(r);
27449
+ }
27450
+ });
27451
+ return out;
27452
+ }
27453
+
27454
+ // The characters [start, end) of @text as runs of one weight:
27455
+ // [{text, bold}, ...], with nothing for an empty stretch.
27456
+ function getBoldRuns(text, bold, start, end) {
27457
+ var runs = [], pos = start, i, r, a, b;
27458
+ for (i = 0; i < bold.length && pos < end; i++) {
27459
+ r = bold[i];
27460
+ a = Math.max(r[0], pos);
27461
+ b = Math.min(r[1], end);
27462
+ if (b <= a) continue;
27463
+ if (a > pos) runs.push({text: text.substring(pos, a), bold: false});
27464
+ runs.push({text: text.substring(a, b), bold: true});
27465
+ pos = b;
27466
+ }
27467
+ if (pos < end) runs.push({text: text.substring(pos, end), bold: false});
27468
+ return runs;
27469
+ }
27470
+
27471
+ // @runs -> @obj with the first plain run as its value and the rest as <tspan>s,
27472
+ // which is the shape the stringifier writes as mixed content. A label with no
27473
+ // bold in it comes out exactly as it did before bold existed: one value and no
27474
+ // children. Returns @obj.
27475
+ function applyRunsToSvgObject(obj, runs) {
27476
+ var i = 0;
27477
+ var children = [];
27478
+ if (runs.length > 0 && !runs[0].bold) {
27479
+ obj.value = runs[0].text;
27480
+ i = 1;
27481
+ } else {
27482
+ obj.value = '';
27483
+ }
27484
+ for (; i < runs.length; i++) {
27485
+ children.push(renderRun(runs[i]));
27486
+ }
27487
+ if (children.length > 0) {
27488
+ obj.children = children.concat(obj.children || []);
27489
+ }
27490
+ return obj;
27491
+ }
27492
+
27493
+ function renderRun(run) {
27494
+ var o = {tag: 'tspan', value: run.text};
27495
+ if (run.bold) o.properties = {'font-weight': LABEL_BOLD_WEIGHT};
27496
+ return o;
27497
+ }
27498
+
27499
+ var SvgLabelMarkup = /*#__PURE__*/Object.freeze({
27500
+ __proto__: null,
27501
+ LABEL_BOLD_WEIGHT: LABEL_BOLD_WEIGHT,
27502
+ applyRunsToSvgObject: applyRunsToSvgObject,
27503
+ getBoldRuns: getBoldRuns,
27504
+ normalizeBoldRanges: normalizeBoldRanges,
27505
+ parseBoldMarkup: parseBoldMarkup,
27506
+ stripBoldMarkup: stripBoldMarkup
27507
+ });
27508
+
27386
27509
  // Accepting \n (two chars) as an alternative to the newline character
27387
27510
  // (sometimes, '\n' is not converted to newline, e.g. in a Makefile)
27388
27511
  // Also accepting <br>
@@ -27399,21 +27522,45 @@
27399
27522
  var anyLabelBreakRxp = /(\n|\\n|<br>|<wbr>)/i;
27400
27523
  var softBreakRxp = /^<wbr>$/i;
27401
27524
 
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.
27525
+ // The lines a label's text is drawn as, without its bold markup.
27405
27526
  function splitLabelLines(text) {
27406
- var parts = String(text).split(anyLabelBreakRxp);
27407
- var lines = [parts[0]];
27527
+ return splitLabelLineRuns(text).map(function(runs) {
27528
+ return runs.map(function(run) { return run.text; }).join('');
27529
+ });
27530
+ }
27531
+
27532
+ // The lines a label's text is drawn as, each one a list of runs of one weight:
27533
+ // [[{text, bold}, ...], ...]. A bold stretch can cross a line break, and is
27534
+ // then a run on each line.
27535
+ //
27536
+ // Whitespace before a soft break is dropped: a trailing space would move an
27537
+ // end- or middle-justified line by a space's width.
27538
+ function splitLabelLineRuns(text) {
27539
+ var o = parseBoldMarkup(String(text));
27540
+ var parts = o.text.split(anyLabelBreakRxp);
27541
+ var pos = parts[0].length;
27542
+ var lines = [getBoldRuns(o.text, o.bold, 0, pos)];
27408
27543
  for (var i = 1; i < parts.length; i += 2) {
27409
27544
  if (softBreakRxp.test(parts[i])) {
27410
- lines[lines.length - 1] = lines[lines.length - 1].replace(/\s+$/, '');
27545
+ trimTrailingSpace(lines[lines.length - 1]);
27411
27546
  }
27412
- lines.push(parts[i + 1]);
27547
+ pos += parts[i].length;
27548
+ lines.push(getBoldRuns(o.text, o.bold, pos, pos + parts[i + 1].length));
27549
+ pos += parts[i + 1].length;
27413
27550
  }
27414
27551
  return lines;
27415
27552
  }
27416
27553
 
27554
+ function trimTrailingSpace(runs) {
27555
+ var last;
27556
+ while (runs.length > 0) {
27557
+ last = runs[runs.length - 1];
27558
+ last.text = last.text.replace(/\s+$/, '');
27559
+ if (last.text) return;
27560
+ runs.pop();
27561
+ }
27562
+ }
27563
+
27417
27564
  function removeSoftBreaks(text) {
27418
27565
  return String(text).replace(/<wbr>/gi, '');
27419
27566
  }
@@ -27464,7 +27611,7 @@
27464
27611
  // already been through it, so calling it here as well costs nothing and means
27465
27612
  // every way into the renderer draws a label in the position it is stored in.
27466
27613
  var rec = resolveLabelPosition(recArg);
27467
- var morelines = splitLabelLines(toLabelString(rec['label-text']));
27614
+ var morelines = splitLabelLineRuns(toLabelString(rec['label-text']));
27468
27615
  var line = morelines.shift();
27469
27616
  var obj;
27470
27617
  var dx = applyAlignmentShift(rec);
@@ -27475,23 +27622,21 @@
27475
27622
  y: dy,
27476
27623
  x: dx
27477
27624
  };
27478
- obj = {
27625
+ obj = applyRunsToSvgObject({
27479
27626
  tag: 'text',
27480
- value: line,
27481
27627
  properties: properties
27482
- };
27628
+ }, line);
27483
27629
  if (morelines.length > 0) {
27484
27630
  // multiline label
27485
- obj.children = [];
27631
+ obj.children = obj.children || [];
27486
27632
  morelines.forEach(function(line) {
27487
- var tspan = {
27633
+ var tspan = applyRunsToSvgObject({
27488
27634
  tag: 'tspan',
27489
- value: line,
27490
27635
  properties: {
27491
27636
  x: dx,
27492
27637
  dy: getLineHeightDy(rec['line-height'])
27493
27638
  }
27494
- };
27639
+ }, line);
27495
27640
  obj.children.push(tspan);
27496
27641
  });
27497
27642
  }
@@ -27591,6 +27736,7 @@
27591
27736
  removeSoftBreaks: removeSoftBreaks,
27592
27737
  renderLabel: renderLabel$1,
27593
27738
  renderStyledLabel: renderStyledLabel,
27739
+ splitLabelLineRuns: splitLabelLineRuns,
27594
27740
  splitLabelLines: splitLabelLines,
27595
27741
  toLabelString: toLabelString,
27596
27742
  toPixels: toPixels
@@ -29627,16 +29773,14 @@
29627
29773
  // a <tspan> inside a <textPath> advances along the path instead of
29628
29774
  // stacking below it, so the lines are joined rather than dropping
29629
29775
  // everything after the first one
29630
- text = text.split(labelNewlineRxp).join(' ');
29631
29776
  addToReport(report, 'joined', id);
29632
29777
  }
29633
- textPath = {
29778
+ textPath = applyRunsToSvgObject({
29634
29779
  tag: 'textPath',
29635
- value: text,
29636
29780
  properties: {
29637
29781
  startOffset: rec['label-start-offset'] || getDefaultStartOffset(rec)
29638
29782
  }
29639
- };
29783
+ }, joinLineRuns(splitLabelLineRuns(text)));
29640
29784
  // the caller replaces this with a reference to a <defs> entry
29641
29785
  textPath.properties[LABEL_PATH_PROPERTY] = d;
29642
29786
  if (rec['label-side']) {
@@ -29700,6 +29844,28 @@
29700
29844
  return '50%';
29701
29845
  }
29702
29846
 
29847
+ // A path label's lines as one line of runs, with a space for each break. The
29848
+ // space takes the weight of the text before it, and runs of one weight that
29849
+ // the join brings together are merged, so that text with no bold in it is a
29850
+ // single run.
29851
+ function joinLineRuns(lines) {
29852
+ var out = [];
29853
+ lines.forEach(function(runs, i) {
29854
+ if (i > 0) appendRun(out, {text: ' ', bold: out.length > 0 && out[out.length - 1].bold});
29855
+ runs.forEach(function(run) { appendRun(out, run); });
29856
+ });
29857
+ return out;
29858
+ }
29859
+
29860
+ function appendRun(runs, run) {
29861
+ var last = runs[runs.length - 1];
29862
+ if (last && last.bold == run.bold) {
29863
+ last.text += run.text;
29864
+ } else {
29865
+ runs.push({text: run.text, bold: run.bold});
29866
+ }
29867
+ }
29868
+
29703
29869
  function addToReport(report, key, id) {
29704
29870
  if (report && report[key]) report[key].push(id);
29705
29871
  }
@@ -40716,6 +40882,22 @@ ${svg}
40716
40882
  }
40717
40883
  }
40718
40884
 
40885
+ // -labels adds a label when given coordinates=, and styles the target's labels
40886
+ // otherwise. Options that belong to one of these are refused in the other,
40887
+ // rather than ignored.
40888
+ function validateLabelsOpts(cmd) {
40889
+ var o = cmd.options;
40890
+ if ('coordinates' in o) {
40891
+ if (o.where || o.ids) {
40892
+ stop$1('where= and ids= select labels to style, and can not be used with coordinates=');
40893
+ }
40894
+ } else if (o.properties) {
40895
+ stop$1('properties= requires coordinates=');
40896
+ } else if ('name' in o && !o.no_replace) {
40897
+ stop$1('name= requires coordinates= or +');
40898
+ }
40899
+ }
40900
+
40719
40901
  function validateExpressionOpt(cmd) {
40720
40902
  if (!cmd.options.expression) {
40721
40903
  error('Command requires a JavaScript expression');
@@ -41696,17 +41878,17 @@ ${svg}
41696
41878
  nameOpt2 = { // for -calc and -info
41697
41879
  describe: 'name the output layer'
41698
41880
  },
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.
41881
+ // label style properties accepted by -labels and -add-label. -style
41882
+ // accepts these too, for backward compatibility, but leaves them out of
41883
+ // its help.
41702
41884
  labelStyleOpts = {
41703
- 'font-family': {describe: 'label font, e.g. Georgia'},
41704
- 'font-size': {describe: 'label font size, e.g. 14'},
41885
+ 'font-family': {describe: 'CSS font family, e.g. Georgia (default is sans-serif)'},
41886
+ 'font-size': {describe: 'font size (default is 12)'},
41705
41887
  'font-style': {describe: 'normal or italic'},
41706
41888
  'font-weight': {describe: 'normal, bold or a numeric weight'},
41707
41889
  'font-stretch': {describe: 'e.g. condensed'},
41708
41890
  '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'},
41891
+ '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
41892
  'text-anchor': {describe: 'start, middle or end'},
41711
41893
  'label-align': {describe: 'how the lines of a multi-line label align: left, center or right'},
41712
41894
  'dominant-baseline': {describe: 'vertical alignment, e.g. central'},
@@ -41717,23 +41899,30 @@ ${svg}
41717
41899
  dy: {describe: 'vertical offset from the anchor'},
41718
41900
  fill: {describe: 'text color'},
41719
41901
  opacity: {describe: 'text opacity'},
41720
- 'halo-width': {describe: 'width of a halo around the text in px'},
41902
+ 'halo-width': {describe: 'width of a halo around the text in px (default is 0, no halo)'},
41721
41903
  'halo-color': {describe: 'halo color (default is white)'},
41722
41904
  'halo-opacity': {describe: 'halo opacity, 0-1'},
41723
- 'label-width': {describe: 'width of a fixed-width text block in px'},
41905
+ 'label-width': {describe: 'width of a fixed-width text block in px (lines are wrapped in the web UI)'},
41724
41906
  callout: {describe: 'line from the anchor to the text: line, elbow or curve'},
41725
41907
  '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'},
41908
+ 'callout-end-size': {describe: 'length of an arrowhead\'s sides in px (default is 10)'},
41727
41909
  'callout-via': {describe: 'x,y of an elbow\'s corner or a point on a curve, in px'},
41728
41910
  'callout-attach': {describe: 'where the callout meets the text, as x,y fractions'},
41729
41911
  'callout-gap': {describe: 'px between the callout and the anchor'},
41730
- 'callout-padding': {describe: 'px between the callout and the text'},
41912
+ 'callout-padding': {describe: 'px between the callout and the text (default is 3)'},
41731
41913
  'callout-color': {describe: 'callout color (defaults to the text color)'},
41732
- 'callout-width': {describe: 'callout line width in px'},
41914
+ 'callout-width': {describe: 'callout line width in px (default is 1)'},
41733
41915
  'callout-opacity': {describe: 'callout opacity, 0-1'},
41734
41916
  css: {describe: 'inline css style'},
41735
41917
  class: {describe: 'name of CSS class or classes (space-separated)'}
41736
41918
  },
41919
+ // a label can carry a symbol at its anchor
41920
+ labelIconOpts = {
41921
+ icon: {describe: 'symbol drawn at the label anchor: circle, square, ring, star'},
41922
+ 'icon-size': {describe: 'size of the anchor symbol in px'},
41923
+ 'icon-color': {describe: 'color of the anchor symbol (defaults to the text color)'},
41924
+ 'icon-opacity': {describe: 'opacity of the anchor symbol, 0-1'}
41925
+ },
41737
41926
  noReplaceOpt2 = { // for -calc and -info
41738
41927
  alias: '+',
41739
41928
  type: 'flag',
@@ -41813,6 +42002,18 @@ ${svg}
41813
42002
  type: 'distance'
41814
42003
  };
41815
42004
 
42005
+ // The label options -style used to document, before -labels took them over.
42006
+ // fill, opacity, css and class are left out because -style documents them
42007
+ // for every kind of feature.
42008
+ function getHiddenLabelStyleOpts() {
42009
+ var shared = ['fill', 'opacity', 'css', 'class'];
42010
+ var opts = {'label-text': {}};
42011
+ Object.keys(labelStyleOpts).forEach(function(name) {
42012
+ if (shared.indexOf(name) == -1) opts[name] = {};
42013
+ });
42014
+ return opts;
42015
+ }
42016
+
41816
42017
  var parser = new CommandParser();
41817
42018
  parser.usage('Usage: mapshaper -<command> [options] ...');
41818
42019
 
@@ -43181,6 +43382,32 @@ ${svg}
43181
43382
  .option('encoding', encodingOpt)
43182
43383
  .option('target', targetOpt);
43183
43384
 
43385
+ parser.command('labels')
43386
+ .describe('style labels, convert points to labels or add a label')
43387
+ .validate(validateLabelsOpts)
43388
+ .option('text', {
43389
+ describe: 'label text (a field, JS expression or literal value)'
43390
+ })
43391
+ .option('label-text', {
43392
+ alias_to: 'text'
43393
+ })
43394
+ .option('coordinates', {
43395
+ describe: 'add a label at x,y, or a path label along x,y,x,y,...'
43396
+ })
43397
+ .option('where', whereOpt)
43398
+ .option('ids', {
43399
+ describe: 'comma-sep. list of ids of the labels to style',
43400
+ type: 'numbers'
43401
+ })
43402
+ .options(labelStyleOpts)
43403
+ .options(labelIconOpts)
43404
+ .option('properties', {
43405
+ describe: 'with coordinates=, other attributes as a JSON object'
43406
+ })
43407
+ .option('name', nameOpt)
43408
+ .option('no-replace', noReplaceOpt)
43409
+ .option('target', targetOpt);
43410
+
43184
43411
  parser.command('lines')
43185
43412
  .describe('convert a polygon or point layer to a polyline layer')
43186
43413
  .option('fields', {
@@ -43744,99 +43971,11 @@ ${svg}
43744
43971
  describe: 'point icon color (defaults to fill color, then black)'
43745
43972
  })
43746
43973
  .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)'
43781
- })
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'
43974
+ describe: 'point icon opacity, 0-1'
43820
43975
  })
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);
43976
+ // deprecated in favor of -labels; accepted, but left out of the help
43977
+ .options(getHiddenLabelStyleOpts())
43978
+ .option('target', targetOpt);
43840
43979
 
43841
43980
  parser.command('repel')
43842
43981
  .describe('move overlapping point symbols apart')
@@ -44082,23 +44221,10 @@ ${svg}
44082
44221
  .option('text', {
44083
44222
  describe: 'label text'
44084
44223
  })
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
44224
+ // superseded by -labels coordinates=; kept so that older session
44225
+ // histories still run
44087
44226
  .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
- })
44227
+ .options(labelIconOpts)
44102
44228
  .option('properties', {
44103
44229
  describe: 'other attributes, as a JSON object'
44104
44230
  })
@@ -51395,6 +51521,621 @@ ${svg}
51395
51521
  return d;
51396
51522
  }
51397
51523
 
51524
+ // Insert cutting points in arcs, where bbox intersects other shapes
51525
+ // Return a polygon layer containing the bounding box vectors, divided at cutting points.
51526
+ function divideDatasetByBBox(dataset, bbox) {
51527
+ var arcs = dataset.arcs;
51528
+ var data = findBBoxCutPoints(arcs, bbox);
51529
+ var map = insertCutPoints(data.cutPoints, arcs);
51530
+ arcs.dedupCoords();
51531
+ remapDividedArcs(dataset, map);
51532
+ // merge bbox dataset with target dataset,
51533
+ // so arcs are shared between target layers and bbox layer
51534
+ var clipDataset = bboxPointsToClipDataset(data.bboxPoints);
51535
+ var mergedDataset = mergeDatasets([dataset, clipDataset]);
51536
+ // TODO: detect if we need to rebuild topology (unlikely), like with the full clip command
51537
+ // buildTopology(mergedDataset);
51538
+ var clipLyr = mergedDataset.layers.pop();
51539
+ dataset.arcs = mergedDataset.arcs;
51540
+ dataset.layers = mergedDataset.layers;
51541
+ return clipLyr;
51542
+ }
51543
+
51544
+ function bboxPointsToClipDataset(arr) {
51545
+ var arcs = [];
51546
+ var shape = [];
51547
+ var layer = {geometry_type: 'polygon', shapes: [[shape]]};
51548
+ var p1, p2;
51549
+ for (var i=0, n=arr.length - 1; i<n; i++) {
51550
+ p1 = arr[i];
51551
+ p2 = arr[i+1];
51552
+ arcs.push([[p1.x, p1.y], [p2.x, p2.y]]);
51553
+ shape.push(i);
51554
+ }
51555
+ return {
51556
+ arcs: new ArcCollection(arcs),
51557
+ layers: [layer]
51558
+ };
51559
+ }
51560
+
51561
+ function findBBoxCutPoints(arcs, bbox) {
51562
+ var left = bbox[0],
51563
+ bottom = bbox[1],
51564
+ right = bbox[2],
51565
+ top = bbox[3];
51566
+
51567
+ // arrays of intersection points along each bbox edge
51568
+ var tt = [],
51569
+ rr = [],
51570
+ bb = [],
51571
+ ll = [];
51572
+
51573
+ arcs.forEachSegment(function(i, j, xx, yy) {
51574
+ var ax = xx[i],
51575
+ ay = yy[i],
51576
+ bx = xx[j],
51577
+ by = yy[j];
51578
+ var hit;
51579
+ if (segmentOutsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
51580
+ if (segmentInsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
51581
+
51582
+ hit = geom.segmentIntersection(left, top, right, top, ax, ay, bx, by);
51583
+ if (hit) addHit(tt, hit, i, j, xx, yy);
51584
+
51585
+ hit = geom.segmentIntersection(left, bottom, right, bottom, ax, ay, bx, by);
51586
+ if (hit) addHit(bb, hit, i, j, xx, yy);
51587
+
51588
+ hit = geom.segmentIntersection(left, bottom, left, top, ax, ay, bx, by);
51589
+ if (hit) addHit(ll, hit, i, j, xx, yy);
51590
+
51591
+ hit = geom.segmentIntersection(right, bottom, right, top, ax, ay, bx, by);
51592
+ if (hit) addHit(rr, hit, i, j, xx, yy);
51593
+ });
51594
+
51595
+ return {
51596
+ cutPoints: ll.concat(bb, rr, tt),
51597
+ bboxPoints: getDividedBBoxPoints(bbox, ll, tt, rr, bb)
51598
+ };
51599
+
51600
+ function addHit(arr, hit, i, j, xx, yy) {
51601
+ if (!hit) return;
51602
+ arr.push(formatHit(hit[0], hit[1], i, j, xx, yy));
51603
+ if (hit.length == 4) {
51604
+ arr.push(formatHit(hit[2], hit[3], i, j, xx, yy));
51605
+ }
51606
+ }
51607
+
51608
+ function formatHit(x, y, i, j, xx, yy) {
51609
+ var ids = formatIntersectingSegment(x, y, i, j, xx, yy);
51610
+ return getCutPoint(x, y, ids[0], ids[1]);
51611
+ }
51612
+ }
51613
+
51614
+ function segmentOutsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
51615
+ return ax < xmin && bx < xmin || ax > xmax && bx > xmax ||
51616
+ ay < ymin && by < ymin || ay > ymax && by > ymax;
51617
+ }
51618
+
51619
+ function segmentInsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
51620
+ return ax > xmin && bx > xmin && ax < xmax && bx < xmax &&
51621
+ ay > ymin && by > ymin && ay < ymax && by < ymax;
51622
+ }
51623
+
51624
+ // Returns an array of points representing the vertices in
51625
+ // the bbox with cutting points inserted.
51626
+ function getDividedBBoxPoints(bbox, ll, tt, rr, bb) {
51627
+ var bl = {x: bbox[0], y: bbox[1]},
51628
+ tl = {x: bbox[0], y: bbox[3]},
51629
+ tr = {x: bbox[2], y: bbox[3]},
51630
+ br = {x: bbox[2], y: bbox[1]};
51631
+ ll = utils.sortOn(ll.concat([bl, tl]), 'y', true);
51632
+ tt = utils.sortOn(tt.concat([tl, tr]), 'x', true);
51633
+ rr = utils.sortOn(rr.concat([tr, br]), 'y', false);
51634
+ bb = utils.sortOn(bb.concat([br, bl]), 'x', false);
51635
+ return ll.concat(tt, rr, bb).reduce(function(memo, p2) {
51636
+ var p1 = memo.length > 0 ? memo[memo.length-1] : null;
51637
+ if (p1 === null || p1.x != p2.x || p1.y != p2.y) memo.push(p2);
51638
+ return memo;
51639
+ }, []);
51640
+ }
51641
+
51642
+ var Bbox2Clipping = /*#__PURE__*/Object.freeze({
51643
+ __proto__: null,
51644
+ divideDatasetByBBox: divideDatasetByBBox,
51645
+ segmentInsideBBox: segmentInsideBBox,
51646
+ segmentOutsideBBox: segmentOutsideBBox
51647
+ });
51648
+
51649
+ cmd.filterGeom = function(lyr, arcs, opts) {
51650
+ if (!layerHasGeometry$1(lyr)) {
51651
+ stop$1("Layer is missing geometry");
51652
+ }
51653
+ if (opts.bbox) {
51654
+ filterByBoundsIntersection(lyr, arcs, opts);
51655
+ }
51656
+ cmd.filterFeatures(lyr, arcs, {remove_empty: true, verbose: false});
51657
+ };
51658
+
51659
+ function filterByBoundsIntersection(lyr, arcs, opts) {
51660
+ var filter = getBoundsIntersectionFilter(opts.bbox, lyr, arcs);
51661
+ noteLayerWillChange(lyr, {operation: 'filter-geom', unit: 'shapes'});
51662
+ editShapes(lyr.shapes, filter);
51663
+ markLayerChanged(lyr, {operation: 'filter-geom', unit: 'shapes'});
51664
+ }
51665
+
51666
+ function getBoundsIntersectionFilter(bbox, lyr, arcs) {
51667
+ var bounds = new Bounds(bbox);
51668
+ var filter = lyr.geometry_type == 'point' ?
51669
+ getPointInBoundsTest(bounds) :
51670
+ getPathBoundsIntersectionTest(bounds, arcs);
51671
+ return filter;
51672
+ }
51673
+
51674
+ function getPointInBoundsTest(bounds) {
51675
+ return function(xy) {
51676
+ var contains = bounds.containsPoint(xy[0], xy[1]);
51677
+ return contains ? xy : null;
51678
+ };
51679
+ }
51680
+
51681
+ // V1 too-simple test: bounding-box intersection
51682
+ // internal.getPathBoundsIntersectionTest = function(bounds, arcs) {
51683
+ // return function(path) {
51684
+ // return bounds.intersects(arcs.getSimpleShapeBounds(path)) ? path : null;
51685
+ // };
51686
+ // };
51687
+
51688
+ function getPathBoundsIntersectionTest(bounds, arcs) {
51689
+ var bbox = bounds.toArray(),
51690
+ left = bbox[0],
51691
+ bottom = bbox[1],
51692
+ right = bbox[2],
51693
+ top = bbox[3];
51694
+
51695
+ return function(path) {
51696
+ // case: bounding boxes don't intersect -> the path doesn't intersect the box
51697
+ if (!bounds.intersects(arcs.getSimpleShapeBounds(path))) {
51698
+ return null;
51699
+ }
51700
+ var intersects = false;
51701
+ var ax, ay, bx, by;
51702
+ var iter = arcs.getShapeIter(path);
51703
+
51704
+ if (iter.hasNext()) {
51705
+ ax = iter.x;
51706
+ ay = iter.y;
51707
+ }
51708
+ while (iter.hasNext()) {
51709
+ bx = ax;
51710
+ by = ay;
51711
+ ax = iter.x;
51712
+ ay = iter.y;
51713
+ if (segmentOutsideBBox(ax, ay, bx, by, left, bottom, right, top)) continue;
51714
+ if (segmentInsideBBox(ax, ay, bx, by, left, bottom, right, top)) {
51715
+ intersects = true;
51716
+ break;
51717
+ }
51718
+ if (geom.segmentIntersection(left, top, right, top, ax, ay, bx, by) ||
51719
+ geom.segmentIntersection(left, bottom, right, bottom, ax, ay, bx, by) ||
51720
+ geom.segmentIntersection(left, bottom, left, top, ax, ay, bx, by) ||
51721
+ geom.segmentIntersection(right, bottom, right, top, ax, ay, bx, by)) {
51722
+ intersects = true;
51723
+ break;
51724
+ }
51725
+ }
51726
+
51727
+ // case: bbox is entirely inside this ring
51728
+ if (!intersects && geom.testPointInRing(left, bottom, path, arcs)) {
51729
+ intersects = true;
51730
+ }
51731
+ return intersects ? path : null;
51732
+ };
51733
+ }
51734
+
51735
+ // Return a function for testing if a shape (path or point) intersects a bounding box
51736
+ // TODO: move this function to a different file
51737
+ function getBBoxIntersectionTest(bbox, lyr, arcs) {
51738
+ var filter = getBoundsIntersectionFilter(bbox, lyr, arcs);
51739
+ return function(shapeId) {
51740
+ var shp = lyr.shapes[shapeId];
51741
+ if (!shp) return false;
51742
+ for (var i=0; i<shp.length; i++) {
51743
+ if (filter(shp[i])) return true;
51744
+ }
51745
+ return false;
51746
+ };
51747
+ }
51748
+
51749
+ // return array of shape ids
51750
+ function findShapesIntersectingBBox(bbox, lyr, arcs) {
51751
+ var test = getBBoxIntersectionTest(bbox, lyr, arcs);
51752
+ var ids = [];
51753
+ for (var i=0; i<lyr.shapes.length; i++) {
51754
+ if (test(i)) ids.push(i);
51755
+ }
51756
+ return ids;
51757
+ }
51758
+
51759
+ var FilterGeom = /*#__PURE__*/Object.freeze({
51760
+ __proto__: null,
51761
+ findShapesIntersectingBBox: findShapesIntersectingBBox,
51762
+ getBBoxIntersectionTest: getBBoxIntersectionTest
51763
+ });
51764
+
51765
+ cmd.filterFeatures = function(lyr, arcs, opts) {
51766
+ var records = lyr.data ? lyr.data.getRecords() : null,
51767
+ shapes = lyr.shapes || null,
51768
+ n = getFeatureCount(lyr),
51769
+ filteredShapes = shapes ? [] : null,
51770
+ filteredRecords = records ? [] : null,
51771
+ filteredLyr = getOutputLayer(lyr, opts),
51772
+ invert = !!opts.invert,
51773
+ filter;
51774
+
51775
+ if (opts.expression) {
51776
+ filter = compileFeatureExpression(opts.expression, lyr, arcs);
51777
+ }
51778
+
51779
+ if (opts.ids) {
51780
+ filter = combineFilters(filter, getIdFilter(opts.ids));
51781
+ }
51782
+
51783
+ if (opts.remove_empty) {
51784
+ filter = combineFilters(filter, getNullGeometryFilter(lyr, arcs));
51785
+ }
51786
+
51787
+ if (opts.bbox) {
51788
+ filter = combineFilters(filter, getBBoxIntersectionTest(opts.bbox, lyr, arcs));
51789
+ }
51790
+
51791
+ if (!filter) {
51792
+ stop$1("Missing a filter criterion");
51793
+ }
51794
+
51795
+ utils.repeat(n, function(shapeId) {
51796
+ var result = filter(shapeId);
51797
+ requireBooleanResult(result);
51798
+ if (invert) result = !result;
51799
+ if (result === true) {
51800
+ if (shapes) filteredShapes.push(shapes[shapeId] || null);
51801
+ if (records) filteredRecords.push(records[shapeId] || null);
51802
+ }
51803
+ });
51804
+
51805
+ if (filteredLyr == lyr) {
51806
+ noteLayerWillChange(lyr, {operation: 'filter'});
51807
+ }
51808
+ filteredLyr.shapes = filteredShapes;
51809
+ filteredLyr.data = filteredRecords ? new DataTable(filteredRecords) : null;
51810
+ if (filteredLyr == lyr) {
51811
+ markLayerChanged(lyr, {operation: 'filter'});
51812
+ }
51813
+ if (opts.no_replace) {
51814
+ // if adding a layer, don't share objects between source and filtered layer
51815
+ filteredLyr = copyLayer(filteredLyr);
51816
+ }
51817
+
51818
+ if (opts.verbose !== false && !opts.quiet) {
51819
+ message(utils.format('Retained %,d of %,d features', getFeatureCount(filteredLyr), n));
51820
+ }
51821
+
51822
+ return filteredLyr;
51823
+ };
51824
+
51825
+ // TODO: update filter command to use this function
51826
+ function filterLayerInPlace(lyr, filter, invert) {
51827
+ var records = lyr.data ? lyr.data.getRecords() : null,
51828
+ shapes = lyr.shapes || null,
51829
+ n = getFeatureCount(lyr),
51830
+ filteredShapes = shapes ? [] : null,
51831
+ filteredRecords = records ? [] : null;
51832
+ utils.repeat(n, function(shapeId) {
51833
+ var result = filter(shapeId);
51834
+ requireBooleanResult(result);
51835
+ if (result === true) {
51836
+ if (shapes) filteredShapes.push(shapes[shapeId] || null);
51837
+ if (records) filteredRecords.push(records[shapeId] || null);
51838
+ }
51839
+ });
51840
+ noteLayerWillChange(lyr, {operation: 'filterLayerInPlace'});
51841
+ lyr.shapes = filteredShapes;
51842
+ lyr.data = filteredRecords ? new DataTable(filteredRecords) : null;
51843
+ markLayerChanged(lyr, {operation: 'filterLayerInPlace'});
51844
+ }
51845
+
51846
+ function getIdFilter(ids) {
51847
+ var set = new Set(ids);
51848
+ return function(i) {
51849
+ return set.has(i);
51850
+ };
51851
+ }
51852
+
51853
+ function getNullGeometryFilter(lyr, arcs) {
51854
+ var shapes = lyr.shapes;
51855
+ if (lyr.geometry_type == 'polygon') {
51856
+ return getEmptyPolygonFilter(shapes, arcs);
51857
+ }
51858
+ return function(i) {return !!shapes[i];};
51859
+ }
51860
+
51861
+ function getEmptyPolygonFilter(shapes, arcs) {
51862
+ return function(i) {
51863
+ var shp = shapes[i];
51864
+ return !!shp && geom.getPlanarShapeArea(shapes[i], arcs) > 0;
51865
+ };
51866
+ }
51867
+
51868
+ function combineFilters(a, b) {
51869
+ return a && b ? function(id) {
51870
+ return a(id) && b(id);
51871
+ } : (a || b);
51872
+ }
51873
+
51874
+ // The icon names accepted by the icon= property of the -style command.
51875
+ // Kept in a module of its own so that both the renderer (svg-symbols.mjs) and
51876
+ // the -style command can use it without forming an import cycle.
51877
+
51878
+ var iconNames = ['circle', 'square', 'ring', 'star'];
51879
+
51880
+ function isSupportedIconName(name) {
51881
+ return iconNames.indexOf(name) > -1;
51882
+ }
51883
+
51884
+ cmd.svgStyle = function(lyr, dataset, opts) {
51885
+ var filterFn, table, fields, hasNewFields, optFields, clearedByPosition, fieldsBefore;
51886
+
51887
+ function hadField(field) {
51888
+ return field in fieldsBefore;
51889
+ }
51890
+
51891
+ if (getFeatureCount(lyr) === 0) {
51892
+ return;
51893
+ }
51894
+ if (!lyr.data) {
51895
+ initDataTable(lyr);
51896
+ }
51897
+ if (opts.where) {
51898
+ filterFn = compileFeatureExpression(opts.where, lyr, dataset.arcs);
51899
+ }
51900
+ if (opts.ids) {
51901
+ filterFn = combineFilters(filterFn, getIdFilter(opts.ids));
51902
+ }
51903
+ if (opts.clear) {
51904
+ lyr.data.getFields().filter(isSupportedSvgStyleProperty).forEach(lyr.data.deleteField, lyr.data);
51905
+ }
51906
+ table = getLayerDataTable(lyr);
51907
+ optFields = getOptionFields(opts);
51908
+ fields = getStyleFields(optFields);
51909
+ // Which of dx/dy/text-anchor a label-pos in this command clears: not the ones
51910
+ // the same command also sets, so that `label-pos=n dx=3` keeps the nudge it
51911
+ // was given instead of clearing it a moment later.
51912
+ //
51913
+ // Clearing at all is what keeps the shorthand usable, since a value on the
51914
+ // record wins over the position: without it, setting a position on a label
51915
+ // that had been dragged would appear to do nothing.
51916
+ clearedByPosition = labelPositionDerivedFields.filter(function(field) {
51917
+ return optFields.indexOf(field) == -1;
51918
+ });
51919
+ hasNewFields = fields.some(function(field) {
51920
+ return !table.fieldExists(field);
51921
+ });
51922
+ // The table's columns as this command found them. Taken once, because the
51923
+ // command adds to them as it runs, and a blanked property must be judged
51924
+ // against what was there before rather than against what it has just made.
51925
+ fieldsBefore = utils.arrayToIndex(table.getFields());
51926
+ if (fields.length > 0) {
51927
+ if (hasNewFields) {
51928
+ table.captureSchemaBefore({operation: 'style', fields: fields});
51929
+ } else {
51930
+ table.captureFieldsBefore(fields, {operation: 'style'});
51931
+ }
51932
+ }
51933
+ Object.keys(opts).forEach(function(optName) {
51934
+ // undo cli parser name conversion; the regex must be global, or a
51935
+ // property with more than one hyphen (e.g. label-start-offset) is silently
51936
+ // skipped rather than applied
51937
+ var svgName = optName.replace(/_/g, '-');
51938
+ if (!isSupportedSvgStyleProperty(svgName)) {
51939
+ return;
51940
+ }
51941
+ var strVal = opts[optName].trim();
51942
+ // An empty value removes the property, rather than being rejected as an
51943
+ // unparseable one. This is how a control gives a property back: a label
51944
+ // dragged off its position clears label-pos, and there is otherwise no
51945
+ // per-property unset -- only -style clear, which clears all of them.
51946
+ var unset = strVal === '' && emptyValueUnsetsProperty(svgName);
51947
+ var accessor = unset ? null : getSymbolPropertyAccessor(strVal, svgName, lyr);
51948
+ var badIcons = svgName == 'icon' ? [] : null;
51949
+ // Removing a position is not setting one, so it neither validates the
51950
+ // value nor clears the offsets the position would have stood for.
51951
+ var posOnPaths = svgName == 'label-pos' && !unset ? [] : null;
51952
+ table.getRecords().forEach(function(rec, i) {
51953
+ if (filterFn && !filterFn(i)) {
51954
+ // make sure field exists if record is excluded by filter
51955
+ setUndefinedFields(rec, [svgName]);
51956
+ if (svgName == 'label-pos') {
51957
+ // ...but a field the position would only have cleared is one this
51958
+ // command is not writing anywhere, so an excluded record has nothing
51959
+ // to stay consistent with
51960
+ setUndefinedFields(rec, labelPositionDerivedFields, {has: hadField});
51961
+ }
51962
+ } else if (unset) {
51963
+ // Nothing to remove, and so nothing to create: removing a property no
51964
+ // record has would otherwise add an empty column for it.
51965
+ if (hadField(svgName) || svgName in rec) rec[svgName] = undefined;
51966
+ } else {
51967
+ rec[svgName] = accessor(i);
51968
+ if (badIcons) {
51969
+ addUnsupportedIconName(badIcons, rec.icon);
51970
+ }
51971
+ if (posOnPaths) {
51972
+ if (!parseLabelPosition(rec['label-pos'])) {
51973
+ stop$1('Unexpected value for label-pos:', rec['label-pos']);
51974
+ }
51975
+ if (shapeIsPathLabel(lyr.shapes && lyr.shapes[i], rec)) {
51976
+ // Not stored, so that ignoring it means ignoring it: a stored
51977
+ // position would show up in the style panel and would start
51978
+ // applying if the label ever lost all but one of its knots. Its
51979
+ // text-anchor is left alone too -- that one does place text along a
51980
+ // path, so a position that had no effect must not clear it.
51981
+ posOnPaths.push(i);
51982
+ rec['label-pos'] = undefined;
51983
+ } else {
51984
+ setUndefinedFields(rec, clearedByPosition, {overwrite: true, has: hadField});
51985
+ }
51986
+ }
51987
+ }
51988
+ });
51989
+ if (badIcons && badIcons.length > 0) {
51990
+ warn(formatUnsupportedIconMessage(badIcons));
51991
+ }
51992
+ if (posOnPaths && posOnPaths.length > 0) {
51993
+ warn(formatPositionOnPathMessage(posOnPaths));
51994
+ }
51995
+ });
51996
+ if (fields.length > 0) {
51997
+ if (hasNewFields) {
51998
+ table.markSchemaChanged({operation: 'style'});
51999
+ } else {
52000
+ table.markFieldsChanged(fields, {operation: 'style'});
52001
+ }
52002
+ }
52003
+ };
52004
+
52005
+ // The style properties this command was given, in SVG spelling.
52006
+ function getOptionFields(opts) {
52007
+ var fields = [];
52008
+ Object.keys(opts).forEach(function(optName) {
52009
+ var svgName = optName.replace(/_/g, '-');
52010
+ if (isSupportedSvgStyleProperty(svgName)) addField(fields, svgName);
52011
+ });
52012
+ return fields;
52013
+ }
52014
+
52015
+ // The fields the command will write, which is what the undo capture covers.
52016
+ // label-pos reaches dx/dy/text-anchor as well -- it no longer stores values in
52017
+ // them, but it does clear them.
52018
+ function getStyleFields(optFields) {
52019
+ var fields = [];
52020
+ optFields.forEach(function(svgName) {
52021
+ addField(fields, svgName);
52022
+ if (svgName == 'label-pos') {
52023
+ labelPositionFields.forEach(function(field) {
52024
+ addField(fields, field);
52025
+ });
52026
+ }
52027
+ });
52028
+ return fields;
52029
+ }
52030
+
52031
+ function addField(fields, field) {
52032
+ if (fields.indexOf(field) == -1) {
52033
+ fields.push(field);
52034
+ }
52035
+ }
52036
+
52037
+ // Icon names can not be validated when options are parsed, because an icon=
52038
+ // value may be a field name or an expression. The assigned values are checked
52039
+ // instead, so that computed names are covered too. Unsupported names are a
52040
+ // warning, not an error -- features with an unsupported name render without an
52041
+ // icon.
52042
+ var maxReportedIconNames = 4;
52043
+
52044
+ function addUnsupportedIconName(names, val) {
52045
+ var name;
52046
+ if (!val) return; // a blank value removes the icon
52047
+ name = String(val);
52048
+ if (isSupportedIconName(name) || names.indexOf(name) > -1) return;
52049
+ names.push(name);
52050
+ }
52051
+
52052
+ function formatUnsupportedIconMessage(names) {
52053
+ var extra = names.length - maxReportedIconNames;
52054
+ var listed = extra > 0 ? names.slice(0, maxReportedIconNames) : names;
52055
+ var str = 'Unsupported icon ' + (names.length > 1 ? 'names' : 'name') + ': ' +
52056
+ listed.join(', ');
52057
+ if (extra > 0) {
52058
+ str += ' (and ' + extra + ' more)';
52059
+ }
52060
+ return str + '. Expected one of: ' + iconNames.join(', ');
52061
+ }
52062
+
52063
+ // Adds @fields to @rec with no value, so that a record the filter excluded
52064
+ // still has the same schema as the ones it kept. With overwrite, also blanks a
52065
+ // value already there -- which is how setting a position takes back the offsets
52066
+ // a label was carrying.
52067
+ //
52068
+ // @has: optional test for whether the layer carries a field at all. A field
52069
+ // nobody has is not created in order to be blanked: -style label-pos=n clears
52070
+ // dx, dy and text-anchor because a value on the record wins over the position,
52071
+ // and there is nothing to win with when the column does not exist. Without
52072
+ // this, clicking a position in the style panel put three empty columns in the
52073
+ // user's table, and three empty columns in their CSV.
52074
+ function setUndefinedFields(rec, fields, opts) {
52075
+ var overwrite = !!(opts && opts.overwrite);
52076
+ var has = opts && opts.has;
52077
+ fields.forEach(function(field) {
52078
+ if (has && !has(field) && field in rec === false) return;
52079
+ if (overwrite || field in rec === false) {
52080
+ rec[field] = undefined;
52081
+ }
52082
+ });
52083
+ }
52084
+
52085
+ // label-pos places text around an anchor point, which a label strung along a
52086
+ // path does not have: its text runs from a start offset in the direction the
52087
+ // path goes. A warning rather than an error, because a layer can hold both
52088
+ // kinds of label and styling all of it at once is reasonable.
52089
+ function formatPositionOnPathMessage(ids) {
52090
+ var extra = ids.length - maxReportedIds;
52091
+ var listed = (extra > 0 ? ids.slice(0, maxReportedIds) : ids).join(', ');
52092
+ return 'Ignoring label-pos on ' + ids.length + ' path ' +
52093
+ (ids.length > 1 ? 'labels' : 'label') + ' (' + listed +
52094
+ (extra > 0 ? ' and ' + extra + ' more' : '') + '). ' +
52095
+ 'Use label-start-offset= and text-anchor= to place text along a path.';
52096
+ }
52097
+
52098
+ var maxReportedIds = 4;
52099
+
52100
+ // -labels styles the labels in a point layer, or turns a point layer into
52101
+ // labels by giving it text=. With coordinates=, it adds one label instead; that
52102
+ // case is cmd.addLabel(), dispatched separately because it accepts an empty
52103
+ // target.
52104
+ //
52105
+ // Styling writes the same properties -style writes, through the same code, so
52106
+ // a label styled by either command is the same label.
52107
+ cmd.labels = function(targetLayers, dataset, opts) {
52108
+ var styleOpts = getStyleOpts(opts);
52109
+ var output = targetLayers.map(function(lyr) {
52110
+ var out;
52111
+ requirePointLayer$1(lyr);
52112
+ out = opts.no_replace ? copyLayer(lyr) : lyr;
52113
+ cmd.svgStyle(out, dataset, styleOpts);
52114
+ return out;
52115
+ });
52116
+ return opts.no_replace ? output : null;
52117
+ };
52118
+
52119
+ // text= is label-text= under the name that reads naturally in this command.
52120
+ function getStyleOpts(opts) {
52121
+ var o = Object.assign({}, opts);
52122
+ if ('text' in o) {
52123
+ o.label_text = o.text;
52124
+ delete o.text;
52125
+ }
52126
+ return o;
52127
+ }
52128
+
52129
+ // Labels are drawn at points. A layer with no geometry is let through, since
52130
+ // it has nothing to conflict with.
52131
+ function requirePointLayer$1(lyr) {
52132
+ if (lyr.geometry_type && lyr.geometry_type != 'point') {
52133
+ stop$1('Labels can only be applied to a point layer; layer "' +
52134
+ (lyr.name || '[unnamed]') + '" contains ' + lyr.geometry_type + 's. ' +
52135
+ 'Use -points to make a point layer to label.');
52136
+ }
52137
+ }
52138
+
51398
52139
  cmd.addLayer = addLayer;
51399
52140
 
51400
52141
  var GEOMETRY_TYPES = ['point', 'polygon', 'polyline'];
@@ -54072,7 +54813,7 @@ ${svg}
54072
54813
  }
54073
54814
 
54074
54815
  cmd.alphaShapes = function(pointLyr, targetDataset, opts) {
54075
- requirePointLayer$1(pointLyr);
54816
+ requirePointLayer$2(pointLyr);
54076
54817
  if (opts.interval > 0 === false) {
54077
54818
  stop$1('Expected a non-negative interval parameter');
54078
54819
  }
@@ -61713,131 +62454,6 @@ ${svg}
61713
62454
  normalizeOverlaySource: normalizeOverlaySource
61714
62455
  });
61715
62456
 
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
62457
  cmd.clipLayers = function(target, src, dataset, opts) {
61842
62458
  return clipLayers(target, src, dataset, "clip", opts);
61843
62459
  };
@@ -64982,7 +65598,7 @@ ${svg}
64982
65598
  cmd.lines = function(lyr, dataset, opts) {
64983
65599
  opts = opts || {};
64984
65600
  if (opts.callouts) {
64985
- requirePointLayer$1(lyr);
65601
+ requirePointLayer$2(lyr);
64986
65602
  return pointsToCallouts(lyr, dataset, opts);
64987
65603
  } else if (lyr.geometry_type == 'point') {
64988
65604
  return pointsToLines(lyr, dataset, opts);
@@ -75469,231 +76085,6 @@ ${svg}
75469
76085
  return api;
75470
76086
  }
75471
76087
 
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
76088
  cmd.evaluateEachFeature = function(lyr, dataset, expArg, opts) {
75698
76089
  var n = getFeatureCount(lyr),
75699
76090
  arcs = dataset.arcs,
@@ -79540,7 +79931,7 @@ ${svg}
79540
79931
  }
79541
79932
 
79542
79933
  cmd.pointToGrid = function(targetLayers, targetDataset, opts) {
79543
- targetLayers.forEach(requirePointLayer$1);
79934
+ targetLayers.forEach(requirePointLayer$2);
79544
79935
  if (opts.interval > 0 === false) {
79545
79936
  stop$1('Expected a non-negative interval parameter');
79546
79937
  }
@@ -82325,232 +82716,6 @@ ${svg}
82325
82716
  stopJob(job);
82326
82717
  };
82327
82718
 
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
82719
  function getStickArrowCoords(d) {
82555
82720
  return getArrowCoords(d, 'stick');
82556
82721
  }
@@ -83082,7 +83247,7 @@ ${svg}
83082
83247
 
83083
83248
  // TODO: refactor to remove duplication in mapshaper-svg-style.js
83084
83249
  cmd.symbols = function(inputLyr, dataset, opts) {
83085
- requirePointLayer$1(inputLyr);
83250
+ requirePointLayer$2(inputLyr);
83086
83251
  var lyr = opts.no_replace ? copyLayer(inputLyr) : inputLyr;
83087
83252
  var shapeMode = !!opts.geographic;
83088
83253
  var metersPerPx;
@@ -83683,6 +83848,9 @@ ${svg}
83683
83848
  if (name == 'rectangle') {
83684
83849
  return !opts.source && !opts.bbox;
83685
83850
  }
83851
+ if (name == 'labels') {
83852
+ return !('coordinates' in opts);
83853
+ }
83686
83854
  return [
83687
83855
  'affine', 'alpha-shapes', 'blur', 'buffer', 'calc', 'check-geometry',
83688
83856
  'classify', 'clean', 'clip', 'cluster', 'contours', 'dashlines', 'data-fill',
@@ -83697,7 +83865,10 @@ ${svg}
83697
83865
  ].indexOf(name) > -1;
83698
83866
  }
83699
83867
 
83700
- function commandAcceptsEmptyTarget(name) {
83868
+ function commandAcceptsEmptyTarget(name, opts) {
83869
+ if (name == 'labels') {
83870
+ return 'coordinates' in opts;
83871
+ }
83701
83872
  return name == 'graticule' || name == 'i' || name == 'help' ||
83702
83873
  name == 'point-grid' || name == 'shape' || name == 'rectangle' || name == 'frame' ||
83703
83874
  name == 'require' || name == 'run' || name == 'define' ||
@@ -83793,7 +83964,7 @@ ${svg}
83793
83964
  stop$1(utils.format('Missing target: %s\nAvailable layers: %s',
83794
83965
  opts.target, getFormattedLayerList(job.catalog)));
83795
83966
  }
83796
- if (!commandAcceptsEmptyTarget(name)) {
83967
+ if (!commandAcceptsEmptyTarget(name, opts)) {
83797
83968
  stop$1("No data is available");
83798
83969
  }
83799
83970
  }
@@ -83816,7 +83987,7 @@ ${svg}
83816
83987
  }
83817
83988
  outputLayers = cmd.addShape(targetLayers, targetDataset, opts);
83818
83989
 
83819
- } else if (name == 'add-label') {
83990
+ } else if (name == 'add-label' || name == 'labels' && 'coordinates' in opts) {
83820
83991
  if (!targetDataset) {
83821
83992
  targetDataset = {info: {}, layers: []};
83822
83993
  targetLayers = targetDataset.layers;
@@ -83824,6 +83995,9 @@ ${svg}
83824
83995
  }
83825
83996
  outputLayers = cmd.addLabel(targetLayers, targetDataset, opts);
83826
83997
 
83998
+ } else if (name == 'labels') {
83999
+ outputLayers = cmd.labels(targetLayers, targetDataset, opts);
84000
+
83827
84001
  } else if (name == 'add-layer') {
83828
84002
  // The new layer arrives in a dataset of its own, so it is added to the
83829
84003
  // catalog here rather than integrated into the target below.
@@ -84401,7 +84575,7 @@ ${svg}
84401
84575
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
84402
84576
  }
84403
84577
 
84404
- var version = "0.7.69";
84578
+ var version = "0.7.70";
84405
84579
 
84406
84580
  // Parse command line args into commands and run them
84407
84581
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -85216,7 +85390,7 @@ ${svg}
85216
85390
  // parse. Null is what every reader already falls back from.
85217
85391
  function measureLabelText(rec) {
85218
85392
  var text = toLabelString(rec && rec['label-text']);
85219
- var font, fontSize, spacing, width;
85393
+ var font, fontSize, spacing, width, failed = false;
85220
85394
  if (!text) return null;
85221
85395
  font = getFontForRecord(rec);
85222
85396
  if (!font) return null;
@@ -85225,10 +85399,22 @@ ${svg}
85225
85399
  if (!(fontSize > 0)) return null;
85226
85400
  // The widest line, which is what the block of a multi-line label is as wide
85227
85401
  // 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);
85402
+ //
85403
+ // A line with bold words in it is measured a run at a time, each in its own
85404
+ // face, which loses the kerning between the last letter of one run and the
85405
+ // first of the next -- a fraction of a pixel at a weight change.
85406
+ width = splitLabelLineRuns(text).reduce(function(max, runs) {
85407
+ var w = runs.reduce(function(sum, run) {
85408
+ var face = run.bold ? getFontForRecord(rec, true) : font;
85409
+ if (!face) {
85410
+ failed = true;
85411
+ return sum;
85412
+ }
85413
+ return sum + measureLine(face, run.text, fontSize, spacing);
85414
+ }, 0);
85230
85415
  return w > max ? w : max;
85231
85416
  }, 0);
85417
+ if (failed) return null;
85232
85418
  return width > 0 ? width : null;
85233
85419
  }
85234
85420
 
@@ -85252,9 +85438,12 @@ ${svg}
85252
85438
  // The font a record is drawn in, opened and remembered. A record with no
85253
85439
  // font-family is drawn in the layer group's default, the same one the GUI
85254
85440
  // measures against -- see getLabelTextDefaults().
85255
- function getFontForRecord(rec) {
85441
+ //
85442
+ // @bold: the face its bold runs are drawn in, which is the keyword's 700
85443
+ // whatever the label's own weight is, as it is in CSS.
85444
+ function getFontForRecord(rec, bold) {
85256
85445
  var family = rec['font-family'] || 'sans-serif';
85257
- var weight = getFontWeight(rec);
85446
+ var weight = bold ? WEIGHT_NAMES[LABEL_BOLD_WEIGHT] : getFontWeight(rec);
85258
85447
  var italic = isItalic(rec);
85259
85448
  var stretch = rec['font-stretch'] || '';
85260
85449
  var key = [family, weight, italic ? 'i' : 'n', stretch].join('|');
@@ -87003,7 +87192,7 @@ ${svg}
87003
87192
  internal.svg = Object.assign({}, SvgStringify, SvgPathUtils, GeojsonToSvg,
87004
87193
  SvgFeatureUtils,
87005
87194
  SvgLabels, SvgSymbols, SvgLabelPaths, SvgLabelFit, SvgLabelAlign,
87006
- SvgLabelMetrics, SvgLabelHalo, SvgLabelCallout);
87195
+ SvgLabelMetrics, SvgLabelHalo, SvgLabelCallout, SvgLabelMarkup);
87007
87196
 
87008
87197
  // Reached through the bundle rather than imported from source, unlike most of
87009
87198
  // what tests use, because these modules load fs and fontkit through the