mapshaper 0.7.68 → 0.7.70
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 +1268 -789
- package/package.json +1 -1
- package/www/mapshaper-gui.js +2001 -278
- package/www/mapshaper.js +1268 -789
- package/www/page.css +120 -6
package/www/mapshaper.js
CHANGED
|
@@ -5882,13 +5882,13 @@
|
|
|
5882
5882
|
return msg;
|
|
5883
5883
|
}
|
|
5884
5884
|
|
|
5885
|
-
function requirePointLayer$
|
|
5885
|
+
function requirePointLayer$2(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$
|
|
5891
|
+
requirePointLayer$2(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$2,
|
|
6132
6132
|
requirePolygonLayer: requirePolygonLayer,
|
|
6133
6133
|
requirePolylineLayer: requirePolylineLayer,
|
|
6134
6134
|
requireSinglePointLayer: requireSinglePointLayer,
|
|
@@ -25424,6 +25424,9 @@
|
|
|
25424
25424
|
// type/data accessors -- doesn't form an import cycle with the
|
|
25425
25425
|
// scalebar-aware renderer registry that lives in furniture.mjs.
|
|
25426
25426
|
|
|
25427
|
+
// Must match the renderers registered in mapshaper-furniture.mjs
|
|
25428
|
+
var furnitureTypes = ['scalebar'];
|
|
25429
|
+
|
|
25427
25430
|
// @lyr dataset layer
|
|
25428
25431
|
function getFurnitureLayerType(lyr) {
|
|
25429
25432
|
var rec = lyr.data && lyr.data.getReadOnlyRecordAt(0);
|
|
@@ -25434,6 +25437,12 @@
|
|
|
25434
25437
|
return lyr.data && lyr.data.getReadOnlyRecordAt(0);
|
|
25435
25438
|
}
|
|
25436
25439
|
|
|
25440
|
+
// A data-only layer holding the settings of a map element such as a scalebar
|
|
25441
|
+
function layerIsFurniture(lyr) {
|
|
25442
|
+
return !!lyr && !lyr.geometry_type &&
|
|
25443
|
+
furnitureTypes.includes(getFurnitureLayerType(lyr));
|
|
25444
|
+
}
|
|
25445
|
+
|
|
25437
25446
|
/*
|
|
25438
25447
|
{
|
|
25439
25448
|
width: size[0],
|
|
@@ -25629,6 +25638,27 @@
|
|
|
25629
25638
|
});
|
|
25630
25639
|
}
|
|
25631
25640
|
|
|
25641
|
+
// Furniture (scalebars, etc.) belongs to a frame by sharing its dataset.
|
|
25642
|
+
// Furniture in a dataset without a frame is an ordinary standalone layer.
|
|
25643
|
+
function getFrameFurnitureLayers(dataset) {
|
|
25644
|
+
if (!dataset || !findFrameLayerInDataset(dataset)) return [];
|
|
25645
|
+
return dataset.layers.filter(layerIsFurniture);
|
|
25646
|
+
}
|
|
25647
|
+
|
|
25648
|
+
function findFrameFurnitureLayer(dataset, type) {
|
|
25649
|
+
return getFrameFurnitureLayers(dataset).find(function(lyr) {
|
|
25650
|
+
return getFurnitureLayerType(lyr) == type;
|
|
25651
|
+
}) || null;
|
|
25652
|
+
}
|
|
25653
|
+
|
|
25654
|
+
// True for a map frame and for the furniture that belongs to it: layers that
|
|
25655
|
+
// describe the map layout rather than holding map content.
|
|
25656
|
+
function isFrameComponentLayer(lyr, dataset) {
|
|
25657
|
+
if (!dataset) return false;
|
|
25658
|
+
if (isFrameLayer(lyr, dataset.arcs)) return true;
|
|
25659
|
+
return layerIsFurniture(lyr) && !!findFrameLayerInDataset(dataset);
|
|
25660
|
+
}
|
|
25661
|
+
|
|
25632
25662
|
function findFrames(catalog) {
|
|
25633
25663
|
return catalog.getLayers().filter(function(o) {
|
|
25634
25664
|
return isFrameLayer(o.layer, o.dataset.arcs);
|
|
@@ -25855,6 +25885,7 @@
|
|
|
25855
25885
|
demoteFrameLayer: demoteFrameLayer,
|
|
25856
25886
|
findFrame: findFrame,
|
|
25857
25887
|
findFrameDataset: findFrameDataset,
|
|
25888
|
+
findFrameFurnitureLayer: findFrameFurnitureLayer,
|
|
25858
25889
|
findFrameLayer: findFrameLayer,
|
|
25859
25890
|
findFrameLayerInDataset: findFrameLayerInDataset,
|
|
25860
25891
|
findFrames: findFrames,
|
|
@@ -25863,11 +25894,13 @@
|
|
|
25863
25894
|
frameReservedFields: frameReservedFields,
|
|
25864
25895
|
getActiveFrame: getActiveFrame,
|
|
25865
25896
|
getFrameData: getFrameData,
|
|
25897
|
+
getFrameFurnitureLayers: getFrameFurnitureLayers,
|
|
25866
25898
|
getFrameLayerBounds: getFrameLayerBounds,
|
|
25867
25899
|
getFrameLayerData: getFrameLayerData,
|
|
25868
25900
|
getFrameSize: getFrameSize,
|
|
25869
25901
|
getMapFrameMetersPerPixel: getMapFrameMetersPerPixel,
|
|
25870
25902
|
getSingleFrameRecord: getSingleFrameRecord,
|
|
25903
|
+
isFrameComponentLayer: isFrameComponentLayer,
|
|
25871
25904
|
isFrameLayer: isFrameLayer,
|
|
25872
25905
|
isFrameReservedField: isFrameReservedField,
|
|
25873
25906
|
parseFrameSize: parseFrameSize,
|
|
@@ -25875,12 +25908,53 @@
|
|
|
25875
25908
|
resolveExportFrame: resolveExportFrame
|
|
25876
25909
|
});
|
|
25877
25910
|
|
|
25911
|
+
// Adds a furniture layer as a standalone dataset, without changing the
|
|
25912
|
+
// default target (so a following -o still exports the content layers)
|
|
25878
25913
|
function addFurnitureLayer(lyr, catalog) {
|
|
25879
25914
|
var o = {
|
|
25880
25915
|
info: {},
|
|
25881
25916
|
layers: [lyr]
|
|
25882
25917
|
};
|
|
25918
|
+
catalog.captureCatalogBefore({operation: 'addFurnitureLayer'});
|
|
25883
25919
|
catalog.getDatasets().push(o);
|
|
25920
|
+
catalog.markCatalogChanged({operation: 'addFurnitureLayer'});
|
|
25921
|
+
}
|
|
25922
|
+
|
|
25923
|
+
// Adds a furniture layer to a frame's dataset, replacing any existing
|
|
25924
|
+
// furniture of the same type. Passing a null layer removes it.
|
|
25925
|
+
// Returns the replaced or removed layer, if any
|
|
25926
|
+
function setFrameFurnitureLayer(frameDataset, type, lyr) {
|
|
25927
|
+
var layers = frameDataset.layers;
|
|
25928
|
+
var idx = layers.findIndex(function(o) {
|
|
25929
|
+
return layerIsFurniture(o) && getFurnitureLayerType(o) == type;
|
|
25930
|
+
});
|
|
25931
|
+
var prev = idx > -1 ? layers[idx] : null;
|
|
25932
|
+
if (!prev && !lyr) return null;
|
|
25933
|
+
noteDatasetWillChange(frameDataset, {operation: 'setFrameFurniture', unit: 'layers'});
|
|
25934
|
+
layers = layers.slice();
|
|
25935
|
+
if (prev && lyr) {
|
|
25936
|
+
lyr.name = prev.name;
|
|
25937
|
+
layers[idx] = lyr;
|
|
25938
|
+
} else if (prev) {
|
|
25939
|
+
layers.splice(idx, 1);
|
|
25940
|
+
} else {
|
|
25941
|
+
layers.push(lyr);
|
|
25942
|
+
}
|
|
25943
|
+
frameDataset.layers = layers;
|
|
25944
|
+
markDatasetChanged(frameDataset, {operation: 'setFrameFurniture', unit: 'layers'});
|
|
25945
|
+
return prev;
|
|
25946
|
+
}
|
|
25947
|
+
|
|
25948
|
+
// Moves a replaced frame's furniture into the dataset of the frame replacing it
|
|
25949
|
+
function moveFrameFurniture(fromDataset, toDataset) {
|
|
25950
|
+
var furniture = fromDataset.layers.filter(layerIsFurniture);
|
|
25951
|
+
if (furniture.length === 0) return;
|
|
25952
|
+
noteDatasetWillChange(fromDataset, {operation: 'moveFrameFurniture', unit: 'layers'});
|
|
25953
|
+
fromDataset.layers = fromDataset.layers.filter(function(lyr) {
|
|
25954
|
+
return !furniture.includes(lyr);
|
|
25955
|
+
});
|
|
25956
|
+
markDatasetChanged(fromDataset, {operation: 'moveFrameFurniture', unit: 'layers'});
|
|
25957
|
+
toDataset.layers = toDataset.layers.concat(furniture);
|
|
25884
25958
|
}
|
|
25885
25959
|
|
|
25886
25960
|
// Pure geometry-to-SVG-path primitives. Extracted from geojson-to-svg.mjs
|
|
@@ -26171,10 +26245,10 @@
|
|
|
26171
26245
|
parts.unshift('hatches');
|
|
26172
26246
|
obj = parseHatches(parts); // hatches is the default, name can be omitted
|
|
26173
26247
|
}
|
|
26174
|
-
|
|
26175
|
-
|
|
26176
|
-
|
|
26177
|
-
|
|
26248
|
+
// Silent: this is also how a value is tested for being a pattern at all
|
|
26249
|
+
// (-style tries every value as a literal before trying it as an
|
|
26250
|
+
// expression), so a failure here is not necessarily a mistake. The SVG
|
|
26251
|
+
// exporter reports the patterns it has to drop.
|
|
26178
26252
|
return obj;
|
|
26179
26253
|
}
|
|
26180
26254
|
|
|
@@ -26314,7 +26388,10 @@
|
|
|
26314
26388
|
function makeSVGPatternFill(str, id) {
|
|
26315
26389
|
var o = parsePattern(str);
|
|
26316
26390
|
var svg;
|
|
26317
|
-
if (!o)
|
|
26391
|
+
if (!o) {
|
|
26392
|
+
message('Invalid pattern, ignoring:', str);
|
|
26393
|
+
return null;
|
|
26394
|
+
}
|
|
26318
26395
|
if (o.type == 'hatches') {
|
|
26319
26396
|
svg = makeHatchPatternSVG(o);
|
|
26320
26397
|
} else if (o.type == 'dots' || o.type == 'squares') {
|
|
@@ -27309,6 +27386,126 @@
|
|
|
27309
27386
|
splitLabelHalos: splitLabelHalos
|
|
27310
27387
|
});
|
|
27311
27388
|
|
|
27389
|
+
// Bold words inside a label: label-text='Israel <b>restricted access</b> here'.
|
|
27390
|
+
//
|
|
27391
|
+
// The markup is part of the text rather than a column beside it, so that the
|
|
27392
|
+
// words and their weight cannot drift apart: an expression, a join or a -labels
|
|
27393
|
+
// text= that rewrites the text rewrites the bold with it. The GUI editor never
|
|
27394
|
+
// shows the tags -- it reads them into ranges and writes them back.
|
|
27395
|
+
//
|
|
27396
|
+
// Only a matched <b>...</b> pair with no other b tag inside it is markup.
|
|
27397
|
+
// Anything else -- an unclosed <b>, a stray </b>, "a < b" -- is text, and is
|
|
27398
|
+
// drawn as typed.
|
|
27399
|
+
//
|
|
27400
|
+
// See docs/development/label-tool-design.md.
|
|
27401
|
+
|
|
27402
|
+
var BOLD_PAIR_RXP = /<b>((?:(?!<\/?b>)[\s\S])*?)<\/b>/gi;
|
|
27403
|
+
var BOLD_TAG_RXP = /<\/?b>/i;
|
|
27404
|
+
|
|
27405
|
+
// The font-weight a bold run is drawn with: the keyword, which a browser and
|
|
27406
|
+
// the Node text measurer both resolve to the family's own bold face.
|
|
27407
|
+
var LABEL_BOLD_WEIGHT = 'bold';
|
|
27408
|
+
|
|
27409
|
+
// @str -> {text, bold}: the text with its bold pairs removed, and the bold
|
|
27410
|
+
// stretches as ascending, non-overlapping [start, end) offsets into it.
|
|
27411
|
+
function parseBoldMarkup(str) {
|
|
27412
|
+
var s = str === null || str === undefined ? '' : String(str);
|
|
27413
|
+
var text = '', bold = [], last = 0, m;
|
|
27414
|
+
if (!BOLD_TAG_RXP.test(s)) return {text: s, bold: bold};
|
|
27415
|
+
BOLD_PAIR_RXP.lastIndex = 0;
|
|
27416
|
+
while ((m = BOLD_PAIR_RXP.exec(s))) {
|
|
27417
|
+
text += s.substring(last, m.index);
|
|
27418
|
+
if (m[1]) {
|
|
27419
|
+
bold.push([text.length, text.length + m[1].length]);
|
|
27420
|
+
text += m[1];
|
|
27421
|
+
}
|
|
27422
|
+
last = m.index + m[0].length;
|
|
27423
|
+
}
|
|
27424
|
+
text += s.substring(last);
|
|
27425
|
+
return {text: text, bold: normalizeBoldRanges(bold)};
|
|
27426
|
+
}
|
|
27427
|
+
|
|
27428
|
+
function stripBoldMarkup(str) {
|
|
27429
|
+
return parseBoldMarkup(str).text;
|
|
27430
|
+
}
|
|
27431
|
+
|
|
27432
|
+
// Sorted, clipped to non-empty, with touching and overlapping ranges merged.
|
|
27433
|
+
// Returns a new array.
|
|
27434
|
+
function normalizeBoldRanges(ranges) {
|
|
27435
|
+
var sorted = (ranges || []).filter(function(r) {
|
|
27436
|
+
return r && r[1] > r[0];
|
|
27437
|
+
}).map(function(r) {
|
|
27438
|
+
return [r[0], r[1]];
|
|
27439
|
+
}).sort(function(a, b) {
|
|
27440
|
+
return a[0] - b[0];
|
|
27441
|
+
});
|
|
27442
|
+
var out = [];
|
|
27443
|
+
sorted.forEach(function(r) {
|
|
27444
|
+
var prev = out[out.length - 1];
|
|
27445
|
+
if (prev && r[0] <= prev[1]) {
|
|
27446
|
+
prev[1] = Math.max(prev[1], r[1]);
|
|
27447
|
+
} else {
|
|
27448
|
+
out.push(r);
|
|
27449
|
+
}
|
|
27450
|
+
});
|
|
27451
|
+
return out;
|
|
27452
|
+
}
|
|
27453
|
+
|
|
27454
|
+
// The characters [start, end) of @text as runs of one weight:
|
|
27455
|
+
// [{text, bold}, ...], with nothing for an empty stretch.
|
|
27456
|
+
function getBoldRuns(text, bold, start, end) {
|
|
27457
|
+
var runs = [], pos = start, i, r, a, b;
|
|
27458
|
+
for (i = 0; i < bold.length && pos < end; i++) {
|
|
27459
|
+
r = bold[i];
|
|
27460
|
+
a = Math.max(r[0], pos);
|
|
27461
|
+
b = Math.min(r[1], end);
|
|
27462
|
+
if (b <= a) continue;
|
|
27463
|
+
if (a > pos) runs.push({text: text.substring(pos, a), bold: false});
|
|
27464
|
+
runs.push({text: text.substring(a, b), bold: true});
|
|
27465
|
+
pos = b;
|
|
27466
|
+
}
|
|
27467
|
+
if (pos < end) runs.push({text: text.substring(pos, end), bold: false});
|
|
27468
|
+
return runs;
|
|
27469
|
+
}
|
|
27470
|
+
|
|
27471
|
+
// @runs -> @obj with the first plain run as its value and the rest as <tspan>s,
|
|
27472
|
+
// which is the shape the stringifier writes as mixed content. A label with no
|
|
27473
|
+
// bold in it comes out exactly as it did before bold existed: one value and no
|
|
27474
|
+
// children. Returns @obj.
|
|
27475
|
+
function applyRunsToSvgObject(obj, runs) {
|
|
27476
|
+
var i = 0;
|
|
27477
|
+
var children = [];
|
|
27478
|
+
if (runs.length > 0 && !runs[0].bold) {
|
|
27479
|
+
obj.value = runs[0].text;
|
|
27480
|
+
i = 1;
|
|
27481
|
+
} else {
|
|
27482
|
+
obj.value = '';
|
|
27483
|
+
}
|
|
27484
|
+
for (; i < runs.length; i++) {
|
|
27485
|
+
children.push(renderRun(runs[i]));
|
|
27486
|
+
}
|
|
27487
|
+
if (children.length > 0) {
|
|
27488
|
+
obj.children = children.concat(obj.children || []);
|
|
27489
|
+
}
|
|
27490
|
+
return obj;
|
|
27491
|
+
}
|
|
27492
|
+
|
|
27493
|
+
function renderRun(run) {
|
|
27494
|
+
var o = {tag: 'tspan', value: run.text};
|
|
27495
|
+
if (run.bold) o.properties = {'font-weight': LABEL_BOLD_WEIGHT};
|
|
27496
|
+
return o;
|
|
27497
|
+
}
|
|
27498
|
+
|
|
27499
|
+
var SvgLabelMarkup = /*#__PURE__*/Object.freeze({
|
|
27500
|
+
__proto__: null,
|
|
27501
|
+
LABEL_BOLD_WEIGHT: LABEL_BOLD_WEIGHT,
|
|
27502
|
+
applyRunsToSvgObject: applyRunsToSvgObject,
|
|
27503
|
+
getBoldRuns: getBoldRuns,
|
|
27504
|
+
normalizeBoldRanges: normalizeBoldRanges,
|
|
27505
|
+
parseBoldMarkup: parseBoldMarkup,
|
|
27506
|
+
stripBoldMarkup: stripBoldMarkup
|
|
27507
|
+
});
|
|
27508
|
+
|
|
27312
27509
|
// Accepting \n (two chars) as an alternative to the newline character
|
|
27313
27510
|
// (sometimes, '\n' is not converted to newline, e.g. in a Makefile)
|
|
27314
27511
|
// Also accepting <br>
|
|
@@ -27325,21 +27522,45 @@
|
|
|
27325
27522
|
var anyLabelBreakRxp = /(\n|\\n|<br>|<wbr>)/i;
|
|
27326
27523
|
var softBreakRxp = /^<wbr>$/i;
|
|
27327
27524
|
|
|
27328
|
-
// The lines a label's text is drawn as
|
|
27329
|
-
// dropped: a trailing space would move an end- or middle-justified line by a
|
|
27330
|
-
// space's width.
|
|
27525
|
+
// The lines a label's text is drawn as, without its bold markup.
|
|
27331
27526
|
function splitLabelLines(text) {
|
|
27332
|
-
|
|
27333
|
-
|
|
27527
|
+
return splitLabelLineRuns(text).map(function(runs) {
|
|
27528
|
+
return runs.map(function(run) { return run.text; }).join('');
|
|
27529
|
+
});
|
|
27530
|
+
}
|
|
27531
|
+
|
|
27532
|
+
// The lines a label's text is drawn as, each one a list of runs of one weight:
|
|
27533
|
+
// [[{text, bold}, ...], ...]. A bold stretch can cross a line break, and is
|
|
27534
|
+
// then a run on each line.
|
|
27535
|
+
//
|
|
27536
|
+
// Whitespace before a soft break is dropped: a trailing space would move an
|
|
27537
|
+
// end- or middle-justified line by a space's width.
|
|
27538
|
+
function splitLabelLineRuns(text) {
|
|
27539
|
+
var o = parseBoldMarkup(String(text));
|
|
27540
|
+
var parts = o.text.split(anyLabelBreakRxp);
|
|
27541
|
+
var pos = parts[0].length;
|
|
27542
|
+
var lines = [getBoldRuns(o.text, o.bold, 0, pos)];
|
|
27334
27543
|
for (var i = 1; i < parts.length; i += 2) {
|
|
27335
27544
|
if (softBreakRxp.test(parts[i])) {
|
|
27336
|
-
lines[lines.length - 1]
|
|
27545
|
+
trimTrailingSpace(lines[lines.length - 1]);
|
|
27337
27546
|
}
|
|
27338
|
-
|
|
27547
|
+
pos += parts[i].length;
|
|
27548
|
+
lines.push(getBoldRuns(o.text, o.bold, pos, pos + parts[i + 1].length));
|
|
27549
|
+
pos += parts[i + 1].length;
|
|
27339
27550
|
}
|
|
27340
27551
|
return lines;
|
|
27341
27552
|
}
|
|
27342
27553
|
|
|
27554
|
+
function trimTrailingSpace(runs) {
|
|
27555
|
+
var last;
|
|
27556
|
+
while (runs.length > 0) {
|
|
27557
|
+
last = runs[runs.length - 1];
|
|
27558
|
+
last.text = last.text.replace(/\s+$/, '');
|
|
27559
|
+
if (last.text) return;
|
|
27560
|
+
runs.pop();
|
|
27561
|
+
}
|
|
27562
|
+
}
|
|
27563
|
+
|
|
27343
27564
|
function removeSoftBreaks(text) {
|
|
27344
27565
|
return String(text).replace(/<wbr>/gi, '');
|
|
27345
27566
|
}
|
|
@@ -27390,7 +27611,7 @@
|
|
|
27390
27611
|
// already been through it, so calling it here as well costs nothing and means
|
|
27391
27612
|
// every way into the renderer draws a label in the position it is stored in.
|
|
27392
27613
|
var rec = resolveLabelPosition(recArg);
|
|
27393
|
-
var morelines =
|
|
27614
|
+
var morelines = splitLabelLineRuns(toLabelString(rec['label-text']));
|
|
27394
27615
|
var line = morelines.shift();
|
|
27395
27616
|
var obj;
|
|
27396
27617
|
var dx = applyAlignmentShift(rec);
|
|
@@ -27401,23 +27622,21 @@
|
|
|
27401
27622
|
y: dy,
|
|
27402
27623
|
x: dx
|
|
27403
27624
|
};
|
|
27404
|
-
obj = {
|
|
27625
|
+
obj = applyRunsToSvgObject({
|
|
27405
27626
|
tag: 'text',
|
|
27406
|
-
value: line,
|
|
27407
27627
|
properties: properties
|
|
27408
|
-
};
|
|
27628
|
+
}, line);
|
|
27409
27629
|
if (morelines.length > 0) {
|
|
27410
27630
|
// multiline label
|
|
27411
|
-
obj.children = [];
|
|
27631
|
+
obj.children = obj.children || [];
|
|
27412
27632
|
morelines.forEach(function(line) {
|
|
27413
|
-
var tspan = {
|
|
27633
|
+
var tspan = applyRunsToSvgObject({
|
|
27414
27634
|
tag: 'tspan',
|
|
27415
|
-
value: line,
|
|
27416
27635
|
properties: {
|
|
27417
27636
|
x: dx,
|
|
27418
27637
|
dy: getLineHeightDy(rec['line-height'])
|
|
27419
27638
|
}
|
|
27420
|
-
};
|
|
27639
|
+
}, line);
|
|
27421
27640
|
obj.children.push(tspan);
|
|
27422
27641
|
});
|
|
27423
27642
|
}
|
|
@@ -27517,6 +27736,7 @@
|
|
|
27517
27736
|
removeSoftBreaks: removeSoftBreaks,
|
|
27518
27737
|
renderLabel: renderLabel$1,
|
|
27519
27738
|
renderStyledLabel: renderStyledLabel,
|
|
27739
|
+
splitLabelLineRuns: splitLabelLineRuns,
|
|
27520
27740
|
splitLabelLines: splitLabelLines,
|
|
27521
27741
|
toLabelString: toLabelString,
|
|
27522
27742
|
toPixels: toPixels
|
|
@@ -28425,22 +28645,60 @@
|
|
|
28425
28645
|
symbolRenderers: symbolRenderers
|
|
28426
28646
|
});
|
|
28427
28647
|
|
|
28648
|
+
// Command options that are stored in the scalebar's data record
|
|
28649
|
+
var scalebarFields = ['label', 'units', 'style', 'font_size', 'font_family',
|
|
28650
|
+
'font_style', 'font_weight', 'color', 'tic_length', 'bar_width', 'label_offset',
|
|
28651
|
+
'position', 'label_position', 'dual_units', 'margin'];
|
|
28652
|
+
|
|
28653
|
+
// With a map frame, the scalebar belongs to the frame, and running the
|
|
28654
|
+
// command again replaces it. Without a frame, each run adds a standalone
|
|
28655
|
+
// scalebar layer, which is exported if it is included in the output layers.
|
|
28428
28656
|
cmd.scalebar = function(catalog, opts) {
|
|
28657
|
+
var frame = getActiveFrame(catalog);
|
|
28658
|
+
if (opts.remove) {
|
|
28659
|
+
if (!frame || !setFrameFurnitureLayer(frame.dataset, 'scalebar', null)) {
|
|
28660
|
+
stop$1('The map frame has no scalebar to remove');
|
|
28661
|
+
}
|
|
28662
|
+
return;
|
|
28663
|
+
}
|
|
28664
|
+
validateScalebarOpts(opts);
|
|
28429
28665
|
var lyr = getScalebarLayer(opts);
|
|
28430
|
-
if (
|
|
28431
|
-
|
|
28666
|
+
if (frame) {
|
|
28667
|
+
setFrameFurnitureLayer(frame.dataset, 'scalebar', lyr);
|
|
28668
|
+
} else {
|
|
28669
|
+
addFurnitureLayer(lyr, catalog);
|
|
28432
28670
|
}
|
|
28433
|
-
addFurnitureLayer(lyr, catalog);
|
|
28434
28671
|
};
|
|
28435
28672
|
|
|
28436
28673
|
function getScalebarLayer(opts) {
|
|
28437
|
-
var
|
|
28674
|
+
var rec = {type: 'scalebar'};
|
|
28675
|
+
scalebarFields.forEach(function(k) {
|
|
28676
|
+
if (opts[k] !== undefined && opts[k] !== null && opts[k] !== '') {
|
|
28677
|
+
rec[k] = opts[k];
|
|
28678
|
+
}
|
|
28679
|
+
});
|
|
28438
28680
|
return {
|
|
28439
|
-
name:
|
|
28440
|
-
data: new DataTable([
|
|
28681
|
+
name: 'scalebar',
|
|
28682
|
+
data: new DataTable([rec])
|
|
28441
28683
|
};
|
|
28442
28684
|
}
|
|
28443
28685
|
|
|
28686
|
+
function validateScalebarOpts(opts) {
|
|
28687
|
+
if (opts.label) {
|
|
28688
|
+
splitScalebarLabel(opts.label).forEach(function(str) {
|
|
28689
|
+
if (!parseScalebarUnits(str) && !isBareDistance(str)) {
|
|
28690
|
+
stop$1(`Expected a distance or units of km, meters, miles or feet in scalebar label (received ${str})`);
|
|
28691
|
+
}
|
|
28692
|
+
});
|
|
28693
|
+
}
|
|
28694
|
+
if (opts.units && !parseLabelUnits(opts.units)) {
|
|
28695
|
+
stop$1('Unsupported scalebar units:', opts.units);
|
|
28696
|
+
}
|
|
28697
|
+
if (opts.style && !/^[ab]$/i.test(opts.style)) {
|
|
28698
|
+
stop$1('Unsupported scalebar style:', opts.style);
|
|
28699
|
+
}
|
|
28700
|
+
}
|
|
28701
|
+
|
|
28444
28702
|
function renderScalebar(d, frame) {
|
|
28445
28703
|
if (!frame.crs) {
|
|
28446
28704
|
message('Unable to render scalebar: unknown CRS.');
|
|
@@ -28453,14 +28711,15 @@
|
|
|
28453
28711
|
var opts = getScalebarOpts(d);
|
|
28454
28712
|
var metersPerPx = getMapFrameMetersPerPixel(frame);
|
|
28455
28713
|
var frameWidthPx = frame.width;
|
|
28456
|
-
var labels = d.label ? d.label
|
|
28457
|
-
var
|
|
28458
|
-
var
|
|
28459
|
-
var
|
|
28714
|
+
var labels = d.label ? splitScalebarLabel(d.label) : [];
|
|
28715
|
+
var units = parseLabelUnits(d.units) || 'mile';
|
|
28716
|
+
var label1 = getScalebarLabel(labels[0], frameWidthPx, metersPerPx, units);
|
|
28717
|
+
var props1 = parseScalebarLabel(label1);
|
|
28718
|
+
var label2 = null;
|
|
28460
28719
|
var props2, length2;
|
|
28461
28720
|
|
|
28462
28721
|
if (props1.km > 0 === false) {
|
|
28463
|
-
message('Unusable scalebar label:',
|
|
28722
|
+
message('Unusable scalebar label:', labels[0] || '');
|
|
28464
28723
|
return [];
|
|
28465
28724
|
}
|
|
28466
28725
|
|
|
@@ -28469,6 +28728,11 @@
|
|
|
28469
28728
|
stop$1("Null scalebar length");
|
|
28470
28729
|
}
|
|
28471
28730
|
|
|
28731
|
+
if (opts.style == 'b' && labels[1]) {
|
|
28732
|
+
label2 = completeLabel(labels[1], getOtherUnits(props1.units));
|
|
28733
|
+
} else if (opts.style == 'b' && opts.dual_units) {
|
|
28734
|
+
label2 = getAutoSecondLabel(length1, metersPerPx, getOtherUnits(props1.units));
|
|
28735
|
+
}
|
|
28472
28736
|
if (label2) {
|
|
28473
28737
|
props2 = parseScalebarLabel(label2);
|
|
28474
28738
|
length2 = Math.round(props2.km / metersPerPx * 1000);
|
|
@@ -28521,8 +28785,13 @@
|
|
|
28521
28785
|
};
|
|
28522
28786
|
|
|
28523
28787
|
function getScalebarOpts(d) {
|
|
28524
|
-
var style = d.style == 'b' || d.style == 'B' ? 'b' : 'a';
|
|
28525
|
-
|
|
28788
|
+
var style = d.style == 'b' || d.style == 'B' || !d.style && d.dual_units ? 'b' : 'a';
|
|
28789
|
+
var opts = Object.assign({}, defaultOpts, styleOpts[style]);
|
|
28790
|
+
Object.keys(d).forEach(function(k) {
|
|
28791
|
+
if (d[k] !== undefined && d[k] !== null && d[k] !== '') opts[k] = d[k];
|
|
28792
|
+
});
|
|
28793
|
+
opts.style = style;
|
|
28794
|
+
return opts;
|
|
28526
28795
|
}
|
|
28527
28796
|
|
|
28528
28797
|
function renderAsSvg(length, text, length2, text2, opts) {
|
|
@@ -28540,12 +28809,11 @@
|
|
|
28540
28809
|
};
|
|
28541
28810
|
}
|
|
28542
28811
|
|
|
28543
|
-
// TODO: generalize to other kinds of furniture as they are developed
|
|
28544
28812
|
function getScalebarPosition(opts) {
|
|
28545
28813
|
var pos = opts.position || 'top-left';
|
|
28546
28814
|
return {
|
|
28547
|
-
ypos: pos.includes('
|
|
28548
|
-
xpos: pos.includes('
|
|
28815
|
+
ypos: pos.includes('bottom') ? 'bottom' : 'top',
|
|
28816
|
+
xpos: pos.includes('right') ? 'right' : 'left'
|
|
28549
28817
|
};
|
|
28550
28818
|
}
|
|
28551
28819
|
|
|
@@ -28559,12 +28827,16 @@
|
|
|
28559
28827
|
'label-text': text,
|
|
28560
28828
|
'font-size': opts.font_size,
|
|
28561
28829
|
'text-anchor': labelPos.anchor,
|
|
28562
|
-
'dominant-baseline': labelPos.ypos == 'top' ? 'auto' : 'hanging'
|
|
28830
|
+
'dominant-baseline': labelPos.ypos == 'top' ? 'auto' : 'hanging',
|
|
28563
28831
|
//// 'dominant-baseline': labelPos == 'top' ? 'text-after-edge' : 'text-before-edge'
|
|
28564
28832
|
// 'text-after-edge' is buggy in Safari and unsupported by Illustrator,
|
|
28565
28833
|
// so I'm using 'hanging' and 'auto', which seem to be well supported.
|
|
28566
28834
|
// downside: requires a kludgy multiplier to calculate scalebar height (see above)
|
|
28567
28835
|
};
|
|
28836
|
+
if (opts.color) labelOpts.fill = opts.color;
|
|
28837
|
+
if (opts.font_family) labelOpts['font-family'] = opts.font_family;
|
|
28838
|
+
if (opts.font_style) labelOpts['font-style'] = opts.font_style;
|
|
28839
|
+
if (opts.font_weight) labelOpts['font-weight'] = opts.font_weight;
|
|
28568
28840
|
return symbolRenderers.label(labelOpts, anchorX, anchorY);
|
|
28569
28841
|
}
|
|
28570
28842
|
|
|
@@ -28624,7 +28896,7 @@
|
|
|
28624
28896
|
}
|
|
28625
28897
|
var bar = importMultiLineString(coords);
|
|
28626
28898
|
Object.assign(bar.properties, {
|
|
28627
|
-
stroke: 'black',
|
|
28899
|
+
stroke: opts.color || 'black',
|
|
28628
28900
|
fill: 'none',
|
|
28629
28901
|
'stroke-width': opts.bar_width,
|
|
28630
28902
|
'stroke-linecap': 'butt',
|
|
@@ -28633,35 +28905,132 @@
|
|
|
28633
28905
|
return bar;
|
|
28634
28906
|
}
|
|
28635
28907
|
|
|
28636
|
-
//
|
|
28908
|
+
// Kilometers per unit
|
|
28909
|
+
var unitSizes = {
|
|
28910
|
+
km: 1,
|
|
28911
|
+
m: 0.001,
|
|
28912
|
+
mile: 1.60934,
|
|
28913
|
+
ft: 0.0003048
|
|
28914
|
+
};
|
|
28915
|
+
|
|
28916
|
+
// Candidate lengths for automatic labels, in ascending order of distance
|
|
28917
|
+
var autoLengths = {
|
|
28918
|
+
imperial: [
|
|
28919
|
+
['ft', '10 20 25 50 100 200 250 500'],
|
|
28920
|
+
// note: removed 1.5 12 and 1,200
|
|
28921
|
+
['mile', '1/8 1/5 1/4 1/2 1 2 3 4 5 8 10 15 20 25 30 40 50 75 100 150 200 250 ' +
|
|
28922
|
+
'300 350 400 500 750 1,000 1,500 2,000 2,500 3,000 4,000 5,000']
|
|
28923
|
+
],
|
|
28924
|
+
metric: [
|
|
28925
|
+
['m', '5 10 20 25 50 100 200 250 500'],
|
|
28926
|
+
['km', '1 2 3 4 5 8 10 15 20 25 30 40 50 75 100 150 200 250 300 350 ' +
|
|
28927
|
+
'400 500 750 1,000 1,500 2,000 2,500 3,000 4,000 5,000']
|
|
28928
|
+
]
|
|
28929
|
+
};
|
|
28930
|
+
|
|
28931
|
+
function getAutoLabels(system) {
|
|
28932
|
+
return autoLengths[system].reduce(function(memo, group) {
|
|
28933
|
+
return memo.concat(group[1].split(' ').map(function(str) {
|
|
28934
|
+
return formatDistanceLabel(str, group[0]);
|
|
28935
|
+
}));
|
|
28936
|
+
}, []);
|
|
28937
|
+
}
|
|
28938
|
+
|
|
28939
|
+
function getUnitSystem(units) {
|
|
28940
|
+
return units == 'km' || units == 'm' ? 'metric' : 'imperial';
|
|
28941
|
+
}
|
|
28942
|
+
|
|
28943
|
+
function getOtherUnits(units) {
|
|
28944
|
+
return getUnitSystem(units) == 'metric' ? 'mile' : 'km';
|
|
28945
|
+
}
|
|
28946
|
+
|
|
28947
|
+
// The unit system of the units= option: returns 'km', 'mile' or ''
|
|
28948
|
+
function parseUnitsOption(str) {
|
|
28949
|
+
var units = parseLabelUnits(str);
|
|
28950
|
+
return units ? (getUnitSystem(units) == 'metric' ? 'km' : 'mile') : '';
|
|
28951
|
+
}
|
|
28952
|
+
|
|
28953
|
+
// Accepts the units= option, which also gives the units of a label that is a
|
|
28954
|
+
// bare number; returns 'km', 'm', 'mile', 'ft' or ''
|
|
28955
|
+
function parseLabelUnits(str) {
|
|
28956
|
+
var s = String(str || '').toLowerCase();
|
|
28957
|
+
if (/^(km|kilomet(er|re)s?|metric)$/.test(s)) return 'km';
|
|
28958
|
+
if (/^(m|meters?|metres?)$/.test(s)) return 'm';
|
|
28959
|
+
if (/^(mi|miles?|imperial)$/.test(s)) return 'mile';
|
|
28960
|
+
if (/^(ft|feet)$/.test(s)) return 'ft';
|
|
28961
|
+
return '';
|
|
28962
|
+
}
|
|
28963
|
+
|
|
28964
|
+
// Splits a dual-unit label like "100 km,50 miles". Only a comma after the units
|
|
28965
|
+
// separates two distances, so "1,000 km" and a bare "1,500" stay whole.
|
|
28966
|
+
function splitScalebarLabel(label) {
|
|
28967
|
+
return String(label).split(/(?<=[^\d\s]),\s*/).map(function(str) {
|
|
28968
|
+
return str.trim();
|
|
28969
|
+
}).filter(Boolean);
|
|
28970
|
+
}
|
|
28971
|
+
|
|
28972
|
+
// A distance with no units, e.g. "150" or "1/2" or "1,000"
|
|
28973
|
+
function isBareDistance(str) {
|
|
28974
|
+
return /^[\d\s.,/]+$/.test(str) && parseScalebarNumber(str) > 0;
|
|
28975
|
+
}
|
|
28976
|
+
|
|
28977
|
+
// Adds units to a label that is a bare number
|
|
28978
|
+
function completeLabel(label, units) {
|
|
28979
|
+
return isBareDistance(label) ? formatDistanceLabel(label.trim(), units) : label;
|
|
28980
|
+
}
|
|
28981
|
+
|
|
28982
|
+
// label: a label from the label= option, or empty for an automatic label
|
|
28983
|
+
function getScalebarLabel(label, frameWidthPx, metersPerPx, defaultUnits) {
|
|
28984
|
+
var units;
|
|
28985
|
+
if (label && parseScalebarNumber(label) > 0) return completeLabel(label, defaultUnits);
|
|
28986
|
+
// A label with units but no number gets an automatic length in those units
|
|
28987
|
+
units = label && parseScalebarUnits(label) || defaultUnits;
|
|
28988
|
+
return getAutoScalebarLabel(frameWidthPx, metersPerPx, units);
|
|
28989
|
+
}
|
|
28990
|
+
|
|
28991
|
+
// unit: 'km' || 'mile' (or 'm', 'ft' for the same systems)
|
|
28992
|
+
// The longest length that fits within 20% of the map's width, or the shortest
|
|
28993
|
+
// that is at least 70px long if the map is too narrow for that
|
|
28637
28994
|
function getAutoScalebarLabel(mapWidth, metersPerPx, unit) {
|
|
28638
|
-
var
|
|
28639
|
-
var
|
|
28640
|
-
|
|
28641
|
-
var
|
|
28642
|
-
|
|
28643
|
-
|
|
28644
|
-
|
|
28645
|
-
|
|
28646
|
-
|
|
28647
|
-
|
|
28648
|
-
|
|
28649
|
-
|
|
28650
|
-
|
|
28995
|
+
var minKm = metersPerPx * 70 / 1000;
|
|
28996
|
+
var maxKm = metersPerPx * mapWidth * 0.2 / 1000;
|
|
28997
|
+
var labels = getAutoLabels(getUnitSystem(unit));
|
|
28998
|
+
var longest = labels.filter(function(label) {
|
|
28999
|
+
var km = parseScalebarLabelToKm(label);
|
|
29000
|
+
return km >= minKm && km <= maxKm;
|
|
29001
|
+
}).pop();
|
|
29002
|
+
return longest || labels.find(function(label) {
|
|
29003
|
+
return parseScalebarLabelToKm(label) >= minKm;
|
|
29004
|
+
}) || '';
|
|
29005
|
+
}
|
|
29006
|
+
|
|
29007
|
+
// The longest automatic length in the given units that fits within the
|
|
29008
|
+
// first part of a dual-unit scalebar
|
|
29009
|
+
function getAutoSecondLabel(maxPx, metersPerPx, unit) {
|
|
29010
|
+
var maxKm = maxPx * metersPerPx / 1000;
|
|
29011
|
+
var minKm = maxKm / 4;
|
|
29012
|
+
var labels = getAutoLabels(getUnitSystem(unit)).filter(function(label) {
|
|
29013
|
+
var km = parseScalebarLabelToKm(label);
|
|
29014
|
+
return km <= maxKm && km >= minKm;
|
|
29015
|
+
});
|
|
29016
|
+
return labels.pop() || null;
|
|
28651
29017
|
}
|
|
28652
29018
|
|
|
28653
29019
|
function formatDistanceLabel(numStr, unit) {
|
|
28654
29020
|
var num = parseScalebarNumber(numStr);
|
|
28655
|
-
var unitStr = unit == 'km' && 'KM' ||
|
|
29021
|
+
var unitStr = unit == 'km' && 'KM' ||
|
|
29022
|
+
unit == 'm' && (num > 1 ? 'METERS' : 'METER') ||
|
|
29023
|
+
unit == 'ft' && (num > 1 ? 'FEET' : 'FOOT') ||
|
|
29024
|
+
num > 1 && 'MILES' || 'MILE';
|
|
28656
29025
|
return numStr + ' ' + unitStr;
|
|
28657
29026
|
}
|
|
28658
29027
|
|
|
28659
|
-
// See test/
|
|
29028
|
+
// See test/scalebar-test.mjs for examples of supported formats
|
|
28660
29029
|
function parseScalebarLabelToKm(str) {
|
|
28661
29030
|
var units = parseScalebarUnits(str);
|
|
28662
29031
|
var value = parseScalebarNumber(str);
|
|
28663
29032
|
if (!units || !value) return NaN;
|
|
28664
|
-
return
|
|
29033
|
+
return value * unitSizes[units];
|
|
28665
29034
|
}
|
|
28666
29035
|
|
|
28667
29036
|
function parseScalebarLabel(label) {
|
|
@@ -28669,7 +29038,7 @@
|
|
|
28669
29038
|
var units = label ? parseScalebarUnits(label) : 'mile';
|
|
28670
29039
|
var km = NaN;
|
|
28671
29040
|
if (units && num) {
|
|
28672
|
-
km =
|
|
29041
|
+
km = num * unitSizes[units];
|
|
28673
29042
|
}
|
|
28674
29043
|
return {
|
|
28675
29044
|
number: num,
|
|
@@ -28678,11 +29047,14 @@
|
|
|
28678
29047
|
};
|
|
28679
29048
|
}
|
|
28680
29049
|
|
|
29050
|
+
// Returns 'km', 'm', 'mile', 'ft' or ''
|
|
28681
29051
|
function parseScalebarUnits(str) {
|
|
28682
|
-
var
|
|
28683
|
-
|
|
28684
|
-
|
|
28685
|
-
return
|
|
29052
|
+
var s = String(str).trim().toLowerCase();
|
|
29053
|
+
if (/(miles?|mi[.]?|英里)$/.test(s)) return 'mile';
|
|
29054
|
+
if (/(k\.m\.|km|kilomet(er|re)s?|kilom.tres?|公里)$/.test(s)) return 'km';
|
|
29055
|
+
if (/(^|[\d\s.])(feet|foot|ft[.]?|英尺)$/.test(s)) return 'ft';
|
|
29056
|
+
if (/(^|[\d\s.])(m|m\.|meters?|metres?|mètres?|米)$/.test(s)) return 'm';
|
|
29057
|
+
return '';
|
|
28686
29058
|
}
|
|
28687
29059
|
|
|
28688
29060
|
function parseScalebarNumber(str) {
|
|
@@ -28702,8 +29074,12 @@
|
|
|
28702
29074
|
__proto__: null,
|
|
28703
29075
|
formatDistanceLabel: formatDistanceLabel,
|
|
28704
29076
|
getScalebarLayer: getScalebarLayer,
|
|
29077
|
+
parseLabelUnits: parseLabelUnits,
|
|
28705
29078
|
parseScalebarLabelToKm: parseScalebarLabelToKm,
|
|
28706
|
-
|
|
29079
|
+
parseScalebarUnits: parseScalebarUnits,
|
|
29080
|
+
parseUnitsOption: parseUnitsOption,
|
|
29081
|
+
renderScalebar: renderScalebar,
|
|
29082
|
+
splitScalebarLabel: splitScalebarLabel
|
|
28707
29083
|
});
|
|
28708
29084
|
|
|
28709
29085
|
var furnitureRenderers = {
|
|
@@ -28725,24 +29101,33 @@
|
|
|
28725
29101
|
function renderFurnitureLayer(lyr, frame) {
|
|
28726
29102
|
var d = getFurnitureLayerData(lyr);
|
|
28727
29103
|
var renderer = furnitureRenderers[d.type];
|
|
29104
|
+
var problem;
|
|
28728
29105
|
if (!renderer) {
|
|
28729
29106
|
stop$1('Missing renderer for', d.type, 'element');
|
|
28730
29107
|
}
|
|
28731
|
-
|
|
28732
|
-
|
|
28733
|
-
|
|
28734
|
-
if (!isProjectedCRS(frame.crs)) {
|
|
28735
|
-
stop$1(`Unable to render ${d.type} (map is unprojected)`);
|
|
29108
|
+
problem = getFurnitureFrameProblem(frame);
|
|
29109
|
+
if (problem) {
|
|
29110
|
+
stop$1(`Unable to render ${d.type} (${problem})`);
|
|
28736
29111
|
}
|
|
28737
29112
|
return renderer(d, frame) || [];
|
|
28738
29113
|
}
|
|
28739
29114
|
|
|
29115
|
+
// Returns a reason why furniture can't be drawn in a frame, or null
|
|
29116
|
+
function getFurnitureFrameProblem(frame) {
|
|
29117
|
+
if (!frame) return 'missing map frame';
|
|
29118
|
+
if (!frame.crs) return 'unknown map projection';
|
|
29119
|
+
if (!isProjectedCRS(frame.crs)) return 'map is unprojected';
|
|
29120
|
+
return null;
|
|
29121
|
+
}
|
|
29122
|
+
|
|
28740
29123
|
var Furniture = /*#__PURE__*/Object.freeze({
|
|
28741
29124
|
__proto__: null,
|
|
29125
|
+
getFurnitureFrameProblem: getFurnitureFrameProblem,
|
|
28742
29126
|
getFurnitureLayerData: getFurnitureLayerData,
|
|
28743
29127
|
getFurnitureLayerType: getFurnitureLayerType,
|
|
28744
29128
|
isFurnitureLayer: isFurnitureLayer,
|
|
28745
29129
|
layerHasFurniture: layerHasFurniture,
|
|
29130
|
+
layerIsFurniture: layerIsFurniture,
|
|
28746
29131
|
renderFurnitureLayer: renderFurnitureLayer
|
|
28747
29132
|
});
|
|
28748
29133
|
|
|
@@ -29388,16 +29773,14 @@
|
|
|
29388
29773
|
// a <tspan> inside a <textPath> advances along the path instead of
|
|
29389
29774
|
// stacking below it, so the lines are joined rather than dropping
|
|
29390
29775
|
// everything after the first one
|
|
29391
|
-
text = text.split(labelNewlineRxp).join(' ');
|
|
29392
29776
|
addToReport(report, 'joined', id);
|
|
29393
29777
|
}
|
|
29394
|
-
textPath = {
|
|
29778
|
+
textPath = applyRunsToSvgObject({
|
|
29395
29779
|
tag: 'textPath',
|
|
29396
|
-
value: text,
|
|
29397
29780
|
properties: {
|
|
29398
29781
|
startOffset: rec['label-start-offset'] || getDefaultStartOffset(rec)
|
|
29399
29782
|
}
|
|
29400
|
-
};
|
|
29783
|
+
}, joinLineRuns(splitLabelLineRuns(text)));
|
|
29401
29784
|
// the caller replaces this with a reference to a <defs> entry
|
|
29402
29785
|
textPath.properties[LABEL_PATH_PROPERTY] = d;
|
|
29403
29786
|
if (rec['label-side']) {
|
|
@@ -29461,6 +29844,28 @@
|
|
|
29461
29844
|
return '50%';
|
|
29462
29845
|
}
|
|
29463
29846
|
|
|
29847
|
+
// A path label's lines as one line of runs, with a space for each break. The
|
|
29848
|
+
// space takes the weight of the text before it, and runs of one weight that
|
|
29849
|
+
// the join brings together are merged, so that text with no bold in it is a
|
|
29850
|
+
// single run.
|
|
29851
|
+
function joinLineRuns(lines) {
|
|
29852
|
+
var out = [];
|
|
29853
|
+
lines.forEach(function(runs, i) {
|
|
29854
|
+
if (i > 0) appendRun(out, {text: ' ', bold: out.length > 0 && out[out.length - 1].bold});
|
|
29855
|
+
runs.forEach(function(run) { appendRun(out, run); });
|
|
29856
|
+
});
|
|
29857
|
+
return out;
|
|
29858
|
+
}
|
|
29859
|
+
|
|
29860
|
+
function appendRun(runs, run) {
|
|
29861
|
+
var last = runs[runs.length - 1];
|
|
29862
|
+
if (last && last.bold == run.bold) {
|
|
29863
|
+
last.text += run.text;
|
|
29864
|
+
} else {
|
|
29865
|
+
runs.push({text: run.text, bold: run.bold});
|
|
29866
|
+
}
|
|
29867
|
+
}
|
|
29868
|
+
|
|
29464
29869
|
function addToReport(report, key, id) {
|
|
29465
29870
|
if (report && report[key]) report[key].push(id);
|
|
29466
29871
|
}
|
|
@@ -39199,6 +39604,9 @@ ${svg}
|
|
|
39199
39604
|
async function exportDatasets(datasets, opts) {
|
|
39200
39605
|
var format = getOutputFormat(datasets[0], opts);
|
|
39201
39606
|
var files;
|
|
39607
|
+
if (format != 'svg' && format != PACKAGE_EXT) {
|
|
39608
|
+
datasets = removeFurnitureLayers(datasets);
|
|
39609
|
+
}
|
|
39202
39610
|
validateRasterExportFormat(datasets, format);
|
|
39203
39611
|
if (format != 'geoparquet' && opts.level !== undefined) {
|
|
39204
39612
|
error('The level= option only applies to GeoParquet output');
|
|
@@ -39274,6 +39682,25 @@ ${svg}
|
|
|
39274
39682
|
return files;
|
|
39275
39683
|
}
|
|
39276
39684
|
|
|
39685
|
+
// Scalebars and other map furniture only have a meaning in SVG output
|
|
39686
|
+
function removeFurnitureLayers(datasets) {
|
|
39687
|
+
var hasFurniture = datasets.some(function(dataset) {
|
|
39688
|
+
return dataset.layers.some(layerIsFurniture);
|
|
39689
|
+
});
|
|
39690
|
+
if (!hasFurniture) return datasets;
|
|
39691
|
+
datasets = datasets.map(function(dataset) {
|
|
39692
|
+
return utils.defaults({
|
|
39693
|
+
layers: dataset.layers.filter(function(lyr) { return !layerIsFurniture(lyr); })
|
|
39694
|
+
}, dataset);
|
|
39695
|
+
}).filter(function(dataset) {
|
|
39696
|
+
return dataset.layers.length > 0;
|
|
39697
|
+
});
|
|
39698
|
+
if (datasets.length === 0) {
|
|
39699
|
+
stop$1('Map furniture layers (e.g. scalebars) can only be exported as SVG');
|
|
39700
|
+
}
|
|
39701
|
+
return datasets;
|
|
39702
|
+
}
|
|
39703
|
+
|
|
39277
39704
|
function validateRasterExportFormat(datasets, format) {
|
|
39278
39705
|
if (format == 'geotiff') {
|
|
39279
39706
|
if (datasetsHaveVectorLayers(datasets)) {
|
|
@@ -40455,6 +40882,22 @@ ${svg}
|
|
|
40455
40882
|
}
|
|
40456
40883
|
}
|
|
40457
40884
|
|
|
40885
|
+
// -labels adds a label when given coordinates=, and styles the target's labels
|
|
40886
|
+
// otherwise. Options that belong to one of these are refused in the other,
|
|
40887
|
+
// rather than ignored.
|
|
40888
|
+
function validateLabelsOpts(cmd) {
|
|
40889
|
+
var o = cmd.options;
|
|
40890
|
+
if ('coordinates' in o) {
|
|
40891
|
+
if (o.where || o.ids) {
|
|
40892
|
+
stop$1('where= and ids= select labels to style, and can not be used with coordinates=');
|
|
40893
|
+
}
|
|
40894
|
+
} else if (o.properties) {
|
|
40895
|
+
stop$1('properties= requires coordinates=');
|
|
40896
|
+
} else if ('name' in o && !o.no_replace) {
|
|
40897
|
+
stop$1('name= requires coordinates= or +');
|
|
40898
|
+
}
|
|
40899
|
+
}
|
|
40900
|
+
|
|
40458
40901
|
function validateExpressionOpt(cmd) {
|
|
40459
40902
|
if (!cmd.options.expression) {
|
|
40460
40903
|
error('Command requires a JavaScript expression');
|
|
@@ -41435,17 +41878,17 @@ ${svg}
|
|
|
41435
41878
|
nameOpt2 = { // for -calc and -info
|
|
41436
41879
|
describe: 'name the output layer'
|
|
41437
41880
|
},
|
|
41438
|
-
// label style properties accepted by -add-label
|
|
41439
|
-
//
|
|
41440
|
-
//
|
|
41881
|
+
// label style properties accepted by -labels and -add-label. -style
|
|
41882
|
+
// accepts these too, for backward compatibility, but leaves them out of
|
|
41883
|
+
// its help.
|
|
41441
41884
|
labelStyleOpts = {
|
|
41442
|
-
'font-family': {describe: '
|
|
41443
|
-
'font-size': {describe: '
|
|
41885
|
+
'font-family': {describe: 'CSS font family, e.g. Georgia (default is sans-serif)'},
|
|
41886
|
+
'font-size': {describe: 'font size (default is 12)'},
|
|
41444
41887
|
'font-style': {describe: 'normal or italic'},
|
|
41445
41888
|
'font-weight': {describe: 'normal, bold or a numeric weight'},
|
|
41446
41889
|
'font-stretch': {describe: 'e.g. condensed'},
|
|
41447
41890
|
'letter-spacing': {describe: 'extra space between letters'},
|
|
41448
|
-
'line-height': {describe: 'spacing between lines of a multi-line label; a bare number is a multiple of the font size'},
|
|
41891
|
+
'line-height': {describe: 'spacing between lines of a multi-line label; a bare number is a multiple of the font size (default is 1.1)'},
|
|
41449
41892
|
'text-anchor': {describe: 'start, middle or end'},
|
|
41450
41893
|
'label-align': {describe: 'how the lines of a multi-line label align: left, center or right'},
|
|
41451
41894
|
'dominant-baseline': {describe: 'vertical alignment, e.g. central'},
|
|
@@ -41456,23 +41899,30 @@ ${svg}
|
|
|
41456
41899
|
dy: {describe: 'vertical offset from the anchor'},
|
|
41457
41900
|
fill: {describe: 'text color'},
|
|
41458
41901
|
opacity: {describe: 'text opacity'},
|
|
41459
|
-
'halo-width': {describe: 'width of a halo around the text in px'},
|
|
41902
|
+
'halo-width': {describe: 'width of a halo around the text in px (default is 0, no halo)'},
|
|
41460
41903
|
'halo-color': {describe: 'halo color (default is white)'},
|
|
41461
41904
|
'halo-opacity': {describe: 'halo opacity, 0-1'},
|
|
41462
|
-
'label-width': {describe: 'width of a fixed-width text block in px'},
|
|
41905
|
+
'label-width': {describe: 'width of a fixed-width text block in px (lines are wrapped in the web UI)'},
|
|
41463
41906
|
callout: {describe: 'line from the anchor to the text: line, elbow or curve'},
|
|
41464
41907
|
'callout-end': {describe: 'arrowhead at the anchor end: arrow, open-arrow or none'},
|
|
41465
|
-
'callout-end-size': {describe: 'length of an arrowhead\'s sides in px'},
|
|
41908
|
+
'callout-end-size': {describe: 'length of an arrowhead\'s sides in px (default is 10)'},
|
|
41466
41909
|
'callout-via': {describe: 'x,y of an elbow\'s corner or a point on a curve, in px'},
|
|
41467
41910
|
'callout-attach': {describe: 'where the callout meets the text, as x,y fractions'},
|
|
41468
41911
|
'callout-gap': {describe: 'px between the callout and the anchor'},
|
|
41469
|
-
'callout-padding': {describe: 'px between the callout and the text'},
|
|
41912
|
+
'callout-padding': {describe: 'px between the callout and the text (default is 3)'},
|
|
41470
41913
|
'callout-color': {describe: 'callout color (defaults to the text color)'},
|
|
41471
|
-
'callout-width': {describe: 'callout line width in px'},
|
|
41914
|
+
'callout-width': {describe: 'callout line width in px (default is 1)'},
|
|
41472
41915
|
'callout-opacity': {describe: 'callout opacity, 0-1'},
|
|
41473
41916
|
css: {describe: 'inline css style'},
|
|
41474
41917
|
class: {describe: 'name of CSS class or classes (space-separated)'}
|
|
41475
41918
|
},
|
|
41919
|
+
// a label can carry a symbol at its anchor
|
|
41920
|
+
labelIconOpts = {
|
|
41921
|
+
icon: {describe: 'symbol drawn at the label anchor: circle, square, ring, star'},
|
|
41922
|
+
'icon-size': {describe: 'size of the anchor symbol in px'},
|
|
41923
|
+
'icon-color': {describe: 'color of the anchor symbol (defaults to the text color)'},
|
|
41924
|
+
'icon-opacity': {describe: 'opacity of the anchor symbol, 0-1'}
|
|
41925
|
+
},
|
|
41476
41926
|
noReplaceOpt2 = { // for -calc and -info
|
|
41477
41927
|
alias: '+',
|
|
41478
41928
|
type: 'flag',
|
|
@@ -41552,6 +42002,18 @@ ${svg}
|
|
|
41552
42002
|
type: 'distance'
|
|
41553
42003
|
};
|
|
41554
42004
|
|
|
42005
|
+
// The label options -style used to document, before -labels took them over.
|
|
42006
|
+
// fill, opacity, css and class are left out because -style documents them
|
|
42007
|
+
// for every kind of feature.
|
|
42008
|
+
function getHiddenLabelStyleOpts() {
|
|
42009
|
+
var shared = ['fill', 'opacity', 'css', 'class'];
|
|
42010
|
+
var opts = {'label-text': {}};
|
|
42011
|
+
Object.keys(labelStyleOpts).forEach(function(name) {
|
|
42012
|
+
if (shared.indexOf(name) == -1) opts[name] = {};
|
|
42013
|
+
});
|
|
42014
|
+
return opts;
|
|
42015
|
+
}
|
|
42016
|
+
|
|
41555
42017
|
var parser = new CommandParser();
|
|
41556
42018
|
parser.usage('Usage: mapshaper -<command> [options] ...');
|
|
41557
42019
|
|
|
@@ -42920,6 +43382,32 @@ ${svg}
|
|
|
42920
43382
|
.option('encoding', encodingOpt)
|
|
42921
43383
|
.option('target', targetOpt);
|
|
42922
43384
|
|
|
43385
|
+
parser.command('labels')
|
|
43386
|
+
.describe('style labels, convert points to labels or add a label')
|
|
43387
|
+
.validate(validateLabelsOpts)
|
|
43388
|
+
.option('text', {
|
|
43389
|
+
describe: 'label text (a field, JS expression or literal value)'
|
|
43390
|
+
})
|
|
43391
|
+
.option('label-text', {
|
|
43392
|
+
alias_to: 'text'
|
|
43393
|
+
})
|
|
43394
|
+
.option('coordinates', {
|
|
43395
|
+
describe: 'add a label at x,y, or a path label along x,y,x,y,...'
|
|
43396
|
+
})
|
|
43397
|
+
.option('where', whereOpt)
|
|
43398
|
+
.option('ids', {
|
|
43399
|
+
describe: 'comma-sep. list of ids of the labels to style',
|
|
43400
|
+
type: 'numbers'
|
|
43401
|
+
})
|
|
43402
|
+
.options(labelStyleOpts)
|
|
43403
|
+
.options(labelIconOpts)
|
|
43404
|
+
.option('properties', {
|
|
43405
|
+
describe: 'with coordinates=, other attributes as a JSON object'
|
|
43406
|
+
})
|
|
43407
|
+
.option('name', nameOpt)
|
|
43408
|
+
.option('no-replace', noReplaceOpt)
|
|
43409
|
+
.option('target', targetOpt);
|
|
43410
|
+
|
|
42923
43411
|
parser.command('lines')
|
|
42924
43412
|
.describe('convert a polygon or point layer to a polyline layer')
|
|
42925
43413
|
.option('fields', {
|
|
@@ -43483,99 +43971,11 @@ ${svg}
|
|
|
43483
43971
|
describe: 'point icon color (defaults to fill color, then black)'
|
|
43484
43972
|
})
|
|
43485
43973
|
.option('icon-opacity', {
|
|
43486
|
-
describe: 'point icon opacity, 0-1
|
|
43487
|
-
})
|
|
43488
|
-
.option('label-text', {
|
|
43489
|
-
describe: 'label text (set this to export points as labels)'
|
|
43490
|
-
})
|
|
43491
|
-
.option('label-pos', {
|
|
43492
|
-
describe: 'label position; one of: n, s, e, w, ne, se, nw, sw, c'
|
|
43493
|
-
})
|
|
43494
|
-
.option('text-anchor', {
|
|
43495
|
-
describe: 'label alignment; one of: start, end, middle (default)'
|
|
43496
|
-
})
|
|
43497
|
-
.option('label-align', {
|
|
43498
|
-
describe: 'alignment of the lines of multi-line labels; left, center or right'
|
|
43499
|
-
})
|
|
43500
|
-
.option('halo-width', {
|
|
43501
|
-
describe: 'width of a halo around label text in px (default is 0, no halo)'
|
|
43502
|
-
})
|
|
43503
|
-
.option('halo-color', {
|
|
43504
|
-
describe: 'color of label halos (default is white)'
|
|
43505
|
-
})
|
|
43506
|
-
.option('halo-opacity', {
|
|
43507
|
-
describe: 'opacity of label halos, 0-1'
|
|
43974
|
+
describe: 'point icon opacity, 0-1'
|
|
43508
43975
|
})
|
|
43509
|
-
|
|
43510
|
-
|
|
43511
|
-
|
|
43512
|
-
.option('dy', {
|
|
43513
|
-
describe: 'y offset of labels (default is 0/baseline-aligned)'
|
|
43514
|
-
})
|
|
43515
|
-
.option('font-size', {
|
|
43516
|
-
describe: 'size of label text (default is 12)'
|
|
43517
|
-
})
|
|
43518
|
-
.option('font-family', {
|
|
43519
|
-
describe: 'CSS font family of labels (default is sans-serif)'
|
|
43520
|
-
})
|
|
43521
|
-
.option('font-weight', {
|
|
43522
|
-
describe: 'CSS font weight property of labels (e.g. bold, 700)'
|
|
43523
|
-
})
|
|
43524
|
-
.option('font-style', {
|
|
43525
|
-
describe: 'CSS font style property of labels (e.g. italic)'
|
|
43526
|
-
})
|
|
43527
|
-
.option('font-stretch', {
|
|
43528
|
-
describe: 'CSS font stretch property of labels (e.g. condensed)'
|
|
43529
|
-
})
|
|
43530
|
-
.option('letter-spacing', {
|
|
43531
|
-
describe: 'CSS letter-spacing property of labels'
|
|
43532
|
-
})
|
|
43533
|
-
.option('line-height', {
|
|
43534
|
-
describe: 'line spacing of multi-line labels; a bare number is a multiple of the font size, as in CSS (default is 1.1)'
|
|
43535
|
-
})
|
|
43536
|
-
.option('dominant-baseline', {
|
|
43537
|
-
describe: 'vertical alignment of labels (e.g. central)'
|
|
43538
|
-
})
|
|
43539
|
-
.option('label-side', {
|
|
43540
|
-
describe: 'which side of its path a label sits on: left or right'
|
|
43541
|
-
})
|
|
43542
|
-
.option('label-start-offset', {
|
|
43543
|
-
describe: 'where label text starts along its path, e.g. 50%'
|
|
43544
|
-
})
|
|
43545
|
-
.option('label-width', {
|
|
43546
|
-
describe: 'width of a fixed-width text block in px (lines are wrapped in the web UI)'
|
|
43547
|
-
})
|
|
43548
|
-
.option('callout', {
|
|
43549
|
-
describe: 'line from a label\'s anchor to its text: line, elbow or curve'
|
|
43550
|
-
})
|
|
43551
|
-
.option('callout-end', {
|
|
43552
|
-
describe: 'arrowhead at the anchor end of a callout: arrow (solid), open-arrow or none'
|
|
43553
|
-
})
|
|
43554
|
-
.option('callout-end-size', {
|
|
43555
|
-
describe: 'length of an arrowhead\'s sides in px (default 10, more for a thicker line)'
|
|
43556
|
-
})
|
|
43557
|
-
.option('callout-via', {
|
|
43558
|
-
describe: 'x,y of an elbow\'s corner or a point on a curve, in px from the anchor'
|
|
43559
|
-
})
|
|
43560
|
-
.option('callout-attach', {
|
|
43561
|
-
describe: 'where a callout meets its text, as x,y fractions of the text box'
|
|
43562
|
-
})
|
|
43563
|
-
.option('callout-gap', {
|
|
43564
|
-
describe: 'px between a callout and its anchor (default: a line meets the icon, an arrow clears it)'
|
|
43565
|
-
})
|
|
43566
|
-
.option('callout-padding', {
|
|
43567
|
-
describe: 'px between a callout and its text (default is 3)'
|
|
43568
|
-
})
|
|
43569
|
-
.option('callout-color', {
|
|
43570
|
-
describe: 'callout color (defaults to the text color)'
|
|
43571
|
-
})
|
|
43572
|
-
.option('callout-width', {
|
|
43573
|
-
describe: 'callout line width in px (default is 1)'
|
|
43574
|
-
})
|
|
43575
|
-
.option('callout-opacity', {
|
|
43576
|
-
describe: 'callout opacity, 0-1'
|
|
43577
|
-
})
|
|
43578
|
-
.option('target', targetOpt);
|
|
43976
|
+
// deprecated in favor of -labels; accepted, but left out of the help
|
|
43977
|
+
.options(getHiddenLabelStyleOpts())
|
|
43978
|
+
.option('target', targetOpt);
|
|
43579
43979
|
|
|
43580
43980
|
parser.command('repel')
|
|
43581
43981
|
.describe('move overlapping point symbols apart')
|
|
@@ -43821,23 +44221,10 @@ ${svg}
|
|
|
43821
44221
|
.option('text', {
|
|
43822
44222
|
describe: 'label text'
|
|
43823
44223
|
})
|
|
43824
|
-
//
|
|
43825
|
-
//
|
|
44224
|
+
// superseded by -labels coordinates=; kept so that older session
|
|
44225
|
+
// histories still run
|
|
43826
44226
|
.options(labelStyleOpts)
|
|
43827
|
-
|
|
43828
|
-
// though they are not text properties
|
|
43829
|
-
.option('icon', {
|
|
43830
|
-
describe: 'symbol drawn at the label anchor: circle, square, ring, star'
|
|
43831
|
-
})
|
|
43832
|
-
.option('icon-size', {
|
|
43833
|
-
describe: 'size of the anchor symbol in px'
|
|
43834
|
-
})
|
|
43835
|
-
.option('icon-color', {
|
|
43836
|
-
describe: 'color of the anchor symbol (defaults to the text color)'
|
|
43837
|
-
})
|
|
43838
|
-
.option('icon-opacity', {
|
|
43839
|
-
describe: 'opacity of the anchor symbol, 0-1'
|
|
43840
|
-
})
|
|
44227
|
+
.options(labelIconOpts)
|
|
43841
44228
|
.option('properties', {
|
|
43842
44229
|
describe: 'other attributes, as a JSON object'
|
|
43843
44230
|
})
|
|
@@ -44131,10 +44518,13 @@ ${svg}
|
|
|
44131
44518
|
.option('target', targetOpt);
|
|
44132
44519
|
|
|
44133
44520
|
parser.command('scalebar')
|
|
44134
|
-
.describe('add a
|
|
44521
|
+
.describe('add a scale bar to the map frame and SVG output')
|
|
44135
44522
|
.option('label', {
|
|
44136
44523
|
DEFAULT: true,
|
|
44137
|
-
describe: 'distance label, e.g. "35 miles"'
|
|
44524
|
+
describe: 'distance label, e.g. "35 miles" (default is automatic)'
|
|
44525
|
+
})
|
|
44526
|
+
.option('units', {
|
|
44527
|
+
describe: 'units of a bare-number or automatic label: km, meters, miles or feet (default is miles)'
|
|
44138
44528
|
})
|
|
44139
44529
|
.option('style', {
|
|
44140
44530
|
describe: 'two options: a or b'
|
|
@@ -44142,6 +44532,18 @@ ${svg}
|
|
|
44142
44532
|
.option('font-size', {
|
|
44143
44533
|
type: 'number'
|
|
44144
44534
|
})
|
|
44535
|
+
.option('font-family', {
|
|
44536
|
+
describe: 'font family of labels'
|
|
44537
|
+
})
|
|
44538
|
+
.option('font-style', {
|
|
44539
|
+
describe: 'font style of labels, e.g. italic'
|
|
44540
|
+
})
|
|
44541
|
+
.option('font-weight', {
|
|
44542
|
+
describe: 'font weight of labels, e.g. bold'
|
|
44543
|
+
})
|
|
44544
|
+
.option('color', {
|
|
44545
|
+
describe: 'color of bar and labels (default is black)'
|
|
44546
|
+
})
|
|
44145
44547
|
.option('tic-length', {
|
|
44146
44548
|
describe: 'length of tic marks (style b)',
|
|
44147
44549
|
type: 'number'
|
|
@@ -44154,18 +44556,22 @@ ${svg}
|
|
|
44154
44556
|
type: 'number'
|
|
44155
44557
|
})
|
|
44156
44558
|
.option('position', {
|
|
44157
|
-
describe: '
|
|
44559
|
+
describe: 'top-left (default), top-right, bottom-left or bottom-right'
|
|
44158
44560
|
})
|
|
44159
44561
|
.option('label-position', {
|
|
44160
44562
|
describe: 'top, bottom, top-center (style a), etc'
|
|
44161
44563
|
})
|
|
44162
44564
|
.option('dual-units', {
|
|
44163
|
-
|
|
44565
|
+
describe: 'show both metric and imperial units (style b)',
|
|
44164
44566
|
type: 'flag'
|
|
44165
44567
|
})
|
|
44166
44568
|
.option('margin', {
|
|
44167
44569
|
describe: 'offset in pixels from edge of map',
|
|
44168
44570
|
type: 'number'
|
|
44571
|
+
})
|
|
44572
|
+
.option('remove', {
|
|
44573
|
+
describe: 'remove the scale bar from the map frame',
|
|
44574
|
+
type: 'flag'
|
|
44169
44575
|
});
|
|
44170
44576
|
|
|
44171
44577
|
parser.command('shape')
|
|
@@ -51115,6 +51521,621 @@ ${svg}
|
|
|
51115
51521
|
return d;
|
|
51116
51522
|
}
|
|
51117
51523
|
|
|
51524
|
+
// Insert cutting points in arcs, where bbox intersects other shapes
|
|
51525
|
+
// Return a polygon layer containing the bounding box vectors, divided at cutting points.
|
|
51526
|
+
function divideDatasetByBBox(dataset, bbox) {
|
|
51527
|
+
var arcs = dataset.arcs;
|
|
51528
|
+
var data = findBBoxCutPoints(arcs, bbox);
|
|
51529
|
+
var map = insertCutPoints(data.cutPoints, arcs);
|
|
51530
|
+
arcs.dedupCoords();
|
|
51531
|
+
remapDividedArcs(dataset, map);
|
|
51532
|
+
// merge bbox dataset with target dataset,
|
|
51533
|
+
// so arcs are shared between target layers and bbox layer
|
|
51534
|
+
var clipDataset = bboxPointsToClipDataset(data.bboxPoints);
|
|
51535
|
+
var mergedDataset = mergeDatasets([dataset, clipDataset]);
|
|
51536
|
+
// TODO: detect if we need to rebuild topology (unlikely), like with the full clip command
|
|
51537
|
+
// buildTopology(mergedDataset);
|
|
51538
|
+
var clipLyr = mergedDataset.layers.pop();
|
|
51539
|
+
dataset.arcs = mergedDataset.arcs;
|
|
51540
|
+
dataset.layers = mergedDataset.layers;
|
|
51541
|
+
return clipLyr;
|
|
51542
|
+
}
|
|
51543
|
+
|
|
51544
|
+
function bboxPointsToClipDataset(arr) {
|
|
51545
|
+
var arcs = [];
|
|
51546
|
+
var shape = [];
|
|
51547
|
+
var layer = {geometry_type: 'polygon', shapes: [[shape]]};
|
|
51548
|
+
var p1, p2;
|
|
51549
|
+
for (var i=0, n=arr.length - 1; i<n; i++) {
|
|
51550
|
+
p1 = arr[i];
|
|
51551
|
+
p2 = arr[i+1];
|
|
51552
|
+
arcs.push([[p1.x, p1.y], [p2.x, p2.y]]);
|
|
51553
|
+
shape.push(i);
|
|
51554
|
+
}
|
|
51555
|
+
return {
|
|
51556
|
+
arcs: new ArcCollection(arcs),
|
|
51557
|
+
layers: [layer]
|
|
51558
|
+
};
|
|
51559
|
+
}
|
|
51560
|
+
|
|
51561
|
+
function findBBoxCutPoints(arcs, bbox) {
|
|
51562
|
+
var left = bbox[0],
|
|
51563
|
+
bottom = bbox[1],
|
|
51564
|
+
right = bbox[2],
|
|
51565
|
+
top = bbox[3];
|
|
51566
|
+
|
|
51567
|
+
// arrays of intersection points along each bbox edge
|
|
51568
|
+
var tt = [],
|
|
51569
|
+
rr = [],
|
|
51570
|
+
bb = [],
|
|
51571
|
+
ll = [];
|
|
51572
|
+
|
|
51573
|
+
arcs.forEachSegment(function(i, j, xx, yy) {
|
|
51574
|
+
var ax = xx[i],
|
|
51575
|
+
ay = yy[i],
|
|
51576
|
+
bx = xx[j],
|
|
51577
|
+
by = yy[j];
|
|
51578
|
+
var hit;
|
|
51579
|
+
if (segmentOutsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
|
|
51580
|
+
if (segmentInsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
|
|
51581
|
+
|
|
51582
|
+
hit = geom.segmentIntersection(left, top, right, top, ax, ay, bx, by);
|
|
51583
|
+
if (hit) addHit(tt, hit, i, j, xx, yy);
|
|
51584
|
+
|
|
51585
|
+
hit = geom.segmentIntersection(left, bottom, right, bottom, ax, ay, bx, by);
|
|
51586
|
+
if (hit) addHit(bb, hit, i, j, xx, yy);
|
|
51587
|
+
|
|
51588
|
+
hit = geom.segmentIntersection(left, bottom, left, top, ax, ay, bx, by);
|
|
51589
|
+
if (hit) addHit(ll, hit, i, j, xx, yy);
|
|
51590
|
+
|
|
51591
|
+
hit = geom.segmentIntersection(right, bottom, right, top, ax, ay, bx, by);
|
|
51592
|
+
if (hit) addHit(rr, hit, i, j, xx, yy);
|
|
51593
|
+
});
|
|
51594
|
+
|
|
51595
|
+
return {
|
|
51596
|
+
cutPoints: ll.concat(bb, rr, tt),
|
|
51597
|
+
bboxPoints: getDividedBBoxPoints(bbox, ll, tt, rr, bb)
|
|
51598
|
+
};
|
|
51599
|
+
|
|
51600
|
+
function addHit(arr, hit, i, j, xx, yy) {
|
|
51601
|
+
if (!hit) return;
|
|
51602
|
+
arr.push(formatHit(hit[0], hit[1], i, j, xx, yy));
|
|
51603
|
+
if (hit.length == 4) {
|
|
51604
|
+
arr.push(formatHit(hit[2], hit[3], i, j, xx, yy));
|
|
51605
|
+
}
|
|
51606
|
+
}
|
|
51607
|
+
|
|
51608
|
+
function formatHit(x, y, i, j, xx, yy) {
|
|
51609
|
+
var ids = formatIntersectingSegment(x, y, i, j, xx, yy);
|
|
51610
|
+
return getCutPoint(x, y, ids[0], ids[1]);
|
|
51611
|
+
}
|
|
51612
|
+
}
|
|
51613
|
+
|
|
51614
|
+
function segmentOutsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
|
|
51615
|
+
return ax < xmin && bx < xmin || ax > xmax && bx > xmax ||
|
|
51616
|
+
ay < ymin && by < ymin || ay > ymax && by > ymax;
|
|
51617
|
+
}
|
|
51618
|
+
|
|
51619
|
+
function segmentInsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
|
|
51620
|
+
return ax > xmin && bx > xmin && ax < xmax && bx < xmax &&
|
|
51621
|
+
ay > ymin && by > ymin && ay < ymax && by < ymax;
|
|
51622
|
+
}
|
|
51623
|
+
|
|
51624
|
+
// Returns an array of points representing the vertices in
|
|
51625
|
+
// the bbox with cutting points inserted.
|
|
51626
|
+
function getDividedBBoxPoints(bbox, ll, tt, rr, bb) {
|
|
51627
|
+
var bl = {x: bbox[0], y: bbox[1]},
|
|
51628
|
+
tl = {x: bbox[0], y: bbox[3]},
|
|
51629
|
+
tr = {x: bbox[2], y: bbox[3]},
|
|
51630
|
+
br = {x: bbox[2], y: bbox[1]};
|
|
51631
|
+
ll = utils.sortOn(ll.concat([bl, tl]), 'y', true);
|
|
51632
|
+
tt = utils.sortOn(tt.concat([tl, tr]), 'x', true);
|
|
51633
|
+
rr = utils.sortOn(rr.concat([tr, br]), 'y', false);
|
|
51634
|
+
bb = utils.sortOn(bb.concat([br, bl]), 'x', false);
|
|
51635
|
+
return ll.concat(tt, rr, bb).reduce(function(memo, p2) {
|
|
51636
|
+
var p1 = memo.length > 0 ? memo[memo.length-1] : null;
|
|
51637
|
+
if (p1 === null || p1.x != p2.x || p1.y != p2.y) memo.push(p2);
|
|
51638
|
+
return memo;
|
|
51639
|
+
}, []);
|
|
51640
|
+
}
|
|
51641
|
+
|
|
51642
|
+
var Bbox2Clipping = /*#__PURE__*/Object.freeze({
|
|
51643
|
+
__proto__: null,
|
|
51644
|
+
divideDatasetByBBox: divideDatasetByBBox,
|
|
51645
|
+
segmentInsideBBox: segmentInsideBBox,
|
|
51646
|
+
segmentOutsideBBox: segmentOutsideBBox
|
|
51647
|
+
});
|
|
51648
|
+
|
|
51649
|
+
cmd.filterGeom = function(lyr, arcs, opts) {
|
|
51650
|
+
if (!layerHasGeometry$1(lyr)) {
|
|
51651
|
+
stop$1("Layer is missing geometry");
|
|
51652
|
+
}
|
|
51653
|
+
if (opts.bbox) {
|
|
51654
|
+
filterByBoundsIntersection(lyr, arcs, opts);
|
|
51655
|
+
}
|
|
51656
|
+
cmd.filterFeatures(lyr, arcs, {remove_empty: true, verbose: false});
|
|
51657
|
+
};
|
|
51658
|
+
|
|
51659
|
+
function filterByBoundsIntersection(lyr, arcs, opts) {
|
|
51660
|
+
var filter = getBoundsIntersectionFilter(opts.bbox, lyr, arcs);
|
|
51661
|
+
noteLayerWillChange(lyr, {operation: 'filter-geom', unit: 'shapes'});
|
|
51662
|
+
editShapes(lyr.shapes, filter);
|
|
51663
|
+
markLayerChanged(lyr, {operation: 'filter-geom', unit: 'shapes'});
|
|
51664
|
+
}
|
|
51665
|
+
|
|
51666
|
+
function getBoundsIntersectionFilter(bbox, lyr, arcs) {
|
|
51667
|
+
var bounds = new Bounds(bbox);
|
|
51668
|
+
var filter = lyr.geometry_type == 'point' ?
|
|
51669
|
+
getPointInBoundsTest(bounds) :
|
|
51670
|
+
getPathBoundsIntersectionTest(bounds, arcs);
|
|
51671
|
+
return filter;
|
|
51672
|
+
}
|
|
51673
|
+
|
|
51674
|
+
function getPointInBoundsTest(bounds) {
|
|
51675
|
+
return function(xy) {
|
|
51676
|
+
var contains = bounds.containsPoint(xy[0], xy[1]);
|
|
51677
|
+
return contains ? xy : null;
|
|
51678
|
+
};
|
|
51679
|
+
}
|
|
51680
|
+
|
|
51681
|
+
// V1 too-simple test: bounding-box intersection
|
|
51682
|
+
// internal.getPathBoundsIntersectionTest = function(bounds, arcs) {
|
|
51683
|
+
// return function(path) {
|
|
51684
|
+
// return bounds.intersects(arcs.getSimpleShapeBounds(path)) ? path : null;
|
|
51685
|
+
// };
|
|
51686
|
+
// };
|
|
51687
|
+
|
|
51688
|
+
function getPathBoundsIntersectionTest(bounds, arcs) {
|
|
51689
|
+
var bbox = bounds.toArray(),
|
|
51690
|
+
left = bbox[0],
|
|
51691
|
+
bottom = bbox[1],
|
|
51692
|
+
right = bbox[2],
|
|
51693
|
+
top = bbox[3];
|
|
51694
|
+
|
|
51695
|
+
return function(path) {
|
|
51696
|
+
// case: bounding boxes don't intersect -> the path doesn't intersect the box
|
|
51697
|
+
if (!bounds.intersects(arcs.getSimpleShapeBounds(path))) {
|
|
51698
|
+
return null;
|
|
51699
|
+
}
|
|
51700
|
+
var intersects = false;
|
|
51701
|
+
var ax, ay, bx, by;
|
|
51702
|
+
var iter = arcs.getShapeIter(path);
|
|
51703
|
+
|
|
51704
|
+
if (iter.hasNext()) {
|
|
51705
|
+
ax = iter.x;
|
|
51706
|
+
ay = iter.y;
|
|
51707
|
+
}
|
|
51708
|
+
while (iter.hasNext()) {
|
|
51709
|
+
bx = ax;
|
|
51710
|
+
by = ay;
|
|
51711
|
+
ax = iter.x;
|
|
51712
|
+
ay = iter.y;
|
|
51713
|
+
if (segmentOutsideBBox(ax, ay, bx, by, left, bottom, right, top)) continue;
|
|
51714
|
+
if (segmentInsideBBox(ax, ay, bx, by, left, bottom, right, top)) {
|
|
51715
|
+
intersects = true;
|
|
51716
|
+
break;
|
|
51717
|
+
}
|
|
51718
|
+
if (geom.segmentIntersection(left, top, right, top, ax, ay, bx, by) ||
|
|
51719
|
+
geom.segmentIntersection(left, bottom, right, bottom, ax, ay, bx, by) ||
|
|
51720
|
+
geom.segmentIntersection(left, bottom, left, top, ax, ay, bx, by) ||
|
|
51721
|
+
geom.segmentIntersection(right, bottom, right, top, ax, ay, bx, by)) {
|
|
51722
|
+
intersects = true;
|
|
51723
|
+
break;
|
|
51724
|
+
}
|
|
51725
|
+
}
|
|
51726
|
+
|
|
51727
|
+
// case: bbox is entirely inside this ring
|
|
51728
|
+
if (!intersects && geom.testPointInRing(left, bottom, path, arcs)) {
|
|
51729
|
+
intersects = true;
|
|
51730
|
+
}
|
|
51731
|
+
return intersects ? path : null;
|
|
51732
|
+
};
|
|
51733
|
+
}
|
|
51734
|
+
|
|
51735
|
+
// Return a function for testing if a shape (path or point) intersects a bounding box
|
|
51736
|
+
// TODO: move this function to a different file
|
|
51737
|
+
function getBBoxIntersectionTest(bbox, lyr, arcs) {
|
|
51738
|
+
var filter = getBoundsIntersectionFilter(bbox, lyr, arcs);
|
|
51739
|
+
return function(shapeId) {
|
|
51740
|
+
var shp = lyr.shapes[shapeId];
|
|
51741
|
+
if (!shp) return false;
|
|
51742
|
+
for (var i=0; i<shp.length; i++) {
|
|
51743
|
+
if (filter(shp[i])) return true;
|
|
51744
|
+
}
|
|
51745
|
+
return false;
|
|
51746
|
+
};
|
|
51747
|
+
}
|
|
51748
|
+
|
|
51749
|
+
// return array of shape ids
|
|
51750
|
+
function findShapesIntersectingBBox(bbox, lyr, arcs) {
|
|
51751
|
+
var test = getBBoxIntersectionTest(bbox, lyr, arcs);
|
|
51752
|
+
var ids = [];
|
|
51753
|
+
for (var i=0; i<lyr.shapes.length; i++) {
|
|
51754
|
+
if (test(i)) ids.push(i);
|
|
51755
|
+
}
|
|
51756
|
+
return ids;
|
|
51757
|
+
}
|
|
51758
|
+
|
|
51759
|
+
var FilterGeom = /*#__PURE__*/Object.freeze({
|
|
51760
|
+
__proto__: null,
|
|
51761
|
+
findShapesIntersectingBBox: findShapesIntersectingBBox,
|
|
51762
|
+
getBBoxIntersectionTest: getBBoxIntersectionTest
|
|
51763
|
+
});
|
|
51764
|
+
|
|
51765
|
+
cmd.filterFeatures = function(lyr, arcs, opts) {
|
|
51766
|
+
var records = lyr.data ? lyr.data.getRecords() : null,
|
|
51767
|
+
shapes = lyr.shapes || null,
|
|
51768
|
+
n = getFeatureCount(lyr),
|
|
51769
|
+
filteredShapes = shapes ? [] : null,
|
|
51770
|
+
filteredRecords = records ? [] : null,
|
|
51771
|
+
filteredLyr = getOutputLayer(lyr, opts),
|
|
51772
|
+
invert = !!opts.invert,
|
|
51773
|
+
filter;
|
|
51774
|
+
|
|
51775
|
+
if (opts.expression) {
|
|
51776
|
+
filter = compileFeatureExpression(opts.expression, lyr, arcs);
|
|
51777
|
+
}
|
|
51778
|
+
|
|
51779
|
+
if (opts.ids) {
|
|
51780
|
+
filter = combineFilters(filter, getIdFilter(opts.ids));
|
|
51781
|
+
}
|
|
51782
|
+
|
|
51783
|
+
if (opts.remove_empty) {
|
|
51784
|
+
filter = combineFilters(filter, getNullGeometryFilter(lyr, arcs));
|
|
51785
|
+
}
|
|
51786
|
+
|
|
51787
|
+
if (opts.bbox) {
|
|
51788
|
+
filter = combineFilters(filter, getBBoxIntersectionTest(opts.bbox, lyr, arcs));
|
|
51789
|
+
}
|
|
51790
|
+
|
|
51791
|
+
if (!filter) {
|
|
51792
|
+
stop$1("Missing a filter criterion");
|
|
51793
|
+
}
|
|
51794
|
+
|
|
51795
|
+
utils.repeat(n, function(shapeId) {
|
|
51796
|
+
var result = filter(shapeId);
|
|
51797
|
+
requireBooleanResult(result);
|
|
51798
|
+
if (invert) result = !result;
|
|
51799
|
+
if (result === true) {
|
|
51800
|
+
if (shapes) filteredShapes.push(shapes[shapeId] || null);
|
|
51801
|
+
if (records) filteredRecords.push(records[shapeId] || null);
|
|
51802
|
+
}
|
|
51803
|
+
});
|
|
51804
|
+
|
|
51805
|
+
if (filteredLyr == lyr) {
|
|
51806
|
+
noteLayerWillChange(lyr, {operation: 'filter'});
|
|
51807
|
+
}
|
|
51808
|
+
filteredLyr.shapes = filteredShapes;
|
|
51809
|
+
filteredLyr.data = filteredRecords ? new DataTable(filteredRecords) : null;
|
|
51810
|
+
if (filteredLyr == lyr) {
|
|
51811
|
+
markLayerChanged(lyr, {operation: 'filter'});
|
|
51812
|
+
}
|
|
51813
|
+
if (opts.no_replace) {
|
|
51814
|
+
// if adding a layer, don't share objects between source and filtered layer
|
|
51815
|
+
filteredLyr = copyLayer(filteredLyr);
|
|
51816
|
+
}
|
|
51817
|
+
|
|
51818
|
+
if (opts.verbose !== false && !opts.quiet) {
|
|
51819
|
+
message(utils.format('Retained %,d of %,d features', getFeatureCount(filteredLyr), n));
|
|
51820
|
+
}
|
|
51821
|
+
|
|
51822
|
+
return filteredLyr;
|
|
51823
|
+
};
|
|
51824
|
+
|
|
51825
|
+
// TODO: update filter command to use this function
|
|
51826
|
+
function filterLayerInPlace(lyr, filter, invert) {
|
|
51827
|
+
var records = lyr.data ? lyr.data.getRecords() : null,
|
|
51828
|
+
shapes = lyr.shapes || null,
|
|
51829
|
+
n = getFeatureCount(lyr),
|
|
51830
|
+
filteredShapes = shapes ? [] : null,
|
|
51831
|
+
filteredRecords = records ? [] : null;
|
|
51832
|
+
utils.repeat(n, function(shapeId) {
|
|
51833
|
+
var result = filter(shapeId);
|
|
51834
|
+
requireBooleanResult(result);
|
|
51835
|
+
if (result === true) {
|
|
51836
|
+
if (shapes) filteredShapes.push(shapes[shapeId] || null);
|
|
51837
|
+
if (records) filteredRecords.push(records[shapeId] || null);
|
|
51838
|
+
}
|
|
51839
|
+
});
|
|
51840
|
+
noteLayerWillChange(lyr, {operation: 'filterLayerInPlace'});
|
|
51841
|
+
lyr.shapes = filteredShapes;
|
|
51842
|
+
lyr.data = filteredRecords ? new DataTable(filteredRecords) : null;
|
|
51843
|
+
markLayerChanged(lyr, {operation: 'filterLayerInPlace'});
|
|
51844
|
+
}
|
|
51845
|
+
|
|
51846
|
+
function getIdFilter(ids) {
|
|
51847
|
+
var set = new Set(ids);
|
|
51848
|
+
return function(i) {
|
|
51849
|
+
return set.has(i);
|
|
51850
|
+
};
|
|
51851
|
+
}
|
|
51852
|
+
|
|
51853
|
+
function getNullGeometryFilter(lyr, arcs) {
|
|
51854
|
+
var shapes = lyr.shapes;
|
|
51855
|
+
if (lyr.geometry_type == 'polygon') {
|
|
51856
|
+
return getEmptyPolygonFilter(shapes, arcs);
|
|
51857
|
+
}
|
|
51858
|
+
return function(i) {return !!shapes[i];};
|
|
51859
|
+
}
|
|
51860
|
+
|
|
51861
|
+
function getEmptyPolygonFilter(shapes, arcs) {
|
|
51862
|
+
return function(i) {
|
|
51863
|
+
var shp = shapes[i];
|
|
51864
|
+
return !!shp && geom.getPlanarShapeArea(shapes[i], arcs) > 0;
|
|
51865
|
+
};
|
|
51866
|
+
}
|
|
51867
|
+
|
|
51868
|
+
function combineFilters(a, b) {
|
|
51869
|
+
return a && b ? function(id) {
|
|
51870
|
+
return a(id) && b(id);
|
|
51871
|
+
} : (a || b);
|
|
51872
|
+
}
|
|
51873
|
+
|
|
51874
|
+
// The icon names accepted by the icon= property of the -style command.
|
|
51875
|
+
// Kept in a module of its own so that both the renderer (svg-symbols.mjs) and
|
|
51876
|
+
// the -style command can use it without forming an import cycle.
|
|
51877
|
+
|
|
51878
|
+
var iconNames = ['circle', 'square', 'ring', 'star'];
|
|
51879
|
+
|
|
51880
|
+
function isSupportedIconName(name) {
|
|
51881
|
+
return iconNames.indexOf(name) > -1;
|
|
51882
|
+
}
|
|
51883
|
+
|
|
51884
|
+
cmd.svgStyle = function(lyr, dataset, opts) {
|
|
51885
|
+
var filterFn, table, fields, hasNewFields, optFields, clearedByPosition, fieldsBefore;
|
|
51886
|
+
|
|
51887
|
+
function hadField(field) {
|
|
51888
|
+
return field in fieldsBefore;
|
|
51889
|
+
}
|
|
51890
|
+
|
|
51891
|
+
if (getFeatureCount(lyr) === 0) {
|
|
51892
|
+
return;
|
|
51893
|
+
}
|
|
51894
|
+
if (!lyr.data) {
|
|
51895
|
+
initDataTable(lyr);
|
|
51896
|
+
}
|
|
51897
|
+
if (opts.where) {
|
|
51898
|
+
filterFn = compileFeatureExpression(opts.where, lyr, dataset.arcs);
|
|
51899
|
+
}
|
|
51900
|
+
if (opts.ids) {
|
|
51901
|
+
filterFn = combineFilters(filterFn, getIdFilter(opts.ids));
|
|
51902
|
+
}
|
|
51903
|
+
if (opts.clear) {
|
|
51904
|
+
lyr.data.getFields().filter(isSupportedSvgStyleProperty).forEach(lyr.data.deleteField, lyr.data);
|
|
51905
|
+
}
|
|
51906
|
+
table = getLayerDataTable(lyr);
|
|
51907
|
+
optFields = getOptionFields(opts);
|
|
51908
|
+
fields = getStyleFields(optFields);
|
|
51909
|
+
// Which of dx/dy/text-anchor a label-pos in this command clears: not the ones
|
|
51910
|
+
// the same command also sets, so that `label-pos=n dx=3` keeps the nudge it
|
|
51911
|
+
// was given instead of clearing it a moment later.
|
|
51912
|
+
//
|
|
51913
|
+
// Clearing at all is what keeps the shorthand usable, since a value on the
|
|
51914
|
+
// record wins over the position: without it, setting a position on a label
|
|
51915
|
+
// that had been dragged would appear to do nothing.
|
|
51916
|
+
clearedByPosition = labelPositionDerivedFields.filter(function(field) {
|
|
51917
|
+
return optFields.indexOf(field) == -1;
|
|
51918
|
+
});
|
|
51919
|
+
hasNewFields = fields.some(function(field) {
|
|
51920
|
+
return !table.fieldExists(field);
|
|
51921
|
+
});
|
|
51922
|
+
// The table's columns as this command found them. Taken once, because the
|
|
51923
|
+
// command adds to them as it runs, and a blanked property must be judged
|
|
51924
|
+
// against what was there before rather than against what it has just made.
|
|
51925
|
+
fieldsBefore = utils.arrayToIndex(table.getFields());
|
|
51926
|
+
if (fields.length > 0) {
|
|
51927
|
+
if (hasNewFields) {
|
|
51928
|
+
table.captureSchemaBefore({operation: 'style', fields: fields});
|
|
51929
|
+
} else {
|
|
51930
|
+
table.captureFieldsBefore(fields, {operation: 'style'});
|
|
51931
|
+
}
|
|
51932
|
+
}
|
|
51933
|
+
Object.keys(opts).forEach(function(optName) {
|
|
51934
|
+
// undo cli parser name conversion; the regex must be global, or a
|
|
51935
|
+
// property with more than one hyphen (e.g. label-start-offset) is silently
|
|
51936
|
+
// skipped rather than applied
|
|
51937
|
+
var svgName = optName.replace(/_/g, '-');
|
|
51938
|
+
if (!isSupportedSvgStyleProperty(svgName)) {
|
|
51939
|
+
return;
|
|
51940
|
+
}
|
|
51941
|
+
var strVal = opts[optName].trim();
|
|
51942
|
+
// An empty value removes the property, rather than being rejected as an
|
|
51943
|
+
// unparseable one. This is how a control gives a property back: a label
|
|
51944
|
+
// dragged off its position clears label-pos, and there is otherwise no
|
|
51945
|
+
// per-property unset -- only -style clear, which clears all of them.
|
|
51946
|
+
var unset = strVal === '' && emptyValueUnsetsProperty(svgName);
|
|
51947
|
+
var accessor = unset ? null : getSymbolPropertyAccessor(strVal, svgName, lyr);
|
|
51948
|
+
var badIcons = svgName == 'icon' ? [] : null;
|
|
51949
|
+
// Removing a position is not setting one, so it neither validates the
|
|
51950
|
+
// value nor clears the offsets the position would have stood for.
|
|
51951
|
+
var posOnPaths = svgName == 'label-pos' && !unset ? [] : null;
|
|
51952
|
+
table.getRecords().forEach(function(rec, i) {
|
|
51953
|
+
if (filterFn && !filterFn(i)) {
|
|
51954
|
+
// make sure field exists if record is excluded by filter
|
|
51955
|
+
setUndefinedFields(rec, [svgName]);
|
|
51956
|
+
if (svgName == 'label-pos') {
|
|
51957
|
+
// ...but a field the position would only have cleared is one this
|
|
51958
|
+
// command is not writing anywhere, so an excluded record has nothing
|
|
51959
|
+
// to stay consistent with
|
|
51960
|
+
setUndefinedFields(rec, labelPositionDerivedFields, {has: hadField});
|
|
51961
|
+
}
|
|
51962
|
+
} else if (unset) {
|
|
51963
|
+
// Nothing to remove, and so nothing to create: removing a property no
|
|
51964
|
+
// record has would otherwise add an empty column for it.
|
|
51965
|
+
if (hadField(svgName) || svgName in rec) rec[svgName] = undefined;
|
|
51966
|
+
} else {
|
|
51967
|
+
rec[svgName] = accessor(i);
|
|
51968
|
+
if (badIcons) {
|
|
51969
|
+
addUnsupportedIconName(badIcons, rec.icon);
|
|
51970
|
+
}
|
|
51971
|
+
if (posOnPaths) {
|
|
51972
|
+
if (!parseLabelPosition(rec['label-pos'])) {
|
|
51973
|
+
stop$1('Unexpected value for label-pos:', rec['label-pos']);
|
|
51974
|
+
}
|
|
51975
|
+
if (shapeIsPathLabel(lyr.shapes && lyr.shapes[i], rec)) {
|
|
51976
|
+
// Not stored, so that ignoring it means ignoring it: a stored
|
|
51977
|
+
// position would show up in the style panel and would start
|
|
51978
|
+
// applying if the label ever lost all but one of its knots. Its
|
|
51979
|
+
// text-anchor is left alone too -- that one does place text along a
|
|
51980
|
+
// path, so a position that had no effect must not clear it.
|
|
51981
|
+
posOnPaths.push(i);
|
|
51982
|
+
rec['label-pos'] = undefined;
|
|
51983
|
+
} else {
|
|
51984
|
+
setUndefinedFields(rec, clearedByPosition, {overwrite: true, has: hadField});
|
|
51985
|
+
}
|
|
51986
|
+
}
|
|
51987
|
+
}
|
|
51988
|
+
});
|
|
51989
|
+
if (badIcons && badIcons.length > 0) {
|
|
51990
|
+
warn(formatUnsupportedIconMessage(badIcons));
|
|
51991
|
+
}
|
|
51992
|
+
if (posOnPaths && posOnPaths.length > 0) {
|
|
51993
|
+
warn(formatPositionOnPathMessage(posOnPaths));
|
|
51994
|
+
}
|
|
51995
|
+
});
|
|
51996
|
+
if (fields.length > 0) {
|
|
51997
|
+
if (hasNewFields) {
|
|
51998
|
+
table.markSchemaChanged({operation: 'style'});
|
|
51999
|
+
} else {
|
|
52000
|
+
table.markFieldsChanged(fields, {operation: 'style'});
|
|
52001
|
+
}
|
|
52002
|
+
}
|
|
52003
|
+
};
|
|
52004
|
+
|
|
52005
|
+
// The style properties this command was given, in SVG spelling.
|
|
52006
|
+
function getOptionFields(opts) {
|
|
52007
|
+
var fields = [];
|
|
52008
|
+
Object.keys(opts).forEach(function(optName) {
|
|
52009
|
+
var svgName = optName.replace(/_/g, '-');
|
|
52010
|
+
if (isSupportedSvgStyleProperty(svgName)) addField(fields, svgName);
|
|
52011
|
+
});
|
|
52012
|
+
return fields;
|
|
52013
|
+
}
|
|
52014
|
+
|
|
52015
|
+
// The fields the command will write, which is what the undo capture covers.
|
|
52016
|
+
// label-pos reaches dx/dy/text-anchor as well -- it no longer stores values in
|
|
52017
|
+
// them, but it does clear them.
|
|
52018
|
+
function getStyleFields(optFields) {
|
|
52019
|
+
var fields = [];
|
|
52020
|
+
optFields.forEach(function(svgName) {
|
|
52021
|
+
addField(fields, svgName);
|
|
52022
|
+
if (svgName == 'label-pos') {
|
|
52023
|
+
labelPositionFields.forEach(function(field) {
|
|
52024
|
+
addField(fields, field);
|
|
52025
|
+
});
|
|
52026
|
+
}
|
|
52027
|
+
});
|
|
52028
|
+
return fields;
|
|
52029
|
+
}
|
|
52030
|
+
|
|
52031
|
+
function addField(fields, field) {
|
|
52032
|
+
if (fields.indexOf(field) == -1) {
|
|
52033
|
+
fields.push(field);
|
|
52034
|
+
}
|
|
52035
|
+
}
|
|
52036
|
+
|
|
52037
|
+
// Icon names can not be validated when options are parsed, because an icon=
|
|
52038
|
+
// value may be a field name or an expression. The assigned values are checked
|
|
52039
|
+
// instead, so that computed names are covered too. Unsupported names are a
|
|
52040
|
+
// warning, not an error -- features with an unsupported name render without an
|
|
52041
|
+
// icon.
|
|
52042
|
+
var maxReportedIconNames = 4;
|
|
52043
|
+
|
|
52044
|
+
function addUnsupportedIconName(names, val) {
|
|
52045
|
+
var name;
|
|
52046
|
+
if (!val) return; // a blank value removes the icon
|
|
52047
|
+
name = String(val);
|
|
52048
|
+
if (isSupportedIconName(name) || names.indexOf(name) > -1) return;
|
|
52049
|
+
names.push(name);
|
|
52050
|
+
}
|
|
52051
|
+
|
|
52052
|
+
function formatUnsupportedIconMessage(names) {
|
|
52053
|
+
var extra = names.length - maxReportedIconNames;
|
|
52054
|
+
var listed = extra > 0 ? names.slice(0, maxReportedIconNames) : names;
|
|
52055
|
+
var str = 'Unsupported icon ' + (names.length > 1 ? 'names' : 'name') + ': ' +
|
|
52056
|
+
listed.join(', ');
|
|
52057
|
+
if (extra > 0) {
|
|
52058
|
+
str += ' (and ' + extra + ' more)';
|
|
52059
|
+
}
|
|
52060
|
+
return str + '. Expected one of: ' + iconNames.join(', ');
|
|
52061
|
+
}
|
|
52062
|
+
|
|
52063
|
+
// Adds @fields to @rec with no value, so that a record the filter excluded
|
|
52064
|
+
// still has the same schema as the ones it kept. With overwrite, also blanks a
|
|
52065
|
+
// value already there -- which is how setting a position takes back the offsets
|
|
52066
|
+
// a label was carrying.
|
|
52067
|
+
//
|
|
52068
|
+
// @has: optional test for whether the layer carries a field at all. A field
|
|
52069
|
+
// nobody has is not created in order to be blanked: -style label-pos=n clears
|
|
52070
|
+
// dx, dy and text-anchor because a value on the record wins over the position,
|
|
52071
|
+
// and there is nothing to win with when the column does not exist. Without
|
|
52072
|
+
// this, clicking a position in the style panel put three empty columns in the
|
|
52073
|
+
// user's table, and three empty columns in their CSV.
|
|
52074
|
+
function setUndefinedFields(rec, fields, opts) {
|
|
52075
|
+
var overwrite = !!(opts && opts.overwrite);
|
|
52076
|
+
var has = opts && opts.has;
|
|
52077
|
+
fields.forEach(function(field) {
|
|
52078
|
+
if (has && !has(field) && field in rec === false) return;
|
|
52079
|
+
if (overwrite || field in rec === false) {
|
|
52080
|
+
rec[field] = undefined;
|
|
52081
|
+
}
|
|
52082
|
+
});
|
|
52083
|
+
}
|
|
52084
|
+
|
|
52085
|
+
// label-pos places text around an anchor point, which a label strung along a
|
|
52086
|
+
// path does not have: its text runs from a start offset in the direction the
|
|
52087
|
+
// path goes. A warning rather than an error, because a layer can hold both
|
|
52088
|
+
// kinds of label and styling all of it at once is reasonable.
|
|
52089
|
+
function formatPositionOnPathMessage(ids) {
|
|
52090
|
+
var extra = ids.length - maxReportedIds;
|
|
52091
|
+
var listed = (extra > 0 ? ids.slice(0, maxReportedIds) : ids).join(', ');
|
|
52092
|
+
return 'Ignoring label-pos on ' + ids.length + ' path ' +
|
|
52093
|
+
(ids.length > 1 ? 'labels' : 'label') + ' (' + listed +
|
|
52094
|
+
(extra > 0 ? ' and ' + extra + ' more' : '') + '). ' +
|
|
52095
|
+
'Use label-start-offset= and text-anchor= to place text along a path.';
|
|
52096
|
+
}
|
|
52097
|
+
|
|
52098
|
+
var maxReportedIds = 4;
|
|
52099
|
+
|
|
52100
|
+
// -labels styles the labels in a point layer, or turns a point layer into
|
|
52101
|
+
// labels by giving it text=. With coordinates=, it adds one label instead; that
|
|
52102
|
+
// case is cmd.addLabel(), dispatched separately because it accepts an empty
|
|
52103
|
+
// target.
|
|
52104
|
+
//
|
|
52105
|
+
// Styling writes the same properties -style writes, through the same code, so
|
|
52106
|
+
// a label styled by either command is the same label.
|
|
52107
|
+
cmd.labels = function(targetLayers, dataset, opts) {
|
|
52108
|
+
var styleOpts = getStyleOpts(opts);
|
|
52109
|
+
var output = targetLayers.map(function(lyr) {
|
|
52110
|
+
var out;
|
|
52111
|
+
requirePointLayer$1(lyr);
|
|
52112
|
+
out = opts.no_replace ? copyLayer(lyr) : lyr;
|
|
52113
|
+
cmd.svgStyle(out, dataset, styleOpts);
|
|
52114
|
+
return out;
|
|
52115
|
+
});
|
|
52116
|
+
return opts.no_replace ? output : null;
|
|
52117
|
+
};
|
|
52118
|
+
|
|
52119
|
+
// text= is label-text= under the name that reads naturally in this command.
|
|
52120
|
+
function getStyleOpts(opts) {
|
|
52121
|
+
var o = Object.assign({}, opts);
|
|
52122
|
+
if ('text' in o) {
|
|
52123
|
+
o.label_text = o.text;
|
|
52124
|
+
delete o.text;
|
|
52125
|
+
}
|
|
52126
|
+
return o;
|
|
52127
|
+
}
|
|
52128
|
+
|
|
52129
|
+
// Labels are drawn at points. A layer with no geometry is let through, since
|
|
52130
|
+
// it has nothing to conflict with.
|
|
52131
|
+
function requirePointLayer$1(lyr) {
|
|
52132
|
+
if (lyr.geometry_type && lyr.geometry_type != 'point') {
|
|
52133
|
+
stop$1('Labels can only be applied to a point layer; layer "' +
|
|
52134
|
+
(lyr.name || '[unnamed]') + '" contains ' + lyr.geometry_type + 's. ' +
|
|
52135
|
+
'Use -points to make a point layer to label.');
|
|
52136
|
+
}
|
|
52137
|
+
}
|
|
52138
|
+
|
|
51118
52139
|
cmd.addLayer = addLayer;
|
|
51119
52140
|
|
|
51120
52141
|
var GEOMETRY_TYPES = ['point', 'polygon', 'polyline'];
|
|
@@ -52161,8 +53182,10 @@ ${svg}
|
|
|
52161
53182
|
// May return NaN for a bbox the frame can't be fitted to.
|
|
52162
53183
|
// @opts.ignore_symbols fit to the coordinates alone
|
|
52163
53184
|
function getFrameContentBbox(targets, getScale, opts) {
|
|
53185
|
+
// a data-only layer has no bounds
|
|
52164
53186
|
var bounds = targets.reduce(function(memo, o) {
|
|
52165
|
-
|
|
53187
|
+
var b = getLayerBounds(o.layer, o.dataset.arcs);
|
|
53188
|
+
return b ? memo.mergeBounds(b) : memo;
|
|
52166
53189
|
}, new Bounds());
|
|
52167
53190
|
var raw, items, bbox;
|
|
52168
53191
|
if (!bounds.hasBounds()) return null;
|
|
@@ -52309,6 +53332,11 @@ ${svg}
|
|
|
52309
53332
|
var extent = getFrameExtent(contentBbox, widthPx, heightPx, opts);
|
|
52310
53333
|
return getFrameScale(extent.bbox, extent.width, getFixedAspect$1(extent, opts));
|
|
52311
53334
|
}, opts);
|
|
53335
|
+
if (!bbox && !expandCommandTargets(targets).some(function(o) {
|
|
53336
|
+
return layerHasGeometry$1(o.layer);
|
|
53337
|
+
})) {
|
|
53338
|
+
stop$1('Unable to fit a frame to a layer with no geometry. Target a layer containing shapes, or use the bbox= option.');
|
|
53339
|
+
}
|
|
52312
53340
|
if (!bbox) {
|
|
52313
53341
|
stop$1('Command target is missing geographical bounds');
|
|
52314
53342
|
}
|
|
@@ -52332,14 +53360,17 @@ ${svg}
|
|
|
52332
53360
|
var crsInfo = getDatasetCrsInfo(targets[0].dataset);
|
|
52333
53361
|
setDatasetCrsInfo(frameDataset, crsInfo);
|
|
52334
53362
|
}
|
|
52335
|
-
frameDataset.layers[0]
|
|
53363
|
+
var frameLyr = frameDataset.layers[0];
|
|
53364
|
+
frameLyr.name = opts.name || 'frame';
|
|
52336
53365
|
if (existingFrame) {
|
|
52337
53366
|
if (!opts.replace) {
|
|
52338
53367
|
stop$1('A map frame already exists:', existingFrame.layer.name || '[unnamed frame]');
|
|
52339
53368
|
}
|
|
53369
|
+
moveFrameFurniture(existingFrame.dataset, frameDataset);
|
|
52340
53370
|
demoteFrameLayer(existingFrame.layer);
|
|
52341
53371
|
}
|
|
52342
|
-
|
|
53372
|
+
// target the frame, not any furniture that came with it
|
|
53373
|
+
catalog.setDefaultTarget([frameLyr], frameDataset);
|
|
52343
53374
|
};
|
|
52344
53375
|
|
|
52345
53376
|
// The frame's extent and nominal size, from the extent of its content and the
|
|
@@ -53782,7 +54813,7 @@ ${svg}
|
|
|
53782
54813
|
}
|
|
53783
54814
|
|
|
53784
54815
|
cmd.alphaShapes = function(pointLyr, targetDataset, opts) {
|
|
53785
|
-
requirePointLayer$
|
|
54816
|
+
requirePointLayer$2(pointLyr);
|
|
53786
54817
|
if (opts.interval > 0 === false) {
|
|
53787
54818
|
stop$1('Expected a non-negative interval parameter');
|
|
53788
54819
|
}
|
|
@@ -61423,131 +62454,6 @@ ${svg}
|
|
|
61423
62454
|
normalizeOverlaySource: normalizeOverlaySource
|
|
61424
62455
|
});
|
|
61425
62456
|
|
|
61426
|
-
// Insert cutting points in arcs, where bbox intersects other shapes
|
|
61427
|
-
// Return a polygon layer containing the bounding box vectors, divided at cutting points.
|
|
61428
|
-
function divideDatasetByBBox(dataset, bbox) {
|
|
61429
|
-
var arcs = dataset.arcs;
|
|
61430
|
-
var data = findBBoxCutPoints(arcs, bbox);
|
|
61431
|
-
var map = insertCutPoints(data.cutPoints, arcs);
|
|
61432
|
-
arcs.dedupCoords();
|
|
61433
|
-
remapDividedArcs(dataset, map);
|
|
61434
|
-
// merge bbox dataset with target dataset,
|
|
61435
|
-
// so arcs are shared between target layers and bbox layer
|
|
61436
|
-
var clipDataset = bboxPointsToClipDataset(data.bboxPoints);
|
|
61437
|
-
var mergedDataset = mergeDatasets([dataset, clipDataset]);
|
|
61438
|
-
// TODO: detect if we need to rebuild topology (unlikely), like with the full clip command
|
|
61439
|
-
// buildTopology(mergedDataset);
|
|
61440
|
-
var clipLyr = mergedDataset.layers.pop();
|
|
61441
|
-
dataset.arcs = mergedDataset.arcs;
|
|
61442
|
-
dataset.layers = mergedDataset.layers;
|
|
61443
|
-
return clipLyr;
|
|
61444
|
-
}
|
|
61445
|
-
|
|
61446
|
-
function bboxPointsToClipDataset(arr) {
|
|
61447
|
-
var arcs = [];
|
|
61448
|
-
var shape = [];
|
|
61449
|
-
var layer = {geometry_type: 'polygon', shapes: [[shape]]};
|
|
61450
|
-
var p1, p2;
|
|
61451
|
-
for (var i=0, n=arr.length - 1; i<n; i++) {
|
|
61452
|
-
p1 = arr[i];
|
|
61453
|
-
p2 = arr[i+1];
|
|
61454
|
-
arcs.push([[p1.x, p1.y], [p2.x, p2.y]]);
|
|
61455
|
-
shape.push(i);
|
|
61456
|
-
}
|
|
61457
|
-
return {
|
|
61458
|
-
arcs: new ArcCollection(arcs),
|
|
61459
|
-
layers: [layer]
|
|
61460
|
-
};
|
|
61461
|
-
}
|
|
61462
|
-
|
|
61463
|
-
function findBBoxCutPoints(arcs, bbox) {
|
|
61464
|
-
var left = bbox[0],
|
|
61465
|
-
bottom = bbox[1],
|
|
61466
|
-
right = bbox[2],
|
|
61467
|
-
top = bbox[3];
|
|
61468
|
-
|
|
61469
|
-
// arrays of intersection points along each bbox edge
|
|
61470
|
-
var tt = [],
|
|
61471
|
-
rr = [],
|
|
61472
|
-
bb = [],
|
|
61473
|
-
ll = [];
|
|
61474
|
-
|
|
61475
|
-
arcs.forEachSegment(function(i, j, xx, yy) {
|
|
61476
|
-
var ax = xx[i],
|
|
61477
|
-
ay = yy[i],
|
|
61478
|
-
bx = xx[j],
|
|
61479
|
-
by = yy[j];
|
|
61480
|
-
var hit;
|
|
61481
|
-
if (segmentOutsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
|
|
61482
|
-
if (segmentInsideBBox(ax, ay, bx, by, left, bottom, right, top)) return;
|
|
61483
|
-
|
|
61484
|
-
hit = geom.segmentIntersection(left, top, right, top, ax, ay, bx, by);
|
|
61485
|
-
if (hit) addHit(tt, hit, i, j, xx, yy);
|
|
61486
|
-
|
|
61487
|
-
hit = geom.segmentIntersection(left, bottom, right, bottom, ax, ay, bx, by);
|
|
61488
|
-
if (hit) addHit(bb, hit, i, j, xx, yy);
|
|
61489
|
-
|
|
61490
|
-
hit = geom.segmentIntersection(left, bottom, left, top, ax, ay, bx, by);
|
|
61491
|
-
if (hit) addHit(ll, hit, i, j, xx, yy);
|
|
61492
|
-
|
|
61493
|
-
hit = geom.segmentIntersection(right, bottom, right, top, ax, ay, bx, by);
|
|
61494
|
-
if (hit) addHit(rr, hit, i, j, xx, yy);
|
|
61495
|
-
});
|
|
61496
|
-
|
|
61497
|
-
return {
|
|
61498
|
-
cutPoints: ll.concat(bb, rr, tt),
|
|
61499
|
-
bboxPoints: getDividedBBoxPoints(bbox, ll, tt, rr, bb)
|
|
61500
|
-
};
|
|
61501
|
-
|
|
61502
|
-
function addHit(arr, hit, i, j, xx, yy) {
|
|
61503
|
-
if (!hit) return;
|
|
61504
|
-
arr.push(formatHit(hit[0], hit[1], i, j, xx, yy));
|
|
61505
|
-
if (hit.length == 4) {
|
|
61506
|
-
arr.push(formatHit(hit[2], hit[3], i, j, xx, yy));
|
|
61507
|
-
}
|
|
61508
|
-
}
|
|
61509
|
-
|
|
61510
|
-
function formatHit(x, y, i, j, xx, yy) {
|
|
61511
|
-
var ids = formatIntersectingSegment(x, y, i, j, xx, yy);
|
|
61512
|
-
return getCutPoint(x, y, ids[0], ids[1]);
|
|
61513
|
-
}
|
|
61514
|
-
}
|
|
61515
|
-
|
|
61516
|
-
function segmentOutsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
|
|
61517
|
-
return ax < xmin && bx < xmin || ax > xmax && bx > xmax ||
|
|
61518
|
-
ay < ymin && by < ymin || ay > ymax && by > ymax;
|
|
61519
|
-
}
|
|
61520
|
-
|
|
61521
|
-
function segmentInsideBBox(ax, ay, bx, by, xmin, ymin, xmax, ymax) {
|
|
61522
|
-
return ax > xmin && bx > xmin && ax < xmax && bx < xmax &&
|
|
61523
|
-
ay > ymin && by > ymin && ay < ymax && by < ymax;
|
|
61524
|
-
}
|
|
61525
|
-
|
|
61526
|
-
// Returns an array of points representing the vertices in
|
|
61527
|
-
// the bbox with cutting points inserted.
|
|
61528
|
-
function getDividedBBoxPoints(bbox, ll, tt, rr, bb) {
|
|
61529
|
-
var bl = {x: bbox[0], y: bbox[1]},
|
|
61530
|
-
tl = {x: bbox[0], y: bbox[3]},
|
|
61531
|
-
tr = {x: bbox[2], y: bbox[3]},
|
|
61532
|
-
br = {x: bbox[2], y: bbox[1]};
|
|
61533
|
-
ll = utils.sortOn(ll.concat([bl, tl]), 'y', true);
|
|
61534
|
-
tt = utils.sortOn(tt.concat([tl, tr]), 'x', true);
|
|
61535
|
-
rr = utils.sortOn(rr.concat([tr, br]), 'y', false);
|
|
61536
|
-
bb = utils.sortOn(bb.concat([br, bl]), 'x', false);
|
|
61537
|
-
return ll.concat(tt, rr, bb).reduce(function(memo, p2) {
|
|
61538
|
-
var p1 = memo.length > 0 ? memo[memo.length-1] : null;
|
|
61539
|
-
if (p1 === null || p1.x != p2.x || p1.y != p2.y) memo.push(p2);
|
|
61540
|
-
return memo;
|
|
61541
|
-
}, []);
|
|
61542
|
-
}
|
|
61543
|
-
|
|
61544
|
-
var Bbox2Clipping = /*#__PURE__*/Object.freeze({
|
|
61545
|
-
__proto__: null,
|
|
61546
|
-
divideDatasetByBBox: divideDatasetByBBox,
|
|
61547
|
-
segmentInsideBBox: segmentInsideBBox,
|
|
61548
|
-
segmentOutsideBBox: segmentOutsideBBox
|
|
61549
|
-
});
|
|
61550
|
-
|
|
61551
62457
|
cmd.clipLayers = function(target, src, dataset, opts) {
|
|
61552
62458
|
return clipLayers(target, src, dataset, "clip", opts);
|
|
61553
62459
|
};
|
|
@@ -64692,7 +65598,7 @@ ${svg}
|
|
|
64692
65598
|
cmd.lines = function(lyr, dataset, opts) {
|
|
64693
65599
|
opts = opts || {};
|
|
64694
65600
|
if (opts.callouts) {
|
|
64695
|
-
requirePointLayer$
|
|
65601
|
+
requirePointLayer$2(lyr);
|
|
64696
65602
|
return pointsToCallouts(lyr, dataset, opts);
|
|
64697
65603
|
} else if (lyr.geometry_type == 'point') {
|
|
64698
65604
|
return pointsToLines(lyr, dataset, opts);
|
|
@@ -75179,231 +76085,6 @@ ${svg}
|
|
|
75179
76085
|
return api;
|
|
75180
76086
|
}
|
|
75181
76087
|
|
|
75182
|
-
cmd.filterGeom = function(lyr, arcs, opts) {
|
|
75183
|
-
if (!layerHasGeometry$1(lyr)) {
|
|
75184
|
-
stop$1("Layer is missing geometry");
|
|
75185
|
-
}
|
|
75186
|
-
if (opts.bbox) {
|
|
75187
|
-
filterByBoundsIntersection(lyr, arcs, opts);
|
|
75188
|
-
}
|
|
75189
|
-
cmd.filterFeatures(lyr, arcs, {remove_empty: true, verbose: false});
|
|
75190
|
-
};
|
|
75191
|
-
|
|
75192
|
-
function filterByBoundsIntersection(lyr, arcs, opts) {
|
|
75193
|
-
var filter = getBoundsIntersectionFilter(opts.bbox, lyr, arcs);
|
|
75194
|
-
noteLayerWillChange(lyr, {operation: 'filter-geom', unit: 'shapes'});
|
|
75195
|
-
editShapes(lyr.shapes, filter);
|
|
75196
|
-
markLayerChanged(lyr, {operation: 'filter-geom', unit: 'shapes'});
|
|
75197
|
-
}
|
|
75198
|
-
|
|
75199
|
-
function getBoundsIntersectionFilter(bbox, lyr, arcs) {
|
|
75200
|
-
var bounds = new Bounds(bbox);
|
|
75201
|
-
var filter = lyr.geometry_type == 'point' ?
|
|
75202
|
-
getPointInBoundsTest(bounds) :
|
|
75203
|
-
getPathBoundsIntersectionTest(bounds, arcs);
|
|
75204
|
-
return filter;
|
|
75205
|
-
}
|
|
75206
|
-
|
|
75207
|
-
function getPointInBoundsTest(bounds) {
|
|
75208
|
-
return function(xy) {
|
|
75209
|
-
var contains = bounds.containsPoint(xy[0], xy[1]);
|
|
75210
|
-
return contains ? xy : null;
|
|
75211
|
-
};
|
|
75212
|
-
}
|
|
75213
|
-
|
|
75214
|
-
// V1 too-simple test: bounding-box intersection
|
|
75215
|
-
// internal.getPathBoundsIntersectionTest = function(bounds, arcs) {
|
|
75216
|
-
// return function(path) {
|
|
75217
|
-
// return bounds.intersects(arcs.getSimpleShapeBounds(path)) ? path : null;
|
|
75218
|
-
// };
|
|
75219
|
-
// };
|
|
75220
|
-
|
|
75221
|
-
function getPathBoundsIntersectionTest(bounds, arcs) {
|
|
75222
|
-
var bbox = bounds.toArray(),
|
|
75223
|
-
left = bbox[0],
|
|
75224
|
-
bottom = bbox[1],
|
|
75225
|
-
right = bbox[2],
|
|
75226
|
-
top = bbox[3];
|
|
75227
|
-
|
|
75228
|
-
return function(path) {
|
|
75229
|
-
// case: bounding boxes don't intersect -> the path doesn't intersect the box
|
|
75230
|
-
if (!bounds.intersects(arcs.getSimpleShapeBounds(path))) {
|
|
75231
|
-
return null;
|
|
75232
|
-
}
|
|
75233
|
-
var intersects = false;
|
|
75234
|
-
var ax, ay, bx, by;
|
|
75235
|
-
var iter = arcs.getShapeIter(path);
|
|
75236
|
-
|
|
75237
|
-
if (iter.hasNext()) {
|
|
75238
|
-
ax = iter.x;
|
|
75239
|
-
ay = iter.y;
|
|
75240
|
-
}
|
|
75241
|
-
while (iter.hasNext()) {
|
|
75242
|
-
bx = ax;
|
|
75243
|
-
by = ay;
|
|
75244
|
-
ax = iter.x;
|
|
75245
|
-
ay = iter.y;
|
|
75246
|
-
if (segmentOutsideBBox(ax, ay, bx, by, left, bottom, right, top)) continue;
|
|
75247
|
-
if (segmentInsideBBox(ax, ay, bx, by, left, bottom, right, top)) {
|
|
75248
|
-
intersects = true;
|
|
75249
|
-
break;
|
|
75250
|
-
}
|
|
75251
|
-
if (geom.segmentIntersection(left, top, right, top, ax, ay, bx, by) ||
|
|
75252
|
-
geom.segmentIntersection(left, bottom, right, bottom, ax, ay, bx, by) ||
|
|
75253
|
-
geom.segmentIntersection(left, bottom, left, top, ax, ay, bx, by) ||
|
|
75254
|
-
geom.segmentIntersection(right, bottom, right, top, ax, ay, bx, by)) {
|
|
75255
|
-
intersects = true;
|
|
75256
|
-
break;
|
|
75257
|
-
}
|
|
75258
|
-
}
|
|
75259
|
-
|
|
75260
|
-
// case: bbox is entirely inside this ring
|
|
75261
|
-
if (!intersects && geom.testPointInRing(left, bottom, path, arcs)) {
|
|
75262
|
-
intersects = true;
|
|
75263
|
-
}
|
|
75264
|
-
return intersects ? path : null;
|
|
75265
|
-
};
|
|
75266
|
-
}
|
|
75267
|
-
|
|
75268
|
-
// Return a function for testing if a shape (path or point) intersects a bounding box
|
|
75269
|
-
// TODO: move this function to a different file
|
|
75270
|
-
function getBBoxIntersectionTest(bbox, lyr, arcs) {
|
|
75271
|
-
var filter = getBoundsIntersectionFilter(bbox, lyr, arcs);
|
|
75272
|
-
return function(shapeId) {
|
|
75273
|
-
var shp = lyr.shapes[shapeId];
|
|
75274
|
-
if (!shp) return false;
|
|
75275
|
-
for (var i=0; i<shp.length; i++) {
|
|
75276
|
-
if (filter(shp[i])) return true;
|
|
75277
|
-
}
|
|
75278
|
-
return false;
|
|
75279
|
-
};
|
|
75280
|
-
}
|
|
75281
|
-
|
|
75282
|
-
// return array of shape ids
|
|
75283
|
-
function findShapesIntersectingBBox(bbox, lyr, arcs) {
|
|
75284
|
-
var test = getBBoxIntersectionTest(bbox, lyr, arcs);
|
|
75285
|
-
var ids = [];
|
|
75286
|
-
for (var i=0; i<lyr.shapes.length; i++) {
|
|
75287
|
-
if (test(i)) ids.push(i);
|
|
75288
|
-
}
|
|
75289
|
-
return ids;
|
|
75290
|
-
}
|
|
75291
|
-
|
|
75292
|
-
var FilterGeom = /*#__PURE__*/Object.freeze({
|
|
75293
|
-
__proto__: null,
|
|
75294
|
-
findShapesIntersectingBBox: findShapesIntersectingBBox,
|
|
75295
|
-
getBBoxIntersectionTest: getBBoxIntersectionTest
|
|
75296
|
-
});
|
|
75297
|
-
|
|
75298
|
-
cmd.filterFeatures = function(lyr, arcs, opts) {
|
|
75299
|
-
var records = lyr.data ? lyr.data.getRecords() : null,
|
|
75300
|
-
shapes = lyr.shapes || null,
|
|
75301
|
-
n = getFeatureCount(lyr),
|
|
75302
|
-
filteredShapes = shapes ? [] : null,
|
|
75303
|
-
filteredRecords = records ? [] : null,
|
|
75304
|
-
filteredLyr = getOutputLayer(lyr, opts),
|
|
75305
|
-
invert = !!opts.invert,
|
|
75306
|
-
filter;
|
|
75307
|
-
|
|
75308
|
-
if (opts.expression) {
|
|
75309
|
-
filter = compileFeatureExpression(opts.expression, lyr, arcs);
|
|
75310
|
-
}
|
|
75311
|
-
|
|
75312
|
-
if (opts.ids) {
|
|
75313
|
-
filter = combineFilters(filter, getIdFilter(opts.ids));
|
|
75314
|
-
}
|
|
75315
|
-
|
|
75316
|
-
if (opts.remove_empty) {
|
|
75317
|
-
filter = combineFilters(filter, getNullGeometryFilter(lyr, arcs));
|
|
75318
|
-
}
|
|
75319
|
-
|
|
75320
|
-
if (opts.bbox) {
|
|
75321
|
-
filter = combineFilters(filter, getBBoxIntersectionTest(opts.bbox, lyr, arcs));
|
|
75322
|
-
}
|
|
75323
|
-
|
|
75324
|
-
if (!filter) {
|
|
75325
|
-
stop$1("Missing a filter criterion");
|
|
75326
|
-
}
|
|
75327
|
-
|
|
75328
|
-
utils.repeat(n, function(shapeId) {
|
|
75329
|
-
var result = filter(shapeId);
|
|
75330
|
-
requireBooleanResult(result);
|
|
75331
|
-
if (invert) result = !result;
|
|
75332
|
-
if (result === true) {
|
|
75333
|
-
if (shapes) filteredShapes.push(shapes[shapeId] || null);
|
|
75334
|
-
if (records) filteredRecords.push(records[shapeId] || null);
|
|
75335
|
-
}
|
|
75336
|
-
});
|
|
75337
|
-
|
|
75338
|
-
if (filteredLyr == lyr) {
|
|
75339
|
-
noteLayerWillChange(lyr, {operation: 'filter'});
|
|
75340
|
-
}
|
|
75341
|
-
filteredLyr.shapes = filteredShapes;
|
|
75342
|
-
filteredLyr.data = filteredRecords ? new DataTable(filteredRecords) : null;
|
|
75343
|
-
if (filteredLyr == lyr) {
|
|
75344
|
-
markLayerChanged(lyr, {operation: 'filter'});
|
|
75345
|
-
}
|
|
75346
|
-
if (opts.no_replace) {
|
|
75347
|
-
// if adding a layer, don't share objects between source and filtered layer
|
|
75348
|
-
filteredLyr = copyLayer(filteredLyr);
|
|
75349
|
-
}
|
|
75350
|
-
|
|
75351
|
-
if (opts.verbose !== false && !opts.quiet) {
|
|
75352
|
-
message(utils.format('Retained %,d of %,d features', getFeatureCount(filteredLyr), n));
|
|
75353
|
-
}
|
|
75354
|
-
|
|
75355
|
-
return filteredLyr;
|
|
75356
|
-
};
|
|
75357
|
-
|
|
75358
|
-
// TODO: update filter command to use this function
|
|
75359
|
-
function filterLayerInPlace(lyr, filter, invert) {
|
|
75360
|
-
var records = lyr.data ? lyr.data.getRecords() : null,
|
|
75361
|
-
shapes = lyr.shapes || null,
|
|
75362
|
-
n = getFeatureCount(lyr),
|
|
75363
|
-
filteredShapes = shapes ? [] : null,
|
|
75364
|
-
filteredRecords = records ? [] : null;
|
|
75365
|
-
utils.repeat(n, function(shapeId) {
|
|
75366
|
-
var result = filter(shapeId);
|
|
75367
|
-
requireBooleanResult(result);
|
|
75368
|
-
if (result === true) {
|
|
75369
|
-
if (shapes) filteredShapes.push(shapes[shapeId] || null);
|
|
75370
|
-
if (records) filteredRecords.push(records[shapeId] || null);
|
|
75371
|
-
}
|
|
75372
|
-
});
|
|
75373
|
-
noteLayerWillChange(lyr, {operation: 'filterLayerInPlace'});
|
|
75374
|
-
lyr.shapes = filteredShapes;
|
|
75375
|
-
lyr.data = filteredRecords ? new DataTable(filteredRecords) : null;
|
|
75376
|
-
markLayerChanged(lyr, {operation: 'filterLayerInPlace'});
|
|
75377
|
-
}
|
|
75378
|
-
|
|
75379
|
-
function getIdFilter(ids) {
|
|
75380
|
-
var set = new Set(ids);
|
|
75381
|
-
return function(i) {
|
|
75382
|
-
return set.has(i);
|
|
75383
|
-
};
|
|
75384
|
-
}
|
|
75385
|
-
|
|
75386
|
-
function getNullGeometryFilter(lyr, arcs) {
|
|
75387
|
-
var shapes = lyr.shapes;
|
|
75388
|
-
if (lyr.geometry_type == 'polygon') {
|
|
75389
|
-
return getEmptyPolygonFilter(shapes, arcs);
|
|
75390
|
-
}
|
|
75391
|
-
return function(i) {return !!shapes[i];};
|
|
75392
|
-
}
|
|
75393
|
-
|
|
75394
|
-
function getEmptyPolygonFilter(shapes, arcs) {
|
|
75395
|
-
return function(i) {
|
|
75396
|
-
var shp = shapes[i];
|
|
75397
|
-
return !!shp && geom.getPlanarShapeArea(shapes[i], arcs) > 0;
|
|
75398
|
-
};
|
|
75399
|
-
}
|
|
75400
|
-
|
|
75401
|
-
function combineFilters(a, b) {
|
|
75402
|
-
return a && b ? function(id) {
|
|
75403
|
-
return a(id) && b(id);
|
|
75404
|
-
} : (a || b);
|
|
75405
|
-
}
|
|
75406
|
-
|
|
75407
76088
|
cmd.evaluateEachFeature = function(lyr, dataset, expArg, opts) {
|
|
75408
76089
|
var n = getFeatureCount(lyr),
|
|
75409
76090
|
arcs = dataset.arcs,
|
|
@@ -79250,7 +79931,7 @@ ${svg}
|
|
|
79250
79931
|
}
|
|
79251
79932
|
|
|
79252
79933
|
cmd.pointToGrid = function(targetLayers, targetDataset, opts) {
|
|
79253
|
-
targetLayers.forEach(requirePointLayer$
|
|
79934
|
+
targetLayers.forEach(requirePointLayer$2);
|
|
79254
79935
|
if (opts.interval > 0 === false) {
|
|
79255
79936
|
stop$1('Expected a non-negative interval parameter');
|
|
79256
79937
|
}
|
|
@@ -82035,232 +82716,6 @@ ${svg}
|
|
|
82035
82716
|
stopJob(job);
|
|
82036
82717
|
};
|
|
82037
82718
|
|
|
82038
|
-
// The icon names accepted by the icon= property of the -style command.
|
|
82039
|
-
// Kept in a module of its own so that both the renderer (svg-symbols.mjs) and
|
|
82040
|
-
// the -style command can use it without forming an import cycle.
|
|
82041
|
-
|
|
82042
|
-
var iconNames = ['circle', 'square', 'ring', 'star'];
|
|
82043
|
-
|
|
82044
|
-
function isSupportedIconName(name) {
|
|
82045
|
-
return iconNames.indexOf(name) > -1;
|
|
82046
|
-
}
|
|
82047
|
-
|
|
82048
|
-
cmd.svgStyle = function(lyr, dataset, opts) {
|
|
82049
|
-
var filterFn, table, fields, hasNewFields, optFields, clearedByPosition, fieldsBefore;
|
|
82050
|
-
|
|
82051
|
-
function hadField(field) {
|
|
82052
|
-
return field in fieldsBefore;
|
|
82053
|
-
}
|
|
82054
|
-
|
|
82055
|
-
if (getFeatureCount(lyr) === 0) {
|
|
82056
|
-
return;
|
|
82057
|
-
}
|
|
82058
|
-
if (!lyr.data) {
|
|
82059
|
-
initDataTable(lyr);
|
|
82060
|
-
}
|
|
82061
|
-
if (opts.where) {
|
|
82062
|
-
filterFn = compileFeatureExpression(opts.where, lyr, dataset.arcs);
|
|
82063
|
-
}
|
|
82064
|
-
if (opts.ids) {
|
|
82065
|
-
filterFn = combineFilters(filterFn, getIdFilter(opts.ids));
|
|
82066
|
-
}
|
|
82067
|
-
if (opts.clear) {
|
|
82068
|
-
lyr.data.getFields().filter(isSupportedSvgStyleProperty).forEach(lyr.data.deleteField, lyr.data);
|
|
82069
|
-
}
|
|
82070
|
-
table = getLayerDataTable(lyr);
|
|
82071
|
-
optFields = getOptionFields(opts);
|
|
82072
|
-
fields = getStyleFields(optFields);
|
|
82073
|
-
// Which of dx/dy/text-anchor a label-pos in this command clears: not the ones
|
|
82074
|
-
// the same command also sets, so that `label-pos=n dx=3` keeps the nudge it
|
|
82075
|
-
// was given instead of clearing it a moment later.
|
|
82076
|
-
//
|
|
82077
|
-
// Clearing at all is what keeps the shorthand usable, since a value on the
|
|
82078
|
-
// record wins over the position: without it, setting a position on a label
|
|
82079
|
-
// that had been dragged would appear to do nothing.
|
|
82080
|
-
clearedByPosition = labelPositionDerivedFields.filter(function(field) {
|
|
82081
|
-
return optFields.indexOf(field) == -1;
|
|
82082
|
-
});
|
|
82083
|
-
hasNewFields = fields.some(function(field) {
|
|
82084
|
-
return !table.fieldExists(field);
|
|
82085
|
-
});
|
|
82086
|
-
// The table's columns as this command found them. Taken once, because the
|
|
82087
|
-
// command adds to them as it runs, and a blanked property must be judged
|
|
82088
|
-
// against what was there before rather than against what it has just made.
|
|
82089
|
-
fieldsBefore = utils.arrayToIndex(table.getFields());
|
|
82090
|
-
if (fields.length > 0) {
|
|
82091
|
-
if (hasNewFields) {
|
|
82092
|
-
table.captureSchemaBefore({operation: 'style', fields: fields});
|
|
82093
|
-
} else {
|
|
82094
|
-
table.captureFieldsBefore(fields, {operation: 'style'});
|
|
82095
|
-
}
|
|
82096
|
-
}
|
|
82097
|
-
Object.keys(opts).forEach(function(optName) {
|
|
82098
|
-
// undo cli parser name conversion; the regex must be global, or a
|
|
82099
|
-
// property with more than one hyphen (e.g. label-start-offset) is silently
|
|
82100
|
-
// skipped rather than applied
|
|
82101
|
-
var svgName = optName.replace(/_/g, '-');
|
|
82102
|
-
if (!isSupportedSvgStyleProperty(svgName)) {
|
|
82103
|
-
return;
|
|
82104
|
-
}
|
|
82105
|
-
var strVal = opts[optName].trim();
|
|
82106
|
-
// An empty value removes the property, rather than being rejected as an
|
|
82107
|
-
// unparseable one. This is how a control gives a property back: a label
|
|
82108
|
-
// dragged off its position clears label-pos, and there is otherwise no
|
|
82109
|
-
// per-property unset -- only -style clear, which clears all of them.
|
|
82110
|
-
var unset = strVal === '' && emptyValueUnsetsProperty(svgName);
|
|
82111
|
-
var accessor = unset ? null : getSymbolPropertyAccessor(strVal, svgName, lyr);
|
|
82112
|
-
var badIcons = svgName == 'icon' ? [] : null;
|
|
82113
|
-
// Removing a position is not setting one, so it neither validates the
|
|
82114
|
-
// value nor clears the offsets the position would have stood for.
|
|
82115
|
-
var posOnPaths = svgName == 'label-pos' && !unset ? [] : null;
|
|
82116
|
-
table.getRecords().forEach(function(rec, i) {
|
|
82117
|
-
if (filterFn && !filterFn(i)) {
|
|
82118
|
-
// make sure field exists if record is excluded by filter
|
|
82119
|
-
setUndefinedFields(rec, [svgName]);
|
|
82120
|
-
if (svgName == 'label-pos') {
|
|
82121
|
-
// ...but a field the position would only have cleared is one this
|
|
82122
|
-
// command is not writing anywhere, so an excluded record has nothing
|
|
82123
|
-
// to stay consistent with
|
|
82124
|
-
setUndefinedFields(rec, labelPositionDerivedFields, {has: hadField});
|
|
82125
|
-
}
|
|
82126
|
-
} else if (unset) {
|
|
82127
|
-
// Nothing to remove, and so nothing to create: removing a property no
|
|
82128
|
-
// record has would otherwise add an empty column for it.
|
|
82129
|
-
if (hadField(svgName) || svgName in rec) rec[svgName] = undefined;
|
|
82130
|
-
} else {
|
|
82131
|
-
rec[svgName] = accessor(i);
|
|
82132
|
-
if (badIcons) {
|
|
82133
|
-
addUnsupportedIconName(badIcons, rec.icon);
|
|
82134
|
-
}
|
|
82135
|
-
if (posOnPaths) {
|
|
82136
|
-
if (!parseLabelPosition(rec['label-pos'])) {
|
|
82137
|
-
stop$1('Unexpected value for label-pos:', rec['label-pos']);
|
|
82138
|
-
}
|
|
82139
|
-
if (shapeIsPathLabel(lyr.shapes && lyr.shapes[i], rec)) {
|
|
82140
|
-
// Not stored, so that ignoring it means ignoring it: a stored
|
|
82141
|
-
// position would show up in the style panel and would start
|
|
82142
|
-
// applying if the label ever lost all but one of its knots. Its
|
|
82143
|
-
// text-anchor is left alone too -- that one does place text along a
|
|
82144
|
-
// path, so a position that had no effect must not clear it.
|
|
82145
|
-
posOnPaths.push(i);
|
|
82146
|
-
rec['label-pos'] = undefined;
|
|
82147
|
-
} else {
|
|
82148
|
-
setUndefinedFields(rec, clearedByPosition, {overwrite: true, has: hadField});
|
|
82149
|
-
}
|
|
82150
|
-
}
|
|
82151
|
-
}
|
|
82152
|
-
});
|
|
82153
|
-
if (badIcons && badIcons.length > 0) {
|
|
82154
|
-
warn(formatUnsupportedIconMessage(badIcons));
|
|
82155
|
-
}
|
|
82156
|
-
if (posOnPaths && posOnPaths.length > 0) {
|
|
82157
|
-
warn(formatPositionOnPathMessage(posOnPaths));
|
|
82158
|
-
}
|
|
82159
|
-
});
|
|
82160
|
-
if (fields.length > 0) {
|
|
82161
|
-
if (hasNewFields) {
|
|
82162
|
-
table.markSchemaChanged({operation: 'style'});
|
|
82163
|
-
} else {
|
|
82164
|
-
table.markFieldsChanged(fields, {operation: 'style'});
|
|
82165
|
-
}
|
|
82166
|
-
}
|
|
82167
|
-
};
|
|
82168
|
-
|
|
82169
|
-
// The style properties this command was given, in SVG spelling.
|
|
82170
|
-
function getOptionFields(opts) {
|
|
82171
|
-
var fields = [];
|
|
82172
|
-
Object.keys(opts).forEach(function(optName) {
|
|
82173
|
-
var svgName = optName.replace(/_/g, '-');
|
|
82174
|
-
if (isSupportedSvgStyleProperty(svgName)) addField(fields, svgName);
|
|
82175
|
-
});
|
|
82176
|
-
return fields;
|
|
82177
|
-
}
|
|
82178
|
-
|
|
82179
|
-
// The fields the command will write, which is what the undo capture covers.
|
|
82180
|
-
// label-pos reaches dx/dy/text-anchor as well -- it no longer stores values in
|
|
82181
|
-
// them, but it does clear them.
|
|
82182
|
-
function getStyleFields(optFields) {
|
|
82183
|
-
var fields = [];
|
|
82184
|
-
optFields.forEach(function(svgName) {
|
|
82185
|
-
addField(fields, svgName);
|
|
82186
|
-
if (svgName == 'label-pos') {
|
|
82187
|
-
labelPositionFields.forEach(function(field) {
|
|
82188
|
-
addField(fields, field);
|
|
82189
|
-
});
|
|
82190
|
-
}
|
|
82191
|
-
});
|
|
82192
|
-
return fields;
|
|
82193
|
-
}
|
|
82194
|
-
|
|
82195
|
-
function addField(fields, field) {
|
|
82196
|
-
if (fields.indexOf(field) == -1) {
|
|
82197
|
-
fields.push(field);
|
|
82198
|
-
}
|
|
82199
|
-
}
|
|
82200
|
-
|
|
82201
|
-
// Icon names can not be validated when options are parsed, because an icon=
|
|
82202
|
-
// value may be a field name or an expression. The assigned values are checked
|
|
82203
|
-
// instead, so that computed names are covered too. Unsupported names are a
|
|
82204
|
-
// warning, not an error -- features with an unsupported name render without an
|
|
82205
|
-
// icon.
|
|
82206
|
-
var maxReportedIconNames = 4;
|
|
82207
|
-
|
|
82208
|
-
function addUnsupportedIconName(names, val) {
|
|
82209
|
-
var name;
|
|
82210
|
-
if (!val) return; // a blank value removes the icon
|
|
82211
|
-
name = String(val);
|
|
82212
|
-
if (isSupportedIconName(name) || names.indexOf(name) > -1) return;
|
|
82213
|
-
names.push(name);
|
|
82214
|
-
}
|
|
82215
|
-
|
|
82216
|
-
function formatUnsupportedIconMessage(names) {
|
|
82217
|
-
var extra = names.length - maxReportedIconNames;
|
|
82218
|
-
var listed = extra > 0 ? names.slice(0, maxReportedIconNames) : names;
|
|
82219
|
-
var str = 'Unsupported icon ' + (names.length > 1 ? 'names' : 'name') + ': ' +
|
|
82220
|
-
listed.join(', ');
|
|
82221
|
-
if (extra > 0) {
|
|
82222
|
-
str += ' (and ' + extra + ' more)';
|
|
82223
|
-
}
|
|
82224
|
-
return str + '. Expected one of: ' + iconNames.join(', ');
|
|
82225
|
-
}
|
|
82226
|
-
|
|
82227
|
-
// Adds @fields to @rec with no value, so that a record the filter excluded
|
|
82228
|
-
// still has the same schema as the ones it kept. With overwrite, also blanks a
|
|
82229
|
-
// value already there -- which is how setting a position takes back the offsets
|
|
82230
|
-
// a label was carrying.
|
|
82231
|
-
//
|
|
82232
|
-
// @has: optional test for whether the layer carries a field at all. A field
|
|
82233
|
-
// nobody has is not created in order to be blanked: -style label-pos=n clears
|
|
82234
|
-
// dx, dy and text-anchor because a value on the record wins over the position,
|
|
82235
|
-
// and there is nothing to win with when the column does not exist. Without
|
|
82236
|
-
// this, clicking a position in the style panel put three empty columns in the
|
|
82237
|
-
// user's table, and three empty columns in their CSV.
|
|
82238
|
-
function setUndefinedFields(rec, fields, opts) {
|
|
82239
|
-
var overwrite = !!(opts && opts.overwrite);
|
|
82240
|
-
var has = opts && opts.has;
|
|
82241
|
-
fields.forEach(function(field) {
|
|
82242
|
-
if (has && !has(field) && field in rec === false) return;
|
|
82243
|
-
if (overwrite || field in rec === false) {
|
|
82244
|
-
rec[field] = undefined;
|
|
82245
|
-
}
|
|
82246
|
-
});
|
|
82247
|
-
}
|
|
82248
|
-
|
|
82249
|
-
// label-pos places text around an anchor point, which a label strung along a
|
|
82250
|
-
// path does not have: its text runs from a start offset in the direction the
|
|
82251
|
-
// path goes. A warning rather than an error, because a layer can hold both
|
|
82252
|
-
// kinds of label and styling all of it at once is reasonable.
|
|
82253
|
-
function formatPositionOnPathMessage(ids) {
|
|
82254
|
-
var extra = ids.length - maxReportedIds;
|
|
82255
|
-
var listed = (extra > 0 ? ids.slice(0, maxReportedIds) : ids).join(', ');
|
|
82256
|
-
return 'Ignoring label-pos on ' + ids.length + ' path ' +
|
|
82257
|
-
(ids.length > 1 ? 'labels' : 'label') + ' (' + listed +
|
|
82258
|
-
(extra > 0 ? ' and ' + extra + ' more' : '') + '). ' +
|
|
82259
|
-
'Use label-start-offset= and text-anchor= to place text along a path.';
|
|
82260
|
-
}
|
|
82261
|
-
|
|
82262
|
-
var maxReportedIds = 4;
|
|
82263
|
-
|
|
82264
82719
|
function getStickArrowCoords(d) {
|
|
82265
82720
|
return getArrowCoords(d, 'stick');
|
|
82266
82721
|
}
|
|
@@ -82792,7 +83247,7 @@ ${svg}
|
|
|
82792
83247
|
|
|
82793
83248
|
// TODO: refactor to remove duplication in mapshaper-svg-style.js
|
|
82794
83249
|
cmd.symbols = function(inputLyr, dataset, opts) {
|
|
82795
|
-
requirePointLayer$
|
|
83250
|
+
requirePointLayer$2(inputLyr);
|
|
82796
83251
|
var lyr = opts.no_replace ? copyLayer(inputLyr) : inputLyr;
|
|
82797
83252
|
var shapeMode = !!opts.geographic;
|
|
82798
83253
|
var metersPerPx;
|
|
@@ -83393,6 +83848,9 @@ ${svg}
|
|
|
83393
83848
|
if (name == 'rectangle') {
|
|
83394
83849
|
return !opts.source && !opts.bbox;
|
|
83395
83850
|
}
|
|
83851
|
+
if (name == 'labels') {
|
|
83852
|
+
return !('coordinates' in opts);
|
|
83853
|
+
}
|
|
83396
83854
|
return [
|
|
83397
83855
|
'affine', 'alpha-shapes', 'blur', 'buffer', 'calc', 'check-geometry',
|
|
83398
83856
|
'classify', 'clean', 'clip', 'cluster', 'contours', 'dashlines', 'data-fill',
|
|
@@ -83407,7 +83865,10 @@ ${svg}
|
|
|
83407
83865
|
].indexOf(name) > -1;
|
|
83408
83866
|
}
|
|
83409
83867
|
|
|
83410
|
-
function commandAcceptsEmptyTarget(name) {
|
|
83868
|
+
function commandAcceptsEmptyTarget(name, opts) {
|
|
83869
|
+
if (name == 'labels') {
|
|
83870
|
+
return 'coordinates' in opts;
|
|
83871
|
+
}
|
|
83411
83872
|
return name == 'graticule' || name == 'i' || name == 'help' ||
|
|
83412
83873
|
name == 'point-grid' || name == 'shape' || name == 'rectangle' || name == 'frame' ||
|
|
83413
83874
|
name == 'require' || name == 'run' || name == 'define' ||
|
|
@@ -83503,7 +83964,7 @@ ${svg}
|
|
|
83503
83964
|
stop$1(utils.format('Missing target: %s\nAvailable layers: %s',
|
|
83504
83965
|
opts.target, getFormattedLayerList(job.catalog)));
|
|
83505
83966
|
}
|
|
83506
|
-
if (!commandAcceptsEmptyTarget(name)) {
|
|
83967
|
+
if (!commandAcceptsEmptyTarget(name, opts)) {
|
|
83507
83968
|
stop$1("No data is available");
|
|
83508
83969
|
}
|
|
83509
83970
|
}
|
|
@@ -83526,7 +83987,7 @@ ${svg}
|
|
|
83526
83987
|
}
|
|
83527
83988
|
outputLayers = cmd.addShape(targetLayers, targetDataset, opts);
|
|
83528
83989
|
|
|
83529
|
-
} else if (name == 'add-label') {
|
|
83990
|
+
} else if (name == 'add-label' || name == 'labels' && 'coordinates' in opts) {
|
|
83530
83991
|
if (!targetDataset) {
|
|
83531
83992
|
targetDataset = {info: {}, layers: []};
|
|
83532
83993
|
targetLayers = targetDataset.layers;
|
|
@@ -83534,6 +83995,9 @@ ${svg}
|
|
|
83534
83995
|
}
|
|
83535
83996
|
outputLayers = cmd.addLabel(targetLayers, targetDataset, opts);
|
|
83536
83997
|
|
|
83998
|
+
} else if (name == 'labels') {
|
|
83999
|
+
outputLayers = cmd.labels(targetLayers, targetDataset, opts);
|
|
84000
|
+
|
|
83537
84001
|
} else if (name == 'add-layer') {
|
|
83538
84002
|
// The new layer arrives in a dataset of its own, so it is added to the
|
|
83539
84003
|
// catalog here rather than integrated into the target below.
|
|
@@ -84111,7 +84575,7 @@ ${svg}
|
|
|
84111
84575
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
84112
84576
|
}
|
|
84113
84577
|
|
|
84114
|
-
var version = "0.7.
|
|
84578
|
+
var version = "0.7.70";
|
|
84115
84579
|
|
|
84116
84580
|
// Parse command line args into commands and run them
|
|
84117
84581
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|
|
@@ -84926,7 +85390,7 @@ ${svg}
|
|
|
84926
85390
|
// parse. Null is what every reader already falls back from.
|
|
84927
85391
|
function measureLabelText(rec) {
|
|
84928
85392
|
var text = toLabelString(rec && rec['label-text']);
|
|
84929
|
-
var font, fontSize, spacing, width;
|
|
85393
|
+
var font, fontSize, spacing, width, failed = false;
|
|
84930
85394
|
if (!text) return null;
|
|
84931
85395
|
font = getFontForRecord(rec);
|
|
84932
85396
|
if (!font) return null;
|
|
@@ -84935,10 +85399,22 @@ ${svg}
|
|
|
84935
85399
|
if (!(fontSize > 0)) return null;
|
|
84936
85400
|
// The widest line, which is what the block of a multi-line label is as wide
|
|
84937
85401
|
// as, and what the browser's getBBox() reports for the same text.
|
|
84938
|
-
|
|
84939
|
-
|
|
85402
|
+
//
|
|
85403
|
+
// A line with bold words in it is measured a run at a time, each in its own
|
|
85404
|
+
// face, which loses the kerning between the last letter of one run and the
|
|
85405
|
+
// first of the next -- a fraction of a pixel at a weight change.
|
|
85406
|
+
width = splitLabelLineRuns(text).reduce(function(max, runs) {
|
|
85407
|
+
var w = runs.reduce(function(sum, run) {
|
|
85408
|
+
var face = run.bold ? getFontForRecord(rec, true) : font;
|
|
85409
|
+
if (!face) {
|
|
85410
|
+
failed = true;
|
|
85411
|
+
return sum;
|
|
85412
|
+
}
|
|
85413
|
+
return sum + measureLine(face, run.text, fontSize, spacing);
|
|
85414
|
+
}, 0);
|
|
84940
85415
|
return w > max ? w : max;
|
|
84941
85416
|
}, 0);
|
|
85417
|
+
if (failed) return null;
|
|
84942
85418
|
return width > 0 ? width : null;
|
|
84943
85419
|
}
|
|
84944
85420
|
|
|
@@ -84962,9 +85438,12 @@ ${svg}
|
|
|
84962
85438
|
// The font a record is drawn in, opened and remembered. A record with no
|
|
84963
85439
|
// font-family is drawn in the layer group's default, the same one the GUI
|
|
84964
85440
|
// measures against -- see getLabelTextDefaults().
|
|
84965
|
-
|
|
85441
|
+
//
|
|
85442
|
+
// @bold: the face its bold runs are drawn in, which is the keyword's 700
|
|
85443
|
+
// whatever the label's own weight is, as it is in CSS.
|
|
85444
|
+
function getFontForRecord(rec, bold) {
|
|
84966
85445
|
var family = rec['font-family'] || 'sans-serif';
|
|
84967
|
-
var weight = getFontWeight(rec);
|
|
85446
|
+
var weight = bold ? WEIGHT_NAMES[LABEL_BOLD_WEIGHT] : getFontWeight(rec);
|
|
84968
85447
|
var italic = isItalic(rec);
|
|
84969
85448
|
var stretch = rec['font-stretch'] || '';
|
|
84970
85449
|
var key = [family, weight, italic ? 'i' : 'n', stretch].join('|');
|
|
@@ -86713,7 +87192,7 @@ ${svg}
|
|
|
86713
87192
|
internal.svg = Object.assign({}, SvgStringify, SvgPathUtils, GeojsonToSvg,
|
|
86714
87193
|
SvgFeatureUtils,
|
|
86715
87194
|
SvgLabels, SvgSymbols, SvgLabelPaths, SvgLabelFit, SvgLabelAlign,
|
|
86716
|
-
SvgLabelMetrics, SvgLabelHalo, SvgLabelCallout);
|
|
87195
|
+
SvgLabelMetrics, SvgLabelHalo, SvgLabelCallout, SvgLabelMarkup);
|
|
86717
87196
|
|
|
86718
87197
|
// Reached through the bundle rather than imported from source, unlike most of
|
|
86719
87198
|
// what tests use, because these modules load fs and fontkit through the
|