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/bin/mapshaper-gui +3 -2
- package/mapshaper.js +1755 -433
- package/package.json +1 -1
- package/www/index.html +16 -3
- package/www/mapshaper-gui.js +2450 -447
- package/www/mapshaper.js +1755 -433
- package/www/page.css +363 -115
package/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
|
|
27155
|
-
var morelines
|
|
27156
|
-
var
|
|
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']
|
|
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
|
-
|
|
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
|
|
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.
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
50748
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
52745
|
+
valid = isValidBbox(bbox);
|
|
51086
52746
|
if (fixedAspect) {
|
|
51087
52747
|
fillOutBbox(bbox, fixedAspect, 1);
|
|
51088
52748
|
}
|
|
51089
52749
|
|
|
51090
|
-
//
|
|
51091
|
-
if (
|
|
51092
|
-
|
|
51093
|
-
|
|
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 (
|
|
51110
|
-
width =
|
|
51111
|
-
units =
|
|
51112
|
-
} else if (
|
|
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 =
|
|
51115
|
-
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
|
-
|
|
51126
|
-
|
|
51127
|
-
|
|
51128
|
-
|
|
51129
|
-
|
|
51130
|
-
|
|
51131
|
-
|
|
51132
|
-
|
|
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
|
|
51167
|
-
|
|
51168
|
-
|
|
51169
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|