mapshaper 0.7.68 → 0.7.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mapshaper.js +1268 -789
- package/package.json +1 -1
- package/www/mapshaper-gui.js +2001 -278
- package/www/mapshaper.js +1268 -789
- package/www/page.css +120 -6
package/www/mapshaper-gui.js
CHANGED
|
@@ -3053,12 +3053,12 @@
|
|
|
3053
3053
|
return decimals >= 0 ? num.toFixed(decimals) : String(num);
|
|
3054
3054
|
}
|
|
3055
3055
|
|
|
3056
|
-
function formatNumber$
|
|
3056
|
+
function formatNumber$4(val) {
|
|
3057
3057
|
return val + '';
|
|
3058
3058
|
}
|
|
3059
3059
|
|
|
3060
3060
|
function formatIntlNumber(val) {
|
|
3061
|
-
var str = formatNumber$
|
|
3061
|
+
var str = formatNumber$4(val);
|
|
3062
3062
|
return '"' + str.replace('.', ',') + '"'; // need to quote if comma-delimited
|
|
3063
3063
|
}
|
|
3064
3064
|
|
|
@@ -3324,7 +3324,7 @@
|
|
|
3324
3324
|
return str;
|
|
3325
3325
|
}
|
|
3326
3326
|
|
|
3327
|
-
function formatValue(val, matches) {
|
|
3327
|
+
function formatValue$2(val, matches) {
|
|
3328
3328
|
var flags = matches[1];
|
|
3329
3329
|
var padding = matches[2];
|
|
3330
3330
|
var decimals = matches[3] ? parseInt(matches[3].substr(1)) : void 0;
|
|
@@ -3420,7 +3420,7 @@
|
|
|
3420
3420
|
error$1("[format()] Data does not match format string; format:", fmt, "data:", arguments);
|
|
3421
3421
|
}
|
|
3422
3422
|
for (var i=0; i<n; i++) {
|
|
3423
|
-
str += formatValue(arguments[i], formatCodes[i]) + literals[i+1];
|
|
3423
|
+
str += formatValue$2(arguments[i], formatCodes[i]) + literals[i+1];
|
|
3424
3424
|
}
|
|
3425
3425
|
return str;
|
|
3426
3426
|
};
|
|
@@ -3607,7 +3607,7 @@
|
|
|
3607
3607
|
endsWith, every, expandoBuffer, extend, extendBuffer,
|
|
3608
3608
|
find, findMedian, findQuantile, findRankByValue, findStringPrefix,
|
|
3609
3609
|
findValueByPct, findValueByRank, forEach, forEachProperty, format,
|
|
3610
|
-
formatDateISO, formatIntlNumber, formatNumber: formatNumber$
|
|
3610
|
+
formatDateISO, formatIntlNumber, formatNumber: formatNumber$4, formatNumberForDisplay,
|
|
3611
3611
|
formatVersionedName, formatter,
|
|
3612
3612
|
genericSort, getArrayBounds, getGenericComparator, getKeyComparator,
|
|
3613
3613
|
getSortedIds, getUniqueName, groupBy,
|
|
@@ -8623,10 +8623,10 @@
|
|
|
8623
8623
|
}
|
|
8624
8624
|
|
|
8625
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
|
|
8627
|
-
// data-only layer has nothing to draw.
|
|
8626
|
+
// has an eye icon in the layers panel. A frame and its furniture are drawn by
|
|
8627
|
+
// preview mode, and a data-only layer has nothing to draw.
|
|
8628
8628
|
function layerIsPinnable(lyr, dataset) {
|
|
8629
|
-
if (
|
|
8629
|
+
if (internal.isFrameComponentLayer(lyr, dataset)) return false;
|
|
8630
8630
|
return internal.layerIsGeometric(lyr) || internal.layerHasRaster(lyr) ||
|
|
8631
8631
|
internal.layerHasFurniture(lyr);
|
|
8632
8632
|
}
|
|
@@ -10888,6 +10888,9 @@
|
|
|
10888
10888
|
if (opts.format == 'svg' || opts.format == 'topojson') {
|
|
10889
10889
|
opts.gui_frame = getGuiFrameContext();
|
|
10890
10890
|
}
|
|
10891
|
+
if (opts.format == 'svg') {
|
|
10892
|
+
targets = addFrameFurnitureTargets(targets, opts.gui_frame);
|
|
10893
|
+
}
|
|
10891
10894
|
try {
|
|
10892
10895
|
var files = await internal.exportTargetLayers(model, targets, opts);
|
|
10893
10896
|
} catch(e) {
|
|
@@ -10953,7 +10956,7 @@
|
|
|
10953
10956
|
function initLayerMenu() {
|
|
10954
10957
|
var list = menu.findChild('.export-layer-list').empty();
|
|
10955
10958
|
var layers = model.getLayers().filter(function(o) {
|
|
10956
|
-
return !internal.
|
|
10959
|
+
return !internal.isFrameComponentLayer(o.layer, o.dataset);
|
|
10957
10960
|
});
|
|
10958
10961
|
sortLayersForMenuDisplay(layers);
|
|
10959
10962
|
|
|
@@ -11147,21 +11150,42 @@
|
|
|
11147
11150
|
};
|
|
11148
11151
|
}
|
|
11149
11152
|
|
|
11153
|
+
// Snapshots keep the frame and its furniture
|
|
11150
11154
|
function addFrameTarget(targets) {
|
|
11151
11155
|
var frame = internal.getActiveFrame(model);
|
|
11152
|
-
var group;
|
|
11153
11156
|
if (!frame) return targets;
|
|
11157
|
+
return addDatasetTargets(targets, frame.dataset,
|
|
11158
|
+
[frame.layer].concat(internal.getFrameFurnitureLayers(frame.dataset)));
|
|
11159
|
+
}
|
|
11160
|
+
|
|
11161
|
+
// The frame's furniture is drawn in SVG output, the way it is in preview.
|
|
11162
|
+
// An unprojected frame can't show a scalebar, so it doesn't block the export.
|
|
11163
|
+
function addFrameFurnitureTargets(targets, frameContext) {
|
|
11164
|
+
var frame = internal.getActiveFrame(model);
|
|
11165
|
+
var furniture = frame ? internal.getFrameFurnitureLayers(frame.dataset) : [];
|
|
11166
|
+
if (furniture.length === 0) return targets;
|
|
11167
|
+
if (internal.getFurnitureFrameProblem(frameContext && frameContext.data)) {
|
|
11168
|
+
console.warn('Map furniture was not exported: the map frame is unprojected');
|
|
11169
|
+
return targets;
|
|
11170
|
+
}
|
|
11171
|
+
return addDatasetTargets(targets, frame.dataset, furniture);
|
|
11172
|
+
}
|
|
11173
|
+
|
|
11174
|
+
function addDatasetTargets(targets, dataset, layers) {
|
|
11175
|
+
var group;
|
|
11154
11176
|
targets = targets.map(function(target) {
|
|
11155
11177
|
return Object.assign({}, target, {layers: target.layers.slice()});
|
|
11156
11178
|
});
|
|
11157
11179
|
group = targets.find(function(target) {
|
|
11158
|
-
return target.dataset ==
|
|
11180
|
+
return target.dataset == dataset;
|
|
11159
11181
|
});
|
|
11160
|
-
if (group) {
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
targets.push({dataset: frame.dataset, layers: [frame.layer]});
|
|
11182
|
+
if (!group) {
|
|
11183
|
+
group = {dataset: dataset, layers: []};
|
|
11184
|
+
targets.push(group);
|
|
11164
11185
|
}
|
|
11186
|
+
layers.forEach(function(lyr) {
|
|
11187
|
+
if (!group.layers.includes(lyr)) group.layers.push(lyr);
|
|
11188
|
+
});
|
|
11165
11189
|
return targets;
|
|
11166
11190
|
}
|
|
11167
11191
|
|
|
@@ -11456,6 +11480,9 @@
|
|
|
11456
11480
|
if (e.frameProperties) {
|
|
11457
11481
|
addMenuItem('frame properties', e.frameProperties, '');
|
|
11458
11482
|
}
|
|
11483
|
+
if (e.scalebarProperties) {
|
|
11484
|
+
addMenuItem(e.scalebarLabel || 'scale bar', e.scalebarProperties, '');
|
|
11485
|
+
}
|
|
11459
11486
|
if (e.resizeFrame) {
|
|
11460
11487
|
addMenuItem('resize frame', e.resizeFrame, '');
|
|
11461
11488
|
}
|
|
@@ -11491,9 +11518,12 @@
|
|
|
11491
11518
|
}
|
|
11492
11519
|
|
|
11493
11520
|
if (e.deleteVertex || e.deletePoint || copyable || e.deleteFeature ||
|
|
11494
|
-
e.flipLabel) {
|
|
11521
|
+
e.flipLabel || e.boldText) {
|
|
11495
11522
|
|
|
11496
11523
|
addMenuLabel('actions');
|
|
11524
|
+
if (e.boldText) {
|
|
11525
|
+
addMenuItem(e.boldText.label, e.boldText.run);
|
|
11526
|
+
}
|
|
11497
11527
|
if (e.deleteVertex) {
|
|
11498
11528
|
addMenuItem('delete vertex', e.deleteVertex);
|
|
11499
11529
|
}
|
|
@@ -11883,6 +11913,8 @@
|
|
|
11883
11913
|
sortLayersForMenuDisplay(model.getLayers()).forEach(function(o) {
|
|
11884
11914
|
var lyr = o.layer;
|
|
11885
11915
|
var isFrame = internal.isFrameLayer(lyr, o.dataset.arcs);
|
|
11916
|
+
// furniture is shown as part of its frame's entry
|
|
11917
|
+
if (!isFrame && internal.isFrameComponentLayer(lyr, o.dataset)) return;
|
|
11886
11918
|
var opts = {
|
|
11887
11919
|
show_source: layerCount < 5,
|
|
11888
11920
|
pinnable: pinnableCount > 0 && isPinnable(lyr, o.dataset)
|
|
@@ -11982,25 +12014,39 @@
|
|
|
11982
12014
|
function deleteLayer() {
|
|
11983
12015
|
var target = findLayerById(id);
|
|
11984
12016
|
var undoTransaction;
|
|
12017
|
+
var furniture;
|
|
11985
12018
|
if (!target) return;
|
|
12019
|
+
// a frame's furniture goes with it, rather than becoming a stray layer
|
|
12020
|
+
furniture = internal.isFrameLayer(target.layer, target.dataset.arcs) ?
|
|
12021
|
+
internal.getFrameFurnitureLayers(target.dataset) : [];
|
|
11986
12022
|
undoTransaction = createUndoTransaction(gui, 'delete layer');
|
|
11987
12023
|
if (map.isVisibleLayer(target.layer)) {
|
|
11988
12024
|
// TODO: check for double map refresh after model.deleteLayer() below
|
|
11989
12025
|
setLayerPinning(target.layer, false);
|
|
11990
12026
|
}
|
|
11991
12027
|
if (undoTransaction) {
|
|
11992
|
-
undoTransaction.run(
|
|
11993
|
-
model.deleteLayer(target.layer, target.dataset);
|
|
11994
|
-
});
|
|
12028
|
+
undoTransaction.run(deleteLayers);
|
|
11995
12029
|
addUndoTransactionToHistory(gui, undoTransaction, {
|
|
11996
12030
|
flags: {select: true, arc_count: true},
|
|
11997
12031
|
entryPrefix: 'delete-layer'
|
|
11998
12032
|
});
|
|
11999
12033
|
} else {
|
|
12034
|
+
deleteLayers();
|
|
12035
|
+
}
|
|
12036
|
+
|
|
12037
|
+
function deleteLayers() {
|
|
12038
|
+
furniture.forEach(function(lyr) {
|
|
12039
|
+
model.deleteLayer(lyr, target.dataset);
|
|
12040
|
+
});
|
|
12000
12041
|
model.deleteLayer(target.layer, target.dataset);
|
|
12001
12042
|
}
|
|
12002
12043
|
}
|
|
12003
12044
|
|
|
12045
|
+
function openScalebarProperties() {
|
|
12046
|
+
var target = findLayerById(id);
|
|
12047
|
+
if (target && gui.scalebarProperties) gui.scalebarProperties.open(target);
|
|
12048
|
+
}
|
|
12049
|
+
|
|
12004
12050
|
// Duplicate a layer by running an equivalent command, so the copy is
|
|
12005
12051
|
// recorded in session history and can be undone. Uses '-filter true +'
|
|
12006
12052
|
// to append a copy without replacing the source, keeps the same name
|
|
@@ -12088,6 +12134,10 @@
|
|
|
12088
12134
|
internal.isFrameLayer(target.layer, target.dataset.arcs);
|
|
12089
12135
|
if (isFrame) {
|
|
12090
12136
|
menuEvent.frameProperties = openFrameProperties;
|
|
12137
|
+
menuEvent.scalebarProperties = openScalebarProperties;
|
|
12138
|
+
menuEvent.scalebarLabel =
|
|
12139
|
+
internal.findFrameFurnitureLayer(target.dataset, 'scalebar') ?
|
|
12140
|
+
'scale bar' : 'add scale bar';
|
|
12091
12141
|
menuEvent.resizeFrame = resizeFrame;
|
|
12092
12142
|
menuEvent.deleteFrame = deleteLayer;
|
|
12093
12143
|
} else {
|
|
@@ -12857,6 +12907,11 @@
|
|
|
12857
12907
|
if (cmd.name == 'style' || cmd.name == 'svg-style') {
|
|
12858
12908
|
return getStyleCullInfo(opts, isStyleProperty);
|
|
12859
12909
|
}
|
|
12910
|
+
// Adding a label or a labeled copy of a layer is not a style edit, and can
|
|
12911
|
+
// not be superseded by one.
|
|
12912
|
+
if (cmd.name == 'labels' && !('coordinates' in opts) && !opts.no_replace) {
|
|
12913
|
+
return getStyleCullInfo(getLabelStyleOpts(opts), isStyleProperty);
|
|
12914
|
+
}
|
|
12860
12915
|
if (cmd.name == 'classify') {
|
|
12861
12916
|
return getClassifyCullInfo(opts);
|
|
12862
12917
|
}
|
|
@@ -12876,6 +12931,16 @@
|
|
|
12876
12931
|
};
|
|
12877
12932
|
}
|
|
12878
12933
|
|
|
12934
|
+
// -labels text= writes the label-text property.
|
|
12935
|
+
function getLabelStyleOpts(opts) {
|
|
12936
|
+
var o = Object.assign({}, opts);
|
|
12937
|
+
if ('text' in o) {
|
|
12938
|
+
o['label-text'] = o.text;
|
|
12939
|
+
delete o.text;
|
|
12940
|
+
}
|
|
12941
|
+
return o;
|
|
12942
|
+
}
|
|
12943
|
+
|
|
12879
12944
|
function getStyleFields$2(opts, isStyleProperty) {
|
|
12880
12945
|
var fields = [];
|
|
12881
12946
|
Object.keys(opts).forEach(function(name) {
|
|
@@ -13874,6 +13939,11 @@
|
|
|
13874
13939
|
var SOFT_BREAK = '<wbr>';
|
|
13875
13940
|
var SOFT_BREAK_RXP = /<wbr>/gi;
|
|
13876
13941
|
|
|
13942
|
+
// A bold stretch, which is the other markup label text carries. See
|
|
13943
|
+
// svg-label-markup.mjs.
|
|
13944
|
+
var BOLD_OPEN = '<b>';
|
|
13945
|
+
var BOLD_CLOSE = '</b>';
|
|
13946
|
+
|
|
13877
13947
|
// How a label lays out its line breaks, which is not the same for the two kinds.
|
|
13878
13948
|
// An anchored label stacks its lines with <tspan>. A path label joins them with
|
|
13879
13949
|
// a space, because a <tspan> inside a <textPath> advances along the curve
|
|
@@ -13909,6 +13979,66 @@
|
|
|
13909
13979
|
return {text: text, breaks: breaks};
|
|
13910
13980
|
}
|
|
13911
13981
|
|
|
13982
|
+
// Data value -> {text, breaks, bold}: the string the user edits, the offsets
|
|
13983
|
+
// in it where the stored value has a soft break, and its bold stretches as
|
|
13984
|
+
// [start, end) offsets into it (see gui-label-bold.mjs).
|
|
13985
|
+
//
|
|
13986
|
+
// The two kinds of markup are independent -- a bold stretch can cross a soft
|
|
13987
|
+
// break, and a soft break can sit at either end of one -- so the bold is read
|
|
13988
|
+
// first and its offsets are moved as the soft breaks come out.
|
|
13989
|
+
function readLabelValue(val) {
|
|
13990
|
+
var str = val === null || val === undefined ? '' : String(val).replace(ANY_NEWLINE, '\n');
|
|
13991
|
+
var o = internal.svg.parseBoldMarkup(str);
|
|
13992
|
+
var starts = [], m;
|
|
13993
|
+
SOFT_BREAK_RXP.lastIndex = 0;
|
|
13994
|
+
while ((m = SOFT_BREAK_RXP.exec(o.text))) starts.push(m.index);
|
|
13995
|
+
return {
|
|
13996
|
+
text: o.text.replace(SOFT_BREAK_RXP, ''),
|
|
13997
|
+
breaks: starts.map(function(start, i) {
|
|
13998
|
+
return start - i * SOFT_BREAK.length;
|
|
13999
|
+
}),
|
|
14000
|
+
bold: internal.svg.normalizeBoldRanges(o.bold.map(function(r) {
|
|
14001
|
+
return [removeSoftBreakOffset(r[0], starts), removeSoftBreakOffset(r[1], starts)];
|
|
14002
|
+
}))
|
|
14003
|
+
};
|
|
14004
|
+
}
|
|
14005
|
+
|
|
14006
|
+
// An offset into text with soft breaks in it -> the same place once they are
|
|
14007
|
+
// removed. One that falls inside a marker goes to its start.
|
|
14008
|
+
function removeSoftBreakOffset(x, starts) {
|
|
14009
|
+
var shift = 0;
|
|
14010
|
+
for (var i = 0; i < starts.length && starts[i] < x; i++) {
|
|
14011
|
+
if (x < starts[i] + SOFT_BREAK.length) return starts[i] - shift;
|
|
14012
|
+
shift += SOFT_BREAK.length;
|
|
14013
|
+
}
|
|
14014
|
+
return x - shift;
|
|
14015
|
+
}
|
|
14016
|
+
|
|
14017
|
+
// The string the user edits, its soft breaks and its bold ranges -> the value
|
|
14018
|
+
// to store, with both kinds of markup written in. Where they meet, a bold
|
|
14019
|
+
// stretch closes before a soft break and opens after one, so that the break
|
|
14020
|
+
// is never inside a pair of tags it is not part of.
|
|
14021
|
+
function writeLabelValue(text, breaks, bold) {
|
|
14022
|
+
var marks = [];
|
|
14023
|
+
(bold || []).forEach(function(r) {
|
|
14024
|
+
marks.push({pos: r[0], order: 2, str: BOLD_OPEN});
|
|
14025
|
+
marks.push({pos: r[1], order: 0, str: BOLD_CLOSE});
|
|
14026
|
+
});
|
|
14027
|
+
(breaks || []).forEach(function(pos) {
|
|
14028
|
+
marks.push({pos: pos, order: 1, str: SOFT_BREAK});
|
|
14029
|
+
});
|
|
14030
|
+
if (marks.length === 0) return text;
|
|
14031
|
+
marks.sort(function(a, b) {
|
|
14032
|
+
return a.pos - b.pos || a.order - b.order;
|
|
14033
|
+
});
|
|
14034
|
+
var out = '', prev = 0;
|
|
14035
|
+
marks.forEach(function(mark) {
|
|
14036
|
+
out += text.substring(prev, mark.pos) + mark.str;
|
|
14037
|
+
prev = mark.pos;
|
|
14038
|
+
});
|
|
14039
|
+
return out + text.substring(prev);
|
|
14040
|
+
}
|
|
14041
|
+
|
|
13912
14042
|
// The string the user edits, plus soft breaks at @breaks (ascending offsets
|
|
13913
14043
|
// into it) -> the same string with the markers written in.
|
|
13914
14044
|
function insertSoftBreaks(text, breaks) {
|
|
@@ -14089,6 +14219,29 @@
|
|
|
14089
14219
|
return Math.min(renderedIndex, length);
|
|
14090
14220
|
}
|
|
14091
14221
|
|
|
14222
|
+
// The word a double-click at edited index @i selects, as [start, end), or an
|
|
14223
|
+
// empty range at @i for a click between words. A click on the right half of a
|
|
14224
|
+
// word's last letter lands on the index after it, which is still that word.
|
|
14225
|
+
//
|
|
14226
|
+
// Hyphens and apostrophes count as part of a word, so that "cease-fire" and
|
|
14227
|
+
// "Israel's" are one word each, as they are one name on the map.
|
|
14228
|
+
function getWordRange(text, i) {
|
|
14229
|
+
var at = isWordChar(text.charAt(i)) ? i : isWordChar(text.charAt(i - 1)) ? i - 1 : -1;
|
|
14230
|
+
var a, b;
|
|
14231
|
+
if (at < 0) return [i, i];
|
|
14232
|
+
a = at;
|
|
14233
|
+
b = at + 1;
|
|
14234
|
+
while (a > 0 && isWordChar(text.charAt(a - 1))) a--;
|
|
14235
|
+
while (b < text.length && isWordChar(text.charAt(b))) b++;
|
|
14236
|
+
return [a, b];
|
|
14237
|
+
}
|
|
14238
|
+
|
|
14239
|
+
var WORD_CHAR_RXP = /[\p{L}\p{N}\p{M}'\u2019-]/u;
|
|
14240
|
+
|
|
14241
|
+
function isWordChar(c) {
|
|
14242
|
+
return !!c && WORD_CHAR_RXP.test(c);
|
|
14243
|
+
}
|
|
14244
|
+
|
|
14092
14245
|
// Strips the placeholder back out, so that a value read off a rendered node is
|
|
14093
14246
|
// not mistaken for text the user typed.
|
|
14094
14247
|
function stripPlaceholder(str) {
|
|
@@ -14153,7 +14306,7 @@
|
|
|
14153
14306
|
function getAddLabelCommand(coords, opts) {
|
|
14154
14307
|
var o = opts || {};
|
|
14155
14308
|
var target = o.target || {mode: 'new', newLayerName: DEFAULT_LABEL_LAYER_NAME};
|
|
14156
|
-
var parts = ['-
|
|
14309
|
+
var parts = ['-labels', 'coordinates=' + formatCoords(coords)];
|
|
14157
14310
|
if (o.text) {
|
|
14158
14311
|
// A real newline would break the command parser, so encodeLabelText()
|
|
14159
14312
|
// writes the two-character escape the label renderer also accepts.
|
|
@@ -14220,7 +14373,7 @@
|
|
|
14220
14373
|
// id: feature id of the label
|
|
14221
14374
|
// target: layer name, or null to use the current target
|
|
14222
14375
|
function getLabelPlacementCommand(opts) {
|
|
14223
|
-
var parts = ['-
|
|
14376
|
+
var parts = ['-labels', 'label-start-offset=' + opts.offset];
|
|
14224
14377
|
if (opts.anchor) {
|
|
14225
14378
|
parts.push('text-anchor=' + opts.anchor);
|
|
14226
14379
|
}
|
|
@@ -14240,7 +14393,7 @@
|
|
|
14240
14393
|
// with and the position goes -- "stop taking a position, carry these offsets
|
|
14241
14394
|
// instead", which is one command because it is one edit.
|
|
14242
14395
|
//
|
|
14243
|
-
// label-pos= is always written, even by a label that has none. -
|
|
14396
|
+
// label-pos= is always written, even by a label that has none. -labels reads an
|
|
14244
14397
|
// empty value as "remove this", and removing a property no record carries adds
|
|
14245
14398
|
// nothing to the layer, so there is nothing to be gained by asking first.
|
|
14246
14399
|
//
|
|
@@ -14250,7 +14403,7 @@
|
|
|
14250
14403
|
// target: layer name, or null to use the current target
|
|
14251
14404
|
// via: (optional) the callout-via the text carried with it
|
|
14252
14405
|
function getLabelOffsetCommand(opts) {
|
|
14253
|
-
var parts = ['-
|
|
14406
|
+
var parts = ['-labels', 'dx=' + opts.dx, 'dy=' + opts.dy,
|
|
14254
14407
|
'text-anchor=' + opts.anchor, 'label-pos='];
|
|
14255
14408
|
if (opts.via) parts.push('callout-via=' + quoteCommandValue(opts.via));
|
|
14256
14409
|
parts.push('ids=' + opts.id);
|
|
@@ -14261,16 +14414,16 @@
|
|
|
14261
14414
|
// The command a drag on one of a label's handles writes, run once on release.
|
|
14262
14415
|
//
|
|
14263
14416
|
// values: {field: value}, where '' removes the field -- which is what
|
|
14264
|
-
// -
|
|
14417
|
+
// -labels reads an empty value as, and how a handle goes back to automatic
|
|
14265
14418
|
// id: feature id of the label
|
|
14266
14419
|
// opts:
|
|
14267
14420
|
// target: layer name, or null to use the current target
|
|
14268
14421
|
// text: (optional) rewrapped label-text, with real newlines and soft
|
|
14269
|
-
// breaks. A second -
|
|
14422
|
+
// breaks. A second -labels in the same string, so that a new width and
|
|
14270
14423
|
// the lines it breaks into are one undo step.
|
|
14271
14424
|
function getLabelStyleCommand(values, id, opts) {
|
|
14272
14425
|
var o = opts || {};
|
|
14273
|
-
var parts = ['-
|
|
14426
|
+
var parts = ['-labels'];
|
|
14274
14427
|
Object.keys(values).forEach(function(name) {
|
|
14275
14428
|
var val = values[name];
|
|
14276
14429
|
parts.push(name + '=' + (val === '' ? '' : quoteCommandValue(String(val))));
|
|
@@ -14292,7 +14445,7 @@
|
|
|
14292
14445
|
function getLabelTextCommand(text, id, target) {
|
|
14293
14446
|
// A real newline would break the command parser, so encodeLabelText() writes
|
|
14294
14447
|
// the two-character escape that the label renderer also accepts.
|
|
14295
|
-
var parts = ['-
|
|
14448
|
+
var parts = ['-labels', 'text=' + quoteCommandValue(encodeLabelText(text))];
|
|
14296
14449
|
parts.push('ids=' + id);
|
|
14297
14450
|
if (target) parts.push('target=' + quoteCommandValue(target));
|
|
14298
14451
|
return parts.join(' ');
|
|
@@ -15407,6 +15560,19 @@
|
|
|
15407
15560
|
return cachedFontStyles[fontName];
|
|
15408
15561
|
}
|
|
15409
15562
|
|
|
15563
|
+
// Whether words in @fontName can be set in bold: a bold the browser
|
|
15564
|
+
// synthesizes is not one the Node measurer can match, so wrapping and the
|
|
15565
|
+
// path-fit check would disagree with what is drawn. A font that cannot be
|
|
15566
|
+
// identified is given the benefit of the doubt -- the browser finds whatever
|
|
15567
|
+
// bold it has, and there is nothing to check against.
|
|
15568
|
+
function fontHasBoldFace(fontName) {
|
|
15569
|
+
var variants = fontName ? getFontStyleVariants(fontName) : null;
|
|
15570
|
+
if (!variants || variants.length === 0) return true;
|
|
15571
|
+
return variants.some(function(variant) {
|
|
15572
|
+
return Number(variant.weight) >= 600;
|
|
15573
|
+
});
|
|
15574
|
+
}
|
|
15575
|
+
|
|
15410
15576
|
// The installed font the browser draws unfonted text in, or '' if it cannot
|
|
15411
15577
|
// be worked out.
|
|
15412
15578
|
//
|
|
@@ -16569,6 +16735,15 @@
|
|
|
16569
16735
|
!/^(button|checkbox|radio|submit)$/.test(node.type);
|
|
16570
16736
|
}
|
|
16571
16737
|
|
|
16738
|
+
// Whether clicking this node puts a native menu on screen, which must then be
|
|
16739
|
+
// left holding the focus: a menu opens on mousedown, so a panel that lets go of
|
|
16740
|
+
// focus on the click that follows closes it again -- it flashes open and shut.
|
|
16741
|
+
// OPTION counts because a browser that reports the chosen option as the click
|
|
16742
|
+
// target is reporting a menu interaction either way.
|
|
16743
|
+
function opensAMenu(node) {
|
|
16744
|
+
return !!node && /^(SELECT|OPTION)$/.test(node.nodeName);
|
|
16745
|
+
}
|
|
16746
|
+
|
|
16572
16747
|
// While the caret is in one of @panel's fields, the keyboard belongs to that
|
|
16573
16748
|
// field. Without this the GUI's own handlers see the keystrokes: a Backspace
|
|
16574
16749
|
// typed into a field takes back the last knot of a curve being drawn, and an
|
|
@@ -16612,20 +16787,43 @@
|
|
|
16612
16787
|
|
|
16613
16788
|
// The fraction an opacity control's contents mean ("50%", " 50 " -> 0.5), or
|
|
16614
16789
|
// null if it is not holding a number. Out-of-range values are clamped rather
|
|
16615
|
-
// than refused: a pasted 150% is an intent, not a mistake.
|
|
16790
|
+
// than refused: a pasted 150% is an intent, not a mistake. A blank field is
|
|
16791
|
+
// null, not zero: it is what a colour with no value shows beside it.
|
|
16616
16792
|
function parseOpacityValue(str) {
|
|
16617
|
-
var
|
|
16618
|
-
|
|
16793
|
+
var txt = String(str).replace('%', '').trim();
|
|
16794
|
+
var pct = Number(txt);
|
|
16795
|
+
if (txt === '' || !isFinite(pct)) return null;
|
|
16619
16796
|
return Math.max(0, Math.min(100, pct)) / 100;
|
|
16620
16797
|
}
|
|
16621
16798
|
|
|
16799
|
+
// A typed dash pattern in the form -style stroke-dasharray= takes: lengths
|
|
16800
|
+
// separated by single spaces. Commas and runs of whitespace are accepted
|
|
16801
|
+
// because SVG accepts them, and a pasted "4, 2" means the same as "4 2".
|
|
16802
|
+
// Returns '' for a blank field. Does not check that the lengths are numbers.
|
|
16803
|
+
function normalizeDashArrayInput(str) {
|
|
16804
|
+
return String(str).trim().split(/[\s,]+/).filter(Boolean).join(' ');
|
|
16805
|
+
}
|
|
16806
|
+
|
|
16622
16807
|
// A stored fraction as the percentage a control shows, or '' for no value --
|
|
16623
16808
|
// which is how a control over a selection that does not agree shows.
|
|
16624
16809
|
function formatOpacityPct(val) {
|
|
16810
|
+
if (isUnsetValue(val)) return '';
|
|
16625
16811
|
val = Number(val);
|
|
16626
16812
|
return isFinite(val) ? Math.round(Math.max(0, Math.min(1, val)) * 100) + '%' : '';
|
|
16627
16813
|
}
|
|
16628
16814
|
|
|
16815
|
+
// The opacity shown beside a colour. A colour with no opacity of its own is
|
|
16816
|
+
// drawn opaque, and says so; with no colour either, there is nothing for an
|
|
16817
|
+
// opacity to apply to, and the field is blank like the colour.
|
|
16818
|
+
function formatColorOpacityPct(opacity, hasColor) {
|
|
16819
|
+
if (isUnsetValue(opacity)) return hasColor ? '100%' : '';
|
|
16820
|
+
return formatOpacityPct(opacity);
|
|
16821
|
+
}
|
|
16822
|
+
|
|
16823
|
+
function isUnsetValue(val) {
|
|
16824
|
+
return val === undefined || val === null || val === '';
|
|
16825
|
+
}
|
|
16826
|
+
|
|
16629
16827
|
// The parts the style panels are built from. They were the label panel's, and
|
|
16630
16828
|
// three panels drawing the same control three ways is how they came to look
|
|
16631
16829
|
// like three programs: a colour was a swatch inside a field in one and a
|
|
@@ -16653,6 +16851,52 @@
|
|
|
16653
16851
|
return section;
|
|
16654
16852
|
}
|
|
16655
16853
|
|
|
16854
|
+
// A switch: a track with a knob that sits left when off and right when on,
|
|
16855
|
+
// which is the direction users expect and the only thing that says which
|
|
16856
|
+
// state is which without a label for each. A third state says that the
|
|
16857
|
+
// features it is asking about disagree -- the knob sits over the join of a
|
|
16858
|
+
// half-and-half track, which is the only control that has to show "some of
|
|
16859
|
+
// them" rather than a value. A section's switch goes in its heading row, and
|
|
16860
|
+
// the section shows only its heading while it is off (the .collapsed class).
|
|
16861
|
+
//
|
|
16862
|
+
// role=checkbox rather than switch, which is what it looks like: 'mixed' is a
|
|
16863
|
+
// legal aria-checked value for a checkbox and not for a switch, and a switch
|
|
16864
|
+
// reporting a mixed selection as unchecked would be telling a screen reader
|
|
16865
|
+
// the one thing the third state exists to avoid saying.
|
|
16866
|
+
//
|
|
16867
|
+
// opts.onChange(on) the switch was clicked
|
|
16868
|
+
// opts.title, opts.className
|
|
16869
|
+
function makePanelToggle(parent, opts) {
|
|
16870
|
+
var track = El('div').addClass('label-toggle').attr('role', 'checkbox').appendTo(parent);
|
|
16871
|
+
var state = 'off';
|
|
16872
|
+
if (opts.className) track.addClass(opts.className);
|
|
16873
|
+
var disabled = false;
|
|
16874
|
+
El('div').addClass('label-toggle-knob').appendTo(track);
|
|
16875
|
+
if (opts.title) track.attr('title', opts.title);
|
|
16876
|
+
// A click on a mixed switch turns everything on. It is the convention, and
|
|
16877
|
+
// it is the reading that reaches a state the switch can describe: the next
|
|
16878
|
+
// click then turns everything off, so both are one click away.
|
|
16879
|
+
track.on('click', function() {
|
|
16880
|
+
if (disabled) return;
|
|
16881
|
+
opts.onChange(state != 'on');
|
|
16882
|
+
});
|
|
16883
|
+
return {
|
|
16884
|
+
// val: 'on', 'off' or 'mixed'; anything else reads as off
|
|
16885
|
+
setState: function(val) {
|
|
16886
|
+
state = val == 'on' || val == 'mixed' ? val : 'off';
|
|
16887
|
+
track.classed('on', state == 'on')
|
|
16888
|
+
.classed('mixed', state == 'mixed')
|
|
16889
|
+
.attr('aria-checked', state == 'mixed' ? 'mixed' :
|
|
16890
|
+
state == 'on' ? 'true' : 'false');
|
|
16891
|
+
},
|
|
16892
|
+
setDisabled: function(off) {
|
|
16893
|
+
disabled = !!off;
|
|
16894
|
+
track.classed('disabled', disabled)
|
|
16895
|
+
.attr('aria-disabled', disabled ? 'true' : 'false');
|
|
16896
|
+
}
|
|
16897
|
+
};
|
|
16898
|
+
}
|
|
16899
|
+
|
|
16656
16900
|
// Deliberately not focusable: the GUI is pointer-only, so a tab stop here
|
|
16657
16901
|
// would lead into a control the keyboard cannot then operate. See the focus
|
|
16658
16902
|
// note in page.css.
|
|
@@ -16685,6 +16929,16 @@
|
|
|
16685
16929
|
return btn;
|
|
16686
16930
|
}
|
|
16687
16931
|
|
|
16932
|
+
// The "?" the rest of the app uses for field help (see .tip-button in
|
|
16933
|
+
// elements.css and the static ones in index.html). The bubble is white-space:
|
|
16934
|
+
// pre, so the line breaks in the text are the ones it gets.
|
|
16935
|
+
function makeFieldTip(parent, text) {
|
|
16936
|
+
var btn = El('div').addClass('tip-button').appendTo(parent).text('?');
|
|
16937
|
+
var anchor = El('div').addClass('tip-anchor').appendTo(btn);
|
|
16938
|
+
El('div').addClass('tip').appendTo(anchor).text(text);
|
|
16939
|
+
return btn;
|
|
16940
|
+
}
|
|
16941
|
+
|
|
16688
16942
|
function setPanelButtonDisabled(el, disabled) {
|
|
16689
16943
|
el.classed('disabled', !!disabled)
|
|
16690
16944
|
.attr('aria-disabled', disabled ? 'true' : 'false');
|
|
@@ -16719,6 +16973,8 @@
|
|
|
16719
16973
|
// opts.onColor(hex) a colour was typed, picked or previewed to a finish
|
|
16720
16974
|
// opts.onOpacity(frac) a usable percentage was typed
|
|
16721
16975
|
// opts.revert() one that was not, so put the row back as it was
|
|
16976
|
+
// opts.noOpacity a colour with no opacity of its own, like a pattern's,
|
|
16977
|
+
// which fill-opacity fades along with its background
|
|
16722
16978
|
//
|
|
16723
16979
|
// control.aside is the row's narrow column, empty, for a field that belongs
|
|
16724
16980
|
// beside the colour -- a stroke's width, say.
|
|
@@ -16756,10 +17012,14 @@
|
|
|
16756
17012
|
if (isHexColor(color)) control.picker.setColor(color);
|
|
16757
17013
|
opts.onColor(color);
|
|
16758
17014
|
});
|
|
16759
|
-
|
|
16760
|
-
|
|
16761
|
-
|
|
16762
|
-
|
|
17015
|
+
if (opts.noOpacity) {
|
|
17016
|
+
makeColorField(colorCell, control.chit, control.input);
|
|
17017
|
+
} else {
|
|
17018
|
+
control.opacity = makeColorOpacityField(colorCell, control.chit, control.input, {
|
|
17019
|
+
onSet: opts.onOpacity,
|
|
17020
|
+
revert: opts.revert
|
|
17021
|
+
}).opacity;
|
|
17022
|
+
}
|
|
16763
17023
|
control.picker = new ColorPicker(colorCell, {
|
|
16764
17024
|
presetRows: layerColorPresetRows,
|
|
16765
17025
|
// A drag in the picker shows on the map's own terms -- the swatch and the
|
|
@@ -16799,13 +17059,13 @@
|
|
|
16799
17059
|
//
|
|
16800
17060
|
// The style panel is worth having open before there is anything to point it at:
|
|
16801
17061
|
// pick a font, then place labels in it. Values set with nothing selected are
|
|
16802
|
-
// held here rather than written to a layer, and -
|
|
17062
|
+
// held here rather than written to a layer, and -labels coordinates= writes them when a
|
|
16803
17063
|
// label finally exists -- so this is a tool default, not data, and it never
|
|
16804
17064
|
// needs its own undo step.
|
|
16805
17065
|
//
|
|
16806
17066
|
// See docs/development/label-tool-design.md.
|
|
16807
17067
|
|
|
16808
|
-
// The style properties -
|
|
17068
|
+
// The style properties -labels accepts. A panel control whose property is
|
|
16809
17069
|
// missing from this list would appear to do nothing when set with no selection,
|
|
16810
17070
|
// so the list is checked rather than assumed.
|
|
16811
17071
|
var NEW_LABEL_STYLE_FIELDS = [
|
|
@@ -16827,7 +17087,7 @@
|
|
|
16827
17087
|
//
|
|
16828
17088
|
// A blank or zero value removes the field instead of setting it: that is how
|
|
16829
17089
|
// the panel says "no icon" (icon='', icon-size=0) and "no inline css", and
|
|
16830
|
-
// carrying those through to -
|
|
17090
|
+
// carrying those through to -labels would write properties that mean
|
|
16831
17091
|
// nothing.
|
|
16832
17092
|
function mergeStyleValues(style, values) {
|
|
16833
17093
|
var out = Object.assign({}, style);
|
|
@@ -16853,7 +17113,7 @@
|
|
|
16853
17113
|
// somewhere and typing looks like it should do, and it is the position an icon
|
|
16854
17114
|
// is drawn to sit behind.
|
|
16855
17115
|
//
|
|
16856
|
-
// Only the tool defaults this. -
|
|
17116
|
+
// Only the tool defaults this. -labels coordinates= with no position still creates a
|
|
16857
17117
|
// label without one, so the default is a choice this tool makes and passes on
|
|
16858
17118
|
// explicitly, not a meaning the command gives to its absence.
|
|
16859
17119
|
var DEFAULT_NEW_LABEL_STYLE = {'label-pos': 'c'};
|
|
@@ -17167,13 +17427,17 @@
|
|
|
17167
17427
|
|
|
17168
17428
|
// Offsets into @text (real newlines, no soft breaks) where a line starts
|
|
17169
17429
|
// because it was wrapped, in ascending order.
|
|
17170
|
-
|
|
17430
|
+
//
|
|
17431
|
+
// @bold: the text's bold stretches, as [start, end) offsets into it. A bold
|
|
17432
|
+
// word is wider, so it moves the breaks.
|
|
17433
|
+
function getSoftBreaks(text, rec, bold) {
|
|
17171
17434
|
var width = getLabelWrapWidth(rec);
|
|
17172
17435
|
var key, breaks;
|
|
17173
17436
|
if (!width || !text) return [];
|
|
17174
|
-
|
|
17437
|
+
bold = bold || [];
|
|
17438
|
+
key = getWrapKey(text, rec, bold);
|
|
17175
17439
|
if (cache$2.has(key)) return cache$2.get(key);
|
|
17176
|
-
breaks = measureSoftBreaks(text, rec, width);
|
|
17440
|
+
breaks = measureSoftBreaks(text, rec, width, bold);
|
|
17177
17441
|
if (cache$2.size >= CACHE_LIMIT) cache$2.clear();
|
|
17178
17442
|
cache$2.set(key, breaks);
|
|
17179
17443
|
return breaks;
|
|
@@ -17183,29 +17447,30 @@
|
|
|
17183
17447
|
// unchanged when neither the old value nor the new one has a soft break, so
|
|
17184
17448
|
// that a label that does not wrap keeps the line breaks it was written with.
|
|
17185
17449
|
function rewrapLabelValue(value, rec) {
|
|
17186
|
-
var o =
|
|
17187
|
-
var breaks = getSoftBreaks(o.text, rec);
|
|
17450
|
+
var o = readLabelValue(value);
|
|
17451
|
+
var breaks = getSoftBreaks(o.text, rec, o.bold);
|
|
17188
17452
|
if (sameSoftBreaks(o.breaks, breaks)) return value;
|
|
17189
|
-
return encodeLabelText(
|
|
17453
|
+
return encodeLabelText(writeLabelValue(o.text, breaks, o.bold));
|
|
17190
17454
|
}
|
|
17191
17455
|
|
|
17192
17456
|
function clearWrapCache() {
|
|
17193
17457
|
cache$2.clear();
|
|
17194
17458
|
}
|
|
17195
17459
|
|
|
17196
|
-
function getWrapKey(text, rec) {
|
|
17460
|
+
function getWrapKey(text, rec, bold) {
|
|
17197
17461
|
var parts = [text];
|
|
17198
17462
|
WRAP_FIELDS.forEach(function(name) {
|
|
17199
17463
|
var val = rec[name];
|
|
17200
17464
|
parts.push(val === null || val === undefined ? '' : String(val));
|
|
17201
17465
|
});
|
|
17202
17466
|
parts.push(rec['class'] || '');
|
|
17467
|
+
parts.push(bold.join(';'));
|
|
17203
17468
|
return parts.join('\u0000');
|
|
17204
17469
|
}
|
|
17205
17470
|
|
|
17206
|
-
function measureSoftBreaks(text, rec, width) {
|
|
17471
|
+
function measureSoftBreaks(text, rec, width, bold) {
|
|
17207
17472
|
var w = getWrapper();
|
|
17208
|
-
var style, fontSize,
|
|
17473
|
+
var style, fontSize, pieces, range;
|
|
17209
17474
|
if (!w) return [];
|
|
17210
17475
|
style = getLabelFontStyle(w, rec);
|
|
17211
17476
|
if (!style) return [];
|
|
@@ -17213,15 +17478,15 @@
|
|
|
17213
17478
|
w.div.style[name] = style[name] || '';
|
|
17214
17479
|
});
|
|
17215
17480
|
w.div.style.width = width + 'px';
|
|
17216
|
-
w.div
|
|
17217
|
-
node = w.div.firstChild;
|
|
17481
|
+
pieces = writeRuns(w.div, text, bold);
|
|
17218
17482
|
fontSize = parseFloat(style.fontSize) || 12;
|
|
17219
17483
|
range = document.createRange();
|
|
17220
17484
|
var breaks = findSoftBreaks(text, function(i) {
|
|
17485
|
+
var piece = findPiece(pieces, i);
|
|
17221
17486
|
var end = isHighSurrogate(text.charCodeAt(i)) ? i + 2 : i + 1;
|
|
17222
17487
|
var r;
|
|
17223
|
-
range.setStart(node, i);
|
|
17224
|
-
range.setEnd(node, Math.min(end,
|
|
17488
|
+
range.setStart(piece.node, i - piece.start);
|
|
17489
|
+
range.setEnd(piece.node, Math.min(end, piece.end) - piece.start);
|
|
17225
17490
|
r = range.getBoundingClientRect();
|
|
17226
17491
|
return r.height > 0 ? r.top : null;
|
|
17227
17492
|
}, fontSize * LINE_HEIGHT / 2);
|
|
@@ -17229,6 +17494,35 @@
|
|
|
17229
17494
|
return breaks;
|
|
17230
17495
|
}
|
|
17231
17496
|
|
|
17497
|
+
// Lays @text out in @div a run at a time, the bold ones in bold spans, and
|
|
17498
|
+
// returns each run's text node with the offsets it covers: [{node, start,
|
|
17499
|
+
// end}]. Offsets in the text become offsets in one of these nodes.
|
|
17500
|
+
function writeRuns(div, text, bold) {
|
|
17501
|
+
var pos = 0;
|
|
17502
|
+
div.textContent = '';
|
|
17503
|
+
return internal.svg.getBoldRuns(text, bold, 0, text.length).map(function(run) {
|
|
17504
|
+
var node = document.createTextNode(run.text);
|
|
17505
|
+
var span;
|
|
17506
|
+
if (run.bold) {
|
|
17507
|
+
span = document.createElement('span');
|
|
17508
|
+
span.style.fontWeight = internal.svg.LABEL_BOLD_WEIGHT;
|
|
17509
|
+
span.appendChild(node);
|
|
17510
|
+
div.appendChild(span);
|
|
17511
|
+
} else {
|
|
17512
|
+
div.appendChild(node);
|
|
17513
|
+
}
|
|
17514
|
+
pos += run.text.length;
|
|
17515
|
+
return {node: node, start: pos - run.text.length, end: pos};
|
|
17516
|
+
});
|
|
17517
|
+
}
|
|
17518
|
+
|
|
17519
|
+
function findPiece(pieces, i) {
|
|
17520
|
+
for (var j = 0; j < pieces.length; j++) {
|
|
17521
|
+
if (i < pieces[j].end) return pieces[j];
|
|
17522
|
+
}
|
|
17523
|
+
return pieces[pieces.length - 1];
|
|
17524
|
+
}
|
|
17525
|
+
|
|
17232
17526
|
function isHighSurrogate(c) {
|
|
17233
17527
|
return c >= 0xD800 && c <= 0xDBFF;
|
|
17234
17528
|
}
|
|
@@ -17700,7 +17994,7 @@
|
|
|
17700
17994
|
// spacing sits above.
|
|
17701
17995
|
haloSection = addSection('Halo');
|
|
17702
17996
|
var haloTitle = haloSection.findChild('.label-style-section-title');
|
|
17703
|
-
haloToggle =
|
|
17997
|
+
haloToggle = makePanelToggle(haloTitle, {
|
|
17704
17998
|
title: 'Draw a halo around the text',
|
|
17705
17999
|
className: 'label-halo-toggle',
|
|
17706
18000
|
onChange: setHaloOn
|
|
@@ -17751,7 +18045,7 @@
|
|
|
17751
18045
|
// a label with no icon: nothing to apply it to.
|
|
17752
18046
|
iconSection = addSection('Icon');
|
|
17753
18047
|
var iconTitle = iconSection.findChild('.label-style-section-title');
|
|
17754
|
-
iconToggle =
|
|
18048
|
+
iconToggle = makePanelToggle(iconTitle, {
|
|
17755
18049
|
title: 'Draw a symbol at the label anchor',
|
|
17756
18050
|
className: 'label-icon-toggle',
|
|
17757
18051
|
onChange: setIconOn
|
|
@@ -17858,7 +18152,7 @@
|
|
|
17858
18152
|
// the map rather than typed here. See docs/development/text-annotation-design.md.
|
|
17859
18153
|
function initCalloutSection() {
|
|
17860
18154
|
calloutSection = addSection('Callout');
|
|
17861
|
-
calloutToggle =
|
|
18155
|
+
calloutToggle = makePanelToggle(calloutSection.findChild('.label-style-section-title'), {
|
|
17862
18156
|
title: 'Draw a line from the label anchor to its text',
|
|
17863
18157
|
className: 'label-callout-toggle',
|
|
17864
18158
|
onChange: setCalloutOn
|
|
@@ -17979,48 +18273,6 @@
|
|
|
17979
18273
|
return makePanelSection(panel, title, opts);
|
|
17980
18274
|
}
|
|
17981
18275
|
|
|
17982
|
-
// A switch: a track with a knob that sits left when off and right when on,
|
|
17983
|
-
// which is the direction users expect and the only thing that says which
|
|
17984
|
-
// state is which without a label for each. A third state says that the
|
|
17985
|
-
// labels it is asking about disagree -- the knob sits over the join of a
|
|
17986
|
-
// half-and-half track, which is the panel's only control that has to show
|
|
17987
|
-
// "some of them" rather than a value.
|
|
17988
|
-
//
|
|
17989
|
-
// role=checkbox rather than switch, which is what it looks like: 'mixed' is a
|
|
17990
|
-
// legal aria-checked value for a checkbox and not for a switch, and a switch
|
|
17991
|
-
// reporting a mixed selection as unchecked would be telling a screen reader
|
|
17992
|
-
// the one thing the third state exists to avoid saying.
|
|
17993
|
-
function makeToggle(parent, opts) {
|
|
17994
|
-
var track = El('div').addClass('label-toggle').attr('role', 'checkbox').appendTo(parent);
|
|
17995
|
-
var state = 'off';
|
|
17996
|
-
if (opts.className) track.addClass(opts.className);
|
|
17997
|
-
var disabled = false;
|
|
17998
|
-
El('div').addClass('label-toggle-knob').appendTo(track);
|
|
17999
|
-
if (opts.title) track.attr('title', opts.title);
|
|
18000
|
-
// A click on a mixed switch turns everything on. It is the convention, and
|
|
18001
|
-
// it is the reading that reaches a state the switch can describe: the next
|
|
18002
|
-
// click then turns everything off, so both are one click away.
|
|
18003
|
-
track.on('click', function() {
|
|
18004
|
-
if (disabled) return;
|
|
18005
|
-
opts.onChange(state != 'on');
|
|
18006
|
-
});
|
|
18007
|
-
return {
|
|
18008
|
-
// val: 'on', 'off' or 'mixed'; anything else reads as off
|
|
18009
|
-
setState: function(val) {
|
|
18010
|
-
state = val == 'on' || val == 'mixed' ? val : 'off';
|
|
18011
|
-
track.classed('on', state == 'on')
|
|
18012
|
-
.classed('mixed', state == 'mixed')
|
|
18013
|
-
.attr('aria-checked', state == 'mixed' ? 'mixed' :
|
|
18014
|
-
state == 'on' ? 'true' : 'false');
|
|
18015
|
-
},
|
|
18016
|
-
setDisabled: function(off) {
|
|
18017
|
-
disabled = !!off;
|
|
18018
|
-
track.classed('disabled', disabled)
|
|
18019
|
-
.attr('aria-disabled', disabled ? 'true' : 'false');
|
|
18020
|
-
}
|
|
18021
|
-
};
|
|
18022
|
-
}
|
|
18023
|
-
|
|
18024
18276
|
function addColorOpacityField(parent, chit, input, onOpacity) {
|
|
18025
18277
|
return makeColorOpacityField(parent, chit, input, {
|
|
18026
18278
|
onSet: onOpacity,
|
|
@@ -19348,14 +19600,6 @@
|
|
|
19348
19600
|
return !!node && /^(INPUT|SELECT|TEXTAREA|BUTTON|OPTION)$/.test(node.nodeName);
|
|
19349
19601
|
}
|
|
19350
19602
|
|
|
19351
|
-
// Whether clicking this node puts a native menu on screen, which must then be
|
|
19352
|
-
// left holding the focus. OPTION counts because a browser that reports the
|
|
19353
|
-
// chosen option as the click target is reporting a menu interaction either
|
|
19354
|
-
// way, and a choice restores the caret through its change handler.
|
|
19355
|
-
function opensAMenu(node) {
|
|
19356
|
-
return !!node && /^(SELECT|OPTION)$/.test(node.nodeName);
|
|
19357
|
-
}
|
|
19358
|
-
|
|
19359
19603
|
// Hands the keyboard back after a control has done its job: to the label
|
|
19360
19604
|
// being typed into if there is one, and to nothing at all otherwise.
|
|
19361
19605
|
function releaseFocus() {
|
|
@@ -19374,7 +19618,7 @@
|
|
|
19374
19618
|
if (styles.length === 0) return;
|
|
19375
19619
|
releaseFocus();
|
|
19376
19620
|
// What the panel is set to is always what the next label gets, whether or
|
|
19377
|
-
// not these values also went to a label that already exists. -
|
|
19621
|
+
// not these values also went to a label that already exists. -labels
|
|
19378
19622
|
// writes them when that label is created, which is why this needs no
|
|
19379
19623
|
// command and no undo step of its own.
|
|
19380
19624
|
if (labelModeIsOn()) {
|
|
@@ -19398,7 +19642,7 @@
|
|
|
19398
19642
|
|
|
19399
19643
|
function applyStyleCommand(styles, ids) {
|
|
19400
19644
|
var lyr = getActiveLayer();
|
|
19401
|
-
var parts = ['-
|
|
19645
|
+
var parts = ['-labels'];
|
|
19402
19646
|
if (!gui.console || !lyr) return;
|
|
19403
19647
|
styles.forEach(function(style) {
|
|
19404
19648
|
parts.push(style[0] + '=' + quoteCommandValue(style[1]));
|
|
@@ -19420,7 +19664,7 @@
|
|
|
19420
19664
|
// A text block's breaks are only right for the width and font they were
|
|
19421
19665
|
// found with, so a change to either rewraps it in the same command: one undo
|
|
19422
19666
|
// step, and no moment at which the block is drawn with its old lines in its
|
|
19423
|
-
// new font. One -
|
|
19667
|
+
// new font. One -labels per label, since each has text of its own.
|
|
19424
19668
|
function getRewrapCommands(styles, ids, lyr) {
|
|
19425
19669
|
var table = lyr.data;
|
|
19426
19670
|
var changes = {};
|
|
@@ -19554,13 +19798,480 @@
|
|
|
19554
19798
|
return measureSvg;
|
|
19555
19799
|
}
|
|
19556
19800
|
|
|
19801
|
+
// The pattern fill controls of the polygon panel, and the fill-pattern codes
|
|
19802
|
+
// they stand for.
|
|
19803
|
+
//
|
|
19804
|
+
// The panel covers the patterns that are one colour over a background: hatch
|
|
19805
|
+
// lines, dots and squares. The background is the feature's own fill, so a
|
|
19806
|
+
// pattern is described by five settings -- {type, color, size, gap, angle} --
|
|
19807
|
+
// and the fill. A hatch has no background in the code: it is a run of stripes,
|
|
19808
|
+
// and the panel's hatch is two of them, the gap in the fill colour and the line
|
|
19809
|
+
// in the pattern colour. Anything else (dashes, several colours, a background
|
|
19810
|
+
// that is not the fill) is a custom pattern, which the panel shows as its code.
|
|
19811
|
+
//
|
|
19812
|
+
// Written into every code rather than read from the fill when drawn, because
|
|
19813
|
+
// the code is what -style stores and what the SVG exporter turns into a
|
|
19814
|
+
// <pattern>: a pattern with no colour of its own would need both to change.
|
|
19815
|
+
|
|
19816
|
+
var patternTypes = ['hatches', 'dots', 'squares'];
|
|
19817
|
+
|
|
19818
|
+
var patternDefaults = {
|
|
19819
|
+
hatches: {size: 1, gap: 3, angle: 45},
|
|
19820
|
+
dots: {size: 2, gap: 3, angle: 0},
|
|
19821
|
+
squares: {size: 2, gap: 2, angle: 0}
|
|
19822
|
+
};
|
|
19823
|
+
|
|
19824
|
+
var defaultPatternColor = '#000000';
|
|
19825
|
+
|
|
19826
|
+
// The settings a newly chosen pattern type starts from. The colour carries
|
|
19827
|
+
// over from the pattern being replaced; the sizes do not, because a hatch
|
|
19828
|
+
// line's width and a dot's diameter look nothing alike at the same number.
|
|
19829
|
+
function getDefaultPatternControls(type, color) {
|
|
19830
|
+
var d = patternDefaults[type];
|
|
19831
|
+
return {type: type, color: color || defaultPatternColor,
|
|
19832
|
+
size: d.size, gap: d.gap, angle: d.angle};
|
|
19833
|
+
}
|
|
19834
|
+
|
|
19835
|
+
// The background a pattern is given over a feature with @fill. A feature with
|
|
19836
|
+
// no fill gets a clear one, so that the pattern is drawn over whatever is
|
|
19837
|
+
// underneath.
|
|
19838
|
+
function getPatternBackground(fill) {
|
|
19839
|
+
return isBlank$1(fill) ? 'none' : String(fill).trim();
|
|
19840
|
+
}
|
|
19841
|
+
|
|
19842
|
+
// Whether the settings describe a pattern the renderer will draw. A hatch with
|
|
19843
|
+
// no gap would be a solid fill, and the parser refuses a stripe of no width.
|
|
19844
|
+
function isValidPatternControls(o) {
|
|
19845
|
+
if (!o || patternTypes.indexOf(o.type) == -1 || isBlank$1(o.color)) return false;
|
|
19846
|
+
if (!(o.size > 0) || !isFinite(o.angle)) return false;
|
|
19847
|
+
return o.type == 'hatches' ? o.gap > 0 : o.gap >= 0;
|
|
19848
|
+
}
|
|
19849
|
+
|
|
19850
|
+
// The fill-pattern code for the settings @o over @background. The angle is left
|
|
19851
|
+
// out where it is the parser's own default, which keeps the common codes as
|
|
19852
|
+
// short as the ones in the documentation.
|
|
19853
|
+
function formatFillPattern(o, background) {
|
|
19854
|
+
var bg = background || 'none';
|
|
19855
|
+
if (o.type == 'hatches') {
|
|
19856
|
+
return 'hatches ' + formatAngle(o.angle, 45) +
|
|
19857
|
+
formatPx(o.gap) + ' ' + bg + ' ' + formatPx(o.size) + ' ' + o.color;
|
|
19858
|
+
}
|
|
19859
|
+
return o.type + ' ' + formatAngle(o.angle, 0) +
|
|
19860
|
+
formatPx(o.size) + ' ' + o.color + ' ' + formatPx(o.gap) + ' ' + bg;
|
|
19861
|
+
}
|
|
19862
|
+
|
|
19863
|
+
// The settings a parsed fill-pattern code (from parsePattern()) was made from,
|
|
19864
|
+
// given the fill of the feature it is on -- or null if the panel's settings
|
|
19865
|
+
// cannot describe it, which makes it a custom pattern.
|
|
19866
|
+
function getPatternControls(parsed, fill) {
|
|
19867
|
+
var bg = getPatternBackground(fill);
|
|
19868
|
+
var line;
|
|
19869
|
+
if (!parsed) return null;
|
|
19870
|
+
if (parsed.type == 'hatches') {
|
|
19871
|
+
if (parsed.widths.length != 2) return null;
|
|
19872
|
+
line = sameColor(parsed.colors[0], bg) ? 1 : sameColor(parsed.colors[1], bg) ? 0 : -1;
|
|
19873
|
+
if (line == -1) return null;
|
|
19874
|
+
return {type: 'hatches', color: parsed.colors[line], size: parsed.widths[line],
|
|
19875
|
+
gap: parsed.widths[1 - line], angle: parsed.rotation};
|
|
19876
|
+
}
|
|
19877
|
+
if (parsed.type == 'dots' || parsed.type == 'squares') {
|
|
19878
|
+
if (parsed.colors.length != 1 || !sameColor(parsed.background, bg)) return null;
|
|
19879
|
+
return {type: parsed.type, color: parsed.colors[0], size: parsed.size,
|
|
19880
|
+
gap: parsed.spacing, angle: parsed.rotation};
|
|
19881
|
+
}
|
|
19882
|
+
return null;
|
|
19883
|
+
}
|
|
19884
|
+
|
|
19885
|
+
// The same pattern over a different fill, or null for a custom pattern, which
|
|
19886
|
+
// is left as it is.
|
|
19887
|
+
function refillPattern(parsed, oldFill, newFill) {
|
|
19888
|
+
var o = getPatternControls(parsed, oldFill);
|
|
19889
|
+
return o ? formatFillPattern(o, getPatternBackground(newFill)) : null;
|
|
19890
|
+
}
|
|
19891
|
+
|
|
19892
|
+
// A -style expression giving each feature the pattern @o over its own fill.
|
|
19893
|
+
// For a change that sets fills the panel cannot know in advance, like
|
|
19894
|
+
// -classify's random colours.
|
|
19895
|
+
function formatFillPatternExpression(o) {
|
|
19896
|
+
var sentinel = '\u0000';
|
|
19897
|
+
var parts = formatFillPattern(o, sentinel).split(sentinel);
|
|
19898
|
+
return JSON.stringify(parts[0]) + ' + (fill || "none") + ' + JSON.stringify(parts[1]);
|
|
19899
|
+
}
|
|
19900
|
+
|
|
19901
|
+
// Collects per-feature edits into as few -style commands as they allow.
|
|
19902
|
+
// @edits: [{id, styles: [[name, value], ...]}]
|
|
19903
|
+
// Returns [{styles, ids}], in the order each set of styles first appears.
|
|
19904
|
+
function groupStyleEdits(edits) {
|
|
19905
|
+
var groups = [];
|
|
19906
|
+
var index = {};
|
|
19907
|
+
edits.forEach(function(edit) {
|
|
19908
|
+
var key = JSON.stringify(edit.styles);
|
|
19909
|
+
if (!(key in index)) {
|
|
19910
|
+
index[key] = groups.length;
|
|
19911
|
+
groups.push({styles: edit.styles, ids: []});
|
|
19912
|
+
}
|
|
19913
|
+
groups[index[key]].ids.push(edit.id);
|
|
19914
|
+
});
|
|
19915
|
+
return groups;
|
|
19916
|
+
}
|
|
19917
|
+
|
|
19918
|
+
function formatAngle(angle, defaultAngle) {
|
|
19919
|
+
return angle == defaultAngle ? '' : formatNumber$3(angle) + 'deg ';
|
|
19920
|
+
}
|
|
19921
|
+
|
|
19922
|
+
function formatPx(val) {
|
|
19923
|
+
return formatNumber$3(val) + 'px';
|
|
19924
|
+
}
|
|
19925
|
+
|
|
19926
|
+
function formatNumber$3(val) {
|
|
19927
|
+
return String(Math.round(Number(val) * 100) / 100);
|
|
19928
|
+
}
|
|
19929
|
+
|
|
19930
|
+
function sameColor(a, b) {
|
|
19931
|
+
return normalizeColor(a) == normalizeColor(b);
|
|
19932
|
+
}
|
|
19933
|
+
|
|
19934
|
+
function normalizeColor(color) {
|
|
19935
|
+
var str = String(color || '').trim().toLowerCase();
|
|
19936
|
+
return str == 'transparent' ? 'none' : str;
|
|
19937
|
+
}
|
|
19938
|
+
|
|
19939
|
+
function isBlank$1(val) {
|
|
19940
|
+
return val === undefined || val === null || String(val).trim() === '';
|
|
19941
|
+
}
|
|
19942
|
+
|
|
19943
|
+
// The polygon panel's "Pattern" section. See gui-fill-pattern.mjs for how its
|
|
19944
|
+
// settings map to fill-pattern codes.
|
|
19945
|
+
//
|
|
19946
|
+
// Switched like the label panel's Halo and Icon sections: a pattern is a
|
|
19947
|
+
// yes/no that the rest of the section then qualifies, and the section shows
|
|
19948
|
+
// only its heading while it is off.
|
|
19949
|
+
//
|
|
19950
|
+
// opts.getRecords() the target layer's records
|
|
19951
|
+
// opts.getTargetIds() the features being styled
|
|
19952
|
+
// opts.applyEdits(edits, title) run per-feature edits: [{id, styles}]
|
|
19953
|
+
// opts.revert() put the panel back as the data has it
|
|
19954
|
+
// opts.releaseFocus()
|
|
19955
|
+
function PatternFillControl(parent, opts) {
|
|
19956
|
+
var section = makePanelSection(parent, 'Pattern');
|
|
19957
|
+
var shown = {type: 'none'};
|
|
19958
|
+
// What switching the pattern on applies when the features have none: the
|
|
19959
|
+
// last pattern the section showed, so that off and on again is a round trip.
|
|
19960
|
+
var lastPattern = null;
|
|
19961
|
+
// Custom was chosen from the menu, and the code field is waiting for a code.
|
|
19962
|
+
// Until one is applied the data still says what it said before, so the
|
|
19963
|
+
// choice has to be remembered or the next refresh would undo it.
|
|
19964
|
+
var customPending = false;
|
|
19965
|
+
var toggle, typeSelect, mixedOption, colorControl, angleField, sizeRow, sizeCaption,
|
|
19966
|
+
sizeField, gapField, customRow, codeInput;
|
|
19967
|
+
|
|
19968
|
+
initRows();
|
|
19969
|
+
|
|
19970
|
+
this.section = section;
|
|
19971
|
+
|
|
19972
|
+
this.update = update;
|
|
19973
|
+
|
|
19974
|
+
this.reset = function() {
|
|
19975
|
+
customPending = false;
|
|
19976
|
+
};
|
|
19977
|
+
|
|
19978
|
+
this.hidePicker = function() {
|
|
19979
|
+
colorControl.picker.hide();
|
|
19980
|
+
};
|
|
19981
|
+
|
|
19982
|
+
// What a saved style records for the pattern, over the fill it records.
|
|
19983
|
+
this.getCode = function(fill) {
|
|
19984
|
+
if (isSimple(shown)) return formatFillPattern(shown, getPatternBackground(fill));
|
|
19985
|
+
if (shown.type == 'custom') return shown.code || '';
|
|
19986
|
+
return '';
|
|
19987
|
+
};
|
|
19988
|
+
|
|
19989
|
+
// Per-feature edits applying @styles to the targets. A new fill is also
|
|
19990
|
+
// written into the background of each feature's pattern, since that is where
|
|
19991
|
+
// the pattern's background colour is kept; custom patterns are left alone.
|
|
19992
|
+
this.getStyleEdits = function(styles) {
|
|
19993
|
+
var records = opts.getRecords();
|
|
19994
|
+
var fill = findStyle(styles, 'fill');
|
|
19995
|
+
var refill = fill !== undefined && findStyle(styles, 'fill-pattern') === undefined;
|
|
19996
|
+
return opts.getTargetIds().map(function(id) {
|
|
19997
|
+
var rec = records[id];
|
|
19998
|
+
var code = rec && rec['fill-pattern'];
|
|
19999
|
+
var edit = {id: id, styles: styles};
|
|
20000
|
+
var refilled;
|
|
20001
|
+
if (refill && !isBlank(code)) {
|
|
20002
|
+
refilled = refillPattern(internal.parsePattern(code), rec.fill, fill);
|
|
20003
|
+
if (refilled) edit.styles = styles.concat([['fill-pattern', refilled]]);
|
|
20004
|
+
}
|
|
20005
|
+
return edit;
|
|
20006
|
+
});
|
|
20007
|
+
};
|
|
20008
|
+
|
|
20009
|
+
// Edits that keep each feature's pattern over its fill after a command that
|
|
20010
|
+
// sets fills the panel cannot know beforehand. A -style expression reads
|
|
20011
|
+
// the new fill when it runs.
|
|
20012
|
+
this.getRefillExpressionEdits = function(ids) {
|
|
20013
|
+
var records = opts.getRecords();
|
|
20014
|
+
var edits = [];
|
|
20015
|
+
ids.forEach(function(id) {
|
|
20016
|
+
var rec = records[id];
|
|
20017
|
+
var code = rec && rec['fill-pattern'];
|
|
20018
|
+
var o = isBlank(code) ? null : getPatternControls(internal.parsePattern(code), rec.fill);
|
|
20019
|
+
if (o) edits.push({id: id, styles: [['fill-pattern', formatFillPatternExpression(o)]]});
|
|
20020
|
+
});
|
|
20021
|
+
return edits;
|
|
20022
|
+
};
|
|
20023
|
+
|
|
20024
|
+
function initRows() {
|
|
20025
|
+
toggle = makePanelToggle(section.findChild('.label-style-section-title'), {
|
|
20026
|
+
title: 'Fill with a pattern',
|
|
20027
|
+
className: 'layer-pattern-toggle',
|
|
20028
|
+
onChange: setPatternOn
|
|
20029
|
+
});
|
|
20030
|
+
var typeRow = El('div').addClass('label-style-row layer-pattern-type-row').appendTo(section);
|
|
20031
|
+
typeSelect = El('select').attr('title', 'Pattern type').appendTo(typeRow)
|
|
20032
|
+
.on('change', function() {
|
|
20033
|
+
selectType(typeSelect.node().value);
|
|
20034
|
+
});
|
|
20035
|
+
[['hatches', 'Hatches'], ['dots', 'Dots'], ['squares', 'Squares'],
|
|
20036
|
+
['custom', 'Custom']].forEach(function(o) {
|
|
20037
|
+
El('option').attr('value', o[0]).appendTo(typeSelect).text(o[1]);
|
|
20038
|
+
});
|
|
20039
|
+
// Shown only while the features being styled have different patterns,
|
|
20040
|
+
// which is not a choice the menu offers.
|
|
20041
|
+
mixedOption = El('option').attr('value', 'mixed').appendTo(typeSelect).text('Mixed');
|
|
20042
|
+
mixedOption.node().disabled = true;
|
|
20043
|
+
|
|
20044
|
+
colorControl = makeColorRow(section, {
|
|
20045
|
+
label: 'Color',
|
|
20046
|
+
noOpacity: true,
|
|
20047
|
+
onColor: function(color) {
|
|
20048
|
+
if (color) applyChange({color: color});
|
|
20049
|
+
},
|
|
20050
|
+
revert: opts.revert
|
|
20051
|
+
});
|
|
20052
|
+
colorControl.row.addClass('layer-pattern-color-row');
|
|
20053
|
+
El('span').appendTo(colorControl.aside).text('Angle');
|
|
20054
|
+
angleField = new SizeField(colorControl.aside, {
|
|
20055
|
+
min: -180, max: 180, step: 15, bigStep: 45,
|
|
20056
|
+
title: 'Pattern angle in degrees',
|
|
20057
|
+
onSet: function(value) {
|
|
20058
|
+
applyChange({angle: value});
|
|
20059
|
+
},
|
|
20060
|
+
onStep: function(delta) {
|
|
20061
|
+
applyChange({angle: Math.max(-180, Math.min(180, shown.angle + delta))});
|
|
20062
|
+
},
|
|
20063
|
+
onDone: opts.releaseFocus
|
|
20064
|
+
});
|
|
20065
|
+
|
|
20066
|
+
sizeRow = El('div').addClass('label-style-row layer-pattern-size-row').appendTo(section);
|
|
20067
|
+
var sizeCell = El('div').addClass('label-split-cell').appendTo(sizeRow);
|
|
20068
|
+
var gapCell = El('div').addClass('label-split-cell').appendTo(sizeRow);
|
|
20069
|
+
sizeCaption = El('span').appendTo(sizeCell).text('Size');
|
|
20070
|
+
sizeField = new SizeField(sizeCell, {
|
|
20071
|
+
min: 0.25, step: 0.5, bigStep: 2, decimals: 2,
|
|
20072
|
+
title: 'Size in px',
|
|
20073
|
+
onSet: function(value) {
|
|
20074
|
+
applyChange({size: value});
|
|
20075
|
+
},
|
|
20076
|
+
onStep: function(delta) {
|
|
20077
|
+
applyChange({size: Math.max(0.25, shown.size + delta)});
|
|
20078
|
+
},
|
|
20079
|
+
onDone: opts.releaseFocus
|
|
20080
|
+
});
|
|
20081
|
+
El('span').appendTo(gapCell).text('Gap');
|
|
20082
|
+
gapField = new SizeField(gapCell, {
|
|
20083
|
+
min: 0, step: 0.5, bigStep: 2, decimals: 2,
|
|
20084
|
+
title: 'Gap in px',
|
|
20085
|
+
onSet: function(value) {
|
|
20086
|
+
applyChange({gap: value});
|
|
20087
|
+
},
|
|
20088
|
+
onStep: function(delta) {
|
|
20089
|
+
applyChange({gap: Math.max(0, shown.gap + delta)});
|
|
20090
|
+
},
|
|
20091
|
+
onDone: opts.releaseFocus
|
|
20092
|
+
});
|
|
20093
|
+
|
|
20094
|
+
customRow = El('div').addClass('label-style-row layer-pattern-code-row').appendTo(section);
|
|
20095
|
+
var caption = El('div').addClass('label-style-row-label').appendTo(customRow).text('Code');
|
|
20096
|
+
makeFieldTip(caption,
|
|
20097
|
+
'The fill-pattern syntax of the -style command.\n' +
|
|
20098
|
+
'See the command reference for details.');
|
|
20099
|
+
codeInput = El('input').attr('type', 'text').attr('title', 'Pattern code').appendTo(customRow)
|
|
20100
|
+
.on('change', function() {
|
|
20101
|
+
applyCode(codeInput.node().value.trim());
|
|
20102
|
+
});
|
|
20103
|
+
}
|
|
20104
|
+
|
|
20105
|
+
function update() {
|
|
20106
|
+
var ids = opts.getTargetIds();
|
|
20107
|
+
var toggleState = getToggleState(ids);
|
|
20108
|
+
var state = getCommonPatternState(ids);
|
|
20109
|
+
if (customPending && state.type != 'custom') {
|
|
20110
|
+
state = {type: 'custom', code: shown.type == 'custom' ? shown.code : getPendingCode(state)};
|
|
20111
|
+
}
|
|
20112
|
+
shown = state;
|
|
20113
|
+
if (isSimple(state) || state.type == 'custom' && state.code) lastPattern = state;
|
|
20114
|
+
toggle.setState(toggleState);
|
|
20115
|
+
section.classed('collapsed', toggleState == 'off');
|
|
20116
|
+
mixedOption.classed('hidden', state.type != 'mixed');
|
|
20117
|
+
typeSelect.node().value = state.type;
|
|
20118
|
+
colorControl.row.classed('hidden', !isSimple(state));
|
|
20119
|
+
sizeRow.classed('hidden', !isSimple(state));
|
|
20120
|
+
customRow.classed('hidden', state.type != 'custom');
|
|
20121
|
+
if (isSimple(state)) {
|
|
20122
|
+
colorControl.showColor(state.color);
|
|
20123
|
+
angleField.setValue(state.angle);
|
|
20124
|
+
sizeCaption.text(state.type == 'hatches' ? 'Width' : 'Size');
|
|
20125
|
+
sizeField.setValue(state.size);
|
|
20126
|
+
gapField.setValue(state.gap);
|
|
20127
|
+
} else {
|
|
20128
|
+
colorControl.picker.hide();
|
|
20129
|
+
}
|
|
20130
|
+
if (state.type == 'custom' && document.activeElement !== codeInput.node()) {
|
|
20131
|
+
codeInput.node().value = state.code || '';
|
|
20132
|
+
}
|
|
20133
|
+
}
|
|
20134
|
+
|
|
20135
|
+
function selectType(type) {
|
|
20136
|
+
if (type == 'custom') {
|
|
20137
|
+
customPending = true;
|
|
20138
|
+
update();
|
|
20139
|
+
codeInput.node().focus();
|
|
20140
|
+
return;
|
|
20141
|
+
}
|
|
20142
|
+
customPending = false;
|
|
20143
|
+
if (patternTypes.indexOf(type) > -1) {
|
|
20144
|
+
applyControls(getDefaultPatternControls(type, isSimple(shown) ? shown.color : null));
|
|
20145
|
+
}
|
|
20146
|
+
}
|
|
20147
|
+
|
|
20148
|
+
// On gives every feature being styled a pattern: the one the features that
|
|
20149
|
+
// have a pattern agree on, if they do, or else the last one shown, or else a
|
|
20150
|
+
// default hatch. Off takes the pattern off all of them.
|
|
20151
|
+
function setPatternOn(on) {
|
|
20152
|
+
var ids = opts.getTargetIds();
|
|
20153
|
+
var common, pattern;
|
|
20154
|
+
customPending = false;
|
|
20155
|
+
if (!on) {
|
|
20156
|
+
applyToTargets(function() { return ''; }, 'Remove pattern fill');
|
|
20157
|
+
return;
|
|
20158
|
+
}
|
|
20159
|
+
common = getCommonPatternState(ids.filter(hasPattern));
|
|
20160
|
+
pattern = isSimple(common) || common.type == 'custom' ? common :
|
|
20161
|
+
lastPattern || getDefaultPatternControls('hatches');
|
|
20162
|
+
if (pattern.type == 'custom') {
|
|
20163
|
+
applyToTargets(function() { return pattern.code; }, 'Pattern fill');
|
|
20164
|
+
} else {
|
|
20165
|
+
applyControls(pattern);
|
|
20166
|
+
}
|
|
20167
|
+
}
|
|
20168
|
+
|
|
20169
|
+
function getToggleState(ids) {
|
|
20170
|
+
var n = ids.filter(hasPattern).length;
|
|
20171
|
+
return n === 0 ? 'off' : n < ids.length ? 'mixed' : 'on';
|
|
20172
|
+
}
|
|
20173
|
+
|
|
20174
|
+
function hasPattern(id) {
|
|
20175
|
+
var rec = opts.getRecords()[id];
|
|
20176
|
+
return !isBlank(rec && rec['fill-pattern']);
|
|
20177
|
+
}
|
|
20178
|
+
|
|
20179
|
+
function applyChange(change) {
|
|
20180
|
+
if (!isSimple(shown)) return;
|
|
20181
|
+
var o = Object.assign({}, shown, change);
|
|
20182
|
+
if (!isValidPatternControls(o)) {
|
|
20183
|
+
opts.revert();
|
|
20184
|
+
return;
|
|
20185
|
+
}
|
|
20186
|
+
applyControls(o);
|
|
20187
|
+
}
|
|
20188
|
+
|
|
20189
|
+
function applyControls(o) {
|
|
20190
|
+
applyToTargets(function(rec) {
|
|
20191
|
+
return formatFillPattern(o, getPatternBackground(rec && rec.fill));
|
|
20192
|
+
}, 'Pattern fill');
|
|
20193
|
+
}
|
|
20194
|
+
|
|
20195
|
+
function applyCode(code) {
|
|
20196
|
+
if (code && !internal.parsePattern(code)) {
|
|
20197
|
+
codeInput.node().value = shown.code || '';
|
|
20198
|
+
opts.revert();
|
|
20199
|
+
return;
|
|
20200
|
+
}
|
|
20201
|
+
if (!code) customPending = false;
|
|
20202
|
+
applyToTargets(function() { return code; }, code ? 'Pattern fill' : 'Remove pattern fill');
|
|
20203
|
+
}
|
|
20204
|
+
|
|
20205
|
+
function applyToTargets(getCode, title) {
|
|
20206
|
+
var records = opts.getRecords();
|
|
20207
|
+
opts.applyEdits(opts.getTargetIds().map(function(id) {
|
|
20208
|
+
return {id: id, styles: [['fill-pattern', getCode(records[id])]]};
|
|
20209
|
+
}), title);
|
|
20210
|
+
}
|
|
20211
|
+
|
|
20212
|
+
// The pattern the features being styled have in common, as panel settings,
|
|
20213
|
+
// {type: 'custom', code}, {type: 'none'} -- or {type: 'mixed'} if they differ.
|
|
20214
|
+
// Features with the same pattern over different fills agree: each pattern's
|
|
20215
|
+
// background is its own feature's fill.
|
|
20216
|
+
function getCommonPatternState(ids) {
|
|
20217
|
+
var records = opts.getRecords();
|
|
20218
|
+
var cache = {};
|
|
20219
|
+
var firstKey = null, first = null;
|
|
20220
|
+
var rec, cacheKey, state, key;
|
|
20221
|
+
for (var i=0; i<ids.length; i++) {
|
|
20222
|
+
rec = records[ids[i]];
|
|
20223
|
+
cacheKey = (rec && rec['fill-pattern']) + '\t' + (rec && rec.fill);
|
|
20224
|
+
state = cache[cacheKey] || (cache[cacheKey] = readPatternState(rec));
|
|
20225
|
+
key = JSON.stringify(state);
|
|
20226
|
+
if (firstKey === null) {
|
|
20227
|
+
firstKey = key;
|
|
20228
|
+
first = state;
|
|
20229
|
+
} else if (key != firstKey) {
|
|
20230
|
+
return {type: 'mixed'};
|
|
20231
|
+
}
|
|
20232
|
+
}
|
|
20233
|
+
return first || {type: 'none'};
|
|
20234
|
+
}
|
|
20235
|
+
|
|
20236
|
+
// The code the Custom field starts from: the pattern being replaced, so that
|
|
20237
|
+
// choosing Custom is a way to see the code for the settings shown.
|
|
20238
|
+
function getPendingCode(state) {
|
|
20239
|
+
var records = opts.getRecords();
|
|
20240
|
+
var ids = opts.getTargetIds();
|
|
20241
|
+
var rec = ids.length > 0 ? records[ids[0]] : null;
|
|
20242
|
+
return isSimple(state) ? formatFillPattern(state, getPatternBackground(rec && rec.fill)) : '';
|
|
20243
|
+
}
|
|
20244
|
+
}
|
|
20245
|
+
|
|
20246
|
+
function readPatternState(rec) {
|
|
20247
|
+
var code = rec && rec['fill-pattern'];
|
|
20248
|
+
if (isBlank(code)) return {type: 'none'};
|
|
20249
|
+
return getPatternControls(internal.parsePattern(code), rec.fill) ||
|
|
20250
|
+
{type: 'custom', code: String(code)};
|
|
20251
|
+
}
|
|
20252
|
+
|
|
20253
|
+
function isSimple(state) {
|
|
20254
|
+
return patternTypes.indexOf(state.type) > -1;
|
|
20255
|
+
}
|
|
20256
|
+
|
|
20257
|
+
function findStyle(styles, name) {
|
|
20258
|
+
for (var i=0; i<styles.length; i++) {
|
|
20259
|
+
if (styles[i][0] == name) return styles[i][1];
|
|
20260
|
+
}
|
|
20261
|
+
return undefined;
|
|
20262
|
+
}
|
|
20263
|
+
|
|
20264
|
+
function isBlank(val) {
|
|
20265
|
+
return val === undefined || val === null || String(val).trim() === '';
|
|
20266
|
+
}
|
|
20267
|
+
|
|
19557
20268
|
var savedStylesKey = 'layer_style_presets';
|
|
19558
|
-
var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'fill', 'fill-opacity'];
|
|
20269
|
+
var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray', 'fill', 'fill-opacity', 'fill-pattern'];
|
|
19559
20270
|
|
|
19560
20271
|
function LayerStyleTool(gui) {
|
|
19561
20272
|
var parent = gui.container.findChild('.mshp-main-map');
|
|
19562
20273
|
var panel = El('div').addClass('label-style-panel layer-style-panel rollover').appendTo(parent).hide();
|
|
19563
|
-
var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthField, randomFillBtn, presetControl, hit;
|
|
20274
|
+
var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthField, dashControl, randomFillBtn, presetControl, patternControl, hit;
|
|
19564
20275
|
var targetLayer = null;
|
|
19565
20276
|
|
|
19566
20277
|
initPanel();
|
|
@@ -19605,6 +20316,7 @@
|
|
|
19605
20316
|
|
|
19606
20317
|
function turnOn() {
|
|
19607
20318
|
targetLayer = getActiveLayer();
|
|
20319
|
+
patternControl.reset();
|
|
19608
20320
|
applyDefaultLineStyle();
|
|
19609
20321
|
panel.show();
|
|
19610
20322
|
updateControls();
|
|
@@ -19614,6 +20326,8 @@
|
|
|
19614
20326
|
panel.hide();
|
|
19615
20327
|
strokeControl.picker.hide();
|
|
19616
20328
|
fillControl.picker.hide();
|
|
20329
|
+
patternControl.hidePicker();
|
|
20330
|
+
patternControl.reset();
|
|
19617
20331
|
targetLayer = null;
|
|
19618
20332
|
}
|
|
19619
20333
|
|
|
@@ -19630,8 +20344,8 @@
|
|
|
19630
20344
|
// picker's Close button, and the panel's own buttons in the browsers that
|
|
19631
20345
|
// focus a button on click. Left alone while the user is typing into a
|
|
19632
20346
|
// field, which a click elsewhere in the panel ends on its own.
|
|
19633
|
-
panel.node().addEventListener('click', function() {
|
|
19634
|
-
if (isTextInput(document.activeElement)) return;
|
|
20347
|
+
panel.node().addEventListener('click', function(e) {
|
|
20348
|
+
if (isTextInput(document.activeElement) || opensAMenu(e.target)) return;
|
|
19635
20349
|
releaseFocus();
|
|
19636
20350
|
});
|
|
19637
20351
|
|
|
@@ -19646,11 +20360,22 @@
|
|
|
19646
20360
|
fillControl = addColorControl(panel, 'Fill', 'fill', '');
|
|
19647
20361
|
strokeControl = addColorControl(panel, 'Stroke', 'stroke', '#000000');
|
|
19648
20362
|
strokeWidthField = addStrokeWidthControl(strokeControl.aside);
|
|
20363
|
+
dashControl = addDashArrayControl(panel);
|
|
19649
20364
|
|
|
19650
20365
|
var buttonRow = El('div').addClass('label-style-row label-panel-button-row').appendTo(panel);
|
|
19651
20366
|
randomFillBtn = makePanelActionButton(buttonRow, 'Random fills', applyRandomFillColors);
|
|
19652
20367
|
makePanelActionButton(buttonRow, 'Clear style', clearLayerStyle);
|
|
19653
20368
|
|
|
20369
|
+
patternControl = new PatternFillControl(panel, {
|
|
20370
|
+
getRecords: function() {
|
|
20371
|
+
return targetLayer && targetLayer.data ? targetLayer.data.getRecords() : [];
|
|
20372
|
+
},
|
|
20373
|
+
getTargetIds: getTargetIds,
|
|
20374
|
+
applyEdits: runStyleEdits,
|
|
20375
|
+
revert: updateControls,
|
|
20376
|
+
releaseFocus: releaseFocus
|
|
20377
|
+
});
|
|
20378
|
+
|
|
19654
20379
|
presetControl = new StylePresetControl(panel, {
|
|
19655
20380
|
storageKey: savedStylesKey,
|
|
19656
20381
|
type: getStyleType,
|
|
@@ -19706,6 +20431,29 @@
|
|
|
19706
20431
|
});
|
|
19707
20432
|
}
|
|
19708
20433
|
|
|
20434
|
+
// A literal stroke-dasharray, in the wide column so that it lines up with
|
|
20435
|
+
// the stroke's colour above it. A blank field makes the line solid again.
|
|
20436
|
+
function addDashArrayControl(parent) {
|
|
20437
|
+
var row = El('div').addClass('label-style-row label-split-row layer-dash-row').appendTo(parent);
|
|
20438
|
+
var cell = El('div').addClass('label-split-cell').appendTo(row);
|
|
20439
|
+
El('div').addClass('label-split-cell').appendTo(row);
|
|
20440
|
+
var caption = El('div').addClass('label-style-row-label').appendTo(cell).text('Dashes');
|
|
20441
|
+
makeFieldTip(caption,
|
|
20442
|
+
'Dash and gap lengths in pixels, separated\n' +
|
|
20443
|
+
'by spaces. "4" gives 4px dashes and 4px gaps.\n' +
|
|
20444
|
+
'"6 3" gives 6px dashes and 3px gaps.');
|
|
20445
|
+
var input = El('input').attr('type', 'text').appendTo(cell)
|
|
20446
|
+
.on('change', function() {
|
|
20447
|
+
var value = normalizeDashArrayInput(input.node().value);
|
|
20448
|
+
if (value && internal.parseStyleLiteral('stroke-dasharray', value) === undefined) {
|
|
20449
|
+
updateControls();
|
|
20450
|
+
return;
|
|
20451
|
+
}
|
|
20452
|
+
applyDashArrayStyle(value);
|
|
20453
|
+
});
|
|
20454
|
+
return {row: row, input: input};
|
|
20455
|
+
}
|
|
20456
|
+
|
|
19709
20457
|
function releaseFocus() {
|
|
19710
20458
|
releasePanelFocus(panel.node());
|
|
19711
20459
|
}
|
|
@@ -19719,10 +20467,14 @@
|
|
|
19719
20467
|
updateEditingStatus(manualIds.length);
|
|
19720
20468
|
strokeControl.row.show();
|
|
19721
20469
|
fillControl.row.classed('hidden', geom != 'polygon');
|
|
20470
|
+
dashControl.row.classed('hidden', geom != 'polyline');
|
|
19722
20471
|
updateColorControl(strokeControl);
|
|
19723
20472
|
updateColorControl(fillControl);
|
|
19724
20473
|
updateStrokeWidthControl();
|
|
20474
|
+
updateDashArrayControl();
|
|
19725
20475
|
randomFillBtn.classed('hidden', geom != 'polygon');
|
|
20476
|
+
patternControl.section.classed('hidden', geom != 'polygon');
|
|
20477
|
+
if (geom == 'polygon') patternControl.update();
|
|
19726
20478
|
presetControl.render();
|
|
19727
20479
|
updateSavedStyleControls();
|
|
19728
20480
|
}
|
|
@@ -19735,10 +20487,13 @@
|
|
|
19735
20487
|
if (!isHexColor(value)) control.picker.hide();
|
|
19736
20488
|
}
|
|
19737
20489
|
|
|
20490
|
+
// A selection whose colours disagree still has colours, so an opacity unset
|
|
20491
|
+
// on all of it shows as full rather than blank.
|
|
19738
20492
|
function updateOpacityControl(control) {
|
|
19739
|
-
var
|
|
19740
|
-
control.opacity.node().value =
|
|
19741
|
-
|
|
20493
|
+
var field = control.field + '-opacity';
|
|
20494
|
+
control.opacity.node().value = styleFieldIsUnsetForTargets(field) ?
|
|
20495
|
+
formatColorOpacityPct(null, !styleFieldIsUnsetForTargets(control.field)) :
|
|
20496
|
+
formatOpacityPct(getCommonStyleValue(field));
|
|
19742
20497
|
}
|
|
19743
20498
|
|
|
19744
20499
|
function updateStrokeWidthControl() {
|
|
@@ -19748,6 +20503,11 @@
|
|
|
19748
20503
|
getDefaultStrokeWidth() : value));
|
|
19749
20504
|
}
|
|
19750
20505
|
|
|
20506
|
+
function updateDashArrayControl() {
|
|
20507
|
+
var value = getCommonStyleValue('stroke-dasharray');
|
|
20508
|
+
dashControl.input.node().value = value === undefined || value === null ? '' : String(value);
|
|
20509
|
+
}
|
|
20510
|
+
|
|
19751
20511
|
function nudgeStrokeWidth(direction) {
|
|
19752
20512
|
var value = Number(getCommonStyleValue('stroke-width'));
|
|
19753
20513
|
if (!isFinite(value)) value = getDefaultStrokeWidth();
|
|
@@ -19778,8 +20538,12 @@
|
|
|
19778
20538
|
if (control.field == 'stroke' && strokeWidthIsUnsetForTargets()) {
|
|
19779
20539
|
styles.push(['stroke-width', 1]);
|
|
19780
20540
|
}
|
|
19781
|
-
|
|
19782
|
-
|
|
20541
|
+
if (control.field == 'fill') {
|
|
20542
|
+
runStyleEdits(patternControl.getStyleEdits(styles));
|
|
20543
|
+
} else {
|
|
20544
|
+
runStyleCommand(styles);
|
|
20545
|
+
}
|
|
20546
|
+
}
|
|
19783
20547
|
|
|
19784
20548
|
function applyStrokeWidthStyle(value) {
|
|
19785
20549
|
var styles = [['stroke-width', value]];
|
|
@@ -19789,12 +20553,39 @@
|
|
|
19789
20553
|
runStyleCommand(styles);
|
|
19790
20554
|
}
|
|
19791
20555
|
|
|
20556
|
+
function applyDashArrayStyle(value) {
|
|
20557
|
+
var styles = [['stroke-dasharray', value]];
|
|
20558
|
+
if (value && styleFieldIsUnsetForTargets('stroke')) {
|
|
20559
|
+
styles.push(['stroke', strokeControl.defaultColor]);
|
|
20560
|
+
}
|
|
20561
|
+
runStyleCommand(styles);
|
|
20562
|
+
}
|
|
20563
|
+
|
|
19792
20564
|
function runStyleCommand(styles) {
|
|
19793
|
-
var parts = ['-style'];
|
|
19794
20565
|
releaseFocus();
|
|
19795
20566
|
syncTargetLayer();
|
|
19796
20567
|
var ids = getTargetIds();
|
|
19797
20568
|
if (!gui.console || !targetLayer || ids.length === 0) return;
|
|
20569
|
+
runCommand(formatStyleCommand(styles, ids), 'Style layer');
|
|
20570
|
+
}
|
|
20571
|
+
|
|
20572
|
+
// Per-feature edits ([{id, styles}]), as one -style command for each set of
|
|
20573
|
+
// styles. They are run as one command string, which is one undo step.
|
|
20574
|
+
function runStyleEdits(edits, title) {
|
|
20575
|
+
releaseFocus();
|
|
20576
|
+
syncTargetLayer();
|
|
20577
|
+
if (!gui.console || !targetLayer || edits.length === 0) return;
|
|
20578
|
+
runCommand(formatStyleEditCommands(edits), title || 'Style layer');
|
|
20579
|
+
}
|
|
20580
|
+
|
|
20581
|
+
function formatStyleEditCommands(edits) {
|
|
20582
|
+
return groupStyleEdits(edits).map(function(group) {
|
|
20583
|
+
return formatStyleCommand(group.styles, group.ids);
|
|
20584
|
+
}).join(' ');
|
|
20585
|
+
}
|
|
20586
|
+
|
|
20587
|
+
function formatStyleCommand(styles, ids) {
|
|
20588
|
+
var parts = ['-style'];
|
|
19798
20589
|
styles.forEach(function(style) {
|
|
19799
20590
|
parts.push(style[0] + '=' + quoteCommandValue(style[1]));
|
|
19800
20591
|
});
|
|
@@ -19802,7 +20593,7 @@
|
|
|
19802
20593
|
if (ids.length < internal.getFeatureCount(targetLayer)) {
|
|
19803
20594
|
parts.push('ids=' + ids.join(','));
|
|
19804
20595
|
}
|
|
19805
|
-
|
|
20596
|
+
return parts.join(' ');
|
|
19806
20597
|
}
|
|
19807
20598
|
|
|
19808
20599
|
function applyDefaultLineStyle() {
|
|
@@ -19816,13 +20607,21 @@
|
|
|
19816
20607
|
}
|
|
19817
20608
|
}
|
|
19818
20609
|
|
|
20610
|
+
// Patterns keep their backgrounds in step with the fills, and the new fills
|
|
20611
|
+
// are not known until -classify has chosen them, so the patterns are
|
|
20612
|
+
// rewritten by an expression that reads each feature's fill when it runs.
|
|
19819
20613
|
function applyRandomFillColors() {
|
|
19820
20614
|
var cmd = '-classify colors=random non-adjacent';
|
|
20615
|
+
var patternEdits;
|
|
19821
20616
|
syncTargetLayer();
|
|
19822
20617
|
if (!gui.console || !targetLayer || targetLayer.geometry_type != 'polygon') return;
|
|
19823
20618
|
if (getActiveLayer() != targetLayer) {
|
|
19824
20619
|
cmd += ' target=' + internal.formatOptionValue(internal.getLayerTargetId(gui.model, targetLayer));
|
|
19825
20620
|
}
|
|
20621
|
+
patternEdits = patternControl.getRefillExpressionEdits(getAllFeatureIds(targetLayer));
|
|
20622
|
+
if (patternEdits.length > 0) {
|
|
20623
|
+
cmd += ' ' + formatStyleEditCommands(patternEdits);
|
|
20624
|
+
}
|
|
19826
20625
|
runCommand(cmd, 'Random fill colors');
|
|
19827
20626
|
}
|
|
19828
20627
|
|
|
@@ -19841,7 +20640,7 @@
|
|
|
19841
20640
|
}
|
|
19842
20641
|
});
|
|
19843
20642
|
if (styles.length > 0) {
|
|
19844
|
-
|
|
20643
|
+
runStyleEdits(patternControl.getStyleEdits(styles));
|
|
19845
20644
|
}
|
|
19846
20645
|
}
|
|
19847
20646
|
|
|
@@ -19850,9 +20649,13 @@
|
|
|
19850
20649
|
addStyleValue(style, 'stroke', getControlValue(strokeControl.input));
|
|
19851
20650
|
addStyleValue(style, 'stroke-width', strokeWidthField.getValue());
|
|
19852
20651
|
addStyleValue(style, 'stroke-opacity', parseOpacityValue(strokeControl.opacity.node().value));
|
|
20652
|
+
if (targetLayer && targetLayer.geometry_type == 'polyline') {
|
|
20653
|
+
addStyleValue(style, 'stroke-dasharray', getControlValue(dashControl.input));
|
|
20654
|
+
}
|
|
19853
20655
|
if (targetLayer && targetLayer.geometry_type == 'polygon') {
|
|
19854
20656
|
addStyleValue(style, 'fill', getControlValue(fillControl.input));
|
|
19855
20657
|
addStyleValue(style, 'fill-opacity', parseOpacityValue(fillControl.opacity.node().value));
|
|
20658
|
+
addStyleValue(style, 'fill-pattern', patternControl.getCode(getControlValue(fillControl.input)));
|
|
19856
20659
|
}
|
|
19857
20660
|
return style;
|
|
19858
20661
|
}
|
|
@@ -20067,8 +20870,8 @@
|
|
|
20067
20870
|
revert: updateControls,
|
|
20068
20871
|
release: releaseFocus
|
|
20069
20872
|
});
|
|
20070
|
-
panel.node().addEventListener('click', function() {
|
|
20071
|
-
if (isTextInput(document.activeElement)) return;
|
|
20873
|
+
panel.node().addEventListener('click', function(e) {
|
|
20874
|
+
if (isTextInput(document.activeElement) || opensAMenu(e.target)) return;
|
|
20072
20875
|
releaseFocus();
|
|
20073
20876
|
});
|
|
20074
20877
|
|
|
@@ -20260,27 +21063,42 @@
|
|
|
20260
21063
|
var radius = getCommonValue('r');
|
|
20261
21064
|
var fill = getCommonValue('fill');
|
|
20262
21065
|
var stroke = getCommonValue('stroke');
|
|
20263
|
-
var fillOpacity = getCommonValue('fill-opacity');
|
|
20264
|
-
var strokeOpacity = getCommonValue('stroke-opacity');
|
|
20265
21066
|
var strokeWidth = getCommonValue('stroke-width');
|
|
20266
21067
|
setCircleRadius(radius);
|
|
20267
21068
|
setCircleColor(circleFillControl, fill);
|
|
20268
21069
|
setCircleColor(circleStrokeControl, stroke);
|
|
20269
|
-
|
|
20270
|
-
|
|
21070
|
+
updateCircleOpacity(circleFillControl);
|
|
21071
|
+
updateCircleOpacity(circleStrokeControl);
|
|
20271
21072
|
setCircleStrokeWidth(strokeWidth === '' ? 0 : strokeWidth);
|
|
20272
21073
|
if (representation != 'circle' && representation != 'unstyled') {
|
|
20273
21074
|
setCircleRadius(defaultCircleRadius);
|
|
20274
21075
|
setCircleColor(circleFillControl, '');
|
|
20275
21076
|
setCircleColor(circleStrokeControl, '');
|
|
20276
|
-
|
|
20277
|
-
|
|
21077
|
+
circleFillControl.opacity.node().value = '';
|
|
21078
|
+
circleStrokeControl.opacity.node().value = '';
|
|
20278
21079
|
setCircleStrokeWidth(0);
|
|
20279
21080
|
}
|
|
20280
21081
|
}
|
|
20281
21082
|
|
|
20282
|
-
|
|
20283
|
-
|
|
21083
|
+
// The opacity to put in the style command: undefined to leave the property
|
|
21084
|
+
// as it is, null for a field holding something that is not an opacity.
|
|
21085
|
+
// A blank field has nothing to say, and 100% over an unset opacity is the
|
|
21086
|
+
// default being shown rather than a value to store.
|
|
21087
|
+
function getCircleOpacityToWrite(control) {
|
|
21088
|
+
var str = control.opacity.node().value.trim();
|
|
21089
|
+
var val = parseOpacityValue(str);
|
|
21090
|
+
if (str === '') return undefined;
|
|
21091
|
+
if (val === 1 && styleFieldIsUnset(control.field + '-opacity')) return undefined;
|
|
21092
|
+
return val;
|
|
21093
|
+
}
|
|
21094
|
+
|
|
21095
|
+
// A selection whose colours disagree still has colours, so an opacity unset
|
|
21096
|
+
// on all of it shows as full rather than blank.
|
|
21097
|
+
function updateCircleOpacity(control) {
|
|
21098
|
+
var field = control.field + '-opacity';
|
|
21099
|
+
control.opacity.node().value = styleFieldIsUnset(field) ?
|
|
21100
|
+
formatColorOpacityPct(null, !styleFieldIsUnset(control.field)) :
|
|
21101
|
+
formatOpacityPct(getCommonValue(field));
|
|
20284
21102
|
}
|
|
20285
21103
|
|
|
20286
21104
|
function renderCreateFields() {
|
|
@@ -20307,9 +21125,8 @@
|
|
|
20307
21125
|
var expr = createExprInput.node().value.trim();
|
|
20308
21126
|
var cmd;
|
|
20309
21127
|
if (!expr || !gui.console) return;
|
|
20310
|
-
cmd =
|
|
20311
|
-
|
|
20312
|
-
'-style label-text=' + quoteCommandValue(expr);
|
|
21128
|
+
cmd = '-labels text=' + quoteCommandValue(expr);
|
|
21129
|
+
if (createCopyCheckbox.node().checked) cmd += ' + name=labels';
|
|
20313
21130
|
runGuiEditCommand(gui, cmd, {
|
|
20314
21131
|
title: 'Create labels',
|
|
20315
21132
|
onSuccess: openLabelStyles
|
|
@@ -20331,16 +21148,16 @@
|
|
|
20331
21148
|
function createSimpleCircles() {
|
|
20332
21149
|
runCommand('-style r=' + defaultCreatedCircleRadius +
|
|
20333
21150
|
' fill=' + quoteCommandValue(defaultCircleFill) +
|
|
20334
|
-
'
|
|
21151
|
+
' stroke-width=0', 'Create circles');
|
|
20335
21152
|
}
|
|
20336
21153
|
|
|
20337
21154
|
function applyCircleStyles() {
|
|
20338
21155
|
var representation = getPointRepresentation();
|
|
20339
21156
|
var radius = getCircleRadius();
|
|
20340
21157
|
var fill = circleFillControl.input.node().value.trim();
|
|
20341
|
-
var fillOpacity =
|
|
21158
|
+
var fillOpacity = getCircleOpacityToWrite(circleFillControl);
|
|
20342
21159
|
var stroke = circleStrokeControl.input.node().value.trim();
|
|
20343
|
-
var strokeOpacity =
|
|
21160
|
+
var strokeOpacity = getCircleOpacityToWrite(circleStrokeControl);
|
|
20344
21161
|
var strokeWidth = getCircleStrokeWidth();
|
|
20345
21162
|
var args;
|
|
20346
21163
|
if (!gui.console || !(representation == 'unstyled' || representation == 'circle') ||
|
|
@@ -20349,11 +21166,9 @@
|
|
|
20349
21166
|
fill = defaultCircleFill;
|
|
20350
21167
|
setCircleColor(circleFillControl, fill);
|
|
20351
21168
|
}
|
|
20352
|
-
args = [
|
|
20353
|
-
|
|
20354
|
-
|
|
20355
|
-
'stroke-width=' + strokeWidth
|
|
20356
|
-
];
|
|
21169
|
+
args = ['stroke-width=' + strokeWidth];
|
|
21170
|
+
if (fillOpacity !== undefined) args.push('fill-opacity=' + fillOpacity);
|
|
21171
|
+
if (strokeOpacity !== undefined) args.push('stroke-opacity=' + strokeOpacity);
|
|
20357
21172
|
if (radius !== null) args.unshift('r=' + radius);
|
|
20358
21173
|
if (fill) args.push('fill=' + quoteCommandValue(fill));
|
|
20359
21174
|
if (stroke) args.push('stroke=' + quoteCommandValue(stroke));
|
|
@@ -20580,7 +21395,7 @@
|
|
|
20580
21395
|
var setDefaultTargets = self.setDefaultTargets;
|
|
20581
21396
|
utils$1.extend(self, EventDispatcher.prototype);
|
|
20582
21397
|
|
|
20583
|
-
// A map frame is composition state, not editable content. Commands may
|
|
21398
|
+
// A map frame (and its furniture) is composition state, not editable content. Commands may
|
|
20584
21399
|
// explicitly target it, but it must not replace the GUI's active content
|
|
20585
21400
|
// layer or become the implicit target of the next console command.
|
|
20586
21401
|
self.setDefaultTargets = function(targets, opts) {
|
|
@@ -20647,15 +21462,25 @@
|
|
|
20647
21462
|
};
|
|
20648
21463
|
|
|
20649
21464
|
self.selectNextLayer = function() {
|
|
20650
|
-
var next = self.findNextLayer
|
|
21465
|
+
var next = findContentLayer(self.findNextLayer);
|
|
20651
21466
|
if (next) self.selectLayer(next.layer, next.dataset);
|
|
20652
21467
|
};
|
|
20653
21468
|
|
|
20654
21469
|
self.selectPrevLayer = function() {
|
|
20655
|
-
var prev = self.findPrevLayer
|
|
21470
|
+
var prev = findContentLayer(self.findPrevLayer);
|
|
20656
21471
|
if (prev) self.selectLayer(prev.layer, prev.dataset);
|
|
20657
21472
|
};
|
|
20658
21473
|
|
|
21474
|
+
// step past the frame and its furniture, which can't be selected
|
|
21475
|
+
function findContentLayer(step) {
|
|
21476
|
+
var start = self.getActiveLayer().layer;
|
|
21477
|
+
var o = step.call(self, start);
|
|
21478
|
+
while (o && o.layer != start && internal.isFrameComponentLayer(o.layer, o.dataset)) {
|
|
21479
|
+
o = step.call(self, o.layer);
|
|
21480
|
+
}
|
|
21481
|
+
return o;
|
|
21482
|
+
}
|
|
21483
|
+
|
|
20659
21484
|
return self;
|
|
20660
21485
|
|
|
20661
21486
|
function getContentTargets(targets) {
|
|
@@ -20663,7 +21488,7 @@
|
|
|
20663
21488
|
return {
|
|
20664
21489
|
dataset: target.dataset,
|
|
20665
21490
|
layers: target.layers.filter(function(lyr) {
|
|
20666
|
-
return !internal.
|
|
21491
|
+
return !internal.isFrameComponentLayer(lyr, target.dataset);
|
|
20667
21492
|
})
|
|
20668
21493
|
};
|
|
20669
21494
|
}).filter(function(target) {
|
|
@@ -20673,7 +21498,7 @@
|
|
|
20673
21498
|
|
|
20674
21499
|
function getFirstContentTarget() {
|
|
20675
21500
|
var target = self.getLayers().find(function(o) {
|
|
20676
|
-
return !internal.
|
|
21501
|
+
return !internal.isFrameComponentLayer(o.layer, o.dataset);
|
|
20677
21502
|
});
|
|
20678
21503
|
return target ? [{dataset: target.dataset, layers: [target.layer]}] : [];
|
|
20679
21504
|
}
|
|
@@ -21286,7 +22111,7 @@
|
|
|
21286
22111
|
// hit test, the reposition pass and the selection cue all find labels by those,
|
|
21287
22112
|
// and a node with no feature behind it must not be found by any of them.
|
|
21288
22113
|
//
|
|
21289
|
-
// @rec: the label's record, as -
|
|
22114
|
+
// @rec: the label's record, as -labels coordinates= will write it
|
|
21290
22115
|
// @shp: its knots, in the layer's display coordinates
|
|
21291
22116
|
// Returns a markup string, or '' if there is nothing to draw.
|
|
21292
22117
|
function renderPendingSymbol(rec, shp, ext, idPrefix) {
|
|
@@ -23056,15 +23881,22 @@
|
|
|
23056
23881
|
this.zoomToBbox = zoomToBbox;
|
|
23057
23882
|
|
|
23058
23883
|
if (gui.options.homeControl) {
|
|
23059
|
-
gui.buttons.addButton("#home-icon")
|
|
23060
|
-
|
|
23061
|
-
|
|
23062
|
-
|
|
23884
|
+
gui.buttons.addButton("#home-icon")
|
|
23885
|
+
.attr('data-tooltip', 'Zoom to fit')
|
|
23886
|
+
.attr('aria-label', 'Zoom to fit')
|
|
23887
|
+
.on('click', function() {
|
|
23888
|
+
if (disabled()) return;
|
|
23889
|
+
gui.dispatchEvent('map_reset');
|
|
23890
|
+
});
|
|
23063
23891
|
}
|
|
23064
23892
|
|
|
23065
23893
|
if (gui.options.zoomControl) {
|
|
23066
|
-
inBtn = gui.buttons.addButton("#zoom-in-icon")
|
|
23067
|
-
|
|
23894
|
+
inBtn = gui.buttons.addButton("#zoom-in-icon")
|
|
23895
|
+
.attr('data-tooltip', 'Zoom in')
|
|
23896
|
+
.attr('aria-label', 'Zoom in');
|
|
23897
|
+
outBtn = gui.buttons.addButton("#zoom-out-icon")
|
|
23898
|
+
.attr('data-tooltip', 'Zoom out')
|
|
23899
|
+
.attr('aria-label', 'Zoom out');
|
|
23068
23900
|
initVariableClick(inBtn.node(), zoomIn);
|
|
23069
23901
|
initVariableClick(outBtn.node(), zoomOut);
|
|
23070
23902
|
ext.on('change', function() {
|
|
@@ -25768,6 +26600,112 @@
|
|
|
25768
26600
|
return true;
|
|
25769
26601
|
}
|
|
25770
26602
|
|
|
26603
|
+
// The bold stretches of a label being edited, as [start, end) offsets into the
|
|
26604
|
+
// text the user edits, and how they follow the text as it changes.
|
|
26605
|
+
//
|
|
26606
|
+
// The editor keeps these beside the textarea's value rather than as markup in
|
|
26607
|
+
// it, so that the caret and the selection index the text the user sees. The
|
|
26608
|
+
// markup they come from and go back to is read and written by
|
|
26609
|
+
// readLabelValue() and writeLabelValue() in gui-label-text.mjs.
|
|
26610
|
+
//
|
|
26611
|
+
// Every function returns a new, normalized list -- sorted, non-empty, and with
|
|
26612
|
+
// no two ranges touching -- and leaves its argument alone.
|
|
26613
|
+
|
|
26614
|
+
function isBoldAt(ranges, i) {
|
|
26615
|
+
for (var j = 0; j < ranges.length; j++) {
|
|
26616
|
+
if (i >= ranges[j][0] && i < ranges[j][1]) return true;
|
|
26617
|
+
}
|
|
26618
|
+
return false;
|
|
26619
|
+
}
|
|
26620
|
+
|
|
26621
|
+
// Whether every character in [start, end) is bold. False for an empty range.
|
|
26622
|
+
function rangeIsAllBold(ranges, start, end) {
|
|
26623
|
+
var pos = start;
|
|
26624
|
+
if (!(end > start)) return false;
|
|
26625
|
+
for (var j = 0; j < ranges.length && pos < end; j++) {
|
|
26626
|
+
if (ranges[j][1] <= pos) continue;
|
|
26627
|
+
if (ranges[j][0] > pos) return false;
|
|
26628
|
+
pos = ranges[j][1];
|
|
26629
|
+
}
|
|
26630
|
+
return pos >= end;
|
|
26631
|
+
}
|
|
26632
|
+
|
|
26633
|
+
// [start, end) set bold (@on) or not.
|
|
26634
|
+
function setBoldRange(ranges, start, end, on) {
|
|
26635
|
+
var out = [];
|
|
26636
|
+
ranges.forEach(function(r) {
|
|
26637
|
+
if (r[1] <= start || r[0] >= end) {
|
|
26638
|
+
out.push([r[0], r[1]]);
|
|
26639
|
+
return;
|
|
26640
|
+
}
|
|
26641
|
+
if (r[0] < start) out.push([r[0], start]);
|
|
26642
|
+
if (r[1] > end) out.push([end, r[1]]);
|
|
26643
|
+
});
|
|
26644
|
+
if (on && end > start) out.push([start, end]);
|
|
26645
|
+
return normalize(out);
|
|
26646
|
+
}
|
|
26647
|
+
|
|
26648
|
+
// What pressing Bold does to [start, end): unbolds a range that is all bold
|
|
26649
|
+
// and bolds any other, including one that is partly bold -- the convention of
|
|
26650
|
+
// every word processor, and the reading that leaves the range one weight.
|
|
26651
|
+
function toggleBoldRange(ranges, start, end) {
|
|
26652
|
+
return setBoldRange(ranges, start, end, !rangeIsAllBold(ranges, start, end));
|
|
26653
|
+
}
|
|
26654
|
+
|
|
26655
|
+
// The ranges for @newText, given the ranges they were for @oldText.
|
|
26656
|
+
//
|
|
26657
|
+
// The edit is found as the text between the longest common prefix and suffix,
|
|
26658
|
+
// which is exact for everything a textarea does in one step -- typing,
|
|
26659
|
+
// deleting, pasting over a selection, an IME composition -- and still gives a
|
|
26660
|
+
// sensible answer for its own undo, which can replace a larger stretch at once.
|
|
26661
|
+
//
|
|
26662
|
+
// Inserted text takes the weight of the character before it, so typing on at
|
|
26663
|
+
// the end of a bold word stays bold and typing after it does not; at the start
|
|
26664
|
+
// of the text it takes the weight of the first character.
|
|
26665
|
+
function updateBoldForEdit(ranges, oldText, newText) {
|
|
26666
|
+
var edit, inherit, mapped;
|
|
26667
|
+
if (ranges.length === 0 || oldText === newText) return normalize(ranges);
|
|
26668
|
+
edit = findEdit(oldText, newText);
|
|
26669
|
+
inherit = edit.start > 0 ? isBoldAt(ranges, edit.start - 1) :
|
|
26670
|
+
isBoldAt(ranges, edit.start + edit.removed);
|
|
26671
|
+
mapped = ranges.map(function(r) {
|
|
26672
|
+
return [mapOffset(r[0], edit), mapOffset(r[1], edit)];
|
|
26673
|
+
});
|
|
26674
|
+
return setBoldRange(normalize(mapped), edit.start, edit.start + edit.inserted,
|
|
26675
|
+
inherit);
|
|
26676
|
+
}
|
|
26677
|
+
|
|
26678
|
+
function sameBoldRanges(a, b) {
|
|
26679
|
+
if (a.length != b.length) return false;
|
|
26680
|
+
for (var i = 0; i < a.length; i++) {
|
|
26681
|
+
if (a[i][0] !== b[i][0] || a[i][1] !== b[i][1]) return false;
|
|
26682
|
+
}
|
|
26683
|
+
return true;
|
|
26684
|
+
}
|
|
26685
|
+
|
|
26686
|
+
// {start, removed, inserted}: characters [start, start + removed) of the old
|
|
26687
|
+
// text became [start, start + inserted) of the new one.
|
|
26688
|
+
function findEdit(a, b) {
|
|
26689
|
+
var max = Math.min(a.length, b.length);
|
|
26690
|
+
var pre = 0, suf = 0;
|
|
26691
|
+
while (pre < max && a.charCodeAt(pre) === b.charCodeAt(pre)) pre++;
|
|
26692
|
+
while (suf < max - pre &&
|
|
26693
|
+
a.charCodeAt(a.length - 1 - suf) === b.charCodeAt(b.length - 1 - suf)) suf++;
|
|
26694
|
+
return {start: pre, removed: a.length - pre - suf, inserted: b.length - pre - suf};
|
|
26695
|
+
}
|
|
26696
|
+
|
|
26697
|
+
// A boundary inside the removed stretch collapses to its start. One after it
|
|
26698
|
+
// moves by the change in length.
|
|
26699
|
+
function mapOffset(x, edit) {
|
|
26700
|
+
if (x <= edit.start) return x;
|
|
26701
|
+
if (x >= edit.start + edit.removed) return x - edit.removed + edit.inserted;
|
|
26702
|
+
return edit.start;
|
|
26703
|
+
}
|
|
26704
|
+
|
|
26705
|
+
function normalize(ranges) {
|
|
26706
|
+
return internal.svg.normalizeBoldRanges(ranges);
|
|
26707
|
+
}
|
|
26708
|
+
|
|
25771
26709
|
// Caret and selection geometry, computed from the SVG character position APIs.
|
|
25772
26710
|
//
|
|
25773
26711
|
// The APIs behave identically on <text> and on <text><textPath>, so this code
|
|
@@ -26087,16 +27025,19 @@
|
|
|
26087
27025
|
// so the tool cannot do this at its own call sites.
|
|
26088
27026
|
self.open = function(target, id, opts) {
|
|
26089
27027
|
var rec = getRecord(target, id);
|
|
27028
|
+
var value;
|
|
26090
27029
|
if (!rec) return false;
|
|
26091
27030
|
self.close();
|
|
27031
|
+
value = readLabelValue(rec['label-text']);
|
|
26092
27032
|
session = {
|
|
26093
27033
|
target: target,
|
|
26094
27034
|
id: id,
|
|
26095
27035
|
pending: null,
|
|
26096
27036
|
created: false,
|
|
26097
27037
|
onClose: opts && opts.onClose || null,
|
|
26098
|
-
startText:
|
|
26099
|
-
startBreaks:
|
|
27038
|
+
startText: value.text,
|
|
27039
|
+
startBreaks: value.breaks,
|
|
27040
|
+
startBold: value.bold,
|
|
26100
27041
|
nodes: null
|
|
26101
27042
|
};
|
|
26102
27043
|
startSession();
|
|
@@ -26142,6 +27083,7 @@
|
|
|
26142
27083
|
onClose: opts.onClose || null,
|
|
26143
27084
|
startText: '',
|
|
26144
27085
|
startBreaks: [],
|
|
27086
|
+
startBold: [],
|
|
26145
27087
|
nodes: null
|
|
26146
27088
|
};
|
|
26147
27089
|
startSession();
|
|
@@ -26150,6 +27092,7 @@
|
|
|
26150
27092
|
|
|
26151
27093
|
function startSession() {
|
|
26152
27094
|
session.text = session.startText;
|
|
27095
|
+
session.bold = session.startBold;
|
|
26153
27096
|
getTextarea().value = session.text;
|
|
26154
27097
|
setCaret(session.text.length, session.text.length);
|
|
26155
27098
|
focusTextarea();
|
|
@@ -26165,6 +27108,74 @@
|
|
|
26165
27108
|
self.refresh();
|
|
26166
27109
|
}
|
|
26167
27110
|
|
|
27111
|
+
// The text a Bold item in the context menu would act on, as {start, end,
|
|
27112
|
+
// bold}, where @bold says whether all of it is bold already; or null. That is
|
|
27113
|
+
// the selection, or with nothing selected, the word under @p -- read when the
|
|
27114
|
+
// menu opens, because by the time an item is chosen the pointer is on the
|
|
27115
|
+
// menu rather than the label.
|
|
27116
|
+
self.getBoldTargetAtPoint = function(p) {
|
|
27117
|
+
var start = getCaretStart(), end = getCaretEnd();
|
|
27118
|
+
var range;
|
|
27119
|
+
if (!session) return null;
|
|
27120
|
+
if (!(end > start)) {
|
|
27121
|
+
start = getEditIndexAtPoint(p);
|
|
27122
|
+
if (start < 0) return null;
|
|
27123
|
+
range = getWordRange(session.text, start);
|
|
27124
|
+
start = range[0];
|
|
27125
|
+
end = range[1];
|
|
27126
|
+
}
|
|
27127
|
+
if (!(end > start)) return null;
|
|
27128
|
+
return {start: start, end: end, bold: rangeIsAllBold(session.bold, start, end)};
|
|
27129
|
+
};
|
|
27130
|
+
|
|
27131
|
+
// Bolds the selected text, or unbolds it if it is all bold already; or the
|
|
27132
|
+
// text from @start to @end, which is selected first. The text stays
|
|
27133
|
+
// selected, so that a second press takes it back.
|
|
27134
|
+
self.toggleBold = function(start, end) {
|
|
27135
|
+
if (!session) return;
|
|
27136
|
+
if (start >= 0 && end > start) {
|
|
27137
|
+
setCaret(start, end);
|
|
27138
|
+
} else {
|
|
27139
|
+
start = getCaretStart();
|
|
27140
|
+
end = getCaretEnd();
|
|
27141
|
+
}
|
|
27142
|
+
if (!(end > start)) return;
|
|
27143
|
+
session.bold = toggleBoldRange(session.bold, start, end);
|
|
27144
|
+
self.refresh();
|
|
27145
|
+
};
|
|
27146
|
+
|
|
27147
|
+
// Selects the text between two points in the label's own coordinate space,
|
|
27148
|
+
// for a drag across it. The selection runs from @p0 towards @p1, so that
|
|
27149
|
+
// shift-arrow then extends it from the end the pointer let go of.
|
|
27150
|
+
self.selectBetweenPoints = function(p0, p1) {
|
|
27151
|
+
var a = getEditIndexAtPoint(p0);
|
|
27152
|
+
var b = getEditIndexAtPoint(p1);
|
|
27153
|
+
if (a < 0 || b < 0) return false;
|
|
27154
|
+
setCaret(Math.min(a, b), Math.max(a, b), b < a ? 'backward' : 'forward');
|
|
27155
|
+
redrawCaret();
|
|
27156
|
+
return true;
|
|
27157
|
+
};
|
|
27158
|
+
|
|
27159
|
+
// Selects the word under @p, for a double-click inside the text.
|
|
27160
|
+
self.selectWordAtPoint = function(p) {
|
|
27161
|
+
var i = getEditIndexAtPoint(p);
|
|
27162
|
+
var range;
|
|
27163
|
+
if (i < 0) return false;
|
|
27164
|
+
range = getWordRange(session.text, i);
|
|
27165
|
+
setCaret(range[0], range[1]);
|
|
27166
|
+
redrawCaret();
|
|
27167
|
+
return true;
|
|
27168
|
+
};
|
|
27169
|
+
|
|
27170
|
+
function getEditIndexAtPoint(p) {
|
|
27171
|
+
var provider, layout, i;
|
|
27172
|
+
if (!session || !session.nodes) return -1;
|
|
27173
|
+
provider = getProvider(session.nodes);
|
|
27174
|
+
layout = getLayout(session);
|
|
27175
|
+
i = getCaretIndexAtPoint(provider, p, getRenderedLength(session.text, layout));
|
|
27176
|
+
return i < 0 ? -1 : getEditIndex(session.text, i, layout);
|
|
27177
|
+
}
|
|
27178
|
+
|
|
26168
27179
|
self.isOpen = function() {
|
|
26169
27180
|
return !!session;
|
|
26170
27181
|
};
|
|
@@ -26195,6 +27206,7 @@
|
|
|
26195
27206
|
// arriving just after a redraw is holding one of those.
|
|
26196
27207
|
if (session.pending && node.closest &&
|
|
26197
27208
|
node.closest('.label-edit-pending')) return true;
|
|
27209
|
+
if (session.nodes && session.nodes.symbol.contains(node)) return true;
|
|
26198
27210
|
return !!groups && (groups.hit.contains(node) || groups.back.contains(node) ||
|
|
26199
27211
|
groups.front.contains(node));
|
|
26200
27212
|
};
|
|
@@ -26253,7 +27265,7 @@
|
|
|
26253
27265
|
if (i < 0) return false;
|
|
26254
27266
|
i = getEditIndex(session.text, i, layout);
|
|
26255
27267
|
setCaret(i, i);
|
|
26256
|
-
|
|
27268
|
+
redrawCaret();
|
|
26257
27269
|
return true;
|
|
26258
27270
|
};
|
|
26259
27271
|
|
|
@@ -26287,7 +27299,7 @@
|
|
|
26287
27299
|
var blank = textHasNoGlyphs(o.text);
|
|
26288
27300
|
var breaks = getBreaks(o);
|
|
26289
27301
|
if (o.pending) {
|
|
26290
|
-
if (!blank) o.pending.create(
|
|
27302
|
+
if (!blank) o.pending.create(writeLabelValue(o.text, breaks, o.bold));
|
|
26291
27303
|
return;
|
|
26292
27304
|
}
|
|
26293
27305
|
if (blank) {
|
|
@@ -26295,8 +27307,9 @@
|
|
|
26295
27307
|
removeLabel(o);
|
|
26296
27308
|
return;
|
|
26297
27309
|
}
|
|
26298
|
-
if (o.text === o.startText && sameSoftBreaks(breaks, o.startBreaks)
|
|
26299
|
-
|
|
27310
|
+
if (o.text === o.startText && sameSoftBreaks(breaks, o.startBreaks) &&
|
|
27311
|
+
sameBoldRanges(o.bold, o.startBold)) return;
|
|
27312
|
+
runGuiEditCommand(gui, getLabelTextCommand(writeLabelValue(o.text, breaks, o.bold),
|
|
26300
27313
|
o.id, o.target.name), {
|
|
26301
27314
|
title: 'Label text'
|
|
26302
27315
|
});
|
|
@@ -26309,7 +27322,7 @@
|
|
|
26309
27322
|
var rec;
|
|
26310
27323
|
if (!shp || shp.length > 1) return [];
|
|
26311
27324
|
rec = o.pending ? o.pending.getStyle() : getRecord(o.target, o.id);
|
|
26312
|
-
return getSoftBreaks(o.text, rec);
|
|
27325
|
+
return getSoftBreaks(o.text, rec, o.bold);
|
|
26313
27326
|
}
|
|
26314
27327
|
|
|
26315
27328
|
// Deletes the label's feature, geometry and all.
|
|
@@ -26396,7 +27409,7 @@
|
|
|
26396
27409
|
var rec = Object.assign({}, o.pending.getStyle());
|
|
26397
27410
|
// With its soft breaks, so that what is aligned and what a callout meets
|
|
26398
27411
|
// is the block as wrapped
|
|
26399
|
-
rec['label-text'] = encodeLabelText(
|
|
27412
|
+
rec['label-text'] = encodeLabelText(writeLabelValue(o.text, getBreaks(o), o.bold));
|
|
26400
27413
|
// No need to expand label-pos here, or to measure its text: the renderer
|
|
26401
27414
|
// resolves the position and asks for the width it needs, so a pending
|
|
26402
27415
|
// label is laid out by exactly the same code as a committed one.
|
|
@@ -26444,11 +27457,15 @@
|
|
|
26444
27457
|
// - Line breaks. An empty <tspan> lays out nothing, so a line just opened
|
|
26445
27458
|
// with Enter had no position for the caret to move to. The placeholder in
|
|
26446
27459
|
// getRenderedLines() gives it one.
|
|
27460
|
+
//
|
|
27461
|
+
// Bold stretches are <tspan>s inside the line they fall on. The character
|
|
27462
|
+
// position APIs count the characters of descendant elements in document
|
|
27463
|
+
// order, so the rendered indexes stay the edited ones whatever the runs are.
|
|
26447
27464
|
function writeText(o) {
|
|
26448
27465
|
var content = o.nodes.content;
|
|
26449
27466
|
var breaks = getBreaks(o);
|
|
26450
|
-
var written = o.text + '\u0000' + breaks.join(',');
|
|
26451
|
-
var lines, i, tspan;
|
|
27467
|
+
var written = o.text + '\u0000' + breaks.join(',') + '\u0000' + o.bold.join(';');
|
|
27468
|
+
var lines, full, pos, i, tspan;
|
|
26452
27469
|
// refresh() runs on every map render, which during a pan is every frame;
|
|
26453
27470
|
// rebuilding text nodes that already say the right thing is pure waste
|
|
26454
27471
|
if (o.writtenTo === content && o.writtenText === written) return;
|
|
@@ -26462,23 +27479,53 @@
|
|
|
26462
27479
|
// or from a label that was multi-line before it was given a path -- and
|
|
26463
27480
|
// turning them into spaces is what export does too.
|
|
26464
27481
|
if (getLayout(o) === LINES_JOINED) {
|
|
26465
|
-
|
|
27482
|
+
full = getRenderedContent(o.text);
|
|
27483
|
+
appendRuns(content, full, o.bold, 0, full.length);
|
|
26466
27484
|
return;
|
|
26467
27485
|
}
|
|
26468
27486
|
// A soft break keeps the space it broke at at the end of its line, where
|
|
26469
27487
|
// the rendered label drops it: here every typed character has to be one
|
|
26470
27488
|
// the caret can sit beside.
|
|
27489
|
+
//
|
|
27490
|
+
// The lines laid end to end are the edited text character for character --
|
|
27491
|
+
// each hard break is the placeholder that opens the line after it -- so a
|
|
27492
|
+
// line's offset in the one is its offset in the other.
|
|
26471
27493
|
lines = getRenderedLines(o.text, breaks);
|
|
26472
|
-
|
|
27494
|
+
full = lines.join('');
|
|
27495
|
+
pos = lines[0].length;
|
|
27496
|
+
appendRuns(content, full, o.bold, 0, pos);
|
|
26473
27497
|
for (i = 1; i < lines.length; i++) {
|
|
26474
27498
|
tspan = document.createElementNS(SVG_NS$3, 'tspan');
|
|
26475
27499
|
tspan.setAttribute('x', o.nodes.text.getAttribute('x') || 0);
|
|
26476
27500
|
tspan.setAttribute('dy', getLineHeight(o));
|
|
26477
|
-
tspan.
|
|
27501
|
+
appendRuns(tspan, full, o.bold, pos, pos + lines[i].length);
|
|
27502
|
+
pos += lines[i].length;
|
|
26478
27503
|
content.appendChild(tspan);
|
|
26479
27504
|
}
|
|
26480
27505
|
}
|
|
26481
27506
|
|
|
27507
|
+
// Characters [start, end) of @str into @parent, the bold ones in bold
|
|
27508
|
+
// <tspan>s and the rest as text. An empty stretch still gets a text node, as
|
|
27509
|
+
// an empty line always did.
|
|
27510
|
+
function appendRuns(parent, str, bold, start, end) {
|
|
27511
|
+
var runs = internal.svg.getBoldRuns(str, bold, start, end);
|
|
27512
|
+
var tspan;
|
|
27513
|
+
if (runs.length === 0) {
|
|
27514
|
+
parent.appendChild(document.createTextNode(''));
|
|
27515
|
+
return;
|
|
27516
|
+
}
|
|
27517
|
+
runs.forEach(function(run) {
|
|
27518
|
+
if (!run.bold) {
|
|
27519
|
+
parent.appendChild(document.createTextNode(run.text));
|
|
27520
|
+
return;
|
|
27521
|
+
}
|
|
27522
|
+
tspan = document.createElementNS(SVG_NS$3, 'tspan');
|
|
27523
|
+
tspan.setAttribute('font-weight', internal.svg.LABEL_BOLD_WEIGHT);
|
|
27524
|
+
tspan.appendChild(document.createTextNode(run.text));
|
|
27525
|
+
parent.appendChild(tspan);
|
|
27526
|
+
});
|
|
27527
|
+
}
|
|
27528
|
+
|
|
26482
27529
|
// Cached per session: asking costs a record lookup and a geometry test, and
|
|
26483
27530
|
// the answer cannot change while a session is open.
|
|
26484
27531
|
function getLayout(o) {
|
|
@@ -26604,18 +27651,33 @@
|
|
|
26604
27651
|
return el;
|
|
26605
27652
|
}
|
|
26606
27653
|
|
|
27654
|
+
// Updated in place rather than rebuilt. Every caret move redraws the
|
|
27655
|
+
// overlay, and a click is one: a region replaced by the first click of a
|
|
27656
|
+
// double-click takes the node the button went down on out of the document,
|
|
27657
|
+
// and the browser then fires neither the click nor the dblclick.
|
|
26607
27658
|
function drawHitRegion(o, g, box, caret, pathId) {
|
|
26608
|
-
var
|
|
26609
|
-
|
|
26610
|
-
if (box
|
|
26611
|
-
|
|
27659
|
+
var area = g.querySelector('.label-edit-hit-area');
|
|
27660
|
+
var line = g.querySelector('.label-edit-hit-baseline');
|
|
27661
|
+
if (box && area) {
|
|
27662
|
+
setRect(area, box);
|
|
27663
|
+
} else if (box) {
|
|
27664
|
+
g.insertBefore(rect(box, 'label-edit-hit-area'), g.firstChild);
|
|
27665
|
+
} else if (area) {
|
|
27666
|
+
g.removeChild(area);
|
|
27667
|
+
}
|
|
27668
|
+
if (!pathId) {
|
|
27669
|
+
if (line) g.removeChild(line);
|
|
27670
|
+
return;
|
|
27671
|
+
}
|
|
26612
27672
|
// A thickened, transparent copy of the baseline: about one em wide, so that
|
|
26613
27673
|
// the region follows the curve rather than boxing it.
|
|
26614
|
-
|
|
26615
|
-
|
|
26616
|
-
|
|
26617
|
-
|
|
26618
|
-
|
|
27674
|
+
if (!line) {
|
|
27675
|
+
line = document.createElementNS(SVG_NS$3, 'use');
|
|
27676
|
+
line.setAttribute('class', 'label-edit-hit-baseline');
|
|
27677
|
+
g.appendChild(line);
|
|
27678
|
+
}
|
|
27679
|
+
line.setAttribute('href', '#' + pathId);
|
|
27680
|
+
line.setAttribute('stroke-width', caret ? caret.ascent + caret.descent : 12);
|
|
26619
27681
|
}
|
|
26620
27682
|
|
|
26621
27683
|
function getLabelPathId(nodes) {
|
|
@@ -26708,12 +27770,16 @@
|
|
|
26708
27770
|
|
|
26709
27771
|
function rect(box, className) {
|
|
26710
27772
|
var el = document.createElementNS(SVG_NS$3, 'rect');
|
|
27773
|
+
setRect(el, box);
|
|
27774
|
+
el.setAttribute('class', className);
|
|
27775
|
+
return el;
|
|
27776
|
+
}
|
|
27777
|
+
|
|
27778
|
+
function setRect(el, box) {
|
|
26711
27779
|
el.setAttribute('x', box.x);
|
|
26712
27780
|
el.setAttribute('y', box.y);
|
|
26713
27781
|
el.setAttribute('width', box.width);
|
|
26714
27782
|
el.setAttribute('height', box.height);
|
|
26715
|
-
el.setAttribute('class', className);
|
|
26716
|
-
return el;
|
|
26717
27783
|
}
|
|
26718
27784
|
|
|
26719
27785
|
function caretLine(caret) {
|
|
@@ -26743,6 +27809,7 @@
|
|
|
26743
27809
|
|
|
26744
27810
|
textarea.addEventListener('input', function() {
|
|
26745
27811
|
if (!session) return;
|
|
27812
|
+
session.bold = updateBoldForEdit(session.bold, session.text, textarea.value);
|
|
26746
27813
|
session.text = textarea.value;
|
|
26747
27814
|
self.refresh();
|
|
26748
27815
|
});
|
|
@@ -26767,6 +27834,13 @@
|
|
|
26767
27834
|
return !e.isComposing && e.keyCode != 229;
|
|
26768
27835
|
}
|
|
26769
27836
|
|
|
27837
|
+
// Cmd-B on a Mac, Ctrl-B elsewhere, as in every text editor. A textarea
|
|
27838
|
+
// has no default for it to replace.
|
|
27839
|
+
function isBoldKey(e) {
|
|
27840
|
+
if (!(e.metaKey || e.ctrlKey) || e.shiftKey || e.altKey) return false;
|
|
27841
|
+
return (e.key || '').toLowerCase() == 'b';
|
|
27842
|
+
}
|
|
27843
|
+
|
|
26770
27844
|
textarea.addEventListener('keydown', function(e) {
|
|
26771
27845
|
if (!session) return;
|
|
26772
27846
|
// The GUI's global key handlers are told to keep out of the way by
|
|
@@ -26793,6 +27867,10 @@
|
|
|
26793
27867
|
e.preventDefault();
|
|
26794
27868
|
e.stopPropagation();
|
|
26795
27869
|
self.close();
|
|
27870
|
+
} else if (isBoldKey(e)) {
|
|
27871
|
+
e.preventDefault();
|
|
27872
|
+
e.stopPropagation();
|
|
27873
|
+
self.toggleBold();
|
|
26796
27874
|
} else {
|
|
26797
27875
|
e.stopPropagation();
|
|
26798
27876
|
}
|
|
@@ -26846,10 +27924,11 @@
|
|
|
26846
27924
|
return textarea ? textarea.selectionEnd : 0;
|
|
26847
27925
|
}
|
|
26848
27926
|
|
|
26849
|
-
|
|
27927
|
+
// @direction: 'forward' (the default) or 'backward', which end of the
|
|
27928
|
+
// selection shift-arrow moves
|
|
27929
|
+
function setCaret(start, end, direction) {
|
|
26850
27930
|
if (!textarea) return;
|
|
26851
|
-
textarea.
|
|
26852
|
-
textarea.selectionEnd = end;
|
|
27931
|
+
textarea.setSelectionRange(start, end, direction || 'forward');
|
|
26853
27932
|
}
|
|
26854
27933
|
}
|
|
26855
27934
|
|
|
@@ -27542,7 +28621,7 @@
|
|
|
27542
28621
|
}
|
|
27543
28622
|
|
|
27544
28623
|
// The label tool: arming a label type, placing an anchor or a curve, turning
|
|
27545
|
-
// the result into
|
|
28624
|
+
// the result into a -labels coordinates= command, and handing the new label to the
|
|
27546
28625
|
// in-place text editor.
|
|
27547
28626
|
//
|
|
27548
28627
|
// Creating a label and editing one are one gesture, not two: a click leaves a
|
|
@@ -27606,6 +28685,14 @@
|
|
|
27606
28685
|
// The mouseup that ended the last block drag, or null. A short one is
|
|
27607
28686
|
// followed by a click from the same mouseup, which it has already handled.
|
|
27608
28687
|
var blockRelease = null;
|
|
28688
|
+
// A drag across the text of the label being typed into, which selects the
|
|
28689
|
+
// characters it passes over: {start}, where start is the pointer's position
|
|
28690
|
+
// at the press, filled in by the first drag event.
|
|
28691
|
+
var selectDrag = null;
|
|
28692
|
+
// The mouseup that ended the last select drag, or null, for the same reason
|
|
28693
|
+
// as blockRelease: the click that follows it would put the caret down and
|
|
28694
|
+
// lose the selection just made.
|
|
28695
|
+
var selectRelease = null;
|
|
27609
28696
|
var editor = new LabelEditor(gui, ext);
|
|
27610
28697
|
var selection = new LabelSelection(gui, ext, hit, function() {
|
|
27611
28698
|
return editor.isOpen() ? editor.getFeatureId() : -1;
|
|
@@ -27635,7 +28722,7 @@
|
|
|
27635
28722
|
// tools do on entry as well (`addEmptyLayer()` in `gui-edit-points.mjs` and
|
|
27636
28723
|
// `gui-draw-lines2.mjs`). It is named rather than left unnamed, unlike
|
|
27637
28724
|
// theirs: it is the layer `getLabelTarget()` would have created for a label
|
|
27638
|
-
// anyway, and naming it is what lets the `-
|
|
28725
|
+
// anyway, and naming it is what lets the `-labels` commands name their
|
|
27639
28726
|
// target and so replay from the session history.
|
|
27640
28727
|
//
|
|
27641
28728
|
// Called before entering the tool's own mode, because getInitialTool() reads
|
|
@@ -27685,6 +28772,8 @@
|
|
|
27685
28772
|
handleDrag = null;
|
|
27686
28773
|
releasedDrag = null;
|
|
27687
28774
|
blockRelease = null;
|
|
28775
|
+
selectDrag = null;
|
|
28776
|
+
selectRelease = null;
|
|
27688
28777
|
shownHandles = null;
|
|
27689
28778
|
clearBlockDrag();
|
|
27690
28779
|
hoverHandle = null;
|
|
@@ -27811,6 +28900,10 @@
|
|
|
27811
28900
|
blockRelease = null;
|
|
27812
28901
|
return;
|
|
27813
28902
|
}
|
|
28903
|
+
if (selectRelease && getDomEvent(e) == selectRelease) {
|
|
28904
|
+
selectRelease = null;
|
|
28905
|
+
return;
|
|
28906
|
+
}
|
|
27814
28907
|
if (armed == 'path' && drawingCurve()) {
|
|
27815
28908
|
extendCurve(pixToMapCoords(e.x, e.y));
|
|
27816
28909
|
return;
|
|
@@ -27883,9 +28976,34 @@
|
|
|
27883
28976
|
e.flipLabel = getFlipAction(target, id);
|
|
27884
28977
|
}
|
|
27885
28978
|
}
|
|
28979
|
+
if (target && editor.isOpen() && clickIsOnEditedLabel(e)) {
|
|
28980
|
+
addBoldAction(e, target);
|
|
28981
|
+
}
|
|
27886
28982
|
gui.contextMenu.open(e, target);
|
|
27887
28983
|
});
|
|
27888
28984
|
|
|
28985
|
+
// Bold for the words being typed: the selection, or with nothing selected,
|
|
28986
|
+
// the word that was right-clicked. The item says what it will do, since a
|
|
28987
|
+
// menu has no pressed state to show that the words are bold already. There
|
|
28988
|
+
// is none in a font with no bold face (see fontHasBoldFace()).
|
|
28989
|
+
function addBoldAction(e, target) {
|
|
28990
|
+
var range = editor.getBoldTargetAtPoint(getLabelSpacePoint(editor.getAnchorCoords(), e));
|
|
28991
|
+
if (!range || !fontHasBoldFace(getEditedFontName(target))) return;
|
|
28992
|
+
e.boldText = {
|
|
28993
|
+
label: range.bold ? 'remove bold' : 'bold text',
|
|
28994
|
+
run: function() { editor.toggleBold(range.start, range.end); }
|
|
28995
|
+
};
|
|
28996
|
+
}
|
|
28997
|
+
|
|
28998
|
+
// The font the label being typed into is drawn in: its own, or for a label
|
|
28999
|
+
// not created yet, the one the panel will give it.
|
|
29000
|
+
function getEditedFontName(target) {
|
|
29001
|
+
var id = editor.getFeatureId();
|
|
29002
|
+
var rec = id > -1 ? getRecord(target, id) : getNewLabelStyle(gui);
|
|
29003
|
+
var font = rec && rec['font-family'];
|
|
29004
|
+
return font || (id > -1 ? getDefaultFontName() : getNewLabelFontName());
|
|
29005
|
+
}
|
|
29006
|
+
|
|
27889
29007
|
// The label a right-click was aimed at, or -1.
|
|
27890
29008
|
//
|
|
27891
29009
|
// A label being typed into is not a hit target -- the editor's own overlay
|
|
@@ -28141,7 +29259,16 @@
|
|
|
28141
29259
|
// takes a drag -- everything else over a label is left alone so that the map
|
|
28142
29260
|
// still pans, which is why these handlers stop the event themselves.
|
|
28143
29261
|
hit.on('dragstart', function(e) {
|
|
28144
|
-
if (!active() || drawingCurve()
|
|
29262
|
+
if (!active() || drawingCurve()) return;
|
|
29263
|
+
// With a label open for typing, a drag across its text selects some of it,
|
|
29264
|
+
// as it would in any text field. Anywhere else the map still pans.
|
|
29265
|
+
if (editor.isOpen()) {
|
|
29266
|
+
if (clickIsOnEditedLabel(e)) {
|
|
29267
|
+
selectDrag = {start: null};
|
|
29268
|
+
consumeDrag(e);
|
|
29269
|
+
}
|
|
29270
|
+
return;
|
|
29271
|
+
}
|
|
28145
29272
|
// The handle comes from the last hover, not from testing the pointer here.
|
|
28146
29273
|
// dragstart arrives with the pointer already moved off the handle -- by the
|
|
28147
29274
|
// first mousemove that made it a drag, which on a quick gesture is further
|
|
@@ -28166,6 +29293,22 @@
|
|
|
28166
29293
|
consumeDrag(e);
|
|
28167
29294
|
});
|
|
28168
29295
|
|
|
29296
|
+
// The selection runs from where the button went down, which dragstart does
|
|
29297
|
+
// not report -- it arrives after the first move -- but every drag event
|
|
29298
|
+
// carries its distance from the press.
|
|
29299
|
+
function updateSelectDrag(e) {
|
|
29300
|
+
var anchor = editor.getAnchorCoords();
|
|
29301
|
+
if (!editor.isOpen() || !anchor) {
|
|
29302
|
+
selectDrag = null;
|
|
29303
|
+
return;
|
|
29304
|
+
}
|
|
29305
|
+
if (!selectDrag.start) {
|
|
29306
|
+
selectDrag.start = {x: e.x - (e.dragX || 0), y: e.y - (e.dragY || 0)};
|
|
29307
|
+
}
|
|
29308
|
+
editor.selectBetweenPoints(getLabelSpacePoint(anchor, selectDrag.start),
|
|
29309
|
+
getLabelSpacePoint(anchor, e));
|
|
29310
|
+
}
|
|
29311
|
+
|
|
28169
29312
|
// handle: {id, index, point, pointer} from findHandle(), or the same shape
|
|
28170
29313
|
// made up for a drag that grabbed a label somewhere other than its knot
|
|
28171
29314
|
function beginKnotDrag(handle) {
|
|
@@ -28191,6 +29334,11 @@
|
|
|
28191
29334
|
|
|
28192
29335
|
hit.on('drag', function(e) {
|
|
28193
29336
|
var shp, p;
|
|
29337
|
+
if (selectDrag) {
|
|
29338
|
+
consumeDrag(e);
|
|
29339
|
+
updateSelectDrag(e);
|
|
29340
|
+
return;
|
|
29341
|
+
}
|
|
28194
29342
|
if (textDrag) {
|
|
28195
29343
|
consumeDrag(e);
|
|
28196
29344
|
updateTextDrag(e);
|
|
@@ -28225,6 +29373,12 @@
|
|
|
28225
29373
|
|
|
28226
29374
|
hit.on('dragend', function(e) {
|
|
28227
29375
|
var o = drag;
|
|
29376
|
+
if (selectDrag) {
|
|
29377
|
+
selectDrag = null;
|
|
29378
|
+
selectRelease = getDomEvent(e);
|
|
29379
|
+
consumeDrag(e);
|
|
29380
|
+
return;
|
|
29381
|
+
}
|
|
28228
29382
|
if (textDrag) {
|
|
28229
29383
|
o = textDrag;
|
|
28230
29384
|
textDrag = null;
|
|
@@ -28865,9 +30019,9 @@
|
|
|
28865
30019
|
selection.refresh(true);
|
|
28866
30020
|
}
|
|
28867
30021
|
|
|
28868
|
-
// Turns the previewed offset into one -
|
|
30022
|
+
// Turns the previewed offset into one -labels.
|
|
28869
30023
|
//
|
|
28870
|
-
// Only the label under the pointer moves, even with several selected: -
|
|
30024
|
+
// Only the label under the pointer moves, even with several selected: -labels
|
|
28871
30025
|
// writes one value to every id it is given, so a group drag would set them
|
|
28872
30026
|
// all to the same absolute offset rather than nudging each by its own delta.
|
|
28873
30027
|
function commitOffsetDrag(o) {
|
|
@@ -29011,7 +30165,7 @@
|
|
|
29011
30165
|
}
|
|
29012
30166
|
|
|
29013
30167
|
function dragging() {
|
|
29014
|
-
return !!(drag || textDrag || handleDrag || blockDrag);
|
|
30168
|
+
return !!(drag || textDrag || handleDrag || blockDrag || selectDrag);
|
|
29015
30169
|
}
|
|
29016
30170
|
|
|
29017
30171
|
// The exception is about the pointer being on the glyphs, not about the mode:
|
|
@@ -29129,6 +30283,12 @@
|
|
|
29129
30283
|
resetHandle(hoverHandle);
|
|
29130
30284
|
return;
|
|
29131
30285
|
}
|
|
30286
|
+
// Inside the text being typed, a double-click selects a word -- which is
|
|
30287
|
+
// the quickest way to pick the words to set in bold.
|
|
30288
|
+
if (editor.isOpen() && clickIsOnEditedLabel(e)) {
|
|
30289
|
+
editor.selectWordAtPoint(getLabelSpacePoint(editor.getAnchorCoords(), e));
|
|
30290
|
+
return;
|
|
30291
|
+
}
|
|
29132
30292
|
// Double-clicking a label reaches straight into its text, which is the
|
|
29133
30293
|
// gesture most editors use and saves selecting it first. The single-click
|
|
29134
30294
|
// route got there already if the label was the selection; this covers the
|
|
@@ -29341,6 +30501,7 @@
|
|
|
29341
30501
|
referenceStyle = { // outline style for reference layers
|
|
29342
30502
|
type: 'outline',
|
|
29343
30503
|
strokeColors: [null, '#87b73b'], // was 78c110
|
|
30504
|
+
// strokeColors: [null, 'rgba(79,140,0,0.67)'],
|
|
29344
30505
|
strokeWidth: 0.85,
|
|
29345
30506
|
dotColor: "#73ba20",
|
|
29346
30507
|
dotSize: 1
|
|
@@ -30220,43 +31381,115 @@
|
|
|
30220
31381
|
}
|
|
30221
31382
|
|
|
30222
31383
|
|
|
30223
|
-
|
|
30224
|
-
|
|
30225
|
-
|
|
30226
|
-
|
|
30227
|
-
|
|
31384
|
+
// symbolScale: the preview symbol scale (1 outside preview). Pattern sizes are
|
|
31385
|
+
// in output pixels, like stroke widths, so the tile grows with the page.
|
|
31386
|
+
// anchor: canvas-pixel position the pattern origin is pinned to, so the tile
|
|
31387
|
+
// travels with the map instead of sticking to the viewport. Panning only
|
|
31388
|
+
// updates this matrix. The tile image is rebuilt when the scale changes,
|
|
31389
|
+
// which is at most once per pattern per render.
|
|
31390
|
+
function getCanvasFillPattern(style, symbolScale, anchor) {
|
|
31391
|
+
var scale = symbolScale > 0 ? symbolScale : 1;
|
|
31392
|
+
var entry = hatches[style.fillPattern];
|
|
31393
|
+
if (!entry || entry.scale != scale) {
|
|
31394
|
+
entry = makePatternEntry(style, scale);
|
|
31395
|
+
hatches[style.fillPattern] = entry;
|
|
30228
31396
|
}
|
|
30229
|
-
|
|
31397
|
+
if (!entry || !entry.pattern) return style.fill || '#000';
|
|
31398
|
+
if (anchor) applyPatternAnchor(entry, anchor);
|
|
31399
|
+
return entry.pattern;
|
|
30230
31400
|
}
|
|
30231
31401
|
|
|
30232
|
-
function
|
|
31402
|
+
function makePatternEntry(style, scale) {
|
|
30233
31403
|
var o = internal.parsePattern(style.fillPattern);
|
|
30234
31404
|
if (!o) return null;
|
|
30235
31405
|
var canv = document.createElement('canvas');
|
|
30236
31406
|
var ctx = canv.getContext('2d');
|
|
30237
|
-
var
|
|
30238
|
-
var
|
|
30239
|
-
|
|
31407
|
+
var k = GUI.getPixelRatio() * scale;
|
|
31408
|
+
var tw = o.tileSize[0], th = o.tileSize[1];
|
|
31409
|
+
// A canvas has whole-pixel dimensions, but the scaled tile generally does
|
|
31410
|
+
// not. The tile is drawn into the nearest whole-pixel canvas, and the
|
|
31411
|
+
// pattern transform stretches it back to its exact size so that the
|
|
31412
|
+
// repeat period doesn't drift across a large polygon.
|
|
31413
|
+
var w = Math.max(1, Math.round(tw * k));
|
|
31414
|
+
var h = Math.max(1, Math.round(th * k));
|
|
30240
31415
|
canv.setAttribute('width', w);
|
|
30241
31416
|
canv.setAttribute('height', h);
|
|
31417
|
+
ctx.scale(w / tw, h / th);
|
|
30242
31418
|
if (o.background) {
|
|
30243
|
-
ctx.fillStyle = o.background;
|
|
30244
|
-
ctx.fillRect(0, 0,
|
|
30245
|
-
}
|
|
30246
|
-
if (o.type == 'dots' || o.type == 'squares') makeDotFill(o, ctx, res);
|
|
30247
|
-
if (o.type == 'dashes') makeDashFill(o, ctx, res);
|
|
30248
|
-
if (o.type == 'hatches') makeHatchFill(o, ctx, res);
|
|
30249
|
-
var pattern = ctx.createPattern(canv, 'repeat');
|
|
30250
|
-
if (o.rotation) {
|
|
30251
|
-
pattern.setTransform(new DOMMatrix('rotate(' + o.rotation + 'deg)'));
|
|
31419
|
+
ctx.fillStyle = getCanvasColor(o.background);
|
|
31420
|
+
ctx.fillRect(0, 0, tw, th);
|
|
30252
31421
|
}
|
|
30253
|
-
|
|
31422
|
+
if (o.type == 'dots' || o.type == 'squares') makeDotFill(o, ctx, 1);
|
|
31423
|
+
if (o.type == 'dashes') makeDashFill(o, ctx, 1);
|
|
31424
|
+
if (o.type == 'hatches') makeHatchFill(o, ctx, 1);
|
|
31425
|
+
return {
|
|
31426
|
+
scale: scale,
|
|
31427
|
+
pattern: ctx.createPattern(canv, 'repeat'),
|
|
31428
|
+
rotation: o.rotation || 0,
|
|
31429
|
+
sx: tw * k / w,
|
|
31430
|
+
sy: th * k / h,
|
|
31431
|
+
w: w,
|
|
31432
|
+
h: h
|
|
31433
|
+
};
|
|
31434
|
+
}
|
|
31435
|
+
|
|
31436
|
+
// Pins the cached pattern to @anchor. Skipped when the view has not moved,
|
|
31437
|
+
// so a hover redraw does not touch the pattern.
|
|
31438
|
+
function applyPatternAnchor(entry, anchor) {
|
|
31439
|
+
if (entry.ax == anchor.x && entry.ay == anchor.y) return;
|
|
31440
|
+
entry.ax = anchor.x;
|
|
31441
|
+
entry.ay = anchor.y;
|
|
31442
|
+
var t = getPatternTransform(entry, anchor);
|
|
31443
|
+
entry.pattern.setTransform(new DOMMatrix([t.a, t.b, t.c, t.d, t.e, t.f]));
|
|
31444
|
+
}
|
|
31445
|
+
|
|
31446
|
+
// Matrix taking pattern-image pixels to canvas pixels: translate to the
|
|
31447
|
+
// anchor, then rotate, then scale. The anchor is folded into one tile so a
|
|
31448
|
+
// map origin far outside the viewport does not blow the matrix precision.
|
|
31449
|
+
function getPatternTransform(entry, anchor) {
|
|
31450
|
+
var p = wrapPatternAnchor(anchor.x, anchor.y, entry);
|
|
31451
|
+
var rad = (entry.rotation || 0) * Math.PI / 180;
|
|
31452
|
+
var c = Math.cos(rad);
|
|
31453
|
+
var s = Math.sin(rad);
|
|
31454
|
+
return {
|
|
31455
|
+
a: c * entry.sx,
|
|
31456
|
+
b: s * entry.sx,
|
|
31457
|
+
c: -s * entry.sy,
|
|
31458
|
+
d: c * entry.sy,
|
|
31459
|
+
e: p.x,
|
|
31460
|
+
f: p.y
|
|
31461
|
+
};
|
|
31462
|
+
}
|
|
31463
|
+
|
|
31464
|
+
// @entry.sx/sy scale image pixels to canvas pixels; the image repeats every
|
|
31465
|
+
// @entry.w by @entry.h pixels. Returns a point in the same place on the
|
|
31466
|
+
// pattern, within one tile of the origin.
|
|
31467
|
+
function wrapPatternAnchor(x, y, entry) {
|
|
31468
|
+
var rad = (entry.rotation || 0) * Math.PI / 180;
|
|
31469
|
+
var c = Math.cos(rad);
|
|
31470
|
+
var s = Math.sin(rad);
|
|
31471
|
+
var rx = c * x + s * y;
|
|
31472
|
+
var ry = -s * x + c * y;
|
|
31473
|
+
var qx = rx / entry.sx - Math.floor(rx / entry.sx / entry.w) * entry.w;
|
|
31474
|
+
var qy = ry / entry.sy - Math.floor(ry / entry.sy / entry.h) * entry.h;
|
|
31475
|
+
var lx = qx * entry.sx;
|
|
31476
|
+
var ly = qy * entry.sy;
|
|
31477
|
+
return {
|
|
31478
|
+
x: c * lx - s * ly,
|
|
31479
|
+
y: s * lx + c * ly
|
|
31480
|
+
};
|
|
31481
|
+
}
|
|
31482
|
+
|
|
31483
|
+
// SVG's "none" is not a canvas colour, and a canvas ignores a fillStyle it
|
|
31484
|
+
// cannot parse -- keeping the last one set, which on a fresh tile is black.
|
|
31485
|
+
function getCanvasColor(color) {
|
|
31486
|
+
return String(color).trim().toLowerCase() == 'none' ? 'transparent' : color;
|
|
30254
31487
|
}
|
|
30255
31488
|
|
|
30256
31489
|
function makeDashFill(o, ctx, res) {
|
|
30257
31490
|
var x = 0;
|
|
30258
31491
|
for (var i=0; i<o.colors.length; i++) {
|
|
30259
|
-
ctx.fillStyle = o.colors[i];
|
|
31492
|
+
ctx.fillStyle = getCanvasColor(o.colors[i]);
|
|
30260
31493
|
ctx.fillRect(x, 0, o.width * res, o.dashes[0] * res);
|
|
30261
31494
|
x += res * (o.spacing + o.width);
|
|
30262
31495
|
}
|
|
@@ -30271,7 +31504,7 @@
|
|
|
30271
31504
|
var x = 0, y = 0;
|
|
30272
31505
|
for (var i=0; i<dots; i++) {
|
|
30273
31506
|
if (o.type == 'dots') ctx.beginPath();
|
|
30274
|
-
ctx.fillStyle = o.colors[(i + Math.floor(i / n)) % n];
|
|
31507
|
+
ctx.fillStyle = getCanvasColor(o.colors[(i + Math.floor(i / n)) % n]);
|
|
30275
31508
|
if (o.type == 'dots') {
|
|
30276
31509
|
ctx.arc(x + r, y + r, r, 0, Math.PI * 2);
|
|
30277
31510
|
} else {
|
|
@@ -30288,8 +31521,8 @@
|
|
|
30288
31521
|
var w;
|
|
30289
31522
|
for (var i=0, x=0; i<o.widths.length; i++) {
|
|
30290
31523
|
w = o.widths[i] * res;
|
|
30291
|
-
ctx.fillStyle = o.colors[i];
|
|
30292
|
-
ctx.fillRect(x, 0,
|
|
31524
|
+
ctx.fillStyle = getCanvasColor(o.colors[i]);
|
|
31525
|
+
ctx.fillRect(x, 0, w, h);
|
|
30293
31526
|
x += w;
|
|
30294
31527
|
}
|
|
30295
31528
|
}
|
|
@@ -30941,27 +32174,66 @@
|
|
|
30941
32174
|
return iter;
|
|
30942
32175
|
}
|
|
30943
32176
|
|
|
32177
|
+
// Same multiplier as ctx.lineWidth. Retina canvases are pixRatio times the
|
|
32178
|
+
// CSS pixel; preview mode multiplies styled strokes by the symbol scale.
|
|
32179
|
+
function getCanvasStrokeScale(pixRatio, lineScale) {
|
|
32180
|
+
return (pixRatio > 1 ? pixRatio : 1) * (lineScale || 1);
|
|
32181
|
+
}
|
|
32182
|
+
|
|
32183
|
+
// Scale a stroke-dasharray into canvas pixels. Scale 1 returns the authored
|
|
32184
|
+
// tokens, so a 100% preview and a non-preview view keep the pattern as stored.
|
|
32185
|
+
function scaleCanvasLineDash(lineDash, scale) {
|
|
32186
|
+
var parts = String(lineDash).split(' ');
|
|
32187
|
+
var out, i;
|
|
32188
|
+
if (!(scale > 0) || scale === 1) return parts;
|
|
32189
|
+
out = new Array(parts.length);
|
|
32190
|
+
for (i = 0; i < parts.length; i++) {
|
|
32191
|
+
out[i] = +parts[i] * scale;
|
|
32192
|
+
}
|
|
32193
|
+
return out;
|
|
32194
|
+
}
|
|
32195
|
+
|
|
32196
|
+
// Where a fill pattern's origin sits, in canvas pixels. In preview that is
|
|
32197
|
+
// the top-left of the page; otherwise the geographic origin, so a pan (which
|
|
32198
|
+
// only changes the translation) carries the pattern with the shapes.
|
|
32199
|
+
function getPatternAnchor(ext) {
|
|
32200
|
+
var t = ext.getTransform(GUI.getPixelRatio());
|
|
32201
|
+
var frame = ext.getFrameData();
|
|
32202
|
+
if (frame && frame.bbox) {
|
|
32203
|
+
return {
|
|
32204
|
+
x: frame.bbox[0] * t.mx + t.bx,
|
|
32205
|
+
y: frame.bbox[3] * t.my + t.by
|
|
32206
|
+
};
|
|
32207
|
+
}
|
|
32208
|
+
return {x: t.bx, y: t.by};
|
|
32209
|
+
}
|
|
32210
|
+
|
|
30944
32211
|
function getPathStart(ext, lineScale) {
|
|
30945
32212
|
var pixRatio = GUI.getPixelRatio();
|
|
30946
32213
|
if (!lineScale) lineScale = 1;
|
|
32214
|
+
// Same factor as lineWidth, so dash length and gap stay in proportion to
|
|
32215
|
+
// the stroke. In preview that factor includes the symbol scale; without it
|
|
32216
|
+
// the pattern stays a fixed screen size while the line grows.
|
|
32217
|
+
var strokeScale = getCanvasStrokeScale(pixRatio, lineScale);
|
|
32218
|
+
// The symbol scale itself, not lineScale: lineScale adds a bump to widen
|
|
32219
|
+
// hover outlines, which a fill has no reason to follow.
|
|
32220
|
+
var patternScale = ext.getSymbolScale() || 1;
|
|
32221
|
+
// Page corner in preview, so the pattern is fixed to the page the way the
|
|
32222
|
+
// exported SVG is. With no frame, the geographic origin, which is enough
|
|
32223
|
+
// for panning: the pattern is not scaled by the geographic zoom then.
|
|
32224
|
+
var patternAnchor = getPatternAnchor(ext);
|
|
30947
32225
|
return function(ctx, style) {
|
|
30948
32226
|
var strokeWidth;
|
|
30949
32227
|
ctx.beginPath();
|
|
30950
32228
|
if (style.strokeWidth > 0) {
|
|
30951
32229
|
strokeWidth = style.strokeWidth;
|
|
30952
|
-
if (pixRatio > 1) {
|
|
30953
|
-
// bump up thin lines on retina, but not to more than 1px
|
|
30954
|
-
// (tests on Chrome showed much faster rendering of 1px lines)
|
|
30955
|
-
// strokeWidth = strokeWidth < 1 ? 1 : strokeWidth * pixRatio;
|
|
30956
|
-
strokeWidth = strokeWidth * pixRatio;
|
|
30957
|
-
}
|
|
30958
32230
|
ctx.lineCap = style.lineCap || 'round';
|
|
30959
32231
|
ctx.lineJoin = style.lineJoin || 'round';
|
|
30960
|
-
ctx.lineWidth = strokeWidth *
|
|
32232
|
+
ctx.lineWidth = strokeWidth * strokeScale;
|
|
30961
32233
|
ctx.strokeStyle = style.strokeColor;
|
|
30962
32234
|
if (style.lineDash){
|
|
30963
32235
|
ctx.lineCap = 'butt';
|
|
30964
|
-
ctx.setLineDash(style.lineDash
|
|
32236
|
+
ctx.setLineDash(scaleCanvasLineDash(style.lineDash, strokeScale));
|
|
30965
32237
|
}
|
|
30966
32238
|
if (style.miterLimit) {
|
|
30967
32239
|
ctx.miterLimit = style.miterLimit;
|
|
@@ -30969,7 +32241,7 @@
|
|
|
30969
32241
|
}
|
|
30970
32242
|
|
|
30971
32243
|
if (style.fillPattern) {
|
|
30972
|
-
ctx.fillStyle = getCanvasFillPattern(style);
|
|
32244
|
+
ctx.fillStyle = getCanvasFillPattern(style, patternScale, patternAnchor);
|
|
30973
32245
|
} else if (style.fillColor) {
|
|
30974
32246
|
ctx.fillStyle = style.fillColor;
|
|
30975
32247
|
}
|
|
@@ -31008,20 +32280,28 @@
|
|
|
31008
32280
|
return internal.svg.getTransform(p, scale);
|
|
31009
32281
|
}
|
|
31010
32282
|
|
|
31011
|
-
function repositionFurniture(container,
|
|
31012
|
-
|
|
31013
|
-
|
|
32283
|
+
function repositionFurniture(container, ext) {
|
|
32284
|
+
if (!ext.getFrameData()) return;
|
|
32285
|
+
El.findAll('.mapshaper-svg-furniture', container).forEach(function(g) {
|
|
32286
|
+
g.setAttribute('transform', getSvgFurnitureTransform(ext));
|
|
32287
|
+
});
|
|
31014
32288
|
}
|
|
31015
32289
|
|
|
32290
|
+
// Returns an SVG string, or '' if the furniture can't be drawn in the current
|
|
32291
|
+
// frame (e.g. the map is unprojected). The scalebar panel explains why.
|
|
31016
32292
|
function renderFurniture(lyr, ext) {
|
|
31017
|
-
var frame = ext.getFrameData();
|
|
31018
|
-
var obj
|
|
31019
|
-
if (
|
|
31020
|
-
|
|
31021
|
-
}
|
|
32293
|
+
var frame = ext.getFrameData();
|
|
32294
|
+
var obj;
|
|
32295
|
+
if (internal.getFurnitureFrameProblem(frame)) return '';
|
|
32296
|
+
obj = internal.getEmptyLayerForSVG(lyr, {});
|
|
31022
32297
|
obj.properties.transform = getSvgFurnitureTransform(ext);
|
|
31023
32298
|
obj.properties.class = 'mapshaper-svg-furniture';
|
|
31024
|
-
|
|
32299
|
+
try {
|
|
32300
|
+
obj.children = internal.renderFurnitureLayer(lyr, frame);
|
|
32301
|
+
} catch(e) {
|
|
32302
|
+
console.error(e);
|
|
32303
|
+
return '';
|
|
32304
|
+
}
|
|
31025
32305
|
return internal.svg.stringify(obj);
|
|
31026
32306
|
}
|
|
31027
32307
|
|
|
@@ -31063,8 +32343,6 @@
|
|
|
31063
32343
|
resize(ext);
|
|
31064
32344
|
if (type == 'label' || type == 'symbol') {
|
|
31065
32345
|
html = renderSymbols(lyr.gui.displayLayer, ext, id);
|
|
31066
|
-
} else if (type == 'furniture') {
|
|
31067
|
-
html = renderFurniture(lyr.gui.displayLayer, ext);
|
|
31068
32346
|
}
|
|
31069
32347
|
g.innerHTML = html;
|
|
31070
32348
|
if (type == 'label' || type == 'symbol') {
|
|
@@ -31078,6 +32356,26 @@
|
|
|
31078
32356
|
}
|
|
31079
32357
|
};
|
|
31080
32358
|
|
|
32359
|
+
// Furniture layers are dataset layers (they have no display layer)
|
|
32360
|
+
el.drawFurniture = function(layers) {
|
|
32361
|
+
el.clear();
|
|
32362
|
+
resize(ext);
|
|
32363
|
+
layers.forEach(function(lyr) {
|
|
32364
|
+
var html = renderFurniture(lyr, ext);
|
|
32365
|
+
var g;
|
|
32366
|
+
if (!html) return;
|
|
32367
|
+
g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
32368
|
+
g.setAttribute('class', 'mapshaper-svg-layer mapshaper-furniture-layer');
|
|
32369
|
+
g.innerHTML = html;
|
|
32370
|
+
svg.append(g);
|
|
32371
|
+
});
|
|
32372
|
+
};
|
|
32373
|
+
|
|
32374
|
+
el.repositionFurniture = function() {
|
|
32375
|
+
resize(ext);
|
|
32376
|
+
repositionFurniture(svg, ext);
|
|
32377
|
+
};
|
|
32378
|
+
|
|
31081
32379
|
function reposition(lyr, type, ext) {
|
|
31082
32380
|
var container = el.findChild('.' + lyr.gui.svg_id);
|
|
31083
32381
|
if (!container || !container.node()) {
|
|
@@ -31091,8 +32389,6 @@
|
|
|
31091
32389
|
// a symbol group's transform absorbs panning, and zooming too when a
|
|
31092
32390
|
// frame is defined; anything it can't absorb needs the baselines rebuilt
|
|
31093
32391
|
updateLabelPaths(container.node(), lyr.gui.displayLayer, ext);
|
|
31094
|
-
} else if (type == 'furniture') {
|
|
31095
|
-
repositionFurniture(container.node(), lyr.gui.displayLayer, ext);
|
|
31096
32392
|
} else {
|
|
31097
32393
|
// container.getElementsByTagName('text')
|
|
31098
32394
|
error('Unsupported symbol type:', type);
|
|
@@ -31248,19 +32544,14 @@
|
|
|
31248
32544
|
});
|
|
31249
32545
|
};
|
|
31250
32546
|
|
|
32547
|
+
// Furniture is sized in frame pixels, so panning and zooming only need to
|
|
32548
|
+
// update its transform; its content changes only with a full redraw.
|
|
31251
32549
|
this.drawFurnitureLayers = function(layers, action) {
|
|
31252
|
-
|
|
31253
|
-
|
|
31254
|
-
|
|
31255
|
-
_furniture.
|
|
32550
|
+
if (action == 'nav' || action == 'hover') {
|
|
32551
|
+
_furniture.repositionFurniture();
|
|
32552
|
+
} else {
|
|
32553
|
+
_furniture.drawFurniture(layers);
|
|
31256
32554
|
}
|
|
31257
|
-
layers.forEach(function(lyr) {
|
|
31258
|
-
if (noRedraw) {
|
|
31259
|
-
_furniture.reposition(lyr, 'furniture');
|
|
31260
|
-
} else {
|
|
31261
|
-
_furniture.drawLayer(lyr, 'furniture');
|
|
31262
|
-
}
|
|
31263
|
-
});
|
|
31264
32555
|
};
|
|
31265
32556
|
|
|
31266
32557
|
// kludge: skip rendering base layers if hovering, except on first hover
|
|
@@ -33111,7 +34402,7 @@
|
|
|
33111
34402
|
|
|
33112
34403
|
function isFrameMapLayer(lyr) {
|
|
33113
34404
|
var dataset = lyr && lyr.gui && lyr.gui.source && lyr.gui.source.dataset;
|
|
33114
|
-
return !!dataset && internal.
|
|
34405
|
+
return !!dataset && internal.isFrameComponentLayer(lyr, dataset);
|
|
33115
34406
|
}
|
|
33116
34407
|
|
|
33117
34408
|
function getDrawableContentLayers() {
|
|
@@ -33121,11 +34412,18 @@
|
|
|
33121
34412
|
});
|
|
33122
34413
|
}
|
|
33123
34414
|
|
|
33124
|
-
|
|
33125
|
-
|
|
33126
|
-
|
|
33127
|
-
|
|
34415
|
+
// The active frame's furniture, plus any visible standalone furniture layers
|
|
34416
|
+
// (e.g. from -scalebar run before a frame was created)
|
|
34417
|
+
function getDrawableFurnitureLayers() {
|
|
34418
|
+
var frame = isPreviewView() ? internal.getActiveFrame(model) : null;
|
|
34419
|
+
if (!frame) return [];
|
|
34420
|
+
var layers = internal.getFrameFurnitureLayers(frame.dataset);
|
|
34421
|
+
getVisibleMapLayers().forEach(function(lyr) {
|
|
34422
|
+
if (internal.layerIsFurniture(lyr) && !layers.includes(lyr) && !lyr.hidden) {
|
|
34423
|
+
layers.push(lyr);
|
|
34424
|
+
}
|
|
33128
34425
|
});
|
|
34426
|
+
return layers;
|
|
33129
34427
|
}
|
|
33130
34428
|
|
|
33131
34429
|
function updateLayerStyles(layers) {
|
|
@@ -33177,7 +34475,6 @@
|
|
|
33177
34475
|
var layersMayHaveChanged = action != 'nav'; // !action;
|
|
33178
34476
|
var fullBounds;
|
|
33179
34477
|
var contentLayers = getDrawableContentLayers();
|
|
33180
|
-
// var furnitureLayers = getDrawableFurnitureLayers();
|
|
33181
34478
|
if (!(_ext.width() > 0 && _ext.height() > 0)) {
|
|
33182
34479
|
// TODO: track down source of these errors
|
|
33183
34480
|
console.error("Collapsed map container, unable to draw.");
|
|
@@ -33218,8 +34515,7 @@
|
|
|
33218
34515
|
}
|
|
33219
34516
|
_renderer.drawOverlayLayers(_overlayLayers, action);
|
|
33220
34517
|
|
|
33221
|
-
|
|
33222
|
-
// _renderer.drawFurnitureLayers(furnitureLayers, action);
|
|
34518
|
+
_renderer.drawFurnitureLayers(getDrawableFurnitureLayers(), action);
|
|
33223
34519
|
// The action says how much was redrawn, which a listener rebuilding its own
|
|
33224
34520
|
// DOM overlays needs: a 'hover' draw leaves the SVG markup and its
|
|
33225
34521
|
// transforms alone, so anything anchored to them is still good.
|
|
@@ -33808,10 +35104,7 @@
|
|
|
33808
35104
|
return;
|
|
33809
35105
|
}
|
|
33810
35106
|
if (!getSourceLayer()) {
|
|
33811
|
-
showPopupAlert(
|
|
33812
|
-
'Add one or more layers before creating a map frame.',
|
|
33813
|
-
'Map frame'
|
|
33814
|
-
);
|
|
35107
|
+
showPopupAlert(getNoSourceMessage(), 'Map frame');
|
|
33815
35108
|
return;
|
|
33816
35109
|
}
|
|
33817
35110
|
var popup = showPopupAlert('', 'Add map frame', {classname: 'frame-create-box'});
|
|
@@ -33956,7 +35249,7 @@
|
|
|
33956
35249
|
function createFromVisibleLayers(width, aspect, margin) {
|
|
33957
35250
|
var entries = getCompositionEntries();
|
|
33958
35251
|
if (!entries.length) {
|
|
33959
|
-
showPopupAlert(
|
|
35252
|
+
showPopupAlert(getNoSourceMessage(), 'Map frame');
|
|
33960
35253
|
return;
|
|
33961
35254
|
}
|
|
33962
35255
|
var ids = entries.map(function(o) {
|
|
@@ -33979,6 +35272,15 @@
|
|
|
33979
35272
|
});
|
|
33980
35273
|
}
|
|
33981
35274
|
|
|
35275
|
+
function getNoSourceMessage() {
|
|
35276
|
+
var active = gui.model.getActiveLayer();
|
|
35277
|
+
if (active && active.layer && !internal.layerHasGeometry(active.layer)) {
|
|
35278
|
+
return 'A map frame is fitted to layers with shapes, and this layer ' +
|
|
35279
|
+
'contains only attribute data. Select or show a map layer to create a frame.';
|
|
35280
|
+
}
|
|
35281
|
+
return 'Add one or more layers before creating a map frame.';
|
|
35282
|
+
}
|
|
35283
|
+
|
|
33982
35284
|
function getFrameTarget() {
|
|
33983
35285
|
return internal.getActiveFrame(gui.model);
|
|
33984
35286
|
}
|
|
@@ -33998,10 +35300,11 @@
|
|
|
33998
35300
|
return entries.length ? entries[0].layer : null;
|
|
33999
35301
|
}
|
|
34000
35302
|
|
|
35303
|
+
// A frame is fitted to shapes, so a data-only layer on show doesn't count
|
|
34001
35304
|
function getCompositionEntries() {
|
|
34002
35305
|
var layers = gui.map.getCompositionLayers();
|
|
34003
35306
|
return gui.model.getLayers().filter(function(o) {
|
|
34004
|
-
return layers.includes(o.layer);
|
|
35307
|
+
return layers.includes(o.layer) && internal.layerHasGeometry(o.layer);
|
|
34005
35308
|
});
|
|
34006
35309
|
}
|
|
34007
35310
|
|
|
@@ -34038,16 +35341,6 @@
|
|
|
34038
35341
|
.text(label).on('click', action);
|
|
34039
35342
|
}
|
|
34040
35343
|
|
|
34041
|
-
// The "?" the rest of the app uses for field help (see .tip-button in
|
|
34042
|
-
// elements.css and the static ones in index.html). The bubble is white-space:
|
|
34043
|
-
// pre, so the line breaks in the text are the ones it gets.
|
|
34044
|
-
function makeFieldTip(parent, text) {
|
|
34045
|
-
var btn = El('div').addClass('tip-button').appendTo(parent).text('?');
|
|
34046
|
-
var anchor = El('div').addClass('tip-anchor').appendTo(btn);
|
|
34047
|
-
El('div').addClass('tip').appendTo(anchor).text(text);
|
|
34048
|
-
return btn;
|
|
34049
|
-
}
|
|
34050
|
-
|
|
34051
35344
|
function getUnitFactor$1(units) {
|
|
34052
35345
|
return units == 'in' ? 72 : units == 'cm' ? 28.3465 : 1;
|
|
34053
35346
|
}
|
|
@@ -34067,7 +35360,7 @@
|
|
|
34067
35360
|
function FrameProperties(gui) {
|
|
34068
35361
|
var target, form, widthInput, heightInput, unitsSelect, aspectValue;
|
|
34069
35362
|
var backgroundControl, neatlineControl, neatlineWidthInput;
|
|
34070
|
-
var boundsValue, crsValue;
|
|
35363
|
+
var boundsValue, crsValue, scalebarButton;
|
|
34071
35364
|
|
|
34072
35365
|
gui.frameProperties = this;
|
|
34073
35366
|
|
|
@@ -34139,6 +35432,14 @@
|
|
|
34139
35432
|
.appendTo(appearance);
|
|
34140
35433
|
makeActionButton(clearRow, 'Clear appearance', clearFrameStyle);
|
|
34141
35434
|
|
|
35435
|
+
var elements = makePanelSection(form, 'Map elements');
|
|
35436
|
+
var elementsRow = El('div')
|
|
35437
|
+
.addClass('label-style-row label-panel-button-row')
|
|
35438
|
+
.appendTo(elements);
|
|
35439
|
+
scalebarButton = makeActionButton(elementsRow, '', function() {
|
|
35440
|
+
if (gui.scalebarProperties) gui.scalebarProperties.open(target);
|
|
35441
|
+
}).addClass('frame-scalebar-btn');
|
|
35442
|
+
|
|
34142
35443
|
var details = makePanelSection(form, 'Details');
|
|
34143
35444
|
boundsValue = makeReadOnlyRow(details, 'Bounds');
|
|
34144
35445
|
crsValue = makeReadOnlyRow(details, 'CRS');
|
|
@@ -34156,11 +35457,13 @@
|
|
|
34156
35457
|
unitsSelect.node().value = units;
|
|
34157
35458
|
aspectValue.text(getAspectText(frame));
|
|
34158
35459
|
backgroundControl.showColor(rec.fill || '');
|
|
34159
|
-
backgroundControl.opacity.node().value =
|
|
35460
|
+
backgroundControl.opacity.node().value = formatColorOpacityPct(rec['fill-opacity'], !!rec.fill);
|
|
34160
35461
|
neatlineControl.showColor(rec.stroke || '');
|
|
34161
|
-
neatlineControl.opacity.node().value =
|
|
35462
|
+
neatlineControl.opacity.node().value = formatColorOpacityPct(rec['stroke-opacity'], !!rec.stroke);
|
|
34162
35463
|
neatlineWidthInput.node().value =
|
|
34163
35464
|
rec['stroke-width'] === undefined ? '' : rec['stroke-width'];
|
|
35465
|
+
scalebarButton.text(internal.findFrameFurnitureLayer(target.dataset, 'scalebar') ?
|
|
35466
|
+
'Edit scale bar' : 'Add scale bar');
|
|
34164
35467
|
boundsValue.text(frame.bbox.map(formatCoordinate).join(', '));
|
|
34165
35468
|
crsValue.text(info.proj4 || '[unknown]');
|
|
34166
35469
|
}
|
|
@@ -34284,15 +35587,6 @@
|
|
|
34284
35587
|
return String(Number(value.toPrecision(12)));
|
|
34285
35588
|
}
|
|
34286
35589
|
|
|
34287
|
-
// A colour with no opacity of its own is drawn opaque, and says so; with no
|
|
34288
|
-
// colour either, there is nothing for an opacity to apply to.
|
|
34289
|
-
function formatOpacity(value, color) {
|
|
34290
|
-
if (value === undefined || value === null || value === '') {
|
|
34291
|
-
return color ? '100%' : '';
|
|
34292
|
-
}
|
|
34293
|
-
return String(Math.round(Number(value) * 100)) + '%';
|
|
34294
|
-
}
|
|
34295
|
-
|
|
34296
35590
|
// A fixed ratio holds when the frame is rescaled; one taken from the extent
|
|
34297
35591
|
// changes whenever the extent does. That difference is the reason to show the
|
|
34298
35592
|
// ratio at all, so it is said rather than implied.
|
|
@@ -34306,6 +35600,434 @@
|
|
|
34306
35600
|
return formatFrameAspectRatio(ratio) + ' (from extent)';
|
|
34307
35601
|
}
|
|
34308
35602
|
|
|
35603
|
+
// Record fields of a scalebar, in the order they are written as options
|
|
35604
|
+
var scalebarOptions = ['units', 'style', 'position', 'label_position',
|
|
35605
|
+
'font_size', 'font_family', 'font_style', 'font_weight', 'color', 'bar_width',
|
|
35606
|
+
'tic_length', 'label_offset', 'margin'];
|
|
35607
|
+
|
|
35608
|
+
// Returns a -scalebar command that recreates a scalebar with the given
|
|
35609
|
+
// settings (a scalebar data record, with any edits applied). -scalebar
|
|
35610
|
+
// replaces the frame's scalebar, so every setting has to be written.
|
|
35611
|
+
function getScalebarCommand(settings) {
|
|
35612
|
+
var parts = ['-scalebar'];
|
|
35613
|
+
var d = settings || {};
|
|
35614
|
+
if (isSet(d.label)) {
|
|
35615
|
+
parts.push(quoteCommandValue(d.label));
|
|
35616
|
+
}
|
|
35617
|
+
scalebarOptions.forEach(function(k) {
|
|
35618
|
+
if (isSet(d[k])) {
|
|
35619
|
+
parts.push(k.replace(/_/g, '-') + '=' + formatValue$1(d[k]));
|
|
35620
|
+
}
|
|
35621
|
+
});
|
|
35622
|
+
if (d.dual_units) {
|
|
35623
|
+
parts.push('dual-units');
|
|
35624
|
+
}
|
|
35625
|
+
return parts.join(' ');
|
|
35626
|
+
}
|
|
35627
|
+
|
|
35628
|
+
function isSet(val) {
|
|
35629
|
+
return val !== undefined && val !== null && val !== '';
|
|
35630
|
+
}
|
|
35631
|
+
|
|
35632
|
+
function formatValue$1(val) {
|
|
35633
|
+
var str = String(val);
|
|
35634
|
+
return /^[\w.#-]+$/.test(str) ? str : quoteCommandValue(str);
|
|
35635
|
+
}
|
|
35636
|
+
|
|
35637
|
+
var positionOptions = [
|
|
35638
|
+
['top-left', 'Top left'],
|
|
35639
|
+
['top-right', 'Top right'],
|
|
35640
|
+
['bottom-left', 'Bottom left'],
|
|
35641
|
+
['bottom-right', 'Bottom right']
|
|
35642
|
+
];
|
|
35643
|
+
|
|
35644
|
+
// What the scalebar is drawn in when it has no color of its own
|
|
35645
|
+
var defaultColor = '#000000';
|
|
35646
|
+
|
|
35647
|
+
// Settings of the frame's scalebar. Every edit sends a complete -scalebar
|
|
35648
|
+
// command, which replaces the scalebar, so edits are undoable and replayable
|
|
35649
|
+
// like any other command.
|
|
35650
|
+
function ScalebarProperties(gui) {
|
|
35651
|
+
var popup, form, note;
|
|
35652
|
+
var labelInput, unitsSelect, styleSelect, dualUnitsBox, positionSelect,
|
|
35653
|
+
labelPositionSelect, fontSizeInput, barWidthInput, marginInput,
|
|
35654
|
+
fontSelect, fontStyleSelect, colorChit, colorInput, colorPicker;
|
|
35655
|
+
|
|
35656
|
+
gui.scalebarProperties = this;
|
|
35657
|
+
|
|
35658
|
+
// The panel doesn't block the map, so anything can change the scalebar or
|
|
35659
|
+
// the frame while it is open: an undo, a frame edit, a console command.
|
|
35660
|
+
gui.model.on('update', function(e) {
|
|
35661
|
+
var flags = e.flags || {};
|
|
35662
|
+
if (!popup || flags.simplify_amount || flags.redraw_only) return;
|
|
35663
|
+
if (getScalebarRecord()) {
|
|
35664
|
+
updateControls();
|
|
35665
|
+
} else {
|
|
35666
|
+
closePanel();
|
|
35667
|
+
}
|
|
35668
|
+
});
|
|
35669
|
+
|
|
35670
|
+
// Adds a scalebar to the frame if it doesn't have one
|
|
35671
|
+
this.open = function(frameTarget) {
|
|
35672
|
+
var frame = frameTarget || internal.getActiveFrame(gui.model);
|
|
35673
|
+
if (!frame) return;
|
|
35674
|
+
if (getScalebarRecord()) {
|
|
35675
|
+
showPanel();
|
|
35676
|
+
} else {
|
|
35677
|
+
runCommand('-scalebar', 'Add scale bar', showPanel);
|
|
35678
|
+
}
|
|
35679
|
+
};
|
|
35680
|
+
|
|
35681
|
+
this.isOpen = function() {
|
|
35682
|
+
return !!popup;
|
|
35683
|
+
};
|
|
35684
|
+
|
|
35685
|
+
// The map stays live, so the panel is placed where it is least likely to
|
|
35686
|
+
// hide the scalebar: at the top of the map, on the side away from it
|
|
35687
|
+
function showPanel() {
|
|
35688
|
+
var d = getScalebarRecord();
|
|
35689
|
+
if (!d) return;
|
|
35690
|
+
closePanel();
|
|
35691
|
+
popup = showPopupAlert('', 'Scale bar', {
|
|
35692
|
+
non_blocking: true,
|
|
35693
|
+
classname: 'scalebar-properties-box'
|
|
35694
|
+
});
|
|
35695
|
+
popup.onClose(function() {
|
|
35696
|
+
popup = form = null;
|
|
35697
|
+
});
|
|
35698
|
+
popup.container().addClass('scalebar-properties-popup');
|
|
35699
|
+
form = El('div')
|
|
35700
|
+
.addClass('label-style-panel scalebar-properties-form')
|
|
35701
|
+
.appendTo(popup.container());
|
|
35702
|
+
initForm();
|
|
35703
|
+
updateControls();
|
|
35704
|
+
placePanel(/right/.test(d.position || '') ? 'left' : 'right');
|
|
35705
|
+
}
|
|
35706
|
+
|
|
35707
|
+
function closePanel() {
|
|
35708
|
+
if (popup) popup.close();
|
|
35709
|
+
}
|
|
35710
|
+
|
|
35711
|
+
function placePanel(side) {
|
|
35712
|
+
var box = popup.container().node().parentNode;
|
|
35713
|
+
var map = gui.container.findChild('.map-layers').node().getBoundingClientRect();
|
|
35714
|
+
// clears the zoom buttons on the right edge of the map
|
|
35715
|
+
var rightInset = 58;
|
|
35716
|
+
var leftInset = 16;
|
|
35717
|
+
box.style.position = 'absolute';
|
|
35718
|
+
box.style.marginTop = '0';
|
|
35719
|
+
box.style.top = Math.round(map.top + 12) + 'px';
|
|
35720
|
+
if (side == 'left') {
|
|
35721
|
+
box.style.left = Math.round(map.left + leftInset) + 'px';
|
|
35722
|
+
} else {
|
|
35723
|
+
box.style.right = Math.round(window.innerWidth - map.right + rightInset) + 'px';
|
|
35724
|
+
}
|
|
35725
|
+
}
|
|
35726
|
+
|
|
35727
|
+
function initForm() {
|
|
35728
|
+
var row;
|
|
35729
|
+
note = El('div').addClass('scalebar-properties-note').appendTo(form).hide();
|
|
35730
|
+
|
|
35731
|
+
row = makeRow(form);
|
|
35732
|
+
labelInput = makeInput(makeCell(row, 'Distance'), 'scalebar-label-input')
|
|
35733
|
+
.attr('placeholder', 'Automatic')
|
|
35734
|
+
.on('change', function() {
|
|
35735
|
+
update({label: labelInput.node().value.trim()});
|
|
35736
|
+
});
|
|
35737
|
+
unitsSelect = makeSelect(makeCell(row, 'Units'), 'scalebar-units-select',
|
|
35738
|
+
[['miles', 'Miles'], ['km', 'Kilometers']])
|
|
35739
|
+
.on('change', function() {
|
|
35740
|
+
var changes = {units: unitsSelect.node().value};
|
|
35741
|
+
var bare = getBareDistance(getScalebarRecord().label);
|
|
35742
|
+
if (bare) changes.label = bare;
|
|
35743
|
+
update(changes);
|
|
35744
|
+
});
|
|
35745
|
+
|
|
35746
|
+
row = makeRow(form);
|
|
35747
|
+
styleSelect = makeSelect(makeCell(row, 'Style'), 'scalebar-style-select',
|
|
35748
|
+
[['a', 'Bar'], ['b', 'Ticks']])
|
|
35749
|
+
.on('change', function() {
|
|
35750
|
+
update({style: styleSelect.node().value});
|
|
35751
|
+
});
|
|
35752
|
+
dualUnitsBox = makeCheckbox(row, 'Metric + imperial', 'scalebar-dual-units')
|
|
35753
|
+
.on('change', function() {
|
|
35754
|
+
update({dual_units: dualUnitsBox.node().checked});
|
|
35755
|
+
});
|
|
35756
|
+
|
|
35757
|
+
row = makeRow(form);
|
|
35758
|
+
positionSelect = makeSelect(makeCell(row, 'Position'), 'scalebar-position-select',
|
|
35759
|
+
positionOptions)
|
|
35760
|
+
.on('change', function() {
|
|
35761
|
+
update({position: positionSelect.node().value});
|
|
35762
|
+
});
|
|
35763
|
+
labelPositionSelect = makeSelect(makeCell(row, 'Labels'),
|
|
35764
|
+
'scalebar-label-position-select', [['top', 'Above'], ['bottom', 'Below']])
|
|
35765
|
+
.on('change', function() {
|
|
35766
|
+
update({label_position: labelPositionSelect.node().value});
|
|
35767
|
+
});
|
|
35768
|
+
|
|
35769
|
+
var appearance = makePanelSection(form, 'Appearance');
|
|
35770
|
+
|
|
35771
|
+
// Font and face menus, as in the label panel
|
|
35772
|
+
row = makeRow(appearance);
|
|
35773
|
+
fontSelect = El('select').attr('title', 'Font').addClass('scalebar-font-select')
|
|
35774
|
+
.appendTo(makeCell(row, 'Font'))
|
|
35775
|
+
.on('change', function() {
|
|
35776
|
+
if (fontSelect.node().value) applyFont(fontSelect.node().value);
|
|
35777
|
+
});
|
|
35778
|
+
renderFontOptions();
|
|
35779
|
+
fontStyleSelect = El('select').attr('title', 'Font style')
|
|
35780
|
+
.addClass('scalebar-font-style-select')
|
|
35781
|
+
.appendTo(makeCell(row, 'Font style'))
|
|
35782
|
+
.on('change', function() {
|
|
35783
|
+
if (fontStyleSelect.node().value) applyFontStyle(fontStyleSelect.node().value);
|
|
35784
|
+
});
|
|
35785
|
+
|
|
35786
|
+
// The picker lines up with the right edge of its parent, so the row is its
|
|
35787
|
+
// parent rather than the narrow color cell
|
|
35788
|
+
row = makeRow(appearance);
|
|
35789
|
+
var colorCell = El('div').addClass('frame-input-cell scalebar-color-cell').appendTo(row);
|
|
35790
|
+
El('span').appendTo(colorCell).text('Color');
|
|
35791
|
+
colorChit = El('div').addClass('label-color-chit').attr('role', 'button')
|
|
35792
|
+
.on('click', function() { colorPicker.toggle(); });
|
|
35793
|
+
colorInput = El('input').attr('type', 'text').attr('title', 'Scale bar color')
|
|
35794
|
+
.on('change', function() {
|
|
35795
|
+
var color = colorInput.node().value.trim();
|
|
35796
|
+
if (!color) {
|
|
35797
|
+
updateControls();
|
|
35798
|
+
return;
|
|
35799
|
+
}
|
|
35800
|
+
if (isHexColor(color)) colorPicker.setColor(color);
|
|
35801
|
+
update({color: color});
|
|
35802
|
+
});
|
|
35803
|
+
makeColorField(colorCell, colorChit, colorInput);
|
|
35804
|
+
colorPicker = new ColorPicker(row, {
|
|
35805
|
+
onPreview: showColor,
|
|
35806
|
+
onChange: function(hex) {
|
|
35807
|
+
showColor(hex);
|
|
35808
|
+
update({color: hex});
|
|
35809
|
+
}
|
|
35810
|
+
});
|
|
35811
|
+
fontSizeInput = makeNumberInput(makeCell(row, 'Font size'), 'font_size',
|
|
35812
|
+
'scalebar-font-size');
|
|
35813
|
+
|
|
35814
|
+
row = makeRow(appearance);
|
|
35815
|
+
barWidthInput = makeNumberInput(makeCell(row, 'Bar width'), 'bar_width',
|
|
35816
|
+
'scalebar-bar-width');
|
|
35817
|
+
marginInput = makeNumberInput(makeCell(row, 'Inset'), 'margin',
|
|
35818
|
+
'scalebar-margin')
|
|
35819
|
+
.attr('title', 'Distance from the edges of the frame, in pixels');
|
|
35820
|
+
|
|
35821
|
+
row = El('div').addClass('label-style-row label-panel-button-row').appendTo(form);
|
|
35822
|
+
makePanelActionButton(row, 'Remove scale bar', function() {
|
|
35823
|
+
runCommand('-scalebar remove', 'Remove scale bar', function() {
|
|
35824
|
+
if (popup) popup.close();
|
|
35825
|
+
});
|
|
35826
|
+
});
|
|
35827
|
+
}
|
|
35828
|
+
|
|
35829
|
+
function updateControls() {
|
|
35830
|
+
var d = getScalebarRecord();
|
|
35831
|
+
var problem;
|
|
35832
|
+
if (!d || !form) return;
|
|
35833
|
+
labelInput.node().value = d.label || '';
|
|
35834
|
+
setSelectValue(unitsSelect, getUnitsMenuValue(d.units ||
|
|
35835
|
+
internal.parseScalebarUnits(d.label || '')));
|
|
35836
|
+
setSelectValue(styleSelect, d.style == 'b' || d.style == 'B' ||
|
|
35837
|
+
!d.style && d.dual_units ? 'b' : 'a');
|
|
35838
|
+
dualUnitsBox.node().checked = !!d.dual_units;
|
|
35839
|
+
setSelectValue(positionSelect, d.position || 'top-left');
|
|
35840
|
+
setSelectValue(labelPositionSelect, d.label_position || 'top');
|
|
35841
|
+
fontSizeInput.node().value = formatValue(d.font_size);
|
|
35842
|
+
barWidthInput.node().value = formatValue(d.bar_width);
|
|
35843
|
+
marginInput.node().value = formatValue(d.margin);
|
|
35844
|
+
updateFontControls(d);
|
|
35845
|
+
showColor(d.color || defaultColor);
|
|
35846
|
+
if (isHexColor(d.color || defaultColor)) colorPicker.setColor(d.color || defaultColor);
|
|
35847
|
+
problem = getFrameProblem();
|
|
35848
|
+
note.text(problem ? 'The scale bar is hidden: ' + problem + '.' : '');
|
|
35849
|
+
if (problem) note.show(); else note.hide();
|
|
35850
|
+
}
|
|
35851
|
+
|
|
35852
|
+
function showColor(color) {
|
|
35853
|
+
colorInput.node().value = color || '';
|
|
35854
|
+
colorChit.css('background-color', color || 'transparent');
|
|
35855
|
+
}
|
|
35856
|
+
|
|
35857
|
+
function renderFontOptions() {
|
|
35858
|
+
getInstalledFonts().forEach(function(group) {
|
|
35859
|
+
var optgroup = El('optgroup').attr('label', group.name).appendTo(fontSelect);
|
|
35860
|
+
group.fonts.forEach(function(fontName) {
|
|
35861
|
+
El('option').attr('value', fontName).appendTo(optgroup).text(fontName);
|
|
35862
|
+
});
|
|
35863
|
+
});
|
|
35864
|
+
}
|
|
35865
|
+
|
|
35866
|
+
// A scalebar with no font of its own is shown in the font it is drawn in
|
|
35867
|
+
function getShownFont(d) {
|
|
35868
|
+
return d.font_family || getDefaultFontName();
|
|
35869
|
+
}
|
|
35870
|
+
|
|
35871
|
+
function updateFontControls(d) {
|
|
35872
|
+
var font = getShownFont(d);
|
|
35873
|
+
var shown = font ? getNearestFontStyleVariant(font, d.font_style, d.font_weight) : null;
|
|
35874
|
+
if (font) {
|
|
35875
|
+
setSelectValue(fontSelect, font);
|
|
35876
|
+
} else {
|
|
35877
|
+
fontSelect.node().selectedIndex = -1;
|
|
35878
|
+
}
|
|
35879
|
+
fontStyleSelect.empty();
|
|
35880
|
+
getFontStyleVariants(font).forEach(function(variant) {
|
|
35881
|
+
El('option').attr('value', variant.value).appendTo(fontStyleSelect).text(variant.label);
|
|
35882
|
+
});
|
|
35883
|
+
fontStyleSelect.node().disabled = !font;
|
|
35884
|
+
if (shown) {
|
|
35885
|
+
fontStyleSelect.node().value = shown.value;
|
|
35886
|
+
} else {
|
|
35887
|
+
fontStyleSelect.node().selectedIndex = -1;
|
|
35888
|
+
}
|
|
35889
|
+
}
|
|
35890
|
+
|
|
35891
|
+
// Keeps the current face, or the nearest one the new font has
|
|
35892
|
+
function applyFont(fontName) {
|
|
35893
|
+
var d = getScalebarRecord();
|
|
35894
|
+
var changes = {font_family: fontName};
|
|
35895
|
+
var variant = getNearestFontStyleVariant(fontName, d.font_style, d.font_weight);
|
|
35896
|
+
if (variant) Object.assign(changes, getFontStyleValues(variant));
|
|
35897
|
+
update(changes);
|
|
35898
|
+
}
|
|
35899
|
+
|
|
35900
|
+
// A face belongs to a font, so a scalebar given one names the font it is
|
|
35901
|
+
// drawn in
|
|
35902
|
+
function applyFontStyle(value) {
|
|
35903
|
+
var parts = value.split('|');
|
|
35904
|
+
var changes = getFontStyleValues({style: parts[0], weight: parts[1]});
|
|
35905
|
+
var d = getScalebarRecord();
|
|
35906
|
+
if (!d.font_family && getDefaultFontName()) {
|
|
35907
|
+
changes.font_family = getDefaultFontName();
|
|
35908
|
+
}
|
|
35909
|
+
update(changes);
|
|
35910
|
+
}
|
|
35911
|
+
|
|
35912
|
+
// Regular is stored as no face at all
|
|
35913
|
+
function getFontStyleValues(variant) {
|
|
35914
|
+
if (variantIsRegular(variant)) {
|
|
35915
|
+
return {font_style: '', font_weight: ''};
|
|
35916
|
+
}
|
|
35917
|
+
return {font_style: variant.style, font_weight: variant.weight};
|
|
35918
|
+
}
|
|
35919
|
+
|
|
35920
|
+
// Applies edits to the current settings and replaces the scalebar. The
|
|
35921
|
+
// units menu always applies, so it is written even when it shows the default.
|
|
35922
|
+
function update(changes) {
|
|
35923
|
+
var d = Object.assign({}, getScalebarRecord(), changes);
|
|
35924
|
+
if (changes.style == 'a') d.dual_units = false;
|
|
35925
|
+
if (changes.dual_units) d.style = 'b';
|
|
35926
|
+
if (!d.units) d.units = unitsSelect.node().value;
|
|
35927
|
+
runCommand(getScalebarCommand(d), 'Update scale bar', updateControls);
|
|
35928
|
+
}
|
|
35929
|
+
|
|
35930
|
+
function makeNumberInput(cell, field, className) {
|
|
35931
|
+
var input = makeInput(cell, className).attr('placeholder', 'auto');
|
|
35932
|
+
input.on('change', function() {
|
|
35933
|
+
var str = input.node().value.trim();
|
|
35934
|
+
var o = {};
|
|
35935
|
+
if (str !== '' && !(Number(str) >= 0)) {
|
|
35936
|
+
updateControls();
|
|
35937
|
+
return;
|
|
35938
|
+
}
|
|
35939
|
+
o[field] = str === '' ? '' : Number(str);
|
|
35940
|
+
update(o);
|
|
35941
|
+
});
|
|
35942
|
+
return input;
|
|
35943
|
+
}
|
|
35944
|
+
|
|
35945
|
+
function getFrameProblem() {
|
|
35946
|
+
var frame = internal.getActiveFrame(gui.model);
|
|
35947
|
+
var crs, data;
|
|
35948
|
+
if (!frame) return 'there is no map frame';
|
|
35949
|
+
crs = frame.layer.gui && frame.layer.gui.dynamic_crs ||
|
|
35950
|
+
internal.getDatasetCRS(frame.dataset);
|
|
35951
|
+
data = internal.getFrameLayerData(frame.layer, frame.dataset.arcs, crs);
|
|
35952
|
+
return internal.getFurnitureFrameProblem(data);
|
|
35953
|
+
}
|
|
35954
|
+
|
|
35955
|
+
function getScalebarRecord() {
|
|
35956
|
+
var frame = internal.getActiveFrame(gui.model);
|
|
35957
|
+
var lyr = frame && internal.findFrameFurnitureLayer(frame.dataset, 'scalebar');
|
|
35958
|
+
return lyr ? lyr.data.getReadOnlyRecordAt(0) : null;
|
|
35959
|
+
}
|
|
35960
|
+
|
|
35961
|
+
function runCommand(cmd, title, done) {
|
|
35962
|
+
runGuiEditCommand(gui, cmd, {
|
|
35963
|
+
title: title,
|
|
35964
|
+
onSuccess: done,
|
|
35965
|
+
onError: updateControls
|
|
35966
|
+
});
|
|
35967
|
+
}
|
|
35968
|
+
}
|
|
35969
|
+
|
|
35970
|
+
function makeRow(parent, className) {
|
|
35971
|
+
return El('div').addClass('label-style-row scalebar-row')
|
|
35972
|
+
.addClass(className || 'scalebar-two-col').appendTo(parent);
|
|
35973
|
+
}
|
|
35974
|
+
|
|
35975
|
+
function makeCell(row, label) {
|
|
35976
|
+
var cell = El('label').addClass('frame-input-cell').appendTo(row);
|
|
35977
|
+
El('span').appendTo(cell).text(label);
|
|
35978
|
+
return cell;
|
|
35979
|
+
}
|
|
35980
|
+
|
|
35981
|
+
function makeInput(parent, className) {
|
|
35982
|
+
return El('input').attr('type', 'text').addClass(className).appendTo(parent);
|
|
35983
|
+
}
|
|
35984
|
+
|
|
35985
|
+
function makeSelect(parent, className, options) {
|
|
35986
|
+
var select = El('select').addClass(className).appendTo(parent);
|
|
35987
|
+
options.forEach(function(o) {
|
|
35988
|
+
El('option').attr('value', o[0]).appendTo(select).text(o[1]);
|
|
35989
|
+
});
|
|
35990
|
+
return select;
|
|
35991
|
+
}
|
|
35992
|
+
|
|
35993
|
+
function makeCheckbox(parent, label, className) {
|
|
35994
|
+
var box = El('input').attr('type', 'checkbox').addClass(className);
|
|
35995
|
+
var wrapper = El('label').addClass('scalebar-checkbox').appendTo(parent);
|
|
35996
|
+
box.appendTo(wrapper);
|
|
35997
|
+
El('span').appendTo(wrapper).text(label);
|
|
35998
|
+
return box;
|
|
35999
|
+
}
|
|
36000
|
+
|
|
36001
|
+
// Shows a value that came from the command line even if the menu lacks it
|
|
36002
|
+
function setSelectValue(select, value) {
|
|
36003
|
+
var node = select.node();
|
|
36004
|
+
var found = Array.prototype.some.call(node.options, function(o) {
|
|
36005
|
+
return o.value == value;
|
|
36006
|
+
});
|
|
36007
|
+
if (!found) {
|
|
36008
|
+
El('option').attr('value', value).appendTo(select).text(value);
|
|
36009
|
+
}
|
|
36010
|
+
node.value = value;
|
|
36011
|
+
}
|
|
36012
|
+
|
|
36013
|
+
// The number of a single-distance label, e.g. "100" from "100 km", so the
|
|
36014
|
+
// units menu can apply to it
|
|
36015
|
+
function getBareDistance(label) {
|
|
36016
|
+
var match = /^([\d\s.,/]*\d)\s*[^\d\s.,/]+$/.exec(String(label || '').trim());
|
|
36017
|
+
return match ? match[1].trim() : '';
|
|
36018
|
+
}
|
|
36019
|
+
|
|
36020
|
+
function getUnitsMenuValue(units) {
|
|
36021
|
+
var parsed = internal.parseLabelUnits(units);
|
|
36022
|
+
return parsed == 'mile' || !parsed ? 'miles' :
|
|
36023
|
+
parsed == 'm' ? 'meters' :
|
|
36024
|
+
parsed == 'ft' ? 'feet' : 'km';
|
|
36025
|
+
}
|
|
36026
|
+
|
|
36027
|
+
function formatValue(val) {
|
|
36028
|
+
return val === undefined || val === null ? '' : String(val);
|
|
36029
|
+
}
|
|
36030
|
+
|
|
34309
36031
|
// This is a new way to handle compatibility problems between
|
|
34310
36032
|
// interactive editing modes and other interface modes
|
|
34311
36033
|
// (by default, interactive modes stay on when, e.g., the user clicks
|
|
@@ -35108,6 +36830,7 @@
|
|
|
35108
36830
|
new PreviewMode(gui);
|
|
35109
36831
|
new FrameResizeTool(gui);
|
|
35110
36832
|
new FrameProperties(gui);
|
|
36833
|
+
new ScalebarProperties(gui);
|
|
35111
36834
|
gui.editToolbar = new EditToolbar(gui);
|
|
35112
36835
|
gui.labelTool = new LabelTool(gui);
|
|
35113
36836
|
gui.layerStyleTool = new LayerStyleTool(gui);
|