mapshaper 0.7.69 → 0.7.71
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 +1707 -844
- package/package.json +1 -1
- package/www/mapshaper-gui.js +1949 -272
- package/www/mapshaper.js +1707 -844
- package/www/page.css +39 -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,
|
|
@@ -8860,30 +8860,6 @@
|
|
|
8860
8860
|
}
|
|
8861
8861
|
}
|
|
8862
8862
|
|
|
8863
|
-
// Test if adding point p to a sequence of points (in pixel coords)
|
|
8864
|
-
// would result in a polyline that deviates from a straight line by
|
|
8865
|
-
// more than a given number of pixels
|
|
8866
|
-
//
|
|
8867
|
-
function pointExceedsTolerance(p, points, tolerance) {
|
|
8868
|
-
if (points.length < 2) return false;
|
|
8869
|
-
var p1 = points[0], p2, dist, angle;
|
|
8870
|
-
for (var i=1; i<points.length; i++) {
|
|
8871
|
-
p2 = points[i];
|
|
8872
|
-
dist = Math.sqrt(geom.pointSegDistSq(p2[0], p2[1], p1[0], p1[1], p[0], p[1]));
|
|
8873
|
-
if (dist > tolerance) return true;
|
|
8874
|
-
}
|
|
8875
|
-
return false;
|
|
8876
|
-
}
|
|
8877
|
-
|
|
8878
|
-
function getAvgPoint(points) {
|
|
8879
|
-
var x=0, y=0;
|
|
8880
|
-
for (var i=0; i<points.length; i++) {
|
|
8881
|
-
x += points[i][0];
|
|
8882
|
-
y += points[i][1];
|
|
8883
|
-
}
|
|
8884
|
-
return [x/points.length, y/points.length];
|
|
8885
|
-
}
|
|
8886
|
-
|
|
8887
8863
|
function setZ(lyr, z) {
|
|
8888
8864
|
lyr.gui.source.dataset.arcs.setRetainedInterval(z);
|
|
8889
8865
|
if (isProjectedLayer(lyr)) {
|
|
@@ -11518,9 +11494,12 @@
|
|
|
11518
11494
|
}
|
|
11519
11495
|
|
|
11520
11496
|
if (e.deleteVertex || e.deletePoint || copyable || e.deleteFeature ||
|
|
11521
|
-
e.flipLabel) {
|
|
11497
|
+
e.flipLabel || e.boldText) {
|
|
11522
11498
|
|
|
11523
11499
|
addMenuLabel('actions');
|
|
11500
|
+
if (e.boldText) {
|
|
11501
|
+
addMenuItem(e.boldText.label, e.boldText.run);
|
|
11502
|
+
}
|
|
11524
11503
|
if (e.deleteVertex) {
|
|
11525
11504
|
addMenuItem('delete vertex', e.deleteVertex);
|
|
11526
11505
|
}
|
|
@@ -12904,6 +12883,11 @@
|
|
|
12904
12883
|
if (cmd.name == 'style' || cmd.name == 'svg-style') {
|
|
12905
12884
|
return getStyleCullInfo(opts, isStyleProperty);
|
|
12906
12885
|
}
|
|
12886
|
+
// Adding a label or a labeled copy of a layer is not a style edit, and can
|
|
12887
|
+
// not be superseded by one.
|
|
12888
|
+
if (cmd.name == 'labels' && !('coordinates' in opts) && !opts.no_replace) {
|
|
12889
|
+
return getStyleCullInfo(getLabelStyleOpts(opts), isStyleProperty);
|
|
12890
|
+
}
|
|
12907
12891
|
if (cmd.name == 'classify') {
|
|
12908
12892
|
return getClassifyCullInfo(opts);
|
|
12909
12893
|
}
|
|
@@ -12923,6 +12907,16 @@
|
|
|
12923
12907
|
};
|
|
12924
12908
|
}
|
|
12925
12909
|
|
|
12910
|
+
// -labels text= writes the label-text property.
|
|
12911
|
+
function getLabelStyleOpts(opts) {
|
|
12912
|
+
var o = Object.assign({}, opts);
|
|
12913
|
+
if ('text' in o) {
|
|
12914
|
+
o['label-text'] = o.text;
|
|
12915
|
+
delete o.text;
|
|
12916
|
+
}
|
|
12917
|
+
return o;
|
|
12918
|
+
}
|
|
12919
|
+
|
|
12926
12920
|
function getStyleFields$2(opts, isStyleProperty) {
|
|
12927
12921
|
var fields = [];
|
|
12928
12922
|
Object.keys(opts).forEach(function(name) {
|
|
@@ -13448,10 +13442,10 @@
|
|
|
13448
13442
|
|
|
13449
13443
|
gui.on('path_extend', function(e) {
|
|
13450
13444
|
var redo = function() {
|
|
13451
|
-
gui.dispatchEvent('redo_path_extend', {
|
|
13445
|
+
gui.dispatchEvent('redo_path_extend', {points: e.points, shapes: e.shapes2});
|
|
13452
13446
|
};
|
|
13453
13447
|
var undo = function() {
|
|
13454
|
-
gui.dispatchEvent('undo_path_extend', {shapes: e.shapes1});
|
|
13448
|
+
gui.dispatchEvent('undo_path_extend', {points: e.points, shapes: e.shapes1});
|
|
13455
13449
|
};
|
|
13456
13450
|
addHistoryState(undo, redo);
|
|
13457
13451
|
});
|
|
@@ -13921,6 +13915,11 @@
|
|
|
13921
13915
|
var SOFT_BREAK = '<wbr>';
|
|
13922
13916
|
var SOFT_BREAK_RXP = /<wbr>/gi;
|
|
13923
13917
|
|
|
13918
|
+
// A bold stretch, which is the other markup label text carries. See
|
|
13919
|
+
// svg-label-markup.mjs.
|
|
13920
|
+
var BOLD_OPEN = '<b>';
|
|
13921
|
+
var BOLD_CLOSE = '</b>';
|
|
13922
|
+
|
|
13924
13923
|
// How a label lays out its line breaks, which is not the same for the two kinds.
|
|
13925
13924
|
// An anchored label stacks its lines with <tspan>. A path label joins them with
|
|
13926
13925
|
// a space, because a <tspan> inside a <textPath> advances along the curve
|
|
@@ -13956,6 +13955,66 @@
|
|
|
13956
13955
|
return {text: text, breaks: breaks};
|
|
13957
13956
|
}
|
|
13958
13957
|
|
|
13958
|
+
// Data value -> {text, breaks, bold}: the string the user edits, the offsets
|
|
13959
|
+
// in it where the stored value has a soft break, and its bold stretches as
|
|
13960
|
+
// [start, end) offsets into it (see gui-label-bold.mjs).
|
|
13961
|
+
//
|
|
13962
|
+
// The two kinds of markup are independent -- a bold stretch can cross a soft
|
|
13963
|
+
// break, and a soft break can sit at either end of one -- so the bold is read
|
|
13964
|
+
// first and its offsets are moved as the soft breaks come out.
|
|
13965
|
+
function readLabelValue(val) {
|
|
13966
|
+
var str = val === null || val === undefined ? '' : String(val).replace(ANY_NEWLINE, '\n');
|
|
13967
|
+
var o = internal.svg.parseBoldMarkup(str);
|
|
13968
|
+
var starts = [], m;
|
|
13969
|
+
SOFT_BREAK_RXP.lastIndex = 0;
|
|
13970
|
+
while ((m = SOFT_BREAK_RXP.exec(o.text))) starts.push(m.index);
|
|
13971
|
+
return {
|
|
13972
|
+
text: o.text.replace(SOFT_BREAK_RXP, ''),
|
|
13973
|
+
breaks: starts.map(function(start, i) {
|
|
13974
|
+
return start - i * SOFT_BREAK.length;
|
|
13975
|
+
}),
|
|
13976
|
+
bold: internal.svg.normalizeBoldRanges(o.bold.map(function(r) {
|
|
13977
|
+
return [removeSoftBreakOffset(r[0], starts), removeSoftBreakOffset(r[1], starts)];
|
|
13978
|
+
}))
|
|
13979
|
+
};
|
|
13980
|
+
}
|
|
13981
|
+
|
|
13982
|
+
// An offset into text with soft breaks in it -> the same place once they are
|
|
13983
|
+
// removed. One that falls inside a marker goes to its start.
|
|
13984
|
+
function removeSoftBreakOffset(x, starts) {
|
|
13985
|
+
var shift = 0;
|
|
13986
|
+
for (var i = 0; i < starts.length && starts[i] < x; i++) {
|
|
13987
|
+
if (x < starts[i] + SOFT_BREAK.length) return starts[i] - shift;
|
|
13988
|
+
shift += SOFT_BREAK.length;
|
|
13989
|
+
}
|
|
13990
|
+
return x - shift;
|
|
13991
|
+
}
|
|
13992
|
+
|
|
13993
|
+
// The string the user edits, its soft breaks and its bold ranges -> the value
|
|
13994
|
+
// to store, with both kinds of markup written in. Where they meet, a bold
|
|
13995
|
+
// stretch closes before a soft break and opens after one, so that the break
|
|
13996
|
+
// is never inside a pair of tags it is not part of.
|
|
13997
|
+
function writeLabelValue(text, breaks, bold) {
|
|
13998
|
+
var marks = [];
|
|
13999
|
+
(bold || []).forEach(function(r) {
|
|
14000
|
+
marks.push({pos: r[0], order: 2, str: BOLD_OPEN});
|
|
14001
|
+
marks.push({pos: r[1], order: 0, str: BOLD_CLOSE});
|
|
14002
|
+
});
|
|
14003
|
+
(breaks || []).forEach(function(pos) {
|
|
14004
|
+
marks.push({pos: pos, order: 1, str: SOFT_BREAK});
|
|
14005
|
+
});
|
|
14006
|
+
if (marks.length === 0) return text;
|
|
14007
|
+
marks.sort(function(a, b) {
|
|
14008
|
+
return a.pos - b.pos || a.order - b.order;
|
|
14009
|
+
});
|
|
14010
|
+
var out = '', prev = 0;
|
|
14011
|
+
marks.forEach(function(mark) {
|
|
14012
|
+
out += text.substring(prev, mark.pos) + mark.str;
|
|
14013
|
+
prev = mark.pos;
|
|
14014
|
+
});
|
|
14015
|
+
return out + text.substring(prev);
|
|
14016
|
+
}
|
|
14017
|
+
|
|
13959
14018
|
// The string the user edits, plus soft breaks at @breaks (ascending offsets
|
|
13960
14019
|
// into it) -> the same string with the markers written in.
|
|
13961
14020
|
function insertSoftBreaks(text, breaks) {
|
|
@@ -14136,6 +14195,29 @@
|
|
|
14136
14195
|
return Math.min(renderedIndex, length);
|
|
14137
14196
|
}
|
|
14138
14197
|
|
|
14198
|
+
// The word a double-click at edited index @i selects, as [start, end), or an
|
|
14199
|
+
// empty range at @i for a click between words. A click on the right half of a
|
|
14200
|
+
// word's last letter lands on the index after it, which is still that word.
|
|
14201
|
+
//
|
|
14202
|
+
// Hyphens and apostrophes count as part of a word, so that "cease-fire" and
|
|
14203
|
+
// "Israel's" are one word each, as they are one name on the map.
|
|
14204
|
+
function getWordRange(text, i) {
|
|
14205
|
+
var at = isWordChar(text.charAt(i)) ? i : isWordChar(text.charAt(i - 1)) ? i - 1 : -1;
|
|
14206
|
+
var a, b;
|
|
14207
|
+
if (at < 0) return [i, i];
|
|
14208
|
+
a = at;
|
|
14209
|
+
b = at + 1;
|
|
14210
|
+
while (a > 0 && isWordChar(text.charAt(a - 1))) a--;
|
|
14211
|
+
while (b < text.length && isWordChar(text.charAt(b))) b++;
|
|
14212
|
+
return [a, b];
|
|
14213
|
+
}
|
|
14214
|
+
|
|
14215
|
+
var WORD_CHAR_RXP = /[\p{L}\p{N}\p{M}'\u2019-]/u;
|
|
14216
|
+
|
|
14217
|
+
function isWordChar(c) {
|
|
14218
|
+
return !!c && WORD_CHAR_RXP.test(c);
|
|
14219
|
+
}
|
|
14220
|
+
|
|
14139
14221
|
// Strips the placeholder back out, so that a value read off a rendered node is
|
|
14140
14222
|
// not mistaken for text the user typed.
|
|
14141
14223
|
function stripPlaceholder(str) {
|
|
@@ -14200,7 +14282,7 @@
|
|
|
14200
14282
|
function getAddLabelCommand(coords, opts) {
|
|
14201
14283
|
var o = opts || {};
|
|
14202
14284
|
var target = o.target || {mode: 'new', newLayerName: DEFAULT_LABEL_LAYER_NAME};
|
|
14203
|
-
var parts = ['-
|
|
14285
|
+
var parts = ['-labels', 'coordinates=' + formatCoords(coords)];
|
|
14204
14286
|
if (o.text) {
|
|
14205
14287
|
// A real newline would break the command parser, so encodeLabelText()
|
|
14206
14288
|
// writes the two-character escape the label renderer also accepts.
|
|
@@ -14267,7 +14349,7 @@
|
|
|
14267
14349
|
// id: feature id of the label
|
|
14268
14350
|
// target: layer name, or null to use the current target
|
|
14269
14351
|
function getLabelPlacementCommand(opts) {
|
|
14270
|
-
var parts = ['-
|
|
14352
|
+
var parts = ['-labels', 'label-start-offset=' + opts.offset];
|
|
14271
14353
|
if (opts.anchor) {
|
|
14272
14354
|
parts.push('text-anchor=' + opts.anchor);
|
|
14273
14355
|
}
|
|
@@ -14287,7 +14369,7 @@
|
|
|
14287
14369
|
// with and the position goes -- "stop taking a position, carry these offsets
|
|
14288
14370
|
// instead", which is one command because it is one edit.
|
|
14289
14371
|
//
|
|
14290
|
-
// label-pos= is always written, even by a label that has none. -
|
|
14372
|
+
// label-pos= is always written, even by a label that has none. -labels reads an
|
|
14291
14373
|
// empty value as "remove this", and removing a property no record carries adds
|
|
14292
14374
|
// nothing to the layer, so there is nothing to be gained by asking first.
|
|
14293
14375
|
//
|
|
@@ -14297,7 +14379,7 @@
|
|
|
14297
14379
|
// target: layer name, or null to use the current target
|
|
14298
14380
|
// via: (optional) the callout-via the text carried with it
|
|
14299
14381
|
function getLabelOffsetCommand(opts) {
|
|
14300
|
-
var parts = ['-
|
|
14382
|
+
var parts = ['-labels', 'dx=' + opts.dx, 'dy=' + opts.dy,
|
|
14301
14383
|
'text-anchor=' + opts.anchor, 'label-pos='];
|
|
14302
14384
|
if (opts.via) parts.push('callout-via=' + quoteCommandValue(opts.via));
|
|
14303
14385
|
parts.push('ids=' + opts.id);
|
|
@@ -14308,16 +14390,16 @@
|
|
|
14308
14390
|
// The command a drag on one of a label's handles writes, run once on release.
|
|
14309
14391
|
//
|
|
14310
14392
|
// values: {field: value}, where '' removes the field -- which is what
|
|
14311
|
-
// -
|
|
14393
|
+
// -labels reads an empty value as, and how a handle goes back to automatic
|
|
14312
14394
|
// id: feature id of the label
|
|
14313
14395
|
// opts:
|
|
14314
14396
|
// target: layer name, or null to use the current target
|
|
14315
14397
|
// text: (optional) rewrapped label-text, with real newlines and soft
|
|
14316
|
-
// breaks. A second -
|
|
14398
|
+
// breaks. A second -labels in the same string, so that a new width and
|
|
14317
14399
|
// the lines it breaks into are one undo step.
|
|
14318
14400
|
function getLabelStyleCommand(values, id, opts) {
|
|
14319
14401
|
var o = opts || {};
|
|
14320
|
-
var parts = ['-
|
|
14402
|
+
var parts = ['-labels'];
|
|
14321
14403
|
Object.keys(values).forEach(function(name) {
|
|
14322
14404
|
var val = values[name];
|
|
14323
14405
|
parts.push(name + '=' + (val === '' ? '' : quoteCommandValue(String(val))));
|
|
@@ -14339,7 +14421,7 @@
|
|
|
14339
14421
|
function getLabelTextCommand(text, id, target) {
|
|
14340
14422
|
// A real newline would break the command parser, so encodeLabelText() writes
|
|
14341
14423
|
// the two-character escape that the label renderer also accepts.
|
|
14342
|
-
var parts = ['-
|
|
14424
|
+
var parts = ['-labels', 'text=' + quoteCommandValue(encodeLabelText(text))];
|
|
14343
14425
|
parts.push('ids=' + id);
|
|
14344
14426
|
if (target) parts.push('target=' + quoteCommandValue(target));
|
|
14345
14427
|
return parts.join(' ');
|
|
@@ -15454,6 +15536,19 @@
|
|
|
15454
15536
|
return cachedFontStyles[fontName];
|
|
15455
15537
|
}
|
|
15456
15538
|
|
|
15539
|
+
// Whether words in @fontName can be set in bold: a bold the browser
|
|
15540
|
+
// synthesizes is not one the Node measurer can match, so wrapping and the
|
|
15541
|
+
// path-fit check would disagree with what is drawn. A font that cannot be
|
|
15542
|
+
// identified is given the benefit of the doubt -- the browser finds whatever
|
|
15543
|
+
// bold it has, and there is nothing to check against.
|
|
15544
|
+
function fontHasBoldFace(fontName) {
|
|
15545
|
+
var variants = fontName ? getFontStyleVariants(fontName) : null;
|
|
15546
|
+
if (!variants || variants.length === 0) return true;
|
|
15547
|
+
return variants.some(function(variant) {
|
|
15548
|
+
return Number(variant.weight) >= 600;
|
|
15549
|
+
});
|
|
15550
|
+
}
|
|
15551
|
+
|
|
15457
15552
|
// The installed font the browser draws unfonted text in, or '' if it cannot
|
|
15458
15553
|
// be worked out.
|
|
15459
15554
|
//
|
|
@@ -15511,6 +15606,11 @@
|
|
|
15511
15606
|
return drawnFont || '';
|
|
15512
15607
|
}
|
|
15513
15608
|
|
|
15609
|
+
// Whether @fontName is one of the fonts above and installed here.
|
|
15610
|
+
function isInstalledFont(fontName) {
|
|
15611
|
+
return getInstalledFontNames().indexOf(fontName) > -1;
|
|
15612
|
+
}
|
|
15613
|
+
|
|
15514
15614
|
function getInstalledFontNames() {
|
|
15515
15615
|
var names = [];
|
|
15516
15616
|
getInstalledFonts().forEach(function(group) {
|
|
@@ -15879,7 +15979,7 @@
|
|
|
15879
15979
|
var tile = El('div')
|
|
15880
15980
|
.addClass('label-color-preset')
|
|
15881
15981
|
.attr('role', 'button')
|
|
15882
|
-
.attr('
|
|
15982
|
+
.attr('aria-label', color)
|
|
15883
15983
|
.appendTo(rowEl)
|
|
15884
15984
|
.css('background-color', color);
|
|
15885
15985
|
if (row.length == 16 && i == 8) tile.addClass('label-color-preset-group-start');
|
|
@@ -16356,7 +16456,7 @@
|
|
|
16356
16456
|
// in a menu that is only on screen while it is being used, and a
|
|
16357
16457
|
// destructive action is better for waiting until intent is shown.
|
|
16358
16458
|
El('div').addClass('label-saved-style-delete').attr('role', 'button')
|
|
16359
|
-
.attr('
|
|
16459
|
+
.attr('aria-label', 'Delete this ' + opts.styleLabel)
|
|
16360
16460
|
.appendTo(itemEl).html('×')
|
|
16361
16461
|
.on('click', function() {
|
|
16362
16462
|
deleteStyle(item);
|
|
@@ -16429,7 +16529,7 @@
|
|
|
16429
16529
|
// Escape. Whether that means giving up the keyboard is the
|
|
16430
16530
|
// caller's question: the field cannot know what else wants it.
|
|
16431
16531
|
// decimals how finely a typed size is kept (default 1)
|
|
16432
|
-
// title
|
|
16532
|
+
// title the field's accessible name (not a tooltip)
|
|
16433
16533
|
function SizeField(parent, opts) {
|
|
16434
16534
|
var o = Object.assign({min: 1, max: 999, step: 1, bigStep: 10}, opts || {});
|
|
16435
16535
|
var box = El('div').addClass('size-field').appendTo(parent);
|
|
@@ -16442,7 +16542,7 @@
|
|
|
16442
16542
|
var dirty = false; // holds typing that has not been committed
|
|
16443
16543
|
var disabled = false;
|
|
16444
16544
|
|
|
16445
|
-
if (o.title) input.attr('
|
|
16545
|
+
if (o.title) input.attr('aria-label', o.title);
|
|
16446
16546
|
|
|
16447
16547
|
// While the caret is in this field the keyboard belongs to it. Without this
|
|
16448
16548
|
// the GUI's own handlers see the keystrokes: Escape would disarm the tool,
|
|
@@ -16616,6 +16716,15 @@
|
|
|
16616
16716
|
!/^(button|checkbox|radio|submit)$/.test(node.type);
|
|
16617
16717
|
}
|
|
16618
16718
|
|
|
16719
|
+
// Whether clicking this node puts a native menu on screen, which must then be
|
|
16720
|
+
// left holding the focus: a menu opens on mousedown, so a panel that lets go of
|
|
16721
|
+
// focus on the click that follows closes it again -- it flashes open and shut.
|
|
16722
|
+
// OPTION counts because a browser that reports the chosen option as the click
|
|
16723
|
+
// target is reporting a menu interaction either way.
|
|
16724
|
+
function opensAMenu(node) {
|
|
16725
|
+
return !!node && /^(SELECT|OPTION)$/.test(node.nodeName);
|
|
16726
|
+
}
|
|
16727
|
+
|
|
16619
16728
|
// While the caret is in one of @panel's fields, the keyboard belongs to that
|
|
16620
16729
|
// field. Without this the GUI's own handlers see the keystrokes: a Backspace
|
|
16621
16730
|
// typed into a field takes back the last knot of a curve being drawn, and an
|
|
@@ -16723,6 +16832,53 @@
|
|
|
16723
16832
|
return section;
|
|
16724
16833
|
}
|
|
16725
16834
|
|
|
16835
|
+
// A switch: a track with a knob that sits left when off and right when on,
|
|
16836
|
+
// which is the direction users expect and the only thing that says which
|
|
16837
|
+
// state is which without a label for each. A third state says that the
|
|
16838
|
+
// features it is asking about disagree -- the knob sits over the join of a
|
|
16839
|
+
// half-and-half track, which is the only control that has to show "some of
|
|
16840
|
+
// them" rather than a value. A section's switch goes in its heading row, and
|
|
16841
|
+
// the section shows only its heading while it is off (the .collapsed class).
|
|
16842
|
+
//
|
|
16843
|
+
// role=checkbox rather than switch, which is what it looks like: 'mixed' is a
|
|
16844
|
+
// legal aria-checked value for a checkbox and not for a switch, and a switch
|
|
16845
|
+
// reporting a mixed selection as unchecked would be telling a screen reader
|
|
16846
|
+
// the one thing the third state exists to avoid saying.
|
|
16847
|
+
//
|
|
16848
|
+
// opts.onChange(on) the switch was clicked
|
|
16849
|
+
// opts.title the switch's accessible name (not a tooltip)
|
|
16850
|
+
// opts.className
|
|
16851
|
+
function makePanelToggle(parent, opts) {
|
|
16852
|
+
var track = El('div').addClass('label-toggle').attr('role', 'checkbox').appendTo(parent);
|
|
16853
|
+
var state = 'off';
|
|
16854
|
+
if (opts.className) track.addClass(opts.className);
|
|
16855
|
+
var disabled = false;
|
|
16856
|
+
El('div').addClass('label-toggle-knob').appendTo(track);
|
|
16857
|
+
if (opts.title) track.attr('aria-label', opts.title);
|
|
16858
|
+
// A click on a mixed switch turns everything on. It is the convention, and
|
|
16859
|
+
// it is the reading that reaches a state the switch can describe: the next
|
|
16860
|
+
// click then turns everything off, so both are one click away.
|
|
16861
|
+
track.on('click', function() {
|
|
16862
|
+
if (disabled) return;
|
|
16863
|
+
opts.onChange(state != 'on');
|
|
16864
|
+
});
|
|
16865
|
+
return {
|
|
16866
|
+
// val: 'on', 'off' or 'mixed'; anything else reads as off
|
|
16867
|
+
setState: function(val) {
|
|
16868
|
+
state = val == 'on' || val == 'mixed' ? val : 'off';
|
|
16869
|
+
track.classed('on', state == 'on')
|
|
16870
|
+
.classed('mixed', state == 'mixed')
|
|
16871
|
+
.attr('aria-checked', state == 'mixed' ? 'mixed' :
|
|
16872
|
+
state == 'on' ? 'true' : 'false');
|
|
16873
|
+
},
|
|
16874
|
+
setDisabled: function(off) {
|
|
16875
|
+
disabled = !!off;
|
|
16876
|
+
track.classed('disabled', disabled)
|
|
16877
|
+
.attr('aria-disabled', disabled ? 'true' : 'false');
|
|
16878
|
+
}
|
|
16879
|
+
};
|
|
16880
|
+
}
|
|
16881
|
+
|
|
16726
16882
|
// Deliberately not focusable: the GUI is pointer-only, so a tab stop here
|
|
16727
16883
|
// would lead into a control the keyboard cannot then operate. See the focus
|
|
16728
16884
|
// note in page.css.
|
|
@@ -16783,7 +16939,7 @@
|
|
|
16783
16939
|
// opacity qualifies the colour, and one border says so where two fields side
|
|
16784
16940
|
// by side said they were separate settings. It also leaves the narrow column
|
|
16785
16941
|
// of the row free for a field that needs it. The opacity has no caption; its
|
|
16786
|
-
// percent sign
|
|
16942
|
+
// percent sign says what it is.
|
|
16787
16943
|
//
|
|
16788
16944
|
// Returns the field's box; the opacity input is made by makeOpacityInput().
|
|
16789
16945
|
function makeColorOpacityField(parent, chit, input, opacityOpts) {
|
|
@@ -16799,6 +16955,8 @@
|
|
|
16799
16955
|
// opts.onColor(hex) a colour was typed, picked or previewed to a finish
|
|
16800
16956
|
// opts.onOpacity(frac) a usable percentage was typed
|
|
16801
16957
|
// opts.revert() one that was not, so put the row back as it was
|
|
16958
|
+
// opts.noOpacity a colour with no opacity of its own, like a pattern's,
|
|
16959
|
+
// which fill-opacity fades along with its background
|
|
16802
16960
|
//
|
|
16803
16961
|
// control.aside is the row's narrow column, empty, for a field that belongs
|
|
16804
16962
|
// beside the colour -- a stroke's width, say.
|
|
@@ -16830,16 +16988,20 @@
|
|
|
16830
16988
|
control.picker.toggle();
|
|
16831
16989
|
});
|
|
16832
16990
|
control.input = El('input').attr('type', 'text')
|
|
16833
|
-
.attr('
|
|
16991
|
+
.attr('aria-label', opts.label + ' color')
|
|
16834
16992
|
.on('change', function() {
|
|
16835
16993
|
var color = control.input.node().value.trim();
|
|
16836
16994
|
if (isHexColor(color)) control.picker.setColor(color);
|
|
16837
16995
|
opts.onColor(color);
|
|
16838
16996
|
});
|
|
16839
|
-
|
|
16840
|
-
|
|
16841
|
-
|
|
16842
|
-
|
|
16997
|
+
if (opts.noOpacity) {
|
|
16998
|
+
makeColorField(colorCell, control.chit, control.input);
|
|
16999
|
+
} else {
|
|
17000
|
+
control.opacity = makeColorOpacityField(colorCell, control.chit, control.input, {
|
|
17001
|
+
onSet: opts.onOpacity,
|
|
17002
|
+
revert: opts.revert
|
|
17003
|
+
}).opacity;
|
|
17004
|
+
}
|
|
16843
17005
|
control.picker = new ColorPicker(colorCell, {
|
|
16844
17006
|
presetRows: layerColorPresetRows,
|
|
16845
17007
|
// A drag in the picker shows on the map's own terms -- the swatch and the
|
|
@@ -16862,7 +17024,7 @@
|
|
|
16862
17024
|
// opts.revert() it was not, so put back what the field was showing
|
|
16863
17025
|
function makeOpacityInput(parent, opts) {
|
|
16864
17026
|
var input = El('input').attr('type', 'text').addClass('label-opacity-input')
|
|
16865
|
-
.attr('
|
|
17027
|
+
.attr('aria-label', 'Opacity, 0-100%')
|
|
16866
17028
|
.appendTo(parent)
|
|
16867
17029
|
.on('change', function() {
|
|
16868
17030
|
var val = parseOpacityValue(input.node().value);
|
|
@@ -16879,13 +17041,13 @@
|
|
|
16879
17041
|
//
|
|
16880
17042
|
// The style panel is worth having open before there is anything to point it at:
|
|
16881
17043
|
// pick a font, then place labels in it. Values set with nothing selected are
|
|
16882
|
-
// held here rather than written to a layer, and -
|
|
17044
|
+
// held here rather than written to a layer, and -labels coordinates= writes them when a
|
|
16883
17045
|
// label finally exists -- so this is a tool default, not data, and it never
|
|
16884
17046
|
// needs its own undo step.
|
|
16885
17047
|
//
|
|
16886
17048
|
// See docs/development/label-tool-design.md.
|
|
16887
17049
|
|
|
16888
|
-
// The style properties -
|
|
17050
|
+
// The style properties -labels accepts. A panel control whose property is
|
|
16889
17051
|
// missing from this list would appear to do nothing when set with no selection,
|
|
16890
17052
|
// so the list is checked rather than assumed.
|
|
16891
17053
|
var NEW_LABEL_STYLE_FIELDS = [
|
|
@@ -16907,7 +17069,7 @@
|
|
|
16907
17069
|
//
|
|
16908
17070
|
// A blank or zero value removes the field instead of setting it: that is how
|
|
16909
17071
|
// the panel says "no icon" (icon='', icon-size=0) and "no inline css", and
|
|
16910
|
-
// carrying those through to -
|
|
17072
|
+
// carrying those through to -labels would write properties that mean
|
|
16911
17073
|
// nothing.
|
|
16912
17074
|
function mergeStyleValues(style, values) {
|
|
16913
17075
|
var out = Object.assign({}, style);
|
|
@@ -16933,7 +17095,7 @@
|
|
|
16933
17095
|
// somewhere and typing looks like it should do, and it is the position an icon
|
|
16934
17096
|
// is drawn to sit behind.
|
|
16935
17097
|
//
|
|
16936
|
-
// Only the tool defaults this. -
|
|
17098
|
+
// Only the tool defaults this. -labels coordinates= with no position still creates a
|
|
16937
17099
|
// label without one, so the default is a choice this tool makes and passes on
|
|
16938
17100
|
// explicitly, not a meaning the command gives to its absence.
|
|
16939
17101
|
var DEFAULT_NEW_LABEL_STYLE = {'label-pos': 'c'};
|
|
@@ -17247,13 +17409,17 @@
|
|
|
17247
17409
|
|
|
17248
17410
|
// Offsets into @text (real newlines, no soft breaks) where a line starts
|
|
17249
17411
|
// because it was wrapped, in ascending order.
|
|
17250
|
-
|
|
17412
|
+
//
|
|
17413
|
+
// @bold: the text's bold stretches, as [start, end) offsets into it. A bold
|
|
17414
|
+
// word is wider, so it moves the breaks.
|
|
17415
|
+
function getSoftBreaks(text, rec, bold) {
|
|
17251
17416
|
var width = getLabelWrapWidth(rec);
|
|
17252
17417
|
var key, breaks;
|
|
17253
17418
|
if (!width || !text) return [];
|
|
17254
|
-
|
|
17419
|
+
bold = bold || [];
|
|
17420
|
+
key = getWrapKey(text, rec, bold);
|
|
17255
17421
|
if (cache$2.has(key)) return cache$2.get(key);
|
|
17256
|
-
breaks = measureSoftBreaks(text, rec, width);
|
|
17422
|
+
breaks = measureSoftBreaks(text, rec, width, bold);
|
|
17257
17423
|
if (cache$2.size >= CACHE_LIMIT) cache$2.clear();
|
|
17258
17424
|
cache$2.set(key, breaks);
|
|
17259
17425
|
return breaks;
|
|
@@ -17263,29 +17429,30 @@
|
|
|
17263
17429
|
// unchanged when neither the old value nor the new one has a soft break, so
|
|
17264
17430
|
// that a label that does not wrap keeps the line breaks it was written with.
|
|
17265
17431
|
function rewrapLabelValue(value, rec) {
|
|
17266
|
-
var o =
|
|
17267
|
-
var breaks = getSoftBreaks(o.text, rec);
|
|
17432
|
+
var o = readLabelValue(value);
|
|
17433
|
+
var breaks = getSoftBreaks(o.text, rec, o.bold);
|
|
17268
17434
|
if (sameSoftBreaks(o.breaks, breaks)) return value;
|
|
17269
|
-
return encodeLabelText(
|
|
17435
|
+
return encodeLabelText(writeLabelValue(o.text, breaks, o.bold));
|
|
17270
17436
|
}
|
|
17271
17437
|
|
|
17272
17438
|
function clearWrapCache() {
|
|
17273
17439
|
cache$2.clear();
|
|
17274
17440
|
}
|
|
17275
17441
|
|
|
17276
|
-
function getWrapKey(text, rec) {
|
|
17442
|
+
function getWrapKey(text, rec, bold) {
|
|
17277
17443
|
var parts = [text];
|
|
17278
17444
|
WRAP_FIELDS.forEach(function(name) {
|
|
17279
17445
|
var val = rec[name];
|
|
17280
17446
|
parts.push(val === null || val === undefined ? '' : String(val));
|
|
17281
17447
|
});
|
|
17282
17448
|
parts.push(rec['class'] || '');
|
|
17449
|
+
parts.push(bold.join(';'));
|
|
17283
17450
|
return parts.join('\u0000');
|
|
17284
17451
|
}
|
|
17285
17452
|
|
|
17286
|
-
function measureSoftBreaks(text, rec, width) {
|
|
17453
|
+
function measureSoftBreaks(text, rec, width, bold) {
|
|
17287
17454
|
var w = getWrapper();
|
|
17288
|
-
var style, fontSize,
|
|
17455
|
+
var style, fontSize, pieces, range;
|
|
17289
17456
|
if (!w) return [];
|
|
17290
17457
|
style = getLabelFontStyle(w, rec);
|
|
17291
17458
|
if (!style) return [];
|
|
@@ -17293,15 +17460,15 @@
|
|
|
17293
17460
|
w.div.style[name] = style[name] || '';
|
|
17294
17461
|
});
|
|
17295
17462
|
w.div.style.width = width + 'px';
|
|
17296
|
-
w.div
|
|
17297
|
-
node = w.div.firstChild;
|
|
17463
|
+
pieces = writeRuns(w.div, text, bold);
|
|
17298
17464
|
fontSize = parseFloat(style.fontSize) || 12;
|
|
17299
17465
|
range = document.createRange();
|
|
17300
17466
|
var breaks = findSoftBreaks(text, function(i) {
|
|
17467
|
+
var piece = findPiece(pieces, i);
|
|
17301
17468
|
var end = isHighSurrogate(text.charCodeAt(i)) ? i + 2 : i + 1;
|
|
17302
17469
|
var r;
|
|
17303
|
-
range.setStart(node, i);
|
|
17304
|
-
range.setEnd(node, Math.min(end,
|
|
17470
|
+
range.setStart(piece.node, i - piece.start);
|
|
17471
|
+
range.setEnd(piece.node, Math.min(end, piece.end) - piece.start);
|
|
17305
17472
|
r = range.getBoundingClientRect();
|
|
17306
17473
|
return r.height > 0 ? r.top : null;
|
|
17307
17474
|
}, fontSize * LINE_HEIGHT / 2);
|
|
@@ -17309,6 +17476,35 @@
|
|
|
17309
17476
|
return breaks;
|
|
17310
17477
|
}
|
|
17311
17478
|
|
|
17479
|
+
// Lays @text out in @div a run at a time, the bold ones in bold spans, and
|
|
17480
|
+
// returns each run's text node with the offsets it covers: [{node, start,
|
|
17481
|
+
// end}]. Offsets in the text become offsets in one of these nodes.
|
|
17482
|
+
function writeRuns(div, text, bold) {
|
|
17483
|
+
var pos = 0;
|
|
17484
|
+
div.textContent = '';
|
|
17485
|
+
return internal.svg.getBoldRuns(text, bold, 0, text.length).map(function(run) {
|
|
17486
|
+
var node = document.createTextNode(run.text);
|
|
17487
|
+
var span;
|
|
17488
|
+
if (run.bold) {
|
|
17489
|
+
span = document.createElement('span');
|
|
17490
|
+
span.style.fontWeight = internal.svg.LABEL_BOLD_WEIGHT;
|
|
17491
|
+
span.appendChild(node);
|
|
17492
|
+
div.appendChild(span);
|
|
17493
|
+
} else {
|
|
17494
|
+
div.appendChild(node);
|
|
17495
|
+
}
|
|
17496
|
+
pos += run.text.length;
|
|
17497
|
+
return {node: node, start: pos - run.text.length, end: pos};
|
|
17498
|
+
});
|
|
17499
|
+
}
|
|
17500
|
+
|
|
17501
|
+
function findPiece(pieces, i) {
|
|
17502
|
+
for (var j = 0; j < pieces.length; j++) {
|
|
17503
|
+
if (i < pieces[j].end) return pieces[j];
|
|
17504
|
+
}
|
|
17505
|
+
return pieces[pieces.length - 1];
|
|
17506
|
+
}
|
|
17507
|
+
|
|
17312
17508
|
function isHighSurrogate(c) {
|
|
17313
17509
|
return c >= 0xD800 && c <= 0xDBFF;
|
|
17314
17510
|
}
|
|
@@ -17359,6 +17555,80 @@
|
|
|
17359
17555
|
return wrapper;
|
|
17360
17556
|
}
|
|
17361
17557
|
|
|
17558
|
+
// Default sizes and colours of the label tool's anchor symbols, which differ by
|
|
17559
|
+
// shape: a star or a ring at the size of a circle reads as smaller, and the NYT
|
|
17560
|
+
// star is a logo, drawn in its own red.
|
|
17561
|
+
var iconDefaults = {
|
|
17562
|
+
circle: {size: 5},
|
|
17563
|
+
square: {size: 5},
|
|
17564
|
+
star: {size: 8},
|
|
17565
|
+
ring: {size: 8},
|
|
17566
|
+
'nyt-star': {size: 14, color: '#cc0000'}
|
|
17567
|
+
};
|
|
17568
|
+
var fallbackSize = 5;
|
|
17569
|
+
|
|
17570
|
+
function getDefaultIconSize(shape) {
|
|
17571
|
+
var o = iconDefaults[shape];
|
|
17572
|
+
return o ? o.size : fallbackSize;
|
|
17573
|
+
}
|
|
17574
|
+
|
|
17575
|
+
// '' for a shape drawn in whatever colour the label gives it.
|
|
17576
|
+
function getDefaultIconColor(shape) {
|
|
17577
|
+
var o = iconDefaults[shape];
|
|
17578
|
+
return o && o.color || '';
|
|
17579
|
+
}
|
|
17580
|
+
|
|
17581
|
+
// The icon-size and icon-color to write when labels whose symbol is @prevShape
|
|
17582
|
+
// ('' for none, or for a selection of mixed shapes) are given @shape, from the
|
|
17583
|
+
// @size and @color they share. A missing size is '' or 0, and a colour of ''
|
|
17584
|
+
// is none; null is a colour the labels do not agree on, which is left alone.
|
|
17585
|
+
//
|
|
17586
|
+
// A value that is the old shape's default follows the shape to its own
|
|
17587
|
+
// default, and a value the user chose is kept -- so switching shapes resizes a
|
|
17588
|
+
// symbol nobody sized, and never undoes a size somebody set. The colour is
|
|
17589
|
+
// returned only if it changes, as '' to remove it.
|
|
17590
|
+
function getIconShapeChange(shape, prevShape, size, color) {
|
|
17591
|
+
var out = {};
|
|
17592
|
+
var sizeVal = Number(size);
|
|
17593
|
+
var prevColor = getDefaultIconColor(prevShape);
|
|
17594
|
+
var nextColor = getDefaultIconColor(shape);
|
|
17595
|
+
if (!(sizeVal > 0) || prevShape && sizeVal == getDefaultIconSize(prevShape)) {
|
|
17596
|
+
out.size = getDefaultIconSize(shape);
|
|
17597
|
+
} else {
|
|
17598
|
+
out.size = sizeVal;
|
|
17599
|
+
}
|
|
17600
|
+
if (color !== null && sameColor$1(color, prevColor) && !sameColor$1(color, nextColor)) {
|
|
17601
|
+
out.color = nextColor;
|
|
17602
|
+
}
|
|
17603
|
+
return out;
|
|
17604
|
+
}
|
|
17605
|
+
|
|
17606
|
+
function sameColor$1(a, b) {
|
|
17607
|
+
return String(a || '').toLowerCase() == String(b || '').toLowerCase();
|
|
17608
|
+
}
|
|
17609
|
+
|
|
17610
|
+
// Features offered only to people at The New York Times, who are the ones with
|
|
17611
|
+
// its NYTFranklin typeface installed. This is a convenience, not access
|
|
17612
|
+
// control: anything gated here must still render for everyone, because a map
|
|
17613
|
+
// made with it can be opened anywhere.
|
|
17614
|
+
//
|
|
17615
|
+
// ?nyt=on or ?nyt=off in the page URL overrides the check, so that the gated
|
|
17616
|
+
// features can be tested (and hidden) on any machine.
|
|
17617
|
+
var NYT_FONT = 'NYTFranklin';
|
|
17618
|
+
var cached;
|
|
17619
|
+
|
|
17620
|
+
function isNytUser() {
|
|
17621
|
+
if (cached === undefined) cached = detectNytUser();
|
|
17622
|
+
return cached;
|
|
17623
|
+
}
|
|
17624
|
+
|
|
17625
|
+
function detectNytUser() {
|
|
17626
|
+
var flag = GUI.getUrlVars().nyt;
|
|
17627
|
+
if (flag == 'on' || flag === true) return true;
|
|
17628
|
+
if (flag == 'off' || flag === false) return false;
|
|
17629
|
+
return isInstalledFont(NYT_FONT);
|
|
17630
|
+
}
|
|
17631
|
+
|
|
17362
17632
|
var fontField = 'font-family';
|
|
17363
17633
|
var fontSizeField = 'font-size';
|
|
17364
17634
|
var fontStyleField = 'font-style';
|
|
@@ -17391,7 +17661,6 @@
|
|
|
17391
17661
|
var defaultFontWeight = '400';
|
|
17392
17662
|
var defaultLabelColor = '#000000';
|
|
17393
17663
|
var defaultIconColor = '#000000';
|
|
17394
|
-
var defaultIconSize = 5;
|
|
17395
17664
|
// The width a halo is switched on at: past the edge of the glyphs, so a
|
|
17396
17665
|
// stroke of twice this.
|
|
17397
17666
|
var defaultHaloWidth = 2;
|
|
@@ -17464,6 +17733,10 @@
|
|
|
17464
17733
|
name: 'star'
|
|
17465
17734
|
}, {
|
|
17466
17735
|
name: 'ring'
|
|
17736
|
+
}, {
|
|
17737
|
+
name: 'nyt-star',
|
|
17738
|
+
title: 'NYT star',
|
|
17739
|
+
nytOnly: true
|
|
17467
17740
|
}];
|
|
17468
17741
|
var defaultIconShape = 'circle';
|
|
17469
17742
|
// Alignment writes label-align rather than text-anchor, because text-anchor
|
|
@@ -17529,14 +17802,15 @@
|
|
|
17529
17802
|
elbow: '<path d="M3.5 12.5L8 4.5H13"></path>',
|
|
17530
17803
|
curve: '<path d="M3.5 12.5Q4.5 4.5 12.5 4"></path>',
|
|
17531
17804
|
none: '<path d="M3 8H13"></path>',
|
|
17532
|
-
arrow: '<path d="M6.
|
|
17805
|
+
arrow: '<path d="M6.84 8H13"></path><path class="fill" d="M2.5 8L8.7 6.05V9.95Z"></path>',
|
|
17533
17806
|
'open-arrow': '<path d="M3.2 8H13"></path><path d="M7.38 5.07L3.2 8L7.38 10.93"></path>'
|
|
17534
17807
|
};
|
|
17535
17808
|
var iconButtonSymbols = {
|
|
17536
17809
|
circle: '<circle cx="8" cy="8" r="4.25"></circle>',
|
|
17537
17810
|
square: '<rect x="4" y="4" width="8" height="8"></rect>',
|
|
17538
17811
|
ring: '<circle cx="8" cy="8" r="3.8"></circle>',
|
|
17539
|
-
star: '<path d="M8 3.2l1.18 2.92 3.14.22-2.42 2 .76 3.06L8 9.75 5.34 11.4l.76-3.06-2.42-2 3.14-.22L8 3.2z"></path>'
|
|
17812
|
+
star: '<path d="M8 3.2l1.18 2.92 3.14.22-2.42 2 .76 3.06L8 9.75 5.34 11.4l.76-3.06-2.42-2 3.14-.22L8 3.2z"></path>',
|
|
17813
|
+
'nyt-star': '<polygon transform="scale(0.4)" points="19.98 5.72 23.15 11.76 29.45 9.15 27.99 15.82 34.5 17.86 29.1 22.03 32.76 27.78 25.95 27.5 25.06 34.27 20.01 29.67 14.98 34.28 14.08 27.52 7.26 27.82 10.92 22.06 5.5 17.9 12 15.84 10.53 9.18 16.84 11.77"></polygon>'
|
|
17540
17814
|
};
|
|
17541
17815
|
|
|
17542
17816
|
function LabelTool(gui) {
|
|
@@ -17551,6 +17825,7 @@
|
|
|
17551
17825
|
// The shape the toggle turns back on, so that switching a symbol off and on
|
|
17552
17826
|
// again does not silently change a star into a circle.
|
|
17553
17827
|
var lastIconShape = defaultIconShape;
|
|
17828
|
+
var shownIconTypes = null;
|
|
17554
17829
|
// Likewise the halo's width, which is what switching one off removes.
|
|
17555
17830
|
var lastHaloWidth = defaultHaloWidth;
|
|
17556
17831
|
// And the callout's shape.
|
|
@@ -17684,7 +17959,7 @@
|
|
|
17684
17959
|
// number otherwise keep theirs, and so does a colour field, because the
|
|
17685
17960
|
// field beside it has one and "Color" says the percentage is its opacity.
|
|
17686
17961
|
var fontRow = El('div').addClass('label-style-row').appendTo(textSection);
|
|
17687
|
-
fontSelect = El('select').attr('
|
|
17962
|
+
fontSelect = El('select').attr('aria-label', 'Font').appendTo(fontRow).on('change', function() {
|
|
17688
17963
|
if (fontSelect.node().value) {
|
|
17689
17964
|
applyFont(fontSelect.node().value);
|
|
17690
17965
|
}
|
|
@@ -17692,7 +17967,7 @@
|
|
|
17692
17967
|
|
|
17693
17968
|
var styleSizeRow = El('div').addClass('label-style-row label-split-row').appendTo(textSection);
|
|
17694
17969
|
var fontStyleRow = El('div').addClass('label-split-cell label-font-style-row').appendTo(styleSizeRow);
|
|
17695
|
-
fontStyleSelect = El('select').attr('
|
|
17970
|
+
fontStyleSelect = El('select').attr('aria-label', 'Font style').appendTo(fontStyleRow).on('change', function() {
|
|
17696
17971
|
if (fontStyleSelect.node().value) {
|
|
17697
17972
|
applyFontStyleVariant(fontStyleSelect.node().value);
|
|
17698
17973
|
}
|
|
@@ -17718,7 +17993,7 @@
|
|
|
17718
17993
|
var textColorCell = El('div').addClass('label-split-cell label-color-row label-text-color-row').appendTo(colorRow);
|
|
17719
17994
|
El('span').appendTo(textColorCell).text('Color');
|
|
17720
17995
|
colorChit = El('div').addClass('label-color-chit').attr('role', 'button');
|
|
17721
|
-
colorInput = El('input').attr('type', 'text').attr('
|
|
17996
|
+
colorInput = El('input').attr('type', 'text').attr('aria-label', 'Text color');
|
|
17722
17997
|
var textColorField = addColorOpacityField(textColorCell, colorChit, colorInput, applyLabelOpacity);
|
|
17723
17998
|
colorFieldBox = textColorField.box;
|
|
17724
17999
|
opacityInput = textColorField.opacity;
|
|
@@ -17751,7 +18026,7 @@
|
|
|
17751
18026
|
applyLabelAlign(item.name);
|
|
17752
18027
|
})
|
|
17753
18028
|
.attr('data-align', item.name)
|
|
17754
|
-
.attr('
|
|
18029
|
+
.attr('aria-label', item.title);
|
|
17755
18030
|
alignBtns[item.name] = btn;
|
|
17756
18031
|
appendAlignButtonSymbol(btn, item.name);
|
|
17757
18032
|
});
|
|
@@ -17780,7 +18055,7 @@
|
|
|
17780
18055
|
// spacing sits above.
|
|
17781
18056
|
haloSection = addSection('Halo');
|
|
17782
18057
|
var haloTitle = haloSection.findChild('.label-style-section-title');
|
|
17783
|
-
haloToggle =
|
|
18058
|
+
haloToggle = makePanelToggle(haloTitle, {
|
|
17784
18059
|
title: 'Draw a halo around the text',
|
|
17785
18060
|
className: 'label-halo-toggle',
|
|
17786
18061
|
onChange: setHaloOn
|
|
@@ -17790,7 +18065,7 @@
|
|
|
17790
18065
|
var haloColorCell = El('div').addClass('label-split-cell label-color-row label-halo-color-row').appendTo(haloColorRow);
|
|
17791
18066
|
El('span').appendTo(haloColorCell).text('Color');
|
|
17792
18067
|
haloColorChit = El('div').addClass('label-color-chit').attr('role', 'button');
|
|
17793
|
-
haloColorInput = El('input').attr('type', 'text').attr('
|
|
18068
|
+
haloColorInput = El('input').attr('type', 'text').attr('aria-label', 'Halo color');
|
|
17794
18069
|
var haloColorField = addColorOpacityField(haloColorCell, haloColorChit, haloColorInput, applyHaloOpacity);
|
|
17795
18070
|
haloColorFieldBox = haloColorField.box;
|
|
17796
18071
|
haloOpacityInput = haloColorField.opacity;
|
|
@@ -17831,7 +18106,7 @@
|
|
|
17831
18106
|
// a label with no icon: nothing to apply it to.
|
|
17832
18107
|
iconSection = addSection('Icon');
|
|
17833
18108
|
var iconTitle = iconSection.findChild('.label-style-section-title');
|
|
17834
|
-
iconToggle =
|
|
18109
|
+
iconToggle = makePanelToggle(iconTitle, {
|
|
17835
18110
|
title: 'Draw a symbol at the label anchor',
|
|
17836
18111
|
className: 'label-icon-toggle',
|
|
17837
18112
|
onChange: setIconOn
|
|
@@ -17841,12 +18116,12 @@
|
|
|
17841
18116
|
var iconRow = El('div').addClass('label-style-row label-icon-shapes-row').appendTo(iconSection);
|
|
17842
18117
|
var iconGroup = iconGroupEl = El('div').addClass('label-btn-group label-icon-buttons').appendTo(iconRow);
|
|
17843
18118
|
iconBtns = {};
|
|
17844
|
-
|
|
18119
|
+
getShownIconTypes().forEach(function(icon) {
|
|
17845
18120
|
var btn = makePanelButton(iconGroup, '', function() {
|
|
17846
18121
|
applyIcon(icon.name);
|
|
17847
18122
|
})
|
|
17848
18123
|
.attr('data-icon', icon.name)
|
|
17849
|
-
.attr('
|
|
18124
|
+
.attr('aria-label', icon.title || icon.name);
|
|
17850
18125
|
iconBtns[icon.name] = btn;
|
|
17851
18126
|
appendIconButtonSymbol(btn, icon.name);
|
|
17852
18127
|
});
|
|
@@ -17855,7 +18130,7 @@
|
|
|
17855
18130
|
var iconColorCell = El('div').addClass('label-split-cell label-color-row label-icon-color-row').appendTo(iconColorRow);
|
|
17856
18131
|
El('span').appendTo(iconColorCell).text('Color');
|
|
17857
18132
|
iconColorChit = El('div').addClass('label-color-chit').attr('role', 'button');
|
|
17858
|
-
iconColorInput = El('input').attr('type', 'text').attr('
|
|
18133
|
+
iconColorInput = El('input').attr('type', 'text').attr('aria-label', 'Symbol color');
|
|
17859
18134
|
var iconColorField = addColorOpacityField(iconColorCell, iconColorChit, iconColorInput, applyIconOpacity);
|
|
17860
18135
|
iconColorFieldBox = iconColorField.box;
|
|
17861
18136
|
iconOpacityInput = iconColorField.opacity;
|
|
@@ -17903,7 +18178,7 @@
|
|
|
17903
18178
|
applyLabelPosition(pos);
|
|
17904
18179
|
})
|
|
17905
18180
|
.attr('data-position', pos)
|
|
17906
|
-
.attr('
|
|
18181
|
+
.attr('aria-label', pos);
|
|
17907
18182
|
});
|
|
17908
18183
|
|
|
17909
18184
|
|
|
@@ -17913,7 +18188,7 @@
|
|
|
17913
18188
|
setLabelPositionMode(gui, item.name, getPositionModeKind());
|
|
17914
18189
|
})
|
|
17915
18190
|
.attr('data-drag-mode', item.name)
|
|
17916
|
-
.attr('
|
|
18191
|
+
.attr('aria-label', item.title);
|
|
17917
18192
|
});
|
|
17918
18193
|
|
|
17919
18194
|
presetControl = new StylePresetControl(panel, {
|
|
@@ -17938,7 +18213,7 @@
|
|
|
17938
18213
|
// the map rather than typed here. See docs/development/text-annotation-design.md.
|
|
17939
18214
|
function initCalloutSection() {
|
|
17940
18215
|
calloutSection = addSection('Callout');
|
|
17941
|
-
calloutToggle =
|
|
18216
|
+
calloutToggle = makePanelToggle(calloutSection.findChild('.label-style-section-title'), {
|
|
17942
18217
|
title: 'Draw a line from the label anchor to its text',
|
|
17943
18218
|
className: 'label-callout-toggle',
|
|
17944
18219
|
onChange: setCalloutOn
|
|
@@ -17998,7 +18273,7 @@
|
|
|
17998
18273
|
var colorCell = El('div').addClass('label-split-cell label-color-row label-callout-color-row').appendTo(colorRow);
|
|
17999
18274
|
El('span').appendTo(colorCell).text('Color');
|
|
18000
18275
|
calloutColorChit = El('div').addClass('label-color-chit').attr('role', 'button');
|
|
18001
|
-
calloutColorInput = El('input').attr('type', 'text').attr('
|
|
18276
|
+
calloutColorInput = El('input').attr('type', 'text').attr('aria-label', 'Callout color');
|
|
18002
18277
|
var calloutColorField = addColorOpacityField(colorCell, calloutColorChit, calloutColorInput, applyCalloutOpacity);
|
|
18003
18278
|
calloutColorFieldBox = calloutColorField.box;
|
|
18004
18279
|
calloutOpacityInput = calloutColorField.opacity;
|
|
@@ -18022,7 +18297,7 @@
|
|
|
18022
18297
|
var gapCell = El('div').addClass('label-split-cell label-spacing-row label-callout-gap-row').appendTo(colorRow);
|
|
18023
18298
|
El('span').appendTo(gapCell).text('Gap');
|
|
18024
18299
|
calloutGapInput = El('input').attr('type', 'text').addClass('label-measure-input')
|
|
18025
|
-
.attr('
|
|
18300
|
+
.attr('aria-label', 'Space between the callout and the anchor, in px')
|
|
18026
18301
|
.attr('placeholder', 'auto')
|
|
18027
18302
|
.attr('data-placeholder', 'auto')
|
|
18028
18303
|
.appendTo(gapCell)
|
|
@@ -18034,7 +18309,7 @@
|
|
|
18034
18309
|
function makeCalloutButton(parent, item, action) {
|
|
18035
18310
|
var btn = makePanelButton(parent, '', action)
|
|
18036
18311
|
.attr('data-callout', item.name)
|
|
18037
|
-
.attr('
|
|
18312
|
+
.attr('aria-label', item.title);
|
|
18038
18313
|
El('<svg class="label-callout-symbol" viewBox="0 0 16 16" aria-hidden="true">' +
|
|
18039
18314
|
calloutButtonSymbols[item.name] + '</svg>').appendTo(btn);
|
|
18040
18315
|
return btn;
|
|
@@ -18059,48 +18334,6 @@
|
|
|
18059
18334
|
return makePanelSection(panel, title, opts);
|
|
18060
18335
|
}
|
|
18061
18336
|
|
|
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
18337
|
function addColorOpacityField(parent, chit, input, onOpacity) {
|
|
18105
18338
|
return makeColorOpacityField(parent, chit, input, {
|
|
18106
18339
|
onSet: onOpacity,
|
|
@@ -18303,8 +18536,10 @@
|
|
|
18303
18536
|
// rest of the selection has nothing to compare.
|
|
18304
18537
|
var iconIds = getIconValueIds();
|
|
18305
18538
|
var iconVal = getCommonValue(iconIds, iconField);
|
|
18306
|
-
var
|
|
18307
|
-
var
|
|
18539
|
+
var iconShape = iconVal || lastIconShape;
|
|
18540
|
+
var iconSize = getShownValue(iconIds, iconSizeField, {useDefault: true, defaultValue: getDefaultIconSize(iconShape)});
|
|
18541
|
+
var iconColor = getShownValue(iconIds, iconColorField, {useDefault: true,
|
|
18542
|
+
defaultValue: getDefaultIconColor(iconShape) || defaultIconColor});
|
|
18308
18543
|
var iconOpacity = getShownValue(iconIds, iconOpacityField, {useDefault: true, defaultValue: 1});
|
|
18309
18544
|
var haloIds = getHaloValueIds();
|
|
18310
18545
|
var haloWidth = getShownValue(haloIds, haloWidthField, {useDefault: true, defaultValue: lastHaloWidth});
|
|
@@ -18649,7 +18884,7 @@
|
|
|
18649
18884
|
// border the buttons share fades with them -- a live border around dead
|
|
18650
18885
|
// buttons is the one part of a disabled control that still looks usable.
|
|
18651
18886
|
iconGroupEl.classed('disabled', off);
|
|
18652
|
-
|
|
18887
|
+
getShownIconTypes().forEach(function(icon) {
|
|
18653
18888
|
iconBtns[icon.name].classed('selected', !off && icon.name == iconVal);
|
|
18654
18889
|
setPanelButtonDisabled(iconBtns[icon.name], off);
|
|
18655
18890
|
});
|
|
@@ -19248,12 +19483,23 @@
|
|
|
19248
19483
|
|
|
19249
19484
|
function getSelectedIcon() {
|
|
19250
19485
|
var out = '';
|
|
19251
|
-
|
|
19486
|
+
getShownIconTypes().forEach(function(icon) {
|
|
19252
19487
|
if (iconBtns[icon.name].hasClass('selected')) out = icon.name;
|
|
19253
19488
|
});
|
|
19254
19489
|
return out;
|
|
19255
19490
|
}
|
|
19256
19491
|
|
|
19492
|
+
// A label that already has an NYT star keeps it without the button: it is
|
|
19493
|
+
// drawn for everyone, and only choosing one is gated.
|
|
19494
|
+
function getShownIconTypes() {
|
|
19495
|
+
if (!shownIconTypes) {
|
|
19496
|
+
shownIconTypes = iconTypes.filter(function(icon) {
|
|
19497
|
+
return !icon.nytOnly || isNytUser();
|
|
19498
|
+
});
|
|
19499
|
+
}
|
|
19500
|
+
return shownIconTypes;
|
|
19501
|
+
}
|
|
19502
|
+
|
|
19257
19503
|
function applyStyleObject(style) {
|
|
19258
19504
|
var styles = [];
|
|
19259
19505
|
savedStyleFields.forEach(function(field) {
|
|
@@ -19284,8 +19530,9 @@
|
|
|
19284
19530
|
// The size the symbols in the selection already share, where they share
|
|
19285
19531
|
// one: a shape applied to a mixed selection gives the labels that had no
|
|
19286
19532
|
// symbol the size of the ones that did, rather than resetting them all to
|
|
19287
|
-
// the default.
|
|
19288
|
-
|
|
19533
|
+
// the default. A size or colour that was the old shape's default becomes
|
|
19534
|
+
// the new one's (see getIconShapeChange()).
|
|
19535
|
+
addIconShapeDefaults(styles, iconName, getIconValueIds());
|
|
19289
19536
|
// The symbol's own opacity goes on with it, because the label's opacity
|
|
19290
19537
|
// is applied to both elements: without this, text set to 50% would give
|
|
19291
19538
|
// a half-faded symbol while the Icon section showed it at 100%.
|
|
@@ -19297,6 +19544,14 @@
|
|
|
19297
19544
|
applyStyleValues(styles);
|
|
19298
19545
|
}
|
|
19299
19546
|
|
|
19547
|
+
function addIconShapeDefaults(styles, iconName, ids) {
|
|
19548
|
+
var color = getCommonValueInfo(ids, iconColorField);
|
|
19549
|
+
var change = getIconShapeChange(iconName, getCommonValue(ids, iconField) || '',
|
|
19550
|
+
getCommonValue(ids, iconSizeField), color.mixed ? null : color.value || '');
|
|
19551
|
+
styles.push([iconSizeField, change.size]);
|
|
19552
|
+
if ('color' in change) styles.push([iconColorField, change.color]);
|
|
19553
|
+
}
|
|
19554
|
+
|
|
19300
19555
|
// Switching a symbol on moves a label sitting at the centre out from under
|
|
19301
19556
|
// it, in the same command, so that the pair is one undo step and the text
|
|
19302
19557
|
// is never briefly drawn over the symbol it just asked for.
|
|
@@ -19337,7 +19592,9 @@
|
|
|
19337
19592
|
}
|
|
19338
19593
|
|
|
19339
19594
|
function nudgeIconSize(delta) {
|
|
19340
|
-
var
|
|
19595
|
+
var ids = getIconTargetIds();
|
|
19596
|
+
var size = getNumericSize(ids, iconSizeField,
|
|
19597
|
+
getDefaultIconSize(getCommonValue(ids, iconField)));
|
|
19341
19598
|
if (!controlsEnabled()) return;
|
|
19342
19599
|
size = Math.max(1, size + delta);
|
|
19343
19600
|
applyIconSize(size);
|
|
@@ -19428,14 +19685,6 @@
|
|
|
19428
19685
|
return !!node && /^(INPUT|SELECT|TEXTAREA|BUTTON|OPTION)$/.test(node.nodeName);
|
|
19429
19686
|
}
|
|
19430
19687
|
|
|
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
19688
|
// Hands the keyboard back after a control has done its job: to the label
|
|
19440
19689
|
// being typed into if there is one, and to nothing at all otherwise.
|
|
19441
19690
|
function releaseFocus() {
|
|
@@ -19454,7 +19703,7 @@
|
|
|
19454
19703
|
if (styles.length === 0) return;
|
|
19455
19704
|
releaseFocus();
|
|
19456
19705
|
// 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. -
|
|
19706
|
+
// not these values also went to a label that already exists. -labels
|
|
19458
19707
|
// writes them when that label is created, which is why this needs no
|
|
19459
19708
|
// command and no undo step of its own.
|
|
19460
19709
|
if (labelModeIsOn()) {
|
|
@@ -19478,7 +19727,7 @@
|
|
|
19478
19727
|
|
|
19479
19728
|
function applyStyleCommand(styles, ids) {
|
|
19480
19729
|
var lyr = getActiveLayer();
|
|
19481
|
-
var parts = ['-
|
|
19730
|
+
var parts = ['-labels'];
|
|
19482
19731
|
if (!gui.console || !lyr) return;
|
|
19483
19732
|
styles.forEach(function(style) {
|
|
19484
19733
|
parts.push(style[0] + '=' + quoteCommandValue(style[1]));
|
|
@@ -19500,7 +19749,7 @@
|
|
|
19500
19749
|
// A text block's breaks are only right for the width and font they were
|
|
19501
19750
|
// found with, so a change to either rewraps it in the same command: one undo
|
|
19502
19751
|
// step, and no moment at which the block is drawn with its old lines in its
|
|
19503
|
-
// new font. One -
|
|
19752
|
+
// new font. One -labels per label, since each has text of its own.
|
|
19504
19753
|
function getRewrapCommands(styles, ids, lyr) {
|
|
19505
19754
|
var table = lyr.data;
|
|
19506
19755
|
var changes = {};
|
|
@@ -19634,14 +19883,513 @@
|
|
|
19634
19883
|
return measureSvg;
|
|
19635
19884
|
}
|
|
19636
19885
|
|
|
19886
|
+
// The pattern fill controls of the polygon panel, and the fill-pattern codes
|
|
19887
|
+
// they stand for.
|
|
19888
|
+
//
|
|
19889
|
+
// The panel covers the patterns that are one colour over a background: hatch
|
|
19890
|
+
// lines, dots and squares. The background is the feature's own fill, so a
|
|
19891
|
+
// pattern is described by five settings -- {type, color, size, gap, angle} --
|
|
19892
|
+
// and the fill. A hatch has no background in the code: it is a run of stripes,
|
|
19893
|
+
// and the panel's hatch is two of them, the gap in the fill colour and the line
|
|
19894
|
+
// in the pattern colour. Anything else (dashes, several colours, a background
|
|
19895
|
+
// that is not the fill) is a custom pattern, which the panel shows as its code.
|
|
19896
|
+
//
|
|
19897
|
+
// Written into every code rather than read from the fill when drawn, because
|
|
19898
|
+
// the code is what -style stores and what the SVG exporter turns into a
|
|
19899
|
+
// <pattern>: a pattern with no colour of its own would need both to change.
|
|
19900
|
+
|
|
19901
|
+
var patternTypes = ['hatches', 'dots', 'squares'];
|
|
19902
|
+
|
|
19903
|
+
var patternDefaults = {
|
|
19904
|
+
hatches: {size: 1, gap: 3, angle: 45},
|
|
19905
|
+
dots: {size: 2, gap: 3, angle: 0},
|
|
19906
|
+
squares: {size: 2, gap: 2, angle: 0}
|
|
19907
|
+
};
|
|
19908
|
+
|
|
19909
|
+
var defaultPatternColor = '#000000';
|
|
19910
|
+
|
|
19911
|
+
// The settings a newly chosen pattern type starts from. The colour carries
|
|
19912
|
+
// over from the pattern being replaced; the sizes do not, because a hatch
|
|
19913
|
+
// line's width and a dot's diameter look nothing alike at the same number.
|
|
19914
|
+
function getDefaultPatternControls(type, color) {
|
|
19915
|
+
var d = patternDefaults[type];
|
|
19916
|
+
return {type: type, color: color || defaultPatternColor,
|
|
19917
|
+
size: d.size, gap: d.gap, angle: d.angle};
|
|
19918
|
+
}
|
|
19919
|
+
|
|
19920
|
+
// The background a pattern is given over a feature with @fill. A feature with
|
|
19921
|
+
// no fill gets a clear one, so that the pattern is drawn over whatever is
|
|
19922
|
+
// underneath.
|
|
19923
|
+
function getPatternBackground(fill) {
|
|
19924
|
+
return isBlank$1(fill) ? 'none' : String(fill).trim();
|
|
19925
|
+
}
|
|
19926
|
+
|
|
19927
|
+
// Whether the settings describe a pattern the renderer will draw. A hatch with
|
|
19928
|
+
// no gap would be a solid fill, and the parser refuses a stripe of no width.
|
|
19929
|
+
function isValidPatternControls(o) {
|
|
19930
|
+
if (!o || patternTypes.indexOf(o.type) == -1 || isBlank$1(o.color)) return false;
|
|
19931
|
+
if (!(o.size > 0) || !isFinite(o.angle)) return false;
|
|
19932
|
+
return o.type == 'hatches' ? o.gap > 0 : o.gap >= 0;
|
|
19933
|
+
}
|
|
19934
|
+
|
|
19935
|
+
// The fill-pattern code for the settings @o over @background. The angle is left
|
|
19936
|
+
// out where it is the parser's own default, which keeps the common codes as
|
|
19937
|
+
// short as the ones in the documentation.
|
|
19938
|
+
function formatFillPattern(o, background) {
|
|
19939
|
+
var bg = background || 'none';
|
|
19940
|
+
if (o.type == 'hatches') {
|
|
19941
|
+
return 'hatches ' + formatAngle(o.angle, 45) +
|
|
19942
|
+
formatPx(o.gap) + ' ' + bg + ' ' + formatPx(o.size) + ' ' + o.color;
|
|
19943
|
+
}
|
|
19944
|
+
return o.type + ' ' + formatAngle(o.angle, 0) +
|
|
19945
|
+
formatPx(o.size) + ' ' + o.color + ' ' + formatPx(o.gap) + ' ' + bg;
|
|
19946
|
+
}
|
|
19947
|
+
|
|
19948
|
+
// The settings a parsed fill-pattern code (from parsePattern()) was made from,
|
|
19949
|
+
// given the fill of the feature it is on -- or null if the panel's settings
|
|
19950
|
+
// cannot describe it, which makes it a custom pattern.
|
|
19951
|
+
function getPatternControls(parsed, fill) {
|
|
19952
|
+
var bg = getPatternBackground(fill);
|
|
19953
|
+
var line;
|
|
19954
|
+
if (!parsed) return null;
|
|
19955
|
+
if (parsed.type == 'hatches') {
|
|
19956
|
+
if (parsed.widths.length != 2) return null;
|
|
19957
|
+
line = sameColor(parsed.colors[0], bg) ? 1 : sameColor(parsed.colors[1], bg) ? 0 : -1;
|
|
19958
|
+
if (line == -1) return null;
|
|
19959
|
+
return {type: 'hatches', color: parsed.colors[line], size: parsed.widths[line],
|
|
19960
|
+
gap: parsed.widths[1 - line], angle: parsed.rotation};
|
|
19961
|
+
}
|
|
19962
|
+
if (parsed.type == 'dots' || parsed.type == 'squares') {
|
|
19963
|
+
if (parsed.colors.length != 1 || !sameColor(parsed.background, bg)) return null;
|
|
19964
|
+
return {type: parsed.type, color: parsed.colors[0], size: parsed.size,
|
|
19965
|
+
gap: parsed.spacing, angle: parsed.rotation};
|
|
19966
|
+
}
|
|
19967
|
+
return null;
|
|
19968
|
+
}
|
|
19969
|
+
|
|
19970
|
+
// The same pattern over a different fill, or null for a custom pattern, which
|
|
19971
|
+
// is left as it is.
|
|
19972
|
+
function refillPattern(parsed, oldFill, newFill) {
|
|
19973
|
+
var o = getPatternControls(parsed, oldFill);
|
|
19974
|
+
return o ? formatFillPattern(o, getPatternBackground(newFill)) : null;
|
|
19975
|
+
}
|
|
19976
|
+
|
|
19977
|
+
// A -style expression giving each feature the pattern @o over its own fill.
|
|
19978
|
+
// For a change that sets fills the panel cannot know in advance, like
|
|
19979
|
+
// -classify's random colours.
|
|
19980
|
+
function formatFillPatternExpression(o) {
|
|
19981
|
+
var sentinel = '\u0000';
|
|
19982
|
+
var parts = formatFillPattern(o, sentinel).split(sentinel);
|
|
19983
|
+
return JSON.stringify(parts[0]) + ' + (fill || "none") + ' + JSON.stringify(parts[1]);
|
|
19984
|
+
}
|
|
19985
|
+
|
|
19986
|
+
// Collects per-feature edits into as few -style commands as they allow.
|
|
19987
|
+
// @edits: [{id, styles: [[name, value], ...]}]
|
|
19988
|
+
// Returns [{styles, ids}], in the order each set of styles first appears.
|
|
19989
|
+
function groupStyleEdits(edits) {
|
|
19990
|
+
var groups = [];
|
|
19991
|
+
var index = {};
|
|
19992
|
+
edits.forEach(function(edit) {
|
|
19993
|
+
var key = JSON.stringify(edit.styles);
|
|
19994
|
+
if (!(key in index)) {
|
|
19995
|
+
index[key] = groups.length;
|
|
19996
|
+
groups.push({styles: edit.styles, ids: []});
|
|
19997
|
+
}
|
|
19998
|
+
groups[index[key]].ids.push(edit.id);
|
|
19999
|
+
});
|
|
20000
|
+
return groups;
|
|
20001
|
+
}
|
|
20002
|
+
|
|
20003
|
+
function formatAngle(angle, defaultAngle) {
|
|
20004
|
+
return angle == defaultAngle ? '' : formatNumber$3(angle) + 'deg ';
|
|
20005
|
+
}
|
|
20006
|
+
|
|
20007
|
+
function formatPx(val) {
|
|
20008
|
+
return formatNumber$3(val) + 'px';
|
|
20009
|
+
}
|
|
20010
|
+
|
|
20011
|
+
function formatNumber$3(val) {
|
|
20012
|
+
return String(Math.round(Number(val) * 100) / 100);
|
|
20013
|
+
}
|
|
20014
|
+
|
|
20015
|
+
function sameColor(a, b) {
|
|
20016
|
+
return normalizeColor(a) == normalizeColor(b);
|
|
20017
|
+
}
|
|
20018
|
+
|
|
20019
|
+
function normalizeColor(color) {
|
|
20020
|
+
var str = String(color || '').trim().toLowerCase();
|
|
20021
|
+
return str == 'transparent' ? 'none' : str;
|
|
20022
|
+
}
|
|
20023
|
+
|
|
20024
|
+
function isBlank$1(val) {
|
|
20025
|
+
return val === undefined || val === null || String(val).trim() === '';
|
|
20026
|
+
}
|
|
20027
|
+
|
|
20028
|
+
// The polygon panel's "Pattern" section. See gui-fill-pattern.mjs for how its
|
|
20029
|
+
// settings map to fill-pattern codes.
|
|
20030
|
+
//
|
|
20031
|
+
// Switched like the label panel's Halo and Icon sections: a pattern is a
|
|
20032
|
+
// yes/no that the rest of the section then qualifies, and the section shows
|
|
20033
|
+
// only its heading while it is off.
|
|
20034
|
+
//
|
|
20035
|
+
// opts.getRecords() the target layer's records
|
|
20036
|
+
// opts.getTargetIds() the features being styled
|
|
20037
|
+
// opts.applyEdits(edits, title) run per-feature edits: [{id, styles}]
|
|
20038
|
+
// opts.revert() put the panel back as the data has it
|
|
20039
|
+
// opts.releaseFocus()
|
|
20040
|
+
function PatternFillControl(parent, opts) {
|
|
20041
|
+
var section = makePanelSection(parent, 'Pattern');
|
|
20042
|
+
var shown = {type: 'none'};
|
|
20043
|
+
// What switching the pattern on applies when the features have none: the
|
|
20044
|
+
// last pattern the section showed, so that off and on again is a round trip.
|
|
20045
|
+
var lastPattern = null;
|
|
20046
|
+
// Custom was chosen from the menu, and the code field is waiting for a code.
|
|
20047
|
+
// Until one is applied the data still says what it said before, so the
|
|
20048
|
+
// choice has to be remembered or the next refresh would undo it.
|
|
20049
|
+
var customPending = false;
|
|
20050
|
+
var toggle, typeSelect, mixedOption, colorControl, angleField, sizeRow, sizeCaption,
|
|
20051
|
+
sizeField, gapField, customRow, codeInput;
|
|
20052
|
+
|
|
20053
|
+
initRows();
|
|
20054
|
+
|
|
20055
|
+
this.section = section;
|
|
20056
|
+
|
|
20057
|
+
this.update = update;
|
|
20058
|
+
|
|
20059
|
+
this.reset = function() {
|
|
20060
|
+
customPending = false;
|
|
20061
|
+
};
|
|
20062
|
+
|
|
20063
|
+
this.hidePicker = function() {
|
|
20064
|
+
colorControl.picker.hide();
|
|
20065
|
+
};
|
|
20066
|
+
|
|
20067
|
+
// What a saved style records for the pattern, over the fill it records.
|
|
20068
|
+
this.getCode = function(fill) {
|
|
20069
|
+
if (isSimple(shown)) return formatFillPattern(shown, getPatternBackground(fill));
|
|
20070
|
+
if (shown.type == 'custom') return shown.code || '';
|
|
20071
|
+
return '';
|
|
20072
|
+
};
|
|
20073
|
+
|
|
20074
|
+
// Per-feature edits applying @styles to the targets. A new fill is also
|
|
20075
|
+
// written into the background of each feature's pattern, since that is where
|
|
20076
|
+
// the pattern's background colour is kept; custom patterns are left alone.
|
|
20077
|
+
this.getStyleEdits = function(styles) {
|
|
20078
|
+
var records = opts.getRecords();
|
|
20079
|
+
var fill = findStyle(styles, 'fill');
|
|
20080
|
+
var refill = fill !== undefined && findStyle(styles, 'fill-pattern') === undefined;
|
|
20081
|
+
return opts.getTargetIds().map(function(id) {
|
|
20082
|
+
var rec = records[id];
|
|
20083
|
+
var code = rec && rec['fill-pattern'];
|
|
20084
|
+
var edit = {id: id, styles: styles};
|
|
20085
|
+
var refilled;
|
|
20086
|
+
if (refill && !isBlank(code)) {
|
|
20087
|
+
refilled = refillPattern(internal.parsePattern(code), rec.fill, fill);
|
|
20088
|
+
if (refilled) edit.styles = styles.concat([['fill-pattern', refilled]]);
|
|
20089
|
+
}
|
|
20090
|
+
return edit;
|
|
20091
|
+
});
|
|
20092
|
+
};
|
|
20093
|
+
|
|
20094
|
+
// Edits that keep each feature's pattern over its fill after a command that
|
|
20095
|
+
// sets fills the panel cannot know beforehand. A -style expression reads
|
|
20096
|
+
// the new fill when it runs.
|
|
20097
|
+
this.getRefillExpressionEdits = function(ids) {
|
|
20098
|
+
var records = opts.getRecords();
|
|
20099
|
+
var edits = [];
|
|
20100
|
+
ids.forEach(function(id) {
|
|
20101
|
+
var rec = records[id];
|
|
20102
|
+
var code = rec && rec['fill-pattern'];
|
|
20103
|
+
var o = isBlank(code) ? null : getPatternControls(internal.parsePattern(code), rec.fill);
|
|
20104
|
+
if (o) edits.push({id: id, styles: [['fill-pattern', formatFillPatternExpression(o)]]});
|
|
20105
|
+
});
|
|
20106
|
+
return edits;
|
|
20107
|
+
};
|
|
20108
|
+
|
|
20109
|
+
function initRows() {
|
|
20110
|
+
toggle = makePanelToggle(section.findChild('.label-style-section-title'), {
|
|
20111
|
+
title: 'Fill with a pattern',
|
|
20112
|
+
className: 'layer-pattern-toggle',
|
|
20113
|
+
onChange: setPatternOn
|
|
20114
|
+
});
|
|
20115
|
+
var typeRow = El('div').addClass('label-style-row layer-pattern-type-row').appendTo(section);
|
|
20116
|
+
typeSelect = El('select').attr('aria-label', 'Pattern type').appendTo(typeRow)
|
|
20117
|
+
.on('change', function() {
|
|
20118
|
+
selectType(typeSelect.node().value);
|
|
20119
|
+
});
|
|
20120
|
+
[['hatches', 'Hatches'], ['dots', 'Dots'], ['squares', 'Squares'],
|
|
20121
|
+
['custom', 'Custom']].forEach(function(o) {
|
|
20122
|
+
El('option').attr('value', o[0]).appendTo(typeSelect).text(o[1]);
|
|
20123
|
+
});
|
|
20124
|
+
// Shown only while the features being styled have different patterns,
|
|
20125
|
+
// which is not a choice the menu offers.
|
|
20126
|
+
mixedOption = El('option').attr('value', 'mixed').appendTo(typeSelect).text('Mixed');
|
|
20127
|
+
mixedOption.node().disabled = true;
|
|
20128
|
+
|
|
20129
|
+
colorControl = makeColorRow(section, {
|
|
20130
|
+
label: 'Color',
|
|
20131
|
+
noOpacity: true,
|
|
20132
|
+
onColor: function(color) {
|
|
20133
|
+
if (color) applyChange({color: color});
|
|
20134
|
+
},
|
|
20135
|
+
revert: opts.revert
|
|
20136
|
+
});
|
|
20137
|
+
colorControl.row.addClass('layer-pattern-color-row');
|
|
20138
|
+
El('span').appendTo(colorControl.aside).text('Angle');
|
|
20139
|
+
angleField = new SizeField(colorControl.aside, {
|
|
20140
|
+
min: -180, max: 180, step: 15, bigStep: 45,
|
|
20141
|
+
title: 'Pattern angle in degrees',
|
|
20142
|
+
onSet: function(value) {
|
|
20143
|
+
applyChange({angle: value});
|
|
20144
|
+
},
|
|
20145
|
+
onStep: function(delta) {
|
|
20146
|
+
applyChange({angle: Math.max(-180, Math.min(180, shown.angle + delta))});
|
|
20147
|
+
},
|
|
20148
|
+
onDone: opts.releaseFocus
|
|
20149
|
+
});
|
|
20150
|
+
|
|
20151
|
+
sizeRow = El('div').addClass('label-style-row layer-pattern-size-row').appendTo(section);
|
|
20152
|
+
var sizeCell = El('div').addClass('label-split-cell').appendTo(sizeRow);
|
|
20153
|
+
var gapCell = El('div').addClass('label-split-cell').appendTo(sizeRow);
|
|
20154
|
+
sizeCaption = El('span').appendTo(sizeCell).text('Size');
|
|
20155
|
+
sizeField = new SizeField(sizeCell, {
|
|
20156
|
+
min: 0.25, step: 0.5, bigStep: 2, decimals: 2,
|
|
20157
|
+
title: 'Size in px',
|
|
20158
|
+
onSet: function(value) {
|
|
20159
|
+
applyChange({size: value});
|
|
20160
|
+
},
|
|
20161
|
+
onStep: function(delta) {
|
|
20162
|
+
applyChange({size: Math.max(0.25, shown.size + delta)});
|
|
20163
|
+
},
|
|
20164
|
+
onDone: opts.releaseFocus
|
|
20165
|
+
});
|
|
20166
|
+
El('span').appendTo(gapCell).text('Gap');
|
|
20167
|
+
gapField = new SizeField(gapCell, {
|
|
20168
|
+
min: 0, step: 0.5, bigStep: 2, decimals: 2,
|
|
20169
|
+
title: 'Gap in px',
|
|
20170
|
+
onSet: function(value) {
|
|
20171
|
+
applyChange({gap: value});
|
|
20172
|
+
},
|
|
20173
|
+
onStep: function(delta) {
|
|
20174
|
+
applyChange({gap: Math.max(0, shown.gap + delta)});
|
|
20175
|
+
},
|
|
20176
|
+
onDone: opts.releaseFocus
|
|
20177
|
+
});
|
|
20178
|
+
|
|
20179
|
+
customRow = El('div').addClass('label-style-row layer-pattern-code-row').appendTo(section);
|
|
20180
|
+
var caption = El('div').addClass('label-style-row-label').appendTo(customRow).text('Code');
|
|
20181
|
+
makeFieldTip(caption,
|
|
20182
|
+
'The fill-pattern syntax of the -style command.\n' +
|
|
20183
|
+
'See the command reference for details.');
|
|
20184
|
+
codeInput = El('input').attr('type', 'text').attr('aria-label', 'Pattern code').appendTo(customRow)
|
|
20185
|
+
.on('change', function() {
|
|
20186
|
+
applyCode(codeInput.node().value.trim());
|
|
20187
|
+
});
|
|
20188
|
+
}
|
|
20189
|
+
|
|
20190
|
+
function update() {
|
|
20191
|
+
var ids = opts.getTargetIds();
|
|
20192
|
+
var toggleState = getToggleState(ids);
|
|
20193
|
+
var state = getCommonPatternState(ids);
|
|
20194
|
+
if (customPending && state.type != 'custom') {
|
|
20195
|
+
state = {type: 'custom', code: shown.type == 'custom' ? shown.code : getPendingCode(state)};
|
|
20196
|
+
}
|
|
20197
|
+
shown = state;
|
|
20198
|
+
if (isSimple(state) || state.type == 'custom' && state.code) lastPattern = state;
|
|
20199
|
+
toggle.setState(toggleState);
|
|
20200
|
+
section.classed('collapsed', toggleState == 'off');
|
|
20201
|
+
mixedOption.classed('hidden', state.type != 'mixed');
|
|
20202
|
+
typeSelect.node().value = state.type;
|
|
20203
|
+
colorControl.row.classed('hidden', !isSimple(state));
|
|
20204
|
+
sizeRow.classed('hidden', !isSimple(state));
|
|
20205
|
+
customRow.classed('hidden', state.type != 'custom');
|
|
20206
|
+
if (isSimple(state)) {
|
|
20207
|
+
colorControl.showColor(state.color);
|
|
20208
|
+
angleField.setValue(state.angle);
|
|
20209
|
+
sizeCaption.text(state.type == 'hatches' ? 'Width' : 'Size');
|
|
20210
|
+
sizeField.setValue(state.size);
|
|
20211
|
+
gapField.setValue(state.gap);
|
|
20212
|
+
} else {
|
|
20213
|
+
colorControl.picker.hide();
|
|
20214
|
+
}
|
|
20215
|
+
if (state.type == 'custom' && document.activeElement !== codeInput.node()) {
|
|
20216
|
+
codeInput.node().value = state.code || '';
|
|
20217
|
+
}
|
|
20218
|
+
}
|
|
20219
|
+
|
|
20220
|
+
function selectType(type) {
|
|
20221
|
+
if (type == 'custom') {
|
|
20222
|
+
customPending = true;
|
|
20223
|
+
update();
|
|
20224
|
+
codeInput.node().focus();
|
|
20225
|
+
return;
|
|
20226
|
+
}
|
|
20227
|
+
customPending = false;
|
|
20228
|
+
if (patternTypes.indexOf(type) > -1) {
|
|
20229
|
+
applyControls(getDefaultPatternControls(type, isSimple(shown) ? shown.color : null));
|
|
20230
|
+
}
|
|
20231
|
+
}
|
|
20232
|
+
|
|
20233
|
+
// On gives every feature being styled a pattern: the one the features that
|
|
20234
|
+
// have a pattern agree on, if they do, or else the last one shown, or else a
|
|
20235
|
+
// default hatch. Off takes the pattern off all of them.
|
|
20236
|
+
function setPatternOn(on) {
|
|
20237
|
+
var ids = opts.getTargetIds();
|
|
20238
|
+
var common, pattern;
|
|
20239
|
+
customPending = false;
|
|
20240
|
+
if (!on) {
|
|
20241
|
+
applyToTargets(function() { return ''; }, 'Remove pattern fill');
|
|
20242
|
+
return;
|
|
20243
|
+
}
|
|
20244
|
+
common = getCommonPatternState(ids.filter(hasPattern));
|
|
20245
|
+
pattern = isSimple(common) || common.type == 'custom' ? common :
|
|
20246
|
+
lastPattern || getDefaultPatternControls('hatches');
|
|
20247
|
+
if (pattern.type == 'custom') {
|
|
20248
|
+
applyToTargets(function() { return pattern.code; }, 'Pattern fill');
|
|
20249
|
+
} else {
|
|
20250
|
+
applyControls(pattern);
|
|
20251
|
+
}
|
|
20252
|
+
}
|
|
20253
|
+
|
|
20254
|
+
function getToggleState(ids) {
|
|
20255
|
+
var n = ids.filter(hasPattern).length;
|
|
20256
|
+
return n === 0 ? 'off' : n < ids.length ? 'mixed' : 'on';
|
|
20257
|
+
}
|
|
20258
|
+
|
|
20259
|
+
function hasPattern(id) {
|
|
20260
|
+
var rec = opts.getRecords()[id];
|
|
20261
|
+
return !isBlank(rec && rec['fill-pattern']);
|
|
20262
|
+
}
|
|
20263
|
+
|
|
20264
|
+
function applyChange(change) {
|
|
20265
|
+
if (!isSimple(shown)) return;
|
|
20266
|
+
var o = Object.assign({}, shown, change);
|
|
20267
|
+
if (!isValidPatternControls(o)) {
|
|
20268
|
+
opts.revert();
|
|
20269
|
+
return;
|
|
20270
|
+
}
|
|
20271
|
+
applyControls(o);
|
|
20272
|
+
}
|
|
20273
|
+
|
|
20274
|
+
function applyControls(o) {
|
|
20275
|
+
applyToTargets(function(rec) {
|
|
20276
|
+
return formatFillPattern(o, getPatternBackground(rec && rec.fill));
|
|
20277
|
+
}, 'Pattern fill');
|
|
20278
|
+
}
|
|
20279
|
+
|
|
20280
|
+
function applyCode(code) {
|
|
20281
|
+
if (code && !internal.parsePattern(code)) {
|
|
20282
|
+
codeInput.node().value = shown.code || '';
|
|
20283
|
+
opts.revert();
|
|
20284
|
+
return;
|
|
20285
|
+
}
|
|
20286
|
+
if (!code) customPending = false;
|
|
20287
|
+
applyToTargets(function() { return code; }, code ? 'Pattern fill' : 'Remove pattern fill');
|
|
20288
|
+
}
|
|
20289
|
+
|
|
20290
|
+
function applyToTargets(getCode, title) {
|
|
20291
|
+
var records = opts.getRecords();
|
|
20292
|
+
opts.applyEdits(opts.getTargetIds().map(function(id) {
|
|
20293
|
+
return {id: id, styles: [['fill-pattern', getCode(records[id])]]};
|
|
20294
|
+
}), title);
|
|
20295
|
+
}
|
|
20296
|
+
|
|
20297
|
+
// The pattern the features being styled have in common, as panel settings,
|
|
20298
|
+
// {type: 'custom', code}, {type: 'none'} -- or {type: 'mixed'} if they differ.
|
|
20299
|
+
// Features with the same pattern over different fills agree: each pattern's
|
|
20300
|
+
// background is its own feature's fill.
|
|
20301
|
+
function getCommonPatternState(ids) {
|
|
20302
|
+
var records = opts.getRecords();
|
|
20303
|
+
var cache = {};
|
|
20304
|
+
var firstKey = null, first = null;
|
|
20305
|
+
var rec, cacheKey, state, key;
|
|
20306
|
+
for (var i=0; i<ids.length; i++) {
|
|
20307
|
+
rec = records[ids[i]];
|
|
20308
|
+
cacheKey = (rec && rec['fill-pattern']) + '\t' + (rec && rec.fill);
|
|
20309
|
+
state = cache[cacheKey] || (cache[cacheKey] = readPatternState(rec));
|
|
20310
|
+
key = JSON.stringify(state);
|
|
20311
|
+
if (firstKey === null) {
|
|
20312
|
+
firstKey = key;
|
|
20313
|
+
first = state;
|
|
20314
|
+
} else if (key != firstKey) {
|
|
20315
|
+
return {type: 'mixed'};
|
|
20316
|
+
}
|
|
20317
|
+
}
|
|
20318
|
+
return first || {type: 'none'};
|
|
20319
|
+
}
|
|
20320
|
+
|
|
20321
|
+
// The code the Custom field starts from: the pattern being replaced, so that
|
|
20322
|
+
// choosing Custom is a way to see the code for the settings shown.
|
|
20323
|
+
function getPendingCode(state) {
|
|
20324
|
+
var records = opts.getRecords();
|
|
20325
|
+
var ids = opts.getTargetIds();
|
|
20326
|
+
var rec = ids.length > 0 ? records[ids[0]] : null;
|
|
20327
|
+
return isSimple(state) ? formatFillPattern(state, getPatternBackground(rec && rec.fill)) : '';
|
|
20328
|
+
}
|
|
20329
|
+
}
|
|
20330
|
+
|
|
20331
|
+
function readPatternState(rec) {
|
|
20332
|
+
var code = rec && rec['fill-pattern'];
|
|
20333
|
+
if (isBlank(code)) return {type: 'none'};
|
|
20334
|
+
return getPatternControls(internal.parsePattern(code), rec.fill) ||
|
|
20335
|
+
{type: 'custom', code: String(code)};
|
|
20336
|
+
}
|
|
20337
|
+
|
|
20338
|
+
function isSimple(state) {
|
|
20339
|
+
return patternTypes.indexOf(state.type) > -1;
|
|
20340
|
+
}
|
|
20341
|
+
|
|
20342
|
+
function findStyle(styles, name) {
|
|
20343
|
+
for (var i=0; i<styles.length; i++) {
|
|
20344
|
+
if (styles[i][0] == name) return styles[i][1];
|
|
20345
|
+
}
|
|
20346
|
+
return undefined;
|
|
20347
|
+
}
|
|
20348
|
+
|
|
20349
|
+
function isBlank(val) {
|
|
20350
|
+
return val === undefined || val === null || String(val).trim() === '';
|
|
20351
|
+
}
|
|
20352
|
+
|
|
19637
20353
|
var savedStylesKey = 'layer_style_presets';
|
|
19638
|
-
var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray', 'fill', 'fill-opacity'];
|
|
20354
|
+
var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray', 'line-start', 'line-end', 'line-end-size', 'fill', 'fill-opacity', 'fill-pattern'];
|
|
20355
|
+
var arrowShapes = [{
|
|
20356
|
+
name: 'arrow',
|
|
20357
|
+
title: 'solid arrowheads'
|
|
20358
|
+
}, {
|
|
20359
|
+
name: 'open-arrow',
|
|
20360
|
+
title: 'open arrowheads'
|
|
20361
|
+
}, {
|
|
20362
|
+
name: 'dot',
|
|
20363
|
+
title: 'dots'
|
|
20364
|
+
}];
|
|
20365
|
+
// The arrows are the label callout's, which point left, the way a line's
|
|
20366
|
+
// start head does; the dot's left edge is where their tips are.
|
|
20367
|
+
var arrowButtonSymbols = {
|
|
20368
|
+
arrow: calloutButtonSymbols.arrow,
|
|
20369
|
+
'open-arrow': calloutButtonSymbols['open-arrow'],
|
|
20370
|
+
dot: '<path d="M5 8H13"></path><circle class="fill" cx="5" cy="8" r="2.6"></circle>'
|
|
20371
|
+
};
|
|
20372
|
+
var arrowPositions = [{
|
|
20373
|
+
name: 'start',
|
|
20374
|
+
label: 'Start',
|
|
20375
|
+
title: 'arrowhead at the start of the line'
|
|
20376
|
+
}, {
|
|
20377
|
+
name: 'end',
|
|
20378
|
+
label: 'End',
|
|
20379
|
+
title: 'arrowhead at the end of the line'
|
|
20380
|
+
}, {
|
|
20381
|
+
name: 'both',
|
|
20382
|
+
label: 'Both',
|
|
20383
|
+
title: 'arrowheads at both ends of the line'
|
|
20384
|
+
}];
|
|
19639
20385
|
|
|
19640
20386
|
function LayerStyleTool(gui) {
|
|
19641
20387
|
var parent = gui.container.findChild('.mshp-main-map');
|
|
19642
20388
|
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;
|
|
20389
|
+
var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthField, dashControl, arrowControl, randomFillBtn, presetControl, patternControl, hit;
|
|
19644
20390
|
var targetLayer = null;
|
|
20391
|
+
// What the arrowhead switch turns on, for lines that have no heads
|
|
20392
|
+
var lastArrow = {shape: 'arrow', position: 'end'};
|
|
19645
20393
|
|
|
19646
20394
|
initPanel();
|
|
19647
20395
|
hit = gui.map.getHitControl && gui.map.getHitControl();
|
|
@@ -19685,6 +20433,7 @@
|
|
|
19685
20433
|
|
|
19686
20434
|
function turnOn() {
|
|
19687
20435
|
targetLayer = getActiveLayer();
|
|
20436
|
+
patternControl.reset();
|
|
19688
20437
|
applyDefaultLineStyle();
|
|
19689
20438
|
panel.show();
|
|
19690
20439
|
updateControls();
|
|
@@ -19694,6 +20443,8 @@
|
|
|
19694
20443
|
panel.hide();
|
|
19695
20444
|
strokeControl.picker.hide();
|
|
19696
20445
|
fillControl.picker.hide();
|
|
20446
|
+
patternControl.hidePicker();
|
|
20447
|
+
patternControl.reset();
|
|
19697
20448
|
targetLayer = null;
|
|
19698
20449
|
}
|
|
19699
20450
|
|
|
@@ -19710,8 +20461,8 @@
|
|
|
19710
20461
|
// picker's Close button, and the panel's own buttons in the browsers that
|
|
19711
20462
|
// focus a button on click. Left alone while the user is typing into a
|
|
19712
20463
|
// field, which a click elsewhere in the panel ends on its own.
|
|
19713
|
-
panel.node().addEventListener('click', function() {
|
|
19714
|
-
if (isTextInput(document.activeElement)) return;
|
|
20464
|
+
panel.node().addEventListener('click', function(e) {
|
|
20465
|
+
if (isTextInput(document.activeElement) || opensAMenu(e.target)) return;
|
|
19715
20466
|
releaseFocus();
|
|
19716
20467
|
});
|
|
19717
20468
|
|
|
@@ -19727,11 +20478,22 @@
|
|
|
19727
20478
|
strokeControl = addColorControl(panel, 'Stroke', 'stroke', '#000000');
|
|
19728
20479
|
strokeWidthField = addStrokeWidthControl(strokeControl.aside);
|
|
19729
20480
|
dashControl = addDashArrayControl(panel);
|
|
20481
|
+
arrowControl = addArrowControl(panel);
|
|
19730
20482
|
|
|
19731
20483
|
var buttonRow = El('div').addClass('label-style-row label-panel-button-row').appendTo(panel);
|
|
19732
20484
|
randomFillBtn = makePanelActionButton(buttonRow, 'Random fills', applyRandomFillColors);
|
|
19733
20485
|
makePanelActionButton(buttonRow, 'Clear style', clearLayerStyle);
|
|
19734
20486
|
|
|
20487
|
+
patternControl = new PatternFillControl(panel, {
|
|
20488
|
+
getRecords: function() {
|
|
20489
|
+
return targetLayer && targetLayer.data ? targetLayer.data.getRecords() : [];
|
|
20490
|
+
},
|
|
20491
|
+
getTargetIds: getTargetIds,
|
|
20492
|
+
applyEdits: runStyleEdits,
|
|
20493
|
+
revert: updateControls,
|
|
20494
|
+
releaseFocus: releaseFocus
|
|
20495
|
+
});
|
|
20496
|
+
|
|
19735
20497
|
presetControl = new StylePresetControl(panel, {
|
|
19736
20498
|
storageKey: savedStylesKey,
|
|
19737
20499
|
type: getStyleType,
|
|
@@ -19810,6 +20572,197 @@
|
|
|
19810
20572
|
return {row: row, input: input};
|
|
19811
20573
|
}
|
|
19812
20574
|
|
|
20575
|
+
// A switch in the heading says whether the lines have arrowheads; the rows
|
|
20576
|
+
// under it are the head's shape with its size beside it, then which ends
|
|
20577
|
+
// get it. A line has one shape for both ends, which is all the panel sets,
|
|
20578
|
+
// though -style can give the two ends different ones.
|
|
20579
|
+
function addArrowControl(parent) {
|
|
20580
|
+
var section = makePanelSection(parent, 'Arrowheads');
|
|
20581
|
+
var toggle = makePanelToggle(section.findChild('.label-style-section-title'), {
|
|
20582
|
+
title: 'Add arrowheads',
|
|
20583
|
+
className: 'layer-arrow-toggle',
|
|
20584
|
+
onChange: setArrowsOn
|
|
20585
|
+
});
|
|
20586
|
+
var shapeRow = El('div').addClass('label-style-row label-split-row').appendTo(section);
|
|
20587
|
+
var shapeCell = El('div').addClass('label-split-cell label-align-row').appendTo(shapeRow);
|
|
20588
|
+
var sizeCell = El('div').addClass('label-split-cell label-spacing-row layer-arrow-size-row').appendTo(shapeRow);
|
|
20589
|
+
var posRow = El('div').addClass('label-style-row label-split-row').appendTo(section);
|
|
20590
|
+
var posCell = El('div').addClass('label-split-cell label-align-row').appendTo(posRow);
|
|
20591
|
+
var control = {section: section, toggle: toggle, shapeBtns: {}, posBtns: {}};
|
|
20592
|
+
El('div').addClass('label-split-cell').appendTo(posRow);
|
|
20593
|
+
El('span').appendTo(shapeCell).text('Shape');
|
|
20594
|
+
var shapeGroup = El('div').addClass('label-btn-group label-callout-buttons layer-arrow-shape-buttons').appendTo(shapeCell);
|
|
20595
|
+
arrowShapes.forEach(function(item) {
|
|
20596
|
+
var btn = makePanelButton(shapeGroup, '', function() {
|
|
20597
|
+
applyArrowShape(item.name);
|
|
20598
|
+
}).attr('data-arrow-shape', item.name).attr('aria-label', item.title);
|
|
20599
|
+
El('<svg class="label-callout-symbol" viewBox="0 0 16 16" aria-hidden="true">' +
|
|
20600
|
+
arrowButtonSymbols[item.name] + '</svg>').appendTo(btn);
|
|
20601
|
+
control.shapeBtns[item.name] = btn;
|
|
20602
|
+
});
|
|
20603
|
+
El('span').appendTo(posCell).text('Ends');
|
|
20604
|
+
var posGroup = El('div').addClass('label-btn-group layer-arrow-position-buttons').appendTo(posCell);
|
|
20605
|
+
arrowPositions.forEach(function(item) {
|
|
20606
|
+
control.posBtns[item.name] = makePanelButton(posGroup, item.label, function() {
|
|
20607
|
+
applyArrowPosition(item.name);
|
|
20608
|
+
}).attr('data-arrow-position', item.name).attr('aria-label', item.title);
|
|
20609
|
+
});
|
|
20610
|
+
El('span').appendTo(sizeCell).text('Size');
|
|
20611
|
+
control.sizeField = new SizeField(sizeCell, {
|
|
20612
|
+
title: 'Arrowhead size in px, the length of its sides or the diameter of a dot',
|
|
20613
|
+
min: 1,
|
|
20614
|
+
max: 60,
|
|
20615
|
+
step: 1,
|
|
20616
|
+
bigStep: 5,
|
|
20617
|
+
onSet: applyArrowSize,
|
|
20618
|
+
onStep: nudgeArrowSize,
|
|
20619
|
+
onDone: releaseFocus
|
|
20620
|
+
});
|
|
20621
|
+
return control;
|
|
20622
|
+
}
|
|
20623
|
+
|
|
20624
|
+
// {shape, position} for one record, 'none' and '' when it has no heads. A
|
|
20625
|
+
// line whose two ends have different shapes reports the start's.
|
|
20626
|
+
function getArrowInfo(rec) {
|
|
20627
|
+
var start = internal.svg.getLineEndType(rec || {}, 'line-start');
|
|
20628
|
+
var end = internal.svg.getLineEndType(rec || {}, 'line-end');
|
|
20629
|
+
var shape = start != 'none' ? start : end;
|
|
20630
|
+
var position = start != 'none' && end != 'none' ? 'both' :
|
|
20631
|
+
start != 'none' ? 'start' : end != 'none' ? 'end' : '';
|
|
20632
|
+
return {shape: shape, position: position};
|
|
20633
|
+
}
|
|
20634
|
+
|
|
20635
|
+
// Switching on gives the lines that have no heads the last shape and ends
|
|
20636
|
+
// the section showed, so that off and on again is a round trip; the lines
|
|
20637
|
+
// that have heads keep theirs. Switching off leaves line-end-size alone,
|
|
20638
|
+
// for the same reason.
|
|
20639
|
+
function setArrowsOn(on) {
|
|
20640
|
+
applyArrowEdits(function(info) {
|
|
20641
|
+
return on ? fillArrowInfo(info) : {shape: 'none', position: ''};
|
|
20642
|
+
});
|
|
20643
|
+
}
|
|
20644
|
+
|
|
20645
|
+
// Each target keeps whichever of shape and position is not being set, so
|
|
20646
|
+
// that changing the shape of a selection whose lines point different ways
|
|
20647
|
+
// leaves them pointing those ways.
|
|
20648
|
+
function applyArrowShape(shape) {
|
|
20649
|
+
applyArrowEdits(function(info) {
|
|
20650
|
+
return {shape: shape, position: fillArrowInfo(info).position};
|
|
20651
|
+
});
|
|
20652
|
+
}
|
|
20653
|
+
|
|
20654
|
+
function applyArrowPosition(position) {
|
|
20655
|
+
applyArrowEdits(function(info) {
|
|
20656
|
+
return {shape: fillArrowInfo(info).shape, position: position};
|
|
20657
|
+
});
|
|
20658
|
+
}
|
|
20659
|
+
|
|
20660
|
+
function fillArrowInfo(info) {
|
|
20661
|
+
return info.shape == 'none' ? {shape: lastArrow.shape, position: lastArrow.position} : info;
|
|
20662
|
+
}
|
|
20663
|
+
|
|
20664
|
+
function applyArrowEdits(getNext) {
|
|
20665
|
+
var records = getTargetRecords();
|
|
20666
|
+
var addStroke = styleFieldIsUnsetForTargets('stroke');
|
|
20667
|
+
var edits = [];
|
|
20668
|
+
getTargetIds().forEach(function(id) {
|
|
20669
|
+
var rec = records[id] || {};
|
|
20670
|
+
var next = getNext(getArrowInfo(rec));
|
|
20671
|
+
var on = next.shape != 'none';
|
|
20672
|
+
var styles = [
|
|
20673
|
+
['line-start', on && next.position != 'end' ? next.shape : ''],
|
|
20674
|
+
['line-end', on && next.position != 'start' ? next.shape : '']
|
|
20675
|
+
].filter(function(style) {
|
|
20676
|
+
// no need to remove what is not there
|
|
20677
|
+
return style[1] || rec[style[0]];
|
|
20678
|
+
});
|
|
20679
|
+
if (on && addStroke) styles.push(['stroke', strokeControl.defaultColor]);
|
|
20680
|
+
if (styles.length > 0) edits.push({id: id, styles: styles});
|
|
20681
|
+
});
|
|
20682
|
+
runStyleEdits(edits);
|
|
20683
|
+
}
|
|
20684
|
+
|
|
20685
|
+
function applyArrowSize(value) {
|
|
20686
|
+
var ids = getArrowTargetIds();
|
|
20687
|
+
if (ids.length === 0) return;
|
|
20688
|
+
runStyleEdits(ids.map(function(id) {
|
|
20689
|
+
return {id: id, styles: [['line-end-size', value]]};
|
|
20690
|
+
}));
|
|
20691
|
+
}
|
|
20692
|
+
|
|
20693
|
+
function nudgeArrowSize(delta) {
|
|
20694
|
+
var shown = getArrowSizeShown(getArrowTargetIds());
|
|
20695
|
+
var size = Number(shown.value);
|
|
20696
|
+
if (!isFinite(size) || shown.value === '') return;
|
|
20697
|
+
applyArrowSize(Math.max(1, Math.round(size + delta)));
|
|
20698
|
+
}
|
|
20699
|
+
|
|
20700
|
+
function getArrowTargetIds() {
|
|
20701
|
+
var records = getTargetRecords();
|
|
20702
|
+
return getTargetIds().filter(function(id) {
|
|
20703
|
+
return getArrowInfo(records[id]).shape != 'none';
|
|
20704
|
+
});
|
|
20705
|
+
}
|
|
20706
|
+
|
|
20707
|
+
// The size a head is drawn at, which is the one its line width gives it
|
|
20708
|
+
// when it has none of its own.
|
|
20709
|
+
function getArrowSizeShown(ids) {
|
|
20710
|
+
var records = getTargetRecords();
|
|
20711
|
+
var value = '', size;
|
|
20712
|
+
for (var i=0; i<ids.length; i++) {
|
|
20713
|
+
size = getArrowSize(records[ids[i]]);
|
|
20714
|
+
if (i > 0 && size !== value) return {value: '', mixed: true};
|
|
20715
|
+
value = size;
|
|
20716
|
+
}
|
|
20717
|
+
return {value: value, mixed: false};
|
|
20718
|
+
}
|
|
20719
|
+
|
|
20720
|
+
function getArrowSize(rec) {
|
|
20721
|
+
var opts = internal.svg.makeLineArrowOpts('arrow', '', rec && rec['line-end-size'],
|
|
20722
|
+
rec && rec['stroke-width'], 1);
|
|
20723
|
+
var size = getArrowInfo(rec).shape == 'dot' ? opts.dotSize : opts.size;
|
|
20724
|
+
return formatNumberValue(Math.round(size * 100) / 100);
|
|
20725
|
+
}
|
|
20726
|
+
|
|
20727
|
+
// The shape and ends shown are those of the lines with heads; lines
|
|
20728
|
+
// without are what the switch's mixed state is for.
|
|
20729
|
+
function updateArrowControl() {
|
|
20730
|
+
var records = getTargetRecords();
|
|
20731
|
+
var ids = getTargetIds();
|
|
20732
|
+
var arrowIds = getArrowTargetIds();
|
|
20733
|
+
var shape, position, info;
|
|
20734
|
+
for (var i=0; i<arrowIds.length; i++) {
|
|
20735
|
+
info = getArrowInfo(records[arrowIds[i]]);
|
|
20736
|
+
if (i === 0) {
|
|
20737
|
+
shape = info.shape;
|
|
20738
|
+
position = info.position;
|
|
20739
|
+
} else {
|
|
20740
|
+
if (info.shape != shape) shape = '';
|
|
20741
|
+
if (info.position != position) position = '';
|
|
20742
|
+
}
|
|
20743
|
+
}
|
|
20744
|
+
if (shape) lastArrow.shape = shape;
|
|
20745
|
+
if (position) lastArrow.position = position;
|
|
20746
|
+
var state = arrowIds.length === 0 ? 'off' :
|
|
20747
|
+
arrowIds.length < ids.length ? 'mixed' : 'on';
|
|
20748
|
+
arrowControl.toggle.setState(state);
|
|
20749
|
+
arrowControl.section.classed('collapsed', state == 'off');
|
|
20750
|
+
arrowShapes.forEach(function(item) {
|
|
20751
|
+
arrowControl.shapeBtns[item.name].classed('selected', item.name == shape);
|
|
20752
|
+
});
|
|
20753
|
+
arrowPositions.forEach(function(item) {
|
|
20754
|
+
arrowControl.posBtns[item.name].classed('selected', item.name == position);
|
|
20755
|
+
});
|
|
20756
|
+
var shown = arrowIds.length > 0 ? getArrowSizeShown(arrowIds) : {value: '', mixed: false};
|
|
20757
|
+
arrowControl.sizeField.setValue(shown.value);
|
|
20758
|
+
arrowControl.sizeField.setPlaceholder(shown.mixed ? 'mixed' : '');
|
|
20759
|
+
arrowControl.sizeField.setDisabled(arrowIds.length === 0);
|
|
20760
|
+
}
|
|
20761
|
+
|
|
20762
|
+
function getTargetRecords() {
|
|
20763
|
+
return targetLayer && targetLayer.data ? targetLayer.data.getRecords() : [];
|
|
20764
|
+
}
|
|
20765
|
+
|
|
19813
20766
|
function releaseFocus() {
|
|
19814
20767
|
releasePanelFocus(panel.node());
|
|
19815
20768
|
}
|
|
@@ -19824,11 +20777,15 @@
|
|
|
19824
20777
|
strokeControl.row.show();
|
|
19825
20778
|
fillControl.row.classed('hidden', geom != 'polygon');
|
|
19826
20779
|
dashControl.row.classed('hidden', geom != 'polyline');
|
|
20780
|
+
arrowControl.section.classed('hidden', geom != 'polyline');
|
|
19827
20781
|
updateColorControl(strokeControl);
|
|
19828
20782
|
updateColorControl(fillControl);
|
|
19829
20783
|
updateStrokeWidthControl();
|
|
19830
20784
|
updateDashArrayControl();
|
|
20785
|
+
if (geom == 'polyline') updateArrowControl();
|
|
19831
20786
|
randomFillBtn.classed('hidden', geom != 'polygon');
|
|
20787
|
+
patternControl.section.classed('hidden', geom != 'polygon');
|
|
20788
|
+
if (geom == 'polygon') patternControl.update();
|
|
19832
20789
|
presetControl.render();
|
|
19833
20790
|
updateSavedStyleControls();
|
|
19834
20791
|
}
|
|
@@ -19892,7 +20849,11 @@
|
|
|
19892
20849
|
if (control.field == 'stroke' && strokeWidthIsUnsetForTargets()) {
|
|
19893
20850
|
styles.push(['stroke-width', 1]);
|
|
19894
20851
|
}
|
|
19895
|
-
|
|
20852
|
+
if (control.field == 'fill') {
|
|
20853
|
+
runStyleEdits(patternControl.getStyleEdits(styles));
|
|
20854
|
+
} else {
|
|
20855
|
+
runStyleCommand(styles);
|
|
20856
|
+
}
|
|
19896
20857
|
}
|
|
19897
20858
|
|
|
19898
20859
|
function applyStrokeWidthStyle(value) {
|
|
@@ -19912,11 +20873,30 @@
|
|
|
19912
20873
|
}
|
|
19913
20874
|
|
|
19914
20875
|
function runStyleCommand(styles) {
|
|
19915
|
-
var parts = ['-style'];
|
|
19916
20876
|
releaseFocus();
|
|
19917
20877
|
syncTargetLayer();
|
|
19918
20878
|
var ids = getTargetIds();
|
|
19919
20879
|
if (!gui.console || !targetLayer || ids.length === 0) return;
|
|
20880
|
+
runCommand(formatStyleCommand(styles, ids), 'Style layer');
|
|
20881
|
+
}
|
|
20882
|
+
|
|
20883
|
+
// Per-feature edits ([{id, styles}]), as one -style command for each set of
|
|
20884
|
+
// styles. They are run as one command string, which is one undo step.
|
|
20885
|
+
function runStyleEdits(edits, title) {
|
|
20886
|
+
releaseFocus();
|
|
20887
|
+
syncTargetLayer();
|
|
20888
|
+
if (!gui.console || !targetLayer || edits.length === 0) return;
|
|
20889
|
+
runCommand(formatStyleEditCommands(edits), title || 'Style layer');
|
|
20890
|
+
}
|
|
20891
|
+
|
|
20892
|
+
function formatStyleEditCommands(edits) {
|
|
20893
|
+
return groupStyleEdits(edits).map(function(group) {
|
|
20894
|
+
return formatStyleCommand(group.styles, group.ids);
|
|
20895
|
+
}).join(' ');
|
|
20896
|
+
}
|
|
20897
|
+
|
|
20898
|
+
function formatStyleCommand(styles, ids) {
|
|
20899
|
+
var parts = ['-style'];
|
|
19920
20900
|
styles.forEach(function(style) {
|
|
19921
20901
|
parts.push(style[0] + '=' + quoteCommandValue(style[1]));
|
|
19922
20902
|
});
|
|
@@ -19924,7 +20904,7 @@
|
|
|
19924
20904
|
if (ids.length < internal.getFeatureCount(targetLayer)) {
|
|
19925
20905
|
parts.push('ids=' + ids.join(','));
|
|
19926
20906
|
}
|
|
19927
|
-
|
|
20907
|
+
return parts.join(' ');
|
|
19928
20908
|
}
|
|
19929
20909
|
|
|
19930
20910
|
function applyDefaultLineStyle() {
|
|
@@ -19938,13 +20918,21 @@
|
|
|
19938
20918
|
}
|
|
19939
20919
|
}
|
|
19940
20920
|
|
|
20921
|
+
// Patterns keep their backgrounds in step with the fills, and the new fills
|
|
20922
|
+
// are not known until -classify has chosen them, so the patterns are
|
|
20923
|
+
// rewritten by an expression that reads each feature's fill when it runs.
|
|
19941
20924
|
function applyRandomFillColors() {
|
|
19942
20925
|
var cmd = '-classify colors=random non-adjacent';
|
|
20926
|
+
var patternEdits;
|
|
19943
20927
|
syncTargetLayer();
|
|
19944
20928
|
if (!gui.console || !targetLayer || targetLayer.geometry_type != 'polygon') return;
|
|
19945
20929
|
if (getActiveLayer() != targetLayer) {
|
|
19946
20930
|
cmd += ' target=' + internal.formatOptionValue(internal.getLayerTargetId(gui.model, targetLayer));
|
|
19947
20931
|
}
|
|
20932
|
+
patternEdits = patternControl.getRefillExpressionEdits(getAllFeatureIds(targetLayer));
|
|
20933
|
+
if (patternEdits.length > 0) {
|
|
20934
|
+
cmd += ' ' + formatStyleEditCommands(patternEdits);
|
|
20935
|
+
}
|
|
19948
20936
|
runCommand(cmd, 'Random fill colors');
|
|
19949
20937
|
}
|
|
19950
20938
|
|
|
@@ -19963,7 +20951,7 @@
|
|
|
19963
20951
|
}
|
|
19964
20952
|
});
|
|
19965
20953
|
if (styles.length > 0) {
|
|
19966
|
-
|
|
20954
|
+
runStyleEdits(patternControl.getStyleEdits(styles));
|
|
19967
20955
|
}
|
|
19968
20956
|
}
|
|
19969
20957
|
|
|
@@ -19974,14 +20962,24 @@
|
|
|
19974
20962
|
addStyleValue(style, 'stroke-opacity', parseOpacityValue(strokeControl.opacity.node().value));
|
|
19975
20963
|
if (targetLayer && targetLayer.geometry_type == 'polyline') {
|
|
19976
20964
|
addStyleValue(style, 'stroke-dasharray', getControlValue(dashControl.input));
|
|
20965
|
+
addArrowStyleValues(style);
|
|
19977
20966
|
}
|
|
19978
20967
|
if (targetLayer && targetLayer.geometry_type == 'polygon') {
|
|
19979
20968
|
addStyleValue(style, 'fill', getControlValue(fillControl.input));
|
|
19980
20969
|
addStyleValue(style, 'fill-opacity', parseOpacityValue(fillControl.opacity.node().value));
|
|
20970
|
+
addStyleValue(style, 'fill-pattern', patternControl.getCode(getControlValue(fillControl.input)));
|
|
19981
20971
|
}
|
|
19982
20972
|
return style;
|
|
19983
20973
|
}
|
|
19984
20974
|
|
|
20975
|
+
// Only when every target agrees, as with the other controls; a size is
|
|
20976
|
+
// saved only if the lines have their own.
|
|
20977
|
+
function addArrowStyleValues(style) {
|
|
20978
|
+
['line-start', 'line-end', 'line-end-size'].forEach(function(field) {
|
|
20979
|
+
addStyleValue(style, field, getCommonStyleValue(field));
|
|
20980
|
+
});
|
|
20981
|
+
}
|
|
20982
|
+
|
|
19985
20983
|
function addStyleValue(style, field, value) {
|
|
19986
20984
|
if (value || value === 0) {
|
|
19987
20985
|
style[field] = value;
|
|
@@ -20192,8 +21190,8 @@
|
|
|
20192
21190
|
revert: updateControls,
|
|
20193
21191
|
release: releaseFocus
|
|
20194
21192
|
});
|
|
20195
|
-
panel.node().addEventListener('click', function() {
|
|
20196
|
-
if (isTextInput(document.activeElement)) return;
|
|
21193
|
+
panel.node().addEventListener('click', function(e) {
|
|
21194
|
+
if (isTextInput(document.activeElement) || opensAMenu(e.target)) return;
|
|
20197
21195
|
releaseFocus();
|
|
20198
21196
|
});
|
|
20199
21197
|
|
|
@@ -20447,9 +21445,8 @@
|
|
|
20447
21445
|
var expr = createExprInput.node().value.trim();
|
|
20448
21446
|
var cmd;
|
|
20449
21447
|
if (!expr || !gui.console) return;
|
|
20450
|
-
cmd =
|
|
20451
|
-
|
|
20452
|
-
'-style label-text=' + quoteCommandValue(expr);
|
|
21448
|
+
cmd = '-labels text=' + quoteCommandValue(expr);
|
|
21449
|
+
if (createCopyCheckbox.node().checked) cmd += ' + name=labels';
|
|
20453
21450
|
runGuiEditCommand(gui, cmd, {
|
|
20454
21451
|
title: 'Create labels',
|
|
20455
21452
|
onSuccess: openLabelStyles
|
|
@@ -21434,7 +22431,7 @@
|
|
|
21434
22431
|
// hit test, the reposition pass and the selection cue all find labels by those,
|
|
21435
22432
|
// and a node with no feature behind it must not be found by any of them.
|
|
21436
22433
|
//
|
|
21437
|
-
// @rec: the label's record, as -
|
|
22434
|
+
// @rec: the label's record, as -labels coordinates= will write it
|
|
21438
22435
|
// @shp: its knots, in the layer's display coordinates
|
|
21439
22436
|
// Returns a markup string, or '' if there is nothing to draw.
|
|
21440
22437
|
function renderPendingSymbol(rec, shp, ext, idPrefix) {
|
|
@@ -22254,6 +23251,7 @@
|
|
|
22254
23251
|
// data.ids = [drawingId];
|
|
22255
23252
|
// data.id = drawingId;
|
|
22256
23253
|
data.ids = utils$1.uniq(data.ids.concat([drawingId]));
|
|
23254
|
+
data.drawing_id = drawingId;
|
|
22257
23255
|
}
|
|
22258
23256
|
if (pinnedOn) {
|
|
22259
23257
|
data.pinned = true;
|
|
@@ -24181,12 +25179,7 @@
|
|
|
24181
25179
|
styleOpts.interactionMode == 'edit_polygons' ||
|
|
24182
25180
|
styleOpts.interactionMode == 'snip_lines') {
|
|
24183
25181
|
// special overlay: shape editing mode
|
|
24184
|
-
|
|
24185
|
-
lyr.gui.style = getLineEditingStyle(hitData);
|
|
24186
|
-
if (activeLyr.geometry_type == 'polygon') {
|
|
24187
|
-
lyr.gui.style.fillColor = hoverFill;
|
|
24188
|
-
}
|
|
24189
|
-
return [lyr];
|
|
25182
|
+
return getShapeEditingLayers(activeLyr, hitData);
|
|
24190
25183
|
}
|
|
24191
25184
|
layers = [];
|
|
24192
25185
|
if (styleOpts.interactionMode == 'label') {
|
|
@@ -24321,15 +25314,50 @@
|
|
|
24321
25314
|
};
|
|
24322
25315
|
}
|
|
24323
25316
|
|
|
25317
|
+
// The path being drawn is shown without its vertices, which are only of use
|
|
25318
|
+
// for reshaping a completed path. The hover markers go on the last layer, so
|
|
25319
|
+
// they are drawn on top.
|
|
25320
|
+
function getShapeEditingLayers(activeLyr, hitData) {
|
|
25321
|
+
var drawingIds = hitData.drawing_id >= 0 ? [hitData.drawing_id] : [];
|
|
25322
|
+
var otherIds = utils$1.difference(hitData.ids || [], drawingIds);
|
|
25323
|
+
var layers = [], lyr;
|
|
25324
|
+
if (drawingIds.length > 0) {
|
|
25325
|
+
lyr = getShapeEditingLayer(activeLyr, hitData, drawingIds, false);
|
|
25326
|
+
// The end of a path being drawn has closely spaced vertices, and pixel
|
|
25327
|
+
// rounding would show as a staircase.
|
|
25328
|
+
lyr.gui.style.unroundedCoords = true;
|
|
25329
|
+
layers.push(lyr);
|
|
25330
|
+
}
|
|
25331
|
+
if (otherIds.length > 0 || layers.length === 0) {
|
|
25332
|
+
layers.push(getShapeEditingLayer(activeLyr, hitData, otherIds, true));
|
|
25333
|
+
}
|
|
25334
|
+
layers.forEach(function(lyr, i) {
|
|
25335
|
+
if (i < layers.length - 1) {
|
|
25336
|
+
lyr.gui.style.vertex_overlay = null;
|
|
25337
|
+
lyr.gui.style.pending_snip = null;
|
|
25338
|
+
}
|
|
25339
|
+
});
|
|
25340
|
+
return layers;
|
|
25341
|
+
}
|
|
25342
|
+
|
|
25343
|
+
function getShapeEditingLayer(activeLyr, hitData, ids, showVertices) {
|
|
25344
|
+
var lyr = getOverlayLayer(activeLyr, ids);
|
|
25345
|
+
lyr.gui.style = getLineEditingStyle(hitData, ids, showVertices);
|
|
25346
|
+
if (activeLyr.geometry_type == 'polygon') {
|
|
25347
|
+
lyr.gui.style.fillColor = hoverFill;
|
|
25348
|
+
}
|
|
25349
|
+
return lyr;
|
|
25350
|
+
}
|
|
25351
|
+
|
|
24324
25352
|
// style for vertex edit mode
|
|
24325
|
-
function getLineEditingStyle(o) {
|
|
25353
|
+
function getLineEditingStyle(o, ids, showVertices) {
|
|
24326
25354
|
var isVertex = o.hit_type == 'vertex' || o.hit_type == 'disabled';
|
|
24327
25355
|
return {
|
|
24328
|
-
ids:
|
|
25356
|
+
ids: ids,
|
|
24329
25357
|
overlay: true,
|
|
24330
25358
|
strokeColor: black,
|
|
24331
25359
|
strokeWidth: 1.2,
|
|
24332
|
-
vertices:
|
|
25360
|
+
vertices: showVertices,
|
|
24333
25361
|
vertex_overlay_color: getVertexOverlayColor(o.hit_type),
|
|
24334
25362
|
vertex_overlay_scale: isVertex ? 2.5 : 2,
|
|
24335
25363
|
vertex_overlay: o.hit_coordinates || null,
|
|
@@ -24530,6 +25558,11 @@
|
|
|
24530
25558
|
// pixel distance threshold for hovering near a vertex or segment midpoint
|
|
24531
25559
|
var HOVER_THRESHOLD$1 = 10;
|
|
24532
25560
|
|
|
25561
|
+
// How far the pointer has to move from where the mouse was pressed before a
|
|
25562
|
+
// drag draws a stroke. This matches the distance under which gui-mouse treats
|
|
25563
|
+
// a press as a click, so a press that stays this close adds only a click vertex.
|
|
25564
|
+
var STROKE_START_DIST = 6;
|
|
25565
|
+
|
|
24533
25566
|
function initLineEditing(gui, ext, hit) {
|
|
24534
25567
|
var hoverVertexInfo;
|
|
24535
25568
|
var prevVertexAddedEvent;
|
|
@@ -24539,7 +25572,8 @@
|
|
|
24539
25572
|
var drawingId = -1; // feature id of path being drawn
|
|
24540
25573
|
var sessionCount = 0;
|
|
24541
25574
|
var alert;
|
|
24542
|
-
var
|
|
25575
|
+
var stroke = null; // freehand stroke in progress (see startStroke())
|
|
25576
|
+
var blockDrag = false; // a drag finished a path; ignore it until the mouse is released
|
|
24543
25577
|
var _dragging = false;
|
|
24544
25578
|
|
|
24545
25579
|
function active() {
|
|
@@ -24559,7 +25593,7 @@
|
|
|
24559
25593
|
}
|
|
24560
25594
|
|
|
24561
25595
|
function pencilIsActive() {
|
|
24562
|
-
return active() && (cmdKeyDown() || pathDrawing()) && !vertexDragging()
|
|
25596
|
+
return active() && (cmdKeyDown() || pathDrawing()) && !vertexDragging();
|
|
24563
25597
|
}
|
|
24564
25598
|
|
|
24565
25599
|
function polygonMode() {
|
|
@@ -24596,19 +25630,32 @@
|
|
|
24596
25630
|
fullRedraw();
|
|
24597
25631
|
});
|
|
24598
25632
|
|
|
25633
|
+
// an undo during a stroke takes back the whole stroke
|
|
25634
|
+
gui.on('undo_redo_pre', function() {
|
|
25635
|
+
if (stroke) {
|
|
25636
|
+
finishStroke();
|
|
25637
|
+
blockDrag = true;
|
|
25638
|
+
}
|
|
25639
|
+
});
|
|
25640
|
+
|
|
24599
25641
|
gui.on('undo_path_add', function(e) {
|
|
24600
25642
|
deleteLastPath(hit.getHitTarget());
|
|
24601
25643
|
clearDrawingInfo();
|
|
24602
25644
|
});
|
|
24603
25645
|
|
|
25646
|
+
// e.points: the vertices that the edit added to the path
|
|
24604
25647
|
gui.on('redo_path_extend', function(e) {
|
|
24605
25648
|
var target = hit.getHitTarget();
|
|
24606
|
-
|
|
24607
|
-
|
|
24608
|
-
|
|
24609
|
-
|
|
25649
|
+
var points = e.points;
|
|
25650
|
+
var last = points[points.length - 1];
|
|
25651
|
+
if (pathDrawing()) {
|
|
25652
|
+
// the vertex following the pointer becomes the first added vertex
|
|
25653
|
+
setVertexCoords(target, [target.gui.displayArcs.getPointCount() - 1], points[0]);
|
|
25654
|
+
points.slice(1).forEach(function(p) { appendVertex$1(target, p); });
|
|
25655
|
+
appendVertex$1(target, prevHoverEvent ? pixToDataCoords(prevHoverEvent.x, prevHoverEvent.y) : last);
|
|
25656
|
+
hit.triggerChangeEvent();
|
|
24610
25657
|
} else {
|
|
24611
|
-
appendVertex$1(target,
|
|
25658
|
+
points.forEach(function(p) { appendVertex$1(target, p); });
|
|
24612
25659
|
}
|
|
24613
25660
|
if (e.shapes) {
|
|
24614
25661
|
replaceDrawnShapes(e.shapes);
|
|
@@ -24617,11 +25664,13 @@
|
|
|
24617
25664
|
|
|
24618
25665
|
gui.on('undo_path_extend', function(e) {
|
|
24619
25666
|
var target = hit.getHitTarget();
|
|
24620
|
-
|
|
25667
|
+
// while drawing, this also removes the vertex that follows the pointer,
|
|
25668
|
+
// and the path's new last vertex takes its place
|
|
25669
|
+
for (var i=0; i<e.points.length; i++) {
|
|
24621
25670
|
deleteLastVertex(target);
|
|
25671
|
+
}
|
|
25672
|
+
if (pathDrawing() && prevHoverEvent) {
|
|
24622
25673
|
updatePathEndpoint(pixToDataCoords(prevHoverEvent.x, prevHoverEvent.y));
|
|
24623
|
-
} else {
|
|
24624
|
-
deleteLastVertex(target);
|
|
24625
25674
|
}
|
|
24626
25675
|
if (e.shapes) {
|
|
24627
25676
|
replaceDrawnShapes(e.shapes);
|
|
@@ -24741,60 +25790,170 @@
|
|
|
24741
25790
|
});
|
|
24742
25791
|
|
|
24743
25792
|
gui.map.getMouse().on('dragend', function(e) {
|
|
24744
|
-
|
|
25793
|
+
finishStroke();
|
|
25794
|
+
blockDrag = false;
|
|
24745
25795
|
});
|
|
24746
25796
|
|
|
24747
25797
|
gui.map.getMouse().on('drag', function(e) {
|
|
25798
|
+
if (blockDrag) {
|
|
25799
|
+
e.stopPropagation(); // don't pan until the mouse is released
|
|
25800
|
+
return;
|
|
25801
|
+
}
|
|
24748
25802
|
if (!pencilIsActive()) {
|
|
24749
|
-
if (!pencilPoints) {
|
|
24750
|
-
// null points signals that a path was just completed -- block panning
|
|
24751
|
-
e.stopPropagation();
|
|
24752
|
-
}
|
|
24753
25803
|
return;
|
|
24754
25804
|
}
|
|
24755
25805
|
if (gui.keyboard.spaceIsPressed()) {
|
|
24756
25806
|
// pan if dragging with spacebar down
|
|
24757
|
-
|
|
25807
|
+
finishStroke();
|
|
24758
25808
|
return;
|
|
24759
25809
|
}
|
|
24760
25810
|
e.stopPropagation(); // prevent panning
|
|
24761
|
-
|
|
24762
|
-
|
|
24763
|
-
|
|
24764
|
-
|
|
24765
|
-
|
|
24766
|
-
|
|
24767
|
-
|
|
24768
|
-
|
|
24769
|
-
|
|
24770
|
-
|
|
24771
|
-
extendCurrentPath(p);
|
|
24772
|
-
} else if (polygonMode() && hoverVertexInfo && pencilPoints.length > 2) {
|
|
24773
|
-
// close path
|
|
24774
|
-
p = hoverVertexInfo.point;
|
|
24775
|
-
appendVertex$1(hit.getHitTarget(), p);
|
|
24776
|
-
extendCurrentPath(p);
|
|
24777
|
-
pencilPoints = null; // stop drawing
|
|
24778
|
-
} else if (pencilPoints.length >= 2 && pointExceedsTolerance(xy, pencilPoints, 1.2)) {
|
|
24779
|
-
xy2 = pencilPoints.pop();
|
|
24780
|
-
p = pixToDataCoords(xy2[0], xy2[1]);
|
|
24781
|
-
extendCurrentPath(p);
|
|
24782
|
-
// kludgy way to get a smoother line (could be better)
|
|
24783
|
-
// not this pencilPoints = [getAvgPoint(pencilPoints), xy2, xy];
|
|
24784
|
-
// not this pencilPoints = pencilPoints.slice(-2).concat([xy2, xy]);
|
|
24785
|
-
pencilPoints = [getAvgPoint(pencilPoints.slice(-3).concat([xy2])), xy2, xy];
|
|
25811
|
+
if (!stroke) {
|
|
25812
|
+
if (geom.distance2D(0, 0, e.dragX, e.dragY) < STROKE_START_DIST) return;
|
|
25813
|
+
startStroke(e);
|
|
25814
|
+
if (!stroke) return;
|
|
25815
|
+
}
|
|
25816
|
+
hoverVertexInfo = polygonMode() ? findPathStartInfo(e) : null;
|
|
25817
|
+
if (hoverVertexInfo && stroke.sampleCount > 1) {
|
|
25818
|
+
// the stroke has returned to the start of the path -- close it
|
|
25819
|
+
finishStroke(hoverVertexInfo);
|
|
25820
|
+
blockDrag = true;
|
|
24786
25821
|
} else {
|
|
24787
|
-
|
|
24788
|
-
pencilPoints.push(xy);
|
|
24789
|
-
updatePathEndpoint(p);
|
|
24790
|
-
addedToPath = false;
|
|
24791
|
-
}
|
|
24792
|
-
if (addedToPath) {
|
|
24793
|
-
//
|
|
24794
|
-
prevVertexAddedEvent = e;
|
|
25822
|
+
addStrokeSample(e);
|
|
24795
25823
|
}
|
|
24796
25824
|
}, null, 3); // higher priority than hit control
|
|
24797
25825
|
|
|
25826
|
+
// A stroke is smoothed as it is drawn. Vertices are placed some way behind
|
|
25827
|
+
// the pointer, where later samples no longer change the curve, and they stay
|
|
25828
|
+
// where they are. Between the last of them and the pointer, the path shows
|
|
25829
|
+
// the pointer's own trace (see updateStrokeTail()).
|
|
25830
|
+
//
|
|
25831
|
+
// The first vertex of the stroke (the anchor) is either the start of a new
|
|
25832
|
+
// path or the vertex that was following the pointer when the mouse was
|
|
25833
|
+
// pressed. Vertices are added to the path as they are placed, but the edit
|
|
25834
|
+
// that records them is made when the stroke ends, so that an undo takes back
|
|
25835
|
+
// the whole stroke.
|
|
25836
|
+
function startStroke(e) {
|
|
25837
|
+
var target = hit.getHitTarget();
|
|
25838
|
+
var anchorCommitted = !pathDrawing();
|
|
25839
|
+
var n, anchor;
|
|
25840
|
+
if (anchorCommitted) {
|
|
25841
|
+
// the path_add edit records the anchor
|
|
25842
|
+
hoverVertexInfo = null;
|
|
25843
|
+
startNewPath(pixToDataCoords(e.x - e.dragX, e.y - e.dragY));
|
|
25844
|
+
} else if (polygonMode() && hoverVertexInfo?.type == 'vertex') {
|
|
25845
|
+
// pressing on a vertex closes the polygon, as a click does
|
|
25846
|
+
extendCurrentPath([getLastVertexCoords(target)], true);
|
|
25847
|
+
blockDrag = true;
|
|
25848
|
+
return;
|
|
25849
|
+
} else if (pointerIsOnLastVertex(target)) {
|
|
25850
|
+
// continue from the last vertex rather than doubling it
|
|
25851
|
+
anchorCommitted = true;
|
|
25852
|
+
} else {
|
|
25853
|
+
// leave the vertex under the pointer behind as the anchor, and add a new
|
|
25854
|
+
// one to follow the pointer
|
|
25855
|
+
appendVertex$1(target, getLastVertexCoords(target));
|
|
25856
|
+
}
|
|
25857
|
+
n = target.gui.displayArcs.getPointCount();
|
|
25858
|
+
anchor = target.gui.displayArcs.getVertex2(n - 2);
|
|
25859
|
+
stroke = {
|
|
25860
|
+
anchorCommitted: anchorCommitted,
|
|
25861
|
+
// data coords of the vertices before the pointer that no edit records yet
|
|
25862
|
+
points: anchorCommitted ? [] : [getVertexCoords(target, n - 2)],
|
|
25863
|
+
// fitted in display coords, which survive a zoom mid-stroke
|
|
25864
|
+
fitter: new internal.GaussianStrokeFitter(anchor, {pixelSize: ext.getPixelSize()}),
|
|
25865
|
+
previewCount: 0, // vertices between the placed vertices and the pointer
|
|
25866
|
+
sampleCount: 0,
|
|
25867
|
+
lastEvent: e
|
|
25868
|
+
};
|
|
25869
|
+
}
|
|
25870
|
+
|
|
25871
|
+
// Test if the vertex following the pointer is on the path's last vertex
|
|
25872
|
+
function pointerIsOnLastVertex(target) {
|
|
25873
|
+
var arcs = target.gui.displayArcs;
|
|
25874
|
+
var n = arcs.getPointCount();
|
|
25875
|
+
var a = arcs.getVertex2(n - 1), b = arcs.getVertex2(n - 2);
|
|
25876
|
+
return geom.distance2D(a[0], a[1], b[0], b[1]) / ext.getPixelSize() < 3;
|
|
25877
|
+
}
|
|
25878
|
+
|
|
25879
|
+
function addStrokeSample(e) {
|
|
25880
|
+
var s = stroke;
|
|
25881
|
+
var placed = s.fitter.addSample(ext.pixCoordsToMapCoords(e.x, e.y));
|
|
25882
|
+
s.sampleCount++;
|
|
25883
|
+
s.lastEvent = e;
|
|
25884
|
+
updateStrokeTail(s, placed, s.fitter.getPreview(), pixToDataCoords(e.x, e.y));
|
|
25885
|
+
}
|
|
25886
|
+
|
|
25887
|
+
// Rewrites the end of the path after the stroke's placed vertices: newly
|
|
25888
|
+
// placed vertices, then the preview of the part of the stroke that has not
|
|
25889
|
+
// been placed yet, then the vertex following the pointer. Existing vertices
|
|
25890
|
+
// are overwritten rather than removed and added again, because adding or
|
|
25891
|
+
// removing a vertex copies the coordinates of the whole layer.
|
|
25892
|
+
// placed, preview: display coords
|
|
25893
|
+
// hover: data coords
|
|
25894
|
+
function updateStrokeTail(s, placed, preview, hover) {
|
|
25895
|
+
var target = hit.getHitTarget();
|
|
25896
|
+
var fixed = toDataCoords(target, placed);
|
|
25897
|
+
var tail = fixed.concat(toDataCoords(target, preview), [hover]);
|
|
25898
|
+
var count = s.previewCount + 1; // vertices after the placed vertices
|
|
25899
|
+
var start = target.gui.displayArcs.getPointCount() - count;
|
|
25900
|
+
var i;
|
|
25901
|
+
for (; count > tail.length; count--) {
|
|
25902
|
+
deleteLastVertex(target);
|
|
25903
|
+
}
|
|
25904
|
+
for (i = 0; i < tail.length; i++) {
|
|
25905
|
+
if (i < count) {
|
|
25906
|
+
setVertexCoords(target, [start + i], tail[i]);
|
|
25907
|
+
} else {
|
|
25908
|
+
appendVertex$1(target, tail[i]);
|
|
25909
|
+
}
|
|
25910
|
+
}
|
|
25911
|
+
s.points = s.points.concat(fixed);
|
|
25912
|
+
s.previewCount = tail.length - fixed.length - 1;
|
|
25913
|
+
hit.triggerChangeEvent();
|
|
25914
|
+
}
|
|
25915
|
+
|
|
25916
|
+
function toDataCoords(target, points) {
|
|
25917
|
+
return points.map(function(p) {
|
|
25918
|
+
return translateDisplayPoint(target, p);
|
|
25919
|
+
});
|
|
25920
|
+
}
|
|
25921
|
+
|
|
25922
|
+
// Completes the stroke in progress up to the pointer, and records the
|
|
25923
|
+
// stroke's vertices as a single edit.
|
|
25924
|
+
// end: (optional) hover info of the vertex that the stroke has to end at
|
|
25925
|
+
function finishStroke(end) {
|
|
25926
|
+
var s = stroke;
|
|
25927
|
+
var target, rest, points, count;
|
|
25928
|
+
if (!s) return;
|
|
25929
|
+
stroke = null;
|
|
25930
|
+
target = hit.getHitTarget();
|
|
25931
|
+
rest = end ? s.fitter.addSample(end.displayPoint) : [];
|
|
25932
|
+
rest = toDataCoords(target, rest.concat(s.fitter.finish()));
|
|
25933
|
+
if (end && rest.length > 0) {
|
|
25934
|
+
rest[rest.length - 1] = end.point;
|
|
25935
|
+
}
|
|
25936
|
+
// remove the preview
|
|
25937
|
+
count = s.previewCount + 1;
|
|
25938
|
+
for (; count > 1; count--) {
|
|
25939
|
+
deleteLastVertex(target);
|
|
25940
|
+
}
|
|
25941
|
+
if (rest.length === 0 && s.points.length > 0) {
|
|
25942
|
+
// the last vertex in the path takes the place of the vertex following the pointer
|
|
25943
|
+
deleteLastVertex(target);
|
|
25944
|
+
rest = [s.points.pop()];
|
|
25945
|
+
} else if (rest.length > 0) {
|
|
25946
|
+
setVertexCoords(target, [target.gui.displayArcs.getPointCount() - 1], rest[0]);
|
|
25947
|
+
}
|
|
25948
|
+
points = s.points.concat(rest);
|
|
25949
|
+
if (points.length === 0 || !s.anchorCommitted && points.length == 1) {
|
|
25950
|
+
hit.triggerChangeEvent(); // the stroke never left its anchor
|
|
25951
|
+
return;
|
|
25952
|
+
}
|
|
25953
|
+
prevVertexAddedEvent = s.lastEvent;
|
|
25954
|
+
extendCurrentPath(points, !!end, s.points.length);
|
|
25955
|
+
}
|
|
25956
|
+
|
|
24798
25957
|
hit.on('drag', function(e) {
|
|
24799
25958
|
if (!vertexDragging() || pathDrawing()) {
|
|
24800
25959
|
return;
|
|
@@ -24874,7 +26033,8 @@
|
|
|
24874
26033
|
if (detectDoubleClick(e)) return; // ignore second click of a dblclick
|
|
24875
26034
|
var p = pixToDataCoords(e.x, e.y);
|
|
24876
26035
|
if (pathDrawing()) {
|
|
24877
|
-
|
|
26036
|
+
// finish the path if a vertex is selected (but not an interpolated point)
|
|
26037
|
+
extendCurrentPath([hoverVertexInfo?.point || p], hoverVertexInfo?.type == 'vertex');
|
|
24878
26038
|
} else if (hoverVertexInfo?.type == 'interpolated') {
|
|
24879
26039
|
// don't start new path if hovering along a segment -- this is
|
|
24880
26040
|
// likely to be an attempt to add a new vertex, not start a new path
|
|
@@ -24974,6 +26134,11 @@
|
|
|
24974
26134
|
|
|
24975
26135
|
function finishCurrentPath() {
|
|
24976
26136
|
if (!pathDrawing()) return;
|
|
26137
|
+
if (stroke) {
|
|
26138
|
+
// finishing mid-drag (e.g. with the Enter key)
|
|
26139
|
+
finishStroke();
|
|
26140
|
+
blockDrag = true;
|
|
26141
|
+
}
|
|
24977
26142
|
var target = hit.getHitTarget();
|
|
24978
26143
|
if (getLastArcLength(target) <= 2) { // includes hover point
|
|
24979
26144
|
// deleteLastPath(target);
|
|
@@ -24997,15 +26162,21 @@
|
|
|
24997
26162
|
updateCursor();
|
|
24998
26163
|
}
|
|
24999
26164
|
|
|
25000
|
-
//
|
|
25001
|
-
|
|
26165
|
+
// points: [x, y] source data coordinates of vertices to add to the path. The
|
|
26166
|
+
// vertex that follows the pointer becomes points[placed], so it must
|
|
26167
|
+
// already be there; the rest are appended after it.
|
|
26168
|
+
// finish: true if the path ends at an existing vertex (which closes a polygon)
|
|
26169
|
+
// placed: (optional) number of points already in the path, before the vertex
|
|
26170
|
+
// that follows the pointer
|
|
26171
|
+
function extendCurrentPath(points, finish, placed) {
|
|
25002
26172
|
var target = hit.getHitTarget();
|
|
25003
26173
|
var shapes1, shapes2;
|
|
25004
|
-
// finish the path if a vertex is selected (but not an interpolated point)
|
|
25005
|
-
var finish = hoverVertexInfo?.type == 'vertex';
|
|
25006
26174
|
if (getLastArcLength(target) < 2) {
|
|
25007
26175
|
stop$1('Defective path');
|
|
25008
26176
|
}
|
|
26177
|
+
for (var i=(placed || 0) + 1; i<points.length; i++) {
|
|
26178
|
+
appendVertex$1(target, points[i]);
|
|
26179
|
+
}
|
|
25009
26180
|
if (finish && polygonMode()) {
|
|
25010
26181
|
shapes1 = target.shapes.slice(initialShapeCount);
|
|
25011
26182
|
try {
|
|
@@ -25017,12 +26188,12 @@
|
|
|
25017
26188
|
}
|
|
25018
26189
|
if (shapes2) {
|
|
25019
26190
|
replaceDrawnShapes(shapes2);
|
|
25020
|
-
gui.dispatchEvent('path_extend', {target,
|
|
26191
|
+
gui.dispatchEvent('path_extend', {target, points, shapes1, shapes2});
|
|
25021
26192
|
clearDrawingInfo();
|
|
25022
26193
|
fullRedraw();
|
|
25023
26194
|
} else {
|
|
25024
|
-
appendVertex$1(target,
|
|
25025
|
-
gui.dispatchEvent('path_extend', {target,
|
|
26195
|
+
appendVertex$1(target, points[points.length - 1]); // the new vertex following the pointer
|
|
26196
|
+
gui.dispatchEvent('path_extend', {target, points});
|
|
25026
26197
|
hit.triggerChangeEvent(); // trigger overlay redraw
|
|
25027
26198
|
}
|
|
25028
26199
|
}
|
|
@@ -25923,6 +27094,112 @@
|
|
|
25923
27094
|
return true;
|
|
25924
27095
|
}
|
|
25925
27096
|
|
|
27097
|
+
// The bold stretches of a label being edited, as [start, end) offsets into the
|
|
27098
|
+
// text the user edits, and how they follow the text as it changes.
|
|
27099
|
+
//
|
|
27100
|
+
// The editor keeps these beside the textarea's value rather than as markup in
|
|
27101
|
+
// it, so that the caret and the selection index the text the user sees. The
|
|
27102
|
+
// markup they come from and go back to is read and written by
|
|
27103
|
+
// readLabelValue() and writeLabelValue() in gui-label-text.mjs.
|
|
27104
|
+
//
|
|
27105
|
+
// Every function returns a new, normalized list -- sorted, non-empty, and with
|
|
27106
|
+
// no two ranges touching -- and leaves its argument alone.
|
|
27107
|
+
|
|
27108
|
+
function isBoldAt(ranges, i) {
|
|
27109
|
+
for (var j = 0; j < ranges.length; j++) {
|
|
27110
|
+
if (i >= ranges[j][0] && i < ranges[j][1]) return true;
|
|
27111
|
+
}
|
|
27112
|
+
return false;
|
|
27113
|
+
}
|
|
27114
|
+
|
|
27115
|
+
// Whether every character in [start, end) is bold. False for an empty range.
|
|
27116
|
+
function rangeIsAllBold(ranges, start, end) {
|
|
27117
|
+
var pos = start;
|
|
27118
|
+
if (!(end > start)) return false;
|
|
27119
|
+
for (var j = 0; j < ranges.length && pos < end; j++) {
|
|
27120
|
+
if (ranges[j][1] <= pos) continue;
|
|
27121
|
+
if (ranges[j][0] > pos) return false;
|
|
27122
|
+
pos = ranges[j][1];
|
|
27123
|
+
}
|
|
27124
|
+
return pos >= end;
|
|
27125
|
+
}
|
|
27126
|
+
|
|
27127
|
+
// [start, end) set bold (@on) or not.
|
|
27128
|
+
function setBoldRange(ranges, start, end, on) {
|
|
27129
|
+
var out = [];
|
|
27130
|
+
ranges.forEach(function(r) {
|
|
27131
|
+
if (r[1] <= start || r[0] >= end) {
|
|
27132
|
+
out.push([r[0], r[1]]);
|
|
27133
|
+
return;
|
|
27134
|
+
}
|
|
27135
|
+
if (r[0] < start) out.push([r[0], start]);
|
|
27136
|
+
if (r[1] > end) out.push([end, r[1]]);
|
|
27137
|
+
});
|
|
27138
|
+
if (on && end > start) out.push([start, end]);
|
|
27139
|
+
return normalize(out);
|
|
27140
|
+
}
|
|
27141
|
+
|
|
27142
|
+
// What pressing Bold does to [start, end): unbolds a range that is all bold
|
|
27143
|
+
// and bolds any other, including one that is partly bold -- the convention of
|
|
27144
|
+
// every word processor, and the reading that leaves the range one weight.
|
|
27145
|
+
function toggleBoldRange(ranges, start, end) {
|
|
27146
|
+
return setBoldRange(ranges, start, end, !rangeIsAllBold(ranges, start, end));
|
|
27147
|
+
}
|
|
27148
|
+
|
|
27149
|
+
// The ranges for @newText, given the ranges they were for @oldText.
|
|
27150
|
+
//
|
|
27151
|
+
// The edit is found as the text between the longest common prefix and suffix,
|
|
27152
|
+
// which is exact for everything a textarea does in one step -- typing,
|
|
27153
|
+
// deleting, pasting over a selection, an IME composition -- and still gives a
|
|
27154
|
+
// sensible answer for its own undo, which can replace a larger stretch at once.
|
|
27155
|
+
//
|
|
27156
|
+
// Inserted text takes the weight of the character before it, so typing on at
|
|
27157
|
+
// the end of a bold word stays bold and typing after it does not; at the start
|
|
27158
|
+
// of the text it takes the weight of the first character.
|
|
27159
|
+
function updateBoldForEdit(ranges, oldText, newText) {
|
|
27160
|
+
var edit, inherit, mapped;
|
|
27161
|
+
if (ranges.length === 0 || oldText === newText) return normalize(ranges);
|
|
27162
|
+
edit = findEdit(oldText, newText);
|
|
27163
|
+
inherit = edit.start > 0 ? isBoldAt(ranges, edit.start - 1) :
|
|
27164
|
+
isBoldAt(ranges, edit.start + edit.removed);
|
|
27165
|
+
mapped = ranges.map(function(r) {
|
|
27166
|
+
return [mapOffset(r[0], edit), mapOffset(r[1], edit)];
|
|
27167
|
+
});
|
|
27168
|
+
return setBoldRange(normalize(mapped), edit.start, edit.start + edit.inserted,
|
|
27169
|
+
inherit);
|
|
27170
|
+
}
|
|
27171
|
+
|
|
27172
|
+
function sameBoldRanges(a, b) {
|
|
27173
|
+
if (a.length != b.length) return false;
|
|
27174
|
+
for (var i = 0; i < a.length; i++) {
|
|
27175
|
+
if (a[i][0] !== b[i][0] || a[i][1] !== b[i][1]) return false;
|
|
27176
|
+
}
|
|
27177
|
+
return true;
|
|
27178
|
+
}
|
|
27179
|
+
|
|
27180
|
+
// {start, removed, inserted}: characters [start, start + removed) of the old
|
|
27181
|
+
// text became [start, start + inserted) of the new one.
|
|
27182
|
+
function findEdit(a, b) {
|
|
27183
|
+
var max = Math.min(a.length, b.length);
|
|
27184
|
+
var pre = 0, suf = 0;
|
|
27185
|
+
while (pre < max && a.charCodeAt(pre) === b.charCodeAt(pre)) pre++;
|
|
27186
|
+
while (suf < max - pre &&
|
|
27187
|
+
a.charCodeAt(a.length - 1 - suf) === b.charCodeAt(b.length - 1 - suf)) suf++;
|
|
27188
|
+
return {start: pre, removed: a.length - pre - suf, inserted: b.length - pre - suf};
|
|
27189
|
+
}
|
|
27190
|
+
|
|
27191
|
+
// A boundary inside the removed stretch collapses to its start. One after it
|
|
27192
|
+
// moves by the change in length.
|
|
27193
|
+
function mapOffset(x, edit) {
|
|
27194
|
+
if (x <= edit.start) return x;
|
|
27195
|
+
if (x >= edit.start + edit.removed) return x - edit.removed + edit.inserted;
|
|
27196
|
+
return edit.start;
|
|
27197
|
+
}
|
|
27198
|
+
|
|
27199
|
+
function normalize(ranges) {
|
|
27200
|
+
return internal.svg.normalizeBoldRanges(ranges);
|
|
27201
|
+
}
|
|
27202
|
+
|
|
25926
27203
|
// Caret and selection geometry, computed from the SVG character position APIs.
|
|
25927
27204
|
//
|
|
25928
27205
|
// The APIs behave identically on <text> and on <text><textPath>, so this code
|
|
@@ -26242,16 +27519,19 @@
|
|
|
26242
27519
|
// so the tool cannot do this at its own call sites.
|
|
26243
27520
|
self.open = function(target, id, opts) {
|
|
26244
27521
|
var rec = getRecord(target, id);
|
|
27522
|
+
var value;
|
|
26245
27523
|
if (!rec) return false;
|
|
26246
27524
|
self.close();
|
|
27525
|
+
value = readLabelValue(rec['label-text']);
|
|
26247
27526
|
session = {
|
|
26248
27527
|
target: target,
|
|
26249
27528
|
id: id,
|
|
26250
27529
|
pending: null,
|
|
26251
27530
|
created: false,
|
|
26252
27531
|
onClose: opts && opts.onClose || null,
|
|
26253
|
-
startText:
|
|
26254
|
-
startBreaks:
|
|
27532
|
+
startText: value.text,
|
|
27533
|
+
startBreaks: value.breaks,
|
|
27534
|
+
startBold: value.bold,
|
|
26255
27535
|
nodes: null
|
|
26256
27536
|
};
|
|
26257
27537
|
startSession();
|
|
@@ -26297,6 +27577,7 @@
|
|
|
26297
27577
|
onClose: opts.onClose || null,
|
|
26298
27578
|
startText: '',
|
|
26299
27579
|
startBreaks: [],
|
|
27580
|
+
startBold: [],
|
|
26300
27581
|
nodes: null
|
|
26301
27582
|
};
|
|
26302
27583
|
startSession();
|
|
@@ -26305,6 +27586,7 @@
|
|
|
26305
27586
|
|
|
26306
27587
|
function startSession() {
|
|
26307
27588
|
session.text = session.startText;
|
|
27589
|
+
session.bold = session.startBold;
|
|
26308
27590
|
getTextarea().value = session.text;
|
|
26309
27591
|
setCaret(session.text.length, session.text.length);
|
|
26310
27592
|
focusTextarea();
|
|
@@ -26320,6 +27602,74 @@
|
|
|
26320
27602
|
self.refresh();
|
|
26321
27603
|
}
|
|
26322
27604
|
|
|
27605
|
+
// The text a Bold item in the context menu would act on, as {start, end,
|
|
27606
|
+
// bold}, where @bold says whether all of it is bold already; or null. That is
|
|
27607
|
+
// the selection, or with nothing selected, the word under @p -- read when the
|
|
27608
|
+
// menu opens, because by the time an item is chosen the pointer is on the
|
|
27609
|
+
// menu rather than the label.
|
|
27610
|
+
self.getBoldTargetAtPoint = function(p) {
|
|
27611
|
+
var start = getCaretStart(), end = getCaretEnd();
|
|
27612
|
+
var range;
|
|
27613
|
+
if (!session) return null;
|
|
27614
|
+
if (!(end > start)) {
|
|
27615
|
+
start = getEditIndexAtPoint(p);
|
|
27616
|
+
if (start < 0) return null;
|
|
27617
|
+
range = getWordRange(session.text, start);
|
|
27618
|
+
start = range[0];
|
|
27619
|
+
end = range[1];
|
|
27620
|
+
}
|
|
27621
|
+
if (!(end > start)) return null;
|
|
27622
|
+
return {start: start, end: end, bold: rangeIsAllBold(session.bold, start, end)};
|
|
27623
|
+
};
|
|
27624
|
+
|
|
27625
|
+
// Bolds the selected text, or unbolds it if it is all bold already; or the
|
|
27626
|
+
// text from @start to @end, which is selected first. The text stays
|
|
27627
|
+
// selected, so that a second press takes it back.
|
|
27628
|
+
self.toggleBold = function(start, end) {
|
|
27629
|
+
if (!session) return;
|
|
27630
|
+
if (start >= 0 && end > start) {
|
|
27631
|
+
setCaret(start, end);
|
|
27632
|
+
} else {
|
|
27633
|
+
start = getCaretStart();
|
|
27634
|
+
end = getCaretEnd();
|
|
27635
|
+
}
|
|
27636
|
+
if (!(end > start)) return;
|
|
27637
|
+
session.bold = toggleBoldRange(session.bold, start, end);
|
|
27638
|
+
self.refresh();
|
|
27639
|
+
};
|
|
27640
|
+
|
|
27641
|
+
// Selects the text between two points in the label's own coordinate space,
|
|
27642
|
+
// for a drag across it. The selection runs from @p0 towards @p1, so that
|
|
27643
|
+
// shift-arrow then extends it from the end the pointer let go of.
|
|
27644
|
+
self.selectBetweenPoints = function(p0, p1) {
|
|
27645
|
+
var a = getEditIndexAtPoint(p0);
|
|
27646
|
+
var b = getEditIndexAtPoint(p1);
|
|
27647
|
+
if (a < 0 || b < 0) return false;
|
|
27648
|
+
setCaret(Math.min(a, b), Math.max(a, b), b < a ? 'backward' : 'forward');
|
|
27649
|
+
redrawCaret();
|
|
27650
|
+
return true;
|
|
27651
|
+
};
|
|
27652
|
+
|
|
27653
|
+
// Selects the word under @p, for a double-click inside the text.
|
|
27654
|
+
self.selectWordAtPoint = function(p) {
|
|
27655
|
+
var i = getEditIndexAtPoint(p);
|
|
27656
|
+
var range;
|
|
27657
|
+
if (i < 0) return false;
|
|
27658
|
+
range = getWordRange(session.text, i);
|
|
27659
|
+
setCaret(range[0], range[1]);
|
|
27660
|
+
redrawCaret();
|
|
27661
|
+
return true;
|
|
27662
|
+
};
|
|
27663
|
+
|
|
27664
|
+
function getEditIndexAtPoint(p) {
|
|
27665
|
+
var provider, layout, i;
|
|
27666
|
+
if (!session || !session.nodes) return -1;
|
|
27667
|
+
provider = getProvider(session.nodes);
|
|
27668
|
+
layout = getLayout(session);
|
|
27669
|
+
i = getCaretIndexAtPoint(provider, p, getRenderedLength(session.text, layout));
|
|
27670
|
+
return i < 0 ? -1 : getEditIndex(session.text, i, layout);
|
|
27671
|
+
}
|
|
27672
|
+
|
|
26323
27673
|
self.isOpen = function() {
|
|
26324
27674
|
return !!session;
|
|
26325
27675
|
};
|
|
@@ -26350,6 +27700,7 @@
|
|
|
26350
27700
|
// arriving just after a redraw is holding one of those.
|
|
26351
27701
|
if (session.pending && node.closest &&
|
|
26352
27702
|
node.closest('.label-edit-pending')) return true;
|
|
27703
|
+
if (session.nodes && session.nodes.symbol.contains(node)) return true;
|
|
26353
27704
|
return !!groups && (groups.hit.contains(node) || groups.back.contains(node) ||
|
|
26354
27705
|
groups.front.contains(node));
|
|
26355
27706
|
};
|
|
@@ -26408,7 +27759,7 @@
|
|
|
26408
27759
|
if (i < 0) return false;
|
|
26409
27760
|
i = getEditIndex(session.text, i, layout);
|
|
26410
27761
|
setCaret(i, i);
|
|
26411
|
-
|
|
27762
|
+
redrawCaret();
|
|
26412
27763
|
return true;
|
|
26413
27764
|
};
|
|
26414
27765
|
|
|
@@ -26442,7 +27793,7 @@
|
|
|
26442
27793
|
var blank = textHasNoGlyphs(o.text);
|
|
26443
27794
|
var breaks = getBreaks(o);
|
|
26444
27795
|
if (o.pending) {
|
|
26445
|
-
if (!blank) o.pending.create(
|
|
27796
|
+
if (!blank) o.pending.create(writeLabelValue(o.text, breaks, o.bold));
|
|
26446
27797
|
return;
|
|
26447
27798
|
}
|
|
26448
27799
|
if (blank) {
|
|
@@ -26450,8 +27801,9 @@
|
|
|
26450
27801
|
removeLabel(o);
|
|
26451
27802
|
return;
|
|
26452
27803
|
}
|
|
26453
|
-
if (o.text === o.startText && sameSoftBreaks(breaks, o.startBreaks)
|
|
26454
|
-
|
|
27804
|
+
if (o.text === o.startText && sameSoftBreaks(breaks, o.startBreaks) &&
|
|
27805
|
+
sameBoldRanges(o.bold, o.startBold)) return;
|
|
27806
|
+
runGuiEditCommand(gui, getLabelTextCommand(writeLabelValue(o.text, breaks, o.bold),
|
|
26455
27807
|
o.id, o.target.name), {
|
|
26456
27808
|
title: 'Label text'
|
|
26457
27809
|
});
|
|
@@ -26464,7 +27816,7 @@
|
|
|
26464
27816
|
var rec;
|
|
26465
27817
|
if (!shp || shp.length > 1) return [];
|
|
26466
27818
|
rec = o.pending ? o.pending.getStyle() : getRecord(o.target, o.id);
|
|
26467
|
-
return getSoftBreaks(o.text, rec);
|
|
27819
|
+
return getSoftBreaks(o.text, rec, o.bold);
|
|
26468
27820
|
}
|
|
26469
27821
|
|
|
26470
27822
|
// Deletes the label's feature, geometry and all.
|
|
@@ -26551,7 +27903,7 @@
|
|
|
26551
27903
|
var rec = Object.assign({}, o.pending.getStyle());
|
|
26552
27904
|
// With its soft breaks, so that what is aligned and what a callout meets
|
|
26553
27905
|
// is the block as wrapped
|
|
26554
|
-
rec['label-text'] = encodeLabelText(
|
|
27906
|
+
rec['label-text'] = encodeLabelText(writeLabelValue(o.text, getBreaks(o), o.bold));
|
|
26555
27907
|
// No need to expand label-pos here, or to measure its text: the renderer
|
|
26556
27908
|
// resolves the position and asks for the width it needs, so a pending
|
|
26557
27909
|
// label is laid out by exactly the same code as a committed one.
|
|
@@ -26599,11 +27951,15 @@
|
|
|
26599
27951
|
// - Line breaks. An empty <tspan> lays out nothing, so a line just opened
|
|
26600
27952
|
// with Enter had no position for the caret to move to. The placeholder in
|
|
26601
27953
|
// getRenderedLines() gives it one.
|
|
27954
|
+
//
|
|
27955
|
+
// Bold stretches are <tspan>s inside the line they fall on. The character
|
|
27956
|
+
// position APIs count the characters of descendant elements in document
|
|
27957
|
+
// order, so the rendered indexes stay the edited ones whatever the runs are.
|
|
26602
27958
|
function writeText(o) {
|
|
26603
27959
|
var content = o.nodes.content;
|
|
26604
27960
|
var breaks = getBreaks(o);
|
|
26605
|
-
var written = o.text + '\u0000' + breaks.join(',');
|
|
26606
|
-
var lines, i, tspan;
|
|
27961
|
+
var written = o.text + '\u0000' + breaks.join(',') + '\u0000' + o.bold.join(';');
|
|
27962
|
+
var lines, full, pos, i, tspan;
|
|
26607
27963
|
// refresh() runs on every map render, which during a pan is every frame;
|
|
26608
27964
|
// rebuilding text nodes that already say the right thing is pure waste
|
|
26609
27965
|
if (o.writtenTo === content && o.writtenText === written) return;
|
|
@@ -26617,23 +27973,53 @@
|
|
|
26617
27973
|
// or from a label that was multi-line before it was given a path -- and
|
|
26618
27974
|
// turning them into spaces is what export does too.
|
|
26619
27975
|
if (getLayout(o) === LINES_JOINED) {
|
|
26620
|
-
|
|
27976
|
+
full = getRenderedContent(o.text);
|
|
27977
|
+
appendRuns(content, full, o.bold, 0, full.length);
|
|
26621
27978
|
return;
|
|
26622
27979
|
}
|
|
26623
27980
|
// A soft break keeps the space it broke at at the end of its line, where
|
|
26624
27981
|
// the rendered label drops it: here every typed character has to be one
|
|
26625
27982
|
// the caret can sit beside.
|
|
27983
|
+
//
|
|
27984
|
+
// The lines laid end to end are the edited text character for character --
|
|
27985
|
+
// each hard break is the placeholder that opens the line after it -- so a
|
|
27986
|
+
// line's offset in the one is its offset in the other.
|
|
26626
27987
|
lines = getRenderedLines(o.text, breaks);
|
|
26627
|
-
|
|
27988
|
+
full = lines.join('');
|
|
27989
|
+
pos = lines[0].length;
|
|
27990
|
+
appendRuns(content, full, o.bold, 0, pos);
|
|
26628
27991
|
for (i = 1; i < lines.length; i++) {
|
|
26629
27992
|
tspan = document.createElementNS(SVG_NS$3, 'tspan');
|
|
26630
27993
|
tspan.setAttribute('x', o.nodes.text.getAttribute('x') || 0);
|
|
26631
27994
|
tspan.setAttribute('dy', getLineHeight(o));
|
|
26632
|
-
tspan.
|
|
27995
|
+
appendRuns(tspan, full, o.bold, pos, pos + lines[i].length);
|
|
27996
|
+
pos += lines[i].length;
|
|
26633
27997
|
content.appendChild(tspan);
|
|
26634
27998
|
}
|
|
26635
27999
|
}
|
|
26636
28000
|
|
|
28001
|
+
// Characters [start, end) of @str into @parent, the bold ones in bold
|
|
28002
|
+
// <tspan>s and the rest as text. An empty stretch still gets a text node, as
|
|
28003
|
+
// an empty line always did.
|
|
28004
|
+
function appendRuns(parent, str, bold, start, end) {
|
|
28005
|
+
var runs = internal.svg.getBoldRuns(str, bold, start, end);
|
|
28006
|
+
var tspan;
|
|
28007
|
+
if (runs.length === 0) {
|
|
28008
|
+
parent.appendChild(document.createTextNode(''));
|
|
28009
|
+
return;
|
|
28010
|
+
}
|
|
28011
|
+
runs.forEach(function(run) {
|
|
28012
|
+
if (!run.bold) {
|
|
28013
|
+
parent.appendChild(document.createTextNode(run.text));
|
|
28014
|
+
return;
|
|
28015
|
+
}
|
|
28016
|
+
tspan = document.createElementNS(SVG_NS$3, 'tspan');
|
|
28017
|
+
tspan.setAttribute('font-weight', internal.svg.LABEL_BOLD_WEIGHT);
|
|
28018
|
+
tspan.appendChild(document.createTextNode(run.text));
|
|
28019
|
+
parent.appendChild(tspan);
|
|
28020
|
+
});
|
|
28021
|
+
}
|
|
28022
|
+
|
|
26637
28023
|
// Cached per session: asking costs a record lookup and a geometry test, and
|
|
26638
28024
|
// the answer cannot change while a session is open.
|
|
26639
28025
|
function getLayout(o) {
|
|
@@ -26759,18 +28145,33 @@
|
|
|
26759
28145
|
return el;
|
|
26760
28146
|
}
|
|
26761
28147
|
|
|
28148
|
+
// Updated in place rather than rebuilt. Every caret move redraws the
|
|
28149
|
+
// overlay, and a click is one: a region replaced by the first click of a
|
|
28150
|
+
// double-click takes the node the button went down on out of the document,
|
|
28151
|
+
// and the browser then fires neither the click nor the dblclick.
|
|
26762
28152
|
function drawHitRegion(o, g, box, caret, pathId) {
|
|
26763
|
-
var
|
|
26764
|
-
|
|
26765
|
-
if (box
|
|
26766
|
-
|
|
28153
|
+
var area = g.querySelector('.label-edit-hit-area');
|
|
28154
|
+
var line = g.querySelector('.label-edit-hit-baseline');
|
|
28155
|
+
if (box && area) {
|
|
28156
|
+
setRect(area, box);
|
|
28157
|
+
} else if (box) {
|
|
28158
|
+
g.insertBefore(rect(box, 'label-edit-hit-area'), g.firstChild);
|
|
28159
|
+
} else if (area) {
|
|
28160
|
+
g.removeChild(area);
|
|
28161
|
+
}
|
|
28162
|
+
if (!pathId) {
|
|
28163
|
+
if (line) g.removeChild(line);
|
|
28164
|
+
return;
|
|
28165
|
+
}
|
|
26767
28166
|
// A thickened, transparent copy of the baseline: about one em wide, so that
|
|
26768
28167
|
// the region follows the curve rather than boxing it.
|
|
26769
|
-
|
|
26770
|
-
|
|
26771
|
-
|
|
26772
|
-
|
|
26773
|
-
|
|
28168
|
+
if (!line) {
|
|
28169
|
+
line = document.createElementNS(SVG_NS$3, 'use');
|
|
28170
|
+
line.setAttribute('class', 'label-edit-hit-baseline');
|
|
28171
|
+
g.appendChild(line);
|
|
28172
|
+
}
|
|
28173
|
+
line.setAttribute('href', '#' + pathId);
|
|
28174
|
+
line.setAttribute('stroke-width', caret ? caret.ascent + caret.descent : 12);
|
|
26774
28175
|
}
|
|
26775
28176
|
|
|
26776
28177
|
function getLabelPathId(nodes) {
|
|
@@ -26863,12 +28264,16 @@
|
|
|
26863
28264
|
|
|
26864
28265
|
function rect(box, className) {
|
|
26865
28266
|
var el = document.createElementNS(SVG_NS$3, 'rect');
|
|
28267
|
+
setRect(el, box);
|
|
28268
|
+
el.setAttribute('class', className);
|
|
28269
|
+
return el;
|
|
28270
|
+
}
|
|
28271
|
+
|
|
28272
|
+
function setRect(el, box) {
|
|
26866
28273
|
el.setAttribute('x', box.x);
|
|
26867
28274
|
el.setAttribute('y', box.y);
|
|
26868
28275
|
el.setAttribute('width', box.width);
|
|
26869
28276
|
el.setAttribute('height', box.height);
|
|
26870
|
-
el.setAttribute('class', className);
|
|
26871
|
-
return el;
|
|
26872
28277
|
}
|
|
26873
28278
|
|
|
26874
28279
|
function caretLine(caret) {
|
|
@@ -26898,6 +28303,7 @@
|
|
|
26898
28303
|
|
|
26899
28304
|
textarea.addEventListener('input', function() {
|
|
26900
28305
|
if (!session) return;
|
|
28306
|
+
session.bold = updateBoldForEdit(session.bold, session.text, textarea.value);
|
|
26901
28307
|
session.text = textarea.value;
|
|
26902
28308
|
self.refresh();
|
|
26903
28309
|
});
|
|
@@ -26922,6 +28328,13 @@
|
|
|
26922
28328
|
return !e.isComposing && e.keyCode != 229;
|
|
26923
28329
|
}
|
|
26924
28330
|
|
|
28331
|
+
// Cmd-B on a Mac, Ctrl-B elsewhere, as in every text editor. A textarea
|
|
28332
|
+
// has no default for it to replace.
|
|
28333
|
+
function isBoldKey(e) {
|
|
28334
|
+
if (!(e.metaKey || e.ctrlKey) || e.shiftKey || e.altKey) return false;
|
|
28335
|
+
return (e.key || '').toLowerCase() == 'b';
|
|
28336
|
+
}
|
|
28337
|
+
|
|
26925
28338
|
textarea.addEventListener('keydown', function(e) {
|
|
26926
28339
|
if (!session) return;
|
|
26927
28340
|
// The GUI's global key handlers are told to keep out of the way by
|
|
@@ -26948,6 +28361,10 @@
|
|
|
26948
28361
|
e.preventDefault();
|
|
26949
28362
|
e.stopPropagation();
|
|
26950
28363
|
self.close();
|
|
28364
|
+
} else if (isBoldKey(e)) {
|
|
28365
|
+
e.preventDefault();
|
|
28366
|
+
e.stopPropagation();
|
|
28367
|
+
self.toggleBold();
|
|
26951
28368
|
} else {
|
|
26952
28369
|
e.stopPropagation();
|
|
26953
28370
|
}
|
|
@@ -27001,10 +28418,11 @@
|
|
|
27001
28418
|
return textarea ? textarea.selectionEnd : 0;
|
|
27002
28419
|
}
|
|
27003
28420
|
|
|
27004
|
-
|
|
28421
|
+
// @direction: 'forward' (the default) or 'backward', which end of the
|
|
28422
|
+
// selection shift-arrow moves
|
|
28423
|
+
function setCaret(start, end, direction) {
|
|
27005
28424
|
if (!textarea) return;
|
|
27006
|
-
textarea.
|
|
27007
|
-
textarea.selectionEnd = end;
|
|
28425
|
+
textarea.setSelectionRange(start, end, direction || 'forward');
|
|
27008
28426
|
}
|
|
27009
28427
|
}
|
|
27010
28428
|
|
|
@@ -27697,7 +29115,7 @@
|
|
|
27697
29115
|
}
|
|
27698
29116
|
|
|
27699
29117
|
// The label tool: arming a label type, placing an anchor or a curve, turning
|
|
27700
|
-
// the result into
|
|
29118
|
+
// the result into a -labels coordinates= command, and handing the new label to the
|
|
27701
29119
|
// in-place text editor.
|
|
27702
29120
|
//
|
|
27703
29121
|
// Creating a label and editing one are one gesture, not two: a click leaves a
|
|
@@ -27761,6 +29179,14 @@
|
|
|
27761
29179
|
// The mouseup that ended the last block drag, or null. A short one is
|
|
27762
29180
|
// followed by a click from the same mouseup, which it has already handled.
|
|
27763
29181
|
var blockRelease = null;
|
|
29182
|
+
// A drag across the text of the label being typed into, which selects the
|
|
29183
|
+
// characters it passes over: {start}, where start is the pointer's position
|
|
29184
|
+
// at the press, filled in by the first drag event.
|
|
29185
|
+
var selectDrag = null;
|
|
29186
|
+
// The mouseup that ended the last select drag, or null, for the same reason
|
|
29187
|
+
// as blockRelease: the click that follows it would put the caret down and
|
|
29188
|
+
// lose the selection just made.
|
|
29189
|
+
var selectRelease = null;
|
|
27764
29190
|
var editor = new LabelEditor(gui, ext);
|
|
27765
29191
|
var selection = new LabelSelection(gui, ext, hit, function() {
|
|
27766
29192
|
return editor.isOpen() ? editor.getFeatureId() : -1;
|
|
@@ -27790,7 +29216,7 @@
|
|
|
27790
29216
|
// tools do on entry as well (`addEmptyLayer()` in `gui-edit-points.mjs` and
|
|
27791
29217
|
// `gui-draw-lines2.mjs`). It is named rather than left unnamed, unlike
|
|
27792
29218
|
// theirs: it is the layer `getLabelTarget()` would have created for a label
|
|
27793
|
-
// anyway, and naming it is what lets the `-
|
|
29219
|
+
// anyway, and naming it is what lets the `-labels` commands name their
|
|
27794
29220
|
// target and so replay from the session history.
|
|
27795
29221
|
//
|
|
27796
29222
|
// Called before entering the tool's own mode, because getInitialTool() reads
|
|
@@ -27840,6 +29266,8 @@
|
|
|
27840
29266
|
handleDrag = null;
|
|
27841
29267
|
releasedDrag = null;
|
|
27842
29268
|
blockRelease = null;
|
|
29269
|
+
selectDrag = null;
|
|
29270
|
+
selectRelease = null;
|
|
27843
29271
|
shownHandles = null;
|
|
27844
29272
|
clearBlockDrag();
|
|
27845
29273
|
hoverHandle = null;
|
|
@@ -27966,6 +29394,10 @@
|
|
|
27966
29394
|
blockRelease = null;
|
|
27967
29395
|
return;
|
|
27968
29396
|
}
|
|
29397
|
+
if (selectRelease && getDomEvent(e) == selectRelease) {
|
|
29398
|
+
selectRelease = null;
|
|
29399
|
+
return;
|
|
29400
|
+
}
|
|
27969
29401
|
if (armed == 'path' && drawingCurve()) {
|
|
27970
29402
|
extendCurve(pixToMapCoords(e.x, e.y));
|
|
27971
29403
|
return;
|
|
@@ -28038,9 +29470,34 @@
|
|
|
28038
29470
|
e.flipLabel = getFlipAction(target, id);
|
|
28039
29471
|
}
|
|
28040
29472
|
}
|
|
29473
|
+
if (target && editor.isOpen() && clickIsOnEditedLabel(e)) {
|
|
29474
|
+
addBoldAction(e, target);
|
|
29475
|
+
}
|
|
28041
29476
|
gui.contextMenu.open(e, target);
|
|
28042
29477
|
});
|
|
28043
29478
|
|
|
29479
|
+
// Bold for the words being typed: the selection, or with nothing selected,
|
|
29480
|
+
// the word that was right-clicked. The item says what it will do, since a
|
|
29481
|
+
// menu has no pressed state to show that the words are bold already. There
|
|
29482
|
+
// is none in a font with no bold face (see fontHasBoldFace()).
|
|
29483
|
+
function addBoldAction(e, target) {
|
|
29484
|
+
var range = editor.getBoldTargetAtPoint(getLabelSpacePoint(editor.getAnchorCoords(), e));
|
|
29485
|
+
if (!range || !fontHasBoldFace(getEditedFontName(target))) return;
|
|
29486
|
+
e.boldText = {
|
|
29487
|
+
label: range.bold ? 'remove bold' : 'bold text',
|
|
29488
|
+
run: function() { editor.toggleBold(range.start, range.end); }
|
|
29489
|
+
};
|
|
29490
|
+
}
|
|
29491
|
+
|
|
29492
|
+
// The font the label being typed into is drawn in: its own, or for a label
|
|
29493
|
+
// not created yet, the one the panel will give it.
|
|
29494
|
+
function getEditedFontName(target) {
|
|
29495
|
+
var id = editor.getFeatureId();
|
|
29496
|
+
var rec = id > -1 ? getRecord(target, id) : getNewLabelStyle(gui);
|
|
29497
|
+
var font = rec && rec['font-family'];
|
|
29498
|
+
return font || (id > -1 ? getDefaultFontName() : getNewLabelFontName());
|
|
29499
|
+
}
|
|
29500
|
+
|
|
28044
29501
|
// The label a right-click was aimed at, or -1.
|
|
28045
29502
|
//
|
|
28046
29503
|
// A label being typed into is not a hit target -- the editor's own overlay
|
|
@@ -28296,7 +29753,16 @@
|
|
|
28296
29753
|
// takes a drag -- everything else over a label is left alone so that the map
|
|
28297
29754
|
// still pans, which is why these handlers stop the event themselves.
|
|
28298
29755
|
hit.on('dragstart', function(e) {
|
|
28299
|
-
if (!active() || drawingCurve()
|
|
29756
|
+
if (!active() || drawingCurve()) return;
|
|
29757
|
+
// With a label open for typing, a drag across its text selects some of it,
|
|
29758
|
+
// as it would in any text field. Anywhere else the map still pans.
|
|
29759
|
+
if (editor.isOpen()) {
|
|
29760
|
+
if (clickIsOnEditedLabel(e)) {
|
|
29761
|
+
selectDrag = {start: null};
|
|
29762
|
+
consumeDrag(e);
|
|
29763
|
+
}
|
|
29764
|
+
return;
|
|
29765
|
+
}
|
|
28300
29766
|
// The handle comes from the last hover, not from testing the pointer here.
|
|
28301
29767
|
// dragstart arrives with the pointer already moved off the handle -- by the
|
|
28302
29768
|
// first mousemove that made it a drag, which on a quick gesture is further
|
|
@@ -28321,6 +29787,22 @@
|
|
|
28321
29787
|
consumeDrag(e);
|
|
28322
29788
|
});
|
|
28323
29789
|
|
|
29790
|
+
// The selection runs from where the button went down, which dragstart does
|
|
29791
|
+
// not report -- it arrives after the first move -- but every drag event
|
|
29792
|
+
// carries its distance from the press.
|
|
29793
|
+
function updateSelectDrag(e) {
|
|
29794
|
+
var anchor = editor.getAnchorCoords();
|
|
29795
|
+
if (!editor.isOpen() || !anchor) {
|
|
29796
|
+
selectDrag = null;
|
|
29797
|
+
return;
|
|
29798
|
+
}
|
|
29799
|
+
if (!selectDrag.start) {
|
|
29800
|
+
selectDrag.start = {x: e.x - (e.dragX || 0), y: e.y - (e.dragY || 0)};
|
|
29801
|
+
}
|
|
29802
|
+
editor.selectBetweenPoints(getLabelSpacePoint(anchor, selectDrag.start),
|
|
29803
|
+
getLabelSpacePoint(anchor, e));
|
|
29804
|
+
}
|
|
29805
|
+
|
|
28324
29806
|
// handle: {id, index, point, pointer} from findHandle(), or the same shape
|
|
28325
29807
|
// made up for a drag that grabbed a label somewhere other than its knot
|
|
28326
29808
|
function beginKnotDrag(handle) {
|
|
@@ -28346,6 +29828,11 @@
|
|
|
28346
29828
|
|
|
28347
29829
|
hit.on('drag', function(e) {
|
|
28348
29830
|
var shp, p;
|
|
29831
|
+
if (selectDrag) {
|
|
29832
|
+
consumeDrag(e);
|
|
29833
|
+
updateSelectDrag(e);
|
|
29834
|
+
return;
|
|
29835
|
+
}
|
|
28349
29836
|
if (textDrag) {
|
|
28350
29837
|
consumeDrag(e);
|
|
28351
29838
|
updateTextDrag(e);
|
|
@@ -28380,6 +29867,12 @@
|
|
|
28380
29867
|
|
|
28381
29868
|
hit.on('dragend', function(e) {
|
|
28382
29869
|
var o = drag;
|
|
29870
|
+
if (selectDrag) {
|
|
29871
|
+
selectDrag = null;
|
|
29872
|
+
selectRelease = getDomEvent(e);
|
|
29873
|
+
consumeDrag(e);
|
|
29874
|
+
return;
|
|
29875
|
+
}
|
|
28383
29876
|
if (textDrag) {
|
|
28384
29877
|
o = textDrag;
|
|
28385
29878
|
textDrag = null;
|
|
@@ -29020,9 +30513,9 @@
|
|
|
29020
30513
|
selection.refresh(true);
|
|
29021
30514
|
}
|
|
29022
30515
|
|
|
29023
|
-
// Turns the previewed offset into one -
|
|
30516
|
+
// Turns the previewed offset into one -labels.
|
|
29024
30517
|
//
|
|
29025
|
-
// Only the label under the pointer moves, even with several selected: -
|
|
30518
|
+
// Only the label under the pointer moves, even with several selected: -labels
|
|
29026
30519
|
// writes one value to every id it is given, so a group drag would set them
|
|
29027
30520
|
// all to the same absolute offset rather than nudging each by its own delta.
|
|
29028
30521
|
function commitOffsetDrag(o) {
|
|
@@ -29166,7 +30659,7 @@
|
|
|
29166
30659
|
}
|
|
29167
30660
|
|
|
29168
30661
|
function dragging() {
|
|
29169
|
-
return !!(drag || textDrag || handleDrag || blockDrag);
|
|
30662
|
+
return !!(drag || textDrag || handleDrag || blockDrag || selectDrag);
|
|
29170
30663
|
}
|
|
29171
30664
|
|
|
29172
30665
|
// The exception is about the pointer being on the glyphs, not about the mode:
|
|
@@ -29284,6 +30777,12 @@
|
|
|
29284
30777
|
resetHandle(hoverHandle);
|
|
29285
30778
|
return;
|
|
29286
30779
|
}
|
|
30780
|
+
// Inside the text being typed, a double-click selects a word -- which is
|
|
30781
|
+
// the quickest way to pick the words to set in bold.
|
|
30782
|
+
if (editor.isOpen() && clickIsOnEditedLabel(e)) {
|
|
30783
|
+
editor.selectWordAtPoint(getLabelSpacePoint(editor.getAnchorCoords(), e));
|
|
30784
|
+
return;
|
|
30785
|
+
}
|
|
29287
30786
|
// Double-clicking a label reaches straight into its text, which is the
|
|
29288
30787
|
// gesture most editors use and saves selecting it first. The single-click
|
|
29289
30788
|
// route got there already if the label was the selection; this covers the
|
|
@@ -29576,10 +31075,20 @@
|
|
|
29576
31075
|
// array of field names of relevant svg display properties
|
|
29577
31076
|
fields = getStyleFields(lyr).filter(function(f) {return f in styleIndex;}),
|
|
29578
31077
|
records = lyr.data.getRecords();
|
|
31078
|
+
// Arrowheads are drawn by the canvas as shapes, not set as attributes, so
|
|
31079
|
+
// they are not among the attribute fields. Assigned on every call, like the
|
|
31080
|
+
// rest, because the style object is reused from one feature to the next.
|
|
31081
|
+
var arrowFields = getLineArrowFields(lyr);
|
|
31082
|
+
var hasStrokeFields = fields.includes('stroke') || fields.includes('stroke-width');
|
|
29579
31083
|
|
|
29580
31084
|
var styler = function(style, i) {
|
|
29581
31085
|
var rec = records[i];
|
|
29582
31086
|
var fname, val;
|
|
31087
|
+
if (arrowFields) {
|
|
31088
|
+
style.lineStart = rec && rec['line-start'];
|
|
31089
|
+
style.lineEnd = rec && rec['line-end'];
|
|
31090
|
+
style.lineEndSize = rec && rec['line-end-size'];
|
|
31091
|
+
}
|
|
29583
31092
|
for (var j=0; j<fields.length; j++) {
|
|
29584
31093
|
fname = fields[j];
|
|
29585
31094
|
val = rec && rec[fname];
|
|
@@ -29601,6 +31110,12 @@
|
|
|
29601
31110
|
}
|
|
29602
31111
|
};
|
|
29603
31112
|
var style = {styler: styler, type: 'styled'};
|
|
31113
|
+
// A line layer styled with nothing but arrowheads is drawn the way SVG
|
|
31114
|
+
// export draws it, with the black 1px line the layer's group gives it.
|
|
31115
|
+
if (arrowFields && !hasStrokeFields) {
|
|
31116
|
+
style.strokeColor = 'black';
|
|
31117
|
+
style.strokeWidth = 1;
|
|
31118
|
+
}
|
|
29604
31119
|
// use squares if radius is missing... (TODO: check behavior with labels, etc)
|
|
29605
31120
|
if (lyr.geometry_type == 'point' && fields.includes('r') === false) {
|
|
29606
31121
|
style.dotSize = 1;
|
|
@@ -29615,7 +31130,18 @@
|
|
|
29615
31130
|
// return fields.indexOf('r') > -1; // require 'r' field for point symbols
|
|
29616
31131
|
return fields.includes('fill') || fields.includes('r'); // support colored squares
|
|
29617
31132
|
}
|
|
29618
|
-
return utils$1.difference(fields, ['opacity', 'class']).length > 0
|
|
31133
|
+
return utils$1.difference(fields, ['opacity', 'class']).length > 0 ||
|
|
31134
|
+
!!getLineArrowFields(lyr);
|
|
31135
|
+
}
|
|
31136
|
+
|
|
31137
|
+
// The arrowhead fields a line layer has, or null
|
|
31138
|
+
function getLineArrowFields(lyr) {
|
|
31139
|
+
var fields;
|
|
31140
|
+
if (lyr.geometry_type != 'polyline' || !lyr.data) return null;
|
|
31141
|
+
fields = lyr.data.getFields().filter(function(f) {
|
|
31142
|
+
return internal.svg.lineArrowFields.includes(f);
|
|
31143
|
+
});
|
|
31144
|
+
return fields.length > 0 ? fields : null;
|
|
29619
31145
|
}
|
|
29620
31146
|
|
|
29621
31147
|
function getStyleFields(lyr) {
|
|
@@ -30411,7 +31937,7 @@
|
|
|
30411
31937
|
canv.setAttribute('height', h);
|
|
30412
31938
|
ctx.scale(w / tw, h / th);
|
|
30413
31939
|
if (o.background) {
|
|
30414
|
-
ctx.fillStyle = o.background;
|
|
31940
|
+
ctx.fillStyle = getCanvasColor(o.background);
|
|
30415
31941
|
ctx.fillRect(0, 0, tw, th);
|
|
30416
31942
|
}
|
|
30417
31943
|
if (o.type == 'dots' || o.type == 'squares') makeDotFill(o, ctx, 1);
|
|
@@ -30475,10 +32001,16 @@
|
|
|
30475
32001
|
};
|
|
30476
32002
|
}
|
|
30477
32003
|
|
|
32004
|
+
// SVG's "none" is not a canvas colour, and a canvas ignores a fillStyle it
|
|
32005
|
+
// cannot parse -- keeping the last one set, which on a fresh tile is black.
|
|
32006
|
+
function getCanvasColor(color) {
|
|
32007
|
+
return String(color).trim().toLowerCase() == 'none' ? 'transparent' : color;
|
|
32008
|
+
}
|
|
32009
|
+
|
|
30478
32010
|
function makeDashFill(o, ctx, res) {
|
|
30479
32011
|
var x = 0;
|
|
30480
32012
|
for (var i=0; i<o.colors.length; i++) {
|
|
30481
|
-
ctx.fillStyle = o.colors[i];
|
|
32013
|
+
ctx.fillStyle = getCanvasColor(o.colors[i]);
|
|
30482
32014
|
ctx.fillRect(x, 0, o.width * res, o.dashes[0] * res);
|
|
30483
32015
|
x += res * (o.spacing + o.width);
|
|
30484
32016
|
}
|
|
@@ -30493,7 +32025,7 @@
|
|
|
30493
32025
|
var x = 0, y = 0;
|
|
30494
32026
|
for (var i=0; i<dots; i++) {
|
|
30495
32027
|
if (o.type == 'dots') ctx.beginPath();
|
|
30496
|
-
ctx.fillStyle = o.colors[(i + Math.floor(i / n)) % n];
|
|
32028
|
+
ctx.fillStyle = getCanvasColor(o.colors[(i + Math.floor(i / n)) % n]);
|
|
30497
32029
|
if (o.type == 'dots') {
|
|
30498
32030
|
ctx.arc(x + r, y + r, r, 0, Math.PI * 2);
|
|
30499
32031
|
} else {
|
|
@@ -30510,8 +32042,8 @@
|
|
|
30510
32042
|
var w;
|
|
30511
32043
|
for (var i=0, x=0; i<o.widths.length; i++) {
|
|
30512
32044
|
w = o.widths[i] * res;
|
|
30513
|
-
ctx.fillStyle = o.colors[i];
|
|
30514
|
-
ctx.fillRect(x, 0,
|
|
32045
|
+
ctx.fillStyle = getCanvasColor(o.colors[i]);
|
|
32046
|
+
ctx.fillRect(x, 0, w, h);
|
|
30515
32047
|
x += w;
|
|
30516
32048
|
}
|
|
30517
32049
|
}
|
|
@@ -30568,7 +32100,7 @@
|
|
|
30568
32100
|
arcs = getArcsForRendering(lyr, ext);
|
|
30569
32101
|
filter = getShapeFilter(arcs, layer.shapes, ext);
|
|
30570
32102
|
canv.drawStyledPaths(layer.shapes, arcs, style, filter);
|
|
30571
|
-
if (style.vertices) {
|
|
32103
|
+
if (style.vertices || style.vertex_overlay || style.pending_snip) {
|
|
30572
32104
|
canv.drawVertices(layer.shapes, arcs, style, filter);
|
|
30573
32105
|
}
|
|
30574
32106
|
}
|
|
@@ -30715,6 +32247,8 @@
|
|
|
30715
32247
|
};
|
|
30716
32248
|
*/
|
|
30717
32249
|
|
|
32250
|
+
// Draws a dot at each vertex (if style.vertices is set), and the markers of
|
|
32251
|
+
// style.vertex_overlay and style.pending_snip.
|
|
30718
32252
|
_self.drawVertices = function(shapes, arcs, style, filter) {
|
|
30719
32253
|
var iter = new internal.ShapeIter(arcs);
|
|
30720
32254
|
var t = getScaledTransform(_ext);
|
|
@@ -30724,7 +32258,7 @@
|
|
|
30724
32258
|
var i, j, p;
|
|
30725
32259
|
_ctx.beginPath();
|
|
30726
32260
|
_ctx.fillStyle = color;
|
|
30727
|
-
for (i=0; i<shapes.length; i++) {
|
|
32261
|
+
for (i=0; style.vertices && i<shapes.length; i++) {
|
|
30728
32262
|
var shp = shapes[i];
|
|
30729
32263
|
if (!shp || filter && !filter(i)) continue;
|
|
30730
32264
|
for (j=0; j<shp.length; j++) {
|
|
@@ -30762,8 +32296,10 @@
|
|
|
30762
32296
|
_self.drawStyledPaths = function(shapes, arcs, style, filter) {
|
|
30763
32297
|
var styleIndex = {};
|
|
30764
32298
|
var batchSize = 100;
|
|
30765
|
-
var
|
|
32299
|
+
var lineScale = getScaledLineScale(_ext, style);
|
|
32300
|
+
var startPath = getPathStart(_ext, lineScale);
|
|
30766
32301
|
var draw = getShapePencil(arcs, _ext);
|
|
32302
|
+
var arrowPencil = null;
|
|
30767
32303
|
var key, item, shp;
|
|
30768
32304
|
var styler = style.styler || null;
|
|
30769
32305
|
var drawStyle = styler ? utils$1.defaults({}, style) : style;
|
|
@@ -30773,6 +32309,13 @@
|
|
|
30773
32309
|
if (styler) {
|
|
30774
32310
|
styler(drawStyle, i);
|
|
30775
32311
|
}
|
|
32312
|
+
// A line with arrowheads is drawn on its own, trimmed to meet its heads;
|
|
32313
|
+
// see svg-line-arrows.mjs. Only lines styled with line-start or line-end
|
|
32314
|
+
// get here, so the batch below is untouched for everything else.
|
|
32315
|
+
if (drawStyle.lineStart || drawStyle.lineEnd) {
|
|
32316
|
+
if (!arrowPencil) arrowPencil = getArrowLinePencil(arcs, _ext, lineScale);
|
|
32317
|
+
if (arrowPencil(shp, _ctx, drawStyle, startPath, draw)) continue;
|
|
32318
|
+
}
|
|
30776
32319
|
if (!drawStyle.batchOverlay && (drawStyle.overlay ||
|
|
30777
32320
|
drawStyle.opacity < 1 || drawStyle.fillOpacity < 1 ||
|
|
30778
32321
|
drawStyle.strokeOpacity < 1 || drawStyle.fillEffect)) {
|
|
@@ -31127,6 +32670,10 @@
|
|
|
31127
32670
|
return x + 0.5 | 0;
|
|
31128
32671
|
}
|
|
31129
32672
|
|
|
32673
|
+
function noRound(x) {
|
|
32674
|
+
return x;
|
|
32675
|
+
}
|
|
32676
|
+
|
|
31130
32677
|
function roundToHalfPix(x) {
|
|
31131
32678
|
return (x * 2 | 0) / 2;
|
|
31132
32679
|
}
|
|
@@ -31138,15 +32685,113 @@
|
|
|
31138
32685
|
if (style.fillEffect) {
|
|
31139
32686
|
ctx.fillStyle = getCanvasFillEffect(ctx, shp, arcs, ext, style);
|
|
31140
32687
|
}
|
|
32688
|
+
// Rounding also drops vertices that land on the same pixel, which keeps
|
|
32689
|
+
// detailed shapes fast to draw, so only a style for a few short paths
|
|
32690
|
+
// should turn it off (see gui-overlay-styler.mjs).
|
|
32691
|
+
var round = style.unroundedCoords ? noRound : roundToPix;
|
|
31141
32692
|
for (var i=0, n=shp ? shp.length : 0; i<n; i++) {
|
|
31142
32693
|
iter.init(shp[i]);
|
|
31143
32694
|
// 0.2 trades visible seams for performance
|
|
31144
32695
|
// drawPath(protectIterForDrawing(iter, ext), t, ctx, 0.2);
|
|
31145
|
-
drawPath2(protectIterForDrawing(iter, ext), t, ctx,
|
|
32696
|
+
drawPath2(protectIterForDrawing(iter, ext), t, ctx, round);
|
|
31146
32697
|
}
|
|
31147
32698
|
};
|
|
31148
32699
|
}
|
|
31149
32700
|
|
|
32701
|
+
// Returns a function that draws one line feature with its arrowheads, and
|
|
32702
|
+
// returns false if the feature has none to draw (a style of 'none', or no
|
|
32703
|
+
// stroke), for the caller to draw it the usual way.
|
|
32704
|
+
//
|
|
32705
|
+
// Heads are sized by the same factor as the line width, so that a preview
|
|
32706
|
+
// matches the exported SVG. Far enough in that paths are clipped to the view
|
|
32707
|
+
// (see protectIterForDrawing()), the line is drawn clipped and untrimmed and
|
|
32708
|
+
// only the heads are added, since a clipped path's ends are not its ends.
|
|
32709
|
+
function getArrowLinePencil(arcs, ext, lineScale) {
|
|
32710
|
+
var t = getScaledTransform(ext);
|
|
32711
|
+
var iter = new internal.ShapeIter(arcs);
|
|
32712
|
+
var strokeScale = getCanvasStrokeScale(GUI.getPixelRatio(), lineScale);
|
|
32713
|
+
var clipped = ext.scale() > 100;
|
|
32714
|
+
return function(shp, ctx, style, startPath, draw) {
|
|
32715
|
+
var opts = internal.svg.makeLineArrowOpts(style.lineStart, style.lineEnd,
|
|
32716
|
+
style.lineEndSize, style.strokeWidth, strokeScale);
|
|
32717
|
+
var heads = [];
|
|
32718
|
+
var i, coords, shape;
|
|
32719
|
+
if (opts.start == 'none' && opts.end == 'none' || !(style.strokeWidth > 0)) {
|
|
32720
|
+
return false;
|
|
32721
|
+
}
|
|
32722
|
+
startPath(ctx, style);
|
|
32723
|
+
for (i=0; i<shp.length; i++) {
|
|
32724
|
+
coords = getPixelCoords(iter, shp[i], t);
|
|
32725
|
+
if (coords.length < 2) continue;
|
|
32726
|
+
shape = internal.svg.getLineArrowShape(coords, opts);
|
|
32727
|
+
heads = heads.concat(shape.heads);
|
|
32728
|
+
if (!clipped) traceCoords(shape.coords, ctx);
|
|
32729
|
+
}
|
|
32730
|
+
if (clipped) draw(shp, ctx, style);
|
|
32731
|
+
endPath(ctx, style);
|
|
32732
|
+
drawArrowHeads(heads, ctx, style, opts.width);
|
|
32733
|
+
return true;
|
|
32734
|
+
};
|
|
32735
|
+
}
|
|
32736
|
+
|
|
32737
|
+
function getPixelCoords(iter, path, t) {
|
|
32738
|
+
var coords = [];
|
|
32739
|
+
var x, y, prev;
|
|
32740
|
+
iter.init(path);
|
|
32741
|
+
while (iter.hasNext()) {
|
|
32742
|
+
x = iter.x * t.mx + t.bx;
|
|
32743
|
+
y = iter.y * t.my + t.by;
|
|
32744
|
+
if (prev && prev[0] == x && prev[1] == y) continue;
|
|
32745
|
+
prev = [x, y];
|
|
32746
|
+
coords.push(prev);
|
|
32747
|
+
}
|
|
32748
|
+
return coords;
|
|
32749
|
+
}
|
|
32750
|
+
|
|
32751
|
+
function traceCoords(coords, ctx) {
|
|
32752
|
+
ctx.moveTo(coords[0][0], coords[0][1]);
|
|
32753
|
+
for (var i=1; i<coords.length; i++) {
|
|
32754
|
+
ctx.lineTo(coords[i][0], coords[i][1]);
|
|
32755
|
+
}
|
|
32756
|
+
}
|
|
32757
|
+
|
|
32758
|
+
// Solid heads and dots are filled with the line's colour; open ones are stroked with
|
|
32759
|
+
// its width, always whole and round-cornered, whatever the line's dashes and
|
|
32760
|
+
// caps.
|
|
32761
|
+
function drawArrowHeads(heads, ctx, style, width) {
|
|
32762
|
+
var alpha = (style.opacity >= 0 ? style.opacity : 1) *
|
|
32763
|
+
(style.strokeOpacity >= 0 ? style.strokeOpacity : 1);
|
|
32764
|
+
var head, p;
|
|
32765
|
+
if (heads.length === 0) return;
|
|
32766
|
+
ctx.globalAlpha = alpha;
|
|
32767
|
+
ctx.fillStyle = style.strokeColor;
|
|
32768
|
+
ctx.strokeStyle = style.strokeColor;
|
|
32769
|
+
ctx.lineWidth = width;
|
|
32770
|
+
ctx.lineCap = 'round';
|
|
32771
|
+
ctx.lineJoin = 'round';
|
|
32772
|
+
ctx.setLineDash([]);
|
|
32773
|
+
for (var i=0; i<heads.length; i++) {
|
|
32774
|
+
head = heads[i];
|
|
32775
|
+
ctx.beginPath();
|
|
32776
|
+
if (head.type == 'dot') {
|
|
32777
|
+
ctx.arc(head.center[0], head.center[1], head.radius, 0, Math.PI * 2);
|
|
32778
|
+
ctx.fill();
|
|
32779
|
+
continue;
|
|
32780
|
+
}
|
|
32781
|
+
p = head.points;
|
|
32782
|
+
ctx.moveTo(p[0][0], p[0][1]);
|
|
32783
|
+
ctx.lineTo(p[1][0], p[1][1]);
|
|
32784
|
+
ctx.lineTo(p[2][0], p[2][1]);
|
|
32785
|
+
if (head.type == 'arrow') {
|
|
32786
|
+
ctx.closePath();
|
|
32787
|
+
ctx.fill();
|
|
32788
|
+
} else {
|
|
32789
|
+
ctx.stroke();
|
|
32790
|
+
}
|
|
32791
|
+
}
|
|
32792
|
+
ctx.globalAlpha = 1;
|
|
32793
|
+
}
|
|
32794
|
+
|
|
31150
32795
|
function protectIterForDrawing(iter, ext) {
|
|
31151
32796
|
var bounds, k;
|
|
31152
32797
|
if (ext.scale() > 100) {
|
|
@@ -34759,13 +36404,13 @@
|
|
|
34759
36404
|
|
|
34760
36405
|
// Font and face menus, as in the label panel
|
|
34761
36406
|
row = makeRow(appearance);
|
|
34762
|
-
fontSelect = El('select').attr('
|
|
36407
|
+
fontSelect = El('select').attr('aria-label', 'Font').addClass('scalebar-font-select')
|
|
34763
36408
|
.appendTo(makeCell(row, 'Font'))
|
|
34764
36409
|
.on('change', function() {
|
|
34765
36410
|
if (fontSelect.node().value) applyFont(fontSelect.node().value);
|
|
34766
36411
|
});
|
|
34767
36412
|
renderFontOptions();
|
|
34768
|
-
fontStyleSelect = El('select').attr('
|
|
36413
|
+
fontStyleSelect = El('select').attr('aria-label', 'Font style')
|
|
34769
36414
|
.addClass('scalebar-font-style-select')
|
|
34770
36415
|
.appendTo(makeCell(row, 'Font style'))
|
|
34771
36416
|
.on('change', function() {
|
|
@@ -34779,7 +36424,7 @@
|
|
|
34779
36424
|
El('span').appendTo(colorCell).text('Color');
|
|
34780
36425
|
colorChit = El('div').addClass('label-color-chit').attr('role', 'button')
|
|
34781
36426
|
.on('click', function() { colorPicker.toggle(); });
|
|
34782
|
-
colorInput = El('input').attr('type', 'text').attr('
|
|
36427
|
+
colorInput = El('input').attr('type', 'text').attr('aria-label', 'Scale bar color')
|
|
34783
36428
|
.on('change', function() {
|
|
34784
36429
|
var color = colorInput.node().value.trim();
|
|
34785
36430
|
if (!color) {
|
|
@@ -34805,7 +36450,7 @@
|
|
|
34805
36450
|
'scalebar-bar-width');
|
|
34806
36451
|
marginInput = makeNumberInput(makeCell(row, 'Inset'), 'margin',
|
|
34807
36452
|
'scalebar-margin')
|
|
34808
|
-
.attr('
|
|
36453
|
+
.attr('aria-label', 'Distance from the edges of the frame, in pixels');
|
|
34809
36454
|
|
|
34810
36455
|
row = El('div').addClass('label-style-row label-panel-button-row').appendTo(form);
|
|
34811
36456
|
makePanelActionButton(row, 'Remove scale bar', function() {
|
|
@@ -36106,6 +37751,25 @@
|
|
|
36106
37751
|
})
|
|
36107
37752
|
};
|
|
36108
37753
|
},
|
|
37754
|
+
// The vertices of each part of each feature in a path layer, in pixels
|
|
37755
|
+
// from the top left of the map, for a test comparing a drawn path with the
|
|
37756
|
+
// pointer positions that drew it.
|
|
37757
|
+
getLayerPathPixels: function(name) {
|
|
37758
|
+
var o = gui.model.getLayers().filter(function(o) {
|
|
37759
|
+
return getLayerName(o.layer) === name;
|
|
37760
|
+
})[0];
|
|
37761
|
+
var ext = gui.map.getExtent();
|
|
37762
|
+
var arcs;
|
|
37763
|
+
if (!o) return null;
|
|
37764
|
+
arcs = o.layer.gui ? o.layer.gui.displayArcs : o.dataset.arcs;
|
|
37765
|
+
return (o.layer.shapes || []).map(function(shp) {
|
|
37766
|
+
return shp ? shp.map(function(ids) {
|
|
37767
|
+
var iter = arcs.getShapeIter(ids), pts = [];
|
|
37768
|
+
while (iter.hasNext()) pts.push(ext.translateCoords(iter.x, iter.y));
|
|
37769
|
+
return pts;
|
|
37770
|
+
}) : null;
|
|
37771
|
+
});
|
|
37772
|
+
},
|
|
36109
37773
|
selectLayer: function(name) {
|
|
36110
37774
|
var target = gui.model.getLayers().filter(function(o) {
|
|
36111
37775
|
return getLayerName(o.layer) === name;
|
|
@@ -36123,6 +37787,19 @@
|
|
|
36123
37787
|
};
|
|
36124
37788
|
});
|
|
36125
37789
|
},
|
|
37790
|
+
// What each overlay layer last drew: the ids of its features, and whether
|
|
37791
|
+
// it marked their vertices. The overlay is drawn to canvas, so there is no
|
|
37792
|
+
// DOM state to assert on.
|
|
37793
|
+
getOverlayInfo: function() {
|
|
37794
|
+
var layers = gui.map && gui.map.getOverlayLayers ?
|
|
37795
|
+
gui.map.getOverlayLayers() : [];
|
|
37796
|
+
return layers.map(function(lyr) {
|
|
37797
|
+
return {
|
|
37798
|
+
ids: lyr.gui.style.ids || null,
|
|
37799
|
+
vertices: !!lyr.gui.style.vertices
|
|
37800
|
+
};
|
|
37801
|
+
});
|
|
37802
|
+
},
|
|
36126
37803
|
// Knot handle positions, in display coordinates.
|
|
36127
37804
|
getLabelPathKnotCoords: function() {
|
|
36128
37805
|
var lyr = getLabelPathGuideLayers(gui).filter(function(lyr) {
|