mapshaper 0.7.66 → 0.7.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mapshaper-gui +3 -2
- package/mapshaper.js +907 -33
- package/package.json +1 -1
- package/www/index.html +6 -0
- package/www/mapshaper-gui.js +2041 -215
- package/www/mapshaper.js +907 -33
- package/www/page.css +108 -9
package/www/mapshaper-gui.js
CHANGED
|
@@ -1031,98 +1031,6 @@
|
|
|
1031
1031
|
return _el;
|
|
1032
1032
|
}
|
|
1033
1033
|
|
|
1034
|
-
function filterLayerByIds(lyr, ids) {
|
|
1035
|
-
var shapes;
|
|
1036
|
-
if (lyr.shapes) {
|
|
1037
|
-
shapes = ids.map(function(id) {
|
|
1038
|
-
return lyr.shapes[id];
|
|
1039
|
-
});
|
|
1040
|
-
return utils$1.defaults({shapes: shapes, data: null}, lyr);
|
|
1041
|
-
}
|
|
1042
|
-
return lyr;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
function formatLayerNameForDisplay(name) {
|
|
1046
|
-
return name || '[unnamed]';
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
function cleanLayerName(raw) {
|
|
1050
|
-
return raw.replace(/[\n\t/\\]/g, '')
|
|
1051
|
-
.replace(/^[.\s]+/, '').replace(/[.\s]+$/, '');
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
function updateLayerStackOrder(layers) {
|
|
1055
|
-
// 1. assign ascending ids to unassigned layers above the range of other layers
|
|
1056
|
-
layers.forEach(function(o, i) {
|
|
1057
|
-
if (!o.layer.menu_order) o.layer.menu_order = 1e6 + i;
|
|
1058
|
-
});
|
|
1059
|
-
// 2. sort in ascending order
|
|
1060
|
-
layers.sort(function(a, b) {
|
|
1061
|
-
return a.layer.menu_order - b.layer.menu_order;
|
|
1062
|
-
});
|
|
1063
|
-
// 3. assign consecutve ids
|
|
1064
|
-
layers.forEach(function(o, i) {
|
|
1065
|
-
o.layer.menu_order = i + 1;
|
|
1066
|
-
});
|
|
1067
|
-
return layers;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
function sortLayersForMenuDisplay(layers) {
|
|
1071
|
-
layers = updateLayerStackOrder(layers);
|
|
1072
|
-
return layers.reverse();
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
function setLayerPinning(lyr, pinned) {
|
|
1076
|
-
lyr.pinned = !!pinned;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
function calcDotScale(layers, ext) {
|
|
1081
|
-
var bbox = ext.getBounds().scale(1.3).toArray(); // add buffer
|
|
1082
|
-
// var topTier = 50000; // can be a bottleneck
|
|
1083
|
-
var topTier = 10000; // short-circuit counting here
|
|
1084
|
-
var count = 0;
|
|
1085
|
-
layers = layers.filter(function(lyr) {
|
|
1086
|
-
return lyr.geometry_type == 'point' && lyr.gui.style?.dotSize > 0;
|
|
1087
|
-
});
|
|
1088
|
-
layers.forEach(function(lyr) {
|
|
1089
|
-
count += countPoints(lyr.gui.displayLayer.shapes, topTier, bbox);
|
|
1090
|
-
});
|
|
1091
|
-
count = Math.min(topTier, count) || 1;
|
|
1092
|
-
var k = Math.pow(5 - utils$1.clamp(Math.log10(count), 1, 4), 1.25);
|
|
1093
|
-
|
|
1094
|
-
// zoom adjustments
|
|
1095
|
-
var mapScale = ext.scale();
|
|
1096
|
-
if (mapScale < 0.5) {
|
|
1097
|
-
k *= Math.pow(mapScale + 0.5, 0.35);
|
|
1098
|
-
} else if (mapScale > 1) {
|
|
1099
|
-
// scale faster at first
|
|
1100
|
-
k *= Math.pow(Math.min(mapScale, 4), 0.25);
|
|
1101
|
-
k *= Math.pow(mapScale, 0.02);
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
// scale down when map is small
|
|
1105
|
-
var smallSide = Math.min(ext.width(), ext.height());
|
|
1106
|
-
k *= utils$1.clamp(smallSide / 500, 0.5, 1);
|
|
1107
|
-
return k;
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
function countPoints(shapes, max, bbox) {
|
|
1111
|
-
var count = 0;
|
|
1112
|
-
var shp, p;
|
|
1113
|
-
// short-circuit point counting above top threshold
|
|
1114
|
-
for (var i=0, n=shapes.length; i<n && count<max; i++) {
|
|
1115
|
-
shp = shapes[i];
|
|
1116
|
-
for (var j=0, m=(shp ? shp.length : 0); j<m; j++) {
|
|
1117
|
-
p = shp[j];
|
|
1118
|
-
if (p[0] > bbox[0] && p[0] < bbox[2] && p[1] > bbox[1] && p[1] < bbox[3]) {
|
|
1119
|
-
count ++;
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
return count;
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
1034
|
async function showPrompt(msg, title) {
|
|
1127
1035
|
var popup = showPopupAlert(msg, title);
|
|
1128
1036
|
return new Promise(function(resolve) {
|
|
@@ -2669,7 +2577,7 @@
|
|
|
2669
2577
|
return obj === Object(obj); // via underscore
|
|
2670
2578
|
}
|
|
2671
2579
|
|
|
2672
|
-
function clamp$
|
|
2580
|
+
function clamp$5(val, min, max) {
|
|
2673
2581
|
return val < min ? min : (val > max ? max : val);
|
|
2674
2582
|
}
|
|
2675
2583
|
|
|
@@ -3145,12 +3053,12 @@
|
|
|
3145
3053
|
return decimals >= 0 ? num.toFixed(decimals) : String(num);
|
|
3146
3054
|
}
|
|
3147
3055
|
|
|
3148
|
-
function formatNumber$
|
|
3056
|
+
function formatNumber$3(val) {
|
|
3149
3057
|
return val + '';
|
|
3150
3058
|
}
|
|
3151
3059
|
|
|
3152
3060
|
function formatIntlNumber(val) {
|
|
3153
|
-
var str = formatNumber$
|
|
3061
|
+
var str = formatNumber$3(val);
|
|
3154
3062
|
return '"' + str.replace('.', ',') + '"'; // need to quote if comma-delimited
|
|
3155
3063
|
}
|
|
3156
3064
|
|
|
@@ -3320,7 +3228,7 @@
|
|
|
3320
3228
|
function findValueByRank(arr, rank) {
|
|
3321
3229
|
if (!arr.length || rank < 1 || rank > arr.length) error$1("[findValueByRank()] invalid input");
|
|
3322
3230
|
|
|
3323
|
-
rank = clamp$
|
|
3231
|
+
rank = clamp$5(rank | 0, 1, arr.length);
|
|
3324
3232
|
var k = rank - 1, // conv. rank to array index
|
|
3325
3233
|
n = arr.length,
|
|
3326
3234
|
l = 0,
|
|
@@ -3694,12 +3602,12 @@
|
|
|
3694
3602
|
// self-import and the resulting Rollup circular-dependency warning.
|
|
3695
3603
|
var utils = {
|
|
3696
3604
|
addThousandsSep, addslashes, arrayToIndex,
|
|
3697
|
-
clamp: clamp$
|
|
3605
|
+
clamp: clamp$5, cleanNumericString, contains, copyElements, countValues, createBuffer,
|
|
3698
3606
|
defaults, difference,
|
|
3699
3607
|
endsWith, every, expandoBuffer, extend, extendBuffer,
|
|
3700
3608
|
find, findMedian, findQuantile, findRankByValue, findStringPrefix,
|
|
3701
3609
|
findValueByPct, findValueByRank, forEach, forEachProperty, format,
|
|
3702
|
-
formatDateISO, formatIntlNumber, formatNumber: formatNumber$
|
|
3610
|
+
formatDateISO, formatIntlNumber, formatNumber: formatNumber$3, formatNumberForDisplay,
|
|
3703
3611
|
formatVersionedName, formatter,
|
|
3704
3612
|
genericSort, getArrayBounds, getGenericComparator, getKeyComparator,
|
|
3705
3613
|
getSortedIds, getUniqueName, groupBy,
|
|
@@ -7433,11 +7341,6 @@
|
|
|
7433
7341
|
model.setDefaultTarget([target.layers[0]], target.dataset);
|
|
7434
7342
|
}
|
|
7435
7343
|
}
|
|
7436
|
-
if (opts.display_all && importTotal === 0) {
|
|
7437
|
-
model.getLayers().forEach(function(o) {
|
|
7438
|
-
setLayerPinning(o.layer, true);
|
|
7439
|
-
});
|
|
7440
|
-
}
|
|
7441
7344
|
model.updated({select: true}); // trigger redraw
|
|
7442
7345
|
}
|
|
7443
7346
|
|
|
@@ -8674,6 +8577,123 @@
|
|
|
8674
8577
|
}
|
|
8675
8578
|
}
|
|
8676
8579
|
|
|
8580
|
+
function filterLayerByIds(lyr, ids) {
|
|
8581
|
+
var shapes;
|
|
8582
|
+
if (lyr.shapes) {
|
|
8583
|
+
shapes = ids.map(function(id) {
|
|
8584
|
+
return lyr.shapes[id];
|
|
8585
|
+
});
|
|
8586
|
+
return utils$1.defaults({shapes: shapes, data: null}, lyr);
|
|
8587
|
+
}
|
|
8588
|
+
return lyr;
|
|
8589
|
+
}
|
|
8590
|
+
|
|
8591
|
+
function formatLayerNameForDisplay(name) {
|
|
8592
|
+
return name || '[unnamed]';
|
|
8593
|
+
}
|
|
8594
|
+
|
|
8595
|
+
function cleanLayerName(raw) {
|
|
8596
|
+
return raw.replace(/[\n\t/\\]/g, '')
|
|
8597
|
+
.replace(/^[.\s]+/, '').replace(/[.\s]+$/, '');
|
|
8598
|
+
}
|
|
8599
|
+
|
|
8600
|
+
function updateLayerStackOrder(layers) {
|
|
8601
|
+
// 1. assign ascending ids to unassigned layers above the range of other layers
|
|
8602
|
+
layers.forEach(function(o, i) {
|
|
8603
|
+
if (!o.layer.menu_order) o.layer.menu_order = 1e6 + i;
|
|
8604
|
+
});
|
|
8605
|
+
// 2. sort in ascending order
|
|
8606
|
+
layers.sort(function(a, b) {
|
|
8607
|
+
return a.layer.menu_order - b.layer.menu_order;
|
|
8608
|
+
});
|
|
8609
|
+
// 3. assign consecutve ids
|
|
8610
|
+
layers.forEach(function(o, i) {
|
|
8611
|
+
o.layer.menu_order = i + 1;
|
|
8612
|
+
});
|
|
8613
|
+
return layers;
|
|
8614
|
+
}
|
|
8615
|
+
|
|
8616
|
+
function sortLayersForMenuDisplay(layers) {
|
|
8617
|
+
layers = updateLayerStackOrder(layers);
|
|
8618
|
+
return layers.reverse();
|
|
8619
|
+
}
|
|
8620
|
+
|
|
8621
|
+
function setLayerPinning(lyr, pinned) {
|
|
8622
|
+
lyr.pinned = !!pinned;
|
|
8623
|
+
}
|
|
8624
|
+
|
|
8625
|
+
// Whether a layer can be shown on the map alongside the active layer, and so
|
|
8626
|
+
// has an eye icon in the layers panel. A frame is drawn by preview mode, and a
|
|
8627
|
+
// data-only layer has nothing to draw.
|
|
8628
|
+
function layerIsPinnable(lyr, dataset) {
|
|
8629
|
+
if (dataset && internal.isFrameLayer(lyr, dataset.arcs)) return false;
|
|
8630
|
+
return internal.layerIsGeometric(lyr) || internal.layerHasRaster(lyr) ||
|
|
8631
|
+
internal.layerHasFurniture(lyr);
|
|
8632
|
+
}
|
|
8633
|
+
|
|
8634
|
+
// Every layer is visible until something says otherwise, so that a layer
|
|
8635
|
+
// made or imported next to another does not hide it -- a reference layer used
|
|
8636
|
+
// as a guide for drawing or labelling would otherwise vanish the moment the
|
|
8637
|
+
// new layer became the active one. Only layers whose visibility has never
|
|
8638
|
+
// been set are touched: a snapshot stores every layer's, and a layer hidden
|
|
8639
|
+
// with its eye icon stays hidden.
|
|
8640
|
+
//
|
|
8641
|
+
// layers: [{layer, dataset}], as from model.getLayers()
|
|
8642
|
+
function showNewLayers(layers) {
|
|
8643
|
+
layers.forEach(function(o) {
|
|
8644
|
+
if (o.layer.pinned === undefined && layerIsPinnable(o.layer, o.dataset)) {
|
|
8645
|
+
setLayerPinning(o.layer, true);
|
|
8646
|
+
}
|
|
8647
|
+
});
|
|
8648
|
+
}
|
|
8649
|
+
|
|
8650
|
+
|
|
8651
|
+
function calcDotScale(layers, ext) {
|
|
8652
|
+
var bbox = ext.getBounds().scale(1.3).toArray(); // add buffer
|
|
8653
|
+
// var topTier = 50000; // can be a bottleneck
|
|
8654
|
+
var topTier = 10000; // short-circuit counting here
|
|
8655
|
+
var count = 0;
|
|
8656
|
+
layers = layers.filter(function(lyr) {
|
|
8657
|
+
return lyr.geometry_type == 'point' && lyr.gui.style?.dotSize > 0;
|
|
8658
|
+
});
|
|
8659
|
+
layers.forEach(function(lyr) {
|
|
8660
|
+
count += countPoints(lyr.gui.displayLayer.shapes, topTier, bbox);
|
|
8661
|
+
});
|
|
8662
|
+
count = Math.min(topTier, count) || 1;
|
|
8663
|
+
var k = Math.pow(5 - utils$1.clamp(Math.log10(count), 1, 4), 1.25);
|
|
8664
|
+
|
|
8665
|
+
// zoom adjustments
|
|
8666
|
+
var mapScale = ext.scale();
|
|
8667
|
+
if (mapScale < 0.5) {
|
|
8668
|
+
k *= Math.pow(mapScale + 0.5, 0.35);
|
|
8669
|
+
} else if (mapScale > 1) {
|
|
8670
|
+
// scale faster at first
|
|
8671
|
+
k *= Math.pow(Math.min(mapScale, 4), 0.25);
|
|
8672
|
+
k *= Math.pow(mapScale, 0.02);
|
|
8673
|
+
}
|
|
8674
|
+
|
|
8675
|
+
// scale down when map is small
|
|
8676
|
+
var smallSide = Math.min(ext.width(), ext.height());
|
|
8677
|
+
k *= utils$1.clamp(smallSide / 500, 0.5, 1);
|
|
8678
|
+
return k;
|
|
8679
|
+
}
|
|
8680
|
+
|
|
8681
|
+
function countPoints(shapes, max, bbox) {
|
|
8682
|
+
var count = 0;
|
|
8683
|
+
var shp, p;
|
|
8684
|
+
// short-circuit point counting above top threshold
|
|
8685
|
+
for (var i=0, n=shapes.length; i<n && count<max; i++) {
|
|
8686
|
+
shp = shapes[i];
|
|
8687
|
+
for (var j=0, m=(shp ? shp.length : 0); j<m; j++) {
|
|
8688
|
+
p = shp[j];
|
|
8689
|
+
if (p[0] > bbox[0] && p[0] < bbox[2] && p[1] > bbox[1] && p[1] < bbox[3]) {
|
|
8690
|
+
count ++;
|
|
8691
|
+
}
|
|
8692
|
+
}
|
|
8693
|
+
}
|
|
8694
|
+
return count;
|
|
8695
|
+
}
|
|
8696
|
+
|
|
8677
8697
|
// lyr: a map layer with gui property
|
|
8678
8698
|
// displayCRS: CRS to use for display, or null (which clears any current display CRS)
|
|
8679
8699
|
function projectLayerForDisplay(lyr, displayCRS) {
|
|
@@ -10392,6 +10412,20 @@
|
|
|
10392
10412
|
return false;
|
|
10393
10413
|
}
|
|
10394
10414
|
|
|
10415
|
+
// Test if the map should be re-framed when the content it was showing had no
|
|
10416
|
+
// area -- a single point, or points in a line -- such as the first label
|
|
10417
|
+
// placed on an otherwise empty map. Such bounds are padded by a token amount,
|
|
10418
|
+
// so mapNeedsReset()'s area-change rule would compare the new bounds against
|
|
10419
|
+
// almost nothing: a second label anywhere in view grows the area by more than
|
|
10420
|
+
// its threshold, and the map zooms in to the two labels. The view is the
|
|
10421
|
+
// only real scale left to compare against, and new content smaller than the
|
|
10422
|
+
// view is no reason to zoom in to it.
|
|
10423
|
+
function mapNeedsResetFromCollapsedBounds(newBounds, viewportBounds, flags) {
|
|
10424
|
+
var chgThreshold = flags.proj ? 1e3 : 1e8;
|
|
10425
|
+
if (!newBounds.intersects(viewportBounds)) return true;
|
|
10426
|
+
return newBounds.area() / viewportBounds.area() > chgThreshold;
|
|
10427
|
+
}
|
|
10428
|
+
|
|
10395
10429
|
// Test if an update may have affected the visible shape of arcs
|
|
10396
10430
|
// @flags Flags from update event
|
|
10397
10431
|
function arcsMayHaveChanged(flags) {
|
|
@@ -11780,7 +11814,9 @@
|
|
|
11780
11814
|
layerCount++;
|
|
11781
11815
|
});
|
|
11782
11816
|
|
|
11783
|
-
|
|
11817
|
+
// Shown for a single layer too: it says whether the next layer made or
|
|
11818
|
+
// imported will be drawn over this one or replace it on the map.
|
|
11819
|
+
if (pinnableCount < 1) {
|
|
11784
11820
|
pinAll.hide();
|
|
11785
11821
|
} else {
|
|
11786
11822
|
pinAll.show();
|
|
@@ -12222,8 +12258,7 @@
|
|
|
12222
12258
|
}
|
|
12223
12259
|
|
|
12224
12260
|
function isPinnable(lyr, dataset) {
|
|
12225
|
-
|
|
12226
|
-
return internal.layerIsGeometric(lyr) || internal.layerHasRaster(lyr) || internal.layerHasFurniture(lyr);
|
|
12261
|
+
return layerIsPinnable(lyr, dataset);
|
|
12227
12262
|
}
|
|
12228
12263
|
|
|
12229
12264
|
function layerCanBeStyled(lyr) {
|
|
@@ -13729,6 +13764,13 @@
|
|
|
13729
13764
|
// keeps them out of a character class so that "\\n" is matched as a pair.
|
|
13730
13765
|
var ANY_NEWLINE = /\r\n|\\n|<br>|\n|\r/gi;
|
|
13731
13766
|
|
|
13767
|
+
// A line break the wrapper inserted rather than one the user typed. It consumes
|
|
13768
|
+
// no character -- the space the line broke at stays before it -- so removing
|
|
13769
|
+
// every one gives back the text as typed. See
|
|
13770
|
+
// docs/development/text-annotation-design.md.
|
|
13771
|
+
var SOFT_BREAK = '<wbr>';
|
|
13772
|
+
var SOFT_BREAK_RXP = /<wbr>/gi;
|
|
13773
|
+
|
|
13732
13774
|
// How a label lays out its line breaks, which is not the same for the two kinds.
|
|
13733
13775
|
// An anchored label stacks its lines with <tspan>. A path label joins them with
|
|
13734
13776
|
// a space, because a <tspan> inside a <textPath> advances along the curve
|
|
@@ -13743,10 +13785,72 @@
|
|
|
13743
13785
|
// all, and a line the user has just opened with Enter and not yet typed into.
|
|
13744
13786
|
var TEXT_PLACEHOLDER = '\u200b';
|
|
13745
13787
|
|
|
13746
|
-
// Data value -> the string the user edits, with real newlines
|
|
13788
|
+
// Data value -> the string the user edits, with real newlines and without the
|
|
13789
|
+
// wrapper's soft breaks, which are rewrapped rather than edited.
|
|
13747
13790
|
function decodeLabelText(val) {
|
|
13748
13791
|
if (val === null || val === undefined || val === '') return '';
|
|
13749
|
-
return String(val).replace(ANY_NEWLINE, '\n');
|
|
13792
|
+
return String(val).replace(ANY_NEWLINE, '\n').replace(SOFT_BREAK_RXP, '');
|
|
13793
|
+
}
|
|
13794
|
+
|
|
13795
|
+
// Data value -> {text, breaks}: the string the user edits, and the offsets in
|
|
13796
|
+
// it where the stored value has a soft break.
|
|
13797
|
+
function readSoftBreaks(val) {
|
|
13798
|
+
var str = val === null || val === undefined ? '' : String(val).replace(ANY_NEWLINE, '\n');
|
|
13799
|
+
var parts = str.split(SOFT_BREAK_RXP);
|
|
13800
|
+
var breaks = [];
|
|
13801
|
+
var text = parts[0];
|
|
13802
|
+
for (var i = 1; i < parts.length; i++) {
|
|
13803
|
+
breaks.push(text.length);
|
|
13804
|
+
text += parts[i];
|
|
13805
|
+
}
|
|
13806
|
+
return {text: text, breaks: breaks};
|
|
13807
|
+
}
|
|
13808
|
+
|
|
13809
|
+
// The string the user edits, plus soft breaks at @breaks (ascending offsets
|
|
13810
|
+
// into it) -> the same string with the markers written in.
|
|
13811
|
+
function insertSoftBreaks(text, breaks) {
|
|
13812
|
+
var out = '', prev = 0;
|
|
13813
|
+
if (!breaks || breaks.length === 0) return text;
|
|
13814
|
+
for (var i = 0; i < breaks.length; i++) {
|
|
13815
|
+
out += text.substring(prev, breaks[i]) + SOFT_BREAK;
|
|
13816
|
+
prev = breaks[i];
|
|
13817
|
+
}
|
|
13818
|
+
return out + text.substring(prev);
|
|
13819
|
+
}
|
|
13820
|
+
|
|
13821
|
+
// Where a laid-out text's lines start because they were wrapped, given each
|
|
13822
|
+
// character's top edge (see gui-label-wrap.mjs).
|
|
13823
|
+
//
|
|
13824
|
+
// @getTop(i) returns the top of character i in px, or null for one with no box.
|
|
13825
|
+
// A line that starts after a typed newline is not a soft break, and neither is
|
|
13826
|
+
// a character with no box -- the break lands on the next one that has a box.
|
|
13827
|
+
// The low half of a surrogate pair is never asked about.
|
|
13828
|
+
function findSoftBreaks(text, getTop, threshold) {
|
|
13829
|
+
var breaks = [], lastTop = null, afterNewline = false, i, c, top;
|
|
13830
|
+
for (i = 0; i < text.length; i++) {
|
|
13831
|
+
c = text.charCodeAt(i);
|
|
13832
|
+
if (c == 10) {
|
|
13833
|
+
afterNewline = true;
|
|
13834
|
+
continue;
|
|
13835
|
+
}
|
|
13836
|
+
if (c >= 0xDC00 && c <= 0xDFFF) continue;
|
|
13837
|
+
top = getTop(i);
|
|
13838
|
+
if (top === null) continue;
|
|
13839
|
+
if (lastTop !== null && !afterNewline && top > lastTop + threshold) {
|
|
13840
|
+
breaks.push(i);
|
|
13841
|
+
}
|
|
13842
|
+
lastTop = top;
|
|
13843
|
+
afterNewline = false;
|
|
13844
|
+
}
|
|
13845
|
+
return breaks;
|
|
13846
|
+
}
|
|
13847
|
+
|
|
13848
|
+
function sameSoftBreaks(a, b) {
|
|
13849
|
+
if (a.length != b.length) return false;
|
|
13850
|
+
for (var i = 0; i < a.length; i++) {
|
|
13851
|
+
if (a[i] !== b[i]) return false;
|
|
13852
|
+
}
|
|
13853
|
+
return true;
|
|
13750
13854
|
}
|
|
13751
13855
|
|
|
13752
13856
|
// The string the user edits -> the data value. Newlines become the escape,
|
|
@@ -13763,12 +13867,28 @@
|
|
|
13763
13867
|
// break that started it. That does two things: an empty line gets something to
|
|
13764
13868
|
// lay out, so a line the user has just opened appears and can hold the caret,
|
|
13765
13869
|
// and the rendered characters stay aligned one-for-one with the edited ones.
|
|
13766
|
-
|
|
13870
|
+
//
|
|
13871
|
+
// @breaks: offsets of soft breaks, which also start a line. A soft break
|
|
13872
|
+
// consumes no character, so its line gets no placeholder and the alignment
|
|
13873
|
+
// holds.
|
|
13874
|
+
function getRenderedLines(str, breaks) {
|
|
13767
13875
|
var text = decodeLabelText(str);
|
|
13876
|
+
var lines = [], start = 0, lead = '', b = 0, i;
|
|
13768
13877
|
if (text === '') return [TEXT_PLACEHOLDER];
|
|
13769
|
-
|
|
13770
|
-
|
|
13771
|
-
|
|
13878
|
+
breaks = breaks || [];
|
|
13879
|
+
for (i = 0; i <= text.length; i++) {
|
|
13880
|
+
while (b < breaks.length && breaks[b] < i) b++;
|
|
13881
|
+
if (i == text.length || text.charAt(i) == '\n') {
|
|
13882
|
+
lines.push(lead + text.substring(start, i));
|
|
13883
|
+
start = i + 1;
|
|
13884
|
+
lead = TEXT_PLACEHOLDER;
|
|
13885
|
+
} else if (breaks[b] === i && i > start) {
|
|
13886
|
+
lines.push(lead + text.substring(start, i));
|
|
13887
|
+
start = i;
|
|
13888
|
+
lead = '';
|
|
13889
|
+
}
|
|
13890
|
+
}
|
|
13891
|
+
return lines;
|
|
13772
13892
|
}
|
|
13773
13893
|
|
|
13774
13894
|
// Whether the text would draw nothing at all: empty, or made only of
|
|
@@ -13959,10 +14079,11 @@
|
|
|
13959
14079
|
//
|
|
13960
14080
|
// label-pos places text relative to an anchor point, which a path label does
|
|
13961
14081
|
// not have -- its text runs along the curve. Writing it there would put a
|
|
13962
|
-
// property on the feature that nothing reads.
|
|
14082
|
+
// property on the feature that nothing reads. label-width is the same: a path
|
|
14083
|
+
// label is one line.
|
|
13963
14084
|
function getStyleFields$1(style, isPathLabel) {
|
|
13964
14085
|
return Object.keys(style || {}).filter(function(name) {
|
|
13965
|
-
return !(isPathLabel && name == 'label-pos');
|
|
14086
|
+
return !(isPathLabel && (name == 'label-pos' || name == 'label-width'));
|
|
13966
14087
|
});
|
|
13967
14088
|
}
|
|
13968
14089
|
|
|
@@ -14024,18 +14145,45 @@
|
|
|
14024
14145
|
// anchor: the value for text-anchor
|
|
14025
14146
|
// id: feature id of the label
|
|
14026
14147
|
// target: layer name, or null to use the current target
|
|
14148
|
+
// via: (optional) the callout-via the text carried with it
|
|
14027
14149
|
function getLabelOffsetCommand(opts) {
|
|
14028
14150
|
var parts = ['-style', 'dx=' + opts.dx, 'dy=' + opts.dy,
|
|
14029
14151
|
'text-anchor=' + opts.anchor, 'label-pos='];
|
|
14152
|
+
if (opts.via) parts.push('callout-via=' + quoteCommandValue(opts.via));
|
|
14030
14153
|
parts.push('ids=' + opts.id);
|
|
14031
14154
|
if (opts.target) parts.push('target=' + quoteCommandValue(opts.target));
|
|
14032
14155
|
return parts.join(' ');
|
|
14033
14156
|
}
|
|
14034
14157
|
|
|
14158
|
+
// The command a drag on one of a label's handles writes, run once on release.
|
|
14159
|
+
//
|
|
14160
|
+
// values: {field: value}, where '' removes the field -- which is what
|
|
14161
|
+
// -style reads an empty value as, and how a handle goes back to automatic
|
|
14162
|
+
// id: feature id of the label
|
|
14163
|
+
// opts:
|
|
14164
|
+
// target: layer name, or null to use the current target
|
|
14165
|
+
// text: (optional) rewrapped label-text, with real newlines and soft
|
|
14166
|
+
// breaks. A second -style in the same string, so that a new width and
|
|
14167
|
+
// the lines it breaks into are one undo step.
|
|
14168
|
+
function getLabelStyleCommand(values, id, opts) {
|
|
14169
|
+
var o = opts || {};
|
|
14170
|
+
var parts = ['-style'];
|
|
14171
|
+
Object.keys(values).forEach(function(name) {
|
|
14172
|
+
var val = values[name];
|
|
14173
|
+
parts.push(name + '=' + (val === '' ? '' : quoteCommandValue(String(val))));
|
|
14174
|
+
});
|
|
14175
|
+
parts.push('ids=' + id);
|
|
14176
|
+
if (o.target) parts.push('target=' + quoteCommandValue(o.target));
|
|
14177
|
+
if (typeof o.text == 'string') {
|
|
14178
|
+
parts.push(getLabelTextCommand(o.text, id, o.target));
|
|
14179
|
+
}
|
|
14180
|
+
return parts.join(' ');
|
|
14181
|
+
}
|
|
14182
|
+
|
|
14035
14183
|
// The command that saves edited text, run once when an editing session ends so
|
|
14036
14184
|
// that a session is one undo step rather than one per keystroke.
|
|
14037
14185
|
//
|
|
14038
|
-
// text: the edited string, with real newlines
|
|
14186
|
+
// text: the edited string, with real newlines and any soft breaks
|
|
14039
14187
|
// id: feature id of the label
|
|
14040
14188
|
// target: layer name, or null to use the current target
|
|
14041
14189
|
function getLabelTextCommand(text, id, target) {
|
|
@@ -15631,16 +15779,16 @@
|
|
|
15631
15779
|
function getCanvasPoint(canvas, evt) {
|
|
15632
15780
|
var rect = canvas.getBoundingClientRect();
|
|
15633
15781
|
return {
|
|
15634
|
-
x: clamp$
|
|
15635
|
-
y: clamp$
|
|
15782
|
+
x: clamp$4(Math.round((evt.clientX - rect.left) / rect.width * (canvas.width - 1)), 0, canvas.width - 1),
|
|
15783
|
+
y: clamp$4(Math.round((evt.clientY - rect.top) / rect.height * (canvas.height - 1)), 0, canvas.height - 1)
|
|
15636
15784
|
};
|
|
15637
15785
|
}
|
|
15638
15786
|
|
|
15639
15787
|
function setPickerColor(hsb) {
|
|
15640
15788
|
pickerColor = {
|
|
15641
|
-
h: clamp$
|
|
15642
|
-
s: clamp$
|
|
15643
|
-
b: clamp$
|
|
15789
|
+
h: clamp$4(Math.round(hsb.h), 0, 255),
|
|
15790
|
+
s: clamp$4(Math.round(hsb.s), 0, 255),
|
|
15791
|
+
b: clamp$4(Math.round(hsb.b), 0, 255)
|
|
15644
15792
|
};
|
|
15645
15793
|
drawColorPicker();
|
|
15646
15794
|
updatePickerFields();
|
|
@@ -15781,7 +15929,7 @@
|
|
|
15781
15929
|
}
|
|
15782
15930
|
|
|
15783
15931
|
function pctToByte(val) {
|
|
15784
|
-
return Math.round(clamp$
|
|
15932
|
+
return Math.round(clamp$4(val, 0, 100) / 100 * 255);
|
|
15785
15933
|
}
|
|
15786
15934
|
|
|
15787
15935
|
function byteToDegrees(val) {
|
|
@@ -15789,14 +15937,14 @@
|
|
|
15789
15937
|
}
|
|
15790
15938
|
|
|
15791
15939
|
function degreesToByte(val) {
|
|
15792
|
-
return Math.round(clamp$
|
|
15940
|
+
return Math.round(clamp$4(val, 0, 360) / 360 * 255);
|
|
15793
15941
|
}
|
|
15794
15942
|
|
|
15795
15943
|
function parseNumberField(str) {
|
|
15796
15944
|
return Number(String(str).replace(/[°%]/g, '').trim());
|
|
15797
15945
|
}
|
|
15798
15946
|
|
|
15799
|
-
function clamp$
|
|
15947
|
+
function clamp$4(val, min, max) {
|
|
15800
15948
|
return isFinite(val) ? Math.max(min, Math.min(max, val)) : min;
|
|
15801
15949
|
}
|
|
15802
15950
|
|
|
@@ -16184,9 +16332,12 @@
|
|
|
16184
16332
|
setDisplay(shown);
|
|
16185
16333
|
return;
|
|
16186
16334
|
}
|
|
16335
|
+
var changed = String(val) !== shown;
|
|
16187
16336
|
setDisplay(String(val));
|
|
16188
|
-
|
|
16337
|
+
// Before onSet, which can take focus away: the blur fires 'change', and
|
|
16338
|
+
// the commit it re-enters has to find this value already shown.
|
|
16189
16339
|
shown = String(val);
|
|
16340
|
+
if (changed && o.onSet) o.onSet(val);
|
|
16190
16341
|
}
|
|
16191
16342
|
|
|
16192
16343
|
function setDisplay(str) {
|
|
@@ -16521,7 +16672,12 @@
|
|
|
16521
16672
|
'dominant-baseline',
|
|
16522
16673
|
'label-pos', 'label-side', 'label-start-offset', 'dx', 'dy',
|
|
16523
16674
|
'fill', 'opacity', 'css', 'class',
|
|
16524
|
-
'
|
|
16675
|
+
'halo-width', 'halo-color', 'halo-opacity',
|
|
16676
|
+
'icon', 'icon-size', 'icon-color', 'icon-opacity',
|
|
16677
|
+
// not callout-via or callout-attach, which are placed for one label's
|
|
16678
|
+
// surroundings rather than chosen as a style
|
|
16679
|
+
'callout', 'callout-end', 'callout-end-size', 'callout-color', 'callout-opacity', 'callout-width',
|
|
16680
|
+
'callout-gap'
|
|
16525
16681
|
];
|
|
16526
16682
|
|
|
16527
16683
|
// values: [[field, value], ...], the form the panel's controls produce
|
|
@@ -16610,23 +16766,49 @@
|
|
|
16610
16766
|
//
|
|
16611
16767
|
// Fixed on entry, so that a stray drag cannot displace text in a session that
|
|
16612
16768
|
// never asked for it, and remembered while the tool stays on.
|
|
16769
|
+
//
|
|
16770
|
+
// Text blocks keep a mode of their own, which starts out Draggable. A block is
|
|
16771
|
+
// usually an annotation set off from the point it is about, and is placed by
|
|
16772
|
+
// dragging it away from its anchor; point text more often sits where its
|
|
16773
|
+
// position puts it and is moved as a whole.
|
|
16774
|
+
//
|
|
16775
|
+
// @kind: 'point' (the default) or 'block' -- see getLabelPositionKind()
|
|
16613
16776
|
var LABEL_POSITION_MODES = ['fixed', 'draggable'];
|
|
16614
16777
|
|
|
16615
|
-
|
|
16616
|
-
|
|
16778
|
+
var DEFAULT_POSITION_MODES = {point: 'fixed', block: 'draggable'};
|
|
16779
|
+
|
|
16780
|
+
function getLabelPositionMode(gui, kind) {
|
|
16781
|
+
var k = kind == 'block' ? 'block' : 'point';
|
|
16782
|
+
var modes = gui.state.label_position_modes || {};
|
|
16783
|
+
return modes[k] || DEFAULT_POSITION_MODES[k];
|
|
16617
16784
|
}
|
|
16618
16785
|
|
|
16619
|
-
|
|
16620
|
-
|
|
16621
|
-
|
|
16786
|
+
// With no @kind, sets both kinds: 'fixed' puts point text in Fixed mode and
|
|
16787
|
+
// blocks back to their own default, which is what the tool does on entry.
|
|
16788
|
+
function setLabelPositionMode(gui, mode, kind) {
|
|
16789
|
+
var modes = gui.state.label_position_modes || {};
|
|
16790
|
+
var val = LABEL_POSITION_MODES.indexOf(mode) > -1 ? mode : 'fixed';
|
|
16791
|
+
if (kind) {
|
|
16792
|
+
modes[kind == 'block' ? 'block' : 'point'] = val;
|
|
16793
|
+
} else {
|
|
16794
|
+
modes = {point: val, block: DEFAULT_POSITION_MODES.block};
|
|
16795
|
+
}
|
|
16796
|
+
gui.state.label_position_modes = modes;
|
|
16622
16797
|
// The panel's toggle and the tool's drag both read this, and neither owns the
|
|
16623
16798
|
// other; the panel also has to relight the grid, whose cells mean something
|
|
16624
16799
|
// different in each mode.
|
|
16625
16800
|
gui.dispatchEvent('label_position_mode_change');
|
|
16626
16801
|
}
|
|
16627
16802
|
|
|
16628
|
-
function labelTextIsDraggable(gui) {
|
|
16629
|
-
return getLabelPositionMode(gui) == 'draggable';
|
|
16803
|
+
function labelTextIsDraggable(gui, kind) {
|
|
16804
|
+
return getLabelPositionMode(gui, kind) == 'draggable';
|
|
16805
|
+
}
|
|
16806
|
+
|
|
16807
|
+
// Which position mode governs a label: 'block' for a text block (a label with
|
|
16808
|
+
// a wrap width), 'point' for any other.
|
|
16809
|
+
function getLabelPositionKind(rec) {
|
|
16810
|
+
var w = rec ? Number(rec['label-width']) : 0;
|
|
16811
|
+
return w > 0 && isFinite(w) ? 'block' : 'point';
|
|
16630
16812
|
}
|
|
16631
16813
|
|
|
16632
16814
|
// The label currently open for text editing, or null: {id, refocus}.
|
|
@@ -16704,6 +16886,9 @@
|
|
|
16704
16886
|
// anchor: the justification it is drawn with
|
|
16705
16887
|
// width: the width of its text, in the same space; 0 if unmeasurable
|
|
16706
16888
|
// aligned: whether it carries a label-align (see below)
|
|
16889
|
+
// keepAnchor: (optional) leave the justification alone, for a text block:
|
|
16890
|
+
// its lines are laid out within a column, and re-justifying them as the
|
|
16891
|
+
// block passes its anchor would rearrange the text rather than move it
|
|
16707
16892
|
// delta: {dx, dy} -- how far the pointer has moved, in the same space.
|
|
16708
16893
|
//
|
|
16709
16894
|
// Returns two descriptions of the same placement:
|
|
@@ -16731,7 +16916,7 @@
|
|
|
16731
16916
|
// that correction, 'start' is the one text-anchor whose dx is the left edge
|
|
16732
16917
|
// itself -- which is also the value that would leave the label where it is
|
|
16733
16918
|
// if the alignment were later removed.
|
|
16734
|
-
var anchor = start.aligned ? 'start' :
|
|
16919
|
+
var anchor = start.aligned ? 'start' : start.keepAnchor ? drawnAnchor :
|
|
16735
16920
|
width > 0 ? getAnchorForCentre(left + width / 2, width) : drawnAnchor;
|
|
16736
16921
|
// An aligned label goes on being drawn with the anchor its alignment gives
|
|
16737
16922
|
// it, whatever the drag writes: the alignment is unchanged, so the renderer
|
|
@@ -16799,6 +16984,159 @@
|
|
|
16799
16984
|
return Math.round(px * PRECISION) / PRECISION;
|
|
16800
16985
|
}
|
|
16801
16986
|
|
|
16987
|
+
// Wrapping a text block to its label-width, with the browser's own line
|
|
16988
|
+
// breaker.
|
|
16989
|
+
//
|
|
16990
|
+
// There is no API that reports where a browser breaks lines, so the text is
|
|
16991
|
+
// laid out in an offscreen <div> at the label's width and font, and read back:
|
|
16992
|
+
// Range.getClientRects() gives each character's line box, and a character
|
|
16993
|
+
// sitting lower than the one before it starts a line. Everything the browser
|
|
16994
|
+
// knows about line breaking -- UAX #14 classes, breaks after hyphens, none at
|
|
16995
|
+
// no-break spaces, CJK, dictionary breaking for Thai -- comes with it.
|
|
16996
|
+
//
|
|
16997
|
+
// The font is read back from a rendered sample of the label rather than
|
|
16998
|
+
// assembled from the record, so that the layer's defaults and any inline css
|
|
16999
|
+
// apply exactly as they do on the map.
|
|
17000
|
+
//
|
|
17001
|
+
// See docs/development/text-annotation-design.md.
|
|
17002
|
+
|
|
17003
|
+
// Properties that change where lines break, and so the ones a change to which
|
|
17004
|
+
// has to rewrap a text block.
|
|
17005
|
+
var WRAP_FIELDS = ['label-width', 'font-family', 'font-size',
|
|
17006
|
+
'font-weight', 'font-style', 'font-stretch', 'letter-spacing', 'css'];
|
|
17007
|
+
|
|
17008
|
+
var COPIED_STYLES = ['fontFamily', 'fontSize', 'fontWeight', 'fontStyle',
|
|
17009
|
+
'fontStretch', 'fontVariant', 'fontKerning', 'fontFeatureSettings',
|
|
17010
|
+
'fontVariationSettings', 'letterSpacing', 'wordSpacing', 'textTransform'];
|
|
17011
|
+
|
|
17012
|
+
// Lines are laid out far apart, so that a character in a fallback font sitting
|
|
17013
|
+
// a little higher or lower than its neighbours is not mistaken for a new line.
|
|
17014
|
+
var LINE_HEIGHT = 3;
|
|
17015
|
+
|
|
17016
|
+
var CACHE_LIMIT = 2000;
|
|
17017
|
+
var cache$2 = new Map();
|
|
17018
|
+
var wrapper = null;
|
|
17019
|
+
|
|
17020
|
+
// The label's width in px, or 0 for a label that does not wrap.
|
|
17021
|
+
function getLabelWrapWidth(rec) {
|
|
17022
|
+
var w = rec ? Number(rec['label-width']) : 0;
|
|
17023
|
+
return w > 0 && isFinite(w) ? w : 0;
|
|
17024
|
+
}
|
|
17025
|
+
|
|
17026
|
+
// Offsets into @text (real newlines, no soft breaks) where a line starts
|
|
17027
|
+
// because it was wrapped, in ascending order.
|
|
17028
|
+
function getSoftBreaks(text, rec) {
|
|
17029
|
+
var width = getLabelWrapWidth(rec);
|
|
17030
|
+
var key, breaks;
|
|
17031
|
+
if (!width || !text) return [];
|
|
17032
|
+
key = getWrapKey(text, rec);
|
|
17033
|
+
if (cache$2.has(key)) return cache$2.get(key);
|
|
17034
|
+
breaks = measureSoftBreaks(text, rec, width);
|
|
17035
|
+
if (cache$2.size >= CACHE_LIMIT) cache$2.clear();
|
|
17036
|
+
cache$2.set(key, breaks);
|
|
17037
|
+
return breaks;
|
|
17038
|
+
}
|
|
17039
|
+
|
|
17040
|
+
// A stored label-text value, rewrapped for @rec's width and font. Returned
|
|
17041
|
+
// unchanged when neither the old value nor the new one has a soft break, so
|
|
17042
|
+
// that a label that does not wrap keeps the line breaks it was written with.
|
|
17043
|
+
function rewrapLabelValue(value, rec) {
|
|
17044
|
+
var o = readSoftBreaks(value);
|
|
17045
|
+
var breaks = getSoftBreaks(o.text, rec);
|
|
17046
|
+
if (sameSoftBreaks(o.breaks, breaks)) return value;
|
|
17047
|
+
return encodeLabelText(insertSoftBreaks(o.text, breaks));
|
|
17048
|
+
}
|
|
17049
|
+
|
|
17050
|
+
function clearWrapCache() {
|
|
17051
|
+
cache$2.clear();
|
|
17052
|
+
}
|
|
17053
|
+
|
|
17054
|
+
function getWrapKey(text, rec) {
|
|
17055
|
+
var parts = [text];
|
|
17056
|
+
WRAP_FIELDS.forEach(function(name) {
|
|
17057
|
+
var val = rec[name];
|
|
17058
|
+
parts.push(val === null || val === undefined ? '' : String(val));
|
|
17059
|
+
});
|
|
17060
|
+
parts.push(rec['class'] || '');
|
|
17061
|
+
return parts.join('\u0000');
|
|
17062
|
+
}
|
|
17063
|
+
|
|
17064
|
+
function measureSoftBreaks(text, rec, width) {
|
|
17065
|
+
var w = getWrapper();
|
|
17066
|
+
var style, fontSize, node, range;
|
|
17067
|
+
if (!w) return [];
|
|
17068
|
+
style = getLabelFontStyle(w, rec);
|
|
17069
|
+
if (!style) return [];
|
|
17070
|
+
COPIED_STYLES.forEach(function(name) {
|
|
17071
|
+
w.div.style[name] = style[name] || '';
|
|
17072
|
+
});
|
|
17073
|
+
w.div.style.width = width + 'px';
|
|
17074
|
+
w.div.textContent = text;
|
|
17075
|
+
node = w.div.firstChild;
|
|
17076
|
+
fontSize = parseFloat(style.fontSize) || 12;
|
|
17077
|
+
range = document.createRange();
|
|
17078
|
+
var breaks = findSoftBreaks(text, function(i) {
|
|
17079
|
+
var end = isHighSurrogate(text.charCodeAt(i)) ? i + 2 : i + 1;
|
|
17080
|
+
var r;
|
|
17081
|
+
range.setStart(node, i);
|
|
17082
|
+
range.setEnd(node, Math.min(end, text.length));
|
|
17083
|
+
r = range.getBoundingClientRect();
|
|
17084
|
+
return r.height > 0 ? r.top : null;
|
|
17085
|
+
}, fontSize * LINE_HEIGHT / 2);
|
|
17086
|
+
w.div.textContent = '';
|
|
17087
|
+
return breaks;
|
|
17088
|
+
}
|
|
17089
|
+
|
|
17090
|
+
function isHighSurrogate(c) {
|
|
17091
|
+
return c >= 0xD800 && c <= 0xDBFF;
|
|
17092
|
+
}
|
|
17093
|
+
|
|
17094
|
+
// The computed font of @rec's label, from a sample rendered by the same code
|
|
17095
|
+
// as the map, inside the defaults a label inherits from its layer's group.
|
|
17096
|
+
function getLabelFontStyle(w, rec) {
|
|
17097
|
+
var sample = Object.assign({}, rec, {'label-text': 'x'});
|
|
17098
|
+
var node, cs, out;
|
|
17099
|
+
delete sample['label-align'];
|
|
17100
|
+
w.svg.innerHTML = internal.svg.stringify({
|
|
17101
|
+
tag: 'g',
|
|
17102
|
+
properties: internal.getLabelTextDefaults(),
|
|
17103
|
+
children: [internal.svg.renderStyledLabel(sample)]
|
|
17104
|
+
});
|
|
17105
|
+
node = w.svg.querySelector('text');
|
|
17106
|
+
if (!node) return null;
|
|
17107
|
+
cs = window.getComputedStyle(node);
|
|
17108
|
+
out = {};
|
|
17109
|
+
COPIED_STYLES.forEach(function(name) {
|
|
17110
|
+
out[name] = cs[name];
|
|
17111
|
+
});
|
|
17112
|
+
w.svg.innerHTML = '';
|
|
17113
|
+
return out;
|
|
17114
|
+
}
|
|
17115
|
+
|
|
17116
|
+
// Positioned off the page rather than hidden with display:none, which gives an
|
|
17117
|
+
// element no layout and its text no lines.
|
|
17118
|
+
function getWrapper() {
|
|
17119
|
+
var box, div, svg;
|
|
17120
|
+
if (wrapper) return wrapper;
|
|
17121
|
+
if (typeof document == 'undefined') return null;
|
|
17122
|
+
box = document.createElement('div');
|
|
17123
|
+
box.setAttribute('class', 'label-wrap-measure');
|
|
17124
|
+
box.setAttribute('aria-hidden', 'true');
|
|
17125
|
+
div = document.createElement('div');
|
|
17126
|
+
div.style.whiteSpace = 'pre-wrap';
|
|
17127
|
+
div.style.overflowWrap = 'break-word';
|
|
17128
|
+
div.style.lineHeight = String(LINE_HEIGHT);
|
|
17129
|
+
div.style.padding = '0';
|
|
17130
|
+
div.style.border = '0';
|
|
17131
|
+
div.style.boxSizing = 'content-box';
|
|
17132
|
+
svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
17133
|
+
box.appendChild(div);
|
|
17134
|
+
box.appendChild(svg);
|
|
17135
|
+
document.body.appendChild(box);
|
|
17136
|
+
wrapper = {div: div, svg: svg};
|
|
17137
|
+
return wrapper;
|
|
17138
|
+
}
|
|
17139
|
+
|
|
16802
17140
|
var fontField = 'font-family';
|
|
16803
17141
|
var fontSizeField = 'font-size';
|
|
16804
17142
|
var fontStyleField = 'font-style';
|
|
@@ -16810,16 +17148,32 @@
|
|
|
16810
17148
|
var textAnchorField = 'text-anchor';
|
|
16811
17149
|
var labelAlignField = 'label-align';
|
|
16812
17150
|
var cssField = 'css';
|
|
17151
|
+
var haloWidthField = 'halo-width';
|
|
17152
|
+
var haloColorField = 'halo-color';
|
|
17153
|
+
var haloOpacityField = 'halo-opacity';
|
|
16813
17154
|
var iconField = 'icon';
|
|
16814
17155
|
var iconSizeField = 'icon-size';
|
|
16815
17156
|
var iconColorField = 'icon-color';
|
|
16816
17157
|
var iconOpacityField = 'icon-opacity';
|
|
17158
|
+
var calloutField = 'callout';
|
|
17159
|
+
var calloutEndField = 'callout-end';
|
|
17160
|
+
var calloutEndSizeField = 'callout-end-size';
|
|
17161
|
+
var calloutColorField = 'callout-color';
|
|
17162
|
+
var calloutOpacityField = 'callout-opacity';
|
|
17163
|
+
var calloutWidthField = 'callout-width';
|
|
17164
|
+
var calloutGapField = 'callout-gap';
|
|
17165
|
+
var defaultCalloutShape = 'line';
|
|
17166
|
+
var defaultCalloutWidth = 1;
|
|
16817
17167
|
var defaultFontSize = 12;
|
|
16818
17168
|
var defaultFontStyle = 'normal';
|
|
16819
17169
|
var defaultFontWeight = '400';
|
|
16820
17170
|
var defaultLabelColor = '#000000';
|
|
16821
17171
|
var defaultIconColor = '#000000';
|
|
16822
17172
|
var defaultIconSize = 5;
|
|
17173
|
+
// The width a halo is switched on at: past the edge of the glyphs, so a
|
|
17174
|
+
// stroke of twice this.
|
|
17175
|
+
var defaultHaloWidth = 2;
|
|
17176
|
+
var defaultHaloColor = internal.svg.DEFAULT_HALO_COLOR;
|
|
16823
17177
|
// The line height field shows this rather than renderLabel()'s 1.1em default,
|
|
16824
17178
|
// and shows it as a placeholder rather than a value, so that a label carries
|
|
16825
17179
|
// no line-height until one is chosen. "auto" is the honest description of a
|
|
@@ -16846,10 +17200,20 @@
|
|
|
16846
17200
|
labelAlignField,
|
|
16847
17201
|
cssField,
|
|
16848
17202
|
'label-pos',
|
|
17203
|
+
haloWidthField,
|
|
17204
|
+
haloColorField,
|
|
17205
|
+
haloOpacityField,
|
|
16849
17206
|
iconField,
|
|
16850
17207
|
iconSizeField,
|
|
16851
17208
|
iconColorField,
|
|
16852
|
-
iconOpacityField
|
|
17209
|
+
iconOpacityField,
|
|
17210
|
+
calloutField,
|
|
17211
|
+
calloutEndField,
|
|
17212
|
+
calloutEndSizeField,
|
|
17213
|
+
calloutColorField,
|
|
17214
|
+
calloutOpacityField,
|
|
17215
|
+
calloutWidthField,
|
|
17216
|
+
calloutGapField
|
|
16853
17217
|
];
|
|
16854
17218
|
var labelPositions = ['nw', 'n', 'ne', 'w', 'c', 'e', 'sw', 's', 'se'];
|
|
16855
17219
|
// The position an icon moves a centred label to when it is switched on: upper
|
|
@@ -16911,6 +17275,41 @@
|
|
|
16911
17275
|
center: '<line x1="3" y1="4.5" x2="13" y2="4.5"></line><line x1="5.5" y1="8" x2="10.5" y2="8"></line><line x1="4" y1="11.5" x2="12" y2="11.5"></line>',
|
|
16912
17276
|
right: '<line x1="3" y1="4.5" x2="13" y2="4.5"></line><line x1="8" y1="8" x2="13" y2="8"></line><line x1="5" y1="11.5" x2="13" y2="11.5"></line>'
|
|
16913
17277
|
};
|
|
17278
|
+
// As with icons, no "none" among the shapes: the section's switch says whether
|
|
17279
|
+
// there is a callout. The ends do have one, since a line with no marker is a
|
|
17280
|
+
// shape of its own rather than the absence of a callout.
|
|
17281
|
+
var calloutShapes = [{
|
|
17282
|
+
name: 'line',
|
|
17283
|
+
title: 'straight'
|
|
17284
|
+
}, {
|
|
17285
|
+
name: 'elbow',
|
|
17286
|
+
title: 'elbow'
|
|
17287
|
+
}, {
|
|
17288
|
+
name: 'curve',
|
|
17289
|
+
title: 'curve'
|
|
17290
|
+
}];
|
|
17291
|
+
var calloutEnds = [{
|
|
17292
|
+
name: 'none',
|
|
17293
|
+
title: 'no marker at the anchor'
|
|
17294
|
+
}, {
|
|
17295
|
+
name: 'arrow',
|
|
17296
|
+
title: 'solid arrowhead at the anchor'
|
|
17297
|
+
}, {
|
|
17298
|
+
name: 'open-arrow',
|
|
17299
|
+
title: 'open arrowhead at the anchor'
|
|
17300
|
+
}];
|
|
17301
|
+
// The anchor is at the lower left of the shapes and at the left of the ends,
|
|
17302
|
+
// which is where the markers go. The arrowheads are drawn to the renderer's
|
|
17303
|
+
// angles, with sides the same visible length -- the open one's stroke
|
|
17304
|
+
// included -- so that they compare as the drawn arrows do.
|
|
17305
|
+
var calloutButtonSymbols = {
|
|
17306
|
+
line: '<path d="M3.5 12.5L12.5 3.5"></path>',
|
|
17307
|
+
elbow: '<path d="M3.5 12.5L8 4.5H13"></path>',
|
|
17308
|
+
curve: '<path d="M3.5 12.5Q4.5 4.5 12.5 4"></path>',
|
|
17309
|
+
none: '<path d="M3 8H13"></path>',
|
|
17310
|
+
arrow: '<path d="M6.7 8H13"></path><path class="fill" d="M2.5 8L8.53 5.57V10.43Z"></path>',
|
|
17311
|
+
'open-arrow': '<path d="M3.2 8H13"></path><path d="M7.38 5.07L3.2 8L7.38 10.93"></path>'
|
|
17312
|
+
};
|
|
16914
17313
|
var iconButtonSymbols = {
|
|
16915
17314
|
circle: '<circle cx="8" cy="8" r="4.25"></circle>',
|
|
16916
17315
|
square: '<rect x="4" y="4" width="8" height="8"></rect>',
|
|
@@ -16925,11 +17324,15 @@
|
|
|
16925
17324
|
// label-style-panel carries the styling the point and layer panels share; the
|
|
16926
17325
|
// second class is this panel's own, as theirs are
|
|
16927
17326
|
var panel = El('div').addClass('label-style-panel text-style-panel rollover').appendTo(parent).hide();
|
|
16928
|
-
var presetControl, fontSelect, fontStyleSelect, fontSizeInput, colorFieldBox, colorChit, colorInput, colorPicker, opacityInput, letterSpacingInput, lineHeightInput, alignBtns, cssInput, posBtns, dragModeBtns, iconToggle, iconGroupEl, iconBtns, iconSizeInput, iconColorFieldBox, iconColorChit, iconColorInput, iconColorPicker, iconOpacityInput, editingStatus, clearLink, closeBtn, hit;
|
|
17327
|
+
var presetControl, fontSelect, fontStyleSelect, fontSizeInput, colorFieldBox, colorChit, colorInput, colorPicker, opacityInput, letterSpacingInput, lineHeightInput, alignBtns, cssInput, posBtns, dragModeBtns, haloToggle, haloWidthInput, haloColorFieldBox, haloColorChit, haloColorInput, haloColorPicker, haloOpacityInput, iconToggle, iconGroupEl, iconBtns, iconSizeInput, iconColorFieldBox, iconColorChit, iconColorInput, iconColorPicker, iconOpacityInput, haloSection, iconSection, calloutSection, calloutToggle, calloutShapeGroupEl, calloutShapeBtns, calloutEndGroupEl, calloutEndBtns, calloutColorFieldBox, calloutColorChit, calloutColorInput, calloutColorPicker, calloutOpacityInput, calloutWidthInput, calloutEndSizeInput, calloutGapInput, editingStatus, clearLink, closeBtn, hit;
|
|
16929
17328
|
var fontOptionsRendered = false;
|
|
16930
17329
|
// The shape the toggle turns back on, so that switching a symbol off and on
|
|
16931
17330
|
// again does not silently change a star into a circle.
|
|
16932
17331
|
var lastIconShape = defaultIconShape;
|
|
17332
|
+
// Likewise the halo's width, which is what switching one off removes.
|
|
17333
|
+
var lastHaloWidth = defaultHaloWidth;
|
|
17334
|
+
// And the callout's shape.
|
|
17335
|
+
var lastCalloutShape = defaultCalloutShape;
|
|
16933
17336
|
|
|
16934
17337
|
initPanel();
|
|
16935
17338
|
gui.addMode(labelStylePanelMode, turnOn, turnOff);
|
|
@@ -17029,8 +17432,18 @@
|
|
|
17029
17432
|
});
|
|
17030
17433
|
|
|
17031
17434
|
var header = El('div').addClass('label-style-panel-title').appendTo(panel).text('Label styles');
|
|
17435
|
+
// In label mode the panel belongs to the mode, so closing it leaves the
|
|
17436
|
+
// mode: a panel that shut while the map stayed armed for placing labels
|
|
17437
|
+
// would leave the tool half on, with no panel to turn it off from. Every
|
|
17438
|
+
// other style panel's × closes a panel and nothing else, and this one
|
|
17439
|
+
// reads as the same button, which is the argument for it doing the whole
|
|
17440
|
+
// of what the user asked for rather than part.
|
|
17032
17441
|
closeBtn = El('button').addClass('label-style-close').appendTo(header).text('×').on('click', function() {
|
|
17033
|
-
|
|
17442
|
+
if (labelModeIsOn()) {
|
|
17443
|
+
gui.interaction.turnOff(); // the tool's own turnOff() takes the panel
|
|
17444
|
+
} else {
|
|
17445
|
+
gui.clearMode();
|
|
17446
|
+
}
|
|
17034
17447
|
});
|
|
17035
17448
|
|
|
17036
17449
|
var selectRow = El('div').addClass('label-style-selection-row').appendTo(panel);
|
|
@@ -17098,6 +17511,9 @@
|
|
|
17098
17511
|
// Letter spacing takes the right-hand column on its own, above line
|
|
17099
17512
|
// height: the two spacing values read as a pair there, and the left of the
|
|
17100
17513
|
// row is where the alignment buttons go.
|
|
17514
|
+
//
|
|
17515
|
+
// A text block's width is not here: it is set by dragging the block's
|
|
17516
|
+
// handle, and a field for it would be clutter on every other label.
|
|
17101
17517
|
var letterRow = El('div').addClass('label-style-row label-split-row').appendTo(textSection);
|
|
17102
17518
|
El('div').addClass('label-split-cell').appendTo(letterRow);
|
|
17103
17519
|
var letterCell = El('div').addClass('label-split-cell label-spacing-row').appendTo(letterRow);
|
|
@@ -17136,15 +17552,69 @@
|
|
|
17136
17552
|
var positionRow = El('label').addClass('label-style-row').appendTo(textSection);
|
|
17137
17553
|
El('span').appendTo(positionRow).text('Offset from anchor');
|
|
17138
17554
|
|
|
17555
|
+
// A halo is a yes/no that its three values then qualify, as a symbol is, so
|
|
17556
|
+
// it is switched the same way and its controls are inert while it is off.
|
|
17557
|
+
// Its rows are ones the Text section already has: a colour and its opacity
|
|
17558
|
+
// on one line, and a width in the narrow column beneath, where letter
|
|
17559
|
+
// spacing sits above.
|
|
17560
|
+
haloSection = addSection('Halo');
|
|
17561
|
+
var haloTitle = haloSection.findChild('.label-style-section-title');
|
|
17562
|
+
haloToggle = makeToggle(haloTitle, {
|
|
17563
|
+
title: 'Draw a halo around the text',
|
|
17564
|
+
className: 'label-halo-toggle',
|
|
17565
|
+
onChange: setHaloOn
|
|
17566
|
+
});
|
|
17567
|
+
|
|
17568
|
+
var haloColorRow = El('div').addClass('label-style-row label-split-row').appendTo(haloSection);
|
|
17569
|
+
var haloColorCell = El('div').addClass('label-split-cell label-color-row').appendTo(haloColorRow);
|
|
17570
|
+
haloColorChit = El('div').addClass('label-color-chit').attr('role', 'button');
|
|
17571
|
+
haloColorInput = El('input').attr('type', 'text').attr('title', 'Halo color');
|
|
17572
|
+
haloColorFieldBox = makeColorField(haloColorCell, haloColorChit, haloColorInput);
|
|
17573
|
+
haloColorChit.on('click', function() {
|
|
17574
|
+
if (this.classList.contains('disabled')) return;
|
|
17575
|
+
haloColorPicker.toggle();
|
|
17576
|
+
});
|
|
17577
|
+
haloColorInput.on('change', function() {
|
|
17578
|
+
var color = haloColorInput.node().value.trim();
|
|
17579
|
+
if (color) {
|
|
17580
|
+
if (isHexColor(color)) {
|
|
17581
|
+
haloColorPicker.setColor(color);
|
|
17582
|
+
}
|
|
17583
|
+
applyHaloColor(color);
|
|
17584
|
+
}
|
|
17585
|
+
});
|
|
17586
|
+
haloColorPicker = initColorPicker(haloColorCell, haloColorChit, haloColorInput, applyHaloColor);
|
|
17587
|
+
|
|
17588
|
+
var haloOpacityCell = El('div').addClass('label-split-cell label-opacity-row').appendTo(haloColorRow);
|
|
17589
|
+
haloOpacityInput = addOpacityInput(haloOpacityCell, applyHaloOpacity);
|
|
17590
|
+
|
|
17591
|
+
var haloWidthRow = El('div').addClass('label-style-row label-split-row').appendTo(haloSection);
|
|
17592
|
+
El('div').addClass('label-split-cell').appendTo(haloWidthRow);
|
|
17593
|
+
var haloWidthCell = El('div').addClass('label-split-cell label-spacing-row').appendTo(haloWidthRow);
|
|
17594
|
+
El('span').appendTo(haloWidthCell).text('Width');
|
|
17595
|
+
// Halves as well as whole pixels: a halo is usually 1 to 2px, and the
|
|
17596
|
+
// step between those two is most of the range anyone uses.
|
|
17597
|
+
haloWidthInput = new SizeField(haloWidthCell, {
|
|
17598
|
+
title: 'How far the halo extends past the text, in px',
|
|
17599
|
+
min: 0.1,
|
|
17600
|
+
max: 50,
|
|
17601
|
+
step: 0.5,
|
|
17602
|
+
bigStep: 2,
|
|
17603
|
+
onSet: applyHaloWidth,
|
|
17604
|
+
onStep: nudgeHaloWidth,
|
|
17605
|
+
onDone: releaseFocus
|
|
17606
|
+
});
|
|
17607
|
+
|
|
17139
17608
|
// Whether the label has a symbol is one question and which symbol it has is
|
|
17140
17609
|
// another, so the first is a switch on the section's heading rather than a
|
|
17141
17610
|
// fifth shape button reading "none". Everything below it is inert while it
|
|
17142
17611
|
// is off, which is also the honest reading of an icon-size or icon-color on
|
|
17143
17612
|
// a label with no icon: nothing to apply it to.
|
|
17144
|
-
|
|
17613
|
+
iconSection = addSection('Icon');
|
|
17145
17614
|
var iconTitle = iconSection.findChild('.label-style-section-title');
|
|
17146
17615
|
iconToggle = makeToggle(iconTitle, {
|
|
17147
17616
|
title: 'Draw a symbol at the label anchor',
|
|
17617
|
+
className: 'label-icon-toggle',
|
|
17148
17618
|
onChange: setIconOn
|
|
17149
17619
|
});
|
|
17150
17620
|
// The size's caption sits on the heading line, over its own column. It
|
|
@@ -17200,6 +17670,8 @@
|
|
|
17200
17670
|
var iconOpacityCell = El('div').addClass('label-split-cell label-opacity-row label-icon-opacity-row').appendTo(iconColorRow);
|
|
17201
17671
|
iconOpacityInput = addOpacityInput(iconOpacityCell, applyIconOpacity);
|
|
17202
17672
|
|
|
17673
|
+
initCalloutSection();
|
|
17674
|
+
|
|
17203
17675
|
var posRow = El('div').addClass('label-style-row label-position-row').appendTo(textSection);
|
|
17204
17676
|
|
|
17205
17677
|
|
|
@@ -17223,7 +17695,7 @@
|
|
|
17223
17695
|
dragModeBtns = {};
|
|
17224
17696
|
labelDragModes.forEach(function(item) {
|
|
17225
17697
|
dragModeBtns[item.name] = makePanelButton(dragModeGroup, item.label, function() {
|
|
17226
|
-
setLabelPositionMode(gui, item.name);
|
|
17698
|
+
setLabelPositionMode(gui, item.name, getPositionModeKind());
|
|
17227
17699
|
})
|
|
17228
17700
|
.attr('data-drag-mode', item.name)
|
|
17229
17701
|
.attr('title', item.title);
|
|
@@ -17244,6 +17716,116 @@
|
|
|
17244
17716
|
});
|
|
17245
17717
|
}
|
|
17246
17718
|
|
|
17719
|
+
// Switched like the halo and the symbol, and shaped like them: the choices on
|
|
17720
|
+
// the first line, a colour and its opacity on the next, then the sizes. The
|
|
17721
|
+
// line's geometry -- its corner or bend, where it meets the text, how far it
|
|
17722
|
+
// stops short of the anchor -- is a matter of positions, which are dragged on
|
|
17723
|
+
// the map rather than typed here. See docs/development/text-annotation-design.md.
|
|
17724
|
+
function initCalloutSection() {
|
|
17725
|
+
calloutSection = addSection('Callout');
|
|
17726
|
+
calloutToggle = makeToggle(calloutSection.findChild('.label-style-section-title'), {
|
|
17727
|
+
title: 'Draw a line from the label anchor to its text',
|
|
17728
|
+
className: 'label-callout-toggle',
|
|
17729
|
+
onChange: setCalloutOn
|
|
17730
|
+
});
|
|
17731
|
+
|
|
17732
|
+
// Each choice in the wide column with the size that qualifies it beside
|
|
17733
|
+
// it, as Alignment has Line height: the line's shape and its width, then
|
|
17734
|
+
// the marker and its size.
|
|
17735
|
+
var shapeRow = El('div').addClass('label-style-row label-split-row').appendTo(calloutSection);
|
|
17736
|
+
var shapeCell = El('div').addClass('label-split-cell label-align-row').appendTo(shapeRow);
|
|
17737
|
+
El('span').appendTo(shapeCell).text('Line');
|
|
17738
|
+
calloutShapeGroupEl = El('div').addClass('label-btn-group label-callout-buttons').appendTo(shapeCell);
|
|
17739
|
+
calloutShapeBtns = {};
|
|
17740
|
+
calloutShapes.forEach(function(item) {
|
|
17741
|
+
calloutShapeBtns[item.name] = makeCalloutButton(calloutShapeGroupEl, item, function() {
|
|
17742
|
+
applyCalloutShape(item.name);
|
|
17743
|
+
});
|
|
17744
|
+
});
|
|
17745
|
+
var widthCell = El('div').addClass('label-split-cell label-spacing-row label-callout-width-row').appendTo(shapeRow);
|
|
17746
|
+
El('span').appendTo(widthCell).text('Width');
|
|
17747
|
+
calloutWidthInput = new SizeField(widthCell, {
|
|
17748
|
+
title: 'Callout line width in px',
|
|
17749
|
+
min: 0.25,
|
|
17750
|
+
max: 20,
|
|
17751
|
+
step: 0.5,
|
|
17752
|
+
bigStep: 2,
|
|
17753
|
+
decimals: 2,
|
|
17754
|
+
onSet: applyCalloutWidth,
|
|
17755
|
+
onStep: nudgeCalloutWidth,
|
|
17756
|
+
onDone: releaseFocus
|
|
17757
|
+
});
|
|
17758
|
+
|
|
17759
|
+
var endRow = El('div').addClass('label-style-row label-split-row').appendTo(calloutSection);
|
|
17760
|
+
var endCell = El('div').addClass('label-split-cell label-align-row').appendTo(endRow);
|
|
17761
|
+
El('span').appendTo(endCell).text('End');
|
|
17762
|
+
calloutEndGroupEl = El('div').addClass('label-btn-group label-callout-buttons').appendTo(endCell);
|
|
17763
|
+
calloutEndBtns = {};
|
|
17764
|
+
calloutEnds.forEach(function(item) {
|
|
17765
|
+
calloutEndBtns[item.name] = makeCalloutButton(calloutEndGroupEl, item, function() {
|
|
17766
|
+
applyCalloutEnd(item.name);
|
|
17767
|
+
});
|
|
17768
|
+
});
|
|
17769
|
+
var endSizeCell = El('div').addClass('label-split-cell label-spacing-row label-callout-end-size-row').appendTo(endRow);
|
|
17770
|
+
El('span').appendTo(endSizeCell).text('Size');
|
|
17771
|
+
calloutEndSizeInput = new SizeField(endSizeCell, {
|
|
17772
|
+
title: 'Arrowhead size in px, the length of its sides',
|
|
17773
|
+
min: 1,
|
|
17774
|
+
max: 60,
|
|
17775
|
+
step: 1,
|
|
17776
|
+
bigStep: 5,
|
|
17777
|
+
onSet: applyCalloutEndSize,
|
|
17778
|
+
onStep: nudgeCalloutEndSize,
|
|
17779
|
+
onDone: releaseFocus
|
|
17780
|
+
});
|
|
17781
|
+
|
|
17782
|
+
var colorRow = El('div').addClass('label-style-row label-split-row').appendTo(calloutSection);
|
|
17783
|
+
var colorCell = El('div').addClass('label-split-cell label-color-row').appendTo(colorRow);
|
|
17784
|
+
calloutColorChit = El('div').addClass('label-color-chit').attr('role', 'button');
|
|
17785
|
+
calloutColorInput = El('input').attr('type', 'text').attr('title', 'Callout color');
|
|
17786
|
+
calloutColorFieldBox = makeColorField(colorCell, calloutColorChit, calloutColorInput);
|
|
17787
|
+
calloutColorChit.on('click', function() {
|
|
17788
|
+
if (this.classList.contains('disabled')) return;
|
|
17789
|
+
calloutColorPicker.toggle();
|
|
17790
|
+
});
|
|
17791
|
+
calloutColorInput.on('change', function() {
|
|
17792
|
+
var color = calloutColorInput.node().value.trim();
|
|
17793
|
+
if (color) {
|
|
17794
|
+
if (isHexColor(color)) {
|
|
17795
|
+
calloutColorPicker.setColor(color);
|
|
17796
|
+
}
|
|
17797
|
+
applyCalloutColor(color);
|
|
17798
|
+
}
|
|
17799
|
+
});
|
|
17800
|
+
calloutColorPicker = initColorPicker(colorCell, calloutColorChit, calloutColorInput, applyCalloutColor);
|
|
17801
|
+
var opacityCell = El('div').addClass('label-split-cell label-opacity-row').appendTo(colorRow);
|
|
17802
|
+
calloutOpacityInput = addOpacityInput(opacityCell, applyCalloutOpacity);
|
|
17803
|
+
|
|
17804
|
+
// The gap in the narrow column, under the sizes: it has a blank state,
|
|
17805
|
+
// "auto", which clears the anchor's symbol, so it is not a size field.
|
|
17806
|
+
var gapRow = El('div').addClass('label-style-row label-split-row').appendTo(calloutSection);
|
|
17807
|
+
El('div').addClass('label-split-cell').appendTo(gapRow);
|
|
17808
|
+
var gapCell = El('div').addClass('label-split-cell label-spacing-row label-callout-gap-row').appendTo(gapRow);
|
|
17809
|
+
El('span').appendTo(gapCell).text('Gap');
|
|
17810
|
+
calloutGapInput = El('input').attr('type', 'text').addClass('label-measure-input')
|
|
17811
|
+
.attr('title', 'Space between the callout and the anchor, in px')
|
|
17812
|
+
.attr('placeholder', 'auto')
|
|
17813
|
+
.attr('data-placeholder', 'auto')
|
|
17814
|
+
.appendTo(gapCell)
|
|
17815
|
+
.on('change', function() {
|
|
17816
|
+
applyCalloutGap(calloutGapInput.node().value.trim());
|
|
17817
|
+
});
|
|
17818
|
+
}
|
|
17819
|
+
|
|
17820
|
+
function makeCalloutButton(parent, item, action) {
|
|
17821
|
+
var btn = makePanelButton(parent, '', action)
|
|
17822
|
+
.attr('data-callout', item.name)
|
|
17823
|
+
.attr('title', item.title);
|
|
17824
|
+
El('<svg class="label-callout-symbol" viewBox="0 0 16 16" aria-hidden="true">' +
|
|
17825
|
+
calloutButtonSymbols[item.name] + '</svg>').appendTo(btn);
|
|
17826
|
+
return btn;
|
|
17827
|
+
}
|
|
17828
|
+
|
|
17247
17829
|
function appendIconButtonSymbol(btn, iconName) {
|
|
17248
17830
|
var svg = '<svg class="label-icon-symbol" viewBox="0 0 16 16" aria-hidden="true">' +
|
|
17249
17831
|
iconButtonSymbols[iconName] + '</svg>';
|
|
@@ -17277,6 +17859,7 @@
|
|
|
17277
17859
|
function makeToggle(parent, opts) {
|
|
17278
17860
|
var track = El('div').addClass('label-toggle').attr('role', 'checkbox').appendTo(parent);
|
|
17279
17861
|
var state = 'off';
|
|
17862
|
+
if (opts.className) track.addClass(opts.className);
|
|
17280
17863
|
var disabled = false;
|
|
17281
17864
|
El('div').addClass('label-toggle-knob').appendTo(track);
|
|
17282
17865
|
if (opts.title) track.attr('title', opts.title);
|
|
@@ -17362,9 +17945,7 @@
|
|
|
17362
17945
|
renderFontOptions();
|
|
17363
17946
|
gui.state.label_style_panel_open = true;
|
|
17364
17947
|
panel.show();
|
|
17365
|
-
|
|
17366
|
-
// user opened, and closing it would leave the tool half on.
|
|
17367
|
-
closeBtn[labelModeIsOn() ? 'hide' : 'show']();
|
|
17948
|
+
closeBtn.show();
|
|
17368
17949
|
textBtn.addClass('selected');
|
|
17369
17950
|
updateControls();
|
|
17370
17951
|
updateSelectionDisplay();
|
|
@@ -17511,6 +18092,10 @@
|
|
|
17511
18092
|
var iconSize = getShownValue(iconIds, iconSizeField, {useDefault: true, defaultValue: defaultIconSize});
|
|
17512
18093
|
var iconColor = getShownValue(iconIds, iconColorField, {useDefault: true, defaultValue: defaultIconColor});
|
|
17513
18094
|
var iconOpacity = getShownValue(iconIds, iconOpacityField, {useDefault: true, defaultValue: 1});
|
|
18095
|
+
var haloIds = getHaloValueIds();
|
|
18096
|
+
var haloWidth = getShownValue(haloIds, haloWidthField, {useDefault: true, defaultValue: lastHaloWidth});
|
|
18097
|
+
var haloColor = getShownValue(haloIds, haloColorField, {useDefault: true, defaultValue: defaultHaloColor});
|
|
18098
|
+
var haloOpacity = getShownValue(haloIds, haloOpacityField, {useDefault: true, defaultValue: 1});
|
|
17514
18099
|
updateEditingStatus(manualIds.length, !!getLabelTextSession(gui));
|
|
17515
18100
|
updateSavedStyleControls();
|
|
17516
18101
|
fontSelect.node().disabled = !showValues;
|
|
@@ -17524,12 +18109,34 @@
|
|
|
17524
18109
|
updateAlignButtons(showValues ? alignVal : '');
|
|
17525
18110
|
updateCssControl(css);
|
|
17526
18111
|
updatePositionButtons(showValues ? posVal : '', ids);
|
|
18112
|
+
var haloOff = updateHaloToggle();
|
|
18113
|
+
updateHaloWidthControl(haloWidth, haloOff);
|
|
18114
|
+
updateSwatchField(haloColorInput, haloColorFieldBox, haloColorChit,
|
|
18115
|
+
haloColorPicker, haloColor, haloOff);
|
|
18116
|
+
updateOpacityControl(haloOpacityInput, haloOpacity, haloOff);
|
|
17527
18117
|
// The symbol's controls keep showing their values while the switch is off,
|
|
17528
18118
|
// greyed: what they show is what the symbol comes back as.
|
|
17529
18119
|
var iconOff = updateIconControls(showValues ? iconVal : '');
|
|
17530
18120
|
updateIconSizeControls(iconSize, iconOff);
|
|
17531
18121
|
updateIconColorControls(iconColor, iconOff);
|
|
17532
18122
|
updateOpacityControl(iconOpacityInput, iconOpacity, iconOff);
|
|
18123
|
+
var calloutOff = updateCalloutValueControls();
|
|
18124
|
+
setSectionCollapsed(haloSection, haloOff, haloColorPicker);
|
|
18125
|
+
setSectionCollapsed(iconSection, iconOff, iconColorPicker);
|
|
18126
|
+
setSectionCollapsed(calloutSection, calloutOff, calloutColorPicker);
|
|
18127
|
+
}
|
|
18128
|
+
|
|
18129
|
+
// A section whose switch is off shows only its heading. What is under it is
|
|
18130
|
+
// inert while it is off, so hiding it loses nothing, and the panel stays short
|
|
18131
|
+
// enough to hold all of them. The values are kept, greyed, for the moment it
|
|
18132
|
+
// is switched back on, which is what they come back as.
|
|
18133
|
+
//
|
|
18134
|
+
// An open picker is closed with its section: it hangs from the colour field,
|
|
18135
|
+
// and a picker left open over a hidden field would change a colour nobody
|
|
18136
|
+
// can see.
|
|
18137
|
+
function setSectionCollapsed(section, collapsed, picker) {
|
|
18138
|
+
section.classed('collapsed', collapsed);
|
|
18139
|
+
if (collapsed && picker.visible()) picker.hide();
|
|
17533
18140
|
}
|
|
17534
18141
|
|
|
17535
18142
|
// Every field that can show a value can also show nothing, which is why each
|
|
@@ -17632,11 +18239,12 @@
|
|
|
17632
18239
|
}));
|
|
17633
18240
|
}
|
|
17634
18241
|
|
|
17635
|
-
// The toggle is the tool's state and not the selection's,
|
|
17636
|
-
//
|
|
17637
|
-
//
|
|
18242
|
+
// The toggle is the tool's state and not the selection's, but there is one
|
|
18243
|
+
// for point text and one for text blocks, and it shows the one for the kind
|
|
18244
|
+
// of label selected. It is inert outside the label tool, where there is no
|
|
18245
|
+
// drag on a label for it to describe.
|
|
17638
18246
|
function updateDragModeButtons() {
|
|
17639
|
-
var mode = getLabelPositionMode(gui);
|
|
18247
|
+
var mode = getLabelPositionMode(gui, getPositionModeKind());
|
|
17640
18248
|
var disabled = !labelModeIsOn();
|
|
17641
18249
|
labelDragModes.forEach(function(item) {
|
|
17642
18250
|
dragModeBtns[item.name].classed('selected', !disabled && item.name == mode);
|
|
@@ -17644,6 +18252,16 @@
|
|
|
17644
18252
|
});
|
|
17645
18253
|
}
|
|
17646
18254
|
|
|
18255
|
+
// 'block' when the labels the panel is pointed at are all text blocks, and
|
|
18256
|
+
// otherwise 'point', including for the next label to be made.
|
|
18257
|
+
function getPositionModeKind() {
|
|
18258
|
+
var table = getActiveTable();
|
|
18259
|
+
var ids = getTargetIds();
|
|
18260
|
+
return ids.length > 0 && table && ids.every(function(id) {
|
|
18261
|
+
return getLabelPositionKind(table.getRecordAt(id)) == 'block';
|
|
18262
|
+
}) ? 'block' : 'point';
|
|
18263
|
+
}
|
|
18264
|
+
|
|
17647
18265
|
function everyLabelIsOnAPath(ids) {
|
|
17648
18266
|
return someLabelIsOnAPath(ids, 'every');
|
|
17649
18267
|
}
|
|
@@ -17781,10 +18399,12 @@
|
|
|
17781
18399
|
if (colorPicker.visible()) {
|
|
17782
18400
|
return; // avoid HSB -> RGB -> HSB rounding jumps after picker commits
|
|
17783
18401
|
}
|
|
18402
|
+
// Only this field's own picker: the halo's or the symbol's may be the one
|
|
18403
|
+
// open, and a text fill like "blue" says nothing about either.
|
|
17784
18404
|
if (isHexColor(colorVal)) {
|
|
17785
18405
|
colorPicker.setColor(colorVal);
|
|
17786
18406
|
} else {
|
|
17787
|
-
|
|
18407
|
+
colorPicker.hide();
|
|
17788
18408
|
}
|
|
17789
18409
|
}
|
|
17790
18410
|
|
|
@@ -17833,25 +18453,307 @@
|
|
|
17833
18453
|
}
|
|
17834
18454
|
|
|
17835
18455
|
function updateIconColorControls(shown, iconOff) {
|
|
18456
|
+
updateSwatchField(iconColorInput, iconColorFieldBox, iconColorChit,
|
|
18457
|
+
iconColorPicker, shown, iconOff);
|
|
18458
|
+
}
|
|
18459
|
+
|
|
18460
|
+
// A colour field belonging to a section with a switch: the symbol's and the
|
|
18461
|
+
// halo's, which both go on showing their colour, greyed, while switched off.
|
|
18462
|
+
function updateSwatchField(input, fieldBox, chit, picker, shown, sectionOff) {
|
|
17836
18463
|
var colorVal = shown.value;
|
|
17837
|
-
var disabled =
|
|
17838
|
-
|
|
17839
|
-
|
|
17840
|
-
setMixedPlaceholder(
|
|
17841
|
-
|
|
17842
|
-
setPanelButtonDisabled(
|
|
17843
|
-
|
|
17844
|
-
|
|
17845
|
-
if (
|
|
18464
|
+
var disabled = sectionOff || !controlsEnabled();
|
|
18465
|
+
input.node().disabled = disabled;
|
|
18466
|
+
input.node().value = colorVal || '';
|
|
18467
|
+
setMixedPlaceholder(input, shown.mixed);
|
|
18468
|
+
fieldBox.classed('disabled', disabled);
|
|
18469
|
+
setPanelButtonDisabled(chit, disabled);
|
|
18470
|
+
chit.classed('mixed', shown.mixed);
|
|
18471
|
+
chit.css('background-color', isHexColor(colorVal) ? colorVal : 'transparent');
|
|
18472
|
+
if (picker.visible()) {
|
|
17846
18473
|
return; // avoid HSB -> RGB -> HSB rounding jumps after picker commits
|
|
17847
18474
|
}
|
|
17848
18475
|
if (isHexColor(colorVal)) {
|
|
17849
|
-
|
|
18476
|
+
picker.setColor(colorVal);
|
|
17850
18477
|
} else {
|
|
17851
|
-
|
|
18478
|
+
picker.hide();
|
|
17852
18479
|
}
|
|
17853
18480
|
}
|
|
17854
18481
|
|
|
18482
|
+
// The switch reads the data, as the symbol's does, so that it follows an
|
|
18483
|
+
// undo. A halo is on for a label whose halo-width is above 0.
|
|
18484
|
+
//
|
|
18485
|
+
// The width, colour and opacity go on showing while it is off, greyed: the
|
|
18486
|
+
// colour and opacity stay on the label, and the width is the one the halo
|
|
18487
|
+
// comes back at.
|
|
18488
|
+
function updateHaloToggle() {
|
|
18489
|
+
var enabled = controlsEnabled();
|
|
18490
|
+
var state = enabled ? getHaloState() : 'off';
|
|
18491
|
+
haloToggle.setState(state);
|
|
18492
|
+
haloToggle.setDisabled(!enabled);
|
|
18493
|
+
return state == 'off';
|
|
18494
|
+
}
|
|
18495
|
+
|
|
18496
|
+
function updateHaloWidthControl(shown, haloOff) {
|
|
18497
|
+
if (!haloOff && Number(shown.value) > 0) lastHaloWidth = Number(shown.value);
|
|
18498
|
+
haloWidthInput.setValue(shown.value || '');
|
|
18499
|
+
haloWidthInput.setPlaceholder(shown.mixed ? MIXED_TEXT : '');
|
|
18500
|
+
haloWidthInput.setDisabled(haloOff || !controlsEnabled());
|
|
18501
|
+
}
|
|
18502
|
+
|
|
18503
|
+
function getHaloState() {
|
|
18504
|
+
var ids = getTargetIds();
|
|
18505
|
+
var table = getActiveTable();
|
|
18506
|
+
if (ids.length === 0) {
|
|
18507
|
+
return internal.svg.labelHasHalo(labelModeIsOn() ? getNewLabelStyle(gui) : null) ? 'on' : 'off';
|
|
18508
|
+
}
|
|
18509
|
+
return getToggleState(ids.map(function(id) {
|
|
18510
|
+
return internal.svg.labelHasHalo(table && table.getRecordAt(id));
|
|
18511
|
+
}));
|
|
18512
|
+
}
|
|
18513
|
+
|
|
18514
|
+
// Switching a halo on gives every target the same width: the one the
|
|
18515
|
+
// selection's halos already share, where they share one, so that a mixed
|
|
18516
|
+
// selection is brought into line with the labels that had a halo rather
|
|
18517
|
+
// than reset. Switching it off removes the width and leaves the colour and
|
|
18518
|
+
// opacity, which is how a halo switched off and on again comes back as it
|
|
18519
|
+
// was.
|
|
18520
|
+
function setHaloOn(on) {
|
|
18521
|
+
var width = on ? getNumericSize(getHaloValueIds(), haloWidthField, lastHaloWidth) : '';
|
|
18522
|
+
applyStyleValues([[haloWidthField, width]]);
|
|
18523
|
+
}
|
|
18524
|
+
|
|
18525
|
+
function applyHaloWidth(value) {
|
|
18526
|
+
applyStyleValues([[haloWidthField, value]], getHaloTargetIds());
|
|
18527
|
+
}
|
|
18528
|
+
|
|
18529
|
+
function nudgeHaloWidth(delta) {
|
|
18530
|
+
var width = getNumericSize(getHaloTargetIds(), haloWidthField, lastHaloWidth);
|
|
18531
|
+
if (!controlsEnabled() || getHaloState() == 'off') return;
|
|
18532
|
+
width = Math.max(0.5, Math.round((width + delta) * 10) / 10);
|
|
18533
|
+
applyHaloWidth(width);
|
|
18534
|
+
}
|
|
18535
|
+
|
|
18536
|
+
function applyHaloColor(color) {
|
|
18537
|
+
applyStyleValues([[haloColorField, color]], getHaloTargetIds());
|
|
18538
|
+
}
|
|
18539
|
+
|
|
18540
|
+
// Full opacity is stored as no halo-opacity, as it is for the text's own.
|
|
18541
|
+
function applyHaloOpacity(value) {
|
|
18542
|
+
applyStyleValues([[haloOpacityField, value >= 1 ? '' : value]], getHaloTargetIds());
|
|
18543
|
+
}
|
|
18544
|
+
|
|
18545
|
+
// The labels a halo's width, colour or opacity goes to: the targets that
|
|
18546
|
+
// have a halo, for the reason the symbol's values go only to labels with a
|
|
18547
|
+
// symbol. Empty for "new labels", as there.
|
|
18548
|
+
function getHaloTargetIds() {
|
|
18549
|
+
var table = getActiveTable();
|
|
18550
|
+
return getTargetIds().filter(function(id) {
|
|
18551
|
+
return internal.svg.labelHasHalo(table && table.getRecordAt(id));
|
|
18552
|
+
});
|
|
18553
|
+
}
|
|
18554
|
+
|
|
18555
|
+
// As getIconValueIds(): the labels the section shows the values of.
|
|
18556
|
+
function getHaloValueIds() {
|
|
18557
|
+
var ids = getHaloTargetIds();
|
|
18558
|
+
return ids.length > 0 ? ids : getTargetIds();
|
|
18559
|
+
}
|
|
18560
|
+
|
|
18561
|
+
// The switch reads the data, as the halo's does. A callout is on for a label
|
|
18562
|
+
// whose callout names a shape.
|
|
18563
|
+
function getCalloutState() {
|
|
18564
|
+
var ids = getTargetIds();
|
|
18565
|
+
var table = getActiveTable();
|
|
18566
|
+
if (ids.length === 0) {
|
|
18567
|
+
return internal.svg.labelHasCallout(labelModeIsOn() ? getNewLabelStyle(gui) : null) ? 'on' : 'off';
|
|
18568
|
+
}
|
|
18569
|
+
return getToggleState(ids.map(function(id) {
|
|
18570
|
+
return internal.svg.labelHasCallout(table && table.getRecordAt(id));
|
|
18571
|
+
}));
|
|
18572
|
+
}
|
|
18573
|
+
|
|
18574
|
+
// The labels a callout's end, colour, width and gap go to: the targets that
|
|
18575
|
+
// have a callout, for the reason the symbol's values go only to labels with a
|
|
18576
|
+
// symbol.
|
|
18577
|
+
function getCalloutTargetIds() {
|
|
18578
|
+
var table = getActiveTable();
|
|
18579
|
+
return getTargetIds().filter(function(id) {
|
|
18580
|
+
return internal.svg.labelHasCallout(table && table.getRecordAt(id));
|
|
18581
|
+
});
|
|
18582
|
+
}
|
|
18583
|
+
|
|
18584
|
+
function getCalloutValueIds() {
|
|
18585
|
+
var ids = getCalloutTargetIds();
|
|
18586
|
+
return ids.length > 0 ? ids : getTargetIds();
|
|
18587
|
+
}
|
|
18588
|
+
|
|
18589
|
+
// Switching a callout on gives every target the shape the selection's
|
|
18590
|
+
// callouts share, or the last one used. Switching it off removes the shape
|
|
18591
|
+
// alone, so that a callout switched off and on again comes back with the
|
|
18592
|
+
// corner, the attachment and the look it had.
|
|
18593
|
+
function setCalloutOn(on) {
|
|
18594
|
+
var shape = on ? getCommonValue(getCalloutTargetIds(), calloutField) || lastCalloutShape : '';
|
|
18595
|
+
applyStyleValues([[calloutField, shape]]);
|
|
18596
|
+
}
|
|
18597
|
+
|
|
18598
|
+
// A shape is a plain statement about every selected label, as a symbol's is.
|
|
18599
|
+
function applyCalloutShape(shape) {
|
|
18600
|
+
applyStyleValues([[calloutField, shape]]);
|
|
18601
|
+
}
|
|
18602
|
+
|
|
18603
|
+
// No marker is stored as no callout-end.
|
|
18604
|
+
function applyCalloutEnd(end) {
|
|
18605
|
+
applyStyleValues([[calloutEndField, end == 'none' ? '' : end]], getCalloutTargetIds());
|
|
18606
|
+
}
|
|
18607
|
+
|
|
18608
|
+
function applyCalloutColor(color) {
|
|
18609
|
+
applyStyleValues([[calloutColorField, color]], getCalloutTargetIds());
|
|
18610
|
+
}
|
|
18611
|
+
|
|
18612
|
+
function applyCalloutOpacity(value) {
|
|
18613
|
+
applyStyleValues([[calloutOpacityField, value >= 1 ? '' : value]], getCalloutTargetIds());
|
|
18614
|
+
}
|
|
18615
|
+
|
|
18616
|
+
function applyCalloutWidth(value) {
|
|
18617
|
+
applyStyleValues([[calloutWidthField, value]], getCalloutTargetIds());
|
|
18618
|
+
}
|
|
18619
|
+
|
|
18620
|
+
function nudgeCalloutWidth(delta) {
|
|
18621
|
+
var width = getNumericSize(getCalloutTargetIds(), calloutWidthField, defaultCalloutWidth);
|
|
18622
|
+
if (!controlsEnabled() || getCalloutState() == 'off') return;
|
|
18623
|
+
applyCalloutWidth(Math.max(0.25, Math.round((width + delta) * 4) / 4));
|
|
18624
|
+
}
|
|
18625
|
+
|
|
18626
|
+
// A marker size goes to the callouts that have a marker to size, as a
|
|
18627
|
+
// symbol's size goes only to labels with a symbol.
|
|
18628
|
+
function applyCalloutEndSize(value) {
|
|
18629
|
+
applyStyleValues([[calloutEndSizeField, value]], getCalloutEndTargetIds());
|
|
18630
|
+
}
|
|
18631
|
+
|
|
18632
|
+
// Stepped from the size each marker is drawn at, which for one with no
|
|
18633
|
+
// callout-end-size is the default its line width gives it.
|
|
18634
|
+
function nudgeCalloutEndSize(delta) {
|
|
18635
|
+
var shown = getCalloutEndSizeShown(getCalloutEndTargetIds());
|
|
18636
|
+
var size = Number(shown.value);
|
|
18637
|
+
if (!controlsEnabled() || !(size > 0)) return;
|
|
18638
|
+
applyCalloutEndSize(Math.max(1, Math.round(size + delta)));
|
|
18639
|
+
}
|
|
18640
|
+
|
|
18641
|
+
function getCalloutEndTargetIds() {
|
|
18642
|
+
var table = getActiveTable();
|
|
18643
|
+
return getCalloutTargetIds().filter(function(id) {
|
|
18644
|
+
return getCalloutEnd(table && table.getRecordAt(id)) != 'none';
|
|
18645
|
+
});
|
|
18646
|
+
}
|
|
18647
|
+
|
|
18648
|
+
function getCalloutEnd(rec) {
|
|
18649
|
+
return internal.svg.getCalloutEndType(rec);
|
|
18650
|
+
}
|
|
18651
|
+
|
|
18652
|
+
// The size a marker is drawn at: its own, or the default for its kind and
|
|
18653
|
+
// line width.
|
|
18654
|
+
function getCalloutEndSizeShown(ids) {
|
|
18655
|
+
return getShownRecordValues(ids, function(rec) {
|
|
18656
|
+
var size = rec ? Number(rec[calloutEndSizeField]) : 0;
|
|
18657
|
+
var width = rec ? Number(rec[calloutWidthField]) : 0;
|
|
18658
|
+
if (size > 0) return size;
|
|
18659
|
+
return internal.svg.getDefaultCalloutEndSize(getCalloutEnd(rec),
|
|
18660
|
+
width > 0 ? width : defaultCalloutWidth);
|
|
18661
|
+
});
|
|
18662
|
+
}
|
|
18663
|
+
|
|
18664
|
+
// Blank is "auto", which clears the anchor's symbol, and 0 is no gap at all:
|
|
18665
|
+
// the two are different, which is why this is not a size field.
|
|
18666
|
+
function applyCalloutGap(str) {
|
|
18667
|
+
var val = Number(str);
|
|
18668
|
+
if (str !== '' && !(isFinite(val) && val >= 0)) {
|
|
18669
|
+
updateControls();
|
|
18670
|
+
return;
|
|
18671
|
+
}
|
|
18672
|
+
applyStyleValues([[calloutGapField, str === '' ? '' : val]], getCalloutTargetIds());
|
|
18673
|
+
}
|
|
18674
|
+
|
|
18675
|
+
// Returns whether the section is off.
|
|
18676
|
+
function updateCalloutValueControls() {
|
|
18677
|
+
var enabled = controlsEnabled();
|
|
18678
|
+
var state = enabled ? getCalloutState() : 'off';
|
|
18679
|
+
var off = state == 'off';
|
|
18680
|
+
var ids = getCalloutValueIds();
|
|
18681
|
+
var shape = enabled ? getCommonValue(ids, calloutField) : '';
|
|
18682
|
+
var end = enabled ? getCommonValue(ids, calloutEndField, {useDefault: true, defaultValue: 'none'}) : '';
|
|
18683
|
+
var width = getShownValue(ids, calloutWidthField, {useDefault: true, defaultValue: defaultCalloutWidth});
|
|
18684
|
+
var gap = getCalloutGapShown(ids);
|
|
18685
|
+
if (shape && shape != 'none') lastCalloutShape = shape;
|
|
18686
|
+
calloutToggle.setState(state);
|
|
18687
|
+
calloutToggle.setDisabled(!enabled);
|
|
18688
|
+
updateButtonGroup(calloutShapeGroupEl, calloutShapeBtns, off ? '' : shape, off);
|
|
18689
|
+
updateButtonGroup(calloutEndGroupEl, calloutEndBtns, off ? '' : end, off);
|
|
18690
|
+
updateSwatchField(calloutColorInput, calloutColorFieldBox, calloutColorChit,
|
|
18691
|
+
calloutColorPicker, getCalloutColorShown(ids), off);
|
|
18692
|
+
updateOpacityControl(calloutOpacityInput,
|
|
18693
|
+
getShownValue(ids, calloutOpacityField, {useDefault: true, defaultValue: 1}), off);
|
|
18694
|
+
calloutWidthInput.setValue(width.value || '');
|
|
18695
|
+
calloutWidthInput.setPlaceholder(width.mixed ? MIXED_TEXT : '');
|
|
18696
|
+
calloutWidthInput.setDisabled(off || !enabled);
|
|
18697
|
+
updateCalloutEndSizeControl(off || !enabled);
|
|
18698
|
+
calloutGapInput.node().disabled = off || !enabled;
|
|
18699
|
+
calloutGapInput.node().value = gap.value;
|
|
18700
|
+
setMixedPlaceholder(calloutGapInput, gap.mixed);
|
|
18701
|
+
return off;
|
|
18702
|
+
}
|
|
18703
|
+
|
|
18704
|
+
// Inert with no marker to size. With nothing selected it describes the next
|
|
18705
|
+
// label's marker, which the new-label style holds.
|
|
18706
|
+
function updateCalloutEndSizeControl(off) {
|
|
18707
|
+
var ids = getTargetIds().length > 0 ? getCalloutEndTargetIds() : [];
|
|
18708
|
+
var noMarker = getTargetIds().length > 0 ? ids.length === 0 :
|
|
18709
|
+
getCalloutEnd(labelModeIsOn() ? getNewLabelStyle(gui) : null) == 'none';
|
|
18710
|
+
var shown = off || noMarker ? {value: '', mixed: false} : getCalloutEndSizeShown(ids);
|
|
18711
|
+
calloutEndSizeInput.setValue(shown.value);
|
|
18712
|
+
calloutEndSizeInput.setPlaceholder(shown.mixed ? MIXED_TEXT : '');
|
|
18713
|
+
calloutEndSizeInput.setDisabled(off || noMarker);
|
|
18714
|
+
}
|
|
18715
|
+
|
|
18716
|
+
function updateButtonGroup(groupEl, btns, selected, off) {
|
|
18717
|
+
groupEl.classed('disabled', off);
|
|
18718
|
+
Object.keys(btns).forEach(function(name) {
|
|
18719
|
+
btns[name].classed('selected', name == selected);
|
|
18720
|
+
setPanelButtonDisabled(btns[name], off);
|
|
18721
|
+
});
|
|
18722
|
+
}
|
|
18723
|
+
|
|
18724
|
+
// The colour a callout is drawn in, which is the text's unless it has its
|
|
18725
|
+
// own -- so a label with red text shows red here, not the black that
|
|
18726
|
+
// getCommonValueInfo() would supply for an unset callout-color.
|
|
18727
|
+
function getCalloutColorShown(ids) {
|
|
18728
|
+
return getShownRecordValues(ids, function(rec) {
|
|
18729
|
+
return rec && (rec[calloutColorField] || rec[fillField]) || defaultLabelColor;
|
|
18730
|
+
});
|
|
18731
|
+
}
|
|
18732
|
+
|
|
18733
|
+
// getCommonValueInfo() reads 0 as unset, and a gap of 0 is a value.
|
|
18734
|
+
function getCalloutGapShown(ids) {
|
|
18735
|
+
return getShownRecordValues(ids, function(rec) {
|
|
18736
|
+
var val = rec ? rec[calloutGapField] : null;
|
|
18737
|
+
return val || val === 0 ? String(val) : '';
|
|
18738
|
+
});
|
|
18739
|
+
}
|
|
18740
|
+
|
|
18741
|
+
// {value, mixed} for a value worked out from each target's record, or from
|
|
18742
|
+
// the new-label style when there is no target.
|
|
18743
|
+
function getShownRecordValues(ids, fn) {
|
|
18744
|
+
var table = getActiveTable();
|
|
18745
|
+
var vals;
|
|
18746
|
+
if (!controlsEnabled()) return {value: '', mixed: false};
|
|
18747
|
+
if (ids.length === 0) {
|
|
18748
|
+
return {value: fn(labelModeIsOn() ? getNewLabelStyle(gui) : null), mixed: false};
|
|
18749
|
+
}
|
|
18750
|
+
vals = ids.map(function(id) {
|
|
18751
|
+
return fn(table && table.getRecordAt(id));
|
|
18752
|
+
});
|
|
18753
|
+
return vals.every(function(val) { return val === vals[0]; }) ?
|
|
18754
|
+
{value: vals[0], mixed: false} : {value: '', mixed: true};
|
|
18755
|
+
}
|
|
18756
|
+
|
|
17855
18757
|
function updateSavedStyleControls() {
|
|
17856
18758
|
presetControl.update();
|
|
17857
18759
|
}
|
|
@@ -18063,15 +18965,44 @@
|
|
|
18063
18965
|
addStyleValue(style, labelAlignField, getSelectedAlignment());
|
|
18064
18966
|
addStyleValue(style, cssField, cssInput.node().value.trim());
|
|
18065
18967
|
addStyleValue(style, 'label-pos', getSelectedLabelPosition());
|
|
18968
|
+
// A style saved without a halo carries no halo-width, so applying it
|
|
18969
|
+
// leaves a halo alone rather than removing one; the same is true of icons.
|
|
18970
|
+
if (getHaloState() == 'on') {
|
|
18971
|
+
addStyleValue(style, haloWidthField, haloWidthInput.getValue());
|
|
18972
|
+
addStyleValue(style, haloColorField, haloColorInput.node().value.trim());
|
|
18973
|
+
addStyleValue(style, haloOpacityField, getOpacityBelowFull(haloOpacityInput));
|
|
18974
|
+
}
|
|
18066
18975
|
addStyleValue(style, iconField, icon);
|
|
18067
18976
|
if (icon) {
|
|
18068
18977
|
addStyleValue(style, iconSizeField, iconSizeInput.getValue());
|
|
18069
18978
|
addStyleValue(style, iconColorField, iconColorInput.node().value.trim());
|
|
18070
18979
|
addStyleValue(style, iconOpacityField, getIconOpacityToWrite());
|
|
18071
18980
|
}
|
|
18981
|
+
// A callout's shape and look, but not its geometry: a corner or an
|
|
18982
|
+
// attachment point is placed for one label's surroundings, and means
|
|
18983
|
+
// nothing on another.
|
|
18984
|
+
if (getCalloutState() == 'on') {
|
|
18985
|
+
addStyleValue(style, calloutField, getSelectedButton(calloutShapeBtns));
|
|
18986
|
+
addStyleValue(style, calloutEndField, getSelectedButton(calloutEndBtns));
|
|
18987
|
+
addStyleValue(style, calloutEndSizeField,
|
|
18988
|
+
getCommonValue(getCalloutEndTargetIds(), calloutEndSizeField));
|
|
18989
|
+
addStyleValue(style, calloutColorField,
|
|
18990
|
+
getCommonValue(getCalloutTargetIds(), calloutColorField));
|
|
18991
|
+
addStyleValue(style, calloutOpacityField, getOpacityBelowFull(calloutOpacityInput));
|
|
18992
|
+
addStyleValue(style, calloutWidthField, calloutWidthInput.getValue());
|
|
18993
|
+
addStyleValue(style, calloutGapField, getCommonValue(getCalloutTargetIds(), calloutGapField));
|
|
18994
|
+
}
|
|
18072
18995
|
return style;
|
|
18073
18996
|
}
|
|
18074
18997
|
|
|
18998
|
+
function getSelectedButton(btns) {
|
|
18999
|
+
var out = '';
|
|
19000
|
+
Object.keys(btns).forEach(function(name) {
|
|
19001
|
+
if (btns[name].hasClass('selected')) out = name;
|
|
19002
|
+
});
|
|
19003
|
+
return out;
|
|
19004
|
+
}
|
|
19005
|
+
|
|
18075
19006
|
// A saved style carries an opacity only if it has one to carry: saving at
|
|
18076
19007
|
// 100% and applying it should not write a property the label did not have.
|
|
18077
19008
|
function getOpacityBelowFull(input) {
|
|
@@ -18274,7 +19205,9 @@
|
|
|
18274
19205
|
|
|
18275
19206
|
function hideColorPicker() {
|
|
18276
19207
|
colorPicker.hide();
|
|
19208
|
+
haloColorPicker.hide();
|
|
18277
19209
|
iconColorPicker.hide();
|
|
19210
|
+
calloutColorPicker.hide();
|
|
18278
19211
|
}
|
|
18279
19212
|
|
|
18280
19213
|
function isFormElement(node) {
|
|
@@ -18339,6 +19272,7 @@
|
|
|
18339
19272
|
if (ids.length < internal.getFeatureCount(lyr)) {
|
|
18340
19273
|
parts.push('ids=' + ids.join(','));
|
|
18341
19274
|
}
|
|
19275
|
+
parts = parts.concat(getRewrapCommands(styles, ids, lyr));
|
|
18342
19276
|
runGuiEditCommand(gui, parts.join(' '), {
|
|
18343
19277
|
title: 'Label styles',
|
|
18344
19278
|
onDone: function() {
|
|
@@ -18349,6 +19283,29 @@
|
|
|
18349
19283
|
});
|
|
18350
19284
|
}
|
|
18351
19285
|
|
|
19286
|
+
// A text block's breaks are only right for the width and font they were
|
|
19287
|
+
// found with, so a change to either rewraps it in the same command: one undo
|
|
19288
|
+
// step, and no moment at which the block is drawn with its old lines in its
|
|
19289
|
+
// new font. One -style per label, since each has text of its own.
|
|
19290
|
+
function getRewrapCommands(styles, ids, lyr) {
|
|
19291
|
+
var table = lyr.data;
|
|
19292
|
+
var changes = {};
|
|
19293
|
+
var out = [];
|
|
19294
|
+
styles.forEach(function(style) { changes[style[0]] = style[1]; });
|
|
19295
|
+
if (!table || !WRAP_FIELDS.some(function(name) { return name in changes; })) {
|
|
19296
|
+
return out;
|
|
19297
|
+
}
|
|
19298
|
+
ids.forEach(function(id) {
|
|
19299
|
+
var rec = table.getRecordAt(id);
|
|
19300
|
+
var value = rec && rec['label-text'];
|
|
19301
|
+
var wrapped;
|
|
19302
|
+
if (!value || everyLabelIsOnAPath([id])) return;
|
|
19303
|
+
wrapped = rewrapLabelValue(value, Object.assign({}, rec, changes));
|
|
19304
|
+
if (wrapped !== value) out.push(getLabelTextCommand(wrapped, id, null));
|
|
19305
|
+
});
|
|
19306
|
+
return out;
|
|
19307
|
+
}
|
|
19308
|
+
|
|
18352
19309
|
// The yellow halo, which belongs to the older label_style mode. The label
|
|
18353
19310
|
// tool draws its selection as an outline instead (gui-label-selection.mjs),
|
|
18354
19311
|
// because a halo says "this text is marked" where an outline says "this is
|
|
@@ -19544,6 +20501,9 @@
|
|
|
19544
20501
|
self.setDefaultTarget([active.layer], active.dataset);
|
|
19545
20502
|
gui.session.setTargetLayer(active.layer); // add -target command to target single layer
|
|
19546
20503
|
}
|
|
20504
|
+
// Before anything hears of the update, so that the map draws a layer that
|
|
20505
|
+
// has just arrived along with the ones already showing.
|
|
20506
|
+
showNewLayers(self.getLayers());
|
|
19547
20507
|
if (flags.select) {
|
|
19548
20508
|
self.dispatchEvent('select', active);
|
|
19549
20509
|
}
|
|
@@ -20140,12 +21100,7 @@
|
|
|
20140
21100
|
var defs = [];
|
|
20141
21101
|
var symbols = lyr.shapes.map(function(shp, i) {
|
|
20142
21102
|
var d = records[i];
|
|
20143
|
-
|
|
20144
|
-
if (!obj) return null;
|
|
20145
|
-
obj.properties.class = addClass(obj.properties.class, 'mapshaper-svg-symbol');
|
|
20146
|
-
obj.properties.transform = getSvgSymbolTransform(shp[0], ext);
|
|
20147
|
-
obj.properties['data-id'] = i;
|
|
20148
|
-
return obj;
|
|
21103
|
+
return shp && d ? renderLayerSymbol(d, shp, ext, view, defs, idPrefix, i) : null;
|
|
20149
21104
|
}).filter(Boolean);
|
|
20150
21105
|
var obj = internal.getEmptyLayerForSVG(lyr, {});
|
|
20151
21106
|
// <defs> has to come first so the layer's own markup can reference it
|
|
@@ -20154,6 +21109,41 @@
|
|
|
20154
21109
|
return internal.svg.stringify(obj);
|
|
20155
21110
|
}
|
|
20156
21111
|
|
|
21112
|
+
function renderLayerSymbol(d, shp, ext, view, defs, idPrefix, i) {
|
|
21113
|
+
var obj = renderSymbol(d, shp, view, defs, idPrefix, i);
|
|
21114
|
+
if (!obj) return null;
|
|
21115
|
+
obj.properties.class = addClass(obj.properties.class, 'mapshaper-svg-symbol');
|
|
21116
|
+
obj.properties.transform = getSvgSymbolTransform(shp[0], ext);
|
|
21117
|
+
obj.properties['data-id'] = i;
|
|
21118
|
+
return obj;
|
|
21119
|
+
}
|
|
21120
|
+
|
|
21121
|
+
// Redraws one anchored label's symbol from @rec, which need not be its record:
|
|
21122
|
+
// a drag previews its edit this way, and the command it commits then redraws
|
|
21123
|
+
// the layer from the data. The node is rendered exactly as renderSymbols()
|
|
21124
|
+
// renders it, so the hit test, the selection cue and the drag's own lookups
|
|
21125
|
+
// find it as they found the one it replaces.
|
|
21126
|
+
//
|
|
21127
|
+
// Anchored labels only: a path label's baseline lives in the layer's <defs>,
|
|
21128
|
+
// which this leaves alone.
|
|
21129
|
+
// Returns the new node, or null if there was nothing to replace.
|
|
21130
|
+
function replaceAnchoredSymbol(container, id, rec, shp, ext) {
|
|
21131
|
+
var old = container && shp && !internal.svg.shapeIsPathLabel(shp, rec) ?
|
|
21132
|
+
container.querySelector('.mapshaper-svg-symbol[data-id="' + id + '"]') : null;
|
|
21133
|
+
var obj = old ? renderLayerSymbol(rec, shp, ext, null, null, '', id) : null;
|
|
21134
|
+
var tmp, node;
|
|
21135
|
+
if (!obj) return null;
|
|
21136
|
+
tmp = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
21137
|
+
tmp.innerHTML = internal.svg.stringify(obj);
|
|
21138
|
+
node = tmp.firstElementChild;
|
|
21139
|
+
if (!node) return null;
|
|
21140
|
+
// Off-screen symbols are hidden by repositionSymbols(), which would not know
|
|
21141
|
+
// to hide this one again until the map moved.
|
|
21142
|
+
if (old.hasAttribute('display')) node.setAttribute('display', old.getAttribute('display'));
|
|
21143
|
+
old.parentNode.replaceChild(node, old);
|
|
21144
|
+
return node;
|
|
21145
|
+
}
|
|
21146
|
+
|
|
20157
21147
|
// Markup for a label that is not in any layer yet: the one being typed into
|
|
20158
21148
|
// before it has been created.
|
|
20159
21149
|
//
|
|
@@ -20690,7 +21680,9 @@
|
|
|
20690
21680
|
// What the click found in the selection as it stood before the click, which
|
|
20691
21681
|
// the click itself is about to replace. The label tool reads this to tell a
|
|
20692
21682
|
// click that selects a label from a click on the label that was already the
|
|
20693
|
-
// whole selection -- its gesture for editing that label's text
|
|
21683
|
+
// whole selection -- its gesture for editing that label's text -- and a click
|
|
21684
|
+
// off a selection from one on empty map, since a click on an unlabelled
|
|
21685
|
+
// feature selects it before the tool hears of the click.
|
|
20694
21686
|
//
|
|
20695
21687
|
// An additive click never qualifies: shift-click means toggle, and it would
|
|
20696
21688
|
// otherwise open a text session on the label it was removing.
|
|
@@ -20701,7 +21693,8 @@
|
|
|
20701
21693
|
var id = hitData.ids.length > 0 ? hitData.ids[0] : -1;
|
|
20702
21694
|
return {
|
|
20703
21695
|
clicked_only_selection: id > -1 && !eventUsesAdditiveSelection(e) &&
|
|
20704
|
-
selectionIds.length == 1 && selectionIds[0] === id
|
|
21696
|
+
selectionIds.length == 1 && selectionIds[0] === id,
|
|
21697
|
+
had_selection: selectionIds.length > 0
|
|
20705
21698
|
};
|
|
20706
21699
|
}
|
|
20707
21700
|
|
|
@@ -24165,6 +25158,222 @@
|
|
|
24165
25158
|
return len > 0 ? geom.distance2D(x1, y1, p[0], p[1]) / len : 0;
|
|
24166
25159
|
}
|
|
24167
25160
|
|
|
25161
|
+
// The handles on a selected anchored label, and what dragging each one
|
|
25162
|
+
// writes, as pure functions of the label's record and the pointer.
|
|
25163
|
+
//
|
|
25164
|
+
// - width: on a text block only, at the bottom corner of its column on the
|
|
25165
|
+
// side that is free to move -- the right for text that starts at its
|
|
25166
|
+
// anchor, the left for text that ends there, the right for centred text,
|
|
25167
|
+
// which grows both ways. Dragging it sets label-width. Point text has none:
|
|
25168
|
+
// a label stays the kind it was made as, and a handle that turned point
|
|
25169
|
+
// text into a block was a way to do that by accident.
|
|
25170
|
+
// - via: the elbow's corner, or the point the curve passes through.
|
|
25171
|
+
// - attach: where the line meets the text, which slides around the padded box.
|
|
25172
|
+
// - gap: where the line stops short of the anchor.
|
|
25173
|
+
//
|
|
25174
|
+
// A corner rather than the middle of the free side for the width handle,
|
|
25175
|
+
// because the middle of a side is where an elbow meets a one-line label, and
|
|
25176
|
+
// the two handles would sit on top of each other.
|
|
25177
|
+
//
|
|
25178
|
+
// Everything is in the label's own space -- origin at the anchor, y down, px
|
|
25179
|
+
// -- which is the space the callout is worked out in. See
|
|
25180
|
+
// docs/development/text-annotation-design.md.
|
|
25181
|
+
|
|
25182
|
+
// The narrowest a text block can be dragged to, px. Narrower than a character
|
|
25183
|
+
// or two, and every character gets a line of its own.
|
|
25184
|
+
var MIN_LABEL_WIDTH = 10;
|
|
25185
|
+
|
|
25186
|
+
// How far out from the anchor the line has to stop before its gap handle is
|
|
25187
|
+
// drawn, in screen px. Nearer than that the handle sits on the anchor's own,
|
|
25188
|
+
// and the anchor is the one that matters more.
|
|
25189
|
+
var MIN_GAP_HANDLE = 6;
|
|
25190
|
+
|
|
25191
|
+
// rec: the label's record
|
|
25192
|
+
// textBox: the rendered text's bbox, {x, y, width, height}, or null
|
|
25193
|
+
// opts:
|
|
25194
|
+
// symbolRadius: radius of the symbol at the anchor, which the default gap
|
|
25195
|
+
// clears
|
|
25196
|
+
// scale: screen px per label px
|
|
25197
|
+
// padding: how far outside the text the selection box is drawn
|
|
25198
|
+
// Returns {handles: [{kind, point}], column}, where column is the x-range
|
|
25199
|
+
// [xmin, xmax] a text block wraps within, or null for point text. The
|
|
25200
|
+
// width handle sits on the column's corner, not the text's.
|
|
25201
|
+
function getAnchoredLabelHandles(rec, textBox, opts) {
|
|
25202
|
+
var o = opts || {};
|
|
25203
|
+
var handles = [];
|
|
25204
|
+
var column = getLabelColumn(rec);
|
|
25205
|
+
var shape = internal.svg.getLabelCalloutShape(rec, o.symbolRadius || 0);
|
|
25206
|
+
var width = textBox && column ?
|
|
25207
|
+
getWidthHandlePoint(rec, textBox, column, o.padding || 0) : null;
|
|
25208
|
+
// Nearer-first matters only for ties, which go to the earlier handle: the
|
|
25209
|
+
// callout's own handles are smaller and sit on the line, so they win.
|
|
25210
|
+
if (shape) {
|
|
25211
|
+
if (shape.via) handles.push({kind: 'via', point: shape.via});
|
|
25212
|
+
handles.push({kind: 'attach', point: shape.attach});
|
|
25213
|
+
if (length(shape.tip) * (o.scale || 1) >= MIN_GAP_HANDLE) {
|
|
25214
|
+
handles.push({kind: 'gap', point: shape.tip});
|
|
25215
|
+
}
|
|
25216
|
+
}
|
|
25217
|
+
if (width) handles.push({kind: 'width', point: width});
|
|
25218
|
+
return {handles: handles, column: column};
|
|
25219
|
+
}
|
|
25220
|
+
|
|
25221
|
+
// The x-range a text block's lines wrap within, from where its drawn anchor
|
|
25222
|
+
// puts it, or null for a label with no width.
|
|
25223
|
+
function getLabelColumn(rec) {
|
|
25224
|
+
var w = Number(rec && rec['label-width']);
|
|
25225
|
+
var drawn, anchor;
|
|
25226
|
+
if (!(w > 0 && isFinite(w))) return null;
|
|
25227
|
+
drawn = internal.svg.getDrawnLabelOffset(rec);
|
|
25228
|
+
anchor = getDrawnAnchor(drawn);
|
|
25229
|
+
if (anchor == 'end') return [drawn.dx - w, drawn.dx];
|
|
25230
|
+
if (anchor == 'middle') return [drawn.dx - w / 2, drawn.dx + w / 2];
|
|
25231
|
+
return [drawn.dx, drawn.dx + w];
|
|
25232
|
+
}
|
|
25233
|
+
|
|
25234
|
+
// An anchor the record does not set is inherited from the layer's group,
|
|
25235
|
+
// which is 'middle' -- see getLabelTextDefaults().
|
|
25236
|
+
function getDrawnAnchor(drawn) {
|
|
25237
|
+
return drawn && drawn['text-anchor'] || 'middle';
|
|
25238
|
+
}
|
|
25239
|
+
|
|
25240
|
+
function getWidthHandlePoint(rec, box, column, pad) {
|
|
25241
|
+
var anchor = getDrawnAnchor(internal.svg.getDrawnLabelOffset(rec));
|
|
25242
|
+
var xmin = Math.min(column[0], box.x);
|
|
25243
|
+
var xmax = Math.max(column[1], box.x + box.width);
|
|
25244
|
+
var y = box.y + box.height + pad;
|
|
25245
|
+
return anchor == 'end' ? [xmin - pad, y] : [xmax + pad, y];
|
|
25246
|
+
}
|
|
25247
|
+
|
|
25248
|
+
// The label-width a drag on the width handle sets, in whole px.
|
|
25249
|
+
// anchor: the drawn text-anchor; dx: the drawn dx, which is the edge (or for
|
|
25250
|
+
// centred text the middle) that stays put
|
|
25251
|
+
// x: where the handle has been dragged to
|
|
25252
|
+
// pad: how far outside the text the handle is drawn
|
|
25253
|
+
function getDraggedWidth(anchor, dx, x, pad) {
|
|
25254
|
+
var w;
|
|
25255
|
+
if (anchor == 'end') w = dx - x - pad;
|
|
25256
|
+
else if (anchor == 'middle') w = 2 * (Math.abs(x - dx) - pad);
|
|
25257
|
+
else w = x - pad - dx;
|
|
25258
|
+
return Math.max(MIN_LABEL_WIDTH, Math.round(w));
|
|
25259
|
+
}
|
|
25260
|
+
|
|
25261
|
+
// Where a dragged via point lands: on a line level with, or plumb with, the
|
|
25262
|
+
// anchor or the attachment point @t when it comes within @tol of one. Those
|
|
25263
|
+
// are the alignments that make an elbow's legs horizontal or vertical.
|
|
25264
|
+
function snapCalloutVia(p, t, tol) {
|
|
25265
|
+
return [snapTo(p[0], [0, t[0]], tol), snapTo(p[1], [0, t[1]], tol)];
|
|
25266
|
+
}
|
|
25267
|
+
|
|
25268
|
+
// Where on the padded @box a dragged attachment point lands, as the
|
|
25269
|
+
// [fx, fy] fractions callout-attach stores: on the edge nearest @p, and at the
|
|
25270
|
+
// middle of that edge or at a corner when within @tol of one. A side also
|
|
25271
|
+
// snaps to the first line's midline, where an automatic elbow meets it.
|
|
25272
|
+
function getAttachFraction(box, p, tol) {
|
|
25273
|
+
var w = box.xmax - box.xmin;
|
|
25274
|
+
var h = box.ymax - box.ymin;
|
|
25275
|
+
var x = clamp$3(p[0], box.xmin, box.xmax);
|
|
25276
|
+
var y = clamp$3(p[1], box.ymin, box.ymax);
|
|
25277
|
+
var edge = getNearestEdge(box, x, y);
|
|
25278
|
+
var fx, fy, midline;
|
|
25279
|
+
if (edge == 'left' || edge == 'right') {
|
|
25280
|
+
fx = edge == 'left' ? 0 : 1;
|
|
25281
|
+
midline = h > 0 && box.midline > box.ymin && box.midline < box.ymax ?
|
|
25282
|
+
(box.midline - box.ymin) / h : 0.5;
|
|
25283
|
+
fy = h > 0 ? snapTo((y - box.ymin) / h, [0, midline, 0.5, 1], tol / h) : 0.5;
|
|
25284
|
+
} else {
|
|
25285
|
+
fy = edge == 'top' ? 0 : 1;
|
|
25286
|
+
fx = w > 0 ? snapTo((x - box.xmin) / w, [0, 0.5, 1], tol / w) : 0.5;
|
|
25287
|
+
}
|
|
25288
|
+
return [roundTo(fx, 1000), roundTo(fy, 1000)];
|
|
25289
|
+
}
|
|
25290
|
+
|
|
25291
|
+
function getNearestEdge(box, x, y) {
|
|
25292
|
+
var d = {
|
|
25293
|
+
left: x - box.xmin,
|
|
25294
|
+
right: box.xmax - x,
|
|
25295
|
+
top: y - box.ymin,
|
|
25296
|
+
bottom: box.ymax - y
|
|
25297
|
+
};
|
|
25298
|
+
return Object.keys(d).reduce(function(memo, key) {
|
|
25299
|
+
return d[key] < d[memo] ? key : memo;
|
|
25300
|
+
}, 'left');
|
|
25301
|
+
}
|
|
25302
|
+
|
|
25303
|
+
// The callout-gap a drag on the gap handle sets: how far from the anchor the
|
|
25304
|
+
// line now stops, px.
|
|
25305
|
+
function getDraggedGap(p) {
|
|
25306
|
+
return roundTo(length(p), 10);
|
|
25307
|
+
}
|
|
25308
|
+
|
|
25309
|
+
// Where a callout's via point goes when its text moves, which takes the
|
|
25310
|
+
// attachment point from @t0 to @t1.
|
|
25311
|
+
//
|
|
25312
|
+
// A curve keeps its shape: the via point is rotated and scaled about the
|
|
25313
|
+
// anchor by whatever takes @t0 to @t1. An elbow keeps its corner in
|
|
25314
|
+
// proportion across the span from anchor to text, and a leg that was level
|
|
25315
|
+
// with the text (or plumb with it) stays so, since that is almost always what
|
|
25316
|
+
// a corner was put there for.
|
|
25317
|
+
//
|
|
25318
|
+
// type: 'elbow' or 'curve'
|
|
25319
|
+
function followCalloutVia(type, via, t0, t1) {
|
|
25320
|
+
var d2, re, im;
|
|
25321
|
+
if (type == 'curve') {
|
|
25322
|
+
d2 = t0[0] * t0[0] + t0[1] * t0[1];
|
|
25323
|
+
if (!(d2 > 0)) return via.slice();
|
|
25324
|
+
re = (t1[0] * t0[0] + t1[1] * t0[1]) / d2;
|
|
25325
|
+
im = (t1[1] * t0[0] - t1[0] * t0[1]) / d2;
|
|
25326
|
+
return [via[0] * re - via[1] * im, via[0] * im + via[1] * re];
|
|
25327
|
+
}
|
|
25328
|
+
return [followElbowCoord(via[0], t0[0], t1[0]), followElbowCoord(via[1], t0[1], t1[1])];
|
|
25329
|
+
}
|
|
25330
|
+
|
|
25331
|
+
// Close enough to count as level, in px: the values are stored in tenths.
|
|
25332
|
+
var LEVEL_TOLERANCE = 0.05;
|
|
25333
|
+
|
|
25334
|
+
function followElbowCoord(v, t0, t1) {
|
|
25335
|
+
if (Math.abs(v - t0) < LEVEL_TOLERANCE) return t1;
|
|
25336
|
+
// Scaling by a span of almost nothing throws the corner miles away, so a
|
|
25337
|
+
// text that started nearly in line with its anchor moves the corner with it
|
|
25338
|
+
// instead.
|
|
25339
|
+
if (Math.abs(t0) < 1) return v + t1 - t0;
|
|
25340
|
+
return v * t1 / t0;
|
|
25341
|
+
}
|
|
25342
|
+
|
|
25343
|
+
// 'x,y', as callout-via and callout-attach store a pair
|
|
25344
|
+
function formatPointPair(p) {
|
|
25345
|
+
return formatNumber$2(p[0]) + ',' + formatNumber$2(p[1]);
|
|
25346
|
+
}
|
|
25347
|
+
|
|
25348
|
+
function formatNumber$2(n) {
|
|
25349
|
+
var val = roundTo(n, 10);
|
|
25350
|
+
return String(val === 0 ? 0 : val); // no "-0"
|
|
25351
|
+
}
|
|
25352
|
+
|
|
25353
|
+
function snapTo(val, targets, tol) {
|
|
25354
|
+
var best = val, bestDist = tol;
|
|
25355
|
+
targets.forEach(function(t) {
|
|
25356
|
+
var d = Math.abs(val - t);
|
|
25357
|
+
if (d <= bestDist) {
|
|
25358
|
+
bestDist = d;
|
|
25359
|
+
best = t;
|
|
25360
|
+
}
|
|
25361
|
+
});
|
|
25362
|
+
return best;
|
|
25363
|
+
}
|
|
25364
|
+
|
|
25365
|
+
function clamp$3(val, min, max) {
|
|
25366
|
+
return val < min ? min : val > max ? max : val;
|
|
25367
|
+
}
|
|
25368
|
+
|
|
25369
|
+
function length(p) {
|
|
25370
|
+
return Math.sqrt(p[0] * p[0] + p[1] * p[1]);
|
|
25371
|
+
}
|
|
25372
|
+
|
|
25373
|
+
function roundTo(n, k) {
|
|
25374
|
+
return Math.round(n * k) / k;
|
|
25375
|
+
}
|
|
25376
|
+
|
|
24168
25377
|
// Sliding a path label's text along its curve, and flipping it across.
|
|
24169
25378
|
//
|
|
24170
25379
|
// Both are one gesture -- a drag on the glyphs, Illustrator's model -- and both
|
|
@@ -24757,6 +25966,7 @@
|
|
|
24757
25966
|
created: false,
|
|
24758
25967
|
onClose: opts && opts.onClose || null,
|
|
24759
25968
|
startText: decodeLabelText(rec['label-text']),
|
|
25969
|
+
startBreaks: readSoftBreaks(rec['label-text']).breaks,
|
|
24760
25970
|
nodes: null
|
|
24761
25971
|
};
|
|
24762
25972
|
startSession();
|
|
@@ -24801,6 +26011,7 @@
|
|
|
24801
26011
|
created: true,
|
|
24802
26012
|
onClose: opts.onClose || null,
|
|
24803
26013
|
startText: '',
|
|
26014
|
+
startBreaks: [],
|
|
24804
26015
|
nodes: null
|
|
24805
26016
|
};
|
|
24806
26017
|
startSession();
|
|
@@ -24938,10 +26149,15 @@
|
|
|
24938
26149
|
// there is no session to abandon -- it was created before this one, perhaps
|
|
24939
26150
|
// by the script the file was built by.
|
|
24940
26151
|
// - An existing label whose text changed has that text **saved**.
|
|
26152
|
+
//
|
|
26153
|
+
// A text block is saved with the soft breaks it was last drawn with, and is
|
|
26154
|
+
// saved even when the typed text did not change if those breaks did: a font
|
|
26155
|
+
// installed since, or a width set from the console, moves them.
|
|
24941
26156
|
function commit(o) {
|
|
24942
26157
|
var blank = textHasNoGlyphs(o.text);
|
|
26158
|
+
var breaks = getBreaks(o);
|
|
24943
26159
|
if (o.pending) {
|
|
24944
|
-
if (!blank) o.pending.create(o.text);
|
|
26160
|
+
if (!blank) o.pending.create(insertSoftBreaks(o.text, breaks));
|
|
24945
26161
|
return;
|
|
24946
26162
|
}
|
|
24947
26163
|
if (blank) {
|
|
@@ -24949,12 +26165,23 @@
|
|
|
24949
26165
|
removeLabel(o);
|
|
24950
26166
|
return;
|
|
24951
26167
|
}
|
|
24952
|
-
if (o.text === o.startText) return;
|
|
24953
|
-
runGuiEditCommand(gui, getLabelTextCommand(o.text,
|
|
26168
|
+
if (o.text === o.startText && sameSoftBreaks(breaks, o.startBreaks)) return;
|
|
26169
|
+
runGuiEditCommand(gui, getLabelTextCommand(insertSoftBreaks(o.text, breaks),
|
|
26170
|
+
o.id, o.target.name), {
|
|
24954
26171
|
title: 'Label text'
|
|
24955
26172
|
});
|
|
24956
26173
|
}
|
|
24957
26174
|
|
|
26175
|
+
// Where the session's text wraps, as offsets into it. Only an anchored label
|
|
26176
|
+
// wraps: a path label's text runs along its curve on one line.
|
|
26177
|
+
function getBreaks(o) {
|
|
26178
|
+
var shp = o.pending ? o.pending.coords : o.target.shapes && o.target.shapes[o.id];
|
|
26179
|
+
var rec;
|
|
26180
|
+
if (!shp || shp.length > 1) return [];
|
|
26181
|
+
rec = o.pending ? o.pending.getStyle() : getRecord(o.target, o.id);
|
|
26182
|
+
return getSoftBreaks(o.text, rec);
|
|
26183
|
+
}
|
|
26184
|
+
|
|
24958
26185
|
// Deletes the label's feature, geometry and all.
|
|
24959
26186
|
//
|
|
24960
26187
|
// -filter is the command for this: dropping a feature is not a label
|
|
@@ -25037,7 +26264,9 @@
|
|
|
25037
26264
|
// when there is no text, exactly as it does for a committed empty label.
|
|
25038
26265
|
function getPendingRecord(o) {
|
|
25039
26266
|
var rec = Object.assign({}, o.pending.getStyle());
|
|
25040
|
-
|
|
26267
|
+
// With its soft breaks, so that what is aligned and what a callout meets
|
|
26268
|
+
// is the block as wrapped
|
|
26269
|
+
rec['label-text'] = encodeLabelText(insertSoftBreaks(o.text, getBreaks(o)));
|
|
25041
26270
|
// No need to expand label-pos here, or to measure its text: the renderer
|
|
25042
26271
|
// resolves the position and asks for the width it needs, so a pending
|
|
25043
26272
|
// label is laid out by exactly the same code as a committed one.
|
|
@@ -25087,12 +26316,14 @@
|
|
|
25087
26316
|
// getRenderedLines() gives it one.
|
|
25088
26317
|
function writeText(o) {
|
|
25089
26318
|
var content = o.nodes.content;
|
|
26319
|
+
var breaks = getBreaks(o);
|
|
26320
|
+
var written = o.text + '\u0000' + breaks.join(',');
|
|
25090
26321
|
var lines, i, tspan;
|
|
25091
26322
|
// refresh() runs on every map render, which during a pan is every frame;
|
|
25092
26323
|
// rebuilding text nodes that already say the right thing is pure waste
|
|
25093
|
-
if (o.writtenTo === content && o.writtenText ===
|
|
26324
|
+
if (o.writtenTo === content && o.writtenText === written) return;
|
|
25094
26325
|
o.writtenTo = content;
|
|
25095
|
-
o.writtenText =
|
|
26326
|
+
o.writtenText = written;
|
|
25096
26327
|
o.nodes.text.setAttributeNS(XML_NS, 'space', 'preserve');
|
|
25097
26328
|
while (content.firstChild) content.removeChild(content.firstChild);
|
|
25098
26329
|
// A <textPath> runs along its baseline, so a second line would advance
|
|
@@ -25104,7 +26335,10 @@
|
|
|
25104
26335
|
content.appendChild(document.createTextNode(getRenderedContent(o.text)));
|
|
25105
26336
|
return;
|
|
25106
26337
|
}
|
|
25107
|
-
|
|
26338
|
+
// A soft break keeps the space it broke at at the end of its line, where
|
|
26339
|
+
// the rendered label drops it: here every typed character has to be one
|
|
26340
|
+
// the caret can sit beside.
|
|
26341
|
+
lines = getRenderedLines(o.text, breaks);
|
|
25108
26342
|
content.appendChild(document.createTextNode(lines[0]));
|
|
25109
26343
|
for (i = 1; i < lines.length; i++) {
|
|
25110
26344
|
tspan = document.createElementNS(SVG_NS$3, 'tspan');
|
|
@@ -25190,6 +26424,7 @@
|
|
|
25190
26424
|
if (pathId) {
|
|
25191
26425
|
groups.back.appendChild(ghostPath(pathId));
|
|
25192
26426
|
} else if (box) {
|
|
26427
|
+
appendColumn(o, groups.back, box);
|
|
25193
26428
|
groups.back.appendChild(rect(box, 'label-edit-box'));
|
|
25194
26429
|
}
|
|
25195
26430
|
getSelectionRects(provider, bands[0], bands[1], rendered).forEach(function(r) {
|
|
@@ -25200,6 +26435,22 @@
|
|
|
25200
26435
|
syncTransform(o, groups);
|
|
25201
26436
|
}
|
|
25202
26437
|
|
|
26438
|
+
// A text block's column -- the width it wraps to -- as a faint dashed box
|
|
26439
|
+
// behind the box around the text, which is usually narrower. Without it a
|
|
26440
|
+
// text block just placed looks like any other label: a caret in a box the
|
|
26441
|
+
// size of nothing.
|
|
26442
|
+
function appendColumn(o, g, box) {
|
|
26443
|
+
var rec = o.pending ? getPendingRecord(o) : getRecord(o.target, o.id);
|
|
26444
|
+
var column = rec ? getLabelColumn(rec) : null;
|
|
26445
|
+
if (!column) return;
|
|
26446
|
+
g.appendChild(rect({
|
|
26447
|
+
x: column[0] - BOX_PADDING$1,
|
|
26448
|
+
y: box.y,
|
|
26449
|
+
width: column[1] - column[0] + BOX_PADDING$1 * 2,
|
|
26450
|
+
height: box.height
|
|
26451
|
+
}, 'label-edit-column'));
|
|
26452
|
+
}
|
|
26453
|
+
|
|
25203
26454
|
// Keeps clicks near the label inside the session instead of ending it.
|
|
25204
26455
|
//
|
|
25205
26456
|
// Glyph-precise hit testing is not enough for this: a curved label's bounding
|
|
@@ -25503,6 +26754,8 @@
|
|
|
25503
26754
|
var BOX_PADDING = 3;
|
|
25504
26755
|
var ANCHOR_RADIUS = 3.5;
|
|
25505
26756
|
var KNOT_RADIUS = 3;
|
|
26757
|
+
var WIDTH_HANDLE_SIZE = 6;
|
|
26758
|
+
var GAP_HANDLE_RADIUS = 2.5;
|
|
25506
26759
|
|
|
25507
26760
|
// How many labels get an outline before the cue falls back to the halo.
|
|
25508
26761
|
//
|
|
@@ -25522,7 +26775,12 @@
|
|
|
25522
26775
|
|
|
25523
26776
|
// getEditingId: returns the feature id of an open text editing session, or -1.
|
|
25524
26777
|
// A label being typed into draws its own box and does not want a second one.
|
|
25525
|
-
|
|
26778
|
+
// getHandles: (optional) function(target, id, textBox) returning
|
|
26779
|
+
// {handles, column} for a selected anchored label, as from
|
|
26780
|
+
// getAnchoredLabelHandles(), or null. The tool decides when a label has
|
|
26781
|
+
// handles, and gives a text block's column from what is on screen, which
|
|
26782
|
+
// during a drag is not the data.
|
|
26783
|
+
function LabelSelection(gui, ext, hit, getEditingId, getHandles) {
|
|
25526
26784
|
var self = {};
|
|
25527
26785
|
var groups = []; // one <g> per drawn cue, in the layer's markup
|
|
25528
26786
|
var marked = []; // text nodes wearing the halo, when there are too many to outline
|
|
@@ -25617,7 +26875,7 @@
|
|
|
25617
26875
|
g.appendChild(curve(nodes.pathId));
|
|
25618
26876
|
if (withHandles) appendKnotHandles(g, target, id);
|
|
25619
26877
|
} else {
|
|
25620
|
-
appendAnchoredCue(g, nodes, rec, id);
|
|
26878
|
+
appendAnchoredCue(g, nodes, rec, id, withHandles ? target : null);
|
|
25621
26879
|
}
|
|
25622
26880
|
groups.push(g);
|
|
25623
26881
|
}
|
|
@@ -25633,14 +26891,76 @@
|
|
|
25633
26891
|
// ring inside it would. What is left -- offset text with nothing at its
|
|
25634
26892
|
// anchor -- is the case where the ring is the only thing that says what the
|
|
25635
26893
|
// text hangs off.
|
|
25636
|
-
|
|
26894
|
+
//
|
|
26895
|
+
// A selected label's handles go in a group of their own, after the symbol
|
|
26896
|
+
// rather than before it: they sit on the callout and on the edge of the box,
|
|
26897
|
+
// and painted beneath the line they would be hidden by it.
|
|
26898
|
+
//
|
|
26899
|
+
// @handleTarget: the layer, when the label is selected and so may have
|
|
26900
|
+
// handles, or null
|
|
26901
|
+
//
|
|
26902
|
+
// A selected text block also shows its column, the width it wraps to, as a
|
|
26903
|
+
// fainter dashed box behind the solid one. The solid box is the label itself
|
|
26904
|
+
// -- the wrapped text, which is what a callout meets -- and is usually
|
|
26905
|
+
// narrower than its column; the column is what the width handle drags.
|
|
26906
|
+
function appendAnchoredCue(g, nodes, rec, id, handleTarget) {
|
|
25637
26907
|
var box = measure(nodes.content);
|
|
26908
|
+
var o = box && handleTarget && getHandles ? getHandles(handleTarget, id, box) : null;
|
|
26909
|
+
var column = o ? o.column : null;
|
|
25638
26910
|
if (!box) return;
|
|
26911
|
+
if (column) g.appendChild(columnRect(box, column, BOX_PADDING));
|
|
25639
26912
|
if (box.width || box.height) g.appendChild(rect(box, BOX_PADDING));
|
|
25640
26913
|
if (id === tetherId) g.appendChild(tether(box));
|
|
25641
26914
|
if (!internal.featureHasSvgSymbol(rec) && !boxHoldsOrigin(box)) {
|
|
25642
26915
|
g.appendChild(anchorMarker());
|
|
25643
26916
|
}
|
|
26917
|
+
if (o && o.handles.length > 0) appendHandles(nodes, o.handles);
|
|
26918
|
+
}
|
|
26919
|
+
|
|
26920
|
+
// The column's own extent across, and the text's up and down: a column has
|
|
26921
|
+
// no height of its own.
|
|
26922
|
+
function columnRect(box, column, pad) {
|
|
26923
|
+
var el = rect({x: column[0], y: box.y, width: column[1] - column[0],
|
|
26924
|
+
height: box.height}, pad);
|
|
26925
|
+
el.setAttribute('class', 'label-cue-column');
|
|
26926
|
+
return el;
|
|
26927
|
+
}
|
|
26928
|
+
|
|
26929
|
+
function appendHandles(nodes, handles) {
|
|
26930
|
+
var g = document.createElementNS(SVG_NS$2, 'g');
|
|
26931
|
+
var transform = nodes.symbol.getAttribute('transform');
|
|
26932
|
+
var display = nodes.symbol.getAttribute('display');
|
|
26933
|
+
g.setAttribute('class', 'label-cue label-cue-handles');
|
|
26934
|
+
if (transform) g.setAttribute('transform', transform);
|
|
26935
|
+
if (display) g.setAttribute('display', display);
|
|
26936
|
+
handles.forEach(function(h) {
|
|
26937
|
+
g.appendChild(labelHandle(h));
|
|
26938
|
+
});
|
|
26939
|
+
nodes.symbol.parentNode.insertBefore(g, nodes.symbol.nextSibling);
|
|
26940
|
+
groups.push(g);
|
|
26941
|
+
}
|
|
26942
|
+
|
|
26943
|
+
// A square for the width handle, which is a corner of the box, and a ring
|
|
26944
|
+
// for the callout's, which are points on its line -- filled for the end
|
|
26945
|
+
// that meets the text, so that the two ends of the line read differently.
|
|
26946
|
+
function labelHandle(h) {
|
|
26947
|
+
var p = h.point;
|
|
26948
|
+
var el;
|
|
26949
|
+
if (h.kind == 'width') {
|
|
26950
|
+
el = document.createElementNS(SVG_NS$2, 'rect');
|
|
26951
|
+
el.setAttribute('x', p[0] - WIDTH_HANDLE_SIZE / 2);
|
|
26952
|
+
el.setAttribute('y', p[1] - WIDTH_HANDLE_SIZE / 2);
|
|
26953
|
+
el.setAttribute('width', WIDTH_HANDLE_SIZE);
|
|
26954
|
+
el.setAttribute('height', WIDTH_HANDLE_SIZE);
|
|
26955
|
+
} else {
|
|
26956
|
+
el = document.createElementNS(SVG_NS$2, 'circle');
|
|
26957
|
+
el.setAttribute('cx', p[0]);
|
|
26958
|
+
el.setAttribute('cy', p[1]);
|
|
26959
|
+
el.setAttribute('r', h.kind == 'gap' ? GAP_HANDLE_RADIUS : KNOT_RADIUS);
|
|
26960
|
+
}
|
|
26961
|
+
el.setAttribute('class', 'label-cue-handle label-cue-' + h.kind);
|
|
26962
|
+
el.setAttribute('data-handle', h.kind);
|
|
26963
|
+
return el;
|
|
25644
26964
|
}
|
|
25645
26965
|
|
|
25646
26966
|
// Whether the label's anchor point is inside the box drawn around its text.
|
|
@@ -26107,8 +27427,17 @@
|
|
|
26107
27427
|
// resolve and still leaves a short polyline to walk per mouse move.
|
|
26108
27428
|
var FLATTEN_PX = 0.5;
|
|
26109
27429
|
|
|
27430
|
+
// How near a dragged handle has to come to an alignment to snap to it, px on
|
|
27431
|
+
// screen
|
|
27432
|
+
var SNAP_PX = 5;
|
|
27433
|
+
|
|
27434
|
+
// The width a text block placed with a click wraps to, px, when the panel does
|
|
27435
|
+
// not set one
|
|
27436
|
+
var DEFAULT_BLOCK_WIDTH = 160;
|
|
27437
|
+
|
|
26110
27438
|
function initLabelTool(gui, ext, hit) {
|
|
26111
|
-
// which kind of label a click creates: null, 'anchor' or 'path'
|
|
27439
|
+
// which kind of label a click creates: null, 'anchor', 'block' or 'path'.
|
|
27440
|
+
// A block is an anchored label with a wrap width.
|
|
26112
27441
|
var armed = null;
|
|
26113
27442
|
var curve = createCurveState();
|
|
26114
27443
|
// The pointer's position while a curve is being drawn, in display
|
|
@@ -26131,11 +27460,27 @@
|
|
|
26131
27460
|
// coordinates. A drag on a label's text starts from here rather than from
|
|
26132
27461
|
// the dragstart event -- see beginTextDrag().
|
|
26133
27462
|
var hoverPoint = null;
|
|
27463
|
+
// A drag on one of a selected anchored label's own handles -- its width or
|
|
27464
|
+
// its callout -- or null. See gui-label-handles.mjs.
|
|
27465
|
+
var handleDrag = null;
|
|
27466
|
+
// The handles last drawn, {id, handles}, which is what the pointer is tested
|
|
27467
|
+
// against: they are worked out when the cue is drawn rather than on every
|
|
27468
|
+
// mouse move, since placing them measures the label.
|
|
27469
|
+
var shownHandles = null;
|
|
27470
|
+
// A drag on the map that is setting a new text block's width, or null
|
|
27471
|
+
var blockDrag = null;
|
|
27472
|
+
// A drag that has been released and whose command has not redrawn the map
|
|
27473
|
+
// yet, or null. Its preview is still what is on screen, so the cue goes on
|
|
27474
|
+
// being drawn from it rather than from the data, which has not changed yet.
|
|
27475
|
+
var releasedDrag = null;
|
|
27476
|
+
// The mouseup that ended the last block drag, or null. A short one is
|
|
27477
|
+
// followed by a click from the same mouseup, which it has already handled.
|
|
27478
|
+
var blockRelease = null;
|
|
26134
27479
|
var editor = new LabelEditor(gui, ext);
|
|
26135
27480
|
var selection = new LabelSelection(gui, ext, hit, function() {
|
|
26136
27481
|
return editor.isOpen() ? editor.getFeatureId() : -1;
|
|
26137
|
-
});
|
|
26138
|
-
var toolbar, anchorBtn, pathBtn, alert;
|
|
27482
|
+
}, getLabelHandles);
|
|
27483
|
+
var toolbar, anchorBtn, blockBtn, pathBtn, alert;
|
|
26139
27484
|
|
|
26140
27485
|
gui.addMode('label_tool', turnOn, turnOff);
|
|
26141
27486
|
|
|
@@ -26207,6 +27552,11 @@
|
|
|
26207
27552
|
hideInstructions();
|
|
26208
27553
|
drag = null;
|
|
26209
27554
|
textDrag = null;
|
|
27555
|
+
handleDrag = null;
|
|
27556
|
+
releasedDrag = null;
|
|
27557
|
+
blockRelease = null;
|
|
27558
|
+
shownHandles = null;
|
|
27559
|
+
clearBlockDrag();
|
|
26210
27560
|
hoverHandle = null;
|
|
26211
27561
|
hoverTextId = -1;
|
|
26212
27562
|
hoverPoint = null;
|
|
@@ -26214,6 +27564,8 @@
|
|
|
26214
27564
|
// go back whether or not it was a crosshair.
|
|
26215
27565
|
gui.container.findChild('.map-layers').classed('label-tool', false);
|
|
26216
27566
|
gui.container.findChild('.map-layers').classed('label-handle', false);
|
|
27567
|
+
gui.container.findChild('.map-layers').classed('label-text-drag', false);
|
|
27568
|
+
gui.container.findChild('.map-layers').classed('label-resize', false);
|
|
26217
27569
|
if (toolbar) toolbar.hide();
|
|
26218
27570
|
if (gui.interaction.getMode() == 'label') {
|
|
26219
27571
|
// the mode change came from somewhere other than the mode menu
|
|
@@ -26239,6 +27591,14 @@
|
|
|
26239
27591
|
}).on('click', function() {
|
|
26240
27592
|
setArmed(armed == 'anchor' ? null : 'anchor');
|
|
26241
27593
|
});
|
|
27594
|
+
// A tool of its own, although a text block is only a label with a width,
|
|
27595
|
+
// because otherwise the width is something found by dragging a handle on a
|
|
27596
|
+
// label that already exists -- and nothing on screen says it is there.
|
|
27597
|
+
blockBtn = toolbar.addButton('#text-block-icon', {
|
|
27598
|
+
tooltip: 'Add a text block'
|
|
27599
|
+
}).on('click', function() {
|
|
27600
|
+
setArmed(armed == 'block' ? null : 'block');
|
|
27601
|
+
});
|
|
26242
27602
|
pathBtn = toolbar.addButton('#curved-text-icon', {
|
|
26243
27603
|
tooltip: 'Add a label along a path'
|
|
26244
27604
|
}).on('click', function() {
|
|
@@ -26258,6 +27618,7 @@
|
|
|
26258
27618
|
function updateButtons() {
|
|
26259
27619
|
if (!toolbar) return;
|
|
26260
27620
|
anchorBtn.setSelected(armed == 'anchor');
|
|
27621
|
+
blockBtn.setSelected(armed == 'block');
|
|
26261
27622
|
pathBtn.setSelected(armed == 'path');
|
|
26262
27623
|
updateCursor();
|
|
26263
27624
|
}
|
|
@@ -26271,22 +27632,34 @@
|
|
|
26271
27632
|
// wrong even with a tool armed.
|
|
26272
27633
|
function updateCursor() {
|
|
26273
27634
|
var el = gui.container.findChild('.map-layers');
|
|
26274
|
-
//
|
|
26275
|
-
//
|
|
26276
|
-
|
|
26277
|
-
|
|
27635
|
+
// The text and the control points get different cursors, because they are
|
|
27636
|
+
// often close together -- a callout's attachment point is on the text's
|
|
27637
|
+
// box, and a path label's knots sit under its glyphs -- and a drag on one
|
|
27638
|
+
// does something quite different from a drag on the other.
|
|
27639
|
+
var onText = active() && hoverTextId > -1;
|
|
27640
|
+
var onHandle = active() && !onText && !!hoverHandle;
|
|
27641
|
+
var resizing = onHandle && hoverHandle.kind == 'width';
|
|
27642
|
+
var placing = active() && !!armed && hit.getHitId() == -1 && !onText && !onHandle;
|
|
26278
27643
|
el.classed('label-tool', placing);
|
|
26279
|
-
el.classed('label-
|
|
27644
|
+
el.classed('label-text-drag', onText);
|
|
27645
|
+
el.classed('label-handle', onHandle && !resizing);
|
|
27646
|
+
el.classed('label-resize', resizing);
|
|
26280
27647
|
}
|
|
26281
27648
|
|
|
26282
27649
|
function showInstructions() {
|
|
27650
|
+
var msg;
|
|
26283
27651
|
hideInstructions();
|
|
26284
27652
|
if (!armed) return;
|
|
26285
|
-
|
|
26286
|
-
'Click on the map to place a label.'
|
|
26287
|
-
|
|
26288
|
-
'to
|
|
26289
|
-
|
|
27653
|
+
if (armed == 'anchor') {
|
|
27654
|
+
msg = 'Click on the map to place a label.';
|
|
27655
|
+
} else if (armed == 'block') {
|
|
27656
|
+
msg = 'Click on the map to place a text block, or drag across it ' +
|
|
27657
|
+
'to set the width the text wraps to.';
|
|
27658
|
+
} else {
|
|
27659
|
+
msg = 'Click to draw a curved path. Type Esc or double-click ' +
|
|
27660
|
+
'to finish. Backspace removes the last point.';
|
|
27661
|
+
}
|
|
27662
|
+
alert = showPopupAlert(msg, null, {non_blocking: true, max_width: '330px'});
|
|
26290
27663
|
}
|
|
26291
27664
|
|
|
26292
27665
|
function hideInstructions() {
|
|
@@ -26302,10 +27675,20 @@
|
|
|
26302
27675
|
// editing acts on exactly one and has nothing to do with the panel.
|
|
26303
27676
|
hit.on('click', function(e) {
|
|
26304
27677
|
if (!active()) return;
|
|
27678
|
+
// A press that moved a little is both a block drag and a click, released
|
|
27679
|
+
// by the same mouseup; the drag has answered it already.
|
|
27680
|
+
if (blockRelease && getDomEvent(e) == blockRelease) {
|
|
27681
|
+
blockRelease = null;
|
|
27682
|
+
return;
|
|
27683
|
+
}
|
|
26305
27684
|
if (armed == 'path' && drawingCurve()) {
|
|
26306
27685
|
extendCurve(pixToMapCoords(e.x, e.y));
|
|
26307
27686
|
return;
|
|
26308
27687
|
}
|
|
27688
|
+
// A label's handles sit on its callout and its box, which the hit test
|
|
27689
|
+
// counts as the label, and a second click there would open its text. A
|
|
27690
|
+
// click on a handle is half of the double-click that resets it.
|
|
27691
|
+
if (hoverHandle && hoverHandle.kind && !editor.isOpen()) return;
|
|
26309
27692
|
// A click away from the text finishes the label being typed into, and does
|
|
26310
27693
|
// nothing else: leaving one label is its own gesture, and placing the next
|
|
26311
27694
|
// one takes another click. This is also what makes one undo step cover one
|
|
@@ -26330,10 +27713,18 @@
|
|
|
26330
27713
|
if (e.id > -1 && clickLabel(e.id, e)) return;
|
|
26331
27714
|
if (editor.isOpen()) return; // a click within the text moved the caret
|
|
26332
27715
|
// Nothing was hit. Any selection is now over with, and the click means
|
|
26333
|
-
// whatever the armed tool says it means
|
|
27716
|
+
// whatever the armed tool says it means -- except with the text block
|
|
27717
|
+
// tool, where clicking off a selected block is how it is let go of, and
|
|
27718
|
+
// a block dropped at the same time would be one more thing to delete.
|
|
27719
|
+
if (armed == 'block' && e.had_selection) {
|
|
27720
|
+
deselectLabels();
|
|
27721
|
+
return;
|
|
27722
|
+
}
|
|
26334
27723
|
deselectLabels();
|
|
26335
27724
|
if (armed == 'anchor') {
|
|
26336
|
-
beginLabel([pixToMapCoords(e.x, e.y)]
|
|
27725
|
+
beginLabel([pixToMapCoords(e.x, e.y)]);
|
|
27726
|
+
} else if (armed == 'block') {
|
|
27727
|
+
beginLabel([pixToMapCoords(e.x, e.y)], getBlockStyle);
|
|
26337
27728
|
} else if (armed == 'path') {
|
|
26338
27729
|
extendCurve(pixToMapCoords(e.x, e.y));
|
|
26339
27730
|
}
|
|
@@ -26531,15 +27922,19 @@
|
|
|
26531
27922
|
return editor.ownsNode(getEventNode(e));
|
|
26532
27923
|
}
|
|
26533
27924
|
|
|
26534
|
-
// The DOM node a hit event came from.
|
|
26535
|
-
// mouse event, whose own originalEvent is the DOM one, so the node is two
|
|
26536
|
-
// levels down.
|
|
27925
|
+
// The DOM node a hit event came from.
|
|
26537
27926
|
function getEventNode(e) {
|
|
26538
|
-
var
|
|
26539
|
-
var domEvt = mouseEvt && mouseEvt.originalEvent || mouseEvt;
|
|
27927
|
+
var domEvt = getDomEvent(e);
|
|
26540
27928
|
return domEvt && domEvt.target || null;
|
|
26541
27929
|
}
|
|
26542
27930
|
|
|
27931
|
+
// The DOM event behind a hit event: its originalEvent is the mouse event,
|
|
27932
|
+
// whose own originalEvent is the DOM one.
|
|
27933
|
+
function getDomEvent(e) {
|
|
27934
|
+
var mouseEvt = e && e.originalEvent;
|
|
27935
|
+
return mouseEvt && mouseEvt.originalEvent || mouseEvt || null;
|
|
27936
|
+
}
|
|
27937
|
+
|
|
26543
27938
|
function deselectLabels() {
|
|
26544
27939
|
if (hit.getSelectionIds().length > 0) hit.clearSelection();
|
|
26545
27940
|
}
|
|
@@ -26574,6 +27969,7 @@
|
|
|
26574
27969
|
// find its nodes again and write the text being edited back into them, and
|
|
26575
27970
|
// the selection cues have to be drawn onto the new markup.
|
|
26576
27971
|
gui.on('map_rendered', function(e) {
|
|
27972
|
+
if (!e || e.action != 'hover') releasedDrag = null;
|
|
26577
27973
|
editor.refresh();
|
|
26578
27974
|
// The cues went with the old markup, so they have to be drawn again --
|
|
26579
27975
|
// except after a 'hover' draw, which leaves the SVG alone. That draw now
|
|
@@ -26628,7 +28024,14 @@
|
|
|
26628
28024
|
if (beginTextDrag(e)) consumeDrag(e);
|
|
26629
28025
|
return;
|
|
26630
28026
|
}
|
|
26631
|
-
if (!hoverHandle)
|
|
28027
|
+
if (!hoverHandle) {
|
|
28028
|
+
if (armed == 'block' && beginBlockDrag(e)) consumeDrag(e);
|
|
28029
|
+
return;
|
|
28030
|
+
}
|
|
28031
|
+
if (hoverHandle.kind) {
|
|
28032
|
+
if (beginHandleDrag(hoverHandle)) consumeDrag(e);
|
|
28033
|
+
return;
|
|
28034
|
+
}
|
|
26632
28035
|
beginKnotDrag(hoverHandle);
|
|
26633
28036
|
consumeDrag(e);
|
|
26634
28037
|
});
|
|
@@ -26663,6 +28066,16 @@
|
|
|
26663
28066
|
updateTextDrag(e);
|
|
26664
28067
|
return;
|
|
26665
28068
|
}
|
|
28069
|
+
if (handleDrag) {
|
|
28070
|
+
consumeDrag(e);
|
|
28071
|
+
updateHandleDrag(e);
|
|
28072
|
+
return;
|
|
28073
|
+
}
|
|
28074
|
+
if (blockDrag) {
|
|
28075
|
+
consumeDrag(e);
|
|
28076
|
+
updateBlockDrag(e);
|
|
28077
|
+
return;
|
|
28078
|
+
}
|
|
26666
28079
|
if (!drag) return;
|
|
26667
28080
|
consumeDrag(e);
|
|
26668
28081
|
shp = getDisplayShapes(drag.target)[drag.id];
|
|
@@ -26685,6 +28098,7 @@
|
|
|
26685
28098
|
if (textDrag) {
|
|
26686
28099
|
o = textDrag;
|
|
26687
28100
|
textDrag = null;
|
|
28101
|
+
if (o.moved) releasedDrag = o;
|
|
26688
28102
|
consumeDrag(e);
|
|
26689
28103
|
selection.setTether(-1);
|
|
26690
28104
|
// A press that never moved is a click, which the click handler has
|
|
@@ -26692,6 +28106,22 @@
|
|
|
26692
28106
|
if (o.moved) commitTextDrag(o);
|
|
26693
28107
|
return;
|
|
26694
28108
|
}
|
|
28109
|
+
if (handleDrag) {
|
|
28110
|
+
o = handleDrag;
|
|
28111
|
+
handleDrag = null;
|
|
28112
|
+
if (o.values) releasedDrag = o;
|
|
28113
|
+
consumeDrag(e);
|
|
28114
|
+
if (o.values) commitHandleDrag(o);
|
|
28115
|
+
return;
|
|
28116
|
+
}
|
|
28117
|
+
if (blockDrag) {
|
|
28118
|
+
o = blockDrag;
|
|
28119
|
+
clearBlockDrag();
|
|
28120
|
+
blockRelease = getDomEvent(e);
|
|
28121
|
+
consumeDrag(e);
|
|
28122
|
+
commitBlockDrag(o);
|
|
28123
|
+
return;
|
|
28124
|
+
}
|
|
26695
28125
|
if (!o) return;
|
|
26696
28126
|
consumeDrag(e);
|
|
26697
28127
|
drag = null;
|
|
@@ -26726,6 +28156,301 @@
|
|
|
26726
28156
|
});
|
|
26727
28157
|
}
|
|
26728
28158
|
|
|
28159
|
+
// The handles a selected anchored label draws, for LabelSelection: only on a
|
|
28160
|
+
// label that is the whole selection, since a handle edits one label, and
|
|
28161
|
+
// not while anything else is going on. Kept as shownHandles for the hover
|
|
28162
|
+
// test, which then tests the pointer against exactly what is on screen.
|
|
28163
|
+
//
|
|
28164
|
+
// During a drag they are worked out from the record being previewed, so that
|
|
28165
|
+
// the handle follows the pointer and the others follow the line.
|
|
28166
|
+
//
|
|
28167
|
+
// Every selected text block gets its column, handles or not, and that too
|
|
28168
|
+
// comes from the preview: the text moves by its attributes during an offset
|
|
28169
|
+
// drag, and a column read from the data stayed where the text had been.
|
|
28170
|
+
function getLabelHandles(target, id, textBox) {
|
|
28171
|
+
var ids = hit.getSelectionIds();
|
|
28172
|
+
var rec = getPreviewRecord(id) || getRecord(target, id);
|
|
28173
|
+
var o;
|
|
28174
|
+
if (!rec || isPathLabel(target, id)) return null;
|
|
28175
|
+
if (!active() || editor.isOpen() || drawingCurve() || textDrag ||
|
|
28176
|
+
ids.length != 1 || ids[0] != id) {
|
|
28177
|
+
shownHandles = null;
|
|
28178
|
+
return {handles: [], column: getLabelColumn(rec)};
|
|
28179
|
+
}
|
|
28180
|
+
o = getAnchoredLabelHandles(rec, textBox, {
|
|
28181
|
+
symbolRadius: internal.svg.getAnchorSymbolRadius(rec),
|
|
28182
|
+
scale: ext.getSymbolScale() || 1,
|
|
28183
|
+
padding: BOX_PADDING
|
|
28184
|
+
});
|
|
28185
|
+
shownHandles = {id: id, target: target, handles: o.handles};
|
|
28186
|
+
return o;
|
|
28187
|
+
}
|
|
28188
|
+
|
|
28189
|
+
function getPreviewRecord(id) {
|
|
28190
|
+
var o = handleDrag || textDrag || releasedDrag;
|
|
28191
|
+
return o && o.id == id && o.previewRec || null;
|
|
28192
|
+
}
|
|
28193
|
+
|
|
28194
|
+
// The label handle nearest the pointer, within reach of it, or null.
|
|
28195
|
+
// Returns a handle shaped like findNearestKnot()'s, plus its kind, its point
|
|
28196
|
+
// in label space and the pointer's, and its distance in screen px.
|
|
28197
|
+
function findLabelHandle(target, e) {
|
|
28198
|
+
var o = shownHandles;
|
|
28199
|
+
var ids = hit.getSelectionIds();
|
|
28200
|
+
var scale = ext.getSymbolScale() || 1;
|
|
28201
|
+
var shp, q, best = null;
|
|
28202
|
+
if (!o || o.target != target || ids.length != 1 || ids[0] != o.id) return null;
|
|
28203
|
+
shp = getDisplayShapes(target)[o.id];
|
|
28204
|
+
if (!shp) return null;
|
|
28205
|
+
q = getLabelSpacePoint(shp[0], e);
|
|
28206
|
+
o.handles.forEach(function(h) {
|
|
28207
|
+
var dist = Math.sqrt(Math.pow(h.point[0] - q.x, 2) +
|
|
28208
|
+
Math.pow(h.point[1] - q.y, 2)) * scale;
|
|
28209
|
+
if (dist <= KNOT_HIT_THRESHOLD && (!best || dist < best.dist)) {
|
|
28210
|
+
best = {id: o.id, kind: h.kind, point: h.point, pointer: [q.x, q.y], dist: dist};
|
|
28211
|
+
}
|
|
28212
|
+
});
|
|
28213
|
+
return best;
|
|
28214
|
+
}
|
|
28215
|
+
|
|
28216
|
+
function beginHandleDrag(handle) {
|
|
28217
|
+
var target = hit.getHitTarget();
|
|
28218
|
+
var rec = getRecord(target, handle.id);
|
|
28219
|
+
var shp = getDisplayShapes(target)[handle.id];
|
|
28220
|
+
var drawn, shape;
|
|
28221
|
+
if (!rec || !shp) return false;
|
|
28222
|
+
drawn = internal.svg.getDrawnLabelOffset(rec);
|
|
28223
|
+
shape = internal.svg.getLabelCalloutShape(rec, internal.svg.getAnchorSymbolRadius(rec));
|
|
28224
|
+
if (handle.kind != 'width' && !shape) return false;
|
|
28225
|
+
handleDrag = {
|
|
28226
|
+
kind: handle.kind,
|
|
28227
|
+
id: handle.id,
|
|
28228
|
+
target: target,
|
|
28229
|
+
rec: rec,
|
|
28230
|
+
anchor: shp[0],
|
|
28231
|
+
// Where within the handle it was grabbed, so that it tracks the pointer
|
|
28232
|
+
// rather than jumping to it -- measured from the last hover, as a knot's
|
|
28233
|
+
// is, since dragstart arrives after the first move.
|
|
28234
|
+
grab: [handle.point[0] - handle.pointer[0], handle.point[1] - handle.pointer[1]],
|
|
28235
|
+
textAnchor: getDrawnAnchor(drawn),
|
|
28236
|
+
dx: drawn.dx,
|
|
28237
|
+
// The padded box the attachment point slides around. It does not move
|
|
28238
|
+
// while the point does: callout-attach is where on the box, not where
|
|
28239
|
+
// the box is.
|
|
28240
|
+
box: shape ? shape.box : null,
|
|
28241
|
+
values: null,
|
|
28242
|
+
previewRec: null
|
|
28243
|
+
};
|
|
28244
|
+
return true;
|
|
28245
|
+
}
|
|
28246
|
+
|
|
28247
|
+
function updateHandleDrag(e) {
|
|
28248
|
+
var o = handleDrag;
|
|
28249
|
+
var q = getLabelSpacePoint(o.anchor, e);
|
|
28250
|
+
var values = getHandleDragValues(o, [q.x + o.grab[0], q.y + o.grab[1]]);
|
|
28251
|
+
if (!values || o.values && sameValues(values, o.values)) return;
|
|
28252
|
+
o.values = values;
|
|
28253
|
+
o.previewRec = Object.assign({}, o.rec, values);
|
|
28254
|
+
previewLabel(o.target, o.id, o.previewRec);
|
|
28255
|
+
}
|
|
28256
|
+
|
|
28257
|
+
// The fields a handle dragged to @p writes, all of them in their stored form.
|
|
28258
|
+
function getHandleDragValues(o, p) {
|
|
28259
|
+
var tol = SNAP_PX / (ext.getSymbolScale() || 1);
|
|
28260
|
+
var width, shape, via;
|
|
28261
|
+
if (o.kind == 'width') {
|
|
28262
|
+
width = getDraggedWidth(o.textAnchor, o.dx, p[0], BOX_PADDING);
|
|
28263
|
+
return {
|
|
28264
|
+
'label-width': width,
|
|
28265
|
+
'label-text': rewrapLabelValue(o.rec['label-text'],
|
|
28266
|
+
Object.assign({}, o.rec, {'label-width': width}))
|
|
28267
|
+
};
|
|
28268
|
+
}
|
|
28269
|
+
if (o.kind == 'via') {
|
|
28270
|
+
// Snapped against where the line meets the text with the via point
|
|
28271
|
+
// where the pointer is, because an automatic attachment faces the via
|
|
28272
|
+
// point and moves with it. Only an elbow's corner snaps: level and plumb
|
|
28273
|
+
// legs are what an elbow is for, but a curve's midpoint has no reason to
|
|
28274
|
+
// line up with anything, and snapping it made the curve hard to place.
|
|
28275
|
+
shape = internal.svg.getLabelCalloutShape(
|
|
28276
|
+
Object.assign({}, o.rec, {'callout-via': formatPointPair(p)}),
|
|
28277
|
+
internal.svg.getAnchorSymbolRadius(o.rec));
|
|
28278
|
+
via = shape && shape.kind != 'bezier' ? snapCalloutVia(p, shape.attach, tol) : p;
|
|
28279
|
+
return {'callout-via': formatPointPair(via)};
|
|
28280
|
+
}
|
|
28281
|
+
if (o.kind == 'attach') {
|
|
28282
|
+
return {'callout-attach': formatPointPair(getAttachFraction(o.box, p, tol))};
|
|
28283
|
+
}
|
|
28284
|
+
if (o.kind == 'gap') {
|
|
28285
|
+
return {'callout-gap': getDraggedGap(p)};
|
|
28286
|
+
}
|
|
28287
|
+
return null;
|
|
28288
|
+
}
|
|
28289
|
+
|
|
28290
|
+
function sameValues(a, b) {
|
|
28291
|
+
return Object.keys(a).every(function(k) { return a[k] === b[k]; });
|
|
28292
|
+
}
|
|
28293
|
+
|
|
28294
|
+
// Redraws one label from @rec without touching its data, then redraws its
|
|
28295
|
+
// cue around the result. The command the drag ends with redraws the layer
|
|
28296
|
+
// from the data, which replaces this.
|
|
28297
|
+
function previewLabel(target, id, rec) {
|
|
28298
|
+
var shp = getDisplayShapes(target)[id];
|
|
28299
|
+
var container = target.gui && target.gui.svg_container;
|
|
28300
|
+
if (!replaceAnchoredSymbol(container, id, rec, shp, ext)) return;
|
|
28301
|
+
selection.refresh(true);
|
|
28302
|
+
}
|
|
28303
|
+
|
|
28304
|
+
// As with an offset drag, the preview is left in place for the command's own
|
|
28305
|
+
// redraw to replace, and is only taken back if the command fails.
|
|
28306
|
+
function commitHandleDrag(o) {
|
|
28307
|
+
var values = Object.assign({}, o.values);
|
|
28308
|
+
var text = values['label-text'];
|
|
28309
|
+
delete values['label-text'];
|
|
28310
|
+
runHandleCommand(o.target, o.id, values,
|
|
28311
|
+
text !== undefined && text !== o.rec['label-text'] ? text : null,
|
|
28312
|
+
handleTitles[o.kind]);
|
|
28313
|
+
}
|
|
28314
|
+
|
|
28315
|
+
var handleTitles = {
|
|
28316
|
+
width: 'Set label width',
|
|
28317
|
+
via: 'Move callout bend',
|
|
28318
|
+
attach: 'Move callout end',
|
|
28319
|
+
gap: 'Set callout gap'
|
|
28320
|
+
};
|
|
28321
|
+
|
|
28322
|
+
// text: rewrapped label-text to write with the values, or null
|
|
28323
|
+
function runHandleCommand(target, id, values, text, title) {
|
|
28324
|
+
var ids = hit.getSelectionIds();
|
|
28325
|
+
runGuiEditCommand(gui, getLabelStyleCommand(values, id, {
|
|
28326
|
+
target: target.name,
|
|
28327
|
+
text: text === null ? undefined : text
|
|
28328
|
+
}), {
|
|
28329
|
+
title: title,
|
|
28330
|
+
// The label stays selected, so its handles are still there to be dragged
|
|
28331
|
+
// again. Re-running the command rebuilds the layer, which drops the
|
|
28332
|
+
// selection.
|
|
28333
|
+
onSuccess: function() { hit.setSelectionIds(ids); },
|
|
28334
|
+
onError: function() { gui.dispatchEvent('map-needs-refresh'); }
|
|
28335
|
+
});
|
|
28336
|
+
}
|
|
28337
|
+
|
|
28338
|
+
// Double-clicking a callout handle gives the label back the callout's own
|
|
28339
|
+
// choice of bend, attachment or gap. The width handle has no automatic value
|
|
28340
|
+
// to go back to: taking the width away would turn the block into point text.
|
|
28341
|
+
function resetHandle(handle) {
|
|
28342
|
+
var target = hit.getHitTarget();
|
|
28343
|
+
var rec = getRecord(target, handle.id);
|
|
28344
|
+
var field = {via: 'callout-via', attach: 'callout-attach',
|
|
28345
|
+
gap: 'callout-gap'}[handle.kind];
|
|
28346
|
+
var values = {};
|
|
28347
|
+
if (!rec || !field || isBlank(rec[field])) return;
|
|
28348
|
+
values[field] = '';
|
|
28349
|
+
runHandleCommand(target, handle.id, values, null, handleTitles[handle.kind]);
|
|
28350
|
+
}
|
|
28351
|
+
|
|
28352
|
+
function isBlank(val) {
|
|
28353
|
+
return val === undefined || val === null || val === '';
|
|
28354
|
+
}
|
|
28355
|
+
|
|
28356
|
+
// Dragging across the map with the text block tool armed sets the new
|
|
28357
|
+
// block's width, which runs between the points pressed and released.
|
|
28358
|
+
function beginBlockDrag(e) {
|
|
28359
|
+
var target = hit.getHitTarget();
|
|
28360
|
+
// Not from a label, whose text a drag there belongs to
|
|
28361
|
+
if (!target || e.id > -1 && isLabel(target, e.id)) return false;
|
|
28362
|
+
if (editor.isOpen()) return false;
|
|
28363
|
+
blockDrag = {
|
|
28364
|
+
target: target,
|
|
28365
|
+
from: getPointerPixels(e),
|
|
28366
|
+
start: hoverPoint || pixToMapCoords(e.x, e.y),
|
|
28367
|
+
width: 0,
|
|
28368
|
+
guide: null
|
|
28369
|
+
};
|
|
28370
|
+
return true;
|
|
28371
|
+
}
|
|
28372
|
+
|
|
28373
|
+
function updateBlockDrag(e) {
|
|
28374
|
+
var o = blockDrag;
|
|
28375
|
+
var scale = ext.getSymbolScale() || 1;
|
|
28376
|
+
o.width = (e.x - o.from[0]) / scale;
|
|
28377
|
+
drawBlockGuide(o, scale);
|
|
28378
|
+
}
|
|
28379
|
+
|
|
28380
|
+
// A dashed box the height of a line, from the press to the pointer, drawn
|
|
28381
|
+
// with a symbol's transform so that it scales as the label will. In the
|
|
28382
|
+
// map's shared <svg> rather than the layer's group, like a pending label,
|
|
28383
|
+
// since a layer with no labels in it yet has no group to draw into.
|
|
28384
|
+
function drawBlockGuide(o, scale) {
|
|
28385
|
+
var container = gui.map.getSvgRoot();
|
|
28386
|
+
var style = getStyleForNewLabel();
|
|
28387
|
+
var h = getNewFontSize(style) * 1.2;
|
|
28388
|
+
var p = ext.translateCoords(o.start[0], o.start[1]);
|
|
28389
|
+
var el = o.guide;
|
|
28390
|
+
if (!container) return;
|
|
28391
|
+
if (!el) {
|
|
28392
|
+
el = o.guide = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
28393
|
+
el.setAttribute('class', 'label-block-guide');
|
|
28394
|
+
container.appendChild(el);
|
|
28395
|
+
}
|
|
28396
|
+
el.setAttribute('transform', internal.svg.getTransform(p, scale));
|
|
28397
|
+
el.setAttribute('x', Math.min(0, o.width));
|
|
28398
|
+
el.setAttribute('y', 0);
|
|
28399
|
+
el.setAttribute('width', Math.abs(o.width));
|
|
28400
|
+
el.setAttribute('height', h);
|
|
28401
|
+
}
|
|
28402
|
+
|
|
28403
|
+
function clearBlockDrag() {
|
|
28404
|
+
var el = blockDrag && blockDrag.guide;
|
|
28405
|
+
if (el && el.parentNode) el.parentNode.removeChild(el);
|
|
28406
|
+
blockDrag = null;
|
|
28407
|
+
}
|
|
28408
|
+
|
|
28409
|
+
// A drag too short to mean a width is a click, and does what a click would:
|
|
28410
|
+
// lets go of the selection if there is one, and otherwise places a block at
|
|
28411
|
+
// the default width. A drag to the left places the block the same as a drag
|
|
28412
|
+
// to the right from where it ended, so that its text is left-aligned like
|
|
28413
|
+
// any other block's.
|
|
28414
|
+
function commitBlockDrag(o) {
|
|
28415
|
+
var width = Math.round(Math.abs(o.width));
|
|
28416
|
+
var start = o.start;
|
|
28417
|
+
var p;
|
|
28418
|
+
if (width < MIN_LABEL_WIDTH) {
|
|
28419
|
+
if (hit.getSelectionIds().length > 0) {
|
|
28420
|
+
deselectLabels();
|
|
28421
|
+
} else {
|
|
28422
|
+
beginLabel([start], getBlockStyle);
|
|
28423
|
+
}
|
|
28424
|
+
return;
|
|
28425
|
+
}
|
|
28426
|
+
deselectLabels();
|
|
28427
|
+
if (o.width < 0) {
|
|
28428
|
+
p = ext.translateCoords(start[0], start[1]);
|
|
28429
|
+
start = pixToMapCoords(p[0] + o.width * (ext.getSymbolScale() || 1), p[1]);
|
|
28430
|
+
}
|
|
28431
|
+
beginLabel([start], function() { return getBlockStyle(width); });
|
|
28432
|
+
}
|
|
28433
|
+
|
|
28434
|
+
// A text block's style: the new label's, with a wrap width -- @width or a
|
|
28435
|
+
// default -- and placed with the top left of its column at the anchor, so
|
|
28436
|
+
// that its text is left-aligned and fills the box a drag drew. The offsets
|
|
28437
|
+
// that do that replace the position the panel names.
|
|
28438
|
+
function getBlockStyle(width) {
|
|
28439
|
+
var style = Object.assign({}, getStyleForNewLabel());
|
|
28440
|
+
delete style['label-pos'];
|
|
28441
|
+
return Object.assign(style, {
|
|
28442
|
+
'label-width': width > 0 ? width : DEFAULT_BLOCK_WIDTH,
|
|
28443
|
+
'text-anchor': 'start',
|
|
28444
|
+
dx: 0,
|
|
28445
|
+
dy: Math.round(getNewFontSize(style) * 0.8)
|
|
28446
|
+
});
|
|
28447
|
+
}
|
|
28448
|
+
|
|
28449
|
+
function getNewFontSize(style) {
|
|
28450
|
+
var px = parseFloat(style['font-size']);
|
|
28451
|
+
return px > 0 ? px : internal.svg.DEFAULT_LABEL_FONT_SIZE || 12;
|
|
28452
|
+
}
|
|
28453
|
+
|
|
26729
28454
|
// What a drag on a selected label's glyphs means, which depends on the kind
|
|
26730
28455
|
// of label and -- for an anchored one -- on the tool's position mode.
|
|
26731
28456
|
//
|
|
@@ -26738,7 +28463,7 @@
|
|
|
26738
28463
|
var shp = id > -1 ? getDisplayShapes(target)[id] : null;
|
|
26739
28464
|
if (id < 0 || !shp) return false;
|
|
26740
28465
|
if (isPathLabel(target, id)) return beginPathTextDrag(e, target, id);
|
|
26741
|
-
if (
|
|
28466
|
+
if (textIsDraggable(target, id)) return beginOffsetDrag(e, target, id);
|
|
26742
28467
|
beginKnotDrag({
|
|
26743
28468
|
id: id,
|
|
26744
28469
|
index: 0,
|
|
@@ -26873,6 +28598,8 @@
|
|
|
26873
28598
|
kind: 'offset',
|
|
26874
28599
|
id: id,
|
|
26875
28600
|
target: target,
|
|
28601
|
+
rec: rec,
|
|
28602
|
+
previewRec: null,
|
|
26876
28603
|
// dx and dy are in the space inside the label's symbol group, so pointer
|
|
26877
28604
|
// movement is divided by the scale that group wears.
|
|
26878
28605
|
scale: ext.getSymbolScale() || 1,
|
|
@@ -26889,7 +28616,8 @@
|
|
|
26889
28616
|
// multi-line label -- which is the width its justification is
|
|
26890
28617
|
// measured against.
|
|
26891
28618
|
width: box ? box.width : 0,
|
|
26892
|
-
aligned: !!internal.svg.getAlignmentAnchor(rec['label-align'])
|
|
28619
|
+
aligned: !!internal.svg.getAlignmentAnchor(rec['label-align']),
|
|
28620
|
+
keepAnchor: getLabelPositionKind(rec) == 'block'
|
|
26893
28621
|
},
|
|
26894
28622
|
// What the attributes said before the preview wrote over them, so that
|
|
26895
28623
|
// the command is what changes the label rather than the drag.
|
|
@@ -26898,14 +28626,32 @@
|
|
|
26898
28626
|
y: nodes.text.getAttribute('y'),
|
|
26899
28627
|
anchor: nodes.text.getAttribute('text-anchor')
|
|
26900
28628
|
},
|
|
28629
|
+
callout: getOffsetDragCallout(rec),
|
|
26901
28630
|
moved: false
|
|
26902
28631
|
};
|
|
26903
28632
|
// The hairline to the anchor, which is what the drag is measured from and
|
|
26904
|
-
// often the only thing on screen that says so.
|
|
26905
|
-
selection.setTether(id);
|
|
28633
|
+
// often the only thing on screen that says so. A callout says it already.
|
|
28634
|
+
if (!textDrag.callout) selection.setTether(id);
|
|
26906
28635
|
return true;
|
|
26907
28636
|
}
|
|
26908
28637
|
|
|
28638
|
+
// What an offset drag needs to carry a label's callout along, or null for a
|
|
28639
|
+
// label without one: the record to redraw it from, and for a callout bent
|
|
28640
|
+
// by hand, its via point and where the line met the text at the start.
|
|
28641
|
+
function getOffsetDragCallout(rec) {
|
|
28642
|
+
var shape;
|
|
28643
|
+
if (!internal.svg.labelHasCallout(rec)) return null;
|
|
28644
|
+
shape = internal.svg.getLabelCalloutShape(rec, internal.svg.getAnchorSymbolRadius(rec));
|
|
28645
|
+
return {
|
|
28646
|
+
rec: rec,
|
|
28647
|
+
box: internal.svg.getLabelTextBox(rec),
|
|
28648
|
+
// the shape's via point is the stored one when there is one
|
|
28649
|
+
via: rec['callout-via'] && shape && shape.via ? shape.via.slice() : null,
|
|
28650
|
+
attach: shape ? shape.attach : null,
|
|
28651
|
+
type: shape && shape.kind == 'bezier' ? 'curve' : 'elbow'
|
|
28652
|
+
};
|
|
28653
|
+
}
|
|
28654
|
+
|
|
26909
28655
|
function updateOffsetDrag(e) {
|
|
26910
28656
|
var o = textDrag;
|
|
26911
28657
|
o.values = getOffsetDragValues(o.start, {
|
|
@@ -26913,7 +28659,41 @@
|
|
|
26913
28659
|
dy: (e.y - o.from[1]) / o.scale
|
|
26914
28660
|
});
|
|
26915
28661
|
o.moved = true;
|
|
26916
|
-
|
|
28662
|
+
// The record as the drag will leave it, which the selection cue reads a
|
|
28663
|
+
// text block's column from while the drag is on
|
|
28664
|
+
o.previewRec = Object.assign({}, o.rec, {
|
|
28665
|
+
dx: o.values.dx,
|
|
28666
|
+
dy: o.values.dy,
|
|
28667
|
+
'text-anchor': o.values['text-anchor']
|
|
28668
|
+
});
|
|
28669
|
+
delete o.previewRec['label-pos'];
|
|
28670
|
+
if (o.callout) {
|
|
28671
|
+
previewCalloutOffset(o, getDomEvent(e));
|
|
28672
|
+
} else {
|
|
28673
|
+
previewOffset(o);
|
|
28674
|
+
}
|
|
28675
|
+
}
|
|
28676
|
+
|
|
28677
|
+
// A label with a callout is redrawn rather than moved by its attributes, as
|
|
28678
|
+
// previewOffset() does, because the line has to be worked out again for
|
|
28679
|
+
// where the text now is. Redrawn from the fields the drag will write, which
|
|
28680
|
+
// is also what makes the preview the same as the result.
|
|
28681
|
+
//
|
|
28682
|
+
// A via point placed by hand goes with the text, unless Alt is held: a bend
|
|
28683
|
+
// put there to clear something usually still has to, and one left behind by
|
|
28684
|
+
// a long drag doubles the line back on itself.
|
|
28685
|
+
function previewCalloutOffset(o, evt) {
|
|
28686
|
+
var c = o.callout;
|
|
28687
|
+
var rec = o.previewRec;
|
|
28688
|
+
var box, t1;
|
|
28689
|
+
o.via = null;
|
|
28690
|
+
if (c.via && c.attach && !(evt && evt.altKey)) {
|
|
28691
|
+
box = internal.svg.getLabelTextBox(rec);
|
|
28692
|
+
t1 = [c.attach[0] + box.xmin - c.box.xmin, c.attach[1] + box.ymin - c.box.ymin];
|
|
28693
|
+
o.via = formatPointPair(followCalloutVia(c.type, c.via, c.attach, t1));
|
|
28694
|
+
rec['callout-via'] = o.via;
|
|
28695
|
+
}
|
|
28696
|
+
previewLabel(o.target, o.id, rec);
|
|
26917
28697
|
}
|
|
26918
28698
|
|
|
26919
28699
|
// Shows the offset by writing it onto the rendered text, as the path drag
|
|
@@ -26939,6 +28719,12 @@
|
|
|
26939
28719
|
|
|
26940
28720
|
function restoreOffset(o) {
|
|
26941
28721
|
var nodes = findLabelNodes(o.target, o.id);
|
|
28722
|
+
releasedDrag = null;
|
|
28723
|
+
if (o.callout) {
|
|
28724
|
+
// redrawn rather than written on, so redrawn back
|
|
28725
|
+
gui.dispatchEvent('map-needs-refresh');
|
|
28726
|
+
return;
|
|
28727
|
+
}
|
|
26942
28728
|
if (!nodes) return;
|
|
26943
28729
|
// The renderer always writes x and y on an anchored label, so the removal
|
|
26944
28730
|
// case is for markup this tool did not draw.
|
|
@@ -26960,6 +28746,7 @@
|
|
|
26960
28746
|
dx: o.values.dx,
|
|
26961
28747
|
dy: o.values.dy,
|
|
26962
28748
|
anchor: o.values['text-anchor'],
|
|
28749
|
+
via: o.via || null,
|
|
26963
28750
|
id: o.id,
|
|
26964
28751
|
target: o.target.name
|
|
26965
28752
|
}), {
|
|
@@ -27062,8 +28849,9 @@
|
|
|
27062
28849
|
function updateHoverHandle(e) {
|
|
27063
28850
|
var found = drawingCurve() || editor.isOpen() || !e.overMap ? null :
|
|
27064
28851
|
findHandle(hit.getHitTarget(), e);
|
|
27065
|
-
var changed = !!found != !!hoverHandle
|
|
27066
|
-
|
|
28852
|
+
var changed = !!found != !!hoverHandle ||
|
|
28853
|
+
!!found && found.kind != hoverHandle.kind;
|
|
28854
|
+
if (dragging()) return;
|
|
27067
28855
|
hoverHandle = found;
|
|
27068
28856
|
if (changed) {
|
|
27069
28857
|
updateHoverText(); // a knot handle takes the glyphs' turn away
|
|
@@ -27082,16 +28870,30 @@
|
|
|
27082
28870
|
// text, and letting the handle win there would leave the text ungrabbable.
|
|
27083
28871
|
function updateHoverText() {
|
|
27084
28872
|
var id = findDraggableText();
|
|
27085
|
-
if (
|
|
28873
|
+
if (dragging()) return;
|
|
27086
28874
|
hoverTextId = id > -1 && (!hoverHandle || glyphsOutrankHandle(id)) ? id : -1;
|
|
27087
28875
|
}
|
|
27088
28876
|
|
|
28877
|
+
// Point text and text blocks each have a position mode -- see
|
|
28878
|
+
// getLabelPositionMode().
|
|
28879
|
+
function textIsDraggable(target, id) {
|
|
28880
|
+
return labelTextIsDraggable(gui, getLabelPositionKind(getRecord(target, id)));
|
|
28881
|
+
}
|
|
28882
|
+
|
|
28883
|
+
function dragging() {
|
|
28884
|
+
return !!(drag || textDrag || handleDrag || blockDrag);
|
|
28885
|
+
}
|
|
28886
|
+
|
|
27089
28887
|
// The exception is about the pointer being on the glyphs, not about the mode:
|
|
27090
28888
|
// an offset label's anchor is out from under its text, and taking the handle
|
|
27091
28889
|
// away there left it with no anchor handle at all -- dragging the symbol of a
|
|
27092
28890
|
// label positioned ne slid its text instead of moving the label.
|
|
28891
|
+
//
|
|
28892
|
+
// Only the anchor: the width and callout handles sit on the edge of the box
|
|
28893
|
+
// and would otherwise never be grabbable in Draggable mode.
|
|
27093
28894
|
function glyphsOutrankHandle(id) {
|
|
27094
|
-
return hoverHandle.id == id &&
|
|
28895
|
+
return !hoverHandle.kind && hoverHandle.id == id &&
|
|
28896
|
+
textIsDraggable(hit.getHitTarget(), id) &&
|
|
27095
28897
|
!isPathLabel(hit.getHitTarget(), id) && pointerIsOverText(id);
|
|
27096
28898
|
}
|
|
27097
28899
|
|
|
@@ -27134,14 +28936,24 @@
|
|
|
27134
28936
|
}
|
|
27135
28937
|
|
|
27136
28938
|
// The handle under the pointer, or null. Only a selected label's handles are
|
|
27137
|
-
// grabbable -- see gui-label-knots.mjs.
|
|
28939
|
+
// grabbable -- see gui-label-knots.mjs. A knot and a label handle within
|
|
28940
|
+
// reach of the pointer go to whichever is nearer: a callout's gap handle can
|
|
28941
|
+
// sit close to the anchor.
|
|
27138
28942
|
function findHandle(target, e) {
|
|
27139
28943
|
var shapes = target && getDisplayShapes(target);
|
|
27140
28944
|
var ids = hit.getSelectionIds();
|
|
27141
28945
|
var p = pixToMapCoords(e.x, e.y);
|
|
27142
|
-
var handle;
|
|
28946
|
+
var handle, labelHandle, knotPix;
|
|
27143
28947
|
if (!shapes || ids.length === 0) return null;
|
|
27144
28948
|
handle = findNearestKnot(shapes, ids, p, scaleThreshold(KNOT_HIT_THRESHOLD));
|
|
28949
|
+
labelHandle = findLabelHandle(target, e);
|
|
28950
|
+
if (handle && labelHandle) {
|
|
28951
|
+
knotPix = ext.translateCoords(handle.point[0], handle.point[1]);
|
|
28952
|
+
if (Math.hypot(knotPix[0] - e.x, knotPix[1] - e.y) < labelHandle.dist) {
|
|
28953
|
+
labelHandle = null;
|
|
28954
|
+
}
|
|
28955
|
+
}
|
|
28956
|
+
if (labelHandle) return labelHandle;
|
|
27145
28957
|
if (handle) handle.pointer = p;
|
|
27146
28958
|
return handle;
|
|
27147
28959
|
}
|
|
@@ -27183,6 +28995,10 @@
|
|
|
27183
28995
|
}
|
|
27184
28996
|
return;
|
|
27185
28997
|
}
|
|
28998
|
+
if (hoverHandle && hoverHandle.kind && !editor.isOpen()) {
|
|
28999
|
+
resetHandle(hoverHandle);
|
|
29000
|
+
return;
|
|
29001
|
+
}
|
|
27186
29002
|
// Double-clicking a label reaches straight into its text, which is the
|
|
27187
29003
|
// gesture most editors use and saves selecting it first. The single-click
|
|
27188
29004
|
// route got there already if the label was the selection; this covers the
|
|
@@ -27295,8 +29111,12 @@
|
|
|
27295
29111
|
// So the click opens a *pending* session, which draws the label and holds its
|
|
27296
29112
|
// text, and the label becomes a feature only when the session ends with
|
|
27297
29113
|
// something in it. See LabelEditor.openPending().
|
|
27298
|
-
|
|
29114
|
+
//
|
|
29115
|
+
// getStyle: (optional) the new label's style, when it is not simply the
|
|
29116
|
+
// panel's -- a text block's adds a width
|
|
29117
|
+
function beginLabel(displayCoords, getStyle) {
|
|
27299
29118
|
var target = hit.getHitTarget();
|
|
29119
|
+
var styleFn = getStyle || getStyleForNewLabel;
|
|
27300
29120
|
if (!target) return;
|
|
27301
29121
|
hideInstructions();
|
|
27302
29122
|
editor.openPending(target, {
|
|
@@ -27305,15 +29125,15 @@
|
|
|
27305
29125
|
// or a position while the caret is sitting there is visible immediately.
|
|
27306
29126
|
// The same style the label will be created with, down to the font it is
|
|
27307
29127
|
// named in, so that committing it changes nothing on screen.
|
|
27308
|
-
getStyle:
|
|
27309
|
-
create: function(text) { createLabel(displayCoords, text); }
|
|
29128
|
+
getStyle: styleFn,
|
|
29129
|
+
create: function(text) { createLabel(displayCoords, text, styleFn()); }
|
|
27310
29130
|
});
|
|
27311
29131
|
}
|
|
27312
29132
|
|
|
27313
29133
|
// Creates the label being typed into. One command carries its geometry, its
|
|
27314
29134
|
// style and its text, so a new label is a single entry in the session history
|
|
27315
29135
|
// and a single step to undo.
|
|
27316
|
-
function createLabel(displayCoords, text) {
|
|
29136
|
+
function createLabel(displayCoords, text, style) {
|
|
27317
29137
|
var target = hit.getHitTarget();
|
|
27318
29138
|
var coords = displayCoords.map(function(p) {
|
|
27319
29139
|
return target ? translateDisplayPoint(target, p) : p;
|
|
@@ -27323,7 +29143,7 @@
|
|
|
27323
29143
|
text: text,
|
|
27324
29144
|
// whatever the style panel was set to while nothing was selected, so that
|
|
27325
29145
|
// a font can be chosen before the first label exists
|
|
27326
|
-
style:
|
|
29146
|
+
style: style
|
|
27327
29147
|
}), {title: 'Add label'});
|
|
27328
29148
|
}
|
|
27329
29149
|
|
|
@@ -27390,7 +29210,7 @@
|
|
|
27390
29210
|
},
|
|
27391
29211
|
referenceStyle = { // outline style for reference layers
|
|
27392
29212
|
type: 'outline',
|
|
27393
|
-
strokeColors: [null, '#
|
|
29213
|
+
strokeColors: [null, '#87b73b'], // was 78c110
|
|
27394
29214
|
strokeWidth: 0.85,
|
|
27395
29215
|
dotColor: "#73ba20",
|
|
27396
29216
|
dotSize: 1
|
|
@@ -30612,6 +32432,9 @@
|
|
|
30612
32432
|
// getContentLayerBounds() instead. Recorded because the difference matters
|
|
30613
32433
|
// when deciding whether an update should reset the view.
|
|
30614
32434
|
var _boundsArePlaceholder = false;
|
|
32435
|
+
// Whether the content's bounds had no width or no height before they were
|
|
32436
|
+
// padded in calcFullBounds() -- see mapNeedsResetFromCollapsedBounds().
|
|
32437
|
+
var _boundsAreCollapsed = false;
|
|
30615
32438
|
|
|
30616
32439
|
var RESIZE_REDRAW_DELAY = 200;
|
|
30617
32440
|
|
|
@@ -30878,6 +32701,7 @@
|
|
|
30878
32701
|
// read before calcFullBounds() below, which describes the map as it is
|
|
30879
32702
|
// after this update
|
|
30880
32703
|
var prevBoundsWerePlaceholder = _boundsArePlaceholder;
|
|
32704
|
+
var prevBoundsWereCollapsed = _boundsAreCollapsed;
|
|
30881
32705
|
var fullBounds;
|
|
30882
32706
|
var needReset;
|
|
30883
32707
|
|
|
@@ -30960,6 +32784,8 @@
|
|
|
30960
32784
|
// screen, so the view they chose it in is the one to keep, unless what
|
|
30961
32785
|
// arrived is not in it.
|
|
30962
32786
|
needReset = !fullBounds.intersects(_ext.getBounds());
|
|
32787
|
+
} else if (prevBoundsWereCollapsed) {
|
|
32788
|
+
needReset = mapNeedsResetFromCollapsedBounds(fullBounds, _ext.getBounds(), e.flags);
|
|
30963
32789
|
} else {
|
|
30964
32790
|
needReset = mapNeedsReset(fullBounds, _ext.getFullBounds(), _ext.getBounds(), e.flags);
|
|
30965
32791
|
}
|
|
@@ -31040,7 +32866,8 @@
|
|
|
31040
32866
|
b.scale(1 + marginPct / 100 * 2);
|
|
31041
32867
|
|
|
31042
32868
|
// Inflate display bounding box of single-point layers and collapsed shapes a bit
|
|
31043
|
-
|
|
32869
|
+
_boundsAreCollapsed = b.width() === 0 || b.height() === 0;
|
|
32870
|
+
if (_boundsAreCollapsed) {
|
|
31044
32871
|
b.padBounds(1e-4, 1e-4, 1e-4, 1e-4);
|
|
31045
32872
|
}
|
|
31046
32873
|
return b;
|
|
@@ -33728,7 +35555,6 @@
|
|
|
33728
35555
|
if (manifest.catalog) {
|
|
33729
35556
|
opts.catalog = manifest.catalog;
|
|
33730
35557
|
}
|
|
33731
|
-
opts.display_all = vars['display-all'] || vars.a || !!manifest.display_all;
|
|
33732
35558
|
opts.quick_view = vars['quick-view'] || vars.q || !!manifest.quick_view;
|
|
33733
35559
|
opts.target = vars.target || manifest.target || null;
|
|
33734
35560
|
opts.name = vars.name || manifest.name || null;
|