mapshaper 0.7.72 → 0.7.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mapshaper.js +2958 -773
- package/package.json +6 -1
- package/www/index.html +2 -3
- package/www/mapshaper-gui.js +2509 -146
- package/www/mapshaper.js +2958 -773
- package/www/page.css +447 -35
package/www/mapshaper.js
CHANGED
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
return obj === Object(obj); // via underscore
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
function clamp$
|
|
81
|
+
function clamp$4(val, min, max) {
|
|
82
82
|
return val < min ? min : (val > max ? max : val);
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -721,7 +721,7 @@
|
|
|
721
721
|
function findValueByRank(arr, rank) {
|
|
722
722
|
if (!arr.length || rank < 1 || rank > arr.length) error$1("[findValueByRank()] invalid input");
|
|
723
723
|
|
|
724
|
-
rank = clamp$
|
|
724
|
+
rank = clamp$4(rank | 0, 1, arr.length);
|
|
725
725
|
var k = rank - 1, // conv. rank to array index
|
|
726
726
|
n = arr.length,
|
|
727
727
|
l = 0,
|
|
@@ -1095,7 +1095,7 @@
|
|
|
1095
1095
|
// self-import and the resulting Rollup circular-dependency warning.
|
|
1096
1096
|
var utils = {
|
|
1097
1097
|
addThousandsSep, addslashes, arrayToIndex,
|
|
1098
|
-
clamp: clamp$
|
|
1098
|
+
clamp: clamp$4, cleanNumericString, contains, copyElements, countValues, createBuffer,
|
|
1099
1099
|
defaults, difference,
|
|
1100
1100
|
endsWith, every, expandoBuffer, extend: extend$1, extendBuffer,
|
|
1101
1101
|
find: find$1, findMedian, findQuantile, findRankByValue, findStringPrefix,
|
|
@@ -6581,7 +6581,7 @@
|
|
|
6581
6581
|
return lon;
|
|
6582
6582
|
}
|
|
6583
6583
|
|
|
6584
|
-
function clamp$
|
|
6584
|
+
function clamp$3(value, min, max) {
|
|
6585
6585
|
return Math.max(min, Math.min(max, value));
|
|
6586
6586
|
}
|
|
6587
6587
|
|
|
@@ -7105,21 +7105,21 @@
|
|
|
7105
7105
|
return [
|
|
7106
7106
|
Math.atan2(y * cosDeltaGamma + z * sinDeltaGamma,
|
|
7107
7107
|
x * cosDeltaPhi + k * sinDeltaPhi),
|
|
7108
|
-
Math.asin(clamp$
|
|
7108
|
+
Math.asin(clamp$3(k * cosDeltaPhi - x * sinDeltaPhi, -1, 1))
|
|
7109
7109
|
];
|
|
7110
7110
|
}
|
|
7111
7111
|
k = z * cosDeltaPhi + x * sinDeltaPhi;
|
|
7112
7112
|
return [
|
|
7113
7113
|
Math.atan2(y * cosDeltaGamma - k * sinDeltaGamma,
|
|
7114
7114
|
x * cosDeltaPhi - z * sinDeltaPhi),
|
|
7115
|
-
Math.asin(clamp$
|
|
7115
|
+
Math.asin(clamp$3(k * cosDeltaGamma + y * sinDeltaGamma, -1, 1))
|
|
7116
7116
|
];
|
|
7117
7117
|
}
|
|
7118
7118
|
|
|
7119
7119
|
function interpolateGreatCircle(a, b, interval) {
|
|
7120
7120
|
var av = degreesToVector$2(a[0], a[1]);
|
|
7121
7121
|
var bv = degreesToVector$2(b[0], b[1]);
|
|
7122
|
-
var angle = Math.acos(clamp$
|
|
7122
|
+
var angle = Math.acos(clamp$3(dot$2(av, bv), -1, 1));
|
|
7123
7123
|
var n = Math.max(1, Math.ceil(angle * R2D$7 / interval));
|
|
7124
7124
|
var sinAngle = Math.sin(angle);
|
|
7125
7125
|
var points = [];
|
|
@@ -7178,7 +7178,7 @@
|
|
|
7178
7178
|
}
|
|
7179
7179
|
|
|
7180
7180
|
function angularDistance(a, b) {
|
|
7181
|
-
return Math.acos(clamp$
|
|
7181
|
+
return Math.acos(clamp$3(dot$2(degreesToVector$2(a[0], a[1]),
|
|
7182
7182
|
degreesToVector$2(b[0], b[1])), -1, 1));
|
|
7183
7183
|
}
|
|
7184
7184
|
|
|
@@ -7194,7 +7194,7 @@
|
|
|
7194
7194
|
function vectorToDegrees$2(p) {
|
|
7195
7195
|
return [
|
|
7196
7196
|
Math.atan2(p[1], p[0]) * R2D$7,
|
|
7197
|
-
Math.asin(clamp$
|
|
7197
|
+
Math.asin(clamp$3(p[2], -1, 1)) * R2D$7
|
|
7198
7198
|
];
|
|
7199
7199
|
}
|
|
7200
7200
|
|
|
@@ -8904,7 +8904,7 @@
|
|
|
8904
8904
|
function interpolateSphericalRadians(a, b, interval) {
|
|
8905
8905
|
var av = radiansToVector(a[0], a[1]);
|
|
8906
8906
|
var bv = radiansToVector(b[0], b[1]);
|
|
8907
|
-
var angle = Math.acos(clamp$
|
|
8907
|
+
var angle = Math.acos(clamp$3(dot$1(av, bv), -1, 1));
|
|
8908
8908
|
var count = Math.max(1, Math.ceil(angle / interval));
|
|
8909
8909
|
var sinAngle = Math.sin(angle);
|
|
8910
8910
|
var points = [];
|
|
@@ -8924,7 +8924,7 @@
|
|
|
8924
8924
|
function vectorToRadians(p) {
|
|
8925
8925
|
return [
|
|
8926
8926
|
Math.atan2(p[1], p[0]),
|
|
8927
|
-
Math.asin(clamp$
|
|
8927
|
+
Math.asin(clamp$3(p[2], -1, 1))
|
|
8928
8928
|
];
|
|
8929
8929
|
}
|
|
8930
8930
|
|
|
@@ -8998,8 +8998,8 @@
|
|
|
8998
8998
|
}
|
|
8999
8999
|
x = qx - BLOCK_HEIGHT;
|
|
9000
9000
|
y = qy + 1.5;
|
|
9001
|
-
x = clamp$
|
|
9002
|
-
y = clamp$
|
|
9001
|
+
x = clamp$3(x, XMIN, XMAX);
|
|
9002
|
+
y = clamp$3(y, YMIN, YMAX);
|
|
9003
9003
|
return {
|
|
9004
9004
|
x: x,
|
|
9005
9005
|
y: y,
|
|
@@ -9109,7 +9109,7 @@
|
|
|
9109
9109
|
var v = radiansToVector(lam, phi);
|
|
9110
9110
|
return [
|
|
9111
9111
|
Math.atan2(dot$1(v, orientation.y), dot$1(v, orientation.x)),
|
|
9112
|
-
Math.asin(clamp$
|
|
9112
|
+
Math.asin(clamp$3(dot$1(v, orientation.z), -1, 1))
|
|
9113
9113
|
];
|
|
9114
9114
|
}
|
|
9115
9115
|
|
|
@@ -9120,7 +9120,7 @@
|
|
|
9120
9120
|
orientation.x[1] * v[0] + orientation.y[1] * v[1] + orientation.z[1] * v[2],
|
|
9121
9121
|
orientation.x[2] * v[0] + orientation.y[2] * v[1] + orientation.z[2] * v[2]
|
|
9122
9122
|
];
|
|
9123
|
-
return [Math.atan2(p[1], p[0]), Math.asin(clamp$
|
|
9123
|
+
return [Math.atan2(p[1], p[0]), Math.asin(clamp$3(p[2], -1, 1))];
|
|
9124
9124
|
}
|
|
9125
9125
|
|
|
9126
9126
|
// Adapted from Justin Kunimune's Imago implementation.
|
|
@@ -9134,7 +9134,7 @@
|
|
|
9134
9134
|
lat1 = lat;
|
|
9135
9135
|
lon1 = lon - lon0;
|
|
9136
9136
|
} else {
|
|
9137
|
-
lat1 = Math.asin(clamp$
|
|
9137
|
+
lat1 = Math.asin(clamp$3(
|
|
9138
9138
|
Math.sin(lat0) * Math.sin(lat) +
|
|
9139
9139
|
Math.cos(lat0) * Math.cos(lat) * Math.cos(lon0 - lon),
|
|
9140
9140
|
-1,
|
|
@@ -9145,7 +9145,7 @@
|
|
|
9145
9145
|
(Math.cos(lat0) * Math.sin(lat) -
|
|
9146
9146
|
Math.sin(lat0) * Math.cos(lat) * Math.cos(lon0 - lon)) /
|
|
9147
9147
|
denominator;
|
|
9148
|
-
lon1 = Math.acos(clamp$
|
|
9148
|
+
lon1 = Math.acos(clamp$3(value, -1, 1)) - Math.PI;
|
|
9149
9149
|
if (Math.sin(lon - lon0) > 0) lon1 = -lon1;
|
|
9150
9150
|
}
|
|
9151
9151
|
return [lat1, normalizeRadians(lon1 - theta0)];
|
|
@@ -9155,7 +9155,7 @@
|
|
|
9155
9155
|
var lat0 = pole.lat;
|
|
9156
9156
|
var lon0 = pole.lon;
|
|
9157
9157
|
lon += pole.meridian;
|
|
9158
|
-
var latOut = Math.asin(clamp$
|
|
9158
|
+
var latOut = Math.asin(clamp$3(
|
|
9159
9159
|
Math.sin(lat0) * Math.sin(lat) -
|
|
9160
9160
|
Math.cos(lat0) * Math.cos(lon) * Math.cos(lat),
|
|
9161
9161
|
-1,
|
|
@@ -9167,8 +9167,8 @@
|
|
|
9167
9167
|
} else {
|
|
9168
9168
|
var value = Math.sin(lat) / Math.cos(lat0) / Math.cos(latOut) -
|
|
9169
9169
|
Math.tan(lat0) * Math.tan(latOut);
|
|
9170
|
-
if (Math.sin(lon) > 0) lonOut = lon0 + Math.acos(clamp$
|
|
9171
|
-
else lonOut = lon0 - Math.acos(clamp$
|
|
9170
|
+
if (Math.sin(lon) > 0) lonOut = lon0 + Math.acos(clamp$3(value, -1, 1));
|
|
9171
|
+
else lonOut = lon0 - Math.acos(clamp$3(value, -1, 1));
|
|
9172
9172
|
}
|
|
9173
9173
|
return [latOut, normalizeRadians(lonOut)];
|
|
9174
9174
|
}
|
|
@@ -9643,8 +9643,8 @@
|
|
|
9643
9643
|
var copy = getLayoutCopy(p);
|
|
9644
9644
|
var q = applyMatrix(copy.matrix, p);
|
|
9645
9645
|
q[0] = wrapLayoutX(q[0] + layoutPhase);
|
|
9646
|
-
q[0] = clamp$
|
|
9647
|
-
q[1] = clamp$
|
|
9646
|
+
q[0] = clamp$3(q[0], RECT_XMIN, RECT_XMAX);
|
|
9647
|
+
q[1] = clamp$3(q[1], RECT_YMIN, RECT_YMAX);
|
|
9648
9648
|
return centerOutputPoint(q);
|
|
9649
9649
|
}
|
|
9650
9650
|
|
|
@@ -9748,8 +9748,8 @@
|
|
|
9748
9748
|
var copy = getLayoutCopy(p);
|
|
9749
9749
|
var q = applyMatrix(copy.matrix, p);
|
|
9750
9750
|
q[0] = wrapLayoutX(q[0] + layoutPhase);
|
|
9751
|
-
q[0] = clamp$
|
|
9752
|
-
q[1] = clamp$
|
|
9751
|
+
q[0] = clamp$3(q[0], RECT_XMIN, RECT_XMAX);
|
|
9752
|
+
q[1] = clamp$3(q[1], RECT_YMIN, RECT_YMAX);
|
|
9753
9753
|
q = centerOutputPoint(q);
|
|
9754
9754
|
var ids = rasterPieceIndex.get(getPieceKey(face, copy.id)) || [];
|
|
9755
9755
|
var region = findPieceContainingPoint(rasterPieces, ids, q);
|
|
@@ -26883,12 +26883,14 @@
|
|
|
26883
26883
|
'line-start': 'lineend',
|
|
26884
26884
|
'line-end': 'lineend',
|
|
26885
26885
|
'line-end-size': 'number',
|
|
26886
|
+
'line-fade': 'number',
|
|
26886
26887
|
'letter-spacing': 'measure',
|
|
26887
26888
|
'line-height': 'measure',
|
|
26888
26889
|
opacity: 'number',
|
|
26889
26890
|
r: 'number',
|
|
26890
26891
|
stroke: 'color',
|
|
26891
26892
|
'stroke-dasharray': 'dasharray',
|
|
26893
|
+
'stroke-linecap': 'linecap',
|
|
26892
26894
|
'stroke-width': 'number',
|
|
26893
26895
|
'stroke-opacity': 'number',
|
|
26894
26896
|
'stroke-miterlimit': 'number',
|
|
@@ -26981,9 +26983,17 @@
|
|
|
26981
26983
|
// symType: point, polygon, polyline, label
|
|
26982
26984
|
function applyStyleAttributes(svgObj, symType, rec, filter) {
|
|
26983
26985
|
var fields = findStylePropertiesBySymbolGeom(Object.keys(rec || {}), symType);
|
|
26986
|
+
var dashed = false, capped = false;
|
|
26984
26987
|
for (var i=0, n=fields.length; i<n; i++) {
|
|
26985
26988
|
if (filter && !filter(fields[i])) continue;
|
|
26986
26989
|
setAttribute(svgObj, fields[i], rec[fields[i]]);
|
|
26990
|
+
if (fields[i] == 'stroke-dasharray' && rec[fields[i]]) dashed = true;
|
|
26991
|
+
if (fields[i] == 'stroke-linecap' && rec[fields[i]]) capped = true;
|
|
26992
|
+
}
|
|
26993
|
+
// kludge for cleaner dashes: butt caps, unless the line has a cap of its own
|
|
26994
|
+
// (the GUI canvas renderer does the same)
|
|
26995
|
+
if (dashed && !capped) {
|
|
26996
|
+
setAttribute(svgObj, 'stroke-linecap', 'butt');
|
|
26987
26997
|
}
|
|
26988
26998
|
// kludge to prevent default black fill on polygons with stroke styles
|
|
26989
26999
|
if ((symType == 'polygon' || symType == 'circle') && rec.stroke && !rec.fill) {
|
|
@@ -26994,10 +27004,6 @@
|
|
|
26994
27004
|
function setAttribute(obj, k, v) {
|
|
26995
27005
|
if (!obj.properties) obj.properties = {};
|
|
26996
27006
|
obj.properties[k] = v;
|
|
26997
|
-
if (k == 'stroke-dasharray' && v) {
|
|
26998
|
-
// kludge for cleaner dashes... make butt the default?
|
|
26999
|
-
obj.properties['stroke-linecap'] = 'butt';
|
|
27000
|
-
}
|
|
27001
27007
|
}
|
|
27002
27008
|
|
|
27003
27009
|
function isSupportedSvgStyleProperty(name) {
|
|
@@ -27196,6 +27202,8 @@
|
|
|
27196
27202
|
val = parseCalloutEnd(strVal);
|
|
27197
27203
|
} else if (type == 'lineend') {
|
|
27198
27204
|
val = parseLineEnd(strVal);
|
|
27205
|
+
} else if (type == 'linecap') {
|
|
27206
|
+
val = parseLineCap(strVal);
|
|
27199
27207
|
} else if (type == 'pointpair') {
|
|
27200
27208
|
val = parsePointPair(strVal);
|
|
27201
27209
|
val = val ? formatPointPair(val) : null;
|
|
@@ -27253,6 +27261,11 @@
|
|
|
27253
27261
|
return /^(arrow|open-arrow|dot|none)$/.test(end) ? end : null;
|
|
27254
27262
|
}
|
|
27255
27263
|
|
|
27264
|
+
function parseLineCap(str) {
|
|
27265
|
+
var cap = String(str).trim().toLowerCase();
|
|
27266
|
+
return /^(round|butt|square)$/.test(cap) ? cap : null;
|
|
27267
|
+
}
|
|
27268
|
+
|
|
27256
27269
|
// "x,y" -> [x, y], or null. Stored as a string so that a pair is set and
|
|
27257
27270
|
// cleared as one value and reads plainly in a table.
|
|
27258
27271
|
function parsePointPair(str) {
|
|
@@ -27372,6 +27385,7 @@
|
|
|
27372
27385
|
parseCalloutEnd: parseCalloutEnd,
|
|
27373
27386
|
parseCalloutType: parseCalloutType,
|
|
27374
27387
|
parseLabelPosition: parseLabelPosition,
|
|
27388
|
+
parseLineCap: parseLineCap,
|
|
27375
27389
|
parseLineEnd: parseLineEnd,
|
|
27376
27390
|
parsePointPair: parsePointPair,
|
|
27377
27391
|
parseStyleLiteral: parseStyleLiteral,
|
|
@@ -27400,7 +27414,8 @@
|
|
|
27400
27414
|
// In the GUI the halo is the text element's own stroke, with paint-order
|
|
27401
27415
|
// putting it underneath. Export draws it as a second copy of the text instead
|
|
27402
27416
|
// -- see splitLabelHalos() -- because Illustrator and Figma ignore paint-order
|
|
27403
|
-
// when they import SVG and would draw the stroke over the letters
|
|
27417
|
+
// when they import SVG and would draw the stroke over the letters, and Chrome
|
|
27418
|
+
// and Safari draw paint-order halos badly where letters overlap.
|
|
27404
27419
|
|
|
27405
27420
|
var DEFAULT_HALO_COLOR = '#ffffff';
|
|
27406
27421
|
var HALO_PAINT_ORDER = 'stroke fill';
|
|
@@ -27461,6 +27476,10 @@
|
|
|
27461
27476
|
// which places it, and its opacity, which would otherwise fade each copy on its
|
|
27462
27477
|
// own and let the halo show through the letters. That is also how opacity
|
|
27463
27478
|
// reads on a single element with paint-order, which is faded as one image.
|
|
27479
|
+
//
|
|
27480
|
+
// Browsers need the copy too: Chrome and Safari paint a text element's stroke
|
|
27481
|
+
// and fill one glyph at a time, so with paint-order the halos of overlapping
|
|
27482
|
+
// letters compound a translucent halo, and cover the letters beside them.
|
|
27464
27483
|
function splitLabelHalos(o) {
|
|
27465
27484
|
if (!o) return o;
|
|
27466
27485
|
if (o.tag == 'text' && isHaloText(o)) return splitHalo(o);
|
|
@@ -29542,6 +29561,48 @@
|
|
|
29542
29561
|
return len;
|
|
29543
29562
|
}
|
|
29544
29563
|
|
|
29564
|
+
// The point @dist along the fitted curve from its first knot, clamped to the
|
|
29565
|
+
// curve's ends, or null if the knots collapse to a single point.
|
|
29566
|
+
function getPointAtCurveLength(knots, dist) {
|
|
29567
|
+
var segments = getCurveSegments(knots);
|
|
29568
|
+
var seg, len, i;
|
|
29569
|
+
if (segments.length === 0) return null;
|
|
29570
|
+
for (i = 0; i < segments.length; i++) {
|
|
29571
|
+
seg = segments[i];
|
|
29572
|
+
len = getCubicLength(seg, 0);
|
|
29573
|
+
if (dist <= len) return getPointAtCubicLength(seg, Math.max(dist, 0));
|
|
29574
|
+
dist -= len;
|
|
29575
|
+
}
|
|
29576
|
+
return [seg.p3[0], seg.p3[1]];
|
|
29577
|
+
}
|
|
29578
|
+
|
|
29579
|
+
// Walks a flattening of the segment, which is shorter than the true curve by
|
|
29580
|
+
// a negligible amount at this many steps.
|
|
29581
|
+
function getPointAtCubicLength(seg, dist) {
|
|
29582
|
+
var steps = 64;
|
|
29583
|
+
var prev = seg.p0, p, d, k, i;
|
|
29584
|
+
for (i = 1; i <= steps; i++) {
|
|
29585
|
+
p = getCubicPoint(seg, i / steps);
|
|
29586
|
+
d = distance2D$1(prev[0], prev[1], p[0], p[1]);
|
|
29587
|
+
if (d > 0 && d >= dist) {
|
|
29588
|
+
k = dist / d;
|
|
29589
|
+
return [prev[0] + (p[0] - prev[0]) * k, prev[1] + (p[1] - prev[1]) * k];
|
|
29590
|
+
}
|
|
29591
|
+
dist -= d;
|
|
29592
|
+
prev = p;
|
|
29593
|
+
}
|
|
29594
|
+
return [seg.p3[0], seg.p3[1]];
|
|
29595
|
+
}
|
|
29596
|
+
|
|
29597
|
+
function getCubicPoint(seg, t) {
|
|
29598
|
+
var mt = 1 - t;
|
|
29599
|
+
var a = mt * mt * mt, b = 3 * mt * mt * t, c = 3 * mt * t * t, d = t * t * t;
|
|
29600
|
+
return [
|
|
29601
|
+
a * seg.p0[0] + b * seg.c1[0] + c * seg.c2[0] + d * seg.p3[0],
|
|
29602
|
+
a * seg.p0[1] + b * seg.c1[1] + c * seg.c2[1] + d * seg.p3[1]
|
|
29603
|
+
];
|
|
29604
|
+
}
|
|
29605
|
+
|
|
29545
29606
|
// Hobby's velocity function constants (Hobby 1986, eq. 11), the approximation
|
|
29546
29607
|
// Metafont uses in place of the transcendental form.
|
|
29547
29608
|
var VEL_A = Math.SQRT2,
|
|
@@ -29822,13 +29883,14 @@
|
|
|
29822
29883
|
getCurveCurl: getCurveCurl,
|
|
29823
29884
|
getCurveLength: getCurveLength,
|
|
29824
29885
|
getCurveSegments: getCurveSegments,
|
|
29886
|
+
getPointAtCurveLength: getPointAtCurveLength,
|
|
29825
29887
|
setCurveCurl: setCurveCurl
|
|
29826
29888
|
});
|
|
29827
29889
|
|
|
29828
|
-
var roundCoord$
|
|
29890
|
+
var roundCoord$3 = getRoundingFunction(0.01);
|
|
29829
29891
|
|
|
29830
29892
|
function stringifyVertex(p) {
|
|
29831
|
-
return ' ' + roundCoord$
|
|
29893
|
+
return ' ' + roundCoord$3(p[0]) + ' ' + roundCoord$3(p[1]);
|
|
29832
29894
|
}
|
|
29833
29895
|
|
|
29834
29896
|
function isCubicCtrl(p) {
|
|
@@ -30125,6 +30187,7 @@
|
|
|
30125
30187
|
LABEL_OVERFLOW_CLASS: LABEL_OVERFLOW_CLASS,
|
|
30126
30188
|
LABEL_PATH_PROPERTY: LABEL_PATH_PROPERTY,
|
|
30127
30189
|
featureIsPathLabel: featureIsPathLabel,
|
|
30190
|
+
getDefaultStartOffset: getDefaultStartOffset,
|
|
30128
30191
|
getLabelPathCoords: getLabelPathCoords,
|
|
30129
30192
|
getLabelPathData: getLabelPathData,
|
|
30130
30193
|
initPathLabelReport: initPathLabelReport,
|
|
@@ -30463,139 +30526,6 @@
|
|
|
30463
30526
|
renderGlowFilter: renderGlowFilter
|
|
30464
30527
|
});
|
|
30465
30528
|
|
|
30466
|
-
// convert object properties to definitions for images and hatch fills
|
|
30467
|
-
function convertPropertiesToDefinitions(obj, defs) {
|
|
30468
|
-
procNode(obj);
|
|
30469
|
-
|
|
30470
|
-
function procNode(obj) {
|
|
30471
|
-
if (obj.glowFilter) {
|
|
30472
|
-
convertGlowFilter(obj, defs);
|
|
30473
|
-
}
|
|
30474
|
-
if (obj.tag == 'textPath' && obj.properties && obj.properties[LABEL_PATH_PROPERTY]) {
|
|
30475
|
-
convertLabelPath(obj.properties, defs);
|
|
30476
|
-
}
|
|
30477
|
-
if (obj.tag == 'path' && obj.properties['fill-pattern']) {
|
|
30478
|
-
convertFillPattern(obj.properties, defs);
|
|
30479
|
-
}
|
|
30480
|
-
if (obj.tag == 'path' && obj.properties['fill-effect']) {
|
|
30481
|
-
convertFillEffect(obj.properties, defs);
|
|
30482
|
-
}
|
|
30483
|
-
if (obj.tag == 'image') {
|
|
30484
|
-
if (/\.svg/.test(obj.properties.href || '')) {
|
|
30485
|
-
convertSvgImage(obj, defs);
|
|
30486
|
-
}
|
|
30487
|
-
} else if (obj.children) {
|
|
30488
|
-
obj.children.forEach(procNode);
|
|
30489
|
-
}
|
|
30490
|
-
}
|
|
30491
|
-
}
|
|
30492
|
-
|
|
30493
|
-
// Moves a label's baseline into <defs>, where it is referenced but never
|
|
30494
|
-
// painted. It must not be left in the document body: a <path> with no stroke or
|
|
30495
|
-
// fill of its own picks up SVG's defaults -- no stroke and *black fill* -- and
|
|
30496
|
-
// renders as a filled blob instead of a line.
|
|
30497
|
-
//
|
|
30498
|
-
// The id is derived from the path data, so identical paths share one
|
|
30499
|
-
// definition and repeated exports of the same map produce identical output.
|
|
30500
|
-
function convertLabelPath(properties, defs) {
|
|
30501
|
-
var d = properties[LABEL_PATH_PROPERTY];
|
|
30502
|
-
var item = utils.find(defs, function(item) {return item.labelPath === d;});
|
|
30503
|
-
delete properties[LABEL_PATH_PROPERTY];
|
|
30504
|
-
if (!item) {
|
|
30505
|
-
item = {
|
|
30506
|
-
labelPath: d,
|
|
30507
|
-
id: 'label-path-' + sha1(d).substr(0, 12)
|
|
30508
|
-
};
|
|
30509
|
-
item.svg = '<path id="' + item.id + '" d="' + d + '"/>\n';
|
|
30510
|
-
defs.push(item);
|
|
30511
|
-
}
|
|
30512
|
-
properties.href = '#' + item.id;
|
|
30513
|
-
}
|
|
30514
|
-
|
|
30515
|
-
function convertSvgImage(obj, defs) {
|
|
30516
|
-
// Same-origin policy prevents embedding images in the web UI
|
|
30517
|
-
var href = obj.properties.href;
|
|
30518
|
-
// Remote URLs were previously fetched at export time so the SVG could be
|
|
30519
|
-
// embedded inline. That capability has been removed (it required a
|
|
30520
|
-
// synchronous-HTTP dependency built on a child-process hack); download
|
|
30521
|
-
// the asset first and reference it by local path instead.
|
|
30522
|
-
if (href.indexOf('http') === 0) {
|
|
30523
|
-
stop$1('Remote SVG asset references are not supported. Download the file first and reference it by a local path:', href);
|
|
30524
|
-
}
|
|
30525
|
-
// look for a previously added definition to use
|
|
30526
|
-
// (assumes that images that share the same href can also use the same defn)
|
|
30527
|
-
var item = utils.find(defs, function(item) {return item.href == href;});
|
|
30528
|
-
if (!item) {
|
|
30529
|
-
item = {
|
|
30530
|
-
href: href,
|
|
30531
|
-
id: urlToId(href) // generating id from href, to try to support multiple inline svgs on page
|
|
30532
|
-
};
|
|
30533
|
-
item.svg = serializeSvgImage(href, item.id);
|
|
30534
|
-
defs.push(item);
|
|
30535
|
-
}
|
|
30536
|
-
if (item.svg) {
|
|
30537
|
-
obj.tag = 'use';
|
|
30538
|
-
obj.properties.href = '#' + item.id;
|
|
30539
|
-
}
|
|
30540
|
-
}
|
|
30541
|
-
|
|
30542
|
-
// Returns the content of an SVG file from a local path.
|
|
30543
|
-
// Returns '' if unable to get the content; the caller leaves the original
|
|
30544
|
-
// <image> tag in the output so the SVG renderer can attempt to load it.
|
|
30545
|
-
function serializeSvgImage(href, id) {
|
|
30546
|
-
var svg = '';
|
|
30547
|
-
try {
|
|
30548
|
-
svg = convertSvgToDefn(getSvgContent(href), id) + '\n';
|
|
30549
|
-
svg = '<!-- ' + href + '-->\n' + svg; // add href as a comment, to aid in debugging
|
|
30550
|
-
} catch(e) {
|
|
30551
|
-
// tried creating a symbol as a fallback... encounted problems with icon
|
|
30552
|
-
// size and placement, giving up on this for now
|
|
30553
|
-
// svg = `<symbol><image xlink:href="${obj.properties.href}" id="${id}"></image></symbol>`;
|
|
30554
|
-
}
|
|
30555
|
-
return svg;
|
|
30556
|
-
}
|
|
30557
|
-
|
|
30558
|
-
// href: A local path
|
|
30559
|
-
function getSvgContent(href) {
|
|
30560
|
-
if (require$1('fs').existsSync(href)) {
|
|
30561
|
-
return require$1('fs').readFileSync(href, 'utf8');
|
|
30562
|
-
}
|
|
30563
|
-
stop$1("Invalid SVG location:", href);
|
|
30564
|
-
}
|
|
30565
|
-
|
|
30566
|
-
function convertSvgToDefn(svg, id) {
|
|
30567
|
-
// Remove stuff before <svg> tag
|
|
30568
|
-
svg = svg.replace(/[^]*<svg/, '<svg');
|
|
30569
|
-
return svg.replace(/^<svg[^>]*>/, function(a) {
|
|
30570
|
-
// set id property of <svg>
|
|
30571
|
-
a = a.replace(/ id="[^"]*"/, '');
|
|
30572
|
-
a = a.replace(/<svg/, '<svg id="' + id + '"');
|
|
30573
|
-
return a;
|
|
30574
|
-
});
|
|
30575
|
-
}
|
|
30576
|
-
|
|
30577
|
-
function urlToId(url) {
|
|
30578
|
-
return sha1(url).substr(0, 12);
|
|
30579
|
-
}
|
|
30580
|
-
|
|
30581
|
-
/*
|
|
30582
|
-
function convertSvgToSymbol(svg, id) {
|
|
30583
|
-
svg = svg.replace(/[^]*<svg/, '<svg');
|
|
30584
|
-
// Remove inkscape tags (there were errors caused when namespaces were
|
|
30585
|
-
// stripped when converting <svg> to <symbol> ... this may be futile, may
|
|
30586
|
-
// have to go back to embedding entire SVG document instead of using symbols)
|
|
30587
|
-
svg = svg.replace(/<metadata[^]*?metadata>/, '');
|
|
30588
|
-
svg = svg.replace(/<sodipodi[^>]*>/, '');
|
|
30589
|
-
// convert <svg> to <symbol>
|
|
30590
|
-
svg = svg.replace(/^<svg[^>]*>/, function(a) {
|
|
30591
|
-
var viewBox = a.match(/viewBox=".*?"/)[0];
|
|
30592
|
-
return '<symbol id="' + id + '" ' + viewBox + '>';
|
|
30593
|
-
});
|
|
30594
|
-
svg = svg.replace('svg>', 'symbol>');
|
|
30595
|
-
return svg;
|
|
30596
|
-
}
|
|
30597
|
-
*/
|
|
30598
|
-
|
|
30599
30529
|
// Markers at the ends of line features: line-start= and line-end= take
|
|
30600
30530
|
// arrow (a filled triangle), open-arrow (a stroked chevron), dot (a filled
|
|
30601
30531
|
// circle centred on the end) or none, and line-end-size= is the length of a
|
|
@@ -30608,6 +30538,14 @@
|
|
|
30608
30538
|
// part of a multi-part line gets its own heads: part order is not something
|
|
30609
30539
|
// the commands that make multi-part lines preserve.
|
|
30610
30540
|
//
|
|
30541
|
+
// line-fade= fades the line in from its tail: 0-1, the share of the line's
|
|
30542
|
+
// length over which it goes from transparent to its full opacity. The tail is
|
|
30543
|
+
// the start, unless only the start has a head. The fade is a straight linear
|
|
30544
|
+
// gradient from the tail to the point that far along the line, which follows a
|
|
30545
|
+
// straight or gently curved line closely; SVG and canvas have no gradient that
|
|
30546
|
+
// follows a path, so a line that bends sharply within its fade will not fade
|
|
30547
|
+
// evenly along its length.
|
|
30548
|
+
//
|
|
30611
30549
|
// Coordinates are px, y down: the SVG export's space, or the canvas's.
|
|
30612
30550
|
|
|
30613
30551
|
var DEFAULT_LINE_WIDTH = 1;
|
|
@@ -30624,16 +30562,26 @@
|
|
|
30624
30562
|
getLineEndType(rec, 'line-end') != 'none';
|
|
30625
30563
|
}
|
|
30626
30564
|
|
|
30565
|
+
// The share of the line that fades, 0 for none
|
|
30566
|
+
function parseLineFade(val) {
|
|
30567
|
+
return isSvgNumber(val) && Number(val) > 0 ? Math.min(Number(val), 1) : 0;
|
|
30568
|
+
}
|
|
30569
|
+
|
|
30570
|
+
// Whether a line is drawn by renderArrowLine() rather than as a plain path
|
|
30571
|
+
function lineHasEndStyles(rec) {
|
|
30572
|
+
return lineHasArrows(rec) || parseLineFade(rec && rec['line-fade']) > 0;
|
|
30573
|
+
}
|
|
30574
|
+
|
|
30627
30575
|
// What getLineArrowShape() needs from a record, at scale @scale (the factor
|
|
30628
30576
|
// the canvas multiplies line widths by; 1 for SVG).
|
|
30629
30577
|
function getLineArrowOpts(rec, scale) {
|
|
30630
30578
|
return makeLineArrowOpts(rec['line-start'], rec['line-end'], rec['line-end-size'],
|
|
30631
|
-
rec['stroke-width'], scale);
|
|
30579
|
+
rec['stroke-width'], scale, rec['line-fade']);
|
|
30632
30580
|
}
|
|
30633
30581
|
|
|
30634
30582
|
// The same, from the values themselves, for the canvas, which has them in a
|
|
30635
30583
|
// style object rather than a record.
|
|
30636
|
-
function makeLineArrowOpts(start, end, size, strokeWidth, scale) {
|
|
30584
|
+
function makeLineArrowOpts(start, end, size, strokeWidth, scale, fade) {
|
|
30637
30585
|
var k = scale > 0 ? scale : 1;
|
|
30638
30586
|
var w = isSvgNumber(strokeWidth) && Number(strokeWidth) >= 0 ?
|
|
30639
30587
|
Number(strokeWidth) : DEFAULT_LINE_WIDTH;
|
|
@@ -30644,18 +30592,20 @@
|
|
|
30644
30592
|
end: end && parseLineEnd(end) || 'none',
|
|
30645
30593
|
size: side * k,
|
|
30646
30594
|
dotSize: (hasSize ? side : side * DOT_SIZE_RATIO) * k,
|
|
30647
|
-
width: w * k
|
|
30595
|
+
width: w * k,
|
|
30596
|
+
fade: parseLineFade(fade)
|
|
30648
30597
|
};
|
|
30649
30598
|
}
|
|
30650
30599
|
|
|
30651
30600
|
// Field names, for a layer that may have them
|
|
30652
|
-
var lineArrowFields = ['line-start', 'line-end', 'line-end-size'];
|
|
30601
|
+
var lineArrowFields = ['line-start', 'line-end', 'line-end-size', 'line-fade'];
|
|
30653
30602
|
|
|
30654
|
-
// One part of a line, with its heads: {coords, heads}, where coords is
|
|
30655
|
-
// line to stroke -- cut back into a solid head, so that its cap does not
|
|
30656
|
-
// past the tip -- and heads is a list of {type, points}: a solid head's
|
|
30603
|
+
// One part of a line, with its heads: {coords, heads, fade}, where coords is
|
|
30604
|
+
// the line to stroke -- cut back into a solid head, so that its cap does not
|
|
30605
|
+
// show past the tip -- and heads is a list of {type, points}: a solid head's
|
|
30657
30606
|
// triangle, tip first, or an open head's chevron, wing to tip to wing. A dot
|
|
30658
|
-
// is {type, center, radius}.
|
|
30607
|
+
// is {type, center, radius}. fade is the axis of the line's fade (see
|
|
30608
|
+
// getLineFadeAxis()), or null.
|
|
30659
30609
|
//
|
|
30660
30610
|
// A head points along the chord from its tip to where the line first reaches
|
|
30661
30611
|
// the head's length from it, rather than along the last segment, which on
|
|
@@ -30663,8 +30613,11 @@
|
|
|
30663
30613
|
// anywhere. The line is then straightened to meet the head along its axis. An
|
|
30664
30614
|
// end whose part is too short to hold its head gets none.
|
|
30665
30615
|
function getLineArrowShape(coords, opts) {
|
|
30666
|
-
var out = {coords: coords, heads: []};
|
|
30616
|
+
var out = {coords: coords, heads: [], fade: null};
|
|
30667
30617
|
var end;
|
|
30618
|
+
if (opts.fade > 0) {
|
|
30619
|
+
out.fade = getLineFadeAxis(coords, opts.start != 'none' && opts.end == 'none', opts.fade);
|
|
30620
|
+
}
|
|
30668
30621
|
if (opts.start != 'none') {
|
|
30669
30622
|
addHead(out, opts.start, opts);
|
|
30670
30623
|
}
|
|
@@ -30676,6 +30629,45 @@
|
|
|
30676
30629
|
return out;
|
|
30677
30630
|
}
|
|
30678
30631
|
|
|
30632
|
+
// {from, to}: the line's tail, where the fade is transparent, and the point
|
|
30633
|
+
// @fraction of the line's length along it from there, where the fade ends.
|
|
30634
|
+
// Measured on the whole line, before the heads trim it. Null for a line too
|
|
30635
|
+
// short to fade, or a closed one faded all the way round, whose two points
|
|
30636
|
+
// would be the same.
|
|
30637
|
+
function getLineFadeAxis(coords, fromEnd, fraction) {
|
|
30638
|
+
var path = fromEnd ? coords.slice().reverse() : coords;
|
|
30639
|
+
var total = 0, target, d, i, to, k;
|
|
30640
|
+
for (i=1; i<path.length; i++) {
|
|
30641
|
+
total += distance$1(path[i - 1], path[i]);
|
|
30642
|
+
}
|
|
30643
|
+
target = total * fraction;
|
|
30644
|
+
to = path[path.length - 1];
|
|
30645
|
+
for (i=1; i<path.length && target > 0; i++) {
|
|
30646
|
+
d = distance$1(path[i - 1], path[i]);
|
|
30647
|
+
if (d >= target) {
|
|
30648
|
+
k = target / d;
|
|
30649
|
+
to = [path[i - 1][0] + (path[i][0] - path[i - 1][0]) * k,
|
|
30650
|
+
path[i - 1][1] + (path[i][1] - path[i - 1][1]) * k];
|
|
30651
|
+
break;
|
|
30652
|
+
}
|
|
30653
|
+
target -= d;
|
|
30654
|
+
}
|
|
30655
|
+
if (!(total > 0) || distance$1(path[0], to) < 0.5) return null;
|
|
30656
|
+
return {from: path[0], to: to};
|
|
30657
|
+
}
|
|
30658
|
+
|
|
30659
|
+
// The fade's two colours: the stroke's, and the same with no opacity, so
|
|
30660
|
+
// that the colour does not shift on its way to transparent. Null for a stroke
|
|
30661
|
+
// colour that cannot be faded (e.g. 'none').
|
|
30662
|
+
function getLineFadeColors(color) {
|
|
30663
|
+
var rgb = parseColor(color || 'black');
|
|
30664
|
+
if (!rgb) return null;
|
|
30665
|
+
return {
|
|
30666
|
+
transparent: 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',0)',
|
|
30667
|
+
solid: color || 'black'
|
|
30668
|
+
};
|
|
30669
|
+
}
|
|
30670
|
+
|
|
30679
30671
|
// Puts a head of @type at the first point of @shape.coords
|
|
30680
30672
|
function addHead(shape, type, opts) {
|
|
30681
30673
|
var coords = shape.coords;
|
|
@@ -30725,23 +30717,64 @@
|
|
|
30725
30717
|
if (rest) shape.coords = rest;
|
|
30726
30718
|
}
|
|
30727
30719
|
|
|
30728
|
-
// A line feature with arrowheads, as SVG: a group holding the line
|
|
30729
|
-
// heads, for the feature's style attributes to go on. @parts is a list
|
|
30730
|
-
// polylines (a LineString's coordinates are one).
|
|
30720
|
+
// A line feature with arrowheads or a fade, as SVG: a group holding the line
|
|
30721
|
+
// and its heads, for the feature's style attributes to go on. @parts is a list
|
|
30722
|
+
// of polylines (a LineString's coordinates are one). A faded part is a path of
|
|
30723
|
+
// its own, since each part has its own gradient.
|
|
30731
30724
|
function renderArrowLine(parts, rec) {
|
|
30732
30725
|
var opts = getLineArrowOpts(rec, 1);
|
|
30733
30726
|
var color = rec.stroke || 'black';
|
|
30727
|
+
var fadeColors = opts.fade > 0 ? getLineFadeColors(color) : null;
|
|
30734
30728
|
var lines = [];
|
|
30735
|
-
var
|
|
30729
|
+
var fadedLines = [];
|
|
30730
|
+
var heads = [];
|
|
30736
30731
|
parts.forEach(function(part) {
|
|
30737
30732
|
var shape = getLineArrowShape(part, opts);
|
|
30738
|
-
|
|
30733
|
+
var coords = shape.coords.map(roundPoint);
|
|
30734
|
+
if (shape.fade && fadeColors) {
|
|
30735
|
+
fadedLines.push(renderFadedLine(coords, shape.fade, fadeColors));
|
|
30736
|
+
} else {
|
|
30737
|
+
lines.push(coords);
|
|
30738
|
+
}
|
|
30739
30739
|
shape.heads.forEach(function(head) {
|
|
30740
|
-
|
|
30740
|
+
heads.push(renderHead(head, color, rec));
|
|
30741
30741
|
});
|
|
30742
30742
|
});
|
|
30743
|
-
children.
|
|
30744
|
-
return {tag: 'g', properties: {}, children: children};
|
|
30743
|
+
var children = lines.length > 0 ? [importMultiLineString(lines)] : [];
|
|
30744
|
+
return {tag: 'g', properties: {}, children: children.concat(fadedLines, heads)};
|
|
30745
|
+
}
|
|
30746
|
+
|
|
30747
|
+
// The gradient is added to the SVG's <defs> by convertLineFade(), which also
|
|
30748
|
+
// points the path's stroke at it.
|
|
30749
|
+
function renderFadedLine(coords, axis, colors) {
|
|
30750
|
+
var obj = importLineString(coords);
|
|
30751
|
+
var from = roundPoint(axis.from);
|
|
30752
|
+
var to = roundPoint(axis.to);
|
|
30753
|
+
obj.lineFade = {x1: from[0], y1: from[1], x2: to[0], y2: to[1], color: colors.solid};
|
|
30754
|
+
return obj;
|
|
30755
|
+
}
|
|
30756
|
+
|
|
30757
|
+
// Replaces the fade an object was marked with by a reference to a
|
|
30758
|
+
// <linearGradient> in @defs, which lines with the same fade share. The
|
|
30759
|
+
// gradient is in the path's own coordinates (userSpaceOnUse): the default, the
|
|
30760
|
+
// path's bounding box, has no height for a horizontal line, and a gradient
|
|
30761
|
+
// over an empty box paints nothing.
|
|
30762
|
+
function convertLineFade(obj, defs) {
|
|
30763
|
+
var f = obj.lineFade;
|
|
30764
|
+
var color = String(f.color).replace(/"/g, '');
|
|
30765
|
+
var body = 'gradientUnits="userSpaceOnUse" x1="' + f.x1 + '" y1="' + f.y1 +
|
|
30766
|
+
'" x2="' + f.x2 + '" y2="' + f.y2 + '"><stop offset="0" stop-color="' + color +
|
|
30767
|
+
'" stop-opacity="0"/><stop offset="1" stop-color="' + color + '"/></linearGradient>';
|
|
30768
|
+
var item = defs.find(function(o) { return o.lineFadeKey === body; });
|
|
30769
|
+
var count;
|
|
30770
|
+
if (!item) {
|
|
30771
|
+
count = defs.filter(function(o) { return !!o.lineFadeKey; }).length;
|
|
30772
|
+
item = {lineFadeKey: body, id: 'line-fade-' + (count + 1)};
|
|
30773
|
+
item.svg = '<linearGradient id="' + item.id + '" ' + body + '\n';
|
|
30774
|
+
defs.push(item);
|
|
30775
|
+
}
|
|
30776
|
+
delete obj.lineFade;
|
|
30777
|
+
obj.properties.stroke = 'url(#' + item.id + ')';
|
|
30745
30778
|
}
|
|
30746
30779
|
|
|
30747
30780
|
function renderHead(head, color, rec) {
|
|
@@ -30779,15 +30812,156 @@
|
|
|
30779
30812
|
|
|
30780
30813
|
var SvgLineArrows = /*#__PURE__*/Object.freeze({
|
|
30781
30814
|
__proto__: null,
|
|
30815
|
+
convertLineFade: convertLineFade,
|
|
30782
30816
|
getLineArrowOpts: getLineArrowOpts,
|
|
30783
30817
|
getLineArrowShape: getLineArrowShape,
|
|
30784
30818
|
getLineEndType: getLineEndType,
|
|
30819
|
+
getLineFadeAxis: getLineFadeAxis,
|
|
30820
|
+
getLineFadeColors: getLineFadeColors,
|
|
30785
30821
|
lineArrowFields: lineArrowFields,
|
|
30786
30822
|
lineHasArrows: lineHasArrows,
|
|
30823
|
+
lineHasEndStyles: lineHasEndStyles,
|
|
30787
30824
|
makeLineArrowOpts: makeLineArrowOpts,
|
|
30825
|
+
parseLineFade: parseLineFade,
|
|
30788
30826
|
renderArrowLine: renderArrowLine
|
|
30789
30827
|
});
|
|
30790
30828
|
|
|
30829
|
+
// convert object properties to definitions for images and hatch fills
|
|
30830
|
+
function convertPropertiesToDefinitions(obj, defs) {
|
|
30831
|
+
procNode(obj);
|
|
30832
|
+
|
|
30833
|
+
function procNode(obj) {
|
|
30834
|
+
if (obj.glowFilter) {
|
|
30835
|
+
convertGlowFilter(obj, defs);
|
|
30836
|
+
}
|
|
30837
|
+
if (obj.lineFade) {
|
|
30838
|
+
convertLineFade(obj, defs);
|
|
30839
|
+
}
|
|
30840
|
+
if (obj.tag == 'textPath' && obj.properties && obj.properties[LABEL_PATH_PROPERTY]) {
|
|
30841
|
+
convertLabelPath(obj.properties, defs);
|
|
30842
|
+
}
|
|
30843
|
+
if (obj.tag == 'path' && obj.properties['fill-pattern']) {
|
|
30844
|
+
convertFillPattern(obj.properties, defs);
|
|
30845
|
+
}
|
|
30846
|
+
if (obj.tag == 'path' && obj.properties['fill-effect']) {
|
|
30847
|
+
convertFillEffect(obj.properties, defs);
|
|
30848
|
+
}
|
|
30849
|
+
if (obj.tag == 'image') {
|
|
30850
|
+
if (/\.svg/.test(obj.properties.href || '')) {
|
|
30851
|
+
convertSvgImage(obj, defs);
|
|
30852
|
+
}
|
|
30853
|
+
} else if (obj.children) {
|
|
30854
|
+
obj.children.forEach(procNode);
|
|
30855
|
+
}
|
|
30856
|
+
}
|
|
30857
|
+
}
|
|
30858
|
+
|
|
30859
|
+
// Moves a label's baseline into <defs>, where it is referenced but never
|
|
30860
|
+
// painted. It must not be left in the document body: a <path> with no stroke or
|
|
30861
|
+
// fill of its own picks up SVG's defaults -- no stroke and *black fill* -- and
|
|
30862
|
+
// renders as a filled blob instead of a line.
|
|
30863
|
+
//
|
|
30864
|
+
// The id is derived from the path data, so identical paths share one
|
|
30865
|
+
// definition and repeated exports of the same map produce identical output.
|
|
30866
|
+
function convertLabelPath(properties, defs) {
|
|
30867
|
+
var d = properties[LABEL_PATH_PROPERTY];
|
|
30868
|
+
var item = utils.find(defs, function(item) {return item.labelPath === d;});
|
|
30869
|
+
delete properties[LABEL_PATH_PROPERTY];
|
|
30870
|
+
if (!item) {
|
|
30871
|
+
item = {
|
|
30872
|
+
labelPath: d,
|
|
30873
|
+
id: 'label-path-' + sha1(d).substr(0, 12)
|
|
30874
|
+
};
|
|
30875
|
+
item.svg = '<path id="' + item.id + '" d="' + d + '"/>\n';
|
|
30876
|
+
defs.push(item);
|
|
30877
|
+
}
|
|
30878
|
+
properties.href = '#' + item.id;
|
|
30879
|
+
}
|
|
30880
|
+
|
|
30881
|
+
function convertSvgImage(obj, defs) {
|
|
30882
|
+
// Same-origin policy prevents embedding images in the web UI
|
|
30883
|
+
var href = obj.properties.href;
|
|
30884
|
+
// Remote URLs were previously fetched at export time so the SVG could be
|
|
30885
|
+
// embedded inline. That capability has been removed (it required a
|
|
30886
|
+
// synchronous-HTTP dependency built on a child-process hack); download
|
|
30887
|
+
// the asset first and reference it by local path instead.
|
|
30888
|
+
if (href.indexOf('http') === 0) {
|
|
30889
|
+
stop$1('Remote SVG asset references are not supported. Download the file first and reference it by a local path:', href);
|
|
30890
|
+
}
|
|
30891
|
+
// look for a previously added definition to use
|
|
30892
|
+
// (assumes that images that share the same href can also use the same defn)
|
|
30893
|
+
var item = utils.find(defs, function(item) {return item.href == href;});
|
|
30894
|
+
if (!item) {
|
|
30895
|
+
item = {
|
|
30896
|
+
href: href,
|
|
30897
|
+
id: urlToId(href) // generating id from href, to try to support multiple inline svgs on page
|
|
30898
|
+
};
|
|
30899
|
+
item.svg = serializeSvgImage(href, item.id);
|
|
30900
|
+
defs.push(item);
|
|
30901
|
+
}
|
|
30902
|
+
if (item.svg) {
|
|
30903
|
+
obj.tag = 'use';
|
|
30904
|
+
obj.properties.href = '#' + item.id;
|
|
30905
|
+
}
|
|
30906
|
+
}
|
|
30907
|
+
|
|
30908
|
+
// Returns the content of an SVG file from a local path.
|
|
30909
|
+
// Returns '' if unable to get the content; the caller leaves the original
|
|
30910
|
+
// <image> tag in the output so the SVG renderer can attempt to load it.
|
|
30911
|
+
function serializeSvgImage(href, id) {
|
|
30912
|
+
var svg = '';
|
|
30913
|
+
try {
|
|
30914
|
+
svg = convertSvgToDefn(getSvgContent(href), id) + '\n';
|
|
30915
|
+
svg = '<!-- ' + href + '-->\n' + svg; // add href as a comment, to aid in debugging
|
|
30916
|
+
} catch(e) {
|
|
30917
|
+
// tried creating a symbol as a fallback... encounted problems with icon
|
|
30918
|
+
// size and placement, giving up on this for now
|
|
30919
|
+
// svg = `<symbol><image xlink:href="${obj.properties.href}" id="${id}"></image></symbol>`;
|
|
30920
|
+
}
|
|
30921
|
+
return svg;
|
|
30922
|
+
}
|
|
30923
|
+
|
|
30924
|
+
// href: A local path
|
|
30925
|
+
function getSvgContent(href) {
|
|
30926
|
+
if (require$1('fs').existsSync(href)) {
|
|
30927
|
+
return require$1('fs').readFileSync(href, 'utf8');
|
|
30928
|
+
}
|
|
30929
|
+
stop$1("Invalid SVG location:", href);
|
|
30930
|
+
}
|
|
30931
|
+
|
|
30932
|
+
function convertSvgToDefn(svg, id) {
|
|
30933
|
+
// Remove stuff before <svg> tag
|
|
30934
|
+
svg = svg.replace(/[^]*<svg/, '<svg');
|
|
30935
|
+
return svg.replace(/^<svg[^>]*>/, function(a) {
|
|
30936
|
+
// set id property of <svg>
|
|
30937
|
+
a = a.replace(/ id="[^"]*"/, '');
|
|
30938
|
+
a = a.replace(/<svg/, '<svg id="' + id + '"');
|
|
30939
|
+
return a;
|
|
30940
|
+
});
|
|
30941
|
+
}
|
|
30942
|
+
|
|
30943
|
+
function urlToId(url) {
|
|
30944
|
+
return sha1(url).substr(0, 12);
|
|
30945
|
+
}
|
|
30946
|
+
|
|
30947
|
+
/*
|
|
30948
|
+
function convertSvgToSymbol(svg, id) {
|
|
30949
|
+
svg = svg.replace(/[^]*<svg/, '<svg');
|
|
30950
|
+
// Remove inkscape tags (there were errors caused when namespaces were
|
|
30951
|
+
// stripped when converting <svg> to <symbol> ... this may be futile, may
|
|
30952
|
+
// have to go back to embedding entire SVG document instead of using symbols)
|
|
30953
|
+
svg = svg.replace(/<metadata[^]*?metadata>/, '');
|
|
30954
|
+
svg = svg.replace(/<sodipodi[^>]*>/, '');
|
|
30955
|
+
// convert <svg> to <symbol>
|
|
30956
|
+
svg = svg.replace(/^<svg[^>]*>/, function(a) {
|
|
30957
|
+
var viewBox = a.match(/viewBox=".*?"/)[0];
|
|
30958
|
+
return '<symbol id="' + id + '" ' + viewBox + '>';
|
|
30959
|
+
});
|
|
30960
|
+
svg = svg.replace('svg>', 'symbol>');
|
|
30961
|
+
return svg;
|
|
30962
|
+
}
|
|
30963
|
+
*/
|
|
30964
|
+
|
|
30791
30965
|
var geojsonImporters = {
|
|
30792
30966
|
Point: importPoint,
|
|
30793
30967
|
Polygon: importPolygon,
|
|
@@ -30810,7 +30984,7 @@
|
|
|
30810
30984
|
// rather than several labels at several points
|
|
30811
30985
|
svgObj = renderPathLabel(d, geom.coordinates,
|
|
30812
30986
|
{report: opts.path_label_report, id: featureId});
|
|
30813
|
-
} else if (msType == 'polyline' && geom.coordinates &&
|
|
30987
|
+
} else if (msType == 'polyline' && geom.coordinates && lineHasEndStyles(d)) {
|
|
30814
30988
|
svgObj = renderArrowLine(geomType == 'LineString' ?
|
|
30815
30989
|
[geom.coordinates] : geom.coordinates, d);
|
|
30816
30990
|
} else if (geomType && geom.coordinates) {
|
|
@@ -30895,20 +31069,19 @@
|
|
|
30895
31069
|
|
|
30896
31070
|
//
|
|
30897
31071
|
function exportSVG(dataset, opts) {
|
|
30898
|
-
var
|
|
30899
|
-
var
|
|
30900
|
-
|
|
30901
|
-
|
|
30902
|
-
|
|
30903
|
-
|
|
30904
|
-
|
|
30905
|
-
if (opts.crisp_paths) {
|
|
30906
|
-
style = `
|
|
30907
|
-
<style>
|
|
30908
|
-
path {shape-rendering: crispEdges;}
|
|
30909
|
-
</style>`;
|
|
30910
|
-
}
|
|
31072
|
+
var o = prepareDatasetForSVG(dataset, opts);
|
|
31073
|
+
var svgFile = {
|
|
31074
|
+
content: renderSVGDocument(o.dataset, o.frame, o.dataset.layers, o.opts),
|
|
31075
|
+
filename: o.opts.file || getOutputFileBase(o.dataset) + '.svg'
|
|
31076
|
+
};
|
|
31077
|
+
return [svgFile].concat(o.opts.svg_image_files);
|
|
31078
|
+
}
|
|
30911
31079
|
|
|
31080
|
+
// Returns a copy of @dataset (unless opts.final is set) with coordinates
|
|
31081
|
+
// transformed to the pixel space of the output frame, along with the frame and
|
|
31082
|
+
// the export options, which are given SVG defaults.
|
|
31083
|
+
function prepareDatasetForSVG(dataset, opts) {
|
|
31084
|
+
var frame;
|
|
30912
31085
|
// TODO: consider moving this logic to mapshaper-export.js
|
|
30913
31086
|
if (opts.final) {
|
|
30914
31087
|
if (dataset.arcs) dataset.arcs.flatten();
|
|
@@ -30924,16 +31097,35 @@
|
|
|
30924
31097
|
frame = getFrameData(dataset, opts);
|
|
30925
31098
|
fitDatasetToFrame(dataset, frame);
|
|
30926
31099
|
setCoordinatePrecision(dataset, opts.precision || 0.01);
|
|
30927
|
-
if (opts.metadata) {
|
|
30928
|
-
metadataJSON = JSON.stringify(getGeospatialMetadata(dataset, frame));
|
|
30929
|
-
}
|
|
30930
31100
|
|
|
30931
31101
|
// error if one or more svg_data fields are not present in any layers
|
|
30932
31102
|
if (opts.svg_data) validateSvgDataFields(dataset.layers, opts.svg_data);
|
|
31103
|
+
return {dataset: dataset, frame: frame, opts: opts};
|
|
31104
|
+
}
|
|
31105
|
+
|
|
31106
|
+
// Renders @layers, a subset of the layers of a dataset prepared by
|
|
31107
|
+
// prepareDatasetForSVG(), as an SVG document.
|
|
31108
|
+
function renderSVGDocument(dataset, frame, layers, opts) {
|
|
31109
|
+
var namespace = 'xmlns="http://www.w3.org/2000/svg"';
|
|
31110
|
+
var defs = [];
|
|
31111
|
+
var frameLyr, frameBackground, frameNeatline;
|
|
31112
|
+
var svg, metadataJSON;
|
|
31113
|
+
var style = '';
|
|
31114
|
+
|
|
31115
|
+
// kludge for map keys
|
|
31116
|
+
if (opts.crisp_paths) {
|
|
31117
|
+
style = `
|
|
31118
|
+
<style>
|
|
31119
|
+
path {shape-rendering: crispEdges;}
|
|
31120
|
+
</style>`;
|
|
31121
|
+
}
|
|
31122
|
+
|
|
31123
|
+
if (opts.metadata) {
|
|
31124
|
+
metadataJSON = JSON.stringify(getGeospatialMetadata(dataset, frame));
|
|
31125
|
+
}
|
|
30933
31126
|
|
|
30934
|
-
layers = dataset.layers;
|
|
30935
31127
|
if (opts.scalebar) {
|
|
30936
|
-
layers.
|
|
31128
|
+
layers = layers.concat(getScalebarLayer({})); // default options
|
|
30937
31129
|
}
|
|
30938
31130
|
frameLyr = layers.find(function(lyr) {
|
|
30939
31131
|
return isFrameLayer(lyr, dataset.arcs);
|
|
@@ -30989,13 +31181,7 @@
|
|
|
30989
31181
|
<svg ${namespace} version="1.2" baseProfile="tiny" width="%d" height="%d" viewBox="%s %s %s %s" ${lineProps}>${style}
|
|
30990
31182
|
${svg}
|
|
30991
31183
|
</svg>`;
|
|
30992
|
-
|
|
30993
|
-
svgFile = {
|
|
30994
|
-
content: svg,
|
|
30995
|
-
filename: opts.file || getOutputFileBase(dataset) + '.svg'
|
|
30996
|
-
};
|
|
30997
|
-
files = [svgFile].concat(opts.svg_image_files);
|
|
30998
|
-
return files;
|
|
31184
|
+
return utils.format(template, frame.width, frame.height, 0, 0, frame.width, frame.height);
|
|
30999
31185
|
}
|
|
31000
31186
|
|
|
31001
31187
|
function exportFrameStylePhase(lyr, dataset, opts, phase) {
|
|
@@ -31059,7 +31245,7 @@ ${svg}
|
|
|
31059
31245
|
|
|
31060
31246
|
function getSvgFileBase(dataset, opts) {
|
|
31061
31247
|
var file = opts.file || getOutputFileBase(dataset) + '.svg';
|
|
31062
|
-
return file.replace(/\.svg$/i, '');
|
|
31248
|
+
return file.replace(/\.(svg|html?)$/i, '');
|
|
31063
31249
|
}
|
|
31064
31250
|
|
|
31065
31251
|
function getMetadataBlock(metadataJSON, viewBox) {
|
|
@@ -31558,12 +31744,1064 @@ ${svg}
|
|
|
31558
31744
|
featureHasLabel: featureHasLabel,
|
|
31559
31745
|
featureHasSvgSymbol: featureHasSvgSymbol,
|
|
31560
31746
|
getEmptyLayerForSVG: getEmptyLayerForSVG,
|
|
31747
|
+
getJpegQuality: getJpegQuality,
|
|
31561
31748
|
getLabelTextDefaults: getLabelTextDefaults,
|
|
31562
31749
|
layerHasLabels: layerHasLabels,
|
|
31563
31750
|
layerHasSvgSymbols: layerHasSvgSymbols,
|
|
31751
|
+
prepareDatasetForSVG: prepareDatasetForSVG,
|
|
31752
|
+
renderSVGDocument: renderSVGDocument,
|
|
31564
31753
|
validateSvgDataFields: validateSvgDataFields
|
|
31565
31754
|
});
|
|
31566
31755
|
|
|
31756
|
+
// Finding the font file a label's font-family names, among the fonts installed
|
|
31757
|
+
// on this computer.
|
|
31758
|
+
//
|
|
31759
|
+
// The browser does this for us in the GUI; in Node there is nothing between
|
|
31760
|
+
// mapshaper and the filesystem, so a family name has to be matched against the
|
|
31761
|
+
// name tables of the files themselves. Names cannot be inferred from
|
|
31762
|
+
// filenames: NYTFranklinLight.otf calls itself "NYTFranklin Light", and a
|
|
31763
|
+
// label asking for NYTFranklin at weight 300 has to reach it.
|
|
31764
|
+
//
|
|
31765
|
+
// See docs/development/label-tool-design.md.
|
|
31766
|
+
|
|
31767
|
+
// Collections (.ttc/.otc) hold several faces in one file and are how macOS
|
|
31768
|
+
// ships Helvetica, Menlo and Avenir, so a lookup that skipped them would miss
|
|
31769
|
+
// the font mapshaper's own tool writes by default there.
|
|
31770
|
+
var FONT_FILE_RXP = /\.(ttf|otf|ttc|otc)$/i;
|
|
31771
|
+
|
|
31772
|
+
// What a generic family means when there is no browser to resolve it. A guess,
|
|
31773
|
+
// but an ordered one: the first of these that is installed is what a browser
|
|
31774
|
+
// on this platform would almost certainly have picked.
|
|
31775
|
+
var GENERIC_FAMILIES$1 = {
|
|
31776
|
+
'sans-serif': ['Helvetica', 'Arial', 'Liberation Sans', 'DejaVu Sans', 'Roboto', 'Segoe UI'],
|
|
31777
|
+
serif: ['Times New Roman', 'Times', 'Liberation Serif', 'DejaVu Serif', 'Georgia'],
|
|
31778
|
+
monospace: ['Menlo', 'Courier New', 'Liberation Mono', 'DejaVu Sans Mono', 'Consolas'],
|
|
31779
|
+
'system-ui': ['Helvetica Neue', 'Segoe UI', 'Cantarell', 'Roboto'],
|
|
31780
|
+
cursive: [],
|
|
31781
|
+
fantasy: []
|
|
31782
|
+
};
|
|
31783
|
+
|
|
31784
|
+
var faceCache = {};
|
|
31785
|
+
var familyCache = {};
|
|
31786
|
+
var fullIndex = null;
|
|
31787
|
+
var fileList = null;
|
|
31788
|
+
|
|
31789
|
+
// Width classes, as OS/2 numbers them 1 to 9. A font-stretch is one of these
|
|
31790
|
+
// keywords or a percentage of normal width.
|
|
31791
|
+
var STRETCH_NAMES = {
|
|
31792
|
+
'ultra-condensed': 1, 'extra-condensed': 2, condensed: 3,
|
|
31793
|
+
'semi-condensed': 4, normal: 5, 'semi-expanded': 6, expanded: 7,
|
|
31794
|
+
'extra-expanded': 8, 'ultra-expanded': 9
|
|
31795
|
+
};
|
|
31796
|
+
var STRETCH_PERCENTS = [50, 62.5, 75, 87.5, 100, 112.5, 125, 150, 200];
|
|
31797
|
+
|
|
31798
|
+
// The face to open for @family in (@weight, @italic, @stretch), or null if this
|
|
31799
|
+
// computer has no such font: {path, postscriptName}.
|
|
31800
|
+
//
|
|
31801
|
+
// postscriptName is how a face inside a collection is named to fontkit, and is
|
|
31802
|
+
// null for a file holding one face.
|
|
31803
|
+
function findFontFace(family, weight, italic, stretch) {
|
|
31804
|
+
var key = [family, weight, italic ? 'i' : 'n', stretch || ''].join('|');
|
|
31805
|
+
if (!(key in faceCache)) {
|
|
31806
|
+
faceCache[key] = lookupFace(family, weight, italic, stretch);
|
|
31807
|
+
}
|
|
31808
|
+
return faceCache[key];
|
|
31809
|
+
}
|
|
31810
|
+
|
|
31811
|
+
// Which of a family's faces answers a request for (@weight, @italic,
|
|
31812
|
+
// @stretch).
|
|
31813
|
+
//
|
|
31814
|
+
// Width first, then upright before oblique, then the nearest weight, then the
|
|
31815
|
+
// heavier of two equally near. The first three are the order CSS matches fonts
|
|
31816
|
+
// in, and the last is the rule the style menu uses to carry a face across a
|
|
31817
|
+
// change of font (getNearestVariant() in gui-label-fonts.mjs), applied here to
|
|
31818
|
+
// faces read from files rather than measured in a browser.
|
|
31819
|
+
//
|
|
31820
|
+
// A missing face is answered with a near one rather than refused: the browser
|
|
31821
|
+
// would synthesize the missing weight or slant from exactly this face, so its
|
|
31822
|
+
// widths are much closer to what is drawn than no measurement at all.
|
|
31823
|
+
function pickFace(faces, weight, italic, stretch) {
|
|
31824
|
+
var wanted = weight > 0 ? weight : 400;
|
|
31825
|
+
var wantedWidth = parseFontStretch(stretch);
|
|
31826
|
+
var best = null;
|
|
31827
|
+
var bestScore = null;
|
|
31828
|
+
(faces || []).forEach(function(face) {
|
|
31829
|
+
var score = [
|
|
31830
|
+
Math.abs((face.width || 5) - wantedWidth),
|
|
31831
|
+
!!face.italic === !!italic ? 0 : 1,
|
|
31832
|
+
Math.abs(face.weight - wanted),
|
|
31833
|
+
face.weight < wanted ? 1 : 0
|
|
31834
|
+
];
|
|
31835
|
+
if (!best || compareScores(score, bestScore) < 0) {
|
|
31836
|
+
best = face;
|
|
31837
|
+
bestScore = score;
|
|
31838
|
+
}
|
|
31839
|
+
});
|
|
31840
|
+
return best;
|
|
31841
|
+
}
|
|
31842
|
+
|
|
31843
|
+
// A font-stretch as the width class it names, or normal width for anything
|
|
31844
|
+
// unreadable. A percentage is taken to the nearest class, the way CSS defines
|
|
31845
|
+
// the keywords.
|
|
31846
|
+
function parseFontStretch(stretch) {
|
|
31847
|
+
var str = String(stretch === null || stretch === undefined ? '' : stretch).trim().toLowerCase();
|
|
31848
|
+
var pct = /^([.0-9]+)%$/.exec(str);
|
|
31849
|
+
var best = 5;
|
|
31850
|
+
if (STRETCH_NAMES[str]) return STRETCH_NAMES[str];
|
|
31851
|
+
if (!pct) return 5;
|
|
31852
|
+
STRETCH_PERCENTS.forEach(function(val, i) {
|
|
31853
|
+
if (Math.abs(val - Number(pct[1])) <
|
|
31854
|
+
Math.abs(STRETCH_PERCENTS[best - 1] - Number(pct[1]))) {
|
|
31855
|
+
best = i + 1;
|
|
31856
|
+
}
|
|
31857
|
+
});
|
|
31858
|
+
return best;
|
|
31859
|
+
}
|
|
31860
|
+
|
|
31861
|
+
// Family names are compared without spaces, punctuation or case, so that
|
|
31862
|
+
// "NYTFranklin" finds "NYT Franklin" and "Helvetica Neue" finds
|
|
31863
|
+
// "HelveticaNeue". Font vendors are not consistent about any of the three.
|
|
31864
|
+
function normalizeFamilyName(name) {
|
|
31865
|
+
return String(name || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
31866
|
+
}
|
|
31867
|
+
|
|
31868
|
+
// The families a face answers to. The typographic family (name ID 16) is the
|
|
31869
|
+
// one a stylesheet means: every weight of NYTFranklin has its own ID 1 family
|
|
31870
|
+
// ("NYTFranklin Light", "NYTFranklin Medium") and they are one family only
|
|
31871
|
+
// under ID 16. Both are indexed, because plenty of fonts have no ID 16.
|
|
31872
|
+
function getFaceFamilies(font) {
|
|
31873
|
+
var records = font && font.name && font.name.records || {};
|
|
31874
|
+
return [pickName(records.preferredFamily), pickName(records.fontFamily),
|
|
31875
|
+
font && font.familyName].filter(Boolean);
|
|
31876
|
+
}
|
|
31877
|
+
|
|
31878
|
+
function clearFontCache() {
|
|
31879
|
+
faceCache = {};
|
|
31880
|
+
familyCache = {};
|
|
31881
|
+
fullIndex = null;
|
|
31882
|
+
fileList = null;
|
|
31883
|
+
}
|
|
31884
|
+
|
|
31885
|
+
function lookupFace(family, weight, italic, stretch) {
|
|
31886
|
+
var names = resolveFamilyNames(family);
|
|
31887
|
+
var face = findBestFace(names, weight, italic, stretch);
|
|
31888
|
+
// The filename guess can find a family and still miss one of its faces:
|
|
31889
|
+
// Segoe UI Semibold lives in seguisb.ttf, which does not begin with the
|
|
31890
|
+
// family's name. Anything other than the face that was asked for is worth
|
|
31891
|
+
// the full index -- once per session -- to be sure it is the nearest this
|
|
31892
|
+
// machine has.
|
|
31893
|
+
if (!fullIndex && !faceAnswersRequest(face, weight, italic, stretch)) {
|
|
31894
|
+
fullIndex = readFaces(getFontFiles());
|
|
31895
|
+
face = findBestFace(names, weight, italic, stretch) || face;
|
|
31896
|
+
}
|
|
31897
|
+
return face;
|
|
31898
|
+
}
|
|
31899
|
+
|
|
31900
|
+
function findBestFace(names, weight, italic, stretch) {
|
|
31901
|
+
for (var i = 0; i < names.length; i++) {
|
|
31902
|
+
var faces = findFamilyFaces(names[i]);
|
|
31903
|
+
if (faces && faces.length > 0) return pickFace(faces, weight, italic, stretch);
|
|
31904
|
+
}
|
|
31905
|
+
return null;
|
|
31906
|
+
}
|
|
31907
|
+
|
|
31908
|
+
function faceAnswersRequest(face, weight, italic, stretch) {
|
|
31909
|
+
return !!face && face.weight == (weight > 0 ? weight : 400) &&
|
|
31910
|
+
!!face.italic === !!italic && (face.width || 5) == parseFontStretch(stretch);
|
|
31911
|
+
}
|
|
31912
|
+
|
|
31913
|
+
// A font-family is a list, and may end in a generic: each name is tried in
|
|
31914
|
+
// turn, exactly as a browser would, and a generic stands for the first of its
|
|
31915
|
+
// candidates that is installed.
|
|
31916
|
+
function resolveFamilyNames(family) {
|
|
31917
|
+
var out = [];
|
|
31918
|
+
splitFamilyList(family).forEach(function(name) {
|
|
31919
|
+
var generic = GENERIC_FAMILIES$1[name.toLowerCase()];
|
|
31920
|
+
if (generic) {
|
|
31921
|
+
out = out.concat(generic);
|
|
31922
|
+
} else {
|
|
31923
|
+
out.push(name);
|
|
31924
|
+
}
|
|
31925
|
+
});
|
|
31926
|
+
return out;
|
|
31927
|
+
}
|
|
31928
|
+
|
|
31929
|
+
function splitFamilyList(family) {
|
|
31930
|
+
return String(family || '').split(',').map(function(name) {
|
|
31931
|
+
return name.trim().replace(/^['"]|['"]$/g, '');
|
|
31932
|
+
}).filter(Boolean);
|
|
31933
|
+
}
|
|
31934
|
+
|
|
31935
|
+
// Two passes, because parsing every font on the computer costs the best part
|
|
31936
|
+
// of a second and most lookups do not need it: the file holding a family is
|
|
31937
|
+
// usually named after it, so files whose name begins with the family's are
|
|
31938
|
+
// parsed first. Whether that was good enough is lookupFace()'s decision.
|
|
31939
|
+
function findFamilyFaces(family) {
|
|
31940
|
+
var key = normalizeFamilyName(family);
|
|
31941
|
+
if (!key) return null;
|
|
31942
|
+
if (fullIndex) return fullIndex[key] || null;
|
|
31943
|
+
if (!(key in familyCache)) {
|
|
31944
|
+
familyCache[key] = readFaces(getLikelyFiles(key))[key] || null;
|
|
31945
|
+
}
|
|
31946
|
+
return familyCache[key];
|
|
31947
|
+
}
|
|
31948
|
+
|
|
31949
|
+
function getLikelyFiles(key) {
|
|
31950
|
+
return getFontFiles().filter(function(file) {
|
|
31951
|
+
return normalizeFamilyName(basename(file)).indexOf(key) === 0;
|
|
31952
|
+
});
|
|
31953
|
+
}
|
|
31954
|
+
|
|
31955
|
+
// Faces by normalized family name. A file that cannot be parsed is skipped
|
|
31956
|
+
// rather than reported: a font directory can hold anything, and a broken font
|
|
31957
|
+
// is not an error in the user's data.
|
|
31958
|
+
function readFaces(files) {
|
|
31959
|
+
var index = {};
|
|
31960
|
+
files.forEach(function(file) {
|
|
31961
|
+
getFileFaces(file).forEach(function(face) {
|
|
31962
|
+
face.families.forEach(function(name) {
|
|
31963
|
+
var key = normalizeFamilyName(name);
|
|
31964
|
+
if (!key) return;
|
|
31965
|
+
if (!index[key]) index[key] = [];
|
|
31966
|
+
index[key].push(face);
|
|
31967
|
+
});
|
|
31968
|
+
});
|
|
31969
|
+
});
|
|
31970
|
+
return index;
|
|
31971
|
+
}
|
|
31972
|
+
|
|
31973
|
+
function getFileFaces(file) {
|
|
31974
|
+
var fontkit = getFontkit();
|
|
31975
|
+
var font, fonts;
|
|
31976
|
+
if (!fontkit) return [];
|
|
31977
|
+
try {
|
|
31978
|
+
font = fontkit.openSync(file);
|
|
31979
|
+
// A collection reports its members in .fonts; a single font is its own.
|
|
31980
|
+
fonts = font && font.fonts || [font];
|
|
31981
|
+
return fonts.filter(Boolean).map(function(one) {
|
|
31982
|
+
return {
|
|
31983
|
+
path: file,
|
|
31984
|
+
// Named rather than numbered because fontkit takes a name, and because
|
|
31985
|
+
// a name survives a font being reinstalled in a different order.
|
|
31986
|
+
postscriptName: font.fonts ? one.postscriptName : null,
|
|
31987
|
+
families: getFaceFamilies(one),
|
|
31988
|
+
weight: getFaceWeight(one),
|
|
31989
|
+
width: getFaceWidth(one),
|
|
31990
|
+
italic: isItalicFace(one)
|
|
31991
|
+
};
|
|
31992
|
+
}).filter(function(face) {
|
|
31993
|
+
return face.families.length > 0;
|
|
31994
|
+
});
|
|
31995
|
+
} catch (e) {
|
|
31996
|
+
return [];
|
|
31997
|
+
}
|
|
31998
|
+
}
|
|
31999
|
+
|
|
32000
|
+
function getFaceWeight(font) {
|
|
32001
|
+
var os2 = font['OS/2'];
|
|
32002
|
+
var weight = os2 && os2.usWeightClass;
|
|
32003
|
+
return weight > 0 ? weight : 400;
|
|
32004
|
+
}
|
|
32005
|
+
|
|
32006
|
+
function getFaceWidth(font) {
|
|
32007
|
+
var os2 = font['OS/2'];
|
|
32008
|
+
var width = os2 && os2.usWidthClass;
|
|
32009
|
+
return width >= 1 && width <= 9 ? width : 5;
|
|
32010
|
+
}
|
|
32011
|
+
|
|
32012
|
+
function isItalicFace(font) {
|
|
32013
|
+
var os2 = font['OS/2'];
|
|
32014
|
+
if (os2 && os2.fsSelection && typeof os2.fsSelection.italic == 'boolean') {
|
|
32015
|
+
return os2.fsSelection.italic;
|
|
32016
|
+
}
|
|
32017
|
+
return !!font.italicAngle;
|
|
32018
|
+
}
|
|
32019
|
+
|
|
32020
|
+
// A name record is a string, or an object of translations to pick English out
|
|
32021
|
+
// of, depending on the font and the version of fontkit.
|
|
32022
|
+
function pickName(rec) {
|
|
32023
|
+
if (!rec) return '';
|
|
32024
|
+
if (typeof rec == 'string') return rec;
|
|
32025
|
+
return rec.en || Object.keys(rec).map(function(k) { return rec[k]; })[0] || '';
|
|
32026
|
+
}
|
|
32027
|
+
|
|
32028
|
+
function compareScores(a, b) {
|
|
32029
|
+
for (var i = 0; i < a.length; i++) {
|
|
32030
|
+
if (a[i] != b[i]) return a[i] - b[i];
|
|
32031
|
+
}
|
|
32032
|
+
return 0;
|
|
32033
|
+
}
|
|
32034
|
+
|
|
32035
|
+
// Every font file on this computer, listed once. Listing is cheap -- a few
|
|
32036
|
+
// hundred directory entries -- next to parsing them, so this is not the part
|
|
32037
|
+
// worth avoiding.
|
|
32038
|
+
function getFontFiles() {
|
|
32039
|
+
var fs = getFs();
|
|
32040
|
+
var files = [];
|
|
32041
|
+
if (fileList) return fileList;
|
|
32042
|
+
if (!fs) return [];
|
|
32043
|
+
getFontDirs().forEach(function(dir) {
|
|
32044
|
+
var entries;
|
|
32045
|
+
try {
|
|
32046
|
+
entries = fs.readdirSync(dir, {recursive: true});
|
|
32047
|
+
} catch (e) {
|
|
32048
|
+
return; // a directory this platform does not have
|
|
32049
|
+
}
|
|
32050
|
+
entries.forEach(function(entry) {
|
|
32051
|
+
var file = dir + '/' + String(entry).split('\\').join('/');
|
|
32052
|
+
if (FONT_FILE_RXP.test(file)) files.push(file);
|
|
32053
|
+
});
|
|
32054
|
+
});
|
|
32055
|
+
fileList = files;
|
|
32056
|
+
return files;
|
|
32057
|
+
}
|
|
32058
|
+
|
|
32059
|
+
// Where each platform keeps fonts, or MAPSHAPER_FONT_PATH if it is set.
|
|
32060
|
+
//
|
|
32061
|
+
// It replaces the platform's directories rather than adding to them, which is
|
|
32062
|
+
// what makes a machine's font situation something a caller can state: a
|
|
32063
|
+
// container with its fonts somewhere of its own, a build that has to produce
|
|
32064
|
+
// the same SVG wherever it runs, a test that needs to know there is nothing to
|
|
32065
|
+
// find. Several directories are separated by : or ;.
|
|
32066
|
+
function getFontDirs() {
|
|
32067
|
+
var home = getHomeDir();
|
|
32068
|
+
var platform = typeof process == 'object' && process.platform || '';
|
|
32069
|
+
var dirs = getEnvDirs();
|
|
32070
|
+
if (dirs.length > 0) return dirs;
|
|
32071
|
+
if (platform == 'darwin') {
|
|
32072
|
+
dirs = ['/System/Library/Fonts', '/Library/Fonts',
|
|
32073
|
+
'/Network/Library/Fonts'];
|
|
32074
|
+
if (home) dirs.push(home + '/Library/Fonts');
|
|
32075
|
+
} else if (platform == 'win32') {
|
|
32076
|
+
dirs = [(getEnv('WINDIR') || 'C:\\Windows') + '/Fonts'];
|
|
32077
|
+
if (home) dirs.push(home + '/AppData/Local/Microsoft/Windows/Fonts');
|
|
32078
|
+
} else {
|
|
32079
|
+
dirs = ['/usr/share/fonts', '/usr/local/share/fonts', '/run/host/fonts'];
|
|
32080
|
+
if (home) {
|
|
32081
|
+
dirs.push(home + '/.fonts', home + '/.local/share/fonts');
|
|
32082
|
+
}
|
|
32083
|
+
}
|
|
32084
|
+
return dirs;
|
|
32085
|
+
}
|
|
32086
|
+
|
|
32087
|
+
function getEnvDirs() {
|
|
32088
|
+
var val = getEnv('MAPSHAPER_FONT_PATH');
|
|
32089
|
+
if (!val) return [];
|
|
32090
|
+
return val.split(/[:;]/).filter(Boolean);
|
|
32091
|
+
}
|
|
32092
|
+
|
|
32093
|
+
function getEnv(name) {
|
|
32094
|
+
return typeof process == 'object' && process.env && process.env[name] || '';
|
|
32095
|
+
}
|
|
32096
|
+
|
|
32097
|
+
function getHomeDir() {
|
|
32098
|
+
var os = safeRequire('os');
|
|
32099
|
+
try {
|
|
32100
|
+
return os && os.homedir() || '';
|
|
32101
|
+
} catch (e) {
|
|
32102
|
+
return '';
|
|
32103
|
+
}
|
|
32104
|
+
}
|
|
32105
|
+
|
|
32106
|
+
function basename(file) {
|
|
32107
|
+
var parts = file.split('/');
|
|
32108
|
+
return parts[parts.length - 1].replace(FONT_FILE_RXP, '');
|
|
32109
|
+
}
|
|
32110
|
+
|
|
32111
|
+
function getFs() {
|
|
32112
|
+
return safeRequire('fs');
|
|
32113
|
+
}
|
|
32114
|
+
|
|
32115
|
+
// Loaded through the require shim and only when a label actually needs
|
|
32116
|
+
// measuring, so that the browser bundle -- which is this same file -- never
|
|
32117
|
+
// reaches for a module it does not have, and a CLI run that touches no labels
|
|
32118
|
+
// never pays for loading it.
|
|
32119
|
+
function getFontkit() {
|
|
32120
|
+
return safeRequire('fontkit');
|
|
32121
|
+
}
|
|
32122
|
+
|
|
32123
|
+
function safeRequire(name) {
|
|
32124
|
+
if (runningInBrowser()) return null;
|
|
32125
|
+
try {
|
|
32126
|
+
return require$1(name) || null;
|
|
32127
|
+
} catch (e) {
|
|
32128
|
+
return null;
|
|
32129
|
+
}
|
|
32130
|
+
}
|
|
32131
|
+
|
|
32132
|
+
var FontLookup = /*#__PURE__*/Object.freeze({
|
|
32133
|
+
__proto__: null,
|
|
32134
|
+
clearFontCache: clearFontCache,
|
|
32135
|
+
findFontFace: findFontFace,
|
|
32136
|
+
getFaceFamilies: getFaceFamilies,
|
|
32137
|
+
normalizeFamilyName: normalizeFamilyName,
|
|
32138
|
+
parseFontStretch: parseFontStretch,
|
|
32139
|
+
pickFace: pickFace
|
|
32140
|
+
});
|
|
32141
|
+
|
|
32142
|
+
// Renders an SVG document as a PNG or JPEG image.
|
|
32143
|
+
//
|
|
32144
|
+
// Node uses resvg, compiled to WebAssembly, which cannot see the fonts
|
|
32145
|
+
// installed on the computer: the files for the families a document names are
|
|
32146
|
+
// found the way label measurement finds them (see mapshaper-font-lookup.mjs)
|
|
32147
|
+
// and handed to it. The browser draws the document on a canvas with its own
|
|
32148
|
+
// fonts.
|
|
32149
|
+
|
|
32150
|
+
var resvgPromise = null;
|
|
32151
|
+
|
|
32152
|
+
// opts:
|
|
32153
|
+
// width, height size of the document, in CSS pixels
|
|
32154
|
+
// scale image pixels per CSS pixel
|
|
32155
|
+
// format 'png' or 'jpeg' (JPEG is drawn over a white background)
|
|
32156
|
+
// quality JPEG quality, 1-100
|
|
32157
|
+
// Returns the bytes of the image file.
|
|
32158
|
+
async function rasterizeSVG(svg, opts) {
|
|
32159
|
+
if (runningInBrowser()) {
|
|
32160
|
+
return rasterizeInBrowser(svg, opts);
|
|
32161
|
+
}
|
|
32162
|
+
return rasterizeWithResvg(svg, opts);
|
|
32163
|
+
}
|
|
32164
|
+
|
|
32165
|
+
async function rasterizeWithResvg(svg, opts) {
|
|
32166
|
+
var resvg = await loadResvg();
|
|
32167
|
+
var renderer = new resvg.Resvg(svg, {
|
|
32168
|
+
fitTo: {mode: 'width', value: getImageSize(opts.width, opts.scale)},
|
|
32169
|
+
background: opts.format == 'jpeg' ? 'white' : undefined,
|
|
32170
|
+
font: svgHasText(svg) ? getResvgFontOptions(svg) : {fontBuffers: []}
|
|
32171
|
+
});
|
|
32172
|
+
var img = renderer.render();
|
|
32173
|
+
var content;
|
|
32174
|
+
try {
|
|
32175
|
+
if (opts.format == 'jpeg') {
|
|
32176
|
+
content = require$1('jpeg-js').encode({
|
|
32177
|
+
data: Buffer.from(img.pixels),
|
|
32178
|
+
width: img.width,
|
|
32179
|
+
height: img.height
|
|
32180
|
+
}, opts.quality).data;
|
|
32181
|
+
} else {
|
|
32182
|
+
content = Buffer.from(img.asPng());
|
|
32183
|
+
}
|
|
32184
|
+
} finally {
|
|
32185
|
+
// wasm memory is not garbage-collected
|
|
32186
|
+
img.free();
|
|
32187
|
+
renderer.free();
|
|
32188
|
+
}
|
|
32189
|
+
return content;
|
|
32190
|
+
}
|
|
32191
|
+
|
|
32192
|
+
function loadResvg() {
|
|
32193
|
+
var resvg, fs;
|
|
32194
|
+
if (!resvgPromise) {
|
|
32195
|
+
resvg = require$1('@resvg/resvg-wasm');
|
|
32196
|
+
fs = require$1('fs');
|
|
32197
|
+
// initWasm() throws if it is called a second time
|
|
32198
|
+
resvgPromise = resvg.initWasm(fs.readFileSync(require$1.resolve('@resvg/resvg-wasm/index_bg.wasm')))
|
|
32199
|
+
.then(function() { return resvg; });
|
|
32200
|
+
}
|
|
32201
|
+
return resvgPromise;
|
|
32202
|
+
}
|
|
32203
|
+
|
|
32204
|
+
function svgHasText(svg) {
|
|
32205
|
+
return /<text\b/.test(svg);
|
|
32206
|
+
}
|
|
32207
|
+
|
|
32208
|
+
// Regular and bold faces of each family named in @svg, plus the faces that
|
|
32209
|
+
// the generic families resolve to on this computer.
|
|
32210
|
+
function getResvgFontOptions(svg) {
|
|
32211
|
+
var paths = [];
|
|
32212
|
+
var opts = {fontBuffers: []};
|
|
32213
|
+
var generics = {
|
|
32214
|
+
sansSerifFamily: 'sans-serif',
|
|
32215
|
+
serifFamily: 'serif',
|
|
32216
|
+
monospaceFamily: 'monospace'
|
|
32217
|
+
};
|
|
32218
|
+
getFontFamilies(svg).concat(Object.values(generics)).forEach(function(family) {
|
|
32219
|
+
[400, 700].forEach(function(weight) {
|
|
32220
|
+
var face = findFontFace(family, weight, false);
|
|
32221
|
+
if (face && !paths.includes(face.path)) paths.push(face.path);
|
|
32222
|
+
});
|
|
32223
|
+
});
|
|
32224
|
+
Object.keys(generics).forEach(function(key) {
|
|
32225
|
+
var face = findFontFace(generics[key], 400, false);
|
|
32226
|
+
if (face) opts[key] = face.families[0];
|
|
32227
|
+
});
|
|
32228
|
+
opts.defaultFontFamily = opts.sansSerifFamily;
|
|
32229
|
+
opts.fontBuffers = paths.map(function(path) {
|
|
32230
|
+
return require$1('fs').readFileSync(path);
|
|
32231
|
+
});
|
|
32232
|
+
return opts;
|
|
32233
|
+
}
|
|
32234
|
+
|
|
32235
|
+
function getFontFamilies(svg) {
|
|
32236
|
+
var families = [];
|
|
32237
|
+
var rxp = /font-family(?:="|:\s*)([^";]+)/g;
|
|
32238
|
+
var match, family;
|
|
32239
|
+
while ((match = rxp.exec(svg)) !== null) {
|
|
32240
|
+
family = match[1].replace(/"|'/g, '"').trim();
|
|
32241
|
+
if (!families.includes(family)) families.push(family);
|
|
32242
|
+
}
|
|
32243
|
+
return families;
|
|
32244
|
+
}
|
|
32245
|
+
|
|
32246
|
+
async function rasterizeInBrowser(svg, opts) {
|
|
32247
|
+
var url = URL.createObjectURL(new Blob([svg], {type: 'image/svg+xml'}));
|
|
32248
|
+
var canvas = document.createElement('canvas');
|
|
32249
|
+
var img = new Image();
|
|
32250
|
+
var ctx, blob;
|
|
32251
|
+
try {
|
|
32252
|
+
img.src = url;
|
|
32253
|
+
await img.decode();
|
|
32254
|
+
canvas.width = getImageSize(opts.width, opts.scale);
|
|
32255
|
+
canvas.height = getImageSize(opts.height, opts.scale);
|
|
32256
|
+
ctx = canvas.getContext('2d');
|
|
32257
|
+
if (opts.format == 'jpeg') {
|
|
32258
|
+
ctx.fillStyle = 'white';
|
|
32259
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
32260
|
+
}
|
|
32261
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
32262
|
+
blob = await new Promise(function(resolve) {
|
|
32263
|
+
canvas.toBlob(resolve, 'image/' + opts.format, opts.quality / 100);
|
|
32264
|
+
});
|
|
32265
|
+
} finally {
|
|
32266
|
+
URL.revokeObjectURL(url);
|
|
32267
|
+
}
|
|
32268
|
+
if (!blob) stop$1('Unable to render the map image');
|
|
32269
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
32270
|
+
}
|
|
32271
|
+
|
|
32272
|
+
function getImageSize(cssPixels, scale) {
|
|
32273
|
+
return Math.max(1, Math.round(cssPixels * scale));
|
|
32274
|
+
}
|
|
32275
|
+
|
|
32276
|
+
// Font families in HTML output, which differ from the families a map's labels
|
|
32277
|
+
// are given (and that SVG output keeps) in two ways.
|
|
32278
|
+
//
|
|
32279
|
+
// Web fonts: a site's own fonts may be loaded under names unlike those of the
|
|
32280
|
+
// same fonts installed on a computer (NYTFranklin is nyt-franklin on
|
|
32281
|
+
// nytimes.com). They are listed before the installed fonts they stand for.
|
|
32282
|
+
// Only the two NYT fonts are mapped.
|
|
32283
|
+
//
|
|
32284
|
+
// Fallbacks: for pages viewed on devices that lack a map's fonts (or before
|
|
32285
|
+
// its web fonts load). A family list that contains a generic family is the
|
|
32286
|
+
// user's own fallback plan and is left alone. Any other list is given the
|
|
32287
|
+
// common fonts of the category its first font belongs to, then that category's
|
|
32288
|
+
// generic family -- the convention ai2html follows, e.g.
|
|
32289
|
+
// nyt-franklin,arial,helvetica,sans-serif.
|
|
32290
|
+
|
|
32291
|
+
var GENERIC_FAMILIES = ['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
|
|
32292
|
+
'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
|
|
32293
|
+
'math', 'emoji', 'fangsong'];
|
|
32294
|
+
|
|
32295
|
+
var FALLBACKS = {
|
|
32296
|
+
'sans-serif': ['Helvetica', 'Arial', 'sans-serif'],
|
|
32297
|
+
serif: ['Times New Roman', 'Times', 'serif'],
|
|
32298
|
+
monospace: ['Menlo', 'Consolas', 'Courier New', 'monospace']
|
|
32299
|
+
};
|
|
32300
|
+
|
|
32301
|
+
// Matched anywhere in a normalized family name, so that the list covers
|
|
32302
|
+
// variants like "Times New Roman", "Bodoni 72" and nyt-cheltenham.
|
|
32303
|
+
var SERIF_FONTS = ['times', 'georgia', 'garamond', 'baskerville', 'palatino',
|
|
32304
|
+
'bookantiqua', 'cambria', 'didot', 'bodoni', 'minion', 'caslon',
|
|
32305
|
+
'centuryschoolbook', 'charter', 'hoefler', 'merriweather', 'playfair',
|
|
32306
|
+
'cheltenham', 'imperial'];
|
|
32307
|
+
|
|
32308
|
+
var MONOSPACE_FONTS = ['courier', 'menlo', 'monaco', 'consolas',
|
|
32309
|
+
'lucidaconsole', 'andalemono', 'sfmono', 'inconsolata'];
|
|
32310
|
+
|
|
32311
|
+
// The web fonts that HTML maps on nytimes.com use. Installed NYT fonts have a
|
|
32312
|
+
// family for each weight ("NYTFranklin Medium") as well as one family for all
|
|
32313
|
+
// of them, which is the name mapshaper writes.
|
|
32314
|
+
var NYT_WEB_FONTS = [['NYTFranklin', 'nyt-franklin'], ['NYTCheltenham', 'nyt-cheltenham']];
|
|
32315
|
+
var NYT_WEIGHTS = {
|
|
32316
|
+
'': null, ExtraLight: '200', Light: '300', Book: '400', Medium: '500',
|
|
32317
|
+
Semibold: '600', Bold: '700', ExtraBold: '800', Headline: '700'
|
|
32318
|
+
};
|
|
32319
|
+
var WEB_FONTS = getNytWebFonts();
|
|
32320
|
+
|
|
32321
|
+
// Returns a CSS font-family value for @family, a font-family list
|
|
32322
|
+
function addFontFallbacks(family) {
|
|
32323
|
+
var names = parseFamilyList(family);
|
|
32324
|
+
var lowerNames, category;
|
|
32325
|
+
if (names.length === 0) return String(family);
|
|
32326
|
+
lowerNames = names.map(function(name) { return name.toLowerCase(); });
|
|
32327
|
+
if (!lowerNames.some(isGenericFamily)) {
|
|
32328
|
+
category = guessFontCategory(lowerNames[0]);
|
|
32329
|
+
FALLBACKS[category].forEach(function(name) {
|
|
32330
|
+
if (!lowerNames.includes(name.toLowerCase())) names.push(name);
|
|
32331
|
+
});
|
|
32332
|
+
}
|
|
32333
|
+
return names.map(formatFamilyName).join(', ');
|
|
32334
|
+
}
|
|
32335
|
+
|
|
32336
|
+
function getNytWebFonts() {
|
|
32337
|
+
var fonts = [];
|
|
32338
|
+
NYT_WEB_FONTS.forEach(function(pair) {
|
|
32339
|
+
Object.keys(NYT_WEIGHTS).forEach(function(weightName) {
|
|
32340
|
+
fonts.push({
|
|
32341
|
+
system: weightName ? pair[0] + ' ' + weightName : pair[0],
|
|
32342
|
+
web: pair[1],
|
|
32343
|
+
weight: NYT_WEIGHTS[weightName]
|
|
32344
|
+
});
|
|
32345
|
+
});
|
|
32346
|
+
});
|
|
32347
|
+
return fonts;
|
|
32348
|
+
}
|
|
32349
|
+
|
|
32350
|
+
// Adds the web font that stands for each system font in @style, a text class's
|
|
32351
|
+
// properties, just before that system font, which stays in the list for
|
|
32352
|
+
// computers that have it installed. The weight of a system family with a weight
|
|
32353
|
+
// in its name (like "NYTFranklin Medium") replaces the label's font-weight, if
|
|
32354
|
+
// it is the label's first font.
|
|
32355
|
+
function applyWebFonts(style) {
|
|
32356
|
+
var names = parseFamilyList(style['font-family']);
|
|
32357
|
+
var out = [];
|
|
32358
|
+
var weight = null;
|
|
32359
|
+
if (names.length === 0) return style;
|
|
32360
|
+
names.forEach(function(name, i) {
|
|
32361
|
+
var font = findWebFont(WEB_FONTS, name);
|
|
32362
|
+
if (font && !containsName(names, font.web) && !containsName(out, font.web)) {
|
|
32363
|
+
out.push(font.web);
|
|
32364
|
+
if (i === 0) weight = font.weight;
|
|
32365
|
+
}
|
|
32366
|
+
out.push(name);
|
|
32367
|
+
});
|
|
32368
|
+
style['font-family'] = out.map(formatFamilyName).join(', ');
|
|
32369
|
+
if (weight) style['font-weight'] = weight;
|
|
32370
|
+
return style;
|
|
32371
|
+
}
|
|
32372
|
+
|
|
32373
|
+
// System font names are compared as the font lookup compares them, so that
|
|
32374
|
+
// NYTFranklin also matches "NYT Franklin"
|
|
32375
|
+
function findWebFont(webFonts, name) {
|
|
32376
|
+
var key = normalizeFamilyName(name);
|
|
32377
|
+
return webFonts.find(function(font) {
|
|
32378
|
+
return normalizeFamilyName(font.system) == key;
|
|
32379
|
+
}) || null;
|
|
32380
|
+
}
|
|
32381
|
+
|
|
32382
|
+
// Web font names are compared exactly (but for case): nyt-franklin and
|
|
32383
|
+
// NYTFranklin normalize to the same name
|
|
32384
|
+
function containsName(names, name) {
|
|
32385
|
+
return names.some(function(name2) {
|
|
32386
|
+
return name2.toLowerCase() == name.toLowerCase();
|
|
32387
|
+
});
|
|
32388
|
+
}
|
|
32389
|
+
|
|
32390
|
+
function guessFontCategory(name) {
|
|
32391
|
+
var key = normalizeFamilyName(name);
|
|
32392
|
+
name = name.toLowerCase();
|
|
32393
|
+
if (containsFontKey(MONOSPACE_FONTS, key) || /\b(mono|code)\b/.test(name)) {
|
|
32394
|
+
return 'monospace';
|
|
32395
|
+
}
|
|
32396
|
+
if (containsFontKey(SERIF_FONTS, key) || /\bslab\b/.test(name) ||
|
|
32397
|
+
/serif/.test(name) && !/sans/.test(name)) {
|
|
32398
|
+
return 'serif';
|
|
32399
|
+
}
|
|
32400
|
+
return 'sans-serif';
|
|
32401
|
+
}
|
|
32402
|
+
|
|
32403
|
+
function containsFontKey(keys, key) {
|
|
32404
|
+
return keys.some(function(k) {
|
|
32405
|
+
return key.includes(k);
|
|
32406
|
+
});
|
|
32407
|
+
}
|
|
32408
|
+
|
|
32409
|
+
function isGenericFamily(name) {
|
|
32410
|
+
return GENERIC_FAMILIES.includes(name);
|
|
32411
|
+
}
|
|
32412
|
+
|
|
32413
|
+
function parseFamilyList(family) {
|
|
32414
|
+
return String(family || '').split(',').map(unquote).filter(Boolean);
|
|
32415
|
+
}
|
|
32416
|
+
|
|
32417
|
+
function unquote(name) {
|
|
32418
|
+
return name.trim().replace(/^['"]|['"]$/g, '').trim();
|
|
32419
|
+
}
|
|
32420
|
+
|
|
32421
|
+
// A name is quoted unless it is a generic family or a run of plain words, so
|
|
32422
|
+
// that CSS cannot read it as a keyword or a malformed identifier.
|
|
32423
|
+
function formatFamilyName(name) {
|
|
32424
|
+
if (isGenericFamily(name.toLowerCase())) return name;
|
|
32425
|
+
if (/^[a-z][a-z-]*$/i.test(name)) return name;
|
|
32426
|
+
return '"' + name.replace(/"/g, '') + '"';
|
|
32427
|
+
}
|
|
32428
|
+
|
|
32429
|
+
// Text styles in HTML output are written as CSS classes rather than as
|
|
32430
|
+
// attributes on each label: one class for each combination of style properties
|
|
32431
|
+
// in the map, so that a page can restyle a map's labels by editing a few rules.
|
|
32432
|
+
|
|
32433
|
+
var TEXT_TAGS = {text: true, tspan: true, textPath: true};
|
|
32434
|
+
|
|
32435
|
+
var TEXT_STYLE_PROPERTIES = [
|
|
32436
|
+
'font-family', 'font-size', 'font-style', 'font-weight', 'font-stretch',
|
|
32437
|
+
'letter-spacing', 'text-anchor', 'dominant-baseline', 'fill', 'fill-opacity',
|
|
32438
|
+
'opacity', 'stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray',
|
|
32439
|
+
'stroke-linejoin', 'paint-order'
|
|
32440
|
+
];
|
|
32441
|
+
|
|
32442
|
+
// The text defaults a layer's group carries for its labels to inherit (see
|
|
32443
|
+
// getLabelTextDefaults()), which are folded into the labels' own classes.
|
|
32444
|
+
var LAYER_TEXT_PROPERTIES = ['font-family', 'font-size', 'text-anchor'];
|
|
32445
|
+
|
|
32446
|
+
// A bare number is px in an SVG attribute, and not valid CSS for these.
|
|
32447
|
+
var LENGTH_PROPERTIES = {'font-size': true, 'letter-spacing': true, 'stroke-width': true};
|
|
32448
|
+
|
|
32449
|
+
// Moves the text style properties of the elements in @layers (SVG objects, one
|
|
32450
|
+
// per layer) into classes named @prefix + n. Returns the classes in the order
|
|
32451
|
+
// they were first used: [{name, style: {property: value, ...}}, ...]
|
|
32452
|
+
function convertTextStylesToClasses(layers, prefix) {
|
|
32453
|
+
var classes = [];
|
|
32454
|
+
var index = {};
|
|
32455
|
+
layers.forEach(function(lyr) {
|
|
32456
|
+
var inherited = moveProperties(lyr.properties, {}, LAYER_TEXT_PROPERTIES);
|
|
32457
|
+
procNode(lyr, inherited, false);
|
|
32458
|
+
});
|
|
32459
|
+
return classes;
|
|
32460
|
+
|
|
32461
|
+
function procNode(node, inherited, inText) {
|
|
32462
|
+
var style, name;
|
|
32463
|
+
if (TEXT_TAGS[node.tag]) {
|
|
32464
|
+
// text inside a text element inherits from it, not from the layer
|
|
32465
|
+
style = Object.assign({}, inText ? null : inherited);
|
|
32466
|
+
moveProperties(node.properties, style, TEXT_STYLE_PROPERTIES);
|
|
32467
|
+
if (Object.keys(style).length > 0) {
|
|
32468
|
+
name = getClassName(style);
|
|
32469
|
+
node.properties.class = node.properties.class ?
|
|
32470
|
+
node.properties.class + ' ' + name : name;
|
|
32471
|
+
}
|
|
32472
|
+
inText = true;
|
|
32473
|
+
}
|
|
32474
|
+
(node.children || []).forEach(function(child) {
|
|
32475
|
+
procNode(child, inherited, inText);
|
|
32476
|
+
});
|
|
32477
|
+
}
|
|
32478
|
+
|
|
32479
|
+
function getClassName(style) {
|
|
32480
|
+
var key = getStyleKey(style);
|
|
32481
|
+
if (!(key in index)) {
|
|
32482
|
+
index[key] = prefix + classes.length;
|
|
32483
|
+
classes.push({name: index[key], style: style});
|
|
32484
|
+
}
|
|
32485
|
+
return index[key];
|
|
32486
|
+
}
|
|
32487
|
+
}
|
|
32488
|
+
|
|
32489
|
+
// Returns CSS rules for @classes, scoped to @selector
|
|
32490
|
+
function formatTextClassesAsCss(classes, selector) {
|
|
32491
|
+
return classes.map(function(o) {
|
|
32492
|
+
var decl = Object.keys(o.style).map(function(k) {
|
|
32493
|
+
return k + ':' + formatCssValue(k, o.style[k]) + ';';
|
|
32494
|
+
}).join('');
|
|
32495
|
+
return selector + ' .' + o.name + ' {' + decl + '}';
|
|
32496
|
+
}).join('\n');
|
|
32497
|
+
}
|
|
32498
|
+
|
|
32499
|
+
function formatCssValue(name, val) {
|
|
32500
|
+
var str = sanitizeCssValue(val);
|
|
32501
|
+
if (LENGTH_PROPERTIES[name] && /^-?(\d+\.?\d*|\.\d+)$/.test(str)) {
|
|
32502
|
+
str += 'px';
|
|
32503
|
+
} else if (name == 'font-family') {
|
|
32504
|
+
str = addFontFallbacks(str);
|
|
32505
|
+
}
|
|
32506
|
+
return str;
|
|
32507
|
+
}
|
|
32508
|
+
|
|
32509
|
+
// Values come from data, and must not be able to end the declaration, the
|
|
32510
|
+
// rule or the <style> element they are written into.
|
|
32511
|
+
function sanitizeCssValue(val) {
|
|
32512
|
+
return String(val).replace(/[{}<>;\\]/g, '').trim();
|
|
32513
|
+
}
|
|
32514
|
+
|
|
32515
|
+
function getStyleKey(style) {
|
|
32516
|
+
return Object.keys(style).sort().map(function(k) {
|
|
32517
|
+
return k + ':' + style[k];
|
|
32518
|
+
}).join(';');
|
|
32519
|
+
}
|
|
32520
|
+
|
|
32521
|
+
function moveProperties(src, dest, names) {
|
|
32522
|
+
if (!src) return dest;
|
|
32523
|
+
names.forEach(function(k) {
|
|
32524
|
+
var val = src[k];
|
|
32525
|
+
delete src[k];
|
|
32526
|
+
// stringify() omits these, so they are not style
|
|
32527
|
+
if (!val && val !== 0) return;
|
|
32528
|
+
dest[k] = val;
|
|
32529
|
+
});
|
|
32530
|
+
return dest;
|
|
32531
|
+
}
|
|
32532
|
+
|
|
32533
|
+
// HTML output, modeled on the ai2html script for Adobe Illustrator: the map's
|
|
32534
|
+
// shapes are drawn in an image, and its point symbols -- labels, icons, dots --
|
|
32535
|
+
// are drawn over it as inline SVG. Each symbol is anchored to a position given
|
|
32536
|
+
// as a percentage of the map's width and height, so that a map that resizes
|
|
32537
|
+
// with its container keeps its symbols where they belong without scaling them.
|
|
32538
|
+
//
|
|
32539
|
+
// A label along a path is anchored the same way, at the point on its path
|
|
32540
|
+
// where its text is attached, and keeps the shape and size of its path.
|
|
32541
|
+
|
|
32542
|
+
var CLASS_PREFIX = 'ms-';
|
|
32543
|
+
|
|
32544
|
+
async function exportHTML(dataset, opts) {
|
|
32545
|
+
var responsiveness = getResponsiveness(opts);
|
|
32546
|
+
var imageFormat = getImageFormat(opts);
|
|
32547
|
+
var pixelRatio = getPixelRatio(opts);
|
|
32548
|
+
var o = prepareDatasetForSVG(dataset, Object.assign({}, opts, {
|
|
32549
|
+
// raster layers are resampled to the pixel density of the image
|
|
32550
|
+
raster_res: opts.raster_res || pixelRatio,
|
|
32551
|
+
linked_images: false
|
|
32552
|
+
}));
|
|
32553
|
+
var frame = o.frame;
|
|
32554
|
+
var base = getHtmlFileBase(o.dataset, opts);
|
|
32555
|
+
var layers = splitLayersForHTML(o.dataset.layers, o.dataset);
|
|
32556
|
+
var imageFile = base + (imageFormat == 'jpeg' ? '.jpg' : '.png');
|
|
32557
|
+
var image = await rasterizeSVG(renderSVGDocument(o.dataset, frame, layers.image, o.opts), {
|
|
32558
|
+
width: frame.width,
|
|
32559
|
+
height: frame.height,
|
|
32560
|
+
scale: pixelRatio,
|
|
32561
|
+
format: imageFormat,
|
|
32562
|
+
quality: getJpegQuality(opts)
|
|
32563
|
+
});
|
|
32564
|
+
var html = renderHtmlFragment({
|
|
32565
|
+
id: getContainerId(base),
|
|
32566
|
+
frame: frame,
|
|
32567
|
+
responsiveness: responsiveness,
|
|
32568
|
+
imageFile: imageFile,
|
|
32569
|
+
overlay: renderOverlay(o.dataset, frame, layers.overlay, o.opts)
|
|
32570
|
+
});
|
|
32571
|
+
return [{
|
|
32572
|
+
filename: opts.file || base + '.html',
|
|
32573
|
+
content: html
|
|
32574
|
+
}, {
|
|
32575
|
+
filename: imageFile,
|
|
32576
|
+
content: image
|
|
32577
|
+
}];
|
|
32578
|
+
}
|
|
32579
|
+
|
|
32580
|
+
// Point layers go in the overlay; everything else is drawn in the image,
|
|
32581
|
+
// beneath them. Layers are listed bottom to top.
|
|
32582
|
+
function splitLayersForHTML(layers, dataset) {
|
|
32583
|
+
var image = [], overlay = [];
|
|
32584
|
+
layers.forEach(function(lyr) {
|
|
32585
|
+
if (layerGoesInOverlay(lyr)) {
|
|
32586
|
+
overlay.push(lyr);
|
|
32587
|
+
} else {
|
|
32588
|
+
if (overlay.length > 0 && !isFrameLayer(lyr, dataset.arcs)) {
|
|
32589
|
+
warn(utils.format('Layer "%s" is drawn in the map image, beneath point layer "%s", which is above it in the layer order.',
|
|
32590
|
+
lyr.name || '[unnamed]', overlay[overlay.length - 1].name || '[unnamed]'));
|
|
32591
|
+
}
|
|
32592
|
+
image.push(lyr);
|
|
32593
|
+
}
|
|
32594
|
+
});
|
|
32595
|
+
return {image: image, overlay: overlay};
|
|
32596
|
+
}
|
|
32597
|
+
|
|
32598
|
+
function layerGoesInOverlay(lyr) {
|
|
32599
|
+
return lyr.geometry_type == 'point' && !layerHasFurniture(lyr) &&
|
|
32600
|
+
!layerHasRaster(lyr);
|
|
32601
|
+
}
|
|
32602
|
+
|
|
32603
|
+
function renderOverlay(dataset, frame, layers, opts) {
|
|
32604
|
+
var defs = [];
|
|
32605
|
+
var objects = layers.map(function(lyr) {
|
|
32606
|
+
var obj = renderOverlayLayer(lyr, dataset, frame, opts);
|
|
32607
|
+
convertPropertiesToDefinitions(obj, defs);
|
|
32608
|
+
return obj;
|
|
32609
|
+
});
|
|
32610
|
+
var classes = convertTextStylesToClasses(objects, CLASS_PREFIX + 'text-');
|
|
32611
|
+
classes.forEach(function(o) {
|
|
32612
|
+
applyWebFonts(o.style);
|
|
32613
|
+
});
|
|
32614
|
+
return {
|
|
32615
|
+
defs: defs,
|
|
32616
|
+
layers: objects,
|
|
32617
|
+
classes: classes
|
|
32618
|
+
};
|
|
32619
|
+
}
|
|
32620
|
+
|
|
32621
|
+
function renderOverlayLayer(lyr, dataset, frame, opts) {
|
|
32622
|
+
var layerObj = getEmptyLayerForSVG(lyr, opts);
|
|
32623
|
+
var geojson = exportDatasetAsGeoJSON(utils.defaults({layers: [lyr]}, dataset), opts);
|
|
32624
|
+
var features = geojson.features || geojson.geometries || (geojson.type ? [geojson] : []);
|
|
32625
|
+
var dataAttributes = getDataAttributes(lyr, opts);
|
|
32626
|
+
var report = initPathLabelReport();
|
|
32627
|
+
var anchors = [];
|
|
32628
|
+
// each feature is rendered at the origin, and placed by a container
|
|
32629
|
+
var localFeatures = features.map(function(feat, i) {
|
|
32630
|
+
var geom = feat && feat.type == 'Feature' ? feat.geometry : feat;
|
|
32631
|
+
var props = feat && feat.properties || {};
|
|
32632
|
+
var localGeom = null;
|
|
32633
|
+
anchors[i] = [];
|
|
32634
|
+
if (!geom || !geom.coordinates) ; else if (featureIsPathLabel(geom, props)) {
|
|
32635
|
+
anchors[i].push(getPathLabelAnchor(geom.coordinates, props));
|
|
32636
|
+
localGeom = {
|
|
32637
|
+
type: 'MultiPoint',
|
|
32638
|
+
coordinates: shiftCoords(geom.coordinates, anchors[i][0])
|
|
32639
|
+
};
|
|
32640
|
+
} else if (geom.type == 'Point' || geom.type == 'MultiPoint') {
|
|
32641
|
+
// the points of a multipoint feature (other than a path label) are drawn
|
|
32642
|
+
// with the same symbol
|
|
32643
|
+
anchors[i] = geom.type == 'Point' ? [geom.coordinates] : geom.coordinates;
|
|
32644
|
+
localGeom = {type: 'Point', coordinates: [0, 0]};
|
|
32645
|
+
}
|
|
32646
|
+
return {
|
|
32647
|
+
type: 'Feature',
|
|
32648
|
+
id: feat && feat.id,
|
|
32649
|
+
properties: props,
|
|
32650
|
+
geometry: localGeom
|
|
32651
|
+
};
|
|
32652
|
+
}).map(function(feat) {
|
|
32653
|
+
if (feat.id === undefined) delete feat.id;
|
|
32654
|
+
return feat;
|
|
32655
|
+
});
|
|
32656
|
+
var symbols = importGeoJSONFeatures(localFeatures, utils.defaults({
|
|
32657
|
+
path_label_report: report
|
|
32658
|
+
}, opts));
|
|
32659
|
+
reportPathLabels(report, lyr);
|
|
32660
|
+
|
|
32661
|
+
symbols.forEach(function(sym, i) {
|
|
32662
|
+
if (isEmptySymbol(sym)) return;
|
|
32663
|
+
removeOriginTransform(sym);
|
|
32664
|
+
anchors[i].forEach(function(xy, j) {
|
|
32665
|
+
layerObj.children.push({
|
|
32666
|
+
tag: 'svg',
|
|
32667
|
+
properties: Object.assign({
|
|
32668
|
+
x: formatPct(xy[0], frame.width),
|
|
32669
|
+
y: formatPct(xy[1], frame.height),
|
|
32670
|
+
overflow: 'visible'
|
|
32671
|
+
}, dataAttributes ? dataAttributes[i] : null),
|
|
32672
|
+
children: [j === 0 ? sym : copySymbolWithoutId(sym)]
|
|
32673
|
+
});
|
|
32674
|
+
});
|
|
32675
|
+
});
|
|
32676
|
+
return layerObj;
|
|
32677
|
+
}
|
|
32678
|
+
|
|
32679
|
+
// The point on a label's path where its text is attached: its start offset,
|
|
32680
|
+
// which is where text-anchor places the text.
|
|
32681
|
+
function getPathLabelAnchor(knots, rec) {
|
|
32682
|
+
var offset = String(rec['label-start-offset'] || getDefaultStartOffset(rec));
|
|
32683
|
+
var len = getCurveLength(knots);
|
|
32684
|
+
var dist = parseFloat(offset);
|
|
32685
|
+
var p;
|
|
32686
|
+
if (/%$/.test(offset)) {
|
|
32687
|
+
dist = dist / 100 * len;
|
|
32688
|
+
}
|
|
32689
|
+
p = getPointAtCurveLength(knots, utils.isFiniteNumber(dist) ? dist : len / 2) || knots[0];
|
|
32690
|
+
return [roundCoord$2(p[0]), roundCoord$2(p[1])];
|
|
32691
|
+
}
|
|
32692
|
+
|
|
32693
|
+
function shiftCoords(coords, origin) {
|
|
32694
|
+
return coords.map(function(p) {
|
|
32695
|
+
return [roundCoord$2(p[0] - origin[0]), roundCoord$2(p[1] - origin[1])];
|
|
32696
|
+
});
|
|
32697
|
+
}
|
|
32698
|
+
|
|
32699
|
+
function roundCoord$2(c) {
|
|
32700
|
+
return Math.round(c * 100) / 100;
|
|
32701
|
+
}
|
|
32702
|
+
|
|
32703
|
+
function getDataAttributes(lyr, opts) {
|
|
32704
|
+
var fields;
|
|
32705
|
+
if (!opts.svg_data || !lyr.data) return null;
|
|
32706
|
+
fields = opts.svg_data.includes('*') ? lyr.data.getFields() :
|
|
32707
|
+
opts.svg_data.filter(function(name) { return lyr.data.fieldExists(name); });
|
|
32708
|
+
return exportDataAttributesForSVG(lyr.data.getRecords(), fields);
|
|
32709
|
+
}
|
|
32710
|
+
|
|
32711
|
+
function isEmptySymbol(o) {
|
|
32712
|
+
return !o || o.tag == 'g' && (!o.children || o.children.length === 0);
|
|
32713
|
+
}
|
|
32714
|
+
|
|
32715
|
+
// Symbols rendered at the origin are given a transform that places them there
|
|
32716
|
+
function removeOriginTransform(o) {
|
|
32717
|
+
if (o.properties && o.properties.transform == 'translate(0 0)') {
|
|
32718
|
+
delete o.properties.transform;
|
|
32719
|
+
}
|
|
32720
|
+
}
|
|
32721
|
+
|
|
32722
|
+
// ids must be unique, so only the first copy of a multipoint symbol keeps it
|
|
32723
|
+
function copySymbolWithoutId(o) {
|
|
32724
|
+
var copy = JSON.parse(JSON.stringify(o));
|
|
32725
|
+
if (copy.properties) delete copy.properties.id;
|
|
32726
|
+
return copy;
|
|
32727
|
+
}
|
|
32728
|
+
|
|
32729
|
+
function formatPct(val, total) {
|
|
32730
|
+
var pct = total > 0 ? val / total * 100 : 0;
|
|
32731
|
+
return String(Math.round(pct * 1000) / 1000) + '%';
|
|
32732
|
+
}
|
|
32733
|
+
|
|
32734
|
+
function renderHtmlFragment(o) {
|
|
32735
|
+
var id = o.id;
|
|
32736
|
+
var w = o.frame.width;
|
|
32737
|
+
var h = o.frame.height;
|
|
32738
|
+
var overlay = o.overlay;
|
|
32739
|
+
var selector = '#' + id;
|
|
32740
|
+
var boxCss = o.responsiveness == 'dynamic' ?
|
|
32741
|
+
`width:100%;aspect-ratio:${w} / ${h};` :
|
|
32742
|
+
`width:${w}px;height:${h}px;`;
|
|
32743
|
+
var css = [
|
|
32744
|
+
`${selector} {position:relative;overflow:hidden;${boxCss}}`,
|
|
32745
|
+
`${selector} .${CLASS_PREFIX}image {position:absolute;top:0;left:0;width:100%;height:100%;max-width:none;margin:0;display:block;}`,
|
|
32746
|
+
`${selector} .${CLASS_PREFIX}overlay {position:absolute;top:0;left:0;width:100%;height:100%;overflow:visible;}`
|
|
32747
|
+
];
|
|
32748
|
+
var classCss = formatTextClassesAsCss(overlay.classes, selector);
|
|
32749
|
+
if (classCss) css.push(classCss);
|
|
32750
|
+
// Hidden from screen readers, which would otherwise read the labels as a
|
|
32751
|
+
// list of unrelated words, and each halo'd label twice
|
|
32752
|
+
var html = `<div id="${stringEscape(id)}" class="${CLASS_PREFIX}map" aria-hidden="true">
|
|
32753
|
+
<style>
|
|
32754
|
+
${css.join('\n')}
|
|
32755
|
+
</style>
|
|
32756
|
+
<img class="${CLASS_PREFIX}image" src="${stringEscape(o.imageFile)}" width="${w}" height="${h}" alt="">`;
|
|
32757
|
+
if (overlay.layers.length > 0) {
|
|
32758
|
+
html += '\n' + renderOverlaySvg(overlay);
|
|
32759
|
+
}
|
|
32760
|
+
return html + '\n</div>\n';
|
|
32761
|
+
}
|
|
32762
|
+
|
|
32763
|
+
function renderOverlaySvg(overlay) {
|
|
32764
|
+
var svg = `<svg class="${CLASS_PREFIX}overlay" stroke-linecap="round" stroke-linejoin="round">\n`;
|
|
32765
|
+
if (overlay.defs.length > 0) {
|
|
32766
|
+
svg += '<defs>\n' + utils.pluck(overlay.defs, 'svg').join('') + '</defs>\n';
|
|
32767
|
+
}
|
|
32768
|
+
svg += overlay.layers.map(stringify).join('\n');
|
|
32769
|
+
return svg + '\n</svg>';
|
|
32770
|
+
}
|
|
32771
|
+
|
|
32772
|
+
function getHtmlFileBase(dataset, opts) {
|
|
32773
|
+
return opts.file ? opts.file.replace(/\.html?$/i, '') : getOutputFileBase(dataset);
|
|
32774
|
+
}
|
|
32775
|
+
|
|
32776
|
+
function getContainerId(base) {
|
|
32777
|
+
return CLASS_PREFIX + base.replace(/[^\w-]+/g, '-');
|
|
32778
|
+
}
|
|
32779
|
+
|
|
32780
|
+
function getResponsiveness(opts) {
|
|
32781
|
+
var val = opts.responsiveness || 'fixed';
|
|
32782
|
+
if (val != 'fixed' && val != 'dynamic') {
|
|
32783
|
+
stop$1('Unsupported responsiveness= option:', val, '(expected fixed or dynamic)');
|
|
32784
|
+
}
|
|
32785
|
+
return val;
|
|
32786
|
+
}
|
|
32787
|
+
|
|
32788
|
+
function getImageFormat(opts) {
|
|
32789
|
+
var fmt = String(opts.image_format || 'png').toLowerCase();
|
|
32790
|
+
if (fmt == 'jpg') fmt = 'jpeg';
|
|
32791
|
+
if (fmt != 'png' && fmt != 'jpeg') {
|
|
32792
|
+
stop$1('Unsupported image-format= option:', opts.image_format, '(expected png or jpg)');
|
|
32793
|
+
}
|
|
32794
|
+
return fmt;
|
|
32795
|
+
}
|
|
32796
|
+
|
|
32797
|
+
function getPixelRatio(opts) {
|
|
32798
|
+
var val = opts.pixel_ratio === undefined ? 2 : opts.pixel_ratio;
|
|
32799
|
+
if (val > 0 === false || val > 8) {
|
|
32800
|
+
stop$1('Expected pixel-ratio= to be a number greater than 0 and no more than 8');
|
|
32801
|
+
}
|
|
32802
|
+
return val;
|
|
32803
|
+
}
|
|
32804
|
+
|
|
31567
32805
|
function exportKML(dataset, opts) {
|
|
31568
32806
|
var toKML = require$1("@placemarkio/tokml").toKML;
|
|
31569
32807
|
var geojsonOpts = Object.assign({combine_layers: true, geojson_type: 'FeatureCollection'}, opts);
|
|
@@ -40261,6 +41499,8 @@ ${svg}
|
|
|
40261
41499
|
format = 'geotiff';
|
|
40262
41500
|
} else if (ext == 'svg') {
|
|
40263
41501
|
format = 'svg';
|
|
41502
|
+
} else if (ext == 'html' || ext == 'htm') {
|
|
41503
|
+
format = 'html';
|
|
40264
41504
|
} else if (ext == 'kml' || ext == 'kmz') {
|
|
40265
41505
|
format = 'kml';
|
|
40266
41506
|
} else if (/json$/.test(ext)) {
|
|
@@ -40319,7 +41559,7 @@ ${svg}
|
|
|
40319
41559
|
async function exportDatasets(datasets, opts) {
|
|
40320
41560
|
var format = getOutputFormat(datasets[0], opts);
|
|
40321
41561
|
var files;
|
|
40322
|
-
if (format != 'svg' && format != PACKAGE_EXT) {
|
|
41562
|
+
if (format != 'svg' && format != 'html' && format != PACKAGE_EXT) {
|
|
40323
41563
|
datasets = removeFurnitureLayers(datasets);
|
|
40324
41564
|
}
|
|
40325
41565
|
validateRasterExportFormat(datasets, format);
|
|
@@ -40333,8 +41573,8 @@ ${svg}
|
|
|
40333
41573
|
opts = utils.defaults({compact: true}, opts);
|
|
40334
41574
|
return exportPackedDatasets(datasets, opts);
|
|
40335
41575
|
}
|
|
40336
|
-
if (format == 'kml' || format == 'svg' || format == '
|
|
40337
|
-
format == 'geojson' && opts.combine_layers) {
|
|
41576
|
+
if (format == 'kml' || format == 'svg' || format == 'html' || format == 'topojson' ||
|
|
41577
|
+
format == 'geopackage' || format == 'geojson' && opts.combine_layers) {
|
|
40338
41578
|
// multi-layer formats: combine multiple datasets into one
|
|
40339
41579
|
if (datasets.length > 1) {
|
|
40340
41580
|
datasets = [mergeDatasetsForExport(datasets)];
|
|
@@ -40353,7 +41593,12 @@ ${svg}
|
|
|
40353
41593
|
datasets = datasets.map(copyDatasetForRenaming);
|
|
40354
41594
|
assignUniqueLayerNames2(datasets);
|
|
40355
41595
|
}
|
|
40356
|
-
if (format == '
|
|
41596
|
+
if (format == 'html') {
|
|
41597
|
+
sortExportLayers(datasets[0]);
|
|
41598
|
+
// HTML bypasses exportFileContent(), because rendering its image is async.
|
|
41599
|
+
files = await exportHTML(copyDatasetForHTMLExport(datasets[0]),
|
|
41600
|
+
utils.defaults({format: format}, opts));
|
|
41601
|
+
} else if (format == 'geopackage') {
|
|
40357
41602
|
if (datasets.length > 1) {
|
|
40358
41603
|
datasets = [mergeDatasetsForExport(datasets)];
|
|
40359
41604
|
}
|
|
@@ -40424,8 +41669,19 @@ ${svg}
|
|
|
40424
41669
|
return;
|
|
40425
41670
|
}
|
|
40426
41671
|
if (!datasetsHaveRasterLayers(datasets)) return;
|
|
40427
|
-
if (format == 'svg' || format == PACKAGE_EXT) return;
|
|
40428
|
-
stop$1('Raster layers can only be exported as GeoTIFF, SVG or ' + PACKAGE_EXT + ' files');
|
|
41672
|
+
if (format == 'svg' || format == 'html' || format == PACKAGE_EXT) return;
|
|
41673
|
+
stop$1('Raster layers can only be exported as GeoTIFF, SVG, HTML or ' + PACKAGE_EXT + ' files');
|
|
41674
|
+
}
|
|
41675
|
+
|
|
41676
|
+
// The parts of exportFileContent() that apply to HTML output: layers are
|
|
41677
|
+
// shallow-copied so they can be given unique names, which become element ids.
|
|
41678
|
+
function copyDatasetForHTMLExport(dataset) {
|
|
41679
|
+
dataset = utils.defaults({
|
|
41680
|
+
layers: dataset.layers.map(function(lyr) {return utils.extend({}, lyr);})
|
|
41681
|
+
}, dataset);
|
|
41682
|
+
assignUniqueLayerNames(dataset.layers);
|
|
41683
|
+
validateLayerData(dataset.layers);
|
|
41684
|
+
return dataset;
|
|
40429
41685
|
}
|
|
40430
41686
|
|
|
40431
41687
|
function datasetsHaveRasterLayers(datasets) {
|
|
@@ -41509,7 +42765,7 @@ ${svg}
|
|
|
41509
42765
|
});
|
|
41510
42766
|
|
|
41511
42767
|
function isSupportedOutputFormat(fmt) {
|
|
41512
|
-
var types = ['geojson', 'topojson', 'json', 'dsv', 'dbf', 'shapefile', 'svg', 'kml', PACKAGE_EXT, 'flatgeobuf', 'geopackage', 'geoparquet', 'geotiff'];
|
|
42768
|
+
var types = ['geojson', 'topojson', 'json', 'dsv', 'dbf', 'shapefile', 'svg', 'html', 'kml', PACKAGE_EXT, 'flatgeobuf', 'geopackage', 'geoparquet', 'geotiff'];
|
|
41513
42769
|
return types.indexOf(fmt) > -1;
|
|
41514
42770
|
}
|
|
41515
42771
|
|
|
@@ -41528,7 +42784,8 @@ ${svg}
|
|
|
41528
42784
|
geopackage: 'GeoPackage',
|
|
41529
42785
|
geoparquet: 'GeoParquet',
|
|
41530
42786
|
geotiff: 'GeoTIFF',
|
|
41531
|
-
svg: 'SVG'
|
|
42787
|
+
svg: 'SVG',
|
|
42788
|
+
html: 'HTML'
|
|
41532
42789
|
}[fmt] || '';
|
|
41533
42790
|
}
|
|
41534
42791
|
|
|
@@ -42715,6 +43972,14 @@ ${svg}
|
|
|
42715
43972
|
offsetOpt = {
|
|
42716
43973
|
describe: 'offset distance or pct of h/w (single value or l,b,r,t list)',
|
|
42717
43974
|
type: 'distance'
|
|
43975
|
+
},
|
|
43976
|
+
frameMarginOpt = {
|
|
43977
|
+
describe: 'padding in display units or pct of frame width; 1-4 values in CSS order, e.g. 5% or 10%,2%',
|
|
43978
|
+
type: 'strings'
|
|
43979
|
+
},
|
|
43980
|
+
frameOffsetOpt = {
|
|
43981
|
+
// undocumented: older form of margin=, one value or an l,b,r,t list
|
|
43982
|
+
type: 'strings'
|
|
42718
43983
|
};
|
|
42719
43984
|
|
|
42720
43985
|
// The label options -style used to document, before -labels took them over.
|
|
@@ -42893,7 +44158,7 @@ ${svg}
|
|
|
42893
44158
|
}
|
|
42894
44159
|
})
|
|
42895
44160
|
.option('format', {
|
|
42896
|
-
describe: 'options: shapefile,geojson,topojson,flatgeobuf,geopackage,geoparquet,geotiff,json,dbf,csv,tsv,svg'
|
|
44161
|
+
describe: 'options: shapefile,geojson,topojson,flatgeobuf,geopackage,geoparquet,geotiff,json,dbf,csv,tsv,svg,html'
|
|
42897
44162
|
})
|
|
42898
44163
|
.option('target', targetOpt)
|
|
42899
44164
|
.option('force', {
|
|
@@ -43020,11 +44285,11 @@ ${svg}
|
|
|
43020
44285
|
type: 'flag'
|
|
43021
44286
|
})
|
|
43022
44287
|
.option('width', {
|
|
43023
|
-
describe: '[SVG/TopoJSON] pixel width of output (SVG default is 800)',
|
|
44288
|
+
describe: '[SVG/HTML/TopoJSON] pixel width of output (SVG default is 800)',
|
|
43024
44289
|
type: 'number'
|
|
43025
44290
|
})
|
|
43026
44291
|
.option('height', {
|
|
43027
|
-
describe: '[SVG/TopoJSON] pixel height of output (optional)',
|
|
44292
|
+
describe: '[SVG/HTML/TopoJSON] pixel height of output (optional)',
|
|
43028
44293
|
type: 'number'
|
|
43029
44294
|
})
|
|
43030
44295
|
.option('max-height', {
|
|
@@ -43059,7 +44324,17 @@ ${svg}
|
|
|
43059
44324
|
type: 'flag'
|
|
43060
44325
|
})
|
|
43061
44326
|
.option('jpeg-quality', {
|
|
43062
|
-
describe: '[SVG] JPEG quality for raster images, 1-100 (default is 85)',
|
|
44327
|
+
describe: '[SVG/HTML] JPEG quality for raster images, 1-100 (default is 85)',
|
|
44328
|
+
type: 'number'
|
|
44329
|
+
})
|
|
44330
|
+
.option('responsiveness', {
|
|
44331
|
+
describe: '[HTML] fixed or dynamic (resizes to fill its container)'
|
|
44332
|
+
})
|
|
44333
|
+
.option('image-format', {
|
|
44334
|
+
describe: '[HTML] format of the map image: png or jpg (default is png)'
|
|
44335
|
+
})
|
|
44336
|
+
.option('pixel-ratio', {
|
|
44337
|
+
describe: '[HTML] image pixels per CSS pixel (default is 2)',
|
|
43063
44338
|
type: 'number'
|
|
43064
44339
|
})
|
|
43065
44340
|
.option('fit-extent', {
|
|
@@ -43348,17 +44623,39 @@ ${svg}
|
|
|
43348
44623
|
type: 'numbers'
|
|
43349
44624
|
})
|
|
43350
44625
|
.option('classes', {
|
|
43351
|
-
describe: 'number of classes (
|
|
43352
|
-
type: '
|
|
44626
|
+
describe: 'number of classes (with pivot=, or below,above the pivot)',
|
|
44627
|
+
type: 'numbers'
|
|
44628
|
+
})
|
|
44629
|
+
.option('pivot', {
|
|
44630
|
+
describe: 'diverging classes: a value, median, mean or auto (0 or median)'
|
|
44631
|
+
})
|
|
44632
|
+
.option('pivot-range', {
|
|
44633
|
+
describe: 'low,high range of the pivot class (diverging classes)',
|
|
44634
|
+
type: 'numbers'
|
|
44635
|
+
})
|
|
44636
|
+
.option('no-pivot-class', {
|
|
44637
|
+
describe: 'diverging classes meet at the pivot (no pivot class)',
|
|
44638
|
+
type: 'flag'
|
|
44639
|
+
})
|
|
44640
|
+
.option('pivot-class', {
|
|
44641
|
+
describe: 'add a pivot class to continuous diverging colors',
|
|
44642
|
+
type: 'flag'
|
|
43353
44643
|
})
|
|
43354
44644
|
.option('invert', {
|
|
43355
44645
|
describe: 'reverse the order of colors/values',
|
|
43356
44646
|
type: 'flag'
|
|
43357
44647
|
})
|
|
43358
44648
|
.option('continuous', {
|
|
43359
|
-
describe: '
|
|
44649
|
+
describe: 'interpolate between values at the class breaks (unclassed colors)',
|
|
43360
44650
|
type: 'flag'
|
|
43361
44651
|
})
|
|
44652
|
+
.option('interpolation', {
|
|
44653
|
+
describe: 'color interpolation: rgb (default) or oklch'
|
|
44654
|
+
})
|
|
44655
|
+
.option('vibrance', {
|
|
44656
|
+
describe: '(oklch) more vivid midtones, 0-1 (default 0)',
|
|
44657
|
+
type: 'number'
|
|
44658
|
+
})
|
|
43362
44659
|
.option('index-field', {
|
|
43363
44660
|
describe: 'apply pre-calculated classes (0 ... n-1, -1)'
|
|
43364
44661
|
})
|
|
@@ -44685,6 +45982,9 @@ ${svg}
|
|
|
44685
45982
|
.option('stroke-dasharray', {
|
|
44686
45983
|
describe: 'stroke dashes. Examples: "4" "2 4"'
|
|
44687
45984
|
})
|
|
45985
|
+
.option('stroke-linecap', {
|
|
45986
|
+
describe: 'line caps: round, butt or square (dashed lines default to butt)'
|
|
45987
|
+
})
|
|
44688
45988
|
.option('stroke-opacity', {
|
|
44689
45989
|
describe: 'stroke opacity'
|
|
44690
45990
|
})
|
|
@@ -44697,6 +45997,9 @@ ${svg}
|
|
|
44697
45997
|
.option('line-end-size', {
|
|
44698
45998
|
describe: 'length of an arrowhead\'s sides in px (default grows with stroke-width)'
|
|
44699
45999
|
})
|
|
46000
|
+
.option('line-fade', {
|
|
46001
|
+
describe: 'share of a line (0-1) that fades in from its tail'
|
|
46002
|
+
})
|
|
44700
46003
|
.option('opacity', {
|
|
44701
46004
|
describe: 'opacity; example: 0.5'
|
|
44702
46005
|
})
|
|
@@ -45019,14 +46322,9 @@ ${svg}
|
|
|
45019
46322
|
describe: 'hold the map scale; derive the size from the new extent',
|
|
45020
46323
|
type: 'flag'
|
|
45021
46324
|
})
|
|
45022
|
-
.option('
|
|
45023
|
-
|
|
45024
|
-
|
|
45025
|
-
})
|
|
45026
|
-
.option('offsets', {
|
|
45027
|
-
describe: 'separate offsets for each side, in l,b,r,t order',
|
|
45028
|
-
type: 'strings'
|
|
45029
|
-
})
|
|
46325
|
+
.option('margin', frameMarginOpt)
|
|
46326
|
+
.option('offset', frameOffsetOpt)
|
|
46327
|
+
.option('offsets', frameOffsetOpt)
|
|
45030
46328
|
.option('remove', {
|
|
45031
46329
|
describe: 'demote the frame to an ordinary rectangle layer',
|
|
45032
46330
|
type: 'flag'
|
|
@@ -45144,14 +46442,9 @@ ${svg}
|
|
|
45144
46442
|
describe: 'frame coordinates (xmin,ymin,xmax,ymax)',
|
|
45145
46443
|
type: 'bbox'
|
|
45146
46444
|
})
|
|
45147
|
-
.option('
|
|
45148
|
-
|
|
45149
|
-
|
|
45150
|
-
})
|
|
45151
|
-
.option('offsets', {
|
|
45152
|
-
describe: 'separate offsets for each side, in l,b,r,t order',
|
|
45153
|
-
type: 'strings'
|
|
45154
|
-
})
|
|
46445
|
+
.option('margin', frameMarginOpt)
|
|
46446
|
+
.option('offset', frameOffsetOpt)
|
|
46447
|
+
.option('offsets', frameOffsetOpt)
|
|
45155
46448
|
.option('ignore-symbols', {
|
|
45156
46449
|
describe: 'fit to point locations, not the extent of symbols and labels',
|
|
45157
46450
|
type: 'flag'
|
|
@@ -54064,6 +55357,7 @@ ${svg}
|
|
|
54064
55357
|
cmd.frame = function(catalog, targets, opts) {
|
|
54065
55358
|
var widthPx, heightPx, bbox;
|
|
54066
55359
|
var existingFrame = getActiveFrame(catalog);
|
|
55360
|
+
var offsets = parseFrameOffsets(opts);
|
|
54067
55361
|
if (opts.width) {
|
|
54068
55362
|
widthPx = parseFrameSize(opts.width).valuePx;
|
|
54069
55363
|
if (widthPx > 0 === false) {
|
|
@@ -54099,7 +55393,7 @@ ${svg}
|
|
|
54099
55393
|
var datasets = utils.pluck(targets, 'dataset');
|
|
54100
55394
|
requireDatasetsHaveCompatibleCRS(datasets, 'Targets include both projected and unprojected coordinates');
|
|
54101
55395
|
bbox = getFrameContentBbox(expandCommandTargets(targets), function(contentBbox) {
|
|
54102
|
-
var extent = getFrameExtent(contentBbox, widthPx, heightPx,
|
|
55396
|
+
var extent = getFrameExtent(contentBbox, widthPx, heightPx, offsets);
|
|
54103
55397
|
return getFrameScale(extent.bbox, extent.width, getFixedAspect$1(extent, opts));
|
|
54104
55398
|
}, opts);
|
|
54105
55399
|
if (!bbox && !expandCommandTargets(targets).some(function(o) {
|
|
@@ -54112,7 +55406,7 @@ ${svg}
|
|
|
54112
55406
|
}
|
|
54113
55407
|
}
|
|
54114
55408
|
|
|
54115
|
-
var extent = getFrameExtent(bbox, widthPx, heightPx,
|
|
55409
|
+
var extent = getFrameExtent(bbox, widthPx, heightPx, offsets);
|
|
54116
55410
|
if (!extent.valid) {
|
|
54117
55411
|
stop$1('Frame has a collapsed bbox');
|
|
54118
55412
|
}
|
|
@@ -54146,12 +55440,10 @@ ${svg}
|
|
|
54146
55440
|
// The frame's extent and nominal size, from the extent of its content and the
|
|
54147
55441
|
// size options. Pure, so that fitting to symbols can ask what scale an extent
|
|
54148
55442
|
// would give the frame.
|
|
54149
|
-
function getFrameExtent(contentBbox, widthPx, heightPx,
|
|
54150
|
-
var offsets = opts.offset || opts.offsets;
|
|
55443
|
+
function getFrameExtent(contentBbox, widthPx, heightPx, offsets) {
|
|
54151
55444
|
var bbox = contentBbox.slice();
|
|
54152
55445
|
var aspectRatio;
|
|
54153
|
-
|
|
54154
|
-
applyPixelOffsets(bbox, widthPx, heightPx, offsets);
|
|
55446
|
+
applyFrameOffsets(bbox, offsets, {width: widthPx, height: heightPx});
|
|
54155
55447
|
aspectRatio = (bbox[2] - bbox[0]) / (bbox[3] - bbox[1]);
|
|
54156
55448
|
if (!widthPx) {
|
|
54157
55449
|
widthPx = roundToDigits(heightPx * aspectRatio, 1);
|
|
@@ -54200,70 +55492,113 @@ ${svg}
|
|
|
54200
55492
|
bbox[3] += vpad / 2;
|
|
54201
55493
|
}
|
|
54202
55494
|
|
|
54203
|
-
|
|
54204
|
-
|
|
54205
|
-
|
|
54206
|
-
|
|
54207
|
-
|
|
54208
|
-
|
|
54209
|
-
|
|
54210
|
-
|
|
54211
|
-
|
|
54212
|
-
|
|
54213
|
-
|
|
54214
|
-
|
|
54215
|
-
|
|
54216
|
-
|
|
54217
|
-
function applyPixelOffsets(bbox, widthPx, heightPx, arg) {
|
|
54218
|
-
var sides = getPixelOffsets(arg);
|
|
54219
|
-
var l = sides[0],
|
|
54220
|
-
b = sides[1],
|
|
54221
|
-
r = sides[2],
|
|
54222
|
-
t = sides[3],
|
|
54223
|
-
scale, w;
|
|
54224
|
-
|
|
54225
|
-
if (widthPx && heightPx) {
|
|
54226
|
-
// add padding to bbox to match pixel dimensions, if needed
|
|
54227
|
-
fillOutBbox(bbox, widthPx, heightPx);
|
|
55495
|
+
// Margins from the margin=, offset= or offsets= option, in l,b,r,t order, or
|
|
55496
|
+
// null if none is given. margin= takes one to four values in CSS order (all
|
|
55497
|
+
// sides; vertical horizontal; top horizontal bottom; top right bottom left).
|
|
55498
|
+
// offset= and offsets= are the older form, and take one value or four in
|
|
55499
|
+
// l,b,r,t order, the order of a bbox. Each side is a length in px plus a share
|
|
55500
|
+
// of the frame's width: as in CSS, a percentage is of the width on every side,
|
|
55501
|
+
// so that 5% is an even margin.
|
|
55502
|
+
function parseFrameOffsets(opts) {
|
|
55503
|
+
var names = ['margin', 'offset', 'offsets'].filter(name => opts[name]);
|
|
55504
|
+
var name = names[0];
|
|
55505
|
+
var tokens;
|
|
55506
|
+
if (names.length > 1) {
|
|
55507
|
+
stop$1(names.map(name => name + '=').join(' and ') + ' are mutually exclusive');
|
|
54228
55508
|
}
|
|
54229
|
-
|
|
54230
|
-
|
|
54231
|
-
|
|
54232
|
-
|
|
54233
|
-
if (
|
|
54234
|
-
|
|
55509
|
+
if (!name) return null;
|
|
55510
|
+
tokens = splitOffsetList(opts[name]);
|
|
55511
|
+
if (name == 'margin') {
|
|
55512
|
+
tokens = expandCssSides(tokens);
|
|
55513
|
+
} else if (tokens.length == 1) {
|
|
55514
|
+
tokens = [tokens[0], tokens[0], tokens[0], tokens[0]];
|
|
55515
|
+
} else if (tokens.length != 4) {
|
|
55516
|
+
stop$1(name + '= expects one value or four, in l,b,r,t order');
|
|
55517
|
+
}
|
|
55518
|
+
return {
|
|
55519
|
+
px: tokens.map(str => str.includes('%') ? 0 : parseSizeParam(str)),
|
|
55520
|
+
pct: tokens.map(str => str.includes('%') ? parsePercent(str) : 0)
|
|
55521
|
+
};
|
|
55522
|
+
}
|
|
55523
|
+
|
|
55524
|
+
// Accepts comma- or space-separated values, e.g. margin=10%,2% or
|
|
55525
|
+
// margin='10% 2%'
|
|
55526
|
+
function splitOffsetList(arg) {
|
|
55527
|
+
return [].concat(arg).join(' ').split(/[\s,]+/).filter(Boolean);
|
|
55528
|
+
}
|
|
55529
|
+
|
|
55530
|
+
// CSS shorthand to l,b,r,t
|
|
55531
|
+
function expandCssSides(arr) {
|
|
55532
|
+
var t, r, b, l;
|
|
55533
|
+
if (arr.length < 1 || arr.length > 4) {
|
|
55534
|
+
stop$1('margin= expects one to four values, in CSS order (top right bottom left)');
|
|
55535
|
+
}
|
|
55536
|
+
t = arr[0];
|
|
55537
|
+
r = arr.length > 1 ? arr[1] : t;
|
|
55538
|
+
b = arr.length > 2 ? arr[2] : t;
|
|
55539
|
+
l = arr.length > 3 ? arr[3] : r;
|
|
55540
|
+
return [l, b, r, t];
|
|
55541
|
+
}
|
|
55542
|
+
|
|
55543
|
+
// Pads @bbox in place by @offsets (from parseFrameOffsets(), or null for no
|
|
55544
|
+
// padding). The frame's display width, which percentage margins are a share
|
|
55545
|
+
// of, is given by @size.width, or follows from @size.height (when the width is
|
|
55546
|
+
// derived from the padded extent) or from @size.scale (map units per px, when
|
|
55547
|
+
// the scale is held and the size follows the extent). Given both a width and
|
|
55548
|
+
// a height, the content is centered on the page and the bbox takes the page's
|
|
55549
|
+
// shape, so the margins are a minimum on two of the sides.
|
|
55550
|
+
function applyFrameOffsets(bbox, offsets, size) {
|
|
55551
|
+
var px = offsets ? offsets.px : [0, 0, 0, 0];
|
|
55552
|
+
var pct = offsets ? offsets.pct : [0, 0, 0, 0];
|
|
55553
|
+
var cw = bbox[2] - bbox[0];
|
|
55554
|
+
var ch = bbox[3] - bbox[1];
|
|
55555
|
+
var hpct = pct[0] + pct[2],
|
|
55556
|
+
vpct = pct[1] + pct[3],
|
|
55557
|
+
hpx = px[0] + px[2],
|
|
55558
|
+
vpx = px[1] + px[3];
|
|
55559
|
+
var widthPx = size.width, heightPx = size.height, scale = size.scale;
|
|
55560
|
+
var margins, innerW, innerH, k, hpad = 0, vpad = 0;
|
|
55561
|
+
|
|
55562
|
+
if (hpct >= 1) {
|
|
55563
|
+
stop$1('Left and right offsets add up to 100% or more of the frame width');
|
|
55564
|
+
}
|
|
55565
|
+
if (widthPx > 0) ; else if (scale > 0) {
|
|
55566
|
+
widthPx = (cw / scale + hpx) / (1 - hpct);
|
|
55567
|
+
} else if (heightPx > 0) {
|
|
55568
|
+
// heightPx = ch / scale + vpx + vpct * widthPx, where
|
|
55569
|
+
// widthPx = (cw / scale + hpx) / (1 - hpct)
|
|
55570
|
+
k = vpct / (1 - hpct);
|
|
55571
|
+
innerH = heightPx - vpx - k * hpx;
|
|
55572
|
+
if (!(innerH > 0)) stopNoRoom();
|
|
55573
|
+
scale = (ch + k * cw) / innerH;
|
|
55574
|
+
widthPx = (cw / scale + hpx) / (1 - hpct);
|
|
54235
55575
|
} else {
|
|
54236
|
-
|
|
55576
|
+
stop$1('Unable to apply offsets to a frame with no display size');
|
|
55577
|
+
}
|
|
55578
|
+
margins = px.map((n, i) => n + pct[i] * widthPx);
|
|
55579
|
+
|
|
55580
|
+
if (!(scale > 0)) {
|
|
55581
|
+
innerW = widthPx - margins[0] - margins[2];
|
|
55582
|
+
if (!(innerW > 0)) stopNoRoom();
|
|
55583
|
+
if (heightPx > 0) {
|
|
55584
|
+
innerH = heightPx - margins[1] - margins[3];
|
|
55585
|
+
if (!(innerH > 0)) stopNoRoom();
|
|
55586
|
+
scale = Math.max(cw / innerW, ch / innerH);
|
|
55587
|
+
hpad = innerW * scale - cw;
|
|
55588
|
+
vpad = innerH * scale - ch;
|
|
55589
|
+
} else {
|
|
55590
|
+
scale = cw / innerW;
|
|
55591
|
+
}
|
|
54237
55592
|
}
|
|
54238
55593
|
|
|
54239
|
-
bbox[0] -= scale *
|
|
54240
|
-
bbox[1] -= scale *
|
|
54241
|
-
bbox[2] += scale *
|
|
54242
|
-
bbox[3] += scale *
|
|
54243
|
-
return scale;
|
|
55594
|
+
bbox[0] -= scale * margins[0] + hpad / 2;
|
|
55595
|
+
bbox[1] -= scale * margins[1] + vpad / 2;
|
|
55596
|
+
bbox[2] += scale * margins[2] + hpad / 2;
|
|
55597
|
+
bbox[3] += scale * margins[3] + vpad / 2;
|
|
54244
55598
|
}
|
|
54245
55599
|
|
|
54246
|
-
function
|
|
54247
|
-
|
|
54248
|
-
return str.includes('%') ? parsePercent(str) : 0;
|
|
54249
|
-
});
|
|
54250
|
-
}
|
|
54251
|
-
|
|
54252
|
-
function getPixelOffsets(arg) {
|
|
54253
|
-
return adjustOffsetsArg(arg).map(str => {
|
|
54254
|
-
return str.includes('%') ? 0 : parseSizeParam(str);
|
|
54255
|
-
});
|
|
54256
|
-
}
|
|
54257
|
-
|
|
54258
|
-
function adjustOffsetsArg(arg) {
|
|
54259
|
-
if (!arg) arg = ['0'];
|
|
54260
|
-
if (arg.length == 1) {
|
|
54261
|
-
return [arg[0], arg[0], arg[0], arg[0]];
|
|
54262
|
-
}
|
|
54263
|
-
if (arg.length != 4) {
|
|
54264
|
-
stop$1('List of offsets should have 4 values');
|
|
54265
|
-
}
|
|
54266
|
-
return arg;
|
|
55600
|
+
function stopNoRoom() {
|
|
55601
|
+
stop$1('Frame offsets leave no room for the map');
|
|
54267
55602
|
}
|
|
54268
55603
|
|
|
54269
55604
|
// Convert width and height args to aspect ratio arg for the rectangle() function
|
|
@@ -54280,10 +55615,10 @@ ${svg}
|
|
|
54280
55615
|
|
|
54281
55616
|
var Frame = /*#__PURE__*/Object.freeze({
|
|
54282
55617
|
__proto__: null,
|
|
54283
|
-
|
|
54284
|
-
applyPixelOffsets: applyPixelOffsets,
|
|
55618
|
+
applyFrameOffsets: applyFrameOffsets,
|
|
54285
55619
|
fillOutBbox: fillOutBbox,
|
|
54286
|
-
getAspectRatioArg: getAspectRatioArg
|
|
55620
|
+
getAspectRatioArg: getAspectRatioArg,
|
|
55621
|
+
parseFrameOffsets: parseFrameOffsets
|
|
54287
55622
|
});
|
|
54288
55623
|
|
|
54289
55624
|
// A frame is a viewport, not data, so it can legitimately extend past the
|
|
@@ -54442,6 +55777,7 @@ ${svg}
|
|
|
54442
55777
|
var rec = lyr.data.getRecords()[0];
|
|
54443
55778
|
var frame = getFrameLayerData(lyr, dataset.arcs);
|
|
54444
55779
|
var fixedAspect = getFixedAspect(rec);
|
|
55780
|
+
var offsets = parseFrameOffsets(opts);
|
|
54445
55781
|
var sizes, contentBbox, update;
|
|
54446
55782
|
|
|
54447
55783
|
// 1. Aspect mode, resolved first because whether the page shape is fixed
|
|
@@ -54459,7 +55795,7 @@ ${svg}
|
|
|
54459
55795
|
// 2. Extent, which fit= finds by trying extents out on steps 3 and 4
|
|
54460
55796
|
if (opts.fit !== undefined) {
|
|
54461
55797
|
contentBbox = getFrameContentBbox(getFitTargets(fitTargets, dataset), function(bbox) {
|
|
54462
|
-
var o = resolveFrameUpdate(bbox, frame, fixedAspect, sizes, opts);
|
|
55798
|
+
var o = resolveFrameUpdate(bbox, frame, fixedAspect, sizes, offsets, opts);
|
|
54463
55799
|
return getFrameScale(o.bbox, o.width, o.fixedAspect);
|
|
54464
55800
|
}, opts);
|
|
54465
55801
|
if (!contentBbox) {
|
|
@@ -54469,7 +55805,7 @@ ${svg}
|
|
|
54469
55805
|
contentBbox = opts.bbox || frame.bbox;
|
|
54470
55806
|
}
|
|
54471
55807
|
|
|
54472
|
-
update = resolveFrameUpdate(contentBbox, frame, fixedAspect, sizes, opts);
|
|
55808
|
+
update = resolveFrameUpdate(contentBbox, frame, fixedAspect, sizes, offsets, opts);
|
|
54473
55809
|
if (!update.valid) {
|
|
54474
55810
|
stop$1('Frame has a collapsed bbox');
|
|
54475
55811
|
}
|
|
@@ -54527,21 +55863,17 @@ ${svg}
|
|
|
54527
55863
|
|
|
54528
55864
|
// Steps 3 and 4 as a pure function of the content extent, so that fit= can
|
|
54529
55865
|
// ask what scale an extent would give the frame.
|
|
54530
|
-
function resolveFrameUpdate(contentBbox, frame, fixedAspect, sizes, opts) {
|
|
55866
|
+
function resolveFrameUpdate(contentBbox, frame, fixedAspect, sizes, offsets, opts) {
|
|
54531
55867
|
var bbox = contentBbox.slice();
|
|
54532
|
-
var offsetArg = opts.offset || opts.offsets;
|
|
54533
55868
|
var width = frame.width;
|
|
54534
55869
|
var units = frame.units || 'px';
|
|
54535
55870
|
var valid, effectiveAspect;
|
|
54536
55871
|
|
|
54537
55872
|
// 3. Padding
|
|
54538
|
-
if (
|
|
54539
|
-
|
|
54540
|
-
|
|
54541
|
-
|
|
54542
|
-
// re-fitted frame to its old shape instead of its new bounds.
|
|
54543
|
-
applyPixelOffsets(bbox, frame.width,
|
|
54544
|
-
fixedAspect ? frame.width / fixedAspect : null, offsetArg);
|
|
55873
|
+
if (offsets) {
|
|
55874
|
+
applyFrameOffsets(bbox, offsets,
|
|
55875
|
+
getPaddingSize(frame, sizes.width && sizes.height ?
|
|
55876
|
+
sizes.width.valuePx / sizes.height.valuePx : fixedAspect, sizes, opts));
|
|
54545
55877
|
}
|
|
54546
55878
|
valid = isValidBbox(bbox);
|
|
54547
55879
|
if (fixedAspect) {
|
|
@@ -54578,6 +55910,25 @@ ${svg}
|
|
|
54578
55910
|
};
|
|
54579
55911
|
}
|
|
54580
55912
|
|
|
55913
|
+
// The size the frame is padded for, which is the size it ends up with: a
|
|
55914
|
+
// percentage offset is a share of the final width, and a px offset is px at
|
|
55915
|
+
// the final scale. A page height is given only when the shape is fixed; a
|
|
55916
|
+
// derived height follows the extent, so letting it pad the bbox would hold a
|
|
55917
|
+
// re-fitted frame to its old shape instead of its new bounds.
|
|
55918
|
+
function getPaddingSize(frame, fixedAspect, sizes, opts) {
|
|
55919
|
+
var width = frame.width;
|
|
55920
|
+
if (opts.fix_scale) {
|
|
55921
|
+
return {scale: getBboxWidth(frame.bbox) / frame.width};
|
|
55922
|
+
}
|
|
55923
|
+
if (sizes.width) {
|
|
55924
|
+
width = sizes.width.valuePx;
|
|
55925
|
+
} else if (sizes.height) {
|
|
55926
|
+
if (!fixedAspect) return {height: sizes.height.valuePx};
|
|
55927
|
+
width = sizes.height.valuePx * fixedAspect;
|
|
55928
|
+
}
|
|
55929
|
+
return {width: width, height: fixedAspect ? width / fixedAspect : null};
|
|
55930
|
+
}
|
|
55931
|
+
|
|
54581
55932
|
function hasUpdateOptions(opts) {
|
|
54582
55933
|
return opts.bbox !== undefined ||
|
|
54583
55934
|
opts.fit !== undefined ||
|
|
@@ -54586,6 +55937,7 @@ ${svg}
|
|
|
54586
55937
|
opts.aspect_ratio !== undefined ||
|
|
54587
55938
|
opts.auto_aspect ||
|
|
54588
55939
|
opts.fix_scale ||
|
|
55940
|
+
opts.margin !== undefined ||
|
|
54589
55941
|
opts.offset !== undefined ||
|
|
54590
55942
|
opts.offsets !== undefined;
|
|
54591
55943
|
}
|
|
@@ -67886,7 +69238,7 @@ ${svg}
|
|
|
67886
69238
|
function sphericalPointDistance(a, b) {
|
|
67887
69239
|
var av = lonLatToVector(a);
|
|
67888
69240
|
var bv = lonLatToVector(b);
|
|
67889
|
-
return Math.acos(clamp$
|
|
69241
|
+
return Math.acos(clamp$2(
|
|
67890
69242
|
av[0] * bv[0] + av[1] * bv[1] + av[2] * bv[2], -1, 1
|
|
67891
69243
|
)) * 180 / Math.PI;
|
|
67892
69244
|
}
|
|
@@ -67901,7 +69253,7 @@ ${svg}
|
|
|
67901
69253
|
function vectorToLonLat(p) {
|
|
67902
69254
|
return [
|
|
67903
69255
|
Math.atan2(p[1], p[0]) * 180 / Math.PI,
|
|
67904
|
-
Math.asin(clamp$
|
|
69256
|
+
Math.asin(clamp$2(p[2], -1, 1)) * 180 / Math.PI
|
|
67905
69257
|
];
|
|
67906
69258
|
}
|
|
67907
69259
|
|
|
@@ -68669,10 +70021,10 @@ ${svg}
|
|
|
68669
70021
|
|
|
68670
70022
|
function getTrianglePixelBounds(grid, p1, p2, p3) {
|
|
68671
70023
|
return [
|
|
68672
|
-
clamp$
|
|
68673
|
-
clamp$
|
|
68674
|
-
clamp$
|
|
68675
|
-
clamp$
|
|
70024
|
+
clamp$2(Math.floor(Math.min(p1.x, p2.x, p3.x)), 0, grid.width - 1),
|
|
70025
|
+
clamp$2(Math.floor(Math.min(p1.y, p2.y, p3.y)), 0, grid.height - 1),
|
|
70026
|
+
clamp$2(Math.ceil(Math.max(p1.x, p2.x, p3.x)), 0, grid.width - 1),
|
|
70027
|
+
clamp$2(Math.ceil(Math.max(p1.y, p2.y, p3.y)), 0, grid.height - 1)
|
|
68676
70028
|
];
|
|
68677
70029
|
}
|
|
68678
70030
|
|
|
@@ -68710,8 +70062,8 @@ ${svg}
|
|
|
68710
70062
|
function copyNearestRasterSample(srcGrid, destGrid, sx, sy, dx, dy, wrapX) {
|
|
68711
70063
|
var srcX = wrapX ?
|
|
68712
70064
|
modulo(Math.floor(sx), srcGrid.width) :
|
|
68713
|
-
clamp$
|
|
68714
|
-
var srcY = clamp$
|
|
70065
|
+
clamp$2(Math.floor(sx), 0, srcGrid.width - 1);
|
|
70066
|
+
var srcY = clamp$2(Math.floor(sy), 0, srcGrid.height - 1);
|
|
68715
70067
|
var src = (srcY * srcGrid.width + srcX) * srcGrid.bands;
|
|
68716
70068
|
var dest = (dy * destGrid.width + dx) * destGrid.bands;
|
|
68717
70069
|
// Nearest sampling copies a nodata sample through unchanged, rather than
|
|
@@ -68728,13 +70080,13 @@ ${svg}
|
|
|
68728
70080
|
if (wrapX) sx = modulo(sx, srcGrid.width);
|
|
68729
70081
|
var srcX = wrapX ?
|
|
68730
70082
|
modulo(Math.floor(sx - 0.5), srcGrid.width) :
|
|
68731
|
-
clamp$
|
|
68732
|
-
var srcY = clamp$
|
|
70083
|
+
clamp$2(Math.floor(sx - 0.5), 0, srcGrid.width - 1);
|
|
70084
|
+
var srcY = clamp$2(Math.floor(sy - 0.5), 0, srcGrid.height - 1);
|
|
68733
70085
|
var srcX2 = wrapX ? (srcX + 1) % srcGrid.width :
|
|
68734
|
-
clamp$
|
|
68735
|
-
var srcY2 = clamp$
|
|
68736
|
-
var tx = clamp$
|
|
68737
|
-
var ty = clamp$
|
|
70086
|
+
clamp$2(srcX + 1, 0, srcGrid.width - 1);
|
|
70087
|
+
var srcY2 = clamp$2(srcY + 1, 0, srcGrid.height - 1);
|
|
70088
|
+
var tx = clamp$2(sx - 0.5 - srcX, 0, 1);
|
|
70089
|
+
var ty = clamp$2(sy - 0.5 - srcY, 0, 1);
|
|
68738
70090
|
var src00 = (srcY * srcGrid.width + srcX) * srcGrid.bands;
|
|
68739
70091
|
var src10 = (srcY * srcGrid.width + srcX2) * srcGrid.bands;
|
|
68740
70092
|
var src01 = (srcY2 * srcGrid.width + srcX) * srcGrid.bands;
|
|
@@ -68841,7 +70193,7 @@ ${svg}
|
|
|
68841
70193
|
return (x - a.x) * (b.y - a.y) - (y - a.y) * (b.x - a.x);
|
|
68842
70194
|
}
|
|
68843
70195
|
|
|
68844
|
-
function clamp$
|
|
70196
|
+
function clamp$2(val, min, max) {
|
|
68845
70197
|
return val < min ? min : val > max ? max : val;
|
|
68846
70198
|
}
|
|
68847
70199
|
|
|
@@ -70473,6 +71825,367 @@ ${svg}
|
|
|
70473
71825
|
cubehelix$1(hue);
|
|
70474
71826
|
var cubehelixLong = cubehelix$1(nogamma);
|
|
70475
71827
|
|
|
71828
|
+
// OKLab conversion (Björn Ottosson, https://bottosson.github.io/posts/oklab/)
|
|
71829
|
+
// RGB objects use 0-255 channels, as returned by parseColor().
|
|
71830
|
+
|
|
71831
|
+
function rgbToOklab(rgb) {
|
|
71832
|
+
var r = toLinear(rgb.r / 255),
|
|
71833
|
+
g = toLinear(rgb.g / 255),
|
|
71834
|
+
b = toLinear(rgb.b / 255);
|
|
71835
|
+
var l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b),
|
|
71836
|
+
m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b),
|
|
71837
|
+
s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
71838
|
+
return {
|
|
71839
|
+
l: 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
|
|
71840
|
+
a: 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
|
|
71841
|
+
b: 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s
|
|
71842
|
+
};
|
|
71843
|
+
}
|
|
71844
|
+
|
|
71845
|
+
// Colors outside the sRGB gamut are clipped per channel.
|
|
71846
|
+
function oklabToRgb(lab) {
|
|
71847
|
+
var rgb = oklabToLinearRgb(lab);
|
|
71848
|
+
return {
|
|
71849
|
+
r: fromLinear(rgb.r) * 255,
|
|
71850
|
+
g: fromLinear(rgb.g) * 255,
|
|
71851
|
+
b: fromLinear(rgb.b) * 255
|
|
71852
|
+
};
|
|
71853
|
+
}
|
|
71854
|
+
|
|
71855
|
+
// OKLCH is OKLab in polar form: lightness, chroma (colorfulness) and hue
|
|
71856
|
+
// angle in degrees.
|
|
71857
|
+
function oklabToOklch(lab) {
|
|
71858
|
+
var h = Math.atan2(lab.b, lab.a) * 180 / Math.PI;
|
|
71859
|
+
return {l: lab.l, c: Math.sqrt(lab.a * lab.a + lab.b * lab.b), h: h < 0 ? h + 360 : h};
|
|
71860
|
+
}
|
|
71861
|
+
|
|
71862
|
+
function oklchToOklab(lch) {
|
|
71863
|
+
var rad = lch.h * Math.PI / 180;
|
|
71864
|
+
return {l: lch.l, a: lch.c * Math.cos(rad), b: lch.c * Math.sin(rad)};
|
|
71865
|
+
}
|
|
71866
|
+
|
|
71867
|
+
// Below this chroma a color is treated as a gray, whose hue means nothing.
|
|
71868
|
+
var ACHROMATIC = 0.002;
|
|
71869
|
+
|
|
71870
|
+
// Lightness may move this far (OKLCH L) to make room for chroma ...
|
|
71871
|
+
var MAX_LIGHTNESS_SHIFT = 0.05;
|
|
71872
|
+
// ... and no more than this share of the lightness step between tiles, so
|
|
71873
|
+
// that steps in lightness stay close to even.
|
|
71874
|
+
var LIGHTNESS_SHIFT_SHARE = 0.2;
|
|
71875
|
+
// Lightness differences count this many times as much as chroma differences
|
|
71876
|
+
// when fitting a color to the gamut (see fitLightness()).
|
|
71877
|
+
var LIGHTNESS_WEIGHT = 2;
|
|
71878
|
+
// Shifts and chroma losses smaller than this are not reported.
|
|
71879
|
+
var DEVIATION = 0.003;
|
|
71880
|
+
|
|
71881
|
+
// Where the light end of a ramp gains less chroma from vibrance than the
|
|
71882
|
+
// color next to it (the gamut has little room near white), the step between
|
|
71883
|
+
// them looks larger than the others. So that color's lightness moves toward
|
|
71884
|
+
// the light end by this many times the difference in chroma gained. Set by
|
|
71885
|
+
// eye; see getSecondLightness().
|
|
71886
|
+
var VIBRANCE_LIGHTNESS = 0.2;
|
|
71887
|
+
|
|
71888
|
+
// The OKLCH chroma that -classify's vibrance=1 adds, and the top of the color
|
|
71889
|
+
// palette panel's slider. More than this fills the gamut at most lightnesses,
|
|
71890
|
+
// leaving the midtones of a ramp all as vivid as they can be.
|
|
71891
|
+
var MAX_VIBRANCE_CHROMA = 0.09;
|
|
71892
|
+
|
|
71893
|
+
// vibrance=: 0-1, scaled to OKLCH chroma
|
|
71894
|
+
function getVibranceChroma(vibrance) {
|
|
71895
|
+
return vibrance * MAX_VIBRANCE_CHROMA;
|
|
71896
|
+
}
|
|
71897
|
+
|
|
71898
|
+
// A user's color with vibrance added to its chroma, as far as the gamut has
|
|
71899
|
+
// room at its lightness and hue. Grays are left alone, having no hue.
|
|
71900
|
+
function getVibrantColor(color, vibrance) {
|
|
71901
|
+
return getVibrantTile(color, vibrance).color;
|
|
71902
|
+
}
|
|
71903
|
+
|
|
71904
|
+
// Like getVibrantColor(), but returns {color, l, c, h, ideal, adjusted}, as
|
|
71905
|
+
// the function from getOklchInterpolator() does. A color is adjusted if the
|
|
71906
|
+
// gamut didn't have room for all of the vibrance.
|
|
71907
|
+
function getVibrantTile(color, vibrance) {
|
|
71908
|
+
var rgb = parseColorOrStop(color);
|
|
71909
|
+
var lch = oklabToOklch(rgbToOklab(rgb));
|
|
71910
|
+
var ideal = {l: lch.l, c: lch.c, h: lch.h};
|
|
71911
|
+
var c = lch.c, out;
|
|
71912
|
+
if (vibrance > 0 && lch.c >= ACHROMATIC) {
|
|
71913
|
+
ideal.c += vibrance;
|
|
71914
|
+
c = getVibrantChroma(lch, vibrance);
|
|
71915
|
+
out = oklchToRgb({l: lch.l, c: c, h: lch.h});
|
|
71916
|
+
out.a = rgb.a;
|
|
71917
|
+
}
|
|
71918
|
+
return {
|
|
71919
|
+
color: formatColor(out || rgb),
|
|
71920
|
+
l: lch.l, c: c, h: lch.h,
|
|
71921
|
+
ideal: ideal,
|
|
71922
|
+
adjusted: ideal.c - c > DEVIATION
|
|
71923
|
+
};
|
|
71924
|
+
}
|
|
71925
|
+
|
|
71926
|
+
function getVibrantChroma(lch, vibrance) {
|
|
71927
|
+
if (!(vibrance > 0) || lch.c < ACHROMATIC) return lch.c;
|
|
71928
|
+
return Math.max(lch.c, Math.min(lch.c + vibrance, getMaxChroma(lch.l, lch.h)));
|
|
71929
|
+
}
|
|
71930
|
+
|
|
71931
|
+
// Returns a function that maps t in [0, 1] to a color string. Lightness,
|
|
71932
|
+
// chroma and hue are each interpolated on their own, so that equal steps in
|
|
71933
|
+
// t are equal steps in each of them; hue takes the shorter way around the
|
|
71934
|
+
// color wheel, unless opts.hue is 'longer'. A gray end takes the hue of the other end, so that a ramp from
|
|
71935
|
+
// white to blue is blue all the way rather than passing through other hues.
|
|
71936
|
+
// See getOklchInterpolator() for options, and for colors outside the sRGB
|
|
71937
|
+
// gamut.
|
|
71938
|
+
function interpolateOklch(color1, color2, opts) {
|
|
71939
|
+
var interpolate = getOklchInterpolator(color1, color2, opts);
|
|
71940
|
+
return function(t) {
|
|
71941
|
+
return interpolate(t).color;
|
|
71942
|
+
};
|
|
71943
|
+
}
|
|
71944
|
+
|
|
71945
|
+
// Like interpolateOklch(), but the function returns {color, l, c, h, ideal,
|
|
71946
|
+
// adjusted}: the color, its OKLCH values, the ideal OKLCH values it was
|
|
71947
|
+
// fitted from ({l, c, h}), and whether fitting changed it noticeably.
|
|
71948
|
+
//
|
|
71949
|
+
// opts.vibrance (OKLCH chroma, default 0) raises the chroma of the midtones.
|
|
71950
|
+
// The sRGB gamut holds little chroma near black and white, so a ramp between
|
|
71951
|
+
// a dark and a light color has dull midtones when its chroma goes in equal
|
|
71952
|
+
// steps. The amount is the same at every hue. Ramps between two grays get no
|
|
71953
|
+
// boost. Vibrance is added to the chroma of every color of the straight
|
|
71954
|
+
// interpolation. The two end colors get it as far as the gamut has room at
|
|
71955
|
+
// their lightness, without being marked as adjusted (see getVibrantColor());
|
|
71956
|
+
// gray ends are left alone. In a classed ramp, the color next to the lighter
|
|
71957
|
+
// end moves toward it in lightness if it gained more chroma than that end,
|
|
71958
|
+
// and the others are evenly spaced from it to the darker end (see
|
|
71959
|
+
// getSecondLightness()).
|
|
71960
|
+
//
|
|
71961
|
+
// opts.steps: the number of tiles from one color to the other, if the ramp
|
|
71962
|
+
// is classed, which limits how far lightness may move (see below).
|
|
71963
|
+
//
|
|
71964
|
+
// opts.hue: 'shorter' (default) or 'longer', the way around the color wheel
|
|
71965
|
+
// that hue takes. A gray end has no hue to go around from, so a ramp with
|
|
71966
|
+
// one always takes the other end's hue.
|
|
71967
|
+
//
|
|
71968
|
+
// A color outside the sRGB gamut keeps its hue. Its lightness may move, up
|
|
71969
|
+
// to MAX_LIGHTNESS_SHIFT (and a share of the lightness step between tiles),
|
|
71970
|
+
// to where the gamut has room for more of its chroma, if the chroma gained
|
|
71971
|
+
// is worth the lightness lost (see fitLightness()). Chroma is then reduced
|
|
71972
|
+
// to fit.
|
|
71973
|
+
function getOklchInterpolator(color1, color2, opts) {
|
|
71974
|
+
var rgb1 = parseColorOrStop(color1),
|
|
71975
|
+
rgb2 = parseColorOrStop(color2);
|
|
71976
|
+
var lch1 = oklabToOklch(rgbToOklab(rgb1)),
|
|
71977
|
+
lch2 = oklabToOklch(rgbToOklab(rgb2));
|
|
71978
|
+
var vibrance = opts && opts.vibrance || 0;
|
|
71979
|
+
var steps = opts && opts.steps;
|
|
71980
|
+
var longHue = opts && opts.hue == 'longer' &&
|
|
71981
|
+
lch1.c >= ACHROMATIC && lch2.c >= ACHROMATIC;
|
|
71982
|
+
var maxShift = MAX_LIGHTNESS_SHIFT;
|
|
71983
|
+
var dh, lSecond, vivid1, vivid2;
|
|
71984
|
+
if (steps > 0) {
|
|
71985
|
+
maxShift = Math.min(maxShift, LIGHTNESS_SHIFT_SHARE * Math.abs(lch2.l - lch1.l) / steps);
|
|
71986
|
+
}
|
|
71987
|
+
if (lch1.c < ACHROMATIC && lch2.c < ACHROMATIC) vibrance = 0;
|
|
71988
|
+
// the ends' chroma with vibrance, before a gray end takes the other's hue
|
|
71989
|
+
vivid1 = getVibrantChroma(lch1, vibrance);
|
|
71990
|
+
vivid2 = getVibrantChroma(lch2, vibrance);
|
|
71991
|
+
if (lch1.c < ACHROMATIC && lch2.c >= ACHROMATIC) lch1.h = lch2.h;
|
|
71992
|
+
if (lch2.c < ACHROMATIC && lch1.c >= ACHROMATIC) lch2.h = lch1.h;
|
|
71993
|
+
dh = lch2.h - lch1.h;
|
|
71994
|
+
if (dh > 180) dh -= 360;
|
|
71995
|
+
if (dh < -180) dh += 360;
|
|
71996
|
+
if (longHue) dh = dh > 0 ? dh - 360 : dh + 360;
|
|
71997
|
+
if (vibrance > 0 && steps >= 2) {
|
|
71998
|
+
lSecond = getSecondLightness(lch1, lch2, dh, vibrance, steps, vivid1, vivid2);
|
|
71999
|
+
}
|
|
72000
|
+
return function(t) {
|
|
72001
|
+
var h = lch1.h + dh * t;
|
|
72002
|
+
var ideal = {
|
|
72003
|
+
l: lch1.l + (lch2.l - lch1.l) * t,
|
|
72004
|
+
c: t <= 0 ? vivid1 : t >= 1 ? vivid2 : lch1.c + (lch2.c - lch1.c) * t,
|
|
72005
|
+
h: normalizeHue(h)
|
|
72006
|
+
};
|
|
72007
|
+
var l = ideal.l, c = ideal.c, rgb;
|
|
72008
|
+
if (t > 0 && t < 1) {
|
|
72009
|
+
if (lSecond !== undefined) {
|
|
72010
|
+
ideal.l = getLightnessFromSecond(t, lch1.l, lch2.l, lSecond, steps);
|
|
72011
|
+
}
|
|
72012
|
+
ideal.c += vibrance;
|
|
72013
|
+
l = fitLightness(ideal.l, ideal.c, ideal.h, maxShift);
|
|
72014
|
+
c = Math.min(ideal.c, getMaxChroma(l, ideal.h));
|
|
72015
|
+
}
|
|
72016
|
+
rgb = oklchToRgb({l: l, c: c, h: ideal.h});
|
|
72017
|
+
rgb.a = rgb1.a + (rgb2.a - rgb1.a) * t;
|
|
72018
|
+
return {
|
|
72019
|
+
color: formatColor(rgb),
|
|
72020
|
+
l: l, c: c, h: ideal.h,
|
|
72021
|
+
ideal: ideal,
|
|
72022
|
+
adjusted: Math.abs(l - ideal.l) > DEVIATION || ideal.c - c > DEVIATION
|
|
72023
|
+
};
|
|
72024
|
+
};
|
|
72025
|
+
}
|
|
72026
|
+
|
|
72027
|
+
// The lightness of the color next to the lighter end of a classed ramp from
|
|
72028
|
+
// lch1 to lch2, boosted by vibrance. Each gains chroma as far as the gamut
|
|
72029
|
+
// has room at its unshifted lightness; where the color gains more than the
|
|
72030
|
+
// end, it moves toward the end by VIBRANCE_LIGHTNESS times the difference,
|
|
72031
|
+
// stopping at the end's lightness.
|
|
72032
|
+
function getSecondLightness(lch1, lch2, dh, vibrance, steps, vivid1, vivid2) {
|
|
72033
|
+
var lightFirst = lch1.l >= lch2.l;
|
|
72034
|
+
var light = lightFirst ? lch1 : lch2;
|
|
72035
|
+
var endGain = (lightFirst ? vivid1 : vivid2) - light.c;
|
|
72036
|
+
var t = lightFirst ? 1 / steps : 1 - 1 / steps;
|
|
72037
|
+
var l = lch1.l + (lch2.l - lch1.l) * t;
|
|
72038
|
+
var c = lch1.c + (lch2.c - lch1.c) * t;
|
|
72039
|
+
var h = normalizeHue(lch1.h + dh * t);
|
|
72040
|
+
var gain = Math.max(0, Math.min(c + vibrance, getMaxChroma(l, h)) - c);
|
|
72041
|
+
var shift = Math.min(VIBRANCE_LIGHTNESS * Math.max(0, gain - endGain), Math.abs(light.l - l));
|
|
72042
|
+
return light.l > l ? l + shift : l - shift;
|
|
72043
|
+
}
|
|
72044
|
+
|
|
72045
|
+
// The lightness at t of a classed ramp from lightness l1 to l2 whose color
|
|
72046
|
+
// next to the lighter end has lightness lSecond; the others are evenly
|
|
72047
|
+
// spaced from it to the darker end.
|
|
72048
|
+
function getLightnessFromSecond(t, l1, l2, lSecond, steps) {
|
|
72049
|
+
var lightFirst = l1 >= l2;
|
|
72050
|
+
var lDark = lightFirst ? l2 : l1;
|
|
72051
|
+
var u = lightFirst ? t : 1 - t; // distance from the light end
|
|
72052
|
+
var uSecond = 1 / steps;
|
|
72053
|
+
if (u <= uSecond) return lSecond;
|
|
72054
|
+
return lSecond + (lDark - lSecond) * (u - uSecond) / (1 - uSecond);
|
|
72055
|
+
}
|
|
72056
|
+
|
|
72057
|
+
function normalizeHue(h) {
|
|
72058
|
+
return h < 0 ? h + 360 : h >= 360 ? h - 360 : h;
|
|
72059
|
+
}
|
|
72060
|
+
|
|
72061
|
+
// Returns the lightness within maxShift of l that brings a color with ideal
|
|
72062
|
+
// lightness l and chroma c (at hue h) closest to its ideal, by a color
|
|
72063
|
+
// difference that weights lightness LIGHTNESS_WEIGHT times as heavily as
|
|
72064
|
+
// chroma: viewers notice lightness differences more, and even lightness
|
|
72065
|
+
// steps matter more than even chroma steps.
|
|
72066
|
+
function fitLightness(l, c, h, maxShift) {
|
|
72067
|
+
var n = 20;
|
|
72068
|
+
var lo = clamp$1(l - maxShift, 0, 1), hi = clamp$1(l + maxShift, 0, 1);
|
|
72069
|
+
var best = l, bestCost = getFitCost(l, l, c, h);
|
|
72070
|
+
var step = (hi - lo) / n, l2, cost, i, a, b, m1, m2;
|
|
72071
|
+
if (bestCost === 0 || !(maxShift > 0)) return l;
|
|
72072
|
+
for (i=0; i<=n; i++) {
|
|
72073
|
+
l2 = lo + step * i;
|
|
72074
|
+
cost = getFitCost(l2, l, c, h);
|
|
72075
|
+
if (cost < bestCost) {
|
|
72076
|
+
best = l2;
|
|
72077
|
+
bestCost = cost;
|
|
72078
|
+
}
|
|
72079
|
+
}
|
|
72080
|
+
// refine between the samples on either side of the best one
|
|
72081
|
+
a = Math.max(lo, best - step);
|
|
72082
|
+
b = Math.min(hi, best + step);
|
|
72083
|
+
for (i=0; i<20; i++) {
|
|
72084
|
+
m1 = a + (b - a) / 3;
|
|
72085
|
+
m2 = b - (b - a) / 3;
|
|
72086
|
+
if (getFitCost(m1, l, c, h) < getFitCost(m2, l, c, h)) {
|
|
72087
|
+
b = m2;
|
|
72088
|
+
} else {
|
|
72089
|
+
a = m1;
|
|
72090
|
+
}
|
|
72091
|
+
}
|
|
72092
|
+
l2 = (a + b) / 2;
|
|
72093
|
+
return getFitCost(l2, l, c, h) < bestCost ? l2 : best;
|
|
72094
|
+
}
|
|
72095
|
+
|
|
72096
|
+
function getFitCost(l2, l, c, h) {
|
|
72097
|
+
var dl = (l2 - l) * LIGHTNESS_WEIGHT;
|
|
72098
|
+
var dc = Math.max(0, c - getMaxChroma(l2, h));
|
|
72099
|
+
return dl * dl + dc * dc;
|
|
72100
|
+
}
|
|
72101
|
+
|
|
72102
|
+
function clamp$1(val, min, max) {
|
|
72103
|
+
return val < min ? min : val > max ? max : val;
|
|
72104
|
+
}
|
|
72105
|
+
|
|
72106
|
+
// The most chroma the sRGB gamut has at a lightness and hue
|
|
72107
|
+
function getMaxChroma(l, h) {
|
|
72108
|
+
return findMaxChroma(l, h, 0.4);
|
|
72109
|
+
}
|
|
72110
|
+
|
|
72111
|
+
// Converts an OKLCH color to sRGB (0-255 channels). A color outside the sRGB
|
|
72112
|
+
// gamut keeps its hue, then its lightness, and gives up chroma: chroma is
|
|
72113
|
+
// reduced to the most the gamut has at that hue and lightness.
|
|
72114
|
+
function oklchToRgb(lch) {
|
|
72115
|
+
var lab = oklchToOklab(lch);
|
|
72116
|
+
if (!oklabIsInGamut(lab)) {
|
|
72117
|
+
lab = oklchToOklab({l: lch.l, c: findMaxChroma(lch.l, lch.h, lch.c), h: lch.h});
|
|
72118
|
+
}
|
|
72119
|
+
return oklabToRgb(lab);
|
|
72120
|
+
}
|
|
72121
|
+
|
|
72122
|
+
function findMaxChroma(l, h, maxChroma) {
|
|
72123
|
+
var lo = 0, hi = maxChroma, mid;
|
|
72124
|
+
for (var i=0; i<24; i++) {
|
|
72125
|
+
mid = (lo + hi) / 2;
|
|
72126
|
+
if (oklabIsInGamut(oklchToOklab({l: l, c: mid, h: h}))) {
|
|
72127
|
+
lo = mid;
|
|
72128
|
+
} else {
|
|
72129
|
+
hi = mid;
|
|
72130
|
+
}
|
|
72131
|
+
}
|
|
72132
|
+
return lo;
|
|
72133
|
+
}
|
|
72134
|
+
|
|
72135
|
+
function oklabIsInGamut(lab) {
|
|
72136
|
+
var rgb = oklabToLinearRgb(lab);
|
|
72137
|
+
var e = 1e-6;
|
|
72138
|
+
return rgb.r >= -e && rgb.r <= 1 + e && rgb.g >= -e && rgb.g <= 1 + e &&
|
|
72139
|
+
rgb.b >= -e && rgb.b <= 1 + e;
|
|
72140
|
+
}
|
|
72141
|
+
|
|
72142
|
+
function oklabToLinearRgb(lab) {
|
|
72143
|
+
var l = cube(lab.l + 0.3963377774 * lab.a + 0.2158037573 * lab.b),
|
|
72144
|
+
m = cube(lab.l - 0.1055613458 * lab.a - 0.0638541728 * lab.b),
|
|
72145
|
+
s = cube(lab.l - 0.0894841775 * lab.a - 1.2914855480 * lab.b);
|
|
72146
|
+
return {
|
|
72147
|
+
r: 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
72148
|
+
g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
72149
|
+
b: -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
|
|
72150
|
+
};
|
|
72151
|
+
}
|
|
72152
|
+
|
|
72153
|
+
function parseColorOrStop(color) {
|
|
72154
|
+
var rgb = parseColor(color);
|
|
72155
|
+
if (!rgb) stop$1('Unsupported color:', color);
|
|
72156
|
+
return rgb;
|
|
72157
|
+
}
|
|
72158
|
+
|
|
72159
|
+
function toLinear(c) {
|
|
72160
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
72161
|
+
}
|
|
72162
|
+
|
|
72163
|
+
function fromLinear(c) {
|
|
72164
|
+
c = c < 0 ? 0 : c > 1 ? 1 : c;
|
|
72165
|
+
return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
72166
|
+
}
|
|
72167
|
+
|
|
72168
|
+
function cube(x) {
|
|
72169
|
+
return x * x * x;
|
|
72170
|
+
}
|
|
72171
|
+
|
|
72172
|
+
var Oklab = /*#__PURE__*/Object.freeze({
|
|
72173
|
+
__proto__: null,
|
|
72174
|
+
MAX_VIBRANCE_CHROMA: MAX_VIBRANCE_CHROMA,
|
|
72175
|
+
getMaxChroma: getMaxChroma,
|
|
72176
|
+
getOklchInterpolator: getOklchInterpolator,
|
|
72177
|
+
getVibranceChroma: getVibranceChroma,
|
|
72178
|
+
getVibrantColor: getVibrantColor,
|
|
72179
|
+
getVibrantTile: getVibrantTile,
|
|
72180
|
+
interpolateOklch: interpolateOklch,
|
|
72181
|
+
oklabIsInGamut: oklabIsInGamut,
|
|
72182
|
+
oklabToOklch: oklabToOklch,
|
|
72183
|
+
oklabToRgb: oklabToRgb,
|
|
72184
|
+
oklchToOklab: oklchToOklab,
|
|
72185
|
+
oklchToRgb: oklchToRgb,
|
|
72186
|
+
rgbToOklab: rgbToOklab
|
|
72187
|
+
});
|
|
72188
|
+
|
|
70476
72189
|
// TODO: support three or more stops
|
|
70477
72190
|
function getGradientFunction(stops) {
|
|
70478
72191
|
var min = stops[0] / 100,
|
|
@@ -70488,8 +72201,41 @@ ${svg}
|
|
|
70488
72201
|
};
|
|
70489
72202
|
}
|
|
70490
72203
|
|
|
70491
|
-
|
|
70492
|
-
|
|
72204
|
+
// Returns the interpolation options of -classify, checked, with
|
|
72205
|
+
// interpolation=oklch filled in when vibrance= is given without a method.
|
|
72206
|
+
// vibrance= (0-1) is returned as OKLCH chroma, as the interpolators take it.
|
|
72207
|
+
function getInterpolationOptions(opts) {
|
|
72208
|
+
var method = opts.interpolation;
|
|
72209
|
+
var vibrance = opts.vibrance;
|
|
72210
|
+
if (method && method != 'rgb' && method != 'oklch') {
|
|
72211
|
+
stop$1('Unsupported interpolation method:', method, '(expected rgb or oklch)');
|
|
72212
|
+
}
|
|
72213
|
+
if (vibrance === undefined) {
|
|
72214
|
+
return {interpolation: method};
|
|
72215
|
+
}
|
|
72216
|
+
if (method && method != 'oklch') {
|
|
72217
|
+
stop$1('vibrance= requires interpolation=oklch');
|
|
72218
|
+
}
|
|
72219
|
+
if (!(vibrance >= 0 && vibrance <= 1)) {
|
|
72220
|
+
stop$1('vibrance= takes a value from 0 to 1');
|
|
72221
|
+
}
|
|
72222
|
+
return {interpolation: 'oklch', vibrance: getVibranceChroma(vibrance)};
|
|
72223
|
+
}
|
|
72224
|
+
|
|
72225
|
+
// opts.interpolation: 'rgb' (default) or 'oklch'; oklch applies only to pairs
|
|
72226
|
+
// of colors. opts.vibrance goes with oklch (see
|
|
72227
|
+
// interpolateOklch()).
|
|
72228
|
+
function getPairInterpolator(a, b, opts) {
|
|
72229
|
+
var method = opts && opts.interpolation;
|
|
72230
|
+
if (method == 'oklch' && parseColor(a) && parseColor(b)) {
|
|
72231
|
+
return interpolateOklch(a, b, opts);
|
|
72232
|
+
}
|
|
72233
|
+
return d3_interpolate(a, b);
|
|
72234
|
+
}
|
|
72235
|
+
|
|
72236
|
+
// opts: interpolation options (see getPairInterpolator())
|
|
72237
|
+
function getStoppedValues(values, stops, opts) {
|
|
72238
|
+
var interpolate = getInterpolatedValueGetter(values, null, opts);
|
|
70493
72239
|
var n = values.length;
|
|
70494
72240
|
var fstop = getGradientFunction(stops);
|
|
70495
72241
|
var values2 = [];
|
|
@@ -70503,11 +72249,11 @@ ${svg}
|
|
|
70503
72249
|
}
|
|
70504
72250
|
|
|
70505
72251
|
// convert a continuous index ([0, n-1], -1) to a corresponding interpolated value
|
|
70506
|
-
function getInterpolatedValueGetter(values, nullValue) {
|
|
72252
|
+
function getInterpolatedValueGetter(values, nullValue, opts) {
|
|
70507
72253
|
var interpolators = [];
|
|
70508
72254
|
var tmax = values.length - 1;
|
|
70509
72255
|
for (var i=1; i<values.length; i++) {
|
|
70510
|
-
interpolators.push(
|
|
72256
|
+
interpolators.push(getPairInterpolator(values[i-1], values[i], opts));
|
|
70511
72257
|
}
|
|
70512
72258
|
return function(t) {
|
|
70513
72259
|
if (t == -1) return nullValue;
|
|
@@ -70523,26 +72269,38 @@ ${svg}
|
|
|
70523
72269
|
// return an array of n values
|
|
70524
72270
|
// assumes that values can be interpolated by d3-interpolate
|
|
70525
72271
|
// (colors and numbers should work)
|
|
70526
|
-
function interpolateValuesToClasses(values, n, stops) {
|
|
70527
|
-
if (values.length == n && !stops) return values;
|
|
72272
|
+
function interpolateValuesToClasses(values, n, stops, opts) {
|
|
72273
|
+
if (values.length == n && !stops) return getVibrantValues(values, opts);
|
|
70528
72274
|
var numPairs = values.length - 1;
|
|
70529
|
-
var
|
|
72275
|
+
var pairOpts = Object.assign({}, opts, {steps: (n - 1) / numPairs});
|
|
72276
|
+
var output = [getVibrantValues(values, opts)[0]];
|
|
70530
72277
|
var k, j, t, intVal;
|
|
70531
72278
|
for (var i=1; i<n-1; i++) {
|
|
70532
72279
|
k = i / (n-1) * numPairs;
|
|
70533
72280
|
j = Math.floor(k);
|
|
70534
72281
|
t = k - j;
|
|
70535
72282
|
// if (convert) t = convert(t);
|
|
70536
|
-
intVal =
|
|
72283
|
+
intVal = getPairInterpolator(values[j], values[j+1], pairOpts)(t);
|
|
70537
72284
|
output.push(intVal);
|
|
70538
72285
|
}
|
|
70539
|
-
output.push(values[values.length - 1]);
|
|
72286
|
+
output.push(getVibrantValues(values, opts)[values.length - 1]);
|
|
70540
72287
|
if (stops) {
|
|
70541
|
-
|
|
72288
|
+
// the colors already have their vibrance
|
|
72289
|
+
output = getStoppedValues(output, stops, {interpolation: opts && opts.interpolation});
|
|
70542
72290
|
}
|
|
70543
72291
|
return output;
|
|
70544
72292
|
}
|
|
70545
72293
|
|
|
72294
|
+
// The user's colors, with vibrance added (see getVibrantColor()), as the
|
|
72295
|
+
// oklch interpolator gives them at the ends of each pair
|
|
72296
|
+
function getVibrantValues(values, opts) {
|
|
72297
|
+
var vibrance = opts && opts.interpolation == 'oklch' && opts.vibrance;
|
|
72298
|
+
if (!(vibrance > 0)) return values;
|
|
72299
|
+
return values.map(function(val) {
|
|
72300
|
+
return parseColor(val) ? getVibrantColor(val, vibrance) : val;
|
|
72301
|
+
});
|
|
72302
|
+
}
|
|
72303
|
+
|
|
70546
72304
|
// convert an index (0 ... n-1, -1, -2) to a corresponding discreet value
|
|
70547
72305
|
function getDiscreteValueGetter(values, nullValue, otherValue) {
|
|
70548
72306
|
var n = values.length;
|
|
@@ -70561,7 +72319,7 @@ ${svg}
|
|
|
70561
72319
|
// get a function to convert class indexes to output values
|
|
70562
72320
|
//
|
|
70563
72321
|
if (opts.continuous) {
|
|
70564
|
-
return getInterpolatedValueGetter(classValues, nullValue);
|
|
72322
|
+
return getInterpolatedValueGetter(classValues, nullValue, opts);
|
|
70565
72323
|
} else {
|
|
70566
72324
|
return getDiscreteValueGetter(classValues, nullValue, opts.other);
|
|
70567
72325
|
}
|
|
@@ -70886,17 +72644,9 @@ ${svg}
|
|
|
70886
72644
|
} else if (opts.breaks) {
|
|
70887
72645
|
// user-defined breaks
|
|
70888
72646
|
breaks = opts.breaks;
|
|
70889
|
-
} else if (method == 'equal-interval') {
|
|
70890
|
-
breaks = getEqualIntervalBreaks(ascending, numBreaks);
|
|
70891
|
-
} else if (method == 'quantile') {
|
|
70892
|
-
breaks = getQuantileBreaks(ascending, numBreaks);
|
|
70893
|
-
} else if (method == 'hybrid') {
|
|
70894
|
-
breaks = getHybridBreaks(ascending, numBreaks);
|
|
70895
|
-
} else if (method == 'nice') {
|
|
70896
|
-
breaks = getNiceBreaks(ascending, numBreaks);
|
|
70897
|
-
message('Nice breaks:', breaks);
|
|
70898
72647
|
} else {
|
|
70899
|
-
|
|
72648
|
+
breaks = getSequentialBreaks(ascending, method, numBreaks);
|
|
72649
|
+
if (method == 'nice') message('Nice breaks:', breaks);
|
|
70900
72650
|
}
|
|
70901
72651
|
|
|
70902
72652
|
printDistributionInfo(ascending, breaks, nullCount);
|
|
@@ -70928,6 +72678,18 @@ ${svg}
|
|
|
70928
72678
|
return classifier;
|
|
70929
72679
|
}
|
|
70930
72680
|
|
|
72681
|
+
// The inner breaks of a sequential method's classes (also used by the GUI,
|
|
72682
|
+
// to show each class's range)
|
|
72683
|
+
// ascending: the data values, in ascending order
|
|
72684
|
+
function getSequentialBreaks(ascending, method, numBreaks) {
|
|
72685
|
+
if (numBreaks === 0) return [];
|
|
72686
|
+
if (method == 'equal-interval') return getEqualIntervalBreaks(ascending, numBreaks);
|
|
72687
|
+
if (method == 'quantile') return getQuantileBreaks(ascending, numBreaks);
|
|
72688
|
+
if (method == 'hybrid') return getHybridBreaks(ascending, numBreaks);
|
|
72689
|
+
if (method == 'nice') return getNiceBreaks(ascending, numBreaks);
|
|
72690
|
+
stop$1('Unknown classification method:', method);
|
|
72691
|
+
}
|
|
72692
|
+
|
|
70931
72693
|
function getClassRanges(breaks, ascending) {
|
|
70932
72694
|
var ranges = [];
|
|
70933
72695
|
var ids, geBound, ltBound, range;
|
|
@@ -71001,7 +72763,7 @@ ${svg}
|
|
|
71001
72763
|
return function(val) {
|
|
71002
72764
|
var i = -1;
|
|
71003
72765
|
if (Number(val) === val) { // exclude null, NaN, strings, etc.
|
|
71004
|
-
if (round) val = val
|
|
72766
|
+
if (round) val = round(val);
|
|
71005
72767
|
i = getClassId(val, breaks);
|
|
71006
72768
|
}
|
|
71007
72769
|
if (inverted && i > -1) {
|
|
@@ -71074,6 +72836,458 @@ ${svg}
|
|
|
71074
72836
|
return arraysAreIdentical(arr, utils.genericSort(arr.map(parseFloat), false));
|
|
71075
72837
|
}
|
|
71076
72838
|
|
|
72839
|
+
// Diverging classification: classes on either side of a pivot value, and an
|
|
72840
|
+
// optional neutral class at the pivot. Each side is classified on its own
|
|
72841
|
+
// data. See docs/development/color-scheme-panel-design.md
|
|
72842
|
+
//
|
|
72843
|
+
// A layout is {pivot, neutral, below, above, breaks, extent, dataRange}:
|
|
72844
|
+
// neutral is the range [lo, hi) of the neutral class, or null; below and
|
|
72845
|
+
// above are the numbers of classes on either side (outside the neutral
|
|
72846
|
+
// class); breaks are the inner breaks of all the classes, in ascending
|
|
72847
|
+
// order, for the sequential classifier. Classes are numbered from the
|
|
72848
|
+
// lowest: below, then the neutral class, then above. extent is where the
|
|
72849
|
+
// outer classes end: the data's min and max, or for equal interval and nice,
|
|
72850
|
+
// whole steps out from the pivot. dataRange is the data's [min, max].
|
|
72851
|
+
|
|
72852
|
+
var intervalMethods = ['equal-interval', 'nice'];
|
|
72853
|
+
var quantileMethods = ['quantile', 'hybrid'];
|
|
72854
|
+
|
|
72855
|
+
function isDivergingClassification(opts) {
|
|
72856
|
+
return opts.pivot !== undefined || !!opts.pivot_range;
|
|
72857
|
+
}
|
|
72858
|
+
|
|
72859
|
+
// Whether a diverging classification has a pivot (neutral) class: classed
|
|
72860
|
+
// output has one unless no-pivot-class is given; continuous output only with
|
|
72861
|
+
// pivot-class or pivot-range=
|
|
72862
|
+
function hasPivotClass(opts) {
|
|
72863
|
+
if (opts.pivot_class && opts.no_pivot_class) {
|
|
72864
|
+
stop$1('Use pivot-class or no-pivot-class, not both');
|
|
72865
|
+
}
|
|
72866
|
+
if (opts.continuous) {
|
|
72867
|
+
return !!(opts.pivot_class || opts.pivot_range);
|
|
72868
|
+
}
|
|
72869
|
+
return !opts.no_pivot_class;
|
|
72870
|
+
}
|
|
72871
|
+
|
|
72872
|
+
// ascending: the data values, in ascending order
|
|
72873
|
+
// opts: pivot, pivot_range, pivot_class, no_pivot_class, continuous,
|
|
72874
|
+
// classes (a total, or the numbers of classes [below, above]), breaks
|
|
72875
|
+
function getDivergingLayout(ascending, method, opts) {
|
|
72876
|
+
var range = opts.pivot_range || null;
|
|
72877
|
+
var hasNeutral = hasPivotClass(opts);
|
|
72878
|
+
var pivot, counts, layout;
|
|
72879
|
+
if (ascending.length === 0) {
|
|
72880
|
+
stop$1('No numeric data to classify');
|
|
72881
|
+
}
|
|
72882
|
+
if (range) {
|
|
72883
|
+
if (range.length != 2 || !(range[0] <= range[1])) {
|
|
72884
|
+
stop$1('pivot-range= takes two numbers, low,high');
|
|
72885
|
+
}
|
|
72886
|
+
if (opts.no_pivot_class) stop$1('pivot-range= sets the range of the pivot class (remove no-pivot-class)');
|
|
72887
|
+
}
|
|
72888
|
+
pivot = resolvePivot(opts.pivot, ascending, range);
|
|
72889
|
+
if (range && (pivot < range[0] || pivot > range[1])) {
|
|
72890
|
+
stop$1('The pivot', pivot, 'is outside pivot-range=', range.join(','));
|
|
72891
|
+
}
|
|
72892
|
+
if (opts.breaks) {
|
|
72893
|
+
if (range) stop$1('Use breaks= or pivot-range=, not both');
|
|
72894
|
+
layout = getBreaksLayout(opts.breaks, pivot, hasNeutral);
|
|
72895
|
+
} else {
|
|
72896
|
+
counts = parseClassCounts(opts.classes);
|
|
72897
|
+
if (intervalMethods.includes(method)) {
|
|
72898
|
+
layout = getIntervalLayout(ascending, pivot, hasNeutral, range, counts, method == 'nice');
|
|
72899
|
+
} else if (quantileMethods.includes(method)) {
|
|
72900
|
+
layout = getQuantileLayout(ascending, pivot, hasNeutral, range, counts, method);
|
|
72901
|
+
} else {
|
|
72902
|
+
stop$1('The', method, 'method does not support pivot=');
|
|
72903
|
+
}
|
|
72904
|
+
}
|
|
72905
|
+
if (layout.below + layout.above === 0 && !layout.neutral) {
|
|
72906
|
+
stop$1('Unable to classify the data around the pivot');
|
|
72907
|
+
}
|
|
72908
|
+
layout.breaks = layout.breaks.map(tidyNumber);
|
|
72909
|
+
if (layout.neutral) layout.neutral = layout.neutral.map(tidyNumber);
|
|
72910
|
+
layout.dataRange = [ascending[0], ascending[ascending.length - 1]];
|
|
72911
|
+
layout.extent = (layout.extent || layout.dataRange).map(tidyNumber);
|
|
72912
|
+
return layout;
|
|
72913
|
+
}
|
|
72914
|
+
|
|
72915
|
+
// The values at the color stops of continuous output: each side with
|
|
72916
|
+
// classes is a ramp of its own, with a stop at each of its breaks, from the
|
|
72917
|
+
// side's inner edge (the pivot or the pivot class) to the end of its outer
|
|
72918
|
+
// class. Returns {below, above}, the stops of each side in ascending order
|
|
72919
|
+
// (m + 1 stops for m classes, none for a side without classes).
|
|
72920
|
+
function getDivergingStops(layout) {
|
|
72921
|
+
var edges = getSideEdges(layout);
|
|
72922
|
+
var below = [], above = [];
|
|
72923
|
+
if (layout.below > 0) {
|
|
72924
|
+
below = [Math.min(layout.extent[0], edges[0])]
|
|
72925
|
+
.concat(layout.breaks.filter(function(b) { return b < edges[0]; }), [edges[0]]);
|
|
72926
|
+
}
|
|
72927
|
+
if (layout.above > 0) {
|
|
72928
|
+
above = [edges[1]]
|
|
72929
|
+
.concat(layout.breaks.filter(function(b) { return b > edges[1]; }),
|
|
72930
|
+
[Math.max(layout.extent[1], edges[1])]);
|
|
72931
|
+
}
|
|
72932
|
+
return {below: below, above: above};
|
|
72933
|
+
}
|
|
72934
|
+
|
|
72935
|
+
// The inner edges of the two sides: the pivot, or the pivot class's range
|
|
72936
|
+
function getSideEdges(layout) {
|
|
72937
|
+
return layout.neutral ? layout.neutral : [layout.pivot, layout.pivot];
|
|
72938
|
+
}
|
|
72939
|
+
|
|
72940
|
+
// The number of colors one side of continuous output takes: one per stop
|
|
72941
|
+
function getContinuousSideStops(classes) {
|
|
72942
|
+
return classes > 0 ? classes + 1 : 0;
|
|
72943
|
+
}
|
|
72944
|
+
|
|
72945
|
+
// Returns a function for continuous output: each side's value is
|
|
72946
|
+
// interpolated between the values at its stops, and the pivot class has the
|
|
72947
|
+
// center value.
|
|
72948
|
+
// values: the side below's values (one per stop, ascending), the center
|
|
72949
|
+
// value (with a pivot class), and the side above's (see
|
|
72950
|
+
// getDivergingClassValues())
|
|
72951
|
+
function getContinuousDivergingClassifier(layout, values, nullValue, opts) {
|
|
72952
|
+
var stops = getDivergingStops(layout);
|
|
72953
|
+
var nb = stops.below.length;
|
|
72954
|
+
var na = stops.above.length;
|
|
72955
|
+
var low = nb > 0 ? getStopInterpolator(stops.below, values.slice(0, nb), opts) : null;
|
|
72956
|
+
var high = na > 0 ? getStopInterpolator(stops.above, values.slice(values.length - na), opts) : null;
|
|
72957
|
+
var center = layout.neutral ? values[nb] : null;
|
|
72958
|
+
var edges = getSideEdges(layout);
|
|
72959
|
+
return function(val) {
|
|
72960
|
+
if (!utils.isFiniteNumber(val)) return nullValue;
|
|
72961
|
+
if (layout.neutral && val >= edges[0] && val < edges[1]) return center;
|
|
72962
|
+
if (val < edges[0]) return (low || high)(val);
|
|
72963
|
+
return (high || low)(val);
|
|
72964
|
+
};
|
|
72965
|
+
}
|
|
72966
|
+
|
|
72967
|
+
function getStopInterpolator(stops, values, opts) {
|
|
72968
|
+
var getValue = getInterpolatedValueGetter(values, null, opts);
|
|
72969
|
+
return function(val) {
|
|
72970
|
+
return getValue(getStopPosition(stops, val));
|
|
72971
|
+
};
|
|
72972
|
+
}
|
|
72973
|
+
|
|
72974
|
+
// The position of a value among ascending stops (0 at the first stop, 1 at
|
|
72975
|
+
// the second, ...), clamped to the stops
|
|
72976
|
+
function getStopPosition(stops, val) {
|
|
72977
|
+
var n = stops.length;
|
|
72978
|
+
if (!(val > stops[0])) return 0;
|
|
72979
|
+
for (var i=1; i<n; i++) {
|
|
72980
|
+
if (val <= stops[i]) {
|
|
72981
|
+
return stops[i] > stops[i - 1] ? i - 1 + (val - stops[i - 1]) / (stops[i] - stops[i - 1]) : i;
|
|
72982
|
+
}
|
|
72983
|
+
}
|
|
72984
|
+
return n - 1;
|
|
72985
|
+
}
|
|
72986
|
+
|
|
72987
|
+
// The CLI's message about a layout
|
|
72988
|
+
function formatDivergingLayout(layout) {
|
|
72989
|
+
var parts = [layout.below + ' below'];
|
|
72990
|
+
if (layout.neutral) {
|
|
72991
|
+
parts.push('a pivot class from ' + layout.neutral[0] + ' to ' + layout.neutral[1]);
|
|
72992
|
+
}
|
|
72993
|
+
parts.push(layout.above + ' above');
|
|
72994
|
+
return 'Pivot: ' + tidyNumber(layout.pivot) + ' (classes: ' + parts.join(', ') + ')';
|
|
72995
|
+
}
|
|
72996
|
+
|
|
72997
|
+
// pivotOpt: a number, 'median', 'mean' or 'auto' (0 if the data has
|
|
72998
|
+
// values on both sides of 0, or else the median); with no pivot=, the
|
|
72999
|
+
// middle of pivot-range=
|
|
73000
|
+
function resolvePivot(pivotOpt, ascending, range) {
|
|
73001
|
+
var n = ascending.length;
|
|
73002
|
+
var val = pivotOpt;
|
|
73003
|
+
if (val === undefined || val === null || val === '') {
|
|
73004
|
+
if (range) return (range[0] + range[1]) / 2;
|
|
73005
|
+
val = 'auto';
|
|
73006
|
+
}
|
|
73007
|
+
if (val == 'auto') {
|
|
73008
|
+
return ascending[0] < 0 && ascending[n - 1] > 0 ? 0 : getMedian(ascending);
|
|
73009
|
+
}
|
|
73010
|
+
if (val == 'median') return getMedian(ascending);
|
|
73011
|
+
if (val == 'mean') return utils.sum(ascending) / n;
|
|
73012
|
+
if (utils.isString(val) && val.trim() !== '' && !isNaN(+val)) val = +val;
|
|
73013
|
+
if (!utils.isFiniteNumber(val)) {
|
|
73014
|
+
stop$1('Invalid pivot:', pivotOpt, '(expected a number, median, mean or auto)');
|
|
73015
|
+
}
|
|
73016
|
+
return val;
|
|
73017
|
+
}
|
|
73018
|
+
|
|
73019
|
+
function getMedian(ascending) {
|
|
73020
|
+
var n = ascending.length;
|
|
73021
|
+
var mid = Math.floor(n / 2);
|
|
73022
|
+
return n % 2 == 1 ? ascending[mid] : (ascending[mid - 1] + ascending[mid]) / 2;
|
|
73023
|
+
}
|
|
73024
|
+
|
|
73025
|
+
// Returns {total} (all the classes, including a neutral class) or
|
|
73026
|
+
// {below, above} (the classes on either side of it)
|
|
73027
|
+
function parseClassCounts(classes, hasNeutral) {
|
|
73028
|
+
var arr = Array.isArray(classes) ? classes : [classes];
|
|
73029
|
+
if (arr.length == 1 && utils.isInteger(arr[0]) && arr[0] >= 2) {
|
|
73030
|
+
return {total: arr[0]};
|
|
73031
|
+
}
|
|
73032
|
+
if (arr.length == 2 && arr.every(isCount) && arr[0] + arr[1] > 0) {
|
|
73033
|
+
return {below: arr[0], above: arr[1]};
|
|
73034
|
+
}
|
|
73035
|
+
stop$1('Invalid classes= for pivot=:', String(classes),
|
|
73036
|
+
'(expected a total number of classes, or the numbers below and above the pivot)');
|
|
73037
|
+
}
|
|
73038
|
+
|
|
73039
|
+
function isCount(n) {
|
|
73040
|
+
return utils.isInteger(n) && n >= 0;
|
|
73041
|
+
}
|
|
73042
|
+
|
|
73043
|
+
// Equal-interval and nice breaks: each side's classes step out from the
|
|
73044
|
+
// pivot. Given a total, the two sides share one step, and the side with
|
|
73045
|
+
// less data gets fewer classes. An automatic neutral class is one step
|
|
73046
|
+
// wide, centered on the pivot.
|
|
73047
|
+
function getIntervalLayout(ascending, pivot, hasNeutral, range, counts, nice) {
|
|
73048
|
+
var extents = [Math.max(0, pivot - ascending[0]), Math.max(0, ascending[ascending.length - 1] - pivot)];
|
|
73049
|
+
var auto = hasNeutral && !range;
|
|
73050
|
+
var fixed = range ? [pivot - range[0], range[1] - pivot] : [0, 0];
|
|
73051
|
+
var steps, offsets, sides;
|
|
73052
|
+
|
|
73053
|
+
// distances from the pivot to each side's classes
|
|
73054
|
+
function getOffsets(steps) {
|
|
73055
|
+
var half;
|
|
73056
|
+
if (!auto) return fixed;
|
|
73057
|
+
half = Math.min(steps[0] || Infinity, steps[1] || Infinity) / 2;
|
|
73058
|
+
return isFinite(half) ? [half, half] : [0, 0];
|
|
73059
|
+
}
|
|
73060
|
+
|
|
73061
|
+
function countSides(steps) {
|
|
73062
|
+
var offsets = getOffsets(steps);
|
|
73063
|
+
return [0, 1].map(function(i) {
|
|
73064
|
+
return getSideCount(extents[i], offsets[i], steps[i]);
|
|
73065
|
+
});
|
|
73066
|
+
}
|
|
73067
|
+
|
|
73068
|
+
if (counts.total) {
|
|
73069
|
+
steps = getCommonStep(extents, counts.total - (hasNeutral ? 1 : 0), auto ? null : fixed, nice);
|
|
73070
|
+
sides = countSides(steps);
|
|
73071
|
+
} else {
|
|
73072
|
+
sides = [counts.below, counts.above];
|
|
73073
|
+
if (auto) {
|
|
73074
|
+
// the neutral class is one step wide, the narrower side's step
|
|
73075
|
+
offsets = getOffsets([0, 1].map(function(i) {
|
|
73076
|
+
return sides[i] > 0 ? extents[i] / (sides[i] + 0.5) : 0;
|
|
73077
|
+
}));
|
|
73078
|
+
} else {
|
|
73079
|
+
offsets = fixed;
|
|
73080
|
+
}
|
|
73081
|
+
sides = sides.map(function(n, i) {
|
|
73082
|
+
return sideHasData(extents[i] > offsets[i], n);
|
|
73083
|
+
});
|
|
73084
|
+
steps = [0, 1].map(function(i) {
|
|
73085
|
+
return sides[i] > 0 ? (extents[i] - offsets[i]) / sides[i] : 0;
|
|
73086
|
+
});
|
|
73087
|
+
if (nice) {
|
|
73088
|
+
steps = steps.map(getNiceStep);
|
|
73089
|
+
sides = countSides(steps);
|
|
73090
|
+
}
|
|
73091
|
+
}
|
|
73092
|
+
offsets = getOffsets(steps);
|
|
73093
|
+
return Object.assign(makeLayout(pivot, hasNeutral, pivot - offsets[0], pivot + offsets[1], sides[0], sides[1],
|
|
73094
|
+
getSteppedBreaks(pivot - offsets[0], -steps[0], sides[0]),
|
|
73095
|
+
getSteppedBreaks(pivot + offsets[1], steps[1], sides[1])), {
|
|
73096
|
+
extent: [pivot - offsets[0] - sides[0] * steps[0], pivot + offsets[1] + sides[1] * steps[1]]
|
|
73097
|
+
});
|
|
73098
|
+
}
|
|
73099
|
+
|
|
73100
|
+
// The number of classes of a given step that reach from a side's inner edge
|
|
73101
|
+
// (offset from the pivot) to its data extent
|
|
73102
|
+
function getSideCount(extent, offset, step) {
|
|
73103
|
+
if (!(extent > offset) || !(step > 0)) return 0;
|
|
73104
|
+
return Math.max(1, Math.ceil((extent - offset) / step - 1e-9));
|
|
73105
|
+
}
|
|
73106
|
+
|
|
73107
|
+
function sideHasData(hasData, count) {
|
|
73108
|
+
return hasData ? count : 0;
|
|
73109
|
+
}
|
|
73110
|
+
|
|
73111
|
+
// Returns [step, step]: the step that gives the two sides a total of n
|
|
73112
|
+
// classes, with the longer side's classes reaching just to the end of its
|
|
73113
|
+
// data (or a total as close to n as the data allows, without going over).
|
|
73114
|
+
// fixed: distances from the pivot to each side's classes, or null for half
|
|
73115
|
+
// a step (an automatic neutral class)
|
|
73116
|
+
// nice: use the nice step that comes closest to n classes
|
|
73117
|
+
function getCommonStep(extents, n, fixed, nice) {
|
|
73118
|
+
var candidates = [];
|
|
73119
|
+
var best = null;
|
|
73120
|
+
var count = function(s) {
|
|
73121
|
+
return extents.reduce(function(memo, extent, i) {
|
|
73122
|
+
return memo + getSideCount(extent, fixed ? fixed[i] : s / 2, s);
|
|
73123
|
+
}, 0);
|
|
73124
|
+
};
|
|
73125
|
+
// the steps at which a side's count of classes changes
|
|
73126
|
+
extents.forEach(function(extent, i) {
|
|
73127
|
+
for (var j=fixed ? 1 : 0; j<=n; j++) {
|
|
73128
|
+
candidates.push(fixed ? (extent - fixed[i]) / j : extent / (j + 0.5));
|
|
73129
|
+
}
|
|
73130
|
+
});
|
|
73131
|
+
candidates.filter(function(s) { return s > 0 && isFinite(s); })
|
|
73132
|
+
.sort(function(a, b) { return b - a; })
|
|
73133
|
+
.forEach(function(s) {
|
|
73134
|
+
var total = count(s);
|
|
73135
|
+
if (total <= n && (!best || total > best.total)) {
|
|
73136
|
+
best = {step: s, total: total};
|
|
73137
|
+
}
|
|
73138
|
+
});
|
|
73139
|
+
if (!best) return [0, 0];
|
|
73140
|
+
if (nice) {
|
|
73141
|
+
best = getNiceSteps(best.step).reduce(function(memo, s) {
|
|
73142
|
+
var total = count(s);
|
|
73143
|
+
var diff = Math.abs(total - n);
|
|
73144
|
+
return !memo || diff < memo.diff || diff == memo.diff && s > memo.step ?
|
|
73145
|
+
{step: s, diff: diff} : memo;
|
|
73146
|
+
}, null);
|
|
73147
|
+
}
|
|
73148
|
+
return [best.step, best.step];
|
|
73149
|
+
}
|
|
73150
|
+
|
|
73151
|
+
// nice numbers: 1, 2, 2.5 or 5 times a power of 10
|
|
73152
|
+
function getNiceSteps(val) {
|
|
73153
|
+
var exp = Math.floor(Math.log10(val));
|
|
73154
|
+
var steps = [];
|
|
73155
|
+
[exp - 1, exp, exp + 1].forEach(function(e) {
|
|
73156
|
+
[1, 2, 2.5, 5].forEach(function(m) {
|
|
73157
|
+
steps.push(tidyNumber(m * Math.pow(10, e)));
|
|
73158
|
+
});
|
|
73159
|
+
});
|
|
73160
|
+
return steps;
|
|
73161
|
+
}
|
|
73162
|
+
|
|
73163
|
+
// the nice number nearest to val (on a log scale)
|
|
73164
|
+
function getNiceStep(val) {
|
|
73165
|
+
if (!(val > 0)) return val;
|
|
73166
|
+
return getNiceSteps(val).reduce(function(memo, s) {
|
|
73167
|
+
return Math.abs(Math.log(s / val)) < Math.abs(Math.log(memo / val)) ? s : memo;
|
|
73168
|
+
});
|
|
73169
|
+
}
|
|
73170
|
+
|
|
73171
|
+
// the inner breaks of n classes stepping out from edge (step < 0 below the
|
|
73172
|
+
// pivot), in ascending order
|
|
73173
|
+
function getSteppedBreaks(edge, step, n) {
|
|
73174
|
+
var breaks = [];
|
|
73175
|
+
for (var i=1; i<n; i++) {
|
|
73176
|
+
breaks.push(edge + step * i);
|
|
73177
|
+
}
|
|
73178
|
+
return step < 0 ? breaks.reverse() : breaks;
|
|
73179
|
+
}
|
|
73180
|
+
|
|
73181
|
+
// Quantile and hybrid breaks: each side's classes divide its own data.
|
|
73182
|
+
// Given a total, the two sides' classes hold the same number of features,
|
|
73183
|
+
// so the side with fewer features gets fewer classes. An automatic neutral
|
|
73184
|
+
// class holds as many features as a class does on average, taken from those
|
|
73185
|
+
// nearest the pivot, so its range is centered on the pivot.
|
|
73186
|
+
function getQuantileLayout(ascending, pivot, hasNeutral, range, counts, method) {
|
|
73187
|
+
var n = ascending.length;
|
|
73188
|
+
var getBreaks = method == 'hybrid' ? getHybridBreaks : getQuantileBreaks;
|
|
73189
|
+
var lo = pivot, hi = pivot;
|
|
73190
|
+
var classCount, inNeutral, dists, d, belowVals, aboveVals, sideClasses, below, above;
|
|
73191
|
+
if (hasNeutral && range) {
|
|
73192
|
+
lo = range[0];
|
|
73193
|
+
hi = range[1];
|
|
73194
|
+
} else if (hasNeutral) {
|
|
73195
|
+
classCount = counts.total || counts.below + counts.above + 1;
|
|
73196
|
+
inNeutral = Math.max(1, Math.round(n / classCount));
|
|
73197
|
+
dists = ascending.map(function(val) { return Math.abs(val - pivot); })
|
|
73198
|
+
.sort(function(a, b) { return a - b; });
|
|
73199
|
+
// halfway to the next value out, so that no value sits on an edge
|
|
73200
|
+
d = inNeutral < n ? (dists[inNeutral - 1] + dists[inNeutral]) / 2 : dists[n - 1] * 2 + 1;
|
|
73201
|
+
lo = pivot - d;
|
|
73202
|
+
hi = pivot + d;
|
|
73203
|
+
}
|
|
73204
|
+
belowVals = ascending.filter(function(val) { return val < lo; });
|
|
73205
|
+
aboveVals = ascending.filter(function(val) { return val >= hi; });
|
|
73206
|
+
if (counts.total) {
|
|
73207
|
+
sideClasses = counts.total - (hasNeutral ? 1 : 0);
|
|
73208
|
+
below = splitClasses(sideClasses, belowVals.length, aboveVals.length);
|
|
73209
|
+
above = belowVals.length + aboveVals.length > 0 ? sideClasses - below : 0;
|
|
73210
|
+
} else {
|
|
73211
|
+
below = sideHasData(belowVals.length > 0, counts.below);
|
|
73212
|
+
above = sideHasData(aboveVals.length > 0, counts.above);
|
|
73213
|
+
}
|
|
73214
|
+
return makeLayout(pivot, hasNeutral, lo, hi, below, above,
|
|
73215
|
+
below > 1 ? getBreaks(belowVals, below - 1) : [],
|
|
73216
|
+
above > 1 ? getBreaks(aboveVals, above - 1) : []);
|
|
73217
|
+
}
|
|
73218
|
+
|
|
73219
|
+
// The number of n classes that go below the pivot, in proportion to the
|
|
73220
|
+
// counts of features, giving each side that has features at least one
|
|
73221
|
+
function splitClasses(n, countBelow, countAbove) {
|
|
73222
|
+
var below;
|
|
73223
|
+
if (countBelow + countAbove === 0) return 0;
|
|
73224
|
+
below = Math.round(n * countBelow / (countBelow + countAbove));
|
|
73225
|
+
if (countBelow > 0 && below === 0 && n > 1) below = 1;
|
|
73226
|
+
if (countAbove > 0 && below == n && n > 1) below = n - 1;
|
|
73227
|
+
return below;
|
|
73228
|
+
}
|
|
73229
|
+
|
|
73230
|
+
// User-defined breaks: a pivot that is one of the breaks divides the
|
|
73231
|
+
// classes; one inside a class makes that class the neutral class.
|
|
73232
|
+
function getBreaksLayout(breaks, pivot, hasNeutral) {
|
|
73233
|
+
var b = breaks.length;
|
|
73234
|
+
var k = breaks.indexOf(pivot);
|
|
73235
|
+
var i;
|
|
73236
|
+
if (k > -1) {
|
|
73237
|
+
return {pivot: pivot, neutral: null, below: k + 1, above: b - k, breaks: breaks};
|
|
73238
|
+
}
|
|
73239
|
+
for (i=0; i<b && breaks[i] < pivot; i++);
|
|
73240
|
+
// the pivot is in class i
|
|
73241
|
+
if (!hasNeutral) {
|
|
73242
|
+
stop$1('Without a pivot class, the pivot must be one of the breaks');
|
|
73243
|
+
}
|
|
73244
|
+
return {
|
|
73245
|
+
pivot: pivot,
|
|
73246
|
+
neutral: [i > 0 ? breaks[i - 1] : -Infinity, i < b ? breaks[i] : Infinity],
|
|
73247
|
+
below: i,
|
|
73248
|
+
above: b - i,
|
|
73249
|
+
breaks: breaks
|
|
73250
|
+
};
|
|
73251
|
+
}
|
|
73252
|
+
|
|
73253
|
+
function makeLayout(pivot, hasNeutral, lo, hi, below, above, breaksBelow, breaksAbove) {
|
|
73254
|
+
var breaks = breaksBelow.concat();
|
|
73255
|
+
if (hasNeutral) {
|
|
73256
|
+
if (below > 0) breaks.push(lo);
|
|
73257
|
+
if (above > 0) breaks.push(hi);
|
|
73258
|
+
} else if (below > 0 && above > 0) {
|
|
73259
|
+
breaks.push(pivot);
|
|
73260
|
+
}
|
|
73261
|
+
return {
|
|
73262
|
+
pivot: pivot,
|
|
73263
|
+
neutral: hasNeutral ? [lo, hi] : null,
|
|
73264
|
+
below: below,
|
|
73265
|
+
above: above,
|
|
73266
|
+
breaks: breaks.concat(breaksAbove)
|
|
73267
|
+
};
|
|
73268
|
+
}
|
|
73269
|
+
|
|
73270
|
+
// rounds off floating point noise, like 0.30000000000000004
|
|
73271
|
+
function tidyNumber(val) {
|
|
73272
|
+
return isFinite(val) ? +val.toPrecision(12) : val;
|
|
73273
|
+
}
|
|
73274
|
+
|
|
73275
|
+
var Diverging = /*#__PURE__*/Object.freeze({
|
|
73276
|
+
__proto__: null,
|
|
73277
|
+
formatDivergingLayout: formatDivergingLayout,
|
|
73278
|
+
getContinuousDivergingClassifier: getContinuousDivergingClassifier,
|
|
73279
|
+
getContinuousSideStops: getContinuousSideStops,
|
|
73280
|
+
getDivergingLayout: getDivergingLayout,
|
|
73281
|
+
getDivergingStops: getDivergingStops,
|
|
73282
|
+
getNiceStep: getNiceStep,
|
|
73283
|
+
getStopPosition: getStopPosition,
|
|
73284
|
+
hasPivotClass: hasPivotClass,
|
|
73285
|
+
isDivergingClassification: isDivergingClassification,
|
|
73286
|
+
parseClassCounts: parseClassCounts,
|
|
73287
|
+
resolvePivot: resolvePivot,
|
|
73288
|
+
splitClasses: splitClasses
|
|
73289
|
+
});
|
|
73290
|
+
|
|
71077
73291
|
function getCategoricalClassifier(classValues, nullVal, opts) {
|
|
71078
73292
|
// categories: strings to match in the data
|
|
71079
73293
|
var categories = opts.categories;
|
|
@@ -71885,7 +74099,7 @@ ${svg}
|
|
|
71885
74099
|
addSchemesFromD3('categorical', 'Category10,Accent,Dark2,Paired,Pastel1,Pastel2,Set1,Set2,Set3,Tableau10');
|
|
71886
74100
|
addSchemesFromD3('sequential', 'Blues,Greens,Greys,Purples,Reds,Oranges,BuGn,BuPu,GnBu,OrRd,PuBuGn,PuBu,PuRd,RdPu,YlGnBu,YlGn,YlOrBr,YlOrRd');
|
|
71887
74101
|
addSchemesFromD3('rainbow', 'Cividis,CubehelixDefault,Rainbow,Warm,Cool,Sinebow,Turbo,Viridis,Magma,Inferno,Plasma');
|
|
71888
|
-
addSchemesFromD3('diverging', 'BrBG,PRGn,
|
|
74102
|
+
addSchemesFromD3('diverging', 'BrBG,PRGn,PiYG,PuOr,RdBu,RdGy,RdYlBu,RdYlGn,Spectral');
|
|
71889
74103
|
testLib(); // make sure these schemes are all available
|
|
71890
74104
|
addCategoricalScheme('Category20',
|
|
71891
74105
|
'1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5');
|
|
@@ -71967,6 +74181,13 @@ ${svg}
|
|
|
71967
74181
|
print ('\nMulti-hue/rainbow\n' + formatStringsAsGrid(index.rainbow));
|
|
71968
74182
|
}
|
|
71969
74183
|
|
|
74184
|
+
// type: categorical, sequential, rainbow or diverging
|
|
74185
|
+
function getColorSchemeNames(type) {
|
|
74186
|
+
initSchemes();
|
|
74187
|
+
if (!index[type]) error('Unknown color scheme type:', type);
|
|
74188
|
+
return index[type].concat();
|
|
74189
|
+
}
|
|
74190
|
+
|
|
71970
74191
|
function pickRandomColorScheme(type) {
|
|
71971
74192
|
initSchemes();
|
|
71972
74193
|
var names = index[type];
|
|
@@ -72016,6 +74237,14 @@ ${svg}
|
|
|
72016
74237
|
return colors;
|
|
72017
74238
|
}
|
|
72018
74239
|
|
|
74240
|
+
// All the colors of a categorical scheme
|
|
74241
|
+
function getCategoricalColors(name) {
|
|
74242
|
+
initSchemes();
|
|
74243
|
+
name = standardName(name);
|
|
74244
|
+
if (!isCategoricalColorScheme(name)) stop$1('Not a categorical color scheme:', name);
|
|
74245
|
+
return (ramps[name] || d3Scales['scheme' + name]).concat();
|
|
74246
|
+
}
|
|
74247
|
+
|
|
72019
74248
|
function wrapColors(colors, n) {
|
|
72020
74249
|
while (colors.length > 0 && colors.length < n) {
|
|
72021
74250
|
colors = colors.concat(colors.slice(0, n - colors.length));
|
|
@@ -72033,7 +74262,8 @@ ${svg}
|
|
|
72033
74262
|
return index.categorical.includes(standardName(name));
|
|
72034
74263
|
}
|
|
72035
74264
|
|
|
72036
|
-
|
|
74265
|
+
// interpOpts: how stops= colors are interpolated (see getPairInterpolator())
|
|
74266
|
+
function getColorRamp(name, n, stops, interpOpts) {
|
|
72037
74267
|
initSchemes();
|
|
72038
74268
|
name = standardName(name);
|
|
72039
74269
|
var ramps = d3Scales['scheme' + name];
|
|
@@ -72051,7 +74281,7 @@ ${svg}
|
|
|
72051
74281
|
ramp = getInterpolatedRamp(interpolate, n);
|
|
72052
74282
|
}
|
|
72053
74283
|
if (stops) {
|
|
72054
|
-
ramp = getStoppedValues(ramp, stops);
|
|
74284
|
+
ramp = getStoppedValues(ramp, stops, interpOpts);
|
|
72055
74285
|
}
|
|
72056
74286
|
return ramp;
|
|
72057
74287
|
}
|
|
@@ -72067,6 +74297,21 @@ ${svg}
|
|
|
72067
74297
|
return ramp;
|
|
72068
74298
|
}
|
|
72069
74299
|
|
|
74300
|
+
var ColorSchemes = /*#__PURE__*/Object.freeze({
|
|
74301
|
+
__proto__: null,
|
|
74302
|
+
getCategoricalColorScheme: getCategoricalColorScheme,
|
|
74303
|
+
getCategoricalColors: getCategoricalColors,
|
|
74304
|
+
getColorRamp: getColorRamp,
|
|
74305
|
+
getColorSchemeNames: getColorSchemeNames,
|
|
74306
|
+
getRandomizedCategoricalColorScheme: getRandomizedCategoricalColorScheme,
|
|
74307
|
+
isCategoricalColorScheme: isCategoricalColorScheme,
|
|
74308
|
+
isColorSchemeName: isColorSchemeName,
|
|
74309
|
+
pickRandomCategoricalScheme: pickRandomCategoricalScheme,
|
|
74310
|
+
pickRandomColorScheme: pickRandomColorScheme,
|
|
74311
|
+
printColorSchemeNames: printColorSchemeNames,
|
|
74312
|
+
wrapColors: wrapColors
|
|
74313
|
+
});
|
|
74314
|
+
|
|
72070
74315
|
function getNullValue(opts) {
|
|
72071
74316
|
var nullValue;
|
|
72072
74317
|
if ('null_value' in opts) {
|
|
@@ -72118,11 +74363,11 @@ ${svg}
|
|
|
72118
74363
|
if (categorical && isCategoricalColorScheme(colorScheme)) {
|
|
72119
74364
|
return getCategoricalColorScheme(colorScheme, n);
|
|
72120
74365
|
} else {
|
|
72121
|
-
return getColorRamp(colorScheme, n, opts.stops);
|
|
74366
|
+
return getColorRamp(colorScheme, n, opts.stops, opts);
|
|
72122
74367
|
}
|
|
72123
74368
|
} else if (opts.colors || opts.values) {
|
|
72124
74369
|
if (categorical) {
|
|
72125
|
-
return getCategoricalValues(opts.colors || opts.values, n);
|
|
74370
|
+
return getCategoricalValues(opts.colors || opts.values, n, !!opts.colors);
|
|
72126
74371
|
} else {
|
|
72127
74372
|
return getInterpolableValues(opts.colors || opts.values, n, opts);
|
|
72128
74373
|
}
|
|
@@ -72132,7 +74377,87 @@ ${svg}
|
|
|
72132
74377
|
}
|
|
72133
74378
|
}
|
|
72134
74379
|
|
|
72135
|
-
|
|
74380
|
+
// The values of the classes of a diverging layout (see getDivergingLayout()):
|
|
74381
|
+
// colors (or values) are spread over the longer side, and the shorter side
|
|
74382
|
+
// uses the ones nearest the center, so equal steps from the pivot get
|
|
74383
|
+
// equally strong colors on both sides. The middle of a scheme or list of
|
|
74384
|
+
// colors is the center color (an even list interpolates one).
|
|
74385
|
+
// With opts.continuous, each side has a value per color stop (see
|
|
74386
|
+
// getDivergingStops()) instead of one per class.
|
|
74387
|
+
function getDivergingClassValues(layoutArg, opts) {
|
|
74388
|
+
var layout = opts.continuous ? Object.assign({}, layoutArg, {
|
|
74389
|
+
below: getContinuousSideStops(layoutArg.below),
|
|
74390
|
+
above: getContinuousSideStops(layoutArg.above)
|
|
74391
|
+
}) : layoutArg;
|
|
74392
|
+
var n = layout.below + layout.above + (layout.neutral ? 1 : 0);
|
|
74393
|
+
var colorArg = opts.colors && opts.colors.length == 1 ? opts.colors[0] : null;
|
|
74394
|
+
var list = opts.colors || opts.values;
|
|
74395
|
+
var colorScheme, values;
|
|
74396
|
+
if (opts.stops) stop$1('stops= is not supported with pivot=');
|
|
74397
|
+
if (colorArg == 'random') {
|
|
74398
|
+
colorScheme = pickRandomColorScheme('diverging');
|
|
74399
|
+
message('Randomly selected color scheme:', colorScheme);
|
|
74400
|
+
} else if (isColorSchemeName(colorArg)) {
|
|
74401
|
+
colorScheme = colorArg;
|
|
74402
|
+
} else if (colorArg && !parseColor(colorArg)) {
|
|
74403
|
+
stop$1('Unrecognized color scheme name:', colorArg);
|
|
74404
|
+
} else if (opts.colors) {
|
|
74405
|
+
opts.colors.forEach(validateColor);
|
|
74406
|
+
}
|
|
74407
|
+
if (!colorScheme && !list) {
|
|
74408
|
+
values = getIndexes(n);
|
|
74409
|
+
} else if (!colorScheme && list.length == n && listFitsLayout(layout, opts)) {
|
|
74410
|
+
// one value per class
|
|
74411
|
+
values = interpolateValuesToClasses(parseValues(list), n, null, opts);
|
|
74412
|
+
} else if (!colorScheme && list.length < 2) {
|
|
74413
|
+
stop$1('Expected a color scheme or a list of two or more values');
|
|
74414
|
+
} else if (opts.invert) {
|
|
74415
|
+
// the sides trade places
|
|
74416
|
+
return getDivergingValues(colorScheme, list, layout.above, layout.below, !!layout.neutral, opts).reverse();
|
|
74417
|
+
} else {
|
|
74418
|
+
return getDivergingValues(colorScheme, list, layout.below, layout.above, !!layout.neutral, opts);
|
|
74419
|
+
}
|
|
74420
|
+
return opts.invert ? values.reverse() : values;
|
|
74421
|
+
}
|
|
74422
|
+
|
|
74423
|
+
// A list with one value per class is used as given if its middle value
|
|
74424
|
+
// falls at the pivot, or if classes= gave the numbers of classes on each side
|
|
74425
|
+
function listFitsLayout(layout, opts) {
|
|
74426
|
+
return layout.below == layout.above ||
|
|
74427
|
+
Array.isArray(opts.classes) && opts.classes.length == 2;
|
|
74428
|
+
}
|
|
74429
|
+
|
|
74430
|
+
function getDivergingValues(colorScheme, list, below, above, neutral, opts) {
|
|
74431
|
+
var k = Math.max(below, above, 1);
|
|
74432
|
+
var full = colorScheme ?
|
|
74433
|
+
getColorRamp(colorScheme, k * 2 + 1, null, opts) :
|
|
74434
|
+
getDivergingRamp(parseValues(list), k, opts);
|
|
74435
|
+
// full: k values below the center value, and k above
|
|
74436
|
+
return full.slice(k - below, k)
|
|
74437
|
+
.concat(neutral ? [full[k]] : [])
|
|
74438
|
+
.concat(full.slice(k + 1, k + 1 + above));
|
|
74439
|
+
}
|
|
74440
|
+
|
|
74441
|
+
// Interpolates each half of a list separately, so the center value stays in
|
|
74442
|
+
// the middle
|
|
74443
|
+
function getDivergingRamp(values, k, opts) {
|
|
74444
|
+
var mid = Math.floor(values.length / 2);
|
|
74445
|
+
var odd = values.length % 2 == 1;
|
|
74446
|
+
var center = odd ? values[mid] : getPairInterpolator(values[mid - 1], values[mid], opts)(0.5);
|
|
74447
|
+
var lower = values.slice(0, mid).concat([center]);
|
|
74448
|
+
var upper = [center].concat(values.slice(odd ? mid + 1 : mid));
|
|
74449
|
+
var lowerRamp = interpolateValuesToClasses(lower, k + 1, null, opts);
|
|
74450
|
+
var upperRamp = interpolateValuesToClasses(upper, k + 1, null, opts);
|
|
74451
|
+
return lowerRamp.concat(upperRamp.slice(1));
|
|
74452
|
+
}
|
|
74453
|
+
|
|
74454
|
+
// A list of colors shorter than the list of categories is repeated, as the
|
|
74455
|
+
// colors of a color scheme are
|
|
74456
|
+
function getCategoricalValues(values, n, valuesAreColors) {
|
|
74457
|
+
if (valuesAreColors && values.length < n) {
|
|
74458
|
+
message('Repeating', values.length, 'colors to match', n, 'categories.');
|
|
74459
|
+
values = wrapColors(values, n);
|
|
74460
|
+
}
|
|
72136
74461
|
if (n != values.length) {
|
|
72137
74462
|
stop$1('Mismatch in number of categories and number of values');
|
|
72138
74463
|
}
|
|
@@ -72149,18 +74474,15 @@ ${svg}
|
|
|
72149
74474
|
|
|
72150
74475
|
// TODO: check for non-interpolatable value types (e.g. boolean, text)
|
|
72151
74476
|
function getInterpolableValues(arr, n, opts) {
|
|
72152
|
-
|
|
72153
|
-
|
|
72154
|
-
return interpolateValuesToClasses(values, n, opts.stops);
|
|
72155
|
-
}
|
|
72156
|
-
return values;
|
|
74477
|
+
// with one value per class, this only adds vibrance= to colors
|
|
74478
|
+
return interpolateValuesToClasses(parseValues(arr), n, opts.stops, opts);
|
|
72157
74479
|
}
|
|
72158
74480
|
|
|
72159
74481
|
// convert strings to numbers if they all parse as numbers
|
|
72160
74482
|
// arr: an array of strings
|
|
72161
74483
|
function parseValues(strings) {
|
|
72162
74484
|
var values = strings;
|
|
72163
|
-
if (strings.every(utils.parseNumber)) {
|
|
74485
|
+
if (strings.every(str => utils.parseNumber(str) !== null)) {
|
|
72164
74486
|
values = strings.map(function(str) {
|
|
72165
74487
|
return +str;
|
|
72166
74488
|
});
|
|
@@ -72293,11 +74615,12 @@ ${svg}
|
|
|
72293
74615
|
var values, nullValue;
|
|
72294
74616
|
var classifyByValue, classifyByRecordId;
|
|
72295
74617
|
var numClasses, numValues;
|
|
72296
|
-
var method;
|
|
74618
|
+
var method, diverging, divergingLayout;
|
|
72297
74619
|
|
|
72298
74620
|
if (opts.color_scheme) {
|
|
72299
74621
|
stop$1('color-scheme is not a valid option, use colors instead');
|
|
72300
74622
|
}
|
|
74623
|
+
opts = Object.assign({}, opts, getInterpolationOptions(opts));
|
|
72301
74624
|
|
|
72302
74625
|
// get data field to use for classification
|
|
72303
74626
|
//
|
|
@@ -72332,17 +74655,35 @@ ${svg}
|
|
|
72332
74655
|
//
|
|
72333
74656
|
// expand categories if value is '*'
|
|
72334
74657
|
// use all unique values if categories option is missing
|
|
74658
|
+
// (empty values are not categories: they get the null value)
|
|
72335
74659
|
if (method == 'categorical') {
|
|
72336
74660
|
if ((!opts.categories || opts.categories.includes('*')) && dataField) {
|
|
72337
|
-
opts.categories = getUniqFieldValues(records, dataField);
|
|
74661
|
+
opts.categories = getUniqFieldValues(records, dataField).filter(isCategoryValue);
|
|
72338
74662
|
}
|
|
72339
74663
|
if (opts.categories && fieldType == 'number') {
|
|
72340
74664
|
opts.categories = opts.categories.map(str => +str);
|
|
72341
74665
|
}
|
|
72342
74666
|
}
|
|
72343
74667
|
|
|
72344
|
-
|
|
72345
|
-
|
|
74668
|
+
diverging = isDivergingClassification(opts);
|
|
74669
|
+
if (diverging) {
|
|
74670
|
+
validateDivergingOptions(method, fieldType);
|
|
74671
|
+
} else if (opts.no_pivot_class || opts.pivot_class) {
|
|
74672
|
+
stop$1(opts.pivot_class ? 'pivot-class' : 'no-pivot-class', 'requires pivot= or pivot-range=');
|
|
74673
|
+
}
|
|
74674
|
+
if (Array.isArray(opts.classes) && !diverging) {
|
|
74675
|
+
if (opts.classes.length != 1) {
|
|
74676
|
+
stop$1('classes= takes two numbers (classes below and above the pivot) only with pivot=');
|
|
74677
|
+
}
|
|
74678
|
+
opts.classes = opts.classes[0];
|
|
74679
|
+
}
|
|
74680
|
+
|
|
74681
|
+
if (diverging) {
|
|
74682
|
+
// the classes depend on the data (see getDivergingLayout())
|
|
74683
|
+
numClasses = 0;
|
|
74684
|
+
} else if (opts.classes) {
|
|
74685
|
+
// continuous output can have a single interval, from the min to the max
|
|
74686
|
+
if (!utils.isInteger(opts.classes) || opts.classes >= (opts.continuous ? 1 : 2) === false) {
|
|
72346
74687
|
stop$1('Invalid number of classes:', opts.classes, '(expected a value greater than 1)');
|
|
72347
74688
|
}
|
|
72348
74689
|
numClasses = opts.classes;
|
|
@@ -72355,24 +74696,37 @@ ${svg}
|
|
|
72355
74696
|
} else if (method == 'categorical' && opts.categories) {
|
|
72356
74697
|
numClasses = opts.categories.length;
|
|
72357
74698
|
} else if (opts.colors && opts.colors.length > 1) {
|
|
72358
|
-
|
|
74699
|
+
// continuous: each color is a stop, at the ends and breaks of the classes
|
|
74700
|
+
numClasses = opts.continuous ? opts.colors.length - 1 : opts.colors.length;
|
|
72359
74701
|
} else if (opts.values && opts.values.length > 1) {
|
|
72360
|
-
numClasses = opts.values.length;
|
|
74702
|
+
numClasses = opts.continuous ? opts.values.length - 1 : opts.values.length;
|
|
72361
74703
|
} else if (method == 'non-adjacent') {
|
|
72362
74704
|
numClasses = 5;
|
|
72363
74705
|
} else {
|
|
72364
74706
|
numClasses = 4;
|
|
72365
74707
|
}
|
|
72366
74708
|
numValues = opts.continuous ? numClasses + 1 : numClasses;
|
|
72367
|
-
if (numValues > 1 === false) {
|
|
74709
|
+
if (numValues > 1 === false && !diverging) {
|
|
72368
74710
|
stop$1('Missing a valid number of values');
|
|
72369
74711
|
}
|
|
72370
74712
|
|
|
72371
74713
|
// get colors or other values
|
|
72372
74714
|
//
|
|
72373
|
-
|
|
72374
|
-
|
|
72375
|
-
|
|
74715
|
+
if (diverging && fieldType === null) {
|
|
74716
|
+
values = []; // no data: every feature gets the null value
|
|
74717
|
+
} else if (diverging) {
|
|
74718
|
+
divergingLayout = getDivergingLayout(getDivergingData(records, dataField, opts), method,
|
|
74719
|
+
Object.assign({}, opts, {classes: getDivergingClassCount(opts)}));
|
|
74720
|
+
message(formatDivergingLayout(divergingLayout));
|
|
74721
|
+
if (opts.continuous) {
|
|
74722
|
+
message(formatDivergingStops(divergingLayout));
|
|
74723
|
+
}
|
|
74724
|
+
values = getDivergingClassValues(divergingLayout, opts);
|
|
74725
|
+
} else {
|
|
74726
|
+
values = getClassValues(method, numValues, opts);
|
|
74727
|
+
if (opts.invert) {
|
|
74728
|
+
values = values.concat().reverse();
|
|
74729
|
+
}
|
|
72376
74730
|
}
|
|
72377
74731
|
if (valuesAreColors) {
|
|
72378
74732
|
message('Colors:', formatValuesForLogging(values));
|
|
@@ -72394,6 +74748,11 @@ ${svg}
|
|
|
72394
74748
|
classifyByValue = getIndexedClassifier(values, nullValue, opts);
|
|
72395
74749
|
} else if (method == 'categorical') {
|
|
72396
74750
|
classifyByValue = getCategoricalClassifier(values, nullValue, opts);
|
|
74751
|
+
} else if (diverging && opts.continuous) {
|
|
74752
|
+
classifyByValue = getContinuousDivergingDataClassifier(divergingLayout, values, nullValue, opts);
|
|
74753
|
+
} else if (diverging) {
|
|
74754
|
+
classifyByValue = getSequentialClassifier$1(values, nullValue, getFieldValues(records, dataField), method,
|
|
74755
|
+
Object.assign({}, opts, {breaks: divergingLayout.breaks}));
|
|
72397
74756
|
} else {
|
|
72398
74757
|
classifyByValue = getSequentialClassifier$1(values, nullValue, getFieldValues(records, dataField), method, opts);
|
|
72399
74758
|
}
|
|
@@ -72426,6 +74785,73 @@ ${svg}
|
|
|
72426
74785
|
lyr.data.markSchemaChanged({operation: 'classify', field: outputField});
|
|
72427
74786
|
};
|
|
72428
74787
|
|
|
74788
|
+
function validateDivergingOptions(method, fieldType, opts) {
|
|
74789
|
+
if (fieldType === null) return; // no data
|
|
74790
|
+
if (fieldType != 'number') {
|
|
74791
|
+
stop$1('pivot= requires a numeric data field');
|
|
74792
|
+
}
|
|
74793
|
+
if (!['quantile', 'equal-interval', 'nice', 'hybrid', 'breaks'].includes(method)) {
|
|
74794
|
+
stop$1('The', method, 'method does not support pivot=');
|
|
74795
|
+
}
|
|
74796
|
+
}
|
|
74797
|
+
|
|
74798
|
+
// Rounds and clamps data values as getDivergingData() does
|
|
74799
|
+
function getContinuousDivergingDataClassifier(layout, values, nullValue, opts) {
|
|
74800
|
+
var classify = getContinuousDivergingClassifier(layout, values, nullValue, opts);
|
|
74801
|
+
var round = opts.precision ? getRoundingFunction(opts.precision) : null;
|
|
74802
|
+
var range = opts.outer_breaks;
|
|
74803
|
+
return function(val) {
|
|
74804
|
+
if (!utils.isFiniteNumber(val)) return nullValue;
|
|
74805
|
+
if (round) val = round(val);
|
|
74806
|
+
if (range) val = Math.min(Math.max(val, range[0]), range[1]);
|
|
74807
|
+
return classify(val);
|
|
74808
|
+
};
|
|
74809
|
+
}
|
|
74810
|
+
|
|
74811
|
+
function formatDivergingStops(layout) {
|
|
74812
|
+
var stops = getDivergingStops(layout);
|
|
74813
|
+
var parts = [];
|
|
74814
|
+
if (stops.below.length > 0) parts.push('below: ' + stops.below.join(', '));
|
|
74815
|
+
if (stops.above.length > 0) parts.push('above: ' + stops.above.join(', '));
|
|
74816
|
+
return 'Color stops (' + parts.join('; ') + ')';
|
|
74817
|
+
}
|
|
74818
|
+
|
|
74819
|
+
// The data that the classes divide: numbers, rounded and clamped as the
|
|
74820
|
+
// sequential classifier does
|
|
74821
|
+
function getDivergingData(records, field, opts) {
|
|
74822
|
+
var values = getFieldValues(records, field);
|
|
74823
|
+
var ascending;
|
|
74824
|
+
if (opts.precision) {
|
|
74825
|
+
values = values.map(getRoundingFunction(opts.precision));
|
|
74826
|
+
}
|
|
74827
|
+
ascending = getAscendingNumbers(values);
|
|
74828
|
+
if (opts.outer_breaks) {
|
|
74829
|
+
ascending = applyDataRange(ascending, opts.outer_breaks);
|
|
74830
|
+
}
|
|
74831
|
+
return ascending;
|
|
74832
|
+
}
|
|
74833
|
+
|
|
74834
|
+
// The total number of classes (including a pivot class), or the numbers of
|
|
74835
|
+
// classes [below, above] the pivot
|
|
74836
|
+
function getDivergingClassCount(opts) {
|
|
74837
|
+
var hasNeutral = hasPivotClass(opts);
|
|
74838
|
+
var list = opts.colors && opts.colors.length > 1 && opts.colors ||
|
|
74839
|
+
opts.values && opts.values.length > 1 && opts.values;
|
|
74840
|
+
if (opts.classes) return opts.classes;
|
|
74841
|
+
if (list && opts.continuous) {
|
|
74842
|
+
// a list of 2K+1 colors has K stops on each side, for K-1 classes
|
|
74843
|
+
return Math.max(2, (Math.floor(list.length / 2) - 1) * 2) + (hasNeutral ? 1 : 0);
|
|
74844
|
+
}
|
|
74845
|
+
if (list) return list.length;
|
|
74846
|
+
return hasNeutral ? 7 : 6;
|
|
74847
|
+
}
|
|
74848
|
+
|
|
74849
|
+
// Like the categorical classifier, counts 0 as a value and other falsy
|
|
74850
|
+
// values (null, undefined, '', NaN) as empty
|
|
74851
|
+
function isCategoryValue(val) {
|
|
74852
|
+
return !!val || val === 0;
|
|
74853
|
+
}
|
|
74854
|
+
|
|
72429
74855
|
function formatValuesForLogging(arr) {
|
|
72430
74856
|
if (arr.some(val => utils.isString(val) && val.indexOf('rgb(') === 0)) {
|
|
72431
74857
|
return formatColorsAsHex(arr);
|
|
@@ -85345,7 +87771,7 @@ ${svg}
|
|
|
85345
87771
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
85346
87772
|
}
|
|
85347
87773
|
|
|
85348
|
-
var version = "0.7.
|
|
87774
|
+
var version = "0.7.74";
|
|
85349
87775
|
|
|
85350
87776
|
// Parse command line args into commands and run them
|
|
85351
87777
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|
|
@@ -85736,392 +88162,6 @@ ${svg}
|
|
|
85736
88162
|
testCommands: testCommands
|
|
85737
88163
|
});
|
|
85738
88164
|
|
|
85739
|
-
// Finding the font file a label's font-family names, among the fonts installed
|
|
85740
|
-
// on this computer.
|
|
85741
|
-
//
|
|
85742
|
-
// The browser does this for us in the GUI; in Node there is nothing between
|
|
85743
|
-
// mapshaper and the filesystem, so a family name has to be matched against the
|
|
85744
|
-
// name tables of the files themselves. Names cannot be inferred from
|
|
85745
|
-
// filenames: NYTFranklinLight.otf calls itself "NYTFranklin Light", and a
|
|
85746
|
-
// label asking for NYTFranklin at weight 300 has to reach it.
|
|
85747
|
-
//
|
|
85748
|
-
// See docs/development/label-tool-design.md.
|
|
85749
|
-
|
|
85750
|
-
// Collections (.ttc/.otc) hold several faces in one file and are how macOS
|
|
85751
|
-
// ships Helvetica, Menlo and Avenir, so a lookup that skipped them would miss
|
|
85752
|
-
// the font mapshaper's own tool writes by default there.
|
|
85753
|
-
var FONT_FILE_RXP = /\.(ttf|otf|ttc|otc)$/i;
|
|
85754
|
-
|
|
85755
|
-
// What a generic family means when there is no browser to resolve it. A guess,
|
|
85756
|
-
// but an ordered one: the first of these that is installed is what a browser
|
|
85757
|
-
// on this platform would almost certainly have picked.
|
|
85758
|
-
var GENERIC_FAMILIES = {
|
|
85759
|
-
'sans-serif': ['Helvetica', 'Arial', 'Liberation Sans', 'DejaVu Sans', 'Roboto', 'Segoe UI'],
|
|
85760
|
-
serif: ['Times New Roman', 'Times', 'Liberation Serif', 'DejaVu Serif', 'Georgia'],
|
|
85761
|
-
monospace: ['Menlo', 'Courier New', 'Liberation Mono', 'DejaVu Sans Mono', 'Consolas'],
|
|
85762
|
-
'system-ui': ['Helvetica Neue', 'Segoe UI', 'Cantarell', 'Roboto'],
|
|
85763
|
-
cursive: [],
|
|
85764
|
-
fantasy: []
|
|
85765
|
-
};
|
|
85766
|
-
|
|
85767
|
-
var faceCache = {};
|
|
85768
|
-
var familyCache = {};
|
|
85769
|
-
var fullIndex = null;
|
|
85770
|
-
var fileList = null;
|
|
85771
|
-
|
|
85772
|
-
// Width classes, as OS/2 numbers them 1 to 9. A font-stretch is one of these
|
|
85773
|
-
// keywords or a percentage of normal width.
|
|
85774
|
-
var STRETCH_NAMES = {
|
|
85775
|
-
'ultra-condensed': 1, 'extra-condensed': 2, condensed: 3,
|
|
85776
|
-
'semi-condensed': 4, normal: 5, 'semi-expanded': 6, expanded: 7,
|
|
85777
|
-
'extra-expanded': 8, 'ultra-expanded': 9
|
|
85778
|
-
};
|
|
85779
|
-
var STRETCH_PERCENTS = [50, 62.5, 75, 87.5, 100, 112.5, 125, 150, 200];
|
|
85780
|
-
|
|
85781
|
-
// The face to open for @family in (@weight, @italic, @stretch), or null if this
|
|
85782
|
-
// computer has no such font: {path, postscriptName}.
|
|
85783
|
-
//
|
|
85784
|
-
// postscriptName is how a face inside a collection is named to fontkit, and is
|
|
85785
|
-
// null for a file holding one face.
|
|
85786
|
-
function findFontFace(family, weight, italic, stretch) {
|
|
85787
|
-
var key = [family, weight, italic ? 'i' : 'n', stretch || ''].join('|');
|
|
85788
|
-
if (!(key in faceCache)) {
|
|
85789
|
-
faceCache[key] = lookupFace(family, weight, italic, stretch);
|
|
85790
|
-
}
|
|
85791
|
-
return faceCache[key];
|
|
85792
|
-
}
|
|
85793
|
-
|
|
85794
|
-
// Which of a family's faces answers a request for (@weight, @italic,
|
|
85795
|
-
// @stretch).
|
|
85796
|
-
//
|
|
85797
|
-
// Width first, then upright before oblique, then the nearest weight, then the
|
|
85798
|
-
// heavier of two equally near. The first three are the order CSS matches fonts
|
|
85799
|
-
// in, and the last is the rule the style menu uses to carry a face across a
|
|
85800
|
-
// change of font (getNearestVariant() in gui-label-fonts.mjs), applied here to
|
|
85801
|
-
// faces read from files rather than measured in a browser.
|
|
85802
|
-
//
|
|
85803
|
-
// A missing face is answered with a near one rather than refused: the browser
|
|
85804
|
-
// would synthesize the missing weight or slant from exactly this face, so its
|
|
85805
|
-
// widths are much closer to what is drawn than no measurement at all.
|
|
85806
|
-
function pickFace(faces, weight, italic, stretch) {
|
|
85807
|
-
var wanted = weight > 0 ? weight : 400;
|
|
85808
|
-
var wantedWidth = parseFontStretch(stretch);
|
|
85809
|
-
var best = null;
|
|
85810
|
-
var bestScore = null;
|
|
85811
|
-
(faces || []).forEach(function(face) {
|
|
85812
|
-
var score = [
|
|
85813
|
-
Math.abs((face.width || 5) - wantedWidth),
|
|
85814
|
-
!!face.italic === !!italic ? 0 : 1,
|
|
85815
|
-
Math.abs(face.weight - wanted),
|
|
85816
|
-
face.weight < wanted ? 1 : 0
|
|
85817
|
-
];
|
|
85818
|
-
if (!best || compareScores(score, bestScore) < 0) {
|
|
85819
|
-
best = face;
|
|
85820
|
-
bestScore = score;
|
|
85821
|
-
}
|
|
85822
|
-
});
|
|
85823
|
-
return best;
|
|
85824
|
-
}
|
|
85825
|
-
|
|
85826
|
-
// A font-stretch as the width class it names, or normal width for anything
|
|
85827
|
-
// unreadable. A percentage is taken to the nearest class, the way CSS defines
|
|
85828
|
-
// the keywords.
|
|
85829
|
-
function parseFontStretch(stretch) {
|
|
85830
|
-
var str = String(stretch === null || stretch === undefined ? '' : stretch).trim().toLowerCase();
|
|
85831
|
-
var pct = /^([.0-9]+)%$/.exec(str);
|
|
85832
|
-
var best = 5;
|
|
85833
|
-
if (STRETCH_NAMES[str]) return STRETCH_NAMES[str];
|
|
85834
|
-
if (!pct) return 5;
|
|
85835
|
-
STRETCH_PERCENTS.forEach(function(val, i) {
|
|
85836
|
-
if (Math.abs(val - Number(pct[1])) <
|
|
85837
|
-
Math.abs(STRETCH_PERCENTS[best - 1] - Number(pct[1]))) {
|
|
85838
|
-
best = i + 1;
|
|
85839
|
-
}
|
|
85840
|
-
});
|
|
85841
|
-
return best;
|
|
85842
|
-
}
|
|
85843
|
-
|
|
85844
|
-
// Family names are compared without spaces, punctuation or case, so that
|
|
85845
|
-
// "NYTFranklin" finds "NYT Franklin" and "Helvetica Neue" finds
|
|
85846
|
-
// "HelveticaNeue". Font vendors are not consistent about any of the three.
|
|
85847
|
-
function normalizeFamilyName(name) {
|
|
85848
|
-
return String(name || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
85849
|
-
}
|
|
85850
|
-
|
|
85851
|
-
// The families a face answers to. The typographic family (name ID 16) is the
|
|
85852
|
-
// one a stylesheet means: every weight of NYTFranklin has its own ID 1 family
|
|
85853
|
-
// ("NYTFranklin Light", "NYTFranklin Medium") and they are one family only
|
|
85854
|
-
// under ID 16. Both are indexed, because plenty of fonts have no ID 16.
|
|
85855
|
-
function getFaceFamilies(font) {
|
|
85856
|
-
var records = font && font.name && font.name.records || {};
|
|
85857
|
-
return [pickName(records.preferredFamily), pickName(records.fontFamily),
|
|
85858
|
-
font && font.familyName].filter(Boolean);
|
|
85859
|
-
}
|
|
85860
|
-
|
|
85861
|
-
function clearFontCache() {
|
|
85862
|
-
faceCache = {};
|
|
85863
|
-
familyCache = {};
|
|
85864
|
-
fullIndex = null;
|
|
85865
|
-
fileList = null;
|
|
85866
|
-
}
|
|
85867
|
-
|
|
85868
|
-
function lookupFace(family, weight, italic, stretch) {
|
|
85869
|
-
var names = resolveFamilyNames(family);
|
|
85870
|
-
var face = findBestFace(names, weight, italic, stretch);
|
|
85871
|
-
// The filename guess can find a family and still miss one of its faces:
|
|
85872
|
-
// Segoe UI Semibold lives in seguisb.ttf, which does not begin with the
|
|
85873
|
-
// family's name. Anything other than the face that was asked for is worth
|
|
85874
|
-
// the full index -- once per session -- to be sure it is the nearest this
|
|
85875
|
-
// machine has.
|
|
85876
|
-
if (!fullIndex && !faceAnswersRequest(face, weight, italic, stretch)) {
|
|
85877
|
-
fullIndex = readFaces(getFontFiles());
|
|
85878
|
-
face = findBestFace(names, weight, italic, stretch) || face;
|
|
85879
|
-
}
|
|
85880
|
-
return face;
|
|
85881
|
-
}
|
|
85882
|
-
|
|
85883
|
-
function findBestFace(names, weight, italic, stretch) {
|
|
85884
|
-
for (var i = 0; i < names.length; i++) {
|
|
85885
|
-
var faces = findFamilyFaces(names[i]);
|
|
85886
|
-
if (faces && faces.length > 0) return pickFace(faces, weight, italic, stretch);
|
|
85887
|
-
}
|
|
85888
|
-
return null;
|
|
85889
|
-
}
|
|
85890
|
-
|
|
85891
|
-
function faceAnswersRequest(face, weight, italic, stretch) {
|
|
85892
|
-
return !!face && face.weight == (weight > 0 ? weight : 400) &&
|
|
85893
|
-
!!face.italic === !!italic && (face.width || 5) == parseFontStretch(stretch);
|
|
85894
|
-
}
|
|
85895
|
-
|
|
85896
|
-
// A font-family is a list, and may end in a generic: each name is tried in
|
|
85897
|
-
// turn, exactly as a browser would, and a generic stands for the first of its
|
|
85898
|
-
// candidates that is installed.
|
|
85899
|
-
function resolveFamilyNames(family) {
|
|
85900
|
-
var out = [];
|
|
85901
|
-
splitFamilyList(family).forEach(function(name) {
|
|
85902
|
-
var generic = GENERIC_FAMILIES[name.toLowerCase()];
|
|
85903
|
-
if (generic) {
|
|
85904
|
-
out = out.concat(generic);
|
|
85905
|
-
} else {
|
|
85906
|
-
out.push(name);
|
|
85907
|
-
}
|
|
85908
|
-
});
|
|
85909
|
-
return out;
|
|
85910
|
-
}
|
|
85911
|
-
|
|
85912
|
-
function splitFamilyList(family) {
|
|
85913
|
-
return String(family || '').split(',').map(function(name) {
|
|
85914
|
-
return name.trim().replace(/^['"]|['"]$/g, '');
|
|
85915
|
-
}).filter(Boolean);
|
|
85916
|
-
}
|
|
85917
|
-
|
|
85918
|
-
// Two passes, because parsing every font on the computer costs the best part
|
|
85919
|
-
// of a second and most lookups do not need it: the file holding a family is
|
|
85920
|
-
// usually named after it, so files whose name begins with the family's are
|
|
85921
|
-
// parsed first. Whether that was good enough is lookupFace()'s decision.
|
|
85922
|
-
function findFamilyFaces(family) {
|
|
85923
|
-
var key = normalizeFamilyName(family);
|
|
85924
|
-
if (!key) return null;
|
|
85925
|
-
if (fullIndex) return fullIndex[key] || null;
|
|
85926
|
-
if (!(key in familyCache)) {
|
|
85927
|
-
familyCache[key] = readFaces(getLikelyFiles(key))[key] || null;
|
|
85928
|
-
}
|
|
85929
|
-
return familyCache[key];
|
|
85930
|
-
}
|
|
85931
|
-
|
|
85932
|
-
function getLikelyFiles(key) {
|
|
85933
|
-
return getFontFiles().filter(function(file) {
|
|
85934
|
-
return normalizeFamilyName(basename(file)).indexOf(key) === 0;
|
|
85935
|
-
});
|
|
85936
|
-
}
|
|
85937
|
-
|
|
85938
|
-
// Faces by normalized family name. A file that cannot be parsed is skipped
|
|
85939
|
-
// rather than reported: a font directory can hold anything, and a broken font
|
|
85940
|
-
// is not an error in the user's data.
|
|
85941
|
-
function readFaces(files) {
|
|
85942
|
-
var index = {};
|
|
85943
|
-
files.forEach(function(file) {
|
|
85944
|
-
getFileFaces(file).forEach(function(face) {
|
|
85945
|
-
face.families.forEach(function(name) {
|
|
85946
|
-
var key = normalizeFamilyName(name);
|
|
85947
|
-
if (!key) return;
|
|
85948
|
-
if (!index[key]) index[key] = [];
|
|
85949
|
-
index[key].push(face);
|
|
85950
|
-
});
|
|
85951
|
-
});
|
|
85952
|
-
});
|
|
85953
|
-
return index;
|
|
85954
|
-
}
|
|
85955
|
-
|
|
85956
|
-
function getFileFaces(file) {
|
|
85957
|
-
var fontkit = getFontkit();
|
|
85958
|
-
var font, fonts;
|
|
85959
|
-
if (!fontkit) return [];
|
|
85960
|
-
try {
|
|
85961
|
-
font = fontkit.openSync(file);
|
|
85962
|
-
// A collection reports its members in .fonts; a single font is its own.
|
|
85963
|
-
fonts = font && font.fonts || [font];
|
|
85964
|
-
return fonts.filter(Boolean).map(function(one) {
|
|
85965
|
-
return {
|
|
85966
|
-
path: file,
|
|
85967
|
-
// Named rather than numbered because fontkit takes a name, and because
|
|
85968
|
-
// a name survives a font being reinstalled in a different order.
|
|
85969
|
-
postscriptName: font.fonts ? one.postscriptName : null,
|
|
85970
|
-
families: getFaceFamilies(one),
|
|
85971
|
-
weight: getFaceWeight(one),
|
|
85972
|
-
width: getFaceWidth(one),
|
|
85973
|
-
italic: isItalicFace(one)
|
|
85974
|
-
};
|
|
85975
|
-
}).filter(function(face) {
|
|
85976
|
-
return face.families.length > 0;
|
|
85977
|
-
});
|
|
85978
|
-
} catch (e) {
|
|
85979
|
-
return [];
|
|
85980
|
-
}
|
|
85981
|
-
}
|
|
85982
|
-
|
|
85983
|
-
function getFaceWeight(font) {
|
|
85984
|
-
var os2 = font['OS/2'];
|
|
85985
|
-
var weight = os2 && os2.usWeightClass;
|
|
85986
|
-
return weight > 0 ? weight : 400;
|
|
85987
|
-
}
|
|
85988
|
-
|
|
85989
|
-
function getFaceWidth(font) {
|
|
85990
|
-
var os2 = font['OS/2'];
|
|
85991
|
-
var width = os2 && os2.usWidthClass;
|
|
85992
|
-
return width >= 1 && width <= 9 ? width : 5;
|
|
85993
|
-
}
|
|
85994
|
-
|
|
85995
|
-
function isItalicFace(font) {
|
|
85996
|
-
var os2 = font['OS/2'];
|
|
85997
|
-
if (os2 && os2.fsSelection && typeof os2.fsSelection.italic == 'boolean') {
|
|
85998
|
-
return os2.fsSelection.italic;
|
|
85999
|
-
}
|
|
86000
|
-
return !!font.italicAngle;
|
|
86001
|
-
}
|
|
86002
|
-
|
|
86003
|
-
// A name record is a string, or an object of translations to pick English out
|
|
86004
|
-
// of, depending on the font and the version of fontkit.
|
|
86005
|
-
function pickName(rec) {
|
|
86006
|
-
if (!rec) return '';
|
|
86007
|
-
if (typeof rec == 'string') return rec;
|
|
86008
|
-
return rec.en || Object.keys(rec).map(function(k) { return rec[k]; })[0] || '';
|
|
86009
|
-
}
|
|
86010
|
-
|
|
86011
|
-
function compareScores(a, b) {
|
|
86012
|
-
for (var i = 0; i < a.length; i++) {
|
|
86013
|
-
if (a[i] != b[i]) return a[i] - b[i];
|
|
86014
|
-
}
|
|
86015
|
-
return 0;
|
|
86016
|
-
}
|
|
86017
|
-
|
|
86018
|
-
// Every font file on this computer, listed once. Listing is cheap -- a few
|
|
86019
|
-
// hundred directory entries -- next to parsing them, so this is not the part
|
|
86020
|
-
// worth avoiding.
|
|
86021
|
-
function getFontFiles() {
|
|
86022
|
-
var fs = getFs();
|
|
86023
|
-
var files = [];
|
|
86024
|
-
if (fileList) return fileList;
|
|
86025
|
-
if (!fs) return [];
|
|
86026
|
-
getFontDirs().forEach(function(dir) {
|
|
86027
|
-
var entries;
|
|
86028
|
-
try {
|
|
86029
|
-
entries = fs.readdirSync(dir, {recursive: true});
|
|
86030
|
-
} catch (e) {
|
|
86031
|
-
return; // a directory this platform does not have
|
|
86032
|
-
}
|
|
86033
|
-
entries.forEach(function(entry) {
|
|
86034
|
-
var file = dir + '/' + String(entry).split('\\').join('/');
|
|
86035
|
-
if (FONT_FILE_RXP.test(file)) files.push(file);
|
|
86036
|
-
});
|
|
86037
|
-
});
|
|
86038
|
-
fileList = files;
|
|
86039
|
-
return files;
|
|
86040
|
-
}
|
|
86041
|
-
|
|
86042
|
-
// Where each platform keeps fonts, or MAPSHAPER_FONT_PATH if it is set.
|
|
86043
|
-
//
|
|
86044
|
-
// It replaces the platform's directories rather than adding to them, which is
|
|
86045
|
-
// what makes a machine's font situation something a caller can state: a
|
|
86046
|
-
// container with its fonts somewhere of its own, a build that has to produce
|
|
86047
|
-
// the same SVG wherever it runs, a test that needs to know there is nothing to
|
|
86048
|
-
// find. Several directories are separated by : or ;.
|
|
86049
|
-
function getFontDirs() {
|
|
86050
|
-
var home = getHomeDir();
|
|
86051
|
-
var platform = typeof process == 'object' && process.platform || '';
|
|
86052
|
-
var dirs = getEnvDirs();
|
|
86053
|
-
if (dirs.length > 0) return dirs;
|
|
86054
|
-
if (platform == 'darwin') {
|
|
86055
|
-
dirs = ['/System/Library/Fonts', '/Library/Fonts',
|
|
86056
|
-
'/Network/Library/Fonts'];
|
|
86057
|
-
if (home) dirs.push(home + '/Library/Fonts');
|
|
86058
|
-
} else if (platform == 'win32') {
|
|
86059
|
-
dirs = [(getEnv('WINDIR') || 'C:\\Windows') + '/Fonts'];
|
|
86060
|
-
if (home) dirs.push(home + '/AppData/Local/Microsoft/Windows/Fonts');
|
|
86061
|
-
} else {
|
|
86062
|
-
dirs = ['/usr/share/fonts', '/usr/local/share/fonts', '/run/host/fonts'];
|
|
86063
|
-
if (home) {
|
|
86064
|
-
dirs.push(home + '/.fonts', home + '/.local/share/fonts');
|
|
86065
|
-
}
|
|
86066
|
-
}
|
|
86067
|
-
return dirs;
|
|
86068
|
-
}
|
|
86069
|
-
|
|
86070
|
-
function getEnvDirs() {
|
|
86071
|
-
var val = getEnv('MAPSHAPER_FONT_PATH');
|
|
86072
|
-
if (!val) return [];
|
|
86073
|
-
return val.split(/[:;]/).filter(Boolean);
|
|
86074
|
-
}
|
|
86075
|
-
|
|
86076
|
-
function getEnv(name) {
|
|
86077
|
-
return typeof process == 'object' && process.env && process.env[name] || '';
|
|
86078
|
-
}
|
|
86079
|
-
|
|
86080
|
-
function getHomeDir() {
|
|
86081
|
-
var os = safeRequire('os');
|
|
86082
|
-
try {
|
|
86083
|
-
return os && os.homedir() || '';
|
|
86084
|
-
} catch (e) {
|
|
86085
|
-
return '';
|
|
86086
|
-
}
|
|
86087
|
-
}
|
|
86088
|
-
|
|
86089
|
-
function basename(file) {
|
|
86090
|
-
var parts = file.split('/');
|
|
86091
|
-
return parts[parts.length - 1].replace(FONT_FILE_RXP, '');
|
|
86092
|
-
}
|
|
86093
|
-
|
|
86094
|
-
function getFs() {
|
|
86095
|
-
return safeRequire('fs');
|
|
86096
|
-
}
|
|
86097
|
-
|
|
86098
|
-
// Loaded through the require shim and only when a label actually needs
|
|
86099
|
-
// measuring, so that the browser bundle -- which is this same file -- never
|
|
86100
|
-
// reaches for a module it does not have, and a CLI run that touches no labels
|
|
86101
|
-
// never pays for loading it.
|
|
86102
|
-
function getFontkit() {
|
|
86103
|
-
return safeRequire('fontkit');
|
|
86104
|
-
}
|
|
86105
|
-
|
|
86106
|
-
function safeRequire(name) {
|
|
86107
|
-
if (runningInBrowser()) return null;
|
|
86108
|
-
try {
|
|
86109
|
-
return require$1(name) || null;
|
|
86110
|
-
} catch (e) {
|
|
86111
|
-
return null;
|
|
86112
|
-
}
|
|
86113
|
-
}
|
|
86114
|
-
|
|
86115
|
-
var FontLookup = /*#__PURE__*/Object.freeze({
|
|
86116
|
-
__proto__: null,
|
|
86117
|
-
clearFontCache: clearFontCache,
|
|
86118
|
-
findFontFace: findFontFace,
|
|
86119
|
-
getFaceFamilies: getFaceFamilies,
|
|
86120
|
-
normalizeFamilyName: normalizeFamilyName,
|
|
86121
|
-
parseFontStretch: parseFontStretch,
|
|
86122
|
-
pickFace: pickFace
|
|
86123
|
-
});
|
|
86124
|
-
|
|
86125
88165
|
// Measuring a label's text outside a browser, from the font files installed on
|
|
86126
88166
|
// this computer.
|
|
86127
88167
|
//
|
|
@@ -87118,6 +89158,139 @@ ${svg}
|
|
|
87118
89158
|
blurRasterLayers: blurRasterLayers
|
|
87119
89159
|
});
|
|
87120
89160
|
|
|
89161
|
+
// A ramp is defined by pins: [{t, color}], where t is a position from 0 (left
|
|
89162
|
+
// end) to 1 (right end). Both ends must be pinned. Pins are stored by position
|
|
89163
|
+
// rather than by tile index so they survive changes in the number of tiles.
|
|
89164
|
+
// See docs/development/color-scheme-panel-design.md
|
|
89165
|
+
|
|
89166
|
+
// Returns an array of n colors. Pinned tiles take their pin's color; the
|
|
89167
|
+
// others are interpolated in OKLCH between the nearest pinned tiles, so that
|
|
89168
|
+
// lightness, chroma and hue each change by equal steps from tile to tile.
|
|
89169
|
+
// opts.vibrance: a midtone chroma boost between each
|
|
89170
|
+
// pair of pinned tiles (see getOklchInterpolator()).
|
|
89171
|
+
function resolveRamp(pins, n, opts) {
|
|
89172
|
+
return resolveRampTiles(pins, n, opts).map(function(tile) {
|
|
89173
|
+
return tile.color;
|
|
89174
|
+
});
|
|
89175
|
+
}
|
|
89176
|
+
|
|
89177
|
+
// Like resolveRamp(), but returns a {color, pinned, adjusted, l, c, h, ideal}
|
|
89178
|
+
// object for each tile. Tiles that had to be fitted to the sRGB gamut are
|
|
89179
|
+
// adjusted, and ideal has the OKLCH values they were fitted from.
|
|
89180
|
+
function resolveRampTiles(pins, n, opts) {
|
|
89181
|
+
var slots = getPinnedSlots(pins, n);
|
|
89182
|
+
var tiles = [];
|
|
89183
|
+
var left = 0, right, interpolate, tile;
|
|
89184
|
+
for (var i=0; i<n; i++) {
|
|
89185
|
+
if (slots[i] > -1) {
|
|
89186
|
+
// vibrance raises a pinned color's chroma only as far as the gamut has
|
|
89187
|
+
// room at its own lightness; it is adjusted if that cut the vibrance short
|
|
89188
|
+
tile = getVibrantTile(pins[slots[i]].color, opts && opts.vibrance);
|
|
89189
|
+
tile.pinned = true;
|
|
89190
|
+
tiles.push(tile);
|
|
89191
|
+
left = i;
|
|
89192
|
+
continue;
|
|
89193
|
+
}
|
|
89194
|
+
if (!interpolate || right < i) {
|
|
89195
|
+
right = i + 1;
|
|
89196
|
+
while (slots[right] == -1) right++;
|
|
89197
|
+
interpolate = getOklchInterpolator(pins[slots[left]].color, pins[slots[right]].color,
|
|
89198
|
+
Object.assign({}, opts, {steps: right - left}));
|
|
89199
|
+
}
|
|
89200
|
+
tile = interpolate((i - left) / (right - left));
|
|
89201
|
+
tile.pinned = false;
|
|
89202
|
+
tiles.push(tile);
|
|
89203
|
+
}
|
|
89204
|
+
return tiles;
|
|
89205
|
+
}
|
|
89206
|
+
|
|
89207
|
+
// Returns an array of n tile slots, each containing the index of the pin
|
|
89208
|
+
// that sets the tile's color, or -1 if the tile is interpolated.
|
|
89209
|
+
// When several pins snap to the same tile, an end pin wins, otherwise the pin
|
|
89210
|
+
// closest to the tile's own position.
|
|
89211
|
+
function getPinnedSlots(pins, n) {
|
|
89212
|
+
var slots = [], dists = [];
|
|
89213
|
+
validateRamp(pins, n);
|
|
89214
|
+
for (var i=0; i<n; i++) {
|
|
89215
|
+
slots.push(-1);
|
|
89216
|
+
dists.push(Infinity);
|
|
89217
|
+
}
|
|
89218
|
+
pins.forEach(function(pin, pinId) {
|
|
89219
|
+
var k = pin.t * (n - 1);
|
|
89220
|
+
var slot = Math.round(k);
|
|
89221
|
+
var dist = isEndPin(pin) ? -1 : Math.abs(k - slot);
|
|
89222
|
+
if (dist < dists[slot]) {
|
|
89223
|
+
slots[slot] = pinId;
|
|
89224
|
+
dists[slot] = dist;
|
|
89225
|
+
}
|
|
89226
|
+
});
|
|
89227
|
+
return slots;
|
|
89228
|
+
}
|
|
89229
|
+
|
|
89230
|
+
// Returns a new pin array with tile i of an n-tile ramp pinned to color,
|
|
89231
|
+
// replacing any pins that snap to the same tile.
|
|
89232
|
+
function setRampPin(pins, n, i, color) {
|
|
89233
|
+
validateTileIndex(i, n);
|
|
89234
|
+
if (!parseColor(color)) stop$1('Unsupported color:', color);
|
|
89235
|
+
return removePinsAtSlot(pins, n, i)
|
|
89236
|
+
.concat({t: getTilePosition(i, n), color: color})
|
|
89237
|
+
.sort(function(a, b) { return a.t - b.t; });
|
|
89238
|
+
}
|
|
89239
|
+
|
|
89240
|
+
// Returns a new pin array with tile i unpinned. End tiles stay pinned.
|
|
89241
|
+
function clearRampPin(pins, n, i) {
|
|
89242
|
+
validateTileIndex(i, n);
|
|
89243
|
+
if (i === 0 || i == n - 1) return pins.concat();
|
|
89244
|
+
return removePinsAtSlot(pins, n, i);
|
|
89245
|
+
}
|
|
89246
|
+
|
|
89247
|
+
function removePinsAtSlot(pins, n, i) {
|
|
89248
|
+
validateRamp(pins, n);
|
|
89249
|
+
return pins.filter(function(pin) {
|
|
89250
|
+
return Math.round(pin.t * (n - 1)) != i;
|
|
89251
|
+
});
|
|
89252
|
+
}
|
|
89253
|
+
|
|
89254
|
+
function getTilePosition(i, n) {
|
|
89255
|
+
return i == n - 1 ? 1 : i / (n - 1);
|
|
89256
|
+
}
|
|
89257
|
+
|
|
89258
|
+
function isEndPin(pin) {
|
|
89259
|
+
return pin.t === 0 || pin.t === 1;
|
|
89260
|
+
}
|
|
89261
|
+
|
|
89262
|
+
function validateTileIndex(i, n) {
|
|
89263
|
+
if (!(i >= 0 && i < n && Math.floor(i) === i)) {
|
|
89264
|
+
stop$1('Invalid ramp tile index:', i);
|
|
89265
|
+
}
|
|
89266
|
+
}
|
|
89267
|
+
|
|
89268
|
+
function validateRamp(pins, n) {
|
|
89269
|
+
if (!(n >= 2 && Math.floor(n) === n)) {
|
|
89270
|
+
stop$1('Invalid number of ramp colors:', n);
|
|
89271
|
+
}
|
|
89272
|
+
if (!Array.isArray(pins)) {
|
|
89273
|
+
stop$1('Expected an array of ramp pins');
|
|
89274
|
+
}
|
|
89275
|
+
pins.forEach(function(pin) {
|
|
89276
|
+
if (!(pin.t >= 0 && pin.t <= 1)) stop$1('Invalid ramp pin position:', pin.t);
|
|
89277
|
+
if (!parseColor(pin.color)) stop$1('Unsupported color:', pin.color);
|
|
89278
|
+
});
|
|
89279
|
+
if (!pins.some(function(pin) { return pin.t === 0; }) ||
|
|
89280
|
+
!pins.some(function(pin) { return pin.t === 1; })) {
|
|
89281
|
+
stop$1('A ramp must have pins at both ends');
|
|
89282
|
+
}
|
|
89283
|
+
}
|
|
89284
|
+
|
|
89285
|
+
var ColorRamps = /*#__PURE__*/Object.freeze({
|
|
89286
|
+
__proto__: null,
|
|
89287
|
+
clearRampPin: clearRampPin,
|
|
89288
|
+
getPinnedSlots: getPinnedSlots,
|
|
89289
|
+
resolveRamp: resolveRamp,
|
|
89290
|
+
resolveRampTiles: resolveRampTiles,
|
|
89291
|
+
setRampPin: setRampPin
|
|
89292
|
+
});
|
|
89293
|
+
|
|
87121
89294
|
function getPolygonRewinder(nodes) {
|
|
87122
89295
|
var splitter = getSelfIntersectionSplitter(nodes);
|
|
87123
89296
|
return rewindPolygon;
|
|
@@ -87958,10 +90131,10 @@ ${svg}
|
|
|
87958
90131
|
// Vertices never move once they are placed.
|
|
87959
90132
|
//
|
|
87960
90133
|
// The trace of the pointer samples is densified to even spacing and smoothed
|
|
87961
|
-
// with
|
|
87962
|
-
//
|
|
87963
|
-
//
|
|
87964
|
-
// smoothed point depends only on the trace within a window around it, so it
|
|
90134
|
+
// with -smooth's Gaussian-weighted quadratic fit, in arc length. The quadratic
|
|
90135
|
+
// keeps small bends from being flattened the way a plain weighted average
|
|
90136
|
+
// flattens them, at the cost of letting a little more of the jitter of a slow
|
|
90137
|
+
// stroke through. A smoothed point depends only on the trace within a window around it, so it
|
|
87965
90138
|
// is final as soon as the trace extends a window radius beyond it. At the
|
|
87966
90139
|
// start of the stroke the trace is extended by an odd reflection about its
|
|
87967
90140
|
// first point, which keeps the smoothed curve starting exactly there, and the
|
|
@@ -87976,14 +90149,15 @@ ${svg}
|
|
|
87976
90149
|
// Distances are in pixels. For samples in other units, the pixelSize option
|
|
87977
90150
|
// gives the size of a pixel in those units.
|
|
87978
90151
|
|
|
87979
|
-
// Standard deviation of the smoothing kernel, in arc length.
|
|
87980
|
-
// the
|
|
87981
|
-
//
|
|
90152
|
+
// Standard deviation of the smoothing kernel, in arc length. Smaller values
|
|
90153
|
+
// follow the hand more closely, which gives a feeling of control; larger ones
|
|
90154
|
+
// are smoother but feel less precise and place vertices further behind the
|
|
90155
|
+
// pointer.
|
|
87982
90156
|
var SIGMA = 8;
|
|
87983
90157
|
|
|
87984
90158
|
// Half-width of the smoothing window, in multiples of SIGMA. This is also how
|
|
87985
90159
|
// far the pointer has to be ahead of a smoothed point for it to be final.
|
|
87986
|
-
var WINDOW_RADIUS =
|
|
90160
|
+
var WINDOW_RADIUS = 4;
|
|
87987
90161
|
|
|
87988
90162
|
// Spacing of the densified trace, and of the smoothed points that are tested
|
|
87989
90163
|
// as vertices. Small enough that a bend of radius SIGMA turns by only a few
|
|
@@ -88004,10 +90178,16 @@ ${svg}
|
|
|
88004
90178
|
var PREVIEW_SPACING = 2;
|
|
88005
90179
|
var PREVIEW_SIGMA = 2;
|
|
88006
90180
|
|
|
90181
|
+
// Weight of -smooth's quadratic correction, which keeps bends from being
|
|
90182
|
+
// flattened: 0 is the plain weighted average, 1 the fully corrected fit.
|
|
90183
|
+
// At 1, small bends keep most of their amplitude.
|
|
90184
|
+
var GAIN = 1;
|
|
90185
|
+
|
|
88007
90186
|
// start: [x, y] first sample
|
|
88008
|
-
// opts: (optional) {sigma, previewSigma, bendAngle (degrees), maxDeviation, pixelSize}
|
|
90187
|
+
// opts: (optional) {sigma, previewSigma, gain, bendAngle (degrees), maxDeviation, pixelSize}
|
|
88009
90188
|
function GaussianStrokeFitter(start, opts) {
|
|
88010
90189
|
var px = opts && opts.pixelSize > 0 ? opts.pixelSize : 1;
|
|
90190
|
+
var gain = opts && opts.gain >= 0 ? opts.gain : GAIN;
|
|
88011
90191
|
var sigma = getOpt(opts, 'sigma', SIGMA) * px;
|
|
88012
90192
|
var radius = sigma * WINDOW_RADIUS;
|
|
88013
90193
|
var previewSigma = Math.min(getOpt(opts, 'previewSigma', PREVIEW_SIGMA) * px, sigma);
|
|
@@ -88111,7 +90291,7 @@ ${svg}
|
|
|
88111
90291
|
wy.push(2 * end[1] - yy[i]);
|
|
88112
90292
|
}
|
|
88113
90293
|
}
|
|
88114
|
-
return smoothPoint(wt, [wx, wy], 0, wt.length, t, 'gaussian', s || sigma, r,
|
|
90294
|
+
return smoothPoint(wt, [wx, wy], 0, wt.length, t, 'gaussian', s || sigma, r, gain);
|
|
88115
90295
|
}
|
|
88116
90296
|
|
|
88117
90297
|
// first index of the trace with arc length > @t
|
|
@@ -88270,12 +90450,17 @@ ${svg}
|
|
|
88270
90450
|
ClipErase,
|
|
88271
90451
|
ClipPoints,
|
|
88272
90452
|
ColorUtils,
|
|
90453
|
+
ColorRamps,
|
|
90454
|
+
ColorSchemes,
|
|
90455
|
+
Oklab,
|
|
88273
90456
|
Colorizer,
|
|
88274
90457
|
CustomProjections,
|
|
88275
90458
|
Dymaxion,
|
|
88276
90459
|
ButterflyProjections,
|
|
88277
90460
|
Narukawa2022,
|
|
88278
90461
|
DataAggregation,
|
|
90462
|
+
Diverging,
|
|
90463
|
+
{getSequentialBreaks, getInterpolatedValueGetter},
|
|
88279
90464
|
DatasetUtils,
|
|
88280
90465
|
DataUtils,
|
|
88281
90466
|
DbfImport,
|