mapshaper 0.7.65 → 0.7.67

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,179 @@
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. That matches paint-order, where the fill covers
27240
+ // the stroke's inner half and nothing else, and it keeps a translucent fill
27241
+ // from showing a second colour through it.
27242
+ //
27243
+ // Things that apply to the label as a whole move to the group: its transform,
27244
+ // which places it, and its opacity, which would otherwise fade each copy on its
27245
+ // own and let the halo show through the letters. That is also how opacity
27246
+ // reads on a single element with paint-order, which is faded as one image.
27247
+ function splitLabelHalos(o) {
27248
+ if (!o) return o;
27249
+ if (o.tag == 'text' && isHaloText(o)) return splitHalo(o);
27250
+ if (o.children) {
27251
+ for (var i = 0; i < o.children.length; i++) {
27252
+ o.children[i] = splitLabelHalos(o.children[i]);
27253
+ }
27254
+ }
27255
+ return o;
27256
+ }
27257
+
27258
+ function isHaloText(o) {
27259
+ return !!o.properties && o.properties['paint-order'] == HALO_PAINT_ORDER;
27260
+ }
27261
+
27262
+ function splitHalo(text) {
27263
+ var halo = cloneSvgObject(text);
27264
+ var group = {tag: 'g', properties: {}, children: [halo, text]};
27265
+ moveProperty(text.properties, group.properties, 'transform');
27266
+ moveProperty(text.properties, group.properties, 'opacity');
27267
+ delete halo.properties.transform;
27268
+ delete halo.properties.opacity;
27269
+ delete halo.properties['paint-order'];
27270
+ delete halo.properties['fill-opacity'];
27271
+ halo.properties.fill = 'none';
27272
+ HALO_STROKE_PROPERTIES.forEach(function(k) {
27273
+ delete text.properties[k];
27274
+ });
27275
+ return group;
27276
+ }
27277
+
27278
+ function moveProperty(src, dest, k) {
27279
+ if (k in src) {
27280
+ dest[k] = src[k];
27281
+ delete src[k];
27282
+ }
27283
+ }
27284
+
27285
+ function cloneSvgObject(o) {
27286
+ var copy = {tag: o.tag};
27287
+ if ('value' in o) copy.value = o.value;
27288
+ if (o.properties) copy.properties = Object.assign({}, o.properties);
27289
+ if (o.children) copy.children = o.children.map(cloneSvgObject);
27290
+ return copy;
27291
+ }
27292
+
27293
+ var SvgLabelHalo = /*#__PURE__*/Object.freeze({
27294
+ __proto__: null,
27295
+ DEFAULT_HALO_COLOR: DEFAULT_HALO_COLOR,
27296
+ applyLabelHalo: applyLabelHalo,
27297
+ labelHasHalo: labelHasHalo,
27298
+ splitLabelHalos: splitLabelHalos
27299
+ });
27300
+
27120
27301
  // Accepting \n (two chars) as an alternative to the newline character
27121
27302
  // (sometimes, '\n' is not converted to newline, e.g. in a Makefile)
27122
27303
  // Also accepting <br>
27123
27304
  var labelNewlineRxp = /\n|\\n|<br>/i;
27124
27305
 
27306
+ // A line break that the GUI's wrapper put in a fixed-width text block, as
27307
+ // against one the user typed. It consumes no character: the space or hyphen
27308
+ // the line broke at stays in the text before it, so removing every soft break
27309
+ // gives back exactly what was typed. See docs/development/text-annotation-design.md.
27310
+ var LABEL_SOFT_BREAK = '<wbr>';
27311
+
27312
+ // Every form of line break, captured, so that a split keeps the delimiters and
27313
+ // a soft break can be told from a hard one.
27314
+ var anyLabelBreakRxp = /(\n|\\n|<br>|<wbr>)/i;
27315
+ var softBreakRxp = /^<wbr>$/i;
27316
+
27317
+ // The lines a label's text is drawn as. Whitespace before a soft break is
27318
+ // dropped: a trailing space would move an end- or middle-justified line by a
27319
+ // space's width.
27320
+ function splitLabelLines(text) {
27321
+ var parts = String(text).split(anyLabelBreakRxp);
27322
+ var lines = [parts[0]];
27323
+ for (var i = 1; i < parts.length; i += 2) {
27324
+ if (softBreakRxp.test(parts[i])) {
27325
+ lines[lines.length - 1] = lines[lines.length - 1].replace(/\s+$/, '');
27326
+ }
27327
+ lines.push(parts[i + 1]);
27328
+ }
27329
+ return lines;
27330
+ }
27331
+
27332
+ function removeSoftBreaks(text) {
27333
+ return String(text).replace(/<wbr>/gi, '');
27334
+ }
27335
+
27125
27336
  // The size a label is drawn at when it carries none of its own, which is the
27126
27337
  // size its layer's group supplies (see getLabelTextDefaults()). Shared with
27127
27338
  // that function rather than written twice: the em offsets the label positions
@@ -27143,7 +27354,7 @@
27143
27354
  var rec = resolveLabelPosition(recArg);
27144
27355
  var o = renderLabel$1(rec);
27145
27356
  applyStyleAttributes(o, 'label', rec);
27146
- return o;
27357
+ return applyLabelHalo(o, rec);
27147
27358
  }
27148
27359
 
27149
27360
  function renderLabel$1(recArg) {
@@ -27151,9 +27362,9 @@
27151
27362
  // already been through it, so calling it here as well costs nothing and means
27152
27363
  // every way into the renderer draws a label in the position it is stored in.
27153
27364
  var rec = resolveLabelPosition(recArg);
27154
- var line = toLabelString(rec['label-text']);
27155
- var morelines, obj;
27156
- var newline = labelNewlineRxp;
27365
+ var morelines = splitLabelLines(toLabelString(rec['label-text']));
27366
+ var line = morelines.shift();
27367
+ var obj;
27157
27368
  var dx = applyAlignmentShift(rec);
27158
27369
  var dy = rec.dy || 0;
27159
27370
  var properties = {
@@ -27162,16 +27373,12 @@
27162
27373
  y: dy,
27163
27374
  x: dx
27164
27375
  };
27165
- if (newline.test(line)) {
27166
- morelines = line.split(newline);
27167
- line = morelines.shift();
27168
- }
27169
27376
  obj = {
27170
27377
  tag: 'text',
27171
27378
  value: line,
27172
27379
  properties: properties
27173
27380
  };
27174
- if (morelines) {
27381
+ if (morelines.length > 0) {
27175
27382
  // multiline label
27176
27383
  obj.children = [];
27177
27384
  morelines.forEach(function(line) {
@@ -27274,11 +27481,598 @@
27274
27481
  var SvgLabels = /*#__PURE__*/Object.freeze({
27275
27482
  __proto__: null,
27276
27483
  DEFAULT_LABEL_FONT_SIZE: DEFAULT_LABEL_FONT_SIZE,
27484
+ LABEL_SOFT_BREAK: LABEL_SOFT_BREAK,
27277
27485
  getDrawnLabelOffset: getDrawnLabelOffset,
27278
27486
  labelNewlineRxp: labelNewlineRxp,
27487
+ removeSoftBreaks: removeSoftBreaks,
27279
27488
  renderLabel: renderLabel$1,
27280
27489
  renderStyledLabel: renderStyledLabel,
27281
- toLabelString: toLabelString
27490
+ splitLabelLines: splitLabelLines,
27491
+ toLabelString: toLabelString,
27492
+ toPixels: toPixels
27493
+ });
27494
+
27495
+ // A callout: a line from a label's anchor to its text, straight, elbowed or
27496
+ // curved, optionally ending in an arrowhead at the anchor. A dot at the anchor
27497
+ // is the label's icon, not a callout marker.
27498
+ //
27499
+ // Everything here is in the label's own space -- origin at the anchor, y down,
27500
+ // in px -- which is the space dx/dy are in and the one the GUI scales with the
27501
+ // frame. Nothing derived is stored: the shape is worked out from the anchor,
27502
+ // the text box and the callout-* fields every time the label is drawn.
27503
+ //
27504
+ // See docs/development/text-annotation-design.md.
27505
+
27506
+ var DEFAULT_PADDING$1 = 3;
27507
+ var DEFAULT_LINE_WIDTH = 1;
27508
+ var DEFAULT_LINE_HEIGHT = '1.1em';
27509
+ // Space left between the anchor's symbol and the end of the line
27510
+ var SYMBOL_CLEARANCE = 2;
27511
+ // How far an automatic curve bows out, as a fraction of its chord
27512
+ var CURVE_BEND = 0.2;
27513
+ // The shortest first leg an automatic elbow into the top or bottom of the text
27514
+ // is drawn with, px
27515
+ var MIN_LEG = 8;
27516
+ // The angle at an arrowhead's point, degrees. The open one is wider, since a
27517
+ // narrow chevron drawn with a line reads as a thickened line more than as an
27518
+ // arrow.
27519
+ var ARROW_ANGLE = 44;
27520
+ var OPEN_ARROW_ANGLE = 70;
27521
+ // How far into a solid arrowhead the line reaches, as a fraction of the
27522
+ // head's length: far enough that its cap is hidden, short of the tip.
27523
+ var ARROW_LINE_OVERLAP = 0.7;
27524
+ // Where glyphs sit relative to their baseline, in ems, for estimating the
27525
+ // height of a block of text that is measured only for its width
27526
+ var ASCENT = 0.8;
27527
+ var DESCENT = 0.2;
27528
+ var MIDLINE = 0.35;
27529
+
27530
+ function labelHasCallout(rec) {
27531
+ var type = rec && rec.callout ? parseCalloutType(rec.callout) : null;
27532
+ return !!type && type != 'none';
27533
+ }
27534
+
27535
+ // The callout for @rec as an SVG object, or null if it has none or there is no
27536
+ // room to draw one.
27537
+ // @symbolRadius: radius of the symbol drawn at the anchor, or 0; the default
27538
+ // gap keeps the line clear of it
27539
+ function renderLabelCallout(rec, symbolRadius) {
27540
+ var shape = getLabelCalloutShape(rec, symbolRadius);
27541
+ return shape ? renderCalloutShape(shape, rec) : null;
27542
+ }
27543
+
27544
+ // The shape renderLabelCallout() draws for @rec, from getCalloutShape(), or
27545
+ // null. The GUI places its handles on this, so they sit where the line is.
27546
+ function getLabelCalloutShape(rec, symbolRadius) {
27547
+ if (!labelHasCallout(rec)) return null;
27548
+ return getCalloutShape({
27549
+ type: parseCalloutType(rec.callout),
27550
+ end: getCalloutEndType(rec),
27551
+ box: getLabelTextBox(rec),
27552
+ via: parsePointPair(rec['callout-via'] || ''),
27553
+ attach: parsePointPair(rec['callout-attach'] || ''),
27554
+ padding: getNumber(rec['callout-padding'], DEFAULT_PADDING$1),
27555
+ gap: getCalloutGap(rec, symbolRadius),
27556
+ width: getLineWidth(rec),
27557
+ endSize: getNumber(rec['callout-end-size'], 0)
27558
+ });
27559
+ }
27560
+
27561
+ // An arrowhead's size when callout-end-size does not say, in px: 10 for the
27562
+ // default line, and growing with the line, so that a heavier line does not end
27563
+ // in a head too small to read.
27564
+ function getDefaultCalloutEndSize(end, lineWidth) {
27565
+ var w = lineWidth > 0 ? lineWidth : DEFAULT_LINE_WIDTH;
27566
+ return 7 + 3 * w;
27567
+ }
27568
+
27569
+ // 'arrow', 'open-arrow' or 'none'
27570
+ function getCalloutEndType(rec) {
27571
+ return rec && parseCalloutEnd(rec['callout-end'] || '') || 'none';
27572
+ }
27573
+
27574
+ // How far short of the anchor the line stops. Unset, an arrowhead stops clear
27575
+ // of the symbol there, so that its point reads against the background; a
27576
+ // plain line runs to the symbol's edge, so that the two read as one object.
27577
+ // The line's round cap then reaches under the symbol, which is drawn over it.
27578
+ function getCalloutGap(rec, symbolRadius) {
27579
+ if (isSvgNumber(rec['callout-gap'])) return Math.max(0, Number(rec['callout-gap']));
27580
+ if (!(symbolRadius > 0)) return 0;
27581
+ return getCalloutEndType(rec) == 'none' ? symbolRadius : symbolRadius + SYMBOL_CLEARANCE;
27582
+ }
27583
+
27584
+ // The block of text a label draws, in the label's space: {xmin, ymin, xmax,
27585
+ // ymax, midline}, where midline is the middle of the first line's capitals.
27586
+ //
27587
+ // The width is measured, since it depends on the font, and the height is
27588
+ // estimated from the font size and line height, since only the width is. With
27589
+ // no measurement the box takes the label's width if it is a text block, and is
27590
+ // otherwise a vertical line through the text's origin -- a callout then meets
27591
+ // the text where it starts rather than not being drawn.
27592
+ function getLabelTextBox(rec) {
27593
+ var fontSize = getFontSizeInPx$1(rec);
27594
+ var offset = getDrawnLabelOffset(rec);
27595
+ var lines = splitLabelLines(toLabelString(rec['label-text']));
27596
+ var lineHeight = measureToPx(rec['line-height'] || DEFAULT_LINE_HEIGHT, fontSize);
27597
+ var width = getMeasuredTextWidth(rec) || getNumber(rec['label-width'], 0);
27598
+ // An anchor the record does not set is inherited from the layer's group,
27599
+ // which is 'middle' -- see getLabelTextDefaults().
27600
+ var anchor = offset['text-anchor'] || 'middle';
27601
+ var xmin = offset.dx - width * (anchor == 'middle' ? 0.5 : anchor == 'end' ? 1 : 0);
27602
+ var baseline = offset.dy + getBaselineShift(rec['dominant-baseline']) * fontSize;
27603
+ if (!(lineHeight > 0)) lineHeight = fontSize * 1.1;
27604
+ return {
27605
+ xmin: xmin,
27606
+ xmax: xmin + width,
27607
+ ymin: baseline - ASCENT * fontSize,
27608
+ ymax: baseline + (lines.length - 1) * lineHeight + DESCENT * fontSize,
27609
+ midline: baseline - MIDLINE * fontSize
27610
+ };
27611
+ }
27612
+
27613
+ // Where a line meets a callout's text and how it gets there from the anchor.
27614
+ //
27615
+ // o.type 'line', 'elbow' or 'curve'
27616
+ // o.end 'arrow' (filled), 'open-arrow' (stroked) or 'none'
27617
+ // o.box the text box, from getLabelTextBox()
27618
+ // o.via [x, y] the elbow's corner or a point the curve passes through, or null
27619
+ // o.attach [fx, fy] where the line meets the text, as fractions of the padded
27620
+ // box, or null
27621
+ // o.padding clearance around the text, px
27622
+ // o.gap how far short of the anchor the line stops, px
27623
+ // o.width line width, px
27624
+ // o.endSize an arrowhead's size, px, or 0 for the default, which grows with
27625
+ // the line width. The size is the length of the head's sides, as
27626
+ // drawn, stroke and all: the two styles are different shapes, and
27627
+ // what makes one look the size of the other is sides the same
27628
+ // length, not the same length along the line.
27629
+ //
27630
+ // Returns {kind, coords, head, openHead, box, attach, via, tip} with coords
27631
+ // from the anchor end to the text end -- a polyline's vertices, or a quadratic
27632
+ // Bezier's three points -- or null when there is nothing to draw: the anchor
27633
+ // inside the padded box, or a gap that leaves no line. head is a filled
27634
+ // triangle, openHead the three points of a stroked chevron, wing to tip to
27635
+ // wing. The rest is what the shape was worked out from: the padded box, the
27636
+ // point where the line meets it, the elbow's corner or the curve's midpoint
27637
+ // (null for a straight line), and where the line stops short of the anchor.
27638
+ function getCalloutShape(o) {
27639
+ var box = padBox(o.box, o.padding || 0);
27640
+ var a = [0, 0];
27641
+ var size = o.endSize > 0 ? o.endSize : getDefaultCalloutEndSize(o.end, o.width);
27642
+ var t, v, path, tip, dir, out, lineWidth, headLen, curved;
27643
+ if (boxContains(box, a)) return null;
27644
+ t = o.attach ? getBoxPoint(box, o.attach) :
27645
+ getAutoAttachment(o.type, box, o.via || null);
27646
+ // A leg too short to read as one, or to hold the marker, is a jog: the line
27647
+ // goes straight up or down to the text instead.
27648
+ if (o.type == 'elbow' && !o.attach && !o.via && isOnTopOrBottom(box, t) &&
27649
+ Math.abs(t[0]) < getMinimumLeg(o.end, size)) {
27650
+ t = [0, t[1]];
27651
+ }
27652
+ if (o.type == 'elbow') {
27653
+ v = o.via || getAutoElbow(a, t, box);
27654
+ // A corner on either end is no corner -- straight under the text, say --
27655
+ // and a leg of no length has no direction for an arrowhead to take.
27656
+ path = {kind: 'polyline', coords: distance(a, v) > 0 && distance(v, t) > 0 ?
27657
+ [a, v, t] : [a, t]};
27658
+ } else if (o.type == 'curve') {
27659
+ v = o.via || getAutoBend(a, t);
27660
+ path = {kind: 'bezier', coords: [a, getControlPoint(a, v, t), t]};
27661
+ } else {
27662
+ path = {kind: 'polyline', coords: [a, t]};
27663
+ }
27664
+ path = trimPath(path, o.gap || 0);
27665
+ if (!path) return null;
27666
+ tip = path.coords[0];
27667
+ out = {kind: path.kind, coords: path.coords, head: null, openHead: null,
27668
+ box: box, attach: t, via: v || null, tip: tip};
27669
+ if (o.end == 'arrow') {
27670
+ headLen = getArrowHeadLength(size, ARROW_ANGLE);
27671
+ curved = path.kind == 'bezier' ? getCurveIntoArrowHead(path, headLen) : null;
27672
+ dir = curved ? curved.dir : getStartDirection(path);
27673
+ if (!dir) return null;
27674
+ out.head = getArrowHead(tip, dir, size, ARROW_ANGLE);
27675
+ // The line stops inside the head, so that its cap does not show past the
27676
+ // tip. A line too short to reach past the head is drawn as the head alone.
27677
+ path = curved ? curved.path : trimPath(path, headLen * ARROW_LINE_OVERLAP);
27678
+ out.coords = path ? path.coords : null;
27679
+ } else if (o.end == 'open-arrow') {
27680
+ // Stroked with the line's round join, which reaches half a line width past
27681
+ // the chevron's point: the point is set back by that much, so that the
27682
+ // arrow ends where a filled one would. The join and the round cap at the
27683
+ // far end each add half a line width to an arm, which is taken off it.
27684
+ lineWidth = o.width > 0 ? o.width : DEFAULT_LINE_WIDTH;
27685
+ path = trimPath(path, lineWidth / 2) || path;
27686
+ dir = getStartDirection(path);
27687
+ if (!dir) return null;
27688
+ out.coords = path.coords;
27689
+ out.openHead = getArrowHead(path.coords[0], dir,
27690
+ Math.max(size - lineWidth, size / 2), OPEN_ARROW_ANGLE);
27691
+ out.openHead = [out.openHead[1], out.openHead[0], out.openHead[2]];
27692
+ }
27693
+ return out;
27694
+ }
27695
+
27696
+ function renderCalloutShape(shape, rec) {
27697
+ var color = rec['callout-color'] || rec.fill || 'black';
27698
+ var opacity = isSvgNumber(rec['callout-opacity']) ? Number(rec['callout-opacity']) :
27699
+ isSvgNumber(rec.opacity) ? Number(rec.opacity) : 1;
27700
+ var props = {class: 'label-callout'};
27701
+ var children = [];
27702
+ if (shape.coords) {
27703
+ children.push({
27704
+ tag: 'path',
27705
+ properties: {
27706
+ d: getPathData(shape),
27707
+ fill: 'none',
27708
+ stroke: color,
27709
+ 'stroke-width': getLineWidth(rec),
27710
+ 'stroke-linecap': 'round',
27711
+ 'stroke-linejoin': 'round'
27712
+ }
27713
+ });
27714
+ }
27715
+ if (shape.head) {
27716
+ children.push({
27717
+ tag: 'path',
27718
+ properties: {d: getPolygonData(shape.head), fill: color}
27719
+ });
27720
+ }
27721
+ if (shape.openHead) {
27722
+ children.push({
27723
+ tag: 'path',
27724
+ properties: {
27725
+ d: 'M ' + shape.openHead.map(formatPoint).join(' L '),
27726
+ fill: 'none',
27727
+ stroke: color,
27728
+ 'stroke-width': getLineWidth(rec),
27729
+ 'stroke-linecap': 'round',
27730
+ 'stroke-linejoin': 'round'
27731
+ }
27732
+ });
27733
+ }
27734
+ if (opacity < 1) props.opacity = opacity;
27735
+ return {tag: 'g', properties: props, children: children};
27736
+ }
27737
+
27738
+ // Drawn from the text to the anchor, which is the direction a leader is read in
27739
+ // and the one a dash pattern starts from. A quadratic's control point is the
27740
+ // same in both directions.
27741
+ function getPathData(shape) {
27742
+ var c = shape.coords;
27743
+ if (shape.kind == 'bezier') {
27744
+ return 'M ' + formatPoint(c[2]) + ' Q ' + formatPoint(c[1]) + ' ' + formatPoint(c[0]);
27745
+ }
27746
+ return 'M ' + c.slice().reverse().map(formatPoint).join(' L ');
27747
+ }
27748
+
27749
+ function getPolygonData(coords) {
27750
+ return 'M ' + coords.map(formatPoint).join(' L ') + ' Z';
27751
+ }
27752
+
27753
+ function formatPoint(p) {
27754
+ return roundToTenths(p[0]) + ' ' + roundToTenths(p[1]);
27755
+ }
27756
+
27757
+ // Attachment
27758
+
27759
+ function getBoxPoint(box, f) {
27760
+ var fx = Math.max(0, Math.min(1, f[0]));
27761
+ var fy = Math.max(0, Math.min(1, f[1]));
27762
+ return [box.xmin + fx * (box.xmax - box.xmin), box.ymin + fy * (box.ymax - box.ymin)];
27763
+ }
27764
+
27765
+ // An elbow meets the side of the text facing the way it comes from, level with
27766
+ // the first line, which is the usual form of an elbowed leader. A straight or
27767
+ // curved line is aimed at the middle of the block and stops at its edge.
27768
+ //
27769
+ // An elbow coming from directly above or below the text cannot reach a side
27770
+ // without crossing the block, so it meets the facing edge instead: in the
27771
+ // middle when it comes from the anchor, and straight above or below a via
27772
+ // point, whose landing is then vertical.
27773
+ function getAutoAttachment(type, box, via) {
27774
+ var cx = (box.xmin + box.xmax) / 2;
27775
+ var cy = (box.ymin + box.ymax) / 2;
27776
+ var from = via || [0, 0];
27777
+ if (type == 'elbow' && isAboveOrBelow(box, from)) {
27778
+ return [via ? from[0] : cx, from[1] > box.ymax ? box.ymax : box.ymin];
27779
+ }
27780
+ if (type == 'elbow') {
27781
+ return [from[0] < cx ? box.xmin : box.xmax,
27782
+ Math.max(box.ymin, Math.min(box.ymax, box.midline))];
27783
+ }
27784
+ return clipToBox(from, [cx, cy], box);
27785
+ }
27786
+
27787
+ function getMinimumLeg(end, size) {
27788
+ return end == 'none' ? MIN_LEG : Math.max(MIN_LEG, size * 1.5);
27789
+ }
27790
+
27791
+ function isAboveOrBelow(box, p) {
27792
+ return p[0] >= box.xmin && p[0] <= box.xmax && (p[1] > box.ymax || p[1] < box.ymin);
27793
+ }
27794
+
27795
+ // Whether @t is on the top or bottom edge of @box, away from its corners
27796
+ function isOnTopOrBottom(box, t) {
27797
+ return t[0] > box.xmin && t[0] < box.xmax && (t[1] == box.ymin || t[1] == box.ymax);
27798
+ }
27799
+
27800
+ // Where the segment from @p (outside the box) to @c (inside it) enters the box.
27801
+ function clipToBox(p, c, box) {
27802
+ var dx = c[0] - p[0];
27803
+ var dy = c[1] - p[1];
27804
+ var s;
27805
+ if (boxContains(box, p)) return c;
27806
+ s = Math.max(0, getEntry(p[0], dx, box.xmin, box.xmax),
27807
+ getEntry(p[1], dy, box.ymin, box.ymax));
27808
+ return [p[0] + s * dx, p[1] + s * dy];
27809
+ }
27810
+
27811
+ function getEntry(p, d, min, max) {
27812
+ if (d === 0) return -Infinity;
27813
+ return Math.min((min - p) / d, (max - p) / d);
27814
+ }
27815
+
27816
+ // Shapes
27817
+
27818
+ // The segment from the anchor rises at 45 degrees to meet a horizontal landing
27819
+ // into the side of the text, or goes straight up or down when the text is too
27820
+ // close horizontally for that.
27821
+ //
27822
+ // Into the top or bottom of the text, the landing is vertical and the line
27823
+ // leaves the anchor horizontally, which keeps the corner clear of the block.
27824
+ function getAutoElbow(a, t, box) {
27825
+ var dx = t[0] - a[0];
27826
+ var dy = Math.abs(t[1] - a[1]);
27827
+ if (isOnTopOrBottom(box, t)) return [t[0], a[1]];
27828
+ if (Math.abs(dx) <= dy) return [a[0], t[1]];
27829
+ return [a[0] + (dx > 0 ? dy : -dy), t[1]];
27830
+ }
27831
+
27832
+ // The chord's midpoint, pushed to its left as seen from the anchor -- which
27833
+ // bows a callout to text on its right upward.
27834
+ function getAutoBend(a, t) {
27835
+ var dx = t[0] - a[0];
27836
+ var dy = t[1] - a[1];
27837
+ return [(a[0] + t[0]) / 2 + dy * CURVE_BEND, (a[1] + t[1]) / 2 - dx * CURVE_BEND];
27838
+ }
27839
+
27840
+ // The control point of the quadratic from @a to @t that passes through @v at
27841
+ // its midpoint. That is also the curve's farthest point from its chord, so a
27842
+ // handle at @v sits where the bend visibly is.
27843
+ function getControlPoint(a, v, t) {
27844
+ return [2 * v[0] - (a[0] + t[0]) / 2, 2 * v[1] - (a[1] + t[1]) / 2];
27845
+ }
27846
+
27847
+ // Trimming
27848
+
27849
+ // @path with its start moved to where it leaves a circle of radius @r around
27850
+ // that start, or null if it never does. A circle rather than a length along the
27851
+ // path, because what the gap has to clear is a symbol drawn at the anchor.
27852
+ function trimPath(path, r) {
27853
+ if (!(r > 0)) return hasLength(path) ? path : null;
27854
+ if (path.kind == 'bezier') return trimBezier(path.coords, r);
27855
+ return trimPolyline(path.coords, r);
27856
+ }
27857
+
27858
+ function hasLength(path) {
27859
+ var c = path.coords;
27860
+ for (var i = 1; i < c.length; i++) {
27861
+ if (distance(c[0], c[i]) > 0) return true;
27862
+ }
27863
+ return false;
27864
+ }
27865
+
27866
+ function trimPolyline(coords, r) {
27867
+ var p0 = coords[0];
27868
+ for (var i = 1; i < coords.length; i++) {
27869
+ if (distance(p0, coords[i]) > r) {
27870
+ return {
27871
+ kind: 'polyline',
27872
+ coords: [getCircleExit(p0, r, coords[i - 1], coords[i])].concat(coords.slice(i))
27873
+ };
27874
+ }
27875
+ }
27876
+ return null;
27877
+ }
27878
+
27879
+ // The point where segment @p-@q, which starts inside the circle and ends
27880
+ // outside it, crosses it.
27881
+ function getCircleExit(c, r, p, q) {
27882
+ var dx = q[0] - p[0];
27883
+ var dy = q[1] - p[1];
27884
+ var ex = p[0] - c[0];
27885
+ var ey = p[1] - c[1];
27886
+ var a = dx * dx + dy * dy;
27887
+ var b = 2 * (dx * ex + dy * ey);
27888
+ var k = ex * ex + ey * ey - r * r;
27889
+ var s = (-b + Math.sqrt(Math.max(0, b * b - 4 * a * k))) / (2 * a);
27890
+ s = Math.max(0, Math.min(1, s));
27891
+ return [p[0] + s * dx, p[1] + s * dy];
27892
+ }
27893
+
27894
+ function trimBezier(c, r) {
27895
+ var steps = 64;
27896
+ var lo = 0, hi = -1, mid, i;
27897
+ for (i = 1; i <= steps; i++) {
27898
+ if (distance(c[0], getBezierPoint(c, i / steps)) > r) {
27899
+ hi = i / steps;
27900
+ lo = (i - 1) / steps;
27901
+ break;
27902
+ }
27903
+ }
27904
+ if (hi < 0) return null;
27905
+ for (i = 0; i < 24; i++) {
27906
+ mid = (lo + hi) / 2;
27907
+ if (distance(c[0], getBezierPoint(c, mid)) > r) hi = mid;
27908
+ else lo = mid;
27909
+ }
27910
+ return {kind: 'bezier', coords: splitBezier(c, hi)};
27911
+ }
27912
+
27913
+ function getBezierPoint(c, t) {
27914
+ var u = 1 - t;
27915
+ return [
27916
+ u * u * c[0][0] + 2 * u * t * c[1][0] + t * t * c[2][0],
27917
+ u * u * c[0][1] + 2 * u * t * c[1][1] + t * t * c[2][1]
27918
+ ];
27919
+ }
27920
+
27921
+ // The part of the quadratic @c from @t to its end
27922
+ function splitBezier(c, t) {
27923
+ return [getBezierPoint(c, t), lerp(c[1], c[2], t), c[2]];
27924
+ }
27925
+
27926
+ // End markers
27927
+
27928
+ // Unit vector from the start of @path into it, or null for a path with no
27929
+ // direction there.
27930
+ function getStartDirection(path) {
27931
+ var c = path.coords;
27932
+ var next = c[1];
27933
+ if (path.kind == 'bezier' && distance(c[0], c[1]) === 0) next = c[2];
27934
+ return getUnitVector(c[0], next);
27935
+ }
27936
+
27937
+ // A solid arrowhead on a curve, and the curve that runs into it:
27938
+ // {dir, path}, or null if the curve does not reach past the head.
27939
+ //
27940
+ // Pointed along the curve's tangent at the tip, the head faces off to one side
27941
+ // of where the line goes on a bend, and the line cut back along the curve then
27942
+ // meets the back of the head off-centre and at a slant. So the head points
27943
+ // along the chord from its tip to where the curve leaves it, which puts that
27944
+ // point at the middle of its base, and the curve is re-fitted to leave the
27945
+ // head along the same line: it starts at the middle of the base, heading
27946
+ // along the head's axis, and keeps its own direction into the text. The
27947
+ // control point that does both is where those two lines cross. Starting on
27948
+ // the base rather than inside the head keeps the line centred exactly, since
27949
+ // a curve bends away from its starting direction; its round cap reaches back
27950
+ // into the head, where it is hidden.
27951
+ //
27952
+ // path: a quadratic Bezier from the anchor end; len: the head's length
27953
+ function getCurveIntoArrowHead(path, len) {
27954
+ var tip = path.coords[0];
27955
+ var rest = trimBezier(path.coords, len);
27956
+ var dir, start, end, ctrl;
27957
+ if (!rest) return null;
27958
+ start = rest.coords[0];
27959
+ dir = getUnitVector(tip, start);
27960
+ if (!dir) return null;
27961
+ end = rest.coords[2];
27962
+ ctrl = intersectRays(start, dir, end, getUnitVector(end, rest.coords[1]));
27963
+ // A curve that doubles back, or runs straight on: a control point on the
27964
+ // head's axis still takes the line out of the head straight.
27965
+ if (!ctrl) ctrl = [start[0] + dir[0] * distance(start, end) / 2,
27966
+ start[1] + dir[1] * distance(start, end) / 2];
27967
+ return {dir: dir, path: {kind: 'bezier', coords: [start, ctrl, end]}};
27968
+ }
27969
+
27970
+ // Where the ray from @p along @d meets the ray from @q along @e, or null if
27971
+ // they do not meet ahead of both
27972
+ function intersectRays(p, d, q, e) {
27973
+ var cross, s, u, qx, qy;
27974
+ if (!d || !e) return null;
27975
+ cross = d[0] * e[1] - d[1] * e[0];
27976
+ if (Math.abs(cross) < 1e-9) return null;
27977
+ qx = q[0] - p[0];
27978
+ qy = q[1] - p[1];
27979
+ s = (qx * e[1] - qy * e[0]) / cross;
27980
+ u = (qx * d[1] - qy * d[0]) / cross;
27981
+ return s > 0 && u > 0 ? [p[0] + d[0] * s, p[1] + d[1] * s] : null;
27982
+ }
27983
+
27984
+ // [tip, wing, wing], for a head with sides @side long meeting at @angle degrees
27985
+ function getArrowHead(tip, dir, side, angle) {
27986
+ var len = getArrowHeadLength(side, angle);
27987
+ var half = side * Math.sin(angle / 2 * Math.PI / 180);
27988
+ var bx = tip[0] + dir[0] * len;
27989
+ var by = tip[1] + dir[1] * len;
27990
+ return [tip, [bx - dir[1] * half, by + dir[0] * half], [bx + dir[1] * half, by - dir[0] * half]];
27991
+ }
27992
+
27993
+ // How far along the line a head with sides @side long reaches
27994
+ function getArrowHeadLength(side, angle) {
27995
+ return side * Math.cos(angle / 2 * Math.PI / 180);
27996
+ }
27997
+
27998
+ // Utilities
27999
+
28000
+ function padBox(box, pad) {
28001
+ return {
28002
+ xmin: box.xmin - pad,
28003
+ xmax: box.xmax + pad,
28004
+ ymin: box.ymin - pad,
28005
+ ymax: box.ymax + pad,
28006
+ midline: box.midline
28007
+ };
28008
+ }
28009
+
28010
+ function boxContains(box, p) {
28011
+ return p[0] > box.xmin && p[0] < box.xmax && p[1] > box.ymin && p[1] < box.ymax;
28012
+ }
28013
+
28014
+ function getUnitVector(p, q) {
28015
+ var dx = q[0] - p[0];
28016
+ var dy = q[1] - p[1];
28017
+ var len = Math.sqrt(dx * dx + dy * dy);
28018
+ return len > 0 ? [dx / len, dy / len] : null;
28019
+ }
28020
+
28021
+ function distance(p, q) {
28022
+ var dx = q[0] - p[0];
28023
+ var dy = q[1] - p[1];
28024
+ return Math.sqrt(dx * dx + dy * dy);
28025
+ }
28026
+
28027
+ function lerp(p, q, t) {
28028
+ return [p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t];
28029
+ }
28030
+
28031
+ function getNumber(val, defaultVal) {
28032
+ return isSvgNumber(val) && Number(val) >= 0 ? Number(val) : defaultVal;
28033
+ }
28034
+
28035
+ function getLineWidth(rec) {
28036
+ var w = getNumber(rec['callout-width'], DEFAULT_LINE_WIDTH);
28037
+ return w > 0 ? w : DEFAULT_LINE_WIDTH;
28038
+ }
28039
+
28040
+ function getFontSizeInPx$1(rec) {
28041
+ var val = rec['font-size'];
28042
+ var px = val === undefined || val === null || val === '' ? null :
28043
+ measureToPx(val, DEFAULT_LABEL_FONT_SIZE);
28044
+ return px > 0 ? px : DEFAULT_LABEL_FONT_SIZE;
28045
+ }
28046
+
28047
+ // px for a number, a px value or an em value, or null
28048
+ function measureToPx(val, emBasis) {
28049
+ var px = toPixels(val, emBasis);
28050
+ var match;
28051
+ if (px !== null) return px;
28052
+ match = /^(-?[.0-9]+)px$/.exec(String(val).trim());
28053
+ return match ? Number(match[1]) : null;
28054
+ }
28055
+
28056
+ // Where the first baseline sits below the text's y, in ems
28057
+ function getBaselineShift(baseline) {
28058
+ var val = String(baseline || '').toLowerCase();
28059
+ if (val == 'central' || val == 'middle') return MIDLINE;
28060
+ if (val == 'hanging' || val == 'text-before-edge') return ASCENT;
28061
+ if (val == 'text-after-edge' || val == 'ideographic') return -DESCENT;
28062
+ return 0;
28063
+ }
28064
+
28065
+ var SvgLabelCallout = /*#__PURE__*/Object.freeze({
28066
+ __proto__: null,
28067
+ getCalloutEndType: getCalloutEndType,
28068
+ getCalloutGap: getCalloutGap,
28069
+ getCalloutShape: getCalloutShape,
28070
+ getControlPoint: getControlPoint,
28071
+ getDefaultCalloutEndSize: getDefaultCalloutEndSize,
28072
+ getLabelCalloutShape: getLabelCalloutShape,
28073
+ getLabelTextBox: getLabelTextBox,
28074
+ labelHasCallout: labelHasCallout,
28075
+ renderLabelCallout: renderLabelCallout
27282
28076
  });
27283
28077
 
27284
28078
  // convert data records (properties like svg-symbol, label-text, fill, r) to svg symbols
@@ -27307,7 +28101,13 @@
27307
28101
  // render label and/or point symbol
27308
28102
  function renderPoint(rec) {
27309
28103
  var children = [];
28104
+ var callout;
27310
28105
  // var halfSize = rec.r || 0; // radius or half of symbol size
28106
+ // Beneath the symbol and the text, which it runs between.
28107
+ if (featureHasLabel(rec) && labelHasCallout(rec)) {
28108
+ callout = renderLabelCallout(rec, getAnchorSymbolRadius(rec));
28109
+ if (callout) children.push(callout);
28110
+ }
27311
28111
  if (featureHasSvgSymbol(rec)) {
27312
28112
  children.push(renderSymbol(rec));
27313
28113
  }
@@ -27333,6 +28133,15 @@
27333
28133
  return empty();
27334
28134
  }
27335
28135
 
28136
+ // How far the symbol at a label's anchor reaches from it, which a callout's
28137
+ // default gap clears. A composite svg-symbol has no single radius, and gets no
28138
+ // clearance.
28139
+ function getAnchorSymbolRadius(d) {
28140
+ if (!featureHasSvgSymbol(d) || d['svg-symbol']) return 0;
28141
+ if (featureHasIcon(d)) return getIconRadius(d, d.icon || 'circle');
28142
+ return d.r > 0 ? d.r : 0;
28143
+ }
28144
+
27336
28145
  function featureHasIcon(d) {
27337
28146
  return !!(d && (d.icon || d['icon-size'] || (d['icon-color'] && d.r > 0)));
27338
28147
  }
@@ -27557,6 +28366,7 @@
27557
28366
 
27558
28367
  var SvgSymbols = /*#__PURE__*/Object.freeze({
27559
28368
  __proto__: null,
28369
+ getAnchorSymbolRadius: getAnchorSymbolRadius,
27560
28370
  getTransform: getTransform,
27561
28371
  renderPoint: renderPoint,
27562
28372
  symbolRenderers: symbolRenderers
@@ -28518,7 +29328,9 @@
28518
29328
  }
28519
29329
  cls = LABEL_OVERFLOW_CLASS;
28520
29330
  }
28521
- text = toLabelString(rec['label-text']);
29331
+ // A soft break is wrapping for a width, which text along a path does not
29332
+ // have, and removing it leaves the space it broke at.
29333
+ text = removeSoftBreaks(toLabelString(rec['label-text']));
28522
29334
  if (labelNewlineRxp.test(text)) {
28523
29335
  // a <tspan> inside a <textPath> advances along the path instead of
28524
29336
  // stacking below it, so the lines are joined rather than dropping
@@ -28546,6 +29358,7 @@
28546
29358
  if (rec.dy) textPath.properties.dy = rec.dy;
28547
29359
  o = {tag: 'text', properties: {}, children: [textPath]};
28548
29360
  applyStyleAttributes(o, 'label', rec);
29361
+ applyLabelHalo(o, rec);
28549
29362
  if (cls) {
28550
29363
  o.properties.class = o.properties.class ? o.properties.class + ' ' + cls : cls;
28551
29364
  }
@@ -28842,6 +29655,11 @@
28842
29655
  // are applied to the <g> container, 'r' property is applied to circle
28843
29656
  applyStyleAttributes(svgObj, msType, d, simpleCircleFilter);
28844
29657
  } else ;
29658
+ // Only here, where SVG is written for other programs to read: the GUI
29659
+ // draws the same labels with paint-order and has no use for the copies.
29660
+ if (labelHasHalo(d)) {
29661
+ svgObj = splitLabelHalos(svgObj);
29662
+ }
28845
29663
  if ('id' in obj) {
28846
29664
  if (!svgObj.properties) {
28847
29665
  svgObj.properties = {};
@@ -40585,6 +41403,20 @@ ${svg}
40585
41403
  dy: {describe: 'vertical offset from the anchor'},
40586
41404
  fill: {describe: 'text color'},
40587
41405
  opacity: {describe: 'text opacity'},
41406
+ 'halo-width': {describe: 'width of a halo around the text in px'},
41407
+ 'halo-color': {describe: 'halo color (default is white)'},
41408
+ 'halo-opacity': {describe: 'halo opacity, 0-1'},
41409
+ 'label-width': {describe: 'width of a fixed-width text block in px'},
41410
+ callout: {describe: 'line from the anchor to the text: line, elbow or curve'},
41411
+ 'callout-end': {describe: 'arrowhead at the anchor end: arrow, open-arrow or none'},
41412
+ 'callout-end-size': {describe: 'length of an arrowhead\'s sides in px'},
41413
+ 'callout-via': {describe: 'x,y of an elbow\'s corner or a point on a curve, in px'},
41414
+ 'callout-attach': {describe: 'where the callout meets the text, as x,y fractions'},
41415
+ 'callout-gap': {describe: 'px between the callout and the anchor'},
41416
+ 'callout-padding': {describe: 'px between the callout and the text'},
41417
+ 'callout-color': {describe: 'callout color (defaults to the text color)'},
41418
+ 'callout-width': {describe: 'callout line width in px'},
41419
+ 'callout-opacity': {describe: 'callout opacity, 0-1'},
40588
41420
  css: {describe: 'inline css style'},
40589
41421
  class: {describe: 'name of CSS class or classes (space-separated)'}
40590
41422
  },
@@ -42612,6 +43444,15 @@ ${svg}
42612
43444
  .option('label-align', {
42613
43445
  describe: 'alignment of the lines of multi-line labels; left, center or right'
42614
43446
  })
43447
+ .option('halo-width', {
43448
+ describe: 'width of a halo around label text in px (default is 0, no halo)'
43449
+ })
43450
+ .option('halo-color', {
43451
+ describe: 'color of label halos (default is white)'
43452
+ })
43453
+ .option('halo-opacity', {
43454
+ describe: 'opacity of label halos, 0-1'
43455
+ })
42615
43456
  .option('dx', {
42616
43457
  describe: 'x offset of labels (default is 0)'
42617
43458
  })
@@ -42648,6 +43489,39 @@ ${svg}
42648
43489
  .option('label-start-offset', {
42649
43490
  describe: 'where label text starts along its path, e.g. 50%'
42650
43491
  })
43492
+ .option('label-width', {
43493
+ describe: 'width of a fixed-width text block in px (lines are wrapped in the web UI)'
43494
+ })
43495
+ .option('callout', {
43496
+ describe: 'line from a label\'s anchor to its text: line, elbow or curve'
43497
+ })
43498
+ .option('callout-end', {
43499
+ describe: 'arrowhead at the anchor end of a callout: arrow (solid), open-arrow or none'
43500
+ })
43501
+ .option('callout-end-size', {
43502
+ describe: 'length of an arrowhead\'s sides in px (default 10, more for a thicker line)'
43503
+ })
43504
+ .option('callout-via', {
43505
+ describe: 'x,y of an elbow\'s corner or a point on a curve, in px from the anchor'
43506
+ })
43507
+ .option('callout-attach', {
43508
+ describe: 'where a callout meets its text, as x,y fractions of the text box'
43509
+ })
43510
+ .option('callout-gap', {
43511
+ describe: 'px between a callout and its anchor (default: a line meets the icon, an arrow clears it)'
43512
+ })
43513
+ .option('callout-padding', {
43514
+ describe: 'px between a callout and its text (default is 3)'
43515
+ })
43516
+ .option('callout-color', {
43517
+ describe: 'callout color (defaults to the text color)'
43518
+ })
43519
+ .option('callout-width', {
43520
+ describe: 'callout line width in px (default is 1)'
43521
+ })
43522
+ .option('callout-opacity', {
43523
+ describe: 'callout opacity, 0-1'
43524
+ })
42651
43525
  .option('target', targetOpt);
42652
43526
 
42653
43527
  parser.command('repel')
@@ -82789,7 +83663,7 @@ ${svg}
82789
83663
  return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
82790
83664
  }
82791
83665
 
82792
- var version = "0.7.65";
83666
+ var version = "0.7.67";
82793
83667
 
82794
83668
  // Parse command line args into commands and run them
82795
83669
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
@@ -83613,7 +84487,7 @@ ${svg}
83613
84487
  if (!(fontSize > 0)) return null;
83614
84488
  // The widest line, which is what the block of a multi-line label is as wide
83615
84489
  // as, and what the browser's getBBox() reports for the same text.
83616
- width = text.split(labelNewlineRxp).reduce(function(max, line) {
84490
+ width = splitLabelLines(text).reduce(function(max, line) {
83617
84491
  var w = measureLine(font, line, fontSize, spacing);
83618
84492
  return w > max ? w : max;
83619
84493
  }, 0);
@@ -85391,7 +86265,7 @@ ${svg}
85391
86265
  internal.svg = Object.assign({}, SvgStringify, SvgPathUtils, GeojsonToSvg,
85392
86266
  SvgFeatureUtils,
85393
86267
  SvgLabels, SvgSymbols, SvgLabelPaths, SvgLabelFit, SvgLabelAlign,
85394
- SvgLabelMetrics);
86268
+ SvgLabelMetrics, SvgLabelHalo, SvgLabelCallout);
85395
86269
 
85396
86270
  // Reached through the bundle rather than imported from source, unlike most of
85397
86271
  // what tests use, because these modules load fs and fontkit through the