mapshaper 0.7.67 → 0.7.68

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
@@ -27236,9 +27236,16 @@
27236
27236
  // The two copies are identical apart from their paint, so they sit in exactly
27237
27237
  // the same place whatever the label's position, alignment or path.
27238
27238
  //
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.
27239
+ // The halo copy is unfilled, which matches paint-order, where the fill covers
27240
+ // the stroke's inner half and nothing else. Its halo-opacity is written as
27241
+ // opacity rather than stroke-opacity, which Illustrator ignores on import; the
27242
+ // two look the same on a copy with nothing painted but its stroke.
27243
+ //
27244
+ // Illustrator's GPU preview compounds a translucent halo where glyphs
27245
+ // overlap; its CPU preview and its output do not. Nothing in the SVG was
27246
+ // found to avoid it: filling the copy (imported as two objects, and no
27247
+ // better), and putting the opacity on a group or two nested groups around
27248
+ // the copy, all show the same thing.
27242
27249
  //
27243
27250
  // Things that apply to the label as a whole move to the group: its transform,
27244
27251
  // which places it, and its opacity, which would otherwise fade each copy on its
@@ -27269,6 +27276,10 @@
27269
27276
  delete halo.properties['paint-order'];
27270
27277
  delete halo.properties['fill-opacity'];
27271
27278
  halo.properties.fill = 'none';
27279
+ if ('stroke-opacity' in halo.properties) {
27280
+ halo.properties.opacity = halo.properties['stroke-opacity'];
27281
+ delete halo.properties['stroke-opacity'];
27282
+ }
27272
27283
  HALO_STROKE_PROPERTIES.forEach(function(k) {
27273
27284
  delete text.properties[k];
27274
27285
  });
@@ -27340,6 +27351,23 @@
27340
27351
  // different one would put the label somewhere it is not drawn.
27341
27352
  var DEFAULT_LABEL_FONT_SIZE = 12;
27342
27353
 
27354
+ // The line height a label is drawn with when it carries none, as a multiple of
27355
+ // its font size.
27356
+ var DEFAULT_LINE_HEIGHT = 1.1;
27357
+
27358
+ // The dy of each line after a label's first, from its line-height.
27359
+ //
27360
+ // A bare number is a multiple of the font size, as in CSS, and not px, as a
27361
+ // bare dy would be in SVG -- so it is written in ems. Nobody who types 1.3
27362
+ // into a field called line height wants lines 1.3px apart, and a multiple
27363
+ // keeps its proportions when the font size changes. Values with units are
27364
+ // passed through.
27365
+ function getLineHeightDy(val) {
27366
+ var measure = val === undefined || val === null || val === '' ?
27367
+ DEFAULT_LINE_HEIGHT : parseSvgMeasure(val);
27368
+ return utils.isFiniteNumber(measure) ? measure + 'em' : measure;
27369
+ }
27370
+
27343
27371
  function toLabelString(val) {
27344
27372
  if (val || val === 0 || val === false) return String(val);
27345
27373
  return '';
@@ -27387,7 +27415,7 @@
27387
27415
  value: line,
27388
27416
  properties: {
27389
27417
  x: dx,
27390
- dy: rec['line-height'] || '1.1em'
27418
+ dy: getLineHeightDy(rec['line-height'])
27391
27419
  }
27392
27420
  };
27393
27421
  obj.children.push(tspan);
@@ -27481,8 +27509,10 @@
27481
27509
  var SvgLabels = /*#__PURE__*/Object.freeze({
27482
27510
  __proto__: null,
27483
27511
  DEFAULT_LABEL_FONT_SIZE: DEFAULT_LABEL_FONT_SIZE,
27512
+ DEFAULT_LINE_HEIGHT: DEFAULT_LINE_HEIGHT,
27484
27513
  LABEL_SOFT_BREAK: LABEL_SOFT_BREAK,
27485
27514
  getDrawnLabelOffset: getDrawnLabelOffset,
27515
+ getLineHeightDy: getLineHeightDy,
27486
27516
  labelNewlineRxp: labelNewlineRxp,
27487
27517
  removeSoftBreaks: removeSoftBreaks,
27488
27518
  renderLabel: renderLabel$1,
@@ -27505,7 +27535,6 @@
27505
27535
 
27506
27536
  var DEFAULT_PADDING$1 = 3;
27507
27537
  var DEFAULT_LINE_WIDTH = 1;
27508
- var DEFAULT_LINE_HEIGHT = '1.1em';
27509
27538
  // Space left between the anchor's symbol and the end of the line
27510
27539
  var SYMBOL_CLEARANCE = 2;
27511
27540
  // How far an automatic curve bows out, as a fraction of its chord
@@ -27526,6 +27555,9 @@
27526
27555
  var ASCENT = 0.8;
27527
27556
  var DESCENT = 0.2;
27528
27557
  var MIDLINE = 0.35;
27558
+ // Average advance of a character, in ems, for estimating the width of text
27559
+ // that cannot be measured
27560
+ var AVERAGE_CHAR_WIDTH = 0.6;
27529
27561
 
27530
27562
  function labelHasCallout(rec) {
27531
27563
  var type = rec && rec.callout ? parseCalloutType(rec.callout) : null;
@@ -27589,18 +27621,24 @@
27589
27621
  // no measurement the box takes the label's width if it is a text block, and is
27590
27622
  // otherwise a vertical line through the text's origin -- a callout then meets
27591
27623
  // the text where it starts rather than not being drawn.
27592
- function getLabelTextBox(rec) {
27624
+ //
27625
+ // opts.estimate_width: with no measurement and no label-width, estimate the
27626
+ // width from the length of the longest line instead of taking it as zero
27627
+ function getLabelTextBox(rec, opts) {
27593
27628
  var fontSize = getFontSizeInPx$1(rec);
27594
27629
  var offset = getDrawnLabelOffset(rec);
27595
27630
  var lines = splitLabelLines(toLabelString(rec['label-text']));
27596
- var lineHeight = measureToPx(rec['line-height'] || DEFAULT_LINE_HEIGHT, fontSize);
27631
+ var lineHeight = measureToPx(getLineHeightDy(rec['line-height']), fontSize);
27597
27632
  var width = getMeasuredTextWidth(rec) || getNumber(rec['label-width'], 0);
27633
+ if (!width && opts && opts.estimate_width) {
27634
+ width = estimateTextWidth(lines, fontSize);
27635
+ }
27598
27636
  // An anchor the record does not set is inherited from the layer's group,
27599
27637
  // which is 'middle' -- see getLabelTextDefaults().
27600
27638
  var anchor = offset['text-anchor'] || 'middle';
27601
27639
  var xmin = offset.dx - width * (anchor == 'middle' ? 0.5 : anchor == 'end' ? 1 : 0);
27602
27640
  var baseline = offset.dy + getBaselineShift(rec['dominant-baseline']) * fontSize;
27603
- if (!(lineHeight > 0)) lineHeight = fontSize * 1.1;
27641
+ if (!(lineHeight > 0)) lineHeight = fontSize * DEFAULT_LINE_HEIGHT;
27604
27642
  return {
27605
27643
  xmin: xmin,
27606
27644
  xmax: xmin + width,
@@ -27610,6 +27648,18 @@
27610
27648
  };
27611
27649
  }
27612
27650
 
27651
+ function estimateTextWidth(lines, fontSize) {
27652
+ var chars = lines.reduce(function(max, line) {
27653
+ return Math.max(max, Array.from(line).length);
27654
+ }, 0);
27655
+ return chars * AVERAGE_CHAR_WIDTH * fontSize;
27656
+ }
27657
+
27658
+ // The size a label's text is drawn at, in px
27659
+ function getLabelFontSize(rec) {
27660
+ return getFontSizeInPx$1(rec);
27661
+ }
27662
+
27613
27663
  // Where a line meets a callout's text and how it gets there from the anchor.
27614
27664
  //
27615
27665
  // o.type 'line', 'elbow' or 'curve'
@@ -27636,7 +27686,7 @@
27636
27686
  // point where the line meets it, the elbow's corner or the curve's midpoint
27637
27687
  // (null for a straight line), and where the line stops short of the anchor.
27638
27688
  function getCalloutShape(o) {
27639
- var box = padBox(o.box, o.padding || 0);
27689
+ var box = padBox$1(o.box, o.padding || 0);
27640
27690
  var a = [0, 0];
27641
27691
  var size = o.endSize > 0 ? o.endSize : getDefaultCalloutEndSize(o.end, o.width);
27642
27692
  var t, v, path, tip, dir, out, lineWidth, headLen, curved;
@@ -27997,7 +28047,7 @@
27997
28047
 
27998
28048
  // Utilities
27999
28049
 
28000
- function padBox(box, pad) {
28050
+ function padBox$1(box, pad) {
28001
28051
  return {
28002
28052
  xmin: box.xmin - pad,
28003
28053
  xmax: box.xmax + pad,
@@ -28070,6 +28120,7 @@
28070
28120
  getControlPoint: getControlPoint,
28071
28121
  getDefaultCalloutEndSize: getDefaultCalloutEndSize,
28072
28122
  getLabelCalloutShape: getLabelCalloutShape,
28123
+ getLabelFontSize: getLabelFontSize,
28073
28124
  getLabelTextBox: getLabelTextBox,
28074
28125
  labelHasCallout: labelHasCallout,
28075
28126
  renderLabelCallout: renderLabelCallout
@@ -28366,7 +28417,9 @@
28366
28417
 
28367
28418
  var SvgSymbols = /*#__PURE__*/Object.freeze({
28368
28419
  __proto__: null,
28420
+ featureHasIcon: featureHasIcon,
28369
28421
  getAnchorSymbolRadius: getAnchorSymbolRadius,
28422
+ getIconRadius: getIconRadius,
28370
28423
  getTransform: getTransform,
28371
28424
  renderPoint: renderPoint,
28372
28425
  symbolRenderers: symbolRenderers
@@ -41392,7 +41445,7 @@ ${svg}
41392
41445
  'font-weight': {describe: 'normal, bold or a numeric weight'},
41393
41446
  'font-stretch': {describe: 'e.g. condensed'},
41394
41447
  'letter-spacing': {describe: 'extra space between letters'},
41395
- 'line-height': {describe: 'spacing between lines of a multi-line label'},
41448
+ 'line-height': {describe: 'spacing between lines of a multi-line label; a bare number is a multiple of the font size'},
41396
41449
  'text-anchor': {describe: 'start, middle or end'},
41397
41450
  'label-align': {describe: 'how the lines of a multi-line label align: left, center or right'},
41398
41451
  'dominant-baseline': {describe: 'vertical alignment, e.g. central'},
@@ -43478,7 +43531,7 @@ ${svg}
43478
43531
  describe: 'CSS letter-spacing property of labels'
43479
43532
  })
43480
43533
  .option('line-height', {
43481
- describe: 'line spacing of multi-line labels (default is 1.1em)'
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)'
43482
43535
  })
43483
43536
  .option('dominant-baseline', {
43484
43537
  describe: 'vertical alignment of labels (e.g. central)'
@@ -43812,6 +43865,13 @@ ${svg}
43812
43865
  describe: 'frame coordinates (xmin,ymin,xmax,ymax)',
43813
43866
  type: 'bbox'
43814
43867
  })
43868
+ .option('fit', {
43869
+ describe: 'layer(s) to fit the frame to, including their symbols and labels'
43870
+ })
43871
+ .option('ignore-symbols', {
43872
+ describe: 'with fit=, fit to point locations, not symbol and label extents',
43873
+ type: 'flag'
43874
+ })
43815
43875
  .option('width', {
43816
43876
  describe: 'nominal frame width (e.g. 5in, 10cm, 600px)'
43817
43877
  })
@@ -43963,6 +44023,10 @@ ${svg}
43963
44023
  describe: 'separate offsets for each side, in l,b,r,t order',
43964
44024
  type: 'strings'
43965
44025
  })
44026
+ .option('ignore-symbols', {
44027
+ describe: 'fit to point locations, not the extent of symbols and labels',
44028
+ type: 'flag'
44029
+ })
43966
44030
  .option('name', nameOpt)
43967
44031
  .option('replace', {
43968
44032
  describe: 'replace the existing map frame',
@@ -51575,8 +51639,637 @@ ${svg}
51575
51639
  bboxToPolygon: bboxToPolygon
51576
51640
  });
51577
51641
 
51642
+ // Apply rotation, scale and/or shift to some or all of the features in a dataset
51643
+ //
51644
+ cmd.affine = function(targetLayers, dataset, opts) {
51645
+ // Need to separate the targeted shapes from any other shapes that share
51646
+ // the same topology. So we duplicate any arcs that are shared by the targeted
51647
+ // shapes and their topological neighbors and remap arc references in the
51648
+ // neighbors to point to the copies.
51649
+ // TODO: explore alternative: if some arcs are shared between transformed and
51650
+ // non-transformed shapes, first remove topology, then tranform, then rebuild topology
51651
+ //
51652
+ var rotateArg = opts.rotate || 0;
51653
+ var scaleArg = opts.scale || 1;
51654
+ var shiftArg = opts.shift ? convertIntervalPair(opts.shift, getDatasetCRS(dataset)) : [0, 0];
51655
+ var arcs = dataset.arcs;
51656
+ var targetShapes = [];
51657
+ var otherShapes = [];
51658
+ var targetPoints = [];
51659
+ var targetPointLayers = [];
51660
+ var targetFlags, otherFlags, transform;
51661
+ dataset.layers.filter(layerHasGeometry$1).forEach(function(lyr) {
51662
+ var hits = [],
51663
+ misses = [],
51664
+ test;
51665
+ if (targetLayers.indexOf(lyr) == -1) {
51666
+ misses = lyr.shapes;
51667
+ } else if (opts.where) {
51668
+ test = compileFeatureExpression(opts.where, lyr, dataset.arcs);
51669
+ lyr.shapes.forEach(function(shp, i) {
51670
+ (test(i) ? hits : misses).push(shp);
51671
+ });
51672
+ } else {
51673
+ hits = lyr.shapes;
51674
+ }
51675
+ if (lyr.geometry_type == 'point') {
51676
+ targetPoints = targetPoints.concat(hits);
51677
+ if (hits.length > 0) targetPointLayers.push(lyr);
51678
+ } else {
51679
+ targetShapes = targetShapes.concat(hits);
51680
+ otherShapes = otherShapes.concat(misses);
51681
+ }
51682
+ });
51683
+ var anchorArg = getAffineAnchor({arcs: dataset.arcs, layers: [{
51684
+ geometry_type: 'point', shapes: targetPoints}, {geometry_type: 'polyline',
51685
+ shapes: targetShapes}]}, opts);
51686
+ transform = getAffineTransform(rotateArg, scaleArg, shiftArg, anchorArg);
51687
+ if (opts.fit_bbox) {
51688
+ transform = getFitBoxTransform(opts.fit_bbox, targetPoints, targetShapes, arcs);
51689
+ }
51690
+ if (targetShapes.length > 0) {
51691
+ targetFlags = new Uint8Array(arcs.size());
51692
+ otherFlags = new Uint8Array(arcs.size());
51693
+ countArcsInShapes(targetShapes, targetFlags);
51694
+ if (otherShapes.length > 0) {
51695
+ countArcsInShapes(otherShapes, otherFlags);
51696
+ applyArrayMask(otherFlags, targetFlags);
51697
+ noteDatasetWillChange(dataset, {operation: 'affine', unit: 'arcs'});
51698
+ dataset.arcs = duplicateSelectedArcs(otherShapes, arcs, otherFlags);
51699
+ markDatasetChanged(dataset, {operation: 'affine', unit: 'arcs'});
51700
+ }
51701
+ dataset.arcs.transformPoints(function(x, y, arcId) {
51702
+ if (arcId < targetFlags.length && targetFlags[arcId] > 0) {
51703
+ return transform(x, y);
51704
+ }
51705
+ });
51706
+ }
51707
+ targetPointLayers.forEach(function(lyr) {
51708
+ noteLayerWillChange(lyr, {operation: 'affine', unit: 'shapes'});
51709
+ });
51710
+ forEachPoint(targetPoints, function(p) {
51711
+ var p2 = transform(p[0], p[1]);
51712
+ p[0] = p2[0];
51713
+ p[1] = p2[1];
51714
+ });
51715
+ targetPointLayers.forEach(function(lyr) {
51716
+ markLayerChanged(lyr, {operation: 'affine', unit: 'shapes'});
51717
+ });
51718
+ };
51719
+
51720
+ function getAffineAnchor(dataset, opts) {
51721
+ var anchor, bounds;
51722
+ if (opts.anchor) {
51723
+ anchor = opts.anchor;
51724
+ } else {
51725
+ // get bounds of selected shapes to calculate center of rotation/scale
51726
+ bounds = getDatasetBounds(dataset);
51727
+ anchor = [bounds.centerX(), bounds.centerY()];
51728
+ }
51729
+ return anchor;
51730
+ }
51731
+
51732
+ // TODO: handle problems with unprojected datasets
51733
+ // option 1: don't allow affine transformation of unprojected data
51734
+ // option 2: error if transformed data exceeds valid coordinate range
51735
+ // source: http://mathworld.wolfram.com/AffineTransformation.html
51736
+ function getAffineTransform(rotation, scale, shift, anchor) {
51737
+ var angle = rotation * Math.PI / 180;
51738
+ var a = scale * Math.cos(angle);
51739
+ var b = -scale * Math.sin(angle);
51740
+ return function(x, y) {
51741
+ var x2 = a * (x - anchor[0]) - b * (y - anchor[1]) + shift[0] + anchor[0];
51742
+ var y2 = b * (x - anchor[0]) + a * (y - anchor[1]) + shift[1] + anchor[1];
51743
+ return [x2, y2];
51744
+ };
51745
+ }
51746
+
51747
+ function getFitBoxTransform(bbox, points, shapes, arcs) {
51748
+ var dataset = {
51749
+ arcs: arcs,
51750
+ layers: []
51751
+ };
51752
+ if (points && points.length) {
51753
+ dataset.layers.push({
51754
+ geometry_type: 'point',
51755
+ shapes: points
51756
+ });
51757
+ }
51758
+ if (shapes && shapes.length) {
51759
+ dataset.layers.push({
51760
+ geometry_type: 'polyline',
51761
+ shapes: shapes
51762
+ });
51763
+ }
51764
+ var frame = calcFrameData(dataset, {fit_bbox: bbox});
51765
+ var fromBounds = new Bounds(frame.bbox);
51766
+ var toBounds = new Bounds(frame.bbox2);
51767
+ var fwd = fromBounds.getTransform(toBounds, false);
51768
+ return function(x, y) {
51769
+ return fwd.transform(x, y);
51770
+ };
51771
+ }
51772
+
51773
+ function applyArrayMask(destArr, maskArr) {
51774
+ for (var i=0, n=destArr.length; i<n; i++) {
51775
+ if (maskArr[i] === 0) destArr[i] = 0;
51776
+ }
51777
+ }
51778
+
51779
+ function duplicateSelectedArcs(shapes, arcs, flags) {
51780
+ var arcCount = 0;
51781
+ var vertexCount = 0;
51782
+ var data = arcs.getVertexData();
51783
+ var xx = [], yy = [], nn = [], map = [], n;
51784
+ for (var i=0, len=flags.length; i<len; i++) {
51785
+ if (flags[i] > 0) {
51786
+ map[i] = arcs.size() + arcCount;
51787
+ n = data.nn[i];
51788
+ utils.copyElements(data.xx, data.ii[i], xx, vertexCount, n);
51789
+ utils.copyElements(data.yy, data.ii[i], yy, vertexCount, n);
51790
+ nn.push(n);
51791
+ vertexCount += n;
51792
+ arcCount++;
51793
+ }
51794
+ }
51795
+ forEachArcId(shapes, function(id) {
51796
+ var absId = absArcId(id);
51797
+ if (flags[absId] > 0) {
51798
+ return id < 0 ? ~map[absId] : map[absId];
51799
+ }
51800
+ });
51801
+ return mergeArcs([arcs, new ArcCollection(nn, xx, yy)]);
51802
+ }
51803
+
51804
+ var roundCoord$1 = getRoundingFunction(0.01);
51805
+
51806
+ function getSymbolFillColor(d) {
51807
+ return d.fill || 'magenta';
51808
+ }
51809
+
51810
+ function getSymbolStrokeColor(d) {
51811
+ return d.stroke || d.fill || 'magenta';
51812
+ }
51813
+
51814
+ function applySymbolStyles(sym, d) {
51815
+ if (sym.type == 'polyline') {
51816
+ sym.stroke = getSymbolStrokeColor(d);
51817
+ } else {
51818
+ sym.fill = getSymbolFillColor(d);
51819
+ }
51820
+ if (d.opacity) {
51821
+ sym.opacity = d.opacity;
51822
+ }
51823
+ return sym;
51824
+ }
51825
+
51826
+ function getSymbolRadius(d) {
51827
+ if (d.radius === 0 || d.length === 0 || d.r === 0) return 0;
51828
+ return d.radius || d.length || d.r || 5; // use a default value
51829
+ }
51830
+
51831
+ // Converts the value of a list-valued option into an array of items. The value
51832
+ // arrives either as a whole string (from a direct call, rather than from the
51833
+ // -symbols option accessor) or as an array of items already resolved one by
51834
+ // one. An item that holds an array of its own, as an expression like
51835
+ // [R, R * 0.5] does, is flattened into the list. Items themselves are left
51836
+ // unsplit, because a single item can contain a comma, as rgba() colors do.
51837
+ function toItemList(val) {
51838
+ var items = utils.isString(val) ? splitListItems(val) : [].concat(val);
51839
+ var list = [];
51840
+ for (var i=0; i<items.length; i++) {
51841
+ if (Array.isArray(items[i])) {
51842
+ list = list.concat(items[i]);
51843
+ } else {
51844
+ list.push(items[i]);
51845
+ }
51846
+ }
51847
+ return list;
51848
+ }
51849
+
51850
+ // Like toItemList(), for options that take numbers. An item that is a string
51851
+ // containing commas is split, so that a data field holding a value like "2,4"
51852
+ // works as a list. Values that aren't numbers become NaN, for the caller to
51853
+ // report or ignore.
51854
+ function toNumberList(val) {
51855
+ var list = [];
51856
+ toItemList(val).forEach(function(item) {
51857
+ if (utils.isString(item) && item.indexOf(',') > -1) {
51858
+ list = list.concat(parseNumberList(item));
51859
+ } else {
51860
+ list.push(Number(item));
51861
+ }
51862
+ });
51863
+ return list;
51864
+ }
51865
+
51866
+ // Returns the radius of the smallest circle centered on a symbol's point that
51867
+ // covers the symbol, or null if the symbol contains a part whose reach can't be
51868
+ // measured.
51869
+ //
51870
+ // Only a symbol drawn around its point can be described this way: a circle, or
51871
+ // a group of the circles and polygons that make up ring, pie and donut symbols.
51872
+ // An arrow is drawn from its point outward, so the circle around its point that
51873
+ // covers it says nothing useful about the space it occupies -- arrows are plain
51874
+ // polygon symbols, which reach this function only as group parts, which they
51875
+ // never are.
51876
+ function getSymbolBoundingRadius(sym) {
51877
+ var max = 0;
51878
+ var parts, r, i;
51879
+ if (!sym) return null;
51880
+ if (sym.type == 'circle') {
51881
+ // Half of a stroke lies outside the circle it follows, which is how a ring
51882
+ // symbol paints its bands.
51883
+ return getPositiveNumber(sym.r) + getStrokeOutset(sym);
51884
+ }
51885
+ if (sym.type == 'polygon') {
51886
+ forEachSymbolCoord(sym.coordinates || [], function(p) {
51887
+ var dist = Math.sqrt(p[0] * p[0] + p[1] * p[1]);
51888
+ if (dist > max) max = dist;
51889
+ });
51890
+ return max + getStrokeOutset(sym);
51891
+ }
51892
+ if (sym.type == 'group') {
51893
+ parts = sym.parts || [];
51894
+ for (i=0; i<parts.length; i++) {
51895
+ r = getSymbolBoundingRadius(parts[i]);
51896
+ if (r === null) return null;
51897
+ if (r > max) max = r;
51898
+ }
51899
+ return max;
51900
+ }
51901
+ return null;
51902
+ }
51903
+
51904
+ // A stroke straddles the path it is drawn on, so half of its width extends
51905
+ // beyond the shape.
51906
+ function getStrokeOutset(sym) {
51907
+ var width = sym['stroke-width'];
51908
+ if (!sym.stroke || sym.stroke == 'none') return 0;
51909
+ // an SVG stroke is one pixel wide unless stroke-width says otherwise
51910
+ if (width === undefined || width === null || width === '') return 0.5;
51911
+ return getPositiveNumber(width) / 2;
51912
+ }
51913
+
51914
+ function getPositiveNumber(val) {
51915
+ var num = +val;
51916
+ return num > 0 ? num : 0;
51917
+ }
51918
+
51919
+ function forEachSymbolCoord(coords, cb) {
51920
+ var isPoint = coords && utils.isNumber(coords[0]);
51921
+ var isNested = !isPoint && coords && Array.isArray(coords[0]);
51922
+ if (isPoint) return cb(coords);
51923
+ for (var i=0; i<coords.length; i++) {
51924
+ if (isNested) forEachSymbolCoord(coords[i], cb);
51925
+ }
51926
+ }
51927
+
51928
+ function flipY(coords) {
51929
+ forEachSymbolCoord(coords, function(p) {
51930
+ p[1] = -p[1];
51931
+ });
51932
+ }
51933
+
51934
+ function scaleAndShiftCoords(coords, scale, shift) {
51935
+ forEachSymbolCoord(coords, function(xy) {
51936
+ xy[0] = xy[0] * scale + shift[0];
51937
+ xy[1] = xy[1] * scale + shift[1];
51938
+ });
51939
+ }
51940
+
51941
+ function roundCoordsForSVG(coords) {
51942
+ forEachSymbolCoord(coords, function(p) {
51943
+ p[0] = roundCoord$1(p[0]);
51944
+ p[1] = roundCoord$1(p[1]);
51945
+ });
51946
+ }
51947
+
51948
+ function rotateCoords(coords, rotation) {
51949
+ if (!rotation) return;
51950
+ var f = getAffineTransform(rotation, 1, [0, 0], [0, 0]);
51951
+ forEachSymbolCoord(coords, function(p) {
51952
+ var p2 = f(p[0], p[1]);
51953
+ p[0] = p2[0];
51954
+ p[1] = p2[1];
51955
+ });
51956
+ }
51957
+
51958
+ // The space that a point's symbol and label take up when drawn, for fitting a
51959
+ // map frame around them.
51960
+ //
51961
+ // Boxes are [xmin, ymin, xmax, ymax] in px, relative to the point, with y down:
51962
+ // the space symbols are drawn in (see renderPoint() in svg-symbols.mjs, which
51963
+ // this follows). A box can leave out parts of what gets drawn -- anti-aliasing,
51964
+ // miter joins, a curve bowing past its control points -- but not by more than a
51965
+ // pixel or so.
51966
+
51967
+ // The box around what renderPoint() draws for @rec, or null if it draws nothing
51968
+ // that can be sized.
51969
+ // @cache: optional Map, for reusing the boxes of svg-symbol strings, which are
51970
+ // often the same for every feature in a layer
51971
+ function getPointSymbolBox(rec, cache) {
51972
+ var box = null;
51973
+ if (!rec) return null;
51974
+ if (featureHasSvgSymbol(rec)) {
51975
+ box = getSymbolBox(rec, cache);
51976
+ }
51977
+ if (featureHasLabel(rec)) {
51978
+ box = mergeBoxes(box, getLabelBox(rec));
51979
+ }
51980
+ return box;
51981
+ }
51982
+
51983
+ // How far the text of a label drawn along a path can reach from the path, in
51984
+ // px. The side the text sits on turns with the path, so it is allowed for on
51985
+ // every side.
51986
+ function getPathLabelPadding(rec) {
51987
+ return getLabelFontSize(rec) + getHaloWidth(rec);
51988
+ }
51989
+
51990
+ function getLabelBox(rec) {
51991
+ var o = getLabelTextBox(rec, {estimate_width: true});
51992
+ var pad = getHaloWidth(rec);
51993
+ var box = [o.xmin - pad, o.ymin - pad, o.xmax + pad, o.ymax + pad];
51994
+ var via = labelHasCallout(rec) ? parsePointPair(rec['callout-via'] || '') : null;
51995
+ if (via) {
51996
+ box = mergeBoxes(box, [via[0], via[1], via[0], via[1]]);
51997
+ }
51998
+ return box;
51999
+ }
52000
+
52001
+ function getHaloWidth(rec) {
52002
+ var w = +rec['halo-width'];
52003
+ return w > 0 ? w : 0;
52004
+ }
52005
+
52006
+ function getSymbolBox(d, cache) {
52007
+ var sym = d['svg-symbol'];
52008
+ if (sym) return getComplexSymbolBox(sym, cache);
52009
+ if (featureHasIcon(d)) return getIconBox(d);
52010
+ if (d.r > 0) return getSquareBox(+d.r + getStrokeOutset(d), 0, 0);
52011
+ return null;
52012
+ }
52013
+
52014
+ function getIconBox(d) {
52015
+ var type = d.icon || 'circle';
52016
+ var r = getIconRadius(d, type);
52017
+ var sw;
52018
+ if (!(r > 0)) return null;
52019
+ if (type == 'ring') {
52020
+ // drawn as a stroke, one px wide unless stroke-width says otherwise
52021
+ sw = +d['stroke-width'];
52022
+ return getSquareBox(r + (sw > 0 ? sw : 1) / 2, 0, 0);
52023
+ }
52024
+ if (type == 'circle' || type == 'square' || type == 'star') {
52025
+ return getSquareBox(r + getStrokeOutset(d), 0, 0);
52026
+ }
52027
+ return null; // unsupported icons are not drawn
52028
+ }
52029
+
52030
+ function getComplexSymbolBox(sym, cache) {
52031
+ var box;
52032
+ if (!utils.isString(sym)) return getSymbolPartBox(sym, 0, 0);
52033
+ if (cache && cache.has(sym)) return cache.get(sym);
52034
+ try {
52035
+ box = getSymbolPartBox(JSON.parse(sym), 0, 0);
52036
+ } catch(e) {
52037
+ box = null;
52038
+ }
52039
+ if (cache) cache.set(sym, box);
52040
+ return box;
52041
+ }
52042
+
52043
+ // Follows the renderers in svg-symbols.mjs: @x, @y is where the part is drawn,
52044
+ // which circles, squares, images, lines and labels are placed at and polygons
52045
+ // and polylines ignore.
52046
+ function getSymbolPartBox(sym, x, y) {
52047
+ var type = sym && sym.type;
52048
+ var w, h;
52049
+ if (!sym || sym.tag) return null; // raw SVG can't be sized
52050
+ if (type == 'circle' || type == 'square') {
52051
+ return getSquareBox(getPositive(sym.r) + getStrokeOutset(sym), x, y);
52052
+ }
52053
+ if (type == 'image') {
52054
+ w = sym.width || 20;
52055
+ h = sym.height || 20;
52056
+ return [x - w / 2, y - h / 2, x + w / 2, y + h / 2];
52057
+ }
52058
+ if (type == 'polygon') {
52059
+ return padBox(getCoordsBox(sym.coordinates), getStrokeOutset(sym));
52060
+ }
52061
+ if (type == 'polyline') {
52062
+ return padBox(getCoordsBox(sym.coordinates), getLineOutset(sym));
52063
+ }
52064
+ if (type == 'line') {
52065
+ return padBox(getCoordsBox([[x, y], [x + (sym.dx || 0), y + (sym.dy || 0)]]),
52066
+ getLineOutset(sym));
52067
+ }
52068
+ if (type == 'label') {
52069
+ return shiftBox(getLabelBox(sym), x, y);
52070
+ }
52071
+ if (type == 'group') {
52072
+ return getGroupBox(sym, x, y);
52073
+ }
52074
+ return null; // offset draws nothing, and unknown types are not drawn
52075
+ }
52076
+
52077
+ // A line part moves the parts after it to its far end; an offset part moves
52078
+ // them into a group translated by its offset.
52079
+ function getGroupBox(sym, x, y) {
52080
+ var box = null;
52081
+ var ox = 0, oy = 0;
52082
+ (sym.parts || []).forEach(function(part) {
52083
+ box = mergeBoxes(box, shiftBox(getSymbolPartBox(part, x, y), ox, oy));
52084
+ if (!part) return;
52085
+ if (part.type == 'line') {
52086
+ x += part.dx || 0;
52087
+ y += part.dy || 0;
52088
+ } else if (part.type == 'offset') {
52089
+ ox += x + (part.dx || 0);
52090
+ oy += y + (part.dy || 0);
52091
+ x = y = 0;
52092
+ }
52093
+ });
52094
+ return box;
52095
+ }
52096
+
52097
+ // A stroked line is one px wide unless stroke-width says otherwise
52098
+ function getLineOutset(sym) {
52099
+ var sw = +sym['stroke-width'];
52100
+ return (sw > 0 ? sw : 1) / 2;
52101
+ }
52102
+
52103
+ function getCoordsBox(coords) {
52104
+ var box = null;
52105
+ forEachSymbolCoord(coords || [], function(p) {
52106
+ if (!isFinite(p[0]) || !isFinite(p[1])) return;
52107
+ box = mergeBoxes(box, [p[0], p[1], p[0], p[1]]);
52108
+ });
52109
+ return box;
52110
+ }
52111
+
52112
+ function getSquareBox(r, x, y) {
52113
+ return r > 0 ? [x - r, y - r, x + r, y + r] : null;
52114
+ }
52115
+
52116
+ function padBox(box, pad) {
52117
+ return box ? [box[0] - pad, box[1] - pad, box[2] + pad, box[3] + pad] : null;
52118
+ }
52119
+
52120
+ function shiftBox(box, dx, dy) {
52121
+ if (!box || !dx && !dy) return box;
52122
+ return [box[0] + dx, box[1] + dy, box[2] + dx, box[3] + dy];
52123
+ }
52124
+
52125
+ function mergeBoxes(a, b) {
52126
+ if (!a) return b;
52127
+ if (!b) return a;
52128
+ return [Math.min(a[0], b[0]), Math.min(a[1], b[1]),
52129
+ Math.max(a[2], b[2]), Math.max(a[3], b[3])];
52130
+ }
52131
+
52132
+ function getPositive(val) {
52133
+ var num = +val;
52134
+ return num > 0 ? num : 0;
52135
+ }
52136
+
52137
+ // Fitting a map frame to its content, including the symbols and labels drawn
52138
+ // at points, which take up room that the point coordinates alone do not.
52139
+ //
52140
+ // Symbols are sized in output pixels, and how many map units a pixel covers is
52141
+ // set by the frame being fitted, so the extent can't simply be padded. It is
52142
+ // found by solving for the scale instead: the smallest scale s (map units per
52143
+ // output pixel) at which the content -- points and their symbols, drawn at s --
52144
+ // gets a frame whose own scale is no larger than s.
52145
+ //
52146
+ // Every point's box moves each side of the extent linearly with s, so the
52147
+ // extent's width and height are convex, piecewise-linear functions of s, and so
52148
+ // is the frame scale they produce. Newton's method converges on the smallest
52149
+ // fixed point of such a function from below, without overshooting, in as many
52150
+ // steps as there are pieces along the way -- usually two or three.
52151
+
52152
+ var MAX_ITERATIONS = 50;
52153
+ var TOLERANCE = 1e-9;
52154
+
52155
+ // Returns the extent of @targets as a bbox array, padded to hold the symbols
52156
+ // and labels at their points, or null if the targets have no extent.
52157
+ //
52158
+ // @targets [{layer, dataset}]
52159
+ // @getScale function(bbox) -> map units per output px of a frame fitted to
52160
+ // @bbox; this is where the caller's offsets and page shape come in.
52161
+ // May return NaN for a bbox the frame can't be fitted to.
52162
+ // @opts.ignore_symbols fit to the coordinates alone
52163
+ function getFrameContentBbox(targets, getScale, opts) {
52164
+ var bounds = targets.reduce(function(memo, o) {
52165
+ return memo.mergeBounds(getLayerBounds(o.layer, o.dataset.arcs));
52166
+ }, new Bounds());
52167
+ var raw, items, bbox;
52168
+ if (!bounds.hasBounds()) return null;
52169
+ raw = bounds.toArray();
52170
+ if (opts && opts.ignore_symbols) return raw;
52171
+ items = getSymbolExtents(targets);
52172
+ if (items.length === 0) return raw;
52173
+ bbox = fitBboxToSymbols(raw, items, getScale);
52174
+ if (!bbox) {
52175
+ warn('Symbols and labels are too large to fit in the frame; ' +
52176
+ 'fitting the frame to point locations only.');
52177
+ return raw;
52178
+ }
52179
+ return bbox;
52180
+ }
52181
+
52182
+ // The map units per output px of a frame with extent @bbox and nominal width
52183
+ // @width. A frame with a fixed page shape is filled out to that shape when it is
52184
+ // rendered (see fitDatasetToFrame()), so its scale is set by whichever of its
52185
+ // width and height is the tighter fit.
52186
+ function getFrameScale(bbox, width, fixedAspect) {
52187
+ var w = bbox[2] - bbox[0];
52188
+ var h = bbox[3] - bbox[1];
52189
+ return fixedAspect > 0 ? Math.max(w, h * fixedAspect) / width : w / width;
52190
+ }
52191
+
52192
+ // @raw bbox of the content without symbols
52193
+ // @items flat array of [x, y, xmin, ymin, xmax, ymax, ...]: each point and the
52194
+ // box around it in px, oriented like the map (y up)
52195
+ // Returns the padded bbox, or null if there is no scale at which the symbols
52196
+ // fit -- when they add up to more than the width of the page.
52197
+ function fitBboxToSymbols(raw, items, getScale) {
52198
+ var s = getScale(raw);
52199
+ var bbox, h, delta, slope, i;
52200
+ for (i = 0; i < MAX_ITERATIONS; i++) {
52201
+ if (!isValidScale(s)) return null;
52202
+ bbox = expandBbox(raw, items, s);
52203
+ h = getScale(bbox);
52204
+ if (!isValidScale(h)) return null;
52205
+ // Content with no extent of its own, like a single point, stops here with
52206
+ // s = 0: nothing about it sets a scale.
52207
+ if (h <= s * (1 + TOLERANCE)) return bbox;
52208
+ if (!(s > 0)) return null;
52209
+ delta = s * 1e-7;
52210
+ slope = (getScale(expandBbox(raw, items, s + delta)) - h) / delta;
52211
+ if (!(slope < 1)) return null;
52212
+ s += (h - s) / (1 - slope);
52213
+ }
52214
+ return expandBbox(raw, items, s);
52215
+ }
52216
+
52217
+ function isValidScale(s) {
52218
+ return s >= 0 && s < Infinity;
52219
+ }
52220
+
52221
+ function expandBbox(raw, items, s) {
52222
+ var xmin = raw[0], ymin = raw[1], xmax = raw[2], ymax = raw[3];
52223
+ var x, y, v;
52224
+ for (var i = 0, n = items.length; i < n; i += 6) {
52225
+ x = items[i];
52226
+ y = items[i + 1];
52227
+ v = x + items[i + 2] * s;
52228
+ if (v < xmin) xmin = v;
52229
+ v = y + items[i + 3] * s;
52230
+ if (v < ymin) ymin = v;
52231
+ v = x + items[i + 4] * s;
52232
+ if (v > xmax) xmax = v;
52233
+ v = y + items[i + 5] * s;
52234
+ if (v > ymax) ymax = v;
52235
+ }
52236
+ return [xmin, ymin, xmax, ymax];
52237
+ }
52238
+
52239
+ // Boxes come from the renderer's space, where y is down, and are flipped here
52240
+ // to the map's.
52241
+ function getSymbolExtents(targets) {
52242
+ var items = [];
52243
+ var cache = new Map();
52244
+ targets.forEach(function(o) {
52245
+ var lyr = o.layer;
52246
+ var records;
52247
+ if (lyr.geometry_type != 'point' || !lyr.shapes || !lyr.data) return;
52248
+ records = lyr.data.getRecords();
52249
+ lyr.shapes.forEach(function(shp, i) {
52250
+ var rec = records[i];
52251
+ var box, pad;
52252
+ if (!shp || !rec) return;
52253
+ if (shp.length > 1 && featureHasLabel(rec)) {
52254
+ // a multipoint label is drawn along a path through its points,
52255
+ // with no symbol (see featureIsPathLabel())
52256
+ pad = getPathLabelPadding(rec);
52257
+ box = [-pad, -pad, pad, pad];
52258
+ } else {
52259
+ box = getPointSymbolBox(rec, cache);
52260
+ }
52261
+ if (!box) return;
52262
+ for (var j = 0; j < shp.length; j++) {
52263
+ if (!shp[j] || !isFinite(shp[j][0]) || !isFinite(shp[j][1])) continue;
52264
+ items.push(shp[j][0], shp[j][1], box[0], -box[3], box[2], -box[1]);
52265
+ }
52266
+ });
52267
+ });
52268
+ return items;
52269
+ }
52270
+
51578
52271
  cmd.frame = function(catalog, targets, opts) {
51579
- var widthPx, heightPx, aspectRatio, bbox;
52272
+ var widthPx, heightPx, bbox;
51580
52273
  var existingFrame = getActiveFrame(catalog);
51581
52274
  if (opts.width) {
51582
52275
  widthPx = parseFrameSize(opts.width).valuePx;
@@ -51612,30 +52305,24 @@ ${svg}
51612
52305
  } else {
51613
52306
  var datasets = utils.pluck(targets, 'dataset');
51614
52307
  requireDatasetsHaveCompatibleCRS(datasets, 'Targets include both projected and unprojected coordinates');
51615
- bbox = getTargetBbox(targets);
52308
+ bbox = getFrameContentBbox(expandCommandTargets(targets), function(contentBbox) {
52309
+ var extent = getFrameExtent(contentBbox, widthPx, heightPx, opts);
52310
+ return getFrameScale(extent.bbox, extent.width, getFixedAspect$1(extent, opts));
52311
+ }, opts);
51616
52312
  if (!bbox) {
51617
52313
  stop$1('Command target is missing geographical bounds');
51618
52314
  }
51619
52315
  }
51620
52316
 
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) {
52317
+ var extent = getFrameExtent(bbox, widthPx, heightPx, opts);
52318
+ if (!extent.valid) {
51625
52319
  stop$1('Frame has a collapsed bbox');
51626
52320
  }
51627
52321
 
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
52322
  var feature = {
51636
52323
  type: 'Feature',
51637
- properties: getFrameProperties(widthPx, heightPx, opts),
51638
- geometry: bboxToPolygon(bbox)
52324
+ properties: getFrameProperties(extent.width, extent.height, opts),
52325
+ geometry: bboxToPolygon(extent.bbox)
51639
52326
  };
51640
52327
  var frameDataset = importGeoJSON(feature);
51641
52328
  // set CRS from target dataset
@@ -51655,6 +52342,34 @@ ${svg}
51655
52342
  catalog.addDataset(frameDataset);
51656
52343
  };
51657
52344
 
52345
+ // The frame's extent and nominal size, from the extent of its content and the
52346
+ // size options. Pure, so that fitting to symbols can ask what scale an extent
52347
+ // would give the frame.
52348
+ function getFrameExtent(contentBbox, widthPx, heightPx, opts) {
52349
+ var offsets = opts.offset || opts.offsets;
52350
+ var bbox = contentBbox.slice();
52351
+ var aspectRatio;
52352
+ applyPercentageOffsets(bbox, offsets);
52353
+ applyPixelOffsets(bbox, widthPx, heightPx, offsets);
52354
+ aspectRatio = (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
52355
+ if (!widthPx) {
52356
+ widthPx = roundToDigits(heightPx * aspectRatio, 1);
52357
+ } else if (!heightPx) {
52358
+ heightPx = roundToDigits(widthPx / aspectRatio, 1);
52359
+ }
52360
+ return {
52361
+ bbox: bbox,
52362
+ width: widthPx,
52363
+ height: heightPx,
52364
+ valid: bbox[3] - bbox[1] > 0 && bbox[2] - bbox[0] > 0
52365
+ };
52366
+ }
52367
+
52368
+ function getFixedAspect$1(extent, opts) {
52369
+ return opts.aspect_ratio > 0 || opts.width && opts.height ?
52370
+ extent.width / extent.height : null;
52371
+ }
52372
+
51658
52373
  function getFrameProperties(width, height, opts) {
51659
52374
  var properties = {
51660
52375
  type: 'frame',
@@ -51750,14 +52465,6 @@ ${svg}
51750
52465
  return arg;
51751
52466
  }
51752
52467
 
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
52468
  // Convert width and height args to aspect ratio arg for the rectangle() function
51762
52469
  function getAspectRatioArg(widthArg, heightArg) {
51763
52470
  // heightArg is a string containing either a number or a
@@ -51902,7 +52609,8 @@ ${svg}
51902
52609
 
51903
52610
  var OPERATION$1 = 'update-frame';
51904
52611
 
51905
- function updateFrame(targetLayers, dataset, opts) {
52612
+ // @fitTargets: [{layer, dataset}] named by the fit= option
52613
+ function updateFrame(targetLayers, dataset, opts, fitTargets) {
51906
52614
  if (!targetLayers || targetLayers.length != 1) {
51907
52615
  stop$1('-update-frame expects a single target layer');
51908
52616
  }
@@ -51926,15 +52634,14 @@ ${svg}
51926
52634
  if (opts.fix_scale && (opts.width !== undefined || opts.height !== undefined)) {
51927
52635
  stop$1('fix-scale cannot be combined with width= or height=');
51928
52636
  }
52637
+ if (opts.bbox !== undefined && opts.fit !== undefined) {
52638
+ stop$1('bbox= and fit= are mutually exclusive');
52639
+ }
51929
52640
 
51930
52641
  var rec = lyr.data.getRecords()[0];
51931
52642
  var frame = getFrameLayerData(lyr, dataset.arcs);
51932
- var bbox = (opts.bbox || frame.bbox).slice();
51933
- var offsetArg = opts.offset || opts.offsets;
51934
52643
  var fixedAspect = getFixedAspect(rec);
51935
- var width = frame.width;
51936
- var units = rec.frame_units || 'px';
51937
- var widthSize, heightSize, height, effectiveAspect;
52644
+ var sizes, contentBbox, update;
51938
52645
 
51939
52646
  // 1. Aspect mode, resolved first because whether the page shape is fixed
51940
52647
  // decides whether it may constrain the extent while offsets are applied.
@@ -51946,8 +52653,87 @@ ${svg}
51946
52653
  }
51947
52654
  fixedAspect = opts.aspect_ratio;
51948
52655
  }
52656
+ sizes = parseSizeOptions(opts);
52657
+
52658
+ // 2. Extent, which fit= finds by trying extents out on steps 3 and 4
52659
+ if (opts.fit !== undefined) {
52660
+ contentBbox = getFrameContentBbox(getFitTargets(fitTargets, dataset), function(bbox) {
52661
+ var o = resolveFrameUpdate(bbox, frame, fixedAspect, sizes, opts);
52662
+ return getFrameScale(o.bbox, o.width, o.fixedAspect);
52663
+ }, opts);
52664
+ if (!contentBbox) {
52665
+ stop$1('Layers to fit are missing geographical bounds');
52666
+ }
52667
+ } else {
52668
+ contentBbox = opts.bbox || frame.bbox;
52669
+ }
52670
+
52671
+ update = resolveFrameUpdate(contentBbox, frame, fixedAspect, sizes, opts);
52672
+ if (!update.valid) {
52673
+ stop$1('Frame has a collapsed bbox');
52674
+ }
52675
+ if (opts.fix_scale) {
52676
+ requirePositiveSize(update.width, 'fix-scale', update.width);
52677
+ }
52678
+
52679
+ noteLayerWillChange(lyr, {operation: OPERATION$1, unit: 'shapes'});
52680
+ lyr.data.captureTableBefore({operation: OPERATION$1});
52681
+ rec.width = update.width;
52682
+ rec.height = update.height;
52683
+ rec.frame_units = update.units;
52684
+ if (update.fixedAspect) {
52685
+ rec.frame_aspect_ratio = update.fixedAspect;
52686
+ } else {
52687
+ delete rec.frame_aspect_ratio;
52688
+ }
52689
+ lyr.data.markChanged({operation: OPERATION$1});
52690
+ rebuildFrameLayerGeometry(lyr, dataset, new Bounds(update.bbox));
52691
+ markLayerChanged(lyr, {operation: OPERATION$1, unit: 'shapes'});
52692
+ }
52693
+
52694
+ // The frame's layers are not content to fit, and a frame is only fitted to
52695
+ // layers it shares coordinates with.
52696
+ function getFitTargets(fitTargets, frameDataset) {
52697
+ var targets = (fitTargets || []).filter(function(o) {
52698
+ return !isFrameLayer(o.layer, o.dataset.arcs);
52699
+ });
52700
+ if (targets.length === 0) {
52701
+ stop$1('fit= found no layers to fit the frame to');
52702
+ }
52703
+ requireDatasetsHaveCompatibleCRS(
52704
+ utils.uniq(targets.map(function(o) { return o.dataset; }).concat(frameDataset)),
52705
+ 'Layers to fit and the frame have incompatible coordinates'
52706
+ );
52707
+ return targets;
52708
+ }
51949
52709
 
51950
- // 2. Extent
52710
+ function parseSizeOptions(opts) {
52711
+ var sizes = {width: null, height: null};
52712
+ if (opts.width !== undefined) {
52713
+ sizes.width = parseFrameSize(opts.width);
52714
+ requirePositiveSize(sizes.width.valuePx, 'width', opts.width);
52715
+ }
52716
+ if (opts.height !== undefined) {
52717
+ sizes.height = parseFrameSize(opts.height);
52718
+ requirePositiveSize(sizes.height.valuePx, 'height', opts.height);
52719
+ }
52720
+ if (sizes.width && sizes.height && opts.aspect_ratio !== undefined &&
52721
+ ratiosDiffer(sizes.width.valuePx / sizes.height.valuePx, opts.aspect_ratio)) {
52722
+ stop$1('Contradictory width, height and aspect-ratio values');
52723
+ }
52724
+ return sizes;
52725
+ }
52726
+
52727
+ // Steps 3 and 4 as a pure function of the content extent, so that fit= can
52728
+ // ask what scale an extent would give the frame.
52729
+ function resolveFrameUpdate(contentBbox, frame, fixedAspect, sizes, opts) {
52730
+ var bbox = contentBbox.slice();
52731
+ var offsetArg = opts.offset || opts.offsets;
52732
+ var width = frame.width;
52733
+ var units = frame.units || 'px';
52734
+ var valid, effectiveAspect;
52735
+
52736
+ // 3. Padding
51951
52737
  if (offsetArg) {
51952
52738
  applyPercentageOffsets(bbox, offsetArg);
51953
52739
  // Pass a page height only when the shape is fixed. A derived height
@@ -51956,65 +52742,44 @@ ${svg}
51956
52742
  applyPixelOffsets(bbox, frame.width,
51957
52743
  fixedAspect ? frame.width / fixedAspect : null, offsetArg);
51958
52744
  }
51959
- requireValidBbox(bbox);
52745
+ valid = isValidBbox(bbox);
51960
52746
  if (fixedAspect) {
51961
52747
  fillOutBbox(bbox, fixedAspect, 1);
51962
52748
  }
51963
52749
 
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;
52750
+ // 4. Nominal size
52751
+ if (sizes.width && sizes.height) {
52752
+ width = sizes.width.valuePx;
52753
+ units = sizes.width.units;
52754
+ fixedAspect = sizes.width.valuePx / sizes.height.valuePx;
51982
52755
  fillOutBbox(bbox, fixedAspect, 1);
51983
- } else if (widthSize) {
51984
- width = widthSize.valuePx;
51985
- units = widthSize.units;
51986
- } else if (heightSize) {
52756
+ } else if (sizes.width) {
52757
+ width = sizes.width.valuePx;
52758
+ units = sizes.width.units;
52759
+ } else if (sizes.height) {
51987
52760
  effectiveAspect = fixedAspect || getBboxAspect(bbox);
51988
- width = heightSize.valuePx * effectiveAspect;
51989
- units = heightSize.units;
52761
+ width = sizes.height.valuePx * effectiveAspect;
52762
+ units = sizes.height.units;
51990
52763
  } else if (opts.fix_scale) {
51991
52764
  // Hold ground units per output pixel, so the page grows and shrinks with
51992
52765
  // the extent instead of the scale changing to fit the extent on it.
51993
52766
  width = frame.width * getBboxWidth(bbox) / getBboxWidth(frame.bbox);
51994
- requirePositiveSize(width, 'fix-scale', width);
51995
52767
  }
51996
52768
 
51997
- requireValidBbox(bbox);
51998
52769
  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'});
52770
+ return {
52771
+ bbox: bbox,
52772
+ width: width,
52773
+ height: Math.round(width / effectiveAspect),
52774
+ units: units,
52775
+ fixedAspect: fixedAspect,
52776
+ valid: valid && isValidBbox(bbox)
52777
+ };
52014
52778
  }
52015
52779
 
52016
52780
  function hasUpdateOptions(opts) {
52017
52781
  return opts.bbox !== undefined ||
52782
+ opts.fit !== undefined ||
52018
52783
  opts.width !== undefined ||
52019
52784
  opts.height !== undefined ||
52020
52785
  opts.aspect_ratio !== undefined ||
@@ -52037,12 +52802,10 @@ ${svg}
52037
52802
  return (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
52038
52803
  }
52039
52804
 
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
- }
52805
+ function isValidBbox(bbox) {
52806
+ return bbox.every(utils.isFiniteNumber) &&
52807
+ bbox[2] - bbox[0] > 0 &&
52808
+ bbox[3] - bbox[1] > 0;
52046
52809
  }
52047
52810
 
52048
52811
  function requirePositiveSize(value, name, arg) {
@@ -52272,168 +53035,6 @@ ${svg}
52272
53035
  stop$1('Unable to import coordinates');
52273
53036
  }
52274
53037
 
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
53038
  const epsilon = 1.1102230246251565e-16;
52438
53039
  const splitter = 134217729;
52439
53040
  const resulterrbound = (3 + 8 * epsilon) * epsilon;
@@ -75976,7 +76577,7 @@ ${svg}
75976
76577
 
75977
76578
  function createMeridianPart(x, ymin, ymax) {
75978
76579
  var coords = densifyPathByInterval([[x, ymin], [x, ymax]], precision);
75979
- meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord$1(x)}));
76580
+ meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord(x)}));
75980
76581
  }
75981
76582
 
75982
76583
  function createParallel(y) {
@@ -76010,7 +76611,7 @@ ${svg}
76010
76611
  }
76011
76612
 
76012
76613
  // remove tiny offsets
76013
- function roundCoord$1(x) {
76614
+ function roundCoord(x) {
76014
76615
  return +x.toFixed(3) || 0;
76015
76616
  }
76016
76617
 
@@ -79050,160 +79651,6 @@ ${svg}
79050
79651
  };
79051
79652
  }
79052
79653
 
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
79654
  // Collision reduction for circular symbols, in the spirit of d3-force's
79208
79655
  // forceCollide. Overlapping symbols are pushed apart, but each one is kept
79209
79656
  // within a fixed distance of its true position.
@@ -83097,7 +83544,8 @@ ${svg}
83097
83544
  job.catalog.addDataset(cmd.addLayer(targetDataset, opts));
83098
83545
 
83099
83546
  } else if (name == 'update-frame') {
83100
- cmd.updateFrame(targetLayers, targetDataset, opts);
83547
+ cmd.updateFrame(targetLayers, targetDataset, opts,
83548
+ opts.fit ? expandCommandTargets(job.catalog.findCommandTargets(opts.fit)) : null);
83101
83549
 
83102
83550
  } else if (name == 'update-label') {
83103
83551
  cmd.updateLabel(targetLayers, targetDataset, opts);
@@ -83663,7 +84111,7 @@ ${svg}
83663
84111
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
83664
84112
  }
83665
84113
 
83666
- var version = "0.7.67";
84114
+ var version = "0.7.68";
83667
84115
 
83668
84116
  // Parse command line args into commands and run them
83669
84117
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.