mapshaper 0.7.70 → 0.7.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mapshaper.js +785 -111
- package/package.json +1 -1
- package/www/mapshaper-gui.js +803 -137
- package/www/mapshaper.js +785 -111
- package/www/page.css +20 -0
package/www/mapshaper-gui.js
CHANGED
|
@@ -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) {
|
|
@@ -15998,7 +15979,7 @@
|
|
|
15998
15979
|
var tile = El('div')
|
|
15999
15980
|
.addClass('label-color-preset')
|
|
16000
15981
|
.attr('role', 'button')
|
|
16001
|
-
.attr('
|
|
15982
|
+
.attr('aria-label', color)
|
|
16002
15983
|
.appendTo(rowEl)
|
|
16003
15984
|
.css('background-color', color);
|
|
16004
15985
|
if (row.length == 16 && i == 8) tile.addClass('label-color-preset-group-start');
|
|
@@ -16475,7 +16456,7 @@
|
|
|
16475
16456
|
// in a menu that is only on screen while it is being used, and a
|
|
16476
16457
|
// destructive action is better for waiting until intent is shown.
|
|
16477
16458
|
El('div').addClass('label-saved-style-delete').attr('role', 'button')
|
|
16478
|
-
.attr('
|
|
16459
|
+
.attr('aria-label', 'Delete this ' + opts.styleLabel)
|
|
16479
16460
|
.appendTo(itemEl).html('×')
|
|
16480
16461
|
.on('click', function() {
|
|
16481
16462
|
deleteStyle(item);
|
|
@@ -16548,7 +16529,7 @@
|
|
|
16548
16529
|
// Escape. Whether that means giving up the keyboard is the
|
|
16549
16530
|
// caller's question: the field cannot know what else wants it.
|
|
16550
16531
|
// decimals how finely a typed size is kept (default 1)
|
|
16551
|
-
// title
|
|
16532
|
+
// title the field's accessible name (not a tooltip)
|
|
16552
16533
|
function SizeField(parent, opts) {
|
|
16553
16534
|
var o = Object.assign({min: 1, max: 999, step: 1, bigStep: 10}, opts || {});
|
|
16554
16535
|
var box = El('div').addClass('size-field').appendTo(parent);
|
|
@@ -16561,7 +16542,7 @@
|
|
|
16561
16542
|
var dirty = false; // holds typing that has not been committed
|
|
16562
16543
|
var disabled = false;
|
|
16563
16544
|
|
|
16564
|
-
if (o.title) input.attr('
|
|
16545
|
+
if (o.title) input.attr('aria-label', o.title);
|
|
16565
16546
|
|
|
16566
16547
|
// While the caret is in this field the keyboard belongs to it. Without this
|
|
16567
16548
|
// the GUI's own handlers see the keystrokes: Escape would disarm the tool,
|
|
@@ -16865,14 +16846,15 @@
|
|
|
16865
16846
|
// the one thing the third state exists to avoid saying.
|
|
16866
16847
|
//
|
|
16867
16848
|
// opts.onChange(on) the switch was clicked
|
|
16868
|
-
// opts.title
|
|
16849
|
+
// opts.title the switch's accessible name (not a tooltip)
|
|
16850
|
+
// opts.className
|
|
16869
16851
|
function makePanelToggle(parent, opts) {
|
|
16870
16852
|
var track = El('div').addClass('label-toggle').attr('role', 'checkbox').appendTo(parent);
|
|
16871
16853
|
var state = 'off';
|
|
16872
16854
|
if (opts.className) track.addClass(opts.className);
|
|
16873
16855
|
var disabled = false;
|
|
16874
16856
|
El('div').addClass('label-toggle-knob').appendTo(track);
|
|
16875
|
-
if (opts.title) track.attr('
|
|
16857
|
+
if (opts.title) track.attr('aria-label', opts.title);
|
|
16876
16858
|
// A click on a mixed switch turns everything on. It is the convention, and
|
|
16877
16859
|
// it is the reading that reaches a state the switch can describe: the next
|
|
16878
16860
|
// click then turns everything off, so both are one click away.
|
|
@@ -16957,7 +16939,7 @@
|
|
|
16957
16939
|
// opacity qualifies the colour, and one border says so where two fields side
|
|
16958
16940
|
// by side said they were separate settings. It also leaves the narrow column
|
|
16959
16941
|
// of the row free for a field that needs it. The opacity has no caption; its
|
|
16960
|
-
// percent sign
|
|
16942
|
+
// percent sign says what it is.
|
|
16961
16943
|
//
|
|
16962
16944
|
// Returns the field's box; the opacity input is made by makeOpacityInput().
|
|
16963
16945
|
function makeColorOpacityField(parent, chit, input, opacityOpts) {
|
|
@@ -17006,7 +16988,7 @@
|
|
|
17006
16988
|
control.picker.toggle();
|
|
17007
16989
|
});
|
|
17008
16990
|
control.input = El('input').attr('type', 'text')
|
|
17009
|
-
.attr('
|
|
16991
|
+
.attr('aria-label', opts.label + ' color')
|
|
17010
16992
|
.on('change', function() {
|
|
17011
16993
|
var color = control.input.node().value.trim();
|
|
17012
16994
|
if (isHexColor(color)) control.picker.setColor(color);
|
|
@@ -17042,7 +17024,7 @@
|
|
|
17042
17024
|
// opts.revert() it was not, so put back what the field was showing
|
|
17043
17025
|
function makeOpacityInput(parent, opts) {
|
|
17044
17026
|
var input = El('input').attr('type', 'text').addClass('label-opacity-input')
|
|
17045
|
-
.attr('
|
|
17027
|
+
.attr('aria-label', 'Opacity, 0-100%')
|
|
17046
17028
|
.appendTo(parent)
|
|
17047
17029
|
.on('change', function() {
|
|
17048
17030
|
var val = parseOpacityValue(input.node().value);
|
|
@@ -17573,6 +17555,80 @@
|
|
|
17573
17555
|
return wrapper;
|
|
17574
17556
|
}
|
|
17575
17557
|
|
|
17558
|
+
// Default sizes and colours of the label tool's anchor symbols, which differ by
|
|
17559
|
+
// shape: a star or a ring at the size of a circle reads as smaller, and the NYT
|
|
17560
|
+
// star is a logo, drawn in its own red.
|
|
17561
|
+
var iconDefaults = {
|
|
17562
|
+
circle: {size: 5},
|
|
17563
|
+
square: {size: 5},
|
|
17564
|
+
star: {size: 8},
|
|
17565
|
+
ring: {size: 8},
|
|
17566
|
+
'nyt-star': {size: 14, color: '#cc0000'}
|
|
17567
|
+
};
|
|
17568
|
+
var fallbackSize = 5;
|
|
17569
|
+
|
|
17570
|
+
function getDefaultIconSize(shape) {
|
|
17571
|
+
var o = iconDefaults[shape];
|
|
17572
|
+
return o ? o.size : fallbackSize;
|
|
17573
|
+
}
|
|
17574
|
+
|
|
17575
|
+
// '' for a shape drawn in whatever colour the label gives it.
|
|
17576
|
+
function getDefaultIconColor(shape) {
|
|
17577
|
+
var o = iconDefaults[shape];
|
|
17578
|
+
return o && o.color || '';
|
|
17579
|
+
}
|
|
17580
|
+
|
|
17581
|
+
// The icon-size and icon-color to write when labels whose symbol is @prevShape
|
|
17582
|
+
// ('' for none, or for a selection of mixed shapes) are given @shape, from the
|
|
17583
|
+
// @size and @color they share. A missing size is '' or 0, and a colour of ''
|
|
17584
|
+
// is none; null is a colour the labels do not agree on, which is left alone.
|
|
17585
|
+
//
|
|
17586
|
+
// A value that is the old shape's default follows the shape to its own
|
|
17587
|
+
// default, and a value the user chose is kept -- so switching shapes resizes a
|
|
17588
|
+
// symbol nobody sized, and never undoes a size somebody set. The colour is
|
|
17589
|
+
// returned only if it changes, as '' to remove it.
|
|
17590
|
+
function getIconShapeChange(shape, prevShape, size, color) {
|
|
17591
|
+
var out = {};
|
|
17592
|
+
var sizeVal = Number(size);
|
|
17593
|
+
var prevColor = getDefaultIconColor(prevShape);
|
|
17594
|
+
var nextColor = getDefaultIconColor(shape);
|
|
17595
|
+
if (!(sizeVal > 0) || prevShape && sizeVal == getDefaultIconSize(prevShape)) {
|
|
17596
|
+
out.size = getDefaultIconSize(shape);
|
|
17597
|
+
} else {
|
|
17598
|
+
out.size = sizeVal;
|
|
17599
|
+
}
|
|
17600
|
+
if (color !== null && sameColor$1(color, prevColor) && !sameColor$1(color, nextColor)) {
|
|
17601
|
+
out.color = nextColor;
|
|
17602
|
+
}
|
|
17603
|
+
return out;
|
|
17604
|
+
}
|
|
17605
|
+
|
|
17606
|
+
function sameColor$1(a, b) {
|
|
17607
|
+
return String(a || '').toLowerCase() == String(b || '').toLowerCase();
|
|
17608
|
+
}
|
|
17609
|
+
|
|
17610
|
+
// Features offered only to people at The New York Times, who are the ones with
|
|
17611
|
+
// its NYTFranklin typeface installed. This is a convenience, not access
|
|
17612
|
+
// control: anything gated here must still render for everyone, because a map
|
|
17613
|
+
// made with it can be opened anywhere.
|
|
17614
|
+
//
|
|
17615
|
+
// ?nyt=on or ?nyt=off in the page URL overrides the check, so that the gated
|
|
17616
|
+
// features can be tested (and hidden) on any machine.
|
|
17617
|
+
var NYT_FONT = 'NYTFranklin';
|
|
17618
|
+
var cached;
|
|
17619
|
+
|
|
17620
|
+
function isNytUser() {
|
|
17621
|
+
if (cached === undefined) cached = detectNytUser();
|
|
17622
|
+
return cached;
|
|
17623
|
+
}
|
|
17624
|
+
|
|
17625
|
+
function detectNytUser() {
|
|
17626
|
+
var flag = GUI.getUrlVars().nyt;
|
|
17627
|
+
if (flag == 'on' || flag === true) return true;
|
|
17628
|
+
if (flag == 'off' || flag === false) return false;
|
|
17629
|
+
return isInstalledFont(NYT_FONT);
|
|
17630
|
+
}
|
|
17631
|
+
|
|
17576
17632
|
var fontField = 'font-family';
|
|
17577
17633
|
var fontSizeField = 'font-size';
|
|
17578
17634
|
var fontStyleField = 'font-style';
|
|
@@ -17605,7 +17661,6 @@
|
|
|
17605
17661
|
var defaultFontWeight = '400';
|
|
17606
17662
|
var defaultLabelColor = '#000000';
|
|
17607
17663
|
var defaultIconColor = '#000000';
|
|
17608
|
-
var defaultIconSize = 5;
|
|
17609
17664
|
// The width a halo is switched on at: past the edge of the glyphs, so a
|
|
17610
17665
|
// stroke of twice this.
|
|
17611
17666
|
var defaultHaloWidth = 2;
|
|
@@ -17678,6 +17733,10 @@
|
|
|
17678
17733
|
name: 'star'
|
|
17679
17734
|
}, {
|
|
17680
17735
|
name: 'ring'
|
|
17736
|
+
}, {
|
|
17737
|
+
name: 'nyt-star',
|
|
17738
|
+
title: 'NYT star',
|
|
17739
|
+
nytOnly: true
|
|
17681
17740
|
}];
|
|
17682
17741
|
var defaultIconShape = 'circle';
|
|
17683
17742
|
// Alignment writes label-align rather than text-anchor, because text-anchor
|
|
@@ -17743,14 +17802,15 @@
|
|
|
17743
17802
|
elbow: '<path d="M3.5 12.5L8 4.5H13"></path>',
|
|
17744
17803
|
curve: '<path d="M3.5 12.5Q4.5 4.5 12.5 4"></path>',
|
|
17745
17804
|
none: '<path d="M3 8H13"></path>',
|
|
17746
|
-
arrow: '<path d="M6.
|
|
17805
|
+
arrow: '<path d="M6.84 8H13"></path><path class="fill" d="M2.5 8L8.7 6.05V9.95Z"></path>',
|
|
17747
17806
|
'open-arrow': '<path d="M3.2 8H13"></path><path d="M7.38 5.07L3.2 8L7.38 10.93"></path>'
|
|
17748
17807
|
};
|
|
17749
17808
|
var iconButtonSymbols = {
|
|
17750
17809
|
circle: '<circle cx="8" cy="8" r="4.25"></circle>',
|
|
17751
17810
|
square: '<rect x="4" y="4" width="8" height="8"></rect>',
|
|
17752
17811
|
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>'
|
|
17812
|
+
star: '<path d="M8 3.2l1.18 2.92 3.14.22-2.42 2 .76 3.06L8 9.75 5.34 11.4l.76-3.06-2.42-2 3.14-.22L8 3.2z"></path>',
|
|
17813
|
+
'nyt-star': '<polygon transform="scale(0.4)" points="19.98 5.72 23.15 11.76 29.45 9.15 27.99 15.82 34.5 17.86 29.1 22.03 32.76 27.78 25.95 27.5 25.06 34.27 20.01 29.67 14.98 34.28 14.08 27.52 7.26 27.82 10.92 22.06 5.5 17.9 12 15.84 10.53 9.18 16.84 11.77"></polygon>'
|
|
17754
17814
|
};
|
|
17755
17815
|
|
|
17756
17816
|
function LabelTool(gui) {
|
|
@@ -17765,6 +17825,7 @@
|
|
|
17765
17825
|
// The shape the toggle turns back on, so that switching a symbol off and on
|
|
17766
17826
|
// again does not silently change a star into a circle.
|
|
17767
17827
|
var lastIconShape = defaultIconShape;
|
|
17828
|
+
var shownIconTypes = null;
|
|
17768
17829
|
// Likewise the halo's width, which is what switching one off removes.
|
|
17769
17830
|
var lastHaloWidth = defaultHaloWidth;
|
|
17770
17831
|
// And the callout's shape.
|
|
@@ -17898,7 +17959,7 @@
|
|
|
17898
17959
|
// number otherwise keep theirs, and so does a colour field, because the
|
|
17899
17960
|
// field beside it has one and "Color" says the percentage is its opacity.
|
|
17900
17961
|
var fontRow = El('div').addClass('label-style-row').appendTo(textSection);
|
|
17901
|
-
fontSelect = El('select').attr('
|
|
17962
|
+
fontSelect = El('select').attr('aria-label', 'Font').appendTo(fontRow).on('change', function() {
|
|
17902
17963
|
if (fontSelect.node().value) {
|
|
17903
17964
|
applyFont(fontSelect.node().value);
|
|
17904
17965
|
}
|
|
@@ -17906,7 +17967,7 @@
|
|
|
17906
17967
|
|
|
17907
17968
|
var styleSizeRow = El('div').addClass('label-style-row label-split-row').appendTo(textSection);
|
|
17908
17969
|
var fontStyleRow = El('div').addClass('label-split-cell label-font-style-row').appendTo(styleSizeRow);
|
|
17909
|
-
fontStyleSelect = El('select').attr('
|
|
17970
|
+
fontStyleSelect = El('select').attr('aria-label', 'Font style').appendTo(fontStyleRow).on('change', function() {
|
|
17910
17971
|
if (fontStyleSelect.node().value) {
|
|
17911
17972
|
applyFontStyleVariant(fontStyleSelect.node().value);
|
|
17912
17973
|
}
|
|
@@ -17932,7 +17993,7 @@
|
|
|
17932
17993
|
var textColorCell = El('div').addClass('label-split-cell label-color-row label-text-color-row').appendTo(colorRow);
|
|
17933
17994
|
El('span').appendTo(textColorCell).text('Color');
|
|
17934
17995
|
colorChit = El('div').addClass('label-color-chit').attr('role', 'button');
|
|
17935
|
-
colorInput = El('input').attr('type', 'text').attr('
|
|
17996
|
+
colorInput = El('input').attr('type', 'text').attr('aria-label', 'Text color');
|
|
17936
17997
|
var textColorField = addColorOpacityField(textColorCell, colorChit, colorInput, applyLabelOpacity);
|
|
17937
17998
|
colorFieldBox = textColorField.box;
|
|
17938
17999
|
opacityInput = textColorField.opacity;
|
|
@@ -17965,7 +18026,7 @@
|
|
|
17965
18026
|
applyLabelAlign(item.name);
|
|
17966
18027
|
})
|
|
17967
18028
|
.attr('data-align', item.name)
|
|
17968
|
-
.attr('
|
|
18029
|
+
.attr('aria-label', item.title);
|
|
17969
18030
|
alignBtns[item.name] = btn;
|
|
17970
18031
|
appendAlignButtonSymbol(btn, item.name);
|
|
17971
18032
|
});
|
|
@@ -18004,7 +18065,7 @@
|
|
|
18004
18065
|
var haloColorCell = El('div').addClass('label-split-cell label-color-row label-halo-color-row').appendTo(haloColorRow);
|
|
18005
18066
|
El('span').appendTo(haloColorCell).text('Color');
|
|
18006
18067
|
haloColorChit = El('div').addClass('label-color-chit').attr('role', 'button');
|
|
18007
|
-
haloColorInput = El('input').attr('type', 'text').attr('
|
|
18068
|
+
haloColorInput = El('input').attr('type', 'text').attr('aria-label', 'Halo color');
|
|
18008
18069
|
var haloColorField = addColorOpacityField(haloColorCell, haloColorChit, haloColorInput, applyHaloOpacity);
|
|
18009
18070
|
haloColorFieldBox = haloColorField.box;
|
|
18010
18071
|
haloOpacityInput = haloColorField.opacity;
|
|
@@ -18055,12 +18116,12 @@
|
|
|
18055
18116
|
var iconRow = El('div').addClass('label-style-row label-icon-shapes-row').appendTo(iconSection);
|
|
18056
18117
|
var iconGroup = iconGroupEl = El('div').addClass('label-btn-group label-icon-buttons').appendTo(iconRow);
|
|
18057
18118
|
iconBtns = {};
|
|
18058
|
-
|
|
18119
|
+
getShownIconTypes().forEach(function(icon) {
|
|
18059
18120
|
var btn = makePanelButton(iconGroup, '', function() {
|
|
18060
18121
|
applyIcon(icon.name);
|
|
18061
18122
|
})
|
|
18062
18123
|
.attr('data-icon', icon.name)
|
|
18063
|
-
.attr('
|
|
18124
|
+
.attr('aria-label', icon.title || icon.name);
|
|
18064
18125
|
iconBtns[icon.name] = btn;
|
|
18065
18126
|
appendIconButtonSymbol(btn, icon.name);
|
|
18066
18127
|
});
|
|
@@ -18069,7 +18130,7 @@
|
|
|
18069
18130
|
var iconColorCell = El('div').addClass('label-split-cell label-color-row label-icon-color-row').appendTo(iconColorRow);
|
|
18070
18131
|
El('span').appendTo(iconColorCell).text('Color');
|
|
18071
18132
|
iconColorChit = El('div').addClass('label-color-chit').attr('role', 'button');
|
|
18072
|
-
iconColorInput = El('input').attr('type', 'text').attr('
|
|
18133
|
+
iconColorInput = El('input').attr('type', 'text').attr('aria-label', 'Symbol color');
|
|
18073
18134
|
var iconColorField = addColorOpacityField(iconColorCell, iconColorChit, iconColorInput, applyIconOpacity);
|
|
18074
18135
|
iconColorFieldBox = iconColorField.box;
|
|
18075
18136
|
iconOpacityInput = iconColorField.opacity;
|
|
@@ -18117,7 +18178,7 @@
|
|
|
18117
18178
|
applyLabelPosition(pos);
|
|
18118
18179
|
})
|
|
18119
18180
|
.attr('data-position', pos)
|
|
18120
|
-
.attr('
|
|
18181
|
+
.attr('aria-label', pos);
|
|
18121
18182
|
});
|
|
18122
18183
|
|
|
18123
18184
|
|
|
@@ -18127,7 +18188,7 @@
|
|
|
18127
18188
|
setLabelPositionMode(gui, item.name, getPositionModeKind());
|
|
18128
18189
|
})
|
|
18129
18190
|
.attr('data-drag-mode', item.name)
|
|
18130
|
-
.attr('
|
|
18191
|
+
.attr('aria-label', item.title);
|
|
18131
18192
|
});
|
|
18132
18193
|
|
|
18133
18194
|
presetControl = new StylePresetControl(panel, {
|
|
@@ -18212,7 +18273,7 @@
|
|
|
18212
18273
|
var colorCell = El('div').addClass('label-split-cell label-color-row label-callout-color-row').appendTo(colorRow);
|
|
18213
18274
|
El('span').appendTo(colorCell).text('Color');
|
|
18214
18275
|
calloutColorChit = El('div').addClass('label-color-chit').attr('role', 'button');
|
|
18215
|
-
calloutColorInput = El('input').attr('type', 'text').attr('
|
|
18276
|
+
calloutColorInput = El('input').attr('type', 'text').attr('aria-label', 'Callout color');
|
|
18216
18277
|
var calloutColorField = addColorOpacityField(colorCell, calloutColorChit, calloutColorInput, applyCalloutOpacity);
|
|
18217
18278
|
calloutColorFieldBox = calloutColorField.box;
|
|
18218
18279
|
calloutOpacityInput = calloutColorField.opacity;
|
|
@@ -18236,7 +18297,7 @@
|
|
|
18236
18297
|
var gapCell = El('div').addClass('label-split-cell label-spacing-row label-callout-gap-row').appendTo(colorRow);
|
|
18237
18298
|
El('span').appendTo(gapCell).text('Gap');
|
|
18238
18299
|
calloutGapInput = El('input').attr('type', 'text').addClass('label-measure-input')
|
|
18239
|
-
.attr('
|
|
18300
|
+
.attr('aria-label', 'Space between the callout and the anchor, in px')
|
|
18240
18301
|
.attr('placeholder', 'auto')
|
|
18241
18302
|
.attr('data-placeholder', 'auto')
|
|
18242
18303
|
.appendTo(gapCell)
|
|
@@ -18248,7 +18309,7 @@
|
|
|
18248
18309
|
function makeCalloutButton(parent, item, action) {
|
|
18249
18310
|
var btn = makePanelButton(parent, '', action)
|
|
18250
18311
|
.attr('data-callout', item.name)
|
|
18251
|
-
.attr('
|
|
18312
|
+
.attr('aria-label', item.title);
|
|
18252
18313
|
El('<svg class="label-callout-symbol" viewBox="0 0 16 16" aria-hidden="true">' +
|
|
18253
18314
|
calloutButtonSymbols[item.name] + '</svg>').appendTo(btn);
|
|
18254
18315
|
return btn;
|
|
@@ -18475,8 +18536,10 @@
|
|
|
18475
18536
|
// rest of the selection has nothing to compare.
|
|
18476
18537
|
var iconIds = getIconValueIds();
|
|
18477
18538
|
var iconVal = getCommonValue(iconIds, iconField);
|
|
18478
|
-
var
|
|
18479
|
-
var
|
|
18539
|
+
var iconShape = iconVal || lastIconShape;
|
|
18540
|
+
var iconSize = getShownValue(iconIds, iconSizeField, {useDefault: true, defaultValue: getDefaultIconSize(iconShape)});
|
|
18541
|
+
var iconColor = getShownValue(iconIds, iconColorField, {useDefault: true,
|
|
18542
|
+
defaultValue: getDefaultIconColor(iconShape) || defaultIconColor});
|
|
18480
18543
|
var iconOpacity = getShownValue(iconIds, iconOpacityField, {useDefault: true, defaultValue: 1});
|
|
18481
18544
|
var haloIds = getHaloValueIds();
|
|
18482
18545
|
var haloWidth = getShownValue(haloIds, haloWidthField, {useDefault: true, defaultValue: lastHaloWidth});
|
|
@@ -18821,7 +18884,7 @@
|
|
|
18821
18884
|
// border the buttons share fades with them -- a live border around dead
|
|
18822
18885
|
// buttons is the one part of a disabled control that still looks usable.
|
|
18823
18886
|
iconGroupEl.classed('disabled', off);
|
|
18824
|
-
|
|
18887
|
+
getShownIconTypes().forEach(function(icon) {
|
|
18825
18888
|
iconBtns[icon.name].classed('selected', !off && icon.name == iconVal);
|
|
18826
18889
|
setPanelButtonDisabled(iconBtns[icon.name], off);
|
|
18827
18890
|
});
|
|
@@ -19420,12 +19483,23 @@
|
|
|
19420
19483
|
|
|
19421
19484
|
function getSelectedIcon() {
|
|
19422
19485
|
var out = '';
|
|
19423
|
-
|
|
19486
|
+
getShownIconTypes().forEach(function(icon) {
|
|
19424
19487
|
if (iconBtns[icon.name].hasClass('selected')) out = icon.name;
|
|
19425
19488
|
});
|
|
19426
19489
|
return out;
|
|
19427
19490
|
}
|
|
19428
19491
|
|
|
19492
|
+
// A label that already has an NYT star keeps it without the button: it is
|
|
19493
|
+
// drawn for everyone, and only choosing one is gated.
|
|
19494
|
+
function getShownIconTypes() {
|
|
19495
|
+
if (!shownIconTypes) {
|
|
19496
|
+
shownIconTypes = iconTypes.filter(function(icon) {
|
|
19497
|
+
return !icon.nytOnly || isNytUser();
|
|
19498
|
+
});
|
|
19499
|
+
}
|
|
19500
|
+
return shownIconTypes;
|
|
19501
|
+
}
|
|
19502
|
+
|
|
19429
19503
|
function applyStyleObject(style) {
|
|
19430
19504
|
var styles = [];
|
|
19431
19505
|
savedStyleFields.forEach(function(field) {
|
|
@@ -19456,8 +19530,9 @@
|
|
|
19456
19530
|
// The size the symbols in the selection already share, where they share
|
|
19457
19531
|
// one: a shape applied to a mixed selection gives the labels that had no
|
|
19458
19532
|
// symbol the size of the ones that did, rather than resetting them all to
|
|
19459
|
-
// the default.
|
|
19460
|
-
|
|
19533
|
+
// the default. A size or colour that was the old shape's default becomes
|
|
19534
|
+
// the new one's (see getIconShapeChange()).
|
|
19535
|
+
addIconShapeDefaults(styles, iconName, getIconValueIds());
|
|
19461
19536
|
// The symbol's own opacity goes on with it, because the label's opacity
|
|
19462
19537
|
// is applied to both elements: without this, text set to 50% would give
|
|
19463
19538
|
// a half-faded symbol while the Icon section showed it at 100%.
|
|
@@ -19469,6 +19544,14 @@
|
|
|
19469
19544
|
applyStyleValues(styles);
|
|
19470
19545
|
}
|
|
19471
19546
|
|
|
19547
|
+
function addIconShapeDefaults(styles, iconName, ids) {
|
|
19548
|
+
var color = getCommonValueInfo(ids, iconColorField);
|
|
19549
|
+
var change = getIconShapeChange(iconName, getCommonValue(ids, iconField) || '',
|
|
19550
|
+
getCommonValue(ids, iconSizeField), color.mixed ? null : color.value || '');
|
|
19551
|
+
styles.push([iconSizeField, change.size]);
|
|
19552
|
+
if ('color' in change) styles.push([iconColorField, change.color]);
|
|
19553
|
+
}
|
|
19554
|
+
|
|
19472
19555
|
// Switching a symbol on moves a label sitting at the centre out from under
|
|
19473
19556
|
// it, in the same command, so that the pair is one undo step and the text
|
|
19474
19557
|
// is never briefly drawn over the symbol it just asked for.
|
|
@@ -19509,7 +19592,9 @@
|
|
|
19509
19592
|
}
|
|
19510
19593
|
|
|
19511
19594
|
function nudgeIconSize(delta) {
|
|
19512
|
-
var
|
|
19595
|
+
var ids = getIconTargetIds();
|
|
19596
|
+
var size = getNumericSize(ids, iconSizeField,
|
|
19597
|
+
getDefaultIconSize(getCommonValue(ids, iconField)));
|
|
19513
19598
|
if (!controlsEnabled()) return;
|
|
19514
19599
|
size = Math.max(1, size + delta);
|
|
19515
19600
|
applyIconSize(size);
|
|
@@ -20028,7 +20113,7 @@
|
|
|
20028
20113
|
onChange: setPatternOn
|
|
20029
20114
|
});
|
|
20030
20115
|
var typeRow = El('div').addClass('label-style-row layer-pattern-type-row').appendTo(section);
|
|
20031
|
-
typeSelect = El('select').attr('
|
|
20116
|
+
typeSelect = El('select').attr('aria-label', 'Pattern type').appendTo(typeRow)
|
|
20032
20117
|
.on('change', function() {
|
|
20033
20118
|
selectType(typeSelect.node().value);
|
|
20034
20119
|
});
|
|
@@ -20096,7 +20181,7 @@
|
|
|
20096
20181
|
makeFieldTip(caption,
|
|
20097
20182
|
'The fill-pattern syntax of the -style command.\n' +
|
|
20098
20183
|
'See the command reference for details.');
|
|
20099
|
-
codeInput = El('input').attr('type', 'text').attr('
|
|
20184
|
+
codeInput = El('input').attr('type', 'text').attr('aria-label', 'Pattern code').appendTo(customRow)
|
|
20100
20185
|
.on('change', function() {
|
|
20101
20186
|
applyCode(codeInput.node().value.trim());
|
|
20102
20187
|
});
|
|
@@ -20266,13 +20351,45 @@
|
|
|
20266
20351
|
}
|
|
20267
20352
|
|
|
20268
20353
|
var savedStylesKey = 'layer_style_presets';
|
|
20269
|
-
var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray', 'fill', 'fill-opacity', 'fill-pattern'];
|
|
20354
|
+
var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray', 'line-start', 'line-end', 'line-end-size', 'fill', 'fill-opacity', 'fill-pattern'];
|
|
20355
|
+
var arrowShapes = [{
|
|
20356
|
+
name: 'arrow',
|
|
20357
|
+
title: 'solid arrowheads'
|
|
20358
|
+
}, {
|
|
20359
|
+
name: 'open-arrow',
|
|
20360
|
+
title: 'open arrowheads'
|
|
20361
|
+
}, {
|
|
20362
|
+
name: 'dot',
|
|
20363
|
+
title: 'dots'
|
|
20364
|
+
}];
|
|
20365
|
+
// The arrows are the label callout's, which point left, the way a line's
|
|
20366
|
+
// start head does; the dot's left edge is where their tips are.
|
|
20367
|
+
var arrowButtonSymbols = {
|
|
20368
|
+
arrow: calloutButtonSymbols.arrow,
|
|
20369
|
+
'open-arrow': calloutButtonSymbols['open-arrow'],
|
|
20370
|
+
dot: '<path d="M5 8H13"></path><circle class="fill" cx="5" cy="8" r="2.6"></circle>'
|
|
20371
|
+
};
|
|
20372
|
+
var arrowPositions = [{
|
|
20373
|
+
name: 'start',
|
|
20374
|
+
label: 'Start',
|
|
20375
|
+
title: 'arrowhead at the start of the line'
|
|
20376
|
+
}, {
|
|
20377
|
+
name: 'end',
|
|
20378
|
+
label: 'End',
|
|
20379
|
+
title: 'arrowhead at the end of the line'
|
|
20380
|
+
}, {
|
|
20381
|
+
name: 'both',
|
|
20382
|
+
label: 'Both',
|
|
20383
|
+
title: 'arrowheads at both ends of the line'
|
|
20384
|
+
}];
|
|
20270
20385
|
|
|
20271
20386
|
function LayerStyleTool(gui) {
|
|
20272
20387
|
var parent = gui.container.findChild('.mshp-main-map');
|
|
20273
20388
|
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;
|
|
20389
|
+
var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthField, dashControl, arrowControl, randomFillBtn, presetControl, patternControl, hit;
|
|
20275
20390
|
var targetLayer = null;
|
|
20391
|
+
// What the arrowhead switch turns on, for lines that have no heads
|
|
20392
|
+
var lastArrow = {shape: 'arrow', position: 'end'};
|
|
20276
20393
|
|
|
20277
20394
|
initPanel();
|
|
20278
20395
|
hit = gui.map.getHitControl && gui.map.getHitControl();
|
|
@@ -20361,6 +20478,7 @@
|
|
|
20361
20478
|
strokeControl = addColorControl(panel, 'Stroke', 'stroke', '#000000');
|
|
20362
20479
|
strokeWidthField = addStrokeWidthControl(strokeControl.aside);
|
|
20363
20480
|
dashControl = addDashArrayControl(panel);
|
|
20481
|
+
arrowControl = addArrowControl(panel);
|
|
20364
20482
|
|
|
20365
20483
|
var buttonRow = El('div').addClass('label-style-row label-panel-button-row').appendTo(panel);
|
|
20366
20484
|
randomFillBtn = makePanelActionButton(buttonRow, 'Random fills', applyRandomFillColors);
|
|
@@ -20454,6 +20572,197 @@
|
|
|
20454
20572
|
return {row: row, input: input};
|
|
20455
20573
|
}
|
|
20456
20574
|
|
|
20575
|
+
// A switch in the heading says whether the lines have arrowheads; the rows
|
|
20576
|
+
// under it are the head's shape with its size beside it, then which ends
|
|
20577
|
+
// get it. A line has one shape for both ends, which is all the panel sets,
|
|
20578
|
+
// though -style can give the two ends different ones.
|
|
20579
|
+
function addArrowControl(parent) {
|
|
20580
|
+
var section = makePanelSection(parent, 'Arrowheads');
|
|
20581
|
+
var toggle = makePanelToggle(section.findChild('.label-style-section-title'), {
|
|
20582
|
+
title: 'Add arrowheads',
|
|
20583
|
+
className: 'layer-arrow-toggle',
|
|
20584
|
+
onChange: setArrowsOn
|
|
20585
|
+
});
|
|
20586
|
+
var shapeRow = El('div').addClass('label-style-row label-split-row').appendTo(section);
|
|
20587
|
+
var shapeCell = El('div').addClass('label-split-cell label-align-row').appendTo(shapeRow);
|
|
20588
|
+
var sizeCell = El('div').addClass('label-split-cell label-spacing-row layer-arrow-size-row').appendTo(shapeRow);
|
|
20589
|
+
var posRow = El('div').addClass('label-style-row label-split-row').appendTo(section);
|
|
20590
|
+
var posCell = El('div').addClass('label-split-cell label-align-row').appendTo(posRow);
|
|
20591
|
+
var control = {section: section, toggle: toggle, shapeBtns: {}, posBtns: {}};
|
|
20592
|
+
El('div').addClass('label-split-cell').appendTo(posRow);
|
|
20593
|
+
El('span').appendTo(shapeCell).text('Shape');
|
|
20594
|
+
var shapeGroup = El('div').addClass('label-btn-group label-callout-buttons layer-arrow-shape-buttons').appendTo(shapeCell);
|
|
20595
|
+
arrowShapes.forEach(function(item) {
|
|
20596
|
+
var btn = makePanelButton(shapeGroup, '', function() {
|
|
20597
|
+
applyArrowShape(item.name);
|
|
20598
|
+
}).attr('data-arrow-shape', item.name).attr('aria-label', item.title);
|
|
20599
|
+
El('<svg class="label-callout-symbol" viewBox="0 0 16 16" aria-hidden="true">' +
|
|
20600
|
+
arrowButtonSymbols[item.name] + '</svg>').appendTo(btn);
|
|
20601
|
+
control.shapeBtns[item.name] = btn;
|
|
20602
|
+
});
|
|
20603
|
+
El('span').appendTo(posCell).text('Ends');
|
|
20604
|
+
var posGroup = El('div').addClass('label-btn-group layer-arrow-position-buttons').appendTo(posCell);
|
|
20605
|
+
arrowPositions.forEach(function(item) {
|
|
20606
|
+
control.posBtns[item.name] = makePanelButton(posGroup, item.label, function() {
|
|
20607
|
+
applyArrowPosition(item.name);
|
|
20608
|
+
}).attr('data-arrow-position', item.name).attr('aria-label', item.title);
|
|
20609
|
+
});
|
|
20610
|
+
El('span').appendTo(sizeCell).text('Size');
|
|
20611
|
+
control.sizeField = new SizeField(sizeCell, {
|
|
20612
|
+
title: 'Arrowhead size in px, the length of its sides or the diameter of a dot',
|
|
20613
|
+
min: 1,
|
|
20614
|
+
max: 60,
|
|
20615
|
+
step: 1,
|
|
20616
|
+
bigStep: 5,
|
|
20617
|
+
onSet: applyArrowSize,
|
|
20618
|
+
onStep: nudgeArrowSize,
|
|
20619
|
+
onDone: releaseFocus
|
|
20620
|
+
});
|
|
20621
|
+
return control;
|
|
20622
|
+
}
|
|
20623
|
+
|
|
20624
|
+
// {shape, position} for one record, 'none' and '' when it has no heads. A
|
|
20625
|
+
// line whose two ends have different shapes reports the start's.
|
|
20626
|
+
function getArrowInfo(rec) {
|
|
20627
|
+
var start = internal.svg.getLineEndType(rec || {}, 'line-start');
|
|
20628
|
+
var end = internal.svg.getLineEndType(rec || {}, 'line-end');
|
|
20629
|
+
var shape = start != 'none' ? start : end;
|
|
20630
|
+
var position = start != 'none' && end != 'none' ? 'both' :
|
|
20631
|
+
start != 'none' ? 'start' : end != 'none' ? 'end' : '';
|
|
20632
|
+
return {shape: shape, position: position};
|
|
20633
|
+
}
|
|
20634
|
+
|
|
20635
|
+
// Switching on gives the lines that have no heads the last shape and ends
|
|
20636
|
+
// the section showed, so that off and on again is a round trip; the lines
|
|
20637
|
+
// that have heads keep theirs. Switching off leaves line-end-size alone,
|
|
20638
|
+
// for the same reason.
|
|
20639
|
+
function setArrowsOn(on) {
|
|
20640
|
+
applyArrowEdits(function(info) {
|
|
20641
|
+
return on ? fillArrowInfo(info) : {shape: 'none', position: ''};
|
|
20642
|
+
});
|
|
20643
|
+
}
|
|
20644
|
+
|
|
20645
|
+
// Each target keeps whichever of shape and position is not being set, so
|
|
20646
|
+
// that changing the shape of a selection whose lines point different ways
|
|
20647
|
+
// leaves them pointing those ways.
|
|
20648
|
+
function applyArrowShape(shape) {
|
|
20649
|
+
applyArrowEdits(function(info) {
|
|
20650
|
+
return {shape: shape, position: fillArrowInfo(info).position};
|
|
20651
|
+
});
|
|
20652
|
+
}
|
|
20653
|
+
|
|
20654
|
+
function applyArrowPosition(position) {
|
|
20655
|
+
applyArrowEdits(function(info) {
|
|
20656
|
+
return {shape: fillArrowInfo(info).shape, position: position};
|
|
20657
|
+
});
|
|
20658
|
+
}
|
|
20659
|
+
|
|
20660
|
+
function fillArrowInfo(info) {
|
|
20661
|
+
return info.shape == 'none' ? {shape: lastArrow.shape, position: lastArrow.position} : info;
|
|
20662
|
+
}
|
|
20663
|
+
|
|
20664
|
+
function applyArrowEdits(getNext) {
|
|
20665
|
+
var records = getTargetRecords();
|
|
20666
|
+
var addStroke = styleFieldIsUnsetForTargets('stroke');
|
|
20667
|
+
var edits = [];
|
|
20668
|
+
getTargetIds().forEach(function(id) {
|
|
20669
|
+
var rec = records[id] || {};
|
|
20670
|
+
var next = getNext(getArrowInfo(rec));
|
|
20671
|
+
var on = next.shape != 'none';
|
|
20672
|
+
var styles = [
|
|
20673
|
+
['line-start', on && next.position != 'end' ? next.shape : ''],
|
|
20674
|
+
['line-end', on && next.position != 'start' ? next.shape : '']
|
|
20675
|
+
].filter(function(style) {
|
|
20676
|
+
// no need to remove what is not there
|
|
20677
|
+
return style[1] || rec[style[0]];
|
|
20678
|
+
});
|
|
20679
|
+
if (on && addStroke) styles.push(['stroke', strokeControl.defaultColor]);
|
|
20680
|
+
if (styles.length > 0) edits.push({id: id, styles: styles});
|
|
20681
|
+
});
|
|
20682
|
+
runStyleEdits(edits);
|
|
20683
|
+
}
|
|
20684
|
+
|
|
20685
|
+
function applyArrowSize(value) {
|
|
20686
|
+
var ids = getArrowTargetIds();
|
|
20687
|
+
if (ids.length === 0) return;
|
|
20688
|
+
runStyleEdits(ids.map(function(id) {
|
|
20689
|
+
return {id: id, styles: [['line-end-size', value]]};
|
|
20690
|
+
}));
|
|
20691
|
+
}
|
|
20692
|
+
|
|
20693
|
+
function nudgeArrowSize(delta) {
|
|
20694
|
+
var shown = getArrowSizeShown(getArrowTargetIds());
|
|
20695
|
+
var size = Number(shown.value);
|
|
20696
|
+
if (!isFinite(size) || shown.value === '') return;
|
|
20697
|
+
applyArrowSize(Math.max(1, Math.round(size + delta)));
|
|
20698
|
+
}
|
|
20699
|
+
|
|
20700
|
+
function getArrowTargetIds() {
|
|
20701
|
+
var records = getTargetRecords();
|
|
20702
|
+
return getTargetIds().filter(function(id) {
|
|
20703
|
+
return getArrowInfo(records[id]).shape != 'none';
|
|
20704
|
+
});
|
|
20705
|
+
}
|
|
20706
|
+
|
|
20707
|
+
// The size a head is drawn at, which is the one its line width gives it
|
|
20708
|
+
// when it has none of its own.
|
|
20709
|
+
function getArrowSizeShown(ids) {
|
|
20710
|
+
var records = getTargetRecords();
|
|
20711
|
+
var value = '', size;
|
|
20712
|
+
for (var i=0; i<ids.length; i++) {
|
|
20713
|
+
size = getArrowSize(records[ids[i]]);
|
|
20714
|
+
if (i > 0 && size !== value) return {value: '', mixed: true};
|
|
20715
|
+
value = size;
|
|
20716
|
+
}
|
|
20717
|
+
return {value: value, mixed: false};
|
|
20718
|
+
}
|
|
20719
|
+
|
|
20720
|
+
function getArrowSize(rec) {
|
|
20721
|
+
var opts = internal.svg.makeLineArrowOpts('arrow', '', rec && rec['line-end-size'],
|
|
20722
|
+
rec && rec['stroke-width'], 1);
|
|
20723
|
+
var size = getArrowInfo(rec).shape == 'dot' ? opts.dotSize : opts.size;
|
|
20724
|
+
return formatNumberValue(Math.round(size * 100) / 100);
|
|
20725
|
+
}
|
|
20726
|
+
|
|
20727
|
+
// The shape and ends shown are those of the lines with heads; lines
|
|
20728
|
+
// without are what the switch's mixed state is for.
|
|
20729
|
+
function updateArrowControl() {
|
|
20730
|
+
var records = getTargetRecords();
|
|
20731
|
+
var ids = getTargetIds();
|
|
20732
|
+
var arrowIds = getArrowTargetIds();
|
|
20733
|
+
var shape, position, info;
|
|
20734
|
+
for (var i=0; i<arrowIds.length; i++) {
|
|
20735
|
+
info = getArrowInfo(records[arrowIds[i]]);
|
|
20736
|
+
if (i === 0) {
|
|
20737
|
+
shape = info.shape;
|
|
20738
|
+
position = info.position;
|
|
20739
|
+
} else {
|
|
20740
|
+
if (info.shape != shape) shape = '';
|
|
20741
|
+
if (info.position != position) position = '';
|
|
20742
|
+
}
|
|
20743
|
+
}
|
|
20744
|
+
if (shape) lastArrow.shape = shape;
|
|
20745
|
+
if (position) lastArrow.position = position;
|
|
20746
|
+
var state = arrowIds.length === 0 ? 'off' :
|
|
20747
|
+
arrowIds.length < ids.length ? 'mixed' : 'on';
|
|
20748
|
+
arrowControl.toggle.setState(state);
|
|
20749
|
+
arrowControl.section.classed('collapsed', state == 'off');
|
|
20750
|
+
arrowShapes.forEach(function(item) {
|
|
20751
|
+
arrowControl.shapeBtns[item.name].classed('selected', item.name == shape);
|
|
20752
|
+
});
|
|
20753
|
+
arrowPositions.forEach(function(item) {
|
|
20754
|
+
arrowControl.posBtns[item.name].classed('selected', item.name == position);
|
|
20755
|
+
});
|
|
20756
|
+
var shown = arrowIds.length > 0 ? getArrowSizeShown(arrowIds) : {value: '', mixed: false};
|
|
20757
|
+
arrowControl.sizeField.setValue(shown.value);
|
|
20758
|
+
arrowControl.sizeField.setPlaceholder(shown.mixed ? 'mixed' : '');
|
|
20759
|
+
arrowControl.sizeField.setDisabled(arrowIds.length === 0);
|
|
20760
|
+
}
|
|
20761
|
+
|
|
20762
|
+
function getTargetRecords() {
|
|
20763
|
+
return targetLayer && targetLayer.data ? targetLayer.data.getRecords() : [];
|
|
20764
|
+
}
|
|
20765
|
+
|
|
20457
20766
|
function releaseFocus() {
|
|
20458
20767
|
releasePanelFocus(panel.node());
|
|
20459
20768
|
}
|
|
@@ -20468,10 +20777,12 @@
|
|
|
20468
20777
|
strokeControl.row.show();
|
|
20469
20778
|
fillControl.row.classed('hidden', geom != 'polygon');
|
|
20470
20779
|
dashControl.row.classed('hidden', geom != 'polyline');
|
|
20780
|
+
arrowControl.section.classed('hidden', geom != 'polyline');
|
|
20471
20781
|
updateColorControl(strokeControl);
|
|
20472
20782
|
updateColorControl(fillControl);
|
|
20473
20783
|
updateStrokeWidthControl();
|
|
20474
20784
|
updateDashArrayControl();
|
|
20785
|
+
if (geom == 'polyline') updateArrowControl();
|
|
20475
20786
|
randomFillBtn.classed('hidden', geom != 'polygon');
|
|
20476
20787
|
patternControl.section.classed('hidden', geom != 'polygon');
|
|
20477
20788
|
if (geom == 'polygon') patternControl.update();
|
|
@@ -20651,6 +20962,7 @@
|
|
|
20651
20962
|
addStyleValue(style, 'stroke-opacity', parseOpacityValue(strokeControl.opacity.node().value));
|
|
20652
20963
|
if (targetLayer && targetLayer.geometry_type == 'polyline') {
|
|
20653
20964
|
addStyleValue(style, 'stroke-dasharray', getControlValue(dashControl.input));
|
|
20965
|
+
addArrowStyleValues(style);
|
|
20654
20966
|
}
|
|
20655
20967
|
if (targetLayer && targetLayer.geometry_type == 'polygon') {
|
|
20656
20968
|
addStyleValue(style, 'fill', getControlValue(fillControl.input));
|
|
@@ -20660,6 +20972,14 @@
|
|
|
20660
20972
|
return style;
|
|
20661
20973
|
}
|
|
20662
20974
|
|
|
20975
|
+
// Only when every target agrees, as with the other controls; a size is
|
|
20976
|
+
// saved only if the lines have their own.
|
|
20977
|
+
function addArrowStyleValues(style) {
|
|
20978
|
+
['line-start', 'line-end', 'line-end-size'].forEach(function(field) {
|
|
20979
|
+
addStyleValue(style, field, getCommonStyleValue(field));
|
|
20980
|
+
});
|
|
20981
|
+
}
|
|
20982
|
+
|
|
20663
20983
|
function addStyleValue(style, field, value) {
|
|
20664
20984
|
if (value || value === 0) {
|
|
20665
20985
|
style[field] = value;
|
|
@@ -22931,6 +23251,7 @@
|
|
|
22931
23251
|
// data.ids = [drawingId];
|
|
22932
23252
|
// data.id = drawingId;
|
|
22933
23253
|
data.ids = utils$1.uniq(data.ids.concat([drawingId]));
|
|
23254
|
+
data.drawing_id = drawingId;
|
|
22934
23255
|
}
|
|
22935
23256
|
if (pinnedOn) {
|
|
22936
23257
|
data.pinned = true;
|
|
@@ -24858,12 +25179,7 @@
|
|
|
24858
25179
|
styleOpts.interactionMode == 'edit_polygons' ||
|
|
24859
25180
|
styleOpts.interactionMode == 'snip_lines') {
|
|
24860
25181
|
// 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];
|
|
25182
|
+
return getShapeEditingLayers(activeLyr, hitData);
|
|
24867
25183
|
}
|
|
24868
25184
|
layers = [];
|
|
24869
25185
|
if (styleOpts.interactionMode == 'label') {
|
|
@@ -24998,15 +25314,50 @@
|
|
|
24998
25314
|
};
|
|
24999
25315
|
}
|
|
25000
25316
|
|
|
25317
|
+
// The path being drawn is shown without its vertices, which are only of use
|
|
25318
|
+
// for reshaping a completed path. The hover markers go on the last layer, so
|
|
25319
|
+
// they are drawn on top.
|
|
25320
|
+
function getShapeEditingLayers(activeLyr, hitData) {
|
|
25321
|
+
var drawingIds = hitData.drawing_id >= 0 ? [hitData.drawing_id] : [];
|
|
25322
|
+
var otherIds = utils$1.difference(hitData.ids || [], drawingIds);
|
|
25323
|
+
var layers = [], lyr;
|
|
25324
|
+
if (drawingIds.length > 0) {
|
|
25325
|
+
lyr = getShapeEditingLayer(activeLyr, hitData, drawingIds, false);
|
|
25326
|
+
// The end of a path being drawn has closely spaced vertices, and pixel
|
|
25327
|
+
// rounding would show as a staircase.
|
|
25328
|
+
lyr.gui.style.unroundedCoords = true;
|
|
25329
|
+
layers.push(lyr);
|
|
25330
|
+
}
|
|
25331
|
+
if (otherIds.length > 0 || layers.length === 0) {
|
|
25332
|
+
layers.push(getShapeEditingLayer(activeLyr, hitData, otherIds, true));
|
|
25333
|
+
}
|
|
25334
|
+
layers.forEach(function(lyr, i) {
|
|
25335
|
+
if (i < layers.length - 1) {
|
|
25336
|
+
lyr.gui.style.vertex_overlay = null;
|
|
25337
|
+
lyr.gui.style.pending_snip = null;
|
|
25338
|
+
}
|
|
25339
|
+
});
|
|
25340
|
+
return layers;
|
|
25341
|
+
}
|
|
25342
|
+
|
|
25343
|
+
function getShapeEditingLayer(activeLyr, hitData, ids, showVertices) {
|
|
25344
|
+
var lyr = getOverlayLayer(activeLyr, ids);
|
|
25345
|
+
lyr.gui.style = getLineEditingStyle(hitData, ids, showVertices);
|
|
25346
|
+
if (activeLyr.geometry_type == 'polygon') {
|
|
25347
|
+
lyr.gui.style.fillColor = hoverFill;
|
|
25348
|
+
}
|
|
25349
|
+
return lyr;
|
|
25350
|
+
}
|
|
25351
|
+
|
|
25001
25352
|
// style for vertex edit mode
|
|
25002
|
-
function getLineEditingStyle(o) {
|
|
25353
|
+
function getLineEditingStyle(o, ids, showVertices) {
|
|
25003
25354
|
var isVertex = o.hit_type == 'vertex' || o.hit_type == 'disabled';
|
|
25004
25355
|
return {
|
|
25005
|
-
ids:
|
|
25356
|
+
ids: ids,
|
|
25006
25357
|
overlay: true,
|
|
25007
25358
|
strokeColor: black,
|
|
25008
25359
|
strokeWidth: 1.2,
|
|
25009
|
-
vertices:
|
|
25360
|
+
vertices: showVertices,
|
|
25010
25361
|
vertex_overlay_color: getVertexOverlayColor(o.hit_type),
|
|
25011
25362
|
vertex_overlay_scale: isVertex ? 2.5 : 2,
|
|
25012
25363
|
vertex_overlay: o.hit_coordinates || null,
|
|
@@ -25207,6 +25558,11 @@
|
|
|
25207
25558
|
// pixel distance threshold for hovering near a vertex or segment midpoint
|
|
25208
25559
|
var HOVER_THRESHOLD$1 = 10;
|
|
25209
25560
|
|
|
25561
|
+
// How far the pointer has to move from where the mouse was pressed before a
|
|
25562
|
+
// drag draws a stroke. This matches the distance under which gui-mouse treats
|
|
25563
|
+
// a press as a click, so a press that stays this close adds only a click vertex.
|
|
25564
|
+
var STROKE_START_DIST = 6;
|
|
25565
|
+
|
|
25210
25566
|
function initLineEditing(gui, ext, hit) {
|
|
25211
25567
|
var hoverVertexInfo;
|
|
25212
25568
|
var prevVertexAddedEvent;
|
|
@@ -25216,7 +25572,8 @@
|
|
|
25216
25572
|
var drawingId = -1; // feature id of path being drawn
|
|
25217
25573
|
var sessionCount = 0;
|
|
25218
25574
|
var alert;
|
|
25219
|
-
var
|
|
25575
|
+
var stroke = null; // freehand stroke in progress (see startStroke())
|
|
25576
|
+
var blockDrag = false; // a drag finished a path; ignore it until the mouse is released
|
|
25220
25577
|
var _dragging = false;
|
|
25221
25578
|
|
|
25222
25579
|
function active() {
|
|
@@ -25236,7 +25593,7 @@
|
|
|
25236
25593
|
}
|
|
25237
25594
|
|
|
25238
25595
|
function pencilIsActive() {
|
|
25239
|
-
return active() && (cmdKeyDown() || pathDrawing()) && !vertexDragging()
|
|
25596
|
+
return active() && (cmdKeyDown() || pathDrawing()) && !vertexDragging();
|
|
25240
25597
|
}
|
|
25241
25598
|
|
|
25242
25599
|
function polygonMode() {
|
|
@@ -25273,19 +25630,32 @@
|
|
|
25273
25630
|
fullRedraw();
|
|
25274
25631
|
});
|
|
25275
25632
|
|
|
25633
|
+
// an undo during a stroke takes back the whole stroke
|
|
25634
|
+
gui.on('undo_redo_pre', function() {
|
|
25635
|
+
if (stroke) {
|
|
25636
|
+
finishStroke();
|
|
25637
|
+
blockDrag = true;
|
|
25638
|
+
}
|
|
25639
|
+
});
|
|
25640
|
+
|
|
25276
25641
|
gui.on('undo_path_add', function(e) {
|
|
25277
25642
|
deleteLastPath(hit.getHitTarget());
|
|
25278
25643
|
clearDrawingInfo();
|
|
25279
25644
|
});
|
|
25280
25645
|
|
|
25646
|
+
// e.points: the vertices that the edit added to the path
|
|
25281
25647
|
gui.on('redo_path_extend', function(e) {
|
|
25282
25648
|
var target = hit.getHitTarget();
|
|
25283
|
-
|
|
25284
|
-
|
|
25285
|
-
|
|
25286
|
-
|
|
25649
|
+
var points = e.points;
|
|
25650
|
+
var last = points[points.length - 1];
|
|
25651
|
+
if (pathDrawing()) {
|
|
25652
|
+
// the vertex following the pointer becomes the first added vertex
|
|
25653
|
+
setVertexCoords(target, [target.gui.displayArcs.getPointCount() - 1], points[0]);
|
|
25654
|
+
points.slice(1).forEach(function(p) { appendVertex$1(target, p); });
|
|
25655
|
+
appendVertex$1(target, prevHoverEvent ? pixToDataCoords(prevHoverEvent.x, prevHoverEvent.y) : last);
|
|
25656
|
+
hit.triggerChangeEvent();
|
|
25287
25657
|
} else {
|
|
25288
|
-
appendVertex$1(target,
|
|
25658
|
+
points.forEach(function(p) { appendVertex$1(target, p); });
|
|
25289
25659
|
}
|
|
25290
25660
|
if (e.shapes) {
|
|
25291
25661
|
replaceDrawnShapes(e.shapes);
|
|
@@ -25294,11 +25664,13 @@
|
|
|
25294
25664
|
|
|
25295
25665
|
gui.on('undo_path_extend', function(e) {
|
|
25296
25666
|
var target = hit.getHitTarget();
|
|
25297
|
-
|
|
25667
|
+
// while drawing, this also removes the vertex that follows the pointer,
|
|
25668
|
+
// and the path's new last vertex takes its place
|
|
25669
|
+
for (var i=0; i<e.points.length; i++) {
|
|
25298
25670
|
deleteLastVertex(target);
|
|
25671
|
+
}
|
|
25672
|
+
if (pathDrawing() && prevHoverEvent) {
|
|
25299
25673
|
updatePathEndpoint(pixToDataCoords(prevHoverEvent.x, prevHoverEvent.y));
|
|
25300
|
-
} else {
|
|
25301
|
-
deleteLastVertex(target);
|
|
25302
25674
|
}
|
|
25303
25675
|
if (e.shapes) {
|
|
25304
25676
|
replaceDrawnShapes(e.shapes);
|
|
@@ -25418,60 +25790,170 @@
|
|
|
25418
25790
|
});
|
|
25419
25791
|
|
|
25420
25792
|
gui.map.getMouse().on('dragend', function(e) {
|
|
25421
|
-
|
|
25793
|
+
finishStroke();
|
|
25794
|
+
blockDrag = false;
|
|
25422
25795
|
});
|
|
25423
25796
|
|
|
25424
25797
|
gui.map.getMouse().on('drag', function(e) {
|
|
25798
|
+
if (blockDrag) {
|
|
25799
|
+
e.stopPropagation(); // don't pan until the mouse is released
|
|
25800
|
+
return;
|
|
25801
|
+
}
|
|
25425
25802
|
if (!pencilIsActive()) {
|
|
25426
|
-
if (!pencilPoints) {
|
|
25427
|
-
// null points signals that a path was just completed -- block panning
|
|
25428
|
-
e.stopPropagation();
|
|
25429
|
-
}
|
|
25430
25803
|
return;
|
|
25431
25804
|
}
|
|
25432
25805
|
if (gui.keyboard.spaceIsPressed()) {
|
|
25433
25806
|
// pan if dragging with spacebar down
|
|
25434
|
-
|
|
25807
|
+
finishStroke();
|
|
25435
25808
|
return;
|
|
25436
25809
|
}
|
|
25437
25810
|
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];
|
|
25811
|
+
if (!stroke) {
|
|
25812
|
+
if (geom.distance2D(0, 0, e.dragX, e.dragY) < STROKE_START_DIST) return;
|
|
25813
|
+
startStroke(e);
|
|
25814
|
+
if (!stroke) return;
|
|
25815
|
+
}
|
|
25816
|
+
hoverVertexInfo = polygonMode() ? findPathStartInfo(e) : null;
|
|
25817
|
+
if (hoverVertexInfo && stroke.sampleCount > 1) {
|
|
25818
|
+
// the stroke has returned to the start of the path -- close it
|
|
25819
|
+
finishStroke(hoverVertexInfo);
|
|
25820
|
+
blockDrag = true;
|
|
25463
25821
|
} else {
|
|
25464
|
-
|
|
25465
|
-
pencilPoints.push(xy);
|
|
25466
|
-
updatePathEndpoint(p);
|
|
25467
|
-
addedToPath = false;
|
|
25468
|
-
}
|
|
25469
|
-
if (addedToPath) {
|
|
25470
|
-
//
|
|
25471
|
-
prevVertexAddedEvent = e;
|
|
25822
|
+
addStrokeSample(e);
|
|
25472
25823
|
}
|
|
25473
25824
|
}, null, 3); // higher priority than hit control
|
|
25474
25825
|
|
|
25826
|
+
// A stroke is smoothed as it is drawn. Vertices are placed some way behind
|
|
25827
|
+
// the pointer, where later samples no longer change the curve, and they stay
|
|
25828
|
+
// where they are. Between the last of them and the pointer, the path shows
|
|
25829
|
+
// the pointer's own trace (see updateStrokeTail()).
|
|
25830
|
+
//
|
|
25831
|
+
// The first vertex of the stroke (the anchor) is either the start of a new
|
|
25832
|
+
// path or the vertex that was following the pointer when the mouse was
|
|
25833
|
+
// pressed. Vertices are added to the path as they are placed, but the edit
|
|
25834
|
+
// that records them is made when the stroke ends, so that an undo takes back
|
|
25835
|
+
// the whole stroke.
|
|
25836
|
+
function startStroke(e) {
|
|
25837
|
+
var target = hit.getHitTarget();
|
|
25838
|
+
var anchorCommitted = !pathDrawing();
|
|
25839
|
+
var n, anchor;
|
|
25840
|
+
if (anchorCommitted) {
|
|
25841
|
+
// the path_add edit records the anchor
|
|
25842
|
+
hoverVertexInfo = null;
|
|
25843
|
+
startNewPath(pixToDataCoords(e.x - e.dragX, e.y - e.dragY));
|
|
25844
|
+
} else if (polygonMode() && hoverVertexInfo?.type == 'vertex') {
|
|
25845
|
+
// pressing on a vertex closes the polygon, as a click does
|
|
25846
|
+
extendCurrentPath([getLastVertexCoords(target)], true);
|
|
25847
|
+
blockDrag = true;
|
|
25848
|
+
return;
|
|
25849
|
+
} else if (pointerIsOnLastVertex(target)) {
|
|
25850
|
+
// continue from the last vertex rather than doubling it
|
|
25851
|
+
anchorCommitted = true;
|
|
25852
|
+
} else {
|
|
25853
|
+
// leave the vertex under the pointer behind as the anchor, and add a new
|
|
25854
|
+
// one to follow the pointer
|
|
25855
|
+
appendVertex$1(target, getLastVertexCoords(target));
|
|
25856
|
+
}
|
|
25857
|
+
n = target.gui.displayArcs.getPointCount();
|
|
25858
|
+
anchor = target.gui.displayArcs.getVertex2(n - 2);
|
|
25859
|
+
stroke = {
|
|
25860
|
+
anchorCommitted: anchorCommitted,
|
|
25861
|
+
// data coords of the vertices before the pointer that no edit records yet
|
|
25862
|
+
points: anchorCommitted ? [] : [getVertexCoords(target, n - 2)],
|
|
25863
|
+
// fitted in display coords, which survive a zoom mid-stroke
|
|
25864
|
+
fitter: new internal.GaussianStrokeFitter(anchor, {pixelSize: ext.getPixelSize()}),
|
|
25865
|
+
previewCount: 0, // vertices between the placed vertices and the pointer
|
|
25866
|
+
sampleCount: 0,
|
|
25867
|
+
lastEvent: e
|
|
25868
|
+
};
|
|
25869
|
+
}
|
|
25870
|
+
|
|
25871
|
+
// Test if the vertex following the pointer is on the path's last vertex
|
|
25872
|
+
function pointerIsOnLastVertex(target) {
|
|
25873
|
+
var arcs = target.gui.displayArcs;
|
|
25874
|
+
var n = arcs.getPointCount();
|
|
25875
|
+
var a = arcs.getVertex2(n - 1), b = arcs.getVertex2(n - 2);
|
|
25876
|
+
return geom.distance2D(a[0], a[1], b[0], b[1]) / ext.getPixelSize() < 3;
|
|
25877
|
+
}
|
|
25878
|
+
|
|
25879
|
+
function addStrokeSample(e) {
|
|
25880
|
+
var s = stroke;
|
|
25881
|
+
var placed = s.fitter.addSample(ext.pixCoordsToMapCoords(e.x, e.y));
|
|
25882
|
+
s.sampleCount++;
|
|
25883
|
+
s.lastEvent = e;
|
|
25884
|
+
updateStrokeTail(s, placed, s.fitter.getPreview(), pixToDataCoords(e.x, e.y));
|
|
25885
|
+
}
|
|
25886
|
+
|
|
25887
|
+
// Rewrites the end of the path after the stroke's placed vertices: newly
|
|
25888
|
+
// placed vertices, then the preview of the part of the stroke that has not
|
|
25889
|
+
// been placed yet, then the vertex following the pointer. Existing vertices
|
|
25890
|
+
// are overwritten rather than removed and added again, because adding or
|
|
25891
|
+
// removing a vertex copies the coordinates of the whole layer.
|
|
25892
|
+
// placed, preview: display coords
|
|
25893
|
+
// hover: data coords
|
|
25894
|
+
function updateStrokeTail(s, placed, preview, hover) {
|
|
25895
|
+
var target = hit.getHitTarget();
|
|
25896
|
+
var fixed = toDataCoords(target, placed);
|
|
25897
|
+
var tail = fixed.concat(toDataCoords(target, preview), [hover]);
|
|
25898
|
+
var count = s.previewCount + 1; // vertices after the placed vertices
|
|
25899
|
+
var start = target.gui.displayArcs.getPointCount() - count;
|
|
25900
|
+
var i;
|
|
25901
|
+
for (; count > tail.length; count--) {
|
|
25902
|
+
deleteLastVertex(target);
|
|
25903
|
+
}
|
|
25904
|
+
for (i = 0; i < tail.length; i++) {
|
|
25905
|
+
if (i < count) {
|
|
25906
|
+
setVertexCoords(target, [start + i], tail[i]);
|
|
25907
|
+
} else {
|
|
25908
|
+
appendVertex$1(target, tail[i]);
|
|
25909
|
+
}
|
|
25910
|
+
}
|
|
25911
|
+
s.points = s.points.concat(fixed);
|
|
25912
|
+
s.previewCount = tail.length - fixed.length - 1;
|
|
25913
|
+
hit.triggerChangeEvent();
|
|
25914
|
+
}
|
|
25915
|
+
|
|
25916
|
+
function toDataCoords(target, points) {
|
|
25917
|
+
return points.map(function(p) {
|
|
25918
|
+
return translateDisplayPoint(target, p);
|
|
25919
|
+
});
|
|
25920
|
+
}
|
|
25921
|
+
|
|
25922
|
+
// Completes the stroke in progress up to the pointer, and records the
|
|
25923
|
+
// stroke's vertices as a single edit.
|
|
25924
|
+
// end: (optional) hover info of the vertex that the stroke has to end at
|
|
25925
|
+
function finishStroke(end) {
|
|
25926
|
+
var s = stroke;
|
|
25927
|
+
var target, rest, points, count;
|
|
25928
|
+
if (!s) return;
|
|
25929
|
+
stroke = null;
|
|
25930
|
+
target = hit.getHitTarget();
|
|
25931
|
+
rest = end ? s.fitter.addSample(end.displayPoint) : [];
|
|
25932
|
+
rest = toDataCoords(target, rest.concat(s.fitter.finish()));
|
|
25933
|
+
if (end && rest.length > 0) {
|
|
25934
|
+
rest[rest.length - 1] = end.point;
|
|
25935
|
+
}
|
|
25936
|
+
// remove the preview
|
|
25937
|
+
count = s.previewCount + 1;
|
|
25938
|
+
for (; count > 1; count--) {
|
|
25939
|
+
deleteLastVertex(target);
|
|
25940
|
+
}
|
|
25941
|
+
if (rest.length === 0 && s.points.length > 0) {
|
|
25942
|
+
// the last vertex in the path takes the place of the vertex following the pointer
|
|
25943
|
+
deleteLastVertex(target);
|
|
25944
|
+
rest = [s.points.pop()];
|
|
25945
|
+
} else if (rest.length > 0) {
|
|
25946
|
+
setVertexCoords(target, [target.gui.displayArcs.getPointCount() - 1], rest[0]);
|
|
25947
|
+
}
|
|
25948
|
+
points = s.points.concat(rest);
|
|
25949
|
+
if (points.length === 0 || !s.anchorCommitted && points.length == 1) {
|
|
25950
|
+
hit.triggerChangeEvent(); // the stroke never left its anchor
|
|
25951
|
+
return;
|
|
25952
|
+
}
|
|
25953
|
+
prevVertexAddedEvent = s.lastEvent;
|
|
25954
|
+
extendCurrentPath(points, !!end, s.points.length);
|
|
25955
|
+
}
|
|
25956
|
+
|
|
25475
25957
|
hit.on('drag', function(e) {
|
|
25476
25958
|
if (!vertexDragging() || pathDrawing()) {
|
|
25477
25959
|
return;
|
|
@@ -25551,7 +26033,8 @@
|
|
|
25551
26033
|
if (detectDoubleClick(e)) return; // ignore second click of a dblclick
|
|
25552
26034
|
var p = pixToDataCoords(e.x, e.y);
|
|
25553
26035
|
if (pathDrawing()) {
|
|
25554
|
-
|
|
26036
|
+
// finish the path if a vertex is selected (but not an interpolated point)
|
|
26037
|
+
extendCurrentPath([hoverVertexInfo?.point || p], hoverVertexInfo?.type == 'vertex');
|
|
25555
26038
|
} else if (hoverVertexInfo?.type == 'interpolated') {
|
|
25556
26039
|
// don't start new path if hovering along a segment -- this is
|
|
25557
26040
|
// likely to be an attempt to add a new vertex, not start a new path
|
|
@@ -25651,6 +26134,11 @@
|
|
|
25651
26134
|
|
|
25652
26135
|
function finishCurrentPath() {
|
|
25653
26136
|
if (!pathDrawing()) return;
|
|
26137
|
+
if (stroke) {
|
|
26138
|
+
// finishing mid-drag (e.g. with the Enter key)
|
|
26139
|
+
finishStroke();
|
|
26140
|
+
blockDrag = true;
|
|
26141
|
+
}
|
|
25654
26142
|
var target = hit.getHitTarget();
|
|
25655
26143
|
if (getLastArcLength(target) <= 2) { // includes hover point
|
|
25656
26144
|
// deleteLastPath(target);
|
|
@@ -25674,15 +26162,21 @@
|
|
|
25674
26162
|
updateCursor();
|
|
25675
26163
|
}
|
|
25676
26164
|
|
|
25677
|
-
//
|
|
25678
|
-
|
|
26165
|
+
// points: [x, y] source data coordinates of vertices to add to the path. The
|
|
26166
|
+
// vertex that follows the pointer becomes points[placed], so it must
|
|
26167
|
+
// already be there; the rest are appended after it.
|
|
26168
|
+
// finish: true if the path ends at an existing vertex (which closes a polygon)
|
|
26169
|
+
// placed: (optional) number of points already in the path, before the vertex
|
|
26170
|
+
// that follows the pointer
|
|
26171
|
+
function extendCurrentPath(points, finish, placed) {
|
|
25679
26172
|
var target = hit.getHitTarget();
|
|
25680
26173
|
var shapes1, shapes2;
|
|
25681
|
-
// finish the path if a vertex is selected (but not an interpolated point)
|
|
25682
|
-
var finish = hoverVertexInfo?.type == 'vertex';
|
|
25683
26174
|
if (getLastArcLength(target) < 2) {
|
|
25684
26175
|
stop$1('Defective path');
|
|
25685
26176
|
}
|
|
26177
|
+
for (var i=(placed || 0) + 1; i<points.length; i++) {
|
|
26178
|
+
appendVertex$1(target, points[i]);
|
|
26179
|
+
}
|
|
25686
26180
|
if (finish && polygonMode()) {
|
|
25687
26181
|
shapes1 = target.shapes.slice(initialShapeCount);
|
|
25688
26182
|
try {
|
|
@@ -25694,12 +26188,12 @@
|
|
|
25694
26188
|
}
|
|
25695
26189
|
if (shapes2) {
|
|
25696
26190
|
replaceDrawnShapes(shapes2);
|
|
25697
|
-
gui.dispatchEvent('path_extend', {target,
|
|
26191
|
+
gui.dispatchEvent('path_extend', {target, points, shapes1, shapes2});
|
|
25698
26192
|
clearDrawingInfo();
|
|
25699
26193
|
fullRedraw();
|
|
25700
26194
|
} else {
|
|
25701
|
-
appendVertex$1(target,
|
|
25702
|
-
gui.dispatchEvent('path_extend', {target,
|
|
26195
|
+
appendVertex$1(target, points[points.length - 1]); // the new vertex following the pointer
|
|
26196
|
+
gui.dispatchEvent('path_extend', {target, points});
|
|
25703
26197
|
hit.triggerChangeEvent(); // trigger overlay redraw
|
|
25704
26198
|
}
|
|
25705
26199
|
}
|
|
@@ -30581,10 +31075,20 @@
|
|
|
30581
31075
|
// array of field names of relevant svg display properties
|
|
30582
31076
|
fields = getStyleFields(lyr).filter(function(f) {return f in styleIndex;}),
|
|
30583
31077
|
records = lyr.data.getRecords();
|
|
31078
|
+
// Arrowheads are drawn by the canvas as shapes, not set as attributes, so
|
|
31079
|
+
// they are not among the attribute fields. Assigned on every call, like the
|
|
31080
|
+
// rest, because the style object is reused from one feature to the next.
|
|
31081
|
+
var arrowFields = getLineArrowFields(lyr);
|
|
31082
|
+
var hasStrokeFields = fields.includes('stroke') || fields.includes('stroke-width');
|
|
30584
31083
|
|
|
30585
31084
|
var styler = function(style, i) {
|
|
30586
31085
|
var rec = records[i];
|
|
30587
31086
|
var fname, val;
|
|
31087
|
+
if (arrowFields) {
|
|
31088
|
+
style.lineStart = rec && rec['line-start'];
|
|
31089
|
+
style.lineEnd = rec && rec['line-end'];
|
|
31090
|
+
style.lineEndSize = rec && rec['line-end-size'];
|
|
31091
|
+
}
|
|
30588
31092
|
for (var j=0; j<fields.length; j++) {
|
|
30589
31093
|
fname = fields[j];
|
|
30590
31094
|
val = rec && rec[fname];
|
|
@@ -30606,6 +31110,12 @@
|
|
|
30606
31110
|
}
|
|
30607
31111
|
};
|
|
30608
31112
|
var style = {styler: styler, type: 'styled'};
|
|
31113
|
+
// A line layer styled with nothing but arrowheads is drawn the way SVG
|
|
31114
|
+
// export draws it, with the black 1px line the layer's group gives it.
|
|
31115
|
+
if (arrowFields && !hasStrokeFields) {
|
|
31116
|
+
style.strokeColor = 'black';
|
|
31117
|
+
style.strokeWidth = 1;
|
|
31118
|
+
}
|
|
30609
31119
|
// use squares if radius is missing... (TODO: check behavior with labels, etc)
|
|
30610
31120
|
if (lyr.geometry_type == 'point' && fields.includes('r') === false) {
|
|
30611
31121
|
style.dotSize = 1;
|
|
@@ -30620,7 +31130,18 @@
|
|
|
30620
31130
|
// return fields.indexOf('r') > -1; // require 'r' field for point symbols
|
|
30621
31131
|
return fields.includes('fill') || fields.includes('r'); // support colored squares
|
|
30622
31132
|
}
|
|
30623
|
-
return utils$1.difference(fields, ['opacity', 'class']).length > 0
|
|
31133
|
+
return utils$1.difference(fields, ['opacity', 'class']).length > 0 ||
|
|
31134
|
+
!!getLineArrowFields(lyr);
|
|
31135
|
+
}
|
|
31136
|
+
|
|
31137
|
+
// The arrowhead fields a line layer has, or null
|
|
31138
|
+
function getLineArrowFields(lyr) {
|
|
31139
|
+
var fields;
|
|
31140
|
+
if (lyr.geometry_type != 'polyline' || !lyr.data) return null;
|
|
31141
|
+
fields = lyr.data.getFields().filter(function(f) {
|
|
31142
|
+
return internal.svg.lineArrowFields.includes(f);
|
|
31143
|
+
});
|
|
31144
|
+
return fields.length > 0 ? fields : null;
|
|
30624
31145
|
}
|
|
30625
31146
|
|
|
30626
31147
|
function getStyleFields(lyr) {
|
|
@@ -31579,7 +32100,7 @@
|
|
|
31579
32100
|
arcs = getArcsForRendering(lyr, ext);
|
|
31580
32101
|
filter = getShapeFilter(arcs, layer.shapes, ext);
|
|
31581
32102
|
canv.drawStyledPaths(layer.shapes, arcs, style, filter);
|
|
31582
|
-
if (style.vertices) {
|
|
32103
|
+
if (style.vertices || style.vertex_overlay || style.pending_snip) {
|
|
31583
32104
|
canv.drawVertices(layer.shapes, arcs, style, filter);
|
|
31584
32105
|
}
|
|
31585
32106
|
}
|
|
@@ -31726,6 +32247,8 @@
|
|
|
31726
32247
|
};
|
|
31727
32248
|
*/
|
|
31728
32249
|
|
|
32250
|
+
// Draws a dot at each vertex (if style.vertices is set), and the markers of
|
|
32251
|
+
// style.vertex_overlay and style.pending_snip.
|
|
31729
32252
|
_self.drawVertices = function(shapes, arcs, style, filter) {
|
|
31730
32253
|
var iter = new internal.ShapeIter(arcs);
|
|
31731
32254
|
var t = getScaledTransform(_ext);
|
|
@@ -31735,7 +32258,7 @@
|
|
|
31735
32258
|
var i, j, p;
|
|
31736
32259
|
_ctx.beginPath();
|
|
31737
32260
|
_ctx.fillStyle = color;
|
|
31738
|
-
for (i=0; i<shapes.length; i++) {
|
|
32261
|
+
for (i=0; style.vertices && i<shapes.length; i++) {
|
|
31739
32262
|
var shp = shapes[i];
|
|
31740
32263
|
if (!shp || filter && !filter(i)) continue;
|
|
31741
32264
|
for (j=0; j<shp.length; j++) {
|
|
@@ -31773,8 +32296,10 @@
|
|
|
31773
32296
|
_self.drawStyledPaths = function(shapes, arcs, style, filter) {
|
|
31774
32297
|
var styleIndex = {};
|
|
31775
32298
|
var batchSize = 100;
|
|
31776
|
-
var
|
|
32299
|
+
var lineScale = getScaledLineScale(_ext, style);
|
|
32300
|
+
var startPath = getPathStart(_ext, lineScale);
|
|
31777
32301
|
var draw = getShapePencil(arcs, _ext);
|
|
32302
|
+
var arrowPencil = null;
|
|
31778
32303
|
var key, item, shp;
|
|
31779
32304
|
var styler = style.styler || null;
|
|
31780
32305
|
var drawStyle = styler ? utils$1.defaults({}, style) : style;
|
|
@@ -31784,6 +32309,13 @@
|
|
|
31784
32309
|
if (styler) {
|
|
31785
32310
|
styler(drawStyle, i);
|
|
31786
32311
|
}
|
|
32312
|
+
// A line with arrowheads is drawn on its own, trimmed to meet its heads;
|
|
32313
|
+
// see svg-line-arrows.mjs. Only lines styled with line-start or line-end
|
|
32314
|
+
// get here, so the batch below is untouched for everything else.
|
|
32315
|
+
if (drawStyle.lineStart || drawStyle.lineEnd) {
|
|
32316
|
+
if (!arrowPencil) arrowPencil = getArrowLinePencil(arcs, _ext, lineScale);
|
|
32317
|
+
if (arrowPencil(shp, _ctx, drawStyle, startPath, draw)) continue;
|
|
32318
|
+
}
|
|
31787
32319
|
if (!drawStyle.batchOverlay && (drawStyle.overlay ||
|
|
31788
32320
|
drawStyle.opacity < 1 || drawStyle.fillOpacity < 1 ||
|
|
31789
32321
|
drawStyle.strokeOpacity < 1 || drawStyle.fillEffect)) {
|
|
@@ -32138,6 +32670,10 @@
|
|
|
32138
32670
|
return x + 0.5 | 0;
|
|
32139
32671
|
}
|
|
32140
32672
|
|
|
32673
|
+
function noRound(x) {
|
|
32674
|
+
return x;
|
|
32675
|
+
}
|
|
32676
|
+
|
|
32141
32677
|
function roundToHalfPix(x) {
|
|
32142
32678
|
return (x * 2 | 0) / 2;
|
|
32143
32679
|
}
|
|
@@ -32149,15 +32685,113 @@
|
|
|
32149
32685
|
if (style.fillEffect) {
|
|
32150
32686
|
ctx.fillStyle = getCanvasFillEffect(ctx, shp, arcs, ext, style);
|
|
32151
32687
|
}
|
|
32688
|
+
// Rounding also drops vertices that land on the same pixel, which keeps
|
|
32689
|
+
// detailed shapes fast to draw, so only a style for a few short paths
|
|
32690
|
+
// should turn it off (see gui-overlay-styler.mjs).
|
|
32691
|
+
var round = style.unroundedCoords ? noRound : roundToPix;
|
|
32152
32692
|
for (var i=0, n=shp ? shp.length : 0; i<n; i++) {
|
|
32153
32693
|
iter.init(shp[i]);
|
|
32154
32694
|
// 0.2 trades visible seams for performance
|
|
32155
32695
|
// drawPath(protectIterForDrawing(iter, ext), t, ctx, 0.2);
|
|
32156
|
-
drawPath2(protectIterForDrawing(iter, ext), t, ctx,
|
|
32696
|
+
drawPath2(protectIterForDrawing(iter, ext), t, ctx, round);
|
|
32697
|
+
}
|
|
32698
|
+
};
|
|
32699
|
+
}
|
|
32700
|
+
|
|
32701
|
+
// Returns a function that draws one line feature with its arrowheads, and
|
|
32702
|
+
// returns false if the feature has none to draw (a style of 'none', or no
|
|
32703
|
+
// stroke), for the caller to draw it the usual way.
|
|
32704
|
+
//
|
|
32705
|
+
// Heads are sized by the same factor as the line width, so that a preview
|
|
32706
|
+
// matches the exported SVG. Far enough in that paths are clipped to the view
|
|
32707
|
+
// (see protectIterForDrawing()), the line is drawn clipped and untrimmed and
|
|
32708
|
+
// only the heads are added, since a clipped path's ends are not its ends.
|
|
32709
|
+
function getArrowLinePencil(arcs, ext, lineScale) {
|
|
32710
|
+
var t = getScaledTransform(ext);
|
|
32711
|
+
var iter = new internal.ShapeIter(arcs);
|
|
32712
|
+
var strokeScale = getCanvasStrokeScale(GUI.getPixelRatio(), lineScale);
|
|
32713
|
+
var clipped = ext.scale() > 100;
|
|
32714
|
+
return function(shp, ctx, style, startPath, draw) {
|
|
32715
|
+
var opts = internal.svg.makeLineArrowOpts(style.lineStart, style.lineEnd,
|
|
32716
|
+
style.lineEndSize, style.strokeWidth, strokeScale);
|
|
32717
|
+
var heads = [];
|
|
32718
|
+
var i, coords, shape;
|
|
32719
|
+
if (opts.start == 'none' && opts.end == 'none' || !(style.strokeWidth > 0)) {
|
|
32720
|
+
return false;
|
|
32157
32721
|
}
|
|
32722
|
+
startPath(ctx, style);
|
|
32723
|
+
for (i=0; i<shp.length; i++) {
|
|
32724
|
+
coords = getPixelCoords(iter, shp[i], t);
|
|
32725
|
+
if (coords.length < 2) continue;
|
|
32726
|
+
shape = internal.svg.getLineArrowShape(coords, opts);
|
|
32727
|
+
heads = heads.concat(shape.heads);
|
|
32728
|
+
if (!clipped) traceCoords(shape.coords, ctx);
|
|
32729
|
+
}
|
|
32730
|
+
if (clipped) draw(shp, ctx, style);
|
|
32731
|
+
endPath(ctx, style);
|
|
32732
|
+
drawArrowHeads(heads, ctx, style, opts.width);
|
|
32733
|
+
return true;
|
|
32158
32734
|
};
|
|
32159
32735
|
}
|
|
32160
32736
|
|
|
32737
|
+
function getPixelCoords(iter, path, t) {
|
|
32738
|
+
var coords = [];
|
|
32739
|
+
var x, y, prev;
|
|
32740
|
+
iter.init(path);
|
|
32741
|
+
while (iter.hasNext()) {
|
|
32742
|
+
x = iter.x * t.mx + t.bx;
|
|
32743
|
+
y = iter.y * t.my + t.by;
|
|
32744
|
+
if (prev && prev[0] == x && prev[1] == y) continue;
|
|
32745
|
+
prev = [x, y];
|
|
32746
|
+
coords.push(prev);
|
|
32747
|
+
}
|
|
32748
|
+
return coords;
|
|
32749
|
+
}
|
|
32750
|
+
|
|
32751
|
+
function traceCoords(coords, ctx) {
|
|
32752
|
+
ctx.moveTo(coords[0][0], coords[0][1]);
|
|
32753
|
+
for (var i=1; i<coords.length; i++) {
|
|
32754
|
+
ctx.lineTo(coords[i][0], coords[i][1]);
|
|
32755
|
+
}
|
|
32756
|
+
}
|
|
32757
|
+
|
|
32758
|
+
// Solid heads and dots are filled with the line's colour; open ones are stroked with
|
|
32759
|
+
// its width, always whole and round-cornered, whatever the line's dashes and
|
|
32760
|
+
// caps.
|
|
32761
|
+
function drawArrowHeads(heads, ctx, style, width) {
|
|
32762
|
+
var alpha = (style.opacity >= 0 ? style.opacity : 1) *
|
|
32763
|
+
(style.strokeOpacity >= 0 ? style.strokeOpacity : 1);
|
|
32764
|
+
var head, p;
|
|
32765
|
+
if (heads.length === 0) return;
|
|
32766
|
+
ctx.globalAlpha = alpha;
|
|
32767
|
+
ctx.fillStyle = style.strokeColor;
|
|
32768
|
+
ctx.strokeStyle = style.strokeColor;
|
|
32769
|
+
ctx.lineWidth = width;
|
|
32770
|
+
ctx.lineCap = 'round';
|
|
32771
|
+
ctx.lineJoin = 'round';
|
|
32772
|
+
ctx.setLineDash([]);
|
|
32773
|
+
for (var i=0; i<heads.length; i++) {
|
|
32774
|
+
head = heads[i];
|
|
32775
|
+
ctx.beginPath();
|
|
32776
|
+
if (head.type == 'dot') {
|
|
32777
|
+
ctx.arc(head.center[0], head.center[1], head.radius, 0, Math.PI * 2);
|
|
32778
|
+
ctx.fill();
|
|
32779
|
+
continue;
|
|
32780
|
+
}
|
|
32781
|
+
p = head.points;
|
|
32782
|
+
ctx.moveTo(p[0][0], p[0][1]);
|
|
32783
|
+
ctx.lineTo(p[1][0], p[1][1]);
|
|
32784
|
+
ctx.lineTo(p[2][0], p[2][1]);
|
|
32785
|
+
if (head.type == 'arrow') {
|
|
32786
|
+
ctx.closePath();
|
|
32787
|
+
ctx.fill();
|
|
32788
|
+
} else {
|
|
32789
|
+
ctx.stroke();
|
|
32790
|
+
}
|
|
32791
|
+
}
|
|
32792
|
+
ctx.globalAlpha = 1;
|
|
32793
|
+
}
|
|
32794
|
+
|
|
32161
32795
|
function protectIterForDrawing(iter, ext) {
|
|
32162
32796
|
var bounds, k;
|
|
32163
32797
|
if (ext.scale() > 100) {
|
|
@@ -35770,13 +36404,13 @@
|
|
|
35770
36404
|
|
|
35771
36405
|
// Font and face menus, as in the label panel
|
|
35772
36406
|
row = makeRow(appearance);
|
|
35773
|
-
fontSelect = El('select').attr('
|
|
36407
|
+
fontSelect = El('select').attr('aria-label', 'Font').addClass('scalebar-font-select')
|
|
35774
36408
|
.appendTo(makeCell(row, 'Font'))
|
|
35775
36409
|
.on('change', function() {
|
|
35776
36410
|
if (fontSelect.node().value) applyFont(fontSelect.node().value);
|
|
35777
36411
|
});
|
|
35778
36412
|
renderFontOptions();
|
|
35779
|
-
fontStyleSelect = El('select').attr('
|
|
36413
|
+
fontStyleSelect = El('select').attr('aria-label', 'Font style')
|
|
35780
36414
|
.addClass('scalebar-font-style-select')
|
|
35781
36415
|
.appendTo(makeCell(row, 'Font style'))
|
|
35782
36416
|
.on('change', function() {
|
|
@@ -35790,7 +36424,7 @@
|
|
|
35790
36424
|
El('span').appendTo(colorCell).text('Color');
|
|
35791
36425
|
colorChit = El('div').addClass('label-color-chit').attr('role', 'button')
|
|
35792
36426
|
.on('click', function() { colorPicker.toggle(); });
|
|
35793
|
-
colorInput = El('input').attr('type', 'text').attr('
|
|
36427
|
+
colorInput = El('input').attr('type', 'text').attr('aria-label', 'Scale bar color')
|
|
35794
36428
|
.on('change', function() {
|
|
35795
36429
|
var color = colorInput.node().value.trim();
|
|
35796
36430
|
if (!color) {
|
|
@@ -35816,7 +36450,7 @@
|
|
|
35816
36450
|
'scalebar-bar-width');
|
|
35817
36451
|
marginInput = makeNumberInput(makeCell(row, 'Inset'), 'margin',
|
|
35818
36452
|
'scalebar-margin')
|
|
35819
|
-
.attr('
|
|
36453
|
+
.attr('aria-label', 'Distance from the edges of the frame, in pixels');
|
|
35820
36454
|
|
|
35821
36455
|
row = El('div').addClass('label-style-row label-panel-button-row').appendTo(form);
|
|
35822
36456
|
makePanelActionButton(row, 'Remove scale bar', function() {
|
|
@@ -37117,6 +37751,25 @@
|
|
|
37117
37751
|
})
|
|
37118
37752
|
};
|
|
37119
37753
|
},
|
|
37754
|
+
// The vertices of each part of each feature in a path layer, in pixels
|
|
37755
|
+
// from the top left of the map, for a test comparing a drawn path with the
|
|
37756
|
+
// pointer positions that drew it.
|
|
37757
|
+
getLayerPathPixels: function(name) {
|
|
37758
|
+
var o = gui.model.getLayers().filter(function(o) {
|
|
37759
|
+
return getLayerName(o.layer) === name;
|
|
37760
|
+
})[0];
|
|
37761
|
+
var ext = gui.map.getExtent();
|
|
37762
|
+
var arcs;
|
|
37763
|
+
if (!o) return null;
|
|
37764
|
+
arcs = o.layer.gui ? o.layer.gui.displayArcs : o.dataset.arcs;
|
|
37765
|
+
return (o.layer.shapes || []).map(function(shp) {
|
|
37766
|
+
return shp ? shp.map(function(ids) {
|
|
37767
|
+
var iter = arcs.getShapeIter(ids), pts = [];
|
|
37768
|
+
while (iter.hasNext()) pts.push(ext.translateCoords(iter.x, iter.y));
|
|
37769
|
+
return pts;
|
|
37770
|
+
}) : null;
|
|
37771
|
+
});
|
|
37772
|
+
},
|
|
37120
37773
|
selectLayer: function(name) {
|
|
37121
37774
|
var target = gui.model.getLayers().filter(function(o) {
|
|
37122
37775
|
return getLayerName(o.layer) === name;
|
|
@@ -37134,6 +37787,19 @@
|
|
|
37134
37787
|
};
|
|
37135
37788
|
});
|
|
37136
37789
|
},
|
|
37790
|
+
// What each overlay layer last drew: the ids of its features, and whether
|
|
37791
|
+
// it marked their vertices. The overlay is drawn to canvas, so there is no
|
|
37792
|
+
// DOM state to assert on.
|
|
37793
|
+
getOverlayInfo: function() {
|
|
37794
|
+
var layers = gui.map && gui.map.getOverlayLayers ?
|
|
37795
|
+
gui.map.getOverlayLayers() : [];
|
|
37796
|
+
return layers.map(function(lyr) {
|
|
37797
|
+
return {
|
|
37798
|
+
ids: lyr.gui.style.ids || null,
|
|
37799
|
+
vertices: !!lyr.gui.style.vertices
|
|
37800
|
+
};
|
|
37801
|
+
});
|
|
37802
|
+
},
|
|
37137
37803
|
// Knot handle positions, in display coordinates.
|
|
37138
37804
|
getLabelPathKnotCoords: function() {
|
|
37139
37805
|
var lyr = getLabelPathGuideLayers(gui).filter(function(lyr) {
|