mapshaper 0.7.67 → 0.7.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mapshaper.js CHANGED
@@ -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
@@ -27236,9 +27310,16 @@
27236
27310
  // The two copies are identical apart from their paint, so they sit in exactly
27237
27311
  // the same place whatever the label's position, alignment or path.
27238
27312
  //
27239
- // The halo copy is unfilled. That matches paint-order, where the fill covers
27240
- // the stroke's inner half and nothing else, and it keeps a translucent fill
27241
- // from showing a second colour through it.
27313
+ // The halo copy is unfilled, which matches paint-order, where the fill covers
27314
+ // the stroke's inner half and nothing else. Its halo-opacity is written as
27315
+ // opacity rather than stroke-opacity, which Illustrator ignores on import; the
27316
+ // two look the same on a copy with nothing painted but its stroke.
27317
+ //
27318
+ // Illustrator's GPU preview compounds a translucent halo where glyphs
27319
+ // overlap; its CPU preview and its output do not. Nothing in the SVG was
27320
+ // found to avoid it: filling the copy (imported as two objects, and no
27321
+ // better), and putting the opacity on a group or two nested groups around
27322
+ // the copy, all show the same thing.
27242
27323
  //
27243
27324
  // Things that apply to the label as a whole move to the group: its transform,
27244
27325
  // which places it, and its opacity, which would otherwise fade each copy on its
@@ -27269,6 +27350,10 @@
27269
27350
  delete halo.properties['paint-order'];
27270
27351
  delete halo.properties['fill-opacity'];
27271
27352
  halo.properties.fill = 'none';
27353
+ if ('stroke-opacity' in halo.properties) {
27354
+ halo.properties.opacity = halo.properties['stroke-opacity'];
27355
+ delete halo.properties['stroke-opacity'];
27356
+ }
27272
27357
  HALO_STROKE_PROPERTIES.forEach(function(k) {
27273
27358
  delete text.properties[k];
27274
27359
  });
@@ -27340,6 +27425,23 @@
27340
27425
  // different one would put the label somewhere it is not drawn.
27341
27426
  var DEFAULT_LABEL_FONT_SIZE = 12;
27342
27427
 
27428
+ // The line height a label is drawn with when it carries none, as a multiple of
27429
+ // its font size.
27430
+ var DEFAULT_LINE_HEIGHT = 1.1;
27431
+
27432
+ // The dy of each line after a label's first, from its line-height.
27433
+ //
27434
+ // A bare number is a multiple of the font size, as in CSS, and not px, as a
27435
+ // bare dy would be in SVG -- so it is written in ems. Nobody who types 1.3
27436
+ // into a field called line height wants lines 1.3px apart, and a multiple
27437
+ // keeps its proportions when the font size changes. Values with units are
27438
+ // passed through.
27439
+ function getLineHeightDy(val) {
27440
+ var measure = val === undefined || val === null || val === '' ?
27441
+ DEFAULT_LINE_HEIGHT : parseSvgMeasure(val);
27442
+ return utils.isFiniteNumber(measure) ? measure + 'em' : measure;
27443
+ }
27444
+
27343
27445
  function toLabelString(val) {
27344
27446
  if (val || val === 0 || val === false) return String(val);
27345
27447
  return '';
@@ -27387,7 +27489,7 @@
27387
27489
  value: line,
27388
27490
  properties: {
27389
27491
  x: dx,
27390
- dy: rec['line-height'] || '1.1em'
27492
+ dy: getLineHeightDy(rec['line-height'])
27391
27493
  }
27392
27494
  };
27393
27495
  obj.children.push(tspan);
@@ -27481,8 +27583,10 @@
27481
27583
  var SvgLabels = /*#__PURE__*/Object.freeze({
27482
27584
  __proto__: null,
27483
27585
  DEFAULT_LABEL_FONT_SIZE: DEFAULT_LABEL_FONT_SIZE,
27586
+ DEFAULT_LINE_HEIGHT: DEFAULT_LINE_HEIGHT,
27484
27587
  LABEL_SOFT_BREAK: LABEL_SOFT_BREAK,
27485
27588
  getDrawnLabelOffset: getDrawnLabelOffset,
27589
+ getLineHeightDy: getLineHeightDy,
27486
27590
  labelNewlineRxp: labelNewlineRxp,
27487
27591
  removeSoftBreaks: removeSoftBreaks,
27488
27592
  renderLabel: renderLabel$1,
@@ -27505,7 +27609,6 @@
27505
27609
 
27506
27610
  var DEFAULT_PADDING$1 = 3;
27507
27611
  var DEFAULT_LINE_WIDTH = 1;
27508
- var DEFAULT_LINE_HEIGHT = '1.1em';
27509
27612
  // Space left between the anchor's symbol and the end of the line
27510
27613
  var SYMBOL_CLEARANCE = 2;
27511
27614
  // How far an automatic curve bows out, as a fraction of its chord
@@ -27526,6 +27629,9 @@
27526
27629
  var ASCENT = 0.8;
27527
27630
  var DESCENT = 0.2;
27528
27631
  var MIDLINE = 0.35;
27632
+ // Average advance of a character, in ems, for estimating the width of text
27633
+ // that cannot be measured
27634
+ var AVERAGE_CHAR_WIDTH = 0.6;
27529
27635
 
27530
27636
  function labelHasCallout(rec) {
27531
27637
  var type = rec && rec.callout ? parseCalloutType(rec.callout) : null;
@@ -27589,18 +27695,24 @@
27589
27695
  // no measurement the box takes the label's width if it is a text block, and is
27590
27696
  // otherwise a vertical line through the text's origin -- a callout then meets
27591
27697
  // the text where it starts rather than not being drawn.
27592
- function getLabelTextBox(rec) {
27698
+ //
27699
+ // opts.estimate_width: with no measurement and no label-width, estimate the
27700
+ // width from the length of the longest line instead of taking it as zero
27701
+ function getLabelTextBox(rec, opts) {
27593
27702
  var fontSize = getFontSizeInPx$1(rec);
27594
27703
  var offset = getDrawnLabelOffset(rec);
27595
27704
  var lines = splitLabelLines(toLabelString(rec['label-text']));
27596
- var lineHeight = measureToPx(rec['line-height'] || DEFAULT_LINE_HEIGHT, fontSize);
27705
+ var lineHeight = measureToPx(getLineHeightDy(rec['line-height']), fontSize);
27597
27706
  var width = getMeasuredTextWidth(rec) || getNumber(rec['label-width'], 0);
27707
+ if (!width && opts && opts.estimate_width) {
27708
+ width = estimateTextWidth(lines, fontSize);
27709
+ }
27598
27710
  // An anchor the record does not set is inherited from the layer's group,
27599
27711
  // which is 'middle' -- see getLabelTextDefaults().
27600
27712
  var anchor = offset['text-anchor'] || 'middle';
27601
27713
  var xmin = offset.dx - width * (anchor == 'middle' ? 0.5 : anchor == 'end' ? 1 : 0);
27602
27714
  var baseline = offset.dy + getBaselineShift(rec['dominant-baseline']) * fontSize;
27603
- if (!(lineHeight > 0)) lineHeight = fontSize * 1.1;
27715
+ if (!(lineHeight > 0)) lineHeight = fontSize * DEFAULT_LINE_HEIGHT;
27604
27716
  return {
27605
27717
  xmin: xmin,
27606
27718
  xmax: xmin + width,
@@ -27610,6 +27722,18 @@
27610
27722
  };
27611
27723
  }
27612
27724
 
27725
+ function estimateTextWidth(lines, fontSize) {
27726
+ var chars = lines.reduce(function(max, line) {
27727
+ return Math.max(max, Array.from(line).length);
27728
+ }, 0);
27729
+ return chars * AVERAGE_CHAR_WIDTH * fontSize;
27730
+ }
27731
+
27732
+ // The size a label's text is drawn at, in px
27733
+ function getLabelFontSize(rec) {
27734
+ return getFontSizeInPx$1(rec);
27735
+ }
27736
+
27613
27737
  // Where a line meets a callout's text and how it gets there from the anchor.
27614
27738
  //
27615
27739
  // o.type 'line', 'elbow' or 'curve'
@@ -27636,7 +27760,7 @@
27636
27760
  // point where the line meets it, the elbow's corner or the curve's midpoint
27637
27761
  // (null for a straight line), and where the line stops short of the anchor.
27638
27762
  function getCalloutShape(o) {
27639
- var box = padBox(o.box, o.padding || 0);
27763
+ var box = padBox$1(o.box, o.padding || 0);
27640
27764
  var a = [0, 0];
27641
27765
  var size = o.endSize > 0 ? o.endSize : getDefaultCalloutEndSize(o.end, o.width);
27642
27766
  var t, v, path, tip, dir, out, lineWidth, headLen, curved;
@@ -27997,7 +28121,7 @@
27997
28121
 
27998
28122
  // Utilities
27999
28123
 
28000
- function padBox(box, pad) {
28124
+ function padBox$1(box, pad) {
28001
28125
  return {
28002
28126
  xmin: box.xmin - pad,
28003
28127
  xmax: box.xmax + pad,
@@ -28070,6 +28194,7 @@
28070
28194
  getControlPoint: getControlPoint,
28071
28195
  getDefaultCalloutEndSize: getDefaultCalloutEndSize,
28072
28196
  getLabelCalloutShape: getLabelCalloutShape,
28197
+ getLabelFontSize: getLabelFontSize,
28073
28198
  getLabelTextBox: getLabelTextBox,
28074
28199
  labelHasCallout: labelHasCallout,
28075
28200
  renderLabelCallout: renderLabelCallout
@@ -28366,28 +28491,68 @@
28366
28491
 
28367
28492
  var SvgSymbols = /*#__PURE__*/Object.freeze({
28368
28493
  __proto__: null,
28494
+ featureHasIcon: featureHasIcon,
28369
28495
  getAnchorSymbolRadius: getAnchorSymbolRadius,
28496
+ getIconRadius: getIconRadius,
28370
28497
  getTransform: getTransform,
28371
28498
  renderPoint: renderPoint,
28372
28499
  symbolRenderers: symbolRenderers
28373
28500
  });
28374
28501
 
28502
+ // Command options that are stored in the scalebar's data record
28503
+ var scalebarFields = ['label', 'units', 'style', 'font_size', 'font_family',
28504
+ 'font_style', 'font_weight', 'color', 'tic_length', 'bar_width', 'label_offset',
28505
+ 'position', 'label_position', 'dual_units', 'margin'];
28506
+
28507
+ // With a map frame, the scalebar belongs to the frame, and running the
28508
+ // command again replaces it. Without a frame, each run adds a standalone
28509
+ // scalebar layer, which is exported if it is included in the output layers.
28375
28510
  cmd.scalebar = function(catalog, opts) {
28511
+ var frame = getActiveFrame(catalog);
28512
+ if (opts.remove) {
28513
+ if (!frame || !setFrameFurnitureLayer(frame.dataset, 'scalebar', null)) {
28514
+ stop$1('The map frame has no scalebar to remove');
28515
+ }
28516
+ return;
28517
+ }
28518
+ validateScalebarOpts(opts);
28376
28519
  var lyr = getScalebarLayer(opts);
28377
- if (opts.label && !parseScalebarUnits(opts.label)) {
28378
- stop$1(`Expected units of km or miles in scalebar label (received ${opts.label})`);
28520
+ if (frame) {
28521
+ setFrameFurnitureLayer(frame.dataset, 'scalebar', lyr);
28522
+ } else {
28523
+ addFurnitureLayer(lyr, catalog);
28379
28524
  }
28380
- addFurnitureLayer(lyr, catalog);
28381
28525
  };
28382
28526
 
28383
28527
  function getScalebarLayer(opts) {
28384
- var obj = utils.defaults({type: 'scalebar'}, opts);
28528
+ var rec = {type: 'scalebar'};
28529
+ scalebarFields.forEach(function(k) {
28530
+ if (opts[k] !== undefined && opts[k] !== null && opts[k] !== '') {
28531
+ rec[k] = opts[k];
28532
+ }
28533
+ });
28385
28534
  return {
28386
- name: opts.name || 'scalebar',
28387
- data: new DataTable([obj])
28535
+ name: 'scalebar',
28536
+ data: new DataTable([rec])
28388
28537
  };
28389
28538
  }
28390
28539
 
28540
+ function validateScalebarOpts(opts) {
28541
+ if (opts.label) {
28542
+ splitScalebarLabel(opts.label).forEach(function(str) {
28543
+ if (!parseScalebarUnits(str) && !isBareDistance(str)) {
28544
+ stop$1(`Expected a distance or units of km, meters, miles or feet in scalebar label (received ${str})`);
28545
+ }
28546
+ });
28547
+ }
28548
+ if (opts.units && !parseLabelUnits(opts.units)) {
28549
+ stop$1('Unsupported scalebar units:', opts.units);
28550
+ }
28551
+ if (opts.style && !/^[ab]$/i.test(opts.style)) {
28552
+ stop$1('Unsupported scalebar style:', opts.style);
28553
+ }
28554
+ }
28555
+
28391
28556
  function renderScalebar(d, frame) {
28392
28557
  if (!frame.crs) {
28393
28558
  message('Unable to render scalebar: unknown CRS.');
@@ -28400,14 +28565,15 @@
28400
28565
  var opts = getScalebarOpts(d);
28401
28566
  var metersPerPx = getMapFrameMetersPerPixel(frame);
28402
28567
  var frameWidthPx = frame.width;
28403
- var labels = d.label ? d.label.split(',') : null;
28404
- var label1 = labels && labels[0] || null;
28405
- var props1 = parseScalebarLabel(label1 || getAutoScalebarLabel(frameWidthPx, metersPerPx, 'mile'));
28406
- var label2 = opts.style == 'b' && labels && labels[1] || null;
28568
+ var labels = d.label ? splitScalebarLabel(d.label) : [];
28569
+ var units = parseLabelUnits(d.units) || 'mile';
28570
+ var label1 = getScalebarLabel(labels[0], frameWidthPx, metersPerPx, units);
28571
+ var props1 = parseScalebarLabel(label1);
28572
+ var label2 = null;
28407
28573
  var props2, length2;
28408
28574
 
28409
28575
  if (props1.km > 0 === false) {
28410
- message('Unusable scalebar label:', label1);
28576
+ message('Unusable scalebar label:', labels[0] || '');
28411
28577
  return [];
28412
28578
  }
28413
28579
 
@@ -28416,6 +28582,11 @@
28416
28582
  stop$1("Null scalebar length");
28417
28583
  }
28418
28584
 
28585
+ if (opts.style == 'b' && labels[1]) {
28586
+ label2 = completeLabel(labels[1], getOtherUnits(props1.units));
28587
+ } else if (opts.style == 'b' && opts.dual_units) {
28588
+ label2 = getAutoSecondLabel(length1, metersPerPx, getOtherUnits(props1.units));
28589
+ }
28419
28590
  if (label2) {
28420
28591
  props2 = parseScalebarLabel(label2);
28421
28592
  length2 = Math.round(props2.km / metersPerPx * 1000);
@@ -28468,8 +28639,13 @@
28468
28639
  };
28469
28640
 
28470
28641
  function getScalebarOpts(d) {
28471
- var style = d.style == 'b' || d.style == 'B' ? 'b' : 'a';
28472
- return Object.assign({}, defaultOpts, styleOpts[style], d, {style: style});
28642
+ var style = d.style == 'b' || d.style == 'B' || !d.style && d.dual_units ? 'b' : 'a';
28643
+ var opts = Object.assign({}, defaultOpts, styleOpts[style]);
28644
+ Object.keys(d).forEach(function(k) {
28645
+ if (d[k] !== undefined && d[k] !== null && d[k] !== '') opts[k] = d[k];
28646
+ });
28647
+ opts.style = style;
28648
+ return opts;
28473
28649
  }
28474
28650
 
28475
28651
  function renderAsSvg(length, text, length2, text2, opts) {
@@ -28487,12 +28663,11 @@
28487
28663
  };
28488
28664
  }
28489
28665
 
28490
- // TODO: generalize to other kinds of furniture as they are developed
28491
28666
  function getScalebarPosition(opts) {
28492
28667
  var pos = opts.position || 'top-left';
28493
28668
  return {
28494
- ypos: pos.includes('top') ? 'top' : 'bottom',
28495
- xpos: pos.includes('left') ? 'left' : 'right'
28669
+ ypos: pos.includes('bottom') ? 'bottom' : 'top',
28670
+ xpos: pos.includes('right') ? 'right' : 'left'
28496
28671
  };
28497
28672
  }
28498
28673
 
@@ -28506,12 +28681,16 @@
28506
28681
  'label-text': text,
28507
28682
  'font-size': opts.font_size,
28508
28683
  'text-anchor': labelPos.anchor,
28509
- 'dominant-baseline': labelPos.ypos == 'top' ? 'auto' : 'hanging'
28684
+ 'dominant-baseline': labelPos.ypos == 'top' ? 'auto' : 'hanging',
28510
28685
  //// 'dominant-baseline': labelPos == 'top' ? 'text-after-edge' : 'text-before-edge'
28511
28686
  // 'text-after-edge' is buggy in Safari and unsupported by Illustrator,
28512
28687
  // so I'm using 'hanging' and 'auto', which seem to be well supported.
28513
28688
  // downside: requires a kludgy multiplier to calculate scalebar height (see above)
28514
28689
  };
28690
+ if (opts.color) labelOpts.fill = opts.color;
28691
+ if (opts.font_family) labelOpts['font-family'] = opts.font_family;
28692
+ if (opts.font_style) labelOpts['font-style'] = opts.font_style;
28693
+ if (opts.font_weight) labelOpts['font-weight'] = opts.font_weight;
28515
28694
  return symbolRenderers.label(labelOpts, anchorX, anchorY);
28516
28695
  }
28517
28696
 
@@ -28571,7 +28750,7 @@
28571
28750
  }
28572
28751
  var bar = importMultiLineString(coords);
28573
28752
  Object.assign(bar.properties, {
28574
- stroke: 'black',
28753
+ stroke: opts.color || 'black',
28575
28754
  fill: 'none',
28576
28755
  'stroke-width': opts.bar_width,
28577
28756
  'stroke-linecap': 'butt',
@@ -28580,35 +28759,132 @@
28580
28759
  return bar;
28581
28760
  }
28582
28761
 
28583
- // unit: 'km' || 'mile'
28762
+ // Kilometers per unit
28763
+ var unitSizes = {
28764
+ km: 1,
28765
+ m: 0.001,
28766
+ mile: 1.60934,
28767
+ ft: 0.0003048
28768
+ };
28769
+
28770
+ // Candidate lengths for automatic labels, in ascending order of distance
28771
+ var autoLengths = {
28772
+ imperial: [
28773
+ ['ft', '10 20 25 50 100 200 250 500'],
28774
+ // note: removed 1.5 12 and 1,200
28775
+ ['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 ' +
28776
+ '300 350 400 500 750 1,000 1,500 2,000 2,500 3,000 4,000 5,000']
28777
+ ],
28778
+ metric: [
28779
+ ['m', '5 10 20 25 50 100 200 250 500'],
28780
+ ['km', '1 2 3 4 5 8 10 15 20 25 30 40 50 75 100 150 200 250 300 350 ' +
28781
+ '400 500 750 1,000 1,500 2,000 2,500 3,000 4,000 5,000']
28782
+ ]
28783
+ };
28784
+
28785
+ function getAutoLabels(system) {
28786
+ return autoLengths[system].reduce(function(memo, group) {
28787
+ return memo.concat(group[1].split(' ').map(function(str) {
28788
+ return formatDistanceLabel(str, group[0]);
28789
+ }));
28790
+ }, []);
28791
+ }
28792
+
28793
+ function getUnitSystem(units) {
28794
+ return units == 'km' || units == 'm' ? 'metric' : 'imperial';
28795
+ }
28796
+
28797
+ function getOtherUnits(units) {
28798
+ return getUnitSystem(units) == 'metric' ? 'mile' : 'km';
28799
+ }
28800
+
28801
+ // The unit system of the units= option: returns 'km', 'mile' or ''
28802
+ function parseUnitsOption(str) {
28803
+ var units = parseLabelUnits(str);
28804
+ return units ? (getUnitSystem(units) == 'metric' ? 'km' : 'mile') : '';
28805
+ }
28806
+
28807
+ // Accepts the units= option, which also gives the units of a label that is a
28808
+ // bare number; returns 'km', 'm', 'mile', 'ft' or ''
28809
+ function parseLabelUnits(str) {
28810
+ var s = String(str || '').toLowerCase();
28811
+ if (/^(km|kilomet(er|re)s?|metric)$/.test(s)) return 'km';
28812
+ if (/^(m|meters?|metres?)$/.test(s)) return 'm';
28813
+ if (/^(mi|miles?|imperial)$/.test(s)) return 'mile';
28814
+ if (/^(ft|feet)$/.test(s)) return 'ft';
28815
+ return '';
28816
+ }
28817
+
28818
+ // Splits a dual-unit label like "100 km,50 miles". Only a comma after the units
28819
+ // separates two distances, so "1,000 km" and a bare "1,500" stay whole.
28820
+ function splitScalebarLabel(label) {
28821
+ return String(label).split(/(?<=[^\d\s]),\s*/).map(function(str) {
28822
+ return str.trim();
28823
+ }).filter(Boolean);
28824
+ }
28825
+
28826
+ // A distance with no units, e.g. "150" or "1/2" or "1,000"
28827
+ function isBareDistance(str) {
28828
+ return /^[\d\s.,/]+$/.test(str) && parseScalebarNumber(str) > 0;
28829
+ }
28830
+
28831
+ // Adds units to a label that is a bare number
28832
+ function completeLabel(label, units) {
28833
+ return isBareDistance(label) ? formatDistanceLabel(label.trim(), units) : label;
28834
+ }
28835
+
28836
+ // label: a label from the label= option, or empty for an automatic label
28837
+ function getScalebarLabel(label, frameWidthPx, metersPerPx, defaultUnits) {
28838
+ var units;
28839
+ if (label && parseScalebarNumber(label) > 0) return completeLabel(label, defaultUnits);
28840
+ // A label with units but no number gets an automatic length in those units
28841
+ units = label && parseScalebarUnits(label) || defaultUnits;
28842
+ return getAutoScalebarLabel(frameWidthPx, metersPerPx, units);
28843
+ }
28844
+
28845
+ // unit: 'km' || 'mile' (or 'm', 'ft' for the same systems)
28846
+ // The longest length that fits within 20% of the map's width, or the shortest
28847
+ // that is at least 70px long if the map is too narrow for that
28584
28848
  function getAutoScalebarLabel(mapWidth, metersPerPx, unit) {
28585
- var minWidth = 70; // 100; // TODO: vary min size based on map width
28586
- var minKm = metersPerPx * minWidth / 1000;
28587
- // note: removed 1.5 12 and 1,200
28588
- var options = ('1/8 1/5 1/4 1/2 1 2 3 4 5 8 10 15 20 25 30 40 50 75 ' +
28589
- '100 150 200 250 300 350 400 500 750 1,000 1,500 2,000 ' +
28590
- '2,500 3,000 4,000 5,000').split(' ');
28591
- return options.reduce(function(memo, str) {
28592
- if (memo) return memo;
28593
- var label = formatDistanceLabel(str, unit);
28594
- if (parseScalebarLabelToKm(label) > minKm) {
28595
- return label;
28596
- }
28597
- }, null) || '';
28849
+ var minKm = metersPerPx * 70 / 1000;
28850
+ var maxKm = metersPerPx * mapWidth * 0.2 / 1000;
28851
+ var labels = getAutoLabels(getUnitSystem(unit));
28852
+ var longest = labels.filter(function(label) {
28853
+ var km = parseScalebarLabelToKm(label);
28854
+ return km >= minKm && km <= maxKm;
28855
+ }).pop();
28856
+ return longest || labels.find(function(label) {
28857
+ return parseScalebarLabelToKm(label) >= minKm;
28858
+ }) || '';
28859
+ }
28860
+
28861
+ // The longest automatic length in the given units that fits within the
28862
+ // first part of a dual-unit scalebar
28863
+ function getAutoSecondLabel(maxPx, metersPerPx, unit) {
28864
+ var maxKm = maxPx * metersPerPx / 1000;
28865
+ var minKm = maxKm / 4;
28866
+ var labels = getAutoLabels(getUnitSystem(unit)).filter(function(label) {
28867
+ var km = parseScalebarLabelToKm(label);
28868
+ return km <= maxKm && km >= minKm;
28869
+ });
28870
+ return labels.pop() || null;
28598
28871
  }
28599
28872
 
28600
28873
  function formatDistanceLabel(numStr, unit) {
28601
28874
  var num = parseScalebarNumber(numStr);
28602
- var unitStr = unit == 'km' && 'KM' || num > 1 && 'MILES' || 'MILE';
28875
+ var unitStr = unit == 'km' && 'KM' ||
28876
+ unit == 'm' && (num > 1 ? 'METERS' : 'METER') ||
28877
+ unit == 'ft' && (num > 1 ? 'FEET' : 'FOOT') ||
28878
+ num > 1 && 'MILES' || 'MILE';
28603
28879
  return numStr + ' ' + unitStr;
28604
28880
  }
28605
28881
 
28606
- // See test/mapshaper-scalebar.js for examples of supported formats
28882
+ // See test/scalebar-test.mjs for examples of supported formats
28607
28883
  function parseScalebarLabelToKm(str) {
28608
28884
  var units = parseScalebarUnits(str);
28609
28885
  var value = parseScalebarNumber(str);
28610
28886
  if (!units || !value) return NaN;
28611
- return units == 'mile' ? value * 1.60934 : value;
28887
+ return value * unitSizes[units];
28612
28888
  }
28613
28889
 
28614
28890
  function parseScalebarLabel(label) {
@@ -28616,7 +28892,7 @@
28616
28892
  var units = label ? parseScalebarUnits(label) : 'mile';
28617
28893
  var km = NaN;
28618
28894
  if (units && num) {
28619
- km = units == 'mile' ? num * 1.60934 : num;
28895
+ km = num * unitSizes[units];
28620
28896
  }
28621
28897
  return {
28622
28898
  number: num,
@@ -28625,11 +28901,14 @@
28625
28901
  };
28626
28902
  }
28627
28903
 
28904
+ // Returns 'km', 'm', 'mile', 'ft' or ''
28628
28905
  function parseScalebarUnits(str) {
28629
- var isMiles = /(miles?|mi[.]?|英里)$/.test(str.toLowerCase());
28630
- var isKm = /(k\.m\.|km|kilometers?|kilom.tres?|公里)$/.test(str.toLowerCase());
28631
- var units = isMiles && 'mile' || isKm && 'km' || '';
28632
- return units;
28906
+ var s = String(str).trim().toLowerCase();
28907
+ if (/(miles?|mi[.]?|英里)$/.test(s)) return 'mile';
28908
+ if (/(k\.m\.|km|kilomet(er|re)s?|kilom.tres?|公里)$/.test(s)) return 'km';
28909
+ if (/(^|[\d\s.])(feet|foot|ft[.]?|英尺)$/.test(s)) return 'ft';
28910
+ if (/(^|[\d\s.])(m|m\.|meters?|metres?|mètres?|米)$/.test(s)) return 'm';
28911
+ return '';
28633
28912
  }
28634
28913
 
28635
28914
  function parseScalebarNumber(str) {
@@ -28649,8 +28928,12 @@
28649
28928
  __proto__: null,
28650
28929
  formatDistanceLabel: formatDistanceLabel,
28651
28930
  getScalebarLayer: getScalebarLayer,
28931
+ parseLabelUnits: parseLabelUnits,
28652
28932
  parseScalebarLabelToKm: parseScalebarLabelToKm,
28653
- renderScalebar: renderScalebar
28933
+ parseScalebarUnits: parseScalebarUnits,
28934
+ parseUnitsOption: parseUnitsOption,
28935
+ renderScalebar: renderScalebar,
28936
+ splitScalebarLabel: splitScalebarLabel
28654
28937
  });
28655
28938
 
28656
28939
  var furnitureRenderers = {
@@ -28672,24 +28955,33 @@
28672
28955
  function renderFurnitureLayer(lyr, frame) {
28673
28956
  var d = getFurnitureLayerData(lyr);
28674
28957
  var renderer = furnitureRenderers[d.type];
28958
+ var problem;
28675
28959
  if (!renderer) {
28676
28960
  stop$1('Missing renderer for', d.type, 'element');
28677
28961
  }
28678
- if (!frame.crs) {
28679
- stop$1(`Unable to render ${d.type} (unknown map projection)`);
28680
- }
28681
- if (!isProjectedCRS(frame.crs)) {
28682
- stop$1(`Unable to render ${d.type} (map is unprojected)`);
28962
+ problem = getFurnitureFrameProblem(frame);
28963
+ if (problem) {
28964
+ stop$1(`Unable to render ${d.type} (${problem})`);
28683
28965
  }
28684
28966
  return renderer(d, frame) || [];
28685
28967
  }
28686
28968
 
28969
+ // Returns a reason why furniture can't be drawn in a frame, or null
28970
+ function getFurnitureFrameProblem(frame) {
28971
+ if (!frame) return 'missing map frame';
28972
+ if (!frame.crs) return 'unknown map projection';
28973
+ if (!isProjectedCRS(frame.crs)) return 'map is unprojected';
28974
+ return null;
28975
+ }
28976
+
28687
28977
  var Furniture = /*#__PURE__*/Object.freeze({
28688
28978
  __proto__: null,
28979
+ getFurnitureFrameProblem: getFurnitureFrameProblem,
28689
28980
  getFurnitureLayerData: getFurnitureLayerData,
28690
28981
  getFurnitureLayerType: getFurnitureLayerType,
28691
28982
  isFurnitureLayer: isFurnitureLayer,
28692
28983
  layerHasFurniture: layerHasFurniture,
28984
+ layerIsFurniture: layerIsFurniture,
28693
28985
  renderFurnitureLayer: renderFurnitureLayer
28694
28986
  });
28695
28987
 
@@ -39146,6 +39438,9 @@ ${svg}
39146
39438
  async function exportDatasets(datasets, opts) {
39147
39439
  var format = getOutputFormat(datasets[0], opts);
39148
39440
  var files;
39441
+ if (format != 'svg' && format != PACKAGE_EXT) {
39442
+ datasets = removeFurnitureLayers(datasets);
39443
+ }
39149
39444
  validateRasterExportFormat(datasets, format);
39150
39445
  if (format != 'geoparquet' && opts.level !== undefined) {
39151
39446
  error('The level= option only applies to GeoParquet output');
@@ -39221,6 +39516,25 @@ ${svg}
39221
39516
  return files;
39222
39517
  }
39223
39518
 
39519
+ // Scalebars and other map furniture only have a meaning in SVG output
39520
+ function removeFurnitureLayers(datasets) {
39521
+ var hasFurniture = datasets.some(function(dataset) {
39522
+ return dataset.layers.some(layerIsFurniture);
39523
+ });
39524
+ if (!hasFurniture) return datasets;
39525
+ datasets = datasets.map(function(dataset) {
39526
+ return utils.defaults({
39527
+ layers: dataset.layers.filter(function(lyr) { return !layerIsFurniture(lyr); })
39528
+ }, dataset);
39529
+ }).filter(function(dataset) {
39530
+ return dataset.layers.length > 0;
39531
+ });
39532
+ if (datasets.length === 0) {
39533
+ stop$1('Map furniture layers (e.g. scalebars) can only be exported as SVG');
39534
+ }
39535
+ return datasets;
39536
+ }
39537
+
39224
39538
  function validateRasterExportFormat(datasets, format) {
39225
39539
  if (format == 'geotiff') {
39226
39540
  if (datasetsHaveVectorLayers(datasets)) {
@@ -41392,7 +41706,7 @@ ${svg}
41392
41706
  'font-weight': {describe: 'normal, bold or a numeric weight'},
41393
41707
  'font-stretch': {describe: 'e.g. condensed'},
41394
41708
  'letter-spacing': {describe: 'extra space between letters'},
41395
- 'line-height': {describe: 'spacing between lines of a multi-line label'},
41709
+ 'line-height': {describe: 'spacing between lines of a multi-line label; a bare number is a multiple of the font size'},
41396
41710
  'text-anchor': {describe: 'start, middle or end'},
41397
41711
  'label-align': {describe: 'how the lines of a multi-line label align: left, center or right'},
41398
41712
  'dominant-baseline': {describe: 'vertical alignment, e.g. central'},
@@ -43478,7 +43792,7 @@ ${svg}
43478
43792
  describe: 'CSS letter-spacing property of labels'
43479
43793
  })
43480
43794
  .option('line-height', {
43481
- describe: 'line spacing of multi-line labels (default is 1.1em)'
43795
+ describe: 'line spacing of multi-line labels; a bare number is a multiple of the font size, as in CSS (default is 1.1)'
43482
43796
  })
43483
43797
  .option('dominant-baseline', {
43484
43798
  describe: 'vertical alignment of labels (e.g. central)'
@@ -43812,6 +44126,13 @@ ${svg}
43812
44126
  describe: 'frame coordinates (xmin,ymin,xmax,ymax)',
43813
44127
  type: 'bbox'
43814
44128
  })
44129
+ .option('fit', {
44130
+ describe: 'layer(s) to fit the frame to, including their symbols and labels'
44131
+ })
44132
+ .option('ignore-symbols', {
44133
+ describe: 'with fit=, fit to point locations, not symbol and label extents',
44134
+ type: 'flag'
44135
+ })
43815
44136
  .option('width', {
43816
44137
  describe: 'nominal frame width (e.g. 5in, 10cm, 600px)'
43817
44138
  })
@@ -43963,6 +44284,10 @@ ${svg}
43963
44284
  describe: 'separate offsets for each side, in l,b,r,t order',
43964
44285
  type: 'strings'
43965
44286
  })
44287
+ .option('ignore-symbols', {
44288
+ describe: 'fit to point locations, not the extent of symbols and labels',
44289
+ type: 'flag'
44290
+ })
43966
44291
  .option('name', nameOpt)
43967
44292
  .option('replace', {
43968
44293
  describe: 'replace the existing map frame',
@@ -44067,10 +44392,13 @@ ${svg}
44067
44392
  .option('target', targetOpt);
44068
44393
 
44069
44394
  parser.command('scalebar')
44070
- .describe('add a simple scale bar to SVG output')
44395
+ .describe('add a scale bar to the map frame and SVG output')
44071
44396
  .option('label', {
44072
44397
  DEFAULT: true,
44073
- describe: 'distance label, e.g. "35 miles"'
44398
+ describe: 'distance label, e.g. "35 miles" (default is automatic)'
44399
+ })
44400
+ .option('units', {
44401
+ describe: 'units of a bare-number or automatic label: km, meters, miles or feet (default is miles)'
44074
44402
  })
44075
44403
  .option('style', {
44076
44404
  describe: 'two options: a or b'
@@ -44078,6 +44406,18 @@ ${svg}
44078
44406
  .option('font-size', {
44079
44407
  type: 'number'
44080
44408
  })
44409
+ .option('font-family', {
44410
+ describe: 'font family of labels'
44411
+ })
44412
+ .option('font-style', {
44413
+ describe: 'font style of labels, e.g. italic'
44414
+ })
44415
+ .option('font-weight', {
44416
+ describe: 'font weight of labels, e.g. bold'
44417
+ })
44418
+ .option('color', {
44419
+ describe: 'color of bar and labels (default is black)'
44420
+ })
44081
44421
  .option('tic-length', {
44082
44422
  describe: 'length of tic marks (style b)',
44083
44423
  type: 'number'
@@ -44090,18 +44430,22 @@ ${svg}
44090
44430
  type: 'number'
44091
44431
  })
44092
44432
  .option('position', {
44093
- describe: 'e.g. bottom-right (default is top-left)'
44433
+ describe: 'top-left (default), top-right, bottom-left or bottom-right'
44094
44434
  })
44095
44435
  .option('label-position', {
44096
44436
  describe: 'top, bottom, top-center (style a), etc'
44097
44437
  })
44098
44438
  .option('dual-units', {
44099
- // describe: 'display both metric and imperial units',
44439
+ describe: 'show both metric and imperial units (style b)',
44100
44440
  type: 'flag'
44101
44441
  })
44102
44442
  .option('margin', {
44103
44443
  describe: 'offset in pixels from edge of map',
44104
44444
  type: 'number'
44445
+ })
44446
+ .option('remove', {
44447
+ describe: 'remove the scale bar from the map frame',
44448
+ type: 'flag'
44105
44449
  });
44106
44450
 
44107
44451
  parser.command('shape')
@@ -51575,8 +51919,639 @@ ${svg}
51575
51919
  bboxToPolygon: bboxToPolygon
51576
51920
  });
51577
51921
 
51922
+ // Apply rotation, scale and/or shift to some or all of the features in a dataset
51923
+ //
51924
+ cmd.affine = function(targetLayers, dataset, opts) {
51925
+ // Need to separate the targeted shapes from any other shapes that share
51926
+ // the same topology. So we duplicate any arcs that are shared by the targeted
51927
+ // shapes and their topological neighbors and remap arc references in the
51928
+ // neighbors to point to the copies.
51929
+ // TODO: explore alternative: if some arcs are shared between transformed and
51930
+ // non-transformed shapes, first remove topology, then tranform, then rebuild topology
51931
+ //
51932
+ var rotateArg = opts.rotate || 0;
51933
+ var scaleArg = opts.scale || 1;
51934
+ var shiftArg = opts.shift ? convertIntervalPair(opts.shift, getDatasetCRS(dataset)) : [0, 0];
51935
+ var arcs = dataset.arcs;
51936
+ var targetShapes = [];
51937
+ var otherShapes = [];
51938
+ var targetPoints = [];
51939
+ var targetPointLayers = [];
51940
+ var targetFlags, otherFlags, transform;
51941
+ dataset.layers.filter(layerHasGeometry$1).forEach(function(lyr) {
51942
+ var hits = [],
51943
+ misses = [],
51944
+ test;
51945
+ if (targetLayers.indexOf(lyr) == -1) {
51946
+ misses = lyr.shapes;
51947
+ } else if (opts.where) {
51948
+ test = compileFeatureExpression(opts.where, lyr, dataset.arcs);
51949
+ lyr.shapes.forEach(function(shp, i) {
51950
+ (test(i) ? hits : misses).push(shp);
51951
+ });
51952
+ } else {
51953
+ hits = lyr.shapes;
51954
+ }
51955
+ if (lyr.geometry_type == 'point') {
51956
+ targetPoints = targetPoints.concat(hits);
51957
+ if (hits.length > 0) targetPointLayers.push(lyr);
51958
+ } else {
51959
+ targetShapes = targetShapes.concat(hits);
51960
+ otherShapes = otherShapes.concat(misses);
51961
+ }
51962
+ });
51963
+ var anchorArg = getAffineAnchor({arcs: dataset.arcs, layers: [{
51964
+ geometry_type: 'point', shapes: targetPoints}, {geometry_type: 'polyline',
51965
+ shapes: targetShapes}]}, opts);
51966
+ transform = getAffineTransform(rotateArg, scaleArg, shiftArg, anchorArg);
51967
+ if (opts.fit_bbox) {
51968
+ transform = getFitBoxTransform(opts.fit_bbox, targetPoints, targetShapes, arcs);
51969
+ }
51970
+ if (targetShapes.length > 0) {
51971
+ targetFlags = new Uint8Array(arcs.size());
51972
+ otherFlags = new Uint8Array(arcs.size());
51973
+ countArcsInShapes(targetShapes, targetFlags);
51974
+ if (otherShapes.length > 0) {
51975
+ countArcsInShapes(otherShapes, otherFlags);
51976
+ applyArrayMask(otherFlags, targetFlags);
51977
+ noteDatasetWillChange(dataset, {operation: 'affine', unit: 'arcs'});
51978
+ dataset.arcs = duplicateSelectedArcs(otherShapes, arcs, otherFlags);
51979
+ markDatasetChanged(dataset, {operation: 'affine', unit: 'arcs'});
51980
+ }
51981
+ dataset.arcs.transformPoints(function(x, y, arcId) {
51982
+ if (arcId < targetFlags.length && targetFlags[arcId] > 0) {
51983
+ return transform(x, y);
51984
+ }
51985
+ });
51986
+ }
51987
+ targetPointLayers.forEach(function(lyr) {
51988
+ noteLayerWillChange(lyr, {operation: 'affine', unit: 'shapes'});
51989
+ });
51990
+ forEachPoint(targetPoints, function(p) {
51991
+ var p2 = transform(p[0], p[1]);
51992
+ p[0] = p2[0];
51993
+ p[1] = p2[1];
51994
+ });
51995
+ targetPointLayers.forEach(function(lyr) {
51996
+ markLayerChanged(lyr, {operation: 'affine', unit: 'shapes'});
51997
+ });
51998
+ };
51999
+
52000
+ function getAffineAnchor(dataset, opts) {
52001
+ var anchor, bounds;
52002
+ if (opts.anchor) {
52003
+ anchor = opts.anchor;
52004
+ } else {
52005
+ // get bounds of selected shapes to calculate center of rotation/scale
52006
+ bounds = getDatasetBounds(dataset);
52007
+ anchor = [bounds.centerX(), bounds.centerY()];
52008
+ }
52009
+ return anchor;
52010
+ }
52011
+
52012
+ // TODO: handle problems with unprojected datasets
52013
+ // option 1: don't allow affine transformation of unprojected data
52014
+ // option 2: error if transformed data exceeds valid coordinate range
52015
+ // source: http://mathworld.wolfram.com/AffineTransformation.html
52016
+ function getAffineTransform(rotation, scale, shift, anchor) {
52017
+ var angle = rotation * Math.PI / 180;
52018
+ var a = scale * Math.cos(angle);
52019
+ var b = -scale * Math.sin(angle);
52020
+ return function(x, y) {
52021
+ var x2 = a * (x - anchor[0]) - b * (y - anchor[1]) + shift[0] + anchor[0];
52022
+ var y2 = b * (x - anchor[0]) + a * (y - anchor[1]) + shift[1] + anchor[1];
52023
+ return [x2, y2];
52024
+ };
52025
+ }
52026
+
52027
+ function getFitBoxTransform(bbox, points, shapes, arcs) {
52028
+ var dataset = {
52029
+ arcs: arcs,
52030
+ layers: []
52031
+ };
52032
+ if (points && points.length) {
52033
+ dataset.layers.push({
52034
+ geometry_type: 'point',
52035
+ shapes: points
52036
+ });
52037
+ }
52038
+ if (shapes && shapes.length) {
52039
+ dataset.layers.push({
52040
+ geometry_type: 'polyline',
52041
+ shapes: shapes
52042
+ });
52043
+ }
52044
+ var frame = calcFrameData(dataset, {fit_bbox: bbox});
52045
+ var fromBounds = new Bounds(frame.bbox);
52046
+ var toBounds = new Bounds(frame.bbox2);
52047
+ var fwd = fromBounds.getTransform(toBounds, false);
52048
+ return function(x, y) {
52049
+ return fwd.transform(x, y);
52050
+ };
52051
+ }
52052
+
52053
+ function applyArrayMask(destArr, maskArr) {
52054
+ for (var i=0, n=destArr.length; i<n; i++) {
52055
+ if (maskArr[i] === 0) destArr[i] = 0;
52056
+ }
52057
+ }
52058
+
52059
+ function duplicateSelectedArcs(shapes, arcs, flags) {
52060
+ var arcCount = 0;
52061
+ var vertexCount = 0;
52062
+ var data = arcs.getVertexData();
52063
+ var xx = [], yy = [], nn = [], map = [], n;
52064
+ for (var i=0, len=flags.length; i<len; i++) {
52065
+ if (flags[i] > 0) {
52066
+ map[i] = arcs.size() + arcCount;
52067
+ n = data.nn[i];
52068
+ utils.copyElements(data.xx, data.ii[i], xx, vertexCount, n);
52069
+ utils.copyElements(data.yy, data.ii[i], yy, vertexCount, n);
52070
+ nn.push(n);
52071
+ vertexCount += n;
52072
+ arcCount++;
52073
+ }
52074
+ }
52075
+ forEachArcId(shapes, function(id) {
52076
+ var absId = absArcId(id);
52077
+ if (flags[absId] > 0) {
52078
+ return id < 0 ? ~map[absId] : map[absId];
52079
+ }
52080
+ });
52081
+ return mergeArcs([arcs, new ArcCollection(nn, xx, yy)]);
52082
+ }
52083
+
52084
+ var roundCoord$1 = getRoundingFunction(0.01);
52085
+
52086
+ function getSymbolFillColor(d) {
52087
+ return d.fill || 'magenta';
52088
+ }
52089
+
52090
+ function getSymbolStrokeColor(d) {
52091
+ return d.stroke || d.fill || 'magenta';
52092
+ }
52093
+
52094
+ function applySymbolStyles(sym, d) {
52095
+ if (sym.type == 'polyline') {
52096
+ sym.stroke = getSymbolStrokeColor(d);
52097
+ } else {
52098
+ sym.fill = getSymbolFillColor(d);
52099
+ }
52100
+ if (d.opacity) {
52101
+ sym.opacity = d.opacity;
52102
+ }
52103
+ return sym;
52104
+ }
52105
+
52106
+ function getSymbolRadius(d) {
52107
+ if (d.radius === 0 || d.length === 0 || d.r === 0) return 0;
52108
+ return d.radius || d.length || d.r || 5; // use a default value
52109
+ }
52110
+
52111
+ // Converts the value of a list-valued option into an array of items. The value
52112
+ // arrives either as a whole string (from a direct call, rather than from the
52113
+ // -symbols option accessor) or as an array of items already resolved one by
52114
+ // one. An item that holds an array of its own, as an expression like
52115
+ // [R, R * 0.5] does, is flattened into the list. Items themselves are left
52116
+ // unsplit, because a single item can contain a comma, as rgba() colors do.
52117
+ function toItemList(val) {
52118
+ var items = utils.isString(val) ? splitListItems(val) : [].concat(val);
52119
+ var list = [];
52120
+ for (var i=0; i<items.length; i++) {
52121
+ if (Array.isArray(items[i])) {
52122
+ list = list.concat(items[i]);
52123
+ } else {
52124
+ list.push(items[i]);
52125
+ }
52126
+ }
52127
+ return list;
52128
+ }
52129
+
52130
+ // Like toItemList(), for options that take numbers. An item that is a string
52131
+ // containing commas is split, so that a data field holding a value like "2,4"
52132
+ // works as a list. Values that aren't numbers become NaN, for the caller to
52133
+ // report or ignore.
52134
+ function toNumberList(val) {
52135
+ var list = [];
52136
+ toItemList(val).forEach(function(item) {
52137
+ if (utils.isString(item) && item.indexOf(',') > -1) {
52138
+ list = list.concat(parseNumberList(item));
52139
+ } else {
52140
+ list.push(Number(item));
52141
+ }
52142
+ });
52143
+ return list;
52144
+ }
52145
+
52146
+ // Returns the radius of the smallest circle centered on a symbol's point that
52147
+ // covers the symbol, or null if the symbol contains a part whose reach can't be
52148
+ // measured.
52149
+ //
52150
+ // Only a symbol drawn around its point can be described this way: a circle, or
52151
+ // a group of the circles and polygons that make up ring, pie and donut symbols.
52152
+ // An arrow is drawn from its point outward, so the circle around its point that
52153
+ // covers it says nothing useful about the space it occupies -- arrows are plain
52154
+ // polygon symbols, which reach this function only as group parts, which they
52155
+ // never are.
52156
+ function getSymbolBoundingRadius(sym) {
52157
+ var max = 0;
52158
+ var parts, r, i;
52159
+ if (!sym) return null;
52160
+ if (sym.type == 'circle') {
52161
+ // Half of a stroke lies outside the circle it follows, which is how a ring
52162
+ // symbol paints its bands.
52163
+ return getPositiveNumber(sym.r) + getStrokeOutset(sym);
52164
+ }
52165
+ if (sym.type == 'polygon') {
52166
+ forEachSymbolCoord(sym.coordinates || [], function(p) {
52167
+ var dist = Math.sqrt(p[0] * p[0] + p[1] * p[1]);
52168
+ if (dist > max) max = dist;
52169
+ });
52170
+ return max + getStrokeOutset(sym);
52171
+ }
52172
+ if (sym.type == 'group') {
52173
+ parts = sym.parts || [];
52174
+ for (i=0; i<parts.length; i++) {
52175
+ r = getSymbolBoundingRadius(parts[i]);
52176
+ if (r === null) return null;
52177
+ if (r > max) max = r;
52178
+ }
52179
+ return max;
52180
+ }
52181
+ return null;
52182
+ }
52183
+
52184
+ // A stroke straddles the path it is drawn on, so half of its width extends
52185
+ // beyond the shape.
52186
+ function getStrokeOutset(sym) {
52187
+ var width = sym['stroke-width'];
52188
+ if (!sym.stroke || sym.stroke == 'none') return 0;
52189
+ // an SVG stroke is one pixel wide unless stroke-width says otherwise
52190
+ if (width === undefined || width === null || width === '') return 0.5;
52191
+ return getPositiveNumber(width) / 2;
52192
+ }
52193
+
52194
+ function getPositiveNumber(val) {
52195
+ var num = +val;
52196
+ return num > 0 ? num : 0;
52197
+ }
52198
+
52199
+ function forEachSymbolCoord(coords, cb) {
52200
+ var isPoint = coords && utils.isNumber(coords[0]);
52201
+ var isNested = !isPoint && coords && Array.isArray(coords[0]);
52202
+ if (isPoint) return cb(coords);
52203
+ for (var i=0; i<coords.length; i++) {
52204
+ if (isNested) forEachSymbolCoord(coords[i], cb);
52205
+ }
52206
+ }
52207
+
52208
+ function flipY(coords) {
52209
+ forEachSymbolCoord(coords, function(p) {
52210
+ p[1] = -p[1];
52211
+ });
52212
+ }
52213
+
52214
+ function scaleAndShiftCoords(coords, scale, shift) {
52215
+ forEachSymbolCoord(coords, function(xy) {
52216
+ xy[0] = xy[0] * scale + shift[0];
52217
+ xy[1] = xy[1] * scale + shift[1];
52218
+ });
52219
+ }
52220
+
52221
+ function roundCoordsForSVG(coords) {
52222
+ forEachSymbolCoord(coords, function(p) {
52223
+ p[0] = roundCoord$1(p[0]);
52224
+ p[1] = roundCoord$1(p[1]);
52225
+ });
52226
+ }
52227
+
52228
+ function rotateCoords(coords, rotation) {
52229
+ if (!rotation) return;
52230
+ var f = getAffineTransform(rotation, 1, [0, 0], [0, 0]);
52231
+ forEachSymbolCoord(coords, function(p) {
52232
+ var p2 = f(p[0], p[1]);
52233
+ p[0] = p2[0];
52234
+ p[1] = p2[1];
52235
+ });
52236
+ }
52237
+
52238
+ // The space that a point's symbol and label take up when drawn, for fitting a
52239
+ // map frame around them.
52240
+ //
52241
+ // Boxes are [xmin, ymin, xmax, ymax] in px, relative to the point, with y down:
52242
+ // the space symbols are drawn in (see renderPoint() in svg-symbols.mjs, which
52243
+ // this follows). A box can leave out parts of what gets drawn -- anti-aliasing,
52244
+ // miter joins, a curve bowing past its control points -- but not by more than a
52245
+ // pixel or so.
52246
+
52247
+ // The box around what renderPoint() draws for @rec, or null if it draws nothing
52248
+ // that can be sized.
52249
+ // @cache: optional Map, for reusing the boxes of svg-symbol strings, which are
52250
+ // often the same for every feature in a layer
52251
+ function getPointSymbolBox(rec, cache) {
52252
+ var box = null;
52253
+ if (!rec) return null;
52254
+ if (featureHasSvgSymbol(rec)) {
52255
+ box = getSymbolBox(rec, cache);
52256
+ }
52257
+ if (featureHasLabel(rec)) {
52258
+ box = mergeBoxes(box, getLabelBox(rec));
52259
+ }
52260
+ return box;
52261
+ }
52262
+
52263
+ // How far the text of a label drawn along a path can reach from the path, in
52264
+ // px. The side the text sits on turns with the path, so it is allowed for on
52265
+ // every side.
52266
+ function getPathLabelPadding(rec) {
52267
+ return getLabelFontSize(rec) + getHaloWidth(rec);
52268
+ }
52269
+
52270
+ function getLabelBox(rec) {
52271
+ var o = getLabelTextBox(rec, {estimate_width: true});
52272
+ var pad = getHaloWidth(rec);
52273
+ var box = [o.xmin - pad, o.ymin - pad, o.xmax + pad, o.ymax + pad];
52274
+ var via = labelHasCallout(rec) ? parsePointPair(rec['callout-via'] || '') : null;
52275
+ if (via) {
52276
+ box = mergeBoxes(box, [via[0], via[1], via[0], via[1]]);
52277
+ }
52278
+ return box;
52279
+ }
52280
+
52281
+ function getHaloWidth(rec) {
52282
+ var w = +rec['halo-width'];
52283
+ return w > 0 ? w : 0;
52284
+ }
52285
+
52286
+ function getSymbolBox(d, cache) {
52287
+ var sym = d['svg-symbol'];
52288
+ if (sym) return getComplexSymbolBox(sym, cache);
52289
+ if (featureHasIcon(d)) return getIconBox(d);
52290
+ if (d.r > 0) return getSquareBox(+d.r + getStrokeOutset(d), 0, 0);
52291
+ return null;
52292
+ }
52293
+
52294
+ function getIconBox(d) {
52295
+ var type = d.icon || 'circle';
52296
+ var r = getIconRadius(d, type);
52297
+ var sw;
52298
+ if (!(r > 0)) return null;
52299
+ if (type == 'ring') {
52300
+ // drawn as a stroke, one px wide unless stroke-width says otherwise
52301
+ sw = +d['stroke-width'];
52302
+ return getSquareBox(r + (sw > 0 ? sw : 1) / 2, 0, 0);
52303
+ }
52304
+ if (type == 'circle' || type == 'square' || type == 'star') {
52305
+ return getSquareBox(r + getStrokeOutset(d), 0, 0);
52306
+ }
52307
+ return null; // unsupported icons are not drawn
52308
+ }
52309
+
52310
+ function getComplexSymbolBox(sym, cache) {
52311
+ var box;
52312
+ if (!utils.isString(sym)) return getSymbolPartBox(sym, 0, 0);
52313
+ if (cache && cache.has(sym)) return cache.get(sym);
52314
+ try {
52315
+ box = getSymbolPartBox(JSON.parse(sym), 0, 0);
52316
+ } catch(e) {
52317
+ box = null;
52318
+ }
52319
+ if (cache) cache.set(sym, box);
52320
+ return box;
52321
+ }
52322
+
52323
+ // Follows the renderers in svg-symbols.mjs: @x, @y is where the part is drawn,
52324
+ // which circles, squares, images, lines and labels are placed at and polygons
52325
+ // and polylines ignore.
52326
+ function getSymbolPartBox(sym, x, y) {
52327
+ var type = sym && sym.type;
52328
+ var w, h;
52329
+ if (!sym || sym.tag) return null; // raw SVG can't be sized
52330
+ if (type == 'circle' || type == 'square') {
52331
+ return getSquareBox(getPositive(sym.r) + getStrokeOutset(sym), x, y);
52332
+ }
52333
+ if (type == 'image') {
52334
+ w = sym.width || 20;
52335
+ h = sym.height || 20;
52336
+ return [x - w / 2, y - h / 2, x + w / 2, y + h / 2];
52337
+ }
52338
+ if (type == 'polygon') {
52339
+ return padBox(getCoordsBox(sym.coordinates), getStrokeOutset(sym));
52340
+ }
52341
+ if (type == 'polyline') {
52342
+ return padBox(getCoordsBox(sym.coordinates), getLineOutset(sym));
52343
+ }
52344
+ if (type == 'line') {
52345
+ return padBox(getCoordsBox([[x, y], [x + (sym.dx || 0), y + (sym.dy || 0)]]),
52346
+ getLineOutset(sym));
52347
+ }
52348
+ if (type == 'label') {
52349
+ return shiftBox(getLabelBox(sym), x, y);
52350
+ }
52351
+ if (type == 'group') {
52352
+ return getGroupBox(sym, x, y);
52353
+ }
52354
+ return null; // offset draws nothing, and unknown types are not drawn
52355
+ }
52356
+
52357
+ // A line part moves the parts after it to its far end; an offset part moves
52358
+ // them into a group translated by its offset.
52359
+ function getGroupBox(sym, x, y) {
52360
+ var box = null;
52361
+ var ox = 0, oy = 0;
52362
+ (sym.parts || []).forEach(function(part) {
52363
+ box = mergeBoxes(box, shiftBox(getSymbolPartBox(part, x, y), ox, oy));
52364
+ if (!part) return;
52365
+ if (part.type == 'line') {
52366
+ x += part.dx || 0;
52367
+ y += part.dy || 0;
52368
+ } else if (part.type == 'offset') {
52369
+ ox += x + (part.dx || 0);
52370
+ oy += y + (part.dy || 0);
52371
+ x = y = 0;
52372
+ }
52373
+ });
52374
+ return box;
52375
+ }
52376
+
52377
+ // A stroked line is one px wide unless stroke-width says otherwise
52378
+ function getLineOutset(sym) {
52379
+ var sw = +sym['stroke-width'];
52380
+ return (sw > 0 ? sw : 1) / 2;
52381
+ }
52382
+
52383
+ function getCoordsBox(coords) {
52384
+ var box = null;
52385
+ forEachSymbolCoord(coords || [], function(p) {
52386
+ if (!isFinite(p[0]) || !isFinite(p[1])) return;
52387
+ box = mergeBoxes(box, [p[0], p[1], p[0], p[1]]);
52388
+ });
52389
+ return box;
52390
+ }
52391
+
52392
+ function getSquareBox(r, x, y) {
52393
+ return r > 0 ? [x - r, y - r, x + r, y + r] : null;
52394
+ }
52395
+
52396
+ function padBox(box, pad) {
52397
+ return box ? [box[0] - pad, box[1] - pad, box[2] + pad, box[3] + pad] : null;
52398
+ }
52399
+
52400
+ function shiftBox(box, dx, dy) {
52401
+ if (!box || !dx && !dy) return box;
52402
+ return [box[0] + dx, box[1] + dy, box[2] + dx, box[3] + dy];
52403
+ }
52404
+
52405
+ function mergeBoxes(a, b) {
52406
+ if (!a) return b;
52407
+ if (!b) return a;
52408
+ return [Math.min(a[0], b[0]), Math.min(a[1], b[1]),
52409
+ Math.max(a[2], b[2]), Math.max(a[3], b[3])];
52410
+ }
52411
+
52412
+ function getPositive(val) {
52413
+ var num = +val;
52414
+ return num > 0 ? num : 0;
52415
+ }
52416
+
52417
+ // Fitting a map frame to its content, including the symbols and labels drawn
52418
+ // at points, which take up room that the point coordinates alone do not.
52419
+ //
52420
+ // Symbols are sized in output pixels, and how many map units a pixel covers is
52421
+ // set by the frame being fitted, so the extent can't simply be padded. It is
52422
+ // found by solving for the scale instead: the smallest scale s (map units per
52423
+ // output pixel) at which the content -- points and their symbols, drawn at s --
52424
+ // gets a frame whose own scale is no larger than s.
52425
+ //
52426
+ // Every point's box moves each side of the extent linearly with s, so the
52427
+ // extent's width and height are convex, piecewise-linear functions of s, and so
52428
+ // is the frame scale they produce. Newton's method converges on the smallest
52429
+ // fixed point of such a function from below, without overshooting, in as many
52430
+ // steps as there are pieces along the way -- usually two or three.
52431
+
52432
+ var MAX_ITERATIONS = 50;
52433
+ var TOLERANCE = 1e-9;
52434
+
52435
+ // Returns the extent of @targets as a bbox array, padded to hold the symbols
52436
+ // and labels at their points, or null if the targets have no extent.
52437
+ //
52438
+ // @targets [{layer, dataset}]
52439
+ // @getScale function(bbox) -> map units per output px of a frame fitted to
52440
+ // @bbox; this is where the caller's offsets and page shape come in.
52441
+ // May return NaN for a bbox the frame can't be fitted to.
52442
+ // @opts.ignore_symbols fit to the coordinates alone
52443
+ function getFrameContentBbox(targets, getScale, opts) {
52444
+ // a data-only layer has no bounds
52445
+ var bounds = targets.reduce(function(memo, o) {
52446
+ var b = getLayerBounds(o.layer, o.dataset.arcs);
52447
+ return b ? memo.mergeBounds(b) : memo;
52448
+ }, new Bounds());
52449
+ var raw, items, bbox;
52450
+ if (!bounds.hasBounds()) return null;
52451
+ raw = bounds.toArray();
52452
+ if (opts && opts.ignore_symbols) return raw;
52453
+ items = getSymbolExtents(targets);
52454
+ if (items.length === 0) return raw;
52455
+ bbox = fitBboxToSymbols(raw, items, getScale);
52456
+ if (!bbox) {
52457
+ warn('Symbols and labels are too large to fit in the frame; ' +
52458
+ 'fitting the frame to point locations only.');
52459
+ return raw;
52460
+ }
52461
+ return bbox;
52462
+ }
52463
+
52464
+ // The map units per output px of a frame with extent @bbox and nominal width
52465
+ // @width. A frame with a fixed page shape is filled out to that shape when it is
52466
+ // rendered (see fitDatasetToFrame()), so its scale is set by whichever of its
52467
+ // width and height is the tighter fit.
52468
+ function getFrameScale(bbox, width, fixedAspect) {
52469
+ var w = bbox[2] - bbox[0];
52470
+ var h = bbox[3] - bbox[1];
52471
+ return fixedAspect > 0 ? Math.max(w, h * fixedAspect) / width : w / width;
52472
+ }
52473
+
52474
+ // @raw bbox of the content without symbols
52475
+ // @items flat array of [x, y, xmin, ymin, xmax, ymax, ...]: each point and the
52476
+ // box around it in px, oriented like the map (y up)
52477
+ // Returns the padded bbox, or null if there is no scale at which the symbols
52478
+ // fit -- when they add up to more than the width of the page.
52479
+ function fitBboxToSymbols(raw, items, getScale) {
52480
+ var s = getScale(raw);
52481
+ var bbox, h, delta, slope, i;
52482
+ for (i = 0; i < MAX_ITERATIONS; i++) {
52483
+ if (!isValidScale(s)) return null;
52484
+ bbox = expandBbox(raw, items, s);
52485
+ h = getScale(bbox);
52486
+ if (!isValidScale(h)) return null;
52487
+ // Content with no extent of its own, like a single point, stops here with
52488
+ // s = 0: nothing about it sets a scale.
52489
+ if (h <= s * (1 + TOLERANCE)) return bbox;
52490
+ if (!(s > 0)) return null;
52491
+ delta = s * 1e-7;
52492
+ slope = (getScale(expandBbox(raw, items, s + delta)) - h) / delta;
52493
+ if (!(slope < 1)) return null;
52494
+ s += (h - s) / (1 - slope);
52495
+ }
52496
+ return expandBbox(raw, items, s);
52497
+ }
52498
+
52499
+ function isValidScale(s) {
52500
+ return s >= 0 && s < Infinity;
52501
+ }
52502
+
52503
+ function expandBbox(raw, items, s) {
52504
+ var xmin = raw[0], ymin = raw[1], xmax = raw[2], ymax = raw[3];
52505
+ var x, y, v;
52506
+ for (var i = 0, n = items.length; i < n; i += 6) {
52507
+ x = items[i];
52508
+ y = items[i + 1];
52509
+ v = x + items[i + 2] * s;
52510
+ if (v < xmin) xmin = v;
52511
+ v = y + items[i + 3] * s;
52512
+ if (v < ymin) ymin = v;
52513
+ v = x + items[i + 4] * s;
52514
+ if (v > xmax) xmax = v;
52515
+ v = y + items[i + 5] * s;
52516
+ if (v > ymax) ymax = v;
52517
+ }
52518
+ return [xmin, ymin, xmax, ymax];
52519
+ }
52520
+
52521
+ // Boxes come from the renderer's space, where y is down, and are flipped here
52522
+ // to the map's.
52523
+ function getSymbolExtents(targets) {
52524
+ var items = [];
52525
+ var cache = new Map();
52526
+ targets.forEach(function(o) {
52527
+ var lyr = o.layer;
52528
+ var records;
52529
+ if (lyr.geometry_type != 'point' || !lyr.shapes || !lyr.data) return;
52530
+ records = lyr.data.getRecords();
52531
+ lyr.shapes.forEach(function(shp, i) {
52532
+ var rec = records[i];
52533
+ var box, pad;
52534
+ if (!shp || !rec) return;
52535
+ if (shp.length > 1 && featureHasLabel(rec)) {
52536
+ // a multipoint label is drawn along a path through its points,
52537
+ // with no symbol (see featureIsPathLabel())
52538
+ pad = getPathLabelPadding(rec);
52539
+ box = [-pad, -pad, pad, pad];
52540
+ } else {
52541
+ box = getPointSymbolBox(rec, cache);
52542
+ }
52543
+ if (!box) return;
52544
+ for (var j = 0; j < shp.length; j++) {
52545
+ if (!shp[j] || !isFinite(shp[j][0]) || !isFinite(shp[j][1])) continue;
52546
+ items.push(shp[j][0], shp[j][1], box[0], -box[3], box[2], -box[1]);
52547
+ }
52548
+ });
52549
+ });
52550
+ return items;
52551
+ }
52552
+
51578
52553
  cmd.frame = function(catalog, targets, opts) {
51579
- var widthPx, heightPx, aspectRatio, bbox;
52554
+ var widthPx, heightPx, bbox;
51580
52555
  var existingFrame = getActiveFrame(catalog);
51581
52556
  if (opts.width) {
51582
52557
  widthPx = parseFrameSize(opts.width).valuePx;
@@ -51612,30 +52587,29 @@ ${svg}
51612
52587
  } else {
51613
52588
  var datasets = utils.pluck(targets, 'dataset');
51614
52589
  requireDatasetsHaveCompatibleCRS(datasets, 'Targets include both projected and unprojected coordinates');
51615
- bbox = getTargetBbox(targets);
52590
+ bbox = getFrameContentBbox(expandCommandTargets(targets), function(contentBbox) {
52591
+ var extent = getFrameExtent(contentBbox, widthPx, heightPx, opts);
52592
+ return getFrameScale(extent.bbox, extent.width, getFixedAspect$1(extent, opts));
52593
+ }, opts);
52594
+ if (!bbox && !expandCommandTargets(targets).some(function(o) {
52595
+ return layerHasGeometry$1(o.layer);
52596
+ })) {
52597
+ stop$1('Unable to fit a frame to a layer with no geometry. Target a layer containing shapes, or use the bbox= option.');
52598
+ }
51616
52599
  if (!bbox) {
51617
52600
  stop$1('Command target is missing geographical bounds');
51618
52601
  }
51619
52602
  }
51620
52603
 
51621
- applyPercentageOffsets(bbox, opts.offset || opts.offsets);
51622
- applyPixelOffsets(bbox, widthPx, heightPx, opts.offset || opts.offsets);
51623
-
51624
- if (bbox[3] - bbox[1] > 0 === false || bbox[2] - bbox[0] > 0 === false) {
52604
+ var extent = getFrameExtent(bbox, widthPx, heightPx, opts);
52605
+ if (!extent.valid) {
51625
52606
  stop$1('Frame has a collapsed bbox');
51626
52607
  }
51627
52608
 
51628
- aspectRatio = (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
51629
- if (!widthPx) {
51630
- widthPx = roundToDigits(heightPx * aspectRatio, 1);
51631
- } else if (!heightPx) {
51632
- heightPx = roundToDigits(widthPx / aspectRatio, 1);
51633
- }
51634
-
51635
52609
  var feature = {
51636
52610
  type: 'Feature',
51637
- properties: getFrameProperties(widthPx, heightPx, opts),
51638
- geometry: bboxToPolygon(bbox)
52611
+ properties: getFrameProperties(extent.width, extent.height, opts),
52612
+ geometry: bboxToPolygon(extent.bbox)
51639
52613
  };
51640
52614
  var frameDataset = importGeoJSON(feature);
51641
52615
  // set CRS from target dataset
@@ -51645,16 +52619,47 @@ ${svg}
51645
52619
  var crsInfo = getDatasetCrsInfo(targets[0].dataset);
51646
52620
  setDatasetCrsInfo(frameDataset, crsInfo);
51647
52621
  }
51648
- frameDataset.layers[0].name = opts.name || 'frame';
52622
+ var frameLyr = frameDataset.layers[0];
52623
+ frameLyr.name = opts.name || 'frame';
51649
52624
  if (existingFrame) {
51650
52625
  if (!opts.replace) {
51651
52626
  stop$1('A map frame already exists:', existingFrame.layer.name || '[unnamed frame]');
51652
52627
  }
52628
+ moveFrameFurniture(existingFrame.dataset, frameDataset);
51653
52629
  demoteFrameLayer(existingFrame.layer);
51654
52630
  }
51655
- catalog.addDataset(frameDataset);
52631
+ // target the frame, not any furniture that came with it
52632
+ catalog.setDefaultTarget([frameLyr], frameDataset);
51656
52633
  };
51657
52634
 
52635
+ // The frame's extent and nominal size, from the extent of its content and the
52636
+ // size options. Pure, so that fitting to symbols can ask what scale an extent
52637
+ // would give the frame.
52638
+ function getFrameExtent(contentBbox, widthPx, heightPx, opts) {
52639
+ var offsets = opts.offset || opts.offsets;
52640
+ var bbox = contentBbox.slice();
52641
+ var aspectRatio;
52642
+ applyPercentageOffsets(bbox, offsets);
52643
+ applyPixelOffsets(bbox, widthPx, heightPx, offsets);
52644
+ aspectRatio = (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
52645
+ if (!widthPx) {
52646
+ widthPx = roundToDigits(heightPx * aspectRatio, 1);
52647
+ } else if (!heightPx) {
52648
+ heightPx = roundToDigits(widthPx / aspectRatio, 1);
52649
+ }
52650
+ return {
52651
+ bbox: bbox,
52652
+ width: widthPx,
52653
+ height: heightPx,
52654
+ valid: bbox[3] - bbox[1] > 0 && bbox[2] - bbox[0] > 0
52655
+ };
52656
+ }
52657
+
52658
+ function getFixedAspect$1(extent, opts) {
52659
+ return opts.aspect_ratio > 0 || opts.width && opts.height ?
52660
+ extent.width / extent.height : null;
52661
+ }
52662
+
51658
52663
  function getFrameProperties(width, height, opts) {
51659
52664
  var properties = {
51660
52665
  type: 'frame',
@@ -51750,14 +52755,6 @@ ${svg}
51750
52755
  return arg;
51751
52756
  }
51752
52757
 
51753
- function getTargetBbox(targets) {
51754
- var expanded = expandCommandTargets(targets);
51755
- var bounds = expanded.reduce(function(memo, o) {
51756
- return memo.mergeBounds(getLayerBounds(o.layer, o.dataset.arcs));
51757
- }, new Bounds());
51758
- return bounds.hasBounds() ? bounds.toArray() : null;
51759
- }
51760
-
51761
52758
  // Convert width and height args to aspect ratio arg for the rectangle() function
51762
52759
  function getAspectRatioArg(widthArg, heightArg) {
51763
52760
  // heightArg is a string containing either a number or a
@@ -51902,7 +52899,8 @@ ${svg}
51902
52899
 
51903
52900
  var OPERATION$1 = 'update-frame';
51904
52901
 
51905
- function updateFrame(targetLayers, dataset, opts) {
52902
+ // @fitTargets: [{layer, dataset}] named by the fit= option
52903
+ function updateFrame(targetLayers, dataset, opts, fitTargets) {
51906
52904
  if (!targetLayers || targetLayers.length != 1) {
51907
52905
  stop$1('-update-frame expects a single target layer');
51908
52906
  }
@@ -51926,15 +52924,14 @@ ${svg}
51926
52924
  if (opts.fix_scale && (opts.width !== undefined || opts.height !== undefined)) {
51927
52925
  stop$1('fix-scale cannot be combined with width= or height=');
51928
52926
  }
52927
+ if (opts.bbox !== undefined && opts.fit !== undefined) {
52928
+ stop$1('bbox= and fit= are mutually exclusive');
52929
+ }
51929
52930
 
51930
52931
  var rec = lyr.data.getRecords()[0];
51931
52932
  var frame = getFrameLayerData(lyr, dataset.arcs);
51932
- var bbox = (opts.bbox || frame.bbox).slice();
51933
- var offsetArg = opts.offset || opts.offsets;
51934
52933
  var fixedAspect = getFixedAspect(rec);
51935
- var width = frame.width;
51936
- var units = rec.frame_units || 'px';
51937
- var widthSize, heightSize, height, effectiveAspect;
52934
+ var sizes, contentBbox, update;
51938
52935
 
51939
52936
  // 1. Aspect mode, resolved first because whether the page shape is fixed
51940
52937
  // decides whether it may constrain the extent while offsets are applied.
@@ -51946,8 +52943,87 @@ ${svg}
51946
52943
  }
51947
52944
  fixedAspect = opts.aspect_ratio;
51948
52945
  }
52946
+ sizes = parseSizeOptions(opts);
52947
+
52948
+ // 2. Extent, which fit= finds by trying extents out on steps 3 and 4
52949
+ if (opts.fit !== undefined) {
52950
+ contentBbox = getFrameContentBbox(getFitTargets(fitTargets, dataset), function(bbox) {
52951
+ var o = resolveFrameUpdate(bbox, frame, fixedAspect, sizes, opts);
52952
+ return getFrameScale(o.bbox, o.width, o.fixedAspect);
52953
+ }, opts);
52954
+ if (!contentBbox) {
52955
+ stop$1('Layers to fit are missing geographical bounds');
52956
+ }
52957
+ } else {
52958
+ contentBbox = opts.bbox || frame.bbox;
52959
+ }
52960
+
52961
+ update = resolveFrameUpdate(contentBbox, frame, fixedAspect, sizes, opts);
52962
+ if (!update.valid) {
52963
+ stop$1('Frame has a collapsed bbox');
52964
+ }
52965
+ if (opts.fix_scale) {
52966
+ requirePositiveSize(update.width, 'fix-scale', update.width);
52967
+ }
52968
+
52969
+ noteLayerWillChange(lyr, {operation: OPERATION$1, unit: 'shapes'});
52970
+ lyr.data.captureTableBefore({operation: OPERATION$1});
52971
+ rec.width = update.width;
52972
+ rec.height = update.height;
52973
+ rec.frame_units = update.units;
52974
+ if (update.fixedAspect) {
52975
+ rec.frame_aspect_ratio = update.fixedAspect;
52976
+ } else {
52977
+ delete rec.frame_aspect_ratio;
52978
+ }
52979
+ lyr.data.markChanged({operation: OPERATION$1});
52980
+ rebuildFrameLayerGeometry(lyr, dataset, new Bounds(update.bbox));
52981
+ markLayerChanged(lyr, {operation: OPERATION$1, unit: 'shapes'});
52982
+ }
52983
+
52984
+ // The frame's layers are not content to fit, and a frame is only fitted to
52985
+ // layers it shares coordinates with.
52986
+ function getFitTargets(fitTargets, frameDataset) {
52987
+ var targets = (fitTargets || []).filter(function(o) {
52988
+ return !isFrameLayer(o.layer, o.dataset.arcs);
52989
+ });
52990
+ if (targets.length === 0) {
52991
+ stop$1('fit= found no layers to fit the frame to');
52992
+ }
52993
+ requireDatasetsHaveCompatibleCRS(
52994
+ utils.uniq(targets.map(function(o) { return o.dataset; }).concat(frameDataset)),
52995
+ 'Layers to fit and the frame have incompatible coordinates'
52996
+ );
52997
+ return targets;
52998
+ }
52999
+
53000
+ function parseSizeOptions(opts) {
53001
+ var sizes = {width: null, height: null};
53002
+ if (opts.width !== undefined) {
53003
+ sizes.width = parseFrameSize(opts.width);
53004
+ requirePositiveSize(sizes.width.valuePx, 'width', opts.width);
53005
+ }
53006
+ if (opts.height !== undefined) {
53007
+ sizes.height = parseFrameSize(opts.height);
53008
+ requirePositiveSize(sizes.height.valuePx, 'height', opts.height);
53009
+ }
53010
+ if (sizes.width && sizes.height && opts.aspect_ratio !== undefined &&
53011
+ ratiosDiffer(sizes.width.valuePx / sizes.height.valuePx, opts.aspect_ratio)) {
53012
+ stop$1('Contradictory width, height and aspect-ratio values');
53013
+ }
53014
+ return sizes;
53015
+ }
51949
53016
 
51950
- // 2. Extent
53017
+ // Steps 3 and 4 as a pure function of the content extent, so that fit= can
53018
+ // ask what scale an extent would give the frame.
53019
+ function resolveFrameUpdate(contentBbox, frame, fixedAspect, sizes, opts) {
53020
+ var bbox = contentBbox.slice();
53021
+ var offsetArg = opts.offset || opts.offsets;
53022
+ var width = frame.width;
53023
+ var units = frame.units || 'px';
53024
+ var valid, effectiveAspect;
53025
+
53026
+ // 3. Padding
51951
53027
  if (offsetArg) {
51952
53028
  applyPercentageOffsets(bbox, offsetArg);
51953
53029
  // Pass a page height only when the shape is fixed. A derived height
@@ -51956,65 +53032,44 @@ ${svg}
51956
53032
  applyPixelOffsets(bbox, frame.width,
51957
53033
  fixedAspect ? frame.width / fixedAspect : null, offsetArg);
51958
53034
  }
51959
- requireValidBbox(bbox);
53035
+ valid = isValidBbox(bbox);
51960
53036
  if (fixedAspect) {
51961
53037
  fillOutBbox(bbox, fixedAspect, 1);
51962
53038
  }
51963
53039
 
51964
- // 3. Nominal size
51965
- if (opts.width !== undefined) {
51966
- widthSize = parseFrameSize(opts.width);
51967
- requirePositiveSize(widthSize.valuePx, 'width', opts.width);
51968
- }
51969
- if (opts.height !== undefined) {
51970
- heightSize = parseFrameSize(opts.height);
51971
- requirePositiveSize(heightSize.valuePx, 'height', opts.height);
51972
- }
51973
- if (widthSize && heightSize) {
51974
- effectiveAspect = widthSize.valuePx / heightSize.valuePx;
51975
- if (opts.aspect_ratio !== undefined &&
51976
- ratiosDiffer(effectiveAspect, opts.aspect_ratio)) {
51977
- stop$1('Contradictory width, height and aspect-ratio values');
51978
- }
51979
- width = widthSize.valuePx;
51980
- units = widthSize.units;
51981
- fixedAspect = effectiveAspect;
53040
+ // 4. Nominal size
53041
+ if (sizes.width && sizes.height) {
53042
+ width = sizes.width.valuePx;
53043
+ units = sizes.width.units;
53044
+ fixedAspect = sizes.width.valuePx / sizes.height.valuePx;
51982
53045
  fillOutBbox(bbox, fixedAspect, 1);
51983
- } else if (widthSize) {
51984
- width = widthSize.valuePx;
51985
- units = widthSize.units;
51986
- } else if (heightSize) {
53046
+ } else if (sizes.width) {
53047
+ width = sizes.width.valuePx;
53048
+ units = sizes.width.units;
53049
+ } else if (sizes.height) {
51987
53050
  effectiveAspect = fixedAspect || getBboxAspect(bbox);
51988
- width = heightSize.valuePx * effectiveAspect;
51989
- units = heightSize.units;
53051
+ width = sizes.height.valuePx * effectiveAspect;
53052
+ units = sizes.height.units;
51990
53053
  } else if (opts.fix_scale) {
51991
53054
  // Hold ground units per output pixel, so the page grows and shrinks with
51992
53055
  // the extent instead of the scale changing to fit the extent on it.
51993
53056
  width = frame.width * getBboxWidth(bbox) / getBboxWidth(frame.bbox);
51994
- requirePositiveSize(width, 'fix-scale', width);
51995
53057
  }
51996
53058
 
51997
- requireValidBbox(bbox);
51998
53059
  effectiveAspect = fixedAspect || getBboxAspect(bbox);
51999
- height = Math.round(width / effectiveAspect);
52000
-
52001
- noteLayerWillChange(lyr, {operation: OPERATION$1, unit: 'shapes'});
52002
- lyr.data.captureTableBefore({operation: OPERATION$1});
52003
- rec.width = width;
52004
- rec.height = height;
52005
- rec.frame_units = units;
52006
- if (fixedAspect) {
52007
- rec.frame_aspect_ratio = fixedAspect;
52008
- } else {
52009
- delete rec.frame_aspect_ratio;
52010
- }
52011
- lyr.data.markChanged({operation: OPERATION$1});
52012
- rebuildFrameLayerGeometry(lyr, dataset, new Bounds(bbox));
52013
- markLayerChanged(lyr, {operation: OPERATION$1, unit: 'shapes'});
53060
+ return {
53061
+ bbox: bbox,
53062
+ width: width,
53063
+ height: Math.round(width / effectiveAspect),
53064
+ units: units,
53065
+ fixedAspect: fixedAspect,
53066
+ valid: valid && isValidBbox(bbox)
53067
+ };
52014
53068
  }
52015
53069
 
52016
53070
  function hasUpdateOptions(opts) {
52017
53071
  return opts.bbox !== undefined ||
53072
+ opts.fit !== undefined ||
52018
53073
  opts.width !== undefined ||
52019
53074
  opts.height !== undefined ||
52020
53075
  opts.aspect_ratio !== undefined ||
@@ -52037,12 +53092,10 @@ ${svg}
52037
53092
  return (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
52038
53093
  }
52039
53094
 
52040
- function requireValidBbox(bbox) {
52041
- if (!bbox.every(utils.isFiniteNumber) ||
52042
- bbox[2] - bbox[0] <= 0 ||
52043
- bbox[3] - bbox[1] <= 0) {
52044
- stop$1('Frame has a collapsed bbox');
52045
- }
53095
+ function isValidBbox(bbox) {
53096
+ return bbox.every(utils.isFiniteNumber) &&
53097
+ bbox[2] - bbox[0] > 0 &&
53098
+ bbox[3] - bbox[1] > 0;
52046
53099
  }
52047
53100
 
52048
53101
  function requirePositiveSize(value, name, arg) {
@@ -52272,168 +53325,6 @@ ${svg}
52272
53325
  stop$1('Unable to import coordinates');
52273
53326
  }
52274
53327
 
52275
- // Apply rotation, scale and/or shift to some or all of the features in a dataset
52276
- //
52277
- cmd.affine = function(targetLayers, dataset, opts) {
52278
- // Need to separate the targeted shapes from any other shapes that share
52279
- // the same topology. So we duplicate any arcs that are shared by the targeted
52280
- // shapes and their topological neighbors and remap arc references in the
52281
- // neighbors to point to the copies.
52282
- // TODO: explore alternative: if some arcs are shared between transformed and
52283
- // non-transformed shapes, first remove topology, then tranform, then rebuild topology
52284
- //
52285
- var rotateArg = opts.rotate || 0;
52286
- var scaleArg = opts.scale || 1;
52287
- var shiftArg = opts.shift ? convertIntervalPair(opts.shift, getDatasetCRS(dataset)) : [0, 0];
52288
- var arcs = dataset.arcs;
52289
- var targetShapes = [];
52290
- var otherShapes = [];
52291
- var targetPoints = [];
52292
- var targetPointLayers = [];
52293
- var targetFlags, otherFlags, transform;
52294
- dataset.layers.filter(layerHasGeometry$1).forEach(function(lyr) {
52295
- var hits = [],
52296
- misses = [],
52297
- test;
52298
- if (targetLayers.indexOf(lyr) == -1) {
52299
- misses = lyr.shapes;
52300
- } else if (opts.where) {
52301
- test = compileFeatureExpression(opts.where, lyr, dataset.arcs);
52302
- lyr.shapes.forEach(function(shp, i) {
52303
- (test(i) ? hits : misses).push(shp);
52304
- });
52305
- } else {
52306
- hits = lyr.shapes;
52307
- }
52308
- if (lyr.geometry_type == 'point') {
52309
- targetPoints = targetPoints.concat(hits);
52310
- if (hits.length > 0) targetPointLayers.push(lyr);
52311
- } else {
52312
- targetShapes = targetShapes.concat(hits);
52313
- otherShapes = otherShapes.concat(misses);
52314
- }
52315
- });
52316
- var anchorArg = getAffineAnchor({arcs: dataset.arcs, layers: [{
52317
- geometry_type: 'point', shapes: targetPoints}, {geometry_type: 'polyline',
52318
- shapes: targetShapes}]}, opts);
52319
- transform = getAffineTransform(rotateArg, scaleArg, shiftArg, anchorArg);
52320
- if (opts.fit_bbox) {
52321
- transform = getFitBoxTransform(opts.fit_bbox, targetPoints, targetShapes, arcs);
52322
- }
52323
- if (targetShapes.length > 0) {
52324
- targetFlags = new Uint8Array(arcs.size());
52325
- otherFlags = new Uint8Array(arcs.size());
52326
- countArcsInShapes(targetShapes, targetFlags);
52327
- if (otherShapes.length > 0) {
52328
- countArcsInShapes(otherShapes, otherFlags);
52329
- applyArrayMask(otherFlags, targetFlags);
52330
- noteDatasetWillChange(dataset, {operation: 'affine', unit: 'arcs'});
52331
- dataset.arcs = duplicateSelectedArcs(otherShapes, arcs, otherFlags);
52332
- markDatasetChanged(dataset, {operation: 'affine', unit: 'arcs'});
52333
- }
52334
- dataset.arcs.transformPoints(function(x, y, arcId) {
52335
- if (arcId < targetFlags.length && targetFlags[arcId] > 0) {
52336
- return transform(x, y);
52337
- }
52338
- });
52339
- }
52340
- targetPointLayers.forEach(function(lyr) {
52341
- noteLayerWillChange(lyr, {operation: 'affine', unit: 'shapes'});
52342
- });
52343
- forEachPoint(targetPoints, function(p) {
52344
- var p2 = transform(p[0], p[1]);
52345
- p[0] = p2[0];
52346
- p[1] = p2[1];
52347
- });
52348
- targetPointLayers.forEach(function(lyr) {
52349
- markLayerChanged(lyr, {operation: 'affine', unit: 'shapes'});
52350
- });
52351
- };
52352
-
52353
- function getAffineAnchor(dataset, opts) {
52354
- var anchor, bounds;
52355
- if (opts.anchor) {
52356
- anchor = opts.anchor;
52357
- } else {
52358
- // get bounds of selected shapes to calculate center of rotation/scale
52359
- bounds = getDatasetBounds(dataset);
52360
- anchor = [bounds.centerX(), bounds.centerY()];
52361
- }
52362
- return anchor;
52363
- }
52364
-
52365
- // TODO: handle problems with unprojected datasets
52366
- // option 1: don't allow affine transformation of unprojected data
52367
- // option 2: error if transformed data exceeds valid coordinate range
52368
- // source: http://mathworld.wolfram.com/AffineTransformation.html
52369
- function getAffineTransform(rotation, scale, shift, anchor) {
52370
- var angle = rotation * Math.PI / 180;
52371
- var a = scale * Math.cos(angle);
52372
- var b = -scale * Math.sin(angle);
52373
- return function(x, y) {
52374
- var x2 = a * (x - anchor[0]) - b * (y - anchor[1]) + shift[0] + anchor[0];
52375
- var y2 = b * (x - anchor[0]) + a * (y - anchor[1]) + shift[1] + anchor[1];
52376
- return [x2, y2];
52377
- };
52378
- }
52379
-
52380
- function getFitBoxTransform(bbox, points, shapes, arcs) {
52381
- var dataset = {
52382
- arcs: arcs,
52383
- layers: []
52384
- };
52385
- if (points && points.length) {
52386
- dataset.layers.push({
52387
- geometry_type: 'point',
52388
- shapes: points
52389
- });
52390
- }
52391
- if (shapes && shapes.length) {
52392
- dataset.layers.push({
52393
- geometry_type: 'polyline',
52394
- shapes: shapes
52395
- });
52396
- }
52397
- var frame = calcFrameData(dataset, {fit_bbox: bbox});
52398
- var fromBounds = new Bounds(frame.bbox);
52399
- var toBounds = new Bounds(frame.bbox2);
52400
- var fwd = fromBounds.getTransform(toBounds, false);
52401
- return function(x, y) {
52402
- return fwd.transform(x, y);
52403
- };
52404
- }
52405
-
52406
- function applyArrayMask(destArr, maskArr) {
52407
- for (var i=0, n=destArr.length; i<n; i++) {
52408
- if (maskArr[i] === 0) destArr[i] = 0;
52409
- }
52410
- }
52411
-
52412
- function duplicateSelectedArcs(shapes, arcs, flags) {
52413
- var arcCount = 0;
52414
- var vertexCount = 0;
52415
- var data = arcs.getVertexData();
52416
- var xx = [], yy = [], nn = [], map = [], n;
52417
- for (var i=0, len=flags.length; i<len; i++) {
52418
- if (flags[i] > 0) {
52419
- map[i] = arcs.size() + arcCount;
52420
- n = data.nn[i];
52421
- utils.copyElements(data.xx, data.ii[i], xx, vertexCount, n);
52422
- utils.copyElements(data.yy, data.ii[i], yy, vertexCount, n);
52423
- nn.push(n);
52424
- vertexCount += n;
52425
- arcCount++;
52426
- }
52427
- }
52428
- forEachArcId(shapes, function(id) {
52429
- var absId = absArcId(id);
52430
- if (flags[absId] > 0) {
52431
- return id < 0 ? ~map[absId] : map[absId];
52432
- }
52433
- });
52434
- return mergeArcs([arcs, new ArcCollection(nn, xx, yy)]);
52435
- }
52436
-
52437
53328
  const epsilon = 1.1102230246251565e-16;
52438
53329
  const splitter = 134217729;
52439
53330
  const resulterrbound = (3 + 8 * epsilon) * epsilon;
@@ -75976,7 +76867,7 @@ ${svg}
75976
76867
 
75977
76868
  function createMeridianPart(x, ymin, ymax) {
75978
76869
  var coords = densifyPathByInterval([[x, ymin], [x, ymax]], precision);
75979
- meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord$1(x)}));
76870
+ meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord(x)}));
75980
76871
  }
75981
76872
 
75982
76873
  function createParallel(y) {
@@ -76010,7 +76901,7 @@ ${svg}
76010
76901
  }
76011
76902
 
76012
76903
  // remove tiny offsets
76013
- function roundCoord$1(x) {
76904
+ function roundCoord(x) {
76014
76905
  return +x.toFixed(3) || 0;
76015
76906
  }
76016
76907
 
@@ -79050,160 +79941,6 @@ ${svg}
79050
79941
  };
79051
79942
  }
79052
79943
 
79053
- var roundCoord = getRoundingFunction(0.01);
79054
-
79055
- function getSymbolFillColor(d) {
79056
- return d.fill || 'magenta';
79057
- }
79058
-
79059
- function getSymbolStrokeColor(d) {
79060
- return d.stroke || d.fill || 'magenta';
79061
- }
79062
-
79063
- function applySymbolStyles(sym, d) {
79064
- if (sym.type == 'polyline') {
79065
- sym.stroke = getSymbolStrokeColor(d);
79066
- } else {
79067
- sym.fill = getSymbolFillColor(d);
79068
- }
79069
- if (d.opacity) {
79070
- sym.opacity = d.opacity;
79071
- }
79072
- return sym;
79073
- }
79074
-
79075
- function getSymbolRadius(d) {
79076
- if (d.radius === 0 || d.length === 0 || d.r === 0) return 0;
79077
- return d.radius || d.length || d.r || 5; // use a default value
79078
- }
79079
-
79080
- // Converts the value of a list-valued option into an array of items. The value
79081
- // arrives either as a whole string (from a direct call, rather than from the
79082
- // -symbols option accessor) or as an array of items already resolved one by
79083
- // one. An item that holds an array of its own, as an expression like
79084
- // [R, R * 0.5] does, is flattened into the list. Items themselves are left
79085
- // unsplit, because a single item can contain a comma, as rgba() colors do.
79086
- function toItemList(val) {
79087
- var items = utils.isString(val) ? splitListItems(val) : [].concat(val);
79088
- var list = [];
79089
- for (var i=0; i<items.length; i++) {
79090
- if (Array.isArray(items[i])) {
79091
- list = list.concat(items[i]);
79092
- } else {
79093
- list.push(items[i]);
79094
- }
79095
- }
79096
- return list;
79097
- }
79098
-
79099
- // Like toItemList(), for options that take numbers. An item that is a string
79100
- // containing commas is split, so that a data field holding a value like "2,4"
79101
- // works as a list. Values that aren't numbers become NaN, for the caller to
79102
- // report or ignore.
79103
- function toNumberList(val) {
79104
- var list = [];
79105
- toItemList(val).forEach(function(item) {
79106
- if (utils.isString(item) && item.indexOf(',') > -1) {
79107
- list = list.concat(parseNumberList(item));
79108
- } else {
79109
- list.push(Number(item));
79110
- }
79111
- });
79112
- return list;
79113
- }
79114
-
79115
- // Returns the radius of the smallest circle centered on a symbol's point that
79116
- // covers the symbol, or null if the symbol contains a part whose reach can't be
79117
- // measured.
79118
- //
79119
- // Only a symbol drawn around its point can be described this way: a circle, or
79120
- // a group of the circles and polygons that make up ring, pie and donut symbols.
79121
- // An arrow is drawn from its point outward, so the circle around its point that
79122
- // covers it says nothing useful about the space it occupies -- arrows are plain
79123
- // polygon symbols, which reach this function only as group parts, which they
79124
- // never are.
79125
- function getSymbolBoundingRadius(sym) {
79126
- var max = 0;
79127
- var parts, r, i;
79128
- if (!sym) return null;
79129
- if (sym.type == 'circle') {
79130
- // Half of a stroke lies outside the circle it follows, which is how a ring
79131
- // symbol paints its bands.
79132
- return getPositiveNumber(sym.r) + getStrokeOutset(sym);
79133
- }
79134
- if (sym.type == 'polygon') {
79135
- forEachSymbolCoord(sym.coordinates || [], function(p) {
79136
- var dist = Math.sqrt(p[0] * p[0] + p[1] * p[1]);
79137
- if (dist > max) max = dist;
79138
- });
79139
- return max + getStrokeOutset(sym);
79140
- }
79141
- if (sym.type == 'group') {
79142
- parts = sym.parts || [];
79143
- for (i=0; i<parts.length; i++) {
79144
- r = getSymbolBoundingRadius(parts[i]);
79145
- if (r === null) return null;
79146
- if (r > max) max = r;
79147
- }
79148
- return max;
79149
- }
79150
- return null;
79151
- }
79152
-
79153
- // A stroke straddles the path it is drawn on, so half of its width extends
79154
- // beyond the shape.
79155
- function getStrokeOutset(sym) {
79156
- var width = sym['stroke-width'];
79157
- if (!sym.stroke || sym.stroke == 'none') return 0;
79158
- // an SVG stroke is one pixel wide unless stroke-width says otherwise
79159
- if (width === undefined || width === null || width === '') return 0.5;
79160
- return getPositiveNumber(width) / 2;
79161
- }
79162
-
79163
- function getPositiveNumber(val) {
79164
- var num = +val;
79165
- return num > 0 ? num : 0;
79166
- }
79167
-
79168
- function forEachSymbolCoord(coords, cb) {
79169
- var isPoint = coords && utils.isNumber(coords[0]);
79170
- var isNested = !isPoint && coords && Array.isArray(coords[0]);
79171
- if (isPoint) return cb(coords);
79172
- for (var i=0; i<coords.length; i++) {
79173
- if (isNested) forEachSymbolCoord(coords[i], cb);
79174
- }
79175
- }
79176
-
79177
- function flipY(coords) {
79178
- forEachSymbolCoord(coords, function(p) {
79179
- p[1] = -p[1];
79180
- });
79181
- }
79182
-
79183
- function scaleAndShiftCoords(coords, scale, shift) {
79184
- forEachSymbolCoord(coords, function(xy) {
79185
- xy[0] = xy[0] * scale + shift[0];
79186
- xy[1] = xy[1] * scale + shift[1];
79187
- });
79188
- }
79189
-
79190
- function roundCoordsForSVG(coords) {
79191
- forEachSymbolCoord(coords, function(p) {
79192
- p[0] = roundCoord(p[0]);
79193
- p[1] = roundCoord(p[1]);
79194
- });
79195
- }
79196
-
79197
- function rotateCoords(coords, rotation) {
79198
- if (!rotation) return;
79199
- var f = getAffineTransform(rotation, 1, [0, 0], [0, 0]);
79200
- forEachSymbolCoord(coords, function(p) {
79201
- var p2 = f(p[0], p[1]);
79202
- p[0] = p2[0];
79203
- p[1] = p2[1];
79204
- });
79205
- }
79206
-
79207
79944
  // Collision reduction for circular symbols, in the spirit of d3-force's
79208
79945
  // forceCollide. Overlapping symbols are pushed apart, but each one is kept
79209
79946
  // within a fixed distance of its true position.
@@ -83097,7 +83834,8 @@ ${svg}
83097
83834
  job.catalog.addDataset(cmd.addLayer(targetDataset, opts));
83098
83835
 
83099
83836
  } else if (name == 'update-frame') {
83100
- cmd.updateFrame(targetLayers, targetDataset, opts);
83837
+ cmd.updateFrame(targetLayers, targetDataset, opts,
83838
+ opts.fit ? expandCommandTargets(job.catalog.findCommandTargets(opts.fit)) : null);
83101
83839
 
83102
83840
  } else if (name == 'update-label') {
83103
83841
  cmd.updateLabel(targetLayers, targetDataset, opts);
@@ -83663,7 +84401,7 @@ ${svg}
83663
84401
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
83664
84402
  }
83665
84403
 
83666
- var version = "0.7.67";
84404
+ var version = "0.7.69";
83667
84405
 
83668
84406
  // Parse command line args into commands and run them
83669
84407
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.