mapshaper 0.7.60 → 0.7.62
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 +2438 -129
- package/package.json +2 -1
- package/www/index.html +12 -6
- package/www/mapshaper-gui.js +7054 -1418
- package/www/mapshaper.js +2438 -129
- package/www/page.css +917 -221
package/mapshaper.js
CHANGED
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// Similar to isFinite() but does not coerce strings or other types
|
|
99
|
-
function isFiniteNumber$
|
|
99
|
+
function isFiniteNumber$2(val) {
|
|
100
100
|
return isValidNumber(val) && val !== Infinity && val !== -Infinity;
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -1106,7 +1106,7 @@
|
|
|
1106
1106
|
getSortedIds, getUniqueName, groupBy,
|
|
1107
1107
|
htmlEscape,
|
|
1108
1108
|
indexOf, indexOn, inherit, initializeArray, intersection,
|
|
1109
|
-
isArray, isArrayLike, isBoolean, isDate, isEven, isFiniteNumber: isFiniteNumber$
|
|
1109
|
+
isArray, isArrayLike, isBoolean, isDate, isEven, isFiniteNumber: isFiniteNumber$2, isFunction,
|
|
1110
1110
|
isInteger, isNonNegNumber, isNumber, isObject, isOdd: isOdd$1, isPromise, isString,
|
|
1111
1111
|
isValidNumber,
|
|
1112
1112
|
lpad, ltrim,
|
|
@@ -5882,13 +5882,13 @@
|
|
|
5882
5882
|
return msg;
|
|
5883
5883
|
}
|
|
5884
5884
|
|
|
5885
|
-
function requirePointLayer(lyr, msg) {
|
|
5885
|
+
function requirePointLayer$1(lyr, msg) {
|
|
5886
5886
|
if (!lyr || lyr.geometry_type !== 'point')
|
|
5887
5887
|
stop$1(layerTypeMessage(lyr, "Expected a point layer", msg));
|
|
5888
5888
|
}
|
|
5889
5889
|
|
|
5890
5890
|
function requireSinglePointLayer(lyr, msg) {
|
|
5891
|
-
requirePointLayer(lyr);
|
|
5891
|
+
requirePointLayer$1(lyr);
|
|
5892
5892
|
if (countMultiPartFeatures(lyr.shapes) > 0) {
|
|
5893
5893
|
stop$1(msg || 'This command requires single points; layer contains multi-point features.');
|
|
5894
5894
|
}
|
|
@@ -6128,7 +6128,7 @@
|
|
|
6128
6128
|
requireDataFields: requireDataFields,
|
|
6129
6129
|
requireNotRasterLayer: requireNotRasterLayer,
|
|
6130
6130
|
requirePathLayer: requirePathLayer,
|
|
6131
|
-
requirePointLayer: requirePointLayer,
|
|
6131
|
+
requirePointLayer: requirePointLayer$1,
|
|
6132
6132
|
requirePolygonLayer: requirePolygonLayer,
|
|
6133
6133
|
requirePolylineLayer: requirePolylineLayer,
|
|
6134
6134
|
requireSinglePointLayer: requireSinglePointLayer,
|
|
@@ -15025,7 +15025,8 @@
|
|
|
15025
15025
|
|
|
15026
15026
|
// input: input file path or a Buffer containing .zip file bytes
|
|
15027
15027
|
function unzipSyncNode(input) {
|
|
15028
|
-
var
|
|
15028
|
+
var AdmZip = require('adm-zip');
|
|
15029
|
+
var zip = new AdmZip(input);
|
|
15029
15030
|
var index = {};
|
|
15030
15031
|
zip.getEntries().forEach(function(entry) {
|
|
15031
15032
|
// entry.entryName // path, including filename
|
|
@@ -15039,7 +15040,8 @@
|
|
|
15039
15040
|
}
|
|
15040
15041
|
|
|
15041
15042
|
function zipSyncNode(files) {
|
|
15042
|
-
var
|
|
15043
|
+
var AdmZip = require('adm-zip');
|
|
15044
|
+
var zip = new AdmZip();
|
|
15043
15045
|
files.forEach(function(o) {
|
|
15044
15046
|
var buf = o.content;
|
|
15045
15047
|
if (buf instanceof ArrayBuffer) {
|
|
@@ -18751,10 +18753,10 @@
|
|
|
18751
18753
|
return d;
|
|
18752
18754
|
}
|
|
18753
18755
|
|
|
18754
|
-
var cache = {};
|
|
18756
|
+
var cache$1 = {};
|
|
18755
18757
|
|
|
18756
18758
|
function getParseRxp(fmt) {
|
|
18757
|
-
if (fmt in cache) return cache[fmt];
|
|
18759
|
+
if (fmt in cache$1) return cache$1[fmt];
|
|
18758
18760
|
var rxp = fmt;
|
|
18759
18761
|
rxp = rxp.replace('[-]', '(?<prefix>-)?'); // optional -
|
|
18760
18762
|
rxp = rxp.replace(/\[[NSEW, +-]{2,}\]/, '(?<prefix>$&)');
|
|
@@ -18778,11 +18780,11 @@
|
|
|
18778
18780
|
rxp = '^' + rxp + '$';
|
|
18779
18781
|
try {
|
|
18780
18782
|
// TODO: make sure all DMS codes have been matched
|
|
18781
|
-
cache[fmt] = new RegExp(rxp);
|
|
18783
|
+
cache$1[fmt] = new RegExp(rxp);
|
|
18782
18784
|
} catch(e) {
|
|
18783
18785
|
stop$1('Invalid DMS format string:', fmt);
|
|
18784
18786
|
}
|
|
18785
|
-
return cache[fmt];
|
|
18787
|
+
return cache$1[fmt];
|
|
18786
18788
|
}
|
|
18787
18789
|
|
|
18788
18790
|
function formatNumber(val, integers, decimals) {
|
|
@@ -20357,6 +20359,7 @@
|
|
|
20357
20359
|
try {
|
|
20358
20360
|
val = func.call(thisVal, rec, ctx);
|
|
20359
20361
|
} catch(e) {
|
|
20362
|
+
if (opts.quiet) throw e;
|
|
20360
20363
|
stop$1(e.name, "in expression [" + exp + "]:", e.message);
|
|
20361
20364
|
}
|
|
20362
20365
|
return val;
|
|
@@ -20378,7 +20381,10 @@
|
|
|
20378
20381
|
try {
|
|
20379
20382
|
return new Function('$$record,$$env', functionBody);
|
|
20380
20383
|
} catch(e) {
|
|
20381
|
-
//
|
|
20384
|
+
// A caller that is only asking whether this string is an expression wants
|
|
20385
|
+
// the answer, not a report: stop() prints in the CLI and opens an alert in
|
|
20386
|
+
// the GUI, both of which are wrong for a question whose answer is "no".
|
|
20387
|
+
if (opts.quiet) throw e;
|
|
20382
20388
|
stop$1(e.name, 'in expression [' + exp + ']');
|
|
20383
20389
|
}
|
|
20384
20390
|
}
|
|
@@ -20891,8 +20897,10 @@
|
|
|
20891
20897
|
ctx2.height = function() {return getLayerBounds(lyr, arcs).height();};
|
|
20892
20898
|
}
|
|
20893
20899
|
|
|
20894
|
-
|
|
20895
|
-
|
|
20900
|
+
// Replace && / || with commas so phase 1 evaluates every operand. Otherwise
|
|
20901
|
+
// short-circuit skips later capture calls (e.g. sum(a)>1 && sum(b)>1).
|
|
20902
|
+
calc1 = compileFeatureExpression(exp.replace(/&&|\|\|/g, ','), lyr, arcs, {
|
|
20903
|
+
context: ctx1, no_assign: true, no_warn: true, no_return: true});
|
|
20896
20904
|
// changed data-only layer to full layer to expose layer geometry, etc
|
|
20897
20905
|
// (why not do this originally?)
|
|
20898
20906
|
// calc2 = compileFeatureExpression(exp, {data: lyr.data}, null,
|
|
@@ -25761,6 +25769,24 @@
|
|
|
25761
25769
|
return text || text === 0; // accept numerical 0 as label text
|
|
25762
25770
|
}
|
|
25763
25771
|
|
|
25772
|
+
// Whether a feature is a label at all, including one whose text is still empty.
|
|
25773
|
+
//
|
|
25774
|
+
// The GUI needs this and export does not. A label is a label from the moment it
|
|
25775
|
+
// is created, before anything has been typed into it, and the editor has to
|
|
25776
|
+
// render a node for it: with no node there is nothing to see, nothing to click
|
|
25777
|
+
// and nowhere to put a caret, so a label the user just made would be invisible
|
|
25778
|
+
// and unrecoverable. Export has no editor and drops the empty ones.
|
|
25779
|
+
function featureIsLabel(d) {
|
|
25780
|
+
return !!d && 'label-text' in d;
|
|
25781
|
+
}
|
|
25782
|
+
|
|
25783
|
+
var SvgFeatureUtils = /*#__PURE__*/Object.freeze({
|
|
25784
|
+
__proto__: null,
|
|
25785
|
+
featureHasLabel: featureHasLabel,
|
|
25786
|
+
featureHasSvgSymbol: featureHasSvgSymbol,
|
|
25787
|
+
featureIsLabel: featureIsLabel
|
|
25788
|
+
});
|
|
25789
|
+
|
|
25764
25790
|
var assignmentRxp = /^([a-z0-9_+-]+)=(?!=)(.*)$/i; // exclude ==
|
|
25765
25791
|
|
|
25766
25792
|
function splitShellTokens(str) {
|
|
@@ -26176,6 +26202,244 @@
|
|
|
26176
26202
|
parsePattern: parsePattern
|
|
26177
26203
|
});
|
|
26178
26204
|
|
|
26205
|
+
// public domain implementation
|
|
26206
|
+
// source: https://github.com/jbt/js-crypto
|
|
26207
|
+
function sha1(str1){
|
|
26208
|
+
for (
|
|
26209
|
+
var blockstart = 0,
|
|
26210
|
+
i = 0,
|
|
26211
|
+
W = [],
|
|
26212
|
+
A, B, C, D, F, G,
|
|
26213
|
+
H = [A=0x67452301, B=0xEFCDAB89, ~A, ~B, 0xC3D2E1F0],
|
|
26214
|
+
word_array = [],
|
|
26215
|
+
temp2,
|
|
26216
|
+
s = unescape(encodeURI(str1)),
|
|
26217
|
+
str_len = s.length;
|
|
26218
|
+
|
|
26219
|
+
i <= str_len;
|
|
26220
|
+
){
|
|
26221
|
+
word_array[i >> 2] |= (s.charCodeAt(i)||128) << (8 * (3 - i++ % 4));
|
|
26222
|
+
}
|
|
26223
|
+
word_array[temp2 = ((str_len + 8) >> 2) | 15] = str_len << 3;
|
|
26224
|
+
|
|
26225
|
+
for (; blockstart <= temp2; blockstart += 16) {
|
|
26226
|
+
A = H; i = 0;
|
|
26227
|
+
|
|
26228
|
+
for (; i < 80;
|
|
26229
|
+
A = [[
|
|
26230
|
+
(G = ((s = A[0]) << 5 | s >>> 27) + A[4] + (W[i] = (i<16) ? ~~word_array[blockstart + i] : G << 1 | G >>> 31) + 1518500249) + ((B = A[1]) & (C = A[2]) | ~B & (D = A[3])),
|
|
26231
|
+
F = G + (B ^ C ^ D) + 341275144,
|
|
26232
|
+
G + (B & C | B & D | C & D) + 882459459,
|
|
26233
|
+
F + 1535694389
|
|
26234
|
+
][0|((i++) / 20)] | 0, s, B << 30 | B >>> 2, C, D]
|
|
26235
|
+
) {
|
|
26236
|
+
G = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16];
|
|
26237
|
+
}
|
|
26238
|
+
|
|
26239
|
+
for(i = 5; i; ) H[--i] = H[i] + A[i] | 0;
|
|
26240
|
+
}
|
|
26241
|
+
|
|
26242
|
+
for(str1 = ''; i < 40; )str1 += (H[i >> 3] >> (7 - i++ % 8) * 4 & 15).toString(16);
|
|
26243
|
+
return str1;
|
|
26244
|
+
}
|
|
26245
|
+
|
|
26246
|
+
// How wide a label's text renders, which is the one thing about a label that
|
|
26247
|
+
// cannot be worked out from the data.
|
|
26248
|
+
//
|
|
26249
|
+
// Two features need it: `label-align`, which holds a block of text still while
|
|
26250
|
+
// the lines inside it re-justify (svg-label-align.mjs), and the path-fit check,
|
|
26251
|
+
// which drops a path label whose text is longer than its path
|
|
26252
|
+
// (svg-label-fit.mjs). Both need font metrics, which reach this module from
|
|
26253
|
+
// whichever of two places can supply them -- the GUI measures by rendering,
|
|
26254
|
+
// and Node reads the installed font files (mapshaper-text-measure.mjs) -- and
|
|
26255
|
+
// the answers are memoized here.
|
|
26256
|
+
//
|
|
26257
|
+
// **A measurement is not the user's data.** It is derived from values the user
|
|
26258
|
+
// did set -- the text and six font properties -- and it lives in this cache
|
|
26259
|
+
// alone: no column in the table, nothing exported, nothing for an edit to keep
|
|
26260
|
+
// in step with. See docs/development/label-tool-design.md.
|
|
26261
|
+
//
|
|
26262
|
+
// The cache is keyed by a fingerprint of exactly the values the width depends
|
|
26263
|
+
// on, which is what makes that safe: a width is a pure function of its inputs,
|
|
26264
|
+
// so a cache entry cannot go stale -- change the text or the font and the
|
|
26265
|
+
// fingerprint changes with it, and the lookup simply misses. Nothing has to be
|
|
26266
|
+
// invalidated, and a feature can be copied, merged, filtered or renumbered
|
|
26267
|
+
// without its measurement following it around, because the measurement was
|
|
26268
|
+
// never attached to the feature in the first place.
|
|
26269
|
+
|
|
26270
|
+
// Properties the width depends on. A change to any of them gives a different
|
|
26271
|
+
// fingerprint, so a measurement is never read for text it does not describe.
|
|
26272
|
+
//
|
|
26273
|
+
// Note that 'css' and 'class' are deliberately absent: both can change the
|
|
26274
|
+
// rendered font through a stylesheet mapshaper cannot see, so no fingerprint
|
|
26275
|
+
// over record values could detect it.
|
|
26276
|
+
var MEASURED_PROPERTIES = ['font-family', 'font-size', 'font-weight',
|
|
26277
|
+
'font-style', 'font-stretch', 'letter-spacing'];
|
|
26278
|
+
|
|
26279
|
+
var HASH_LENGTH = 12;
|
|
26280
|
+
|
|
26281
|
+
// Entries are a dozen bytes of key and a number, and one per distinct text and
|
|
26282
|
+
// font -- typing a label adds one per keystroke, since each prefix is its own
|
|
26283
|
+
// string. The cap is generous enough never to be reached in an editing session
|
|
26284
|
+
// and small enough to bound a session that runs for days.
|
|
26285
|
+
var CACHE_LIMIT = 20000;
|
|
26286
|
+
|
|
26287
|
+
var cache = new Map();
|
|
26288
|
+
var measureFn = null;
|
|
26289
|
+
var measuring = false;
|
|
26290
|
+
|
|
26291
|
+
// Installed at startup, by the GUI in a browser and by mapshaper-api.mjs
|
|
26292
|
+
// outside one: (rec) -> width in px, or null.
|
|
26293
|
+
//
|
|
26294
|
+
// An inversion, and a deliberate one. The alternative was for the GUI to
|
|
26295
|
+
// measure ahead of every reader -- before each render, before each export,
|
|
26296
|
+
// after each edit -- which is three hooks to keep in step and a fourth for the
|
|
26297
|
+
// console, where a user can type -o svg without going near the export dialog.
|
|
26298
|
+
// A reader that can ask for a measurement needs no hooks at all, and where
|
|
26299
|
+
// nothing can be measured -- a font this machine has not got, a browser too
|
|
26300
|
+
// old to measure in -- every reader falls back exactly as it did before this
|
|
26301
|
+
// existed.
|
|
26302
|
+
function setTextMeasureFunction(fn) {
|
|
26303
|
+
measureFn = fn || null;
|
|
26304
|
+
}
|
|
26305
|
+
|
|
26306
|
+
// The width of @rec's text in px at its own font size, or null if it cannot be
|
|
26307
|
+
// known. Measures on demand and remembers the answer, including a failure to
|
|
26308
|
+
// measure, so a label that cannot be measured is not measured repeatedly.
|
|
26309
|
+
function getMeasuredTextWidth(rec) {
|
|
26310
|
+
var hash, width;
|
|
26311
|
+
if (!rec || !rec['label-text']) return null;
|
|
26312
|
+
hash = getTextWidthKey(rec);
|
|
26313
|
+
if (cache.has(hash)) return cache.get(hash) || null;
|
|
26314
|
+
// Nothing to measure with is not an answer about this text, so it is not
|
|
26315
|
+
// remembered as one. Caching it would mean that whatever rendered before a
|
|
26316
|
+
// measure function was installed -- or during any window in which none is --
|
|
26317
|
+
// decided the width of that text for the rest of the session.
|
|
26318
|
+
if (!measureFn) return null;
|
|
26319
|
+
width = measure(rec);
|
|
26320
|
+
if (cache.size >= CACHE_LIMIT) cache.clear();
|
|
26321
|
+
cache.set(hash, width || 0);
|
|
26322
|
+
return width;
|
|
26323
|
+
}
|
|
26324
|
+
|
|
26325
|
+
// A measurement is itself a render, so it must not ask for one: rendering an
|
|
26326
|
+
// aligned label reads a width, and a measurement taken in the middle of that
|
|
26327
|
+
// would be measuring in order to measure. The GUI's measure function drops
|
|
26328
|
+
// label-align for this reason; the flag is the backstop.
|
|
26329
|
+
function measure(rec) {
|
|
26330
|
+
var width;
|
|
26331
|
+
if (measuring) return null;
|
|
26332
|
+
measuring = true;
|
|
26333
|
+
try {
|
|
26334
|
+
width = measureFn(rec);
|
|
26335
|
+
} catch (e) {
|
|
26336
|
+
width = null;
|
|
26337
|
+
}
|
|
26338
|
+
measuring = false;
|
|
26339
|
+
return width > 0 ? width : null;
|
|
26340
|
+
}
|
|
26341
|
+
|
|
26342
|
+
// Fingerprint of the values a width depends on, and so the key it is kept
|
|
26343
|
+
// under.
|
|
26344
|
+
function getTextWidthKey(rec) {
|
|
26345
|
+
var parts = [toHashInput(rec && rec['label-text'])];
|
|
26346
|
+
for (var i = 0; i < MEASURED_PROPERTIES.length; i++) {
|
|
26347
|
+
parts.push(toHashInput(rec && rec[MEASURED_PROPERTIES[i]]));
|
|
26348
|
+
}
|
|
26349
|
+
// \n is safe as a separator: label-text may contain newlines, but it is the
|
|
26350
|
+
// only multi-line field and it is always first
|
|
26351
|
+
return sha1(parts.join('\n')).substr(0, HASH_LENGTH);
|
|
26352
|
+
}
|
|
26353
|
+
|
|
26354
|
+
function toHashInput(val) {
|
|
26355
|
+
return val === null || val === undefined ? '' : String(val);
|
|
26356
|
+
}
|
|
26357
|
+
|
|
26358
|
+
// For tests, and for a session that wants to measure again from scratch.
|
|
26359
|
+
function clearTextWidthCache() {
|
|
26360
|
+
cache.clear();
|
|
26361
|
+
}
|
|
26362
|
+
|
|
26363
|
+
function getTextWidthCacheSize() {
|
|
26364
|
+
return cache.size;
|
|
26365
|
+
}
|
|
26366
|
+
|
|
26367
|
+
var SvgLabelMetrics = /*#__PURE__*/Object.freeze({
|
|
26368
|
+
__proto__: null,
|
|
26369
|
+
clearTextWidthCache: clearTextWidthCache,
|
|
26370
|
+
getMeasuredTextWidth: getMeasuredTextWidth,
|
|
26371
|
+
getTextWidthCacheSize: getTextWidthCacheSize,
|
|
26372
|
+
getTextWidthKey: getTextWidthKey,
|
|
26373
|
+
setTextMeasureFunction: setTextMeasureFunction
|
|
26374
|
+
});
|
|
26375
|
+
|
|
26376
|
+
// Line justification, as a property of its own because SVG has no such thing.
|
|
26377
|
+
//
|
|
26378
|
+
// text-anchor does two jobs at once: it justifies the lines of a label, and it
|
|
26379
|
+
// decides where the block of them sits relative to x. A label pinned north of
|
|
26380
|
+
// its anchor is centred on x because that is what its position means, so a user
|
|
26381
|
+
// who asks for left-aligned lines and gets text-anchor=start has answered the
|
|
26382
|
+
// first question and silently changed the answer to the second: the block
|
|
26383
|
+
// slides half its own width to the right, off the point it labels.
|
|
26384
|
+
//
|
|
26385
|
+
// label-align asks only the first question. Holding the block still while the
|
|
26386
|
+
// lines re-justify means moving x the other way by the same amount, and the
|
|
26387
|
+
// amount is half or all of the block's width -- a font metric, which is why
|
|
26388
|
+
// this needs a measurement rather than arithmetic. The width comes from
|
|
26389
|
+
// svg-label-metrics.mjs, which measures in the GUI and remembers.
|
|
26390
|
+
//
|
|
26391
|
+
// With no usable measurement the lines are still re-justified and the block
|
|
26392
|
+
// still moves. A label that reads the way it was asked to read, in the wrong
|
|
26393
|
+
// place, is closer to the request than one that ignores it -- and the block was
|
|
26394
|
+
// moving before this property existed.
|
|
26395
|
+
//
|
|
26396
|
+
// See docs/development/label-tool-design.md.
|
|
26397
|
+
|
|
26398
|
+
// How far to the left of x the text sits, as a fraction of its own width
|
|
26399
|
+
var anchorOffsets = {start: 0, middle: 0.5, end: 1};
|
|
26400
|
+
|
|
26401
|
+
var alignAnchors = {left: 'start', center: 'middle', right: 'end'};
|
|
26402
|
+
|
|
26403
|
+
function parseLabelAlign(str) {
|
|
26404
|
+
var align = String(str).trim().toLowerCase();
|
|
26405
|
+
return align in alignAnchors ? align : null;
|
|
26406
|
+
}
|
|
26407
|
+
|
|
26408
|
+
// The text-anchor an alignment is rendered as, or null if there isn't one --
|
|
26409
|
+
// an unset property, or a value that reached the record from an expression or
|
|
26410
|
+
// a data file rather than through the commands, which reject one.
|
|
26411
|
+
function getAlignmentAnchor(align) {
|
|
26412
|
+
var parsed = align ? parseLabelAlign(align) : null;
|
|
26413
|
+
return parsed ? alignAnchors[parsed] : null;
|
|
26414
|
+
}
|
|
26415
|
+
|
|
26416
|
+
// How far x has to move to leave the block where it was, in px. Zero unless
|
|
26417
|
+
// the record carries an alignment that disagrees with the anchor its position
|
|
26418
|
+
// implies and a measurement to work from.
|
|
26419
|
+
//
|
|
26420
|
+
// @positionAnchor is supplied by the caller rather than read from the record,
|
|
26421
|
+
// partly to keep this module out of a cycle with the position table in
|
|
26422
|
+
// svg-properties.mjs, and partly because the comparison is against the anchor
|
|
26423
|
+
// the *position* implies: an explicit text-anchor is the thing label-align
|
|
26424
|
+
// replaces, so treating it as where the block belongs would hold the label in
|
|
26425
|
+
// a place it was never drawn.
|
|
26426
|
+
function getAlignmentShift(rec, positionAnchor) {
|
|
26427
|
+
var anchor = getAlignmentAnchor(rec && rec['label-align']);
|
|
26428
|
+
var width;
|
|
26429
|
+
if (!anchor || anchor == positionAnchor) return 0;
|
|
26430
|
+
width = getMeasuredTextWidth(rec);
|
|
26431
|
+
if (!width) return 0;
|
|
26432
|
+
return (anchorOffsets[anchor] - anchorOffsets[positionAnchor]) * width;
|
|
26433
|
+
}
|
|
26434
|
+
|
|
26435
|
+
var SvgLabelAlign = /*#__PURE__*/Object.freeze({
|
|
26436
|
+
__proto__: null,
|
|
26437
|
+
getAlignmentAnchor: getAlignmentAnchor,
|
|
26438
|
+
getAlignmentShift: getAlignmentShift,
|
|
26439
|
+
getMeasuredTextWidth: getMeasuredTextWidth,
|
|
26440
|
+
parseLabelAlign: parseLabelAlign
|
|
26441
|
+
});
|
|
26442
|
+
|
|
26179
26443
|
// parsing hints for -style command cli options
|
|
26180
26444
|
// null values indicate the lack of a function for parsing/identifying this property
|
|
26181
26445
|
// (in which case a heuristic is used for distinguishing a string literal from an expression)
|
|
@@ -26183,6 +26447,7 @@
|
|
|
26183
26447
|
// css: null,
|
|
26184
26448
|
css: 'inlinecss',
|
|
26185
26449
|
class: 'classname',
|
|
26450
|
+
'dominant-baseline': null,
|
|
26186
26451
|
dx: 'measure',
|
|
26187
26452
|
dy: 'measure',
|
|
26188
26453
|
fill: 'color',
|
|
@@ -26195,8 +26460,20 @@
|
|
|
26195
26460
|
'font-weight': null,
|
|
26196
26461
|
icon: null,
|
|
26197
26462
|
'icon-color': 'color',
|
|
26463
|
+
// opacity of the symbol at a label's anchor, apart from the text's. Needed
|
|
26464
|
+
// because a label's own opacity properties are applied to both of the
|
|
26465
|
+
// elements its record produces -- see getIconStyleData().
|
|
26466
|
+
'icon-opacity': 'number',
|
|
26198
26467
|
'icon-size': 'number',
|
|
26468
|
+
// how the lines of a multi-line label line up with each other, as against
|
|
26469
|
+
// text-anchor, which also decides where the block of them sits -- see
|
|
26470
|
+
// svg-label-align.mjs
|
|
26471
|
+
'label-align': 'labelalign',
|
|
26199
26472
|
'label-pos': 'labelposition',
|
|
26473
|
+
// which side of its path a label's text sits on
|
|
26474
|
+
'label-side': null,
|
|
26475
|
+
// where the text starts along its path; a length or a percentage
|
|
26476
|
+
'label-start-offset': null,
|
|
26200
26477
|
'label-text': null, // leaving this null
|
|
26201
26478
|
'letter-spacing': 'measure',
|
|
26202
26479
|
'line-height': 'measure',
|
|
@@ -26212,6 +26489,15 @@
|
|
|
26212
26489
|
'text-anchor': null
|
|
26213
26490
|
};
|
|
26214
26491
|
|
|
26492
|
+
// Properties an empty string is a value for rather than the absence of one:
|
|
26493
|
+
// the text of a label, which is empty while it is being typed, and the two
|
|
26494
|
+
// that take any string at all. See emptyValueUnsetsProperty().
|
|
26495
|
+
var propertiesTakingEmptyValues = {
|
|
26496
|
+
'label-text': true,
|
|
26497
|
+
css: true,
|
|
26498
|
+
class: true
|
|
26499
|
+
};
|
|
26500
|
+
|
|
26215
26501
|
// The -symbols command accepts some options that are not supported by -style
|
|
26216
26502
|
// (different symbol types accept different combinations of properties...)
|
|
26217
26503
|
var symbolPropertyTypes = utils.extend({
|
|
@@ -26263,18 +26549,25 @@
|
|
|
26263
26549
|
'fill,font-family,font-size,text-anchor,font-weight,font-style,font-stretch,letter-spacing,dominant-baseline'.split(',')))
|
|
26264
26550
|
};
|
|
26265
26551
|
|
|
26266
|
-
|
|
26552
|
+
// The properties a label position stands for, and the shorthand itself.
|
|
26553
|
+
var labelPositionDerivedFields = ['dx', 'dy', 'text-anchor'];
|
|
26554
|
+
var labelPositionFields = ['label-pos'].concat(labelPositionDerivedFields);
|
|
26267
26555
|
|
|
26556
|
+
// dx is '0' and not 0 in the centred positions, so that every position in the
|
|
26557
|
+
// table has the same type. These values are normally resolved for rendering and
|
|
26558
|
+
// never stored, but dragging a label materializes them into its record, and a
|
|
26559
|
+
// column holding 0 from one position and '0.45em' from another is one
|
|
26560
|
+
// -merge-layers refuses.
|
|
26268
26561
|
var labelPositionStyles = {
|
|
26269
|
-
n: {dx: 0, dy: '-0.5em', 'text-anchor': 'middle'},
|
|
26270
|
-
s: {dx: 0, dy: '1.1em', 'text-anchor': 'middle'},
|
|
26562
|
+
n: {dx: '0', dy: '-0.5em', 'text-anchor': 'middle'},
|
|
26563
|
+
s: {dx: '0', dy: '1.1em', 'text-anchor': 'middle'},
|
|
26271
26564
|
e: {dx: '0.45em', dy: '0.23em', 'text-anchor': 'start'},
|
|
26272
26565
|
w: {dx: '-0.45em', dy: '0.23em', 'text-anchor': 'end'},
|
|
26273
26566
|
ne: {dx: '0.4em', dy: '-0.15em', 'text-anchor': 'start'},
|
|
26274
26567
|
se: {dx: '0.4em', dy: '0.7em', 'text-anchor': 'start'},
|
|
26275
26568
|
nw: {dx: '-0.4em', dy: '-0.15em', 'text-anchor': 'end'},
|
|
26276
26569
|
sw: {dx: '-0.4em', dy: '0.7em', 'text-anchor': 'end'},
|
|
26277
|
-
c: {dx: 0, dy: '0.25em', 'text-anchor': 'middle'}
|
|
26570
|
+
c: {dx: '0', dy: '0.25em', 'text-anchor': 'middle'}
|
|
26278
26571
|
};
|
|
26279
26572
|
|
|
26280
26573
|
// symType: point, polygon, polyline, label
|
|
@@ -26303,6 +26596,46 @@
|
|
|
26303
26596
|
return name in stylePropertyTypes;
|
|
26304
26597
|
}
|
|
26305
26598
|
|
|
26599
|
+
// Whether an empty value removes this property rather than being stored in it.
|
|
26600
|
+
//
|
|
26601
|
+
// True for a property with no empty value to store, like a number, a color or a
|
|
26602
|
+
// font weight: -style fill= takes the fill back off a feature, where it used to
|
|
26603
|
+
// be an error. It is the only per-property unset there is -- -style clear
|
|
26604
|
+
// removes every style property at once -- and the panel needs one, since a
|
|
26605
|
+
// control returning to its default has to be able to say so.
|
|
26606
|
+
//
|
|
26607
|
+
// False where the empty string is itself a value: inline css, a class name, and
|
|
26608
|
+
// the text of a label, which is empty while it is being typed. Everything else
|
|
26609
|
+
// with no type rule -- a font family, a text-anchor, an icon name -- has no
|
|
26610
|
+
// meaning for an empty string either, so storing one there would leave a column
|
|
26611
|
+
// of nothing behind and an attribute the renderer has to ignore.
|
|
26612
|
+
function emptyValueUnsetsProperty(name) {
|
|
26613
|
+
if (!(name in stylePropertyTypes) || name in propertiesTakingEmptyValues) {
|
|
26614
|
+
return false;
|
|
26615
|
+
}
|
|
26616
|
+
var type = stylePropertyTypes[name];
|
|
26617
|
+
return !type || parseSvgLiteralValue('', type) === null;
|
|
26618
|
+
}
|
|
26619
|
+
|
|
26620
|
+
// Converts a style value to the type that property is stored in -- the same
|
|
26621
|
+
// conversion -style applies to a literal. Returns undefined if the value is not
|
|
26622
|
+
// usable for the property, and the value unchanged for a property with no type
|
|
26623
|
+
// rule, where any string is a literal.
|
|
26624
|
+
//
|
|
26625
|
+
// -style resolves a value three ways: as a literal, as the name of a data field
|
|
26626
|
+
// or as an expression over the feature. A command that sets properties on a
|
|
26627
|
+
// single feature it is creating has no feature to read a field from, so it
|
|
26628
|
+
// wants the first of those on its own -- but it has to agree with -style about
|
|
26629
|
+
// the result, or the same value given to the two commands ends up stored as two
|
|
26630
|
+
// different types in one column.
|
|
26631
|
+
function parseStyleLiteral(name, val) {
|
|
26632
|
+
var type = stylePropertyTypes[name];
|
|
26633
|
+
var parsed;
|
|
26634
|
+
if (!type) return val; // no rule for this property: the value is the value
|
|
26635
|
+
parsed = parseSvgLiteralValue(String(val).trim(), type);
|
|
26636
|
+
return parsed === null ? undefined : parsed;
|
|
26637
|
+
}
|
|
26638
|
+
|
|
26306
26639
|
function isSupportedSvgSymbolProperty(name) {
|
|
26307
26640
|
return name in symbolPropertyTypes;
|
|
26308
26641
|
}
|
|
@@ -26406,10 +26739,18 @@
|
|
|
26406
26739
|
stop$1('Unexpected value for', name + ':', strVal);
|
|
26407
26740
|
}
|
|
26408
26741
|
|
|
26742
|
+
// Whether @strVal works as an expression, and the function if it does.
|
|
26743
|
+
//
|
|
26744
|
+
// This is a guess being checked, so the failures are expected and have to be
|
|
26745
|
+
// silent: quiet keeps the expression compiler from reporting them, which in
|
|
26746
|
+
// the GUI means an alert over a style change that went on to work perfectly
|
|
26747
|
+
// well -- a label typed as "Saint-Denis" or an offset of "59.77%" is not an
|
|
26748
|
+
// expression, and neither is an error.
|
|
26409
26749
|
function parseStyleExpression(strVal, lyr) {
|
|
26410
26750
|
var func;
|
|
26411
26751
|
try {
|
|
26412
|
-
func = compileFeatureExpression(strVal, lyr, null,
|
|
26752
|
+
func = compileFeatureExpression(strVal, lyr, null,
|
|
26753
|
+
{no_warn: true, quiet: true});
|
|
26413
26754
|
func(0); // check for runtime errors (e.g. undefined variables)
|
|
26414
26755
|
} catch(e) {
|
|
26415
26756
|
func = null;
|
|
@@ -26439,6 +26780,8 @@
|
|
|
26439
26780
|
val = strVal; // TODO: validate
|
|
26440
26781
|
} else if (type == 'labelposition') {
|
|
26441
26782
|
val = parseLabelPosition(strVal);
|
|
26783
|
+
} else if (type == 'labelalign') {
|
|
26784
|
+
val = parseLabelAlign(strVal);
|
|
26442
26785
|
}
|
|
26443
26786
|
// else {
|
|
26444
26787
|
// // unknown type -- assume literal value
|
|
@@ -26481,13 +26824,67 @@
|
|
|
26481
26824
|
return Object.assign({'label-pos': pos}, labelPositionStyles[pos.toLowerCase()]);
|
|
26482
26825
|
}
|
|
26483
26826
|
|
|
26484
|
-
|
|
26485
|
-
|
|
26486
|
-
|
|
26487
|
-
|
|
26488
|
-
|
|
26489
|
-
|
|
26490
|
-
|
|
26827
|
+
// Fills in the offsets and justification a label's position stands for, for
|
|
26828
|
+
// rendering. Returns @rec itself when there is nothing to add, and a copy when
|
|
26829
|
+
// there is: a renderer must not write into the record it was handed.
|
|
26830
|
+
//
|
|
26831
|
+
// label-pos is the only one of the four that is stored. The other three used to
|
|
26832
|
+
// be written alongside it, which made the table below a code generator rather
|
|
26833
|
+
// than a lookup: four columns in the user's data where one was meant, and the
|
|
26834
|
+
// table's own values -- '0' next to '0.45em' -- became the types of a column
|
|
26835
|
+
// that -merge-layers then had to agree about.
|
|
26836
|
+
//
|
|
26837
|
+
// A value on the record wins over the position, per property, so that
|
|
26838
|
+
// `label-pos=n dx=3` reads as "north, nudged 3px right" rather than losing the
|
|
26839
|
+
// north. That is also what makes this change invisible to files written before
|
|
26840
|
+
// it: they carry all three alongside label-pos, with exactly the values this
|
|
26841
|
+
// would supply.
|
|
26842
|
+
function resolveLabelPosition(rec) {
|
|
26843
|
+
var style = rec && rec['label-pos'] ? getLabelPositionStyle(rec['label-pos']) : null;
|
|
26844
|
+
var out = null;
|
|
26845
|
+
var field, i;
|
|
26846
|
+
// An unusable position renders as if it were unset. The commands that set it
|
|
26847
|
+
// reject one, so reaching here means it was written by an expression or came
|
|
26848
|
+
// from a data file, where stopping the render is the wrong response.
|
|
26849
|
+
if (style) {
|
|
26850
|
+
for (i = 0; i < labelPositionDerivedFields.length; i++) {
|
|
26851
|
+
field = labelPositionDerivedFields[i];
|
|
26852
|
+
if (hasStyleValue(rec, field)) continue;
|
|
26853
|
+
if (!out) out = Object.assign({}, rec);
|
|
26854
|
+
out[field] = style[field];
|
|
26855
|
+
}
|
|
26856
|
+
}
|
|
26857
|
+
out = resolveLabelAlignment(out || rec) || out;
|
|
26858
|
+
return out || rec;
|
|
26859
|
+
}
|
|
26860
|
+
|
|
26861
|
+
// label-align wins over both the position's justification and a text-anchor of
|
|
26862
|
+
// the record's own, because it is the only one of the three that is asking
|
|
26863
|
+
// about justification alone. Where the block ends up is then the renderer's to
|
|
26864
|
+
// correct -- see getAlignmentShift().
|
|
26865
|
+
function resolveLabelAlignment(rec) {
|
|
26866
|
+
var anchor = getAlignmentAnchor(rec['label-align']);
|
|
26867
|
+
var out;
|
|
26868
|
+
if (!anchor || rec['text-anchor'] === anchor) return null;
|
|
26869
|
+
out = Object.assign({}, rec);
|
|
26870
|
+
out['text-anchor'] = anchor;
|
|
26871
|
+
return out;
|
|
26872
|
+
}
|
|
26873
|
+
|
|
26874
|
+
// The anchor a label's position implies, which is where its block of text is
|
|
26875
|
+
// drawn whatever the lines inside it do. 'start' is both the SVG default and
|
|
26876
|
+
// what an unpositioned label is drawn with.
|
|
26877
|
+
function getLabelPositionAnchor(rec) {
|
|
26878
|
+
var style = rec && rec['label-pos'] ? getLabelPositionStyle(rec['label-pos']) : null;
|
|
26879
|
+
return style && style['text-anchor'] || 'start';
|
|
26880
|
+
}
|
|
26881
|
+
|
|
26882
|
+
// Presence, not truthiness. `dy=0` is how a label cancels the vertical offset
|
|
26883
|
+
// its position carries, and the `rec.dy || 0` idiom used by the renderers would
|
|
26884
|
+
// read that as absent and hand back the offset it was written to remove.
|
|
26885
|
+
function hasStyleValue(rec, field) {
|
|
26886
|
+
var val = rec[field];
|
|
26887
|
+
return field in rec && val !== undefined && val !== null && val !== '';
|
|
26491
26888
|
}
|
|
26492
26889
|
|
|
26493
26890
|
function isSvgMeasure(o) {
|
|
@@ -26507,25 +26904,42 @@
|
|
|
26507
26904
|
var SvgProperties = /*#__PURE__*/Object.freeze({
|
|
26508
26905
|
__proto__: null,
|
|
26509
26906
|
applyStyleAttributes: applyStyleAttributes,
|
|
26907
|
+
emptyValueUnsetsProperty: emptyValueUnsetsProperty,
|
|
26510
26908
|
findStylePropertiesBySymbolGeom: findStylePropertiesBySymbolGeom,
|
|
26909
|
+
getLabelPositionAnchor: getLabelPositionAnchor,
|
|
26511
26910
|
getLabelPositionStyle: getLabelPositionStyle,
|
|
26512
26911
|
getPropertyAccessor: getPropertyAccessor,
|
|
26513
26912
|
getSymbolDataAccessor: getSymbolDataAccessor,
|
|
26514
26913
|
getSymbolListAccessor: getSymbolListAccessor,
|
|
26515
26914
|
getSymbolPropertyAccessor: getSymbolPropertyAccessor,
|
|
26915
|
+
hasStyleValue: hasStyleValue,
|
|
26516
26916
|
isSupportedSvgStyleProperty: isSupportedSvgStyleProperty,
|
|
26517
26917
|
isSvgClassName: isSvgClassName,
|
|
26518
26918
|
isSvgColor: isSvgColor,
|
|
26519
26919
|
isSvgMeasure: isSvgMeasure,
|
|
26520
26920
|
isSvgNumber: isSvgNumber,
|
|
26921
|
+
labelPositionDerivedFields: labelPositionDerivedFields,
|
|
26521
26922
|
labelPositionFields: labelPositionFields,
|
|
26522
26923
|
mightBeExpression: mightBeExpression,
|
|
26523
26924
|
parseBoolean: parseBoolean,
|
|
26524
26925
|
parseLabelPosition: parseLabelPosition,
|
|
26926
|
+
parseStyleLiteral: parseStyleLiteral,
|
|
26525
26927
|
parseSvgMeasure: parseSvgMeasure,
|
|
26526
|
-
|
|
26928
|
+
resolveLabelPosition: resolveLabelPosition
|
|
26527
26929
|
});
|
|
26528
26930
|
|
|
26931
|
+
// Accepting \n (two chars) as an alternative to the newline character
|
|
26932
|
+
// (sometimes, '\n' is not converted to newline, e.g. in a Makefile)
|
|
26933
|
+
// Also accepting <br>
|
|
26934
|
+
var labelNewlineRxp = /\n|\\n|<br>/i;
|
|
26935
|
+
|
|
26936
|
+
// The size a label is drawn at when it carries none of its own, which is the
|
|
26937
|
+
// size its layer's group supplies (see getLabelTextDefaults()). Shared with
|
|
26938
|
+
// that function rather than written twice: the em offsets the label positions
|
|
26939
|
+
// use are resolved against this number, and a correction computed against a
|
|
26940
|
+
// different one would put the label somewhere it is not drawn.
|
|
26941
|
+
var DEFAULT_LABEL_FONT_SIZE = 12;
|
|
26942
|
+
|
|
26529
26943
|
function toLabelString(val) {
|
|
26530
26944
|
if (val || val === 0 || val === false) return String(val);
|
|
26531
26945
|
return '';
|
|
@@ -26533,20 +26947,25 @@
|
|
|
26533
26947
|
|
|
26534
26948
|
// Kludge for applying fill and other styles to a <text> element
|
|
26535
26949
|
// (for rendering labels in the GUI with the dot in Canvas, not SVG)
|
|
26536
|
-
function renderStyledLabel(
|
|
26950
|
+
function renderStyledLabel(recArg) {
|
|
26951
|
+
// Resolved once and used for both, so that the offsets a position implies and
|
|
26952
|
+
// the justification it implies cannot come from different places: text-anchor
|
|
26953
|
+
// is written by applyStyleAttributes() and dx/dy by renderLabel().
|
|
26954
|
+
var rec = resolveLabelPosition(recArg);
|
|
26537
26955
|
var o = renderLabel$1(rec);
|
|
26538
26956
|
applyStyleAttributes(o, 'label', rec);
|
|
26539
26957
|
return o;
|
|
26540
26958
|
}
|
|
26541
26959
|
|
|
26542
|
-
function renderLabel$1(
|
|
26960
|
+
function renderLabel$1(recArg) {
|
|
26961
|
+
// Idempotent, and free on a record that has no position to resolve or has
|
|
26962
|
+
// already been through it, so calling it here as well costs nothing and means
|
|
26963
|
+
// every way into the renderer draws a label in the position it is stored in.
|
|
26964
|
+
var rec = resolveLabelPosition(recArg);
|
|
26543
26965
|
var line = toLabelString(rec['label-text']);
|
|
26544
26966
|
var morelines, obj;
|
|
26545
|
-
|
|
26546
|
-
|
|
26547
|
-
// Also accepting <br>
|
|
26548
|
-
var newline = /\n|\\n|<br>/i;
|
|
26549
|
-
var dx = rec.dx || 0;
|
|
26967
|
+
var newline = labelNewlineRxp;
|
|
26968
|
+
var dx = applyAlignmentShift(rec);
|
|
26550
26969
|
var dy = rec.dy || 0;
|
|
26551
26970
|
var properties = {
|
|
26552
26971
|
// using x, y instead of dx, dy for shift, because Illustrator doesn't apply
|
|
@@ -26581,10 +27000,96 @@
|
|
|
26581
27000
|
return obj;
|
|
26582
27001
|
}
|
|
26583
27002
|
|
|
27003
|
+
// The label's dx, moved to leave its block of text where its position put it
|
|
27004
|
+
// when label-align re-justifies the lines inside it. Returns dx untouched when
|
|
27005
|
+
// there is nothing to correct, so a label without an alignment keeps the value
|
|
27006
|
+
// it was stored with, in the units it was stored in.
|
|
27007
|
+
//
|
|
27008
|
+
// Only anchored labels are corrected. A path label's text follows its curve
|
|
27009
|
+
// from a start offset, so its alignment picks which part of the text sits at
|
|
27010
|
+
// that point -- there is no block beside an anchor to hold still, and its dx
|
|
27011
|
+
// means an offset from the path rather than from a point.
|
|
27012
|
+
function applyAlignmentShift(rec) {
|
|
27013
|
+
var dx = rec.dx || 0;
|
|
27014
|
+
var shift = getAlignmentShift(rec, getLabelPositionAnchor(rec));
|
|
27015
|
+
var px;
|
|
27016
|
+
if (!shift) return dx;
|
|
27017
|
+
px = toPixels(dx, rec['font-size']);
|
|
27018
|
+
// A dx in units the shift cannot be added to -- pt, %, anything but px and
|
|
27019
|
+
// em -- keeps its value, and the block moves as it did before. Correcting it
|
|
27020
|
+
// would mean choosing a pixel size for a unit whose whole point is that
|
|
27021
|
+
// something else decides.
|
|
27022
|
+
if (px === null) return dx;
|
|
27023
|
+
return roundShift(px + shift);
|
|
27024
|
+
}
|
|
27025
|
+
|
|
27026
|
+
// Everything a record says about where its text sits relative to its anchor,
|
|
27027
|
+
// resolved into numbers: the offsets the text is drawn at in px, and the
|
|
27028
|
+
// justification it is drawn with.
|
|
27029
|
+
//
|
|
27030
|
+
// This is what dragging a label starts from. A drag materializes the position
|
|
27031
|
+
// the label was in and adds its own delta to it, and the numbers it starts
|
|
27032
|
+
// from have to be the ones the label is actually drawn with -- otherwise the
|
|
27033
|
+
// text jumps on the first pixel of movement. Hence resolving it here, through
|
|
27034
|
+
// the same functions as the renderer above, rather than in the GUI against a
|
|
27035
|
+
// second copy of the rules.
|
|
27036
|
+
//
|
|
27037
|
+
// An offset in units this cannot convert -- pt, %, anything but px and em --
|
|
27038
|
+
// resolves to 0. The alternative is refusing the drag over a value that
|
|
27039
|
+
// reaches a label only from an expression or a data file, and a drag that puts
|
|
27040
|
+
// the text where the pointer is says more about where it went than a gesture
|
|
27041
|
+
// that does nothing.
|
|
27042
|
+
function getDrawnLabelOffset(recArg) {
|
|
27043
|
+
var rec = resolveLabelPosition(recArg);
|
|
27044
|
+
var dx = toPixels(applyAlignmentShift(rec), rec['font-size']);
|
|
27045
|
+
var dy = toPixels(rec.dy || 0, rec['font-size']);
|
|
27046
|
+
return {
|
|
27047
|
+
dx: dx === null ? 0 : dx,
|
|
27048
|
+
dy: dy === null ? 0 : dy,
|
|
27049
|
+
'text-anchor': rec['text-anchor'] || ''
|
|
27050
|
+
};
|
|
27051
|
+
}
|
|
27052
|
+
|
|
27053
|
+
// A measure in px, or null if it cannot be known.
|
|
27054
|
+
//
|
|
27055
|
+
// An em value needs the font size it is relative to. Most labels do not carry
|
|
27056
|
+
// one: the six positions that hold text clear of its anchor offset it in ems,
|
|
27057
|
+
// and font-size is usually inherited from the layer's group rather than set on
|
|
27058
|
+
// the label -- so a missing size is the normal case here and not a reason to
|
|
27059
|
+
// give up. It resolves against the same default the renderer applies, which is
|
|
27060
|
+
// why that default is one constant and not two.
|
|
27061
|
+
//
|
|
27062
|
+
// What is left is a size that is neither a number nor absent, or a measure in
|
|
27063
|
+
// units the correction cannot be expressed in -- pt, %, anything but px and
|
|
27064
|
+
// em. Those keep their value, because correcting them would mean choosing a
|
|
27065
|
+
// pixel size for a unit whose whole point is that something else decides.
|
|
27066
|
+
function toPixels(measure, fontSizeArg) {
|
|
27067
|
+
var val = parseSvgMeasure(measure);
|
|
27068
|
+
var fontSize = fontSizeArg === undefined || fontSizeArg === null ||
|
|
27069
|
+
fontSizeArg === '' ? DEFAULT_LABEL_FONT_SIZE : Number(fontSizeArg);
|
|
27070
|
+
var em;
|
|
27071
|
+
if (utils.isFiniteNumber(val)) return val;
|
|
27072
|
+
em = /^(-?[.0-9]+)em$/.exec(String(val));
|
|
27073
|
+
if (!em) return null;
|
|
27074
|
+
if (fontSize > 0 === false) return null;
|
|
27075
|
+
return Number(em[1]) * fontSize;
|
|
27076
|
+
}
|
|
27077
|
+
|
|
27078
|
+
// Tenths of a pixel. The serializer prints what it is given, and a correction
|
|
27079
|
+
// is a product of two measurements -- left alone it arrives as
|
|
27080
|
+
// 4.800000000000001.
|
|
27081
|
+
function roundShift(px) {
|
|
27082
|
+
return Math.round(px * 10) / 10;
|
|
27083
|
+
}
|
|
27084
|
+
|
|
26584
27085
|
var SvgLabels = /*#__PURE__*/Object.freeze({
|
|
26585
27086
|
__proto__: null,
|
|
27087
|
+
DEFAULT_LABEL_FONT_SIZE: DEFAULT_LABEL_FONT_SIZE,
|
|
27088
|
+
getDrawnLabelOffset: getDrawnLabelOffset,
|
|
27089
|
+
labelNewlineRxp: labelNewlineRxp,
|
|
26586
27090
|
renderLabel: renderLabel$1,
|
|
26587
|
-
renderStyledLabel: renderStyledLabel
|
|
27091
|
+
renderStyledLabel: renderStyledLabel,
|
|
27092
|
+
toLabelString: toLabelString
|
|
26588
27093
|
});
|
|
26589
27094
|
|
|
26590
27095
|
// convert data records (properties like svg-symbol, label-text, fill, r) to svg symbols
|
|
@@ -26672,10 +27177,20 @@
|
|
|
26672
27177
|
return size / 2;
|
|
26673
27178
|
}
|
|
26674
27179
|
|
|
27180
|
+
// A label with a symbol renders two elements from one record, and every opacity
|
|
27181
|
+
// property in commonProperties is applied to both, so the text's opacity fades
|
|
27182
|
+
// the symbol with it. icon-color and icon-opacity are how a symbol is styled
|
|
27183
|
+
// apart from the text: each overrides its counterpart here.
|
|
27184
|
+
//
|
|
27185
|
+
// icon-opacity maps to opacity rather than fill-opacity because a ring is drawn
|
|
27186
|
+
// as a stroked circle with no fill, which a fill opacity would leave alone.
|
|
26675
27187
|
function getIconStyleData(d, r) {
|
|
26676
27188
|
var o = utils.extend({}, d);
|
|
26677
27189
|
o.r = r;
|
|
26678
27190
|
o.fill = d['icon-color'] || d.fill || 'black';
|
|
27191
|
+
if (isSvgNumber(d['icon-opacity'])) {
|
|
27192
|
+
o.opacity = Number(d['icon-opacity']);
|
|
27193
|
+
}
|
|
26679
27194
|
return o;
|
|
26680
27195
|
}
|
|
26681
27196
|
|
|
@@ -27179,16 +27694,19 @@
|
|
|
27179
27694
|
renderFurnitureLayer: renderFurnitureLayer
|
|
27180
27695
|
});
|
|
27181
27696
|
|
|
27697
|
+
// Tags whose content is rendered text, where whitespace added for legibility
|
|
27698
|
+
// would show up in the output.
|
|
27699
|
+
var textTags = {text: true, tspan: true, textPath: true};
|
|
27700
|
+
|
|
27182
27701
|
function stringify(obj) {
|
|
27183
27702
|
var svg, joinStr;
|
|
27184
27703
|
if (!obj || !obj.tag) return '';
|
|
27185
27704
|
svg = '<' + obj.tag;
|
|
27186
|
-
// w.s. is significant in text elements
|
|
27187
27705
|
if (obj.properties) {
|
|
27188
27706
|
svg += stringifyProperties(obj.properties);
|
|
27189
27707
|
}
|
|
27190
27708
|
if (obj.children || obj.value) {
|
|
27191
|
-
joinStr = obj.tag
|
|
27709
|
+
joinStr = obj.tag in textTags ? '' : '\n';
|
|
27192
27710
|
svg += '>' + joinStr;
|
|
27193
27711
|
if (obj.value) {
|
|
27194
27712
|
svg += stringEscape(obj.value);
|
|
@@ -27247,47 +27765,618 @@
|
|
|
27247
27765
|
stringifyProperties: stringifyProperties
|
|
27248
27766
|
});
|
|
27249
27767
|
|
|
27250
|
-
//
|
|
27251
|
-
//
|
|
27252
|
-
|
|
27253
|
-
|
|
27254
|
-
|
|
27255
|
-
|
|
27256
|
-
|
|
27257
|
-
|
|
27258
|
-
|
|
27259
|
-
|
|
27260
|
-
|
|
27261
|
-
|
|
27262
|
-
|
|
27768
|
+
// Fits an interpolating spline through a sequence of knots and flattens it to
|
|
27769
|
+
// a polyline. Used to derive a label path from the knots stored as a label's
|
|
27770
|
+
// multipoint geometry (see docs/development/label-tool-design.md).
|
|
27771
|
+
//
|
|
27772
|
+
// The curve passes through every knot -- knots are not Bezier handles. This is
|
|
27773
|
+
// the behavior of Illustrator's Curvature tool, and it is what lets a curve be
|
|
27774
|
+
// re-edited by moving the points the user originally clicked.
|
|
27775
|
+
//
|
|
27776
|
+
// The fit is Hobby's algorithm: John Hobby, "Smooth, Easy to Compute
|
|
27777
|
+
// Interpolating Splines", Stanford CS-TR-85-1047 (1985) / Discrete &
|
|
27778
|
+
// Computational Geometry 1 (1986). It is the spline Metafont, MetaPost and
|
|
27779
|
+
// TikZ draw, chosen here because it was designed for the aesthetics of the
|
|
27780
|
+
// result rather than for analytic tidiness.
|
|
27781
|
+
//
|
|
27782
|
+
// Hobby picks a tangent direction at each knot such that "mock curvature" -- a
|
|
27783
|
+
// first-order approximation of curvature that keeps the system linear -- is
|
|
27784
|
+
// equal on both sides of the knot. The curve is therefore near enough to
|
|
27785
|
+
// curvature-continuous to look it, where an earlier centripetal Catmull-Rom
|
|
27786
|
+
// fit here was only tangent-continuous and visibly kinked at every knot: its
|
|
27787
|
+
// curvature jumped by 55% to 220% of the curve's mean curvature at each one.
|
|
27788
|
+
//
|
|
27789
|
+
// Two properties of the method matter to the label tool. The solve is a single
|
|
27790
|
+
// tridiagonal system rather than an iteration, so the fit is deterministic and
|
|
27791
|
+
// the GUI preview and the CLI export cannot drift apart. And the curves are
|
|
27792
|
+
// invariant under translation, rotation and scaling (Hobby 1986), which is
|
|
27793
|
+
// what lets the tool fit in display coordinates while storing knots in map
|
|
27794
|
+
// coordinates.
|
|
27795
|
+
|
|
27796
|
+
// Curl controls how the curve behaves at the two ends of a run. At 0 it
|
|
27797
|
+
// approaches a straight line there; at 1 (Metafont's default) it approaches a
|
|
27798
|
+
// circular arc, which on a path that turns hard near its end throws the curve
|
|
27799
|
+
// into a wide loop beyond the last knot. Label paths are short enough that the
|
|
27800
|
+
// end segments are most of the curve, so they want a much flatter end than
|
|
27801
|
+
// Metafont does.
|
|
27802
|
+
var DEFAULT_CURL = 0.15;
|
|
27803
|
+
var curl = DEFAULT_CURL;
|
|
27804
|
+
|
|
27805
|
+
// Temporary knob for judging the end behavior by eye; see
|
|
27806
|
+
// window.mapshaper.setLabelCurveCurl() in the GUI. Expected to be retired once
|
|
27807
|
+
// a default is settled on.
|
|
27808
|
+
function setCurveCurl(val) {
|
|
27809
|
+
curl = val >= 0 ? +val : DEFAULT_CURL;
|
|
27810
|
+
return curl;
|
|
27811
|
+
}
|
|
27812
|
+
|
|
27813
|
+
function getCurveCurl() {
|
|
27814
|
+
return curl;
|
|
27815
|
+
}
|
|
27816
|
+
|
|
27817
|
+
// Cap on subdivision levels per curve segment, bounding output at 2^8 = 256
|
|
27818
|
+
// vertices per segment if a pathological input (e.g. knots far enough apart to
|
|
27819
|
+
// exhaust float precision) never satisfies the flatness test.
|
|
27820
|
+
var MAX_SUBDIVIDE_DEPTH = 8;
|
|
27821
|
+
|
|
27822
|
+
// Relative accuracy of the arc-length estimate, and a matching depth cap. The
|
|
27823
|
+
// bracket between a cubic's chord and its control polygon narrows by roughly a
|
|
27824
|
+
// factor of four per subdivision, so this converges in well under ten levels.
|
|
27825
|
+
var LENGTH_TOLERANCE = 1e-5;
|
|
27826
|
+
var MAX_LENGTH_DEPTH = 16;
|
|
27827
|
+
|
|
27828
|
+
// knots: array of [x, y] in a single coordinate space
|
|
27829
|
+
// tolerance: max deviation of the output polyline from the true curve, in the
|
|
27830
|
+
// same units as the knots
|
|
27831
|
+
// Returns an array of [x, y] starting at the first knot, ending at the last,
|
|
27832
|
+
// and passing through every knot in between.
|
|
27833
|
+
function fitCurveThroughKnots(knots, tolerance) {
|
|
27834
|
+
var pts = dedupeKnots(knots || []),
|
|
27835
|
+
segments, out, i;
|
|
27836
|
+
if (pts.length < 2) return pts;
|
|
27837
|
+
if (tolerance > 0 === false) {
|
|
27838
|
+
// with no usable tolerance there is nothing to flatten against; the knots
|
|
27839
|
+
// are the curve at its coarsest
|
|
27840
|
+
return pts;
|
|
27841
|
+
}
|
|
27842
|
+
segments = fitRun(pts);
|
|
27843
|
+
out = [pts[0].slice()];
|
|
27844
|
+
for (i = 0; i < segments.length; i++) {
|
|
27845
|
+
flattenCubic(segments[i].p0, segments[i].c1, segments[i].c2,
|
|
27846
|
+
segments[i].p3, tolerance, 0, out);
|
|
27847
|
+
out.push([segments[i].p3[0], segments[i].p3[1]]);
|
|
27848
|
+
}
|
|
27849
|
+
return out;
|
|
27850
|
+
}
|
|
27263
27851
|
|
|
27264
|
-
|
|
27265
|
-
|
|
27266
|
-
|
|
27852
|
+
// Returns the curve's control points as cubic Bezier segments, without
|
|
27853
|
+
// flattening. Kept separate from the flattening so that SVG export can emit
|
|
27854
|
+
// true curves rather than a densified polyline.
|
|
27855
|
+
// Returns [{p0, c1, c2, p3}, ...], one per knot interval.
|
|
27856
|
+
function getCurveSegments(knots) {
|
|
27857
|
+
var pts = dedupeKnots(knots || []);
|
|
27858
|
+
if (pts.length < 2) return [];
|
|
27859
|
+
return fitRun(pts);
|
|
27860
|
+
}
|
|
27861
|
+
|
|
27862
|
+
// Arc length of the fitted curve, in the same units as the knots. Export uses
|
|
27863
|
+
// this to decide whether a label's text fits its path, so it is measured on the
|
|
27864
|
+
// true curve rather than on a flattened approximation of it -- a flattening
|
|
27865
|
+
// tolerance tight enough to be accurate in output pixels would be meaningless
|
|
27866
|
+
// if the knots were in degrees or metres, and this estimate is scale-free.
|
|
27867
|
+
function getCurveLength(knots) {
|
|
27868
|
+
var segments = getCurveSegments(knots);
|
|
27869
|
+
var len = 0, i;
|
|
27870
|
+
for (i = 0; i < segments.length; i++) {
|
|
27871
|
+
len += getCubicLength(segments[i], 0);
|
|
27267
27872
|
}
|
|
27268
|
-
|
|
27873
|
+
return len;
|
|
27874
|
+
}
|
|
27269
27875
|
|
|
27270
|
-
|
|
27271
|
-
|
|
27876
|
+
// Hobby's velocity function constants (Hobby 1986, eq. 11), the approximation
|
|
27877
|
+
// Metafont uses in place of the transcendental form.
|
|
27878
|
+
var VEL_A = Math.SQRT2,
|
|
27879
|
+
VEL_B = 1 / 16,
|
|
27880
|
+
VEL_C = (3 - Math.sqrt(5)) / 2;
|
|
27881
|
+
|
|
27882
|
+
// Fits a run of knots, returning a cubic per interval.
|
|
27883
|
+
function fitRun(pts) {
|
|
27884
|
+
var n = pts.length - 1, // segment count
|
|
27885
|
+
dd = [], om = [], psi = [], segments = [],
|
|
27886
|
+
theta, phi, i;
|
|
27887
|
+
if (n < 1) return [];
|
|
27888
|
+
// two knots have no interior knot to bend around
|
|
27889
|
+
if (n === 1) return [straightSegment(pts[0], pts[1])];
|
|
27890
|
+
for (i = 0; i < n; i++) {
|
|
27891
|
+
dd.push(distance2D$1(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1]));
|
|
27892
|
+
om.push(Math.atan2(pts[i + 1][1] - pts[i][1], pts[i + 1][0] - pts[i][0]));
|
|
27893
|
+
}
|
|
27894
|
+
// psi[i] is how far the polyline turns at knot i
|
|
27895
|
+
psi.push(0);
|
|
27896
|
+
for (i = 1; i < n; i++) psi.push(wrapAngle(om[i] - om[i - 1]));
|
|
27272
27897
|
|
|
27273
|
-
|
|
27274
|
-
|
|
27275
|
-
|
|
27276
|
-
|
|
27277
|
-
|
|
27278
|
-
|
|
27279
|
-
|
|
27280
|
-
|
|
27281
|
-
|
|
27898
|
+
theta = solveDepartureAngles(dd, psi, n);
|
|
27899
|
+
phi = getArrivalAngles(theta, psi, n);
|
|
27900
|
+
|
|
27901
|
+
for (i = 0; i < n; i++) {
|
|
27902
|
+
segments.push(buildSegment(pts[i], pts[i + 1], dd[i], om[i],
|
|
27903
|
+
theta[i], phi[i + 1]));
|
|
27904
|
+
}
|
|
27905
|
+
return segments;
|
|
27906
|
+
}
|
|
27907
|
+
|
|
27908
|
+
// The tridiagonal system of Hobby's mock-curvature conditions, in the
|
|
27909
|
+
// departure angle at each knot measured from the chord leaving it. Written for
|
|
27910
|
+
// unit tension throughout; the general form carries a tension per knot, which
|
|
27911
|
+
// the label tool has no use for.
|
|
27912
|
+
//
|
|
27913
|
+
// The interior rows come from equalizing mock curvature across knot i; the
|
|
27914
|
+
// first and last rows are the curl conditions that close the system at the
|
|
27915
|
+
// ends of an open run.
|
|
27916
|
+
function solveDepartureAngles(dd, psi, n) {
|
|
27917
|
+
var lo = [], di = [], up = [], r = [], i;
|
|
27918
|
+
lo.push(0);
|
|
27919
|
+
di.push(2 + curl);
|
|
27920
|
+
up.push(1 + 2 * curl);
|
|
27921
|
+
r.push(-(1 + 2 * curl) * psi[1]);
|
|
27922
|
+
for (i = 1; i < n - 1; i++) {
|
|
27923
|
+
lo.push(dd[i]);
|
|
27924
|
+
di.push(2 * (dd[i] + dd[i - 1]));
|
|
27925
|
+
up.push(dd[i - 1]);
|
|
27926
|
+
r.push(-2 * dd[i] * psi[i] - dd[i - 1] * psi[i + 1]);
|
|
27927
|
+
}
|
|
27928
|
+
if (n > 1) {
|
|
27929
|
+
lo.push(dd[n - 1]);
|
|
27930
|
+
di.push(2 * (dd[n - 1] + dd[n - 2]) -
|
|
27931
|
+
dd[n - 2] * (1 + 2 * curl) / (2 + curl));
|
|
27932
|
+
up.push(0);
|
|
27933
|
+
r.push(-2 * dd[n - 1] * psi[n - 1]);
|
|
27934
|
+
}
|
|
27935
|
+
return solveTridiagonal(lo, di, up, r);
|
|
27936
|
+
}
|
|
27937
|
+
|
|
27938
|
+
// The angle at which the curve arrives at each knot, again measured from the
|
|
27939
|
+
// chord. At an interior knot the departure and arrival angles have to account
|
|
27940
|
+
// between them for the whole turn in the polyline; at the far end of the run
|
|
27941
|
+
// the curl condition fixes it.
|
|
27942
|
+
function getArrivalAngles(theta, psi, n) {
|
|
27943
|
+
var phi = [0], i;
|
|
27944
|
+
for (i = 1; i < n; i++) phi.push(-psi[i] - theta[i]);
|
|
27945
|
+
phi.push((1 + 2 * curl) * theta[n - 1] / (2 + curl));
|
|
27946
|
+
return phi;
|
|
27947
|
+
}
|
|
27948
|
+
|
|
27949
|
+
// Turns a pair of angles into a cubic, by way of Hobby's velocity functions --
|
|
27950
|
+
// how far along each tangent the control point sits, as a multiple of a third
|
|
27951
|
+
// of the chord.
|
|
27952
|
+
function buildSegment(p0, p1, d, om, theta, phi) {
|
|
27953
|
+
var alpha = VEL_A * (Math.sin(theta) - VEL_B * Math.sin(phi)) *
|
|
27954
|
+
(Math.sin(phi) - VEL_B * Math.sin(theta)) *
|
|
27955
|
+
(Math.cos(theta) - Math.cos(phi)),
|
|
27956
|
+
rho = (2 + alpha) /
|
|
27957
|
+
(1 + (1 - VEL_C) * Math.cos(theta) + VEL_C * Math.cos(phi)),
|
|
27958
|
+
sigma = (2 - alpha) /
|
|
27959
|
+
(1 + (1 - VEL_C) * Math.cos(phi) + VEL_C * Math.cos(theta));
|
|
27960
|
+
return {
|
|
27961
|
+
p0: p0,
|
|
27962
|
+
c1: [p0[0] + d * rho / 3 * Math.cos(theta + om),
|
|
27963
|
+
p0[1] + d * rho / 3 * Math.sin(theta + om)],
|
|
27964
|
+
c2: [p1[0] - d * sigma / 3 * Math.cos(om - phi),
|
|
27965
|
+
p1[1] - d * sigma / 3 * Math.sin(om - phi)],
|
|
27966
|
+
p3: p1
|
|
27967
|
+
};
|
|
27968
|
+
}
|
|
27969
|
+
|
|
27970
|
+
// A cubic that is exactly its chord, with the control points at the thirds --
|
|
27971
|
+
// the degenerate case of the above when both angles are zero.
|
|
27972
|
+
function straightSegment(a, b) {
|
|
27973
|
+
return {
|
|
27974
|
+
p0: a,
|
|
27975
|
+
c1: [a[0] + (b[0] - a[0]) / 3, a[1] + (b[1] - a[1]) / 3],
|
|
27976
|
+
c2: [a[0] + 2 * (b[0] - a[0]) / 3, a[1] + 2 * (b[1] - a[1]) / 3],
|
|
27977
|
+
p3: b
|
|
27978
|
+
};
|
|
27979
|
+
}
|
|
27980
|
+
|
|
27981
|
+
// Thomas algorithm. Hobby shows the system is diagonally dominant, so this
|
|
27982
|
+
// needs no pivoting.
|
|
27983
|
+
function solveTridiagonal(lo, di, up, r) {
|
|
27984
|
+
var n = di.length,
|
|
27985
|
+
cp = [], dp = [], x = new Array(n), i, m;
|
|
27986
|
+
cp.push(up[0] / di[0]);
|
|
27987
|
+
dp.push(r[0] / di[0]);
|
|
27988
|
+
for (i = 1; i < n; i++) {
|
|
27989
|
+
m = di[i] - lo[i] * cp[i - 1];
|
|
27990
|
+
cp.push(up[i] / m);
|
|
27991
|
+
dp.push((r[i] - lo[i] * dp[i - 1]) / m);
|
|
27992
|
+
}
|
|
27993
|
+
x[n - 1] = dp[n - 1];
|
|
27994
|
+
for (i = n - 2; i >= 0; i--) {
|
|
27995
|
+
x[i] = dp[i] - cp[i] * x[i + 1];
|
|
27996
|
+
}
|
|
27997
|
+
return x;
|
|
27998
|
+
}
|
|
27999
|
+
|
|
28000
|
+
// Into (-PI, PI], so that a turn is measured the short way round.
|
|
28001
|
+
function wrapAngle(a) {
|
|
28002
|
+
while (a > Math.PI) a -= 2 * Math.PI;
|
|
28003
|
+
while (a <= -Math.PI) a += 2 * Math.PI;
|
|
28004
|
+
return a;
|
|
28005
|
+
}
|
|
28006
|
+
|
|
28007
|
+
// A cubic's arc length is bracketed below by its chord and above by its control
|
|
28008
|
+
// polygon. Subdividing narrows the bracket; the midpoint of a narrow one is the
|
|
28009
|
+
// length. (Gravesen's method.)
|
|
28010
|
+
function getCubicLength(seg, depth) {
|
|
28011
|
+
var chord = distance2D$1(seg.p0[0], seg.p0[1], seg.p3[0], seg.p3[1]);
|
|
28012
|
+
var poly = distance2D$1(seg.p0[0], seg.p0[1], seg.c1[0], seg.c1[1]) +
|
|
28013
|
+
distance2D$1(seg.c1[0], seg.c1[1], seg.c2[0], seg.c2[1]) +
|
|
28014
|
+
distance2D$1(seg.c2[0], seg.c2[1], seg.p3[0], seg.p3[1]);
|
|
28015
|
+
var halves;
|
|
28016
|
+
if (poly === 0 || depth >= MAX_LENGTH_DEPTH ||
|
|
28017
|
+
poly - chord <= LENGTH_TOLERANCE * poly) {
|
|
28018
|
+
return (chord + poly) / 2;
|
|
28019
|
+
}
|
|
28020
|
+
halves = subdivideCubic(seg.p0, seg.c1, seg.c2, seg.p3);
|
|
28021
|
+
return getCubicLength(halves.left, depth + 1) +
|
|
28022
|
+
getCubicLength(halves.right, depth + 1);
|
|
28023
|
+
}
|
|
28024
|
+
|
|
28025
|
+
// Removes non-finite and consecutively duplicated knots. A duplicate spans a
|
|
28026
|
+
// zero-length chord, which would leave the direction of the polyline there
|
|
28027
|
+
// undefined -- and double-clicking to finish a path is an easy way to make one.
|
|
28028
|
+
function dedupeKnots(knots) {
|
|
28029
|
+
var points = [], i, p, prev;
|
|
28030
|
+
for (i = 0; i < knots.length; i++) {
|
|
28031
|
+
p = knots[i];
|
|
28032
|
+
if (!p || p.length < 2 || !isFiniteNumber$1(p[0]) || !isFiniteNumber$1(p[1])) continue;
|
|
28033
|
+
if (prev && p[0] === prev[0] && p[1] === prev[1]) continue;
|
|
28034
|
+
points.push([p[0], p[1]]);
|
|
28035
|
+
prev = p;
|
|
28036
|
+
}
|
|
28037
|
+
return points;
|
|
28038
|
+
}
|
|
28039
|
+
|
|
28040
|
+
// Recursively subdivides a cubic until its control points lie within
|
|
28041
|
+
// @tolerance of its chord, emitting only the vertices strictly inside the
|
|
28042
|
+
// curve -- the caller adds the endpoint.
|
|
28043
|
+
//
|
|
28044
|
+
// Testing the control points is conservative: a cubic's greatest deviation
|
|
28045
|
+
// from its chord is at most 3/4 of the greater control point distance, so the
|
|
28046
|
+
// flattened path stays within 0.75 * tolerance of the true curve.
|
|
28047
|
+
function flattenCubic(p0, c1, c2, p3, tolerance, depth, out) {
|
|
28048
|
+
var tolSq = tolerance * tolerance;
|
|
28049
|
+
var halves;
|
|
28050
|
+
if (depth >= MAX_SUBDIVIDE_DEPTH) return;
|
|
28051
|
+
if (pointSegDistSq2(c1[0], c1[1], p0[0], p0[1], p3[0], p3[1]) <= tolSq &&
|
|
28052
|
+
pointSegDistSq2(c2[0], c2[1], p0[0], p0[1], p3[0], p3[1]) <= tolSq) {
|
|
28053
|
+
return; // flat enough -- the chord stands in for the curve
|
|
28054
|
+
}
|
|
28055
|
+
halves = subdivideCubic(p0, c1, c2, p3);
|
|
28056
|
+
flattenCubic(halves.left.p0, halves.left.c1, halves.left.c2, halves.left.p3,
|
|
28057
|
+
tolerance, depth + 1, out);
|
|
28058
|
+
out.push([halves.left.p3[0], halves.left.p3[1]]); // curve midpoint
|
|
28059
|
+
flattenCubic(halves.right.p0, halves.right.c1, halves.right.c2, halves.right.p3,
|
|
28060
|
+
tolerance, depth + 1, out);
|
|
28061
|
+
}
|
|
28062
|
+
|
|
28063
|
+
// de Casteljau split at t = 0.5
|
|
28064
|
+
function subdivideCubic(p0, c1, c2, p3) {
|
|
28065
|
+
var a = mid(p0, c1),
|
|
28066
|
+
b = mid(c1, c2),
|
|
28067
|
+
c = mid(c2, p3),
|
|
28068
|
+
d = mid(a, b),
|
|
28069
|
+
e = mid(b, c),
|
|
28070
|
+
m = mid(d, e);
|
|
28071
|
+
return {
|
|
28072
|
+
left: {p0: p0, c1: a, c2: d, p3: m},
|
|
28073
|
+
right: {p0: m, c1: e, c2: c, p3: p3}
|
|
28074
|
+
};
|
|
28075
|
+
}
|
|
28076
|
+
|
|
28077
|
+
function mid(a, b) {
|
|
28078
|
+
return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
28079
|
+
}
|
|
28080
|
+
|
|
28081
|
+
function isFiniteNumber$1(n) {
|
|
28082
|
+
return typeof n == 'number' && isFinite(n);
|
|
28083
|
+
}
|
|
28084
|
+
|
|
28085
|
+
var CurveFit = /*#__PURE__*/Object.freeze({
|
|
28086
|
+
__proto__: null,
|
|
28087
|
+
fitCurveThroughKnots: fitCurveThroughKnots,
|
|
28088
|
+
getCurveCurl: getCurveCurl,
|
|
28089
|
+
getCurveLength: getCurveLength,
|
|
28090
|
+
getCurveSegments: getCurveSegments,
|
|
28091
|
+
setCurveCurl: setCurveCurl
|
|
28092
|
+
});
|
|
28093
|
+
|
|
28094
|
+
var roundCoord$2 = getRoundingFunction(0.01);
|
|
28095
|
+
|
|
28096
|
+
function stringifyVertex(p) {
|
|
28097
|
+
return ' ' + roundCoord$2(p[0]) + ' ' + roundCoord$2(p[1]);
|
|
28098
|
+
}
|
|
28099
|
+
|
|
28100
|
+
function isCubicCtrl(p) {
|
|
28101
|
+
return p.length > 2 && p[2] == 'C';
|
|
28102
|
+
}
|
|
28103
|
+
|
|
28104
|
+
function stringifyPolygonCoords(coords) {
|
|
28105
|
+
var parts = [];
|
|
28106
|
+
for (var i=0; i<coords.length; i++) {
|
|
28107
|
+
parts.push(stringifyLineStringCoords(coords[i]) + ' Z');
|
|
28108
|
+
}
|
|
28109
|
+
return parts.length > 0 ? parts.join(' ') : '';
|
|
28110
|
+
}
|
|
28111
|
+
|
|
28112
|
+
function stringifyLineStringCoords(coords) {
|
|
28113
|
+
if (coords.length === 0) return '';
|
|
28114
|
+
var d = 'M';
|
|
28115
|
+
var fromCurve = false;
|
|
28116
|
+
var p, i, n;
|
|
28117
|
+
for (i=0, n=coords.length; i<n; i++) {
|
|
28118
|
+
p = coords[i];
|
|
28119
|
+
if (isCubicCtrl(p)) {
|
|
28120
|
+
// TODO: add defensive check
|
|
28121
|
+
d += ' C' + stringifyVertex(p) + stringifyVertex(coords[++i]) + stringifyVertex(coords[++i]);
|
|
28122
|
+
fromCurve = true;
|
|
28123
|
+
} else if (fromCurve) {
|
|
28124
|
+
d += ' L' + stringifyVertex(p);
|
|
28125
|
+
fromCurve = false;
|
|
28126
|
+
} else {
|
|
28127
|
+
d += stringifyVertex(p);
|
|
27282
28128
|
}
|
|
28129
|
+
}
|
|
28130
|
+
return d;
|
|
28131
|
+
}
|
|
27283
28132
|
|
|
27284
|
-
|
|
28133
|
+
var SvgPathUtils = /*#__PURE__*/Object.freeze({
|
|
28134
|
+
__proto__: null,
|
|
28135
|
+
stringifyLineStringCoords: stringifyLineStringCoords,
|
|
28136
|
+
stringifyPolygonCoords: stringifyPolygonCoords
|
|
28137
|
+
});
|
|
28138
|
+
|
|
28139
|
+
// Decides whether a path-aligned label's text fits its path.
|
|
28140
|
+
//
|
|
28141
|
+
// The decision compares two numbers, and only one of them is geometry. Path
|
|
28142
|
+
// length export computes; text width takes font metrics, which come from
|
|
28143
|
+
// svg-label-metrics.mjs -- measured by rendering in the GUI, and read from the
|
|
28144
|
+
// installed font files in Node.
|
|
28145
|
+
//
|
|
28146
|
+
// What that module holds is the measured width, not a fits/doesn't-fit verdict,
|
|
28147
|
+
// because a verdict would depend on output size and a width does not: text is
|
|
28148
|
+
// exported at its native font-size, so its rendered width in output pixels is
|
|
28149
|
+
// the same at every output size, while the path shrinks and grows.
|
|
28150
|
+
//
|
|
28151
|
+
// See docs/development/label-tool-design.md.
|
|
28152
|
+
|
|
28153
|
+
// One of:
|
|
28154
|
+
// 'fits' the text is known to fit
|
|
28155
|
+
// 'overflow' the text is known not to fit; export drops the label
|
|
28156
|
+
// 'unmeasured' no width is available, so fit is unknown
|
|
28157
|
+
//
|
|
28158
|
+
// 'unmeasured' renders, because silently deleting a label from a map is a worse
|
|
28159
|
+
// outcome than drawing one that overflows: an overflow is visible and fixable,
|
|
28160
|
+
// a deletion is neither. It is what a machine without the label's font gets,
|
|
28161
|
+
// and what every reader got outside the GUI before Node could measure.
|
|
28162
|
+
function getLabelFitState(rec, pathLength) {
|
|
28163
|
+
var width = getMeasuredTextWidth(rec);
|
|
28164
|
+
if (!width) return 'unmeasured';
|
|
28165
|
+
return width <= pathLength ? 'fits' : 'overflow';
|
|
28166
|
+
}
|
|
28167
|
+
|
|
28168
|
+
var SvgLabelFit = /*#__PURE__*/Object.freeze({
|
|
28169
|
+
__proto__: null,
|
|
28170
|
+
getLabelFitState: getLabelFitState
|
|
28171
|
+
});
|
|
28172
|
+
|
|
28173
|
+
// Renders a path-aligned label as <text><textPath>, with the path itself
|
|
28174
|
+
// hoisted into <defs> by svg-definitions.mjs.
|
|
28175
|
+
//
|
|
28176
|
+
// A label's knots are its geometry, so a point label is an anchored label and a
|
|
28177
|
+
// multipoint label is a path-aligned one. Note that this changes what a
|
|
28178
|
+
// multipoint feature carrying label-text used to export as: previously the same
|
|
28179
|
+
// label was drawn once at every point. Nothing depended on that -- the
|
|
28180
|
+
// duplicate rendering was a known wart, flagged by a commented-out warning in
|
|
28181
|
+
// layerHasLabels().
|
|
28182
|
+
//
|
|
28183
|
+
// See docs/development/label-tool-design.md.
|
|
28184
|
+
|
|
28185
|
+
// Number of dropped/joined ids to name in a report before summarizing the rest.
|
|
28186
|
+
var MAX_REPORTED_IDS = 10;
|
|
28187
|
+
|
|
28188
|
+
// Property that carries a label's path data until a consumer moves it into a
|
|
28189
|
+
// <defs> entry and replaces it with a reference. Export and the GUI both do
|
|
28190
|
+
// this but assign ids differently -- export derives them from the path data so
|
|
28191
|
+
// that output is byte-stable and identical paths share a definition, while the
|
|
28192
|
+
// GUI namespaces them per rendered layer.
|
|
28193
|
+
var LABEL_PATH_PROPERTY = 'label-path-d';
|
|
28194
|
+
|
|
28195
|
+
// Class marking a label whose text is too long for its path. Only the GUI sets
|
|
28196
|
+
// it, via keepOverflow; export drops such labels instead.
|
|
28197
|
+
var LABEL_OVERFLOW_CLASS = 'label-overflow';
|
|
28198
|
+
|
|
28199
|
+
function featureIsPathLabel(geom, rec) {
|
|
28200
|
+
return !!(geom && geom.type == 'MultiPoint' && geom.coordinates &&
|
|
28201
|
+
geom.coordinates.length > 1 && featureHasLabel(rec));
|
|
28202
|
+
}
|
|
28203
|
+
|
|
28204
|
+
// featureIsPathLabel() against a point layer's shape -- an array of coordinate
|
|
28205
|
+
// pairs. Separate so that the GUI's render loop does not have to build a
|
|
28206
|
+
// geometry object per shape just to ask the question.
|
|
28207
|
+
//
|
|
28208
|
+
// It is the GUI's test, and so it accepts a label whose text is still empty
|
|
28209
|
+
// where the export test does not: a path label is one being typed into before
|
|
28210
|
+
// it has any text, and it has to keep its curve while that is true.
|
|
28211
|
+
function shapeIsPathLabel(shp, rec) {
|
|
28212
|
+
return !!(shp && shp.length > 1 && featureIsLabel(rec));
|
|
28213
|
+
}
|
|
28214
|
+
|
|
28215
|
+
// The 'd' attribute of a label's path, as true cubic curves rather than a
|
|
28216
|
+
// densified polyline -- worth it here because an exported label path is
|
|
28217
|
+
// re-editable in Illustrator, and because a smooth baseline is what the text
|
|
28218
|
+
// is positioned against.
|
|
28219
|
+
function getLabelPathData(knots) {
|
|
28220
|
+
var segments = getCurveSegments(knots);
|
|
28221
|
+
var coords, seg, i;
|
|
28222
|
+
if (segments.length === 0) return null; // knots collapsed to a single point
|
|
28223
|
+
coords = [[segments[0].p0[0], segments[0].p0[1]]];
|
|
28224
|
+
for (i = 0; i < segments.length; i++) {
|
|
28225
|
+
seg = segments[i];
|
|
28226
|
+
// the 'C' tag marks a control point for the path writer, which then
|
|
28227
|
+
// consumes the following two coordinates as the second control point and
|
|
28228
|
+
// the segment's endpoint
|
|
28229
|
+
coords.push([seg.c1[0], seg.c1[1], 'C'], [seg.c2[0], seg.c2[1]],
|
|
28230
|
+
[seg.p3[0], seg.p3[1]]);
|
|
28231
|
+
}
|
|
28232
|
+
return stringifyLineStringCoords(coords);
|
|
28233
|
+
}
|
|
28234
|
+
|
|
28235
|
+
function initPathLabelReport() {
|
|
28236
|
+
return {dropped: [], joined: []};
|
|
28237
|
+
}
|
|
28238
|
+
|
|
28239
|
+
// Returns an SVG object, or null if the label is not rendered.
|
|
28240
|
+
// knots: the feature's coordinates, in the units the label is drawn in
|
|
28241
|
+
// opts:
|
|
28242
|
+
// report: optional collector, see initPathLabelReport()
|
|
28243
|
+
// id: feature id, used only in reports
|
|
28244
|
+
// keepOverflow: render a label that doesn't fit, marked with
|
|
28245
|
+
// LABEL_OVERFLOW_CLASS, instead of dropping it. The editor sets this,
|
|
28246
|
+
// because hiding a broken label makes it unfindable and so unfixable.
|
|
28247
|
+
function renderPathLabel(rec, knots, opts) {
|
|
28248
|
+
var d = getLabelPathData(knots);
|
|
28249
|
+
var report = opts && opts.report;
|
|
28250
|
+
var id = opts && opts.id;
|
|
28251
|
+
var state, text, textPath, o, cls;
|
|
28252
|
+
if (!d) return null;
|
|
28253
|
+
state = getLabelFitState(rec, getCurveLength(knots));
|
|
28254
|
+
if (state == 'overflow') {
|
|
28255
|
+
if (!(opts && opts.keepOverflow)) {
|
|
28256
|
+
addToReport(report, 'dropped', id);
|
|
28257
|
+
return null;
|
|
28258
|
+
}
|
|
28259
|
+
cls = LABEL_OVERFLOW_CLASS;
|
|
28260
|
+
}
|
|
28261
|
+
text = toLabelString(rec['label-text']);
|
|
28262
|
+
if (labelNewlineRxp.test(text)) {
|
|
28263
|
+
// a <tspan> inside a <textPath> advances along the path instead of
|
|
28264
|
+
// stacking below it, so the lines are joined rather than dropping
|
|
28265
|
+
// everything after the first one
|
|
28266
|
+
text = text.split(labelNewlineRxp).join(' ');
|
|
28267
|
+
addToReport(report, 'joined', id);
|
|
27285
28268
|
}
|
|
28269
|
+
textPath = {
|
|
28270
|
+
tag: 'textPath',
|
|
28271
|
+
value: text,
|
|
28272
|
+
properties: {
|
|
28273
|
+
startOffset: rec['label-start-offset'] || getDefaultStartOffset(rec)
|
|
28274
|
+
}
|
|
28275
|
+
};
|
|
28276
|
+
// the caller replaces this with a reference to a <defs> entry
|
|
28277
|
+
textPath.properties[LABEL_PATH_PROPERTY] = d;
|
|
28278
|
+
if (rec['label-side']) {
|
|
28279
|
+
textPath.properties.side = rec['label-side'];
|
|
28280
|
+
}
|
|
28281
|
+
// dx and dy shift the text along and across the path respectively. They go
|
|
28282
|
+
// on the <textPath>, not the <text>: x and y on a <text> are ignored once it
|
|
28283
|
+
// has a <textPath> child, so renderLabel()'s trick of using x/y for offsets
|
|
28284
|
+
// does not carry over.
|
|
28285
|
+
if (rec.dx) textPath.properties.dx = rec.dx;
|
|
28286
|
+
if (rec.dy) textPath.properties.dy = rec.dy;
|
|
28287
|
+
o = {tag: 'text', properties: {}, children: [textPath]};
|
|
28288
|
+
applyStyleAttributes(o, 'label', rec);
|
|
28289
|
+
if (cls) {
|
|
28290
|
+
o.properties.class = o.properties.class ? o.properties.class + ' ' + cls : cls;
|
|
28291
|
+
}
|
|
28292
|
+
return o;
|
|
28293
|
+
}
|
|
27286
28294
|
|
|
27287
|
-
|
|
27288
|
-
|
|
28295
|
+
// Maps a label's knots into the coordinate space *inside* its rendered symbol
|
|
28296
|
+
// group, which is translated to the label's first knot and scaled by
|
|
28297
|
+
// @symbolScale. @transform maps CRS coordinates to screen pixels.
|
|
28298
|
+
//
|
|
28299
|
+
// The result depends only on the ratio between the two scales, which is what
|
|
28300
|
+
// makes the framed case cheap: with a frame defined, zooming multiplies the
|
|
28301
|
+
// view scale and the symbol scale by the same factor, so these coordinates --
|
|
28302
|
+
// and the path built from them -- do not change. Panning cancels too, because
|
|
28303
|
+
// the coordinates are relative to the first knot. With no frame the symbol
|
|
28304
|
+
// scale is fixed at 1, so zooming does change them and the path has to be
|
|
28305
|
+
// rebuilt.
|
|
28306
|
+
function getLabelPathCoords(knots, transform, symbolScale) {
|
|
28307
|
+
var origin = transform.transform(knots[0][0], knots[0][1]);
|
|
28308
|
+
var coords = [];
|
|
28309
|
+
var p, i;
|
|
28310
|
+
for (i = 0; i < knots.length; i++) {
|
|
28311
|
+
p = transform.transform(knots[i][0], knots[i][1]);
|
|
28312
|
+
coords.push([(p[0] - origin[0]) / symbolScale, (p[1] - origin[1]) / symbolScale]);
|
|
28313
|
+
}
|
|
28314
|
+
return coords;
|
|
28315
|
+
}
|
|
28316
|
+
|
|
28317
|
+
// Replaces a rendered path label's marker property with a reference to a
|
|
28318
|
+
// definition, and returns the path data the caller now has to define. Kept next
|
|
28319
|
+
// to the renderer so that the element's shape is only assumed in one place.
|
|
28320
|
+
function referenceLabelPath(el, pathId) {
|
|
28321
|
+
var props = el.children[0].properties;
|
|
28322
|
+
var d = props[LABEL_PATH_PROPERTY];
|
|
28323
|
+
delete props[LABEL_PATH_PROPERTY];
|
|
28324
|
+
props.href = '#' + pathId;
|
|
28325
|
+
return d;
|
|
28326
|
+
}
|
|
28327
|
+
|
|
28328
|
+
// startOffset positions the text's anchor along the path, so the default that
|
|
28329
|
+
// leaves text where the anchor implies depends on text-anchor. 'middle' is the
|
|
28330
|
+
// layer-level default set by getEmptyLayerForSVG().
|
|
28331
|
+
function getDefaultStartOffset(rec) {
|
|
28332
|
+
var anchor = rec['text-anchor'] || 'middle';
|
|
28333
|
+
if (anchor == 'start') return '0%';
|
|
28334
|
+
if (anchor == 'end') return '100%';
|
|
28335
|
+
return '50%';
|
|
28336
|
+
}
|
|
28337
|
+
|
|
28338
|
+
function addToReport(report, key, id) {
|
|
28339
|
+
if (report && report[key]) report[key].push(id);
|
|
27289
28340
|
}
|
|
27290
28341
|
|
|
28342
|
+
function reportPathLabels(report, lyr) {
|
|
28343
|
+
var name = lyr && lyr.name || '[unnamed]';
|
|
28344
|
+
if (!report) return;
|
|
28345
|
+
if (report.dropped.length > 0) {
|
|
28346
|
+
// A drop is reported rather than silent: an absent label is otherwise
|
|
28347
|
+
// indistinguishable from a bug.
|
|
28348
|
+
message(utils.format('Dropped %,d path label%s from layer "%s" because the text is longer than the path: %s.',
|
|
28349
|
+
report.dropped.length, utils.pluralSuffix(report.dropped.length), name,
|
|
28350
|
+
formatIds(report.dropped)));
|
|
28351
|
+
}
|
|
28352
|
+
if (report.joined.length > 0) {
|
|
28353
|
+
warn(utils.format('%,d path label%s in layer "%s" contain%s a line break; multi-line text on a path is not supported, so the lines were joined: %s.',
|
|
28354
|
+
report.joined.length, utils.pluralSuffix(report.joined.length), name,
|
|
28355
|
+
report.joined.length == 1 ? 's' : '', formatIds(report.joined)));
|
|
28356
|
+
}
|
|
28357
|
+
}
|
|
28358
|
+
|
|
28359
|
+
function formatIds(ids) {
|
|
28360
|
+
var extra = ids.length - MAX_REPORTED_IDS;
|
|
28361
|
+
var listed = extra > 0 ? ids.slice(0, MAX_REPORTED_IDS) : ids;
|
|
28362
|
+
var str = (listed.length == 1 ? 'feature ' : 'features ') + listed.join(', ');
|
|
28363
|
+
return extra > 0 ? str + ' and ' + extra + ' more' : str;
|
|
28364
|
+
}
|
|
28365
|
+
|
|
28366
|
+
var SvgLabelPaths = /*#__PURE__*/Object.freeze({
|
|
28367
|
+
__proto__: null,
|
|
28368
|
+
LABEL_OVERFLOW_CLASS: LABEL_OVERFLOW_CLASS,
|
|
28369
|
+
LABEL_PATH_PROPERTY: LABEL_PATH_PROPERTY,
|
|
28370
|
+
featureIsPathLabel: featureIsPathLabel,
|
|
28371
|
+
getLabelPathCoords: getLabelPathCoords,
|
|
28372
|
+
getLabelPathData: getLabelPathData,
|
|
28373
|
+
initPathLabelReport: initPathLabelReport,
|
|
28374
|
+
referenceLabelPath: referenceLabelPath,
|
|
28375
|
+
renderPathLabel: renderPathLabel,
|
|
28376
|
+
reportPathLabels: reportPathLabels,
|
|
28377
|
+
shapeIsPathLabel: shapeIsPathLabel
|
|
28378
|
+
});
|
|
28379
|
+
|
|
27291
28380
|
function getSphereEffectParams() {
|
|
27292
28381
|
return {
|
|
27293
28382
|
cx: 0.5,
|
|
@@ -27334,6 +28423,9 @@
|
|
|
27334
28423
|
procNode(obj);
|
|
27335
28424
|
|
|
27336
28425
|
function procNode(obj) {
|
|
28426
|
+
if (obj.tag == 'textPath' && obj.properties && obj.properties[LABEL_PATH_PROPERTY]) {
|
|
28427
|
+
convertLabelPath(obj.properties, defs);
|
|
28428
|
+
}
|
|
27337
28429
|
if (obj.tag == 'path' && obj.properties['fill-pattern']) {
|
|
27338
28430
|
convertFillPattern(obj.properties, defs);
|
|
27339
28431
|
}
|
|
@@ -27350,6 +28442,28 @@
|
|
|
27350
28442
|
}
|
|
27351
28443
|
}
|
|
27352
28444
|
|
|
28445
|
+
// Moves a label's baseline into <defs>, where it is referenced but never
|
|
28446
|
+
// painted. It must not be left in the document body: a <path> with no stroke or
|
|
28447
|
+
// fill of its own picks up SVG's defaults -- no stroke and *black fill* -- and
|
|
28448
|
+
// renders as a filled blob instead of a line.
|
|
28449
|
+
//
|
|
28450
|
+
// The id is derived from the path data, so identical paths share one
|
|
28451
|
+
// definition and repeated exports of the same map produce identical output.
|
|
28452
|
+
function convertLabelPath(properties, defs) {
|
|
28453
|
+
var d = properties[LABEL_PATH_PROPERTY];
|
|
28454
|
+
var item = utils.find(defs, function(item) {return item.labelPath === d;});
|
|
28455
|
+
delete properties[LABEL_PATH_PROPERTY];
|
|
28456
|
+
if (!item) {
|
|
28457
|
+
item = {
|
|
28458
|
+
labelPath: d,
|
|
28459
|
+
id: 'label-path-' + sha1(d).substr(0, 12)
|
|
28460
|
+
};
|
|
28461
|
+
item.svg = '<path id="' + item.id + '" d="' + d + '"/>\n';
|
|
28462
|
+
defs.push(item);
|
|
28463
|
+
}
|
|
28464
|
+
properties.href = '#' + item.id;
|
|
28465
|
+
}
|
|
28466
|
+
|
|
27353
28467
|
function convertSvgImage(obj, defs) {
|
|
27354
28468
|
// Same-origin policy prevents embedding images in the web UI
|
|
27355
28469
|
var href = obj.properties.href;
|
|
@@ -27445,13 +28559,18 @@
|
|
|
27445
28559
|
|
|
27446
28560
|
function importGeoJSONFeatures(features, opts) {
|
|
27447
28561
|
opts = opts || {};
|
|
27448
|
-
return features.map(function(obj) {
|
|
28562
|
+
return features.map(function(obj, featureId) {
|
|
27449
28563
|
var geom = obj.type == 'Feature' ? obj.geometry : obj; // could be null
|
|
27450
28564
|
var geomType = geom && geom.type;
|
|
27451
28565
|
var msType = GeoJSON.translateGeoJSONType(geomType);
|
|
27452
28566
|
var d = obj.properties || {};
|
|
27453
28567
|
var svgObj = null;
|
|
27454
|
-
if (
|
|
28568
|
+
if (featureIsPathLabel(geom, d)) {
|
|
28569
|
+
// a label's knots are its geometry, so this is one label along a curve
|
|
28570
|
+
// rather than several labels at several points
|
|
28571
|
+
svgObj = renderPathLabel(d, geom.coordinates,
|
|
28572
|
+
{report: opts.path_label_report, id: featureId});
|
|
28573
|
+
} else if (geomType && geom.coordinates) {
|
|
27455
28574
|
svgObj = geojsonImporters[geomType](geom.coordinates, d);
|
|
27456
28575
|
}
|
|
27457
28576
|
if (!svgObj) {
|
|
@@ -27868,7 +28987,12 @@ ${svg}
|
|
|
27868
28987
|
var geojson = exportDatasetAsGeoJSON(d, opts);
|
|
27869
28988
|
var features = geojson.features || geojson.geometries || (geojson.type ? [geojson] : []);
|
|
27870
28989
|
warnIfIllustratorPathLimitExceeded(lyr, features);
|
|
27871
|
-
|
|
28990
|
+
// path labels that don't fit their path render as nothing, and are reported
|
|
28991
|
+
// rather than dropped silently
|
|
28992
|
+
var pathLabelReport = initPathLabelReport();
|
|
28993
|
+
var children = importGeoJSONFeatures(features,
|
|
28994
|
+
utils.defaults({path_label_report: pathLabelReport}, opts));
|
|
28995
|
+
reportPathLabels(pathLabelReport, lyr);
|
|
27872
28996
|
// Drop empty placeholder <g/> elements (features whose geometry was null in the
|
|
27873
28997
|
// source data, collapsed during simplification, or otherwise produced no
|
|
27874
28998
|
// visible output). Keep the layer's records in lockstep so that data-*
|
|
@@ -28044,14 +29168,25 @@ ${svg}
|
|
|
28044
29168
|
|
|
28045
29169
|
// add default text properties to layers with labels
|
|
28046
29170
|
if (layerHasLabels(lyr) || layerHasSvgSymbols(lyr) || layerHasFurniture(lyr)) {
|
|
28047
|
-
layerObj.properties
|
|
28048
|
-
layerObj.properties['font-size'] = '12';
|
|
28049
|
-
layerObj.properties['text-anchor'] = 'middle';
|
|
29171
|
+
utils.extend(layerObj.properties, getLabelTextDefaults());
|
|
28050
29172
|
}
|
|
28051
29173
|
|
|
28052
29174
|
return layerObj;
|
|
28053
29175
|
}
|
|
28054
29176
|
|
|
29177
|
+
// The text properties a label layer's container carries, which its labels
|
|
29178
|
+
// inherit instead of each holding a copy. Exported because a label rendered
|
|
29179
|
+
// outside any container -- the one the GUI draws while it is being typed, which
|
|
29180
|
+
// belongs to no layer until it is created -- has to be given the same ones, or
|
|
29181
|
+
// it changes size and alignment at the moment it becomes a feature.
|
|
29182
|
+
function getLabelTextDefaults() {
|
|
29183
|
+
return {
|
|
29184
|
+
'font-family': 'sans-serif',
|
|
29185
|
+
'font-size': String(DEFAULT_LABEL_FONT_SIZE),
|
|
29186
|
+
'text-anchor': 'middle'
|
|
29187
|
+
};
|
|
29188
|
+
}
|
|
29189
|
+
|
|
28055
29190
|
function layerHasSvgSymbols(lyr) {
|
|
28056
29191
|
return lyr.geometry_type == 'point' && lyr.data && (
|
|
28057
29192
|
lyr.data.fieldExists('svg-symbol') ||
|
|
@@ -28083,6 +29218,7 @@ ${svg}
|
|
|
28083
29218
|
featureHasLabel: featureHasLabel,
|
|
28084
29219
|
featureHasSvgSymbol: featureHasSvgSymbol,
|
|
28085
29220
|
getEmptyLayerForSVG: getEmptyLayerForSVG,
|
|
29221
|
+
getLabelTextDefaults: getLabelTextDefaults,
|
|
28086
29222
|
layerHasLabels: layerHasLabels,
|
|
28087
29223
|
layerHasSvgSymbols: layerHasSvgSymbols,
|
|
28088
29224
|
validateSvgDataFields: validateSvgDataFields
|
|
@@ -39079,6 +40215,30 @@ ${svg}
|
|
|
39079
40215
|
nameOpt2 = { // for -calc and -info
|
|
39080
40216
|
describe: 'name the output layer'
|
|
39081
40217
|
},
|
|
40218
|
+
// label style properties accepted by -add-label, so that a label can be
|
|
40219
|
+
// created and styled in one command. These are a subset of the
|
|
40220
|
+
// properties -style accepts -- the ones that apply to text.
|
|
40221
|
+
labelStyleOpts = {
|
|
40222
|
+
'font-family': {describe: 'label font, e.g. Georgia'},
|
|
40223
|
+
'font-size': {describe: 'label font size, e.g. 14'},
|
|
40224
|
+
'font-style': {describe: 'normal or italic'},
|
|
40225
|
+
'font-weight': {describe: 'normal, bold or a numeric weight'},
|
|
40226
|
+
'font-stretch': {describe: 'e.g. condensed'},
|
|
40227
|
+
'letter-spacing': {describe: 'extra space between letters'},
|
|
40228
|
+
'line-height': {describe: 'spacing between lines of a multi-line label'},
|
|
40229
|
+
'text-anchor': {describe: 'start, middle or end'},
|
|
40230
|
+
'label-align': {describe: 'how the lines of a multi-line label align: left, center or right'},
|
|
40231
|
+
'dominant-baseline': {describe: 'vertical alignment, e.g. central'},
|
|
40232
|
+
'label-pos': {describe: 'position relative to the anchor: n s e w ne se nw sw c'},
|
|
40233
|
+
'label-side': {describe: 'which side of its path the text sits on: left or right'},
|
|
40234
|
+
'label-start-offset': {describe: 'where text starts along its path, e.g. 50%'},
|
|
40235
|
+
dx: {describe: 'horizontal offset from the anchor'},
|
|
40236
|
+
dy: {describe: 'vertical offset from the anchor'},
|
|
40237
|
+
fill: {describe: 'text color'},
|
|
40238
|
+
opacity: {describe: 'text opacity'},
|
|
40239
|
+
css: {describe: 'inline css style'},
|
|
40240
|
+
class: {describe: 'name of CSS class or classes (space-separated)'}
|
|
40241
|
+
},
|
|
39082
40242
|
noReplaceOpt2 = { // for -calc and -info
|
|
39083
40243
|
alias: '+',
|
|
39084
40244
|
type: 'flag',
|
|
@@ -40352,6 +41512,10 @@ ${svg}
|
|
|
40352
41512
|
describe: 'size of grid cells in degrees (default is 10)',
|
|
40353
41513
|
type: 'number'
|
|
40354
41514
|
})
|
|
41515
|
+
.option('base', {
|
|
41516
|
+
describe: 'longitude to align meridians to (default is 0)',
|
|
41517
|
+
type: 'number'
|
|
41518
|
+
})
|
|
40355
41519
|
.option('polygon', {
|
|
40356
41520
|
describe: 'create a polygon to match the outline of the graticule',
|
|
40357
41521
|
type: 'flag'
|
|
@@ -41084,6 +42248,9 @@ ${svg}
|
|
|
41084
42248
|
.option('icon-color', {
|
|
41085
42249
|
describe: 'point icon color (defaults to fill color, then black)'
|
|
41086
42250
|
})
|
|
42251
|
+
.option('icon-opacity', {
|
|
42252
|
+
describe: 'point icon opacity, 0-1 (defaults to the label\'s opacity)'
|
|
42253
|
+
})
|
|
41087
42254
|
.option('label-text', {
|
|
41088
42255
|
describe: 'label text (set this to export points as labels)'
|
|
41089
42256
|
})
|
|
@@ -41093,6 +42260,9 @@ ${svg}
|
|
|
41093
42260
|
.option('text-anchor', {
|
|
41094
42261
|
describe: 'label alignment; one of: start, end, middle (default)'
|
|
41095
42262
|
})
|
|
42263
|
+
.option('label-align', {
|
|
42264
|
+
describe: 'alignment of the lines of multi-line labels; left, center or right'
|
|
42265
|
+
})
|
|
41096
42266
|
.option('dx', {
|
|
41097
42267
|
describe: 'x offset of labels (default is 0)'
|
|
41098
42268
|
})
|
|
@@ -41120,6 +42290,15 @@ ${svg}
|
|
|
41120
42290
|
.option('line-height', {
|
|
41121
42291
|
describe: 'line spacing of multi-line labels (default is 1.1em)'
|
|
41122
42292
|
})
|
|
42293
|
+
.option('dominant-baseline', {
|
|
42294
|
+
describe: 'vertical alignment of labels (e.g. central)'
|
|
42295
|
+
})
|
|
42296
|
+
.option('label-side', {
|
|
42297
|
+
describe: 'which side of its path a label sits on: left or right'
|
|
42298
|
+
})
|
|
42299
|
+
.option('label-start-offset', {
|
|
42300
|
+
describe: 'where label text starts along its path, e.g. 50%'
|
|
42301
|
+
})
|
|
41123
42302
|
.option('target', targetOpt);
|
|
41124
42303
|
|
|
41125
42304
|
parser.command('repel')
|
|
@@ -41357,6 +42536,64 @@ ${svg}
|
|
|
41357
42536
|
.option('target', targetOpt)
|
|
41358
42537
|
.option('no-replace', noReplaceOpt);
|
|
41359
42538
|
|
|
42539
|
+
// used by GUI
|
|
42540
|
+
parser.command('add-label')
|
|
42541
|
+
// .describe('add a map label to a point layer')
|
|
42542
|
+
.option('coordinates', {
|
|
42543
|
+
describe: 'x,y of the label anchor, or x,y,x,y,... of a curve\'s knots'
|
|
42544
|
+
})
|
|
42545
|
+
.option('text', {
|
|
42546
|
+
describe: 'label text'
|
|
42547
|
+
})
|
|
42548
|
+
// label style properties, so that creating and styling a label is one
|
|
42549
|
+
// command; anything not listed here can be set with a following -style
|
|
42550
|
+
.options(labelStyleOpts)
|
|
42551
|
+
// a label can carry a symbol at its anchor, so these come along too even
|
|
42552
|
+
// though they are not text properties
|
|
42553
|
+
.option('icon', {
|
|
42554
|
+
describe: 'symbol drawn at the label anchor: circle, square, ring, star'
|
|
42555
|
+
})
|
|
42556
|
+
.option('icon-size', {
|
|
42557
|
+
describe: 'size of the anchor symbol in px'
|
|
42558
|
+
})
|
|
42559
|
+
.option('icon-color', {
|
|
42560
|
+
describe: 'color of the anchor symbol (defaults to the text color)'
|
|
42561
|
+
})
|
|
42562
|
+
.option('icon-opacity', {
|
|
42563
|
+
describe: 'opacity of the anchor symbol, 0-1'
|
|
42564
|
+
})
|
|
42565
|
+
.option('properties', {
|
|
42566
|
+
describe: 'other attributes, as a JSON object'
|
|
42567
|
+
})
|
|
42568
|
+
.option('name', nameOpt)
|
|
42569
|
+
.option('target', targetOpt)
|
|
42570
|
+
.option('no-replace', noReplaceOpt);
|
|
42571
|
+
|
|
42572
|
+
// used by GUI
|
|
42573
|
+
parser.command('add-layer')
|
|
42574
|
+
// .describe('create an empty layer, to add shapes or labels to')
|
|
42575
|
+
.option('geometry-type', {
|
|
42576
|
+
describe: 'point, polygon or polyline'
|
|
42577
|
+
})
|
|
42578
|
+
.option('name', {
|
|
42579
|
+
describe: 'name of the new layer'
|
|
42580
|
+
})
|
|
42581
|
+
// only the CRS is taken from the target, so that a shape drawn at a
|
|
42582
|
+
// projected coordinate is not read as lat-long
|
|
42583
|
+
.option('target', targetOpt);
|
|
42584
|
+
|
|
42585
|
+
// used by GUI
|
|
42586
|
+
parser.command('update-label')
|
|
42587
|
+
// .describe('move the anchor or curve knots of an existing label')
|
|
42588
|
+
.option('ids', {
|
|
42589
|
+
describe: 'feature id of the label to update',
|
|
42590
|
+
type: 'numbers'
|
|
42591
|
+
})
|
|
42592
|
+
.option('coordinates', {
|
|
42593
|
+
describe: 'x,y of the label anchor, or x,y,x,y,... of a curve\'s knots'
|
|
42594
|
+
})
|
|
42595
|
+
.option('target', targetOpt);
|
|
42596
|
+
|
|
41360
42597
|
parser.command('alpha-shapes')
|
|
41361
42598
|
// .describe('convert points to alpha shapes (aka concave hulls)')
|
|
41362
42599
|
.option('interval', {
|
|
@@ -48301,6 +49538,377 @@ ${svg}
|
|
|
48301
49538
|
});
|
|
48302
49539
|
}
|
|
48303
49540
|
|
|
49541
|
+
// Geometry concerns shared by the commands that write a label's knots,
|
|
49542
|
+
// -add-label and -update-label. A label's knots are its geometry, so both
|
|
49543
|
+
// commands accept coordinates in the same forms and answer the same questions
|
|
49544
|
+
// about them. See docs/development/label-tool-design.md.
|
|
49545
|
+
|
|
49546
|
+
// Accepts "x,y,x,y,..." or a JSON array, either flat or as an array of pairs.
|
|
49547
|
+
function parseLabelCoords(arg) {
|
|
49548
|
+
var arr, coords;
|
|
49549
|
+
if (arg === undefined || arg === null || arg === '') {
|
|
49550
|
+
stop$1('Missing required coordinates parameter');
|
|
49551
|
+
}
|
|
49552
|
+
arr = utils.isString(arg) ? parseCoordString(arg) : arg;
|
|
49553
|
+
if (!Array.isArray(arr) || arr.length === 0) {
|
|
49554
|
+
stop$1('Unable to parse coordinates:', arg);
|
|
49555
|
+
}
|
|
49556
|
+
coords = Array.isArray(arr[0]) ? arr : transposeCoords$1(arr, arg);
|
|
49557
|
+
coords.forEach(function(p) {
|
|
49558
|
+
if (!Array.isArray(p) || p.length != 2 ||
|
|
49559
|
+
!utils.isFiniteNumber(p[0]) || !utils.isFiniteNumber(p[1])) {
|
|
49560
|
+
stop$1('Invalid coordinate pair:', JSON.stringify(p));
|
|
49561
|
+
}
|
|
49562
|
+
});
|
|
49563
|
+
return coords;
|
|
49564
|
+
}
|
|
49565
|
+
|
|
49566
|
+
function parseJsonArg(arg, name) {
|
|
49567
|
+
if (!utils.isString(arg)) return arg;
|
|
49568
|
+
try {
|
|
49569
|
+
return JSON.parse(arg);
|
|
49570
|
+
} catch(e) {
|
|
49571
|
+
stop$1('Unable to parse', name + ':', arg);
|
|
49572
|
+
}
|
|
49573
|
+
}
|
|
49574
|
+
|
|
49575
|
+
// A curve is fitted in the coordinate space it is stored in, so a fit done in
|
|
49576
|
+
// degrees bows incorrectly away from the equator. A modest curve in a small
|
|
49577
|
+
// area is unaffected, so this warns rather than failing.
|
|
49578
|
+
function warnIfCurveIsUnprojected(dataset, knotCount) {
|
|
49579
|
+
var crs;
|
|
49580
|
+
if (knotCount < 2) return; // no curve to distort
|
|
49581
|
+
if (!dataset) return;
|
|
49582
|
+
crs = getDatasetCRS(dataset);
|
|
49583
|
+
if (crs && crs.is_latlong) {
|
|
49584
|
+
warn('Fitting a label curve to unprojected coordinates. Consider using -proj first.');
|
|
49585
|
+
}
|
|
49586
|
+
}
|
|
49587
|
+
|
|
49588
|
+
function parseCoordString(str) {
|
|
49589
|
+
var s = String(str).trim();
|
|
49590
|
+
if (s.startsWith('[')) return parseJsonArg(s, 'coordinates');
|
|
49591
|
+
return s.split(/[,\s]+/).map(function(part) {
|
|
49592
|
+
var n = Number(part);
|
|
49593
|
+
if (part === '' || isNaN(n)) {
|
|
49594
|
+
stop$1('Unable to parse coordinates:', str);
|
|
49595
|
+
}
|
|
49596
|
+
return n;
|
|
49597
|
+
});
|
|
49598
|
+
}
|
|
49599
|
+
|
|
49600
|
+
function transposeCoords$1(arr, srcArg) {
|
|
49601
|
+
var coords = [], i;
|
|
49602
|
+
if (arr.length % 2 !== 0) {
|
|
49603
|
+
stop$1('Expected an even number of coordinates; received', arr.length,
|
|
49604
|
+
'in:', srcArg);
|
|
49605
|
+
}
|
|
49606
|
+
for (i = 0; i < arr.length; i += 2) {
|
|
49607
|
+
coords.push([arr[i], arr[i + 1]]);
|
|
49608
|
+
}
|
|
49609
|
+
return coords;
|
|
49610
|
+
}
|
|
49611
|
+
|
|
49612
|
+
cmd.addLabel = addLabel;
|
|
49613
|
+
|
|
49614
|
+
// Adds a map label to a point layer. One coordinate pair makes an anchored
|
|
49615
|
+
// label; several make a path-aligned label whose points are the knots its
|
|
49616
|
+
// curve is fitted through. See docs/development/label-tool-design.md.
|
|
49617
|
+
//
|
|
49618
|
+
// Options consumed directly by this command, rather than passed through as
|
|
49619
|
+
// label properties.
|
|
49620
|
+
var RESERVED_OPTIONS = {
|
|
49621
|
+
coordinates: true,
|
|
49622
|
+
name: true,
|
|
49623
|
+
no_replace: true,
|
|
49624
|
+
properties: true,
|
|
49625
|
+
target: true,
|
|
49626
|
+
text: true
|
|
49627
|
+
};
|
|
49628
|
+
|
|
49629
|
+
function addLabel(targetLayers, targetDataset, opts) {
|
|
49630
|
+
var targetLyr, coords, feature, dataset, outputLyr, merged;
|
|
49631
|
+
if (targetLayers.length > 1) {
|
|
49632
|
+
stop$1('Command expects a single target layer');
|
|
49633
|
+
}
|
|
49634
|
+
targetLyr = targetLayers[0]; // may be undefined
|
|
49635
|
+
if (targetLyr && !opts.no_replace) {
|
|
49636
|
+
requirePointTarget(targetLyr);
|
|
49637
|
+
}
|
|
49638
|
+
coords = parseLabelCoords(opts.coordinates);
|
|
49639
|
+
feature = toLabelFeature(opts, coords);
|
|
49640
|
+
matchTargetFieldTypes(feature.properties, targetLyr);
|
|
49641
|
+
warnIfCurveIsUnprojected(targetDataset, coords.length);
|
|
49642
|
+
dataset = importGeoJSON(feature);
|
|
49643
|
+
outputLyr = mergeDatasetsIntoDataset(targetDataset, [dataset])[0];
|
|
49644
|
+
if (opts.no_replace || !targetLyr) {
|
|
49645
|
+
setOutputLayerName(outputLyr, targetLyr && targetLyr.name, 'labels', opts);
|
|
49646
|
+
return [outputLyr];
|
|
49647
|
+
}
|
|
49648
|
+
// verbose: false silences "Fields [...] are missing from one or more layers".
|
|
49649
|
+
// Labels differ in which style properties they carry -- one has an icon, the
|
|
49650
|
+
// next a css rule -- so a one-label layer almost never has the same fields as
|
|
49651
|
+
// the layer it is joining.
|
|
49652
|
+
//
|
|
49653
|
+
// force stays on for the same reason.
|
|
49654
|
+
merged = cmd.mergeLayers([targetLyr, outputLyr], {force: true, verbose: false});
|
|
49655
|
+
// The target's name is restored because mergeLayers() drops empty layers
|
|
49656
|
+
// before merging: adding the first label to an empty layer hands back the
|
|
49657
|
+
// one-label layer on its own, which has no name because the name was on the
|
|
49658
|
+
// layer that was dropped. A layer called 'labels' would lose that name on
|
|
49659
|
+
// its first label, and the label tool creates exactly such a layer when it
|
|
49660
|
+
// opens with nothing loaded.
|
|
49661
|
+
merged[0].name = targetLyr.name;
|
|
49662
|
+
return merged;
|
|
49663
|
+
}
|
|
49664
|
+
|
|
49665
|
+
// Makes the new label's values match the types the target layer already holds,
|
|
49666
|
+
// so that adding a label cannot be the thing that breaks a layer's schema.
|
|
49667
|
+
//
|
|
49668
|
+
// Only string and number are worth reconciling. Anything else in a style field
|
|
49669
|
+
// is odd enough that quietly rewriting it would hide a real problem.
|
|
49670
|
+
function matchTargetFieldTypes(d, targetLyr) {
|
|
49671
|
+
var records = targetLyr && targetLyr.data ? targetLyr.data.getRecords() : null;
|
|
49672
|
+
if (!records || records.length === 0) return;
|
|
49673
|
+
Object.keys(d).forEach(function(key) {
|
|
49674
|
+
var type = getColumnType(key, records);
|
|
49675
|
+
var val = d[key];
|
|
49676
|
+
if (!type || type === typeof val) return;
|
|
49677
|
+
if (type == 'string' && utils.isNumber(val)) {
|
|
49678
|
+
d[key] = String(val);
|
|
49679
|
+
} else if (type == 'number' && utils.isString(val)) {
|
|
49680
|
+
if (isFiniteString(val)) {
|
|
49681
|
+
d[key] = Number(val);
|
|
49682
|
+
} else {
|
|
49683
|
+
stringifyColumn(key, records);
|
|
49684
|
+
}
|
|
49685
|
+
}
|
|
49686
|
+
});
|
|
49687
|
+
}
|
|
49688
|
+
|
|
49689
|
+
// Widens a column of numbers to strings, for a value that cannot be a number:
|
|
49690
|
+
// '0.45em' is a length with units, which is what a label position east of its
|
|
49691
|
+
// anchor expands to.
|
|
49692
|
+
//
|
|
49693
|
+
// Every number has a faithful string form, and these values are written out as
|
|
49694
|
+
// SVG attributes, so restating 0 as '0' changes nothing that is drawn or
|
|
49695
|
+
// exported. Narrowing the other way is what is not always possible, which is
|
|
49696
|
+
// why this is the direction the column moves.
|
|
49697
|
+
//
|
|
49698
|
+
// Reached by a layer whose dx column holds numbers -- written before label
|
|
49699
|
+
// positions stored dx as a string -- which would otherwise refuse every label
|
|
49700
|
+
// offered an em offset.
|
|
49701
|
+
function stringifyColumn(key, records) {
|
|
49702
|
+
for (var i = 0; i < records.length; i++) {
|
|
49703
|
+
if (records[i] && utils.isNumber(records[i][key])) {
|
|
49704
|
+
records[i][key] = String(records[i][key]);
|
|
49705
|
+
}
|
|
49706
|
+
}
|
|
49707
|
+
}
|
|
49708
|
+
|
|
49709
|
+
function isFiniteString(str) {
|
|
49710
|
+
return str.trim() !== '' && utils.isFiniteNumber(Number(str));
|
|
49711
|
+
}
|
|
49712
|
+
|
|
49713
|
+
// Both kinds of label are point features, so a non-point target can never
|
|
49714
|
+
// receive one. Refusing is better than silently making a new layer, because
|
|
49715
|
+
// the CLI user named a target explicitly.
|
|
49716
|
+
function requirePointTarget(lyr) {
|
|
49717
|
+
if (lyr.geometry_type && lyr.geometry_type != 'point') {
|
|
49718
|
+
stop$1('Labels can only be added to a point layer; target "' +
|
|
49719
|
+
(lyr.name || '[unnamed]') + '" contains ' + lyr.geometry_type + 's. ' +
|
|
49720
|
+
'Use no-replace to add labels as a new layer.');
|
|
49721
|
+
}
|
|
49722
|
+
}
|
|
49723
|
+
|
|
49724
|
+
function toLabelFeature(opts, coords) {
|
|
49725
|
+
return {
|
|
49726
|
+
type: 'Feature',
|
|
49727
|
+
properties: getLabelProperties(opts, coords.length),
|
|
49728
|
+
geometry: coords.length == 1 ?
|
|
49729
|
+
{type: 'Point', coordinates: coords[0]} :
|
|
49730
|
+
{type: 'MultiPoint', coordinates: coords}
|
|
49731
|
+
};
|
|
49732
|
+
}
|
|
49733
|
+
|
|
49734
|
+
function getLabelProperties(opts, knotCount) {
|
|
49735
|
+
var d = {};
|
|
49736
|
+
if (opts.properties) {
|
|
49737
|
+
utils.extend(d, parseJsonArg(opts.properties, 'properties'));
|
|
49738
|
+
}
|
|
49739
|
+
// an explicit empty string is meaningful: it creates a label that the user
|
|
49740
|
+
// is about to type into, which is how the GUI starts an editing session
|
|
49741
|
+
d['label-text'] = 'text' in opts ? String(opts.text) : '';
|
|
49742
|
+
|
|
49743
|
+
// any option matching a -style property becomes a label property, so that
|
|
49744
|
+
// creating and styling a label is one command and one history entry
|
|
49745
|
+
Object.keys(opts).forEach(function(key) {
|
|
49746
|
+
var name = key.replace(/_/g, '-');
|
|
49747
|
+
var val;
|
|
49748
|
+
if (key in RESERVED_OPTIONS) return;
|
|
49749
|
+
if (!isSupportedSvgStyleProperty(name)) return;
|
|
49750
|
+
// Stored as the type -style would store it in. Copying the option string
|
|
49751
|
+
// through instead left icon-size=20 as "20" here and 20 from -style, so a
|
|
49752
|
+
// layer holding both kinds of label had a column with two types in it --
|
|
49753
|
+
// which the merge below then refused, making the next label impossible to
|
|
49754
|
+
// add.
|
|
49755
|
+
val = parseStyleLiteral(name, opts[key]);
|
|
49756
|
+
if (val === undefined) {
|
|
49757
|
+
stop$1('Unexpected value for', name + ':', opts[key]);
|
|
49758
|
+
}
|
|
49759
|
+
d[name] = val;
|
|
49760
|
+
// label-pos is the only one of the four position properties stored: the
|
|
49761
|
+
// offsets and justification it stands for are resolved when the label is
|
|
49762
|
+
// drawn. A dx= or dy= given as well is kept and wins there, so the two can
|
|
49763
|
+
// be combined to nudge a label off a standard position.
|
|
49764
|
+
if (name == 'label-pos' && !parseLabelPosition(val)) {
|
|
49765
|
+
stop$1('Unexpected value for label-pos:', opts[key]);
|
|
49766
|
+
}
|
|
49767
|
+
});
|
|
49768
|
+
|
|
49769
|
+
if (d['label-pos'] && knotCount > 1) {
|
|
49770
|
+
// A path label's text runs along its curve from a start offset, so it has
|
|
49771
|
+
// no position around an anchor to take. Warned about rather than rejected,
|
|
49772
|
+
// so that a script can style a mixed layer in one pass.
|
|
49773
|
+
warn('Ignoring label-pos on a label with', knotCount, 'points.',
|
|
49774
|
+
'Use label-start-offset= and text-anchor= to place text along a path.');
|
|
49775
|
+
delete d['label-pos'];
|
|
49776
|
+
}
|
|
49777
|
+
return d;
|
|
49778
|
+
}
|
|
49779
|
+
|
|
49780
|
+
cmd.addLayer = addLayer;
|
|
49781
|
+
|
|
49782
|
+
var GEOMETRY_TYPES = ['point', 'polygon', 'polyline'];
|
|
49783
|
+
|
|
49784
|
+
// Creates an empty layer, for features that are about to be added to it: by the
|
|
49785
|
+
// GUI's drawing and label tools, or by -add-shape and -add-label in a script.
|
|
49786
|
+
//
|
|
49787
|
+
// The layer goes into a dataset of its own rather than joining the target's, so
|
|
49788
|
+
// that shapes drawn into it have their own topology and editing them cannot
|
|
49789
|
+
// disturb the arcs of the layer it was created beside. What it does take from
|
|
49790
|
+
// the target is the CRS, without which a shape drawn at a projected coordinate
|
|
49791
|
+
// would be read as lat-long.
|
|
49792
|
+
//
|
|
49793
|
+
// targetDataset: the current target, or undefined if there is none
|
|
49794
|
+
// opts:
|
|
49795
|
+
// geometry_type: 'point', 'polygon' or 'polyline'
|
|
49796
|
+
// name: name for the new layer
|
|
49797
|
+
// Returns a new dataset, for the caller to add to the catalog.
|
|
49798
|
+
function addLayer(targetDataset, opts) {
|
|
49799
|
+
var type = opts.geometry_type;
|
|
49800
|
+
var dataset;
|
|
49801
|
+
if (!type) {
|
|
49802
|
+
stop$1('Missing required geometry-type parameter:', GEOMETRY_TYPES.join('|'));
|
|
49803
|
+
}
|
|
49804
|
+
if (!GEOMETRY_TYPES.includes(type)) {
|
|
49805
|
+
stop$1('Unsupported geometry type:', type + '.', 'Expected',
|
|
49806
|
+
GEOMETRY_TYPES.join('|'));
|
|
49807
|
+
}
|
|
49808
|
+
dataset = {
|
|
49809
|
+
layers: [{
|
|
49810
|
+
name: opts.name || undefined,
|
|
49811
|
+
geometry_type: type,
|
|
49812
|
+
shapes: []
|
|
49813
|
+
}],
|
|
49814
|
+
info: {}
|
|
49815
|
+
};
|
|
49816
|
+
// A path layer needs somewhere to put its arcs. A point layer holds
|
|
49817
|
+
// coordinates in its shapes and never has an ArcCollection.
|
|
49818
|
+
if (type != 'point') {
|
|
49819
|
+
dataset.arcs = new ArcCollection();
|
|
49820
|
+
}
|
|
49821
|
+
if (targetDataset) {
|
|
49822
|
+
setDatasetCrsInfo(dataset, getDatasetCrsInfo(targetDataset));
|
|
49823
|
+
}
|
|
49824
|
+
return dataset;
|
|
49825
|
+
}
|
|
49826
|
+
|
|
49827
|
+
var OPERATION = 'update-label';
|
|
49828
|
+
|
|
49829
|
+
cmd.updateLabel = updateLabel;
|
|
49830
|
+
|
|
49831
|
+
// Rewrites the knots of a label that already exists.
|
|
49832
|
+
//
|
|
49833
|
+
// -add-label creates and -style sets properties, so neither can move a label:
|
|
49834
|
+
// a label's knots are its geometry, not an attribute. This is the command the
|
|
49835
|
+
// GUI emits when a knot or an anchor is dragged, so that an interactive move is
|
|
49836
|
+
// an ordinary command with undo and session history rather than a direct
|
|
49837
|
+
// mutation with hand-rolled undo.
|
|
49838
|
+
//
|
|
49839
|
+
// See docs/development/label-tool-design.md.
|
|
49840
|
+
function updateLabel(targetLayers, dataset, opts) {
|
|
49841
|
+
var lyr, id, coords, rec;
|
|
49842
|
+
if (targetLayers.length > 1) {
|
|
49843
|
+
// One coordinate list describes one label, so there is no sensible reading
|
|
49844
|
+
// of this across layers.
|
|
49845
|
+
stop$1('Command expects a single target layer');
|
|
49846
|
+
}
|
|
49847
|
+
lyr = targetLayers[0];
|
|
49848
|
+
id = getTargetId(lyr, opts);
|
|
49849
|
+
coords = parseLabelCoords(opts.coordinates);
|
|
49850
|
+
rec = getLayerDataTable(lyr).getRecords()[id];
|
|
49851
|
+
if (!featureIsLabel(rec)) {
|
|
49852
|
+
// Moving an arbitrary point through a command named -update-label would be
|
|
49853
|
+
// a surprising way to succeed, and in the GUI it would mean the hit test
|
|
49854
|
+
// handed over the wrong feature.
|
|
49855
|
+
stop$1('Feature', id, 'in layer "' + layerName(lyr) + '" is not a label ' +
|
|
49856
|
+
'(it has no label-text property)');
|
|
49857
|
+
}
|
|
49858
|
+
warnIfCurveIsUnprojected(dataset, coords.length);
|
|
49859
|
+
// Undo works from what a command declares it is about to change, so an edit
|
|
49860
|
+
// that does not say so is an edit that cannot be undone -- the shapes array
|
|
49861
|
+
// keeps its identity here, leaving nothing for the transaction to notice
|
|
49862
|
+
// afterwards.
|
|
49863
|
+
noteLayerWillChange(lyr, {operation: OPERATION, unit: 'shapes'});
|
|
49864
|
+
// Point shapes hold their coordinates directly, so this is the whole edit.
|
|
49865
|
+
// The pairs are copied rather than aliased: the GUI drags a live copy of the
|
|
49866
|
+
// knots and would otherwise keep a handle on the layer's own arrays.
|
|
49867
|
+
lyr.shapes[id] = coords.map(function(p) { return [p[0], p[1]]; });
|
|
49868
|
+
markLayerChanged(lyr, {operation: OPERATION, unit: 'shapes'});
|
|
49869
|
+
// Nothing about the label's text is touched, and nothing needs to be: a text
|
|
49870
|
+
// measurement describes the *text*, and moving a knot changes the length of
|
|
49871
|
+
// the *path*. The fit check compares the two at render time, against the
|
|
49872
|
+
// curve as it then stands, so a move can turn a fitting label into an
|
|
49873
|
+
// overflowing one with no measurement going out of date.
|
|
49874
|
+
}
|
|
49875
|
+
|
|
49876
|
+
// Which feature to move. One coordinate list can only describe one label, so
|
|
49877
|
+
// this takes a single id rather than the ids= list that -style accepts.
|
|
49878
|
+
function getTargetId(lyr, opts) {
|
|
49879
|
+
var ids = opts.ids;
|
|
49880
|
+
var id;
|
|
49881
|
+
requirePointLayer(lyr);
|
|
49882
|
+
if (!ids || ids.length === 0) {
|
|
49883
|
+
stop$1('Missing required ids parameter (the feature id of the label to update)');
|
|
49884
|
+
}
|
|
49885
|
+
if (ids.length > 1) {
|
|
49886
|
+
stop$1('Command expects a single feature id; received', ids.join(','));
|
|
49887
|
+
}
|
|
49888
|
+
id = ids[0];
|
|
49889
|
+
if (id >= 0 === false || id >= getFeatureCount(lyr)) {
|
|
49890
|
+
stop$1('Layer "' + layerName(lyr) + '" has no feature with id', id);
|
|
49891
|
+
}
|
|
49892
|
+
if (!lyr.shapes || !lyr.shapes[id]) {
|
|
49893
|
+
stop$1('Feature', id, 'in layer "' + layerName(lyr) + '" has no geometry');
|
|
49894
|
+
}
|
|
49895
|
+
return id;
|
|
49896
|
+
}
|
|
49897
|
+
|
|
49898
|
+
function requirePointLayer(lyr) {
|
|
49899
|
+
if (!lyr || getFeatureCount(lyr) === 0) {
|
|
49900
|
+
stop$1('Missing a target layer containing labels');
|
|
49901
|
+
}
|
|
49902
|
+
if (lyr.geometry_type != 'point') {
|
|
49903
|
+
stop$1('Labels can only be updated in a point layer; target "' +
|
|
49904
|
+
layerName(lyr) + '" contains ' + (lyr.geometry_type || 'no geometry'));
|
|
49905
|
+
}
|
|
49906
|
+
}
|
|
49907
|
+
|
|
49908
|
+
function layerName(lyr) {
|
|
49909
|
+
return lyr && lyr.name || '[unnamed]';
|
|
49910
|
+
}
|
|
49911
|
+
|
|
48304
49912
|
cmd.addShape = addShape;
|
|
48305
49913
|
|
|
48306
49914
|
function addShape(targetLayers, targetDataset, opts) {
|
|
@@ -49340,7 +50948,7 @@ ${svg}
|
|
|
49340
50948
|
}
|
|
49341
50949
|
|
|
49342
50950
|
cmd.alphaShapes = function(pointLyr, targetDataset, opts) {
|
|
49343
|
-
requirePointLayer(pointLyr);
|
|
50951
|
+
requirePointLayer$1(pointLyr);
|
|
49344
50952
|
if (opts.interval > 0 === false) {
|
|
49345
50953
|
stop$1('Expected a non-negative interval parameter');
|
|
49346
50954
|
}
|
|
@@ -60725,7 +62333,7 @@ ${svg}
|
|
|
60725
62333
|
cmd.lines = function(lyr, dataset, opts) {
|
|
60726
62334
|
opts = opts || {};
|
|
60727
62335
|
if (opts.callouts) {
|
|
60728
|
-
requirePointLayer(lyr);
|
|
62336
|
+
requirePointLayer$1(lyr);
|
|
60729
62337
|
return pointsToCallouts(lyr, dataset, opts);
|
|
60730
62338
|
} else if (lyr.geometry_type == 'point') {
|
|
60731
62339
|
return pointsToLines(lyr, dataset, opts);
|
|
@@ -72614,17 +74222,19 @@ ${svg}
|
|
|
72614
74222
|
//
|
|
72615
74223
|
function createGraticule(P, outlined, opts) {
|
|
72616
74224
|
var interval = opts.interval || 10;
|
|
74225
|
+
var base = opts.base || 0;
|
|
72617
74226
|
if (Math.round(interval) != interval || interval > 0 === false) {
|
|
72618
74227
|
stop$1('Invalid interval:', interval);
|
|
72619
74228
|
}
|
|
74229
|
+
if (!utils.isFiniteNumber(base)) {
|
|
74230
|
+
stop$1('Invalid base:', opts.base);
|
|
74231
|
+
}
|
|
72620
74232
|
P.lam0 * 180 / Math.PI;
|
|
72621
74233
|
var precision = interval > 10 ? 1 : 0.5; // degrees between each vertex
|
|
72622
|
-
var xstep = interval;
|
|
72623
74234
|
var ystep = interval;
|
|
72624
74235
|
var xstepMajor = 90;
|
|
72625
|
-
var xn = Math.round(360 / xstep);
|
|
72626
74236
|
var yn = Math.round(180 / ystep) + 1;
|
|
72627
|
-
var xx =
|
|
74237
|
+
var xx = getGraticuleMeridianLongitudes(interval, base);
|
|
72628
74238
|
var yy = utils.range(yn, -90, ystep);
|
|
72629
74239
|
var meridians = [];
|
|
72630
74240
|
var parallels = [];
|
|
@@ -72674,7 +74284,7 @@ ${svg}
|
|
|
72674
74284
|
|
|
72675
74285
|
function createMeridianPart(x, ymin, ymax) {
|
|
72676
74286
|
var coords = densifyPathByInterval([[x, ymin], [x, ymax]], precision);
|
|
72677
|
-
meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord$
|
|
74287
|
+
meridians.push(graticuleFeature(coords, {type: 'meridian', value: roundCoord$1(x)}));
|
|
72678
74288
|
}
|
|
72679
74289
|
|
|
72680
74290
|
function createParallel(y) {
|
|
@@ -72690,8 +74300,25 @@ ${svg}
|
|
|
72690
74300
|
}
|
|
72691
74301
|
}
|
|
72692
74302
|
|
|
74303
|
+
// Longitudes of meridians at interval spacing, aligned so that base (mod
|
|
74304
|
+
// interval) is included. Values are wrapped to (-180, 180].
|
|
74305
|
+
function getGraticuleMeridianLongitudes(interval, base) {
|
|
74306
|
+
var offset = ((base % interval) + interval) % interval;
|
|
74307
|
+
var n = Math.round(360 / interval);
|
|
74308
|
+
var longitudes = [];
|
|
74309
|
+
for (var i = 0; i < n; i++) {
|
|
74310
|
+
longitudes.push(wrapMeridianLongitude(offset + i * interval));
|
|
74311
|
+
}
|
|
74312
|
+
return longitudes;
|
|
74313
|
+
}
|
|
74314
|
+
|
|
74315
|
+
function wrapMeridianLongitude(lon) {
|
|
74316
|
+
lon -= Math.floor((lon + 180) / 360) * 360; // [-180, 180)
|
|
74317
|
+
return lon === -180 ? 180 : lon;
|
|
74318
|
+
}
|
|
74319
|
+
|
|
72693
74320
|
// remove tiny offsets
|
|
72694
|
-
function roundCoord$
|
|
74321
|
+
function roundCoord$1(x) {
|
|
72695
74322
|
return +x.toFixed(3) || 0;
|
|
72696
74323
|
}
|
|
72697
74324
|
|
|
@@ -75330,7 +76957,7 @@ ${svg}
|
|
|
75330
76957
|
}
|
|
75331
76958
|
|
|
75332
76959
|
cmd.pointToGrid = function(targetLayers, targetDataset, opts) {
|
|
75333
|
-
targetLayers.forEach(requirePointLayer);
|
|
76960
|
+
targetLayers.forEach(requirePointLayer$1);
|
|
75334
76961
|
if (opts.interval > 0 === false) {
|
|
75335
76962
|
stop$1('Expected a non-negative interval parameter');
|
|
75336
76963
|
}
|
|
@@ -75731,7 +77358,7 @@ ${svg}
|
|
|
75731
77358
|
};
|
|
75732
77359
|
}
|
|
75733
77360
|
|
|
75734
|
-
var roundCoord
|
|
77361
|
+
var roundCoord = getRoundingFunction(0.01);
|
|
75735
77362
|
|
|
75736
77363
|
function getSymbolFillColor(d) {
|
|
75737
77364
|
return d.fill || 'magenta';
|
|
@@ -75870,8 +77497,8 @@ ${svg}
|
|
|
75870
77497
|
|
|
75871
77498
|
function roundCoordsForSVG(coords) {
|
|
75872
77499
|
forEachSymbolCoord(coords, function(p) {
|
|
75873
|
-
p[0] = roundCoord
|
|
75874
|
-
p[1] = roundCoord
|
|
77500
|
+
p[0] = roundCoord(p[0]);
|
|
77501
|
+
p[1] = roundCoord(p[1]);
|
|
75875
77502
|
});
|
|
75876
77503
|
}
|
|
75877
77504
|
|
|
@@ -78280,7 +79907,12 @@ ${svg}
|
|
|
78280
79907
|
}
|
|
78281
79908
|
|
|
78282
79909
|
cmd.svgStyle = function(lyr, dataset, opts) {
|
|
78283
|
-
var filterFn, table, fields, hasNewFields;
|
|
79910
|
+
var filterFn, table, fields, hasNewFields, optFields, clearedByPosition, fieldsBefore;
|
|
79911
|
+
|
|
79912
|
+
function hadField(field) {
|
|
79913
|
+
return field in fieldsBefore;
|
|
79914
|
+
}
|
|
79915
|
+
|
|
78284
79916
|
if (getFeatureCount(lyr) === 0) {
|
|
78285
79917
|
return;
|
|
78286
79918
|
}
|
|
@@ -78297,10 +79929,25 @@ ${svg}
|
|
|
78297
79929
|
lyr.data.getFields().filter(isSupportedSvgStyleProperty).forEach(lyr.data.deleteField, lyr.data);
|
|
78298
79930
|
}
|
|
78299
79931
|
table = getLayerDataTable(lyr);
|
|
78300
|
-
|
|
79932
|
+
optFields = getOptionFields(opts);
|
|
79933
|
+
fields = getStyleFields(optFields);
|
|
79934
|
+
// Which of dx/dy/text-anchor a label-pos in this command clears: not the ones
|
|
79935
|
+
// the same command also sets, so that `label-pos=n dx=3` keeps the nudge it
|
|
79936
|
+
// was given instead of clearing it a moment later.
|
|
79937
|
+
//
|
|
79938
|
+
// Clearing at all is what keeps the shorthand usable, since a value on the
|
|
79939
|
+
// record wins over the position: without it, setting a position on a label
|
|
79940
|
+
// that had been dragged would appear to do nothing.
|
|
79941
|
+
clearedByPosition = labelPositionDerivedFields.filter(function(field) {
|
|
79942
|
+
return optFields.indexOf(field) == -1;
|
|
79943
|
+
});
|
|
78301
79944
|
hasNewFields = fields.some(function(field) {
|
|
78302
79945
|
return !table.fieldExists(field);
|
|
78303
79946
|
});
|
|
79947
|
+
// The table's columns as this command found them. Taken once, because the
|
|
79948
|
+
// command adds to them as it runs, and a blanked property must be judged
|
|
79949
|
+
// against what was there before rather than against what it has just made.
|
|
79950
|
+
fieldsBefore = utils.arrayToIndex(table.getFields());
|
|
78304
79951
|
if (fields.length > 0) {
|
|
78305
79952
|
if (hasNewFields) {
|
|
78306
79953
|
table.captureSchemaBefore({operation: 'style', fields: fields});
|
|
@@ -78309,32 +79956,67 @@ ${svg}
|
|
|
78309
79956
|
}
|
|
78310
79957
|
}
|
|
78311
79958
|
Object.keys(opts).forEach(function(optName) {
|
|
78312
|
-
|
|
79959
|
+
// undo cli parser name conversion; the regex must be global, or a
|
|
79960
|
+
// property with more than one hyphen (e.g. label-start-offset) is silently
|
|
79961
|
+
// skipped rather than applied
|
|
79962
|
+
var svgName = optName.replace(/_/g, '-');
|
|
78313
79963
|
if (!isSupportedSvgStyleProperty(svgName)) {
|
|
78314
79964
|
return;
|
|
78315
79965
|
}
|
|
78316
79966
|
var strVal = opts[optName].trim();
|
|
78317
|
-
|
|
79967
|
+
// An empty value removes the property, rather than being rejected as an
|
|
79968
|
+
// unparseable one. This is how a control gives a property back: a label
|
|
79969
|
+
// dragged off its position clears label-pos, and there is otherwise no
|
|
79970
|
+
// per-property unset -- only -style clear, which clears all of them.
|
|
79971
|
+
var unset = strVal === '' && emptyValueUnsetsProperty(svgName);
|
|
79972
|
+
var accessor = unset ? null : getSymbolPropertyAccessor(strVal, svgName, lyr);
|
|
78318
79973
|
var badIcons = svgName == 'icon' ? [] : null;
|
|
79974
|
+
// Removing a position is not setting one, so it neither validates the
|
|
79975
|
+
// value nor clears the offsets the position would have stood for.
|
|
79976
|
+
var posOnPaths = svgName == 'label-pos' && !unset ? [] : null;
|
|
78319
79977
|
table.getRecords().forEach(function(rec, i) {
|
|
78320
79978
|
if (filterFn && !filterFn(i)) {
|
|
78321
79979
|
// make sure field exists if record is excluded by filter
|
|
78322
|
-
setUndefinedFields(rec,
|
|
79980
|
+
setUndefinedFields(rec, [svgName]);
|
|
79981
|
+
if (svgName == 'label-pos') {
|
|
79982
|
+
// ...but a field the position would only have cleared is one this
|
|
79983
|
+
// command is not writing anywhere, so an excluded record has nothing
|
|
79984
|
+
// to stay consistent with
|
|
79985
|
+
setUndefinedFields(rec, labelPositionDerivedFields, {has: hadField});
|
|
79986
|
+
}
|
|
79987
|
+
} else if (unset) {
|
|
79988
|
+
// Nothing to remove, and so nothing to create: removing a property no
|
|
79989
|
+
// record has would otherwise add an empty column for it.
|
|
79990
|
+
if (hadField(svgName) || svgName in rec) rec[svgName] = undefined;
|
|
78323
79991
|
} else {
|
|
78324
79992
|
rec[svgName] = accessor(i);
|
|
78325
79993
|
if (badIcons) {
|
|
78326
79994
|
addUnsupportedIconName(badIcons, rec.icon);
|
|
78327
79995
|
}
|
|
78328
|
-
if (
|
|
78329
|
-
if (!
|
|
79996
|
+
if (posOnPaths) {
|
|
79997
|
+
if (!parseLabelPosition(rec['label-pos'])) {
|
|
78330
79998
|
stop$1('Unexpected value for label-pos:', rec['label-pos']);
|
|
78331
79999
|
}
|
|
80000
|
+
if (shapeIsPathLabel(lyr.shapes && lyr.shapes[i], rec)) {
|
|
80001
|
+
// Not stored, so that ignoring it means ignoring it: a stored
|
|
80002
|
+
// position would show up in the style panel and would start
|
|
80003
|
+
// applying if the label ever lost all but one of its knots. Its
|
|
80004
|
+
// text-anchor is left alone too -- that one does place text along a
|
|
80005
|
+
// path, so a position that had no effect must not clear it.
|
|
80006
|
+
posOnPaths.push(i);
|
|
80007
|
+
rec['label-pos'] = undefined;
|
|
80008
|
+
} else {
|
|
80009
|
+
setUndefinedFields(rec, clearedByPosition, {overwrite: true, has: hadField});
|
|
80010
|
+
}
|
|
78332
80011
|
}
|
|
78333
80012
|
}
|
|
78334
80013
|
});
|
|
78335
80014
|
if (badIcons && badIcons.length > 0) {
|
|
78336
80015
|
warn(formatUnsupportedIconMessage(badIcons));
|
|
78337
80016
|
}
|
|
80017
|
+
if (posOnPaths && posOnPaths.length > 0) {
|
|
80018
|
+
warn(formatPositionOnPathMessage(posOnPaths));
|
|
80019
|
+
}
|
|
78338
80020
|
});
|
|
78339
80021
|
if (fields.length > 0) {
|
|
78340
80022
|
if (hasNewFields) {
|
|
@@ -78345,11 +80027,22 @@ ${svg}
|
|
|
78345
80027
|
}
|
|
78346
80028
|
};
|
|
78347
80029
|
|
|
78348
|
-
|
|
80030
|
+
// The style properties this command was given, in SVG spelling.
|
|
80031
|
+
function getOptionFields(opts) {
|
|
78349
80032
|
var fields = [];
|
|
78350
80033
|
Object.keys(opts).forEach(function(optName) {
|
|
78351
|
-
var svgName = optName.replace(
|
|
78352
|
-
if (
|
|
80034
|
+
var svgName = optName.replace(/_/g, '-');
|
|
80035
|
+
if (isSupportedSvgStyleProperty(svgName)) addField(fields, svgName);
|
|
80036
|
+
});
|
|
80037
|
+
return fields;
|
|
80038
|
+
}
|
|
80039
|
+
|
|
80040
|
+
// The fields the command will write, which is what the undo capture covers.
|
|
80041
|
+
// label-pos reaches dx/dy/text-anchor as well -- it no longer stores values in
|
|
80042
|
+
// them, but it does clear them.
|
|
80043
|
+
function getStyleFields(optFields) {
|
|
80044
|
+
var fields = [];
|
|
80045
|
+
optFields.forEach(function(svgName) {
|
|
78353
80046
|
addField(fields, svgName);
|
|
78354
80047
|
if (svgName == 'label-pos') {
|
|
78355
80048
|
labelPositionFields.forEach(function(field) {
|
|
@@ -78392,14 +80085,43 @@ ${svg}
|
|
|
78392
80085
|
return str + '. Expected one of: ' + iconNames.join(', ');
|
|
78393
80086
|
}
|
|
78394
80087
|
|
|
78395
|
-
|
|
80088
|
+
// Adds @fields to @rec with no value, so that a record the filter excluded
|
|
80089
|
+
// still has the same schema as the ones it kept. With overwrite, also blanks a
|
|
80090
|
+
// value already there -- which is how setting a position takes back the offsets
|
|
80091
|
+
// a label was carrying.
|
|
80092
|
+
//
|
|
80093
|
+
// @has: optional test for whether the layer carries a field at all. A field
|
|
80094
|
+
// nobody has is not created in order to be blanked: -style label-pos=n clears
|
|
80095
|
+
// dx, dy and text-anchor because a value on the record wins over the position,
|
|
80096
|
+
// and there is nothing to win with when the column does not exist. Without
|
|
80097
|
+
// this, clicking a position in the style panel put three empty columns in the
|
|
80098
|
+
// user's table, and three empty columns in their CSV.
|
|
80099
|
+
function setUndefinedFields(rec, fields, opts) {
|
|
80100
|
+
var overwrite = !!(opts && opts.overwrite);
|
|
80101
|
+
var has = opts && opts.has;
|
|
78396
80102
|
fields.forEach(function(field) {
|
|
78397
|
-
if (field in rec === false)
|
|
80103
|
+
if (has && !has(field) && field in rec === false) return;
|
|
80104
|
+
if (overwrite || field in rec === false) {
|
|
78398
80105
|
rec[field] = undefined;
|
|
78399
80106
|
}
|
|
78400
80107
|
});
|
|
78401
80108
|
}
|
|
78402
80109
|
|
|
80110
|
+
// label-pos places text around an anchor point, which a label strung along a
|
|
80111
|
+
// path does not have: its text runs from a start offset in the direction the
|
|
80112
|
+
// path goes. A warning rather than an error, because a layer can hold both
|
|
80113
|
+
// kinds of label and styling all of it at once is reasonable.
|
|
80114
|
+
function formatPositionOnPathMessage(ids) {
|
|
80115
|
+
var extra = ids.length - maxReportedIds;
|
|
80116
|
+
var listed = (extra > 0 ? ids.slice(0, maxReportedIds) : ids).join(', ');
|
|
80117
|
+
return 'Ignoring label-pos on ' + ids.length + ' path ' +
|
|
80118
|
+
(ids.length > 1 ? 'labels' : 'label') + ' (' + listed +
|
|
80119
|
+
(extra > 0 ? ' and ' + extra + ' more' : '') + '). ' +
|
|
80120
|
+
'Use label-start-offset= and text-anchor= to place text along a path.';
|
|
80121
|
+
}
|
|
80122
|
+
|
|
80123
|
+
var maxReportedIds = 4;
|
|
80124
|
+
|
|
78403
80125
|
function getStickArrowCoords(d) {
|
|
78404
80126
|
return getArrowCoords(d, 'stick');
|
|
78405
80127
|
}
|
|
@@ -78931,7 +80653,7 @@ ${svg}
|
|
|
78931
80653
|
|
|
78932
80654
|
// TODO: refactor to remove duplication in mapshaper-svg-style.js
|
|
78933
80655
|
cmd.symbols = function(inputLyr, dataset, opts) {
|
|
78934
|
-
requirePointLayer(inputLyr);
|
|
80656
|
+
requirePointLayer$1(inputLyr);
|
|
78935
80657
|
var lyr = opts.no_replace ? copyLayer(inputLyr) : inputLyr;
|
|
78936
80658
|
var shapeMode = !!opts.geographic;
|
|
78937
80659
|
var metersPerPx;
|
|
@@ -79552,6 +81274,7 @@ ${svg}
|
|
|
79552
81274
|
name == 'require' || name == 'run' || name == 'define' ||
|
|
79553
81275
|
name == 'include' || name == 'print' || name == 'comment' || name == 'if' || name == 'elif' ||
|
|
79554
81276
|
name == 'else' || name == 'endif' || name == 'stop' || name == 'add-shape' ||
|
|
81277
|
+
name == 'add-label' || name == 'add-layer' ||
|
|
79555
81278
|
name == 'scalebar' || name == 'vars' || name == 'defaults';
|
|
79556
81279
|
}
|
|
79557
81280
|
|
|
@@ -79663,6 +81386,27 @@ ${svg}
|
|
|
79663
81386
|
job.catalog.addDataset(targetDataset);
|
|
79664
81387
|
}
|
|
79665
81388
|
outputLayers = cmd.addShape(targetLayers, targetDataset, opts);
|
|
81389
|
+
|
|
81390
|
+
} else if (name == 'add-label') {
|
|
81391
|
+
if (!targetDataset) {
|
|
81392
|
+
targetDataset = {info: {}, layers: []};
|
|
81393
|
+
targetLayers = targetDataset.layers;
|
|
81394
|
+
job.catalog.addDataset(targetDataset);
|
|
81395
|
+
}
|
|
81396
|
+
outputLayers = cmd.addLabel(targetLayers, targetDataset, opts);
|
|
81397
|
+
|
|
81398
|
+
} else if (name == 'add-layer') {
|
|
81399
|
+
// The new layer arrives in a dataset of its own, so it is added to the
|
|
81400
|
+
// catalog here rather than integrated into the target below.
|
|
81401
|
+
// captureCatalogBefore() gives an undo transaction the state before the
|
|
81402
|
+
// change; addDataset() marks the change itself. Both are no-ops in the
|
|
81403
|
+
// CLI, where nothing is listening.
|
|
81404
|
+
job.catalog.captureCatalogBefore({operation: 'addLayer'});
|
|
81405
|
+
job.catalog.addDataset(cmd.addLayer(targetDataset, opts));
|
|
81406
|
+
|
|
81407
|
+
} else if (name == 'update-label') {
|
|
81408
|
+
cmd.updateLabel(targetLayers, targetDataset, opts);
|
|
81409
|
+
|
|
79666
81410
|
} else if (name == 'affine') {
|
|
79667
81411
|
cmd.affine(targetLayers, targetDataset, opts);
|
|
79668
81412
|
|
|
@@ -80211,7 +81955,7 @@ ${svg}
|
|
|
80211
81955
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
80212
81956
|
}
|
|
80213
81957
|
|
|
80214
|
-
var version = "0.7.
|
|
81958
|
+
var version = "0.7.62";
|
|
80215
81959
|
|
|
80216
81960
|
// Parse command line args into commands and run them
|
|
80217
81961
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|
|
@@ -80602,49 +82346,597 @@ ${svg}
|
|
|
80602
82346
|
testCommands: testCommands
|
|
80603
82347
|
});
|
|
80604
82348
|
|
|
80605
|
-
|
|
82349
|
+
// Finding the font file a label's font-family names, among the fonts installed
|
|
82350
|
+
// on this computer.
|
|
82351
|
+
//
|
|
82352
|
+
// The browser does this for us in the GUI; in Node there is nothing between
|
|
82353
|
+
// mapshaper and the filesystem, so a family name has to be matched against the
|
|
82354
|
+
// name tables of the files themselves. Names cannot be inferred from
|
|
82355
|
+
// filenames: NYTFranklinLight.otf calls itself "NYTFranklin Light", and a
|
|
82356
|
+
// label asking for NYTFranklin at weight 300 has to reach it.
|
|
82357
|
+
//
|
|
82358
|
+
// See docs/development/label-tool-design.md.
|
|
82359
|
+
|
|
82360
|
+
// Collections (.ttc/.otc) hold several faces in one file and are how macOS
|
|
82361
|
+
// ships Helvetica, Menlo and Avenir, so a lookup that skipped them would miss
|
|
82362
|
+
// the font mapshaper's own tool writes by default there.
|
|
82363
|
+
var FONT_FILE_RXP = /\.(ttf|otf|ttc|otc)$/i;
|
|
82364
|
+
|
|
82365
|
+
// What a generic family means when there is no browser to resolve it. A guess,
|
|
82366
|
+
// but an ordered one: the first of these that is installed is what a browser
|
|
82367
|
+
// on this platform would almost certainly have picked.
|
|
82368
|
+
var GENERIC_FAMILIES = {
|
|
82369
|
+
'sans-serif': ['Helvetica', 'Arial', 'Liberation Sans', 'DejaVu Sans', 'Roboto', 'Segoe UI'],
|
|
82370
|
+
serif: ['Times New Roman', 'Times', 'Liberation Serif', 'DejaVu Serif', 'Georgia'],
|
|
82371
|
+
monospace: ['Menlo', 'Courier New', 'Liberation Mono', 'DejaVu Sans Mono', 'Consolas'],
|
|
82372
|
+
'system-ui': ['Helvetica Neue', 'Segoe UI', 'Cantarell', 'Roboto'],
|
|
82373
|
+
cursive: [],
|
|
82374
|
+
fantasy: []
|
|
82375
|
+
};
|
|
80606
82376
|
|
|
80607
|
-
|
|
80608
|
-
|
|
82377
|
+
var faceCache = {};
|
|
82378
|
+
var familyCache = {};
|
|
82379
|
+
var fullIndex = null;
|
|
82380
|
+
var fileList = null;
|
|
82381
|
+
|
|
82382
|
+
// Width classes, as OS/2 numbers them 1 to 9. A font-stretch is one of these
|
|
82383
|
+
// keywords or a percentage of normal width.
|
|
82384
|
+
var STRETCH_NAMES = {
|
|
82385
|
+
'ultra-condensed': 1, 'extra-condensed': 2, condensed: 3,
|
|
82386
|
+
'semi-condensed': 4, normal: 5, 'semi-expanded': 6, expanded: 7,
|
|
82387
|
+
'extra-expanded': 8, 'ultra-expanded': 9
|
|
82388
|
+
};
|
|
82389
|
+
var STRETCH_PERCENTS = [50, 62.5, 75, 87.5, 100, 112.5, 125, 150, 200];
|
|
82390
|
+
|
|
82391
|
+
// The face to open for @family in (@weight, @italic, @stretch), or null if this
|
|
82392
|
+
// computer has no such font: {path, postscriptName}.
|
|
82393
|
+
//
|
|
82394
|
+
// postscriptName is how a face inside a collection is named to fontkit, and is
|
|
82395
|
+
// null for a file holding one face.
|
|
82396
|
+
function findFontFace(family, weight, italic, stretch) {
|
|
82397
|
+
var key = [family, weight, italic ? 'i' : 'n', stretch || ''].join('|');
|
|
82398
|
+
if (!(key in faceCache)) {
|
|
82399
|
+
faceCache[key] = lookupFace(family, weight, italic, stretch);
|
|
82400
|
+
}
|
|
82401
|
+
return faceCache[key];
|
|
80609
82402
|
}
|
|
80610
82403
|
|
|
80611
|
-
|
|
80612
|
-
|
|
82404
|
+
// Which of a family's faces answers a request for (@weight, @italic,
|
|
82405
|
+
// @stretch).
|
|
82406
|
+
//
|
|
82407
|
+
// Width first, then upright before oblique, then the nearest weight, then the
|
|
82408
|
+
// heavier of two equally near. The first three are the order CSS matches fonts
|
|
82409
|
+
// in, and the last is the rule the style menu uses to carry a face across a
|
|
82410
|
+
// change of font (getNearestVariant() in gui-label-fonts.mjs), applied here to
|
|
82411
|
+
// faces read from files rather than measured in a browser.
|
|
82412
|
+
//
|
|
82413
|
+
// A missing face is answered with a near one rather than refused: the browser
|
|
82414
|
+
// would synthesize the missing weight or slant from exactly this face, so its
|
|
82415
|
+
// widths are much closer to what is drawn than no measurement at all.
|
|
82416
|
+
function pickFace(faces, weight, italic, stretch) {
|
|
82417
|
+
var wanted = weight > 0 ? weight : 400;
|
|
82418
|
+
var wantedWidth = parseFontStretch(stretch);
|
|
82419
|
+
var best = null;
|
|
82420
|
+
var bestScore = null;
|
|
82421
|
+
(faces || []).forEach(function(face) {
|
|
82422
|
+
var score = [
|
|
82423
|
+
Math.abs((face.width || 5) - wantedWidth),
|
|
82424
|
+
!!face.italic === !!italic ? 0 : 1,
|
|
82425
|
+
Math.abs(face.weight - wanted),
|
|
82426
|
+
face.weight < wanted ? 1 : 0
|
|
82427
|
+
];
|
|
82428
|
+
if (!best || compareScores(score, bestScore) < 0) {
|
|
82429
|
+
best = face;
|
|
82430
|
+
bestScore = score;
|
|
82431
|
+
}
|
|
82432
|
+
});
|
|
82433
|
+
return best;
|
|
80613
82434
|
}
|
|
80614
82435
|
|
|
80615
|
-
|
|
80616
|
-
|
|
80617
|
-
|
|
80618
|
-
|
|
82436
|
+
// A font-stretch as the width class it names, or normal width for anything
|
|
82437
|
+
// unreadable. A percentage is taken to the nearest class, the way CSS defines
|
|
82438
|
+
// the keywords.
|
|
82439
|
+
function parseFontStretch(stretch) {
|
|
82440
|
+
var str = String(stretch === null || stretch === undefined ? '' : stretch).trim().toLowerCase();
|
|
82441
|
+
var pct = /^([.0-9]+)%$/.exec(str);
|
|
82442
|
+
var best = 5;
|
|
82443
|
+
if (STRETCH_NAMES[str]) return STRETCH_NAMES[str];
|
|
82444
|
+
if (!pct) return 5;
|
|
82445
|
+
STRETCH_PERCENTS.forEach(function(val, i) {
|
|
82446
|
+
if (Math.abs(val - Number(pct[1])) <
|
|
82447
|
+
Math.abs(STRETCH_PERCENTS[best - 1] - Number(pct[1]))) {
|
|
82448
|
+
best = i + 1;
|
|
82449
|
+
}
|
|
82450
|
+
});
|
|
82451
|
+
return best;
|
|
82452
|
+
}
|
|
82453
|
+
|
|
82454
|
+
// Family names are compared without spaces, punctuation or case, so that
|
|
82455
|
+
// "NYTFranklin" finds "NYT Franklin" and "Helvetica Neue" finds
|
|
82456
|
+
// "HelveticaNeue". Font vendors are not consistent about any of the three.
|
|
82457
|
+
function normalizeFamilyName(name) {
|
|
82458
|
+
return String(name || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
82459
|
+
}
|
|
82460
|
+
|
|
82461
|
+
// The families a face answers to. The typographic family (name ID 16) is the
|
|
82462
|
+
// one a stylesheet means: every weight of NYTFranklin has its own ID 1 family
|
|
82463
|
+
// ("NYTFranklin Light", "NYTFranklin Medium") and they are one family only
|
|
82464
|
+
// under ID 16. Both are indexed, because plenty of fonts have no ID 16.
|
|
82465
|
+
function getFaceFamilies(font) {
|
|
82466
|
+
var records = font && font.name && font.name.records || {};
|
|
82467
|
+
return [pickName(records.preferredFamily), pickName(records.fontFamily),
|
|
82468
|
+
font && font.familyName].filter(Boolean);
|
|
82469
|
+
}
|
|
82470
|
+
|
|
82471
|
+
function clearFontCache() {
|
|
82472
|
+
faceCache = {};
|
|
82473
|
+
familyCache = {};
|
|
82474
|
+
fullIndex = null;
|
|
82475
|
+
fileList = null;
|
|
82476
|
+
}
|
|
82477
|
+
|
|
82478
|
+
function lookupFace(family, weight, italic, stretch) {
|
|
82479
|
+
var names = resolveFamilyNames(family);
|
|
82480
|
+
var face = findBestFace(names, weight, italic, stretch);
|
|
82481
|
+
// The filename guess can find a family and still miss one of its faces:
|
|
82482
|
+
// Segoe UI Semibold lives in seguisb.ttf, which does not begin with the
|
|
82483
|
+
// family's name. Anything other than the face that was asked for is worth
|
|
82484
|
+
// the full index -- once per session -- to be sure it is the nearest this
|
|
82485
|
+
// machine has.
|
|
82486
|
+
if (!fullIndex && !faceAnswersRequest(face, weight, italic, stretch)) {
|
|
82487
|
+
fullIndex = readFaces(getFontFiles());
|
|
82488
|
+
face = findBestFace(names, weight, italic, stretch) || face;
|
|
80619
82489
|
}
|
|
80620
|
-
return
|
|
82490
|
+
return face;
|
|
80621
82491
|
}
|
|
80622
82492
|
|
|
80623
|
-
function
|
|
80624
|
-
|
|
80625
|
-
|
|
80626
|
-
|
|
80627
|
-
|
|
80628
|
-
|
|
80629
|
-
|
|
80630
|
-
|
|
80631
|
-
|
|
80632
|
-
|
|
80633
|
-
|
|
80634
|
-
|
|
80635
|
-
|
|
80636
|
-
|
|
82493
|
+
function findBestFace(names, weight, italic, stretch) {
|
|
82494
|
+
for (var i = 0; i < names.length; i++) {
|
|
82495
|
+
var faces = findFamilyFaces(names[i]);
|
|
82496
|
+
if (faces && faces.length > 0) return pickFace(faces, weight, italic, stretch);
|
|
82497
|
+
}
|
|
82498
|
+
return null;
|
|
82499
|
+
}
|
|
82500
|
+
|
|
82501
|
+
function faceAnswersRequest(face, weight, italic, stretch) {
|
|
82502
|
+
return !!face && face.weight == (weight > 0 ? weight : 400) &&
|
|
82503
|
+
!!face.italic === !!italic && (face.width || 5) == parseFontStretch(stretch);
|
|
82504
|
+
}
|
|
82505
|
+
|
|
82506
|
+
// A font-family is a list, and may end in a generic: each name is tried in
|
|
82507
|
+
// turn, exactly as a browser would, and a generic stands for the first of its
|
|
82508
|
+
// candidates that is installed.
|
|
82509
|
+
function resolveFamilyNames(family) {
|
|
82510
|
+
var out = [];
|
|
82511
|
+
splitFamilyList(family).forEach(function(name) {
|
|
82512
|
+
var generic = GENERIC_FAMILIES[name.toLowerCase()];
|
|
82513
|
+
if (generic) {
|
|
82514
|
+
out = out.concat(generic);
|
|
80637
82515
|
} else {
|
|
80638
|
-
|
|
82516
|
+
out.push(name);
|
|
80639
82517
|
}
|
|
82518
|
+
});
|
|
82519
|
+
return out;
|
|
82520
|
+
}
|
|
82521
|
+
|
|
82522
|
+
function splitFamilyList(family) {
|
|
82523
|
+
return String(family || '').split(',').map(function(name) {
|
|
82524
|
+
return name.trim().replace(/^['"]|['"]$/g, '');
|
|
82525
|
+
}).filter(Boolean);
|
|
82526
|
+
}
|
|
82527
|
+
|
|
82528
|
+
// Two passes, because parsing every font on the computer costs the best part
|
|
82529
|
+
// of a second and most lookups do not need it: the file holding a family is
|
|
82530
|
+
// usually named after it, so files whose name begins with the family's are
|
|
82531
|
+
// parsed first. Whether that was good enough is lookupFace()'s decision.
|
|
82532
|
+
function findFamilyFaces(family) {
|
|
82533
|
+
var key = normalizeFamilyName(family);
|
|
82534
|
+
if (!key) return null;
|
|
82535
|
+
if (fullIndex) return fullIndex[key] || null;
|
|
82536
|
+
if (!(key in familyCache)) {
|
|
82537
|
+
familyCache[key] = readFaces(getLikelyFiles(key))[key] || null;
|
|
80640
82538
|
}
|
|
80641
|
-
return
|
|
82539
|
+
return familyCache[key];
|
|
80642
82540
|
}
|
|
80643
82541
|
|
|
80644
|
-
|
|
82542
|
+
function getLikelyFiles(key) {
|
|
82543
|
+
return getFontFiles().filter(function(file) {
|
|
82544
|
+
return normalizeFamilyName(basename(file)).indexOf(key) === 0;
|
|
82545
|
+
});
|
|
82546
|
+
}
|
|
82547
|
+
|
|
82548
|
+
// Faces by normalized family name. A file that cannot be parsed is skipped
|
|
82549
|
+
// rather than reported: a font directory can hold anything, and a broken font
|
|
82550
|
+
// is not an error in the user's data.
|
|
82551
|
+
function readFaces(files) {
|
|
82552
|
+
var index = {};
|
|
82553
|
+
files.forEach(function(file) {
|
|
82554
|
+
getFileFaces(file).forEach(function(face) {
|
|
82555
|
+
face.families.forEach(function(name) {
|
|
82556
|
+
var key = normalizeFamilyName(name);
|
|
82557
|
+
if (!key) return;
|
|
82558
|
+
if (!index[key]) index[key] = [];
|
|
82559
|
+
index[key].push(face);
|
|
82560
|
+
});
|
|
82561
|
+
});
|
|
82562
|
+
});
|
|
82563
|
+
return index;
|
|
82564
|
+
}
|
|
82565
|
+
|
|
82566
|
+
function getFileFaces(file) {
|
|
82567
|
+
var fontkit = getFontkit();
|
|
82568
|
+
var font, fonts;
|
|
82569
|
+
if (!fontkit) return [];
|
|
82570
|
+
try {
|
|
82571
|
+
font = fontkit.openSync(file);
|
|
82572
|
+
// A collection reports its members in .fonts; a single font is its own.
|
|
82573
|
+
fonts = font && font.fonts || [font];
|
|
82574
|
+
return fonts.filter(Boolean).map(function(one) {
|
|
82575
|
+
return {
|
|
82576
|
+
path: file,
|
|
82577
|
+
// Named rather than numbered because fontkit takes a name, and because
|
|
82578
|
+
// a name survives a font being reinstalled in a different order.
|
|
82579
|
+
postscriptName: font.fonts ? one.postscriptName : null,
|
|
82580
|
+
families: getFaceFamilies(one),
|
|
82581
|
+
weight: getFaceWeight(one),
|
|
82582
|
+
width: getFaceWidth(one),
|
|
82583
|
+
italic: isItalicFace(one)
|
|
82584
|
+
};
|
|
82585
|
+
}).filter(function(face) {
|
|
82586
|
+
return face.families.length > 0;
|
|
82587
|
+
});
|
|
82588
|
+
} catch (e) {
|
|
82589
|
+
return [];
|
|
82590
|
+
}
|
|
82591
|
+
}
|
|
82592
|
+
|
|
82593
|
+
function getFaceWeight(font) {
|
|
82594
|
+
var os2 = font['OS/2'];
|
|
82595
|
+
var weight = os2 && os2.usWeightClass;
|
|
82596
|
+
return weight > 0 ? weight : 400;
|
|
82597
|
+
}
|
|
82598
|
+
|
|
82599
|
+
function getFaceWidth(font) {
|
|
82600
|
+
var os2 = font['OS/2'];
|
|
82601
|
+
var width = os2 && os2.usWidthClass;
|
|
82602
|
+
return width >= 1 && width <= 9 ? width : 5;
|
|
82603
|
+
}
|
|
82604
|
+
|
|
82605
|
+
function isItalicFace(font) {
|
|
82606
|
+
var os2 = font['OS/2'];
|
|
82607
|
+
if (os2 && os2.fsSelection && typeof os2.fsSelection.italic == 'boolean') {
|
|
82608
|
+
return os2.fsSelection.italic;
|
|
82609
|
+
}
|
|
82610
|
+
return !!font.italicAngle;
|
|
82611
|
+
}
|
|
82612
|
+
|
|
82613
|
+
// A name record is a string, or an object of translations to pick English out
|
|
82614
|
+
// of, depending on the font and the version of fontkit.
|
|
82615
|
+
function pickName(rec) {
|
|
82616
|
+
if (!rec) return '';
|
|
82617
|
+
if (typeof rec == 'string') return rec;
|
|
82618
|
+
return rec.en || Object.keys(rec).map(function(k) { return rec[k]; })[0] || '';
|
|
82619
|
+
}
|
|
82620
|
+
|
|
82621
|
+
function compareScores(a, b) {
|
|
82622
|
+
for (var i = 0; i < a.length; i++) {
|
|
82623
|
+
if (a[i] != b[i]) return a[i] - b[i];
|
|
82624
|
+
}
|
|
82625
|
+
return 0;
|
|
82626
|
+
}
|
|
82627
|
+
|
|
82628
|
+
// Every font file on this computer, listed once. Listing is cheap -- a few
|
|
82629
|
+
// hundred directory entries -- next to parsing them, so this is not the part
|
|
82630
|
+
// worth avoiding.
|
|
82631
|
+
function getFontFiles() {
|
|
82632
|
+
var fs = getFs();
|
|
82633
|
+
var files = [];
|
|
82634
|
+
if (fileList) return fileList;
|
|
82635
|
+
if (!fs) return [];
|
|
82636
|
+
getFontDirs().forEach(function(dir) {
|
|
82637
|
+
var entries;
|
|
82638
|
+
try {
|
|
82639
|
+
entries = fs.readdirSync(dir, {recursive: true});
|
|
82640
|
+
} catch (e) {
|
|
82641
|
+
return; // a directory this platform does not have
|
|
82642
|
+
}
|
|
82643
|
+
entries.forEach(function(entry) {
|
|
82644
|
+
var file = dir + '/' + String(entry).split('\\').join('/');
|
|
82645
|
+
if (FONT_FILE_RXP.test(file)) files.push(file);
|
|
82646
|
+
});
|
|
82647
|
+
});
|
|
82648
|
+
fileList = files;
|
|
82649
|
+
return files;
|
|
82650
|
+
}
|
|
82651
|
+
|
|
82652
|
+
// Where each platform keeps fonts, or MAPSHAPER_FONT_PATH if it is set.
|
|
82653
|
+
//
|
|
82654
|
+
// It replaces the platform's directories rather than adding to them, which is
|
|
82655
|
+
// what makes a machine's font situation something a caller can state: a
|
|
82656
|
+
// container with its fonts somewhere of its own, a build that has to produce
|
|
82657
|
+
// the same SVG wherever it runs, a test that needs to know there is nothing to
|
|
82658
|
+
// find. Several directories are separated by : or ;.
|
|
82659
|
+
function getFontDirs() {
|
|
82660
|
+
var home = getHomeDir();
|
|
82661
|
+
var platform = typeof process == 'object' && process.platform || '';
|
|
82662
|
+
var dirs = getEnvDirs();
|
|
82663
|
+
if (dirs.length > 0) return dirs;
|
|
82664
|
+
if (platform == 'darwin') {
|
|
82665
|
+
dirs = ['/System/Library/Fonts', '/Library/Fonts',
|
|
82666
|
+
'/Network/Library/Fonts'];
|
|
82667
|
+
if (home) dirs.push(home + '/Library/Fonts');
|
|
82668
|
+
} else if (platform == 'win32') {
|
|
82669
|
+
dirs = [(getEnv('WINDIR') || 'C:\\Windows') + '/Fonts'];
|
|
82670
|
+
if (home) dirs.push(home + '/AppData/Local/Microsoft/Windows/Fonts');
|
|
82671
|
+
} else {
|
|
82672
|
+
dirs = ['/usr/share/fonts', '/usr/local/share/fonts', '/run/host/fonts'];
|
|
82673
|
+
if (home) {
|
|
82674
|
+
dirs.push(home + '/.fonts', home + '/.local/share/fonts');
|
|
82675
|
+
}
|
|
82676
|
+
}
|
|
82677
|
+
return dirs;
|
|
82678
|
+
}
|
|
82679
|
+
|
|
82680
|
+
function getEnvDirs() {
|
|
82681
|
+
var val = getEnv('MAPSHAPER_FONT_PATH');
|
|
82682
|
+
if (!val) return [];
|
|
82683
|
+
return val.split(/[:;]/).filter(Boolean);
|
|
82684
|
+
}
|
|
82685
|
+
|
|
82686
|
+
function getEnv(name) {
|
|
82687
|
+
return typeof process == 'object' && process.env && process.env[name] || '';
|
|
82688
|
+
}
|
|
82689
|
+
|
|
82690
|
+
function getHomeDir() {
|
|
82691
|
+
var os = safeRequire('os');
|
|
82692
|
+
try {
|
|
82693
|
+
return os && os.homedir() || '';
|
|
82694
|
+
} catch (e) {
|
|
82695
|
+
return '';
|
|
82696
|
+
}
|
|
82697
|
+
}
|
|
82698
|
+
|
|
82699
|
+
function basename(file) {
|
|
82700
|
+
var parts = file.split('/');
|
|
82701
|
+
return parts[parts.length - 1].replace(FONT_FILE_RXP, '');
|
|
82702
|
+
}
|
|
82703
|
+
|
|
82704
|
+
function getFs() {
|
|
82705
|
+
return safeRequire('fs');
|
|
82706
|
+
}
|
|
82707
|
+
|
|
82708
|
+
// Loaded through the require shim and only when a label actually needs
|
|
82709
|
+
// measuring, so that the browser bundle -- which is this same file -- never
|
|
82710
|
+
// reaches for a module it does not have, and a CLI run that touches no labels
|
|
82711
|
+
// never pays for loading it.
|
|
82712
|
+
function getFontkit() {
|
|
82713
|
+
return safeRequire('fontkit');
|
|
82714
|
+
}
|
|
82715
|
+
|
|
82716
|
+
function safeRequire(name) {
|
|
82717
|
+
if (runningInBrowser()) return null;
|
|
82718
|
+
try {
|
|
82719
|
+
return require$1(name) || null;
|
|
82720
|
+
} catch (e) {
|
|
82721
|
+
return null;
|
|
82722
|
+
}
|
|
82723
|
+
}
|
|
82724
|
+
|
|
82725
|
+
var FontLookup = /*#__PURE__*/Object.freeze({
|
|
80645
82726
|
__proto__: null,
|
|
80646
|
-
|
|
80647
|
-
|
|
82727
|
+
clearFontCache: clearFontCache,
|
|
82728
|
+
findFontFace: findFontFace,
|
|
82729
|
+
getFaceFamilies: getFaceFamilies,
|
|
82730
|
+
normalizeFamilyName: normalizeFamilyName,
|
|
82731
|
+
parseFontStretch: parseFontStretch,
|
|
82732
|
+
pickFace: pickFace
|
|
82733
|
+
});
|
|
82734
|
+
|
|
82735
|
+
// Measuring a label's text outside a browser, from the font files installed on
|
|
82736
|
+
// this computer.
|
|
82737
|
+
//
|
|
82738
|
+
// The GUI measures by rendering (gui-label-measure.mjs) and the core asks it
|
|
82739
|
+
// for widths through svg-label-metrics.mjs. Nothing answered that question in
|
|
82740
|
+
// Node, so `-style label-align=left` from the command line re-justified a
|
|
82741
|
+
// label's lines and left the block where it was, and the path-fit check could
|
|
82742
|
+
// not tell whether a label was longer than its curve.
|
|
82743
|
+
//
|
|
82744
|
+
// Advance widths plus kerning, which is what a browser lays out with: summing
|
|
82745
|
+
// hmtx advances alone is exact for most text but out by up to 5% on strings
|
|
82746
|
+
// like "AVATAR Toledo", and the pair positioning that closes that gap is what
|
|
82747
|
+
// fontkit's layout() applies. Measured against Chrome on the same fonts, this
|
|
82748
|
+
// agrees to a hundredth of a pixel.
|
|
82749
|
+
//
|
|
82750
|
+
// See docs/development/label-tool-design.md.
|
|
82751
|
+
|
|
82752
|
+
// Weight keywords. Anything else is a number, or 400 if it is not.
|
|
82753
|
+
var WEIGHT_NAMES = {normal: 400, bold: 700, lighter: 300, bolder: 700};
|
|
82754
|
+
|
|
82755
|
+
var fontCache = {};
|
|
82756
|
+
|
|
82757
|
+
// Installed for every Node use of mapshaper -- the CLI, the API and a script
|
|
82758
|
+
// that only exports -- rather than at one entry point, because a width is read
|
|
82759
|
+
// during rendering and export, which both of those reach without going near a
|
|
82760
|
+
// command of their own. Costs nothing until a label is measured: no font is
|
|
82761
|
+
// read, and fontkit is not even loaded, until then.
|
|
82762
|
+
function initNodeTextMeasurement() {
|
|
82763
|
+
if (runningInBrowser()) return false;
|
|
82764
|
+
setTextMeasureFunction(measureLabelText);
|
|
82765
|
+
return true;
|
|
82766
|
+
}
|
|
82767
|
+
|
|
82768
|
+
// The width of @rec's text in px, or null if it cannot be known -- an unusable
|
|
82769
|
+
// record, a font this computer does not have, or a font file that will not
|
|
82770
|
+
// parse. Null is what every reader already falls back from.
|
|
82771
|
+
function measureLabelText(rec) {
|
|
82772
|
+
var text = toLabelString(rec && rec['label-text']);
|
|
82773
|
+
var font, fontSize, spacing, width;
|
|
82774
|
+
if (!text) return null;
|
|
82775
|
+
font = getFontForRecord(rec);
|
|
82776
|
+
if (!font) return null;
|
|
82777
|
+
fontSize = getFontSizeInPx(rec);
|
|
82778
|
+
spacing = getLetterSpacingInPx(rec, fontSize);
|
|
82779
|
+
if (!(fontSize > 0)) return null;
|
|
82780
|
+
// The widest line, which is what the block of a multi-line label is as wide
|
|
82781
|
+
// as, and what the browser's getBBox() reports for the same text.
|
|
82782
|
+
width = text.split(labelNewlineRxp).reduce(function(max, line) {
|
|
82783
|
+
var w = measureLine(font, line, fontSize, spacing);
|
|
82784
|
+
return w > max ? w : max;
|
|
82785
|
+
}, 0);
|
|
82786
|
+
return width > 0 ? width : null;
|
|
82787
|
+
}
|
|
82788
|
+
|
|
82789
|
+
// Font units scaled to the size the label is drawn at, plus letter-spacing.
|
|
82790
|
+
//
|
|
82791
|
+
// Spacing is added after every character including the last, which is what the
|
|
82792
|
+
// browser does -- letter-spacing=2 on a four-character label widens it by 8px,
|
|
82793
|
+
// not 6.
|
|
82794
|
+
function measureLine(font, line, fontSize, spacing) {
|
|
82795
|
+
var chars = Array.from(line).length;
|
|
82796
|
+
var advance;
|
|
82797
|
+
if (!line) return 0;
|
|
82798
|
+
try {
|
|
82799
|
+
advance = font.layout(line).advanceWidth;
|
|
82800
|
+
} catch (e) {
|
|
82801
|
+
return 0;
|
|
82802
|
+
}
|
|
82803
|
+
return advance / font.unitsPerEm * fontSize + chars * spacing;
|
|
82804
|
+
}
|
|
82805
|
+
|
|
82806
|
+
// The font a record is drawn in, opened and remembered. A record with no
|
|
82807
|
+
// font-family is drawn in the layer group's default, the same one the GUI
|
|
82808
|
+
// measures against -- see getLabelTextDefaults().
|
|
82809
|
+
function getFontForRecord(rec) {
|
|
82810
|
+
var family = rec['font-family'] || 'sans-serif';
|
|
82811
|
+
var weight = getFontWeight(rec);
|
|
82812
|
+
var italic = isItalic(rec);
|
|
82813
|
+
var stretch = rec['font-stretch'] || '';
|
|
82814
|
+
var key = [family, weight, italic ? 'i' : 'n', stretch].join('|');
|
|
82815
|
+
if (!(key in fontCache)) {
|
|
82816
|
+
fontCache[key] = openFace(findFontFace(family, weight, italic, stretch),
|
|
82817
|
+
family, weight);
|
|
82818
|
+
}
|
|
82819
|
+
return fontCache[key];
|
|
82820
|
+
}
|
|
82821
|
+
|
|
82822
|
+
function openFace(face, family, weight) {
|
|
82823
|
+
var fontkit = loadFontkit();
|
|
82824
|
+
var font;
|
|
82825
|
+
if (!face || !fontkit) {
|
|
82826
|
+
// Once per font, and only for a font the user named: a label falling back
|
|
82827
|
+
// to the layer default is the ordinary case and not worth a warning, but a
|
|
82828
|
+
// label asking for a font this computer has not got is worth knowing
|
|
82829
|
+
// about, because its alignment is the thing that will be wrong.
|
|
82830
|
+
if (!face && family != 'sans-serif') {
|
|
82831
|
+
warnOnce('[label] Unable to measure text in font "' + family +
|
|
82832
|
+
'" (not installed?); alignment may be off.');
|
|
82833
|
+
}
|
|
82834
|
+
return null;
|
|
82835
|
+
}
|
|
82836
|
+
try {
|
|
82837
|
+
font = fontkit.openSync(face.path, face.postscriptName || undefined);
|
|
82838
|
+
return font ? applyVariation(font, weight) : null;
|
|
82839
|
+
} catch (e) {
|
|
82840
|
+
return null;
|
|
82841
|
+
}
|
|
82842
|
+
}
|
|
82843
|
+
|
|
82844
|
+
// A variable font set to the weight asked for. One file covers a range of
|
|
82845
|
+
// weights, and its glyphs are wider at the heavy end: measuring at the file's
|
|
82846
|
+
// default instance would report Light widths for Bold text. Only the weight
|
|
82847
|
+
// axis is applied, because that is the one a label can ask for -- slant and
|
|
82848
|
+
// width usually arrive as separate faces, which the lookup has already chosen
|
|
82849
|
+
// between.
|
|
82850
|
+
function applyVariation(font, weight) {
|
|
82851
|
+
var settings = getVariationSettings(font.variationAxes, weight);
|
|
82852
|
+
if (!settings) return font;
|
|
82853
|
+
try {
|
|
82854
|
+
return font.getVariation(settings) || font;
|
|
82855
|
+
} catch (e) {
|
|
82856
|
+
return font;
|
|
82857
|
+
}
|
|
82858
|
+
}
|
|
82859
|
+
|
|
82860
|
+
// The variation to set, or null for a font that is not variable or is already
|
|
82861
|
+
// at the weight wanted. A weight outside the axis is clamped to it, the way a
|
|
82862
|
+
// browser does.
|
|
82863
|
+
function getVariationSettings(axes, weight) {
|
|
82864
|
+
var axis = axes && axes.wght;
|
|
82865
|
+
var val;
|
|
82866
|
+
if (!axis) return null;
|
|
82867
|
+
val = Math.max(axis.min, Math.min(axis.max, weight));
|
|
82868
|
+
return val == axis.default ? null : {wght: val};
|
|
82869
|
+
}
|
|
82870
|
+
|
|
82871
|
+
// px, for a font-size that may be a number, a px value, an em value relative
|
|
82872
|
+
// to the layer default, or a pt value. Anything else takes the default rather
|
|
82873
|
+
// than failing the measurement: a size mapshaper cannot read is one the
|
|
82874
|
+
// renderer is also reading its own way, and a width from the default size is
|
|
82875
|
+
// closer than no width at all.
|
|
82876
|
+
function getFontSizeInPx(rec) {
|
|
82877
|
+
var val = rec && rec['font-size'];
|
|
82878
|
+
var px = toPixelMeasure(val, DEFAULT_LABEL_FONT_SIZE);
|
|
82879
|
+
return px === null ? DEFAULT_LABEL_FONT_SIZE : px;
|
|
82880
|
+
}
|
|
82881
|
+
|
|
82882
|
+
// px, for a letter-spacing relative to the label's own size rather than to the
|
|
82883
|
+
// layer default: 0.1em on 24px text is 2.4px.
|
|
82884
|
+
function getLetterSpacingInPx(rec, fontSize) {
|
|
82885
|
+
var px = toPixelMeasure(rec && rec['letter-spacing'], fontSize);
|
|
82886
|
+
return px === null ? 0 : px;
|
|
82887
|
+
}
|
|
82888
|
+
|
|
82889
|
+
function getFontWeight(rec) {
|
|
82890
|
+
var val = rec && rec['font-weight'];
|
|
82891
|
+
var named = WEIGHT_NAMES[String(val).toLowerCase()];
|
|
82892
|
+
if (named) return named;
|
|
82893
|
+
return Number(val) > 0 ? Number(val) : 400;
|
|
82894
|
+
}
|
|
82895
|
+
|
|
82896
|
+
function isItalic(rec) {
|
|
82897
|
+
var val = String(rec && rec['font-style'] || '').toLowerCase();
|
|
82898
|
+
return val == 'italic' || val == 'oblique';
|
|
82899
|
+
}
|
|
82900
|
+
|
|
82901
|
+
function toPixelMeasure(val, emBasis) {
|
|
82902
|
+
var measure, match;
|
|
82903
|
+
// An absent value is not a zero: a label with no font-size of its own is
|
|
82904
|
+
// drawn at the layer's size, and one with no letter-spacing is not spaced.
|
|
82905
|
+
if (val === null || val === undefined || val === '') return null;
|
|
82906
|
+
measure = parseSvgMeasure(val);
|
|
82907
|
+
if (typeof measure == 'number' && !isNaN(measure)) return measure;
|
|
82908
|
+
match = /^(-?[.0-9]+)(em|px|pt)$/.exec(String(measure));
|
|
82909
|
+
if (!match) return null;
|
|
82910
|
+
if (match[2] == 'em') return Number(match[1]) * emBasis;
|
|
82911
|
+
if (match[2] == 'pt') return Number(match[1]) * 4 / 3;
|
|
82912
|
+
return Number(match[1]);
|
|
82913
|
+
}
|
|
82914
|
+
|
|
82915
|
+
function loadFontkit() {
|
|
82916
|
+
if (runningInBrowser()) return null;
|
|
82917
|
+
try {
|
|
82918
|
+
return require$1('fontkit') || null;
|
|
82919
|
+
} catch (e) {
|
|
82920
|
+
warnOnce('[label] fontkit is not available; labels cannot be measured.');
|
|
82921
|
+
return null;
|
|
82922
|
+
}
|
|
82923
|
+
}
|
|
82924
|
+
|
|
82925
|
+
// For tests, and for a session that has installed a font since it started.
|
|
82926
|
+
function clearMeasuredFontCache() {
|
|
82927
|
+
fontCache = {};
|
|
82928
|
+
}
|
|
82929
|
+
|
|
82930
|
+
var TextMeasure = /*#__PURE__*/Object.freeze({
|
|
82931
|
+
__proto__: null,
|
|
82932
|
+
clearMeasuredFontCache: clearMeasuredFontCache,
|
|
82933
|
+
getFontSizeInPx: getFontSizeInPx,
|
|
82934
|
+
getFontWeight: getFontWeight,
|
|
82935
|
+
getLetterSpacingInPx: getLetterSpacingInPx,
|
|
82936
|
+
getVariationSettings: getVariationSettings,
|
|
82937
|
+
initNodeTextMeasurement: initNodeTextMeasurement,
|
|
82938
|
+
isItalic: isItalic,
|
|
82939
|
+
measureLabelText: measureLabelText
|
|
80648
82940
|
});
|
|
80649
82941
|
|
|
80650
82942
|
// Return an array containing points from a path iterator, clipped to a bounding box
|
|
@@ -82262,7 +84554,16 @@ ${svg}
|
|
|
82262
84554
|
// export only functions called by the GUI.
|
|
82263
84555
|
var internal = {};
|
|
82264
84556
|
|
|
82265
|
-
internal.svg = Object.assign({}, SvgStringify, SvgPathUtils, GeojsonToSvg,
|
|
84557
|
+
internal.svg = Object.assign({}, SvgStringify, SvgPathUtils, GeojsonToSvg,
|
|
84558
|
+
SvgFeatureUtils,
|
|
84559
|
+
SvgLabels, SvgSymbols, SvgLabelPaths, SvgLabelFit, SvgLabelAlign,
|
|
84560
|
+
SvgLabelMetrics);
|
|
84561
|
+
|
|
84562
|
+
// Reached through the bundle rather than imported from source, unlike most of
|
|
84563
|
+
// what tests use, because these modules load fs and fontkit through the
|
|
84564
|
+
// require shim -- which resolves to a stub outside the bundle, there being no
|
|
84565
|
+
// require() in an ES module.
|
|
84566
|
+
internal.fonts = Object.assign({}, FontLookup, TextMeasure);
|
|
82266
84567
|
|
|
82267
84568
|
// Assign functions and objects exported from modules to the 'internal' namespace
|
|
82268
84569
|
// to maintain compatibility with tests and to expose (some of) them to the GUI.
|
|
@@ -82307,6 +84608,7 @@ ${svg}
|
|
|
82307
84608
|
// BufferCommon,
|
|
82308
84609
|
Calc,
|
|
82309
84610
|
CalcUtils,
|
|
84611
|
+
CurveFit,
|
|
82310
84612
|
Catalog$1,
|
|
82311
84613
|
ClipErase,
|
|
82312
84614
|
ClipPoints,
|
|
@@ -82440,6 +84742,13 @@ ${svg}
|
|
|
82440
84742
|
Zip
|
|
82441
84743
|
);
|
|
82442
84744
|
|
|
84745
|
+
// Outside a browser, label text is measured from the installed font files --
|
|
84746
|
+
// see mapshaper-text-measure.mjs. Installed here rather than in the CLI's
|
|
84747
|
+
// entry point because a width is read while rendering and exporting, which the
|
|
84748
|
+
// API reaches without a command line, and it costs nothing until a label
|
|
84749
|
+
// actually needs measuring.
|
|
84750
|
+
initNodeTextMeasurement();
|
|
84751
|
+
|
|
82443
84752
|
// the mapshaper public api only has 4 functions
|
|
82444
84753
|
var api = {
|
|
82445
84754
|
runCommands,
|