mapshaper 0.7.66 → 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/www/mapshaper.js CHANGED
@@ -7549,19 +7549,19 @@
7549
7549
  pointN = [CK.lengthMG, CK.lengthMG * Math.tan(30 * D2R$5)];
7550
7550
  CK.pointA = [CK.lengthMA, 0];
7551
7551
  CK.pointB = lineIntersection(CK.pointM, 30, CK.pointA, 45);
7552
- CK.lengthAG = distance(CK.pointA, CK.pointG);
7553
- CK.lengthAB = distance(CK.pointA, CK.pointB);
7554
- lengthMB = distance(CK.pointM, CK.pointB);
7555
- lengthMN = distance(CK.pointM, pointN);
7556
- lengthNG = distance(pointN, CK.pointG);
7552
+ CK.lengthAG = distance$1(CK.pointA, CK.pointG);
7553
+ CK.lengthAB = distance$1(CK.pointA, CK.pointB);
7554
+ lengthMB = distance$1(CK.pointM, CK.pointB);
7555
+ lengthMN = distance$1(CK.pointM, pointN);
7556
+ lengthNG = distance$1(pointN, CK.pointG);
7557
7557
  CK.pointD = interpolate(lengthMB, lengthMN, pointN, CK.pointM);
7558
7558
  CK.pointF = [CK.lengthMG, lengthNG - lengthMB];
7559
7559
  CK.pointE = [
7560
7560
  pointN[0] - CK.lengthMA * Math.sin(30 * D2R$5),
7561
7561
  pointN[1] - CK.lengthMA * Math.cos(30 * D2R$5)
7562
7562
  ];
7563
- CK.lengthGF = distance(CK.pointG, CK.pointF);
7564
- CK.lengthBD = distance(CK.pointB, CK.pointD);
7563
+ CK.lengthGF = distance$1(CK.pointG, CK.pointF);
7564
+ CK.lengthBD = distance$1(CK.pointB, CK.pointD);
7565
7565
  CK.lengthGFE = CK.lengthGF + CK.lengthAB;
7566
7566
  CK.deltaMEq = CK.lengthGFE / 45;
7567
7567
  CK.lengthAP75 = 15 * CK.lengthParallel73to90At0;
@@ -7588,7 +7588,7 @@
7588
7588
  k * CK.pointD[0] - CK.pointD[1])
7589
7589
  );
7590
7590
  CK.pointC[0] = k * CK.pointC[1];
7591
- CK.radius = distance(CK.pointC, CK.pointD);
7591
+ CK.radius = distance$1(CK.pointC, CK.pointD);
7592
7592
  }
7593
7593
 
7594
7594
  function ll2mp(lon, lat) {
@@ -7619,12 +7619,12 @@
7619
7619
  );
7620
7620
  var l15;
7621
7621
  if (hit[0]) {
7622
- l15 = lT + distance(jointT(m), hit[1]);
7622
+ l15 = lT + distance$1(jointT(m), hit[1]);
7623
7623
  } else {
7624
7624
  hit = circleLineIntersection(
7625
7625
  CK.pointC, CK.radius, jointE(m), jointT(m)
7626
7626
  );
7627
- l15 = lT - distance(jointT(m), hit[1]);
7627
+ l15 = lT - distance$1(jointT(m), hit[1]);
7628
7628
  }
7629
7629
  return p <= 15 ? zoneK(m, p, l15) : zoneL(m, p, l15);
7630
7630
  }
@@ -7660,8 +7660,8 @@
7660
7660
  function zoneH(m, p) {
7661
7661
  var p75 = parallel75(45);
7662
7662
  var p73 = parallel73(m).parallel73;
7663
- var lF = distance(CK.pointT, CK.pointB);
7664
- var lF75 = distance(CK.pointB, p75);
7663
+ var lF = distance$1(CK.pointT, CK.pointB);
7664
+ var lF75 = distance$1(CK.pointB, p75);
7665
7665
  var l = (75 - p) * (lF75 + lF) / 2;
7666
7666
  return l <= lF75 ?
7667
7667
  interpolate(l, lF75, p75, CK.pointB) :
@@ -7688,7 +7688,7 @@
7688
7688
  function zoneJ(m, p) {
7689
7689
  var p75 = parallel75(m);
7690
7690
  var p73a = parallel73(m);
7691
- var lF75 = distance(jointF(m), p75);
7691
+ var lF75 = distance$1(jointF(m), p75);
7692
7692
  var l = (75 - p) * (lF75 - p73a.lengthParallel73) / 2;
7693
7693
  return l <= lF75 ?
7694
7694
  interpolate(l, lF75, p75, jointF(m)) :
@@ -7745,11 +7745,11 @@
7745
7745
  }
7746
7746
 
7747
7747
  function lengthTorridSegment(m) {
7748
- return distance(jointE(m), jointT(m));
7748
+ return distance$1(jointE(m), jointT(m));
7749
7749
  }
7750
7750
 
7751
7751
  function lengthMiddleSegment(m) {
7752
- return distance(jointT(m), jointF(m));
7752
+ return distance$1(jointT(m), jointF(m));
7753
7753
  }
7754
7754
 
7755
7755
  function parallel73(m) {
@@ -7757,15 +7757,15 @@
7757
7757
  var jF = jointF(m);
7758
7758
  if (m <= 30) {
7759
7759
  p73 = radialPoint(CK.pointA, CK.lengthAP73, m);
7760
- lF = distance(jF, p73);
7760
+ lF = distance$1(jF, p73);
7761
7761
  } else {
7762
7762
  p73 = lineIntersection(CK.pointT, -60, jF, m);
7763
- lF = distance(jF, p73);
7763
+ lF = distance$1(jF, p73);
7764
7764
  if (m > 44) {
7765
7765
  xy = lineIntersection(CK.pointT, -60, jF, 2 * m / 3);
7766
7766
  if (xy[0] > p73[0]) {
7767
7767
  p73 = xy;
7768
- lF = -distance(jF, p73);
7768
+ lF = -distance$1(jF, p73);
7769
7769
  }
7770
7770
  }
7771
7771
  }
@@ -7838,7 +7838,7 @@
7838
7838
  return createCahillKeyesTransform(mg, true);
7839
7839
  }
7840
7840
 
7841
- function distance(a, b) {
7841
+ function distance$1(a, b) {
7842
7842
  return Math.hypot(a[0] - b[0], a[1] - b[1]);
7843
7843
  }
7844
7844
 
@@ -26647,6 +26647,10 @@
26647
26647
  'font-style': null,
26648
26648
  'font-stretch': null,
26649
26649
  'font-weight': null,
26650
+ // a stroke around a label's text, painted beneath it -- see svg-label-halo.mjs
26651
+ 'halo-color': 'color',
26652
+ 'halo-opacity': 'number',
26653
+ 'halo-width': 'number',
26650
26654
  icon: null,
26651
26655
  'icon-color': 'color',
26652
26656
  // opacity of the symbol at a label's anchor, apart from the text's. Needed
@@ -26664,6 +26668,20 @@
26664
26668
  // where the text starts along its path; a length or a percentage
26665
26669
  'label-start-offset': null,
26666
26670
  'label-text': null, // leaving this null
26671
+ // width of a fixed-width text block, whose lines the GUI wraps and stores
26672
+ // with <wbr> soft breaks -- see docs/development/text-annotation-design.md
26673
+ 'label-width': 'number',
26674
+ // a line from a label's anchor to its text -- see svg-label-callout.mjs
26675
+ callout: 'callout',
26676
+ 'callout-end': 'calloutend',
26677
+ 'callout-end-size': 'number',
26678
+ 'callout-via': 'pointpair',
26679
+ 'callout-attach': 'pointpair',
26680
+ 'callout-gap': 'number',
26681
+ 'callout-padding': 'number',
26682
+ 'callout-color': 'color',
26683
+ 'callout-width': 'number',
26684
+ 'callout-opacity': 'number',
26667
26685
  'letter-spacing': 'measure',
26668
26686
  'line-height': 'measure',
26669
26687
  opacity: 'number',
@@ -26971,6 +26989,13 @@
26971
26989
  val = parseLabelPosition(strVal);
26972
26990
  } else if (type == 'labelalign') {
26973
26991
  val = parseLabelAlign(strVal);
26992
+ } else if (type == 'callout') {
26993
+ val = parseCalloutType(strVal);
26994
+ } else if (type == 'calloutend') {
26995
+ val = parseCalloutEnd(strVal);
26996
+ } else if (type == 'pointpair') {
26997
+ val = parsePointPair(strVal);
26998
+ val = val ? formatPointPair(val) : null;
26974
26999
  }
26975
27000
  // else {
26976
27001
  // // unknown type -- assume literal value
@@ -27007,6 +27032,29 @@
27007
27032
  return /^(n|s|e|w|ne|se|nw|sw|c)$/i.test(pos) ? pos : null;
27008
27033
  }
27009
27034
 
27035
+ // 'none' is kept rather than rejected, so that a data field or an expression
27036
+ // can switch a callout off for some features; it renders as no callout.
27037
+ function parseCalloutType(str) {
27038
+ var type = String(str).trim().toLowerCase();
27039
+ return /^(line|elbow|curve|none)$/.test(type) ? type : null;
27040
+ }
27041
+
27042
+ function parseCalloutEnd(str) {
27043
+ var end = String(str).trim().toLowerCase();
27044
+ return /^(arrow|open-arrow|none)$/.test(end) ? end : null;
27045
+ }
27046
+
27047
+ // "x,y" -> [x, y], or null. Stored as a string so that a pair is set and
27048
+ // cleared as one value and reads plainly in a table.
27049
+ function parsePointPair(str) {
27050
+ var match = /^\s*(-?[0-9]*\.?[0-9]+)\s*,\s*(-?[0-9]*\.?[0-9]+)\s*$/.exec(String(str));
27051
+ return match ? [Number(match[1]), Number(match[2])] : null;
27052
+ }
27053
+
27054
+ function formatPointPair(p) {
27055
+ return p[0] + ',' + p[1];
27056
+ }
27057
+
27010
27058
  function getLabelPositionStyle(pos) {
27011
27059
  pos = parseLabelPosition(pos);
27012
27060
  if (!pos) return null;
@@ -27095,6 +27143,7 @@
27095
27143
  applyStyleAttributes: applyStyleAttributes,
27096
27144
  emptyValueUnsetsProperty: emptyValueUnsetsProperty,
27097
27145
  findStylePropertiesBySymbolGeom: findStylePropertiesBySymbolGeom,
27146
+ formatPointPair: formatPointPair,
27098
27147
  getLabelPositionAnchor: getLabelPositionAnchor,
27099
27148
  getLabelPositionStyle: getLabelPositionStyle,
27100
27149
  getPropertyAccessor: getPropertyAccessor,
@@ -27111,17 +27160,190 @@
27111
27160
  labelPositionFields: labelPositionFields,
27112
27161
  mightBeExpression: mightBeExpression,
27113
27162
  parseBoolean: parseBoolean,
27163
+ parseCalloutEnd: parseCalloutEnd,
27164
+ parseCalloutType: parseCalloutType,
27114
27165
  parseLabelPosition: parseLabelPosition,
27166
+ parsePointPair: parsePointPair,
27115
27167
  parseStyleLiteral: parseStyleLiteral,
27116
27168
  parseSvgMeasure: parseSvgMeasure,
27117
27169
  resolveLabelPosition: resolveLabelPosition
27118
27170
  });
27119
27171
 
27172
+ // A halo is a stroke around a label's glyphs, painted underneath their fill so
27173
+ // that it widens the letters' outline without eating into them. Three
27174
+ // properties describe one, named after the icon-* properties they sit beside
27175
+ // in the panel:
27176
+ //
27177
+ // halo-width how far the halo reaches past the edge of the glyphs, in px.
27178
+ // A halo is drawn when this is above 0 and not otherwise.
27179
+ // halo-color defaults to white, the halo nearly every map wants
27180
+ // halo-opacity 0-1, apart from the text's own opacity
27181
+ //
27182
+ // Not stroke, stroke-width and stroke-opacity, which labels already accept and
27183
+ // which would then mean two things at once: a label's record styles its icon
27184
+ // too (see getIconStyleData()), so a halo stored as a stroke would ring the
27185
+ // symbol as well as the text.
27186
+ //
27187
+ // The stroke is twice halo-width because a stroke is centred on the outline:
27188
+ // the inner half is hidden under the fill, and only the outer half shows.
27189
+ //
27190
+ // In the GUI the halo is the text element's own stroke, with paint-order
27191
+ // putting it underneath. Export draws it as a second copy of the text instead
27192
+ // -- see splitLabelHalos() -- because Illustrator and Figma ignore paint-order
27193
+ // when they import SVG and would draw the stroke over the letters.
27194
+
27195
+ var DEFAULT_HALO_COLOR = '#ffffff';
27196
+ var HALO_PAINT_ORDER = 'stroke fill';
27197
+
27198
+ // The properties of the text element that belong to the halo once it has one.
27199
+ // The label's own stroke properties are among them: a halo is the text's
27200
+ // stroke, and a dash pattern or a second colour meant for the letters has
27201
+ // nowhere left to go.
27202
+ var HALO_STROKE_PROPERTIES = ['stroke', 'stroke-width', 'stroke-opacity',
27203
+ 'stroke-dasharray', 'stroke-linecap', 'stroke-linejoin', 'paint-order'];
27204
+
27205
+ function labelHasHalo(rec) {
27206
+ return !!rec && rec['halo-width'] > 0;
27207
+ }
27208
+
27209
+ // Paints @rec's halo on @o, a rendered <text> element, and returns it.
27210
+ // Leaves @o alone when the label has no halo, so that labels without one
27211
+ // render exactly as they did before halos existed.
27212
+ function applyLabelHalo(o, rec) {
27213
+ var props, opacity;
27214
+ if (!labelHasHalo(rec)) return o;
27215
+ props = o.properties || (o.properties = {});
27216
+ HALO_STROKE_PROPERTIES.forEach(function(k) {
27217
+ delete props[k];
27218
+ });
27219
+ props.stroke = rec['halo-color'] || DEFAULT_HALO_COLOR;
27220
+ props['stroke-width'] = rec['halo-width'] * 2;
27221
+ opacity = rec['halo-opacity'];
27222
+ if (isSvgNumber(opacity) && Number(opacity) < 1) {
27223
+ props['stroke-opacity'] = Number(opacity);
27224
+ }
27225
+ // Round, because the default miter join puts spikes on the sharp corners of
27226
+ // letters like M and V, which read as blemishes at any halo width.
27227
+ props['stroke-linejoin'] = 'round';
27228
+ props['paint-order'] = HALO_PAINT_ORDER;
27229
+ return o;
27230
+ }
27231
+
27232
+ // Rewrites every halo'd <text> in @o as a group of two: a stroked copy of the
27233
+ // text underneath, and the text itself on top with no stroke. Returns @o, or
27234
+ // the group that replaces it if @o is itself a halo'd label.
27235
+ //
27236
+ // The two copies are identical apart from their paint, so they sit in exactly
27237
+ // the same place whatever the label's position, alignment or path.
27238
+ //
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.
27249
+ //
27250
+ // Things that apply to the label as a whole move to the group: its transform,
27251
+ // which places it, and its opacity, which would otherwise fade each copy on its
27252
+ // own and let the halo show through the letters. That is also how opacity
27253
+ // reads on a single element with paint-order, which is faded as one image.
27254
+ function splitLabelHalos(o) {
27255
+ if (!o) return o;
27256
+ if (o.tag == 'text' && isHaloText(o)) return splitHalo(o);
27257
+ if (o.children) {
27258
+ for (var i = 0; i < o.children.length; i++) {
27259
+ o.children[i] = splitLabelHalos(o.children[i]);
27260
+ }
27261
+ }
27262
+ return o;
27263
+ }
27264
+
27265
+ function isHaloText(o) {
27266
+ return !!o.properties && o.properties['paint-order'] == HALO_PAINT_ORDER;
27267
+ }
27268
+
27269
+ function splitHalo(text) {
27270
+ var halo = cloneSvgObject(text);
27271
+ var group = {tag: 'g', properties: {}, children: [halo, text]};
27272
+ moveProperty(text.properties, group.properties, 'transform');
27273
+ moveProperty(text.properties, group.properties, 'opacity');
27274
+ delete halo.properties.transform;
27275
+ delete halo.properties.opacity;
27276
+ delete halo.properties['paint-order'];
27277
+ delete halo.properties['fill-opacity'];
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
+ }
27283
+ HALO_STROKE_PROPERTIES.forEach(function(k) {
27284
+ delete text.properties[k];
27285
+ });
27286
+ return group;
27287
+ }
27288
+
27289
+ function moveProperty(src, dest, k) {
27290
+ if (k in src) {
27291
+ dest[k] = src[k];
27292
+ delete src[k];
27293
+ }
27294
+ }
27295
+
27296
+ function cloneSvgObject(o) {
27297
+ var copy = {tag: o.tag};
27298
+ if ('value' in o) copy.value = o.value;
27299
+ if (o.properties) copy.properties = Object.assign({}, o.properties);
27300
+ if (o.children) copy.children = o.children.map(cloneSvgObject);
27301
+ return copy;
27302
+ }
27303
+
27304
+ var SvgLabelHalo = /*#__PURE__*/Object.freeze({
27305
+ __proto__: null,
27306
+ DEFAULT_HALO_COLOR: DEFAULT_HALO_COLOR,
27307
+ applyLabelHalo: applyLabelHalo,
27308
+ labelHasHalo: labelHasHalo,
27309
+ splitLabelHalos: splitLabelHalos
27310
+ });
27311
+
27120
27312
  // Accepting \n (two chars) as an alternative to the newline character
27121
27313
  // (sometimes, '\n' is not converted to newline, e.g. in a Makefile)
27122
27314
  // Also accepting <br>
27123
27315
  var labelNewlineRxp = /\n|\\n|<br>/i;
27124
27316
 
27317
+ // A line break that the GUI's wrapper put in a fixed-width text block, as
27318
+ // against one the user typed. It consumes no character: the space or hyphen
27319
+ // the line broke at stays in the text before it, so removing every soft break
27320
+ // gives back exactly what was typed. See docs/development/text-annotation-design.md.
27321
+ var LABEL_SOFT_BREAK = '<wbr>';
27322
+
27323
+ // Every form of line break, captured, so that a split keeps the delimiters and
27324
+ // a soft break can be told from a hard one.
27325
+ var anyLabelBreakRxp = /(\n|\\n|<br>|<wbr>)/i;
27326
+ var softBreakRxp = /^<wbr>$/i;
27327
+
27328
+ // The lines a label's text is drawn as. Whitespace before a soft break is
27329
+ // dropped: a trailing space would move an end- or middle-justified line by a
27330
+ // space's width.
27331
+ function splitLabelLines(text) {
27332
+ var parts = String(text).split(anyLabelBreakRxp);
27333
+ var lines = [parts[0]];
27334
+ for (var i = 1; i < parts.length; i += 2) {
27335
+ if (softBreakRxp.test(parts[i])) {
27336
+ lines[lines.length - 1] = lines[lines.length - 1].replace(/\s+$/, '');
27337
+ }
27338
+ lines.push(parts[i + 1]);
27339
+ }
27340
+ return lines;
27341
+ }
27342
+
27343
+ function removeSoftBreaks(text) {
27344
+ return String(text).replace(/<wbr>/gi, '');
27345
+ }
27346
+
27125
27347
  // The size a label is drawn at when it carries none of its own, which is the
27126
27348
  // size its layer's group supplies (see getLabelTextDefaults()). Shared with
27127
27349
  // that function rather than written twice: the em offsets the label positions
@@ -27129,6 +27351,23 @@
27129
27351
  // different one would put the label somewhere it is not drawn.
27130
27352
  var DEFAULT_LABEL_FONT_SIZE = 12;
27131
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
+
27132
27371
  function toLabelString(val) {
27133
27372
  if (val || val === 0 || val === false) return String(val);
27134
27373
  return '';
@@ -27143,7 +27382,7 @@
27143
27382
  var rec = resolveLabelPosition(recArg);
27144
27383
  var o = renderLabel$1(rec);
27145
27384
  applyStyleAttributes(o, 'label', rec);
27146
- return o;
27385
+ return applyLabelHalo(o, rec);
27147
27386
  }
27148
27387
 
27149
27388
  function renderLabel$1(recArg) {
@@ -27151,9 +27390,9 @@
27151
27390
  // already been through it, so calling it here as well costs nothing and means
27152
27391
  // every way into the renderer draws a label in the position it is stored in.
27153
27392
  var rec = resolveLabelPosition(recArg);
27154
- var line = toLabelString(rec['label-text']);
27155
- var morelines, obj;
27156
- var newline = labelNewlineRxp;
27393
+ var morelines = splitLabelLines(toLabelString(rec['label-text']));
27394
+ var line = morelines.shift();
27395
+ var obj;
27157
27396
  var dx = applyAlignmentShift(rec);
27158
27397
  var dy = rec.dy || 0;
27159
27398
  var properties = {
@@ -27162,16 +27401,12 @@
27162
27401
  y: dy,
27163
27402
  x: dx
27164
27403
  };
27165
- if (newline.test(line)) {
27166
- morelines = line.split(newline);
27167
- line = morelines.shift();
27168
- }
27169
27404
  obj = {
27170
27405
  tag: 'text',
27171
27406
  value: line,
27172
27407
  properties: properties
27173
27408
  };
27174
- if (morelines) {
27409
+ if (morelines.length > 0) {
27175
27410
  // multiline label
27176
27411
  obj.children = [];
27177
27412
  morelines.forEach(function(line) {
@@ -27180,7 +27415,7 @@
27180
27415
  value: line,
27181
27416
  properties: {
27182
27417
  x: dx,
27183
- dy: rec['line-height'] || '1.1em'
27418
+ dy: getLineHeightDy(rec['line-height'])
27184
27419
  }
27185
27420
  };
27186
27421
  obj.children.push(tspan);
@@ -27274,11 +27509,621 @@
27274
27509
  var SvgLabels = /*#__PURE__*/Object.freeze({
27275
27510
  __proto__: null,
27276
27511
  DEFAULT_LABEL_FONT_SIZE: DEFAULT_LABEL_FONT_SIZE,
27512
+ DEFAULT_LINE_HEIGHT: DEFAULT_LINE_HEIGHT,
27513
+ LABEL_SOFT_BREAK: LABEL_SOFT_BREAK,
27277
27514
  getDrawnLabelOffset: getDrawnLabelOffset,
27515
+ getLineHeightDy: getLineHeightDy,
27278
27516
  labelNewlineRxp: labelNewlineRxp,
27517
+ removeSoftBreaks: removeSoftBreaks,
27279
27518
  renderLabel: renderLabel$1,
27280
27519
  renderStyledLabel: renderStyledLabel,
27281
- toLabelString: toLabelString
27520
+ splitLabelLines: splitLabelLines,
27521
+ toLabelString: toLabelString,
27522
+ toPixels: toPixels
27523
+ });
27524
+
27525
+ // A callout: a line from a label's anchor to its text, straight, elbowed or
27526
+ // curved, optionally ending in an arrowhead at the anchor. A dot at the anchor
27527
+ // is the label's icon, not a callout marker.
27528
+ //
27529
+ // Everything here is in the label's own space -- origin at the anchor, y down,
27530
+ // in px -- which is the space dx/dy are in and the one the GUI scales with the
27531
+ // frame. Nothing derived is stored: the shape is worked out from the anchor,
27532
+ // the text box and the callout-* fields every time the label is drawn.
27533
+ //
27534
+ // See docs/development/text-annotation-design.md.
27535
+
27536
+ var DEFAULT_PADDING$1 = 3;
27537
+ var DEFAULT_LINE_WIDTH = 1;
27538
+ // Space left between the anchor's symbol and the end of the line
27539
+ var SYMBOL_CLEARANCE = 2;
27540
+ // How far an automatic curve bows out, as a fraction of its chord
27541
+ var CURVE_BEND = 0.2;
27542
+ // The shortest first leg an automatic elbow into the top or bottom of the text
27543
+ // is drawn with, px
27544
+ var MIN_LEG = 8;
27545
+ // The angle at an arrowhead's point, degrees. The open one is wider, since a
27546
+ // narrow chevron drawn with a line reads as a thickened line more than as an
27547
+ // arrow.
27548
+ var ARROW_ANGLE = 44;
27549
+ var OPEN_ARROW_ANGLE = 70;
27550
+ // How far into a solid arrowhead the line reaches, as a fraction of the
27551
+ // head's length: far enough that its cap is hidden, short of the tip.
27552
+ var ARROW_LINE_OVERLAP = 0.7;
27553
+ // Where glyphs sit relative to their baseline, in ems, for estimating the
27554
+ // height of a block of text that is measured only for its width
27555
+ var ASCENT = 0.8;
27556
+ var DESCENT = 0.2;
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;
27561
+
27562
+ function labelHasCallout(rec) {
27563
+ var type = rec && rec.callout ? parseCalloutType(rec.callout) : null;
27564
+ return !!type && type != 'none';
27565
+ }
27566
+
27567
+ // The callout for @rec as an SVG object, or null if it has none or there is no
27568
+ // room to draw one.
27569
+ // @symbolRadius: radius of the symbol drawn at the anchor, or 0; the default
27570
+ // gap keeps the line clear of it
27571
+ function renderLabelCallout(rec, symbolRadius) {
27572
+ var shape = getLabelCalloutShape(rec, symbolRadius);
27573
+ return shape ? renderCalloutShape(shape, rec) : null;
27574
+ }
27575
+
27576
+ // The shape renderLabelCallout() draws for @rec, from getCalloutShape(), or
27577
+ // null. The GUI places its handles on this, so they sit where the line is.
27578
+ function getLabelCalloutShape(rec, symbolRadius) {
27579
+ if (!labelHasCallout(rec)) return null;
27580
+ return getCalloutShape({
27581
+ type: parseCalloutType(rec.callout),
27582
+ end: getCalloutEndType(rec),
27583
+ box: getLabelTextBox(rec),
27584
+ via: parsePointPair(rec['callout-via'] || ''),
27585
+ attach: parsePointPair(rec['callout-attach'] || ''),
27586
+ padding: getNumber(rec['callout-padding'], DEFAULT_PADDING$1),
27587
+ gap: getCalloutGap(rec, symbolRadius),
27588
+ width: getLineWidth(rec),
27589
+ endSize: getNumber(rec['callout-end-size'], 0)
27590
+ });
27591
+ }
27592
+
27593
+ // An arrowhead's size when callout-end-size does not say, in px: 10 for the
27594
+ // default line, and growing with the line, so that a heavier line does not end
27595
+ // in a head too small to read.
27596
+ function getDefaultCalloutEndSize(end, lineWidth) {
27597
+ var w = lineWidth > 0 ? lineWidth : DEFAULT_LINE_WIDTH;
27598
+ return 7 + 3 * w;
27599
+ }
27600
+
27601
+ // 'arrow', 'open-arrow' or 'none'
27602
+ function getCalloutEndType(rec) {
27603
+ return rec && parseCalloutEnd(rec['callout-end'] || '') || 'none';
27604
+ }
27605
+
27606
+ // How far short of the anchor the line stops. Unset, an arrowhead stops clear
27607
+ // of the symbol there, so that its point reads against the background; a
27608
+ // plain line runs to the symbol's edge, so that the two read as one object.
27609
+ // The line's round cap then reaches under the symbol, which is drawn over it.
27610
+ function getCalloutGap(rec, symbolRadius) {
27611
+ if (isSvgNumber(rec['callout-gap'])) return Math.max(0, Number(rec['callout-gap']));
27612
+ if (!(symbolRadius > 0)) return 0;
27613
+ return getCalloutEndType(rec) == 'none' ? symbolRadius : symbolRadius + SYMBOL_CLEARANCE;
27614
+ }
27615
+
27616
+ // The block of text a label draws, in the label's space: {xmin, ymin, xmax,
27617
+ // ymax, midline}, where midline is the middle of the first line's capitals.
27618
+ //
27619
+ // The width is measured, since it depends on the font, and the height is
27620
+ // estimated from the font size and line height, since only the width is. With
27621
+ // no measurement the box takes the label's width if it is a text block, and is
27622
+ // otherwise a vertical line through the text's origin -- a callout then meets
27623
+ // the text where it starts rather than not being drawn.
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) {
27628
+ var fontSize = getFontSizeInPx$1(rec);
27629
+ var offset = getDrawnLabelOffset(rec);
27630
+ var lines = splitLabelLines(toLabelString(rec['label-text']));
27631
+ var lineHeight = measureToPx(getLineHeightDy(rec['line-height']), fontSize);
27632
+ var width = getMeasuredTextWidth(rec) || getNumber(rec['label-width'], 0);
27633
+ if (!width && opts && opts.estimate_width) {
27634
+ width = estimateTextWidth(lines, fontSize);
27635
+ }
27636
+ // An anchor the record does not set is inherited from the layer's group,
27637
+ // which is 'middle' -- see getLabelTextDefaults().
27638
+ var anchor = offset['text-anchor'] || 'middle';
27639
+ var xmin = offset.dx - width * (anchor == 'middle' ? 0.5 : anchor == 'end' ? 1 : 0);
27640
+ var baseline = offset.dy + getBaselineShift(rec['dominant-baseline']) * fontSize;
27641
+ if (!(lineHeight > 0)) lineHeight = fontSize * DEFAULT_LINE_HEIGHT;
27642
+ return {
27643
+ xmin: xmin,
27644
+ xmax: xmin + width,
27645
+ ymin: baseline - ASCENT * fontSize,
27646
+ ymax: baseline + (lines.length - 1) * lineHeight + DESCENT * fontSize,
27647
+ midline: baseline - MIDLINE * fontSize
27648
+ };
27649
+ }
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
+
27663
+ // Where a line meets a callout's text and how it gets there from the anchor.
27664
+ //
27665
+ // o.type 'line', 'elbow' or 'curve'
27666
+ // o.end 'arrow' (filled), 'open-arrow' (stroked) or 'none'
27667
+ // o.box the text box, from getLabelTextBox()
27668
+ // o.via [x, y] the elbow's corner or a point the curve passes through, or null
27669
+ // o.attach [fx, fy] where the line meets the text, as fractions of the padded
27670
+ // box, or null
27671
+ // o.padding clearance around the text, px
27672
+ // o.gap how far short of the anchor the line stops, px
27673
+ // o.width line width, px
27674
+ // o.endSize an arrowhead's size, px, or 0 for the default, which grows with
27675
+ // the line width. The size is the length of the head's sides, as
27676
+ // drawn, stroke and all: the two styles are different shapes, and
27677
+ // what makes one look the size of the other is sides the same
27678
+ // length, not the same length along the line.
27679
+ //
27680
+ // Returns {kind, coords, head, openHead, box, attach, via, tip} with coords
27681
+ // from the anchor end to the text end -- a polyline's vertices, or a quadratic
27682
+ // Bezier's three points -- or null when there is nothing to draw: the anchor
27683
+ // inside the padded box, or a gap that leaves no line. head is a filled
27684
+ // triangle, openHead the three points of a stroked chevron, wing to tip to
27685
+ // wing. The rest is what the shape was worked out from: the padded box, the
27686
+ // point where the line meets it, the elbow's corner or the curve's midpoint
27687
+ // (null for a straight line), and where the line stops short of the anchor.
27688
+ function getCalloutShape(o) {
27689
+ var box = padBox$1(o.box, o.padding || 0);
27690
+ var a = [0, 0];
27691
+ var size = o.endSize > 0 ? o.endSize : getDefaultCalloutEndSize(o.end, o.width);
27692
+ var t, v, path, tip, dir, out, lineWidth, headLen, curved;
27693
+ if (boxContains(box, a)) return null;
27694
+ t = o.attach ? getBoxPoint(box, o.attach) :
27695
+ getAutoAttachment(o.type, box, o.via || null);
27696
+ // A leg too short to read as one, or to hold the marker, is a jog: the line
27697
+ // goes straight up or down to the text instead.
27698
+ if (o.type == 'elbow' && !o.attach && !o.via && isOnTopOrBottom(box, t) &&
27699
+ Math.abs(t[0]) < getMinimumLeg(o.end, size)) {
27700
+ t = [0, t[1]];
27701
+ }
27702
+ if (o.type == 'elbow') {
27703
+ v = o.via || getAutoElbow(a, t, box);
27704
+ // A corner on either end is no corner -- straight under the text, say --
27705
+ // and a leg of no length has no direction for an arrowhead to take.
27706
+ path = {kind: 'polyline', coords: distance(a, v) > 0 && distance(v, t) > 0 ?
27707
+ [a, v, t] : [a, t]};
27708
+ } else if (o.type == 'curve') {
27709
+ v = o.via || getAutoBend(a, t);
27710
+ path = {kind: 'bezier', coords: [a, getControlPoint(a, v, t), t]};
27711
+ } else {
27712
+ path = {kind: 'polyline', coords: [a, t]};
27713
+ }
27714
+ path = trimPath(path, o.gap || 0);
27715
+ if (!path) return null;
27716
+ tip = path.coords[0];
27717
+ out = {kind: path.kind, coords: path.coords, head: null, openHead: null,
27718
+ box: box, attach: t, via: v || null, tip: tip};
27719
+ if (o.end == 'arrow') {
27720
+ headLen = getArrowHeadLength(size, ARROW_ANGLE);
27721
+ curved = path.kind == 'bezier' ? getCurveIntoArrowHead(path, headLen) : null;
27722
+ dir = curved ? curved.dir : getStartDirection(path);
27723
+ if (!dir) return null;
27724
+ out.head = getArrowHead(tip, dir, size, ARROW_ANGLE);
27725
+ // The line stops inside the head, so that its cap does not show past the
27726
+ // tip. A line too short to reach past the head is drawn as the head alone.
27727
+ path = curved ? curved.path : trimPath(path, headLen * ARROW_LINE_OVERLAP);
27728
+ out.coords = path ? path.coords : null;
27729
+ } else if (o.end == 'open-arrow') {
27730
+ // Stroked with the line's round join, which reaches half a line width past
27731
+ // the chevron's point: the point is set back by that much, so that the
27732
+ // arrow ends where a filled one would. The join and the round cap at the
27733
+ // far end each add half a line width to an arm, which is taken off it.
27734
+ lineWidth = o.width > 0 ? o.width : DEFAULT_LINE_WIDTH;
27735
+ path = trimPath(path, lineWidth / 2) || path;
27736
+ dir = getStartDirection(path);
27737
+ if (!dir) return null;
27738
+ out.coords = path.coords;
27739
+ out.openHead = getArrowHead(path.coords[0], dir,
27740
+ Math.max(size - lineWidth, size / 2), OPEN_ARROW_ANGLE);
27741
+ out.openHead = [out.openHead[1], out.openHead[0], out.openHead[2]];
27742
+ }
27743
+ return out;
27744
+ }
27745
+
27746
+ function renderCalloutShape(shape, rec) {
27747
+ var color = rec['callout-color'] || rec.fill || 'black';
27748
+ var opacity = isSvgNumber(rec['callout-opacity']) ? Number(rec['callout-opacity']) :
27749
+ isSvgNumber(rec.opacity) ? Number(rec.opacity) : 1;
27750
+ var props = {class: 'label-callout'};
27751
+ var children = [];
27752
+ if (shape.coords) {
27753
+ children.push({
27754
+ tag: 'path',
27755
+ properties: {
27756
+ d: getPathData(shape),
27757
+ fill: 'none',
27758
+ stroke: color,
27759
+ 'stroke-width': getLineWidth(rec),
27760
+ 'stroke-linecap': 'round',
27761
+ 'stroke-linejoin': 'round'
27762
+ }
27763
+ });
27764
+ }
27765
+ if (shape.head) {
27766
+ children.push({
27767
+ tag: 'path',
27768
+ properties: {d: getPolygonData(shape.head), fill: color}
27769
+ });
27770
+ }
27771
+ if (shape.openHead) {
27772
+ children.push({
27773
+ tag: 'path',
27774
+ properties: {
27775
+ d: 'M ' + shape.openHead.map(formatPoint).join(' L '),
27776
+ fill: 'none',
27777
+ stroke: color,
27778
+ 'stroke-width': getLineWidth(rec),
27779
+ 'stroke-linecap': 'round',
27780
+ 'stroke-linejoin': 'round'
27781
+ }
27782
+ });
27783
+ }
27784
+ if (opacity < 1) props.opacity = opacity;
27785
+ return {tag: 'g', properties: props, children: children};
27786
+ }
27787
+
27788
+ // Drawn from the text to the anchor, which is the direction a leader is read in
27789
+ // and the one a dash pattern starts from. A quadratic's control point is the
27790
+ // same in both directions.
27791
+ function getPathData(shape) {
27792
+ var c = shape.coords;
27793
+ if (shape.kind == 'bezier') {
27794
+ return 'M ' + formatPoint(c[2]) + ' Q ' + formatPoint(c[1]) + ' ' + formatPoint(c[0]);
27795
+ }
27796
+ return 'M ' + c.slice().reverse().map(formatPoint).join(' L ');
27797
+ }
27798
+
27799
+ function getPolygonData(coords) {
27800
+ return 'M ' + coords.map(formatPoint).join(' L ') + ' Z';
27801
+ }
27802
+
27803
+ function formatPoint(p) {
27804
+ return roundToTenths(p[0]) + ' ' + roundToTenths(p[1]);
27805
+ }
27806
+
27807
+ // Attachment
27808
+
27809
+ function getBoxPoint(box, f) {
27810
+ var fx = Math.max(0, Math.min(1, f[0]));
27811
+ var fy = Math.max(0, Math.min(1, f[1]));
27812
+ return [box.xmin + fx * (box.xmax - box.xmin), box.ymin + fy * (box.ymax - box.ymin)];
27813
+ }
27814
+
27815
+ // An elbow meets the side of the text facing the way it comes from, level with
27816
+ // the first line, which is the usual form of an elbowed leader. A straight or
27817
+ // curved line is aimed at the middle of the block and stops at its edge.
27818
+ //
27819
+ // An elbow coming from directly above or below the text cannot reach a side
27820
+ // without crossing the block, so it meets the facing edge instead: in the
27821
+ // middle when it comes from the anchor, and straight above or below a via
27822
+ // point, whose landing is then vertical.
27823
+ function getAutoAttachment(type, box, via) {
27824
+ var cx = (box.xmin + box.xmax) / 2;
27825
+ var cy = (box.ymin + box.ymax) / 2;
27826
+ var from = via || [0, 0];
27827
+ if (type == 'elbow' && isAboveOrBelow(box, from)) {
27828
+ return [via ? from[0] : cx, from[1] > box.ymax ? box.ymax : box.ymin];
27829
+ }
27830
+ if (type == 'elbow') {
27831
+ return [from[0] < cx ? box.xmin : box.xmax,
27832
+ Math.max(box.ymin, Math.min(box.ymax, box.midline))];
27833
+ }
27834
+ return clipToBox(from, [cx, cy], box);
27835
+ }
27836
+
27837
+ function getMinimumLeg(end, size) {
27838
+ return end == 'none' ? MIN_LEG : Math.max(MIN_LEG, size * 1.5);
27839
+ }
27840
+
27841
+ function isAboveOrBelow(box, p) {
27842
+ return p[0] >= box.xmin && p[0] <= box.xmax && (p[1] > box.ymax || p[1] < box.ymin);
27843
+ }
27844
+
27845
+ // Whether @t is on the top or bottom edge of @box, away from its corners
27846
+ function isOnTopOrBottom(box, t) {
27847
+ return t[0] > box.xmin && t[0] < box.xmax && (t[1] == box.ymin || t[1] == box.ymax);
27848
+ }
27849
+
27850
+ // Where the segment from @p (outside the box) to @c (inside it) enters the box.
27851
+ function clipToBox(p, c, box) {
27852
+ var dx = c[0] - p[0];
27853
+ var dy = c[1] - p[1];
27854
+ var s;
27855
+ if (boxContains(box, p)) return c;
27856
+ s = Math.max(0, getEntry(p[0], dx, box.xmin, box.xmax),
27857
+ getEntry(p[1], dy, box.ymin, box.ymax));
27858
+ return [p[0] + s * dx, p[1] + s * dy];
27859
+ }
27860
+
27861
+ function getEntry(p, d, min, max) {
27862
+ if (d === 0) return -Infinity;
27863
+ return Math.min((min - p) / d, (max - p) / d);
27864
+ }
27865
+
27866
+ // Shapes
27867
+
27868
+ // The segment from the anchor rises at 45 degrees to meet a horizontal landing
27869
+ // into the side of the text, or goes straight up or down when the text is too
27870
+ // close horizontally for that.
27871
+ //
27872
+ // Into the top or bottom of the text, the landing is vertical and the line
27873
+ // leaves the anchor horizontally, which keeps the corner clear of the block.
27874
+ function getAutoElbow(a, t, box) {
27875
+ var dx = t[0] - a[0];
27876
+ var dy = Math.abs(t[1] - a[1]);
27877
+ if (isOnTopOrBottom(box, t)) return [t[0], a[1]];
27878
+ if (Math.abs(dx) <= dy) return [a[0], t[1]];
27879
+ return [a[0] + (dx > 0 ? dy : -dy), t[1]];
27880
+ }
27881
+
27882
+ // The chord's midpoint, pushed to its left as seen from the anchor -- which
27883
+ // bows a callout to text on its right upward.
27884
+ function getAutoBend(a, t) {
27885
+ var dx = t[0] - a[0];
27886
+ var dy = t[1] - a[1];
27887
+ return [(a[0] + t[0]) / 2 + dy * CURVE_BEND, (a[1] + t[1]) / 2 - dx * CURVE_BEND];
27888
+ }
27889
+
27890
+ // The control point of the quadratic from @a to @t that passes through @v at
27891
+ // its midpoint. That is also the curve's farthest point from its chord, so a
27892
+ // handle at @v sits where the bend visibly is.
27893
+ function getControlPoint(a, v, t) {
27894
+ return [2 * v[0] - (a[0] + t[0]) / 2, 2 * v[1] - (a[1] + t[1]) / 2];
27895
+ }
27896
+
27897
+ // Trimming
27898
+
27899
+ // @path with its start moved to where it leaves a circle of radius @r around
27900
+ // that start, or null if it never does. A circle rather than a length along the
27901
+ // path, because what the gap has to clear is a symbol drawn at the anchor.
27902
+ function trimPath(path, r) {
27903
+ if (!(r > 0)) return hasLength(path) ? path : null;
27904
+ if (path.kind == 'bezier') return trimBezier(path.coords, r);
27905
+ return trimPolyline(path.coords, r);
27906
+ }
27907
+
27908
+ function hasLength(path) {
27909
+ var c = path.coords;
27910
+ for (var i = 1; i < c.length; i++) {
27911
+ if (distance(c[0], c[i]) > 0) return true;
27912
+ }
27913
+ return false;
27914
+ }
27915
+
27916
+ function trimPolyline(coords, r) {
27917
+ var p0 = coords[0];
27918
+ for (var i = 1; i < coords.length; i++) {
27919
+ if (distance(p0, coords[i]) > r) {
27920
+ return {
27921
+ kind: 'polyline',
27922
+ coords: [getCircleExit(p0, r, coords[i - 1], coords[i])].concat(coords.slice(i))
27923
+ };
27924
+ }
27925
+ }
27926
+ return null;
27927
+ }
27928
+
27929
+ // The point where segment @p-@q, which starts inside the circle and ends
27930
+ // outside it, crosses it.
27931
+ function getCircleExit(c, r, p, q) {
27932
+ var dx = q[0] - p[0];
27933
+ var dy = q[1] - p[1];
27934
+ var ex = p[0] - c[0];
27935
+ var ey = p[1] - c[1];
27936
+ var a = dx * dx + dy * dy;
27937
+ var b = 2 * (dx * ex + dy * ey);
27938
+ var k = ex * ex + ey * ey - r * r;
27939
+ var s = (-b + Math.sqrt(Math.max(0, b * b - 4 * a * k))) / (2 * a);
27940
+ s = Math.max(0, Math.min(1, s));
27941
+ return [p[0] + s * dx, p[1] + s * dy];
27942
+ }
27943
+
27944
+ function trimBezier(c, r) {
27945
+ var steps = 64;
27946
+ var lo = 0, hi = -1, mid, i;
27947
+ for (i = 1; i <= steps; i++) {
27948
+ if (distance(c[0], getBezierPoint(c, i / steps)) > r) {
27949
+ hi = i / steps;
27950
+ lo = (i - 1) / steps;
27951
+ break;
27952
+ }
27953
+ }
27954
+ if (hi < 0) return null;
27955
+ for (i = 0; i < 24; i++) {
27956
+ mid = (lo + hi) / 2;
27957
+ if (distance(c[0], getBezierPoint(c, mid)) > r) hi = mid;
27958
+ else lo = mid;
27959
+ }
27960
+ return {kind: 'bezier', coords: splitBezier(c, hi)};
27961
+ }
27962
+
27963
+ function getBezierPoint(c, t) {
27964
+ var u = 1 - t;
27965
+ return [
27966
+ u * u * c[0][0] + 2 * u * t * c[1][0] + t * t * c[2][0],
27967
+ u * u * c[0][1] + 2 * u * t * c[1][1] + t * t * c[2][1]
27968
+ ];
27969
+ }
27970
+
27971
+ // The part of the quadratic @c from @t to its end
27972
+ function splitBezier(c, t) {
27973
+ return [getBezierPoint(c, t), lerp(c[1], c[2], t), c[2]];
27974
+ }
27975
+
27976
+ // End markers
27977
+
27978
+ // Unit vector from the start of @path into it, or null for a path with no
27979
+ // direction there.
27980
+ function getStartDirection(path) {
27981
+ var c = path.coords;
27982
+ var next = c[1];
27983
+ if (path.kind == 'bezier' && distance(c[0], c[1]) === 0) next = c[2];
27984
+ return getUnitVector(c[0], next);
27985
+ }
27986
+
27987
+ // A solid arrowhead on a curve, and the curve that runs into it:
27988
+ // {dir, path}, or null if the curve does not reach past the head.
27989
+ //
27990
+ // Pointed along the curve's tangent at the tip, the head faces off to one side
27991
+ // of where the line goes on a bend, and the line cut back along the curve then
27992
+ // meets the back of the head off-centre and at a slant. So the head points
27993
+ // along the chord from its tip to where the curve leaves it, which puts that
27994
+ // point at the middle of its base, and the curve is re-fitted to leave the
27995
+ // head along the same line: it starts at the middle of the base, heading
27996
+ // along the head's axis, and keeps its own direction into the text. The
27997
+ // control point that does both is where those two lines cross. Starting on
27998
+ // the base rather than inside the head keeps the line centred exactly, since
27999
+ // a curve bends away from its starting direction; its round cap reaches back
28000
+ // into the head, where it is hidden.
28001
+ //
28002
+ // path: a quadratic Bezier from the anchor end; len: the head's length
28003
+ function getCurveIntoArrowHead(path, len) {
28004
+ var tip = path.coords[0];
28005
+ var rest = trimBezier(path.coords, len);
28006
+ var dir, start, end, ctrl;
28007
+ if (!rest) return null;
28008
+ start = rest.coords[0];
28009
+ dir = getUnitVector(tip, start);
28010
+ if (!dir) return null;
28011
+ end = rest.coords[2];
28012
+ ctrl = intersectRays(start, dir, end, getUnitVector(end, rest.coords[1]));
28013
+ // A curve that doubles back, or runs straight on: a control point on the
28014
+ // head's axis still takes the line out of the head straight.
28015
+ if (!ctrl) ctrl = [start[0] + dir[0] * distance(start, end) / 2,
28016
+ start[1] + dir[1] * distance(start, end) / 2];
28017
+ return {dir: dir, path: {kind: 'bezier', coords: [start, ctrl, end]}};
28018
+ }
28019
+
28020
+ // Where the ray from @p along @d meets the ray from @q along @e, or null if
28021
+ // they do not meet ahead of both
28022
+ function intersectRays(p, d, q, e) {
28023
+ var cross, s, u, qx, qy;
28024
+ if (!d || !e) return null;
28025
+ cross = d[0] * e[1] - d[1] * e[0];
28026
+ if (Math.abs(cross) < 1e-9) return null;
28027
+ qx = q[0] - p[0];
28028
+ qy = q[1] - p[1];
28029
+ s = (qx * e[1] - qy * e[0]) / cross;
28030
+ u = (qx * d[1] - qy * d[0]) / cross;
28031
+ return s > 0 && u > 0 ? [p[0] + d[0] * s, p[1] + d[1] * s] : null;
28032
+ }
28033
+
28034
+ // [tip, wing, wing], for a head with sides @side long meeting at @angle degrees
28035
+ function getArrowHead(tip, dir, side, angle) {
28036
+ var len = getArrowHeadLength(side, angle);
28037
+ var half = side * Math.sin(angle / 2 * Math.PI / 180);
28038
+ var bx = tip[0] + dir[0] * len;
28039
+ var by = tip[1] + dir[1] * len;
28040
+ return [tip, [bx - dir[1] * half, by + dir[0] * half], [bx + dir[1] * half, by - dir[0] * half]];
28041
+ }
28042
+
28043
+ // How far along the line a head with sides @side long reaches
28044
+ function getArrowHeadLength(side, angle) {
28045
+ return side * Math.cos(angle / 2 * Math.PI / 180);
28046
+ }
28047
+
28048
+ // Utilities
28049
+
28050
+ function padBox$1(box, pad) {
28051
+ return {
28052
+ xmin: box.xmin - pad,
28053
+ xmax: box.xmax + pad,
28054
+ ymin: box.ymin - pad,
28055
+ ymax: box.ymax + pad,
28056
+ midline: box.midline
28057
+ };
28058
+ }
28059
+
28060
+ function boxContains(box, p) {
28061
+ return p[0] > box.xmin && p[0] < box.xmax && p[1] > box.ymin && p[1] < box.ymax;
28062
+ }
28063
+
28064
+ function getUnitVector(p, q) {
28065
+ var dx = q[0] - p[0];
28066
+ var dy = q[1] - p[1];
28067
+ var len = Math.sqrt(dx * dx + dy * dy);
28068
+ return len > 0 ? [dx / len, dy / len] : null;
28069
+ }
28070
+
28071
+ function distance(p, q) {
28072
+ var dx = q[0] - p[0];
28073
+ var dy = q[1] - p[1];
28074
+ return Math.sqrt(dx * dx + dy * dy);
28075
+ }
28076
+
28077
+ function lerp(p, q, t) {
28078
+ return [p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t];
28079
+ }
28080
+
28081
+ function getNumber(val, defaultVal) {
28082
+ return isSvgNumber(val) && Number(val) >= 0 ? Number(val) : defaultVal;
28083
+ }
28084
+
28085
+ function getLineWidth(rec) {
28086
+ var w = getNumber(rec['callout-width'], DEFAULT_LINE_WIDTH);
28087
+ return w > 0 ? w : DEFAULT_LINE_WIDTH;
28088
+ }
28089
+
28090
+ function getFontSizeInPx$1(rec) {
28091
+ var val = rec['font-size'];
28092
+ var px = val === undefined || val === null || val === '' ? null :
28093
+ measureToPx(val, DEFAULT_LABEL_FONT_SIZE);
28094
+ return px > 0 ? px : DEFAULT_LABEL_FONT_SIZE;
28095
+ }
28096
+
28097
+ // px for a number, a px value or an em value, or null
28098
+ function measureToPx(val, emBasis) {
28099
+ var px = toPixels(val, emBasis);
28100
+ var match;
28101
+ if (px !== null) return px;
28102
+ match = /^(-?[.0-9]+)px$/.exec(String(val).trim());
28103
+ return match ? Number(match[1]) : null;
28104
+ }
28105
+
28106
+ // Where the first baseline sits below the text's y, in ems
28107
+ function getBaselineShift(baseline) {
28108
+ var val = String(baseline || '').toLowerCase();
28109
+ if (val == 'central' || val == 'middle') return MIDLINE;
28110
+ if (val == 'hanging' || val == 'text-before-edge') return ASCENT;
28111
+ if (val == 'text-after-edge' || val == 'ideographic') return -DESCENT;
28112
+ return 0;
28113
+ }
28114
+
28115
+ var SvgLabelCallout = /*#__PURE__*/Object.freeze({
28116
+ __proto__: null,
28117
+ getCalloutEndType: getCalloutEndType,
28118
+ getCalloutGap: getCalloutGap,
28119
+ getCalloutShape: getCalloutShape,
28120
+ getControlPoint: getControlPoint,
28121
+ getDefaultCalloutEndSize: getDefaultCalloutEndSize,
28122
+ getLabelCalloutShape: getLabelCalloutShape,
28123
+ getLabelFontSize: getLabelFontSize,
28124
+ getLabelTextBox: getLabelTextBox,
28125
+ labelHasCallout: labelHasCallout,
28126
+ renderLabelCallout: renderLabelCallout
27282
28127
  });
27283
28128
 
27284
28129
  // convert data records (properties like svg-symbol, label-text, fill, r) to svg symbols
@@ -27307,7 +28152,13 @@
27307
28152
  // render label and/or point symbol
27308
28153
  function renderPoint(rec) {
27309
28154
  var children = [];
28155
+ var callout;
27310
28156
  // var halfSize = rec.r || 0; // radius or half of symbol size
28157
+ // Beneath the symbol and the text, which it runs between.
28158
+ if (featureHasLabel(rec) && labelHasCallout(rec)) {
28159
+ callout = renderLabelCallout(rec, getAnchorSymbolRadius(rec));
28160
+ if (callout) children.push(callout);
28161
+ }
27311
28162
  if (featureHasSvgSymbol(rec)) {
27312
28163
  children.push(renderSymbol(rec));
27313
28164
  }
@@ -27333,6 +28184,15 @@
27333
28184
  return empty();
27334
28185
  }
27335
28186
 
28187
+ // How far the symbol at a label's anchor reaches from it, which a callout's
28188
+ // default gap clears. A composite svg-symbol has no single radius, and gets no
28189
+ // clearance.
28190
+ function getAnchorSymbolRadius(d) {
28191
+ if (!featureHasSvgSymbol(d) || d['svg-symbol']) return 0;
28192
+ if (featureHasIcon(d)) return getIconRadius(d, d.icon || 'circle');
28193
+ return d.r > 0 ? d.r : 0;
28194
+ }
28195
+
27336
28196
  function featureHasIcon(d) {
27337
28197
  return !!(d && (d.icon || d['icon-size'] || (d['icon-color'] && d.r > 0)));
27338
28198
  }
@@ -27557,6 +28417,9 @@
27557
28417
 
27558
28418
  var SvgSymbols = /*#__PURE__*/Object.freeze({
27559
28419
  __proto__: null,
28420
+ featureHasIcon: featureHasIcon,
28421
+ getAnchorSymbolRadius: getAnchorSymbolRadius,
28422
+ getIconRadius: getIconRadius,
27560
28423
  getTransform: getTransform,
27561
28424
  renderPoint: renderPoint,
27562
28425
  symbolRenderers: symbolRenderers
@@ -28518,7 +29381,9 @@
28518
29381
  }
28519
29382
  cls = LABEL_OVERFLOW_CLASS;
28520
29383
  }
28521
- text = toLabelString(rec['label-text']);
29384
+ // A soft break is wrapping for a width, which text along a path does not
29385
+ // have, and removing it leaves the space it broke at.
29386
+ text = removeSoftBreaks(toLabelString(rec['label-text']));
28522
29387
  if (labelNewlineRxp.test(text)) {
28523
29388
  // a <tspan> inside a <textPath> advances along the path instead of
28524
29389
  // stacking below it, so the lines are joined rather than dropping
@@ -28546,6 +29411,7 @@
28546
29411
  if (rec.dy) textPath.properties.dy = rec.dy;
28547
29412
  o = {tag: 'text', properties: {}, children: [textPath]};
28548
29413
  applyStyleAttributes(o, 'label', rec);
29414
+ applyLabelHalo(o, rec);
28549
29415
  if (cls) {
28550
29416
  o.properties.class = o.properties.class ? o.properties.class + ' ' + cls : cls;
28551
29417
  }
@@ -28842,6 +29708,11 @@
28842
29708
  // are applied to the <g> container, 'r' property is applied to circle
28843
29709
  applyStyleAttributes(svgObj, msType, d, simpleCircleFilter);
28844
29710
  } else ;
29711
+ // Only here, where SVG is written for other programs to read: the GUI
29712
+ // draws the same labels with paint-order and has no use for the copies.
29713
+ if (labelHasHalo(d)) {
29714
+ svgObj = splitLabelHalos(svgObj);
29715
+ }
28845
29716
  if ('id' in obj) {
28846
29717
  if (!svgObj.properties) {
28847
29718
  svgObj.properties = {};
@@ -40574,7 +41445,7 @@ ${svg}
40574
41445
  'font-weight': {describe: 'normal, bold or a numeric weight'},
40575
41446
  'font-stretch': {describe: 'e.g. condensed'},
40576
41447
  'letter-spacing': {describe: 'extra space between letters'},
40577
- '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'},
40578
41449
  'text-anchor': {describe: 'start, middle or end'},
40579
41450
  'label-align': {describe: 'how the lines of a multi-line label align: left, center or right'},
40580
41451
  'dominant-baseline': {describe: 'vertical alignment, e.g. central'},
@@ -40585,6 +41456,20 @@ ${svg}
40585
41456
  dy: {describe: 'vertical offset from the anchor'},
40586
41457
  fill: {describe: 'text color'},
40587
41458
  opacity: {describe: 'text opacity'},
41459
+ 'halo-width': {describe: 'width of a halo around the text in px'},
41460
+ 'halo-color': {describe: 'halo color (default is white)'},
41461
+ 'halo-opacity': {describe: 'halo opacity, 0-1'},
41462
+ 'label-width': {describe: 'width of a fixed-width text block in px'},
41463
+ callout: {describe: 'line from the anchor to the text: line, elbow or curve'},
41464
+ 'callout-end': {describe: 'arrowhead at the anchor end: arrow, open-arrow or none'},
41465
+ 'callout-end-size': {describe: 'length of an arrowhead\'s sides in px'},
41466
+ 'callout-via': {describe: 'x,y of an elbow\'s corner or a point on a curve, in px'},
41467
+ 'callout-attach': {describe: 'where the callout meets the text, as x,y fractions'},
41468
+ 'callout-gap': {describe: 'px between the callout and the anchor'},
41469
+ 'callout-padding': {describe: 'px between the callout and the text'},
41470
+ 'callout-color': {describe: 'callout color (defaults to the text color)'},
41471
+ 'callout-width': {describe: 'callout line width in px'},
41472
+ 'callout-opacity': {describe: 'callout opacity, 0-1'},
40588
41473
  css: {describe: 'inline css style'},
40589
41474
  class: {describe: 'name of CSS class or classes (space-separated)'}
40590
41475
  },
@@ -42612,6 +43497,15 @@ ${svg}
42612
43497
  .option('label-align', {
42613
43498
  describe: 'alignment of the lines of multi-line labels; left, center or right'
42614
43499
  })
43500
+ .option('halo-width', {
43501
+ describe: 'width of a halo around label text in px (default is 0, no halo)'
43502
+ })
43503
+ .option('halo-color', {
43504
+ describe: 'color of label halos (default is white)'
43505
+ })
43506
+ .option('halo-opacity', {
43507
+ describe: 'opacity of label halos, 0-1'
43508
+ })
42615
43509
  .option('dx', {
42616
43510
  describe: 'x offset of labels (default is 0)'
42617
43511
  })
@@ -42637,7 +43531,7 @@ ${svg}
42637
43531
  describe: 'CSS letter-spacing property of labels'
42638
43532
  })
42639
43533
  .option('line-height', {
42640
- 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)'
42641
43535
  })
42642
43536
  .option('dominant-baseline', {
42643
43537
  describe: 'vertical alignment of labels (e.g. central)'
@@ -42648,6 +43542,39 @@ ${svg}
42648
43542
  .option('label-start-offset', {
42649
43543
  describe: 'where label text starts along its path, e.g. 50%'
42650
43544
  })
43545
+ .option('label-width', {
43546
+ describe: 'width of a fixed-width text block in px (lines are wrapped in the web UI)'
43547
+ })
43548
+ .option('callout', {
43549
+ describe: 'line from a label\'s anchor to its text: line, elbow or curve'
43550
+ })
43551
+ .option('callout-end', {
43552
+ describe: 'arrowhead at the anchor end of a callout: arrow (solid), open-arrow or none'
43553
+ })
43554
+ .option('callout-end-size', {
43555
+ describe: 'length of an arrowhead\'s sides in px (default 10, more for a thicker line)'
43556
+ })
43557
+ .option('callout-via', {
43558
+ describe: 'x,y of an elbow\'s corner or a point on a curve, in px from the anchor'
43559
+ })
43560
+ .option('callout-attach', {
43561
+ describe: 'where a callout meets its text, as x,y fractions of the text box'
43562
+ })
43563
+ .option('callout-gap', {
43564
+ describe: 'px between a callout and its anchor (default: a line meets the icon, an arrow clears it)'
43565
+ })
43566
+ .option('callout-padding', {
43567
+ describe: 'px between a callout and its text (default is 3)'
43568
+ })
43569
+ .option('callout-color', {
43570
+ describe: 'callout color (defaults to the text color)'
43571
+ })
43572
+ .option('callout-width', {
43573
+ describe: 'callout line width in px (default is 1)'
43574
+ })
43575
+ .option('callout-opacity', {
43576
+ describe: 'callout opacity, 0-1'
43577
+ })
42651
43578
  .option('target', targetOpt);
42652
43579
 
42653
43580
  parser.command('repel')
@@ -42938,6 +43865,13 @@ ${svg}
42938
43865
  describe: 'frame coordinates (xmin,ymin,xmax,ymax)',
42939
43866
  type: 'bbox'
42940
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
+ })
42941
43875
  .option('width', {
42942
43876
  describe: 'nominal frame width (e.g. 5in, 10cm, 600px)'
42943
43877
  })
@@ -43089,6 +44023,10 @@ ${svg}
43089
44023
  describe: 'separate offsets for each side, in l,b,r,t order',
43090
44024
  type: 'strings'
43091
44025
  })
44026
+ .option('ignore-symbols', {
44027
+ describe: 'fit to point locations, not the extent of symbols and labels',
44028
+ type: 'flag'
44029
+ })
43092
44030
  .option('name', nameOpt)
43093
44031
  .option('replace', {
43094
44032
  describe: 'replace the existing map frame',
@@ -50701,8 +51639,637 @@ ${svg}
50701
51639
  bboxToPolygon: bboxToPolygon
50702
51640
  });
50703
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
+
50704
52271
  cmd.frame = function(catalog, targets, opts) {
50705
- var widthPx, heightPx, aspectRatio, bbox;
52272
+ var widthPx, heightPx, bbox;
50706
52273
  var existingFrame = getActiveFrame(catalog);
50707
52274
  if (opts.width) {
50708
52275
  widthPx = parseFrameSize(opts.width).valuePx;
@@ -50738,30 +52305,24 @@ ${svg}
50738
52305
  } else {
50739
52306
  var datasets = utils.pluck(targets, 'dataset');
50740
52307
  requireDatasetsHaveCompatibleCRS(datasets, 'Targets include both projected and unprojected coordinates');
50741
- 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);
50742
52312
  if (!bbox) {
50743
52313
  stop$1('Command target is missing geographical bounds');
50744
52314
  }
50745
52315
  }
50746
52316
 
50747
- applyPercentageOffsets(bbox, opts.offset || opts.offsets);
50748
- applyPixelOffsets(bbox, widthPx, heightPx, opts.offset || opts.offsets);
50749
-
50750
- 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) {
50751
52319
  stop$1('Frame has a collapsed bbox');
50752
52320
  }
50753
52321
 
50754
- aspectRatio = (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
50755
- if (!widthPx) {
50756
- widthPx = roundToDigits(heightPx * aspectRatio, 1);
50757
- } else if (!heightPx) {
50758
- heightPx = roundToDigits(widthPx / aspectRatio, 1);
50759
- }
50760
-
50761
52322
  var feature = {
50762
52323
  type: 'Feature',
50763
- properties: getFrameProperties(widthPx, heightPx, opts),
50764
- geometry: bboxToPolygon(bbox)
52324
+ properties: getFrameProperties(extent.width, extent.height, opts),
52325
+ geometry: bboxToPolygon(extent.bbox)
50765
52326
  };
50766
52327
  var frameDataset = importGeoJSON(feature);
50767
52328
  // set CRS from target dataset
@@ -50781,6 +52342,34 @@ ${svg}
50781
52342
  catalog.addDataset(frameDataset);
50782
52343
  };
50783
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
+
50784
52373
  function getFrameProperties(width, height, opts) {
50785
52374
  var properties = {
50786
52375
  type: 'frame',
@@ -50876,14 +52465,6 @@ ${svg}
50876
52465
  return arg;
50877
52466
  }
50878
52467
 
50879
- function getTargetBbox(targets) {
50880
- var expanded = expandCommandTargets(targets);
50881
- var bounds = expanded.reduce(function(memo, o) {
50882
- return memo.mergeBounds(getLayerBounds(o.layer, o.dataset.arcs));
50883
- }, new Bounds());
50884
- return bounds.hasBounds() ? bounds.toArray() : null;
50885
- }
50886
-
50887
52468
  // Convert width and height args to aspect ratio arg for the rectangle() function
50888
52469
  function getAspectRatioArg(widthArg, heightArg) {
50889
52470
  // heightArg is a string containing either a number or a
@@ -51028,7 +52609,8 @@ ${svg}
51028
52609
 
51029
52610
  var OPERATION$1 = 'update-frame';
51030
52611
 
51031
- function updateFrame(targetLayers, dataset, opts) {
52612
+ // @fitTargets: [{layer, dataset}] named by the fit= option
52613
+ function updateFrame(targetLayers, dataset, opts, fitTargets) {
51032
52614
  if (!targetLayers || targetLayers.length != 1) {
51033
52615
  stop$1('-update-frame expects a single target layer');
51034
52616
  }
@@ -51052,15 +52634,14 @@ ${svg}
51052
52634
  if (opts.fix_scale && (opts.width !== undefined || opts.height !== undefined)) {
51053
52635
  stop$1('fix-scale cannot be combined with width= or height=');
51054
52636
  }
52637
+ if (opts.bbox !== undefined && opts.fit !== undefined) {
52638
+ stop$1('bbox= and fit= are mutually exclusive');
52639
+ }
51055
52640
 
51056
52641
  var rec = lyr.data.getRecords()[0];
51057
52642
  var frame = getFrameLayerData(lyr, dataset.arcs);
51058
- var bbox = (opts.bbox || frame.bbox).slice();
51059
- var offsetArg = opts.offset || opts.offsets;
51060
52643
  var fixedAspect = getFixedAspect(rec);
51061
- var width = frame.width;
51062
- var units = rec.frame_units || 'px';
51063
- var widthSize, heightSize, height, effectiveAspect;
52644
+ var sizes, contentBbox, update;
51064
52645
 
51065
52646
  // 1. Aspect mode, resolved first because whether the page shape is fixed
51066
52647
  // decides whether it may constrain the extent while offsets are applied.
@@ -51072,8 +52653,87 @@ ${svg}
51072
52653
  }
51073
52654
  fixedAspect = opts.aspect_ratio;
51074
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
+ }
52709
+
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;
51075
52735
 
51076
- // 2. Extent
52736
+ // 3. Padding
51077
52737
  if (offsetArg) {
51078
52738
  applyPercentageOffsets(bbox, offsetArg);
51079
52739
  // Pass a page height only when the shape is fixed. A derived height
@@ -51082,65 +52742,44 @@ ${svg}
51082
52742
  applyPixelOffsets(bbox, frame.width,
51083
52743
  fixedAspect ? frame.width / fixedAspect : null, offsetArg);
51084
52744
  }
51085
- requireValidBbox(bbox);
52745
+ valid = isValidBbox(bbox);
51086
52746
  if (fixedAspect) {
51087
52747
  fillOutBbox(bbox, fixedAspect, 1);
51088
52748
  }
51089
52749
 
51090
- // 3. Nominal size
51091
- if (opts.width !== undefined) {
51092
- widthSize = parseFrameSize(opts.width);
51093
- requirePositiveSize(widthSize.valuePx, 'width', opts.width);
51094
- }
51095
- if (opts.height !== undefined) {
51096
- heightSize = parseFrameSize(opts.height);
51097
- requirePositiveSize(heightSize.valuePx, 'height', opts.height);
51098
- }
51099
- if (widthSize && heightSize) {
51100
- effectiveAspect = widthSize.valuePx / heightSize.valuePx;
51101
- if (opts.aspect_ratio !== undefined &&
51102
- ratiosDiffer(effectiveAspect, opts.aspect_ratio)) {
51103
- stop$1('Contradictory width, height and aspect-ratio values');
51104
- }
51105
- width = widthSize.valuePx;
51106
- units = widthSize.units;
51107
- 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;
51108
52755
  fillOutBbox(bbox, fixedAspect, 1);
51109
- } else if (widthSize) {
51110
- width = widthSize.valuePx;
51111
- units = widthSize.units;
51112
- } else if (heightSize) {
52756
+ } else if (sizes.width) {
52757
+ width = sizes.width.valuePx;
52758
+ units = sizes.width.units;
52759
+ } else if (sizes.height) {
51113
52760
  effectiveAspect = fixedAspect || getBboxAspect(bbox);
51114
- width = heightSize.valuePx * effectiveAspect;
51115
- units = heightSize.units;
52761
+ width = sizes.height.valuePx * effectiveAspect;
52762
+ units = sizes.height.units;
51116
52763
  } else if (opts.fix_scale) {
51117
52764
  // Hold ground units per output pixel, so the page grows and shrinks with
51118
52765
  // the extent instead of the scale changing to fit the extent on it.
51119
52766
  width = frame.width * getBboxWidth(bbox) / getBboxWidth(frame.bbox);
51120
- requirePositiveSize(width, 'fix-scale', width);
51121
52767
  }
51122
52768
 
51123
- requireValidBbox(bbox);
51124
52769
  effectiveAspect = fixedAspect || getBboxAspect(bbox);
51125
- height = Math.round(width / effectiveAspect);
51126
-
51127
- noteLayerWillChange(lyr, {operation: OPERATION$1, unit: 'shapes'});
51128
- lyr.data.captureTableBefore({operation: OPERATION$1});
51129
- rec.width = width;
51130
- rec.height = height;
51131
- rec.frame_units = units;
51132
- if (fixedAspect) {
51133
- rec.frame_aspect_ratio = fixedAspect;
51134
- } else {
51135
- delete rec.frame_aspect_ratio;
51136
- }
51137
- lyr.data.markChanged({operation: OPERATION$1});
51138
- rebuildFrameLayerGeometry(lyr, dataset, new Bounds(bbox));
51139
- 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
+ };
51140
52778
  }
51141
52779
 
51142
52780
  function hasUpdateOptions(opts) {
51143
52781
  return opts.bbox !== undefined ||
52782
+ opts.fit !== undefined ||
51144
52783
  opts.width !== undefined ||
51145
52784
  opts.height !== undefined ||
51146
52785
  opts.aspect_ratio !== undefined ||
@@ -51163,12 +52802,10 @@ ${svg}
51163
52802
  return (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
51164
52803
  }
51165
52804
 
51166
- function requireValidBbox(bbox) {
51167
- if (!bbox.every(utils.isFiniteNumber) ||
51168
- bbox[2] - bbox[0] <= 0 ||
51169
- bbox[3] - bbox[1] <= 0) {
51170
- stop$1('Frame has a collapsed bbox');
51171
- }
52805
+ function isValidBbox(bbox) {
52806
+ return bbox.every(utils.isFiniteNumber) &&
52807
+ bbox[2] - bbox[0] > 0 &&
52808
+ bbox[3] - bbox[1] > 0;
51172
52809
  }
51173
52810
 
51174
52811
  function requirePositiveSize(value, name, arg) {
@@ -51398,168 +53035,6 @@ ${svg}
51398
53035
  stop$1('Unable to import coordinates');
51399
53036
  }
51400
53037
 
51401
- // Apply rotation, scale and/or shift to some or all of the features in a dataset
51402
- //
51403
- cmd.affine = function(targetLayers, dataset, opts) {
51404
- // Need to separate the targeted shapes from any other shapes that share
51405
- // the same topology. So we duplicate any arcs that are shared by the targeted
51406
- // shapes and their topological neighbors and remap arc references in the
51407
- // neighbors to point to the copies.
51408
- // TODO: explore alternative: if some arcs are shared between transformed and
51409
- // non-transformed shapes, first remove topology, then tranform, then rebuild topology
51410
- //
51411
- var rotateArg = opts.rotate || 0;
51412
- var scaleArg = opts.scale || 1;
51413
- var shiftArg = opts.shift ? convertIntervalPair(opts.shift, getDatasetCRS(dataset)) : [0, 0];
51414
- var arcs = dataset.arcs;
51415
- var targetShapes = [];
51416
- var otherShapes = [];
51417
- var targetPoints = [];
51418
- var targetPointLayers = [];
51419
- var targetFlags, otherFlags, transform;
51420
- dataset.layers.filter(layerHasGeometry$1).forEach(function(lyr) {
51421
- var hits = [],
51422
- misses = [],
51423
- test;
51424
- if (targetLayers.indexOf(lyr) == -1) {
51425
- misses = lyr.shapes;
51426
- } else if (opts.where) {
51427
- test = compileFeatureExpression(opts.where, lyr, dataset.arcs);
51428
- lyr.shapes.forEach(function(shp, i) {
51429
- (test(i) ? hits : misses).push(shp);
51430
- });
51431
- } else {
51432
- hits = lyr.shapes;
51433
- }
51434
- if (lyr.geometry_type == 'point') {
51435
- targetPoints = targetPoints.concat(hits);
51436
- if (hits.length > 0) targetPointLayers.push(lyr);
51437
- } else {
51438
- targetShapes = targetShapes.concat(hits);
51439
- otherShapes = otherShapes.concat(misses);
51440
- }
51441
- });
51442
- var anchorArg = getAffineAnchor({arcs: dataset.arcs, layers: [{
51443
- geometry_type: 'point', shapes: targetPoints}, {geometry_type: 'polyline',
51444
- shapes: targetShapes}]}, opts);
51445
- transform = getAffineTransform(rotateArg, scaleArg, shiftArg, anchorArg);
51446
- if (opts.fit_bbox) {
51447
- transform = getFitBoxTransform(opts.fit_bbox, targetPoints, targetShapes, arcs);
51448
- }
51449
- if (targetShapes.length > 0) {
51450
- targetFlags = new Uint8Array(arcs.size());
51451
- otherFlags = new Uint8Array(arcs.size());
51452
- countArcsInShapes(targetShapes, targetFlags);
51453
- if (otherShapes.length > 0) {
51454
- countArcsInShapes(otherShapes, otherFlags);
51455
- applyArrayMask(otherFlags, targetFlags);
51456
- noteDatasetWillChange(dataset, {operation: 'affine', unit: 'arcs'});
51457
- dataset.arcs = duplicateSelectedArcs(otherShapes, arcs, otherFlags);
51458
- markDatasetChanged(dataset, {operation: 'affine', unit: 'arcs'});
51459
- }
51460
- dataset.arcs.transformPoints(function(x, y, arcId) {
51461
- if (arcId < targetFlags.length && targetFlags[arcId] > 0) {
51462
- return transform(x, y);
51463
- }
51464
- });
51465
- }
51466
- targetPointLayers.forEach(function(lyr) {
51467
- noteLayerWillChange(lyr, {operation: 'affine', unit: 'shapes'});
51468
- });
51469
- forEachPoint(targetPoints, function(p) {
51470
- var p2 = transform(p[0], p[1]);
51471
- p[0] = p2[0];
51472
- p[1] = p2[1];
51473
- });
51474
- targetPointLayers.forEach(function(lyr) {
51475
- markLayerChanged(lyr, {operation: 'affine', unit: 'shapes'});
51476
- });
51477
- };
51478
-
51479
- function getAffineAnchor(dataset, opts) {
51480
- var anchor, bounds;
51481
- if (opts.anchor) {
51482
- anchor = opts.anchor;
51483
- } else {
51484
- // get bounds of selected shapes to calculate center of rotation/scale
51485
- bounds = getDatasetBounds(dataset);
51486
- anchor = [bounds.centerX(), bounds.centerY()];
51487
- }
51488
- return anchor;
51489
- }
51490
-
51491
- // TODO: handle problems with unprojected datasets
51492
- // option 1: don't allow affine transformation of unprojected data
51493
- // option 2: error if transformed data exceeds valid coordinate range
51494
- // source: http://mathworld.wolfram.com/AffineTransformation.html
51495
- function getAffineTransform(rotation, scale, shift, anchor) {
51496
- var angle = rotation * Math.PI / 180;
51497
- var a = scale * Math.cos(angle);
51498
- var b = -scale * Math.sin(angle);
51499
- return function(x, y) {
51500
- var x2 = a * (x - anchor[0]) - b * (y - anchor[1]) + shift[0] + anchor[0];
51501
- var y2 = b * (x - anchor[0]) + a * (y - anchor[1]) + shift[1] + anchor[1];
51502
- return [x2, y2];
51503
- };
51504
- }
51505
-
51506
- function getFitBoxTransform(bbox, points, shapes, arcs) {
51507
- var dataset = {
51508
- arcs: arcs,
51509
- layers: []
51510
- };
51511
- if (points && points.length) {
51512
- dataset.layers.push({
51513
- geometry_type: 'point',
51514
- shapes: points
51515
- });
51516
- }
51517
- if (shapes && shapes.length) {
51518
- dataset.layers.push({
51519
- geometry_type: 'polyline',
51520
- shapes: shapes
51521
- });
51522
- }
51523
- var frame = calcFrameData(dataset, {fit_bbox: bbox});
51524
- var fromBounds = new Bounds(frame.bbox);
51525
- var toBounds = new Bounds(frame.bbox2);
51526
- var fwd = fromBounds.getTransform(toBounds, false);
51527
- return function(x, y) {
51528
- return fwd.transform(x, y);
51529
- };
51530
- }
51531
-
51532
- function applyArrayMask(destArr, maskArr) {
51533
- for (var i=0, n=destArr.length; i<n; i++) {
51534
- if (maskArr[i] === 0) destArr[i] = 0;
51535
- }
51536
- }
51537
-
51538
- function duplicateSelectedArcs(shapes, arcs, flags) {
51539
- var arcCount = 0;
51540
- var vertexCount = 0;
51541
- var data = arcs.getVertexData();
51542
- var xx = [], yy = [], nn = [], map = [], n;
51543
- for (var i=0, len=flags.length; i<len; i++) {
51544
- if (flags[i] > 0) {
51545
- map[i] = arcs.size() + arcCount;
51546
- n = data.nn[i];
51547
- utils.copyElements(data.xx, data.ii[i], xx, vertexCount, n);
51548
- utils.copyElements(data.yy, data.ii[i], yy, vertexCount, n);
51549
- nn.push(n);
51550
- vertexCount += n;
51551
- arcCount++;
51552
- }
51553
- }
51554
- forEachArcId(shapes, function(id) {
51555
- var absId = absArcId(id);
51556
- if (flags[absId] > 0) {
51557
- return id < 0 ? ~map[absId] : map[absId];
51558
- }
51559
- });
51560
- return mergeArcs([arcs, new ArcCollection(nn, xx, yy)]);
51561
- }
51562
-
51563
53038
  const epsilon = 1.1102230246251565e-16;
51564
53039
  const splitter = 134217729;
51565
53040
  const resulterrbound = (3 + 8 * epsilon) * epsilon;
@@ -75102,7 +76577,7 @@ ${svg}
75102
76577
 
75103
76578
  function createMeridianPart(x, ymin, ymax) {
75104
76579
  var coords = densifyPathByInterval([[x, ymin], [x, ymax]], precision);
75105
- meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord$1(x)}));
76580
+ meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord(x)}));
75106
76581
  }
75107
76582
 
75108
76583
  function createParallel(y) {
@@ -75136,7 +76611,7 @@ ${svg}
75136
76611
  }
75137
76612
 
75138
76613
  // remove tiny offsets
75139
- function roundCoord$1(x) {
76614
+ function roundCoord(x) {
75140
76615
  return +x.toFixed(3) || 0;
75141
76616
  }
75142
76617
 
@@ -78176,160 +79651,6 @@ ${svg}
78176
79651
  };
78177
79652
  }
78178
79653
 
78179
- var roundCoord = getRoundingFunction(0.01);
78180
-
78181
- function getSymbolFillColor(d) {
78182
- return d.fill || 'magenta';
78183
- }
78184
-
78185
- function getSymbolStrokeColor(d) {
78186
- return d.stroke || d.fill || 'magenta';
78187
- }
78188
-
78189
- function applySymbolStyles(sym, d) {
78190
- if (sym.type == 'polyline') {
78191
- sym.stroke = getSymbolStrokeColor(d);
78192
- } else {
78193
- sym.fill = getSymbolFillColor(d);
78194
- }
78195
- if (d.opacity) {
78196
- sym.opacity = d.opacity;
78197
- }
78198
- return sym;
78199
- }
78200
-
78201
- function getSymbolRadius(d) {
78202
- if (d.radius === 0 || d.length === 0 || d.r === 0) return 0;
78203
- return d.radius || d.length || d.r || 5; // use a default value
78204
- }
78205
-
78206
- // Converts the value of a list-valued option into an array of items. The value
78207
- // arrives either as a whole string (from a direct call, rather than from the
78208
- // -symbols option accessor) or as an array of items already resolved one by
78209
- // one. An item that holds an array of its own, as an expression like
78210
- // [R, R * 0.5] does, is flattened into the list. Items themselves are left
78211
- // unsplit, because a single item can contain a comma, as rgba() colors do.
78212
- function toItemList(val) {
78213
- var items = utils.isString(val) ? splitListItems(val) : [].concat(val);
78214
- var list = [];
78215
- for (var i=0; i<items.length; i++) {
78216
- if (Array.isArray(items[i])) {
78217
- list = list.concat(items[i]);
78218
- } else {
78219
- list.push(items[i]);
78220
- }
78221
- }
78222
- return list;
78223
- }
78224
-
78225
- // Like toItemList(), for options that take numbers. An item that is a string
78226
- // containing commas is split, so that a data field holding a value like "2,4"
78227
- // works as a list. Values that aren't numbers become NaN, for the caller to
78228
- // report or ignore.
78229
- function toNumberList(val) {
78230
- var list = [];
78231
- toItemList(val).forEach(function(item) {
78232
- if (utils.isString(item) && item.indexOf(',') > -1) {
78233
- list = list.concat(parseNumberList(item));
78234
- } else {
78235
- list.push(Number(item));
78236
- }
78237
- });
78238
- return list;
78239
- }
78240
-
78241
- // Returns the radius of the smallest circle centered on a symbol's point that
78242
- // covers the symbol, or null if the symbol contains a part whose reach can't be
78243
- // measured.
78244
- //
78245
- // Only a symbol drawn around its point can be described this way: a circle, or
78246
- // a group of the circles and polygons that make up ring, pie and donut symbols.
78247
- // An arrow is drawn from its point outward, so the circle around its point that
78248
- // covers it says nothing useful about the space it occupies -- arrows are plain
78249
- // polygon symbols, which reach this function only as group parts, which they
78250
- // never are.
78251
- function getSymbolBoundingRadius(sym) {
78252
- var max = 0;
78253
- var parts, r, i;
78254
- if (!sym) return null;
78255
- if (sym.type == 'circle') {
78256
- // Half of a stroke lies outside the circle it follows, which is how a ring
78257
- // symbol paints its bands.
78258
- return getPositiveNumber(sym.r) + getStrokeOutset(sym);
78259
- }
78260
- if (sym.type == 'polygon') {
78261
- forEachSymbolCoord(sym.coordinates || [], function(p) {
78262
- var dist = Math.sqrt(p[0] * p[0] + p[1] * p[1]);
78263
- if (dist > max) max = dist;
78264
- });
78265
- return max + getStrokeOutset(sym);
78266
- }
78267
- if (sym.type == 'group') {
78268
- parts = sym.parts || [];
78269
- for (i=0; i<parts.length; i++) {
78270
- r = getSymbolBoundingRadius(parts[i]);
78271
- if (r === null) return null;
78272
- if (r > max) max = r;
78273
- }
78274
- return max;
78275
- }
78276
- return null;
78277
- }
78278
-
78279
- // A stroke straddles the path it is drawn on, so half of its width extends
78280
- // beyond the shape.
78281
- function getStrokeOutset(sym) {
78282
- var width = sym['stroke-width'];
78283
- if (!sym.stroke || sym.stroke == 'none') return 0;
78284
- // an SVG stroke is one pixel wide unless stroke-width says otherwise
78285
- if (width === undefined || width === null || width === '') return 0.5;
78286
- return getPositiveNumber(width) / 2;
78287
- }
78288
-
78289
- function getPositiveNumber(val) {
78290
- var num = +val;
78291
- return num > 0 ? num : 0;
78292
- }
78293
-
78294
- function forEachSymbolCoord(coords, cb) {
78295
- var isPoint = coords && utils.isNumber(coords[0]);
78296
- var isNested = !isPoint && coords && Array.isArray(coords[0]);
78297
- if (isPoint) return cb(coords);
78298
- for (var i=0; i<coords.length; i++) {
78299
- if (isNested) forEachSymbolCoord(coords[i], cb);
78300
- }
78301
- }
78302
-
78303
- function flipY(coords) {
78304
- forEachSymbolCoord(coords, function(p) {
78305
- p[1] = -p[1];
78306
- });
78307
- }
78308
-
78309
- function scaleAndShiftCoords(coords, scale, shift) {
78310
- forEachSymbolCoord(coords, function(xy) {
78311
- xy[0] = xy[0] * scale + shift[0];
78312
- xy[1] = xy[1] * scale + shift[1];
78313
- });
78314
- }
78315
-
78316
- function roundCoordsForSVG(coords) {
78317
- forEachSymbolCoord(coords, function(p) {
78318
- p[0] = roundCoord(p[0]);
78319
- p[1] = roundCoord(p[1]);
78320
- });
78321
- }
78322
-
78323
- function rotateCoords(coords, rotation) {
78324
- if (!rotation) return;
78325
- var f = getAffineTransform(rotation, 1, [0, 0], [0, 0]);
78326
- forEachSymbolCoord(coords, function(p) {
78327
- var p2 = f(p[0], p[1]);
78328
- p[0] = p2[0];
78329
- p[1] = p2[1];
78330
- });
78331
- }
78332
-
78333
79654
  // Collision reduction for circular symbols, in the spirit of d3-force's
78334
79655
  // forceCollide. Overlapping symbols are pushed apart, but each one is kept
78335
79656
  // within a fixed distance of its true position.
@@ -82223,7 +83544,8 @@ ${svg}
82223
83544
  job.catalog.addDataset(cmd.addLayer(targetDataset, opts));
82224
83545
 
82225
83546
  } else if (name == 'update-frame') {
82226
- cmd.updateFrame(targetLayers, targetDataset, opts);
83547
+ cmd.updateFrame(targetLayers, targetDataset, opts,
83548
+ opts.fit ? expandCommandTargets(job.catalog.findCommandTargets(opts.fit)) : null);
82227
83549
 
82228
83550
  } else if (name == 'update-label') {
82229
83551
  cmd.updateLabel(targetLayers, targetDataset, opts);
@@ -82789,7 +84111,7 @@ ${svg}
82789
84111
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
82790
84112
  }
82791
84113
 
82792
- var version = "0.7.66";
84114
+ var version = "0.7.68";
82793
84115
 
82794
84116
  // Parse command line args into commands and run them
82795
84117
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -83613,7 +84935,7 @@ ${svg}
83613
84935
  if (!(fontSize > 0)) return null;
83614
84936
  // The widest line, which is what the block of a multi-line label is as wide
83615
84937
  // as, and what the browser's getBBox() reports for the same text.
83616
- width = text.split(labelNewlineRxp).reduce(function(max, line) {
84938
+ width = splitLabelLines(text).reduce(function(max, line) {
83617
84939
  var w = measureLine(font, line, fontSize, spacing);
83618
84940
  return w > max ? w : max;
83619
84941
  }, 0);
@@ -85391,7 +86713,7 @@ ${svg}
85391
86713
  internal.svg = Object.assign({}, SvgStringify, SvgPathUtils, GeojsonToSvg,
85392
86714
  SvgFeatureUtils,
85393
86715
  SvgLabels, SvgSymbols, SvgLabelPaths, SvgLabelFit, SvgLabelAlign,
85394
- SvgLabelMetrics);
86716
+ SvgLabelMetrics, SvgLabelHalo, SvgLabelCallout);
85395
86717
 
85396
86718
  // Reached through the bundle rather than imported from source, unlike most of
85397
86719
  // what tests use, because these modules load fs and fontkit through the