mapshaper 0.7.70 → 0.7.72
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 +1147 -124
- package/package.json +1 -1
- package/www/mapshaper-gui.js +1493 -253
- package/www/mapshaper.js +1147 -124
- package/www/page.css +41 -2
package/www/mapshaper-gui.js
CHANGED
|
@@ -3021,7 +3021,7 @@
|
|
|
3021
3021
|
return str + repeatString(pad, size - str.length);
|
|
3022
3022
|
}
|
|
3023
3023
|
|
|
3024
|
-
function trim(str) {
|
|
3024
|
+
function trim$1(str) {
|
|
3025
3025
|
return ltrim(rtrim(str));
|
|
3026
3026
|
}
|
|
3027
3027
|
|
|
@@ -3625,7 +3625,7 @@
|
|
|
3625
3625
|
range, regexEscape, reorderArray: reorderArray$1, repeat, repeatString,
|
|
3626
3626
|
replaceArray, rpad, rtrim,
|
|
3627
3627
|
shuffle, some, sortArrayIndex, sortOn, splitLines, sum,
|
|
3628
|
-
toArray, toBuffer, trim, trimQuotes,
|
|
3628
|
+
toArray, toBuffer, trim: trim$1, trimQuotes,
|
|
3629
3629
|
uniq, uniqifyNames,
|
|
3630
3630
|
wildcardToRegExp
|
|
3631
3631
|
};
|
|
@@ -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)) {
|
|
@@ -13466,10 +13442,10 @@
|
|
|
13466
13442
|
|
|
13467
13443
|
gui.on('path_extend', function(e) {
|
|
13468
13444
|
var redo = function() {
|
|
13469
|
-
gui.dispatchEvent('redo_path_extend', {
|
|
13445
|
+
gui.dispatchEvent('redo_path_extend', {points: e.points, shapes: e.shapes2});
|
|
13470
13446
|
};
|
|
13471
13447
|
var undo = function() {
|
|
13472
|
-
gui.dispatchEvent('undo_path_extend', {shapes: e.shapes1});
|
|
13448
|
+
gui.dispatchEvent('undo_path_extend', {points: e.points, shapes: e.shapes1});
|
|
13473
13449
|
};
|
|
13474
13450
|
addHistoryState(undo, redo);
|
|
13475
13451
|
});
|
|
@@ -15630,6 +15606,11 @@
|
|
|
15630
15606
|
return drawnFont || '';
|
|
15631
15607
|
}
|
|
15632
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
|
+
|
|
15633
15614
|
function getInstalledFontNames() {
|
|
15634
15615
|
var names = [];
|
|
15635
15616
|
getInstalledFonts().forEach(function(group) {
|
|
@@ -15911,6 +15892,11 @@
|
|
|
15911
15892
|
var colorPicker = El('div').addClass('label-color-picker').appendTo(parent).hide();
|
|
15912
15893
|
var sbCanvas, hueCanvas, sbMarker, hueMarker, pickerHsbInputs;
|
|
15913
15894
|
var pickerColor = {h: 0, s: 0, b: 0};
|
|
15895
|
+
// The hex value the picker was set to, until it is moved off it. HSB is
|
|
15896
|
+
// held in bytes, which cannot represent every RGB colour, so a colour that
|
|
15897
|
+
// was typed or picked from a preset would otherwise come back as a
|
|
15898
|
+
// neighbour of itself (#ff8800 as #ff8a00).
|
|
15899
|
+
var pickerHex = null;
|
|
15914
15900
|
var presetRows = opts.presetRows || [grayscaleColorPresets];
|
|
15915
15901
|
|
|
15916
15902
|
init();
|
|
@@ -15939,13 +15925,15 @@
|
|
|
15939
15925
|
|
|
15940
15926
|
this.setColor = function(color) {
|
|
15941
15927
|
if (isHexColor(color)) {
|
|
15942
|
-
setPickerColor(hexToHsb(color));
|
|
15928
|
+
setPickerColor(hexToHsb(color), color);
|
|
15943
15929
|
}
|
|
15944
15930
|
};
|
|
15945
15931
|
|
|
15946
|
-
this.getColor =
|
|
15947
|
-
|
|
15948
|
-
|
|
15932
|
+
this.getColor = getPickerHex;
|
|
15933
|
+
|
|
15934
|
+
function getPickerHex() {
|
|
15935
|
+
return pickerHex || hsbToHex(pickerColor);
|
|
15936
|
+
}
|
|
15949
15937
|
|
|
15950
15938
|
// The picker is placed in viewport coordinates (see .label-color-picker), so
|
|
15951
15939
|
// it has to be put against the field it belongs to each time it opens. The
|
|
@@ -15998,7 +15986,7 @@
|
|
|
15998
15986
|
var tile = El('div')
|
|
15999
15987
|
.addClass('label-color-preset')
|
|
16000
15988
|
.attr('role', 'button')
|
|
16001
|
-
.attr('
|
|
15989
|
+
.attr('aria-label', color)
|
|
16002
15990
|
.appendTo(rowEl)
|
|
16003
15991
|
.css('background-color', color);
|
|
16004
15992
|
if (row.length == 16 && i == 8) tile.addClass('label-color-preset-group-start');
|
|
@@ -16010,7 +15998,7 @@
|
|
|
16010
15998
|
}
|
|
16011
15999
|
|
|
16012
16000
|
function applyPreset(color) {
|
|
16013
|
-
setPickerColor(hexToHsb(color));
|
|
16001
|
+
setPickerColor(hexToHsb(color), color);
|
|
16014
16002
|
commitPickerColor();
|
|
16015
16003
|
}
|
|
16016
16004
|
|
|
@@ -16080,15 +16068,17 @@
|
|
|
16080
16068
|
};
|
|
16081
16069
|
}
|
|
16082
16070
|
|
|
16083
|
-
|
|
16071
|
+
// @hex: the exact colour, when @hsb was made from one
|
|
16072
|
+
function setPickerColor(hsb, hex) {
|
|
16084
16073
|
pickerColor = {
|
|
16085
16074
|
h: clamp$4(Math.round(hsb.h), 0, 255),
|
|
16086
16075
|
s: clamp$4(Math.round(hsb.s), 0, 255),
|
|
16087
16076
|
b: clamp$4(Math.round(hsb.b), 0, 255)
|
|
16088
16077
|
};
|
|
16078
|
+
pickerHex = hex || null;
|
|
16089
16079
|
drawColorPicker();
|
|
16090
16080
|
updatePickerFields();
|
|
16091
|
-
if (opts.onPreview) opts.onPreview(
|
|
16081
|
+
if (opts.onPreview) opts.onPreview(getPickerHex());
|
|
16092
16082
|
}
|
|
16093
16083
|
|
|
16094
16084
|
function updatePickerFields() {
|
|
@@ -16144,7 +16134,7 @@
|
|
|
16144
16134
|
}
|
|
16145
16135
|
|
|
16146
16136
|
function commitPickerColor() {
|
|
16147
|
-
if (opts.onChange) opts.onChange(
|
|
16137
|
+
if (opts.onChange) opts.onChange(getPickerHex());
|
|
16148
16138
|
}
|
|
16149
16139
|
|
|
16150
16140
|
function getMarkerColor(rgb) {
|
|
@@ -16475,7 +16465,7 @@
|
|
|
16475
16465
|
// in a menu that is only on screen while it is being used, and a
|
|
16476
16466
|
// destructive action is better for waiting until intent is shown.
|
|
16477
16467
|
El('div').addClass('label-saved-style-delete').attr('role', 'button')
|
|
16478
|
-
.attr('
|
|
16468
|
+
.attr('aria-label', 'Delete this ' + opts.styleLabel)
|
|
16479
16469
|
.appendTo(itemEl).html('×')
|
|
16480
16470
|
.on('click', function() {
|
|
16481
16471
|
deleteStyle(item);
|
|
@@ -16548,7 +16538,7 @@
|
|
|
16548
16538
|
// Escape. Whether that means giving up the keyboard is the
|
|
16549
16539
|
// caller's question: the field cannot know what else wants it.
|
|
16550
16540
|
// decimals how finely a typed size is kept (default 1)
|
|
16551
|
-
// title
|
|
16541
|
+
// title the field's accessible name (not a tooltip)
|
|
16552
16542
|
function SizeField(parent, opts) {
|
|
16553
16543
|
var o = Object.assign({min: 1, max: 999, step: 1, bigStep: 10}, opts || {});
|
|
16554
16544
|
var box = El('div').addClass('size-field').appendTo(parent);
|
|
@@ -16561,7 +16551,7 @@
|
|
|
16561
16551
|
var dirty = false; // holds typing that has not been committed
|
|
16562
16552
|
var disabled = false;
|
|
16563
16553
|
|
|
16564
|
-
if (o.title) input.attr('
|
|
16554
|
+
if (o.title) input.attr('aria-label', o.title);
|
|
16565
16555
|
|
|
16566
16556
|
// While the caret is in this field the keyboard belongs to it. Without this
|
|
16567
16557
|
// the GUI's own handlers see the keystrokes: Escape would disarm the tool,
|
|
@@ -16865,14 +16855,15 @@
|
|
|
16865
16855
|
// the one thing the third state exists to avoid saying.
|
|
16866
16856
|
//
|
|
16867
16857
|
// opts.onChange(on) the switch was clicked
|
|
16868
|
-
// opts.title
|
|
16858
|
+
// opts.title the switch's accessible name (not a tooltip)
|
|
16859
|
+
// opts.className
|
|
16869
16860
|
function makePanelToggle(parent, opts) {
|
|
16870
16861
|
var track = El('div').addClass('label-toggle').attr('role', 'checkbox').appendTo(parent);
|
|
16871
16862
|
var state = 'off';
|
|
16872
16863
|
if (opts.className) track.addClass(opts.className);
|
|
16873
16864
|
var disabled = false;
|
|
16874
16865
|
El('div').addClass('label-toggle-knob').appendTo(track);
|
|
16875
|
-
if (opts.title) track.attr('
|
|
16866
|
+
if (opts.title) track.attr('aria-label', opts.title);
|
|
16876
16867
|
// A click on a mixed switch turns everything on. It is the convention, and
|
|
16877
16868
|
// it is the reading that reaches a state the switch can describe: the next
|
|
16878
16869
|
// click then turns everything off, so both are one click away.
|
|
@@ -16912,7 +16903,7 @@
|
|
|
16912
16903
|
});
|
|
16913
16904
|
}
|
|
16914
16905
|
|
|
16915
|
-
// A button with a word on it rather than a glyph: Clear style, Random
|
|
16906
|
+
// A button with a word on it rather than a glyph: Clear style, Random fill,
|
|
16916
16907
|
// Create. Shaped like the panel's fields rather than like its 19px icon
|
|
16917
16908
|
// buttons, because what it does is named rather than drawn, and it is as wide
|
|
16918
16909
|
// as the name.
|
|
@@ -16957,7 +16948,7 @@
|
|
|
16957
16948
|
// opacity qualifies the colour, and one border says so where two fields side
|
|
16958
16949
|
// by side said they were separate settings. It also leaves the narrow column
|
|
16959
16950
|
// of the row free for a field that needs it. The opacity has no caption; its
|
|
16960
|
-
// percent sign
|
|
16951
|
+
// percent sign says what it is.
|
|
16961
16952
|
//
|
|
16962
16953
|
// Returns the field's box; the opacity input is made by makeOpacityInput().
|
|
16963
16954
|
function makeColorOpacityField(parent, chit, input, opacityOpts) {
|
|
@@ -16970,7 +16961,10 @@
|
|
|
16970
16961
|
// split row. Fill and Stroke are each one of these.
|
|
16971
16962
|
//
|
|
16972
16963
|
// opts.label the caption over the field
|
|
16973
|
-
// opts.onColor(hex) a colour was typed, picked or previewed to a finish
|
|
16964
|
+
// opts.onColor(hex) a colour was typed, picked or previewed to a finish;
|
|
16965
|
+
// '' when the field was emptied, which unsets a colour
|
|
16966
|
+
// that can be unset and restores the default of one
|
|
16967
|
+
// that cannot
|
|
16974
16968
|
// opts.onOpacity(frac) a usable percentage was typed
|
|
16975
16969
|
// opts.revert() one that was not, so put the row back as it was
|
|
16976
16970
|
// opts.noOpacity a colour with no opacity of its own, like a pattern's,
|
|
@@ -17006,7 +17000,7 @@
|
|
|
17006
17000
|
control.picker.toggle();
|
|
17007
17001
|
});
|
|
17008
17002
|
control.input = El('input').attr('type', 'text')
|
|
17009
|
-
.attr('
|
|
17003
|
+
.attr('aria-label', opts.label + ' color')
|
|
17010
17004
|
.on('change', function() {
|
|
17011
17005
|
var color = control.input.node().value.trim();
|
|
17012
17006
|
if (isHexColor(color)) control.picker.setColor(color);
|
|
@@ -17042,7 +17036,7 @@
|
|
|
17042
17036
|
// opts.revert() it was not, so put back what the field was showing
|
|
17043
17037
|
function makeOpacityInput(parent, opts) {
|
|
17044
17038
|
var input = El('input').attr('type', 'text').addClass('label-opacity-input')
|
|
17045
|
-
.attr('
|
|
17039
|
+
.attr('aria-label', 'Opacity, 0-100%')
|
|
17046
17040
|
.appendTo(parent)
|
|
17047
17041
|
.on('change', function() {
|
|
17048
17042
|
var val = parseOpacityValue(input.node().value);
|
|
@@ -17238,6 +17232,36 @@
|
|
|
17238
17232
|
return gui.state.label_text_session || null;
|
|
17239
17233
|
}
|
|
17240
17234
|
|
|
17235
|
+
// The label tool's armed placement tool: 'anchor', 'block', 'path' or null.
|
|
17236
|
+
// The tool owns it; the panel reads it, because an armed tool with nothing
|
|
17237
|
+
// selected is what points the panel at new labels rather than at all of them.
|
|
17238
|
+
function setLabelToolArmed(gui, mode) {
|
|
17239
|
+
var val = mode || null;
|
|
17240
|
+
if ((gui.state.label_tool_armed || null) === val) return;
|
|
17241
|
+
gui.state.label_tool_armed = val;
|
|
17242
|
+
gui.dispatchEvent('label_tool_armed_change');
|
|
17243
|
+
}
|
|
17244
|
+
|
|
17245
|
+
function getLabelToolArmed(gui) {
|
|
17246
|
+
return gui.state.label_tool_armed || null;
|
|
17247
|
+
}
|
|
17248
|
+
|
|
17249
|
+
// The "Editing:" line at the top of the style panels, which says what a
|
|
17250
|
+
// change to the panel's controls will act on. One wording for every panel.
|
|
17251
|
+
//
|
|
17252
|
+
// o.selected how many features are selected
|
|
17253
|
+
// o.total how many features the layer has
|
|
17254
|
+
// o.newLabels (label panel) a placement tool is armed with nothing
|
|
17255
|
+
// selected, so the controls set the style of the next label
|
|
17256
|
+
// o.editingText (label panel) a label is open for typing
|
|
17257
|
+
function formatEditingStatus(o) {
|
|
17258
|
+
var what = o.editingText ? 'this label' :
|
|
17259
|
+
o.selected > 0 ? o.selected + ' selected' :
|
|
17260
|
+
o.newLabels ? 'new labels' :
|
|
17261
|
+
o.total > 0 ? 'all' : 'none (empty layer)';
|
|
17262
|
+
return 'Editing: ' + what;
|
|
17263
|
+
}
|
|
17264
|
+
|
|
17241
17265
|
// Dragging an anchored label's text away from its anchor: what the drag
|
|
17242
17266
|
// writes, and which of the nine positions it ends up nearest.
|
|
17243
17267
|
//
|
|
@@ -17573,6 +17597,80 @@
|
|
|
17573
17597
|
return wrapper;
|
|
17574
17598
|
}
|
|
17575
17599
|
|
|
17600
|
+
// Default sizes and colours of the label tool's anchor symbols, which differ by
|
|
17601
|
+
// shape: a star or a ring at the size of a circle reads as smaller, and the NYT
|
|
17602
|
+
// star is a logo, drawn in its own red.
|
|
17603
|
+
var iconDefaults = {
|
|
17604
|
+
circle: {size: 5},
|
|
17605
|
+
square: {size: 5},
|
|
17606
|
+
star: {size: 8},
|
|
17607
|
+
ring: {size: 8},
|
|
17608
|
+
'nyt-star': {size: 14, color: '#cc0000'}
|
|
17609
|
+
};
|
|
17610
|
+
var fallbackSize = 5;
|
|
17611
|
+
|
|
17612
|
+
function getDefaultIconSize(shape) {
|
|
17613
|
+
var o = iconDefaults[shape];
|
|
17614
|
+
return o ? o.size : fallbackSize;
|
|
17615
|
+
}
|
|
17616
|
+
|
|
17617
|
+
// '' for a shape drawn in whatever colour the label gives it.
|
|
17618
|
+
function getDefaultIconColor(shape) {
|
|
17619
|
+
var o = iconDefaults[shape];
|
|
17620
|
+
return o && o.color || '';
|
|
17621
|
+
}
|
|
17622
|
+
|
|
17623
|
+
// The icon-size and icon-color to write when labels whose symbol is @prevShape
|
|
17624
|
+
// ('' for none, or for a selection of mixed shapes) are given @shape, from the
|
|
17625
|
+
// @size and @color they share. A missing size is '' or 0, and a colour of ''
|
|
17626
|
+
// is none; null is a colour the labels do not agree on, which is left alone.
|
|
17627
|
+
//
|
|
17628
|
+
// A value that is the old shape's default follows the shape to its own
|
|
17629
|
+
// default, and a value the user chose is kept -- so switching shapes resizes a
|
|
17630
|
+
// symbol nobody sized, and never undoes a size somebody set. The colour is
|
|
17631
|
+
// returned only if it changes, as '' to remove it.
|
|
17632
|
+
function getIconShapeChange(shape, prevShape, size, color) {
|
|
17633
|
+
var out = {};
|
|
17634
|
+
var sizeVal = Number(size);
|
|
17635
|
+
var prevColor = getDefaultIconColor(prevShape);
|
|
17636
|
+
var nextColor = getDefaultIconColor(shape);
|
|
17637
|
+
if (!(sizeVal > 0) || prevShape && sizeVal == getDefaultIconSize(prevShape)) {
|
|
17638
|
+
out.size = getDefaultIconSize(shape);
|
|
17639
|
+
} else {
|
|
17640
|
+
out.size = sizeVal;
|
|
17641
|
+
}
|
|
17642
|
+
if (color !== null && sameColor$1(color, prevColor) && !sameColor$1(color, nextColor)) {
|
|
17643
|
+
out.color = nextColor;
|
|
17644
|
+
}
|
|
17645
|
+
return out;
|
|
17646
|
+
}
|
|
17647
|
+
|
|
17648
|
+
function sameColor$1(a, b) {
|
|
17649
|
+
return String(a || '').toLowerCase() == String(b || '').toLowerCase();
|
|
17650
|
+
}
|
|
17651
|
+
|
|
17652
|
+
// Features offered only to people at The New York Times, who are the ones with
|
|
17653
|
+
// its NYTFranklin typeface installed. This is a convenience, not access
|
|
17654
|
+
// control: anything gated here must still render for everyone, because a map
|
|
17655
|
+
// made with it can be opened anywhere.
|
|
17656
|
+
//
|
|
17657
|
+
// ?nyt=on or ?nyt=off in the page URL overrides the check, so that the gated
|
|
17658
|
+
// features can be tested (and hidden) on any machine.
|
|
17659
|
+
var NYT_FONT = 'NYTFranklin';
|
|
17660
|
+
var cached;
|
|
17661
|
+
|
|
17662
|
+
function isNytUser() {
|
|
17663
|
+
if (cached === undefined) cached = detectNytUser();
|
|
17664
|
+
return cached;
|
|
17665
|
+
}
|
|
17666
|
+
|
|
17667
|
+
function detectNytUser() {
|
|
17668
|
+
var flag = GUI.getUrlVars().nyt;
|
|
17669
|
+
if (flag == 'on' || flag === true) return true;
|
|
17670
|
+
if (flag == 'off' || flag === false) return false;
|
|
17671
|
+
return isInstalledFont(NYT_FONT);
|
|
17672
|
+
}
|
|
17673
|
+
|
|
17576
17674
|
var fontField = 'font-family';
|
|
17577
17675
|
var fontSizeField = 'font-size';
|
|
17578
17676
|
var fontStyleField = 'font-style';
|
|
@@ -17605,7 +17703,6 @@
|
|
|
17605
17703
|
var defaultFontWeight = '400';
|
|
17606
17704
|
var defaultLabelColor = '#000000';
|
|
17607
17705
|
var defaultIconColor = '#000000';
|
|
17608
|
-
var defaultIconSize = 5;
|
|
17609
17706
|
// The width a halo is switched on at: past the edge of the glyphs, so a
|
|
17610
17707
|
// stroke of twice this.
|
|
17611
17708
|
var defaultHaloWidth = 2;
|
|
@@ -17678,6 +17775,10 @@
|
|
|
17678
17775
|
name: 'star'
|
|
17679
17776
|
}, {
|
|
17680
17777
|
name: 'ring'
|
|
17778
|
+
}, {
|
|
17779
|
+
name: 'nyt-star',
|
|
17780
|
+
title: 'NYT star',
|
|
17781
|
+
nytOnly: true
|
|
17681
17782
|
}];
|
|
17682
17783
|
var defaultIconShape = 'circle';
|
|
17683
17784
|
// Alignment writes label-align rather than text-anchor, because text-anchor
|
|
@@ -17733,6 +17834,9 @@
|
|
|
17733
17834
|
}, {
|
|
17734
17835
|
name: 'open-arrow',
|
|
17735
17836
|
title: 'open arrowhead at the anchor'
|
|
17837
|
+
}, {
|
|
17838
|
+
name: 'ring',
|
|
17839
|
+
title: 'ring around the anchor'
|
|
17736
17840
|
}];
|
|
17737
17841
|
// The anchor is at the lower left of the shapes and at the left of the ends,
|
|
17738
17842
|
// which is where the markers go. The arrowheads are drawn to the renderer's
|
|
@@ -17743,14 +17847,16 @@
|
|
|
17743
17847
|
elbow: '<path d="M3.5 12.5L8 4.5H13"></path>',
|
|
17744
17848
|
curve: '<path d="M3.5 12.5Q4.5 4.5 12.5 4"></path>',
|
|
17745
17849
|
none: '<path d="M3 8H13"></path>',
|
|
17746
|
-
arrow: '<path d="M6.
|
|
17747
|
-
'open-arrow': '<path d="M3.2 8H13"></path><path d="M7.38 5.07L3.2 8L7.38 10.93"></path>'
|
|
17850
|
+
arrow: '<path d="M6.84 8H13"></path><path class="fill" d="M2.5 8L8.7 6.05V9.95Z"></path>',
|
|
17851
|
+
'open-arrow': '<path d="M3.2 8H13"></path><path d="M7.38 5.07L3.2 8L7.38 10.93"></path>',
|
|
17852
|
+
ring: '<path d="M8.4 8H13"></path><circle cx="5.6" cy="8" r="2.8"></circle>'
|
|
17748
17853
|
};
|
|
17749
17854
|
var iconButtonSymbols = {
|
|
17750
17855
|
circle: '<circle cx="8" cy="8" r="4.25"></circle>',
|
|
17751
17856
|
square: '<rect x="4" y="4" width="8" height="8"></rect>',
|
|
17752
17857
|
ring: '<circle cx="8" cy="8" r="3.8"></circle>',
|
|
17753
|
-
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>'
|
|
17858
|
+
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>',
|
|
17859
|
+
'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>'
|
|
17754
17860
|
};
|
|
17755
17861
|
|
|
17756
17862
|
function LabelTool(gui) {
|
|
@@ -17765,6 +17871,7 @@
|
|
|
17765
17871
|
// The shape the toggle turns back on, so that switching a symbol off and on
|
|
17766
17872
|
// again does not silently change a star into a circle.
|
|
17767
17873
|
var lastIconShape = defaultIconShape;
|
|
17874
|
+
var shownIconTypes = null;
|
|
17768
17875
|
// Likewise the halo's width, which is what switching one off removes.
|
|
17769
17876
|
var lastHaloWidth = defaultHaloWidth;
|
|
17770
17877
|
// And the callout's shape.
|
|
@@ -17793,6 +17900,11 @@
|
|
|
17793
17900
|
// a label opened for typing becomes what the controls act on
|
|
17794
17901
|
if (panel.visible()) updateControls();
|
|
17795
17902
|
});
|
|
17903
|
+
gui.on('label_tool_armed_change', function() {
|
|
17904
|
+
// arming a tool points an empty selection at new labels, and disarming
|
|
17905
|
+
// points it back at the whole layer
|
|
17906
|
+
if (panel.visible()) updateControls();
|
|
17907
|
+
});
|
|
17796
17908
|
gui.on('label_position_mode_change', function() {
|
|
17797
17909
|
// The tool sets the mode back to Fixed when it opens, and the toggle is
|
|
17798
17910
|
// the only thing on screen that says which of the two is on.
|
|
@@ -17898,7 +18010,7 @@
|
|
|
17898
18010
|
// number otherwise keep theirs, and so does a colour field, because the
|
|
17899
18011
|
// field beside it has one and "Color" says the percentage is its opacity.
|
|
17900
18012
|
var fontRow = El('div').addClass('label-style-row').appendTo(textSection);
|
|
17901
|
-
fontSelect = El('select').attr('
|
|
18013
|
+
fontSelect = El('select').attr('aria-label', 'Font').appendTo(fontRow).on('change', function() {
|
|
17902
18014
|
if (fontSelect.node().value) {
|
|
17903
18015
|
applyFont(fontSelect.node().value);
|
|
17904
18016
|
}
|
|
@@ -17906,7 +18018,7 @@
|
|
|
17906
18018
|
|
|
17907
18019
|
var styleSizeRow = El('div').addClass('label-style-row label-split-row').appendTo(textSection);
|
|
17908
18020
|
var fontStyleRow = El('div').addClass('label-split-cell label-font-style-row').appendTo(styleSizeRow);
|
|
17909
|
-
fontStyleSelect = El('select').attr('
|
|
18021
|
+
fontStyleSelect = El('select').attr('aria-label', 'Font style').appendTo(fontStyleRow).on('change', function() {
|
|
17910
18022
|
if (fontStyleSelect.node().value) {
|
|
17911
18023
|
applyFontStyleVariant(fontStyleSelect.node().value);
|
|
17912
18024
|
}
|
|
@@ -17932,7 +18044,7 @@
|
|
|
17932
18044
|
var textColorCell = El('div').addClass('label-split-cell label-color-row label-text-color-row').appendTo(colorRow);
|
|
17933
18045
|
El('span').appendTo(textColorCell).text('Color');
|
|
17934
18046
|
colorChit = El('div').addClass('label-color-chit').attr('role', 'button');
|
|
17935
|
-
colorInput = El('input').attr('type', 'text').attr('
|
|
18047
|
+
colorInput = El('input').attr('type', 'text').attr('aria-label', 'Text color');
|
|
17936
18048
|
var textColorField = addColorOpacityField(textColorCell, colorChit, colorInput, applyLabelOpacity);
|
|
17937
18049
|
colorFieldBox = textColorField.box;
|
|
17938
18050
|
opacityInput = textColorField.opacity;
|
|
@@ -17942,12 +18054,9 @@
|
|
|
17942
18054
|
});
|
|
17943
18055
|
colorInput.on('change', function() {
|
|
17944
18056
|
var color = colorInput.node().value.trim();
|
|
17945
|
-
|
|
17946
|
-
|
|
17947
|
-
|
|
17948
|
-
}
|
|
17949
|
-
applyLabelColor(color);
|
|
17950
|
-
}
|
|
18057
|
+
// An emptied field unsets the colour.
|
|
18058
|
+
if (isHexColor(color)) colorPicker.setColor(color);
|
|
18059
|
+
applyLabelColor(color);
|
|
17951
18060
|
});
|
|
17952
18061
|
colorPicker = initColorPicker(textColorCell, colorChit, colorInput, applyLabelColor);
|
|
17953
18062
|
|
|
@@ -17965,7 +18074,7 @@
|
|
|
17965
18074
|
applyLabelAlign(item.name);
|
|
17966
18075
|
})
|
|
17967
18076
|
.attr('data-align', item.name)
|
|
17968
|
-
.attr('
|
|
18077
|
+
.attr('aria-label', item.title);
|
|
17969
18078
|
alignBtns[item.name] = btn;
|
|
17970
18079
|
appendAlignButtonSymbol(btn, item.name);
|
|
17971
18080
|
});
|
|
@@ -18004,7 +18113,7 @@
|
|
|
18004
18113
|
var haloColorCell = El('div').addClass('label-split-cell label-color-row label-halo-color-row').appendTo(haloColorRow);
|
|
18005
18114
|
El('span').appendTo(haloColorCell).text('Color');
|
|
18006
18115
|
haloColorChit = El('div').addClass('label-color-chit').attr('role', 'button');
|
|
18007
|
-
haloColorInput = El('input').attr('type', 'text').attr('
|
|
18116
|
+
haloColorInput = El('input').attr('type', 'text').attr('aria-label', 'Halo color');
|
|
18008
18117
|
var haloColorField = addColorOpacityField(haloColorCell, haloColorChit, haloColorInput, applyHaloOpacity);
|
|
18009
18118
|
haloColorFieldBox = haloColorField.box;
|
|
18010
18119
|
haloOpacityInput = haloColorField.opacity;
|
|
@@ -18014,12 +18123,8 @@
|
|
|
18014
18123
|
});
|
|
18015
18124
|
haloColorInput.on('change', function() {
|
|
18016
18125
|
var color = haloColorInput.node().value.trim();
|
|
18017
|
-
if (color)
|
|
18018
|
-
|
|
18019
|
-
haloColorPicker.setColor(color);
|
|
18020
|
-
}
|
|
18021
|
-
applyHaloColor(color);
|
|
18022
|
-
}
|
|
18126
|
+
if (isHexColor(color)) haloColorPicker.setColor(color);
|
|
18127
|
+
applyHaloColor(color);
|
|
18023
18128
|
});
|
|
18024
18129
|
haloColorPicker = initColorPicker(haloColorCell, haloColorChit, haloColorInput, applyHaloColor);
|
|
18025
18130
|
|
|
@@ -18055,12 +18160,12 @@
|
|
|
18055
18160
|
var iconRow = El('div').addClass('label-style-row label-icon-shapes-row').appendTo(iconSection);
|
|
18056
18161
|
var iconGroup = iconGroupEl = El('div').addClass('label-btn-group label-icon-buttons').appendTo(iconRow);
|
|
18057
18162
|
iconBtns = {};
|
|
18058
|
-
|
|
18163
|
+
getShownIconTypes().forEach(function(icon) {
|
|
18059
18164
|
var btn = makePanelButton(iconGroup, '', function() {
|
|
18060
18165
|
applyIcon(icon.name);
|
|
18061
18166
|
})
|
|
18062
18167
|
.attr('data-icon', icon.name)
|
|
18063
|
-
.attr('
|
|
18168
|
+
.attr('aria-label', icon.title || icon.name);
|
|
18064
18169
|
iconBtns[icon.name] = btn;
|
|
18065
18170
|
appendIconButtonSymbol(btn, icon.name);
|
|
18066
18171
|
});
|
|
@@ -18069,7 +18174,7 @@
|
|
|
18069
18174
|
var iconColorCell = El('div').addClass('label-split-cell label-color-row label-icon-color-row').appendTo(iconColorRow);
|
|
18070
18175
|
El('span').appendTo(iconColorCell).text('Color');
|
|
18071
18176
|
iconColorChit = El('div').addClass('label-color-chit').attr('role', 'button');
|
|
18072
|
-
iconColorInput = El('input').attr('type', 'text').attr('
|
|
18177
|
+
iconColorInput = El('input').attr('type', 'text').attr('aria-label', 'Symbol color');
|
|
18073
18178
|
var iconColorField = addColorOpacityField(iconColorCell, iconColorChit, iconColorInput, applyIconOpacity);
|
|
18074
18179
|
iconColorFieldBox = iconColorField.box;
|
|
18075
18180
|
iconOpacityInput = iconColorField.opacity;
|
|
@@ -18079,12 +18184,8 @@
|
|
|
18079
18184
|
});
|
|
18080
18185
|
iconColorInput.on('change', function() {
|
|
18081
18186
|
var color = iconColorInput.node().value.trim();
|
|
18082
|
-
if (color)
|
|
18083
|
-
|
|
18084
|
-
iconColorPicker.setColor(color);
|
|
18085
|
-
}
|
|
18086
|
-
applyIconColor(color);
|
|
18087
|
-
}
|
|
18187
|
+
if (isHexColor(color)) iconColorPicker.setColor(color);
|
|
18188
|
+
applyIconColor(color);
|
|
18088
18189
|
});
|
|
18089
18190
|
iconColorPicker = initColorPicker(iconColorCell, iconColorChit, iconColorInput, applyIconColor);
|
|
18090
18191
|
|
|
@@ -18117,7 +18218,7 @@
|
|
|
18117
18218
|
applyLabelPosition(pos);
|
|
18118
18219
|
})
|
|
18119
18220
|
.attr('data-position', pos)
|
|
18120
|
-
.attr('
|
|
18221
|
+
.attr('aria-label', pos);
|
|
18121
18222
|
});
|
|
18122
18223
|
|
|
18123
18224
|
|
|
@@ -18127,7 +18228,7 @@
|
|
|
18127
18228
|
setLabelPositionMode(gui, item.name, getPositionModeKind());
|
|
18128
18229
|
})
|
|
18129
18230
|
.attr('data-drag-mode', item.name)
|
|
18130
|
-
.attr('
|
|
18231
|
+
.attr('aria-label', item.title);
|
|
18131
18232
|
});
|
|
18132
18233
|
|
|
18133
18234
|
presetControl = new StylePresetControl(panel, {
|
|
@@ -18198,7 +18299,7 @@
|
|
|
18198
18299
|
var endSizeCell = El('div').addClass('label-split-cell label-spacing-row label-callout-end-size-row').appendTo(endRow);
|
|
18199
18300
|
El('span').appendTo(endSizeCell).text('Size');
|
|
18200
18301
|
calloutEndSizeInput = new SizeField(endSizeCell, {
|
|
18201
|
-
title: '
|
|
18302
|
+
title: 'Marker size in px: an arrowhead\'s sides, or a ring\'s diameter',
|
|
18202
18303
|
min: 1,
|
|
18203
18304
|
max: 60,
|
|
18204
18305
|
step: 1,
|
|
@@ -18212,7 +18313,7 @@
|
|
|
18212
18313
|
var colorCell = El('div').addClass('label-split-cell label-color-row label-callout-color-row').appendTo(colorRow);
|
|
18213
18314
|
El('span').appendTo(colorCell).text('Color');
|
|
18214
18315
|
calloutColorChit = El('div').addClass('label-color-chit').attr('role', 'button');
|
|
18215
|
-
calloutColorInput = El('input').attr('type', 'text').attr('
|
|
18316
|
+
calloutColorInput = El('input').attr('type', 'text').attr('aria-label', 'Callout color');
|
|
18216
18317
|
var calloutColorField = addColorOpacityField(colorCell, calloutColorChit, calloutColorInput, applyCalloutOpacity);
|
|
18217
18318
|
calloutColorFieldBox = calloutColorField.box;
|
|
18218
18319
|
calloutOpacityInput = calloutColorField.opacity;
|
|
@@ -18222,12 +18323,8 @@
|
|
|
18222
18323
|
});
|
|
18223
18324
|
calloutColorInput.on('change', function() {
|
|
18224
18325
|
var color = calloutColorInput.node().value.trim();
|
|
18225
|
-
if (color)
|
|
18226
|
-
|
|
18227
|
-
calloutColorPicker.setColor(color);
|
|
18228
|
-
}
|
|
18229
|
-
applyCalloutColor(color);
|
|
18230
|
-
}
|
|
18326
|
+
if (isHexColor(color)) calloutColorPicker.setColor(color);
|
|
18327
|
+
applyCalloutColor(color);
|
|
18231
18328
|
});
|
|
18232
18329
|
calloutColorPicker = initColorPicker(colorCell, calloutColorChit, calloutColorInput, applyCalloutColor);
|
|
18233
18330
|
|
|
@@ -18236,7 +18333,7 @@
|
|
|
18236
18333
|
var gapCell = El('div').addClass('label-split-cell label-spacing-row label-callout-gap-row').appendTo(colorRow);
|
|
18237
18334
|
El('span').appendTo(gapCell).text('Gap');
|
|
18238
18335
|
calloutGapInput = El('input').attr('type', 'text').addClass('label-measure-input')
|
|
18239
|
-
.attr('
|
|
18336
|
+
.attr('aria-label', 'Space between the callout and the anchor, in px')
|
|
18240
18337
|
.attr('placeholder', 'auto')
|
|
18241
18338
|
.attr('data-placeholder', 'auto')
|
|
18242
18339
|
.appendTo(gapCell)
|
|
@@ -18248,7 +18345,7 @@
|
|
|
18248
18345
|
function makeCalloutButton(parent, item, action) {
|
|
18249
18346
|
var btn = makePanelButton(parent, '', action)
|
|
18250
18347
|
.attr('data-callout', item.name)
|
|
18251
|
-
.attr('
|
|
18348
|
+
.attr('aria-label', item.title);
|
|
18252
18349
|
El('<svg class="label-callout-symbol" viewBox="0 0 16 16" aria-hidden="true">' +
|
|
18253
18350
|
calloutButtonSymbols[item.name] + '</svg>').appendTo(btn);
|
|
18254
18351
|
return btn;
|
|
@@ -18349,6 +18446,16 @@
|
|
|
18349
18446
|
return !!(gui.interaction && gui.interaction.getMode() == labelMode);
|
|
18350
18447
|
}
|
|
18351
18448
|
|
|
18449
|
+
// Whether an empty selection means the next label rather than every label:
|
|
18450
|
+
// only while a placement tool is armed, or a label not yet made is being
|
|
18451
|
+
// typed. With no tool armed the panel styles the whole layer, as the line
|
|
18452
|
+
// and polygon panels do.
|
|
18453
|
+
function editingNewLabels() {
|
|
18454
|
+
var session = getLabelTextSession(gui);
|
|
18455
|
+
if (!labelModeIsOn()) return false;
|
|
18456
|
+
return !!getLabelToolArmed(gui) || !!(session && session.id == -1);
|
|
18457
|
+
}
|
|
18458
|
+
|
|
18352
18459
|
function updateVisibility() {
|
|
18353
18460
|
var enabled = activeLayerHasLabels();
|
|
18354
18461
|
textBtn.classed('disabled', !enabled);
|
|
@@ -18432,13 +18539,14 @@
|
|
|
18432
18539
|
if (session) return [];
|
|
18433
18540
|
ids = getSelectionIds();
|
|
18434
18541
|
if (ids.length > 0) return ids;
|
|
18435
|
-
return
|
|
18542
|
+
return editingNewLabels() ? [] : getAllLabelIds();
|
|
18436
18543
|
}
|
|
18437
18544
|
|
|
18438
|
-
// With nothing selected the controls edit the style that
|
|
18439
|
-
// so they stay live even when there is no label, and
|
|
18545
|
+
// With a tool armed and nothing selected the controls edit the style that
|
|
18546
|
+
// new labels are given, so they stay live even when there is no label, and
|
|
18547
|
+
// no layer, yet. With no tool armed and no labels, there is nothing to edit.
|
|
18440
18548
|
function controlsEnabled() {
|
|
18441
|
-
return getTargetIds().length > 0 ||
|
|
18549
|
+
return getTargetIds().length > 0 || editingNewLabels();
|
|
18442
18550
|
}
|
|
18443
18551
|
|
|
18444
18552
|
function getAllLabelIds() {
|
|
@@ -18475,8 +18583,10 @@
|
|
|
18475
18583
|
// rest of the selection has nothing to compare.
|
|
18476
18584
|
var iconIds = getIconValueIds();
|
|
18477
18585
|
var iconVal = getCommonValue(iconIds, iconField);
|
|
18478
|
-
var
|
|
18479
|
-
var
|
|
18586
|
+
var iconShape = iconVal || lastIconShape;
|
|
18587
|
+
var iconSize = getShownValue(iconIds, iconSizeField, {useDefault: true, defaultValue: getDefaultIconSize(iconShape)});
|
|
18588
|
+
var iconColor = getShownValue(iconIds, iconColorField, {useDefault: true,
|
|
18589
|
+
defaultValue: getDefaultIconColor(iconShape) || defaultIconColor});
|
|
18480
18590
|
var iconOpacity = getShownValue(iconIds, iconOpacityField, {useDefault: true, defaultValue: 1});
|
|
18481
18591
|
var haloIds = getHaloValueIds();
|
|
18482
18592
|
var haloWidth = getShownValue(haloIds, haloWidthField, {useDefault: true, defaultValue: lastHaloWidth});
|
|
@@ -18666,22 +18776,21 @@
|
|
|
18666
18776
|
});
|
|
18667
18777
|
}
|
|
18668
18778
|
|
|
18779
|
+
// What the controls will act on. With a tool armed an empty selection is the
|
|
18780
|
+
// next label rather than the whole layer, and saying "all" there is what
|
|
18781
|
+
// would make the panel untrustworthy.
|
|
18669
18782
|
function updateEditingStatus(count, editingText) {
|
|
18670
|
-
editingStatus.text(
|
|
18783
|
+
editingStatus.text(formatEditingStatus({
|
|
18784
|
+
selected: count,
|
|
18785
|
+
total: getActiveLayer() ? internal.getFeatureCount(getActiveLayer()) : 0,
|
|
18786
|
+
newLabels: editingNewLabels(),
|
|
18787
|
+
editingText: editingText
|
|
18788
|
+
}));
|
|
18671
18789
|
// "deselect" is for a selection the user made; a label being typed into is
|
|
18672
18790
|
// left by clicking away or pressing Escape, not from here.
|
|
18673
18791
|
clearLink.classed('hidden', count === 0 || editingText);
|
|
18674
18792
|
}
|
|
18675
18793
|
|
|
18676
|
-
// What the controls will act on. In label mode an empty selection is the next
|
|
18677
|
-
// label rather than the whole layer, and saying "all" there is what would
|
|
18678
|
-
// make the panel untrustworthy.
|
|
18679
|
-
function describeTarget(count, editingText) {
|
|
18680
|
-
if (editingText) return 'this label';
|
|
18681
|
-
if (count > 0) return count + ' selected';
|
|
18682
|
-
return labelModeIsOn() ? 'new labels' : 'all';
|
|
18683
|
-
}
|
|
18684
|
-
|
|
18685
18794
|
function updateFontSizeControls(shown) {
|
|
18686
18795
|
fontSizeInput.setValue(shown.value || '');
|
|
18687
18796
|
fontSizeInput.setPlaceholder(shown.mixed ? MIXED_TEXT : '');
|
|
@@ -18821,7 +18930,7 @@
|
|
|
18821
18930
|
// border the buttons share fades with them -- a live border around dead
|
|
18822
18931
|
// buttons is the one part of a disabled control that still looks usable.
|
|
18823
18932
|
iconGroupEl.classed('disabled', off);
|
|
18824
|
-
|
|
18933
|
+
getShownIconTypes().forEach(function(icon) {
|
|
18825
18934
|
iconBtns[icon.name].classed('selected', !off && icon.name == iconVal);
|
|
18826
18935
|
setPanelButtonDisabled(iconBtns[icon.name], off);
|
|
18827
18936
|
});
|
|
@@ -18890,7 +18999,7 @@
|
|
|
18890
18999
|
var ids = getTargetIds();
|
|
18891
19000
|
var table = getActiveTable();
|
|
18892
19001
|
if (ids.length === 0) {
|
|
18893
|
-
return internal.svg.labelHasHalo(
|
|
19002
|
+
return internal.svg.labelHasHalo(editingNewLabels() ? getNewLabelStyle(gui) : null) ? 'on' : 'off';
|
|
18894
19003
|
}
|
|
18895
19004
|
return getToggleState(ids.map(function(id) {
|
|
18896
19005
|
return internal.svg.labelHasHalo(table && table.getRecordAt(id));
|
|
@@ -18950,7 +19059,7 @@
|
|
|
18950
19059
|
var ids = getTargetIds();
|
|
18951
19060
|
var table = getActiveTable();
|
|
18952
19061
|
if (ids.length === 0) {
|
|
18953
|
-
return internal.svg.labelHasCallout(
|
|
19062
|
+
return internal.svg.labelHasCallout(editingNewLabels() ? getNewLabelStyle(gui) : null) ? 'on' : 'off';
|
|
18954
19063
|
}
|
|
18955
19064
|
return getToggleState(ids.map(function(id) {
|
|
18956
19065
|
return internal.svg.labelHasCallout(table && table.getRecordAt(id));
|
|
@@ -19092,7 +19201,7 @@
|
|
|
19092
19201
|
function updateCalloutEndSizeControl(off) {
|
|
19093
19202
|
var ids = getTargetIds().length > 0 ? getCalloutEndTargetIds() : [];
|
|
19094
19203
|
var noMarker = getTargetIds().length > 0 ? ids.length === 0 :
|
|
19095
|
-
getCalloutEnd(
|
|
19204
|
+
getCalloutEnd(editingNewLabels() ? getNewLabelStyle(gui) : null) == 'none';
|
|
19096
19205
|
var shown = off || noMarker ? {value: '', mixed: false} : getCalloutEndSizeShown(ids);
|
|
19097
19206
|
calloutEndSizeInput.setValue(shown.value);
|
|
19098
19207
|
calloutEndSizeInput.setPlaceholder(shown.mixed ? MIXED_TEXT : '');
|
|
@@ -19131,7 +19240,7 @@
|
|
|
19131
19240
|
var vals;
|
|
19132
19241
|
if (!controlsEnabled()) return {value: '', mixed: false};
|
|
19133
19242
|
if (ids.length === 0) {
|
|
19134
|
-
return {value: fn(
|
|
19243
|
+
return {value: fn(editingNewLabels() ? getNewLabelStyle(gui) : null), mixed: false};
|
|
19135
19244
|
}
|
|
19136
19245
|
vals = ids.map(function(id) {
|
|
19137
19246
|
return fn(table && table.getRecordAt(id));
|
|
@@ -19207,7 +19316,7 @@
|
|
|
19207
19316
|
var records = getActiveTable() && getActiveTable().getRecords();
|
|
19208
19317
|
var value, val;
|
|
19209
19318
|
if (ids.length === 0) {
|
|
19210
|
-
return resolveAlignment(
|
|
19319
|
+
return resolveAlignment(editingNewLabels() ? getNewLabelStyle(gui) : null);
|
|
19211
19320
|
}
|
|
19212
19321
|
for (var i = 0; i < ids.length; i++) {
|
|
19213
19322
|
val = resolveAlignment(records && records[ids[i]]);
|
|
@@ -19227,7 +19336,7 @@
|
|
|
19227
19336
|
// label will be created with, falling back to the same default an existing
|
|
19228
19337
|
// label would show.
|
|
19229
19338
|
function getNewLabelValue(field, opts) {
|
|
19230
|
-
var style =
|
|
19339
|
+
var style = editingNewLabels() ? getNewLabelStyle(gui) : null;
|
|
19231
19340
|
var val = style ? style[field] : '';
|
|
19232
19341
|
if (val || val === 0) return val;
|
|
19233
19342
|
return opts && opts.useDefault ? opts.defaultValue : '';
|
|
@@ -19318,7 +19427,7 @@
|
|
|
19318
19427
|
var records = getActiveTable() && getActiveTable().getRecords();
|
|
19319
19428
|
var ids = getTargetIds();
|
|
19320
19429
|
if (ids.length === 0) {
|
|
19321
|
-
return !!(
|
|
19430
|
+
return !!(editingNewLabels() && getNewLabelStyle(gui)[textAnchorField]);
|
|
19322
19431
|
}
|
|
19323
19432
|
return ids.some(function(id) {
|
|
19324
19433
|
var rec = records && records[id];
|
|
@@ -19420,12 +19529,23 @@
|
|
|
19420
19529
|
|
|
19421
19530
|
function getSelectedIcon() {
|
|
19422
19531
|
var out = '';
|
|
19423
|
-
|
|
19532
|
+
getShownIconTypes().forEach(function(icon) {
|
|
19424
19533
|
if (iconBtns[icon.name].hasClass('selected')) out = icon.name;
|
|
19425
19534
|
});
|
|
19426
19535
|
return out;
|
|
19427
19536
|
}
|
|
19428
19537
|
|
|
19538
|
+
// A label that already has an NYT star keeps it without the button: it is
|
|
19539
|
+
// drawn for everyone, and only choosing one is gated.
|
|
19540
|
+
function getShownIconTypes() {
|
|
19541
|
+
if (!shownIconTypes) {
|
|
19542
|
+
shownIconTypes = iconTypes.filter(function(icon) {
|
|
19543
|
+
return !icon.nytOnly || isNytUser();
|
|
19544
|
+
});
|
|
19545
|
+
}
|
|
19546
|
+
return shownIconTypes;
|
|
19547
|
+
}
|
|
19548
|
+
|
|
19429
19549
|
function applyStyleObject(style) {
|
|
19430
19550
|
var styles = [];
|
|
19431
19551
|
savedStyleFields.forEach(function(field) {
|
|
@@ -19456,8 +19576,9 @@
|
|
|
19456
19576
|
// The size the symbols in the selection already share, where they share
|
|
19457
19577
|
// one: a shape applied to a mixed selection gives the labels that had no
|
|
19458
19578
|
// symbol the size of the ones that did, rather than resetting them all to
|
|
19459
|
-
// the default.
|
|
19460
|
-
|
|
19579
|
+
// the default. A size or colour that was the old shape's default becomes
|
|
19580
|
+
// the new one's (see getIconShapeChange()).
|
|
19581
|
+
addIconShapeDefaults(styles, iconName, getIconValueIds());
|
|
19461
19582
|
// The symbol's own opacity goes on with it, because the label's opacity
|
|
19462
19583
|
// is applied to both elements: without this, text set to 50% would give
|
|
19463
19584
|
// a half-faded symbol while the Icon section showed it at 100%.
|
|
@@ -19469,6 +19590,14 @@
|
|
|
19469
19590
|
applyStyleValues(styles);
|
|
19470
19591
|
}
|
|
19471
19592
|
|
|
19593
|
+
function addIconShapeDefaults(styles, iconName, ids) {
|
|
19594
|
+
var color = getCommonValueInfo(ids, iconColorField);
|
|
19595
|
+
var change = getIconShapeChange(iconName, getCommonValue(ids, iconField) || '',
|
|
19596
|
+
getCommonValue(ids, iconSizeField), color.mixed ? null : color.value || '');
|
|
19597
|
+
styles.push([iconSizeField, change.size]);
|
|
19598
|
+
if ('color' in change) styles.push([iconColorField, change.color]);
|
|
19599
|
+
}
|
|
19600
|
+
|
|
19472
19601
|
// Switching a symbol on moves a label sitting at the centre out from under
|
|
19473
19602
|
// it, in the same command, so that the pair is one undo step and the text
|
|
19474
19603
|
// is never briefly drawn over the symbol it just asked for.
|
|
@@ -19509,7 +19638,9 @@
|
|
|
19509
19638
|
}
|
|
19510
19639
|
|
|
19511
19640
|
function nudgeIconSize(delta) {
|
|
19512
|
-
var
|
|
19641
|
+
var ids = getIconTargetIds();
|
|
19642
|
+
var size = getNumericSize(ids, iconSizeField,
|
|
19643
|
+
getDefaultIconSize(getCommonValue(ids, iconField)));
|
|
19513
19644
|
if (!controlsEnabled()) return;
|
|
19514
19645
|
size = Math.max(1, size + delta);
|
|
19515
19646
|
applyIconSize(size);
|
|
@@ -19836,13 +19967,13 @@
|
|
|
19836
19967
|
// no fill gets a clear one, so that the pattern is drawn over whatever is
|
|
19837
19968
|
// underneath.
|
|
19838
19969
|
function getPatternBackground(fill) {
|
|
19839
|
-
return isBlank$
|
|
19970
|
+
return isBlank$2(fill) ? 'none' : String(fill).trim();
|
|
19840
19971
|
}
|
|
19841
19972
|
|
|
19842
19973
|
// Whether the settings describe a pattern the renderer will draw. A hatch with
|
|
19843
19974
|
// no gap would be a solid fill, and the parser refuses a stripe of no width.
|
|
19844
19975
|
function isValidPatternControls(o) {
|
|
19845
|
-
if (!o || patternTypes.indexOf(o.type) == -1 || isBlank$
|
|
19976
|
+
if (!o || patternTypes.indexOf(o.type) == -1 || isBlank$2(o.color)) return false;
|
|
19846
19977
|
if (!(o.size > 0) || !isFinite(o.angle)) return false;
|
|
19847
19978
|
return o.type == 'hatches' ? o.gap > 0 : o.gap >= 0;
|
|
19848
19979
|
}
|
|
@@ -19936,7 +20067,7 @@
|
|
|
19936
20067
|
return str == 'transparent' ? 'none' : str;
|
|
19937
20068
|
}
|
|
19938
20069
|
|
|
19939
|
-
function isBlank$
|
|
20070
|
+
function isBlank$2(val) {
|
|
19940
20071
|
return val === undefined || val === null || String(val).trim() === '';
|
|
19941
20072
|
}
|
|
19942
20073
|
|
|
@@ -19953,7 +20084,7 @@
|
|
|
19953
20084
|
// opts.revert() put the panel back as the data has it
|
|
19954
20085
|
// opts.releaseFocus()
|
|
19955
20086
|
function PatternFillControl(parent, opts) {
|
|
19956
|
-
var section = makePanelSection(parent, '
|
|
20087
|
+
var section = makePanelSection(parent, 'Patterns');
|
|
19957
20088
|
var shown = {type: 'none'};
|
|
19958
20089
|
// What switching the pattern on applies when the features have none: the
|
|
19959
20090
|
// last pattern the section showed, so that off and on again is a round trip.
|
|
@@ -19998,7 +20129,7 @@
|
|
|
19998
20129
|
var code = rec && rec['fill-pattern'];
|
|
19999
20130
|
var edit = {id: id, styles: styles};
|
|
20000
20131
|
var refilled;
|
|
20001
|
-
if (refill && !isBlank(code)) {
|
|
20132
|
+
if (refill && !isBlank$1(code)) {
|
|
20002
20133
|
refilled = refillPattern(internal.parsePattern(code), rec.fill, fill);
|
|
20003
20134
|
if (refilled) edit.styles = styles.concat([['fill-pattern', refilled]]);
|
|
20004
20135
|
}
|
|
@@ -20015,7 +20146,7 @@
|
|
|
20015
20146
|
ids.forEach(function(id) {
|
|
20016
20147
|
var rec = records[id];
|
|
20017
20148
|
var code = rec && rec['fill-pattern'];
|
|
20018
|
-
var o = isBlank(code) ? null : getPatternControls(internal.parsePattern(code), rec.fill);
|
|
20149
|
+
var o = isBlank$1(code) ? null : getPatternControls(internal.parsePattern(code), rec.fill);
|
|
20019
20150
|
if (o) edits.push({id: id, styles: [['fill-pattern', formatFillPatternExpression(o)]]});
|
|
20020
20151
|
});
|
|
20021
20152
|
return edits;
|
|
@@ -20028,7 +20159,7 @@
|
|
|
20028
20159
|
onChange: setPatternOn
|
|
20029
20160
|
});
|
|
20030
20161
|
var typeRow = El('div').addClass('label-style-row layer-pattern-type-row').appendTo(section);
|
|
20031
|
-
typeSelect = El('select').attr('
|
|
20162
|
+
typeSelect = El('select').attr('aria-label', 'Pattern type').appendTo(typeRow)
|
|
20032
20163
|
.on('change', function() {
|
|
20033
20164
|
selectType(typeSelect.node().value);
|
|
20034
20165
|
});
|
|
@@ -20045,7 +20176,12 @@
|
|
|
20045
20176
|
label: 'Color',
|
|
20046
20177
|
noOpacity: true,
|
|
20047
20178
|
onColor: function(color) {
|
|
20048
|
-
|
|
20179
|
+
// A pattern has to have a colour, so an emptied field is its default.
|
|
20180
|
+
if (!color) {
|
|
20181
|
+
color = defaultPatternColor;
|
|
20182
|
+
colorControl.showColor(color);
|
|
20183
|
+
}
|
|
20184
|
+
applyChange({color: color});
|
|
20049
20185
|
},
|
|
20050
20186
|
revert: opts.revert
|
|
20051
20187
|
});
|
|
@@ -20096,7 +20232,7 @@
|
|
|
20096
20232
|
makeFieldTip(caption,
|
|
20097
20233
|
'The fill-pattern syntax of the -style command.\n' +
|
|
20098
20234
|
'See the command reference for details.');
|
|
20099
|
-
codeInput = El('input').attr('type', 'text').attr('
|
|
20235
|
+
codeInput = El('input').attr('type', 'text').attr('aria-label', 'Pattern code').appendTo(customRow)
|
|
20100
20236
|
.on('change', function() {
|
|
20101
20237
|
applyCode(codeInput.node().value.trim());
|
|
20102
20238
|
});
|
|
@@ -20173,7 +20309,7 @@
|
|
|
20173
20309
|
|
|
20174
20310
|
function hasPattern(id) {
|
|
20175
20311
|
var rec = opts.getRecords()[id];
|
|
20176
|
-
return !isBlank(rec && rec['fill-pattern']);
|
|
20312
|
+
return !isBlank$1(rec && rec['fill-pattern']);
|
|
20177
20313
|
}
|
|
20178
20314
|
|
|
20179
20315
|
function applyChange(change) {
|
|
@@ -20245,7 +20381,7 @@
|
|
|
20245
20381
|
|
|
20246
20382
|
function readPatternState(rec) {
|
|
20247
20383
|
var code = rec && rec['fill-pattern'];
|
|
20248
|
-
if (isBlank(code)) return {type: 'none'};
|
|
20384
|
+
if (isBlank$1(code)) return {type: 'none'};
|
|
20249
20385
|
return getPatternControls(internal.parsePattern(code), rec.fill) ||
|
|
20250
20386
|
{type: 'custom', code: String(code)};
|
|
20251
20387
|
}
|
|
@@ -20261,18 +20397,330 @@
|
|
|
20261
20397
|
return undefined;
|
|
20262
20398
|
}
|
|
20263
20399
|
|
|
20400
|
+
function isBlank$1(val) {
|
|
20401
|
+
return val === undefined || val === null || String(val).trim() === '';
|
|
20402
|
+
}
|
|
20403
|
+
|
|
20404
|
+
// The polygon panel's "Effects" section: an outer and an inner glow, each a
|
|
20405
|
+
// color, an opacity and a width. See svg-glow.mjs for how they are drawn.
|
|
20406
|
+
//
|
|
20407
|
+
// A glow is there when its color is set, so each glow is a color row with no
|
|
20408
|
+
// switch of its own: picking a color adds the glow, and emptying the color
|
|
20409
|
+
// field removes it. The section's switch opens the section, and switching it
|
|
20410
|
+
// off removes both glows and their settings.
|
|
20411
|
+
//
|
|
20412
|
+
// A width or opacity typed while no feature has the glow is kept, and goes
|
|
20413
|
+
// with the color when one is picked.
|
|
20414
|
+
//
|
|
20415
|
+
// opts.getRecords() the target layer's records
|
|
20416
|
+
// opts.getTargetIds() the features being styled
|
|
20417
|
+
// opts.applyEdits(edits, title) run per-feature edits: [{id, styles}]
|
|
20418
|
+
// opts.revert() put the panel back as the data has it
|
|
20419
|
+
// opts.releaseFocus()
|
|
20420
|
+
|
|
20421
|
+
var glowTypes = ['outer', 'inner'];
|
|
20422
|
+
var glowTitles = {outer: 'Outer glow', inner: 'Inner glow'};
|
|
20423
|
+
|
|
20424
|
+
function GlowEffectsControl(parent, opts) {
|
|
20425
|
+
var section = makePanelSection(parent, 'Effects');
|
|
20426
|
+
var rows = {};
|
|
20427
|
+
// Switched on, with no glow to show yet.
|
|
20428
|
+
var opened = false;
|
|
20429
|
+
var pending = {outer: {}, inner: {}};
|
|
20430
|
+
var toggle = makePanelToggle(section.findChild('.label-style-section-title'), {
|
|
20431
|
+
title: 'Add glow effects',
|
|
20432
|
+
className: 'layer-effects-toggle',
|
|
20433
|
+
onChange: function(on) {
|
|
20434
|
+
var edits;
|
|
20435
|
+
opened = on;
|
|
20436
|
+
pending = {outer: {}, inner: {}};
|
|
20437
|
+
edits = on ? [] : getEffectsOffEdits(opts.getRecords(), opts.getTargetIds());
|
|
20438
|
+
if (edits.length > 0) {
|
|
20439
|
+
opts.applyEdits(edits, 'Remove glow effects');
|
|
20440
|
+
} else {
|
|
20441
|
+
update();
|
|
20442
|
+
}
|
|
20443
|
+
}
|
|
20444
|
+
});
|
|
20445
|
+
glowTypes.forEach(function(type) {
|
|
20446
|
+
rows[type] = addGlowRow(type);
|
|
20447
|
+
});
|
|
20448
|
+
|
|
20449
|
+
this.section = section;
|
|
20450
|
+
this.update = update;
|
|
20451
|
+
this.hidePickers = hidePickers;
|
|
20452
|
+
|
|
20453
|
+
this.reset = function() {
|
|
20454
|
+
opened = false;
|
|
20455
|
+
pending = {outer: {}, inner: {}};
|
|
20456
|
+
hidePickers();
|
|
20457
|
+
};
|
|
20458
|
+
|
|
20459
|
+
// The glows every target has, for a saved style.
|
|
20460
|
+
this.addStyleValues = function(style) {
|
|
20461
|
+
var records = opts.getRecords();
|
|
20462
|
+
var ids = opts.getTargetIds();
|
|
20463
|
+
glowTypes.forEach(function(type) {
|
|
20464
|
+
if (getGlowState(records, ids, type) != 'on') return;
|
|
20465
|
+
['color', 'width', 'opacity'].forEach(function(name) {
|
|
20466
|
+
var field = getGlowField(type, name);
|
|
20467
|
+
var val = getCommonValue(records, ids, field);
|
|
20468
|
+
if (val || val === 0) style[field] = val;
|
|
20469
|
+
});
|
|
20470
|
+
});
|
|
20471
|
+
};
|
|
20472
|
+
|
|
20473
|
+
function hidePickers() {
|
|
20474
|
+
rows.outer.picker.hide();
|
|
20475
|
+
rows.inner.picker.hide();
|
|
20476
|
+
}
|
|
20477
|
+
|
|
20478
|
+
function addGlowRow(type) {
|
|
20479
|
+
var o = makeColorRow(section, {
|
|
20480
|
+
label: glowTitles[type],
|
|
20481
|
+
onColor: function(color) {
|
|
20482
|
+
applyGlowColor(type, color);
|
|
20483
|
+
},
|
|
20484
|
+
onOpacity: function(value) {
|
|
20485
|
+
applyGlowValue(type, 'opacity', value);
|
|
20486
|
+
},
|
|
20487
|
+
revert: opts.revert
|
|
20488
|
+
});
|
|
20489
|
+
o.row.addClass('layer-glow-row layer-' + type + '-glow-row');
|
|
20490
|
+
El('span').appendTo(o.aside).text('Width');
|
|
20491
|
+
o.width = new SizeField(o.aside, {
|
|
20492
|
+
min: 0.5,
|
|
20493
|
+
max: 200,
|
|
20494
|
+
step: 1,
|
|
20495
|
+
bigStep: 5,
|
|
20496
|
+
decimals: 1,
|
|
20497
|
+
title: 'How far the glow reaches from the edge, in px',
|
|
20498
|
+
onSet: function(value) {
|
|
20499
|
+
applyGlowValue(type, 'width', value);
|
|
20500
|
+
},
|
|
20501
|
+
onStep: function(delta) {
|
|
20502
|
+
var shown = Number(o.width.getValue());
|
|
20503
|
+
if (!(shown > 0)) shown = internal.svg.DEFAULT_GLOW_WIDTH;
|
|
20504
|
+
applyGlowValue(type, 'width', Math.max(0.5, Math.round(shown + delta)));
|
|
20505
|
+
},
|
|
20506
|
+
onDone: opts.releaseFocus
|
|
20507
|
+
});
|
|
20508
|
+
return o;
|
|
20509
|
+
}
|
|
20510
|
+
|
|
20511
|
+
function applyGlowColor(type, color) {
|
|
20512
|
+
var edits = getGlowColorEdits(opts.getRecords(), opts.getTargetIds(), type, color, pending[type]);
|
|
20513
|
+
if (color) pending[type] = {};
|
|
20514
|
+
if (edits.length > 0) {
|
|
20515
|
+
opts.applyEdits(edits, color ? glowTitles[type] : 'Remove ' + glowTitles[type].toLowerCase());
|
|
20516
|
+
} else {
|
|
20517
|
+
update();
|
|
20518
|
+
}
|
|
20519
|
+
}
|
|
20520
|
+
|
|
20521
|
+
// A width or opacity goes to the features that have the glow, as the label
|
|
20522
|
+
// panel's halo settings go only to the labels that have a halo.
|
|
20523
|
+
function applyGlowValue(type, name, value) {
|
|
20524
|
+
var records = opts.getRecords();
|
|
20525
|
+
var ids = opts.getTargetIds().filter(function(id) {
|
|
20526
|
+
return hasGlow(records[id], type);
|
|
20527
|
+
});
|
|
20528
|
+
if (ids.length === 0) {
|
|
20529
|
+
pending[type][name] = value;
|
|
20530
|
+
update();
|
|
20531
|
+
return;
|
|
20532
|
+
}
|
|
20533
|
+
opts.applyEdits(ids.map(function(id) {
|
|
20534
|
+
return {id: id, styles: [[getGlowField(type, name), formatGlowValue(name, value)]]};
|
|
20535
|
+
}));
|
|
20536
|
+
}
|
|
20537
|
+
|
|
20538
|
+
function update() {
|
|
20539
|
+
var records = opts.getRecords();
|
|
20540
|
+
var ids = opts.getTargetIds();
|
|
20541
|
+
var state = getEffectsState(records, ids);
|
|
20542
|
+
if (state != 'off') opened = true;
|
|
20543
|
+
toggle.setState(state == 'off' && opened ? 'on' : state);
|
|
20544
|
+
section.classed('collapsed', state == 'off' && !opened);
|
|
20545
|
+
if (state == 'off' && !opened) hidePickers();
|
|
20546
|
+
glowTypes.forEach(function(type) {
|
|
20547
|
+
updateGlowRow(type, records, ids);
|
|
20548
|
+
});
|
|
20549
|
+
}
|
|
20550
|
+
|
|
20551
|
+
function updateGlowRow(type, records, ids) {
|
|
20552
|
+
var o = rows[type];
|
|
20553
|
+
var shown = getShownGlow(records, ids, type, pending[type]);
|
|
20554
|
+
o.showColor(shown.color);
|
|
20555
|
+
o.input.attr('placeholder', shown.mixed ? 'mixed' : '');
|
|
20556
|
+
o.chit.classed('mixed', shown.mixed);
|
|
20557
|
+
o.opacity.node().value = shown.opacity === '' ? '' : formatOpacityPct(shown.opacity);
|
|
20558
|
+
o.width.setValue(shown.width === '' ? '' : String(shown.width));
|
|
20559
|
+
o.width.setPlaceholder(shown.width === '' ? 'mixed' : '');
|
|
20560
|
+
}
|
|
20561
|
+
}
|
|
20562
|
+
|
|
20563
|
+
function getGlowField(type, name) {
|
|
20564
|
+
return type + '-glow-' + name;
|
|
20565
|
+
}
|
|
20566
|
+
|
|
20567
|
+
// In the panel, a feature has a glow when the glow's color is set.
|
|
20568
|
+
function hasGlow(rec, type) {
|
|
20569
|
+
return !!rec && !isBlank(rec[getGlowField(type, 'color')]);
|
|
20570
|
+
}
|
|
20571
|
+
|
|
20572
|
+
// 'on', 'off' or 'mixed', over the features @ids
|
|
20573
|
+
function getGlowState(records, ids, type) {
|
|
20574
|
+
var n = ids.filter(function(id) { return hasGlow(records[id], type); }).length;
|
|
20575
|
+
return n === 0 ? 'off' : n < ids.length ? 'mixed' : 'on';
|
|
20576
|
+
}
|
|
20577
|
+
|
|
20578
|
+
function getEffectsState(records, ids) {
|
|
20579
|
+
var n = ids.filter(function(id) {
|
|
20580
|
+
return hasGlow(records[id], 'outer') || hasGlow(records[id], 'inner');
|
|
20581
|
+
}).length;
|
|
20582
|
+
return n === 0 ? 'off' : n < ids.length ? 'mixed' : 'on';
|
|
20583
|
+
}
|
|
20584
|
+
|
|
20585
|
+
// Per-feature edits for a color typed or picked for one glow. A color adds the
|
|
20586
|
+
// glow to every feature, with the @pending width and opacity where a feature
|
|
20587
|
+
// has none of its own; an empty color removes the glow's color.
|
|
20588
|
+
function getGlowColorEdits(records, ids, type, color, pending) {
|
|
20589
|
+
var colorField = getGlowField(type, 'color');
|
|
20590
|
+
var edits = [];
|
|
20591
|
+
ids.forEach(function(id) {
|
|
20592
|
+
var rec = records[id] || {};
|
|
20593
|
+
var styles = [];
|
|
20594
|
+
if (!color) {
|
|
20595
|
+
if (!isBlank(rec[colorField])) styles.push([colorField, '']);
|
|
20596
|
+
} else {
|
|
20597
|
+
if (String(rec[colorField]).trim() != color) styles.push([colorField, color]);
|
|
20598
|
+
['width', 'opacity'].forEach(function(name) {
|
|
20599
|
+
var field = getGlowField(type, name);
|
|
20600
|
+
if (pending && name in pending && isBlank(rec[field])) {
|
|
20601
|
+
var val = formatGlowValue(name, pending[name]);
|
|
20602
|
+
if (val !== '') styles.push([field, val]);
|
|
20603
|
+
}
|
|
20604
|
+
});
|
|
20605
|
+
}
|
|
20606
|
+
if (styles.length > 0) edits.push({id: id, styles: styles});
|
|
20607
|
+
});
|
|
20608
|
+
return edits;
|
|
20609
|
+
}
|
|
20610
|
+
|
|
20611
|
+
// Switching Effects off removes every glow setting, which mean nothing
|
|
20612
|
+
// without a color.
|
|
20613
|
+
function getEffectsOffEdits(records, ids) {
|
|
20614
|
+
var edits = [];
|
|
20615
|
+
ids.forEach(function(id) {
|
|
20616
|
+
var rec = records[id] || {};
|
|
20617
|
+
var styles = internal.svg.glowFields.filter(function(field) {
|
|
20618
|
+
return !isBlank(rec[field]);
|
|
20619
|
+
}).map(function(field) {
|
|
20620
|
+
return [field, ''];
|
|
20621
|
+
});
|
|
20622
|
+
if (styles.length > 0) edits.push({id: id, styles: styles});
|
|
20623
|
+
});
|
|
20624
|
+
return edits;
|
|
20625
|
+
}
|
|
20626
|
+
|
|
20627
|
+
// What a glow's row shows. The color is blank where no feature has the glow,
|
|
20628
|
+
// and blank and mixed where only some do or their colors differ. The width
|
|
20629
|
+
// and opacity are those the glowing features agree on, '' where they differ;
|
|
20630
|
+
// with no glowing features, they are the @pending ones, the default width and
|
|
20631
|
+
// no opacity.
|
|
20632
|
+
function getShownGlow(records, ids, type, pending) {
|
|
20633
|
+
var glowIds = ids.filter(function(id) { return hasGlow(records[id], type); });
|
|
20634
|
+
var defaultWidth = internal.svg.DEFAULT_GLOW_WIDTH;
|
|
20635
|
+
var color, mixed;
|
|
20636
|
+
pending = pending || {};
|
|
20637
|
+
if (glowIds.length === 0) {
|
|
20638
|
+
return {
|
|
20639
|
+
color: '',
|
|
20640
|
+
mixed: false,
|
|
20641
|
+
width: 'width' in pending ? pending.width : defaultWidth,
|
|
20642
|
+
opacity: 'opacity' in pending ? pending.opacity : ''
|
|
20643
|
+
};
|
|
20644
|
+
}
|
|
20645
|
+
color = getCommonValue(records, glowIds, getGlowField(type, 'color'), undefined, trim);
|
|
20646
|
+
mixed = glowIds.length < ids.length || color === '';
|
|
20647
|
+
return {
|
|
20648
|
+
color: mixed ? '' : color,
|
|
20649
|
+
mixed: mixed,
|
|
20650
|
+
width: getCommonValue(records, glowIds, getGlowField(type, 'width'), defaultWidth, Number),
|
|
20651
|
+
opacity: getCommonValue(records, glowIds, getGlowField(type, 'opacity'), 1, Number)
|
|
20652
|
+
};
|
|
20653
|
+
}
|
|
20654
|
+
|
|
20655
|
+
// Full opacity is the default, and stored as no opacity at all.
|
|
20656
|
+
function formatGlowValue(name, value) {
|
|
20657
|
+
return name == 'opacity' && value >= 1 ? '' : value;
|
|
20658
|
+
}
|
|
20659
|
+
|
|
20660
|
+
// The value of @field that the features @ids share, or '' if they differ.
|
|
20661
|
+
function getCommonValue(records, ids, field, defaultValue, convert) {
|
|
20662
|
+
var value, val;
|
|
20663
|
+
for (var i=0; i<ids.length; i++) {
|
|
20664
|
+
val = records[ids[i]] && records[ids[i]][field];
|
|
20665
|
+
if (isBlank(val)) val = defaultValue === undefined ? '' : defaultValue;
|
|
20666
|
+
if (convert && val !== '' && val !== null) val = convert(val);
|
|
20667
|
+
if (i === 0) {
|
|
20668
|
+
value = val;
|
|
20669
|
+
} else if (val !== value) {
|
|
20670
|
+
return '';
|
|
20671
|
+
}
|
|
20672
|
+
}
|
|
20673
|
+
return ids.length > 0 ? value : '';
|
|
20674
|
+
}
|
|
20675
|
+
|
|
20676
|
+
function trim(val) {
|
|
20677
|
+
return String(val).trim();
|
|
20678
|
+
}
|
|
20679
|
+
|
|
20264
20680
|
function isBlank(val) {
|
|
20265
20681
|
return val === undefined || val === null || String(val).trim() === '';
|
|
20266
20682
|
}
|
|
20267
20683
|
|
|
20268
20684
|
var savedStylesKey = 'layer_style_presets';
|
|
20269
|
-
var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray', 'fill', 'fill-opacity', 'fill-pattern'];
|
|
20685
|
+
var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray', 'line-start', 'line-end', 'line-end-size', 'fill', 'fill-opacity', 'fill-pattern'].concat(internal.svg.glowFields);
|
|
20686
|
+
var arrowShapes = [{
|
|
20687
|
+
name: 'arrow',
|
|
20688
|
+
title: 'solid arrowheads'
|
|
20689
|
+
}, {
|
|
20690
|
+
name: 'open-arrow',
|
|
20691
|
+
title: 'open arrowheads'
|
|
20692
|
+
}, {
|
|
20693
|
+
name: 'dot',
|
|
20694
|
+
title: 'dots'
|
|
20695
|
+
}];
|
|
20696
|
+
// The arrows are the label callout's, which point left, the way a line's
|
|
20697
|
+
// start head does; the dot's left edge is where their tips are.
|
|
20698
|
+
var arrowButtonSymbols = {
|
|
20699
|
+
arrow: calloutButtonSymbols.arrow,
|
|
20700
|
+
'open-arrow': calloutButtonSymbols['open-arrow'],
|
|
20701
|
+
dot: '<path d="M5 8H13"></path><circle class="fill" cx="5" cy="8" r="2.6"></circle>'
|
|
20702
|
+
};
|
|
20703
|
+
var arrowPositions = [{
|
|
20704
|
+
name: 'start',
|
|
20705
|
+
label: 'Start',
|
|
20706
|
+
title: 'arrowhead at the start of the line'
|
|
20707
|
+
}, {
|
|
20708
|
+
name: 'end',
|
|
20709
|
+
label: 'End',
|
|
20710
|
+
title: 'arrowhead at the end of the line'
|
|
20711
|
+
}, {
|
|
20712
|
+
name: 'both',
|
|
20713
|
+
label: 'Both',
|
|
20714
|
+
title: 'arrowheads at both ends of the line'
|
|
20715
|
+
}];
|
|
20270
20716
|
|
|
20271
20717
|
function LayerStyleTool(gui) {
|
|
20272
20718
|
var parent = gui.container.findChild('.mshp-main-map');
|
|
20273
20719
|
var panel = El('div').addClass('label-style-panel layer-style-panel rollover').appendTo(parent).hide();
|
|
20274
|
-
var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthField, dashControl, randomFillBtn, presetControl, patternControl, hit;
|
|
20720
|
+
var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthField, dashControl, arrowControl, randomFillBtn, presetControl, patternControl, glowControl, hit;
|
|
20275
20721
|
var targetLayer = null;
|
|
20722
|
+
// What the arrowhead switch turns on, for lines that have no heads
|
|
20723
|
+
var lastArrow = {shape: 'arrow', position: 'end'};
|
|
20276
20724
|
|
|
20277
20725
|
initPanel();
|
|
20278
20726
|
hit = gui.map.getHitControl && gui.map.getHitControl();
|
|
@@ -20328,6 +20776,7 @@
|
|
|
20328
20776
|
fillControl.picker.hide();
|
|
20329
20777
|
patternControl.hidePicker();
|
|
20330
20778
|
patternControl.reset();
|
|
20779
|
+
glowControl.reset();
|
|
20331
20780
|
targetLayer = null;
|
|
20332
20781
|
}
|
|
20333
20782
|
|
|
@@ -20357,13 +20806,15 @@
|
|
|
20357
20806
|
editingStatus = El('span').addClass('label-editing-status').appendTo(editRow);
|
|
20358
20807
|
clearLink = El('span').addClass('label-editing-clear colored-text').appendTo(editRow).text('deselect').on('click', clearSelection);
|
|
20359
20808
|
|
|
20360
|
-
fillControl = addColorControl(panel, 'Fill', 'fill', '');
|
|
20361
20809
|
strokeControl = addColorControl(panel, 'Stroke', 'stroke', '#000000');
|
|
20362
20810
|
strokeWidthField = addStrokeWidthControl(strokeControl.aside);
|
|
20811
|
+
fillControl = addColorControl(panel, 'Fill', 'fill', '');
|
|
20812
|
+
randomFillBtn = makePanelActionButton(fillControl.aside, 'Random fill', applyRandomFillColors)
|
|
20813
|
+
.addClass('layer-random-fill-btn');
|
|
20363
20814
|
dashControl = addDashArrayControl(panel);
|
|
20815
|
+
arrowControl = addArrowControl(panel);
|
|
20364
20816
|
|
|
20365
20817
|
var buttonRow = El('div').addClass('label-style-row label-panel-button-row').appendTo(panel);
|
|
20366
|
-
randomFillBtn = makePanelActionButton(buttonRow, 'Random fills', applyRandomFillColors);
|
|
20367
20818
|
makePanelActionButton(buttonRow, 'Clear style', clearLayerStyle);
|
|
20368
20819
|
|
|
20369
20820
|
patternControl = new PatternFillControl(panel, {
|
|
@@ -20376,6 +20827,14 @@
|
|
|
20376
20827
|
releaseFocus: releaseFocus
|
|
20377
20828
|
});
|
|
20378
20829
|
|
|
20830
|
+
glowControl = new GlowEffectsControl(panel, {
|
|
20831
|
+
getRecords: getTargetRecords,
|
|
20832
|
+
getTargetIds: getTargetIds,
|
|
20833
|
+
applyEdits: runStyleEdits,
|
|
20834
|
+
revert: updateControls,
|
|
20835
|
+
releaseFocus: releaseFocus
|
|
20836
|
+
});
|
|
20837
|
+
|
|
20379
20838
|
presetControl = new StylePresetControl(panel, {
|
|
20380
20839
|
storageKey: savedStylesKey,
|
|
20381
20840
|
type: getStyleType,
|
|
@@ -20397,10 +20856,9 @@
|
|
|
20397
20856
|
var control = makeColorRow(parent, {
|
|
20398
20857
|
label: label,
|
|
20399
20858
|
onColor: function(color) {
|
|
20400
|
-
//
|
|
20401
|
-
//
|
|
20402
|
-
|
|
20403
|
-
if (color) applyColorControlStyle(control, color);
|
|
20859
|
+
// An emptied field unsets the colour: -style reads an empty value as
|
|
20860
|
+
// "remove this".
|
|
20861
|
+
applyColorControlStyle(control, color);
|
|
20404
20862
|
},
|
|
20405
20863
|
onOpacity: function(value) {
|
|
20406
20864
|
applyLayerStyle(field + '-opacity', value);
|
|
@@ -20454,6 +20912,197 @@
|
|
|
20454
20912
|
return {row: row, input: input};
|
|
20455
20913
|
}
|
|
20456
20914
|
|
|
20915
|
+
// A switch in the heading says whether the lines have arrowheads; the rows
|
|
20916
|
+
// under it are the head's shape with its size beside it, then which ends
|
|
20917
|
+
// get it. A line has one shape for both ends, which is all the panel sets,
|
|
20918
|
+
// though -style can give the two ends different ones.
|
|
20919
|
+
function addArrowControl(parent) {
|
|
20920
|
+
var section = makePanelSection(parent, 'Arrowheads');
|
|
20921
|
+
var toggle = makePanelToggle(section.findChild('.label-style-section-title'), {
|
|
20922
|
+
title: 'Add arrowheads',
|
|
20923
|
+
className: 'layer-arrow-toggle',
|
|
20924
|
+
onChange: setArrowsOn
|
|
20925
|
+
});
|
|
20926
|
+
var shapeRow = El('div').addClass('label-style-row label-split-row').appendTo(section);
|
|
20927
|
+
var shapeCell = El('div').addClass('label-split-cell label-align-row').appendTo(shapeRow);
|
|
20928
|
+
var sizeCell = El('div').addClass('label-split-cell label-spacing-row layer-arrow-size-row').appendTo(shapeRow);
|
|
20929
|
+
var posRow = El('div').addClass('label-style-row label-split-row').appendTo(section);
|
|
20930
|
+
var posCell = El('div').addClass('label-split-cell label-align-row').appendTo(posRow);
|
|
20931
|
+
var control = {section: section, toggle: toggle, shapeBtns: {}, posBtns: {}};
|
|
20932
|
+
El('div').addClass('label-split-cell').appendTo(posRow);
|
|
20933
|
+
El('span').appendTo(shapeCell).text('Shape');
|
|
20934
|
+
var shapeGroup = El('div').addClass('label-btn-group label-callout-buttons layer-arrow-shape-buttons').appendTo(shapeCell);
|
|
20935
|
+
arrowShapes.forEach(function(item) {
|
|
20936
|
+
var btn = makePanelButton(shapeGroup, '', function() {
|
|
20937
|
+
applyArrowShape(item.name);
|
|
20938
|
+
}).attr('data-arrow-shape', item.name).attr('aria-label', item.title);
|
|
20939
|
+
El('<svg class="label-callout-symbol" viewBox="0 0 16 16" aria-hidden="true">' +
|
|
20940
|
+
arrowButtonSymbols[item.name] + '</svg>').appendTo(btn);
|
|
20941
|
+
control.shapeBtns[item.name] = btn;
|
|
20942
|
+
});
|
|
20943
|
+
El('span').appendTo(posCell).text('Ends');
|
|
20944
|
+
var posGroup = El('div').addClass('label-btn-group layer-arrow-position-buttons').appendTo(posCell);
|
|
20945
|
+
arrowPositions.forEach(function(item) {
|
|
20946
|
+
control.posBtns[item.name] = makePanelButton(posGroup, item.label, function() {
|
|
20947
|
+
applyArrowPosition(item.name);
|
|
20948
|
+
}).attr('data-arrow-position', item.name).attr('aria-label', item.title);
|
|
20949
|
+
});
|
|
20950
|
+
El('span').appendTo(sizeCell).text('Size');
|
|
20951
|
+
control.sizeField = new SizeField(sizeCell, {
|
|
20952
|
+
title: 'Arrowhead size in px, the length of its sides or the diameter of a dot',
|
|
20953
|
+
min: 1,
|
|
20954
|
+
max: 60,
|
|
20955
|
+
step: 1,
|
|
20956
|
+
bigStep: 5,
|
|
20957
|
+
onSet: applyArrowSize,
|
|
20958
|
+
onStep: nudgeArrowSize,
|
|
20959
|
+
onDone: releaseFocus
|
|
20960
|
+
});
|
|
20961
|
+
return control;
|
|
20962
|
+
}
|
|
20963
|
+
|
|
20964
|
+
// {shape, position} for one record, 'none' and '' when it has no heads. A
|
|
20965
|
+
// line whose two ends have different shapes reports the start's.
|
|
20966
|
+
function getArrowInfo(rec) {
|
|
20967
|
+
var start = internal.svg.getLineEndType(rec || {}, 'line-start');
|
|
20968
|
+
var end = internal.svg.getLineEndType(rec || {}, 'line-end');
|
|
20969
|
+
var shape = start != 'none' ? start : end;
|
|
20970
|
+
var position = start != 'none' && end != 'none' ? 'both' :
|
|
20971
|
+
start != 'none' ? 'start' : end != 'none' ? 'end' : '';
|
|
20972
|
+
return {shape: shape, position: position};
|
|
20973
|
+
}
|
|
20974
|
+
|
|
20975
|
+
// Switching on gives the lines that have no heads the last shape and ends
|
|
20976
|
+
// the section showed, so that off and on again is a round trip; the lines
|
|
20977
|
+
// that have heads keep theirs. Switching off leaves line-end-size alone,
|
|
20978
|
+
// for the same reason.
|
|
20979
|
+
function setArrowsOn(on) {
|
|
20980
|
+
applyArrowEdits(function(info) {
|
|
20981
|
+
return on ? fillArrowInfo(info) : {shape: 'none', position: ''};
|
|
20982
|
+
});
|
|
20983
|
+
}
|
|
20984
|
+
|
|
20985
|
+
// Each target keeps whichever of shape and position is not being set, so
|
|
20986
|
+
// that changing the shape of a selection whose lines point different ways
|
|
20987
|
+
// leaves them pointing those ways.
|
|
20988
|
+
function applyArrowShape(shape) {
|
|
20989
|
+
applyArrowEdits(function(info) {
|
|
20990
|
+
return {shape: shape, position: fillArrowInfo(info).position};
|
|
20991
|
+
});
|
|
20992
|
+
}
|
|
20993
|
+
|
|
20994
|
+
function applyArrowPosition(position) {
|
|
20995
|
+
applyArrowEdits(function(info) {
|
|
20996
|
+
return {shape: fillArrowInfo(info).shape, position: position};
|
|
20997
|
+
});
|
|
20998
|
+
}
|
|
20999
|
+
|
|
21000
|
+
function fillArrowInfo(info) {
|
|
21001
|
+
return info.shape == 'none' ? {shape: lastArrow.shape, position: lastArrow.position} : info;
|
|
21002
|
+
}
|
|
21003
|
+
|
|
21004
|
+
function applyArrowEdits(getNext) {
|
|
21005
|
+
var records = getTargetRecords();
|
|
21006
|
+
var addStroke = styleFieldIsUnsetForTargets('stroke');
|
|
21007
|
+
var edits = [];
|
|
21008
|
+
getTargetIds().forEach(function(id) {
|
|
21009
|
+
var rec = records[id] || {};
|
|
21010
|
+
var next = getNext(getArrowInfo(rec));
|
|
21011
|
+
var on = next.shape != 'none';
|
|
21012
|
+
var styles = [
|
|
21013
|
+
['line-start', on && next.position != 'end' ? next.shape : ''],
|
|
21014
|
+
['line-end', on && next.position != 'start' ? next.shape : '']
|
|
21015
|
+
].filter(function(style) {
|
|
21016
|
+
// no need to remove what is not there
|
|
21017
|
+
return style[1] || rec[style[0]];
|
|
21018
|
+
});
|
|
21019
|
+
if (on && addStroke) styles.push(['stroke', strokeControl.defaultColor]);
|
|
21020
|
+
if (styles.length > 0) edits.push({id: id, styles: styles});
|
|
21021
|
+
});
|
|
21022
|
+
runStyleEdits(edits);
|
|
21023
|
+
}
|
|
21024
|
+
|
|
21025
|
+
function applyArrowSize(value) {
|
|
21026
|
+
var ids = getArrowTargetIds();
|
|
21027
|
+
if (ids.length === 0) return;
|
|
21028
|
+
runStyleEdits(ids.map(function(id) {
|
|
21029
|
+
return {id: id, styles: [['line-end-size', value]]};
|
|
21030
|
+
}));
|
|
21031
|
+
}
|
|
21032
|
+
|
|
21033
|
+
function nudgeArrowSize(delta) {
|
|
21034
|
+
var shown = getArrowSizeShown(getArrowTargetIds());
|
|
21035
|
+
var size = Number(shown.value);
|
|
21036
|
+
if (!isFinite(size) || shown.value === '') return;
|
|
21037
|
+
applyArrowSize(Math.max(1, Math.round(size + delta)));
|
|
21038
|
+
}
|
|
21039
|
+
|
|
21040
|
+
function getArrowTargetIds() {
|
|
21041
|
+
var records = getTargetRecords();
|
|
21042
|
+
return getTargetIds().filter(function(id) {
|
|
21043
|
+
return getArrowInfo(records[id]).shape != 'none';
|
|
21044
|
+
});
|
|
21045
|
+
}
|
|
21046
|
+
|
|
21047
|
+
// The size a head is drawn at, which is the one its line width gives it
|
|
21048
|
+
// when it has none of its own.
|
|
21049
|
+
function getArrowSizeShown(ids) {
|
|
21050
|
+
var records = getTargetRecords();
|
|
21051
|
+
var value = '', size;
|
|
21052
|
+
for (var i=0; i<ids.length; i++) {
|
|
21053
|
+
size = getArrowSize(records[ids[i]]);
|
|
21054
|
+
if (i > 0 && size !== value) return {value: '', mixed: true};
|
|
21055
|
+
value = size;
|
|
21056
|
+
}
|
|
21057
|
+
return {value: value, mixed: false};
|
|
21058
|
+
}
|
|
21059
|
+
|
|
21060
|
+
function getArrowSize(rec) {
|
|
21061
|
+
var opts = internal.svg.makeLineArrowOpts('arrow', '', rec && rec['line-end-size'],
|
|
21062
|
+
rec && rec['stroke-width'], 1);
|
|
21063
|
+
var size = getArrowInfo(rec).shape == 'dot' ? opts.dotSize : opts.size;
|
|
21064
|
+
return formatNumberValue(Math.round(size * 100) / 100);
|
|
21065
|
+
}
|
|
21066
|
+
|
|
21067
|
+
// The shape and ends shown are those of the lines with heads; lines
|
|
21068
|
+
// without are what the switch's mixed state is for.
|
|
21069
|
+
function updateArrowControl() {
|
|
21070
|
+
var records = getTargetRecords();
|
|
21071
|
+
var ids = getTargetIds();
|
|
21072
|
+
var arrowIds = getArrowTargetIds();
|
|
21073
|
+
var shape, position, info;
|
|
21074
|
+
for (var i=0; i<arrowIds.length; i++) {
|
|
21075
|
+
info = getArrowInfo(records[arrowIds[i]]);
|
|
21076
|
+
if (i === 0) {
|
|
21077
|
+
shape = info.shape;
|
|
21078
|
+
position = info.position;
|
|
21079
|
+
} else {
|
|
21080
|
+
if (info.shape != shape) shape = '';
|
|
21081
|
+
if (info.position != position) position = '';
|
|
21082
|
+
}
|
|
21083
|
+
}
|
|
21084
|
+
if (shape) lastArrow.shape = shape;
|
|
21085
|
+
if (position) lastArrow.position = position;
|
|
21086
|
+
var state = arrowIds.length === 0 ? 'off' :
|
|
21087
|
+
arrowIds.length < ids.length ? 'mixed' : 'on';
|
|
21088
|
+
arrowControl.toggle.setState(state);
|
|
21089
|
+
arrowControl.section.classed('collapsed', state == 'off');
|
|
21090
|
+
arrowShapes.forEach(function(item) {
|
|
21091
|
+
arrowControl.shapeBtns[item.name].classed('selected', item.name == shape);
|
|
21092
|
+
});
|
|
21093
|
+
arrowPositions.forEach(function(item) {
|
|
21094
|
+
arrowControl.posBtns[item.name].classed('selected', item.name == position);
|
|
21095
|
+
});
|
|
21096
|
+
var shown = arrowIds.length > 0 ? getArrowSizeShown(arrowIds) : {value: '', mixed: false};
|
|
21097
|
+
arrowControl.sizeField.setValue(shown.value);
|
|
21098
|
+
arrowControl.sizeField.setPlaceholder(shown.mixed ? 'mixed' : '');
|
|
21099
|
+
arrowControl.sizeField.setDisabled(arrowIds.length === 0);
|
|
21100
|
+
}
|
|
21101
|
+
|
|
21102
|
+
function getTargetRecords() {
|
|
21103
|
+
return targetLayer && targetLayer.data ? targetLayer.data.getRecords() : [];
|
|
21104
|
+
}
|
|
21105
|
+
|
|
20457
21106
|
function releaseFocus() {
|
|
20458
21107
|
releasePanelFocus(panel.node());
|
|
20459
21108
|
}
|
|
@@ -20468,13 +21117,17 @@
|
|
|
20468
21117
|
strokeControl.row.show();
|
|
20469
21118
|
fillControl.row.classed('hidden', geom != 'polygon');
|
|
20470
21119
|
dashControl.row.classed('hidden', geom != 'polyline');
|
|
21120
|
+
arrowControl.section.classed('hidden', geom != 'polyline');
|
|
20471
21121
|
updateColorControl(strokeControl);
|
|
20472
21122
|
updateColorControl(fillControl);
|
|
20473
21123
|
updateStrokeWidthControl();
|
|
20474
21124
|
updateDashArrayControl();
|
|
21125
|
+
if (geom == 'polyline') updateArrowControl();
|
|
20475
21126
|
randomFillBtn.classed('hidden', geom != 'polygon');
|
|
20476
21127
|
patternControl.section.classed('hidden', geom != 'polygon');
|
|
20477
21128
|
if (geom == 'polygon') patternControl.update();
|
|
21129
|
+
glowControl.section.classed('hidden', geom != 'polygon');
|
|
21130
|
+
if (geom == 'polygon') glowControl.update();
|
|
20478
21131
|
presetControl.render();
|
|
20479
21132
|
updateSavedStyleControls();
|
|
20480
21133
|
}
|
|
@@ -20535,7 +21188,7 @@
|
|
|
20535
21188
|
|
|
20536
21189
|
function applyColorControlStyle(control, color) {
|
|
20537
21190
|
var styles = [[control.field, color]];
|
|
20538
|
-
if (control.field == 'stroke' && strokeWidthIsUnsetForTargets()) {
|
|
21191
|
+
if (color && control.field == 'stroke' && strokeWidthIsUnsetForTargets()) {
|
|
20539
21192
|
styles.push(['stroke-width', 1]);
|
|
20540
21193
|
}
|
|
20541
21194
|
if (control.field == 'fill') {
|
|
@@ -20651,15 +21304,25 @@
|
|
|
20651
21304
|
addStyleValue(style, 'stroke-opacity', parseOpacityValue(strokeControl.opacity.node().value));
|
|
20652
21305
|
if (targetLayer && targetLayer.geometry_type == 'polyline') {
|
|
20653
21306
|
addStyleValue(style, 'stroke-dasharray', getControlValue(dashControl.input));
|
|
21307
|
+
addArrowStyleValues(style);
|
|
20654
21308
|
}
|
|
20655
21309
|
if (targetLayer && targetLayer.geometry_type == 'polygon') {
|
|
20656
21310
|
addStyleValue(style, 'fill', getControlValue(fillControl.input));
|
|
20657
21311
|
addStyleValue(style, 'fill-opacity', parseOpacityValue(fillControl.opacity.node().value));
|
|
20658
21312
|
addStyleValue(style, 'fill-pattern', patternControl.getCode(getControlValue(fillControl.input)));
|
|
21313
|
+
glowControl.addStyleValues(style);
|
|
20659
21314
|
}
|
|
20660
21315
|
return style;
|
|
20661
21316
|
}
|
|
20662
21317
|
|
|
21318
|
+
// Only when every target agrees, as with the other controls; a size is
|
|
21319
|
+
// saved only if the lines have their own.
|
|
21320
|
+
function addArrowStyleValues(style) {
|
|
21321
|
+
['line-start', 'line-end', 'line-end-size'].forEach(function(field) {
|
|
21322
|
+
addStyleValue(style, field, getCommonStyleValue(field));
|
|
21323
|
+
});
|
|
21324
|
+
}
|
|
21325
|
+
|
|
20663
21326
|
function addStyleValue(style, field, value) {
|
|
20664
21327
|
if (value || value === 0) {
|
|
20665
21328
|
style[field] = value;
|
|
@@ -20759,9 +21422,13 @@
|
|
|
20759
21422
|
updateControls();
|
|
20760
21423
|
}
|
|
20761
21424
|
|
|
21425
|
+
// An empty layer has nothing for the controls to act on, so they are shown
|
|
21426
|
+
// disabled rather than left to do nothing.
|
|
20762
21427
|
function updateEditingStatus(count) {
|
|
20763
|
-
|
|
21428
|
+
var total = targetLayer ? internal.getFeatureCount(targetLayer) : 0;
|
|
21429
|
+
editingStatus.text(formatEditingStatus({selected: count, total: total}));
|
|
20764
21430
|
clearLink.classed('hidden', count === 0);
|
|
21431
|
+
panel.classed('no-targets', total === 0);
|
|
20765
21432
|
}
|
|
20766
21433
|
|
|
20767
21434
|
function getActiveLayer() {
|
|
@@ -20988,8 +21655,17 @@
|
|
|
20988
21655
|
var control = makeColorRow(circlesSection, {
|
|
20989
21656
|
label: label,
|
|
20990
21657
|
// Every circle property is applied together, from what the controls are
|
|
20991
|
-
// showing: the fields are one style, not five.
|
|
20992
|
-
|
|
21658
|
+
// showing: the fields are one style, not five. An emptied colour field
|
|
21659
|
+
// unsets the colour, which applying the fields would leave alone.
|
|
21660
|
+
onColor: function(color) {
|
|
21661
|
+
if (color) {
|
|
21662
|
+
applyCircleStyles();
|
|
21663
|
+
} else if (!styleFieldIsUnset(field)) {
|
|
21664
|
+
runStyleCommand([field + '=' + quoteCommandValue('')], 'Remove circle ' + field);
|
|
21665
|
+
} else {
|
|
21666
|
+
updateControls();
|
|
21667
|
+
}
|
|
21668
|
+
},
|
|
20993
21669
|
onOpacity: applyCircleStyles,
|
|
20994
21670
|
revert: updateControls
|
|
20995
21671
|
});
|
|
@@ -21339,9 +22015,13 @@
|
|
|
21339
22015
|
updateControls();
|
|
21340
22016
|
}
|
|
21341
22017
|
|
|
22018
|
+
// As in the line and polygon panels: an empty layer's controls are shown
|
|
22019
|
+
// disabled rather than left to do nothing.
|
|
21342
22020
|
function updateEditingStatus(count) {
|
|
21343
|
-
|
|
22021
|
+
var total = targetLayer ? internal.getFeatureCount(targetLayer) : 0;
|
|
22022
|
+
editingStatus.text(formatEditingStatus({selected: count, total: total}));
|
|
21344
22023
|
clearLink.classed('hidden', count === 0);
|
|
22024
|
+
panel.classed('no-targets', total === 0);
|
|
21345
22025
|
}
|
|
21346
22026
|
|
|
21347
22027
|
function setCircleColor(control, color) {
|
|
@@ -22931,6 +23611,7 @@
|
|
|
22931
23611
|
// data.ids = [drawingId];
|
|
22932
23612
|
// data.id = drawingId;
|
|
22933
23613
|
data.ids = utils$1.uniq(data.ids.concat([drawingId]));
|
|
23614
|
+
data.drawing_id = drawingId;
|
|
22934
23615
|
}
|
|
22935
23616
|
if (pinnedOn) {
|
|
22936
23617
|
data.pinned = true;
|
|
@@ -24858,12 +25539,7 @@
|
|
|
24858
25539
|
styleOpts.interactionMode == 'edit_polygons' ||
|
|
24859
25540
|
styleOpts.interactionMode == 'snip_lines') {
|
|
24860
25541
|
// special overlay: shape editing mode
|
|
24861
|
-
|
|
24862
|
-
lyr.gui.style = getLineEditingStyle(hitData);
|
|
24863
|
-
if (activeLyr.geometry_type == 'polygon') {
|
|
24864
|
-
lyr.gui.style.fillColor = hoverFill;
|
|
24865
|
-
}
|
|
24866
|
-
return [lyr];
|
|
25542
|
+
return getShapeEditingLayers(activeLyr, hitData);
|
|
24867
25543
|
}
|
|
24868
25544
|
layers = [];
|
|
24869
25545
|
if (styleOpts.interactionMode == 'label') {
|
|
@@ -24998,15 +25674,50 @@
|
|
|
24998
25674
|
};
|
|
24999
25675
|
}
|
|
25000
25676
|
|
|
25677
|
+
// The path being drawn is shown without its vertices, which are only of use
|
|
25678
|
+
// for reshaping a completed path. The hover markers go on the last layer, so
|
|
25679
|
+
// they are drawn on top.
|
|
25680
|
+
function getShapeEditingLayers(activeLyr, hitData) {
|
|
25681
|
+
var drawingIds = hitData.drawing_id >= 0 ? [hitData.drawing_id] : [];
|
|
25682
|
+
var otherIds = utils$1.difference(hitData.ids || [], drawingIds);
|
|
25683
|
+
var layers = [], lyr;
|
|
25684
|
+
if (drawingIds.length > 0) {
|
|
25685
|
+
lyr = getShapeEditingLayer(activeLyr, hitData, drawingIds, false);
|
|
25686
|
+
// The end of a path being drawn has closely spaced vertices, and pixel
|
|
25687
|
+
// rounding would show as a staircase.
|
|
25688
|
+
lyr.gui.style.unroundedCoords = true;
|
|
25689
|
+
layers.push(lyr);
|
|
25690
|
+
}
|
|
25691
|
+
if (otherIds.length > 0 || layers.length === 0) {
|
|
25692
|
+
layers.push(getShapeEditingLayer(activeLyr, hitData, otherIds, true));
|
|
25693
|
+
}
|
|
25694
|
+
layers.forEach(function(lyr, i) {
|
|
25695
|
+
if (i < layers.length - 1) {
|
|
25696
|
+
lyr.gui.style.vertex_overlay = null;
|
|
25697
|
+
lyr.gui.style.pending_snip = null;
|
|
25698
|
+
}
|
|
25699
|
+
});
|
|
25700
|
+
return layers;
|
|
25701
|
+
}
|
|
25702
|
+
|
|
25703
|
+
function getShapeEditingLayer(activeLyr, hitData, ids, showVertices) {
|
|
25704
|
+
var lyr = getOverlayLayer(activeLyr, ids);
|
|
25705
|
+
lyr.gui.style = getLineEditingStyle(hitData, ids, showVertices);
|
|
25706
|
+
if (activeLyr.geometry_type == 'polygon') {
|
|
25707
|
+
lyr.gui.style.fillColor = hoverFill;
|
|
25708
|
+
}
|
|
25709
|
+
return lyr;
|
|
25710
|
+
}
|
|
25711
|
+
|
|
25001
25712
|
// style for vertex edit mode
|
|
25002
|
-
function getLineEditingStyle(o) {
|
|
25713
|
+
function getLineEditingStyle(o, ids, showVertices) {
|
|
25003
25714
|
var isVertex = o.hit_type == 'vertex' || o.hit_type == 'disabled';
|
|
25004
25715
|
return {
|
|
25005
|
-
ids:
|
|
25716
|
+
ids: ids,
|
|
25006
25717
|
overlay: true,
|
|
25007
25718
|
strokeColor: black,
|
|
25008
25719
|
strokeWidth: 1.2,
|
|
25009
|
-
vertices:
|
|
25720
|
+
vertices: showVertices,
|
|
25010
25721
|
vertex_overlay_color: getVertexOverlayColor(o.hit_type),
|
|
25011
25722
|
vertex_overlay_scale: isVertex ? 2.5 : 2,
|
|
25012
25723
|
vertex_overlay: o.hit_coordinates || null,
|
|
@@ -25207,6 +25918,11 @@
|
|
|
25207
25918
|
// pixel distance threshold for hovering near a vertex or segment midpoint
|
|
25208
25919
|
var HOVER_THRESHOLD$1 = 10;
|
|
25209
25920
|
|
|
25921
|
+
// How far the pointer has to move from where the mouse was pressed before a
|
|
25922
|
+
// drag draws a stroke. This matches the distance under which gui-mouse treats
|
|
25923
|
+
// a press as a click, so a press that stays this close adds only a click vertex.
|
|
25924
|
+
var STROKE_START_DIST = 6;
|
|
25925
|
+
|
|
25210
25926
|
function initLineEditing(gui, ext, hit) {
|
|
25211
25927
|
var hoverVertexInfo;
|
|
25212
25928
|
var prevVertexAddedEvent;
|
|
@@ -25216,7 +25932,8 @@
|
|
|
25216
25932
|
var drawingId = -1; // feature id of path being drawn
|
|
25217
25933
|
var sessionCount = 0;
|
|
25218
25934
|
var alert;
|
|
25219
|
-
var
|
|
25935
|
+
var stroke = null; // freehand stroke in progress (see startStroke())
|
|
25936
|
+
var blockDrag = false; // a drag finished a path; ignore it until the mouse is released
|
|
25220
25937
|
var _dragging = false;
|
|
25221
25938
|
|
|
25222
25939
|
function active() {
|
|
@@ -25236,7 +25953,7 @@
|
|
|
25236
25953
|
}
|
|
25237
25954
|
|
|
25238
25955
|
function pencilIsActive() {
|
|
25239
|
-
return active() && (cmdKeyDown() || pathDrawing()) && !vertexDragging()
|
|
25956
|
+
return active() && (cmdKeyDown() || pathDrawing()) && !vertexDragging();
|
|
25240
25957
|
}
|
|
25241
25958
|
|
|
25242
25959
|
function polygonMode() {
|
|
@@ -25273,19 +25990,32 @@
|
|
|
25273
25990
|
fullRedraw();
|
|
25274
25991
|
});
|
|
25275
25992
|
|
|
25993
|
+
// an undo during a stroke takes back the whole stroke
|
|
25994
|
+
gui.on('undo_redo_pre', function() {
|
|
25995
|
+
if (stroke) {
|
|
25996
|
+
finishStroke();
|
|
25997
|
+
blockDrag = true;
|
|
25998
|
+
}
|
|
25999
|
+
});
|
|
26000
|
+
|
|
25276
26001
|
gui.on('undo_path_add', function(e) {
|
|
25277
26002
|
deleteLastPath(hit.getHitTarget());
|
|
25278
26003
|
clearDrawingInfo();
|
|
25279
26004
|
});
|
|
25280
26005
|
|
|
26006
|
+
// e.points: the vertices that the edit added to the path
|
|
25281
26007
|
gui.on('redo_path_extend', function(e) {
|
|
25282
26008
|
var target = hit.getHitTarget();
|
|
25283
|
-
|
|
25284
|
-
|
|
25285
|
-
|
|
25286
|
-
|
|
26009
|
+
var points = e.points;
|
|
26010
|
+
var last = points[points.length - 1];
|
|
26011
|
+
if (pathDrawing()) {
|
|
26012
|
+
// the vertex following the pointer becomes the first added vertex
|
|
26013
|
+
setVertexCoords(target, [target.gui.displayArcs.getPointCount() - 1], points[0]);
|
|
26014
|
+
points.slice(1).forEach(function(p) { appendVertex$1(target, p); });
|
|
26015
|
+
appendVertex$1(target, prevHoverEvent ? pixToDataCoords(prevHoverEvent.x, prevHoverEvent.y) : last);
|
|
26016
|
+
hit.triggerChangeEvent();
|
|
25287
26017
|
} else {
|
|
25288
|
-
appendVertex$1(target,
|
|
26018
|
+
points.forEach(function(p) { appendVertex$1(target, p); });
|
|
25289
26019
|
}
|
|
25290
26020
|
if (e.shapes) {
|
|
25291
26021
|
replaceDrawnShapes(e.shapes);
|
|
@@ -25294,11 +26024,13 @@
|
|
|
25294
26024
|
|
|
25295
26025
|
gui.on('undo_path_extend', function(e) {
|
|
25296
26026
|
var target = hit.getHitTarget();
|
|
25297
|
-
|
|
26027
|
+
// while drawing, this also removes the vertex that follows the pointer,
|
|
26028
|
+
// and the path's new last vertex takes its place
|
|
26029
|
+
for (var i=0; i<e.points.length; i++) {
|
|
25298
26030
|
deleteLastVertex(target);
|
|
26031
|
+
}
|
|
26032
|
+
if (pathDrawing() && prevHoverEvent) {
|
|
25299
26033
|
updatePathEndpoint(pixToDataCoords(prevHoverEvent.x, prevHoverEvent.y));
|
|
25300
|
-
} else {
|
|
25301
|
-
deleteLastVertex(target);
|
|
25302
26034
|
}
|
|
25303
26035
|
if (e.shapes) {
|
|
25304
26036
|
replaceDrawnShapes(e.shapes);
|
|
@@ -25418,60 +26150,170 @@
|
|
|
25418
26150
|
});
|
|
25419
26151
|
|
|
25420
26152
|
gui.map.getMouse().on('dragend', function(e) {
|
|
25421
|
-
|
|
26153
|
+
finishStroke();
|
|
26154
|
+
blockDrag = false;
|
|
25422
26155
|
});
|
|
25423
26156
|
|
|
25424
26157
|
gui.map.getMouse().on('drag', function(e) {
|
|
26158
|
+
if (blockDrag) {
|
|
26159
|
+
e.stopPropagation(); // don't pan until the mouse is released
|
|
26160
|
+
return;
|
|
26161
|
+
}
|
|
25425
26162
|
if (!pencilIsActive()) {
|
|
25426
|
-
if (!pencilPoints) {
|
|
25427
|
-
// null points signals that a path was just completed -- block panning
|
|
25428
|
-
e.stopPropagation();
|
|
25429
|
-
}
|
|
25430
26163
|
return;
|
|
25431
26164
|
}
|
|
25432
26165
|
if (gui.keyboard.spaceIsPressed()) {
|
|
25433
26166
|
// pan if dragging with spacebar down
|
|
25434
|
-
|
|
26167
|
+
finishStroke();
|
|
25435
26168
|
return;
|
|
25436
26169
|
}
|
|
25437
26170
|
e.stopPropagation(); // prevent panning
|
|
25438
|
-
|
|
25439
|
-
|
|
25440
|
-
|
|
25441
|
-
|
|
25442
|
-
|
|
25443
|
-
|
|
25444
|
-
|
|
25445
|
-
|
|
25446
|
-
|
|
25447
|
-
|
|
25448
|
-
extendCurrentPath(p);
|
|
25449
|
-
} else if (polygonMode() && hoverVertexInfo && pencilPoints.length > 2) {
|
|
25450
|
-
// close path
|
|
25451
|
-
p = hoverVertexInfo.point;
|
|
25452
|
-
appendVertex$1(hit.getHitTarget(), p);
|
|
25453
|
-
extendCurrentPath(p);
|
|
25454
|
-
pencilPoints = null; // stop drawing
|
|
25455
|
-
} else if (pencilPoints.length >= 2 && pointExceedsTolerance(xy, pencilPoints, 1.2)) {
|
|
25456
|
-
xy2 = pencilPoints.pop();
|
|
25457
|
-
p = pixToDataCoords(xy2[0], xy2[1]);
|
|
25458
|
-
extendCurrentPath(p);
|
|
25459
|
-
// kludgy way to get a smoother line (could be better)
|
|
25460
|
-
// not this pencilPoints = [getAvgPoint(pencilPoints), xy2, xy];
|
|
25461
|
-
// not this pencilPoints = pencilPoints.slice(-2).concat([xy2, xy]);
|
|
25462
|
-
pencilPoints = [getAvgPoint(pencilPoints.slice(-3).concat([xy2])), xy2, xy];
|
|
26171
|
+
if (!stroke) {
|
|
26172
|
+
if (geom.distance2D(0, 0, e.dragX, e.dragY) < STROKE_START_DIST) return;
|
|
26173
|
+
startStroke(e);
|
|
26174
|
+
if (!stroke) return;
|
|
26175
|
+
}
|
|
26176
|
+
hoverVertexInfo = polygonMode() ? findPathStartInfo(e) : null;
|
|
26177
|
+
if (hoverVertexInfo && stroke.sampleCount > 1) {
|
|
26178
|
+
// the stroke has returned to the start of the path -- close it
|
|
26179
|
+
finishStroke(hoverVertexInfo);
|
|
26180
|
+
blockDrag = true;
|
|
25463
26181
|
} else {
|
|
25464
|
-
|
|
25465
|
-
pencilPoints.push(xy);
|
|
25466
|
-
updatePathEndpoint(p);
|
|
25467
|
-
addedToPath = false;
|
|
25468
|
-
}
|
|
25469
|
-
if (addedToPath) {
|
|
25470
|
-
//
|
|
25471
|
-
prevVertexAddedEvent = e;
|
|
26182
|
+
addStrokeSample(e);
|
|
25472
26183
|
}
|
|
25473
26184
|
}, null, 3); // higher priority than hit control
|
|
25474
26185
|
|
|
26186
|
+
// A stroke is smoothed as it is drawn. Vertices are placed some way behind
|
|
26187
|
+
// the pointer, where later samples no longer change the curve, and they stay
|
|
26188
|
+
// where they are. Between the last of them and the pointer, the path shows
|
|
26189
|
+
// the pointer's own trace (see updateStrokeTail()).
|
|
26190
|
+
//
|
|
26191
|
+
// The first vertex of the stroke (the anchor) is either the start of a new
|
|
26192
|
+
// path or the vertex that was following the pointer when the mouse was
|
|
26193
|
+
// pressed. Vertices are added to the path as they are placed, but the edit
|
|
26194
|
+
// that records them is made when the stroke ends, so that an undo takes back
|
|
26195
|
+
// the whole stroke.
|
|
26196
|
+
function startStroke(e) {
|
|
26197
|
+
var target = hit.getHitTarget();
|
|
26198
|
+
var anchorCommitted = !pathDrawing();
|
|
26199
|
+
var n, anchor;
|
|
26200
|
+
if (anchorCommitted) {
|
|
26201
|
+
// the path_add edit records the anchor
|
|
26202
|
+
hoverVertexInfo = null;
|
|
26203
|
+
startNewPath(pixToDataCoords(e.x - e.dragX, e.y - e.dragY));
|
|
26204
|
+
} else if (polygonMode() && hoverVertexInfo?.type == 'vertex') {
|
|
26205
|
+
// pressing on a vertex closes the polygon, as a click does
|
|
26206
|
+
extendCurrentPath([getLastVertexCoords(target)], true);
|
|
26207
|
+
blockDrag = true;
|
|
26208
|
+
return;
|
|
26209
|
+
} else if (pointerIsOnLastVertex(target)) {
|
|
26210
|
+
// continue from the last vertex rather than doubling it
|
|
26211
|
+
anchorCommitted = true;
|
|
26212
|
+
} else {
|
|
26213
|
+
// leave the vertex under the pointer behind as the anchor, and add a new
|
|
26214
|
+
// one to follow the pointer
|
|
26215
|
+
appendVertex$1(target, getLastVertexCoords(target));
|
|
26216
|
+
}
|
|
26217
|
+
n = target.gui.displayArcs.getPointCount();
|
|
26218
|
+
anchor = target.gui.displayArcs.getVertex2(n - 2);
|
|
26219
|
+
stroke = {
|
|
26220
|
+
anchorCommitted: anchorCommitted,
|
|
26221
|
+
// data coords of the vertices before the pointer that no edit records yet
|
|
26222
|
+
points: anchorCommitted ? [] : [getVertexCoords(target, n - 2)],
|
|
26223
|
+
// fitted in display coords, which survive a zoom mid-stroke
|
|
26224
|
+
fitter: new internal.GaussianStrokeFitter(anchor, {pixelSize: ext.getPixelSize()}),
|
|
26225
|
+
previewCount: 0, // vertices between the placed vertices and the pointer
|
|
26226
|
+
sampleCount: 0,
|
|
26227
|
+
lastEvent: e
|
|
26228
|
+
};
|
|
26229
|
+
}
|
|
26230
|
+
|
|
26231
|
+
// Test if the vertex following the pointer is on the path's last vertex
|
|
26232
|
+
function pointerIsOnLastVertex(target) {
|
|
26233
|
+
var arcs = target.gui.displayArcs;
|
|
26234
|
+
var n = arcs.getPointCount();
|
|
26235
|
+
var a = arcs.getVertex2(n - 1), b = arcs.getVertex2(n - 2);
|
|
26236
|
+
return geom.distance2D(a[0], a[1], b[0], b[1]) / ext.getPixelSize() < 3;
|
|
26237
|
+
}
|
|
26238
|
+
|
|
26239
|
+
function addStrokeSample(e) {
|
|
26240
|
+
var s = stroke;
|
|
26241
|
+
var placed = s.fitter.addSample(ext.pixCoordsToMapCoords(e.x, e.y));
|
|
26242
|
+
s.sampleCount++;
|
|
26243
|
+
s.lastEvent = e;
|
|
26244
|
+
updateStrokeTail(s, placed, s.fitter.getPreview(), pixToDataCoords(e.x, e.y));
|
|
26245
|
+
}
|
|
26246
|
+
|
|
26247
|
+
// Rewrites the end of the path after the stroke's placed vertices: newly
|
|
26248
|
+
// placed vertices, then the preview of the part of the stroke that has not
|
|
26249
|
+
// been placed yet, then the vertex following the pointer. Existing vertices
|
|
26250
|
+
// are overwritten rather than removed and added again, because adding or
|
|
26251
|
+
// removing a vertex copies the coordinates of the whole layer.
|
|
26252
|
+
// placed, preview: display coords
|
|
26253
|
+
// hover: data coords
|
|
26254
|
+
function updateStrokeTail(s, placed, preview, hover) {
|
|
26255
|
+
var target = hit.getHitTarget();
|
|
26256
|
+
var fixed = toDataCoords(target, placed);
|
|
26257
|
+
var tail = fixed.concat(toDataCoords(target, preview), [hover]);
|
|
26258
|
+
var count = s.previewCount + 1; // vertices after the placed vertices
|
|
26259
|
+
var start = target.gui.displayArcs.getPointCount() - count;
|
|
26260
|
+
var i;
|
|
26261
|
+
for (; count > tail.length; count--) {
|
|
26262
|
+
deleteLastVertex(target);
|
|
26263
|
+
}
|
|
26264
|
+
for (i = 0; i < tail.length; i++) {
|
|
26265
|
+
if (i < count) {
|
|
26266
|
+
setVertexCoords(target, [start + i], tail[i]);
|
|
26267
|
+
} else {
|
|
26268
|
+
appendVertex$1(target, tail[i]);
|
|
26269
|
+
}
|
|
26270
|
+
}
|
|
26271
|
+
s.points = s.points.concat(fixed);
|
|
26272
|
+
s.previewCount = tail.length - fixed.length - 1;
|
|
26273
|
+
hit.triggerChangeEvent();
|
|
26274
|
+
}
|
|
26275
|
+
|
|
26276
|
+
function toDataCoords(target, points) {
|
|
26277
|
+
return points.map(function(p) {
|
|
26278
|
+
return translateDisplayPoint(target, p);
|
|
26279
|
+
});
|
|
26280
|
+
}
|
|
26281
|
+
|
|
26282
|
+
// Completes the stroke in progress up to the pointer, and records the
|
|
26283
|
+
// stroke's vertices as a single edit.
|
|
26284
|
+
// end: (optional) hover info of the vertex that the stroke has to end at
|
|
26285
|
+
function finishStroke(end) {
|
|
26286
|
+
var s = stroke;
|
|
26287
|
+
var target, rest, points, count;
|
|
26288
|
+
if (!s) return;
|
|
26289
|
+
stroke = null;
|
|
26290
|
+
target = hit.getHitTarget();
|
|
26291
|
+
rest = end ? s.fitter.addSample(end.displayPoint) : [];
|
|
26292
|
+
rest = toDataCoords(target, rest.concat(s.fitter.finish()));
|
|
26293
|
+
if (end && rest.length > 0) {
|
|
26294
|
+
rest[rest.length - 1] = end.point;
|
|
26295
|
+
}
|
|
26296
|
+
// remove the preview
|
|
26297
|
+
count = s.previewCount + 1;
|
|
26298
|
+
for (; count > 1; count--) {
|
|
26299
|
+
deleteLastVertex(target);
|
|
26300
|
+
}
|
|
26301
|
+
if (rest.length === 0 && s.points.length > 0) {
|
|
26302
|
+
// the last vertex in the path takes the place of the vertex following the pointer
|
|
26303
|
+
deleteLastVertex(target);
|
|
26304
|
+
rest = [s.points.pop()];
|
|
26305
|
+
} else if (rest.length > 0) {
|
|
26306
|
+
setVertexCoords(target, [target.gui.displayArcs.getPointCount() - 1], rest[0]);
|
|
26307
|
+
}
|
|
26308
|
+
points = s.points.concat(rest);
|
|
26309
|
+
if (points.length === 0 || !s.anchorCommitted && points.length == 1) {
|
|
26310
|
+
hit.triggerChangeEvent(); // the stroke never left its anchor
|
|
26311
|
+
return;
|
|
26312
|
+
}
|
|
26313
|
+
prevVertexAddedEvent = s.lastEvent;
|
|
26314
|
+
extendCurrentPath(points, !!end, s.points.length);
|
|
26315
|
+
}
|
|
26316
|
+
|
|
25475
26317
|
hit.on('drag', function(e) {
|
|
25476
26318
|
if (!vertexDragging() || pathDrawing()) {
|
|
25477
26319
|
return;
|
|
@@ -25551,7 +26393,8 @@
|
|
|
25551
26393
|
if (detectDoubleClick(e)) return; // ignore second click of a dblclick
|
|
25552
26394
|
var p = pixToDataCoords(e.x, e.y);
|
|
25553
26395
|
if (pathDrawing()) {
|
|
25554
|
-
|
|
26396
|
+
// finish the path if a vertex is selected (but not an interpolated point)
|
|
26397
|
+
extendCurrentPath([hoverVertexInfo?.point || p], hoverVertexInfo?.type == 'vertex');
|
|
25555
26398
|
} else if (hoverVertexInfo?.type == 'interpolated') {
|
|
25556
26399
|
// don't start new path if hovering along a segment -- this is
|
|
25557
26400
|
// likely to be an attempt to add a new vertex, not start a new path
|
|
@@ -25651,6 +26494,11 @@
|
|
|
25651
26494
|
|
|
25652
26495
|
function finishCurrentPath() {
|
|
25653
26496
|
if (!pathDrawing()) return;
|
|
26497
|
+
if (stroke) {
|
|
26498
|
+
// finishing mid-drag (e.g. with the Enter key)
|
|
26499
|
+
finishStroke();
|
|
26500
|
+
blockDrag = true;
|
|
26501
|
+
}
|
|
25654
26502
|
var target = hit.getHitTarget();
|
|
25655
26503
|
if (getLastArcLength(target) <= 2) { // includes hover point
|
|
25656
26504
|
// deleteLastPath(target);
|
|
@@ -25674,15 +26522,21 @@
|
|
|
25674
26522
|
updateCursor();
|
|
25675
26523
|
}
|
|
25676
26524
|
|
|
25677
|
-
//
|
|
25678
|
-
|
|
26525
|
+
// points: [x, y] source data coordinates of vertices to add to the path. The
|
|
26526
|
+
// vertex that follows the pointer becomes points[placed], so it must
|
|
26527
|
+
// already be there; the rest are appended after it.
|
|
26528
|
+
// finish: true if the path ends at an existing vertex (which closes a polygon)
|
|
26529
|
+
// placed: (optional) number of points already in the path, before the vertex
|
|
26530
|
+
// that follows the pointer
|
|
26531
|
+
function extendCurrentPath(points, finish, placed) {
|
|
25679
26532
|
var target = hit.getHitTarget();
|
|
25680
26533
|
var shapes1, shapes2;
|
|
25681
|
-
// finish the path if a vertex is selected (but not an interpolated point)
|
|
25682
|
-
var finish = hoverVertexInfo?.type == 'vertex';
|
|
25683
26534
|
if (getLastArcLength(target) < 2) {
|
|
25684
26535
|
stop$1('Defective path');
|
|
25685
26536
|
}
|
|
26537
|
+
for (var i=(placed || 0) + 1; i<points.length; i++) {
|
|
26538
|
+
appendVertex$1(target, points[i]);
|
|
26539
|
+
}
|
|
25686
26540
|
if (finish && polygonMode()) {
|
|
25687
26541
|
shapes1 = target.shapes.slice(initialShapeCount);
|
|
25688
26542
|
try {
|
|
@@ -25694,12 +26548,12 @@
|
|
|
25694
26548
|
}
|
|
25695
26549
|
if (shapes2) {
|
|
25696
26550
|
replaceDrawnShapes(shapes2);
|
|
25697
|
-
gui.dispatchEvent('path_extend', {target,
|
|
26551
|
+
gui.dispatchEvent('path_extend', {target, points, shapes1, shapes2});
|
|
25698
26552
|
clearDrawingInfo();
|
|
25699
26553
|
fullRedraw();
|
|
25700
26554
|
} else {
|
|
25701
|
-
appendVertex$1(target,
|
|
25702
|
-
gui.dispatchEvent('path_extend', {target,
|
|
26555
|
+
appendVertex$1(target, points[points.length - 1]); // the new vertex following the pointer
|
|
26556
|
+
gui.dispatchEvent('path_extend', {target, points});
|
|
25703
26557
|
hit.triggerChangeEvent(); // trigger overlay redraw
|
|
25704
26558
|
}
|
|
25705
26559
|
}
|
|
@@ -28765,6 +29619,7 @@
|
|
|
28765
29619
|
selection.turnOff();
|
|
28766
29620
|
abandonCurve();
|
|
28767
29621
|
armed = null;
|
|
29622
|
+
setLabelToolArmed(gui, null);
|
|
28768
29623
|
updateButtons();
|
|
28769
29624
|
hideInstructions();
|
|
28770
29625
|
drag = null;
|
|
@@ -28830,6 +29685,7 @@
|
|
|
28830
29685
|
if (armed == mode) return;
|
|
28831
29686
|
if (drawingCurve()) abandonCurve();
|
|
28832
29687
|
armed = mode;
|
|
29688
|
+
setLabelToolArmed(gui, mode);
|
|
28833
29689
|
updateButtons();
|
|
28834
29690
|
showInstructions();
|
|
28835
29691
|
}
|
|
@@ -30581,10 +31437,29 @@
|
|
|
30581
31437
|
// array of field names of relevant svg display properties
|
|
30582
31438
|
fields = getStyleFields(lyr).filter(function(f) {return f in styleIndex;}),
|
|
30583
31439
|
records = lyr.data.getRecords();
|
|
31440
|
+
// Arrowheads are drawn by the canvas as shapes, not set as attributes, so
|
|
31441
|
+
// they are not among the attribute fields. Assigned on every call, like the
|
|
31442
|
+
// rest, because the style object is reused from one feature to the next.
|
|
31443
|
+
var arrowFields = getLineArrowFields(lyr);
|
|
31444
|
+
var hasStrokeFields = fields.includes('stroke') || fields.includes('stroke-width');
|
|
31445
|
+
// Glows are drawn from the record rather than set as attributes, like the
|
|
31446
|
+
// arrowheads. An outer glow that the whole layer shares is drawn once for the
|
|
31447
|
+
// layer (see svg-glow.mjs), so the shapes are then given none of their own.
|
|
31448
|
+
var hasGlows = layerHasGlowFields(lyr);
|
|
31449
|
+
var layerOuterGlow = hasGlows ? internal.svg.getLayerOuterGlow(lyr) : null;
|
|
30584
31450
|
|
|
30585
31451
|
var styler = function(style, i) {
|
|
30586
31452
|
var rec = records[i];
|
|
30587
31453
|
var fname, val;
|
|
31454
|
+
if (arrowFields) {
|
|
31455
|
+
style.lineStart = rec && rec['line-start'];
|
|
31456
|
+
style.lineEnd = rec && rec['line-end'];
|
|
31457
|
+
style.lineEndSize = rec && rec['line-end-size'];
|
|
31458
|
+
}
|
|
31459
|
+
if (hasGlows) {
|
|
31460
|
+
style.outerGlow = layerOuterGlow ? null : internal.svg.getPolygonGlow(rec, 'outer');
|
|
31461
|
+
style.innerGlow = internal.svg.getPolygonGlow(rec, 'inner');
|
|
31462
|
+
}
|
|
30588
31463
|
for (var j=0; j<fields.length; j++) {
|
|
30589
31464
|
fname = fields[j];
|
|
30590
31465
|
val = rec && rec[fname];
|
|
@@ -30606,6 +31481,18 @@
|
|
|
30606
31481
|
}
|
|
30607
31482
|
};
|
|
30608
31483
|
var style = {styler: styler, type: 'styled'};
|
|
31484
|
+
if (hasGlows) {
|
|
31485
|
+
style.layerOuterGlow = layerOuterGlow;
|
|
31486
|
+
// How far past its shapes the layer draws, which is how far outside the
|
|
31487
|
+
// view a shape can be and still reach into it.
|
|
31488
|
+
style.glowReach = internal.svg.getMaxGlowWidth(records) * internal.svg.GLOW_REACH;
|
|
31489
|
+
}
|
|
31490
|
+
// A line layer styled with nothing but arrowheads is drawn the way SVG
|
|
31491
|
+
// export draws it, with the black 1px line the layer's group gives it.
|
|
31492
|
+
if (arrowFields && !hasStrokeFields) {
|
|
31493
|
+
style.strokeColor = 'black';
|
|
31494
|
+
style.strokeWidth = 1;
|
|
31495
|
+
}
|
|
30609
31496
|
// use squares if radius is missing... (TODO: check behavior with labels, etc)
|
|
30610
31497
|
if (lyr.geometry_type == 'point' && fields.includes('r') === false) {
|
|
30611
31498
|
style.dotSize = 1;
|
|
@@ -30620,7 +31507,25 @@
|
|
|
30620
31507
|
// return fields.indexOf('r') > -1; // require 'r' field for point symbols
|
|
30621
31508
|
return fields.includes('fill') || fields.includes('r'); // support colored squares
|
|
30622
31509
|
}
|
|
30623
|
-
return utils$1.difference(fields, ['opacity', 'class']).length > 0
|
|
31510
|
+
return utils$1.difference(fields, ['opacity', 'class']).length > 0 ||
|
|
31511
|
+
!!getLineArrowFields(lyr) || layerHasGlowFields(lyr);
|
|
31512
|
+
}
|
|
31513
|
+
|
|
31514
|
+
function layerHasGlowFields(lyr) {
|
|
31515
|
+
if (lyr.geometry_type != 'polygon' || !lyr.data) return false;
|
|
31516
|
+
return lyr.data.getFields().some(function(f) {
|
|
31517
|
+
return internal.svg.glowFields.includes(f);
|
|
31518
|
+
});
|
|
31519
|
+
}
|
|
31520
|
+
|
|
31521
|
+
// The arrowhead fields a line layer has, or null
|
|
31522
|
+
function getLineArrowFields(lyr) {
|
|
31523
|
+
var fields;
|
|
31524
|
+
if (lyr.geometry_type != 'polyline' || !lyr.data) return null;
|
|
31525
|
+
fields = lyr.data.getFields().filter(function(f) {
|
|
31526
|
+
return internal.svg.lineArrowFields.includes(f);
|
|
31527
|
+
});
|
|
31528
|
+
return fields.length > 0 ? fields : null;
|
|
30624
31529
|
}
|
|
30625
31530
|
|
|
30626
31531
|
function getStyleFields(lyr) {
|
|
@@ -31527,6 +32432,102 @@
|
|
|
31527
32432
|
}
|
|
31528
32433
|
}
|
|
31529
32434
|
|
|
32435
|
+
// Canvas rendering of polygon glows, matching the SVG filters in svg-glow.mjs.
|
|
32436
|
+
//
|
|
32437
|
+
// What glows is drawn first into a scratch canvas: one shape, or a whole layer
|
|
32438
|
+
// for a layer's outer glow. The glows are then made from that image's alpha,
|
|
32439
|
+
// the way a filter works from SourceAlpha:
|
|
32440
|
+
//
|
|
32441
|
+
// outer the image's shadow, with the image itself cut out of it
|
|
32442
|
+
// inner the shadow of the image's inverse, kept only inside the image
|
|
32443
|
+
//
|
|
32444
|
+
// A shadow is only drawn with the image that casts it, so the image is drawn
|
|
32445
|
+
// off the edge of the canvas and its shadow is offset back into place. Drawn
|
|
32446
|
+
// in place and clipped, the image leaks into its own antialiased edge.
|
|
32447
|
+
//
|
|
32448
|
+
// A shadow's shadowBlur is twice the standard deviation of its blur, and a
|
|
32449
|
+
// glow's width is twice the standard deviation of the filter's, so the two
|
|
32450
|
+
// are the same number. Shadows are drawn in device pixels whatever the
|
|
32451
|
+
// transform, and the scratch canvases are never transformed when one is.
|
|
32452
|
+
|
|
32453
|
+
var scratch = {};
|
|
32454
|
+
|
|
32455
|
+
function getScratchCanvas(name, w, h) {
|
|
32456
|
+
var o = scratch[name];
|
|
32457
|
+
if (!o) {
|
|
32458
|
+
o = scratch[name] = {canvas: document.createElement('canvas')};
|
|
32459
|
+
o.ctx = o.canvas.getContext('2d');
|
|
32460
|
+
}
|
|
32461
|
+
// Grown and never shrunk: resizing a canvas reallocates it, and there can
|
|
32462
|
+
// be a resize for every shape drawn.
|
|
32463
|
+
if (o.canvas.width < w || o.canvas.height < h) {
|
|
32464
|
+
o.canvas.width = Math.max(o.canvas.width, w);
|
|
32465
|
+
o.canvas.height = Math.max(o.canvas.height, h);
|
|
32466
|
+
}
|
|
32467
|
+
o.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
32468
|
+
o.ctx.clearRect(0, 0, w, h);
|
|
32469
|
+
return o;
|
|
32470
|
+
}
|
|
32471
|
+
|
|
32472
|
+
// A canvas to draw what glows into, @w by @h device pixels. Separate from the
|
|
32473
|
+
// ones compositeGlows() uses, so a shape can be drawn into it while a layer is
|
|
32474
|
+
// being drawn into another.
|
|
32475
|
+
// @name: 'shape' or 'layer'
|
|
32476
|
+
function getGlowSourceCanvas(name, w, h) {
|
|
32477
|
+
return getScratchCanvas('source-' + name, w, h);
|
|
32478
|
+
}
|
|
32479
|
+
|
|
32480
|
+
// How far past a shape its glows reach, in device pixels.
|
|
32481
|
+
function getGlowMargin(outer, inner, pxScale) {
|
|
32482
|
+
var width = Math.max(outer ? outer.width : 0, inner ? inner.width : 0);
|
|
32483
|
+
return Math.ceil(width * pxScale * internal.svg.GLOW_REACH) + 2;
|
|
32484
|
+
}
|
|
32485
|
+
|
|
32486
|
+
// Draws @src (the top-left @w by @h pixels of a canvas) into @ctx at @x, @y,
|
|
32487
|
+
// with its glows: the outer one underneath and the inner one on top.
|
|
32488
|
+
// @pxScale: device pixels per px of glow width
|
|
32489
|
+
function compositeGlows(ctx, src, x, y, w, h, outer, inner, pxScale) {
|
|
32490
|
+
var glow, inverse;
|
|
32491
|
+
if (outer) {
|
|
32492
|
+
glow = getScratchCanvas('glow', w, h);
|
|
32493
|
+
drawShadow(glow.ctx, src, w, h, outer, pxScale);
|
|
32494
|
+
glow.ctx.globalCompositeOperation = 'destination-out';
|
|
32495
|
+
glow.ctx.drawImage(src, 0, 0, w, h, 0, 0, w, h);
|
|
32496
|
+
glow.ctx.globalCompositeOperation = 'source-over';
|
|
32497
|
+
ctx.drawImage(glow.canvas, 0, 0, w, h, x, y, w, h);
|
|
32498
|
+
}
|
|
32499
|
+
ctx.drawImage(src, 0, 0, w, h, x, y, w, h);
|
|
32500
|
+
if (inner) {
|
|
32501
|
+
inverse = getScratchCanvas('inverse', w, h);
|
|
32502
|
+
inverse.ctx.fillStyle = '#000';
|
|
32503
|
+
inverse.ctx.fillRect(0, 0, w, h);
|
|
32504
|
+
inverse.ctx.globalCompositeOperation = 'destination-out';
|
|
32505
|
+
inverse.ctx.drawImage(src, 0, 0, w, h, 0, 0, w, h);
|
|
32506
|
+
inverse.ctx.globalCompositeOperation = 'source-over';
|
|
32507
|
+
glow = getScratchCanvas('glow', w, h);
|
|
32508
|
+
drawShadow(glow.ctx, inverse.canvas, w, h, inner, pxScale);
|
|
32509
|
+
glow.ctx.globalCompositeOperation = 'destination-in';
|
|
32510
|
+
glow.ctx.drawImage(src, 0, 0, w, h, 0, 0, w, h);
|
|
32511
|
+
glow.ctx.globalCompositeOperation = 'source-over';
|
|
32512
|
+
ctx.drawImage(glow.canvas, 0, 0, w, h, x, y, w, h);
|
|
32513
|
+
}
|
|
32514
|
+
}
|
|
32515
|
+
|
|
32516
|
+
// The shadow of the top-left @w by @h pixels of @img, with no image. The glow's
|
|
32517
|
+
// opacity is globalAlpha rather than part of the shadow's color, which can be
|
|
32518
|
+
// any CSS color and would have to be parsed to take one.
|
|
32519
|
+
function drawShadow(ctx, img, w, h, glow, pxScale) {
|
|
32520
|
+
var offset = w + glow.width * pxScale * 4 + 10;
|
|
32521
|
+
ctx.save();
|
|
32522
|
+
ctx.globalAlpha = glow.opacity;
|
|
32523
|
+
ctx.shadowColor = glow.color;
|
|
32524
|
+
ctx.shadowBlur = glow.width * pxScale;
|
|
32525
|
+
ctx.shadowOffsetX = offset;
|
|
32526
|
+
ctx.shadowOffsetY = 0;
|
|
32527
|
+
ctx.drawImage(img, 0, 0, w, h, -offset, 0, w, h);
|
|
32528
|
+
ctx.restore();
|
|
32529
|
+
}
|
|
32530
|
+
|
|
31530
32531
|
// TODO: consider moving this upstream
|
|
31531
32532
|
function getArcsForRendering(lyr, ext) {
|
|
31532
32533
|
var dataset = lyr.gui.source.dataset;
|
|
@@ -31577,9 +32578,9 @@
|
|
|
31577
32578
|
}
|
|
31578
32579
|
} else {
|
|
31579
32580
|
arcs = getArcsForRendering(lyr, ext);
|
|
31580
|
-
filter = getShapeFilter(arcs, layer.shapes, ext);
|
|
32581
|
+
filter = getShapeFilter(arcs, layer.shapes, ext, style.overlay ? 0 : style.glowReach);
|
|
31581
32582
|
canv.drawStyledPaths(layer.shapes, arcs, style, filter);
|
|
31582
|
-
if (style.vertices) {
|
|
32583
|
+
if (style.vertices || style.vertex_overlay || style.pending_snip) {
|
|
31583
32584
|
canv.drawVertices(layer.shapes, arcs, style, filter);
|
|
31584
32585
|
}
|
|
31585
32586
|
}
|
|
@@ -31615,10 +32616,17 @@
|
|
|
31615
32616
|
// view. The filter takes a shape index and tests the shape's bbox against
|
|
31616
32617
|
// the viewport. At nearly full extent the test is a no-op, so we return
|
|
31617
32618
|
// null to let the caller skip it entirely.
|
|
31618
|
-
|
|
32619
|
+
// reach: how far past its shapes the layer draws, in px (e.g. a glow)
|
|
32620
|
+
function getShapeFilter(arcs, shapes, ext, reach) {
|
|
31619
32621
|
if (ext.scale() < 1.1) return null;
|
|
31620
32622
|
var view = ext.getBounds();
|
|
31621
32623
|
var b = new Bounds$1();
|
|
32624
|
+
var pad;
|
|
32625
|
+
if (reach > 0) {
|
|
32626
|
+
pad = reach * (ext.getSymbolScale() || 1) * ext.getPixelSize();
|
|
32627
|
+
view = new Bounds$1(view.toArray());
|
|
32628
|
+
view.padBounds(pad, pad, pad, pad);
|
|
32629
|
+
}
|
|
31622
32630
|
return function(i) {
|
|
31623
32631
|
var shp = shapes[i];
|
|
31624
32632
|
if (!shp) return false;
|
|
@@ -31726,6 +32734,8 @@
|
|
|
31726
32734
|
};
|
|
31727
32735
|
*/
|
|
31728
32736
|
|
|
32737
|
+
// Draws a dot at each vertex (if style.vertices is set), and the markers of
|
|
32738
|
+
// style.vertex_overlay and style.pending_snip.
|
|
31729
32739
|
_self.drawVertices = function(shapes, arcs, style, filter) {
|
|
31730
32740
|
var iter = new internal.ShapeIter(arcs);
|
|
31731
32741
|
var t = getScaledTransform(_ext);
|
|
@@ -31735,7 +32745,7 @@
|
|
|
31735
32745
|
var i, j, p;
|
|
31736
32746
|
_ctx.beginPath();
|
|
31737
32747
|
_ctx.fillStyle = color;
|
|
31738
|
-
for (i=0; i<shapes.length; i++) {
|
|
32748
|
+
for (i=0; style.vertices && i<shapes.length; i++) {
|
|
31739
32749
|
var shp = shapes[i];
|
|
31740
32750
|
if (!shp || filter && !filter(i)) continue;
|
|
31741
32751
|
for (j=0; j<shp.length; j++) {
|
|
@@ -31773,50 +32783,143 @@
|
|
|
31773
32783
|
_self.drawStyledPaths = function(shapes, arcs, style, filter) {
|
|
31774
32784
|
var styleIndex = {};
|
|
31775
32785
|
var batchSize = 100;
|
|
31776
|
-
var
|
|
32786
|
+
var lineScale = getScaledLineScale(_ext, style);
|
|
32787
|
+
var startPath = getPathStart(_ext, lineScale);
|
|
31777
32788
|
var draw = getShapePencil(arcs, _ext);
|
|
32789
|
+
var arrowPencil = null;
|
|
31778
32790
|
var key, item, shp;
|
|
31779
32791
|
var styler = style.styler || null;
|
|
31780
32792
|
var drawStyle = styler ? utils$1.defaults({}, style) : style;
|
|
31781
|
-
|
|
31782
|
-
|
|
31783
|
-
|
|
31784
|
-
|
|
31785
|
-
|
|
31786
|
-
|
|
31787
|
-
|
|
31788
|
-
|
|
31789
|
-
|
|
31790
|
-
|
|
31791
|
-
|
|
31792
|
-
|
|
31793
|
-
|
|
31794
|
-
|
|
31795
|
-
|
|
31796
|
-
|
|
31797
|
-
|
|
31798
|
-
|
|
31799
|
-
|
|
32793
|
+
// Hover and selection overlays copy the layer's style; they do not glow.
|
|
32794
|
+
var glows = !style.overlay;
|
|
32795
|
+
var glowScale = getCanvasStrokeScale(GUI.getPixelRatio(), lineScale);
|
|
32796
|
+
var layerGlow = glows && style.layerOuterGlow ?
|
|
32797
|
+
startLayerGlow(style.layerOuterGlow, glowScale) : null;
|
|
32798
|
+
try {
|
|
32799
|
+
drawShapes();
|
|
32800
|
+
} finally {
|
|
32801
|
+
if (layerGlow) endLayerGlow(layerGlow);
|
|
32802
|
+
}
|
|
32803
|
+
if (layerGlow) compositeLayerGlow(layerGlow);
|
|
32804
|
+
|
|
32805
|
+
function drawShapes() {
|
|
32806
|
+
var glowPencil = null;
|
|
32807
|
+
for (var i=0; i<shapes.length; i++) {
|
|
32808
|
+
shp = shapes[i];
|
|
32809
|
+
if (!shp || filter && !filter(i)) continue;
|
|
32810
|
+
if (styler) {
|
|
32811
|
+
styler(drawStyle, i);
|
|
32812
|
+
}
|
|
32813
|
+
if (glows && (drawStyle.outerGlow || drawStyle.innerGlow)) {
|
|
32814
|
+
// Shapes before this one are drawn first, as they are in SVG, where
|
|
32815
|
+
// a shape's outer glow lies over the shapes that precede it.
|
|
32816
|
+
flushBatches();
|
|
32817
|
+
if (!glowPencil) glowPencil = getGlowShapePencil(arcs, startPath, draw, glowScale);
|
|
32818
|
+
glowPencil(shp, drawStyle);
|
|
32819
|
+
continue;
|
|
32820
|
+
}
|
|
32821
|
+
// A line with arrowheads is drawn on its own, trimmed to meet its heads;
|
|
32822
|
+
// see svg-line-arrows.mjs. Only lines styled with line-start or line-end
|
|
32823
|
+
// get here, so the batch below is untouched for everything else.
|
|
32824
|
+
if (drawStyle.lineStart || drawStyle.lineEnd) {
|
|
32825
|
+
if (!arrowPencil) arrowPencil = getArrowLinePencil(arcs, _ext, lineScale);
|
|
32826
|
+
if (arrowPencil(shp, _ctx, drawStyle, startPath, draw)) continue;
|
|
32827
|
+
}
|
|
32828
|
+
if (!drawStyle.batchOverlay && (drawStyle.overlay ||
|
|
32829
|
+
drawStyle.opacity < 1 || drawStyle.fillOpacity < 1 ||
|
|
32830
|
+
drawStyle.strokeOpacity < 1 || drawStyle.fillEffect)) {
|
|
32831
|
+
// don't batch shapes with opacity, in case they overlap
|
|
32832
|
+
drawPaths([shp], startPath, draw, drawStyle);
|
|
32833
|
+
continue;
|
|
32834
|
+
}
|
|
32835
|
+
key = getStyleKey(drawStyle);
|
|
32836
|
+
if (key in styleIndex === false) {
|
|
32837
|
+
styleIndex[key] = {
|
|
32838
|
+
style: utils$1.defaults({}, drawStyle),
|
|
32839
|
+
shapes: []
|
|
32840
|
+
};
|
|
32841
|
+
}
|
|
32842
|
+
item = styleIndex[key];
|
|
32843
|
+
item.shapes.push(shp);
|
|
32844
|
+
if (item.shapes.length >= batchSize) {
|
|
32845
|
+
drawPaths(item.shapes, startPath, draw, item.style);
|
|
32846
|
+
item.shapes = [];
|
|
32847
|
+
}
|
|
31800
32848
|
}
|
|
31801
|
-
|
|
31802
|
-
|
|
31803
|
-
|
|
32849
|
+
flushBatches();
|
|
32850
|
+
}
|
|
32851
|
+
|
|
32852
|
+
function flushBatches() {
|
|
32853
|
+
Object.keys(styleIndex).forEach(function(key) {
|
|
32854
|
+
var item = styleIndex[key];
|
|
32855
|
+
if (item.shapes.length === 0) return;
|
|
31804
32856
|
drawPaths(item.shapes, startPath, draw, item.style);
|
|
31805
32857
|
item.shapes = [];
|
|
31806
|
-
}
|
|
32858
|
+
});
|
|
31807
32859
|
}
|
|
31808
|
-
Object.keys(styleIndex).forEach(function(key) {
|
|
31809
|
-
var item = styleIndex[key];
|
|
31810
|
-
drawPaths(item.shapes, startPath, draw, item.style);
|
|
31811
|
-
});
|
|
31812
32860
|
};
|
|
31813
32861
|
|
|
31814
|
-
function drawPaths(shapes, beginPath, drawShape, style) {
|
|
31815
|
-
|
|
32862
|
+
function drawPaths(shapes, beginPath, drawShape, style, ctx) {
|
|
32863
|
+
ctx = ctx || _ctx;
|
|
32864
|
+
beginPath(ctx, style);
|
|
31816
32865
|
for (var i=0, n=shapes.length; i<n; i++) {
|
|
31817
|
-
drawShape(shapes[i],
|
|
32866
|
+
drawShape(shapes[i], ctx, style);
|
|
31818
32867
|
}
|
|
31819
|
-
endPath(
|
|
32868
|
+
endPath(ctx, style);
|
|
32869
|
+
}
|
|
32870
|
+
|
|
32871
|
+
// A layer's own outer glow is made from the whole layer, so the layer is
|
|
32872
|
+
// drawn into a canvas of its own and then onto this one with its glow. The
|
|
32873
|
+
// canvas is larger than this one by the reach of the glow, so that shapes
|
|
32874
|
+
// just outside the view still cast theirs into it.
|
|
32875
|
+
function startLayerGlow(glow, pxScale) {
|
|
32876
|
+
var margin = getGlowMargin(glow, null, pxScale);
|
|
32877
|
+
var w = _canvas.width + margin * 2;
|
|
32878
|
+
var h = _canvas.height + margin * 2;
|
|
32879
|
+
var src = getGlowSourceCanvas('layer', w, h);
|
|
32880
|
+
var o = {glow: glow, pxScale: pxScale, margin: margin, w: w, h: h,
|
|
32881
|
+
src: src, ctx: _ctx};
|
|
32882
|
+
src.ctx.setTransform(1, 0, 0, 1, margin, margin);
|
|
32883
|
+
_ctx = src.ctx;
|
|
32884
|
+
return o;
|
|
32885
|
+
}
|
|
32886
|
+
|
|
32887
|
+
function endLayerGlow(o) {
|
|
32888
|
+
o.src.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
32889
|
+
_ctx = o.ctx;
|
|
32890
|
+
}
|
|
32891
|
+
|
|
32892
|
+
function compositeLayerGlow(o) {
|
|
32893
|
+
compositeGlows(_ctx, o.src.canvas, -o.margin, -o.margin, o.w, o.h,
|
|
32894
|
+
o.glow, null, o.pxScale);
|
|
32895
|
+
}
|
|
32896
|
+
|
|
32897
|
+
// Draws one shape with its own glows, which are made from the shape alone.
|
|
32898
|
+
// The shape is drawn into a canvas the size of its bounding box and the
|
|
32899
|
+
// reach of its glows, cut down to the part that can be seen.
|
|
32900
|
+
function getGlowShapePencil(arcs, startPath, draw, pxScale) {
|
|
32901
|
+
var t = getScaledTransform(_ext);
|
|
32902
|
+
var b = new Bounds$1();
|
|
32903
|
+
return function(shp, style) {
|
|
32904
|
+
var outer = style.outerGlow, inner = style.innerGlow;
|
|
32905
|
+
var margin = getGlowMargin(outer, inner, pxScale) +
|
|
32906
|
+
Math.ceil((style.strokeWidth > 0 ? style.strokeWidth * pxScale : 0) / 2);
|
|
32907
|
+
var viewMargin = margin * 2;
|
|
32908
|
+
var x0, y0, x1, y1, src;
|
|
32909
|
+
b.empty();
|
|
32910
|
+
arcs.getMultiShapeBounds(shp, b);
|
|
32911
|
+
if (!b.hasBounds()) return;
|
|
32912
|
+
x0 = Math.floor(Math.max(b.xmin * t.mx + t.bx - margin, -viewMargin));
|
|
32913
|
+
x1 = Math.ceil(Math.min(b.xmax * t.mx + t.bx + margin, _canvas.width + viewMargin));
|
|
32914
|
+
y0 = Math.floor(Math.max(b.ymax * t.my + t.by - margin, -viewMargin));
|
|
32915
|
+
y1 = Math.ceil(Math.min(b.ymin * t.my + t.by + margin, _canvas.height + viewMargin));
|
|
32916
|
+
if (x1 <= x0 || y1 <= y0) return;
|
|
32917
|
+
src = getGlowSourceCanvas('shape', x1 - x0, y1 - y0);
|
|
32918
|
+
src.ctx.setTransform(1, 0, 0, 1, -x0, -y0);
|
|
32919
|
+
drawPaths([shp], startPath, draw, style, src.ctx);
|
|
32920
|
+
src.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
32921
|
+
compositeGlows(_ctx, src.canvas, x0, y0, x1 - x0, y1 - y0, outer, inner, pxScale);
|
|
32922
|
+
};
|
|
31820
32923
|
}
|
|
31821
32924
|
|
|
31822
32925
|
_self.drawSquareDots = function(shapes, style) {
|
|
@@ -32138,6 +33241,10 @@
|
|
|
32138
33241
|
return x + 0.5 | 0;
|
|
32139
33242
|
}
|
|
32140
33243
|
|
|
33244
|
+
function noRound(x) {
|
|
33245
|
+
return x;
|
|
33246
|
+
}
|
|
33247
|
+
|
|
32141
33248
|
function roundToHalfPix(x) {
|
|
32142
33249
|
return (x * 2 | 0) / 2;
|
|
32143
33250
|
}
|
|
@@ -32149,15 +33256,113 @@
|
|
|
32149
33256
|
if (style.fillEffect) {
|
|
32150
33257
|
ctx.fillStyle = getCanvasFillEffect(ctx, shp, arcs, ext, style);
|
|
32151
33258
|
}
|
|
33259
|
+
// Rounding also drops vertices that land on the same pixel, which keeps
|
|
33260
|
+
// detailed shapes fast to draw, so only a style for a few short paths
|
|
33261
|
+
// should turn it off (see gui-overlay-styler.mjs).
|
|
33262
|
+
var round = style.unroundedCoords ? noRound : roundToPix;
|
|
32152
33263
|
for (var i=0, n=shp ? shp.length : 0; i<n; i++) {
|
|
32153
33264
|
iter.init(shp[i]);
|
|
32154
33265
|
// 0.2 trades visible seams for performance
|
|
32155
33266
|
// drawPath(protectIterForDrawing(iter, ext), t, ctx, 0.2);
|
|
32156
|
-
drawPath2(protectIterForDrawing(iter, ext), t, ctx,
|
|
33267
|
+
drawPath2(protectIterForDrawing(iter, ext), t, ctx, round);
|
|
33268
|
+
}
|
|
33269
|
+
};
|
|
33270
|
+
}
|
|
33271
|
+
|
|
33272
|
+
// Returns a function that draws one line feature with its arrowheads, and
|
|
33273
|
+
// returns false if the feature has none to draw (a style of 'none', or no
|
|
33274
|
+
// stroke), for the caller to draw it the usual way.
|
|
33275
|
+
//
|
|
33276
|
+
// Heads are sized by the same factor as the line width, so that a preview
|
|
33277
|
+
// matches the exported SVG. Far enough in that paths are clipped to the view
|
|
33278
|
+
// (see protectIterForDrawing()), the line is drawn clipped and untrimmed and
|
|
33279
|
+
// only the heads are added, since a clipped path's ends are not its ends.
|
|
33280
|
+
function getArrowLinePencil(arcs, ext, lineScale) {
|
|
33281
|
+
var t = getScaledTransform(ext);
|
|
33282
|
+
var iter = new internal.ShapeIter(arcs);
|
|
33283
|
+
var strokeScale = getCanvasStrokeScale(GUI.getPixelRatio(), lineScale);
|
|
33284
|
+
var clipped = ext.scale() > 100;
|
|
33285
|
+
return function(shp, ctx, style, startPath, draw) {
|
|
33286
|
+
var opts = internal.svg.makeLineArrowOpts(style.lineStart, style.lineEnd,
|
|
33287
|
+
style.lineEndSize, style.strokeWidth, strokeScale);
|
|
33288
|
+
var heads = [];
|
|
33289
|
+
var i, coords, shape;
|
|
33290
|
+
if (opts.start == 'none' && opts.end == 'none' || !(style.strokeWidth > 0)) {
|
|
33291
|
+
return false;
|
|
32157
33292
|
}
|
|
33293
|
+
startPath(ctx, style);
|
|
33294
|
+
for (i=0; i<shp.length; i++) {
|
|
33295
|
+
coords = getPixelCoords(iter, shp[i], t);
|
|
33296
|
+
if (coords.length < 2) continue;
|
|
33297
|
+
shape = internal.svg.getLineArrowShape(coords, opts);
|
|
33298
|
+
heads = heads.concat(shape.heads);
|
|
33299
|
+
if (!clipped) traceCoords(shape.coords, ctx);
|
|
33300
|
+
}
|
|
33301
|
+
if (clipped) draw(shp, ctx, style);
|
|
33302
|
+
endPath(ctx, style);
|
|
33303
|
+
drawArrowHeads(heads, ctx, style, opts.width);
|
|
33304
|
+
return true;
|
|
32158
33305
|
};
|
|
32159
33306
|
}
|
|
32160
33307
|
|
|
33308
|
+
function getPixelCoords(iter, path, t) {
|
|
33309
|
+
var coords = [];
|
|
33310
|
+
var x, y, prev;
|
|
33311
|
+
iter.init(path);
|
|
33312
|
+
while (iter.hasNext()) {
|
|
33313
|
+
x = iter.x * t.mx + t.bx;
|
|
33314
|
+
y = iter.y * t.my + t.by;
|
|
33315
|
+
if (prev && prev[0] == x && prev[1] == y) continue;
|
|
33316
|
+
prev = [x, y];
|
|
33317
|
+
coords.push(prev);
|
|
33318
|
+
}
|
|
33319
|
+
return coords;
|
|
33320
|
+
}
|
|
33321
|
+
|
|
33322
|
+
function traceCoords(coords, ctx) {
|
|
33323
|
+
ctx.moveTo(coords[0][0], coords[0][1]);
|
|
33324
|
+
for (var i=1; i<coords.length; i++) {
|
|
33325
|
+
ctx.lineTo(coords[i][0], coords[i][1]);
|
|
33326
|
+
}
|
|
33327
|
+
}
|
|
33328
|
+
|
|
33329
|
+
// Solid heads and dots are filled with the line's colour; open ones are stroked with
|
|
33330
|
+
// its width, always whole and round-cornered, whatever the line's dashes and
|
|
33331
|
+
// caps.
|
|
33332
|
+
function drawArrowHeads(heads, ctx, style, width) {
|
|
33333
|
+
var alpha = (style.opacity >= 0 ? style.opacity : 1) *
|
|
33334
|
+
(style.strokeOpacity >= 0 ? style.strokeOpacity : 1);
|
|
33335
|
+
var head, p;
|
|
33336
|
+
if (heads.length === 0) return;
|
|
33337
|
+
ctx.globalAlpha = alpha;
|
|
33338
|
+
ctx.fillStyle = style.strokeColor;
|
|
33339
|
+
ctx.strokeStyle = style.strokeColor;
|
|
33340
|
+
ctx.lineWidth = width;
|
|
33341
|
+
ctx.lineCap = 'round';
|
|
33342
|
+
ctx.lineJoin = 'round';
|
|
33343
|
+
ctx.setLineDash([]);
|
|
33344
|
+
for (var i=0; i<heads.length; i++) {
|
|
33345
|
+
head = heads[i];
|
|
33346
|
+
ctx.beginPath();
|
|
33347
|
+
if (head.type == 'dot') {
|
|
33348
|
+
ctx.arc(head.center[0], head.center[1], head.radius, 0, Math.PI * 2);
|
|
33349
|
+
ctx.fill();
|
|
33350
|
+
continue;
|
|
33351
|
+
}
|
|
33352
|
+
p = head.points;
|
|
33353
|
+
ctx.moveTo(p[0][0], p[0][1]);
|
|
33354
|
+
ctx.lineTo(p[1][0], p[1][1]);
|
|
33355
|
+
ctx.lineTo(p[2][0], p[2][1]);
|
|
33356
|
+
if (head.type == 'arrow') {
|
|
33357
|
+
ctx.closePath();
|
|
33358
|
+
ctx.fill();
|
|
33359
|
+
} else {
|
|
33360
|
+
ctx.stroke();
|
|
33361
|
+
}
|
|
33362
|
+
}
|
|
33363
|
+
ctx.globalAlpha = 1;
|
|
33364
|
+
}
|
|
33365
|
+
|
|
32161
33366
|
function protectIterForDrawing(iter, ext) {
|
|
32162
33367
|
var bounds, k;
|
|
32163
33368
|
if (ext.scale() > 100) {
|
|
@@ -35400,8 +36605,9 @@
|
|
|
35400
36605
|
var appearance = makePanelSection(form, 'Appearance');
|
|
35401
36606
|
backgroundControl = makeColorRow(appearance, {
|
|
35402
36607
|
label: 'Background',
|
|
36608
|
+
// An emptied colour field unsets the colour.
|
|
35403
36609
|
onColor: function(color) {
|
|
35404
|
-
|
|
36610
|
+
applyFrameStyle([['fill', color]]);
|
|
35405
36611
|
},
|
|
35406
36612
|
onOpacity: function(value) {
|
|
35407
36613
|
applyFrameStyle([['fill-opacity', value]]);
|
|
@@ -35411,9 +36617,8 @@
|
|
|
35411
36617
|
neatlineControl = makeColorRow(appearance, {
|
|
35412
36618
|
label: 'Neatline',
|
|
35413
36619
|
onColor: function(color) {
|
|
35414
|
-
if (!color) return;
|
|
35415
36620
|
var styles = [['stroke', color]];
|
|
35416
|
-
if (!getStyleValue('stroke-width')) styles.push(['stroke-width', 1]);
|
|
36621
|
+
if (color && !getStyleValue('stroke-width')) styles.push(['stroke-width', 1]);
|
|
35417
36622
|
applyFrameStyle(styles);
|
|
35418
36623
|
},
|
|
35419
36624
|
onOpacity: function(value) {
|
|
@@ -35770,13 +36975,13 @@
|
|
|
35770
36975
|
|
|
35771
36976
|
// Font and face menus, as in the label panel
|
|
35772
36977
|
row = makeRow(appearance);
|
|
35773
|
-
fontSelect = El('select').attr('
|
|
36978
|
+
fontSelect = El('select').attr('aria-label', 'Font').addClass('scalebar-font-select')
|
|
35774
36979
|
.appendTo(makeCell(row, 'Font'))
|
|
35775
36980
|
.on('change', function() {
|
|
35776
36981
|
if (fontSelect.node().value) applyFont(fontSelect.node().value);
|
|
35777
36982
|
});
|
|
35778
36983
|
renderFontOptions();
|
|
35779
|
-
fontStyleSelect = El('select').attr('
|
|
36984
|
+
fontStyleSelect = El('select').attr('aria-label', 'Font style')
|
|
35780
36985
|
.addClass('scalebar-font-style-select')
|
|
35781
36986
|
.appendTo(makeCell(row, 'Font style'))
|
|
35782
36987
|
.on('change', function() {
|
|
@@ -35790,10 +36995,13 @@
|
|
|
35790
36995
|
El('span').appendTo(colorCell).text('Color');
|
|
35791
36996
|
colorChit = El('div').addClass('label-color-chit').attr('role', 'button')
|
|
35792
36997
|
.on('click', function() { colorPicker.toggle(); });
|
|
35793
|
-
colorInput = El('input').attr('type', 'text').attr('
|
|
36998
|
+
colorInput = El('input').attr('type', 'text').attr('aria-label', 'Scale bar color')
|
|
35794
36999
|
.on('change', function() {
|
|
35795
37000
|
var color = colorInput.node().value.trim();
|
|
35796
|
-
|
|
37001
|
+
var d = getScalebarRecord();
|
|
37002
|
+
// An emptied field unsets the colour, so the scalebar is drawn in
|
|
37003
|
+
// the default one.
|
|
37004
|
+
if (!color && !(d && d.color)) {
|
|
35797
37005
|
updateControls();
|
|
35798
37006
|
return;
|
|
35799
37007
|
}
|
|
@@ -35816,7 +37024,7 @@
|
|
|
35816
37024
|
'scalebar-bar-width');
|
|
35817
37025
|
marginInput = makeNumberInput(makeCell(row, 'Inset'), 'margin',
|
|
35818
37026
|
'scalebar-margin')
|
|
35819
|
-
.attr('
|
|
37027
|
+
.attr('aria-label', 'Distance from the edges of the frame, in pixels');
|
|
35820
37028
|
|
|
35821
37029
|
row = El('div').addClass('label-style-row label-panel-button-row').appendTo(form);
|
|
35822
37030
|
makePanelActionButton(row, 'Remove scale bar', function() {
|
|
@@ -37117,6 +38325,25 @@
|
|
|
37117
38325
|
})
|
|
37118
38326
|
};
|
|
37119
38327
|
},
|
|
38328
|
+
// The vertices of each part of each feature in a path layer, in pixels
|
|
38329
|
+
// from the top left of the map, for a test comparing a drawn path with the
|
|
38330
|
+
// pointer positions that drew it.
|
|
38331
|
+
getLayerPathPixels: function(name) {
|
|
38332
|
+
var o = gui.model.getLayers().filter(function(o) {
|
|
38333
|
+
return getLayerName(o.layer) === name;
|
|
38334
|
+
})[0];
|
|
38335
|
+
var ext = gui.map.getExtent();
|
|
38336
|
+
var arcs;
|
|
38337
|
+
if (!o) return null;
|
|
38338
|
+
arcs = o.layer.gui ? o.layer.gui.displayArcs : o.dataset.arcs;
|
|
38339
|
+
return (o.layer.shapes || []).map(function(shp) {
|
|
38340
|
+
return shp ? shp.map(function(ids) {
|
|
38341
|
+
var iter = arcs.getShapeIter(ids), pts = [];
|
|
38342
|
+
while (iter.hasNext()) pts.push(ext.translateCoords(iter.x, iter.y));
|
|
38343
|
+
return pts;
|
|
38344
|
+
}) : null;
|
|
38345
|
+
});
|
|
38346
|
+
},
|
|
37120
38347
|
selectLayer: function(name) {
|
|
37121
38348
|
var target = gui.model.getLayers().filter(function(o) {
|
|
37122
38349
|
return getLayerName(o.layer) === name;
|
|
@@ -37134,6 +38361,19 @@
|
|
|
37134
38361
|
};
|
|
37135
38362
|
});
|
|
37136
38363
|
},
|
|
38364
|
+
// What each overlay layer last drew: the ids of its features, and whether
|
|
38365
|
+
// it marked their vertices. The overlay is drawn to canvas, so there is no
|
|
38366
|
+
// DOM state to assert on.
|
|
38367
|
+
getOverlayInfo: function() {
|
|
38368
|
+
var layers = gui.map && gui.map.getOverlayLayers ?
|
|
38369
|
+
gui.map.getOverlayLayers() : [];
|
|
38370
|
+
return layers.map(function(lyr) {
|
|
38371
|
+
return {
|
|
38372
|
+
ids: lyr.gui.style.ids || null,
|
|
38373
|
+
vertices: !!lyr.gui.style.vertices
|
|
38374
|
+
};
|
|
38375
|
+
});
|
|
38376
|
+
},
|
|
37137
38377
|
// Knot handle positions, in display coordinates.
|
|
37138
38378
|
getLabelPathKnotCoords: function() {
|
|
37139
38379
|
var lyr = getLabelPathGuideLayers(gui).filter(function(lyr) {
|