mapshaper 0.7.68 → 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/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,
@@ -25424,6 +25424,9 @@
25424
25424
  // type/data accessors -- doesn't form an import cycle with the
25425
25425
  // scalebar-aware renderer registry that lives in furniture.mjs.
25426
25426
 
25427
+ // Must match the renderers registered in mapshaper-furniture.mjs
25428
+ var furnitureTypes = ['scalebar'];
25429
+
25427
25430
  // @lyr dataset layer
25428
25431
  function getFurnitureLayerType(lyr) {
25429
25432
  var rec = lyr.data && lyr.data.getReadOnlyRecordAt(0);
@@ -25434,6 +25437,12 @@
25434
25437
  return lyr.data && lyr.data.getReadOnlyRecordAt(0);
25435
25438
  }
25436
25439
 
25440
+ // A data-only layer holding the settings of a map element such as a scalebar
25441
+ function layerIsFurniture(lyr) {
25442
+ return !!lyr && !lyr.geometry_type &&
25443
+ furnitureTypes.includes(getFurnitureLayerType(lyr));
25444
+ }
25445
+
25437
25446
  /*
25438
25447
  {
25439
25448
  width: size[0],
@@ -25629,6 +25638,27 @@
25629
25638
  });
25630
25639
  }
25631
25640
 
25641
+ // Furniture (scalebars, etc.) belongs to a frame by sharing its dataset.
25642
+ // Furniture in a dataset without a frame is an ordinary standalone layer.
25643
+ function getFrameFurnitureLayers(dataset) {
25644
+ if (!dataset || !findFrameLayerInDataset(dataset)) return [];
25645
+ return dataset.layers.filter(layerIsFurniture);
25646
+ }
25647
+
25648
+ function findFrameFurnitureLayer(dataset, type) {
25649
+ return getFrameFurnitureLayers(dataset).find(function(lyr) {
25650
+ return getFurnitureLayerType(lyr) == type;
25651
+ }) || null;
25652
+ }
25653
+
25654
+ // True for a map frame and for the furniture that belongs to it: layers that
25655
+ // describe the map layout rather than holding map content.
25656
+ function isFrameComponentLayer(lyr, dataset) {
25657
+ if (!dataset) return false;
25658
+ if (isFrameLayer(lyr, dataset.arcs)) return true;
25659
+ return layerIsFurniture(lyr) && !!findFrameLayerInDataset(dataset);
25660
+ }
25661
+
25632
25662
  function findFrames(catalog) {
25633
25663
  return catalog.getLayers().filter(function(o) {
25634
25664
  return isFrameLayer(o.layer, o.dataset.arcs);
@@ -25855,6 +25885,7 @@
25855
25885
  demoteFrameLayer: demoteFrameLayer,
25856
25886
  findFrame: findFrame,
25857
25887
  findFrameDataset: findFrameDataset,
25888
+ findFrameFurnitureLayer: findFrameFurnitureLayer,
25858
25889
  findFrameLayer: findFrameLayer,
25859
25890
  findFrameLayerInDataset: findFrameLayerInDataset,
25860
25891
  findFrames: findFrames,
@@ -25863,11 +25894,13 @@
25863
25894
  frameReservedFields: frameReservedFields,
25864
25895
  getActiveFrame: getActiveFrame,
25865
25896
  getFrameData: getFrameData,
25897
+ getFrameFurnitureLayers: getFrameFurnitureLayers,
25866
25898
  getFrameLayerBounds: getFrameLayerBounds,
25867
25899
  getFrameLayerData: getFrameLayerData,
25868
25900
  getFrameSize: getFrameSize,
25869
25901
  getMapFrameMetersPerPixel: getMapFrameMetersPerPixel,
25870
25902
  getSingleFrameRecord: getSingleFrameRecord,
25903
+ isFrameComponentLayer: isFrameComponentLayer,
25871
25904
  isFrameLayer: isFrameLayer,
25872
25905
  isFrameReservedField: isFrameReservedField,
25873
25906
  parseFrameSize: parseFrameSize,
@@ -25875,12 +25908,53 @@
25875
25908
  resolveExportFrame: resolveExportFrame
25876
25909
  });
25877
25910
 
25911
+ // Adds a furniture layer as a standalone dataset, without changing the
25912
+ // default target (so a following -o still exports the content layers)
25878
25913
  function addFurnitureLayer(lyr, catalog) {
25879
25914
  var o = {
25880
25915
  info: {},
25881
25916
  layers: [lyr]
25882
25917
  };
25918
+ catalog.captureCatalogBefore({operation: 'addFurnitureLayer'});
25883
25919
  catalog.getDatasets().push(o);
25920
+ catalog.markCatalogChanged({operation: 'addFurnitureLayer'});
25921
+ }
25922
+
25923
+ // Adds a furniture layer to a frame's dataset, replacing any existing
25924
+ // furniture of the same type. Passing a null layer removes it.
25925
+ // Returns the replaced or removed layer, if any
25926
+ function setFrameFurnitureLayer(frameDataset, type, lyr) {
25927
+ var layers = frameDataset.layers;
25928
+ var idx = layers.findIndex(function(o) {
25929
+ return layerIsFurniture(o) && getFurnitureLayerType(o) == type;
25930
+ });
25931
+ var prev = idx > -1 ? layers[idx] : null;
25932
+ if (!prev && !lyr) return null;
25933
+ noteDatasetWillChange(frameDataset, {operation: 'setFrameFurniture', unit: 'layers'});
25934
+ layers = layers.slice();
25935
+ if (prev && lyr) {
25936
+ lyr.name = prev.name;
25937
+ layers[idx] = lyr;
25938
+ } else if (prev) {
25939
+ layers.splice(idx, 1);
25940
+ } else {
25941
+ layers.push(lyr);
25942
+ }
25943
+ frameDataset.layers = layers;
25944
+ markDatasetChanged(frameDataset, {operation: 'setFrameFurniture', unit: 'layers'});
25945
+ return prev;
25946
+ }
25947
+
25948
+ // Moves a replaced frame's furniture into the dataset of the frame replacing it
25949
+ function moveFrameFurniture(fromDataset, toDataset) {
25950
+ var furniture = fromDataset.layers.filter(layerIsFurniture);
25951
+ if (furniture.length === 0) return;
25952
+ noteDatasetWillChange(fromDataset, {operation: 'moveFrameFurniture', unit: 'layers'});
25953
+ fromDataset.layers = fromDataset.layers.filter(function(lyr) {
25954
+ return !furniture.includes(lyr);
25955
+ });
25956
+ markDatasetChanged(fromDataset, {operation: 'moveFrameFurniture', unit: 'layers'});
25957
+ toDataset.layers = toDataset.layers.concat(furniture);
25884
25958
  }
25885
25959
 
25886
25960
  // Pure geometry-to-SVG-path primitives. Extracted from geojson-to-svg.mjs
@@ -26171,10 +26245,10 @@
26171
26245
  parts.unshift('hatches');
26172
26246
  obj = parseHatches(parts); // hatches is the default, name can be omitted
26173
26247
  }
26174
- if (!obj) {
26175
- // consider
26176
- message('Invalid pattern, ignoring:', str);
26177
- }
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.
26178
26252
  return obj;
26179
26253
  }
26180
26254
 
@@ -26314,7 +26388,10 @@
26314
26388
  function makeSVGPatternFill(str, id) {
26315
26389
  var o = parsePattern(str);
26316
26390
  var svg;
26317
- if (!o) return null;
26391
+ if (!o) {
26392
+ message('Invalid pattern, ignoring:', str);
26393
+ return null;
26394
+ }
26318
26395
  if (o.type == 'hatches') {
26319
26396
  svg = makeHatchPatternSVG(o);
26320
26397
  } else if (o.type == 'dots' || o.type == 'squares') {
@@ -27309,6 +27386,126 @@
27309
27386
  splitLabelHalos: splitLabelHalos
27310
27387
  });
27311
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
+
27312
27509
  // Accepting \n (two chars) as an alternative to the newline character
27313
27510
  // (sometimes, '\n' is not converted to newline, e.g. in a Makefile)
27314
27511
  // Also accepting <br>
@@ -27325,21 +27522,45 @@
27325
27522
  var anyLabelBreakRxp = /(\n|\\n|<br>|<wbr>)/i;
27326
27523
  var softBreakRxp = /^<wbr>$/i;
27327
27524
 
27328
- // The lines a label's text is drawn as. Whitespace before a soft break is
27329
- // dropped: a trailing space would move an end- or middle-justified line by a
27330
- // space's width.
27525
+ // The lines a label's text is drawn as, without its bold markup.
27331
27526
  function splitLabelLines(text) {
27332
- var parts = String(text).split(anyLabelBreakRxp);
27333
- 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)];
27334
27543
  for (var i = 1; i < parts.length; i += 2) {
27335
27544
  if (softBreakRxp.test(parts[i])) {
27336
- lines[lines.length - 1] = lines[lines.length - 1].replace(/\s+$/, '');
27545
+ trimTrailingSpace(lines[lines.length - 1]);
27337
27546
  }
27338
- 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;
27339
27550
  }
27340
27551
  return lines;
27341
27552
  }
27342
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
+
27343
27564
  function removeSoftBreaks(text) {
27344
27565
  return String(text).replace(/<wbr>/gi, '');
27345
27566
  }
@@ -27390,7 +27611,7 @@
27390
27611
  // already been through it, so calling it here as well costs nothing and means
27391
27612
  // every way into the renderer draws a label in the position it is stored in.
27392
27613
  var rec = resolveLabelPosition(recArg);
27393
- var morelines = splitLabelLines(toLabelString(rec['label-text']));
27614
+ var morelines = splitLabelLineRuns(toLabelString(rec['label-text']));
27394
27615
  var line = morelines.shift();
27395
27616
  var obj;
27396
27617
  var dx = applyAlignmentShift(rec);
@@ -27401,23 +27622,21 @@
27401
27622
  y: dy,
27402
27623
  x: dx
27403
27624
  };
27404
- obj = {
27625
+ obj = applyRunsToSvgObject({
27405
27626
  tag: 'text',
27406
- value: line,
27407
27627
  properties: properties
27408
- };
27628
+ }, line);
27409
27629
  if (morelines.length > 0) {
27410
27630
  // multiline label
27411
- obj.children = [];
27631
+ obj.children = obj.children || [];
27412
27632
  morelines.forEach(function(line) {
27413
- var tspan = {
27633
+ var tspan = applyRunsToSvgObject({
27414
27634
  tag: 'tspan',
27415
- value: line,
27416
27635
  properties: {
27417
27636
  x: dx,
27418
27637
  dy: getLineHeightDy(rec['line-height'])
27419
27638
  }
27420
- };
27639
+ }, line);
27421
27640
  obj.children.push(tspan);
27422
27641
  });
27423
27642
  }
@@ -27517,6 +27736,7 @@
27517
27736
  removeSoftBreaks: removeSoftBreaks,
27518
27737
  renderLabel: renderLabel$1,
27519
27738
  renderStyledLabel: renderStyledLabel,
27739
+ splitLabelLineRuns: splitLabelLineRuns,
27520
27740
  splitLabelLines: splitLabelLines,
27521
27741
  toLabelString: toLabelString,
27522
27742
  toPixels: toPixels
@@ -28425,22 +28645,60 @@
28425
28645
  symbolRenderers: symbolRenderers
28426
28646
  });
28427
28647
 
28648
+ // Command options that are stored in the scalebar's data record
28649
+ var scalebarFields = ['label', 'units', 'style', 'font_size', 'font_family',
28650
+ 'font_style', 'font_weight', 'color', 'tic_length', 'bar_width', 'label_offset',
28651
+ 'position', 'label_position', 'dual_units', 'margin'];
28652
+
28653
+ // With a map frame, the scalebar belongs to the frame, and running the
28654
+ // command again replaces it. Without a frame, each run adds a standalone
28655
+ // scalebar layer, which is exported if it is included in the output layers.
28428
28656
  cmd.scalebar = function(catalog, opts) {
28657
+ var frame = getActiveFrame(catalog);
28658
+ if (opts.remove) {
28659
+ if (!frame || !setFrameFurnitureLayer(frame.dataset, 'scalebar', null)) {
28660
+ stop$1('The map frame has no scalebar to remove');
28661
+ }
28662
+ return;
28663
+ }
28664
+ validateScalebarOpts(opts);
28429
28665
  var lyr = getScalebarLayer(opts);
28430
- if (opts.label && !parseScalebarUnits(opts.label)) {
28431
- stop$1(`Expected units of km or miles in scalebar label (received ${opts.label})`);
28666
+ if (frame) {
28667
+ setFrameFurnitureLayer(frame.dataset, 'scalebar', lyr);
28668
+ } else {
28669
+ addFurnitureLayer(lyr, catalog);
28432
28670
  }
28433
- addFurnitureLayer(lyr, catalog);
28434
28671
  };
28435
28672
 
28436
28673
  function getScalebarLayer(opts) {
28437
- var obj = utils.defaults({type: 'scalebar'}, opts);
28674
+ var rec = {type: 'scalebar'};
28675
+ scalebarFields.forEach(function(k) {
28676
+ if (opts[k] !== undefined && opts[k] !== null && opts[k] !== '') {
28677
+ rec[k] = opts[k];
28678
+ }
28679
+ });
28438
28680
  return {
28439
- name: opts.name || 'scalebar',
28440
- data: new DataTable([obj])
28681
+ name: 'scalebar',
28682
+ data: new DataTable([rec])
28441
28683
  };
28442
28684
  }
28443
28685
 
28686
+ function validateScalebarOpts(opts) {
28687
+ if (opts.label) {
28688
+ splitScalebarLabel(opts.label).forEach(function(str) {
28689
+ if (!parseScalebarUnits(str) && !isBareDistance(str)) {
28690
+ stop$1(`Expected a distance or units of km, meters, miles or feet in scalebar label (received ${str})`);
28691
+ }
28692
+ });
28693
+ }
28694
+ if (opts.units && !parseLabelUnits(opts.units)) {
28695
+ stop$1('Unsupported scalebar units:', opts.units);
28696
+ }
28697
+ if (opts.style && !/^[ab]$/i.test(opts.style)) {
28698
+ stop$1('Unsupported scalebar style:', opts.style);
28699
+ }
28700
+ }
28701
+
28444
28702
  function renderScalebar(d, frame) {
28445
28703
  if (!frame.crs) {
28446
28704
  message('Unable to render scalebar: unknown CRS.');
@@ -28453,14 +28711,15 @@
28453
28711
  var opts = getScalebarOpts(d);
28454
28712
  var metersPerPx = getMapFrameMetersPerPixel(frame);
28455
28713
  var frameWidthPx = frame.width;
28456
- var labels = d.label ? d.label.split(',') : null;
28457
- var label1 = labels && labels[0] || null;
28458
- var props1 = parseScalebarLabel(label1 || getAutoScalebarLabel(frameWidthPx, metersPerPx, 'mile'));
28459
- var label2 = opts.style == 'b' && labels && labels[1] || null;
28714
+ var labels = d.label ? splitScalebarLabel(d.label) : [];
28715
+ var units = parseLabelUnits(d.units) || 'mile';
28716
+ var label1 = getScalebarLabel(labels[0], frameWidthPx, metersPerPx, units);
28717
+ var props1 = parseScalebarLabel(label1);
28718
+ var label2 = null;
28460
28719
  var props2, length2;
28461
28720
 
28462
28721
  if (props1.km > 0 === false) {
28463
- message('Unusable scalebar label:', label1);
28722
+ message('Unusable scalebar label:', labels[0] || '');
28464
28723
  return [];
28465
28724
  }
28466
28725
 
@@ -28469,6 +28728,11 @@
28469
28728
  stop$1("Null scalebar length");
28470
28729
  }
28471
28730
 
28731
+ if (opts.style == 'b' && labels[1]) {
28732
+ label2 = completeLabel(labels[1], getOtherUnits(props1.units));
28733
+ } else if (opts.style == 'b' && opts.dual_units) {
28734
+ label2 = getAutoSecondLabel(length1, metersPerPx, getOtherUnits(props1.units));
28735
+ }
28472
28736
  if (label2) {
28473
28737
  props2 = parseScalebarLabel(label2);
28474
28738
  length2 = Math.round(props2.km / metersPerPx * 1000);
@@ -28521,8 +28785,13 @@
28521
28785
  };
28522
28786
 
28523
28787
  function getScalebarOpts(d) {
28524
- var style = d.style == 'b' || d.style == 'B' ? 'b' : 'a';
28525
- return Object.assign({}, defaultOpts, styleOpts[style], d, {style: style});
28788
+ var style = d.style == 'b' || d.style == 'B' || !d.style && d.dual_units ? 'b' : 'a';
28789
+ var opts = Object.assign({}, defaultOpts, styleOpts[style]);
28790
+ Object.keys(d).forEach(function(k) {
28791
+ if (d[k] !== undefined && d[k] !== null && d[k] !== '') opts[k] = d[k];
28792
+ });
28793
+ opts.style = style;
28794
+ return opts;
28526
28795
  }
28527
28796
 
28528
28797
  function renderAsSvg(length, text, length2, text2, opts) {
@@ -28540,12 +28809,11 @@
28540
28809
  };
28541
28810
  }
28542
28811
 
28543
- // TODO: generalize to other kinds of furniture as they are developed
28544
28812
  function getScalebarPosition(opts) {
28545
28813
  var pos = opts.position || 'top-left';
28546
28814
  return {
28547
- ypos: pos.includes('top') ? 'top' : 'bottom',
28548
- xpos: pos.includes('left') ? 'left' : 'right'
28815
+ ypos: pos.includes('bottom') ? 'bottom' : 'top',
28816
+ xpos: pos.includes('right') ? 'right' : 'left'
28549
28817
  };
28550
28818
  }
28551
28819
 
@@ -28559,12 +28827,16 @@
28559
28827
  'label-text': text,
28560
28828
  'font-size': opts.font_size,
28561
28829
  'text-anchor': labelPos.anchor,
28562
- 'dominant-baseline': labelPos.ypos == 'top' ? 'auto' : 'hanging'
28830
+ 'dominant-baseline': labelPos.ypos == 'top' ? 'auto' : 'hanging',
28563
28831
  //// 'dominant-baseline': labelPos == 'top' ? 'text-after-edge' : 'text-before-edge'
28564
28832
  // 'text-after-edge' is buggy in Safari and unsupported by Illustrator,
28565
28833
  // so I'm using 'hanging' and 'auto', which seem to be well supported.
28566
28834
  // downside: requires a kludgy multiplier to calculate scalebar height (see above)
28567
28835
  };
28836
+ if (opts.color) labelOpts.fill = opts.color;
28837
+ if (opts.font_family) labelOpts['font-family'] = opts.font_family;
28838
+ if (opts.font_style) labelOpts['font-style'] = opts.font_style;
28839
+ if (opts.font_weight) labelOpts['font-weight'] = opts.font_weight;
28568
28840
  return symbolRenderers.label(labelOpts, anchorX, anchorY);
28569
28841
  }
28570
28842
 
@@ -28624,7 +28896,7 @@
28624
28896
  }
28625
28897
  var bar = importMultiLineString(coords);
28626
28898
  Object.assign(bar.properties, {
28627
- stroke: 'black',
28899
+ stroke: opts.color || 'black',
28628
28900
  fill: 'none',
28629
28901
  'stroke-width': opts.bar_width,
28630
28902
  'stroke-linecap': 'butt',
@@ -28633,35 +28905,132 @@
28633
28905
  return bar;
28634
28906
  }
28635
28907
 
28636
- // unit: 'km' || 'mile'
28908
+ // Kilometers per unit
28909
+ var unitSizes = {
28910
+ km: 1,
28911
+ m: 0.001,
28912
+ mile: 1.60934,
28913
+ ft: 0.0003048
28914
+ };
28915
+
28916
+ // Candidate lengths for automatic labels, in ascending order of distance
28917
+ var autoLengths = {
28918
+ imperial: [
28919
+ ['ft', '10 20 25 50 100 200 250 500'],
28920
+ // note: removed 1.5 12 and 1,200
28921
+ ['mile', '1/8 1/5 1/4 1/2 1 2 3 4 5 8 10 15 20 25 30 40 50 75 100 150 200 250 ' +
28922
+ '300 350 400 500 750 1,000 1,500 2,000 2,500 3,000 4,000 5,000']
28923
+ ],
28924
+ metric: [
28925
+ ['m', '5 10 20 25 50 100 200 250 500'],
28926
+ ['km', '1 2 3 4 5 8 10 15 20 25 30 40 50 75 100 150 200 250 300 350 ' +
28927
+ '400 500 750 1,000 1,500 2,000 2,500 3,000 4,000 5,000']
28928
+ ]
28929
+ };
28930
+
28931
+ function getAutoLabels(system) {
28932
+ return autoLengths[system].reduce(function(memo, group) {
28933
+ return memo.concat(group[1].split(' ').map(function(str) {
28934
+ return formatDistanceLabel(str, group[0]);
28935
+ }));
28936
+ }, []);
28937
+ }
28938
+
28939
+ function getUnitSystem(units) {
28940
+ return units == 'km' || units == 'm' ? 'metric' : 'imperial';
28941
+ }
28942
+
28943
+ function getOtherUnits(units) {
28944
+ return getUnitSystem(units) == 'metric' ? 'mile' : 'km';
28945
+ }
28946
+
28947
+ // The unit system of the units= option: returns 'km', 'mile' or ''
28948
+ function parseUnitsOption(str) {
28949
+ var units = parseLabelUnits(str);
28950
+ return units ? (getUnitSystem(units) == 'metric' ? 'km' : 'mile') : '';
28951
+ }
28952
+
28953
+ // Accepts the units= option, which also gives the units of a label that is a
28954
+ // bare number; returns 'km', 'm', 'mile', 'ft' or ''
28955
+ function parseLabelUnits(str) {
28956
+ var s = String(str || '').toLowerCase();
28957
+ if (/^(km|kilomet(er|re)s?|metric)$/.test(s)) return 'km';
28958
+ if (/^(m|meters?|metres?)$/.test(s)) return 'm';
28959
+ if (/^(mi|miles?|imperial)$/.test(s)) return 'mile';
28960
+ if (/^(ft|feet)$/.test(s)) return 'ft';
28961
+ return '';
28962
+ }
28963
+
28964
+ // Splits a dual-unit label like "100 km,50 miles". Only a comma after the units
28965
+ // separates two distances, so "1,000 km" and a bare "1,500" stay whole.
28966
+ function splitScalebarLabel(label) {
28967
+ return String(label).split(/(?<=[^\d\s]),\s*/).map(function(str) {
28968
+ return str.trim();
28969
+ }).filter(Boolean);
28970
+ }
28971
+
28972
+ // A distance with no units, e.g. "150" or "1/2" or "1,000"
28973
+ function isBareDistance(str) {
28974
+ return /^[\d\s.,/]+$/.test(str) && parseScalebarNumber(str) > 0;
28975
+ }
28976
+
28977
+ // Adds units to a label that is a bare number
28978
+ function completeLabel(label, units) {
28979
+ return isBareDistance(label) ? formatDistanceLabel(label.trim(), units) : label;
28980
+ }
28981
+
28982
+ // label: a label from the label= option, or empty for an automatic label
28983
+ function getScalebarLabel(label, frameWidthPx, metersPerPx, defaultUnits) {
28984
+ var units;
28985
+ if (label && parseScalebarNumber(label) > 0) return completeLabel(label, defaultUnits);
28986
+ // A label with units but no number gets an automatic length in those units
28987
+ units = label && parseScalebarUnits(label) || defaultUnits;
28988
+ return getAutoScalebarLabel(frameWidthPx, metersPerPx, units);
28989
+ }
28990
+
28991
+ // unit: 'km' || 'mile' (or 'm', 'ft' for the same systems)
28992
+ // The longest length that fits within 20% of the map's width, or the shortest
28993
+ // that is at least 70px long if the map is too narrow for that
28637
28994
  function getAutoScalebarLabel(mapWidth, metersPerPx, unit) {
28638
- var minWidth = 70; // 100; // TODO: vary min size based on map width
28639
- var minKm = metersPerPx * minWidth / 1000;
28640
- // note: removed 1.5 12 and 1,200
28641
- var options = ('1/8 1/5 1/4 1/2 1 2 3 4 5 8 10 15 20 25 30 40 50 75 ' +
28642
- '100 150 200 250 300 350 400 500 750 1,000 1,500 2,000 ' +
28643
- '2,500 3,000 4,000 5,000').split(' ');
28644
- return options.reduce(function(memo, str) {
28645
- if (memo) return memo;
28646
- var label = formatDistanceLabel(str, unit);
28647
- if (parseScalebarLabelToKm(label) > minKm) {
28648
- return label;
28649
- }
28650
- }, null) || '';
28995
+ var minKm = metersPerPx * 70 / 1000;
28996
+ var maxKm = metersPerPx * mapWidth * 0.2 / 1000;
28997
+ var labels = getAutoLabels(getUnitSystem(unit));
28998
+ var longest = labels.filter(function(label) {
28999
+ var km = parseScalebarLabelToKm(label);
29000
+ return km >= minKm && km <= maxKm;
29001
+ }).pop();
29002
+ return longest || labels.find(function(label) {
29003
+ return parseScalebarLabelToKm(label) >= minKm;
29004
+ }) || '';
29005
+ }
29006
+
29007
+ // The longest automatic length in the given units that fits within the
29008
+ // first part of a dual-unit scalebar
29009
+ function getAutoSecondLabel(maxPx, metersPerPx, unit) {
29010
+ var maxKm = maxPx * metersPerPx / 1000;
29011
+ var minKm = maxKm / 4;
29012
+ var labels = getAutoLabels(getUnitSystem(unit)).filter(function(label) {
29013
+ var km = parseScalebarLabelToKm(label);
29014
+ return km <= maxKm && km >= minKm;
29015
+ });
29016
+ return labels.pop() || null;
28651
29017
  }
28652
29018
 
28653
29019
  function formatDistanceLabel(numStr, unit) {
28654
29020
  var num = parseScalebarNumber(numStr);
28655
- var unitStr = unit == 'km' && 'KM' || num > 1 && 'MILES' || 'MILE';
29021
+ var unitStr = unit == 'km' && 'KM' ||
29022
+ unit == 'm' && (num > 1 ? 'METERS' : 'METER') ||
29023
+ unit == 'ft' && (num > 1 ? 'FEET' : 'FOOT') ||
29024
+ num > 1 && 'MILES' || 'MILE';
28656
29025
  return numStr + ' ' + unitStr;
28657
29026
  }
28658
29027
 
28659
- // See test/mapshaper-scalebar.js for examples of supported formats
29028
+ // See test/scalebar-test.mjs for examples of supported formats
28660
29029
  function parseScalebarLabelToKm(str) {
28661
29030
  var units = parseScalebarUnits(str);
28662
29031
  var value = parseScalebarNumber(str);
28663
29032
  if (!units || !value) return NaN;
28664
- return units == 'mile' ? value * 1.60934 : value;
29033
+ return value * unitSizes[units];
28665
29034
  }
28666
29035
 
28667
29036
  function parseScalebarLabel(label) {
@@ -28669,7 +29038,7 @@
28669
29038
  var units = label ? parseScalebarUnits(label) : 'mile';
28670
29039
  var km = NaN;
28671
29040
  if (units && num) {
28672
- km = units == 'mile' ? num * 1.60934 : num;
29041
+ km = num * unitSizes[units];
28673
29042
  }
28674
29043
  return {
28675
29044
  number: num,
@@ -28678,11 +29047,14 @@
28678
29047
  };
28679
29048
  }
28680
29049
 
29050
+ // Returns 'km', 'm', 'mile', 'ft' or ''
28681
29051
  function parseScalebarUnits(str) {
28682
- var isMiles = /(miles?|mi[.]?|英里)$/.test(str.toLowerCase());
28683
- var isKm = /(k\.m\.|km|kilometers?|kilom.tres?|公里)$/.test(str.toLowerCase());
28684
- var units = isMiles && 'mile' || isKm && 'km' || '';
28685
- return units;
29052
+ var s = String(str).trim().toLowerCase();
29053
+ if (/(miles?|mi[.]?|英里)$/.test(s)) return 'mile';
29054
+ if (/(k\.m\.|km|kilomet(er|re)s?|kilom.tres?|公里)$/.test(s)) return 'km';
29055
+ if (/(^|[\d\s.])(feet|foot|ft[.]?|英尺)$/.test(s)) return 'ft';
29056
+ if (/(^|[\d\s.])(m|m\.|meters?|metres?|mètres?|米)$/.test(s)) return 'm';
29057
+ return '';
28686
29058
  }
28687
29059
 
28688
29060
  function parseScalebarNumber(str) {
@@ -28702,8 +29074,12 @@
28702
29074
  __proto__: null,
28703
29075
  formatDistanceLabel: formatDistanceLabel,
28704
29076
  getScalebarLayer: getScalebarLayer,
29077
+ parseLabelUnits: parseLabelUnits,
28705
29078
  parseScalebarLabelToKm: parseScalebarLabelToKm,
28706
- renderScalebar: renderScalebar
29079
+ parseScalebarUnits: parseScalebarUnits,
29080
+ parseUnitsOption: parseUnitsOption,
29081
+ renderScalebar: renderScalebar,
29082
+ splitScalebarLabel: splitScalebarLabel
28707
29083
  });
28708
29084
 
28709
29085
  var furnitureRenderers = {
@@ -28725,24 +29101,33 @@
28725
29101
  function renderFurnitureLayer(lyr, frame) {
28726
29102
  var d = getFurnitureLayerData(lyr);
28727
29103
  var renderer = furnitureRenderers[d.type];
29104
+ var problem;
28728
29105
  if (!renderer) {
28729
29106
  stop$1('Missing renderer for', d.type, 'element');
28730
29107
  }
28731
- if (!frame.crs) {
28732
- stop$1(`Unable to render ${d.type} (unknown map projection)`);
28733
- }
28734
- if (!isProjectedCRS(frame.crs)) {
28735
- stop$1(`Unable to render ${d.type} (map is unprojected)`);
29108
+ problem = getFurnitureFrameProblem(frame);
29109
+ if (problem) {
29110
+ stop$1(`Unable to render ${d.type} (${problem})`);
28736
29111
  }
28737
29112
  return renderer(d, frame) || [];
28738
29113
  }
28739
29114
 
29115
+ // Returns a reason why furniture can't be drawn in a frame, or null
29116
+ function getFurnitureFrameProblem(frame) {
29117
+ if (!frame) return 'missing map frame';
29118
+ if (!frame.crs) return 'unknown map projection';
29119
+ if (!isProjectedCRS(frame.crs)) return 'map is unprojected';
29120
+ return null;
29121
+ }
29122
+
28740
29123
  var Furniture = /*#__PURE__*/Object.freeze({
28741
29124
  __proto__: null,
29125
+ getFurnitureFrameProblem: getFurnitureFrameProblem,
28742
29126
  getFurnitureLayerData: getFurnitureLayerData,
28743
29127
  getFurnitureLayerType: getFurnitureLayerType,
28744
29128
  isFurnitureLayer: isFurnitureLayer,
28745
29129
  layerHasFurniture: layerHasFurniture,
29130
+ layerIsFurniture: layerIsFurniture,
28746
29131
  renderFurnitureLayer: renderFurnitureLayer
28747
29132
  });
28748
29133
 
@@ -29388,16 +29773,14 @@
29388
29773
  // a <tspan> inside a <textPath> advances along the path instead of
29389
29774
  // stacking below it, so the lines are joined rather than dropping
29390
29775
  // everything after the first one
29391
- text = text.split(labelNewlineRxp).join(' ');
29392
29776
  addToReport(report, 'joined', id);
29393
29777
  }
29394
- textPath = {
29778
+ textPath = applyRunsToSvgObject({
29395
29779
  tag: 'textPath',
29396
- value: text,
29397
29780
  properties: {
29398
29781
  startOffset: rec['label-start-offset'] || getDefaultStartOffset(rec)
29399
29782
  }
29400
- };
29783
+ }, joinLineRuns(splitLabelLineRuns(text)));
29401
29784
  // the caller replaces this with a reference to a <defs> entry
29402
29785
  textPath.properties[LABEL_PATH_PROPERTY] = d;
29403
29786
  if (rec['label-side']) {
@@ -29461,6 +29844,28 @@
29461
29844
  return '50%';
29462
29845
  }
29463
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
+
29464
29869
  function addToReport(report, key, id) {
29465
29870
  if (report && report[key]) report[key].push(id);
29466
29871
  }
@@ -39199,6 +39604,9 @@ ${svg}
39199
39604
  async function exportDatasets(datasets, opts) {
39200
39605
  var format = getOutputFormat(datasets[0], opts);
39201
39606
  var files;
39607
+ if (format != 'svg' && format != PACKAGE_EXT) {
39608
+ datasets = removeFurnitureLayers(datasets);
39609
+ }
39202
39610
  validateRasterExportFormat(datasets, format);
39203
39611
  if (format != 'geoparquet' && opts.level !== undefined) {
39204
39612
  error('The level= option only applies to GeoParquet output');
@@ -39274,6 +39682,25 @@ ${svg}
39274
39682
  return files;
39275
39683
  }
39276
39684
 
39685
+ // Scalebars and other map furniture only have a meaning in SVG output
39686
+ function removeFurnitureLayers(datasets) {
39687
+ var hasFurniture = datasets.some(function(dataset) {
39688
+ return dataset.layers.some(layerIsFurniture);
39689
+ });
39690
+ if (!hasFurniture) return datasets;
39691
+ datasets = datasets.map(function(dataset) {
39692
+ return utils.defaults({
39693
+ layers: dataset.layers.filter(function(lyr) { return !layerIsFurniture(lyr); })
39694
+ }, dataset);
39695
+ }).filter(function(dataset) {
39696
+ return dataset.layers.length > 0;
39697
+ });
39698
+ if (datasets.length === 0) {
39699
+ stop$1('Map furniture layers (e.g. scalebars) can only be exported as SVG');
39700
+ }
39701
+ return datasets;
39702
+ }
39703
+
39277
39704
  function validateRasterExportFormat(datasets, format) {
39278
39705
  if (format == 'geotiff') {
39279
39706
  if (datasetsHaveVectorLayers(datasets)) {
@@ -40455,6 +40882,22 @@ ${svg}
40455
40882
  }
40456
40883
  }
40457
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
+
40458
40901
  function validateExpressionOpt(cmd) {
40459
40902
  if (!cmd.options.expression) {
40460
40903
  error('Command requires a JavaScript expression');
@@ -41435,17 +41878,17 @@ ${svg}
41435
41878
  nameOpt2 = { // for -calc and -info
41436
41879
  describe: 'name the output layer'
41437
41880
  },
41438
- // label style properties accepted by -add-label, so that a label can be
41439
- // created and styled in one command. These are a subset of the
41440
- // 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.
41441
41884
  labelStyleOpts = {
41442
- 'font-family': {describe: 'label font, e.g. Georgia'},
41443
- '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)'},
41444
41887
  'font-style': {describe: 'normal or italic'},
41445
41888
  'font-weight': {describe: 'normal, bold or a numeric weight'},
41446
41889
  'font-stretch': {describe: 'e.g. condensed'},
41447
41890
  'letter-spacing': {describe: 'extra space between letters'},
41448
- '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)'},
41449
41892
  'text-anchor': {describe: 'start, middle or end'},
41450
41893
  'label-align': {describe: 'how the lines of a multi-line label align: left, center or right'},
41451
41894
  'dominant-baseline': {describe: 'vertical alignment, e.g. central'},
@@ -41456,23 +41899,30 @@ ${svg}
41456
41899
  dy: {describe: 'vertical offset from the anchor'},
41457
41900
  fill: {describe: 'text color'},
41458
41901
  opacity: {describe: 'text opacity'},
41459
- '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)'},
41460
41903
  'halo-color': {describe: 'halo color (default is white)'},
41461
41904
  'halo-opacity': {describe: 'halo opacity, 0-1'},
41462
- '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)'},
41463
41906
  callout: {describe: 'line from the anchor to the text: line, elbow or curve'},
41464
41907
  'callout-end': {describe: 'arrowhead at the anchor end: arrow, open-arrow or none'},
41465
- '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)'},
41466
41909
  'callout-via': {describe: 'x,y of an elbow\'s corner or a point on a curve, in px'},
41467
41910
  'callout-attach': {describe: 'where the callout meets the text, as x,y fractions'},
41468
41911
  'callout-gap': {describe: 'px between the callout and the anchor'},
41469
- 'callout-padding': {describe: 'px between the callout and the text'},
41912
+ 'callout-padding': {describe: 'px between the callout and the text (default is 3)'},
41470
41913
  'callout-color': {describe: 'callout color (defaults to the text color)'},
41471
- 'callout-width': {describe: 'callout line width in px'},
41914
+ 'callout-width': {describe: 'callout line width in px (default is 1)'},
41472
41915
  'callout-opacity': {describe: 'callout opacity, 0-1'},
41473
41916
  css: {describe: 'inline css style'},
41474
41917
  class: {describe: 'name of CSS class or classes (space-separated)'}
41475
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
+ },
41476
41926
  noReplaceOpt2 = { // for -calc and -info
41477
41927
  alias: '+',
41478
41928
  type: 'flag',
@@ -41552,6 +42002,18 @@ ${svg}
41552
42002
  type: 'distance'
41553
42003
  };
41554
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
+
41555
42017
  var parser = new CommandParser();
41556
42018
  parser.usage('Usage: mapshaper -<command> [options] ...');
41557
42019
 
@@ -42920,6 +43382,32 @@ ${svg}
42920
43382
  .option('encoding', encodingOpt)
42921
43383
  .option('target', targetOpt);
42922
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
+
42923
43411
  parser.command('lines')
42924
43412
  .describe('convert a polygon or point layer to a polyline layer')
42925
43413
  .option('fields', {
@@ -43483,99 +43971,11 @@ ${svg}
43483
43971
  describe: 'point icon color (defaults to fill color, then black)'
43484
43972
  })
43485
43973
  .option('icon-opacity', {
43486
- describe: 'point icon opacity, 0-1 (defaults to the label\'s opacity)'
43487
- })
43488
- .option('label-text', {
43489
- describe: 'label text (set this to export points as labels)'
43490
- })
43491
- .option('label-pos', {
43492
- describe: 'label position; one of: n, s, e, w, ne, se, nw, sw, c'
43493
- })
43494
- .option('text-anchor', {
43495
- describe: 'label alignment; one of: start, end, middle (default)'
43496
- })
43497
- .option('label-align', {
43498
- describe: 'alignment of the lines of multi-line labels; left, center or right'
43499
- })
43500
- .option('halo-width', {
43501
- describe: 'width of a halo around label text in px (default is 0, no halo)'
43502
- })
43503
- .option('halo-color', {
43504
- describe: 'color of label halos (default is white)'
43505
- })
43506
- .option('halo-opacity', {
43507
- describe: 'opacity of label halos, 0-1'
43974
+ describe: 'point icon opacity, 0-1'
43508
43975
  })
43509
- .option('dx', {
43510
- describe: 'x offset of labels (default is 0)'
43511
- })
43512
- .option('dy', {
43513
- describe: 'y offset of labels (default is 0/baseline-aligned)'
43514
- })
43515
- .option('font-size', {
43516
- describe: 'size of label text (default is 12)'
43517
- })
43518
- .option('font-family', {
43519
- describe: 'CSS font family of labels (default is sans-serif)'
43520
- })
43521
- .option('font-weight', {
43522
- describe: 'CSS font weight property of labels (e.g. bold, 700)'
43523
- })
43524
- .option('font-style', {
43525
- describe: 'CSS font style property of labels (e.g. italic)'
43526
- })
43527
- .option('font-stretch', {
43528
- describe: 'CSS font stretch property of labels (e.g. condensed)'
43529
- })
43530
- .option('letter-spacing', {
43531
- describe: 'CSS letter-spacing property of labels'
43532
- })
43533
- .option('line-height', {
43534
- describe: 'line spacing of multi-line labels; a bare number is a multiple of the font size, as in CSS (default is 1.1)'
43535
- })
43536
- .option('dominant-baseline', {
43537
- describe: 'vertical alignment of labels (e.g. central)'
43538
- })
43539
- .option('label-side', {
43540
- describe: 'which side of its path a label sits on: left or right'
43541
- })
43542
- .option('label-start-offset', {
43543
- describe: 'where label text starts along its path, e.g. 50%'
43544
- })
43545
- .option('label-width', {
43546
- describe: 'width of a fixed-width text block in px (lines are wrapped in the web UI)'
43547
- })
43548
- .option('callout', {
43549
- describe: 'line from a label\'s anchor to its text: line, elbow or curve'
43550
- })
43551
- .option('callout-end', {
43552
- describe: 'arrowhead at the anchor end of a callout: arrow (solid), open-arrow or none'
43553
- })
43554
- .option('callout-end-size', {
43555
- describe: 'length of an arrowhead\'s sides in px (default 10, more for a thicker line)'
43556
- })
43557
- .option('callout-via', {
43558
- describe: 'x,y of an elbow\'s corner or a point on a curve, in px from the anchor'
43559
- })
43560
- .option('callout-attach', {
43561
- describe: 'where a callout meets its text, as x,y fractions of the text box'
43562
- })
43563
- .option('callout-gap', {
43564
- describe: 'px between a callout and its anchor (default: a line meets the icon, an arrow clears it)'
43565
- })
43566
- .option('callout-padding', {
43567
- describe: 'px between a callout and its text (default is 3)'
43568
- })
43569
- .option('callout-color', {
43570
- describe: 'callout color (defaults to the text color)'
43571
- })
43572
- .option('callout-width', {
43573
- describe: 'callout line width in px (default is 1)'
43574
- })
43575
- .option('callout-opacity', {
43576
- describe: 'callout opacity, 0-1'
43577
- })
43578
- .option('target', targetOpt);
43976
+ // deprecated in favor of -labels; accepted, but left out of the help
43977
+ .options(getHiddenLabelStyleOpts())
43978
+ .option('target', targetOpt);
43579
43979
 
43580
43980
  parser.command('repel')
43581
43981
  .describe('move overlapping point symbols apart')
@@ -43821,23 +44221,10 @@ ${svg}
43821
44221
  .option('text', {
43822
44222
  describe: 'label text'
43823
44223
  })
43824
- // label style properties, so that creating and styling a label is one
43825
- // 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
43826
44226
  .options(labelStyleOpts)
43827
- // a label can carry a symbol at its anchor, so these come along too even
43828
- // though they are not text properties
43829
- .option('icon', {
43830
- describe: 'symbol drawn at the label anchor: circle, square, ring, star'
43831
- })
43832
- .option('icon-size', {
43833
- describe: 'size of the anchor symbol in px'
43834
- })
43835
- .option('icon-color', {
43836
- describe: 'color of the anchor symbol (defaults to the text color)'
43837
- })
43838
- .option('icon-opacity', {
43839
- describe: 'opacity of the anchor symbol, 0-1'
43840
- })
44227
+ .options(labelIconOpts)
43841
44228
  .option('properties', {
43842
44229
  describe: 'other attributes, as a JSON object'
43843
44230
  })
@@ -44131,10 +44518,13 @@ ${svg}
44131
44518
  .option('target', targetOpt);
44132
44519
 
44133
44520
  parser.command('scalebar')
44134
- .describe('add a simple scale bar to SVG output')
44521
+ .describe('add a scale bar to the map frame and SVG output')
44135
44522
  .option('label', {
44136
44523
  DEFAULT: true,
44137
- describe: 'distance label, e.g. "35 miles"'
44524
+ describe: 'distance label, e.g. "35 miles" (default is automatic)'
44525
+ })
44526
+ .option('units', {
44527
+ describe: 'units of a bare-number or automatic label: km, meters, miles or feet (default is miles)'
44138
44528
  })
44139
44529
  .option('style', {
44140
44530
  describe: 'two options: a or b'
@@ -44142,6 +44532,18 @@ ${svg}
44142
44532
  .option('font-size', {
44143
44533
  type: 'number'
44144
44534
  })
44535
+ .option('font-family', {
44536
+ describe: 'font family of labels'
44537
+ })
44538
+ .option('font-style', {
44539
+ describe: 'font style of labels, e.g. italic'
44540
+ })
44541
+ .option('font-weight', {
44542
+ describe: 'font weight of labels, e.g. bold'
44543
+ })
44544
+ .option('color', {
44545
+ describe: 'color of bar and labels (default is black)'
44546
+ })
44145
44547
  .option('tic-length', {
44146
44548
  describe: 'length of tic marks (style b)',
44147
44549
  type: 'number'
@@ -44154,18 +44556,22 @@ ${svg}
44154
44556
  type: 'number'
44155
44557
  })
44156
44558
  .option('position', {
44157
- describe: 'e.g. bottom-right (default is top-left)'
44559
+ describe: 'top-left (default), top-right, bottom-left or bottom-right'
44158
44560
  })
44159
44561
  .option('label-position', {
44160
44562
  describe: 'top, bottom, top-center (style a), etc'
44161
44563
  })
44162
44564
  .option('dual-units', {
44163
- // describe: 'display both metric and imperial units',
44565
+ describe: 'show both metric and imperial units (style b)',
44164
44566
  type: 'flag'
44165
44567
  })
44166
44568
  .option('margin', {
44167
44569
  describe: 'offset in pixels from edge of map',
44168
44570
  type: 'number'
44571
+ })
44572
+ .option('remove', {
44573
+ describe: 'remove the scale bar from the map frame',
44574
+ type: 'flag'
44169
44575
  });
44170
44576
 
44171
44577
  parser.command('shape')
@@ -51115,6 +51521,621 @@ ${svg}
51115
51521
  return d;
51116
51522
  }
51117
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
+
51118
52139
  cmd.addLayer = addLayer;
51119
52140
 
51120
52141
  var GEOMETRY_TYPES = ['point', 'polygon', 'polyline'];
@@ -52161,8 +53182,10 @@ ${svg}
52161
53182
  // May return NaN for a bbox the frame can't be fitted to.
52162
53183
  // @opts.ignore_symbols fit to the coordinates alone
52163
53184
  function getFrameContentBbox(targets, getScale, opts) {
53185
+ // a data-only layer has no bounds
52164
53186
  var bounds = targets.reduce(function(memo, o) {
52165
- return memo.mergeBounds(getLayerBounds(o.layer, o.dataset.arcs));
53187
+ var b = getLayerBounds(o.layer, o.dataset.arcs);
53188
+ return b ? memo.mergeBounds(b) : memo;
52166
53189
  }, new Bounds());
52167
53190
  var raw, items, bbox;
52168
53191
  if (!bounds.hasBounds()) return null;
@@ -52309,6 +53332,11 @@ ${svg}
52309
53332
  var extent = getFrameExtent(contentBbox, widthPx, heightPx, opts);
52310
53333
  return getFrameScale(extent.bbox, extent.width, getFixedAspect$1(extent, opts));
52311
53334
  }, opts);
53335
+ if (!bbox && !expandCommandTargets(targets).some(function(o) {
53336
+ return layerHasGeometry$1(o.layer);
53337
+ })) {
53338
+ stop$1('Unable to fit a frame to a layer with no geometry. Target a layer containing shapes, or use the bbox= option.');
53339
+ }
52312
53340
  if (!bbox) {
52313
53341
  stop$1('Command target is missing geographical bounds');
52314
53342
  }
@@ -52332,14 +53360,17 @@ ${svg}
52332
53360
  var crsInfo = getDatasetCrsInfo(targets[0].dataset);
52333
53361
  setDatasetCrsInfo(frameDataset, crsInfo);
52334
53362
  }
52335
- frameDataset.layers[0].name = opts.name || 'frame';
53363
+ var frameLyr = frameDataset.layers[0];
53364
+ frameLyr.name = opts.name || 'frame';
52336
53365
  if (existingFrame) {
52337
53366
  if (!opts.replace) {
52338
53367
  stop$1('A map frame already exists:', existingFrame.layer.name || '[unnamed frame]');
52339
53368
  }
53369
+ moveFrameFurniture(existingFrame.dataset, frameDataset);
52340
53370
  demoteFrameLayer(existingFrame.layer);
52341
53371
  }
52342
- catalog.addDataset(frameDataset);
53372
+ // target the frame, not any furniture that came with it
53373
+ catalog.setDefaultTarget([frameLyr], frameDataset);
52343
53374
  };
52344
53375
 
52345
53376
  // The frame's extent and nominal size, from the extent of its content and the
@@ -53782,7 +54813,7 @@ ${svg}
53782
54813
  }
53783
54814
 
53784
54815
  cmd.alphaShapes = function(pointLyr, targetDataset, opts) {
53785
- requirePointLayer$1(pointLyr);
54816
+ requirePointLayer$2(pointLyr);
53786
54817
  if (opts.interval > 0 === false) {
53787
54818
  stop$1('Expected a non-negative interval parameter');
53788
54819
  }
@@ -61423,131 +62454,6 @@ ${svg}
61423
62454
  normalizeOverlaySource: normalizeOverlaySource
61424
62455
  });
61425
62456
 
61426
- // Insert cutting points in arcs, where bbox intersects other shapes
61427
- // Return a polygon layer containing the bounding box vectors, divided at cutting points.
61428
- function divideDatasetByBBox(dataset, bbox) {
61429
- var arcs = dataset.arcs;
61430
- var data = findBBoxCutPoints(arcs, bbox);
61431
- var map = insertCutPoints(data.cutPoints, arcs);
61432
- arcs.dedupCoords();
61433
- remapDividedArcs(dataset, map);
61434
- // merge bbox dataset with target dataset,
61435
- // so arcs are shared between target layers and bbox layer
61436
- var clipDataset = bboxPointsToClipDataset(data.bboxPoints);
61437
- var mergedDataset = mergeDatasets([dataset, clipDataset]);
61438
- // TODO: detect if we need to rebuild topology (unlikely), like with the full clip command
61439
- // buildTopology(mergedDataset);
61440
- var clipLyr = mergedDataset.layers.pop();
61441
- dataset.arcs = mergedDataset.arcs;
61442
- dataset.layers = mergedDataset.layers;
61443
- return clipLyr;
61444
- }
61445
-
61446
- function bboxPointsToClipDataset(arr) {
61447
- var arcs = [];
61448
- var shape = [];
61449
- var layer = {geometry_type: 'polygon', shapes: [[shape]]};
61450
- var p1, p2;
61451
- for (var i=0, n=arr.length - 1; i<n; i++) {
61452
- p1 = arr[i];
61453
- p2 = arr[i+1];
61454
- arcs.push([[p1.x, p1.y], [p2.x, p2.y]]);
61455
- shape.push(i);
61456
- }
61457
- return {
61458
- arcs: new ArcCollection(arcs),
61459
- layers: [layer]
61460
- };
61461
- }
61462
-
61463
- function findBBoxCutPoints(arcs, bbox) {
61464
- var left = bbox[0],
61465
- bottom = bbox[1],
61466
- right = bbox[2],
61467
- top = bbox[3];
61468
-
61469
- // arrays of intersection points along each bbox edge
61470
- var tt = [],
61471
- rr = [],
61472
- bb = [],
61473
- ll = [];
61474
-
61475
- arcs.forEachSegment(function(i, j, xx, yy) {
61476
- var ax = xx[i],
61477
- ay = yy[i],
61478
- bx = xx[j],
61479
- by = yy[j];
61480
- var hit;
61481
- if (segmentOutsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
61482
- if (segmentInsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
61483
-
61484
- hit = geom.segmentIntersection(left, top, right, top, ax, ay, bx, by);
61485
- if (hit) addHit(tt, hit, i, j, xx, yy);
61486
-
61487
- hit = geom.segmentIntersection(left, bottom, right, bottom, ax, ay, bx, by);
61488
- if (hit) addHit(bb, hit, i, j, xx, yy);
61489
-
61490
- hit = geom.segmentIntersection(left, bottom, left, top, ax, ay, bx, by);
61491
- if (hit) addHit(ll, hit, i, j, xx, yy);
61492
-
61493
- hit = geom.segmentIntersection(right, bottom, right, top, ax, ay, bx, by);
61494
- if (hit) addHit(rr, hit, i, j, xx, yy);
61495
- });
61496
-
61497
- return {
61498
- cutPoints: ll.concat(bb, rr, tt),
61499
- bboxPoints: getDividedBBoxPoints(bbox, ll, tt, rr, bb)
61500
- };
61501
-
61502
- function addHit(arr, hit, i, j, xx, yy) {
61503
- if (!hit) return;
61504
- arr.push(formatHit(hit[0], hit[1], i, j, xx, yy));
61505
- if (hit.length == 4) {
61506
- arr.push(formatHit(hit[2], hit[3], i, j, xx, yy));
61507
- }
61508
- }
61509
-
61510
- function formatHit(x, y, i, j, xx, yy) {
61511
- var ids = formatIntersectingSegment(x, y, i, j, xx, yy);
61512
- return getCutPoint(x, y, ids[0], ids[1]);
61513
- }
61514
- }
61515
-
61516
- function segmentOutsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
61517
- return ax < xmin && bx < xmin || ax > xmax && bx > xmax ||
61518
- ay < ymin && by < ymin || ay > ymax && by > ymax;
61519
- }
61520
-
61521
- function segmentInsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
61522
- return ax > xmin && bx > xmin && ax < xmax && bx < xmax &&
61523
- ay > ymin && by > ymin && ay < ymax && by < ymax;
61524
- }
61525
-
61526
- // Returns an array of points representing the vertices in
61527
- // the bbox with cutting points inserted.
61528
- function getDividedBBoxPoints(bbox, ll, tt, rr, bb) {
61529
- var bl = {x: bbox[0], y: bbox[1]},
61530
- tl = {x: bbox[0], y: bbox[3]},
61531
- tr = {x: bbox[2], y: bbox[3]},
61532
- br = {x: bbox[2], y: bbox[1]};
61533
- ll = utils.sortOn(ll.concat([bl, tl]), 'y', true);
61534
- tt = utils.sortOn(tt.concat([tl, tr]), 'x', true);
61535
- rr = utils.sortOn(rr.concat([tr, br]), 'y', false);
61536
- bb = utils.sortOn(bb.concat([br, bl]), 'x', false);
61537
- return ll.concat(tt, rr, bb).reduce(function(memo, p2) {
61538
- var p1 = memo.length > 0 ? memo[memo.length-1] : null;
61539
- if (p1 === null || p1.x != p2.x || p1.y != p2.y) memo.push(p2);
61540
- return memo;
61541
- }, []);
61542
- }
61543
-
61544
- var Bbox2Clipping = /*#__PURE__*/Object.freeze({
61545
- __proto__: null,
61546
- divideDatasetByBBox: divideDatasetByBBox,
61547
- segmentInsideBBox: segmentInsideBBox,
61548
- segmentOutsideBBox: segmentOutsideBBox
61549
- });
61550
-
61551
62457
  cmd.clipLayers = function(target, src, dataset, opts) {
61552
62458
  return clipLayers(target, src, dataset, "clip", opts);
61553
62459
  };
@@ -64692,7 +65598,7 @@ ${svg}
64692
65598
  cmd.lines = function(lyr, dataset, opts) {
64693
65599
  opts = opts || {};
64694
65600
  if (opts.callouts) {
64695
- requirePointLayer$1(lyr);
65601
+ requirePointLayer$2(lyr);
64696
65602
  return pointsToCallouts(lyr, dataset, opts);
64697
65603
  } else if (lyr.geometry_type == 'point') {
64698
65604
  return pointsToLines(lyr, dataset, opts);
@@ -75179,231 +76085,6 @@ ${svg}
75179
76085
  return api;
75180
76086
  }
75181
76087
 
75182
- cmd.filterGeom = function(lyr, arcs, opts) {
75183
- if (!layerHasGeometry$1(lyr)) {
75184
- stop$1("Layer is missing geometry");
75185
- }
75186
- if (opts.bbox) {
75187
- filterByBoundsIntersection(lyr, arcs, opts);
75188
- }
75189
- cmd.filterFeatures(lyr, arcs, {remove_empty: true, verbose: false});
75190
- };
75191
-
75192
- function filterByBoundsIntersection(lyr, arcs, opts) {
75193
- var filter = getBoundsIntersectionFilter(opts.bbox, lyr, arcs);
75194
- noteLayerWillChange(lyr, {operation: 'filter-geom', unit: 'shapes'});
75195
- editShapes(lyr.shapes, filter);
75196
- markLayerChanged(lyr, {operation: 'filter-geom', unit: 'shapes'});
75197
- }
75198
-
75199
- function getBoundsIntersectionFilter(bbox, lyr, arcs) {
75200
- var bounds = new Bounds(bbox);
75201
- var filter = lyr.geometry_type == 'point' ?
75202
- getPointInBoundsTest(bounds) :
75203
- getPathBoundsIntersectionTest(bounds, arcs);
75204
- return filter;
75205
- }
75206
-
75207
- function getPointInBoundsTest(bounds) {
75208
- return function(xy) {
75209
- var contains = bounds.containsPoint(xy[0], xy[1]);
75210
- return contains ? xy : null;
75211
- };
75212
- }
75213
-
75214
- // V1 too-simple test: bounding-box intersection
75215
- // internal.getPathBoundsIntersectionTest = function(bounds, arcs) {
75216
- // return function(path) {
75217
- // return bounds.intersects(arcs.getSimpleShapeBounds(path)) ? path : null;
75218
- // };
75219
- // };
75220
-
75221
- function getPathBoundsIntersectionTest(bounds, arcs) {
75222
- var bbox = bounds.toArray(),
75223
- left = bbox[0],
75224
- bottom = bbox[1],
75225
- right = bbox[2],
75226
- top = bbox[3];
75227
-
75228
- return function(path) {
75229
- // case: bounding boxes don't intersect -> the path doesn't intersect the box
75230
- if (!bounds.intersects(arcs.getSimpleShapeBounds(path))) {
75231
- return null;
75232
- }
75233
- var intersects = false;
75234
- var ax, ay, bx, by;
75235
- var iter = arcs.getShapeIter(path);
75236
-
75237
- if (iter.hasNext()) {
75238
- ax = iter.x;
75239
- ay = iter.y;
75240
- }
75241
- while (iter.hasNext()) {
75242
- bx = ax;
75243
- by = ay;
75244
- ax = iter.x;
75245
- ay = iter.y;
75246
- if (segmentOutsideBBox(ax, ay, bx, by, left, bottom, right, top)) continue;
75247
- if (segmentInsideBBox(ax, ay, bx, by, left, bottom, right, top)) {
75248
- intersects = true;
75249
- break;
75250
- }
75251
- if (geom.segmentIntersection(left, top, right, top, ax, ay, bx, by) ||
75252
- geom.segmentIntersection(left, bottom, right, bottom, ax, ay, bx, by) ||
75253
- geom.segmentIntersection(left, bottom, left, top, ax, ay, bx, by) ||
75254
- geom.segmentIntersection(right, bottom, right, top, ax, ay, bx, by)) {
75255
- intersects = true;
75256
- break;
75257
- }
75258
- }
75259
-
75260
- // case: bbox is entirely inside this ring
75261
- if (!intersects && geom.testPointInRing(left, bottom, path, arcs)) {
75262
- intersects = true;
75263
- }
75264
- return intersects ? path : null;
75265
- };
75266
- }
75267
-
75268
- // Return a function for testing if a shape (path or point) intersects a bounding box
75269
- // TODO: move this function to a different file
75270
- function getBBoxIntersectionTest(bbox, lyr, arcs) {
75271
- var filter = getBoundsIntersectionFilter(bbox, lyr, arcs);
75272
- return function(shapeId) {
75273
- var shp = lyr.shapes[shapeId];
75274
- if (!shp) return false;
75275
- for (var i=0; i<shp.length; i++) {
75276
- if (filter(shp[i])) return true;
75277
- }
75278
- return false;
75279
- };
75280
- }
75281
-
75282
- // return array of shape ids
75283
- function findShapesIntersectingBBox(bbox, lyr, arcs) {
75284
- var test = getBBoxIntersectionTest(bbox, lyr, arcs);
75285
- var ids = [];
75286
- for (var i=0; i<lyr.shapes.length; i++) {
75287
- if (test(i)) ids.push(i);
75288
- }
75289
- return ids;
75290
- }
75291
-
75292
- var FilterGeom = /*#__PURE__*/Object.freeze({
75293
- __proto__: null,
75294
- findShapesIntersectingBBox: findShapesIntersectingBBox,
75295
- getBBoxIntersectionTest: getBBoxIntersectionTest
75296
- });
75297
-
75298
- cmd.filterFeatures = function(lyr, arcs, opts) {
75299
- var records = lyr.data ? lyr.data.getRecords() : null,
75300
- shapes = lyr.shapes || null,
75301
- n = getFeatureCount(lyr),
75302
- filteredShapes = shapes ? [] : null,
75303
- filteredRecords = records ? [] : null,
75304
- filteredLyr = getOutputLayer(lyr, opts),
75305
- invert = !!opts.invert,
75306
- filter;
75307
-
75308
- if (opts.expression) {
75309
- filter = compileFeatureExpression(opts.expression, lyr, arcs);
75310
- }
75311
-
75312
- if (opts.ids) {
75313
- filter = combineFilters(filter, getIdFilter(opts.ids));
75314
- }
75315
-
75316
- if (opts.remove_empty) {
75317
- filter = combineFilters(filter, getNullGeometryFilter(lyr, arcs));
75318
- }
75319
-
75320
- if (opts.bbox) {
75321
- filter = combineFilters(filter, getBBoxIntersectionTest(opts.bbox, lyr, arcs));
75322
- }
75323
-
75324
- if (!filter) {
75325
- stop$1("Missing a filter criterion");
75326
- }
75327
-
75328
- utils.repeat(n, function(shapeId) {
75329
- var result = filter(shapeId);
75330
- requireBooleanResult(result);
75331
- if (invert) result = !result;
75332
- if (result === true) {
75333
- if (shapes) filteredShapes.push(shapes[shapeId] || null);
75334
- if (records) filteredRecords.push(records[shapeId] || null);
75335
- }
75336
- });
75337
-
75338
- if (filteredLyr == lyr) {
75339
- noteLayerWillChange(lyr, {operation: 'filter'});
75340
- }
75341
- filteredLyr.shapes = filteredShapes;
75342
- filteredLyr.data = filteredRecords ? new DataTable(filteredRecords) : null;
75343
- if (filteredLyr == lyr) {
75344
- markLayerChanged(lyr, {operation: 'filter'});
75345
- }
75346
- if (opts.no_replace) {
75347
- // if adding a layer, don't share objects between source and filtered layer
75348
- filteredLyr = copyLayer(filteredLyr);
75349
- }
75350
-
75351
- if (opts.verbose !== false && !opts.quiet) {
75352
- message(utils.format('Retained %,d of %,d features', getFeatureCount(filteredLyr), n));
75353
- }
75354
-
75355
- return filteredLyr;
75356
- };
75357
-
75358
- // TODO: update filter command to use this function
75359
- function filterLayerInPlace(lyr, filter, invert) {
75360
- var records = lyr.data ? lyr.data.getRecords() : null,
75361
- shapes = lyr.shapes || null,
75362
- n = getFeatureCount(lyr),
75363
- filteredShapes = shapes ? [] : null,
75364
- filteredRecords = records ? [] : null;
75365
- utils.repeat(n, function(shapeId) {
75366
- var result = filter(shapeId);
75367
- requireBooleanResult(result);
75368
- if (result === true) {
75369
- if (shapes) filteredShapes.push(shapes[shapeId] || null);
75370
- if (records) filteredRecords.push(records[shapeId] || null);
75371
- }
75372
- });
75373
- noteLayerWillChange(lyr, {operation: 'filterLayerInPlace'});
75374
- lyr.shapes = filteredShapes;
75375
- lyr.data = filteredRecords ? new DataTable(filteredRecords) : null;
75376
- markLayerChanged(lyr, {operation: 'filterLayerInPlace'});
75377
- }
75378
-
75379
- function getIdFilter(ids) {
75380
- var set = new Set(ids);
75381
- return function(i) {
75382
- return set.has(i);
75383
- };
75384
- }
75385
-
75386
- function getNullGeometryFilter(lyr, arcs) {
75387
- var shapes = lyr.shapes;
75388
- if (lyr.geometry_type == 'polygon') {
75389
- return getEmptyPolygonFilter(shapes, arcs);
75390
- }
75391
- return function(i) {return !!shapes[i];};
75392
- }
75393
-
75394
- function getEmptyPolygonFilter(shapes, arcs) {
75395
- return function(i) {
75396
- var shp = shapes[i];
75397
- return !!shp && geom.getPlanarShapeArea(shapes[i], arcs) > 0;
75398
- };
75399
- }
75400
-
75401
- function combineFilters(a, b) {
75402
- return a && b ? function(id) {
75403
- return a(id) && b(id);
75404
- } : (a || b);
75405
- }
75406
-
75407
76088
  cmd.evaluateEachFeature = function(lyr, dataset, expArg, opts) {
75408
76089
  var n = getFeatureCount(lyr),
75409
76090
  arcs = dataset.arcs,
@@ -79250,7 +79931,7 @@ ${svg}
79250
79931
  }
79251
79932
 
79252
79933
  cmd.pointToGrid = function(targetLayers, targetDataset, opts) {
79253
- targetLayers.forEach(requirePointLayer$1);
79934
+ targetLayers.forEach(requirePointLayer$2);
79254
79935
  if (opts.interval > 0 === false) {
79255
79936
  stop$1('Expected a non-negative interval parameter');
79256
79937
  }
@@ -82035,232 +82716,6 @@ ${svg}
82035
82716
  stopJob(job);
82036
82717
  };
82037
82718
 
82038
- // The icon names accepted by the icon= property of the -style command.
82039
- // Kept in a module of its own so that both the renderer (svg-symbols.mjs) and
82040
- // the -style command can use it without forming an import cycle.
82041
-
82042
- var iconNames = ['circle', 'square', 'ring', 'star'];
82043
-
82044
- function isSupportedIconName(name) {
82045
- return iconNames.indexOf(name) > -1;
82046
- }
82047
-
82048
- cmd.svgStyle = function(lyr, dataset, opts) {
82049
- var filterFn, table, fields, hasNewFields, optFields, clearedByPosition, fieldsBefore;
82050
-
82051
- function hadField(field) {
82052
- return field in fieldsBefore;
82053
- }
82054
-
82055
- if (getFeatureCount(lyr) === 0) {
82056
- return;
82057
- }
82058
- if (!lyr.data) {
82059
- initDataTable(lyr);
82060
- }
82061
- if (opts.where) {
82062
- filterFn = compileFeatureExpression(opts.where, lyr, dataset.arcs);
82063
- }
82064
- if (opts.ids) {
82065
- filterFn = combineFilters(filterFn, getIdFilter(opts.ids));
82066
- }
82067
- if (opts.clear) {
82068
- lyr.data.getFields().filter(isSupportedSvgStyleProperty).forEach(lyr.data.deleteField, lyr.data);
82069
- }
82070
- table = getLayerDataTable(lyr);
82071
- optFields = getOptionFields(opts);
82072
- fields = getStyleFields(optFields);
82073
- // Which of dx/dy/text-anchor a label-pos in this command clears: not the ones
82074
- // the same command also sets, so that `label-pos=n dx=3` keeps the nudge it
82075
- // was given instead of clearing it a moment later.
82076
- //
82077
- // Clearing at all is what keeps the shorthand usable, since a value on the
82078
- // record wins over the position: without it, setting a position on a label
82079
- // that had been dragged would appear to do nothing.
82080
- clearedByPosition = labelPositionDerivedFields.filter(function(field) {
82081
- return optFields.indexOf(field) == -1;
82082
- });
82083
- hasNewFields = fields.some(function(field) {
82084
- return !table.fieldExists(field);
82085
- });
82086
- // The table's columns as this command found them. Taken once, because the
82087
- // command adds to them as it runs, and a blanked property must be judged
82088
- // against what was there before rather than against what it has just made.
82089
- fieldsBefore = utils.arrayToIndex(table.getFields());
82090
- if (fields.length > 0) {
82091
- if (hasNewFields) {
82092
- table.captureSchemaBefore({operation: 'style', fields: fields});
82093
- } else {
82094
- table.captureFieldsBefore(fields, {operation: 'style'});
82095
- }
82096
- }
82097
- Object.keys(opts).forEach(function(optName) {
82098
- // undo cli parser name conversion; the regex must be global, or a
82099
- // property with more than one hyphen (e.g. label-start-offset) is silently
82100
- // skipped rather than applied
82101
- var svgName = optName.replace(/_/g, '-');
82102
- if (!isSupportedSvgStyleProperty(svgName)) {
82103
- return;
82104
- }
82105
- var strVal = opts[optName].trim();
82106
- // An empty value removes the property, rather than being rejected as an
82107
- // unparseable one. This is how a control gives a property back: a label
82108
- // dragged off its position clears label-pos, and there is otherwise no
82109
- // per-property unset -- only -style clear, which clears all of them.
82110
- var unset = strVal === '' && emptyValueUnsetsProperty(svgName);
82111
- var accessor = unset ? null : getSymbolPropertyAccessor(strVal, svgName, lyr);
82112
- var badIcons = svgName == 'icon' ? [] : null;
82113
- // Removing a position is not setting one, so it neither validates the
82114
- // value nor clears the offsets the position would have stood for.
82115
- var posOnPaths = svgName == 'label-pos' && !unset ? [] : null;
82116
- table.getRecords().forEach(function(rec, i) {
82117
- if (filterFn && !filterFn(i)) {
82118
- // make sure field exists if record is excluded by filter
82119
- setUndefinedFields(rec, [svgName]);
82120
- if (svgName == 'label-pos') {
82121
- // ...but a field the position would only have cleared is one this
82122
- // command is not writing anywhere, so an excluded record has nothing
82123
- // to stay consistent with
82124
- setUndefinedFields(rec, labelPositionDerivedFields, {has: hadField});
82125
- }
82126
- } else if (unset) {
82127
- // Nothing to remove, and so nothing to create: removing a property no
82128
- // record has would otherwise add an empty column for it.
82129
- if (hadField(svgName) || svgName in rec) rec[svgName] = undefined;
82130
- } else {
82131
- rec[svgName] = accessor(i);
82132
- if (badIcons) {
82133
- addUnsupportedIconName(badIcons, rec.icon);
82134
- }
82135
- if (posOnPaths) {
82136
- if (!parseLabelPosition(rec['label-pos'])) {
82137
- stop$1('Unexpected value for label-pos:', rec['label-pos']);
82138
- }
82139
- if (shapeIsPathLabel(lyr.shapes && lyr.shapes[i], rec)) {
82140
- // Not stored, so that ignoring it means ignoring it: a stored
82141
- // position would show up in the style panel and would start
82142
- // applying if the label ever lost all but one of its knots. Its
82143
- // text-anchor is left alone too -- that one does place text along a
82144
- // path, so a position that had no effect must not clear it.
82145
- posOnPaths.push(i);
82146
- rec['label-pos'] = undefined;
82147
- } else {
82148
- setUndefinedFields(rec, clearedByPosition, {overwrite: true, has: hadField});
82149
- }
82150
- }
82151
- }
82152
- });
82153
- if (badIcons && badIcons.length > 0) {
82154
- warn(formatUnsupportedIconMessage(badIcons));
82155
- }
82156
- if (posOnPaths && posOnPaths.length > 0) {
82157
- warn(formatPositionOnPathMessage(posOnPaths));
82158
- }
82159
- });
82160
- if (fields.length > 0) {
82161
- if (hasNewFields) {
82162
- table.markSchemaChanged({operation: 'style'});
82163
- } else {
82164
- table.markFieldsChanged(fields, {operation: 'style'});
82165
- }
82166
- }
82167
- };
82168
-
82169
- // The style properties this command was given, in SVG spelling.
82170
- function getOptionFields(opts) {
82171
- var fields = [];
82172
- Object.keys(opts).forEach(function(optName) {
82173
- var svgName = optName.replace(/_/g, '-');
82174
- if (isSupportedSvgStyleProperty(svgName)) addField(fields, svgName);
82175
- });
82176
- return fields;
82177
- }
82178
-
82179
- // The fields the command will write, which is what the undo capture covers.
82180
- // label-pos reaches dx/dy/text-anchor as well -- it no longer stores values in
82181
- // them, but it does clear them.
82182
- function getStyleFields(optFields) {
82183
- var fields = [];
82184
- optFields.forEach(function(svgName) {
82185
- addField(fields, svgName);
82186
- if (svgName == 'label-pos') {
82187
- labelPositionFields.forEach(function(field) {
82188
- addField(fields, field);
82189
- });
82190
- }
82191
- });
82192
- return fields;
82193
- }
82194
-
82195
- function addField(fields, field) {
82196
- if (fields.indexOf(field) == -1) {
82197
- fields.push(field);
82198
- }
82199
- }
82200
-
82201
- // Icon names can not be validated when options are parsed, because an icon=
82202
- // value may be a field name or an expression. The assigned values are checked
82203
- // instead, so that computed names are covered too. Unsupported names are a
82204
- // warning, not an error -- features with an unsupported name render without an
82205
- // icon.
82206
- var maxReportedIconNames = 4;
82207
-
82208
- function addUnsupportedIconName(names, val) {
82209
- var name;
82210
- if (!val) return; // a blank value removes the icon
82211
- name = String(val);
82212
- if (isSupportedIconName(name) || names.indexOf(name) > -1) return;
82213
- names.push(name);
82214
- }
82215
-
82216
- function formatUnsupportedIconMessage(names) {
82217
- var extra = names.length - maxReportedIconNames;
82218
- var listed = extra > 0 ? names.slice(0, maxReportedIconNames) : names;
82219
- var str = 'Unsupported icon ' + (names.length > 1 ? 'names' : 'name') + ': ' +
82220
- listed.join(', ');
82221
- if (extra > 0) {
82222
- str += ' (and ' + extra + ' more)';
82223
- }
82224
- return str + '. Expected one of: ' + iconNames.join(', ');
82225
- }
82226
-
82227
- // Adds @fields to @rec with no value, so that a record the filter excluded
82228
- // still has the same schema as the ones it kept. With overwrite, also blanks a
82229
- // value already there -- which is how setting a position takes back the offsets
82230
- // a label was carrying.
82231
- //
82232
- // @has: optional test for whether the layer carries a field at all. A field
82233
- // nobody has is not created in order to be blanked: -style label-pos=n clears
82234
- // dx, dy and text-anchor because a value on the record wins over the position,
82235
- // and there is nothing to win with when the column does not exist. Without
82236
- // this, clicking a position in the style panel put three empty columns in the
82237
- // user's table, and three empty columns in their CSV.
82238
- function setUndefinedFields(rec, fields, opts) {
82239
- var overwrite = !!(opts && opts.overwrite);
82240
- var has = opts && opts.has;
82241
- fields.forEach(function(field) {
82242
- if (has && !has(field) && field in rec === false) return;
82243
- if (overwrite || field in rec === false) {
82244
- rec[field] = undefined;
82245
- }
82246
- });
82247
- }
82248
-
82249
- // label-pos places text around an anchor point, which a label strung along a
82250
- // path does not have: its text runs from a start offset in the direction the
82251
- // path goes. A warning rather than an error, because a layer can hold both
82252
- // kinds of label and styling all of it at once is reasonable.
82253
- function formatPositionOnPathMessage(ids) {
82254
- var extra = ids.length - maxReportedIds;
82255
- var listed = (extra > 0 ? ids.slice(0, maxReportedIds) : ids).join(', ');
82256
- return 'Ignoring label-pos on ' + ids.length + ' path ' +
82257
- (ids.length > 1 ? 'labels' : 'label') + ' (' + listed +
82258
- (extra > 0 ? ' and ' + extra + ' more' : '') + '). ' +
82259
- 'Use label-start-offset= and text-anchor= to place text along a path.';
82260
- }
82261
-
82262
- var maxReportedIds = 4;
82263
-
82264
82719
  function getStickArrowCoords(d) {
82265
82720
  return getArrowCoords(d, 'stick');
82266
82721
  }
@@ -82792,7 +83247,7 @@ ${svg}
82792
83247
 
82793
83248
  // TODO: refactor to remove duplication in mapshaper-svg-style.js
82794
83249
  cmd.symbols = function(inputLyr, dataset, opts) {
82795
- requirePointLayer$1(inputLyr);
83250
+ requirePointLayer$2(inputLyr);
82796
83251
  var lyr = opts.no_replace ? copyLayer(inputLyr) : inputLyr;
82797
83252
  var shapeMode = !!opts.geographic;
82798
83253
  var metersPerPx;
@@ -83393,6 +83848,9 @@ ${svg}
83393
83848
  if (name == 'rectangle') {
83394
83849
  return !opts.source && !opts.bbox;
83395
83850
  }
83851
+ if (name == 'labels') {
83852
+ return !('coordinates' in opts);
83853
+ }
83396
83854
  return [
83397
83855
  'affine', 'alpha-shapes', 'blur', 'buffer', 'calc', 'check-geometry',
83398
83856
  'classify', 'clean', 'clip', 'cluster', 'contours', 'dashlines', 'data-fill',
@@ -83407,7 +83865,10 @@ ${svg}
83407
83865
  ].indexOf(name) > -1;
83408
83866
  }
83409
83867
 
83410
- function commandAcceptsEmptyTarget(name) {
83868
+ function commandAcceptsEmptyTarget(name, opts) {
83869
+ if (name == 'labels') {
83870
+ return 'coordinates' in opts;
83871
+ }
83411
83872
  return name == 'graticule' || name == 'i' || name == 'help' ||
83412
83873
  name == 'point-grid' || name == 'shape' || name == 'rectangle' || name == 'frame' ||
83413
83874
  name == 'require' || name == 'run' || name == 'define' ||
@@ -83503,7 +83964,7 @@ ${svg}
83503
83964
  stop$1(utils.format('Missing target: %s\nAvailable layers: %s',
83504
83965
  opts.target, getFormattedLayerList(job.catalog)));
83505
83966
  }
83506
- if (!commandAcceptsEmptyTarget(name)) {
83967
+ if (!commandAcceptsEmptyTarget(name, opts)) {
83507
83968
  stop$1("No data is available");
83508
83969
  }
83509
83970
  }
@@ -83526,7 +83987,7 @@ ${svg}
83526
83987
  }
83527
83988
  outputLayers = cmd.addShape(targetLayers, targetDataset, opts);
83528
83989
 
83529
- } else if (name == 'add-label') {
83990
+ } else if (name == 'add-label' || name == 'labels' && 'coordinates' in opts) {
83530
83991
  if (!targetDataset) {
83531
83992
  targetDataset = {info: {}, layers: []};
83532
83993
  targetLayers = targetDataset.layers;
@@ -83534,6 +83995,9 @@ ${svg}
83534
83995
  }
83535
83996
  outputLayers = cmd.addLabel(targetLayers, targetDataset, opts);
83536
83997
 
83998
+ } else if (name == 'labels') {
83999
+ outputLayers = cmd.labels(targetLayers, targetDataset, opts);
84000
+
83537
84001
  } else if (name == 'add-layer') {
83538
84002
  // The new layer arrives in a dataset of its own, so it is added to the
83539
84003
  // catalog here rather than integrated into the target below.
@@ -84111,7 +84575,7 @@ ${svg}
84111
84575
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
84112
84576
  }
84113
84577
 
84114
- var version = "0.7.68";
84578
+ var version = "0.7.70";
84115
84579
 
84116
84580
  // Parse command line args into commands and run them
84117
84581
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -84926,7 +85390,7 @@ ${svg}
84926
85390
  // parse. Null is what every reader already falls back from.
84927
85391
  function measureLabelText(rec) {
84928
85392
  var text = toLabelString(rec && rec['label-text']);
84929
- var font, fontSize, spacing, width;
85393
+ var font, fontSize, spacing, width, failed = false;
84930
85394
  if (!text) return null;
84931
85395
  font = getFontForRecord(rec);
84932
85396
  if (!font) return null;
@@ -84935,10 +85399,22 @@ ${svg}
84935
85399
  if (!(fontSize > 0)) return null;
84936
85400
  // The widest line, which is what the block of a multi-line label is as wide
84937
85401
  // as, and what the browser's getBBox() reports for the same text.
84938
- width = splitLabelLines(text).reduce(function(max, line) {
84939
- 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);
84940
85415
  return w > max ? w : max;
84941
85416
  }, 0);
85417
+ if (failed) return null;
84942
85418
  return width > 0 ? width : null;
84943
85419
  }
84944
85420
 
@@ -84962,9 +85438,12 @@ ${svg}
84962
85438
  // The font a record is drawn in, opened and remembered. A record with no
84963
85439
  // font-family is drawn in the layer group's default, the same one the GUI
84964
85440
  // measures against -- see getLabelTextDefaults().
84965
- 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) {
84966
85445
  var family = rec['font-family'] || 'sans-serif';
84967
- var weight = getFontWeight(rec);
85446
+ var weight = bold ? WEIGHT_NAMES[LABEL_BOLD_WEIGHT] : getFontWeight(rec);
84968
85447
  var italic = isItalic(rec);
84969
85448
  var stretch = rec['font-stretch'] || '';
84970
85449
  var key = [family, weight, italic ? 'i' : 'n', stretch].join('|');
@@ -86713,7 +87192,7 @@ ${svg}
86713
87192
  internal.svg = Object.assign({}, SvgStringify, SvgPathUtils, GeojsonToSvg,
86714
87193
  SvgFeatureUtils,
86715
87194
  SvgLabels, SvgSymbols, SvgLabelPaths, SvgLabelFit, SvgLabelAlign,
86716
- SvgLabelMetrics, SvgLabelHalo, SvgLabelCallout);
87195
+ SvgLabelMetrics, SvgLabelHalo, SvgLabelCallout, SvgLabelMarkup);
86717
87196
 
86718
87197
  // Reached through the bundle rather than imported from source, unlike most of
86719
87198
  // what tests use, because these modules load fs and fontkit through the