mapshaper 0.7.65 → 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 +2633 -399
- package/www/mapshaper.js +907 -33
- package/www/page.css +177 -11
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) {
|
|
@@ -11373,10 +11407,22 @@
|
|
|
11373
11407
|
}
|
|
11374
11408
|
|
|
11375
11409
|
if (lyr && lyr.gui.geographic) {
|
|
11410
|
+
// Above the actions, because it is what they would be applied to: the
|
|
11411
|
+
// labels that share something with the one pointed at, read and chosen
|
|
11412
|
+
// before "copy as GeoJSON" or "delete label" means anything definite.
|
|
11413
|
+
// Each item carries the number it would select, so that what the click
|
|
11414
|
+
// is about to do is visible before it happens.
|
|
11415
|
+
if (e.selectActions?.length) {
|
|
11416
|
+
addMenuLabel('select');
|
|
11417
|
+
e.selectActions.forEach(function(action) {
|
|
11418
|
+
addMenuItem(action.label + getCountHtml(action.count), action.run);
|
|
11419
|
+
});
|
|
11420
|
+
}
|
|
11421
|
+
|
|
11376
11422
|
if (e.deleteVertex || e.deletePoint || copyable || e.deleteFeature ||
|
|
11377
11423
|
e.flipLabel) {
|
|
11378
11424
|
|
|
11379
|
-
addMenuLabel('
|
|
11425
|
+
addMenuLabel('actions');
|
|
11380
11426
|
if (e.deleteVertex) {
|
|
11381
11427
|
addMenuItem('delete vertex', e.deleteVertex);
|
|
11382
11428
|
}
|
|
@@ -11524,6 +11570,12 @@
|
|
|
11524
11570
|
return 'band values';
|
|
11525
11571
|
}
|
|
11526
11572
|
|
|
11573
|
+
// How many features a "select" item would select, in the grey of a menu
|
|
11574
|
+
// heading: it qualifies the item rather than being part of what it says.
|
|
11575
|
+
function getCountHtml(count) {
|
|
11576
|
+
return ' <span class="contextmenu-count">' + count + '</span>';
|
|
11577
|
+
}
|
|
11578
|
+
|
|
11527
11579
|
// A color tile, used in place of the bullet that other menu items get.
|
|
11528
11580
|
function getSwatchHtml(color) {
|
|
11529
11581
|
return '<span class="contextmenu-swatch" style="background-color: ' +
|
|
@@ -11762,7 +11814,9 @@
|
|
|
11762
11814
|
layerCount++;
|
|
11763
11815
|
});
|
|
11764
11816
|
|
|
11765
|
-
|
|
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) {
|
|
11766
11820
|
pinAll.hide();
|
|
11767
11821
|
} else {
|
|
11768
11822
|
pinAll.show();
|
|
@@ -12204,8 +12258,7 @@
|
|
|
12204
12258
|
}
|
|
12205
12259
|
|
|
12206
12260
|
function isPinnable(lyr, dataset) {
|
|
12207
|
-
|
|
12208
|
-
return internal.layerIsGeometric(lyr) || internal.layerHasRaster(lyr) || internal.layerHasFurniture(lyr);
|
|
12261
|
+
return layerIsPinnable(lyr, dataset);
|
|
12209
12262
|
}
|
|
12210
12263
|
|
|
12211
12264
|
function layerCanBeStyled(lyr) {
|
|
@@ -13711,6 +13764,13 @@
|
|
|
13711
13764
|
// keeps them out of a character class so that "\\n" is matched as a pair.
|
|
13712
13765
|
var ANY_NEWLINE = /\r\n|\\n|<br>|\n|\r/gi;
|
|
13713
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
|
+
|
|
13714
13774
|
// How a label lays out its line breaks, which is not the same for the two kinds.
|
|
13715
13775
|
// An anchored label stacks its lines with <tspan>. A path label joins them with
|
|
13716
13776
|
// a space, because a <tspan> inside a <textPath> advances along the curve
|
|
@@ -13725,10 +13785,72 @@
|
|
|
13725
13785
|
// all, and a line the user has just opened with Enter and not yet typed into.
|
|
13726
13786
|
var TEXT_PLACEHOLDER = '\u200b';
|
|
13727
13787
|
|
|
13728
|
-
// 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.
|
|
13729
13790
|
function decodeLabelText(val) {
|
|
13730
13791
|
if (val === null || val === undefined || val === '') return '';
|
|
13731
|
-
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;
|
|
13732
13854
|
}
|
|
13733
13855
|
|
|
13734
13856
|
// The string the user edits -> the data value. Newlines become the escape,
|
|
@@ -13745,12 +13867,28 @@
|
|
|
13745
13867
|
// break that started it. That does two things: an empty line gets something to
|
|
13746
13868
|
// lay out, so a line the user has just opened appears and can hold the caret,
|
|
13747
13869
|
// and the rendered characters stay aligned one-for-one with the edited ones.
|
|
13748
|
-
|
|
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) {
|
|
13749
13875
|
var text = decodeLabelText(str);
|
|
13876
|
+
var lines = [], start = 0, lead = '', b = 0, i;
|
|
13750
13877
|
if (text === '') return [TEXT_PLACEHOLDER];
|
|
13751
|
-
|
|
13752
|
-
|
|
13753
|
-
|
|
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;
|
|
13754
13892
|
}
|
|
13755
13893
|
|
|
13756
13894
|
// Whether the text would draw nothing at all: empty, or made only of
|
|
@@ -13941,10 +14079,11 @@
|
|
|
13941
14079
|
//
|
|
13942
14080
|
// label-pos places text relative to an anchor point, which a path label does
|
|
13943
14081
|
// not have -- its text runs along the curve. Writing it there would put a
|
|
13944
|
-
// 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.
|
|
13945
14084
|
function getStyleFields$1(style, isPathLabel) {
|
|
13946
14085
|
return Object.keys(style || {}).filter(function(name) {
|
|
13947
|
-
return !(isPathLabel && name == 'label-pos');
|
|
14086
|
+
return !(isPathLabel && (name == 'label-pos' || name == 'label-width'));
|
|
13948
14087
|
});
|
|
13949
14088
|
}
|
|
13950
14089
|
|
|
@@ -14006,18 +14145,45 @@
|
|
|
14006
14145
|
// anchor: the value for text-anchor
|
|
14007
14146
|
// id: feature id of the label
|
|
14008
14147
|
// target: layer name, or null to use the current target
|
|
14148
|
+
// via: (optional) the callout-via the text carried with it
|
|
14009
14149
|
function getLabelOffsetCommand(opts) {
|
|
14010
14150
|
var parts = ['-style', 'dx=' + opts.dx, 'dy=' + opts.dy,
|
|
14011
14151
|
'text-anchor=' + opts.anchor, 'label-pos='];
|
|
14152
|
+
if (opts.via) parts.push('callout-via=' + quoteCommandValue(opts.via));
|
|
14012
14153
|
parts.push('ids=' + opts.id);
|
|
14013
14154
|
if (opts.target) parts.push('target=' + quoteCommandValue(opts.target));
|
|
14014
14155
|
return parts.join(' ');
|
|
14015
14156
|
}
|
|
14016
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
|
+
|
|
14017
14183
|
// The command that saves edited text, run once when an editing session ends so
|
|
14018
14184
|
// that a session is one undo step rather than one per keystroke.
|
|
14019
14185
|
//
|
|
14020
|
-
// text: the edited string, with real newlines
|
|
14186
|
+
// text: the edited string, with real newlines and any soft breaks
|
|
14021
14187
|
// id: feature id of the label
|
|
14022
14188
|
// target: layer name, or null to use the current target
|
|
14023
14189
|
function getLabelTextCommand(text, id, target) {
|
|
@@ -15613,16 +15779,16 @@
|
|
|
15613
15779
|
function getCanvasPoint(canvas, evt) {
|
|
15614
15780
|
var rect = canvas.getBoundingClientRect();
|
|
15615
15781
|
return {
|
|
15616
|
-
x: clamp$
|
|
15617
|
-
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)
|
|
15618
15784
|
};
|
|
15619
15785
|
}
|
|
15620
15786
|
|
|
15621
15787
|
function setPickerColor(hsb) {
|
|
15622
15788
|
pickerColor = {
|
|
15623
|
-
h: clamp$
|
|
15624
|
-
s: clamp$
|
|
15625
|
-
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)
|
|
15626
15792
|
};
|
|
15627
15793
|
drawColorPicker();
|
|
15628
15794
|
updatePickerFields();
|
|
@@ -15763,7 +15929,7 @@
|
|
|
15763
15929
|
}
|
|
15764
15930
|
|
|
15765
15931
|
function pctToByte(val) {
|
|
15766
|
-
return Math.round(clamp$
|
|
15932
|
+
return Math.round(clamp$4(val, 0, 100) / 100 * 255);
|
|
15767
15933
|
}
|
|
15768
15934
|
|
|
15769
15935
|
function byteToDegrees(val) {
|
|
@@ -15771,14 +15937,14 @@
|
|
|
15771
15937
|
}
|
|
15772
15938
|
|
|
15773
15939
|
function degreesToByte(val) {
|
|
15774
|
-
return Math.round(clamp$
|
|
15940
|
+
return Math.round(clamp$4(val, 0, 360) / 360 * 255);
|
|
15775
15941
|
}
|
|
15776
15942
|
|
|
15777
15943
|
function parseNumberField(str) {
|
|
15778
15944
|
return Number(String(str).replace(/[°%]/g, '').trim());
|
|
15779
15945
|
}
|
|
15780
15946
|
|
|
15781
|
-
function clamp$
|
|
15947
|
+
function clamp$4(val, min, max) {
|
|
15782
15948
|
return isFinite(val) ? Math.max(min, Math.min(max, val)) : min;
|
|
15783
15949
|
}
|
|
15784
15950
|
|
|
@@ -16166,9 +16332,12 @@
|
|
|
16166
16332
|
setDisplay(shown);
|
|
16167
16333
|
return;
|
|
16168
16334
|
}
|
|
16335
|
+
var changed = String(val) !== shown;
|
|
16169
16336
|
setDisplay(String(val));
|
|
16170
|
-
|
|
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.
|
|
16171
16339
|
shown = String(val);
|
|
16340
|
+
if (changed && o.onSet) o.onSet(val);
|
|
16172
16341
|
}
|
|
16173
16342
|
|
|
16174
16343
|
function setDisplay(str) {
|
|
@@ -16193,6 +16362,13 @@
|
|
|
16193
16362
|
setDisplay(shown);
|
|
16194
16363
|
};
|
|
16195
16364
|
|
|
16365
|
+
// What the field shows while it is blank, or '' for nothing. The panel uses
|
|
16366
|
+
// it to say that the selected features disagree about the size, which is one
|
|
16367
|
+
// of the two reasons the field can be empty.
|
|
16368
|
+
this.setPlaceholder = function(str) {
|
|
16369
|
+
input.attr('placeholder', str || '');
|
|
16370
|
+
};
|
|
16371
|
+
|
|
16196
16372
|
this.setDisabled = function(off) {
|
|
16197
16373
|
disabled = !!off;
|
|
16198
16374
|
input.node().disabled = disabled;
|
|
@@ -16496,7 +16672,12 @@
|
|
|
16496
16672
|
'dominant-baseline',
|
|
16497
16673
|
'label-pos', 'label-side', 'label-start-offset', 'dx', 'dy',
|
|
16498
16674
|
'fill', 'opacity', 'css', 'class',
|
|
16499
|
-
'
|
|
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'
|
|
16500
16681
|
];
|
|
16501
16682
|
|
|
16502
16683
|
// values: [[field, value], ...], the form the panel's controls produce
|
|
@@ -16551,6 +16732,24 @@
|
|
|
16551
16732
|
gui.state.new_label_style = null;
|
|
16552
16733
|
}
|
|
16553
16734
|
|
|
16735
|
+
// What a two-state control shows for a group of labels: 'on', 'off', or
|
|
16736
|
+
// 'mixed' when they disagree.
|
|
16737
|
+
//
|
|
16738
|
+
// The panel's other controls say "they disagree" by showing nothing -- an empty
|
|
16739
|
+
// select, an unlit button, a blank field. A switch has no empty state to fall
|
|
16740
|
+
// back on, so it needs a third one named here.
|
|
16741
|
+
//
|
|
16742
|
+
// flags: one truthy/falsy value per label, in any order
|
|
16743
|
+
function getToggleState(flags) {
|
|
16744
|
+
var on = 0;
|
|
16745
|
+
var i;
|
|
16746
|
+
for (i = 0; i < flags.length; i++) {
|
|
16747
|
+
if (flags[i]) on++;
|
|
16748
|
+
}
|
|
16749
|
+
if (on === 0) return 'off';
|
|
16750
|
+
return on == flags.length ? 'on' : 'mixed';
|
|
16751
|
+
}
|
|
16752
|
+
|
|
16554
16753
|
// What a drag on a label's glyphs means: 'fixed' moves the label, anchor and
|
|
16555
16754
|
// text together, and 'draggable' leaves the anchor where it is and offsets the
|
|
16556
16755
|
// text from it.
|
|
@@ -16567,23 +16766,49 @@
|
|
|
16567
16766
|
//
|
|
16568
16767
|
// Fixed on entry, so that a stray drag cannot displace text in a session that
|
|
16569
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()
|
|
16570
16776
|
var LABEL_POSITION_MODES = ['fixed', 'draggable'];
|
|
16571
16777
|
|
|
16572
|
-
|
|
16573
|
-
|
|
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];
|
|
16574
16784
|
}
|
|
16575
16785
|
|
|
16576
|
-
|
|
16577
|
-
|
|
16578
|
-
|
|
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;
|
|
16579
16797
|
// The panel's toggle and the tool's drag both read this, and neither owns the
|
|
16580
16798
|
// other; the panel also has to relight the grid, whose cells mean something
|
|
16581
16799
|
// different in each mode.
|
|
16582
16800
|
gui.dispatchEvent('label_position_mode_change');
|
|
16583
16801
|
}
|
|
16584
16802
|
|
|
16585
|
-
function labelTextIsDraggable(gui) {
|
|
16586
|
-
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';
|
|
16587
16812
|
}
|
|
16588
16813
|
|
|
16589
16814
|
// The label currently open for text editing, or null: {id, refocus}.
|
|
@@ -16661,6 +16886,9 @@
|
|
|
16661
16886
|
// anchor: the justification it is drawn with
|
|
16662
16887
|
// width: the width of its text, in the same space; 0 if unmeasurable
|
|
16663
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
|
|
16664
16892
|
// delta: {dx, dy} -- how far the pointer has moved, in the same space.
|
|
16665
16893
|
//
|
|
16666
16894
|
// Returns two descriptions of the same placement:
|
|
@@ -16688,7 +16916,7 @@
|
|
|
16688
16916
|
// that correction, 'start' is the one text-anchor whose dx is the left edge
|
|
16689
16917
|
// itself -- which is also the value that would leave the label where it is
|
|
16690
16918
|
// if the alignment were later removed.
|
|
16691
|
-
var anchor = start.aligned ? 'start' :
|
|
16919
|
+
var anchor = start.aligned ? 'start' : start.keepAnchor ? drawnAnchor :
|
|
16692
16920
|
width > 0 ? getAnchorForCentre(left + width / 2, width) : drawnAnchor;
|
|
16693
16921
|
// An aligned label goes on being drawn with the anchor its alignment gives
|
|
16694
16922
|
// it, whatever the drag writes: the alignment is unchanged, so the renderer
|
|
@@ -16756,32 +16984,206 @@
|
|
|
16756
16984
|
return Math.round(px * PRECISION) / PRECISION;
|
|
16757
16985
|
}
|
|
16758
16986
|
|
|
16759
|
-
|
|
16760
|
-
|
|
16761
|
-
|
|
16762
|
-
|
|
16763
|
-
|
|
16764
|
-
|
|
16765
|
-
|
|
16766
|
-
|
|
16767
|
-
|
|
16768
|
-
|
|
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
|
+
|
|
17140
|
+
var fontField = 'font-family';
|
|
17141
|
+
var fontSizeField = 'font-size';
|
|
17142
|
+
var fontStyleField = 'font-style';
|
|
17143
|
+
var fontWeightField = 'font-weight';
|
|
17144
|
+
var fillField = 'fill';
|
|
17145
|
+
var opacityField = 'opacity';
|
|
17146
|
+
var letterSpacingField = 'letter-spacing';
|
|
17147
|
+
var lineHeightField = 'line-height';
|
|
17148
|
+
var textAnchorField = 'text-anchor';
|
|
17149
|
+
var labelAlignField = 'label-align';
|
|
16769
17150
|
var cssField = 'css';
|
|
17151
|
+
var haloWidthField = 'halo-width';
|
|
17152
|
+
var haloColorField = 'halo-color';
|
|
17153
|
+
var haloOpacityField = 'halo-opacity';
|
|
16770
17154
|
var iconField = 'icon';
|
|
16771
17155
|
var iconSizeField = 'icon-size';
|
|
16772
17156
|
var iconColorField = 'icon-color';
|
|
16773
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;
|
|
16774
17167
|
var defaultFontSize = 12;
|
|
16775
17168
|
var defaultFontStyle = 'normal';
|
|
16776
17169
|
var defaultFontWeight = '400';
|
|
16777
17170
|
var defaultLabelColor = '#000000';
|
|
16778
17171
|
var defaultIconColor = '#000000';
|
|
16779
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;
|
|
16780
17177
|
// The line height field shows this rather than renderLabel()'s 1.1em default,
|
|
16781
17178
|
// and shows it as a placeholder rather than a value, so that a label carries
|
|
16782
17179
|
// no line-height until one is chosen. "auto" is the honest description of a
|
|
16783
17180
|
// blank field: something else decides.
|
|
16784
17181
|
var lineHeightPlaceholder = 'auto';
|
|
17182
|
+
// What a control shows for a selection whose labels disagree about it. One
|
|
17183
|
+
// word, used in every field and menu that can be in that state: the panel
|
|
17184
|
+
// otherwise says it by showing nothing, which is also what an unset property
|
|
17185
|
+
// looks like.
|
|
17186
|
+
var MIXED_TEXT = 'mixed';
|
|
16785
17187
|
var labelStyleMode = 'label_style';
|
|
16786
17188
|
var labelStylePanelMode = 'label_style_tool';
|
|
16787
17189
|
var labelMode = 'label';
|
|
@@ -16798,10 +17200,20 @@
|
|
|
16798
17200
|
labelAlignField,
|
|
16799
17201
|
cssField,
|
|
16800
17202
|
'label-pos',
|
|
17203
|
+
haloWidthField,
|
|
17204
|
+
haloColorField,
|
|
17205
|
+
haloOpacityField,
|
|
16801
17206
|
iconField,
|
|
16802
17207
|
iconSizeField,
|
|
16803
17208
|
iconColorField,
|
|
16804
|
-
iconOpacityField
|
|
17209
|
+
iconOpacityField,
|
|
17210
|
+
calloutField,
|
|
17211
|
+
calloutEndField,
|
|
17212
|
+
calloutEndSizeField,
|
|
17213
|
+
calloutColorField,
|
|
17214
|
+
calloutOpacityField,
|
|
17215
|
+
calloutWidthField,
|
|
17216
|
+
calloutGapField
|
|
16805
17217
|
];
|
|
16806
17218
|
var labelPositions = ['nw', 'n', 'ne', 'w', 'c', 'e', 'sw', 's', 'se'];
|
|
16807
17219
|
// The position an icon moves a centred label to when it is switched on: upper
|
|
@@ -16863,6 +17275,41 @@
|
|
|
16863
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>',
|
|
16864
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>'
|
|
16865
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
|
+
};
|
|
16866
17313
|
var iconButtonSymbols = {
|
|
16867
17314
|
circle: '<circle cx="8" cy="8" r="4.25"></circle>',
|
|
16868
17315
|
square: '<rect x="4" y="4" width="8" height="8"></rect>',
|
|
@@ -16877,11 +17324,15 @@
|
|
|
16877
17324
|
// label-style-panel carries the styling the point and layer panels share; the
|
|
16878
17325
|
// second class is this panel's own, as theirs are
|
|
16879
17326
|
var panel = El('div').addClass('label-style-panel text-style-panel rollover').appendTo(parent).hide();
|
|
16880
|
-
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;
|
|
16881
17328
|
var fontOptionsRendered = false;
|
|
16882
17329
|
// The shape the toggle turns back on, so that switching a symbol off and on
|
|
16883
17330
|
// again does not silently change a star into a circle.
|
|
16884
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;
|
|
16885
17336
|
|
|
16886
17337
|
initPanel();
|
|
16887
17338
|
gui.addMode(labelStylePanelMode, turnOn, turnOff);
|
|
@@ -16981,8 +17432,18 @@
|
|
|
16981
17432
|
});
|
|
16982
17433
|
|
|
16983
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.
|
|
16984
17441
|
closeBtn = El('button').addClass('label-style-close').appendTo(header).text('×').on('click', function() {
|
|
16985
|
-
|
|
17442
|
+
if (labelModeIsOn()) {
|
|
17443
|
+
gui.interaction.turnOff(); // the tool's own turnOff() takes the panel
|
|
17444
|
+
} else {
|
|
17445
|
+
gui.clearMode();
|
|
17446
|
+
}
|
|
16986
17447
|
});
|
|
16987
17448
|
|
|
16988
17449
|
var selectRow = El('div').addClass('label-style-selection-row').appendTo(panel);
|
|
@@ -17050,6 +17511,9 @@
|
|
|
17050
17511
|
// Letter spacing takes the right-hand column on its own, above line
|
|
17051
17512
|
// height: the two spacing values read as a pair there, and the left of the
|
|
17052
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.
|
|
17053
17517
|
var letterRow = El('div').addClass('label-style-row label-split-row').appendTo(textSection);
|
|
17054
17518
|
El('div').addClass('label-split-cell').appendTo(letterRow);
|
|
17055
17519
|
var letterCell = El('div').addClass('label-split-cell label-spacing-row').appendTo(letterRow);
|
|
@@ -17088,15 +17552,69 @@
|
|
|
17088
17552
|
var positionRow = El('label').addClass('label-style-row').appendTo(textSection);
|
|
17089
17553
|
El('span').appendTo(positionRow).text('Offset from anchor');
|
|
17090
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
|
+
|
|
17091
17608
|
// Whether the label has a symbol is one question and which symbol it has is
|
|
17092
17609
|
// another, so the first is a switch on the section's heading rather than a
|
|
17093
17610
|
// fifth shape button reading "none". Everything below it is inert while it
|
|
17094
17611
|
// is off, which is also the honest reading of an icon-size or icon-color on
|
|
17095
17612
|
// a label with no icon: nothing to apply it to.
|
|
17096
|
-
|
|
17613
|
+
iconSection = addSection('Icon');
|
|
17097
17614
|
var iconTitle = iconSection.findChild('.label-style-section-title');
|
|
17098
17615
|
iconToggle = makeToggle(iconTitle, {
|
|
17099
17616
|
title: 'Draw a symbol at the label anchor',
|
|
17617
|
+
className: 'label-icon-toggle',
|
|
17100
17618
|
onChange: setIconOn
|
|
17101
17619
|
});
|
|
17102
17620
|
// The size's caption sits on the heading line, over its own column. It
|
|
@@ -17152,6 +17670,8 @@
|
|
|
17152
17670
|
var iconOpacityCell = El('div').addClass('label-split-cell label-opacity-row label-icon-opacity-row').appendTo(iconColorRow);
|
|
17153
17671
|
iconOpacityInput = addOpacityInput(iconOpacityCell, applyIconOpacity);
|
|
17154
17672
|
|
|
17673
|
+
initCalloutSection();
|
|
17674
|
+
|
|
17155
17675
|
var posRow = El('div').addClass('label-style-row label-position-row').appendTo(textSection);
|
|
17156
17676
|
|
|
17157
17677
|
|
|
@@ -17175,7 +17695,7 @@
|
|
|
17175
17695
|
dragModeBtns = {};
|
|
17176
17696
|
labelDragModes.forEach(function(item) {
|
|
17177
17697
|
dragModeBtns[item.name] = makePanelButton(dragModeGroup, item.label, function() {
|
|
17178
|
-
setLabelPositionMode(gui, item.name);
|
|
17698
|
+
setLabelPositionMode(gui, item.name, getPositionModeKind());
|
|
17179
17699
|
})
|
|
17180
17700
|
.attr('data-drag-mode', item.name)
|
|
17181
17701
|
.attr('title', item.title);
|
|
@@ -17196,6 +17716,116 @@
|
|
|
17196
17716
|
});
|
|
17197
17717
|
}
|
|
17198
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
|
+
|
|
17199
17829
|
function appendIconButtonSymbol(btn, iconName) {
|
|
17200
17830
|
var svg = '<svg class="label-icon-symbol" viewBox="0 0 16 16" aria-hidden="true">' +
|
|
17201
17831
|
iconButtonSymbols[iconName] + '</svg>';
|
|
@@ -17215,26 +17845,39 @@
|
|
|
17215
17845
|
return makePanelSection(panel, title, opts);
|
|
17216
17846
|
}
|
|
17217
17847
|
|
|
17218
|
-
// A
|
|
17219
|
-
//
|
|
17220
|
-
//
|
|
17848
|
+
// A switch: a track with a knob that sits left when off and right when on,
|
|
17849
|
+
// which is the direction users expect and the only thing that says which
|
|
17850
|
+
// state is which without a label for each. A third state says that the
|
|
17851
|
+
// labels it is asking about disagree -- the knob sits over the join of a
|
|
17852
|
+
// half-and-half track, which is the panel's only control that has to show
|
|
17853
|
+
// "some of them" rather than a value.
|
|
17854
|
+
//
|
|
17855
|
+
// role=checkbox rather than switch, which is what it looks like: 'mixed' is a
|
|
17856
|
+
// legal aria-checked value for a checkbox and not for a switch, and a switch
|
|
17857
|
+
// reporting a mixed selection as unchecked would be telling a screen reader
|
|
17858
|
+
// the one thing the third state exists to avoid saying.
|
|
17221
17859
|
function makeToggle(parent, opts) {
|
|
17222
|
-
var track = El('div').addClass('label-toggle').attr('role', '
|
|
17223
|
-
var
|
|
17860
|
+
var track = El('div').addClass('label-toggle').attr('role', 'checkbox').appendTo(parent);
|
|
17861
|
+
var state = 'off';
|
|
17862
|
+
if (opts.className) track.addClass(opts.className);
|
|
17224
17863
|
var disabled = false;
|
|
17225
17864
|
El('div').addClass('label-toggle-knob').appendTo(track);
|
|
17226
17865
|
if (opts.title) track.attr('title', opts.title);
|
|
17866
|
+
// A click on a mixed switch turns everything on. It is the convention, and
|
|
17867
|
+
// it is the reading that reaches a state the switch can describe: the next
|
|
17868
|
+
// click then turns everything off, so both are one click away.
|
|
17227
17869
|
track.on('click', function() {
|
|
17228
17870
|
if (disabled) return;
|
|
17229
|
-
opts.onChange(
|
|
17871
|
+
opts.onChange(state != 'on');
|
|
17230
17872
|
});
|
|
17231
17873
|
return {
|
|
17232
|
-
|
|
17233
|
-
|
|
17234
|
-
|
|
17235
|
-
|
|
17236
|
-
|
|
17237
|
-
|
|
17874
|
+
// val: 'on', 'off' or 'mixed'; anything else reads as off
|
|
17875
|
+
setState: function(val) {
|
|
17876
|
+
state = val == 'on' || val == 'mixed' ? val : 'off';
|
|
17877
|
+
track.classed('on', state == 'on')
|
|
17878
|
+
.classed('mixed', state == 'mixed')
|
|
17879
|
+
.attr('aria-checked', state == 'mixed' ? 'mixed' :
|
|
17880
|
+
state == 'on' ? 'true' : 'false');
|
|
17238
17881
|
},
|
|
17239
17882
|
setDisabled: function(off) {
|
|
17240
17883
|
disabled = !!off;
|
|
@@ -17257,6 +17900,9 @@
|
|
|
17257
17900
|
function makeMeasureInput(parent, field, placeholder) {
|
|
17258
17901
|
var input = El('input').attr('type', 'text').addClass('label-measure-input')
|
|
17259
17902
|
.attr('placeholder', placeholder)
|
|
17903
|
+
// The field's own placeholder, kept because the shown one is replaced
|
|
17904
|
+
// while the selection disagrees -- see setMixedPlaceholder().
|
|
17905
|
+
.attr('data-placeholder', placeholder)
|
|
17260
17906
|
.appendTo(parent)
|
|
17261
17907
|
.on('change', function() {
|
|
17262
17908
|
applyStyleValues([[field, input.node().value.trim()]]);
|
|
@@ -17299,9 +17945,7 @@
|
|
|
17299
17945
|
renderFontOptions();
|
|
17300
17946
|
gui.state.label_style_panel_open = true;
|
|
17301
17947
|
panel.show();
|
|
17302
|
-
|
|
17303
|
-
// user opened, and closing it would leave the tool half on.
|
|
17304
|
-
closeBtn[labelModeIsOn() ? 'hide' : 'show']();
|
|
17948
|
+
closeBtn.show();
|
|
17305
17949
|
textBtn.addClass('selected');
|
|
17306
17950
|
updateControls();
|
|
17307
17951
|
updateSelectionDisplay();
|
|
@@ -17428,51 +18072,98 @@
|
|
|
17428
18072
|
// With nothing selected the menu shows the font the next label will be
|
|
17429
18073
|
// made in; with labels selected it shows the font they are drawn in, which
|
|
17430
18074
|
// for a label carrying none is whatever sans-serif resolves to here.
|
|
17431
|
-
var
|
|
18075
|
+
var font = getShownValue(ids, fontField,
|
|
17432
18076
|
{useDefault: true, defaultValue: getFontNameForTarget(ids)});
|
|
17433
|
-
var
|
|
18077
|
+
var fontSize = getShownValue(ids, fontSizeField, {useDefault: true, defaultValue: defaultFontSize});
|
|
17434
18078
|
var fontStyleVal = getCommonValue(ids, fontStyleField, {useDefault: true, defaultValue: defaultFontStyle});
|
|
17435
18079
|
var fontWeightVal = getCommonValue(ids, fontWeightField, {useDefault: true, defaultValue: defaultFontWeight});
|
|
17436
|
-
var
|
|
17437
|
-
var
|
|
17438
|
-
var
|
|
17439
|
-
var
|
|
18080
|
+
var fill = getShownValue(ids, fillField, {useDefault: true, defaultValue: defaultLabelColor});
|
|
18081
|
+
var opacity = getShownValue(ids, opacityField, {useDefault: true, defaultValue: 1});
|
|
18082
|
+
var letterSpacing = getShownValue(ids, letterSpacingField);
|
|
18083
|
+
var lineHeight = getShownValue(ids, lineHeightField);
|
|
17440
18084
|
var alignVal = getCommonAlignment(ids);
|
|
17441
|
-
var
|
|
18085
|
+
var css = getShownValue(ids, cssField);
|
|
17442
18086
|
var posVal = getCommonValue(ids, 'label-pos');
|
|
17443
|
-
|
|
17444
|
-
|
|
17445
|
-
|
|
17446
|
-
var
|
|
18087
|
+
// From the labels that have a symbol, so that a selection where only some
|
|
18088
|
+
// do shows the symbols it has rather than blanking every field because the
|
|
18089
|
+
// rest of the selection has nothing to compare.
|
|
18090
|
+
var iconIds = getIconValueIds();
|
|
18091
|
+
var iconVal = getCommonValue(iconIds, iconField);
|
|
18092
|
+
var iconSize = getShownValue(iconIds, iconSizeField, {useDefault: true, defaultValue: defaultIconSize});
|
|
18093
|
+
var iconColor = getShownValue(iconIds, iconColorField, {useDefault: true, defaultValue: defaultIconColor});
|
|
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});
|
|
17447
18099
|
updateEditingStatus(manualIds.length, !!getLabelTextSession(gui));
|
|
17448
18100
|
updateSavedStyleControls();
|
|
17449
18101
|
fontSelect.node().disabled = !showValues;
|
|
17450
|
-
updateFontControl(
|
|
17451
|
-
updateFontStyleControls(
|
|
17452
|
-
updateFontSizeControls(
|
|
17453
|
-
updateColorControls(
|
|
17454
|
-
updateOpacityControl(opacityInput,
|
|
17455
|
-
updateMeasureControl(letterSpacingInput,
|
|
17456
|
-
updateMeasureControl(lineHeightInput,
|
|
18102
|
+
updateFontControl(font);
|
|
18103
|
+
updateFontStyleControls(font.value, fontStyleVal, fontWeightVal);
|
|
18104
|
+
updateFontSizeControls(fontSize);
|
|
18105
|
+
updateColorControls(fill);
|
|
18106
|
+
updateOpacityControl(opacityInput, opacity);
|
|
18107
|
+
updateMeasureControl(letterSpacingInput, letterSpacing);
|
|
18108
|
+
updateMeasureControl(lineHeightInput, lineHeight);
|
|
17457
18109
|
updateAlignButtons(showValues ? alignVal : '');
|
|
17458
|
-
updateCssControl(
|
|
18110
|
+
updateCssControl(css);
|
|
17459
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);
|
|
17460
18117
|
// The symbol's controls keep showing their values while the switch is off,
|
|
17461
18118
|
// greyed: what they show is what the symbol comes back as.
|
|
17462
18119
|
var iconOff = updateIconControls(showValues ? iconVal : '');
|
|
17463
|
-
updateIconSizeControls(
|
|
17464
|
-
updateIconColorControls(
|
|
17465
|
-
updateOpacityControl(iconOpacityInput,
|
|
17466
|
-
|
|
17467
|
-
|
|
17468
|
-
|
|
18120
|
+
updateIconSizeControls(iconSize, iconOff);
|
|
18121
|
+
updateIconColorControls(iconColor, iconOff);
|
|
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();
|
|
18140
|
+
}
|
|
18141
|
+
|
|
18142
|
+
// Every field that can show a value can also show nothing, which is why each
|
|
18143
|
+
// of these takes a {value, mixed} rather than a value: a field blank because
|
|
18144
|
+
// the selected labels disagree says so, in the placeholder, where one blank
|
|
18145
|
+
// because the property is unset shows what the renderer will do instead.
|
|
18146
|
+
function updateOpacityControl(input, shown, disabled) {
|
|
17469
18147
|
input.node().disabled = disabled || !controlsEnabled();
|
|
17470
|
-
input.node().value =
|
|
18148
|
+
input.node().value = shown.value === '' ? '' : formatOpacityPct(shown.value);
|
|
18149
|
+
setMixedPlaceholder(input, shown.mixed);
|
|
17471
18150
|
}
|
|
17472
18151
|
|
|
17473
|
-
function updateMeasureControl(input,
|
|
18152
|
+
function updateMeasureControl(input, shown) {
|
|
18153
|
+
var val = shown.value;
|
|
17474
18154
|
input.node().disabled = !controlsEnabled();
|
|
17475
18155
|
input.node().value = val || val === 0 ? String(val) : '';
|
|
18156
|
+
setMixedPlaceholder(input, shown.mixed);
|
|
18157
|
+
}
|
|
18158
|
+
|
|
18159
|
+
// A field showing nothing because the labels disagree says "mixed" where it
|
|
18160
|
+
// would otherwise show its own placeholder -- the spacing fields show the
|
|
18161
|
+
// value the renderer uses when the property is absent, which is not what is
|
|
18162
|
+
// true of a mixed selection.
|
|
18163
|
+
function setMixedPlaceholder(input, mixed) {
|
|
18164
|
+
var el = input.node();
|
|
18165
|
+
var own = el.getAttribute('data-placeholder') || '';
|
|
18166
|
+
el.setAttribute('placeholder', mixed ? MIXED_TEXT : own);
|
|
17476
18167
|
}
|
|
17477
18168
|
|
|
17478
18169
|
// Alignment stays live whatever is selected, including nothing. It was
|
|
@@ -17496,7 +18187,6 @@
|
|
|
17496
18187
|
// to take. The commands ignore a position given for one, and a disabled
|
|
17497
18188
|
// button says so where a console warning would not.
|
|
17498
18189
|
var disabled = !controlsEnabled() || everyLabelIsOnAPath(ids);
|
|
17499
|
-
var locked = !disabled && gridIsLockedToCentre(ids);
|
|
17500
18190
|
// Faintly, and only when no cell is lit: a label carrying offsets is not
|
|
17501
18191
|
// at any of the nine, but one of them is roughly where it is and clicking
|
|
17502
18192
|
// it is the way back.
|
|
@@ -17504,37 +18194,11 @@
|
|
|
17504
18194
|
labelPositions.forEach(function(name) {
|
|
17505
18195
|
posBtns[name].classed('selected', !disabled && name == pos);
|
|
17506
18196
|
posBtns[name].classed('nearest', name == nearest);
|
|
17507
|
-
setPanelButtonDisabled(posBtns[name], disabled
|
|
18197
|
+
setPanelButtonDisabled(posBtns[name], disabled);
|
|
17508
18198
|
});
|
|
17509
18199
|
updateDragModeButtons();
|
|
17510
18200
|
}
|
|
17511
18201
|
|
|
17512
|
-
// The nine positions place text around something, so with nothing drawn at
|
|
17513
|
-
// the anchor the grid is locked to the centre cell: there is no answer to
|
|
17514
|
-
// "north-east of what?", and the centre is what -add-label gives a new label
|
|
17515
|
-
// anyway. The cell stays clickable, because text over its own symbol is a
|
|
17516
|
-
// real thing to ask for.
|
|
17517
|
-
//
|
|
17518
|
-
// The test is whether the label draws a symbol, not whether it has an icon:
|
|
17519
|
-
// a styled dots layer given label text by the point panel's "Create labels"
|
|
17520
|
-
// keeps its r and fill, and keying this on icon= alone would lock the
|
|
17521
|
-
// commonest labels in the app to the middle of their own dots.
|
|
17522
|
-
//
|
|
17523
|
-
// A default and a guard rather than an invariant. A label that arrives
|
|
17524
|
-
// positioned with nothing at its anchor -- from the CLI, from an expression,
|
|
17525
|
-
// from the legacy positioning mode -- is shown as it is, with the grid live:
|
|
17526
|
-
// refusing to display what is in the record is worse than letting an odd
|
|
17527
|
-
// state be edited. Dragging is not gated on a symbol either, since a labels
|
|
17528
|
-
// layer whose dots live in a different layer still needs its text placed.
|
|
17529
|
-
function gridIsLockedToCentre(ids) {
|
|
17530
|
-
var table = getActiveTable();
|
|
17531
|
-
if (!everyTargetLacksASymbol(ids)) return false;
|
|
17532
|
-
if (ids.length === 0) return !labelIsPlaced(getNewLabelStyle(gui));
|
|
17533
|
-
return ids.every(function(id) {
|
|
17534
|
-
return !labelIsPlaced(table && table.getRecordAt(id));
|
|
17535
|
-
});
|
|
17536
|
-
}
|
|
17537
|
-
|
|
17538
18202
|
// Whether a label has been put somewhere other than on top of its anchor: a
|
|
17539
18203
|
// position of its own other than the centre one, or offsets from a drag.
|
|
17540
18204
|
function labelIsPlaced(rec) {
|
|
@@ -17544,16 +18208,6 @@
|
|
|
17544
18208
|
internal.hasStyleValue(rec, 'dy');
|
|
17545
18209
|
}
|
|
17546
18210
|
|
|
17547
|
-
function everyTargetLacksASymbol(ids) {
|
|
17548
|
-
var table = getActiveTable();
|
|
17549
|
-
if (ids.length === 0) {
|
|
17550
|
-
return !internal.featureHasSvgSymbol(getNewLabelStyle(gui));
|
|
17551
|
-
}
|
|
17552
|
-
return ids.every(function(id) {
|
|
17553
|
-
return !internal.featureHasSvgSymbol(table && table.getRecordAt(id));
|
|
17554
|
-
});
|
|
17555
|
-
}
|
|
17556
|
-
|
|
17557
18211
|
// Which of the nine positions a dragged label is nearest, or ''.
|
|
17558
18212
|
//
|
|
17559
18213
|
// Only for a single label: the cell is a hint about where one label sits,
|
|
@@ -17585,11 +18239,12 @@
|
|
|
17585
18239
|
}));
|
|
17586
18240
|
}
|
|
17587
18241
|
|
|
17588
|
-
// The toggle is the tool's state and not the selection's,
|
|
17589
|
-
//
|
|
17590
|
-
//
|
|
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.
|
|
17591
18246
|
function updateDragModeButtons() {
|
|
17592
|
-
var mode = getLabelPositionMode(gui);
|
|
18247
|
+
var mode = getLabelPositionMode(gui, getPositionModeKind());
|
|
17593
18248
|
var disabled = !labelModeIsOn();
|
|
17594
18249
|
labelDragModes.forEach(function(item) {
|
|
17595
18250
|
dragModeBtns[item.name].classed('selected', !disabled && item.name == mode);
|
|
@@ -17597,6 +18252,16 @@
|
|
|
17597
18252
|
});
|
|
17598
18253
|
}
|
|
17599
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
|
+
|
|
17600
18265
|
function everyLabelIsOnAPath(ids) {
|
|
17601
18266
|
return someLabelIsOnAPath(ids, 'every');
|
|
17602
18267
|
}
|
|
@@ -17631,8 +18296,9 @@
|
|
|
17631
18296
|
return labelModeIsOn() ? 'new labels' : 'all';
|
|
17632
18297
|
}
|
|
17633
18298
|
|
|
17634
|
-
function updateFontSizeControls(
|
|
17635
|
-
fontSizeInput.setValue(
|
|
18299
|
+
function updateFontSizeControls(shown) {
|
|
18300
|
+
fontSizeInput.setValue(shown.value || '');
|
|
18301
|
+
fontSizeInput.setPlaceholder(shown.mixed ? MIXED_TEXT : '');
|
|
17636
18302
|
fontSizeInput.setDisabled(!controlsEnabled());
|
|
17637
18303
|
}
|
|
17638
18304
|
|
|
@@ -17645,15 +18311,43 @@
|
|
|
17645
18311
|
// here, and a project moves between machines. It is added to the menu as
|
|
17646
18312
|
// itself rather than replaced by an installed font, since the name is the
|
|
17647
18313
|
// user's data and choosing something else for them would restyle the label.
|
|
17648
|
-
function updateFontControl(
|
|
18314
|
+
function updateFontControl(shown) {
|
|
18315
|
+
var fontVal = shown.value;
|
|
18316
|
+
// A font menu is a list of fonts, so "they are in different fonts" cannot
|
|
18317
|
+
// be one of the fonts: it is an entry that appears only while it is true,
|
|
18318
|
+
// and picking it does nothing (applyFont() ignores a blank value). The
|
|
18319
|
+
// list stays live, so choosing a font is how the selection is brought into
|
|
18320
|
+
// line -- as it is for the face menu below.
|
|
18321
|
+
setMixedOption(fontSelect, shown.mixed);
|
|
17649
18322
|
fontSelect.node().value = fontVal || '';
|
|
17650
18323
|
if (fontVal && fontSelect.node().selectedIndex < 0) {
|
|
17651
18324
|
El('option').attr('value', fontVal).text(fontVal).appendTo(fontSelect);
|
|
17652
18325
|
fontSelect.node().value = fontVal;
|
|
17653
18326
|
}
|
|
17654
|
-
// Nothing selected, rather than the first font in the list, for a
|
|
17655
|
-
//
|
|
17656
|
-
if (!fontVal) fontSelect.node().selectedIndex = -1;
|
|
18327
|
+
// Nothing selected, rather than the first font in the list, for a target
|
|
18328
|
+
// there is nothing to say about -- a disabled menu with no labels behind it.
|
|
18329
|
+
if (!fontVal && !shown.mixed) fontSelect.node().selectedIndex = -1;
|
|
18330
|
+
}
|
|
18331
|
+
|
|
18332
|
+
// Adds or removes a menu's "mixed" entry, which carries no value: a select
|
|
18333
|
+
// showing nothing looks like a control that failed to load, and this is the
|
|
18334
|
+
// one state the panel cannot express by lighting no button.
|
|
18335
|
+
//
|
|
18336
|
+
// First in the list, and removed as soon as the selection agrees, so that it
|
|
18337
|
+
// is never an option among the real ones.
|
|
18338
|
+
function setMixedOption(select, mixed) {
|
|
18339
|
+
var el = select.node();
|
|
18340
|
+
var first = el.options[0];
|
|
18341
|
+
var has = !!first && first.value === '' && first.dataset.mixed == 'true';
|
|
18342
|
+
if (mixed && !has) {
|
|
18343
|
+
first = document.createElement('option');
|
|
18344
|
+
first.value = '';
|
|
18345
|
+
first.textContent = MIXED_TEXT;
|
|
18346
|
+
first.dataset.mixed = 'true';
|
|
18347
|
+
el.insertBefore(first, el.firstChild);
|
|
18348
|
+
} else if (!mixed && has) {
|
|
18349
|
+
el.removeChild(first);
|
|
18350
|
+
}
|
|
17657
18351
|
}
|
|
17658
18352
|
|
|
17659
18353
|
// The faces the chosen font is installed with, and no entry for "whatever
|
|
@@ -17673,12 +18367,13 @@
|
|
|
17673
18367
|
El('option').attr('value', variant.value).appendTo(fontStyleSelect).text(variant.label);
|
|
17674
18368
|
});
|
|
17675
18369
|
}
|
|
18370
|
+
// The faces are rebuilt above, so the "mixed" entry goes on afterwards.
|
|
18371
|
+
setMixedOption(fontStyleSelect, mixed);
|
|
17676
18372
|
fontStyleSelect.node().disabled = disabled;
|
|
17677
18373
|
fontStyleSelect.node().value = shown ? shown.value : '';
|
|
17678
|
-
// Nothing selected rather than the first face, for a
|
|
17679
|
-
//
|
|
17680
|
-
|
|
17681
|
-
if (!shown) fontStyleSelect.node().selectedIndex = -1;
|
|
18374
|
+
// Nothing selected rather than the first face, for a menu with no font to
|
|
18375
|
+
// list the faces of.
|
|
18376
|
+
if (!shown && !mixed) fontStyleSelect.node().selectedIndex = -1;
|
|
17682
18377
|
}
|
|
17683
18378
|
|
|
17684
18379
|
// The face the panel shows for a label in @fontName. A font-style and
|
|
@@ -17689,76 +18384,374 @@
|
|
|
17689
18384
|
return getNearestFontStyleVariant(fontName, fontStyleVal, fontWeightVal);
|
|
17690
18385
|
}
|
|
17691
18386
|
|
|
17692
|
-
function updateColorControls(
|
|
18387
|
+
function updateColorControls(shown) {
|
|
18388
|
+
var colorVal = shown.value;
|
|
17693
18389
|
var disabled = !controlsEnabled();
|
|
17694
18390
|
colorInput.node().disabled = disabled;
|
|
17695
18391
|
colorInput.node().value = colorVal || '';
|
|
18392
|
+
setMixedPlaceholder(colorInput, shown.mixed);
|
|
17696
18393
|
colorFieldBox.classed('disabled', disabled);
|
|
17697
18394
|
setPanelButtonDisabled(colorChit, disabled);
|
|
18395
|
+
// An empty swatch is how the field says "no colour", so a mixed one is
|
|
18396
|
+
// marked instead of being left blank to mean two things.
|
|
18397
|
+
colorChit.classed('mixed', shown.mixed);
|
|
17698
18398
|
colorChit.css('background-color', isHexColor(colorVal) ? colorVal : 'transparent');
|
|
17699
18399
|
if (colorPicker.visible()) {
|
|
17700
18400
|
return; // avoid HSB -> RGB -> HSB rounding jumps after picker commits
|
|
17701
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.
|
|
17702
18404
|
if (isHexColor(colorVal)) {
|
|
17703
18405
|
colorPicker.setColor(colorVal);
|
|
17704
18406
|
} else {
|
|
17705
|
-
|
|
18407
|
+
colorPicker.hide();
|
|
17706
18408
|
}
|
|
17707
18409
|
}
|
|
17708
18410
|
|
|
17709
|
-
function updateCssControl(
|
|
18411
|
+
function updateCssControl(shown) {
|
|
17710
18412
|
cssInput.node().disabled = !controlsEnabled();
|
|
17711
|
-
cssInput.node().value =
|
|
18413
|
+
cssInput.node().value = shown.value || '';
|
|
18414
|
+
setMixedPlaceholder(cssInput, shown.mixed);
|
|
17712
18415
|
}
|
|
17713
18416
|
|
|
17714
18417
|
// The toggle reads the data rather than holding a state of its own: a symbol
|
|
17715
18418
|
// is on when the target has one, which is what makes it follow an undo.
|
|
17716
18419
|
//
|
|
17717
|
-
// A selection where only some labels have a symbol
|
|
17718
|
-
// section stays usable
|
|
17719
|
-
//
|
|
17720
|
-
//
|
|
18420
|
+
// A selection where only some labels have a symbol shows the switch mixed,
|
|
18421
|
+
// and the section stays usable: there are symbols in the selection to style.
|
|
18422
|
+
// What the controls below it then act on narrows to the labels that have one
|
|
18423
|
+
// -- see getIconTargetIds() -- so styling a symbol never creates one. The
|
|
18424
|
+
// switch is still the only way to ask for that, and clicking a shape is the
|
|
18425
|
+
// one thing in the section that applies to every selected label, because
|
|
18426
|
+
// choosing a shape for a group is a plain statement about all of it.
|
|
17721
18427
|
function updateIconControls(iconVal) {
|
|
17722
18428
|
var enabled = controlsEnabled();
|
|
17723
|
-
var
|
|
18429
|
+
var state = enabled ? getIconState() : 'off';
|
|
18430
|
+
var off = state == 'off';
|
|
17724
18431
|
if (iconVal) lastIconShape = iconVal;
|
|
17725
|
-
iconToggle.setState(
|
|
18432
|
+
iconToggle.setState(state);
|
|
17726
18433
|
iconToggle.setDisabled(!enabled);
|
|
17727
18434
|
// The group is faded as a whole rather than button by button, so that the
|
|
17728
18435
|
// border the buttons share fades with them -- a live border around dead
|
|
17729
18436
|
// buttons is the one part of a disabled control that still looks usable.
|
|
17730
|
-
iconGroupEl.classed('disabled',
|
|
18437
|
+
iconGroupEl.classed('disabled', off);
|
|
17731
18438
|
iconTypes.forEach(function(icon) {
|
|
17732
|
-
iconBtns[icon.name].classed('selected',
|
|
17733
|
-
setPanelButtonDisabled(iconBtns[icon.name],
|
|
18439
|
+
iconBtns[icon.name].classed('selected', !off && icon.name == iconVal);
|
|
18440
|
+
setPanelButtonDisabled(iconBtns[icon.name], off);
|
|
17734
18441
|
});
|
|
17735
|
-
return
|
|
18442
|
+
return off;
|
|
17736
18443
|
}
|
|
17737
18444
|
|
|
17738
18445
|
function setIconOn(on) {
|
|
17739
18446
|
applyIcon(on ? lastIconShape : '');
|
|
17740
18447
|
}
|
|
17741
18448
|
|
|
17742
|
-
function updateIconSizeControls(
|
|
17743
|
-
iconSizeInput.setValue(
|
|
18449
|
+
function updateIconSizeControls(shown, iconOff) {
|
|
18450
|
+
iconSizeInput.setValue(shown.value || '');
|
|
18451
|
+
iconSizeInput.setPlaceholder(shown.mixed ? MIXED_TEXT : '');
|
|
17744
18452
|
iconSizeInput.setDisabled(iconOff || !controlsEnabled());
|
|
17745
18453
|
}
|
|
17746
18454
|
|
|
17747
|
-
function updateIconColorControls(
|
|
17748
|
-
|
|
17749
|
-
|
|
17750
|
-
|
|
17751
|
-
|
|
17752
|
-
|
|
17753
|
-
|
|
17754
|
-
|
|
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) {
|
|
18463
|
+
var colorVal = shown.value;
|
|
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()) {
|
|
17755
18473
|
return; // avoid HSB -> RGB -> HSB rounding jumps after picker commits
|
|
17756
18474
|
}
|
|
17757
18475
|
if (isHexColor(colorVal)) {
|
|
17758
|
-
|
|
18476
|
+
picker.setColor(colorVal);
|
|
17759
18477
|
} else {
|
|
17760
|
-
|
|
18478
|
+
picker.hide();
|
|
18479
|
+
}
|
|
18480
|
+
}
|
|
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};
|
|
17761
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};
|
|
17762
18755
|
}
|
|
17763
18756
|
|
|
17764
18757
|
function updateSavedStyleControls() {
|
|
@@ -17766,29 +18759,57 @@
|
|
|
17766
18759
|
}
|
|
17767
18760
|
|
|
17768
18761
|
function getCommonValue(ids, field, opts) {
|
|
18762
|
+
return getCommonValueInfo(ids, field, opts).value;
|
|
18763
|
+
}
|
|
18764
|
+
|
|
18765
|
+
// What the target labels say about one property:
|
|
18766
|
+
//
|
|
18767
|
+
// value: the value they agree on, or '' if they do not
|
|
18768
|
+
// mixed: whether they disagree
|
|
18769
|
+
//
|
|
18770
|
+
// Both, because a control showing nothing has to be able to say which of the
|
|
18771
|
+
// two kinds of nothing it means. Blank is also what an unset property looks
|
|
18772
|
+
// like -- a label carries no letter-spacing until one is chosen -- and a
|
|
18773
|
+
// field that reads the same either way leaves the user to guess whether the
|
|
18774
|
+
// selection is uniform.
|
|
18775
|
+
function getCommonValueInfo(ids, field, opts) {
|
|
17769
18776
|
var table = getActiveTable();
|
|
17770
18777
|
var records = table && table.getRecords();
|
|
17771
18778
|
var value, val, hasValue;
|
|
17772
|
-
if (ids.length === 0)
|
|
17773
|
-
|
|
18779
|
+
if (ids.length === 0) {
|
|
18780
|
+
return {value: getNewLabelValue(field, opts), mixed: false};
|
|
18781
|
+
}
|
|
18782
|
+
if (!records) return {value: '', mixed: false};
|
|
17774
18783
|
for (var i=0; i<ids.length; i++) {
|
|
17775
18784
|
val = records[ids[i]] && records[ids[i]][field];
|
|
17776
18785
|
if (!val) {
|
|
17777
|
-
|
|
17778
|
-
|
|
17779
|
-
|
|
17780
|
-
|
|
17781
|
-
|
|
18786
|
+
// A property nobody set reads as what it renders as, where the caller
|
|
18787
|
+
// says what that is: a label with font-size=12 and one with no
|
|
18788
|
+
// font-size are drawn the same and agree. Without a default the two
|
|
18789
|
+
// disagree, which is right for a property that means nothing when it
|
|
18790
|
+
// is absent -- one label carrying inline css and one not are not in
|
|
18791
|
+
// the same state.
|
|
18792
|
+
val = opts && opts.useDefault ? opts.defaultValue : '';
|
|
17782
18793
|
} else {
|
|
17783
18794
|
hasValue = true;
|
|
17784
18795
|
}
|
|
17785
18796
|
if (i === 0) {
|
|
17786
18797
|
value = val;
|
|
17787
18798
|
} else if (val != value) {
|
|
17788
|
-
return '';
|
|
18799
|
+
return {value: '', mixed: true};
|
|
17789
18800
|
}
|
|
17790
18801
|
}
|
|
17791
|
-
return
|
|
18802
|
+
return {
|
|
18803
|
+
value: hasValue || opts && opts.useDefault ? value : '',
|
|
18804
|
+
mixed: false
|
|
18805
|
+
};
|
|
18806
|
+
}
|
|
18807
|
+
|
|
18808
|
+
// The same, as a control shows it: nothing at all, and nothing to say about
|
|
18809
|
+
// it, while there is no target to read.
|
|
18810
|
+
function getShownValue(ids, field, opts) {
|
|
18811
|
+
if (!controlsEnabled()) return {value: '', mixed: false};
|
|
18812
|
+
return getCommonValueInfo(ids, field, opts);
|
|
17792
18813
|
}
|
|
17793
18814
|
|
|
17794
18815
|
// The alignment the labels are *drawn* with, rather than the one they carry.
|
|
@@ -17944,15 +18965,44 @@
|
|
|
17944
18965
|
addStyleValue(style, labelAlignField, getSelectedAlignment());
|
|
17945
18966
|
addStyleValue(style, cssField, cssInput.node().value.trim());
|
|
17946
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
|
+
}
|
|
17947
18975
|
addStyleValue(style, iconField, icon);
|
|
17948
18976
|
if (icon) {
|
|
17949
18977
|
addStyleValue(style, iconSizeField, iconSizeInput.getValue());
|
|
17950
18978
|
addStyleValue(style, iconColorField, iconColorInput.node().value.trim());
|
|
17951
18979
|
addStyleValue(style, iconOpacityField, getIconOpacityToWrite());
|
|
17952
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
|
+
}
|
|
17953
18995
|
return style;
|
|
17954
18996
|
}
|
|
17955
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
|
+
|
|
17956
19006
|
// A saved style carries an opacity only if it has one to carry: saving at
|
|
17957
19007
|
// 100% and applying it should not write a property the label did not have.
|
|
17958
19008
|
function getOpacityBelowFull(input) {
|
|
@@ -18017,7 +19067,11 @@
|
|
|
18017
19067
|
var ids = getTargetIds();
|
|
18018
19068
|
var styles = [[iconField, iconName || '']];
|
|
18019
19069
|
if (iconName) {
|
|
18020
|
-
|
|
19070
|
+
// The size the symbols in the selection already share, where they share
|
|
19071
|
+
// one: a shape applied to a mixed selection gives the labels that had no
|
|
19072
|
+
// symbol the size of the ones that did, rather than resetting them all to
|
|
19073
|
+
// the default.
|
|
19074
|
+
styles.push([iconSizeField, getNumericSize(getIconValueIds(), iconSizeField, defaultIconSize)]);
|
|
18021
19075
|
// The symbol's own opacity goes on with it, because the label's opacity
|
|
18022
19076
|
// is applied to both elements: without this, text set to 50% would give
|
|
18023
19077
|
// a half-faded symbol while the Icon section showed it at 100%.
|
|
@@ -18029,25 +19083,21 @@
|
|
|
18029
19083
|
applyStyleValues(styles);
|
|
18030
19084
|
}
|
|
18031
19085
|
|
|
18032
|
-
// Switching a symbol on
|
|
18033
|
-
//
|
|
18034
|
-
//
|
|
19086
|
+
// Switching a symbol on moves a label sitting at the centre out from under
|
|
19087
|
+
// it, in the same command, so that the pair is one undo step and the text
|
|
19088
|
+
// is never briefly drawn over the symbol it just asked for.
|
|
18035
19089
|
//
|
|
18036
|
-
//
|
|
18037
|
-
//
|
|
18038
|
-
//
|
|
18039
|
-
//
|
|
18040
|
-
// destructive, since a hand-placed label loses its placement; being one
|
|
18041
|
-
// undoable command is what makes that acceptable.
|
|
19090
|
+
// Switching one off moves nothing. It used to put the label back to the
|
|
19091
|
+
// centre, which threw away a placement the user had made by hand for the
|
|
19092
|
+
// sake of an invariant -- that text with nothing at its anchor sits on the
|
|
19093
|
+
// anchor -- that was never worth what it cost.
|
|
18042
19094
|
//
|
|
18043
19095
|
// Not for path labels, whose text runs along a curve: the commands ignore a
|
|
18044
19096
|
// position given for one and warn about it, and a warning from switching a
|
|
18045
19097
|
// symbol on would be about something the user did not ask for.
|
|
18046
19098
|
function addIconPositionChange(styles, ids, iconOn) {
|
|
18047
|
-
if (anyLabelIsOnAPath(ids)) return;
|
|
18048
|
-
if (
|
|
18049
|
-
styles.push(['label-pos', 'c']);
|
|
18050
|
-
} else if (everyTargetIsCentred(ids)) {
|
|
19099
|
+
if (anyLabelIsOnAPath(ids) || !iconOn) return;
|
|
19100
|
+
if (everyTargetIsCentred(ids)) {
|
|
18051
19101
|
styles.push(['label-pos', labelPositionBesideIcon]);
|
|
18052
19102
|
}
|
|
18053
19103
|
}
|
|
@@ -18061,45 +19111,74 @@
|
|
|
18061
19111
|
}
|
|
18062
19112
|
|
|
18063
19113
|
function applyIconSize(value) {
|
|
18064
|
-
applyStyleValues([[iconSizeField, value]]);
|
|
19114
|
+
applyStyleValues([[iconSizeField, value]], getIconTargetIds());
|
|
18065
19115
|
}
|
|
18066
19116
|
|
|
18067
19117
|
function applyIconColor(color) {
|
|
18068
|
-
applyStyleValues([[iconColorField, color]]);
|
|
19118
|
+
applyStyleValues([[iconColorField, color]], getIconTargetIds());
|
|
18069
19119
|
}
|
|
18070
19120
|
|
|
18071
19121
|
function applyIconOpacity(value) {
|
|
18072
|
-
applyStyleValues([[iconOpacityField, value]]);
|
|
19122
|
+
applyStyleValues([[iconOpacityField, value]], getIconTargetIds());
|
|
18073
19123
|
}
|
|
18074
19124
|
|
|
18075
19125
|
function nudgeIconSize(delta) {
|
|
18076
|
-
var
|
|
18077
|
-
var size = getNumericSize(ids, iconSizeField, defaultIconSize);
|
|
19126
|
+
var size = getNumericSize(getIconTargetIds(), iconSizeField, defaultIconSize);
|
|
18078
19127
|
if (!controlsEnabled()) return;
|
|
18079
19128
|
size = Math.max(1, size + delta);
|
|
18080
19129
|
applyIconSize(size);
|
|
18081
19130
|
}
|
|
18082
19131
|
|
|
19132
|
+
// Whether the target labels have a symbol: 'on', 'off', or 'mixed' when only
|
|
19133
|
+
// some of them do.
|
|
19134
|
+
//
|
|
18083
19135
|
// An icon-size or icon-color on a label with no symbol draws nothing, which
|
|
18084
19136
|
// is why the shape, size and colour controls are inert until the toggle puts
|
|
18085
19137
|
// a symbol there to style.
|
|
18086
|
-
function
|
|
19138
|
+
function getIconState() {
|
|
18087
19139
|
var ids = getTargetIds();
|
|
18088
19140
|
var table = getActiveTable();
|
|
18089
|
-
if (ids.length === 0) return
|
|
18090
|
-
return ids.
|
|
19141
|
+
if (ids.length === 0) return getNewLabelValue(iconField) ? 'on' : 'off';
|
|
19142
|
+
return getToggleState(ids.map(function(id) {
|
|
18091
19143
|
var rec = table && table.getRecordAt(id);
|
|
18092
|
-
return
|
|
19144
|
+
return !!(rec && rec[iconField]);
|
|
19145
|
+
}));
|
|
19146
|
+
}
|
|
19147
|
+
|
|
19148
|
+
// The labels an icon-size, icon-color or icon-opacity goes to: those in the
|
|
19149
|
+
// target that have a symbol, which is all of them unless the switch is mixed.
|
|
19150
|
+
// Writing one of these to a label with no icon would add a property that
|
|
19151
|
+
// draws nothing and a column to the user's table.
|
|
19152
|
+
//
|
|
19153
|
+
// Empty for a target with no symbol anywhere, which is the state those
|
|
19154
|
+
// controls are disabled in, and empty for "new labels", where the style is
|
|
19155
|
+
// held by the tool rather than by any feature.
|
|
19156
|
+
function getIconTargetIds() {
|
|
19157
|
+
var ids = getTargetIds();
|
|
19158
|
+
var table = getActiveTable();
|
|
19159
|
+
return ids.filter(function(id) {
|
|
19160
|
+
var rec = table && table.getRecordAt(id);
|
|
19161
|
+
return !!(rec && rec[iconField]);
|
|
18093
19162
|
});
|
|
18094
19163
|
}
|
|
18095
19164
|
|
|
19165
|
+
// The labels the section's controls *show* the values of, which is not quite
|
|
19166
|
+
// the set they write to: with no symbol anywhere the fields go on showing
|
|
19167
|
+
// what the target carries, greyed, because that is what the symbol comes back
|
|
19168
|
+
// as when the switch is turned on. Narrowing there would show the values held
|
|
19169
|
+
// for the next label instead of the ones stored on the selection.
|
|
19170
|
+
function getIconValueIds() {
|
|
19171
|
+
var ids = getIconTargetIds();
|
|
19172
|
+
return ids.length > 0 ? ids : getTargetIds();
|
|
19173
|
+
}
|
|
19174
|
+
|
|
18096
19175
|
function getIconOpacityToWrite() {
|
|
18097
19176
|
var val = parseOpacityValue(iconOpacityInput.node().value);
|
|
18098
19177
|
if (val !== null) return val;
|
|
18099
19178
|
// The field reads blank while the section is off, so a symbol switched off
|
|
18100
19179
|
// and on again takes back the fade still stored on the label rather than
|
|
18101
19180
|
// being reset to full.
|
|
18102
|
-
var stored = getCommonValue(
|
|
19181
|
+
var stored = getCommonValue(getIconValueIds(), iconOpacityField, {useDefault: true, defaultValue: 1});
|
|
18103
19182
|
val = stored === '' ? 1 : Number(stored);
|
|
18104
19183
|
return isFinite(val) ? val : 1;
|
|
18105
19184
|
}
|
|
@@ -18126,7 +19205,9 @@
|
|
|
18126
19205
|
|
|
18127
19206
|
function hideColorPicker() {
|
|
18128
19207
|
colorPicker.hide();
|
|
19208
|
+
haloColorPicker.hide();
|
|
18129
19209
|
iconColorPicker.hide();
|
|
19210
|
+
calloutColorPicker.hide();
|
|
18130
19211
|
}
|
|
18131
19212
|
|
|
18132
19213
|
function isFormElement(node) {
|
|
@@ -18152,7 +19233,10 @@
|
|
|
18152
19233
|
}
|
|
18153
19234
|
}
|
|
18154
19235
|
|
|
18155
|
-
|
|
19236
|
+
// idsArg: (optional) the labels to write to, when they are not the whole
|
|
19237
|
+
// target -- an icon property goes only to the labels that have a symbol.
|
|
19238
|
+
function applyStyleValues(styles, idsArg) {
|
|
19239
|
+
var ids = idsArg || getTargetIds();
|
|
18156
19240
|
if (styles.length === 0) return;
|
|
18157
19241
|
releaseFocus();
|
|
18158
19242
|
// What the panel is set to is always what the next label gets, whether or
|
|
@@ -18165,8 +19249,8 @@
|
|
|
18165
19249
|
// style, so it has to be redrawn to show the change -- it is the preview.
|
|
18166
19250
|
refreshPendingLabel();
|
|
18167
19251
|
}
|
|
18168
|
-
if (
|
|
18169
|
-
applyStyleCommand(styles);
|
|
19252
|
+
if (ids.length > 0) {
|
|
19253
|
+
applyStyleCommand(styles, ids);
|
|
18170
19254
|
return;
|
|
18171
19255
|
}
|
|
18172
19256
|
if (!labelModeIsOn()) return;
|
|
@@ -18178,9 +19262,8 @@
|
|
|
18178
19262
|
if (session && session.id == -1 && session.refresh) session.refresh();
|
|
18179
19263
|
}
|
|
18180
19264
|
|
|
18181
|
-
function applyStyleCommand(styles) {
|
|
19265
|
+
function applyStyleCommand(styles, ids) {
|
|
18182
19266
|
var lyr = getActiveLayer();
|
|
18183
|
-
var ids = getTargetIds();
|
|
18184
19267
|
var parts = ['-style'];
|
|
18185
19268
|
if (!gui.console || !lyr) return;
|
|
18186
19269
|
styles.forEach(function(style) {
|
|
@@ -18189,6 +19272,7 @@
|
|
|
18189
19272
|
if (ids.length < internal.getFeatureCount(lyr)) {
|
|
18190
19273
|
parts.push('ids=' + ids.join(','));
|
|
18191
19274
|
}
|
|
19275
|
+
parts = parts.concat(getRewrapCommands(styles, ids, lyr));
|
|
18192
19276
|
runGuiEditCommand(gui, parts.join(' '), {
|
|
18193
19277
|
title: 'Label styles',
|
|
18194
19278
|
onDone: function() {
|
|
@@ -18199,6 +19283,29 @@
|
|
|
18199
19283
|
});
|
|
18200
19284
|
}
|
|
18201
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
|
+
|
|
18202
19309
|
// The yellow halo, which belongs to the older label_style mode. The label
|
|
18203
19310
|
// tool draws its selection as an outline instead (gui-label-selection.mjs),
|
|
18204
19311
|
// because a halo says "this text is marked" where an outline says "this is
|
|
@@ -19394,6 +20501,9 @@
|
|
|
19394
20501
|
self.setDefaultTarget([active.layer], active.dataset);
|
|
19395
20502
|
gui.session.setTargetLayer(active.layer); // add -target command to target single layer
|
|
19396
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());
|
|
19397
20507
|
if (flags.select) {
|
|
19398
20508
|
self.dispatchEvent('select', active);
|
|
19399
20509
|
}
|
|
@@ -19990,12 +21100,7 @@
|
|
|
19990
21100
|
var defs = [];
|
|
19991
21101
|
var symbols = lyr.shapes.map(function(shp, i) {
|
|
19992
21102
|
var d = records[i];
|
|
19993
|
-
|
|
19994
|
-
if (!obj) return null;
|
|
19995
|
-
obj.properties.class = addClass(obj.properties.class, 'mapshaper-svg-symbol');
|
|
19996
|
-
obj.properties.transform = getSvgSymbolTransform(shp[0], ext);
|
|
19997
|
-
obj.properties['data-id'] = i;
|
|
19998
|
-
return obj;
|
|
21103
|
+
return shp && d ? renderLayerSymbol(d, shp, ext, view, defs, idPrefix, i) : null;
|
|
19999
21104
|
}).filter(Boolean);
|
|
20000
21105
|
var obj = internal.getEmptyLayerForSVG(lyr, {});
|
|
20001
21106
|
// <defs> has to come first so the layer's own markup can reference it
|
|
@@ -20004,6 +21109,41 @@
|
|
|
20004
21109
|
return internal.svg.stringify(obj);
|
|
20005
21110
|
}
|
|
20006
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
|
+
|
|
20007
21147
|
// Markup for a label that is not in any layer yet: the one being typed into
|
|
20008
21148
|
// before it has been created.
|
|
20009
21149
|
//
|
|
@@ -20540,7 +21680,9 @@
|
|
|
20540
21680
|
// What the click found in the selection as it stood before the click, which
|
|
20541
21681
|
// the click itself is about to replace. The label tool reads this to tell a
|
|
20542
21682
|
// click that selects a label from a click on the label that was already the
|
|
20543
|
-
// 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.
|
|
20544
21686
|
//
|
|
20545
21687
|
// An additive click never qualifies: shift-click means toggle, and it would
|
|
20546
21688
|
// otherwise open a text session on the label it was removing.
|
|
@@ -20551,7 +21693,8 @@
|
|
|
20551
21693
|
var id = hitData.ids.length > 0 ? hitData.ids[0] : -1;
|
|
20552
21694
|
return {
|
|
20553
21695
|
clicked_only_selection: id > -1 && !eventUsesAdditiveSelection(e) &&
|
|
20554
|
-
selectionIds.length == 1 && selectionIds[0] === id
|
|
21696
|
+
selectionIds.length == 1 && selectionIds[0] === id,
|
|
21697
|
+
had_selection: selectionIds.length > 0
|
|
20555
21698
|
};
|
|
20556
21699
|
}
|
|
20557
21700
|
|
|
@@ -24015,32 +25158,248 @@
|
|
|
24015
25158
|
return len > 0 ? geom.distance2D(x1, y1, p[0], p[1]) / len : 0;
|
|
24016
25159
|
}
|
|
24017
25160
|
|
|
24018
|
-
//
|
|
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.
|
|
24019
25163
|
//
|
|
24020
|
-
//
|
|
24021
|
-
//
|
|
24022
|
-
//
|
|
24023
|
-
//
|
|
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.
|
|
24024
25173
|
//
|
|
24025
|
-
//
|
|
24026
|
-
//
|
|
24027
|
-
//
|
|
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.
|
|
24028
25177
|
//
|
|
24029
|
-
//
|
|
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.
|
|
24030
25181
|
|
|
24031
|
-
//
|
|
24032
|
-
//
|
|
24033
|
-
|
|
24034
|
-
|
|
24035
|
-
//
|
|
24036
|
-
//
|
|
24037
|
-
//
|
|
24038
|
-
|
|
24039
|
-
|
|
24040
|
-
|
|
24041
|
-
|
|
24042
|
-
|
|
24043
|
-
|
|
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
|
+
|
|
25377
|
+
// Sliding a path label's text along its curve, and flipping it across.
|
|
25378
|
+
//
|
|
25379
|
+
// Both are one gesture -- a drag on the glyphs, Illustrator's model -- and both
|
|
25380
|
+
// are answered by the same question about the pointer: where it falls on the
|
|
25381
|
+
// curve, and which side of it the pointer is on. The first gives an offset
|
|
25382
|
+
// along the path, the second gives the side; crossing the curve mid-drag flips.
|
|
25383
|
+
//
|
|
25384
|
+
// The math is here, away from the tool, because it is worth testing on its own
|
|
25385
|
+
// and needs nothing from the map: it works on a polyline, which the caller
|
|
25386
|
+
// flattens the curve into at whatever tolerance suits the view.
|
|
25387
|
+
//
|
|
25388
|
+
// See docs/development/label-tool-design.md.
|
|
25389
|
+
|
|
25390
|
+
// Where a point falls on a polyline, or null if there is no polyline to fall
|
|
25391
|
+
// on.
|
|
25392
|
+
//
|
|
25393
|
+
// Returns:
|
|
25394
|
+
// t: position of the nearest point, as a fraction of arc length
|
|
25395
|
+
// side: which side of the path the point is on, as the sign of the cross
|
|
25396
|
+
// product of the segment direction with the offset from it -- 0 when
|
|
25397
|
+
// the point is exactly on the line
|
|
25398
|
+
// dist: how far the point is from the path
|
|
25399
|
+
function projectOntoPolyline(points, p) {
|
|
25400
|
+
var best = null;
|
|
25401
|
+
var before = 0;
|
|
25402
|
+
var total, i, seg;
|
|
24044
25403
|
if (!points || points.length < 2) return null;
|
|
24045
25404
|
for (i = 1; i < points.length; i++) {
|
|
24046
25405
|
seg = projectOntoSegment(points[i - 1], points[i], p);
|
|
@@ -24607,6 +25966,7 @@
|
|
|
24607
25966
|
created: false,
|
|
24608
25967
|
onClose: opts && opts.onClose || null,
|
|
24609
25968
|
startText: decodeLabelText(rec['label-text']),
|
|
25969
|
+
startBreaks: readSoftBreaks(rec['label-text']).breaks,
|
|
24610
25970
|
nodes: null
|
|
24611
25971
|
};
|
|
24612
25972
|
startSession();
|
|
@@ -24651,6 +26011,7 @@
|
|
|
24651
26011
|
created: true,
|
|
24652
26012
|
onClose: opts.onClose || null,
|
|
24653
26013
|
startText: '',
|
|
26014
|
+
startBreaks: [],
|
|
24654
26015
|
nodes: null
|
|
24655
26016
|
};
|
|
24656
26017
|
startSession();
|
|
@@ -24788,10 +26149,15 @@
|
|
|
24788
26149
|
// there is no session to abandon -- it was created before this one, perhaps
|
|
24789
26150
|
// by the script the file was built by.
|
|
24790
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.
|
|
24791
26156
|
function commit(o) {
|
|
24792
26157
|
var blank = textHasNoGlyphs(o.text);
|
|
26158
|
+
var breaks = getBreaks(o);
|
|
24793
26159
|
if (o.pending) {
|
|
24794
|
-
if (!blank) o.pending.create(o.text);
|
|
26160
|
+
if (!blank) o.pending.create(insertSoftBreaks(o.text, breaks));
|
|
24795
26161
|
return;
|
|
24796
26162
|
}
|
|
24797
26163
|
if (blank) {
|
|
@@ -24799,12 +26165,23 @@
|
|
|
24799
26165
|
removeLabel(o);
|
|
24800
26166
|
return;
|
|
24801
26167
|
}
|
|
24802
|
-
if (o.text === o.startText) return;
|
|
24803
|
-
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), {
|
|
24804
26171
|
title: 'Label text'
|
|
24805
26172
|
});
|
|
24806
26173
|
}
|
|
24807
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
|
+
|
|
24808
26185
|
// Deletes the label's feature, geometry and all.
|
|
24809
26186
|
//
|
|
24810
26187
|
// -filter is the command for this: dropping a feature is not a label
|
|
@@ -24887,7 +26264,9 @@
|
|
|
24887
26264
|
// when there is no text, exactly as it does for a committed empty label.
|
|
24888
26265
|
function getPendingRecord(o) {
|
|
24889
26266
|
var rec = Object.assign({}, o.pending.getStyle());
|
|
24890
|
-
|
|
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)));
|
|
24891
26270
|
// No need to expand label-pos here, or to measure its text: the renderer
|
|
24892
26271
|
// resolves the position and asks for the width it needs, so a pending
|
|
24893
26272
|
// label is laid out by exactly the same code as a committed one.
|
|
@@ -24937,12 +26316,14 @@
|
|
|
24937
26316
|
// getRenderedLines() gives it one.
|
|
24938
26317
|
function writeText(o) {
|
|
24939
26318
|
var content = o.nodes.content;
|
|
26319
|
+
var breaks = getBreaks(o);
|
|
26320
|
+
var written = o.text + '\u0000' + breaks.join(',');
|
|
24940
26321
|
var lines, i, tspan;
|
|
24941
26322
|
// refresh() runs on every map render, which during a pan is every frame;
|
|
24942
26323
|
// rebuilding text nodes that already say the right thing is pure waste
|
|
24943
|
-
if (o.writtenTo === content && o.writtenText ===
|
|
26324
|
+
if (o.writtenTo === content && o.writtenText === written) return;
|
|
24944
26325
|
o.writtenTo = content;
|
|
24945
|
-
o.writtenText =
|
|
26326
|
+
o.writtenText = written;
|
|
24946
26327
|
o.nodes.text.setAttributeNS(XML_NS, 'space', 'preserve');
|
|
24947
26328
|
while (content.firstChild) content.removeChild(content.firstChild);
|
|
24948
26329
|
// A <textPath> runs along its baseline, so a second line would advance
|
|
@@ -24954,7 +26335,10 @@
|
|
|
24954
26335
|
content.appendChild(document.createTextNode(getRenderedContent(o.text)));
|
|
24955
26336
|
return;
|
|
24956
26337
|
}
|
|
24957
|
-
|
|
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);
|
|
24958
26342
|
content.appendChild(document.createTextNode(lines[0]));
|
|
24959
26343
|
for (i = 1; i < lines.length; i++) {
|
|
24960
26344
|
tspan = document.createElementNS(SVG_NS$3, 'tspan');
|
|
@@ -25040,6 +26424,7 @@
|
|
|
25040
26424
|
if (pathId) {
|
|
25041
26425
|
groups.back.appendChild(ghostPath(pathId));
|
|
25042
26426
|
} else if (box) {
|
|
26427
|
+
appendColumn(o, groups.back, box);
|
|
25043
26428
|
groups.back.appendChild(rect(box, 'label-edit-box'));
|
|
25044
26429
|
}
|
|
25045
26430
|
getSelectionRects(provider, bands[0], bands[1], rendered).forEach(function(r) {
|
|
@@ -25050,6 +26435,22 @@
|
|
|
25050
26435
|
syncTransform(o, groups);
|
|
25051
26436
|
}
|
|
25052
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
|
+
|
|
25053
26454
|
// Keeps clicks near the label inside the session instead of ending it.
|
|
25054
26455
|
//
|
|
25055
26456
|
// Glyph-precise hit testing is not enough for this: a curved label's bounding
|
|
@@ -25353,6 +26754,8 @@
|
|
|
25353
26754
|
var BOX_PADDING = 3;
|
|
25354
26755
|
var ANCHOR_RADIUS = 3.5;
|
|
25355
26756
|
var KNOT_RADIUS = 3;
|
|
26757
|
+
var WIDTH_HANDLE_SIZE = 6;
|
|
26758
|
+
var GAP_HANDLE_RADIUS = 2.5;
|
|
25356
26759
|
|
|
25357
26760
|
// How many labels get an outline before the cue falls back to the halo.
|
|
25358
26761
|
//
|
|
@@ -25361,13 +26764,26 @@
|
|
|
25361
26764
|
// layer instead of rebuilding it, and the cue moves with the map by wearing the
|
|
25362
26765
|
// label's own transform -- but a select-all on a big layer would still pay it,
|
|
25363
26766
|
// for an outline per label too small to tell apart anyway.
|
|
26767
|
+
//
|
|
26768
|
+
// Over the cap the selected labels wear a halo instead (.label-cue-marked),
|
|
26769
|
+
// which is a class on the text node and costs no measurement. It says less
|
|
26770
|
+
// than an outline -- no anchors, no knots, no box -- but a selection of
|
|
26771
|
+
// hundreds is a group being restyled rather than objects being handled one by
|
|
26772
|
+
// one, and the one thing it has to say is which labels are in it. This used to
|
|
26773
|
+
// draw nothing at all, so selecting a whole layer left the map unchanged.
|
|
25364
26774
|
var MAX_OUTLINES = 200;
|
|
25365
26775
|
|
|
25366
26776
|
// getEditingId: returns the feature id of an open text editing session, or -1.
|
|
25367
26777
|
// A label being typed into draws its own box and does not want a second one.
|
|
25368
|
-
|
|
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) {
|
|
25369
26784
|
var self = {};
|
|
25370
26785
|
var groups = []; // one <g> per drawn cue, in the layer's markup
|
|
26786
|
+
var marked = []; // text nodes wearing the halo, when there are too many to outline
|
|
25371
26787
|
var drawn = null; // what those cues represent, so hover does not redraw them
|
|
25372
26788
|
var on = false;
|
|
25373
26789
|
var tetherId = -1; // the label whose text is being dragged off its anchor
|
|
@@ -25397,9 +26813,11 @@
|
|
|
25397
26813
|
// markup and takes the old cues with it.
|
|
25398
26814
|
self.refresh = function(force) {
|
|
25399
26815
|
var target = hit.getHitTarget();
|
|
25400
|
-
var
|
|
25401
|
-
var
|
|
25402
|
-
var
|
|
26816
|
+
var selected = on ? hit.getSelectionIds() : [];
|
|
26817
|
+
var tooMany = selected.length > MAX_OUTLINES;
|
|
26818
|
+
var ids = tooMany ? [] : selected;
|
|
26819
|
+
var hoverId = on ? getHoverId(selected) : -1;
|
|
26820
|
+
var key = selected.join(',') + '/' + hoverId + '/' + tetherId;
|
|
25403
26821
|
// Hover fires on every pointer move, and most of them change nothing here.
|
|
25404
26822
|
if (!force && drawn === key) return;
|
|
25405
26823
|
clearAll();
|
|
@@ -25414,6 +26832,9 @@
|
|
|
25414
26832
|
// would offer handles that cannot be grabbed.
|
|
25415
26833
|
draw(target, id, 'label-cue-selected', true);
|
|
25416
26834
|
});
|
|
26835
|
+
// Too many to outline: a halo on the glyphs instead, which is the whole cue
|
|
26836
|
+
// for those labels.
|
|
26837
|
+
if (tooMany) markAll(target, selected);
|
|
25417
26838
|
};
|
|
25418
26839
|
|
|
25419
26840
|
// The label under the pointer, when showing it would say something: not one
|
|
@@ -25425,10 +26846,23 @@
|
|
|
25425
26846
|
return id;
|
|
25426
26847
|
}
|
|
25427
26848
|
|
|
25428
|
-
//
|
|
25429
|
-
|
|
25430
|
-
|
|
25431
|
-
|
|
26849
|
+
// Puts the halo on each selected label's glyphs. A class of its own rather
|
|
26850
|
+
// than the label_style mode's yellow one: that class is cleared on every
|
|
26851
|
+
// model update while the label tool is on, so the two would fight.
|
|
26852
|
+
function markAll(target, ids) {
|
|
26853
|
+
ids.forEach(function(id) {
|
|
26854
|
+
var nodes = findNodes(target, id);
|
|
26855
|
+
if (!nodes) return;
|
|
26856
|
+
nodes.text.classList.add('label-cue-marked');
|
|
26857
|
+
marked.push(nodes.text);
|
|
26858
|
+
});
|
|
26859
|
+
}
|
|
26860
|
+
|
|
26861
|
+
function clearMarks() {
|
|
26862
|
+
marked.forEach(function(node) {
|
|
26863
|
+
node.classList.remove('label-cue-marked');
|
|
26864
|
+
});
|
|
26865
|
+
marked = [];
|
|
25432
26866
|
}
|
|
25433
26867
|
|
|
25434
26868
|
function draw(target, id, className, withHandles) {
|
|
@@ -25441,7 +26875,7 @@
|
|
|
25441
26875
|
g.appendChild(curve(nodes.pathId));
|
|
25442
26876
|
if (withHandles) appendKnotHandles(g, target, id);
|
|
25443
26877
|
} else {
|
|
25444
|
-
appendAnchoredCue(g, nodes, rec, id);
|
|
26878
|
+
appendAnchoredCue(g, nodes, rec, id, withHandles ? target : null);
|
|
25445
26879
|
}
|
|
25446
26880
|
groups.push(g);
|
|
25447
26881
|
}
|
|
@@ -25457,14 +26891,76 @@
|
|
|
25457
26891
|
// ring inside it would. What is left -- offset text with nothing at its
|
|
25458
26892
|
// anchor -- is the case where the ring is the only thing that says what the
|
|
25459
26893
|
// text hangs off.
|
|
25460
|
-
|
|
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) {
|
|
25461
26907
|
var box = measure(nodes.content);
|
|
26908
|
+
var o = box && handleTarget && getHandles ? getHandles(handleTarget, id, box) : null;
|
|
26909
|
+
var column = o ? o.column : null;
|
|
25462
26910
|
if (!box) return;
|
|
26911
|
+
if (column) g.appendChild(columnRect(box, column, BOX_PADDING));
|
|
25463
26912
|
if (box.width || box.height) g.appendChild(rect(box, BOX_PADDING));
|
|
25464
26913
|
if (id === tetherId) g.appendChild(tether(box));
|
|
25465
26914
|
if (!internal.featureHasSvgSymbol(rec) && !boxHoldsOrigin(box)) {
|
|
25466
26915
|
g.appendChild(anchorMarker());
|
|
25467
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;
|
|
25468
26964
|
}
|
|
25469
26965
|
|
|
25470
26966
|
// Whether the label's anchor point is inside the box drawn around its text.
|
|
@@ -25502,7 +26998,8 @@
|
|
|
25502
26998
|
var content;
|
|
25503
26999
|
if (!text) return null;
|
|
25504
27000
|
content = text.querySelector('textPath') || text;
|
|
25505
|
-
return {symbol: symbol,
|
|
27001
|
+
return {symbol: symbol, text: text, content: content,
|
|
27002
|
+
pathId: getPathId(content)};
|
|
25506
27003
|
}
|
|
25507
27004
|
|
|
25508
27005
|
// The id of the baseline a path label is laid along, or null for an anchored
|
|
@@ -25599,12 +27096,183 @@
|
|
|
25599
27096
|
if (g.parentNode) g.parentNode.removeChild(g);
|
|
25600
27097
|
});
|
|
25601
27098
|
groups = [];
|
|
27099
|
+
clearMarks();
|
|
25602
27100
|
drawn = null;
|
|
25603
27101
|
}
|
|
25604
27102
|
|
|
25605
27103
|
return self;
|
|
25606
27104
|
}
|
|
25607
27105
|
|
|
27106
|
+
// Selecting a group of labels by pointing at one of them: every label on the
|
|
27107
|
+
// layer, or the ones that share its text style, its colour or its symbol.
|
|
27108
|
+
//
|
|
27109
|
+
// Styling a group is something the panel could already do -- a control shows
|
|
27110
|
+
// nothing when the selected labels disagree, and writes to all of them -- but
|
|
27111
|
+
// there was no way to select more than a shift-click's worth. These are the
|
|
27112
|
+
// predicates the context menu offers, as pure functions so that they can be
|
|
27113
|
+
// tested without a map.
|
|
27114
|
+
//
|
|
27115
|
+
// See docs/development/label-tool-design.md.
|
|
27116
|
+
|
|
27117
|
+
// The properties that make up "the same text style": how the glyphs are drawn,
|
|
27118
|
+
// and nothing about where the text sits.
|
|
27119
|
+
//
|
|
27120
|
+
// Alignment, position, offsets and a path label's start offset are left out
|
|
27121
|
+
// deliberately. They are what the panel's other sections edit, and two labels
|
|
27122
|
+
// in the same font at the same size are in the same text style whether one of
|
|
27123
|
+
// them is centred and the other hangs north-east of its anchor -- which is
|
|
27124
|
+
// usually the difference between a label placed by hand and one that was not.
|
|
27125
|
+
//
|
|
27126
|
+
// css and class are in, because either can carry anything the properties above
|
|
27127
|
+
// carry and more: two labels are not in the same text style if one of them is
|
|
27128
|
+
// wearing a stylesheet the other is not.
|
|
27129
|
+
var TEXT_STYLE_FIELDS = [
|
|
27130
|
+
'font-family', 'font-size', 'font-style', 'font-weight', 'font-stretch',
|
|
27131
|
+
'letter-spacing', 'line-height', 'css', 'class'
|
|
27132
|
+
];
|
|
27133
|
+
|
|
27134
|
+
// The colour of the text, and not its opacity: the item says colour, and a
|
|
27135
|
+
// label faded to 50% is the same colour as one that is not.
|
|
27136
|
+
var FILL_FIELDS = ['fill'];
|
|
27137
|
+
|
|
27138
|
+
// The shape of the symbol, and not its size or colour, for the same reason.
|
|
27139
|
+
var ICON_FIELDS = ['icon'];
|
|
27140
|
+
|
|
27141
|
+
// What a property means when a label does not carry it.
|
|
27142
|
+
//
|
|
27143
|
+
// Compared after this substitution, so that a label with font-size=12 and one
|
|
27144
|
+
// with no font-size match: they render identically, and the panel already
|
|
27145
|
+
// treats them as agreeing. Without it the commonest pair of labels on a map --
|
|
27146
|
+
// one the panel has touched and one it has not -- would count as different
|
|
27147
|
+
// text styles while looking the same.
|
|
27148
|
+
//
|
|
27149
|
+
// font-family is deliberately absent. An unset font is whatever the browser
|
|
27150
|
+
// resolves sans-serif to, which differs by machine and is a real difference
|
|
27151
|
+
// from a font the user named, so an unset font is its own value and matches
|
|
27152
|
+
// only another unset one. css, class and icon are absent because they have no
|
|
27153
|
+
// default: not having one is not the same as having a particular one.
|
|
27154
|
+
var STYLE_DEFAULTS = {
|
|
27155
|
+
'font-size': 12,
|
|
27156
|
+
'font-style': 'normal',
|
|
27157
|
+
'font-weight': '400',
|
|
27158
|
+
'letter-spacing': 0,
|
|
27159
|
+
fill: '#000000'
|
|
27160
|
+
};
|
|
27161
|
+
|
|
27162
|
+
// The items the "select" section offers, in menu order.
|
|
27163
|
+
//
|
|
27164
|
+
// needs: a property the pointed-at label must carry for the item to be worth
|
|
27165
|
+
// offering. "Same icon" on a label with no symbol would select every label
|
|
27166
|
+
// that has none, which is a question nobody asked and which "all labels"
|
|
27167
|
+
// nearly answers anyway.
|
|
27168
|
+
// always: offered whatever it matches, rather than only when it says something
|
|
27169
|
+
// the other items do not. See getLabelSelectActions().
|
|
27170
|
+
var SELECT_KINDS = [
|
|
27171
|
+
{name: 'all', label: 'all labels'},
|
|
27172
|
+
{name: 'text-style', label: 'same text style', fields: TEXT_STYLE_FIELDS,
|
|
27173
|
+
always: true},
|
|
27174
|
+
{name: 'fill', label: 'same fill color', fields: FILL_FIELDS},
|
|
27175
|
+
{name: 'icon', label: 'same icon', fields: ICON_FIELDS, needs: 'icon'}
|
|
27176
|
+
];
|
|
27177
|
+
|
|
27178
|
+
// Every label on the layer, in ascending id order.
|
|
27179
|
+
//
|
|
27180
|
+
// Not every feature: a labels layer can hold plain points as well -- a dots
|
|
27181
|
+
// layer given label text by the point panel is one -- and a point is not
|
|
27182
|
+
// something the label panel can style.
|
|
27183
|
+
//
|
|
27184
|
+
// records: the layer's data records, or null
|
|
27185
|
+
// isLabel: (rec) -> whether a record is a label at all. Injected rather than
|
|
27186
|
+
// imported so that this module stays testable without the GUI's internals;
|
|
27187
|
+
// the caller passes internal.svg.featureIsLabel.
|
|
27188
|
+
function getAllLabelIds(records, isLabel) {
|
|
27189
|
+
var ids = [];
|
|
27190
|
+
var i;
|
|
27191
|
+
for (i = 0; records && i < records.length; i++) {
|
|
27192
|
+
if (isLabel(records[i])) ids.push(i);
|
|
27193
|
+
}
|
|
27194
|
+
return ids;
|
|
27195
|
+
}
|
|
27196
|
+
|
|
27197
|
+
// The labels that match label @id on @kind, in ascending id order.
|
|
27198
|
+
//
|
|
27199
|
+
// records: the layer's data records, or null
|
|
27200
|
+
// id: the label the predicate is taken from, which is always in the result
|
|
27201
|
+
// kind: 'all', 'text-style', 'fill' or 'icon'
|
|
27202
|
+
// isLabel: as getAllLabelIds()
|
|
27203
|
+
function getMatchingLabelIds(records, id, kind, isLabel) {
|
|
27204
|
+
var defn = findKind(kind);
|
|
27205
|
+
var rec = records && records[id];
|
|
27206
|
+
if (!defn || !rec || !isLabel(rec)) return [];
|
|
27207
|
+
return getAllLabelIds(records, isLabel).filter(function(i) {
|
|
27208
|
+
return !defn.fields || fieldsMatch(records[i], rec, defn.fields);
|
|
27209
|
+
});
|
|
27210
|
+
}
|
|
27211
|
+
|
|
27212
|
+
// The menu items worth offering for a right-click on label @id, as
|
|
27213
|
+
// [{name, label, ids}, ...].
|
|
27214
|
+
//
|
|
27215
|
+
// "All labels" and "same text style" are always offered, the first because it
|
|
27216
|
+
// asks nothing of the layer and the second because it is the item this section
|
|
27217
|
+
// is mostly for: styling the type of a map as a group. Offering it only when it
|
|
27218
|
+
// differs from "all labels" would make it come and go between one right-click
|
|
27219
|
+
// and the next, and its count is worth reading either way -- "same text style
|
|
27220
|
+
// 40" over a layer of 40 says the labels are uniform, which is a fact about the
|
|
27221
|
+
// layer and not a reason to hide the item.
|
|
27222
|
+
//
|
|
27223
|
+
// The rest are dropped rather than shown and disabled when:
|
|
27224
|
+
//
|
|
27225
|
+
// - They would select the label already pointed at and nothing else, which
|
|
27226
|
+
// would look like a way of narrowing the selection to one label -- a plain
|
|
27227
|
+
// click on that label already is one.
|
|
27228
|
+
// - They match every label on the layer, which "all labels" says more plainly.
|
|
27229
|
+
// A layer with one label on it gets no section at all: every item there would
|
|
27230
|
+
// select the label that was right-clicked, which clicking it already does.
|
|
27231
|
+
function getLabelSelectActions(records, id, isLabel) {
|
|
27232
|
+
var rec = records && records[id];
|
|
27233
|
+
var all = getMatchingLabelIds(records, id, 'all', isLabel);
|
|
27234
|
+
var out = [];
|
|
27235
|
+
if (!rec || all.length < 2) return out;
|
|
27236
|
+
SELECT_KINDS.forEach(function(defn) {
|
|
27237
|
+
var always = defn.name == 'all' || defn.always;
|
|
27238
|
+
var ids;
|
|
27239
|
+
if (defn.needs && !hasValue(rec[defn.needs])) return;
|
|
27240
|
+
ids = defn.name == 'all' ? all :
|
|
27241
|
+
getMatchingLabelIds(records, id, defn.name, isLabel);
|
|
27242
|
+
if (!always && (ids.length < 2 || ids.length === all.length)) return;
|
|
27243
|
+
out.push({name: defn.name, label: defn.label, ids: ids});
|
|
27244
|
+
});
|
|
27245
|
+
return out;
|
|
27246
|
+
}
|
|
27247
|
+
|
|
27248
|
+
function findKind(name) {
|
|
27249
|
+
var found = null;
|
|
27250
|
+
SELECT_KINDS.forEach(function(defn) {
|
|
27251
|
+
if (defn.name == name) found = defn;
|
|
27252
|
+
});
|
|
27253
|
+
return found;
|
|
27254
|
+
}
|
|
27255
|
+
|
|
27256
|
+
function fieldsMatch(a, b, fields) {
|
|
27257
|
+
return fields.every(function(field) {
|
|
27258
|
+
return styleValue(a, field) === styleValue(b, field);
|
|
27259
|
+
});
|
|
27260
|
+
}
|
|
27261
|
+
|
|
27262
|
+
// A property as it is compared: what the label carries, or what it means to
|
|
27263
|
+
// carry nothing, as a string so that 12 and '12' are one value.
|
|
27264
|
+
function styleValue(rec, field) {
|
|
27265
|
+
var val = rec ? rec[field] : null;
|
|
27266
|
+
if (!hasValue(val)) {
|
|
27267
|
+
val = field in STYLE_DEFAULTS ? STYLE_DEFAULTS[field] : '';
|
|
27268
|
+
}
|
|
27269
|
+
return String(val);
|
|
27270
|
+
}
|
|
27271
|
+
|
|
27272
|
+
function hasValue(val) {
|
|
27273
|
+
return !(val === undefined || val === null || val === '');
|
|
27274
|
+
}
|
|
27275
|
+
|
|
25608
27276
|
// State of a label curve being drawn, and the operations the curvature tool
|
|
25609
27277
|
// performs on it.
|
|
25610
27278
|
//
|
|
@@ -25759,8 +27427,17 @@
|
|
|
25759
27427
|
// resolve and still leaves a short polyline to walk per mouse move.
|
|
25760
27428
|
var FLATTEN_PX = 0.5;
|
|
25761
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
|
+
|
|
25762
27438
|
function initLabelTool(gui, ext, hit) {
|
|
25763
|
-
// 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.
|
|
25764
27441
|
var armed = null;
|
|
25765
27442
|
var curve = createCurveState();
|
|
25766
27443
|
// The pointer's position while a curve is being drawn, in display
|
|
@@ -25783,11 +27460,27 @@
|
|
|
25783
27460
|
// coordinates. A drag on a label's text starts from here rather than from
|
|
25784
27461
|
// the dragstart event -- see beginTextDrag().
|
|
25785
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;
|
|
25786
27479
|
var editor = new LabelEditor(gui, ext);
|
|
25787
27480
|
var selection = new LabelSelection(gui, ext, hit, function() {
|
|
25788
27481
|
return editor.isOpen() ? editor.getFeatureId() : -1;
|
|
25789
|
-
});
|
|
25790
|
-
var toolbar, anchorBtn, pathBtn, alert;
|
|
27482
|
+
}, getLabelHandles);
|
|
27483
|
+
var toolbar, anchorBtn, blockBtn, pathBtn, alert;
|
|
25791
27484
|
|
|
25792
27485
|
gui.addMode('label_tool', turnOn, turnOff);
|
|
25793
27486
|
|
|
@@ -25859,6 +27552,11 @@
|
|
|
25859
27552
|
hideInstructions();
|
|
25860
27553
|
drag = null;
|
|
25861
27554
|
textDrag = null;
|
|
27555
|
+
handleDrag = null;
|
|
27556
|
+
releasedDrag = null;
|
|
27557
|
+
blockRelease = null;
|
|
27558
|
+
shownHandles = null;
|
|
27559
|
+
clearBlockDrag();
|
|
25862
27560
|
hoverHandle = null;
|
|
25863
27561
|
hoverTextId = -1;
|
|
25864
27562
|
hoverPoint = null;
|
|
@@ -25866,6 +27564,8 @@
|
|
|
25866
27564
|
// go back whether or not it was a crosshair.
|
|
25867
27565
|
gui.container.findChild('.map-layers').classed('label-tool', false);
|
|
25868
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);
|
|
25869
27569
|
if (toolbar) toolbar.hide();
|
|
25870
27570
|
if (gui.interaction.getMode() == 'label') {
|
|
25871
27571
|
// the mode change came from somewhere other than the mode menu
|
|
@@ -25891,6 +27591,14 @@
|
|
|
25891
27591
|
}).on('click', function() {
|
|
25892
27592
|
setArmed(armed == 'anchor' ? null : 'anchor');
|
|
25893
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
|
+
});
|
|
25894
27602
|
pathBtn = toolbar.addButton('#curved-text-icon', {
|
|
25895
27603
|
tooltip: 'Add a label along a path'
|
|
25896
27604
|
}).on('click', function() {
|
|
@@ -25910,6 +27618,7 @@
|
|
|
25910
27618
|
function updateButtons() {
|
|
25911
27619
|
if (!toolbar) return;
|
|
25912
27620
|
anchorBtn.setSelected(armed == 'anchor');
|
|
27621
|
+
blockBtn.setSelected(armed == 'block');
|
|
25913
27622
|
pathBtn.setSelected(armed == 'path');
|
|
25914
27623
|
updateCursor();
|
|
25915
27624
|
}
|
|
@@ -25923,22 +27632,34 @@
|
|
|
25923
27632
|
// wrong even with a tool armed.
|
|
25924
27633
|
function updateCursor() {
|
|
25925
27634
|
var el = gui.container.findChild('.map-layers');
|
|
25926
|
-
//
|
|
25927
|
-
//
|
|
25928
|
-
|
|
25929
|
-
|
|
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;
|
|
25930
27643
|
el.classed('label-tool', placing);
|
|
25931
|
-
el.classed('label-
|
|
27644
|
+
el.classed('label-text-drag', onText);
|
|
27645
|
+
el.classed('label-handle', onHandle && !resizing);
|
|
27646
|
+
el.classed('label-resize', resizing);
|
|
25932
27647
|
}
|
|
25933
27648
|
|
|
25934
27649
|
function showInstructions() {
|
|
27650
|
+
var msg;
|
|
25935
27651
|
hideInstructions();
|
|
25936
27652
|
if (!armed) return;
|
|
25937
|
-
|
|
25938
|
-
'Click on the map to place a label.'
|
|
25939
|
-
|
|
25940
|
-
'to
|
|
25941
|
-
|
|
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'});
|
|
25942
27663
|
}
|
|
25943
27664
|
|
|
25944
27665
|
function hideInstructions() {
|
|
@@ -25954,10 +27675,20 @@
|
|
|
25954
27675
|
// editing acts on exactly one and has nothing to do with the panel.
|
|
25955
27676
|
hit.on('click', function(e) {
|
|
25956
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
|
+
}
|
|
25957
27684
|
if (armed == 'path' && drawingCurve()) {
|
|
25958
27685
|
extendCurve(pixToMapCoords(e.x, e.y));
|
|
25959
27686
|
return;
|
|
25960
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;
|
|
25961
27692
|
// A click away from the text finishes the label being typed into, and does
|
|
25962
27693
|
// nothing else: leaving one label is its own gesture, and placing the next
|
|
25963
27694
|
// one takes another click. This is also what makes one undo step cover one
|
|
@@ -25982,10 +27713,18 @@
|
|
|
25982
27713
|
if (e.id > -1 && clickLabel(e.id, e)) return;
|
|
25983
27714
|
if (editor.isOpen()) return; // a click within the text moved the caret
|
|
25984
27715
|
// Nothing was hit. Any selection is now over with, and the click means
|
|
25985
|
-
// 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
|
+
}
|
|
25986
27723
|
deselectLabels();
|
|
25987
27724
|
if (armed == 'anchor') {
|
|
25988
|
-
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);
|
|
25989
27728
|
} else if (armed == 'path') {
|
|
25990
27729
|
extendCurve(pixToMapCoords(e.x, e.y));
|
|
25991
27730
|
}
|
|
@@ -26004,6 +27743,7 @@
|
|
|
26004
27743
|
if (!active()) return;
|
|
26005
27744
|
id = getRightClickedLabel(e);
|
|
26006
27745
|
if (target && id > -1) {
|
|
27746
|
+
e.selectActions = getSelectActions(target, id);
|
|
26007
27747
|
e.deleteFeature = getDeleteAction(target, id);
|
|
26008
27748
|
// Nobody guesses that text is dragged across its own curve, and there is
|
|
26009
27749
|
// no bracket drawn to suggest it, so the gesture has a second way in.
|
|
@@ -26032,6 +27772,43 @@
|
|
|
26032
27772
|
return -1;
|
|
26033
27773
|
}
|
|
26034
27774
|
|
|
27775
|
+
// The "select" items for a right-click on label @id: the labels that share
|
|
27776
|
+
// something with it, so that a style can be changed across a group without
|
|
27777
|
+
// shift-clicking every one of it.
|
|
27778
|
+
//
|
|
27779
|
+
// The predicate comes from the label pointed at rather than from whatever
|
|
27780
|
+
// happens to be selected -- a right-click deliberately leaves the selection
|
|
27781
|
+
// alone -- and running an item replaces the selection with what it matched.
|
|
27782
|
+
function getSelectActions(target, id) {
|
|
27783
|
+
var records = target.data ? target.data.getRecords() : null;
|
|
27784
|
+
return getLabelSelectActions(records, id, internal.svg.featureIsLabel)
|
|
27785
|
+
.map(function(action) {
|
|
27786
|
+
return {
|
|
27787
|
+
label: action.label,
|
|
27788
|
+
count: action.ids.length,
|
|
27789
|
+
run: function() { selectLabels(action.ids); }
|
|
27790
|
+
};
|
|
27791
|
+
});
|
|
27792
|
+
}
|
|
27793
|
+
|
|
27794
|
+
// A label being typed into is not a label being styled, so a group selection
|
|
27795
|
+
// ends the session -- which saves its text, as clicking away would. Closing
|
|
27796
|
+
// first and selecting second, because closing puts the label it was editing
|
|
27797
|
+
// back in the selection when the selection is empty.
|
|
27798
|
+
function selectLabels(ids) {
|
|
27799
|
+
if (editor.isOpen()) editor.close();
|
|
27800
|
+
hit.setSelectionIds(ids);
|
|
27801
|
+
}
|
|
27802
|
+
|
|
27803
|
+
// Cmd/Ctrl-A: the keyboard way to the menu's "all labels", for the commonest
|
|
27804
|
+
// of the group selections -- restyling a layer's labels as one.
|
|
27805
|
+
function selectAllLabels() {
|
|
27806
|
+
var target = hit.getHitTarget();
|
|
27807
|
+
var records = target && target.data ? target.data.getRecords() : null;
|
|
27808
|
+
var ids = getAllLabelIds(records, internal.svg.featureIsLabel);
|
|
27809
|
+
if (ids.length > 0) selectLabels(ids);
|
|
27810
|
+
}
|
|
27811
|
+
|
|
26035
27812
|
// Deleting a label ends whatever was being done to it first: its text
|
|
26036
27813
|
// session would otherwise write the text back to an id that now belongs to
|
|
26037
27814
|
// the label that moved up into the gap, and the same shift is why the
|
|
@@ -26145,15 +27922,19 @@
|
|
|
26145
27922
|
return editor.ownsNode(getEventNode(e));
|
|
26146
27923
|
}
|
|
26147
27924
|
|
|
26148
|
-
// The DOM node a hit event came from.
|
|
26149
|
-
// mouse event, whose own originalEvent is the DOM one, so the node is two
|
|
26150
|
-
// levels down.
|
|
27925
|
+
// The DOM node a hit event came from.
|
|
26151
27926
|
function getEventNode(e) {
|
|
26152
|
-
var
|
|
26153
|
-
var domEvt = mouseEvt && mouseEvt.originalEvent || mouseEvt;
|
|
27927
|
+
var domEvt = getDomEvent(e);
|
|
26154
27928
|
return domEvt && domEvt.target || null;
|
|
26155
27929
|
}
|
|
26156
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
|
+
|
|
26157
27938
|
function deselectLabels() {
|
|
26158
27939
|
if (hit.getSelectionIds().length > 0) hit.clearSelection();
|
|
26159
27940
|
}
|
|
@@ -26188,6 +27969,7 @@
|
|
|
26188
27969
|
// find its nodes again and write the text being edited back into them, and
|
|
26189
27970
|
// the selection cues have to be drawn onto the new markup.
|
|
26190
27971
|
gui.on('map_rendered', function(e) {
|
|
27972
|
+
if (!e || e.action != 'hover') releasedDrag = null;
|
|
26191
27973
|
editor.refresh();
|
|
26192
27974
|
// The cues went with the old markup, so they have to be drawn again --
|
|
26193
27975
|
// except after a 'hover' draw, which leaves the SVG alone. That draw now
|
|
@@ -26242,7 +28024,14 @@
|
|
|
26242
28024
|
if (beginTextDrag(e)) consumeDrag(e);
|
|
26243
28025
|
return;
|
|
26244
28026
|
}
|
|
26245
|
-
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
|
+
}
|
|
26246
28035
|
beginKnotDrag(hoverHandle);
|
|
26247
28036
|
consumeDrag(e);
|
|
26248
28037
|
});
|
|
@@ -26277,6 +28066,16 @@
|
|
|
26277
28066
|
updateTextDrag(e);
|
|
26278
28067
|
return;
|
|
26279
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
|
+
}
|
|
26280
28079
|
if (!drag) return;
|
|
26281
28080
|
consumeDrag(e);
|
|
26282
28081
|
shp = getDisplayShapes(drag.target)[drag.id];
|
|
@@ -26299,6 +28098,7 @@
|
|
|
26299
28098
|
if (textDrag) {
|
|
26300
28099
|
o = textDrag;
|
|
26301
28100
|
textDrag = null;
|
|
28101
|
+
if (o.moved) releasedDrag = o;
|
|
26302
28102
|
consumeDrag(e);
|
|
26303
28103
|
selection.setTether(-1);
|
|
26304
28104
|
// A press that never moved is a click, which the click handler has
|
|
@@ -26306,6 +28106,22 @@
|
|
|
26306
28106
|
if (o.moved) commitTextDrag(o);
|
|
26307
28107
|
return;
|
|
26308
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
|
+
}
|
|
26309
28125
|
if (!o) return;
|
|
26310
28126
|
consumeDrag(e);
|
|
26311
28127
|
drag = null;
|
|
@@ -26340,6 +28156,301 @@
|
|
|
26340
28156
|
});
|
|
26341
28157
|
}
|
|
26342
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
|
+
|
|
26343
28454
|
// What a drag on a selected label's glyphs means, which depends on the kind
|
|
26344
28455
|
// of label and -- for an anchored one -- on the tool's position mode.
|
|
26345
28456
|
//
|
|
@@ -26352,7 +28463,7 @@
|
|
|
26352
28463
|
var shp = id > -1 ? getDisplayShapes(target)[id] : null;
|
|
26353
28464
|
if (id < 0 || !shp) return false;
|
|
26354
28465
|
if (isPathLabel(target, id)) return beginPathTextDrag(e, target, id);
|
|
26355
|
-
if (
|
|
28466
|
+
if (textIsDraggable(target, id)) return beginOffsetDrag(e, target, id);
|
|
26356
28467
|
beginKnotDrag({
|
|
26357
28468
|
id: id,
|
|
26358
28469
|
index: 0,
|
|
@@ -26487,6 +28598,8 @@
|
|
|
26487
28598
|
kind: 'offset',
|
|
26488
28599
|
id: id,
|
|
26489
28600
|
target: target,
|
|
28601
|
+
rec: rec,
|
|
28602
|
+
previewRec: null,
|
|
26490
28603
|
// dx and dy are in the space inside the label's symbol group, so pointer
|
|
26491
28604
|
// movement is divided by the scale that group wears.
|
|
26492
28605
|
scale: ext.getSymbolScale() || 1,
|
|
@@ -26503,7 +28616,8 @@
|
|
|
26503
28616
|
// multi-line label -- which is the width its justification is
|
|
26504
28617
|
// measured against.
|
|
26505
28618
|
width: box ? box.width : 0,
|
|
26506
|
-
aligned: !!internal.svg.getAlignmentAnchor(rec['label-align'])
|
|
28619
|
+
aligned: !!internal.svg.getAlignmentAnchor(rec['label-align']),
|
|
28620
|
+
keepAnchor: getLabelPositionKind(rec) == 'block'
|
|
26507
28621
|
},
|
|
26508
28622
|
// What the attributes said before the preview wrote over them, so that
|
|
26509
28623
|
// the command is what changes the label rather than the drag.
|
|
@@ -26512,14 +28626,32 @@
|
|
|
26512
28626
|
y: nodes.text.getAttribute('y'),
|
|
26513
28627
|
anchor: nodes.text.getAttribute('text-anchor')
|
|
26514
28628
|
},
|
|
28629
|
+
callout: getOffsetDragCallout(rec),
|
|
26515
28630
|
moved: false
|
|
26516
28631
|
};
|
|
26517
28632
|
// The hairline to the anchor, which is what the drag is measured from and
|
|
26518
|
-
// often the only thing on screen that says so.
|
|
26519
|
-
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);
|
|
26520
28635
|
return true;
|
|
26521
28636
|
}
|
|
26522
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
|
+
|
|
26523
28655
|
function updateOffsetDrag(e) {
|
|
26524
28656
|
var o = textDrag;
|
|
26525
28657
|
o.values = getOffsetDragValues(o.start, {
|
|
@@ -26527,7 +28659,41 @@
|
|
|
26527
28659
|
dy: (e.y - o.from[1]) / o.scale
|
|
26528
28660
|
});
|
|
26529
28661
|
o.moved = true;
|
|
26530
|
-
|
|
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);
|
|
26531
28697
|
}
|
|
26532
28698
|
|
|
26533
28699
|
// Shows the offset by writing it onto the rendered text, as the path drag
|
|
@@ -26553,6 +28719,12 @@
|
|
|
26553
28719
|
|
|
26554
28720
|
function restoreOffset(o) {
|
|
26555
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
|
+
}
|
|
26556
28728
|
if (!nodes) return;
|
|
26557
28729
|
// The renderer always writes x and y on an anchored label, so the removal
|
|
26558
28730
|
// case is for markup this tool did not draw.
|
|
@@ -26574,6 +28746,7 @@
|
|
|
26574
28746
|
dx: o.values.dx,
|
|
26575
28747
|
dy: o.values.dy,
|
|
26576
28748
|
anchor: o.values['text-anchor'],
|
|
28749
|
+
via: o.via || null,
|
|
26577
28750
|
id: o.id,
|
|
26578
28751
|
target: o.target.name
|
|
26579
28752
|
}), {
|
|
@@ -26676,8 +28849,9 @@
|
|
|
26676
28849
|
function updateHoverHandle(e) {
|
|
26677
28850
|
var found = drawingCurve() || editor.isOpen() || !e.overMap ? null :
|
|
26678
28851
|
findHandle(hit.getHitTarget(), e);
|
|
26679
|
-
var changed = !!found != !!hoverHandle
|
|
26680
|
-
|
|
28852
|
+
var changed = !!found != !!hoverHandle ||
|
|
28853
|
+
!!found && found.kind != hoverHandle.kind;
|
|
28854
|
+
if (dragging()) return;
|
|
26681
28855
|
hoverHandle = found;
|
|
26682
28856
|
if (changed) {
|
|
26683
28857
|
updateHoverText(); // a knot handle takes the glyphs' turn away
|
|
@@ -26696,16 +28870,30 @@
|
|
|
26696
28870
|
// text, and letting the handle win there would leave the text ungrabbable.
|
|
26697
28871
|
function updateHoverText() {
|
|
26698
28872
|
var id = findDraggableText();
|
|
26699
|
-
if (
|
|
28873
|
+
if (dragging()) return;
|
|
26700
28874
|
hoverTextId = id > -1 && (!hoverHandle || glyphsOutrankHandle(id)) ? id : -1;
|
|
26701
28875
|
}
|
|
26702
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
|
+
|
|
26703
28887
|
// The exception is about the pointer being on the glyphs, not about the mode:
|
|
26704
28888
|
// an offset label's anchor is out from under its text, and taking the handle
|
|
26705
28889
|
// away there left it with no anchor handle at all -- dragging the symbol of a
|
|
26706
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.
|
|
26707
28894
|
function glyphsOutrankHandle(id) {
|
|
26708
|
-
return hoverHandle.id == id &&
|
|
28895
|
+
return !hoverHandle.kind && hoverHandle.id == id &&
|
|
28896
|
+
textIsDraggable(hit.getHitTarget(), id) &&
|
|
26709
28897
|
!isPathLabel(hit.getHitTarget(), id) && pointerIsOverText(id);
|
|
26710
28898
|
}
|
|
26711
28899
|
|
|
@@ -26748,14 +28936,24 @@
|
|
|
26748
28936
|
}
|
|
26749
28937
|
|
|
26750
28938
|
// The handle under the pointer, or null. Only a selected label's handles are
|
|
26751
|
-
// 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.
|
|
26752
28942
|
function findHandle(target, e) {
|
|
26753
28943
|
var shapes = target && getDisplayShapes(target);
|
|
26754
28944
|
var ids = hit.getSelectionIds();
|
|
26755
28945
|
var p = pixToMapCoords(e.x, e.y);
|
|
26756
|
-
var handle;
|
|
28946
|
+
var handle, labelHandle, knotPix;
|
|
26757
28947
|
if (!shapes || ids.length === 0) return null;
|
|
26758
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;
|
|
26759
28957
|
if (handle) handle.pointer = p;
|
|
26760
28958
|
return handle;
|
|
26761
28959
|
}
|
|
@@ -26797,6 +28995,10 @@
|
|
|
26797
28995
|
}
|
|
26798
28996
|
return;
|
|
26799
28997
|
}
|
|
28998
|
+
if (hoverHandle && hoverHandle.kind && !editor.isOpen()) {
|
|
28999
|
+
resetHandle(hoverHandle);
|
|
29000
|
+
return;
|
|
29001
|
+
}
|
|
26800
29002
|
// Double-clicking a label reaches straight into its text, which is the
|
|
26801
29003
|
// gesture most editors use and saves selecting it first. The single-click
|
|
26802
29004
|
// route got there already if the label was the selection; this covers the
|
|
@@ -26807,8 +29009,17 @@
|
|
|
26807
29009
|
gui.keyboard.on('keydown', function(e) {
|
|
26808
29010
|
if (!active()) return;
|
|
26809
29011
|
// While a session is open the textarea has focus and handles its own keys;
|
|
26810
|
-
// it stops propagation for the ones it acts on.
|
|
29012
|
+
// it stops propagation for the ones it acts on. Cmd-A there means select
|
|
29013
|
+
// the text being typed, which is the browser's to handle.
|
|
26811
29014
|
if (editor.isOpen()) return;
|
|
29015
|
+
// Not while a curve is being drawn: that gesture owns the keyboard, and
|
|
29016
|
+
// selecting the layer in the middle of placing a label is not what the
|
|
29017
|
+
// keystroke can have meant.
|
|
29018
|
+
if (isSelectAll(e) && !drawingCurve()) {
|
|
29019
|
+
selectAllLabels();
|
|
29020
|
+
consume(e); // the default is to select the whole page
|
|
29021
|
+
return;
|
|
29022
|
+
}
|
|
26812
29023
|
if (e.keyName == 'esc') {
|
|
26813
29024
|
// One rung per press, innermost first: the curve being drawn, then the
|
|
26814
29025
|
// selection, then the armed tool. The hit control's own escape handler
|
|
@@ -26839,6 +29050,16 @@
|
|
|
26839
29050
|
}
|
|
26840
29051
|
}, null, 10);
|
|
26841
29052
|
|
|
29053
|
+
// Cmd-A on a Mac, Ctrl-A elsewhere. Read from the key rather than the
|
|
29054
|
+
// keyCode, as undo and redo are.
|
|
29055
|
+
function isSelectAll(e) {
|
|
29056
|
+
var evt = e.originalEvent;
|
|
29057
|
+
if (!evt || !(evt.metaKey || evt.ctrlKey) || evt.shiftKey || evt.altKey) {
|
|
29058
|
+
return false;
|
|
29059
|
+
}
|
|
29060
|
+
return (evt.key || '').toLowerCase() == 'a';
|
|
29061
|
+
}
|
|
29062
|
+
|
|
26842
29063
|
// Suppresses the key's default action as well as the rest of the GUI's
|
|
26843
29064
|
// handlers. The default matters here: finishing a curve opens an editing
|
|
26844
29065
|
// session and focuses its textarea, and without this the same Enter goes on
|
|
@@ -26890,8 +29111,12 @@
|
|
|
26890
29111
|
// So the click opens a *pending* session, which draws the label and holds its
|
|
26891
29112
|
// text, and the label becomes a feature only when the session ends with
|
|
26892
29113
|
// something in it. See LabelEditor.openPending().
|
|
26893
|
-
|
|
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) {
|
|
26894
29118
|
var target = hit.getHitTarget();
|
|
29119
|
+
var styleFn = getStyle || getStyleForNewLabel;
|
|
26895
29120
|
if (!target) return;
|
|
26896
29121
|
hideInstructions();
|
|
26897
29122
|
editor.openPending(target, {
|
|
@@ -26900,15 +29125,15 @@
|
|
|
26900
29125
|
// or a position while the caret is sitting there is visible immediately.
|
|
26901
29126
|
// The same style the label will be created with, down to the font it is
|
|
26902
29127
|
// named in, so that committing it changes nothing on screen.
|
|
26903
|
-
getStyle:
|
|
26904
|
-
create: function(text) { createLabel(displayCoords, text); }
|
|
29128
|
+
getStyle: styleFn,
|
|
29129
|
+
create: function(text) { createLabel(displayCoords, text, styleFn()); }
|
|
26905
29130
|
});
|
|
26906
29131
|
}
|
|
26907
29132
|
|
|
26908
29133
|
// Creates the label being typed into. One command carries its geometry, its
|
|
26909
29134
|
// style and its text, so a new label is a single entry in the session history
|
|
26910
29135
|
// and a single step to undo.
|
|
26911
|
-
function createLabel(displayCoords, text) {
|
|
29136
|
+
function createLabel(displayCoords, text, style) {
|
|
26912
29137
|
var target = hit.getHitTarget();
|
|
26913
29138
|
var coords = displayCoords.map(function(p) {
|
|
26914
29139
|
return target ? translateDisplayPoint(target, p) : p;
|
|
@@ -26918,7 +29143,7 @@
|
|
|
26918
29143
|
text: text,
|
|
26919
29144
|
// whatever the style panel was set to while nothing was selected, so that
|
|
26920
29145
|
// a font can be chosen before the first label exists
|
|
26921
|
-
style:
|
|
29146
|
+
style: style
|
|
26922
29147
|
}), {title: 'Add label'});
|
|
26923
29148
|
}
|
|
26924
29149
|
|
|
@@ -26985,7 +29210,7 @@
|
|
|
26985
29210
|
},
|
|
26986
29211
|
referenceStyle = { // outline style for reference layers
|
|
26987
29212
|
type: 'outline',
|
|
26988
|
-
strokeColors: [null, '#
|
|
29213
|
+
strokeColors: [null, '#87b73b'], // was 78c110
|
|
26989
29214
|
strokeWidth: 0.85,
|
|
26990
29215
|
dotColor: "#73ba20",
|
|
26991
29216
|
dotSize: 1
|
|
@@ -30207,6 +32432,9 @@
|
|
|
30207
32432
|
// getContentLayerBounds() instead. Recorded because the difference matters
|
|
30208
32433
|
// when deciding whether an update should reset the view.
|
|
30209
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;
|
|
30210
32438
|
|
|
30211
32439
|
var RESIZE_REDRAW_DELAY = 200;
|
|
30212
32440
|
|
|
@@ -30473,6 +32701,7 @@
|
|
|
30473
32701
|
// read before calcFullBounds() below, which describes the map as it is
|
|
30474
32702
|
// after this update
|
|
30475
32703
|
var prevBoundsWerePlaceholder = _boundsArePlaceholder;
|
|
32704
|
+
var prevBoundsWereCollapsed = _boundsAreCollapsed;
|
|
30476
32705
|
var fullBounds;
|
|
30477
32706
|
var needReset;
|
|
30478
32707
|
|
|
@@ -30555,6 +32784,8 @@
|
|
|
30555
32784
|
// screen, so the view they chose it in is the one to keep, unless what
|
|
30556
32785
|
// arrived is not in it.
|
|
30557
32786
|
needReset = !fullBounds.intersects(_ext.getBounds());
|
|
32787
|
+
} else if (prevBoundsWereCollapsed) {
|
|
32788
|
+
needReset = mapNeedsResetFromCollapsedBounds(fullBounds, _ext.getBounds(), e.flags);
|
|
30558
32789
|
} else {
|
|
30559
32790
|
needReset = mapNeedsReset(fullBounds, _ext.getFullBounds(), _ext.getBounds(), e.flags);
|
|
30560
32791
|
}
|
|
@@ -30635,7 +32866,8 @@
|
|
|
30635
32866
|
b.scale(1 + marginPct / 100 * 2);
|
|
30636
32867
|
|
|
30637
32868
|
// Inflate display bounding box of single-point layers and collapsed shapes a bit
|
|
30638
|
-
|
|
32869
|
+
_boundsAreCollapsed = b.width() === 0 || b.height() === 0;
|
|
32870
|
+
if (_boundsAreCollapsed) {
|
|
30639
32871
|
b.padBounds(1e-4, 1e-4, 1e-4, 1e-4);
|
|
30640
32872
|
}
|
|
30641
32873
|
return b;
|
|
@@ -30847,8 +33079,7 @@
|
|
|
30847
33079
|
var mapLayers = gui.container.findChild('.map-layers').node();
|
|
30848
33080
|
var map = gui.container.findChild('.mshp-main-map');
|
|
30849
33081
|
var toggle = gui.buttons.addButton('#frame-tool-icon')
|
|
30850
|
-
.addClass('menu-btn preview-toggle')
|
|
30851
|
-
.attr('title', 'Toggle map preview');
|
|
33082
|
+
.addClass('menu-btn preview-toggle');
|
|
30852
33083
|
var readout = El('div')
|
|
30853
33084
|
.addClass('preview-readout')
|
|
30854
33085
|
.appendTo(map)
|
|
@@ -30968,7 +33199,11 @@
|
|
|
30968
33199
|
var available = hasFrame();
|
|
30969
33200
|
toggle.removeClass('disabled');
|
|
30970
33201
|
toggle.classed('selected', available && self.isOn());
|
|
30971
|
-
|
|
33202
|
+
// data-tooltip, not title: the CSS label in .nav-btn[data-tooltip] appears
|
|
33203
|
+
// without the browser's delay and matches the arrow menu's type.
|
|
33204
|
+
var label = available ? 'Toggle map preview' : 'Add map frame';
|
|
33205
|
+
toggle.attr('data-tooltip', label);
|
|
33206
|
+
toggle.attr('aria-label', label);
|
|
30972
33207
|
toggle.attr('aria-disabled', 'false');
|
|
30973
33208
|
toggle.attr('aria-pressed', available && self.isOn() ? 'true' : 'false');
|
|
30974
33209
|
if (gui.map.isPreviewView()) {
|
|
@@ -33320,7 +35555,6 @@
|
|
|
33320
35555
|
if (manifest.catalog) {
|
|
33321
35556
|
opts.catalog = manifest.catalog;
|
|
33322
35557
|
}
|
|
33323
|
-
opts.display_all = vars['display-all'] || vars.a || !!manifest.display_all;
|
|
33324
35558
|
opts.quick_view = vars['quick-view'] || vars.q || !!manifest.quick_view;
|
|
33325
35559
|
opts.target = vars.target || manifest.target || null;
|
|
33326
35560
|
opts.name = vars.name || manifest.name || null;
|