mapshaper 0.7.69 → 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 +925 -736
- package/package.json +1 -1
- package/www/mapshaper-gui.js +1152 -141
- package/www/mapshaper.js +925 -736
- package/www/page.css +19 -3
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
|
|
|
@@ -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,
|
|
@@ -11518,9 +11518,12 @@
|
|
|
11518
11518
|
}
|
|
11519
11519
|
|
|
11520
11520
|
if (e.deleteVertex || e.deletePoint || copyable || e.deleteFeature ||
|
|
11521
|
-
e.flipLabel) {
|
|
11521
|
+
e.flipLabel || e.boldText) {
|
|
11522
11522
|
|
|
11523
11523
|
addMenuLabel('actions');
|
|
11524
|
+
if (e.boldText) {
|
|
11525
|
+
addMenuItem(e.boldText.label, e.boldText.run);
|
|
11526
|
+
}
|
|
11524
11527
|
if (e.deleteVertex) {
|
|
11525
11528
|
addMenuItem('delete vertex', e.deleteVertex);
|
|
11526
11529
|
}
|
|
@@ -12904,6 +12907,11 @@
|
|
|
12904
12907
|
if (cmd.name == 'style' || cmd.name == 'svg-style') {
|
|
12905
12908
|
return getStyleCullInfo(opts, isStyleProperty);
|
|
12906
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
|
+
}
|
|
12907
12915
|
if (cmd.name == 'classify') {
|
|
12908
12916
|
return getClassifyCullInfo(opts);
|
|
12909
12917
|
}
|
|
@@ -12923,6 +12931,16 @@
|
|
|
12923
12931
|
};
|
|
12924
12932
|
}
|
|
12925
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
|
+
|
|
12926
12944
|
function getStyleFields$2(opts, isStyleProperty) {
|
|
12927
12945
|
var fields = [];
|
|
12928
12946
|
Object.keys(opts).forEach(function(name) {
|
|
@@ -13921,6 +13939,11 @@
|
|
|
13921
13939
|
var SOFT_BREAK = '<wbr>';
|
|
13922
13940
|
var SOFT_BREAK_RXP = /<wbr>/gi;
|
|
13923
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
|
+
|
|
13924
13947
|
// How a label lays out its line breaks, which is not the same for the two kinds.
|
|
13925
13948
|
// An anchored label stacks its lines with <tspan>. A path label joins them with
|
|
13926
13949
|
// a space, because a <tspan> inside a <textPath> advances along the curve
|
|
@@ -13956,6 +13979,66 @@
|
|
|
13956
13979
|
return {text: text, breaks: breaks};
|
|
13957
13980
|
}
|
|
13958
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
|
+
|
|
13959
14042
|
// The string the user edits, plus soft breaks at @breaks (ascending offsets
|
|
13960
14043
|
// into it) -> the same string with the markers written in.
|
|
13961
14044
|
function insertSoftBreaks(text, breaks) {
|
|
@@ -14136,6 +14219,29 @@
|
|
|
14136
14219
|
return Math.min(renderedIndex, length);
|
|
14137
14220
|
}
|
|
14138
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
|
+
|
|
14139
14245
|
// Strips the placeholder back out, so that a value read off a rendered node is
|
|
14140
14246
|
// not mistaken for text the user typed.
|
|
14141
14247
|
function stripPlaceholder(str) {
|
|
@@ -14200,7 +14306,7 @@
|
|
|
14200
14306
|
function getAddLabelCommand(coords, opts) {
|
|
14201
14307
|
var o = opts || {};
|
|
14202
14308
|
var target = o.target || {mode: 'new', newLayerName: DEFAULT_LABEL_LAYER_NAME};
|
|
14203
|
-
var parts = ['-
|
|
14309
|
+
var parts = ['-labels', 'coordinates=' + formatCoords(coords)];
|
|
14204
14310
|
if (o.text) {
|
|
14205
14311
|
// A real newline would break the command parser, so encodeLabelText()
|
|
14206
14312
|
// writes the two-character escape the label renderer also accepts.
|
|
@@ -14267,7 +14373,7 @@
|
|
|
14267
14373
|
// id: feature id of the label
|
|
14268
14374
|
// target: layer name, or null to use the current target
|
|
14269
14375
|
function getLabelPlacementCommand(opts) {
|
|
14270
|
-
var parts = ['-
|
|
14376
|
+
var parts = ['-labels', 'label-start-offset=' + opts.offset];
|
|
14271
14377
|
if (opts.anchor) {
|
|
14272
14378
|
parts.push('text-anchor=' + opts.anchor);
|
|
14273
14379
|
}
|
|
@@ -14287,7 +14393,7 @@
|
|
|
14287
14393
|
// with and the position goes -- "stop taking a position, carry these offsets
|
|
14288
14394
|
// instead", which is one command because it is one edit.
|
|
14289
14395
|
//
|
|
14290
|
-
// 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
|
|
14291
14397
|
// empty value as "remove this", and removing a property no record carries adds
|
|
14292
14398
|
// nothing to the layer, so there is nothing to be gained by asking first.
|
|
14293
14399
|
//
|
|
@@ -14297,7 +14403,7 @@
|
|
|
14297
14403
|
// target: layer name, or null to use the current target
|
|
14298
14404
|
// via: (optional) the callout-via the text carried with it
|
|
14299
14405
|
function getLabelOffsetCommand(opts) {
|
|
14300
|
-
var parts = ['-
|
|
14406
|
+
var parts = ['-labels', 'dx=' + opts.dx, 'dy=' + opts.dy,
|
|
14301
14407
|
'text-anchor=' + opts.anchor, 'label-pos='];
|
|
14302
14408
|
if (opts.via) parts.push('callout-via=' + quoteCommandValue(opts.via));
|
|
14303
14409
|
parts.push('ids=' + opts.id);
|
|
@@ -14308,16 +14414,16 @@
|
|
|
14308
14414
|
// The command a drag on one of a label's handles writes, run once on release.
|
|
14309
14415
|
//
|
|
14310
14416
|
// values: {field: value}, where '' removes the field -- which is what
|
|
14311
|
-
// -
|
|
14417
|
+
// -labels reads an empty value as, and how a handle goes back to automatic
|
|
14312
14418
|
// id: feature id of the label
|
|
14313
14419
|
// opts:
|
|
14314
14420
|
// target: layer name, or null to use the current target
|
|
14315
14421
|
// text: (optional) rewrapped label-text, with real newlines and soft
|
|
14316
|
-
// breaks. A second -
|
|
14422
|
+
// breaks. A second -labels in the same string, so that a new width and
|
|
14317
14423
|
// the lines it breaks into are one undo step.
|
|
14318
14424
|
function getLabelStyleCommand(values, id, opts) {
|
|
14319
14425
|
var o = opts || {};
|
|
14320
|
-
var parts = ['-
|
|
14426
|
+
var parts = ['-labels'];
|
|
14321
14427
|
Object.keys(values).forEach(function(name) {
|
|
14322
14428
|
var val = values[name];
|
|
14323
14429
|
parts.push(name + '=' + (val === '' ? '' : quoteCommandValue(String(val))));
|
|
@@ -14339,7 +14445,7 @@
|
|
|
14339
14445
|
function getLabelTextCommand(text, id, target) {
|
|
14340
14446
|
// A real newline would break the command parser, so encodeLabelText() writes
|
|
14341
14447
|
// the two-character escape that the label renderer also accepts.
|
|
14342
|
-
var parts = ['-
|
|
14448
|
+
var parts = ['-labels', 'text=' + quoteCommandValue(encodeLabelText(text))];
|
|
14343
14449
|
parts.push('ids=' + id);
|
|
14344
14450
|
if (target) parts.push('target=' + quoteCommandValue(target));
|
|
14345
14451
|
return parts.join(' ');
|
|
@@ -15454,6 +15560,19 @@
|
|
|
15454
15560
|
return cachedFontStyles[fontName];
|
|
15455
15561
|
}
|
|
15456
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
|
+
|
|
15457
15576
|
// The installed font the browser draws unfonted text in, or '' if it cannot
|
|
15458
15577
|
// be worked out.
|
|
15459
15578
|
//
|
|
@@ -16616,6 +16735,15 @@
|
|
|
16616
16735
|
!/^(button|checkbox|radio|submit)$/.test(node.type);
|
|
16617
16736
|
}
|
|
16618
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
|
+
|
|
16619
16747
|
// While the caret is in one of @panel's fields, the keyboard belongs to that
|
|
16620
16748
|
// field. Without this the GUI's own handlers see the keystrokes: a Backspace
|
|
16621
16749
|
// typed into a field takes back the last knot of a curve being drawn, and an
|
|
@@ -16723,6 +16851,52 @@
|
|
|
16723
16851
|
return section;
|
|
16724
16852
|
}
|
|
16725
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
|
+
|
|
16726
16900
|
// Deliberately not focusable: the GUI is pointer-only, so a tab stop here
|
|
16727
16901
|
// would lead into a control the keyboard cannot then operate. See the focus
|
|
16728
16902
|
// note in page.css.
|
|
@@ -16799,6 +16973,8 @@
|
|
|
16799
16973
|
// opts.onColor(hex) a colour was typed, picked or previewed to a finish
|
|
16800
16974
|
// opts.onOpacity(frac) a usable percentage was typed
|
|
16801
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
|
|
16802
16978
|
//
|
|
16803
16979
|
// control.aside is the row's narrow column, empty, for a field that belongs
|
|
16804
16980
|
// beside the colour -- a stroke's width, say.
|
|
@@ -16836,10 +17012,14 @@
|
|
|
16836
17012
|
if (isHexColor(color)) control.picker.setColor(color);
|
|
16837
17013
|
opts.onColor(color);
|
|
16838
17014
|
});
|
|
16839
|
-
|
|
16840
|
-
|
|
16841
|
-
|
|
16842
|
-
|
|
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
|
+
}
|
|
16843
17023
|
control.picker = new ColorPicker(colorCell, {
|
|
16844
17024
|
presetRows: layerColorPresetRows,
|
|
16845
17025
|
// A drag in the picker shows on the map's own terms -- the swatch and the
|
|
@@ -16879,13 +17059,13 @@
|
|
|
16879
17059
|
//
|
|
16880
17060
|
// The style panel is worth having open before there is anything to point it at:
|
|
16881
17061
|
// pick a font, then place labels in it. Values set with nothing selected are
|
|
16882
|
-
// 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
|
|
16883
17063
|
// label finally exists -- so this is a tool default, not data, and it never
|
|
16884
17064
|
// needs its own undo step.
|
|
16885
17065
|
//
|
|
16886
17066
|
// See docs/development/label-tool-design.md.
|
|
16887
17067
|
|
|
16888
|
-
// The style properties -
|
|
17068
|
+
// The style properties -labels accepts. A panel control whose property is
|
|
16889
17069
|
// missing from this list would appear to do nothing when set with no selection,
|
|
16890
17070
|
// so the list is checked rather than assumed.
|
|
16891
17071
|
var NEW_LABEL_STYLE_FIELDS = [
|
|
@@ -16907,7 +17087,7 @@
|
|
|
16907
17087
|
//
|
|
16908
17088
|
// A blank or zero value removes the field instead of setting it: that is how
|
|
16909
17089
|
// the panel says "no icon" (icon='', icon-size=0) and "no inline css", and
|
|
16910
|
-
// carrying those through to -
|
|
17090
|
+
// carrying those through to -labels would write properties that mean
|
|
16911
17091
|
// nothing.
|
|
16912
17092
|
function mergeStyleValues(style, values) {
|
|
16913
17093
|
var out = Object.assign({}, style);
|
|
@@ -16933,7 +17113,7 @@
|
|
|
16933
17113
|
// somewhere and typing looks like it should do, and it is the position an icon
|
|
16934
17114
|
// is drawn to sit behind.
|
|
16935
17115
|
//
|
|
16936
|
-
// Only the tool defaults this. -
|
|
17116
|
+
// Only the tool defaults this. -labels coordinates= with no position still creates a
|
|
16937
17117
|
// label without one, so the default is a choice this tool makes and passes on
|
|
16938
17118
|
// explicitly, not a meaning the command gives to its absence.
|
|
16939
17119
|
var DEFAULT_NEW_LABEL_STYLE = {'label-pos': 'c'};
|
|
@@ -17247,13 +17427,17 @@
|
|
|
17247
17427
|
|
|
17248
17428
|
// Offsets into @text (real newlines, no soft breaks) where a line starts
|
|
17249
17429
|
// because it was wrapped, in ascending order.
|
|
17250
|
-
|
|
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) {
|
|
17251
17434
|
var width = getLabelWrapWidth(rec);
|
|
17252
17435
|
var key, breaks;
|
|
17253
17436
|
if (!width || !text) return [];
|
|
17254
|
-
|
|
17437
|
+
bold = bold || [];
|
|
17438
|
+
key = getWrapKey(text, rec, bold);
|
|
17255
17439
|
if (cache$2.has(key)) return cache$2.get(key);
|
|
17256
|
-
breaks = measureSoftBreaks(text, rec, width);
|
|
17440
|
+
breaks = measureSoftBreaks(text, rec, width, bold);
|
|
17257
17441
|
if (cache$2.size >= CACHE_LIMIT) cache$2.clear();
|
|
17258
17442
|
cache$2.set(key, breaks);
|
|
17259
17443
|
return breaks;
|
|
@@ -17263,29 +17447,30 @@
|
|
|
17263
17447
|
// unchanged when neither the old value nor the new one has a soft break, so
|
|
17264
17448
|
// that a label that does not wrap keeps the line breaks it was written with.
|
|
17265
17449
|
function rewrapLabelValue(value, rec) {
|
|
17266
|
-
var o =
|
|
17267
|
-
var breaks = getSoftBreaks(o.text, rec);
|
|
17450
|
+
var o = readLabelValue(value);
|
|
17451
|
+
var breaks = getSoftBreaks(o.text, rec, o.bold);
|
|
17268
17452
|
if (sameSoftBreaks(o.breaks, breaks)) return value;
|
|
17269
|
-
return encodeLabelText(
|
|
17453
|
+
return encodeLabelText(writeLabelValue(o.text, breaks, o.bold));
|
|
17270
17454
|
}
|
|
17271
17455
|
|
|
17272
17456
|
function clearWrapCache() {
|
|
17273
17457
|
cache$2.clear();
|
|
17274
17458
|
}
|
|
17275
17459
|
|
|
17276
|
-
function getWrapKey(text, rec) {
|
|
17460
|
+
function getWrapKey(text, rec, bold) {
|
|
17277
17461
|
var parts = [text];
|
|
17278
17462
|
WRAP_FIELDS.forEach(function(name) {
|
|
17279
17463
|
var val = rec[name];
|
|
17280
17464
|
parts.push(val === null || val === undefined ? '' : String(val));
|
|
17281
17465
|
});
|
|
17282
17466
|
parts.push(rec['class'] || '');
|
|
17467
|
+
parts.push(bold.join(';'));
|
|
17283
17468
|
return parts.join('\u0000');
|
|
17284
17469
|
}
|
|
17285
17470
|
|
|
17286
|
-
function measureSoftBreaks(text, rec, width) {
|
|
17471
|
+
function measureSoftBreaks(text, rec, width, bold) {
|
|
17287
17472
|
var w = getWrapper();
|
|
17288
|
-
var style, fontSize,
|
|
17473
|
+
var style, fontSize, pieces, range;
|
|
17289
17474
|
if (!w) return [];
|
|
17290
17475
|
style = getLabelFontStyle(w, rec);
|
|
17291
17476
|
if (!style) return [];
|
|
@@ -17293,15 +17478,15 @@
|
|
|
17293
17478
|
w.div.style[name] = style[name] || '';
|
|
17294
17479
|
});
|
|
17295
17480
|
w.div.style.width = width + 'px';
|
|
17296
|
-
w.div
|
|
17297
|
-
node = w.div.firstChild;
|
|
17481
|
+
pieces = writeRuns(w.div, text, bold);
|
|
17298
17482
|
fontSize = parseFloat(style.fontSize) || 12;
|
|
17299
17483
|
range = document.createRange();
|
|
17300
17484
|
var breaks = findSoftBreaks(text, function(i) {
|
|
17485
|
+
var piece = findPiece(pieces, i);
|
|
17301
17486
|
var end = isHighSurrogate(text.charCodeAt(i)) ? i + 2 : i + 1;
|
|
17302
17487
|
var r;
|
|
17303
|
-
range.setStart(node, i);
|
|
17304
|
-
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);
|
|
17305
17490
|
r = range.getBoundingClientRect();
|
|
17306
17491
|
return r.height > 0 ? r.top : null;
|
|
17307
17492
|
}, fontSize * LINE_HEIGHT / 2);
|
|
@@ -17309,6 +17494,35 @@
|
|
|
17309
17494
|
return breaks;
|
|
17310
17495
|
}
|
|
17311
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
|
+
|
|
17312
17526
|
function isHighSurrogate(c) {
|
|
17313
17527
|
return c >= 0xD800 && c <= 0xDBFF;
|
|
17314
17528
|
}
|
|
@@ -17780,7 +17994,7 @@
|
|
|
17780
17994
|
// spacing sits above.
|
|
17781
17995
|
haloSection = addSection('Halo');
|
|
17782
17996
|
var haloTitle = haloSection.findChild('.label-style-section-title');
|
|
17783
|
-
haloToggle =
|
|
17997
|
+
haloToggle = makePanelToggle(haloTitle, {
|
|
17784
17998
|
title: 'Draw a halo around the text',
|
|
17785
17999
|
className: 'label-halo-toggle',
|
|
17786
18000
|
onChange: setHaloOn
|
|
@@ -17831,7 +18045,7 @@
|
|
|
17831
18045
|
// a label with no icon: nothing to apply it to.
|
|
17832
18046
|
iconSection = addSection('Icon');
|
|
17833
18047
|
var iconTitle = iconSection.findChild('.label-style-section-title');
|
|
17834
|
-
iconToggle =
|
|
18048
|
+
iconToggle = makePanelToggle(iconTitle, {
|
|
17835
18049
|
title: 'Draw a symbol at the label anchor',
|
|
17836
18050
|
className: 'label-icon-toggle',
|
|
17837
18051
|
onChange: setIconOn
|
|
@@ -17938,7 +18152,7 @@
|
|
|
17938
18152
|
// the map rather than typed here. See docs/development/text-annotation-design.md.
|
|
17939
18153
|
function initCalloutSection() {
|
|
17940
18154
|
calloutSection = addSection('Callout');
|
|
17941
|
-
calloutToggle =
|
|
18155
|
+
calloutToggle = makePanelToggle(calloutSection.findChild('.label-style-section-title'), {
|
|
17942
18156
|
title: 'Draw a line from the label anchor to its text',
|
|
17943
18157
|
className: 'label-callout-toggle',
|
|
17944
18158
|
onChange: setCalloutOn
|
|
@@ -18059,48 +18273,6 @@
|
|
|
18059
18273
|
return makePanelSection(panel, title, opts);
|
|
18060
18274
|
}
|
|
18061
18275
|
|
|
18062
|
-
// A switch: a track with a knob that sits left when off and right when on,
|
|
18063
|
-
// which is the direction users expect and the only thing that says which
|
|
18064
|
-
// state is which without a label for each. A third state says that the
|
|
18065
|
-
// labels it is asking about disagree -- the knob sits over the join of a
|
|
18066
|
-
// half-and-half track, which is the panel's only control that has to show
|
|
18067
|
-
// "some of them" rather than a value.
|
|
18068
|
-
//
|
|
18069
|
-
// role=checkbox rather than switch, which is what it looks like: 'mixed' is a
|
|
18070
|
-
// legal aria-checked value for a checkbox and not for a switch, and a switch
|
|
18071
|
-
// reporting a mixed selection as unchecked would be telling a screen reader
|
|
18072
|
-
// the one thing the third state exists to avoid saying.
|
|
18073
|
-
function makeToggle(parent, opts) {
|
|
18074
|
-
var track = El('div').addClass('label-toggle').attr('role', 'checkbox').appendTo(parent);
|
|
18075
|
-
var state = 'off';
|
|
18076
|
-
if (opts.className) track.addClass(opts.className);
|
|
18077
|
-
var disabled = false;
|
|
18078
|
-
El('div').addClass('label-toggle-knob').appendTo(track);
|
|
18079
|
-
if (opts.title) track.attr('title', opts.title);
|
|
18080
|
-
// A click on a mixed switch turns everything on. It is the convention, and
|
|
18081
|
-
// it is the reading that reaches a state the switch can describe: the next
|
|
18082
|
-
// click then turns everything off, so both are one click away.
|
|
18083
|
-
track.on('click', function() {
|
|
18084
|
-
if (disabled) return;
|
|
18085
|
-
opts.onChange(state != 'on');
|
|
18086
|
-
});
|
|
18087
|
-
return {
|
|
18088
|
-
// val: 'on', 'off' or 'mixed'; anything else reads as off
|
|
18089
|
-
setState: function(val) {
|
|
18090
|
-
state = val == 'on' || val == 'mixed' ? val : 'off';
|
|
18091
|
-
track.classed('on', state == 'on')
|
|
18092
|
-
.classed('mixed', state == 'mixed')
|
|
18093
|
-
.attr('aria-checked', state == 'mixed' ? 'mixed' :
|
|
18094
|
-
state == 'on' ? 'true' : 'false');
|
|
18095
|
-
},
|
|
18096
|
-
setDisabled: function(off) {
|
|
18097
|
-
disabled = !!off;
|
|
18098
|
-
track.classed('disabled', disabled)
|
|
18099
|
-
.attr('aria-disabled', disabled ? 'true' : 'false');
|
|
18100
|
-
}
|
|
18101
|
-
};
|
|
18102
|
-
}
|
|
18103
|
-
|
|
18104
18276
|
function addColorOpacityField(parent, chit, input, onOpacity) {
|
|
18105
18277
|
return makeColorOpacityField(parent, chit, input, {
|
|
18106
18278
|
onSet: onOpacity,
|
|
@@ -19428,14 +19600,6 @@
|
|
|
19428
19600
|
return !!node && /^(INPUT|SELECT|TEXTAREA|BUTTON|OPTION)$/.test(node.nodeName);
|
|
19429
19601
|
}
|
|
19430
19602
|
|
|
19431
|
-
// Whether clicking this node puts a native menu on screen, which must then be
|
|
19432
|
-
// left holding the focus. OPTION counts because a browser that reports the
|
|
19433
|
-
// chosen option as the click target is reporting a menu interaction either
|
|
19434
|
-
// way, and a choice restores the caret through its change handler.
|
|
19435
|
-
function opensAMenu(node) {
|
|
19436
|
-
return !!node && /^(SELECT|OPTION)$/.test(node.nodeName);
|
|
19437
|
-
}
|
|
19438
|
-
|
|
19439
19603
|
// Hands the keyboard back after a control has done its job: to the label
|
|
19440
19604
|
// being typed into if there is one, and to nothing at all otherwise.
|
|
19441
19605
|
function releaseFocus() {
|
|
@@ -19454,7 +19618,7 @@
|
|
|
19454
19618
|
if (styles.length === 0) return;
|
|
19455
19619
|
releaseFocus();
|
|
19456
19620
|
// What the panel is set to is always what the next label gets, whether or
|
|
19457
|
-
// not these values also went to a label that already exists. -
|
|
19621
|
+
// not these values also went to a label that already exists. -labels
|
|
19458
19622
|
// writes them when that label is created, which is why this needs no
|
|
19459
19623
|
// command and no undo step of its own.
|
|
19460
19624
|
if (labelModeIsOn()) {
|
|
@@ -19478,7 +19642,7 @@
|
|
|
19478
19642
|
|
|
19479
19643
|
function applyStyleCommand(styles, ids) {
|
|
19480
19644
|
var lyr = getActiveLayer();
|
|
19481
|
-
var parts = ['-
|
|
19645
|
+
var parts = ['-labels'];
|
|
19482
19646
|
if (!gui.console || !lyr) return;
|
|
19483
19647
|
styles.forEach(function(style) {
|
|
19484
19648
|
parts.push(style[0] + '=' + quoteCommandValue(style[1]));
|
|
@@ -19500,7 +19664,7 @@
|
|
|
19500
19664
|
// A text block's breaks are only right for the width and font they were
|
|
19501
19665
|
// found with, so a change to either rewraps it in the same command: one undo
|
|
19502
19666
|
// step, and no moment at which the block is drawn with its old lines in its
|
|
19503
|
-
// new font. One -
|
|
19667
|
+
// new font. One -labels per label, since each has text of its own.
|
|
19504
19668
|
function getRewrapCommands(styles, ids, lyr) {
|
|
19505
19669
|
var table = lyr.data;
|
|
19506
19670
|
var changes = {};
|
|
@@ -19634,13 +19798,480 @@
|
|
|
19634
19798
|
return measureSvg;
|
|
19635
19799
|
}
|
|
19636
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
|
+
|
|
19637
20268
|
var savedStylesKey = 'layer_style_presets';
|
|
19638
|
-
var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray', 'fill', 'fill-opacity'];
|
|
20269
|
+
var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray', 'fill', 'fill-opacity', 'fill-pattern'];
|
|
19639
20270
|
|
|
19640
20271
|
function LayerStyleTool(gui) {
|
|
19641
20272
|
var parent = gui.container.findChild('.mshp-main-map');
|
|
19642
20273
|
var panel = El('div').addClass('label-style-panel layer-style-panel rollover').appendTo(parent).hide();
|
|
19643
|
-
var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthField, dashControl, randomFillBtn, presetControl, hit;
|
|
20274
|
+
var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthField, dashControl, randomFillBtn, presetControl, patternControl, hit;
|
|
19644
20275
|
var targetLayer = null;
|
|
19645
20276
|
|
|
19646
20277
|
initPanel();
|
|
@@ -19685,6 +20316,7 @@
|
|
|
19685
20316
|
|
|
19686
20317
|
function turnOn() {
|
|
19687
20318
|
targetLayer = getActiveLayer();
|
|
20319
|
+
patternControl.reset();
|
|
19688
20320
|
applyDefaultLineStyle();
|
|
19689
20321
|
panel.show();
|
|
19690
20322
|
updateControls();
|
|
@@ -19694,6 +20326,8 @@
|
|
|
19694
20326
|
panel.hide();
|
|
19695
20327
|
strokeControl.picker.hide();
|
|
19696
20328
|
fillControl.picker.hide();
|
|
20329
|
+
patternControl.hidePicker();
|
|
20330
|
+
patternControl.reset();
|
|
19697
20331
|
targetLayer = null;
|
|
19698
20332
|
}
|
|
19699
20333
|
|
|
@@ -19710,8 +20344,8 @@
|
|
|
19710
20344
|
// picker's Close button, and the panel's own buttons in the browsers that
|
|
19711
20345
|
// focus a button on click. Left alone while the user is typing into a
|
|
19712
20346
|
// field, which a click elsewhere in the panel ends on its own.
|
|
19713
|
-
panel.node().addEventListener('click', function() {
|
|
19714
|
-
if (isTextInput(document.activeElement)) return;
|
|
20347
|
+
panel.node().addEventListener('click', function(e) {
|
|
20348
|
+
if (isTextInput(document.activeElement) || opensAMenu(e.target)) return;
|
|
19715
20349
|
releaseFocus();
|
|
19716
20350
|
});
|
|
19717
20351
|
|
|
@@ -19732,6 +20366,16 @@
|
|
|
19732
20366
|
randomFillBtn = makePanelActionButton(buttonRow, 'Random fills', applyRandomFillColors);
|
|
19733
20367
|
makePanelActionButton(buttonRow, 'Clear style', clearLayerStyle);
|
|
19734
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
|
+
|
|
19735
20379
|
presetControl = new StylePresetControl(panel, {
|
|
19736
20380
|
storageKey: savedStylesKey,
|
|
19737
20381
|
type: getStyleType,
|
|
@@ -19829,6 +20473,8 @@
|
|
|
19829
20473
|
updateStrokeWidthControl();
|
|
19830
20474
|
updateDashArrayControl();
|
|
19831
20475
|
randomFillBtn.classed('hidden', geom != 'polygon');
|
|
20476
|
+
patternControl.section.classed('hidden', geom != 'polygon');
|
|
20477
|
+
if (geom == 'polygon') patternControl.update();
|
|
19832
20478
|
presetControl.render();
|
|
19833
20479
|
updateSavedStyleControls();
|
|
19834
20480
|
}
|
|
@@ -19892,7 +20538,11 @@
|
|
|
19892
20538
|
if (control.field == 'stroke' && strokeWidthIsUnsetForTargets()) {
|
|
19893
20539
|
styles.push(['stroke-width', 1]);
|
|
19894
20540
|
}
|
|
19895
|
-
|
|
20541
|
+
if (control.field == 'fill') {
|
|
20542
|
+
runStyleEdits(patternControl.getStyleEdits(styles));
|
|
20543
|
+
} else {
|
|
20544
|
+
runStyleCommand(styles);
|
|
20545
|
+
}
|
|
19896
20546
|
}
|
|
19897
20547
|
|
|
19898
20548
|
function applyStrokeWidthStyle(value) {
|
|
@@ -19912,11 +20562,30 @@
|
|
|
19912
20562
|
}
|
|
19913
20563
|
|
|
19914
20564
|
function runStyleCommand(styles) {
|
|
19915
|
-
var parts = ['-style'];
|
|
19916
20565
|
releaseFocus();
|
|
19917
20566
|
syncTargetLayer();
|
|
19918
20567
|
var ids = getTargetIds();
|
|
19919
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'];
|
|
19920
20589
|
styles.forEach(function(style) {
|
|
19921
20590
|
parts.push(style[0] + '=' + quoteCommandValue(style[1]));
|
|
19922
20591
|
});
|
|
@@ -19924,7 +20593,7 @@
|
|
|
19924
20593
|
if (ids.length < internal.getFeatureCount(targetLayer)) {
|
|
19925
20594
|
parts.push('ids=' + ids.join(','));
|
|
19926
20595
|
}
|
|
19927
|
-
|
|
20596
|
+
return parts.join(' ');
|
|
19928
20597
|
}
|
|
19929
20598
|
|
|
19930
20599
|
function applyDefaultLineStyle() {
|
|
@@ -19938,13 +20607,21 @@
|
|
|
19938
20607
|
}
|
|
19939
20608
|
}
|
|
19940
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.
|
|
19941
20613
|
function applyRandomFillColors() {
|
|
19942
20614
|
var cmd = '-classify colors=random non-adjacent';
|
|
20615
|
+
var patternEdits;
|
|
19943
20616
|
syncTargetLayer();
|
|
19944
20617
|
if (!gui.console || !targetLayer || targetLayer.geometry_type != 'polygon') return;
|
|
19945
20618
|
if (getActiveLayer() != targetLayer) {
|
|
19946
20619
|
cmd += ' target=' + internal.formatOptionValue(internal.getLayerTargetId(gui.model, targetLayer));
|
|
19947
20620
|
}
|
|
20621
|
+
patternEdits = patternControl.getRefillExpressionEdits(getAllFeatureIds(targetLayer));
|
|
20622
|
+
if (patternEdits.length > 0) {
|
|
20623
|
+
cmd += ' ' + formatStyleEditCommands(patternEdits);
|
|
20624
|
+
}
|
|
19948
20625
|
runCommand(cmd, 'Random fill colors');
|
|
19949
20626
|
}
|
|
19950
20627
|
|
|
@@ -19963,7 +20640,7 @@
|
|
|
19963
20640
|
}
|
|
19964
20641
|
});
|
|
19965
20642
|
if (styles.length > 0) {
|
|
19966
|
-
|
|
20643
|
+
runStyleEdits(patternControl.getStyleEdits(styles));
|
|
19967
20644
|
}
|
|
19968
20645
|
}
|
|
19969
20646
|
|
|
@@ -19978,6 +20655,7 @@
|
|
|
19978
20655
|
if (targetLayer && targetLayer.geometry_type == 'polygon') {
|
|
19979
20656
|
addStyleValue(style, 'fill', getControlValue(fillControl.input));
|
|
19980
20657
|
addStyleValue(style, 'fill-opacity', parseOpacityValue(fillControl.opacity.node().value));
|
|
20658
|
+
addStyleValue(style, 'fill-pattern', patternControl.getCode(getControlValue(fillControl.input)));
|
|
19981
20659
|
}
|
|
19982
20660
|
return style;
|
|
19983
20661
|
}
|
|
@@ -20192,8 +20870,8 @@
|
|
|
20192
20870
|
revert: updateControls,
|
|
20193
20871
|
release: releaseFocus
|
|
20194
20872
|
});
|
|
20195
|
-
panel.node().addEventListener('click', function() {
|
|
20196
|
-
if (isTextInput(document.activeElement)) return;
|
|
20873
|
+
panel.node().addEventListener('click', function(e) {
|
|
20874
|
+
if (isTextInput(document.activeElement) || opensAMenu(e.target)) return;
|
|
20197
20875
|
releaseFocus();
|
|
20198
20876
|
});
|
|
20199
20877
|
|
|
@@ -20447,9 +21125,8 @@
|
|
|
20447
21125
|
var expr = createExprInput.node().value.trim();
|
|
20448
21126
|
var cmd;
|
|
20449
21127
|
if (!expr || !gui.console) return;
|
|
20450
|
-
cmd =
|
|
20451
|
-
|
|
20452
|
-
'-style label-text=' + quoteCommandValue(expr);
|
|
21128
|
+
cmd = '-labels text=' + quoteCommandValue(expr);
|
|
21129
|
+
if (createCopyCheckbox.node().checked) cmd += ' + name=labels';
|
|
20453
21130
|
runGuiEditCommand(gui, cmd, {
|
|
20454
21131
|
title: 'Create labels',
|
|
20455
21132
|
onSuccess: openLabelStyles
|
|
@@ -21434,7 +22111,7 @@
|
|
|
21434
22111
|
// hit test, the reposition pass and the selection cue all find labels by those,
|
|
21435
22112
|
// and a node with no feature behind it must not be found by any of them.
|
|
21436
22113
|
//
|
|
21437
|
-
// @rec: the label's record, as -
|
|
22114
|
+
// @rec: the label's record, as -labels coordinates= will write it
|
|
21438
22115
|
// @shp: its knots, in the layer's display coordinates
|
|
21439
22116
|
// Returns a markup string, or '' if there is nothing to draw.
|
|
21440
22117
|
function renderPendingSymbol(rec, shp, ext, idPrefix) {
|
|
@@ -25923,6 +26600,112 @@
|
|
|
25923
26600
|
return true;
|
|
25924
26601
|
}
|
|
25925
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
|
+
|
|
25926
26709
|
// Caret and selection geometry, computed from the SVG character position APIs.
|
|
25927
26710
|
//
|
|
25928
26711
|
// The APIs behave identically on <text> and on <text><textPath>, so this code
|
|
@@ -26242,16 +27025,19 @@
|
|
|
26242
27025
|
// so the tool cannot do this at its own call sites.
|
|
26243
27026
|
self.open = function(target, id, opts) {
|
|
26244
27027
|
var rec = getRecord(target, id);
|
|
27028
|
+
var value;
|
|
26245
27029
|
if (!rec) return false;
|
|
26246
27030
|
self.close();
|
|
27031
|
+
value = readLabelValue(rec['label-text']);
|
|
26247
27032
|
session = {
|
|
26248
27033
|
target: target,
|
|
26249
27034
|
id: id,
|
|
26250
27035
|
pending: null,
|
|
26251
27036
|
created: false,
|
|
26252
27037
|
onClose: opts && opts.onClose || null,
|
|
26253
|
-
startText:
|
|
26254
|
-
startBreaks:
|
|
27038
|
+
startText: value.text,
|
|
27039
|
+
startBreaks: value.breaks,
|
|
27040
|
+
startBold: value.bold,
|
|
26255
27041
|
nodes: null
|
|
26256
27042
|
};
|
|
26257
27043
|
startSession();
|
|
@@ -26297,6 +27083,7 @@
|
|
|
26297
27083
|
onClose: opts.onClose || null,
|
|
26298
27084
|
startText: '',
|
|
26299
27085
|
startBreaks: [],
|
|
27086
|
+
startBold: [],
|
|
26300
27087
|
nodes: null
|
|
26301
27088
|
};
|
|
26302
27089
|
startSession();
|
|
@@ -26305,6 +27092,7 @@
|
|
|
26305
27092
|
|
|
26306
27093
|
function startSession() {
|
|
26307
27094
|
session.text = session.startText;
|
|
27095
|
+
session.bold = session.startBold;
|
|
26308
27096
|
getTextarea().value = session.text;
|
|
26309
27097
|
setCaret(session.text.length, session.text.length);
|
|
26310
27098
|
focusTextarea();
|
|
@@ -26320,6 +27108,74 @@
|
|
|
26320
27108
|
self.refresh();
|
|
26321
27109
|
}
|
|
26322
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
|
+
|
|
26323
27179
|
self.isOpen = function() {
|
|
26324
27180
|
return !!session;
|
|
26325
27181
|
};
|
|
@@ -26350,6 +27206,7 @@
|
|
|
26350
27206
|
// arriving just after a redraw is holding one of those.
|
|
26351
27207
|
if (session.pending && node.closest &&
|
|
26352
27208
|
node.closest('.label-edit-pending')) return true;
|
|
27209
|
+
if (session.nodes && session.nodes.symbol.contains(node)) return true;
|
|
26353
27210
|
return !!groups && (groups.hit.contains(node) || groups.back.contains(node) ||
|
|
26354
27211
|
groups.front.contains(node));
|
|
26355
27212
|
};
|
|
@@ -26408,7 +27265,7 @@
|
|
|
26408
27265
|
if (i < 0) return false;
|
|
26409
27266
|
i = getEditIndex(session.text, i, layout);
|
|
26410
27267
|
setCaret(i, i);
|
|
26411
|
-
|
|
27268
|
+
redrawCaret();
|
|
26412
27269
|
return true;
|
|
26413
27270
|
};
|
|
26414
27271
|
|
|
@@ -26442,7 +27299,7 @@
|
|
|
26442
27299
|
var blank = textHasNoGlyphs(o.text);
|
|
26443
27300
|
var breaks = getBreaks(o);
|
|
26444
27301
|
if (o.pending) {
|
|
26445
|
-
if (!blank) o.pending.create(
|
|
27302
|
+
if (!blank) o.pending.create(writeLabelValue(o.text, breaks, o.bold));
|
|
26446
27303
|
return;
|
|
26447
27304
|
}
|
|
26448
27305
|
if (blank) {
|
|
@@ -26450,8 +27307,9 @@
|
|
|
26450
27307
|
removeLabel(o);
|
|
26451
27308
|
return;
|
|
26452
27309
|
}
|
|
26453
|
-
if (o.text === o.startText && sameSoftBreaks(breaks, o.startBreaks)
|
|
26454
|
-
|
|
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),
|
|
26455
27313
|
o.id, o.target.name), {
|
|
26456
27314
|
title: 'Label text'
|
|
26457
27315
|
});
|
|
@@ -26464,7 +27322,7 @@
|
|
|
26464
27322
|
var rec;
|
|
26465
27323
|
if (!shp || shp.length > 1) return [];
|
|
26466
27324
|
rec = o.pending ? o.pending.getStyle() : getRecord(o.target, o.id);
|
|
26467
|
-
return getSoftBreaks(o.text, rec);
|
|
27325
|
+
return getSoftBreaks(o.text, rec, o.bold);
|
|
26468
27326
|
}
|
|
26469
27327
|
|
|
26470
27328
|
// Deletes the label's feature, geometry and all.
|
|
@@ -26551,7 +27409,7 @@
|
|
|
26551
27409
|
var rec = Object.assign({}, o.pending.getStyle());
|
|
26552
27410
|
// With its soft breaks, so that what is aligned and what a callout meets
|
|
26553
27411
|
// is the block as wrapped
|
|
26554
|
-
rec['label-text'] = encodeLabelText(
|
|
27412
|
+
rec['label-text'] = encodeLabelText(writeLabelValue(o.text, getBreaks(o), o.bold));
|
|
26555
27413
|
// No need to expand label-pos here, or to measure its text: the renderer
|
|
26556
27414
|
// resolves the position and asks for the width it needs, so a pending
|
|
26557
27415
|
// label is laid out by exactly the same code as a committed one.
|
|
@@ -26599,11 +27457,15 @@
|
|
|
26599
27457
|
// - Line breaks. An empty <tspan> lays out nothing, so a line just opened
|
|
26600
27458
|
// with Enter had no position for the caret to move to. The placeholder in
|
|
26601
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.
|
|
26602
27464
|
function writeText(o) {
|
|
26603
27465
|
var content = o.nodes.content;
|
|
26604
27466
|
var breaks = getBreaks(o);
|
|
26605
|
-
var written = o.text + '\u0000' + breaks.join(',');
|
|
26606
|
-
var lines, i, tspan;
|
|
27467
|
+
var written = o.text + '\u0000' + breaks.join(',') + '\u0000' + o.bold.join(';');
|
|
27468
|
+
var lines, full, pos, i, tspan;
|
|
26607
27469
|
// refresh() runs on every map render, which during a pan is every frame;
|
|
26608
27470
|
// rebuilding text nodes that already say the right thing is pure waste
|
|
26609
27471
|
if (o.writtenTo === content && o.writtenText === written) return;
|
|
@@ -26617,23 +27479,53 @@
|
|
|
26617
27479
|
// or from a label that was multi-line before it was given a path -- and
|
|
26618
27480
|
// turning them into spaces is what export does too.
|
|
26619
27481
|
if (getLayout(o) === LINES_JOINED) {
|
|
26620
|
-
|
|
27482
|
+
full = getRenderedContent(o.text);
|
|
27483
|
+
appendRuns(content, full, o.bold, 0, full.length);
|
|
26621
27484
|
return;
|
|
26622
27485
|
}
|
|
26623
27486
|
// A soft break keeps the space it broke at at the end of its line, where
|
|
26624
27487
|
// the rendered label drops it: here every typed character has to be one
|
|
26625
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.
|
|
26626
27493
|
lines = getRenderedLines(o.text, breaks);
|
|
26627
|
-
|
|
27494
|
+
full = lines.join('');
|
|
27495
|
+
pos = lines[0].length;
|
|
27496
|
+
appendRuns(content, full, o.bold, 0, pos);
|
|
26628
27497
|
for (i = 1; i < lines.length; i++) {
|
|
26629
27498
|
tspan = document.createElementNS(SVG_NS$3, 'tspan');
|
|
26630
27499
|
tspan.setAttribute('x', o.nodes.text.getAttribute('x') || 0);
|
|
26631
27500
|
tspan.setAttribute('dy', getLineHeight(o));
|
|
26632
|
-
tspan.
|
|
27501
|
+
appendRuns(tspan, full, o.bold, pos, pos + lines[i].length);
|
|
27502
|
+
pos += lines[i].length;
|
|
26633
27503
|
content.appendChild(tspan);
|
|
26634
27504
|
}
|
|
26635
27505
|
}
|
|
26636
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
|
+
|
|
26637
27529
|
// Cached per session: asking costs a record lookup and a geometry test, and
|
|
26638
27530
|
// the answer cannot change while a session is open.
|
|
26639
27531
|
function getLayout(o) {
|
|
@@ -26759,18 +27651,33 @@
|
|
|
26759
27651
|
return el;
|
|
26760
27652
|
}
|
|
26761
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.
|
|
26762
27658
|
function drawHitRegion(o, g, box, caret, pathId) {
|
|
26763
|
-
var
|
|
26764
|
-
|
|
26765
|
-
if (box
|
|
26766
|
-
|
|
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
|
+
}
|
|
26767
27672
|
// A thickened, transparent copy of the baseline: about one em wide, so that
|
|
26768
27673
|
// the region follows the curve rather than boxing it.
|
|
26769
|
-
|
|
26770
|
-
|
|
26771
|
-
|
|
26772
|
-
|
|
26773
|
-
|
|
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);
|
|
26774
27681
|
}
|
|
26775
27682
|
|
|
26776
27683
|
function getLabelPathId(nodes) {
|
|
@@ -26863,12 +27770,16 @@
|
|
|
26863
27770
|
|
|
26864
27771
|
function rect(box, className) {
|
|
26865
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) {
|
|
26866
27779
|
el.setAttribute('x', box.x);
|
|
26867
27780
|
el.setAttribute('y', box.y);
|
|
26868
27781
|
el.setAttribute('width', box.width);
|
|
26869
27782
|
el.setAttribute('height', box.height);
|
|
26870
|
-
el.setAttribute('class', className);
|
|
26871
|
-
return el;
|
|
26872
27783
|
}
|
|
26873
27784
|
|
|
26874
27785
|
function caretLine(caret) {
|
|
@@ -26898,6 +27809,7 @@
|
|
|
26898
27809
|
|
|
26899
27810
|
textarea.addEventListener('input', function() {
|
|
26900
27811
|
if (!session) return;
|
|
27812
|
+
session.bold = updateBoldForEdit(session.bold, session.text, textarea.value);
|
|
26901
27813
|
session.text = textarea.value;
|
|
26902
27814
|
self.refresh();
|
|
26903
27815
|
});
|
|
@@ -26922,6 +27834,13 @@
|
|
|
26922
27834
|
return !e.isComposing && e.keyCode != 229;
|
|
26923
27835
|
}
|
|
26924
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
|
+
|
|
26925
27844
|
textarea.addEventListener('keydown', function(e) {
|
|
26926
27845
|
if (!session) return;
|
|
26927
27846
|
// The GUI's global key handlers are told to keep out of the way by
|
|
@@ -26948,6 +27867,10 @@
|
|
|
26948
27867
|
e.preventDefault();
|
|
26949
27868
|
e.stopPropagation();
|
|
26950
27869
|
self.close();
|
|
27870
|
+
} else if (isBoldKey(e)) {
|
|
27871
|
+
e.preventDefault();
|
|
27872
|
+
e.stopPropagation();
|
|
27873
|
+
self.toggleBold();
|
|
26951
27874
|
} else {
|
|
26952
27875
|
e.stopPropagation();
|
|
26953
27876
|
}
|
|
@@ -27001,10 +27924,11 @@
|
|
|
27001
27924
|
return textarea ? textarea.selectionEnd : 0;
|
|
27002
27925
|
}
|
|
27003
27926
|
|
|
27004
|
-
|
|
27927
|
+
// @direction: 'forward' (the default) or 'backward', which end of the
|
|
27928
|
+
// selection shift-arrow moves
|
|
27929
|
+
function setCaret(start, end, direction) {
|
|
27005
27930
|
if (!textarea) return;
|
|
27006
|
-
textarea.
|
|
27007
|
-
textarea.selectionEnd = end;
|
|
27931
|
+
textarea.setSelectionRange(start, end, direction || 'forward');
|
|
27008
27932
|
}
|
|
27009
27933
|
}
|
|
27010
27934
|
|
|
@@ -27697,7 +28621,7 @@
|
|
|
27697
28621
|
}
|
|
27698
28622
|
|
|
27699
28623
|
// The label tool: arming a label type, placing an anchor or a curve, turning
|
|
27700
|
-
// the result into
|
|
28624
|
+
// the result into a -labels coordinates= command, and handing the new label to the
|
|
27701
28625
|
// in-place text editor.
|
|
27702
28626
|
//
|
|
27703
28627
|
// Creating a label and editing one are one gesture, not two: a click leaves a
|
|
@@ -27761,6 +28685,14 @@
|
|
|
27761
28685
|
// The mouseup that ended the last block drag, or null. A short one is
|
|
27762
28686
|
// followed by a click from the same mouseup, which it has already handled.
|
|
27763
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;
|
|
27764
28696
|
var editor = new LabelEditor(gui, ext);
|
|
27765
28697
|
var selection = new LabelSelection(gui, ext, hit, function() {
|
|
27766
28698
|
return editor.isOpen() ? editor.getFeatureId() : -1;
|
|
@@ -27790,7 +28722,7 @@
|
|
|
27790
28722
|
// tools do on entry as well (`addEmptyLayer()` in `gui-edit-points.mjs` and
|
|
27791
28723
|
// `gui-draw-lines2.mjs`). It is named rather than left unnamed, unlike
|
|
27792
28724
|
// theirs: it is the layer `getLabelTarget()` would have created for a label
|
|
27793
|
-
// anyway, and naming it is what lets the `-
|
|
28725
|
+
// anyway, and naming it is what lets the `-labels` commands name their
|
|
27794
28726
|
// target and so replay from the session history.
|
|
27795
28727
|
//
|
|
27796
28728
|
// Called before entering the tool's own mode, because getInitialTool() reads
|
|
@@ -27840,6 +28772,8 @@
|
|
|
27840
28772
|
handleDrag = null;
|
|
27841
28773
|
releasedDrag = null;
|
|
27842
28774
|
blockRelease = null;
|
|
28775
|
+
selectDrag = null;
|
|
28776
|
+
selectRelease = null;
|
|
27843
28777
|
shownHandles = null;
|
|
27844
28778
|
clearBlockDrag();
|
|
27845
28779
|
hoverHandle = null;
|
|
@@ -27966,6 +28900,10 @@
|
|
|
27966
28900
|
blockRelease = null;
|
|
27967
28901
|
return;
|
|
27968
28902
|
}
|
|
28903
|
+
if (selectRelease && getDomEvent(e) == selectRelease) {
|
|
28904
|
+
selectRelease = null;
|
|
28905
|
+
return;
|
|
28906
|
+
}
|
|
27969
28907
|
if (armed == 'path' && drawingCurve()) {
|
|
27970
28908
|
extendCurve(pixToMapCoords(e.x, e.y));
|
|
27971
28909
|
return;
|
|
@@ -28038,9 +28976,34 @@
|
|
|
28038
28976
|
e.flipLabel = getFlipAction(target, id);
|
|
28039
28977
|
}
|
|
28040
28978
|
}
|
|
28979
|
+
if (target && editor.isOpen() && clickIsOnEditedLabel(e)) {
|
|
28980
|
+
addBoldAction(e, target);
|
|
28981
|
+
}
|
|
28041
28982
|
gui.contextMenu.open(e, target);
|
|
28042
28983
|
});
|
|
28043
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
|
+
|
|
28044
29007
|
// The label a right-click was aimed at, or -1.
|
|
28045
29008
|
//
|
|
28046
29009
|
// A label being typed into is not a hit target -- the editor's own overlay
|
|
@@ -28296,7 +29259,16 @@
|
|
|
28296
29259
|
// takes a drag -- everything else over a label is left alone so that the map
|
|
28297
29260
|
// still pans, which is why these handlers stop the event themselves.
|
|
28298
29261
|
hit.on('dragstart', function(e) {
|
|
28299
|
-
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
|
+
}
|
|
28300
29272
|
// The handle comes from the last hover, not from testing the pointer here.
|
|
28301
29273
|
// dragstart arrives with the pointer already moved off the handle -- by the
|
|
28302
29274
|
// first mousemove that made it a drag, which on a quick gesture is further
|
|
@@ -28321,6 +29293,22 @@
|
|
|
28321
29293
|
consumeDrag(e);
|
|
28322
29294
|
});
|
|
28323
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
|
+
|
|
28324
29312
|
// handle: {id, index, point, pointer} from findHandle(), or the same shape
|
|
28325
29313
|
// made up for a drag that grabbed a label somewhere other than its knot
|
|
28326
29314
|
function beginKnotDrag(handle) {
|
|
@@ -28346,6 +29334,11 @@
|
|
|
28346
29334
|
|
|
28347
29335
|
hit.on('drag', function(e) {
|
|
28348
29336
|
var shp, p;
|
|
29337
|
+
if (selectDrag) {
|
|
29338
|
+
consumeDrag(e);
|
|
29339
|
+
updateSelectDrag(e);
|
|
29340
|
+
return;
|
|
29341
|
+
}
|
|
28349
29342
|
if (textDrag) {
|
|
28350
29343
|
consumeDrag(e);
|
|
28351
29344
|
updateTextDrag(e);
|
|
@@ -28380,6 +29373,12 @@
|
|
|
28380
29373
|
|
|
28381
29374
|
hit.on('dragend', function(e) {
|
|
28382
29375
|
var o = drag;
|
|
29376
|
+
if (selectDrag) {
|
|
29377
|
+
selectDrag = null;
|
|
29378
|
+
selectRelease = getDomEvent(e);
|
|
29379
|
+
consumeDrag(e);
|
|
29380
|
+
return;
|
|
29381
|
+
}
|
|
28383
29382
|
if (textDrag) {
|
|
28384
29383
|
o = textDrag;
|
|
28385
29384
|
textDrag = null;
|
|
@@ -29020,9 +30019,9 @@
|
|
|
29020
30019
|
selection.refresh(true);
|
|
29021
30020
|
}
|
|
29022
30021
|
|
|
29023
|
-
// Turns the previewed offset into one -
|
|
30022
|
+
// Turns the previewed offset into one -labels.
|
|
29024
30023
|
//
|
|
29025
|
-
// Only the label under the pointer moves, even with several selected: -
|
|
30024
|
+
// Only the label under the pointer moves, even with several selected: -labels
|
|
29026
30025
|
// writes one value to every id it is given, so a group drag would set them
|
|
29027
30026
|
// all to the same absolute offset rather than nudging each by its own delta.
|
|
29028
30027
|
function commitOffsetDrag(o) {
|
|
@@ -29166,7 +30165,7 @@
|
|
|
29166
30165
|
}
|
|
29167
30166
|
|
|
29168
30167
|
function dragging() {
|
|
29169
|
-
return !!(drag || textDrag || handleDrag || blockDrag);
|
|
30168
|
+
return !!(drag || textDrag || handleDrag || blockDrag || selectDrag);
|
|
29170
30169
|
}
|
|
29171
30170
|
|
|
29172
30171
|
// The exception is about the pointer being on the glyphs, not about the mode:
|
|
@@ -29284,6 +30283,12 @@
|
|
|
29284
30283
|
resetHandle(hoverHandle);
|
|
29285
30284
|
return;
|
|
29286
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
|
+
}
|
|
29287
30292
|
// Double-clicking a label reaches straight into its text, which is the
|
|
29288
30293
|
// gesture most editors use and saves selecting it first. The single-click
|
|
29289
30294
|
// route got there already if the label was the selection; this covers the
|
|
@@ -30411,7 +31416,7 @@
|
|
|
30411
31416
|
canv.setAttribute('height', h);
|
|
30412
31417
|
ctx.scale(w / tw, h / th);
|
|
30413
31418
|
if (o.background) {
|
|
30414
|
-
ctx.fillStyle = o.background;
|
|
31419
|
+
ctx.fillStyle = getCanvasColor(o.background);
|
|
30415
31420
|
ctx.fillRect(0, 0, tw, th);
|
|
30416
31421
|
}
|
|
30417
31422
|
if (o.type == 'dots' || o.type == 'squares') makeDotFill(o, ctx, 1);
|
|
@@ -30475,10 +31480,16 @@
|
|
|
30475
31480
|
};
|
|
30476
31481
|
}
|
|
30477
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;
|
|
31487
|
+
}
|
|
31488
|
+
|
|
30478
31489
|
function makeDashFill(o, ctx, res) {
|
|
30479
31490
|
var x = 0;
|
|
30480
31491
|
for (var i=0; i<o.colors.length; i++) {
|
|
30481
|
-
ctx.fillStyle = o.colors[i];
|
|
31492
|
+
ctx.fillStyle = getCanvasColor(o.colors[i]);
|
|
30482
31493
|
ctx.fillRect(x, 0, o.width * res, o.dashes[0] * res);
|
|
30483
31494
|
x += res * (o.spacing + o.width);
|
|
30484
31495
|
}
|
|
@@ -30493,7 +31504,7 @@
|
|
|
30493
31504
|
var x = 0, y = 0;
|
|
30494
31505
|
for (var i=0; i<dots; i++) {
|
|
30495
31506
|
if (o.type == 'dots') ctx.beginPath();
|
|
30496
|
-
ctx.fillStyle = o.colors[(i + Math.floor(i / n)) % n];
|
|
31507
|
+
ctx.fillStyle = getCanvasColor(o.colors[(i + Math.floor(i / n)) % n]);
|
|
30497
31508
|
if (o.type == 'dots') {
|
|
30498
31509
|
ctx.arc(x + r, y + r, r, 0, Math.PI * 2);
|
|
30499
31510
|
} else {
|
|
@@ -30510,8 +31521,8 @@
|
|
|
30510
31521
|
var w;
|
|
30511
31522
|
for (var i=0, x=0; i<o.widths.length; i++) {
|
|
30512
31523
|
w = o.widths[i] * res;
|
|
30513
|
-
ctx.fillStyle = o.colors[i];
|
|
30514
|
-
ctx.fillRect(x, 0,
|
|
31524
|
+
ctx.fillStyle = getCanvasColor(o.colors[i]);
|
|
31525
|
+
ctx.fillRect(x, 0, w, h);
|
|
30515
31526
|
x += w;
|
|
30516
31527
|
}
|
|
30517
31528
|
}
|