mapshaper 0.7.68 → 0.7.69
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 +344 -54
- package/package.json +1 -1
- package/www/mapshaper-gui.js +850 -138
- package/www/mapshaper.js +344 -54
- package/www/page.css +101 -3
package/www/mapshaper-gui.js
CHANGED
|
@@ -3324,7 +3324,7 @@
|
|
|
3324
3324
|
return str;
|
|
3325
3325
|
}
|
|
3326
3326
|
|
|
3327
|
-
function formatValue(val, matches) {
|
|
3327
|
+
function formatValue$2(val, matches) {
|
|
3328
3328
|
var flags = matches[1];
|
|
3329
3329
|
var padding = matches[2];
|
|
3330
3330
|
var decimals = matches[3] ? parseInt(matches[3].substr(1)) : void 0;
|
|
@@ -3420,7 +3420,7 @@
|
|
|
3420
3420
|
error$1("[format()] Data does not match format string; format:", fmt, "data:", arguments);
|
|
3421
3421
|
}
|
|
3422
3422
|
for (var i=0; i<n; i++) {
|
|
3423
|
-
str += formatValue(arguments[i], formatCodes[i]) + literals[i+1];
|
|
3423
|
+
str += formatValue$2(arguments[i], formatCodes[i]) + literals[i+1];
|
|
3424
3424
|
}
|
|
3425
3425
|
return str;
|
|
3426
3426
|
};
|
|
@@ -8623,10 +8623,10 @@
|
|
|
8623
8623
|
}
|
|
8624
8624
|
|
|
8625
8625
|
// Whether a layer can be shown on the map alongside the active layer, and so
|
|
8626
|
-
// has an eye icon in the layers panel. A frame
|
|
8627
|
-
// data-only layer has nothing to draw.
|
|
8626
|
+
// has an eye icon in the layers panel. A frame and its furniture are drawn by
|
|
8627
|
+
// preview mode, and a data-only layer has nothing to draw.
|
|
8628
8628
|
function layerIsPinnable(lyr, dataset) {
|
|
8629
|
-
if (
|
|
8629
|
+
if (internal.isFrameComponentLayer(lyr, dataset)) return false;
|
|
8630
8630
|
return internal.layerIsGeometric(lyr) || internal.layerHasRaster(lyr) ||
|
|
8631
8631
|
internal.layerHasFurniture(lyr);
|
|
8632
8632
|
}
|
|
@@ -10888,6 +10888,9 @@
|
|
|
10888
10888
|
if (opts.format == 'svg' || opts.format == 'topojson') {
|
|
10889
10889
|
opts.gui_frame = getGuiFrameContext();
|
|
10890
10890
|
}
|
|
10891
|
+
if (opts.format == 'svg') {
|
|
10892
|
+
targets = addFrameFurnitureTargets(targets, opts.gui_frame);
|
|
10893
|
+
}
|
|
10891
10894
|
try {
|
|
10892
10895
|
var files = await internal.exportTargetLayers(model, targets, opts);
|
|
10893
10896
|
} catch(e) {
|
|
@@ -10953,7 +10956,7 @@
|
|
|
10953
10956
|
function initLayerMenu() {
|
|
10954
10957
|
var list = menu.findChild('.export-layer-list').empty();
|
|
10955
10958
|
var layers = model.getLayers().filter(function(o) {
|
|
10956
|
-
return !internal.
|
|
10959
|
+
return !internal.isFrameComponentLayer(o.layer, o.dataset);
|
|
10957
10960
|
});
|
|
10958
10961
|
sortLayersForMenuDisplay(layers);
|
|
10959
10962
|
|
|
@@ -11147,21 +11150,42 @@
|
|
|
11147
11150
|
};
|
|
11148
11151
|
}
|
|
11149
11152
|
|
|
11153
|
+
// Snapshots keep the frame and its furniture
|
|
11150
11154
|
function addFrameTarget(targets) {
|
|
11151
11155
|
var frame = internal.getActiveFrame(model);
|
|
11152
|
-
var group;
|
|
11153
11156
|
if (!frame) return targets;
|
|
11157
|
+
return addDatasetTargets(targets, frame.dataset,
|
|
11158
|
+
[frame.layer].concat(internal.getFrameFurnitureLayers(frame.dataset)));
|
|
11159
|
+
}
|
|
11160
|
+
|
|
11161
|
+
// The frame's furniture is drawn in SVG output, the way it is in preview.
|
|
11162
|
+
// An unprojected frame can't show a scalebar, so it doesn't block the export.
|
|
11163
|
+
function addFrameFurnitureTargets(targets, frameContext) {
|
|
11164
|
+
var frame = internal.getActiveFrame(model);
|
|
11165
|
+
var furniture = frame ? internal.getFrameFurnitureLayers(frame.dataset) : [];
|
|
11166
|
+
if (furniture.length === 0) return targets;
|
|
11167
|
+
if (internal.getFurnitureFrameProblem(frameContext && frameContext.data)) {
|
|
11168
|
+
console.warn('Map furniture was not exported: the map frame is unprojected');
|
|
11169
|
+
return targets;
|
|
11170
|
+
}
|
|
11171
|
+
return addDatasetTargets(targets, frame.dataset, furniture);
|
|
11172
|
+
}
|
|
11173
|
+
|
|
11174
|
+
function addDatasetTargets(targets, dataset, layers) {
|
|
11175
|
+
var group;
|
|
11154
11176
|
targets = targets.map(function(target) {
|
|
11155
11177
|
return Object.assign({}, target, {layers: target.layers.slice()});
|
|
11156
11178
|
});
|
|
11157
11179
|
group = targets.find(function(target) {
|
|
11158
|
-
return target.dataset ==
|
|
11180
|
+
return target.dataset == dataset;
|
|
11159
11181
|
});
|
|
11160
|
-
if (group) {
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
targets.push({dataset: frame.dataset, layers: [frame.layer]});
|
|
11182
|
+
if (!group) {
|
|
11183
|
+
group = {dataset: dataset, layers: []};
|
|
11184
|
+
targets.push(group);
|
|
11164
11185
|
}
|
|
11186
|
+
layers.forEach(function(lyr) {
|
|
11187
|
+
if (!group.layers.includes(lyr)) group.layers.push(lyr);
|
|
11188
|
+
});
|
|
11165
11189
|
return targets;
|
|
11166
11190
|
}
|
|
11167
11191
|
|
|
@@ -11456,6 +11480,9 @@
|
|
|
11456
11480
|
if (e.frameProperties) {
|
|
11457
11481
|
addMenuItem('frame properties', e.frameProperties, '');
|
|
11458
11482
|
}
|
|
11483
|
+
if (e.scalebarProperties) {
|
|
11484
|
+
addMenuItem(e.scalebarLabel || 'scale bar', e.scalebarProperties, '');
|
|
11485
|
+
}
|
|
11459
11486
|
if (e.resizeFrame) {
|
|
11460
11487
|
addMenuItem('resize frame', e.resizeFrame, '');
|
|
11461
11488
|
}
|
|
@@ -11883,6 +11910,8 @@
|
|
|
11883
11910
|
sortLayersForMenuDisplay(model.getLayers()).forEach(function(o) {
|
|
11884
11911
|
var lyr = o.layer;
|
|
11885
11912
|
var isFrame = internal.isFrameLayer(lyr, o.dataset.arcs);
|
|
11913
|
+
// furniture is shown as part of its frame's entry
|
|
11914
|
+
if (!isFrame && internal.isFrameComponentLayer(lyr, o.dataset)) return;
|
|
11886
11915
|
var opts = {
|
|
11887
11916
|
show_source: layerCount < 5,
|
|
11888
11917
|
pinnable: pinnableCount > 0 && isPinnable(lyr, o.dataset)
|
|
@@ -11982,25 +12011,39 @@
|
|
|
11982
12011
|
function deleteLayer() {
|
|
11983
12012
|
var target = findLayerById(id);
|
|
11984
12013
|
var undoTransaction;
|
|
12014
|
+
var furniture;
|
|
11985
12015
|
if (!target) return;
|
|
12016
|
+
// a frame's furniture goes with it, rather than becoming a stray layer
|
|
12017
|
+
furniture = internal.isFrameLayer(target.layer, target.dataset.arcs) ?
|
|
12018
|
+
internal.getFrameFurnitureLayers(target.dataset) : [];
|
|
11986
12019
|
undoTransaction = createUndoTransaction(gui, 'delete layer');
|
|
11987
12020
|
if (map.isVisibleLayer(target.layer)) {
|
|
11988
12021
|
// TODO: check for double map refresh after model.deleteLayer() below
|
|
11989
12022
|
setLayerPinning(target.layer, false);
|
|
11990
12023
|
}
|
|
11991
12024
|
if (undoTransaction) {
|
|
11992
|
-
undoTransaction.run(
|
|
11993
|
-
model.deleteLayer(target.layer, target.dataset);
|
|
11994
|
-
});
|
|
12025
|
+
undoTransaction.run(deleteLayers);
|
|
11995
12026
|
addUndoTransactionToHistory(gui, undoTransaction, {
|
|
11996
12027
|
flags: {select: true, arc_count: true},
|
|
11997
12028
|
entryPrefix: 'delete-layer'
|
|
11998
12029
|
});
|
|
11999
12030
|
} else {
|
|
12031
|
+
deleteLayers();
|
|
12032
|
+
}
|
|
12033
|
+
|
|
12034
|
+
function deleteLayers() {
|
|
12035
|
+
furniture.forEach(function(lyr) {
|
|
12036
|
+
model.deleteLayer(lyr, target.dataset);
|
|
12037
|
+
});
|
|
12000
12038
|
model.deleteLayer(target.layer, target.dataset);
|
|
12001
12039
|
}
|
|
12002
12040
|
}
|
|
12003
12041
|
|
|
12042
|
+
function openScalebarProperties() {
|
|
12043
|
+
var target = findLayerById(id);
|
|
12044
|
+
if (target && gui.scalebarProperties) gui.scalebarProperties.open(target);
|
|
12045
|
+
}
|
|
12046
|
+
|
|
12004
12047
|
// Duplicate a layer by running an equivalent command, so the copy is
|
|
12005
12048
|
// recorded in session history and can be undone. Uses '-filter true +'
|
|
12006
12049
|
// to append a copy without replacing the source, keeps the same name
|
|
@@ -12088,6 +12131,10 @@
|
|
|
12088
12131
|
internal.isFrameLayer(target.layer, target.dataset.arcs);
|
|
12089
12132
|
if (isFrame) {
|
|
12090
12133
|
menuEvent.frameProperties = openFrameProperties;
|
|
12134
|
+
menuEvent.scalebarProperties = openScalebarProperties;
|
|
12135
|
+
menuEvent.scalebarLabel =
|
|
12136
|
+
internal.findFrameFurnitureLayer(target.dataset, 'scalebar') ?
|
|
12137
|
+
'scale bar' : 'add scale bar';
|
|
12091
12138
|
menuEvent.resizeFrame = resizeFrame;
|
|
12092
12139
|
menuEvent.deleteFrame = deleteLayer;
|
|
12093
12140
|
} else {
|
|
@@ -16612,20 +16659,43 @@
|
|
|
16612
16659
|
|
|
16613
16660
|
// The fraction an opacity control's contents mean ("50%", " 50 " -> 0.5), or
|
|
16614
16661
|
// null if it is not holding a number. Out-of-range values are clamped rather
|
|
16615
|
-
// than refused: a pasted 150% is an intent, not a mistake.
|
|
16662
|
+
// than refused: a pasted 150% is an intent, not a mistake. A blank field is
|
|
16663
|
+
// null, not zero: it is what a colour with no value shows beside it.
|
|
16616
16664
|
function parseOpacityValue(str) {
|
|
16617
|
-
var
|
|
16618
|
-
|
|
16665
|
+
var txt = String(str).replace('%', '').trim();
|
|
16666
|
+
var pct = Number(txt);
|
|
16667
|
+
if (txt === '' || !isFinite(pct)) return null;
|
|
16619
16668
|
return Math.max(0, Math.min(100, pct)) / 100;
|
|
16620
16669
|
}
|
|
16621
16670
|
|
|
16671
|
+
// A typed dash pattern in the form -style stroke-dasharray= takes: lengths
|
|
16672
|
+
// separated by single spaces. Commas and runs of whitespace are accepted
|
|
16673
|
+
// because SVG accepts them, and a pasted "4, 2" means the same as "4 2".
|
|
16674
|
+
// Returns '' for a blank field. Does not check that the lengths are numbers.
|
|
16675
|
+
function normalizeDashArrayInput(str) {
|
|
16676
|
+
return String(str).trim().split(/[\s,]+/).filter(Boolean).join(' ');
|
|
16677
|
+
}
|
|
16678
|
+
|
|
16622
16679
|
// A stored fraction as the percentage a control shows, or '' for no value --
|
|
16623
16680
|
// which is how a control over a selection that does not agree shows.
|
|
16624
16681
|
function formatOpacityPct(val) {
|
|
16682
|
+
if (isUnsetValue(val)) return '';
|
|
16625
16683
|
val = Number(val);
|
|
16626
16684
|
return isFinite(val) ? Math.round(Math.max(0, Math.min(1, val)) * 100) + '%' : '';
|
|
16627
16685
|
}
|
|
16628
16686
|
|
|
16687
|
+
// The opacity shown beside a colour. A colour with no opacity of its own is
|
|
16688
|
+
// drawn opaque, and says so; with no colour either, there is nothing for an
|
|
16689
|
+
// opacity to apply to, and the field is blank like the colour.
|
|
16690
|
+
function formatColorOpacityPct(opacity, hasColor) {
|
|
16691
|
+
if (isUnsetValue(opacity)) return hasColor ? '100%' : '';
|
|
16692
|
+
return formatOpacityPct(opacity);
|
|
16693
|
+
}
|
|
16694
|
+
|
|
16695
|
+
function isUnsetValue(val) {
|
|
16696
|
+
return val === undefined || val === null || val === '';
|
|
16697
|
+
}
|
|
16698
|
+
|
|
16629
16699
|
// The parts the style panels are built from. They were the label panel's, and
|
|
16630
16700
|
// three panels drawing the same control three ways is how they came to look
|
|
16631
16701
|
// like three programs: a colour was a swatch inside a field in one and a
|
|
@@ -16685,6 +16755,16 @@
|
|
|
16685
16755
|
return btn;
|
|
16686
16756
|
}
|
|
16687
16757
|
|
|
16758
|
+
// The "?" the rest of the app uses for field help (see .tip-button in
|
|
16759
|
+
// elements.css and the static ones in index.html). The bubble is white-space:
|
|
16760
|
+
// pre, so the line breaks in the text are the ones it gets.
|
|
16761
|
+
function makeFieldTip(parent, text) {
|
|
16762
|
+
var btn = El('div').addClass('tip-button').appendTo(parent).text('?');
|
|
16763
|
+
var anchor = El('div').addClass('tip-anchor').appendTo(btn);
|
|
16764
|
+
El('div').addClass('tip').appendTo(anchor).text(text);
|
|
16765
|
+
return btn;
|
|
16766
|
+
}
|
|
16767
|
+
|
|
16688
16768
|
function setPanelButtonDisabled(el, disabled) {
|
|
16689
16769
|
el.classed('disabled', !!disabled)
|
|
16690
16770
|
.attr('aria-disabled', disabled ? 'true' : 'false');
|
|
@@ -19555,12 +19635,12 @@
|
|
|
19555
19635
|
}
|
|
19556
19636
|
|
|
19557
19637
|
var savedStylesKey = 'layer_style_presets';
|
|
19558
|
-
var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'fill', 'fill-opacity'];
|
|
19638
|
+
var styleFields = ['stroke', 'stroke-width', 'stroke-opacity', 'stroke-dasharray', 'fill', 'fill-opacity'];
|
|
19559
19639
|
|
|
19560
19640
|
function LayerStyleTool(gui) {
|
|
19561
19641
|
var parent = gui.container.findChild('.mshp-main-map');
|
|
19562
19642
|
var panel = El('div').addClass('label-style-panel layer-style-panel rollover').appendTo(parent).hide();
|
|
19563
|
-
var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthField, randomFillBtn, presetControl, hit;
|
|
19643
|
+
var title, editingStatus, clearLink, strokeControl, fillControl, strokeWidthField, dashControl, randomFillBtn, presetControl, hit;
|
|
19564
19644
|
var targetLayer = null;
|
|
19565
19645
|
|
|
19566
19646
|
initPanel();
|
|
@@ -19646,6 +19726,7 @@
|
|
|
19646
19726
|
fillControl = addColorControl(panel, 'Fill', 'fill', '');
|
|
19647
19727
|
strokeControl = addColorControl(panel, 'Stroke', 'stroke', '#000000');
|
|
19648
19728
|
strokeWidthField = addStrokeWidthControl(strokeControl.aside);
|
|
19729
|
+
dashControl = addDashArrayControl(panel);
|
|
19649
19730
|
|
|
19650
19731
|
var buttonRow = El('div').addClass('label-style-row label-panel-button-row').appendTo(panel);
|
|
19651
19732
|
randomFillBtn = makePanelActionButton(buttonRow, 'Random fills', applyRandomFillColors);
|
|
@@ -19706,6 +19787,29 @@
|
|
|
19706
19787
|
});
|
|
19707
19788
|
}
|
|
19708
19789
|
|
|
19790
|
+
// A literal stroke-dasharray, in the wide column so that it lines up with
|
|
19791
|
+
// the stroke's colour above it. A blank field makes the line solid again.
|
|
19792
|
+
function addDashArrayControl(parent) {
|
|
19793
|
+
var row = El('div').addClass('label-style-row label-split-row layer-dash-row').appendTo(parent);
|
|
19794
|
+
var cell = El('div').addClass('label-split-cell').appendTo(row);
|
|
19795
|
+
El('div').addClass('label-split-cell').appendTo(row);
|
|
19796
|
+
var caption = El('div').addClass('label-style-row-label').appendTo(cell).text('Dashes');
|
|
19797
|
+
makeFieldTip(caption,
|
|
19798
|
+
'Dash and gap lengths in pixels, separated\n' +
|
|
19799
|
+
'by spaces. "4" gives 4px dashes and 4px gaps.\n' +
|
|
19800
|
+
'"6 3" gives 6px dashes and 3px gaps.');
|
|
19801
|
+
var input = El('input').attr('type', 'text').appendTo(cell)
|
|
19802
|
+
.on('change', function() {
|
|
19803
|
+
var value = normalizeDashArrayInput(input.node().value);
|
|
19804
|
+
if (value && internal.parseStyleLiteral('stroke-dasharray', value) === undefined) {
|
|
19805
|
+
updateControls();
|
|
19806
|
+
return;
|
|
19807
|
+
}
|
|
19808
|
+
applyDashArrayStyle(value);
|
|
19809
|
+
});
|
|
19810
|
+
return {row: row, input: input};
|
|
19811
|
+
}
|
|
19812
|
+
|
|
19709
19813
|
function releaseFocus() {
|
|
19710
19814
|
releasePanelFocus(panel.node());
|
|
19711
19815
|
}
|
|
@@ -19719,9 +19823,11 @@
|
|
|
19719
19823
|
updateEditingStatus(manualIds.length);
|
|
19720
19824
|
strokeControl.row.show();
|
|
19721
19825
|
fillControl.row.classed('hidden', geom != 'polygon');
|
|
19826
|
+
dashControl.row.classed('hidden', geom != 'polyline');
|
|
19722
19827
|
updateColorControl(strokeControl);
|
|
19723
19828
|
updateColorControl(fillControl);
|
|
19724
19829
|
updateStrokeWidthControl();
|
|
19830
|
+
updateDashArrayControl();
|
|
19725
19831
|
randomFillBtn.classed('hidden', geom != 'polygon');
|
|
19726
19832
|
presetControl.render();
|
|
19727
19833
|
updateSavedStyleControls();
|
|
@@ -19735,10 +19841,13 @@
|
|
|
19735
19841
|
if (!isHexColor(value)) control.picker.hide();
|
|
19736
19842
|
}
|
|
19737
19843
|
|
|
19844
|
+
// A selection whose colours disagree still has colours, so an opacity unset
|
|
19845
|
+
// on all of it shows as full rather than blank.
|
|
19738
19846
|
function updateOpacityControl(control) {
|
|
19739
|
-
var
|
|
19740
|
-
control.opacity.node().value =
|
|
19741
|
-
|
|
19847
|
+
var field = control.field + '-opacity';
|
|
19848
|
+
control.opacity.node().value = styleFieldIsUnsetForTargets(field) ?
|
|
19849
|
+
formatColorOpacityPct(null, !styleFieldIsUnsetForTargets(control.field)) :
|
|
19850
|
+
formatOpacityPct(getCommonStyleValue(field));
|
|
19742
19851
|
}
|
|
19743
19852
|
|
|
19744
19853
|
function updateStrokeWidthControl() {
|
|
@@ -19748,6 +19857,11 @@
|
|
|
19748
19857
|
getDefaultStrokeWidth() : value));
|
|
19749
19858
|
}
|
|
19750
19859
|
|
|
19860
|
+
function updateDashArrayControl() {
|
|
19861
|
+
var value = getCommonStyleValue('stroke-dasharray');
|
|
19862
|
+
dashControl.input.node().value = value === undefined || value === null ? '' : String(value);
|
|
19863
|
+
}
|
|
19864
|
+
|
|
19751
19865
|
function nudgeStrokeWidth(direction) {
|
|
19752
19866
|
var value = Number(getCommonStyleValue('stroke-width'));
|
|
19753
19867
|
if (!isFinite(value)) value = getDefaultStrokeWidth();
|
|
@@ -19789,6 +19903,14 @@
|
|
|
19789
19903
|
runStyleCommand(styles);
|
|
19790
19904
|
}
|
|
19791
19905
|
|
|
19906
|
+
function applyDashArrayStyle(value) {
|
|
19907
|
+
var styles = [['stroke-dasharray', value]];
|
|
19908
|
+
if (value && styleFieldIsUnsetForTargets('stroke')) {
|
|
19909
|
+
styles.push(['stroke', strokeControl.defaultColor]);
|
|
19910
|
+
}
|
|
19911
|
+
runStyleCommand(styles);
|
|
19912
|
+
}
|
|
19913
|
+
|
|
19792
19914
|
function runStyleCommand(styles) {
|
|
19793
19915
|
var parts = ['-style'];
|
|
19794
19916
|
releaseFocus();
|
|
@@ -19850,6 +19972,9 @@
|
|
|
19850
19972
|
addStyleValue(style, 'stroke', getControlValue(strokeControl.input));
|
|
19851
19973
|
addStyleValue(style, 'stroke-width', strokeWidthField.getValue());
|
|
19852
19974
|
addStyleValue(style, 'stroke-opacity', parseOpacityValue(strokeControl.opacity.node().value));
|
|
19975
|
+
if (targetLayer && targetLayer.geometry_type == 'polyline') {
|
|
19976
|
+
addStyleValue(style, 'stroke-dasharray', getControlValue(dashControl.input));
|
|
19977
|
+
}
|
|
19853
19978
|
if (targetLayer && targetLayer.geometry_type == 'polygon') {
|
|
19854
19979
|
addStyleValue(style, 'fill', getControlValue(fillControl.input));
|
|
19855
19980
|
addStyleValue(style, 'fill-opacity', parseOpacityValue(fillControl.opacity.node().value));
|
|
@@ -20260,27 +20385,42 @@
|
|
|
20260
20385
|
var radius = getCommonValue('r');
|
|
20261
20386
|
var fill = getCommonValue('fill');
|
|
20262
20387
|
var stroke = getCommonValue('stroke');
|
|
20263
|
-
var fillOpacity = getCommonValue('fill-opacity');
|
|
20264
|
-
var strokeOpacity = getCommonValue('stroke-opacity');
|
|
20265
20388
|
var strokeWidth = getCommonValue('stroke-width');
|
|
20266
20389
|
setCircleRadius(radius);
|
|
20267
20390
|
setCircleColor(circleFillControl, fill);
|
|
20268
20391
|
setCircleColor(circleStrokeControl, stroke);
|
|
20269
|
-
|
|
20270
|
-
|
|
20392
|
+
updateCircleOpacity(circleFillControl);
|
|
20393
|
+
updateCircleOpacity(circleStrokeControl);
|
|
20271
20394
|
setCircleStrokeWidth(strokeWidth === '' ? 0 : strokeWidth);
|
|
20272
20395
|
if (representation != 'circle' && representation != 'unstyled') {
|
|
20273
20396
|
setCircleRadius(defaultCircleRadius);
|
|
20274
20397
|
setCircleColor(circleFillControl, '');
|
|
20275
20398
|
setCircleColor(circleStrokeControl, '');
|
|
20276
|
-
|
|
20277
|
-
|
|
20399
|
+
circleFillControl.opacity.node().value = '';
|
|
20400
|
+
circleStrokeControl.opacity.node().value = '';
|
|
20278
20401
|
setCircleStrokeWidth(0);
|
|
20279
20402
|
}
|
|
20280
20403
|
}
|
|
20281
20404
|
|
|
20282
|
-
|
|
20283
|
-
|
|
20405
|
+
// The opacity to put in the style command: undefined to leave the property
|
|
20406
|
+
// as it is, null for a field holding something that is not an opacity.
|
|
20407
|
+
// A blank field has nothing to say, and 100% over an unset opacity is the
|
|
20408
|
+
// default being shown rather than a value to store.
|
|
20409
|
+
function getCircleOpacityToWrite(control) {
|
|
20410
|
+
var str = control.opacity.node().value.trim();
|
|
20411
|
+
var val = parseOpacityValue(str);
|
|
20412
|
+
if (str === '') return undefined;
|
|
20413
|
+
if (val === 1 && styleFieldIsUnset(control.field + '-opacity')) return undefined;
|
|
20414
|
+
return val;
|
|
20415
|
+
}
|
|
20416
|
+
|
|
20417
|
+
// A selection whose colours disagree still has colours, so an opacity unset
|
|
20418
|
+
// on all of it shows as full rather than blank.
|
|
20419
|
+
function updateCircleOpacity(control) {
|
|
20420
|
+
var field = control.field + '-opacity';
|
|
20421
|
+
control.opacity.node().value = styleFieldIsUnset(field) ?
|
|
20422
|
+
formatColorOpacityPct(null, !styleFieldIsUnset(control.field)) :
|
|
20423
|
+
formatOpacityPct(getCommonValue(field));
|
|
20284
20424
|
}
|
|
20285
20425
|
|
|
20286
20426
|
function renderCreateFields() {
|
|
@@ -20331,16 +20471,16 @@
|
|
|
20331
20471
|
function createSimpleCircles() {
|
|
20332
20472
|
runCommand('-style r=' + defaultCreatedCircleRadius +
|
|
20333
20473
|
' fill=' + quoteCommandValue(defaultCircleFill) +
|
|
20334
|
-
'
|
|
20474
|
+
' stroke-width=0', 'Create circles');
|
|
20335
20475
|
}
|
|
20336
20476
|
|
|
20337
20477
|
function applyCircleStyles() {
|
|
20338
20478
|
var representation = getPointRepresentation();
|
|
20339
20479
|
var radius = getCircleRadius();
|
|
20340
20480
|
var fill = circleFillControl.input.node().value.trim();
|
|
20341
|
-
var fillOpacity =
|
|
20481
|
+
var fillOpacity = getCircleOpacityToWrite(circleFillControl);
|
|
20342
20482
|
var stroke = circleStrokeControl.input.node().value.trim();
|
|
20343
|
-
var strokeOpacity =
|
|
20483
|
+
var strokeOpacity = getCircleOpacityToWrite(circleStrokeControl);
|
|
20344
20484
|
var strokeWidth = getCircleStrokeWidth();
|
|
20345
20485
|
var args;
|
|
20346
20486
|
if (!gui.console || !(representation == 'unstyled' || representation == 'circle') ||
|
|
@@ -20349,11 +20489,9 @@
|
|
|
20349
20489
|
fill = defaultCircleFill;
|
|
20350
20490
|
setCircleColor(circleFillControl, fill);
|
|
20351
20491
|
}
|
|
20352
|
-
args = [
|
|
20353
|
-
|
|
20354
|
-
|
|
20355
|
-
'stroke-width=' + strokeWidth
|
|
20356
|
-
];
|
|
20492
|
+
args = ['stroke-width=' + strokeWidth];
|
|
20493
|
+
if (fillOpacity !== undefined) args.push('fill-opacity=' + fillOpacity);
|
|
20494
|
+
if (strokeOpacity !== undefined) args.push('stroke-opacity=' + strokeOpacity);
|
|
20357
20495
|
if (radius !== null) args.unshift('r=' + radius);
|
|
20358
20496
|
if (fill) args.push('fill=' + quoteCommandValue(fill));
|
|
20359
20497
|
if (stroke) args.push('stroke=' + quoteCommandValue(stroke));
|
|
@@ -20580,7 +20718,7 @@
|
|
|
20580
20718
|
var setDefaultTargets = self.setDefaultTargets;
|
|
20581
20719
|
utils$1.extend(self, EventDispatcher.prototype);
|
|
20582
20720
|
|
|
20583
|
-
// A map frame is composition state, not editable content. Commands may
|
|
20721
|
+
// A map frame (and its furniture) is composition state, not editable content. Commands may
|
|
20584
20722
|
// explicitly target it, but it must not replace the GUI's active content
|
|
20585
20723
|
// layer or become the implicit target of the next console command.
|
|
20586
20724
|
self.setDefaultTargets = function(targets, opts) {
|
|
@@ -20647,15 +20785,25 @@
|
|
|
20647
20785
|
};
|
|
20648
20786
|
|
|
20649
20787
|
self.selectNextLayer = function() {
|
|
20650
|
-
var next = self.findNextLayer
|
|
20788
|
+
var next = findContentLayer(self.findNextLayer);
|
|
20651
20789
|
if (next) self.selectLayer(next.layer, next.dataset);
|
|
20652
20790
|
};
|
|
20653
20791
|
|
|
20654
20792
|
self.selectPrevLayer = function() {
|
|
20655
|
-
var prev = self.findPrevLayer
|
|
20793
|
+
var prev = findContentLayer(self.findPrevLayer);
|
|
20656
20794
|
if (prev) self.selectLayer(prev.layer, prev.dataset);
|
|
20657
20795
|
};
|
|
20658
20796
|
|
|
20797
|
+
// step past the frame and its furniture, which can't be selected
|
|
20798
|
+
function findContentLayer(step) {
|
|
20799
|
+
var start = self.getActiveLayer().layer;
|
|
20800
|
+
var o = step.call(self, start);
|
|
20801
|
+
while (o && o.layer != start && internal.isFrameComponentLayer(o.layer, o.dataset)) {
|
|
20802
|
+
o = step.call(self, o.layer);
|
|
20803
|
+
}
|
|
20804
|
+
return o;
|
|
20805
|
+
}
|
|
20806
|
+
|
|
20659
20807
|
return self;
|
|
20660
20808
|
|
|
20661
20809
|
function getContentTargets(targets) {
|
|
@@ -20663,7 +20811,7 @@
|
|
|
20663
20811
|
return {
|
|
20664
20812
|
dataset: target.dataset,
|
|
20665
20813
|
layers: target.layers.filter(function(lyr) {
|
|
20666
|
-
return !internal.
|
|
20814
|
+
return !internal.isFrameComponentLayer(lyr, target.dataset);
|
|
20667
20815
|
})
|
|
20668
20816
|
};
|
|
20669
20817
|
}).filter(function(target) {
|
|
@@ -20673,7 +20821,7 @@
|
|
|
20673
20821
|
|
|
20674
20822
|
function getFirstContentTarget() {
|
|
20675
20823
|
var target = self.getLayers().find(function(o) {
|
|
20676
|
-
return !internal.
|
|
20824
|
+
return !internal.isFrameComponentLayer(o.layer, o.dataset);
|
|
20677
20825
|
});
|
|
20678
20826
|
return target ? [{dataset: target.dataset, layers: [target.layer]}] : [];
|
|
20679
20827
|
}
|
|
@@ -23056,15 +23204,22 @@
|
|
|
23056
23204
|
this.zoomToBbox = zoomToBbox;
|
|
23057
23205
|
|
|
23058
23206
|
if (gui.options.homeControl) {
|
|
23059
|
-
gui.buttons.addButton("#home-icon")
|
|
23060
|
-
|
|
23061
|
-
|
|
23062
|
-
|
|
23207
|
+
gui.buttons.addButton("#home-icon")
|
|
23208
|
+
.attr('data-tooltip', 'Zoom to fit')
|
|
23209
|
+
.attr('aria-label', 'Zoom to fit')
|
|
23210
|
+
.on('click', function() {
|
|
23211
|
+
if (disabled()) return;
|
|
23212
|
+
gui.dispatchEvent('map_reset');
|
|
23213
|
+
});
|
|
23063
23214
|
}
|
|
23064
23215
|
|
|
23065
23216
|
if (gui.options.zoomControl) {
|
|
23066
|
-
inBtn = gui.buttons.addButton("#zoom-in-icon")
|
|
23067
|
-
|
|
23217
|
+
inBtn = gui.buttons.addButton("#zoom-in-icon")
|
|
23218
|
+
.attr('data-tooltip', 'Zoom in')
|
|
23219
|
+
.attr('aria-label', 'Zoom in');
|
|
23220
|
+
outBtn = gui.buttons.addButton("#zoom-out-icon")
|
|
23221
|
+
.attr('data-tooltip', 'Zoom out')
|
|
23222
|
+
.attr('aria-label', 'Zoom out');
|
|
23068
23223
|
initVariableClick(inBtn.node(), zoomIn);
|
|
23069
23224
|
initVariableClick(outBtn.node(), zoomOut);
|
|
23070
23225
|
ext.on('change', function() {
|
|
@@ -29341,6 +29496,7 @@
|
|
|
29341
29496
|
referenceStyle = { // outline style for reference layers
|
|
29342
29497
|
type: 'outline',
|
|
29343
29498
|
strokeColors: [null, '#87b73b'], // was 78c110
|
|
29499
|
+
// strokeColors: [null, 'rgba(79,140,0,0.67)'],
|
|
29344
29500
|
strokeWidth: 0.85,
|
|
29345
29501
|
dotColor: "#73ba20",
|
|
29346
29502
|
dotSize: 1
|
|
@@ -30220,37 +30376,103 @@
|
|
|
30220
30376
|
}
|
|
30221
30377
|
|
|
30222
30378
|
|
|
30223
|
-
|
|
30224
|
-
|
|
30225
|
-
|
|
30226
|
-
|
|
30227
|
-
|
|
30379
|
+
// symbolScale: the preview symbol scale (1 outside preview). Pattern sizes are
|
|
30380
|
+
// in output pixels, like stroke widths, so the tile grows with the page.
|
|
30381
|
+
// anchor: canvas-pixel position the pattern origin is pinned to, so the tile
|
|
30382
|
+
// travels with the map instead of sticking to the viewport. Panning only
|
|
30383
|
+
// updates this matrix. The tile image is rebuilt when the scale changes,
|
|
30384
|
+
// which is at most once per pattern per render.
|
|
30385
|
+
function getCanvasFillPattern(style, symbolScale, anchor) {
|
|
30386
|
+
var scale = symbolScale > 0 ? symbolScale : 1;
|
|
30387
|
+
var entry = hatches[style.fillPattern];
|
|
30388
|
+
if (!entry || entry.scale != scale) {
|
|
30389
|
+
entry = makePatternEntry(style, scale);
|
|
30390
|
+
hatches[style.fillPattern] = entry;
|
|
30228
30391
|
}
|
|
30229
|
-
|
|
30392
|
+
if (!entry || !entry.pattern) return style.fill || '#000';
|
|
30393
|
+
if (anchor) applyPatternAnchor(entry, anchor);
|
|
30394
|
+
return entry.pattern;
|
|
30230
30395
|
}
|
|
30231
30396
|
|
|
30232
|
-
function
|
|
30397
|
+
function makePatternEntry(style, scale) {
|
|
30233
30398
|
var o = internal.parsePattern(style.fillPattern);
|
|
30234
30399
|
if (!o) return null;
|
|
30235
30400
|
var canv = document.createElement('canvas');
|
|
30236
30401
|
var ctx = canv.getContext('2d');
|
|
30237
|
-
var
|
|
30238
|
-
var
|
|
30239
|
-
|
|
30402
|
+
var k = GUI.getPixelRatio() * scale;
|
|
30403
|
+
var tw = o.tileSize[0], th = o.tileSize[1];
|
|
30404
|
+
// A canvas has whole-pixel dimensions, but the scaled tile generally does
|
|
30405
|
+
// not. The tile is drawn into the nearest whole-pixel canvas, and the
|
|
30406
|
+
// pattern transform stretches it back to its exact size so that the
|
|
30407
|
+
// repeat period doesn't drift across a large polygon.
|
|
30408
|
+
var w = Math.max(1, Math.round(tw * k));
|
|
30409
|
+
var h = Math.max(1, Math.round(th * k));
|
|
30240
30410
|
canv.setAttribute('width', w);
|
|
30241
30411
|
canv.setAttribute('height', h);
|
|
30412
|
+
ctx.scale(w / tw, h / th);
|
|
30242
30413
|
if (o.background) {
|
|
30243
30414
|
ctx.fillStyle = o.background;
|
|
30244
|
-
ctx.fillRect(0, 0,
|
|
30415
|
+
ctx.fillRect(0, 0, tw, th);
|
|
30245
30416
|
}
|
|
30246
|
-
if (o.type == 'dots' || o.type == 'squares') makeDotFill(o, ctx,
|
|
30247
|
-
if (o.type == 'dashes') makeDashFill(o, ctx,
|
|
30248
|
-
if (o.type == 'hatches') makeHatchFill(o, ctx,
|
|
30249
|
-
|
|
30250
|
-
|
|
30251
|
-
pattern.
|
|
30252
|
-
|
|
30253
|
-
|
|
30417
|
+
if (o.type == 'dots' || o.type == 'squares') makeDotFill(o, ctx, 1);
|
|
30418
|
+
if (o.type == 'dashes') makeDashFill(o, ctx, 1);
|
|
30419
|
+
if (o.type == 'hatches') makeHatchFill(o, ctx, 1);
|
|
30420
|
+
return {
|
|
30421
|
+
scale: scale,
|
|
30422
|
+
pattern: ctx.createPattern(canv, 'repeat'),
|
|
30423
|
+
rotation: o.rotation || 0,
|
|
30424
|
+
sx: tw * k / w,
|
|
30425
|
+
sy: th * k / h,
|
|
30426
|
+
w: w,
|
|
30427
|
+
h: h
|
|
30428
|
+
};
|
|
30429
|
+
}
|
|
30430
|
+
|
|
30431
|
+
// Pins the cached pattern to @anchor. Skipped when the view has not moved,
|
|
30432
|
+
// so a hover redraw does not touch the pattern.
|
|
30433
|
+
function applyPatternAnchor(entry, anchor) {
|
|
30434
|
+
if (entry.ax == anchor.x && entry.ay == anchor.y) return;
|
|
30435
|
+
entry.ax = anchor.x;
|
|
30436
|
+
entry.ay = anchor.y;
|
|
30437
|
+
var t = getPatternTransform(entry, anchor);
|
|
30438
|
+
entry.pattern.setTransform(new DOMMatrix([t.a, t.b, t.c, t.d, t.e, t.f]));
|
|
30439
|
+
}
|
|
30440
|
+
|
|
30441
|
+
// Matrix taking pattern-image pixels to canvas pixels: translate to the
|
|
30442
|
+
// anchor, then rotate, then scale. The anchor is folded into one tile so a
|
|
30443
|
+
// map origin far outside the viewport does not blow the matrix precision.
|
|
30444
|
+
function getPatternTransform(entry, anchor) {
|
|
30445
|
+
var p = wrapPatternAnchor(anchor.x, anchor.y, entry);
|
|
30446
|
+
var rad = (entry.rotation || 0) * Math.PI / 180;
|
|
30447
|
+
var c = Math.cos(rad);
|
|
30448
|
+
var s = Math.sin(rad);
|
|
30449
|
+
return {
|
|
30450
|
+
a: c * entry.sx,
|
|
30451
|
+
b: s * entry.sx,
|
|
30452
|
+
c: -s * entry.sy,
|
|
30453
|
+
d: c * entry.sy,
|
|
30454
|
+
e: p.x,
|
|
30455
|
+
f: p.y
|
|
30456
|
+
};
|
|
30457
|
+
}
|
|
30458
|
+
|
|
30459
|
+
// @entry.sx/sy scale image pixels to canvas pixels; the image repeats every
|
|
30460
|
+
// @entry.w by @entry.h pixels. Returns a point in the same place on the
|
|
30461
|
+
// pattern, within one tile of the origin.
|
|
30462
|
+
function wrapPatternAnchor(x, y, entry) {
|
|
30463
|
+
var rad = (entry.rotation || 0) * Math.PI / 180;
|
|
30464
|
+
var c = Math.cos(rad);
|
|
30465
|
+
var s = Math.sin(rad);
|
|
30466
|
+
var rx = c * x + s * y;
|
|
30467
|
+
var ry = -s * x + c * y;
|
|
30468
|
+
var qx = rx / entry.sx - Math.floor(rx / entry.sx / entry.w) * entry.w;
|
|
30469
|
+
var qy = ry / entry.sy - Math.floor(ry / entry.sy / entry.h) * entry.h;
|
|
30470
|
+
var lx = qx * entry.sx;
|
|
30471
|
+
var ly = qy * entry.sy;
|
|
30472
|
+
return {
|
|
30473
|
+
x: c * lx - s * ly,
|
|
30474
|
+
y: s * lx + c * ly
|
|
30475
|
+
};
|
|
30254
30476
|
}
|
|
30255
30477
|
|
|
30256
30478
|
function makeDashFill(o, ctx, res) {
|
|
@@ -30941,27 +31163,66 @@
|
|
|
30941
31163
|
return iter;
|
|
30942
31164
|
}
|
|
30943
31165
|
|
|
31166
|
+
// Same multiplier as ctx.lineWidth. Retina canvases are pixRatio times the
|
|
31167
|
+
// CSS pixel; preview mode multiplies styled strokes by the symbol scale.
|
|
31168
|
+
function getCanvasStrokeScale(pixRatio, lineScale) {
|
|
31169
|
+
return (pixRatio > 1 ? pixRatio : 1) * (lineScale || 1);
|
|
31170
|
+
}
|
|
31171
|
+
|
|
31172
|
+
// Scale a stroke-dasharray into canvas pixels. Scale 1 returns the authored
|
|
31173
|
+
// tokens, so a 100% preview and a non-preview view keep the pattern as stored.
|
|
31174
|
+
function scaleCanvasLineDash(lineDash, scale) {
|
|
31175
|
+
var parts = String(lineDash).split(' ');
|
|
31176
|
+
var out, i;
|
|
31177
|
+
if (!(scale > 0) || scale === 1) return parts;
|
|
31178
|
+
out = new Array(parts.length);
|
|
31179
|
+
for (i = 0; i < parts.length; i++) {
|
|
31180
|
+
out[i] = +parts[i] * scale;
|
|
31181
|
+
}
|
|
31182
|
+
return out;
|
|
31183
|
+
}
|
|
31184
|
+
|
|
31185
|
+
// Where a fill pattern's origin sits, in canvas pixels. In preview that is
|
|
31186
|
+
// the top-left of the page; otherwise the geographic origin, so a pan (which
|
|
31187
|
+
// only changes the translation) carries the pattern with the shapes.
|
|
31188
|
+
function getPatternAnchor(ext) {
|
|
31189
|
+
var t = ext.getTransform(GUI.getPixelRatio());
|
|
31190
|
+
var frame = ext.getFrameData();
|
|
31191
|
+
if (frame && frame.bbox) {
|
|
31192
|
+
return {
|
|
31193
|
+
x: frame.bbox[0] * t.mx + t.bx,
|
|
31194
|
+
y: frame.bbox[3] * t.my + t.by
|
|
31195
|
+
};
|
|
31196
|
+
}
|
|
31197
|
+
return {x: t.bx, y: t.by};
|
|
31198
|
+
}
|
|
31199
|
+
|
|
30944
31200
|
function getPathStart(ext, lineScale) {
|
|
30945
31201
|
var pixRatio = GUI.getPixelRatio();
|
|
30946
31202
|
if (!lineScale) lineScale = 1;
|
|
31203
|
+
// Same factor as lineWidth, so dash length and gap stay in proportion to
|
|
31204
|
+
// the stroke. In preview that factor includes the symbol scale; without it
|
|
31205
|
+
// the pattern stays a fixed screen size while the line grows.
|
|
31206
|
+
var strokeScale = getCanvasStrokeScale(pixRatio, lineScale);
|
|
31207
|
+
// The symbol scale itself, not lineScale: lineScale adds a bump to widen
|
|
31208
|
+
// hover outlines, which a fill has no reason to follow.
|
|
31209
|
+
var patternScale = ext.getSymbolScale() || 1;
|
|
31210
|
+
// Page corner in preview, so the pattern is fixed to the page the way the
|
|
31211
|
+
// exported SVG is. With no frame, the geographic origin, which is enough
|
|
31212
|
+
// for panning: the pattern is not scaled by the geographic zoom then.
|
|
31213
|
+
var patternAnchor = getPatternAnchor(ext);
|
|
30947
31214
|
return function(ctx, style) {
|
|
30948
31215
|
var strokeWidth;
|
|
30949
31216
|
ctx.beginPath();
|
|
30950
31217
|
if (style.strokeWidth > 0) {
|
|
30951
31218
|
strokeWidth = style.strokeWidth;
|
|
30952
|
-
if (pixRatio > 1) {
|
|
30953
|
-
// bump up thin lines on retina, but not to more than 1px
|
|
30954
|
-
// (tests on Chrome showed much faster rendering of 1px lines)
|
|
30955
|
-
// strokeWidth = strokeWidth < 1 ? 1 : strokeWidth * pixRatio;
|
|
30956
|
-
strokeWidth = strokeWidth * pixRatio;
|
|
30957
|
-
}
|
|
30958
31219
|
ctx.lineCap = style.lineCap || 'round';
|
|
30959
31220
|
ctx.lineJoin = style.lineJoin || 'round';
|
|
30960
|
-
ctx.lineWidth = strokeWidth *
|
|
31221
|
+
ctx.lineWidth = strokeWidth * strokeScale;
|
|
30961
31222
|
ctx.strokeStyle = style.strokeColor;
|
|
30962
31223
|
if (style.lineDash){
|
|
30963
31224
|
ctx.lineCap = 'butt';
|
|
30964
|
-
ctx.setLineDash(style.lineDash
|
|
31225
|
+
ctx.setLineDash(scaleCanvasLineDash(style.lineDash, strokeScale));
|
|
30965
31226
|
}
|
|
30966
31227
|
if (style.miterLimit) {
|
|
30967
31228
|
ctx.miterLimit = style.miterLimit;
|
|
@@ -30969,7 +31230,7 @@
|
|
|
30969
31230
|
}
|
|
30970
31231
|
|
|
30971
31232
|
if (style.fillPattern) {
|
|
30972
|
-
ctx.fillStyle = getCanvasFillPattern(style);
|
|
31233
|
+
ctx.fillStyle = getCanvasFillPattern(style, patternScale, patternAnchor);
|
|
30973
31234
|
} else if (style.fillColor) {
|
|
30974
31235
|
ctx.fillStyle = style.fillColor;
|
|
30975
31236
|
}
|
|
@@ -31008,20 +31269,28 @@
|
|
|
31008
31269
|
return internal.svg.getTransform(p, scale);
|
|
31009
31270
|
}
|
|
31010
31271
|
|
|
31011
|
-
function repositionFurniture(container,
|
|
31012
|
-
|
|
31013
|
-
|
|
31272
|
+
function repositionFurniture(container, ext) {
|
|
31273
|
+
if (!ext.getFrameData()) return;
|
|
31274
|
+
El.findAll('.mapshaper-svg-furniture', container).forEach(function(g) {
|
|
31275
|
+
g.setAttribute('transform', getSvgFurnitureTransform(ext));
|
|
31276
|
+
});
|
|
31014
31277
|
}
|
|
31015
31278
|
|
|
31279
|
+
// Returns an SVG string, or '' if the furniture can't be drawn in the current
|
|
31280
|
+
// frame (e.g. the map is unprojected). The scalebar panel explains why.
|
|
31016
31281
|
function renderFurniture(lyr, ext) {
|
|
31017
|
-
var frame = ext.getFrameData();
|
|
31018
|
-
var obj
|
|
31019
|
-
if (
|
|
31020
|
-
|
|
31021
|
-
}
|
|
31282
|
+
var frame = ext.getFrameData();
|
|
31283
|
+
var obj;
|
|
31284
|
+
if (internal.getFurnitureFrameProblem(frame)) return '';
|
|
31285
|
+
obj = internal.getEmptyLayerForSVG(lyr, {});
|
|
31022
31286
|
obj.properties.transform = getSvgFurnitureTransform(ext);
|
|
31023
31287
|
obj.properties.class = 'mapshaper-svg-furniture';
|
|
31024
|
-
|
|
31288
|
+
try {
|
|
31289
|
+
obj.children = internal.renderFurnitureLayer(lyr, frame);
|
|
31290
|
+
} catch(e) {
|
|
31291
|
+
console.error(e);
|
|
31292
|
+
return '';
|
|
31293
|
+
}
|
|
31025
31294
|
return internal.svg.stringify(obj);
|
|
31026
31295
|
}
|
|
31027
31296
|
|
|
@@ -31063,8 +31332,6 @@
|
|
|
31063
31332
|
resize(ext);
|
|
31064
31333
|
if (type == 'label' || type == 'symbol') {
|
|
31065
31334
|
html = renderSymbols(lyr.gui.displayLayer, ext, id);
|
|
31066
|
-
} else if (type == 'furniture') {
|
|
31067
|
-
html = renderFurniture(lyr.gui.displayLayer, ext);
|
|
31068
31335
|
}
|
|
31069
31336
|
g.innerHTML = html;
|
|
31070
31337
|
if (type == 'label' || type == 'symbol') {
|
|
@@ -31078,6 +31345,26 @@
|
|
|
31078
31345
|
}
|
|
31079
31346
|
};
|
|
31080
31347
|
|
|
31348
|
+
// Furniture layers are dataset layers (they have no display layer)
|
|
31349
|
+
el.drawFurniture = function(layers) {
|
|
31350
|
+
el.clear();
|
|
31351
|
+
resize(ext);
|
|
31352
|
+
layers.forEach(function(lyr) {
|
|
31353
|
+
var html = renderFurniture(lyr, ext);
|
|
31354
|
+
var g;
|
|
31355
|
+
if (!html) return;
|
|
31356
|
+
g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
31357
|
+
g.setAttribute('class', 'mapshaper-svg-layer mapshaper-furniture-layer');
|
|
31358
|
+
g.innerHTML = html;
|
|
31359
|
+
svg.append(g);
|
|
31360
|
+
});
|
|
31361
|
+
};
|
|
31362
|
+
|
|
31363
|
+
el.repositionFurniture = function() {
|
|
31364
|
+
resize(ext);
|
|
31365
|
+
repositionFurniture(svg, ext);
|
|
31366
|
+
};
|
|
31367
|
+
|
|
31081
31368
|
function reposition(lyr, type, ext) {
|
|
31082
31369
|
var container = el.findChild('.' + lyr.gui.svg_id);
|
|
31083
31370
|
if (!container || !container.node()) {
|
|
@@ -31091,8 +31378,6 @@
|
|
|
31091
31378
|
// a symbol group's transform absorbs panning, and zooming too when a
|
|
31092
31379
|
// frame is defined; anything it can't absorb needs the baselines rebuilt
|
|
31093
31380
|
updateLabelPaths(container.node(), lyr.gui.displayLayer, ext);
|
|
31094
|
-
} else if (type == 'furniture') {
|
|
31095
|
-
repositionFurniture(container.node(), lyr.gui.displayLayer, ext);
|
|
31096
31381
|
} else {
|
|
31097
31382
|
// container.getElementsByTagName('text')
|
|
31098
31383
|
error('Unsupported symbol type:', type);
|
|
@@ -31248,19 +31533,14 @@
|
|
|
31248
31533
|
});
|
|
31249
31534
|
};
|
|
31250
31535
|
|
|
31536
|
+
// Furniture is sized in frame pixels, so panning and zooming only need to
|
|
31537
|
+
// update its transform; its content changes only with a full redraw.
|
|
31251
31538
|
this.drawFurnitureLayers = function(layers, action) {
|
|
31252
|
-
|
|
31253
|
-
|
|
31254
|
-
|
|
31255
|
-
_furniture.
|
|
31539
|
+
if (action == 'nav' || action == 'hover') {
|
|
31540
|
+
_furniture.repositionFurniture();
|
|
31541
|
+
} else {
|
|
31542
|
+
_furniture.drawFurniture(layers);
|
|
31256
31543
|
}
|
|
31257
|
-
layers.forEach(function(lyr) {
|
|
31258
|
-
if (noRedraw) {
|
|
31259
|
-
_furniture.reposition(lyr, 'furniture');
|
|
31260
|
-
} else {
|
|
31261
|
-
_furniture.drawLayer(lyr, 'furniture');
|
|
31262
|
-
}
|
|
31263
|
-
});
|
|
31264
31544
|
};
|
|
31265
31545
|
|
|
31266
31546
|
// kludge: skip rendering base layers if hovering, except on first hover
|
|
@@ -33111,7 +33391,7 @@
|
|
|
33111
33391
|
|
|
33112
33392
|
function isFrameMapLayer(lyr) {
|
|
33113
33393
|
var dataset = lyr && lyr.gui && lyr.gui.source && lyr.gui.source.dataset;
|
|
33114
|
-
return !!dataset && internal.
|
|
33394
|
+
return !!dataset && internal.isFrameComponentLayer(lyr, dataset);
|
|
33115
33395
|
}
|
|
33116
33396
|
|
|
33117
33397
|
function getDrawableContentLayers() {
|
|
@@ -33121,11 +33401,18 @@
|
|
|
33121
33401
|
});
|
|
33122
33402
|
}
|
|
33123
33403
|
|
|
33124
|
-
|
|
33125
|
-
|
|
33126
|
-
|
|
33127
|
-
|
|
33404
|
+
// The active frame's furniture, plus any visible standalone furniture layers
|
|
33405
|
+
// (e.g. from -scalebar run before a frame was created)
|
|
33406
|
+
function getDrawableFurnitureLayers() {
|
|
33407
|
+
var frame = isPreviewView() ? internal.getActiveFrame(model) : null;
|
|
33408
|
+
if (!frame) return [];
|
|
33409
|
+
var layers = internal.getFrameFurnitureLayers(frame.dataset);
|
|
33410
|
+
getVisibleMapLayers().forEach(function(lyr) {
|
|
33411
|
+
if (internal.layerIsFurniture(lyr) && !layers.includes(lyr) && !lyr.hidden) {
|
|
33412
|
+
layers.push(lyr);
|
|
33413
|
+
}
|
|
33128
33414
|
});
|
|
33415
|
+
return layers;
|
|
33129
33416
|
}
|
|
33130
33417
|
|
|
33131
33418
|
function updateLayerStyles(layers) {
|
|
@@ -33177,7 +33464,6 @@
|
|
|
33177
33464
|
var layersMayHaveChanged = action != 'nav'; // !action;
|
|
33178
33465
|
var fullBounds;
|
|
33179
33466
|
var contentLayers = getDrawableContentLayers();
|
|
33180
|
-
// var furnitureLayers = getDrawableFurnitureLayers();
|
|
33181
33467
|
if (!(_ext.width() > 0 && _ext.height() > 0)) {
|
|
33182
33468
|
// TODO: track down source of these errors
|
|
33183
33469
|
console.error("Collapsed map container, unable to draw.");
|
|
@@ -33218,8 +33504,7 @@
|
|
|
33218
33504
|
}
|
|
33219
33505
|
_renderer.drawOverlayLayers(_overlayLayers, action);
|
|
33220
33506
|
|
|
33221
|
-
|
|
33222
|
-
// _renderer.drawFurnitureLayers(furnitureLayers, action);
|
|
33507
|
+
_renderer.drawFurnitureLayers(getDrawableFurnitureLayers(), action);
|
|
33223
33508
|
// The action says how much was redrawn, which a listener rebuilding its own
|
|
33224
33509
|
// DOM overlays needs: a 'hover' draw leaves the SVG markup and its
|
|
33225
33510
|
// transforms alone, so anything anchored to them is still good.
|
|
@@ -33808,10 +34093,7 @@
|
|
|
33808
34093
|
return;
|
|
33809
34094
|
}
|
|
33810
34095
|
if (!getSourceLayer()) {
|
|
33811
|
-
showPopupAlert(
|
|
33812
|
-
'Add one or more layers before creating a map frame.',
|
|
33813
|
-
'Map frame'
|
|
33814
|
-
);
|
|
34096
|
+
showPopupAlert(getNoSourceMessage(), 'Map frame');
|
|
33815
34097
|
return;
|
|
33816
34098
|
}
|
|
33817
34099
|
var popup = showPopupAlert('', 'Add map frame', {classname: 'frame-create-box'});
|
|
@@ -33956,7 +34238,7 @@
|
|
|
33956
34238
|
function createFromVisibleLayers(width, aspect, margin) {
|
|
33957
34239
|
var entries = getCompositionEntries();
|
|
33958
34240
|
if (!entries.length) {
|
|
33959
|
-
showPopupAlert(
|
|
34241
|
+
showPopupAlert(getNoSourceMessage(), 'Map frame');
|
|
33960
34242
|
return;
|
|
33961
34243
|
}
|
|
33962
34244
|
var ids = entries.map(function(o) {
|
|
@@ -33979,6 +34261,15 @@
|
|
|
33979
34261
|
});
|
|
33980
34262
|
}
|
|
33981
34263
|
|
|
34264
|
+
function getNoSourceMessage() {
|
|
34265
|
+
var active = gui.model.getActiveLayer();
|
|
34266
|
+
if (active && active.layer && !internal.layerHasGeometry(active.layer)) {
|
|
34267
|
+
return 'A map frame is fitted to layers with shapes, and this layer ' +
|
|
34268
|
+
'contains only attribute data. Select or show a map layer to create a frame.';
|
|
34269
|
+
}
|
|
34270
|
+
return 'Add one or more layers before creating a map frame.';
|
|
34271
|
+
}
|
|
34272
|
+
|
|
33982
34273
|
function getFrameTarget() {
|
|
33983
34274
|
return internal.getActiveFrame(gui.model);
|
|
33984
34275
|
}
|
|
@@ -33998,10 +34289,11 @@
|
|
|
33998
34289
|
return entries.length ? entries[0].layer : null;
|
|
33999
34290
|
}
|
|
34000
34291
|
|
|
34292
|
+
// A frame is fitted to shapes, so a data-only layer on show doesn't count
|
|
34001
34293
|
function getCompositionEntries() {
|
|
34002
34294
|
var layers = gui.map.getCompositionLayers();
|
|
34003
34295
|
return gui.model.getLayers().filter(function(o) {
|
|
34004
|
-
return layers.includes(o.layer);
|
|
34296
|
+
return layers.includes(o.layer) && internal.layerHasGeometry(o.layer);
|
|
34005
34297
|
});
|
|
34006
34298
|
}
|
|
34007
34299
|
|
|
@@ -34038,16 +34330,6 @@
|
|
|
34038
34330
|
.text(label).on('click', action);
|
|
34039
34331
|
}
|
|
34040
34332
|
|
|
34041
|
-
// The "?" the rest of the app uses for field help (see .tip-button in
|
|
34042
|
-
// elements.css and the static ones in index.html). The bubble is white-space:
|
|
34043
|
-
// pre, so the line breaks in the text are the ones it gets.
|
|
34044
|
-
function makeFieldTip(parent, text) {
|
|
34045
|
-
var btn = El('div').addClass('tip-button').appendTo(parent).text('?');
|
|
34046
|
-
var anchor = El('div').addClass('tip-anchor').appendTo(btn);
|
|
34047
|
-
El('div').addClass('tip').appendTo(anchor).text(text);
|
|
34048
|
-
return btn;
|
|
34049
|
-
}
|
|
34050
|
-
|
|
34051
34333
|
function getUnitFactor$1(units) {
|
|
34052
34334
|
return units == 'in' ? 72 : units == 'cm' ? 28.3465 : 1;
|
|
34053
34335
|
}
|
|
@@ -34067,7 +34349,7 @@
|
|
|
34067
34349
|
function FrameProperties(gui) {
|
|
34068
34350
|
var target, form, widthInput, heightInput, unitsSelect, aspectValue;
|
|
34069
34351
|
var backgroundControl, neatlineControl, neatlineWidthInput;
|
|
34070
|
-
var boundsValue, crsValue;
|
|
34352
|
+
var boundsValue, crsValue, scalebarButton;
|
|
34071
34353
|
|
|
34072
34354
|
gui.frameProperties = this;
|
|
34073
34355
|
|
|
@@ -34139,6 +34421,14 @@
|
|
|
34139
34421
|
.appendTo(appearance);
|
|
34140
34422
|
makeActionButton(clearRow, 'Clear appearance', clearFrameStyle);
|
|
34141
34423
|
|
|
34424
|
+
var elements = makePanelSection(form, 'Map elements');
|
|
34425
|
+
var elementsRow = El('div')
|
|
34426
|
+
.addClass('label-style-row label-panel-button-row')
|
|
34427
|
+
.appendTo(elements);
|
|
34428
|
+
scalebarButton = makeActionButton(elementsRow, '', function() {
|
|
34429
|
+
if (gui.scalebarProperties) gui.scalebarProperties.open(target);
|
|
34430
|
+
}).addClass('frame-scalebar-btn');
|
|
34431
|
+
|
|
34142
34432
|
var details = makePanelSection(form, 'Details');
|
|
34143
34433
|
boundsValue = makeReadOnlyRow(details, 'Bounds');
|
|
34144
34434
|
crsValue = makeReadOnlyRow(details, 'CRS');
|
|
@@ -34156,11 +34446,13 @@
|
|
|
34156
34446
|
unitsSelect.node().value = units;
|
|
34157
34447
|
aspectValue.text(getAspectText(frame));
|
|
34158
34448
|
backgroundControl.showColor(rec.fill || '');
|
|
34159
|
-
backgroundControl.opacity.node().value =
|
|
34449
|
+
backgroundControl.opacity.node().value = formatColorOpacityPct(rec['fill-opacity'], !!rec.fill);
|
|
34160
34450
|
neatlineControl.showColor(rec.stroke || '');
|
|
34161
|
-
neatlineControl.opacity.node().value =
|
|
34451
|
+
neatlineControl.opacity.node().value = formatColorOpacityPct(rec['stroke-opacity'], !!rec.stroke);
|
|
34162
34452
|
neatlineWidthInput.node().value =
|
|
34163
34453
|
rec['stroke-width'] === undefined ? '' : rec['stroke-width'];
|
|
34454
|
+
scalebarButton.text(internal.findFrameFurnitureLayer(target.dataset, 'scalebar') ?
|
|
34455
|
+
'Edit scale bar' : 'Add scale bar');
|
|
34164
34456
|
boundsValue.text(frame.bbox.map(formatCoordinate).join(', '));
|
|
34165
34457
|
crsValue.text(info.proj4 || '[unknown]');
|
|
34166
34458
|
}
|
|
@@ -34284,15 +34576,6 @@
|
|
|
34284
34576
|
return String(Number(value.toPrecision(12)));
|
|
34285
34577
|
}
|
|
34286
34578
|
|
|
34287
|
-
// A colour with no opacity of its own is drawn opaque, and says so; with no
|
|
34288
|
-
// colour either, there is nothing for an opacity to apply to.
|
|
34289
|
-
function formatOpacity(value, color) {
|
|
34290
|
-
if (value === undefined || value === null || value === '') {
|
|
34291
|
-
return color ? '100%' : '';
|
|
34292
|
-
}
|
|
34293
|
-
return String(Math.round(Number(value) * 100)) + '%';
|
|
34294
|
-
}
|
|
34295
|
-
|
|
34296
34579
|
// A fixed ratio holds when the frame is rescaled; one taken from the extent
|
|
34297
34580
|
// changes whenever the extent does. That difference is the reason to show the
|
|
34298
34581
|
// ratio at all, so it is said rather than implied.
|
|
@@ -34306,6 +34589,434 @@
|
|
|
34306
34589
|
return formatFrameAspectRatio(ratio) + ' (from extent)';
|
|
34307
34590
|
}
|
|
34308
34591
|
|
|
34592
|
+
// Record fields of a scalebar, in the order they are written as options
|
|
34593
|
+
var scalebarOptions = ['units', 'style', 'position', 'label_position',
|
|
34594
|
+
'font_size', 'font_family', 'font_style', 'font_weight', 'color', 'bar_width',
|
|
34595
|
+
'tic_length', 'label_offset', 'margin'];
|
|
34596
|
+
|
|
34597
|
+
// Returns a -scalebar command that recreates a scalebar with the given
|
|
34598
|
+
// settings (a scalebar data record, with any edits applied). -scalebar
|
|
34599
|
+
// replaces the frame's scalebar, so every setting has to be written.
|
|
34600
|
+
function getScalebarCommand(settings) {
|
|
34601
|
+
var parts = ['-scalebar'];
|
|
34602
|
+
var d = settings || {};
|
|
34603
|
+
if (isSet(d.label)) {
|
|
34604
|
+
parts.push(quoteCommandValue(d.label));
|
|
34605
|
+
}
|
|
34606
|
+
scalebarOptions.forEach(function(k) {
|
|
34607
|
+
if (isSet(d[k])) {
|
|
34608
|
+
parts.push(k.replace(/_/g, '-') + '=' + formatValue$1(d[k]));
|
|
34609
|
+
}
|
|
34610
|
+
});
|
|
34611
|
+
if (d.dual_units) {
|
|
34612
|
+
parts.push('dual-units');
|
|
34613
|
+
}
|
|
34614
|
+
return parts.join(' ');
|
|
34615
|
+
}
|
|
34616
|
+
|
|
34617
|
+
function isSet(val) {
|
|
34618
|
+
return val !== undefined && val !== null && val !== '';
|
|
34619
|
+
}
|
|
34620
|
+
|
|
34621
|
+
function formatValue$1(val) {
|
|
34622
|
+
var str = String(val);
|
|
34623
|
+
return /^[\w.#-]+$/.test(str) ? str : quoteCommandValue(str);
|
|
34624
|
+
}
|
|
34625
|
+
|
|
34626
|
+
var positionOptions = [
|
|
34627
|
+
['top-left', 'Top left'],
|
|
34628
|
+
['top-right', 'Top right'],
|
|
34629
|
+
['bottom-left', 'Bottom left'],
|
|
34630
|
+
['bottom-right', 'Bottom right']
|
|
34631
|
+
];
|
|
34632
|
+
|
|
34633
|
+
// What the scalebar is drawn in when it has no color of its own
|
|
34634
|
+
var defaultColor = '#000000';
|
|
34635
|
+
|
|
34636
|
+
// Settings of the frame's scalebar. Every edit sends a complete -scalebar
|
|
34637
|
+
// command, which replaces the scalebar, so edits are undoable and replayable
|
|
34638
|
+
// like any other command.
|
|
34639
|
+
function ScalebarProperties(gui) {
|
|
34640
|
+
var popup, form, note;
|
|
34641
|
+
var labelInput, unitsSelect, styleSelect, dualUnitsBox, positionSelect,
|
|
34642
|
+
labelPositionSelect, fontSizeInput, barWidthInput, marginInput,
|
|
34643
|
+
fontSelect, fontStyleSelect, colorChit, colorInput, colorPicker;
|
|
34644
|
+
|
|
34645
|
+
gui.scalebarProperties = this;
|
|
34646
|
+
|
|
34647
|
+
// The panel doesn't block the map, so anything can change the scalebar or
|
|
34648
|
+
// the frame while it is open: an undo, a frame edit, a console command.
|
|
34649
|
+
gui.model.on('update', function(e) {
|
|
34650
|
+
var flags = e.flags || {};
|
|
34651
|
+
if (!popup || flags.simplify_amount || flags.redraw_only) return;
|
|
34652
|
+
if (getScalebarRecord()) {
|
|
34653
|
+
updateControls();
|
|
34654
|
+
} else {
|
|
34655
|
+
closePanel();
|
|
34656
|
+
}
|
|
34657
|
+
});
|
|
34658
|
+
|
|
34659
|
+
// Adds a scalebar to the frame if it doesn't have one
|
|
34660
|
+
this.open = function(frameTarget) {
|
|
34661
|
+
var frame = frameTarget || internal.getActiveFrame(gui.model);
|
|
34662
|
+
if (!frame) return;
|
|
34663
|
+
if (getScalebarRecord()) {
|
|
34664
|
+
showPanel();
|
|
34665
|
+
} else {
|
|
34666
|
+
runCommand('-scalebar', 'Add scale bar', showPanel);
|
|
34667
|
+
}
|
|
34668
|
+
};
|
|
34669
|
+
|
|
34670
|
+
this.isOpen = function() {
|
|
34671
|
+
return !!popup;
|
|
34672
|
+
};
|
|
34673
|
+
|
|
34674
|
+
// The map stays live, so the panel is placed where it is least likely to
|
|
34675
|
+
// hide the scalebar: at the top of the map, on the side away from it
|
|
34676
|
+
function showPanel() {
|
|
34677
|
+
var d = getScalebarRecord();
|
|
34678
|
+
if (!d) return;
|
|
34679
|
+
closePanel();
|
|
34680
|
+
popup = showPopupAlert('', 'Scale bar', {
|
|
34681
|
+
non_blocking: true,
|
|
34682
|
+
classname: 'scalebar-properties-box'
|
|
34683
|
+
});
|
|
34684
|
+
popup.onClose(function() {
|
|
34685
|
+
popup = form = null;
|
|
34686
|
+
});
|
|
34687
|
+
popup.container().addClass('scalebar-properties-popup');
|
|
34688
|
+
form = El('div')
|
|
34689
|
+
.addClass('label-style-panel scalebar-properties-form')
|
|
34690
|
+
.appendTo(popup.container());
|
|
34691
|
+
initForm();
|
|
34692
|
+
updateControls();
|
|
34693
|
+
placePanel(/right/.test(d.position || '') ? 'left' : 'right');
|
|
34694
|
+
}
|
|
34695
|
+
|
|
34696
|
+
function closePanel() {
|
|
34697
|
+
if (popup) popup.close();
|
|
34698
|
+
}
|
|
34699
|
+
|
|
34700
|
+
function placePanel(side) {
|
|
34701
|
+
var box = popup.container().node().parentNode;
|
|
34702
|
+
var map = gui.container.findChild('.map-layers').node().getBoundingClientRect();
|
|
34703
|
+
// clears the zoom buttons on the right edge of the map
|
|
34704
|
+
var rightInset = 58;
|
|
34705
|
+
var leftInset = 16;
|
|
34706
|
+
box.style.position = 'absolute';
|
|
34707
|
+
box.style.marginTop = '0';
|
|
34708
|
+
box.style.top = Math.round(map.top + 12) + 'px';
|
|
34709
|
+
if (side == 'left') {
|
|
34710
|
+
box.style.left = Math.round(map.left + leftInset) + 'px';
|
|
34711
|
+
} else {
|
|
34712
|
+
box.style.right = Math.round(window.innerWidth - map.right + rightInset) + 'px';
|
|
34713
|
+
}
|
|
34714
|
+
}
|
|
34715
|
+
|
|
34716
|
+
function initForm() {
|
|
34717
|
+
var row;
|
|
34718
|
+
note = El('div').addClass('scalebar-properties-note').appendTo(form).hide();
|
|
34719
|
+
|
|
34720
|
+
row = makeRow(form);
|
|
34721
|
+
labelInput = makeInput(makeCell(row, 'Distance'), 'scalebar-label-input')
|
|
34722
|
+
.attr('placeholder', 'Automatic')
|
|
34723
|
+
.on('change', function() {
|
|
34724
|
+
update({label: labelInput.node().value.trim()});
|
|
34725
|
+
});
|
|
34726
|
+
unitsSelect = makeSelect(makeCell(row, 'Units'), 'scalebar-units-select',
|
|
34727
|
+
[['miles', 'Miles'], ['km', 'Kilometers']])
|
|
34728
|
+
.on('change', function() {
|
|
34729
|
+
var changes = {units: unitsSelect.node().value};
|
|
34730
|
+
var bare = getBareDistance(getScalebarRecord().label);
|
|
34731
|
+
if (bare) changes.label = bare;
|
|
34732
|
+
update(changes);
|
|
34733
|
+
});
|
|
34734
|
+
|
|
34735
|
+
row = makeRow(form);
|
|
34736
|
+
styleSelect = makeSelect(makeCell(row, 'Style'), 'scalebar-style-select',
|
|
34737
|
+
[['a', 'Bar'], ['b', 'Ticks']])
|
|
34738
|
+
.on('change', function() {
|
|
34739
|
+
update({style: styleSelect.node().value});
|
|
34740
|
+
});
|
|
34741
|
+
dualUnitsBox = makeCheckbox(row, 'Metric + imperial', 'scalebar-dual-units')
|
|
34742
|
+
.on('change', function() {
|
|
34743
|
+
update({dual_units: dualUnitsBox.node().checked});
|
|
34744
|
+
});
|
|
34745
|
+
|
|
34746
|
+
row = makeRow(form);
|
|
34747
|
+
positionSelect = makeSelect(makeCell(row, 'Position'), 'scalebar-position-select',
|
|
34748
|
+
positionOptions)
|
|
34749
|
+
.on('change', function() {
|
|
34750
|
+
update({position: positionSelect.node().value});
|
|
34751
|
+
});
|
|
34752
|
+
labelPositionSelect = makeSelect(makeCell(row, 'Labels'),
|
|
34753
|
+
'scalebar-label-position-select', [['top', 'Above'], ['bottom', 'Below']])
|
|
34754
|
+
.on('change', function() {
|
|
34755
|
+
update({label_position: labelPositionSelect.node().value});
|
|
34756
|
+
});
|
|
34757
|
+
|
|
34758
|
+
var appearance = makePanelSection(form, 'Appearance');
|
|
34759
|
+
|
|
34760
|
+
// Font and face menus, as in the label panel
|
|
34761
|
+
row = makeRow(appearance);
|
|
34762
|
+
fontSelect = El('select').attr('title', 'Font').addClass('scalebar-font-select')
|
|
34763
|
+
.appendTo(makeCell(row, 'Font'))
|
|
34764
|
+
.on('change', function() {
|
|
34765
|
+
if (fontSelect.node().value) applyFont(fontSelect.node().value);
|
|
34766
|
+
});
|
|
34767
|
+
renderFontOptions();
|
|
34768
|
+
fontStyleSelect = El('select').attr('title', 'Font style')
|
|
34769
|
+
.addClass('scalebar-font-style-select')
|
|
34770
|
+
.appendTo(makeCell(row, 'Font style'))
|
|
34771
|
+
.on('change', function() {
|
|
34772
|
+
if (fontStyleSelect.node().value) applyFontStyle(fontStyleSelect.node().value);
|
|
34773
|
+
});
|
|
34774
|
+
|
|
34775
|
+
// The picker lines up with the right edge of its parent, so the row is its
|
|
34776
|
+
// parent rather than the narrow color cell
|
|
34777
|
+
row = makeRow(appearance);
|
|
34778
|
+
var colorCell = El('div').addClass('frame-input-cell scalebar-color-cell').appendTo(row);
|
|
34779
|
+
El('span').appendTo(colorCell).text('Color');
|
|
34780
|
+
colorChit = El('div').addClass('label-color-chit').attr('role', 'button')
|
|
34781
|
+
.on('click', function() { colorPicker.toggle(); });
|
|
34782
|
+
colorInput = El('input').attr('type', 'text').attr('title', 'Scale bar color')
|
|
34783
|
+
.on('change', function() {
|
|
34784
|
+
var color = colorInput.node().value.trim();
|
|
34785
|
+
if (!color) {
|
|
34786
|
+
updateControls();
|
|
34787
|
+
return;
|
|
34788
|
+
}
|
|
34789
|
+
if (isHexColor(color)) colorPicker.setColor(color);
|
|
34790
|
+
update({color: color});
|
|
34791
|
+
});
|
|
34792
|
+
makeColorField(colorCell, colorChit, colorInput);
|
|
34793
|
+
colorPicker = new ColorPicker(row, {
|
|
34794
|
+
onPreview: showColor,
|
|
34795
|
+
onChange: function(hex) {
|
|
34796
|
+
showColor(hex);
|
|
34797
|
+
update({color: hex});
|
|
34798
|
+
}
|
|
34799
|
+
});
|
|
34800
|
+
fontSizeInput = makeNumberInput(makeCell(row, 'Font size'), 'font_size',
|
|
34801
|
+
'scalebar-font-size');
|
|
34802
|
+
|
|
34803
|
+
row = makeRow(appearance);
|
|
34804
|
+
barWidthInput = makeNumberInput(makeCell(row, 'Bar width'), 'bar_width',
|
|
34805
|
+
'scalebar-bar-width');
|
|
34806
|
+
marginInput = makeNumberInput(makeCell(row, 'Inset'), 'margin',
|
|
34807
|
+
'scalebar-margin')
|
|
34808
|
+
.attr('title', 'Distance from the edges of the frame, in pixels');
|
|
34809
|
+
|
|
34810
|
+
row = El('div').addClass('label-style-row label-panel-button-row').appendTo(form);
|
|
34811
|
+
makePanelActionButton(row, 'Remove scale bar', function() {
|
|
34812
|
+
runCommand('-scalebar remove', 'Remove scale bar', function() {
|
|
34813
|
+
if (popup) popup.close();
|
|
34814
|
+
});
|
|
34815
|
+
});
|
|
34816
|
+
}
|
|
34817
|
+
|
|
34818
|
+
function updateControls() {
|
|
34819
|
+
var d = getScalebarRecord();
|
|
34820
|
+
var problem;
|
|
34821
|
+
if (!d || !form) return;
|
|
34822
|
+
labelInput.node().value = d.label || '';
|
|
34823
|
+
setSelectValue(unitsSelect, getUnitsMenuValue(d.units ||
|
|
34824
|
+
internal.parseScalebarUnits(d.label || '')));
|
|
34825
|
+
setSelectValue(styleSelect, d.style == 'b' || d.style == 'B' ||
|
|
34826
|
+
!d.style && d.dual_units ? 'b' : 'a');
|
|
34827
|
+
dualUnitsBox.node().checked = !!d.dual_units;
|
|
34828
|
+
setSelectValue(positionSelect, d.position || 'top-left');
|
|
34829
|
+
setSelectValue(labelPositionSelect, d.label_position || 'top');
|
|
34830
|
+
fontSizeInput.node().value = formatValue(d.font_size);
|
|
34831
|
+
barWidthInput.node().value = formatValue(d.bar_width);
|
|
34832
|
+
marginInput.node().value = formatValue(d.margin);
|
|
34833
|
+
updateFontControls(d);
|
|
34834
|
+
showColor(d.color || defaultColor);
|
|
34835
|
+
if (isHexColor(d.color || defaultColor)) colorPicker.setColor(d.color || defaultColor);
|
|
34836
|
+
problem = getFrameProblem();
|
|
34837
|
+
note.text(problem ? 'The scale bar is hidden: ' + problem + '.' : '');
|
|
34838
|
+
if (problem) note.show(); else note.hide();
|
|
34839
|
+
}
|
|
34840
|
+
|
|
34841
|
+
function showColor(color) {
|
|
34842
|
+
colorInput.node().value = color || '';
|
|
34843
|
+
colorChit.css('background-color', color || 'transparent');
|
|
34844
|
+
}
|
|
34845
|
+
|
|
34846
|
+
function renderFontOptions() {
|
|
34847
|
+
getInstalledFonts().forEach(function(group) {
|
|
34848
|
+
var optgroup = El('optgroup').attr('label', group.name).appendTo(fontSelect);
|
|
34849
|
+
group.fonts.forEach(function(fontName) {
|
|
34850
|
+
El('option').attr('value', fontName).appendTo(optgroup).text(fontName);
|
|
34851
|
+
});
|
|
34852
|
+
});
|
|
34853
|
+
}
|
|
34854
|
+
|
|
34855
|
+
// A scalebar with no font of its own is shown in the font it is drawn in
|
|
34856
|
+
function getShownFont(d) {
|
|
34857
|
+
return d.font_family || getDefaultFontName();
|
|
34858
|
+
}
|
|
34859
|
+
|
|
34860
|
+
function updateFontControls(d) {
|
|
34861
|
+
var font = getShownFont(d);
|
|
34862
|
+
var shown = font ? getNearestFontStyleVariant(font, d.font_style, d.font_weight) : null;
|
|
34863
|
+
if (font) {
|
|
34864
|
+
setSelectValue(fontSelect, font);
|
|
34865
|
+
} else {
|
|
34866
|
+
fontSelect.node().selectedIndex = -1;
|
|
34867
|
+
}
|
|
34868
|
+
fontStyleSelect.empty();
|
|
34869
|
+
getFontStyleVariants(font).forEach(function(variant) {
|
|
34870
|
+
El('option').attr('value', variant.value).appendTo(fontStyleSelect).text(variant.label);
|
|
34871
|
+
});
|
|
34872
|
+
fontStyleSelect.node().disabled = !font;
|
|
34873
|
+
if (shown) {
|
|
34874
|
+
fontStyleSelect.node().value = shown.value;
|
|
34875
|
+
} else {
|
|
34876
|
+
fontStyleSelect.node().selectedIndex = -1;
|
|
34877
|
+
}
|
|
34878
|
+
}
|
|
34879
|
+
|
|
34880
|
+
// Keeps the current face, or the nearest one the new font has
|
|
34881
|
+
function applyFont(fontName) {
|
|
34882
|
+
var d = getScalebarRecord();
|
|
34883
|
+
var changes = {font_family: fontName};
|
|
34884
|
+
var variant = getNearestFontStyleVariant(fontName, d.font_style, d.font_weight);
|
|
34885
|
+
if (variant) Object.assign(changes, getFontStyleValues(variant));
|
|
34886
|
+
update(changes);
|
|
34887
|
+
}
|
|
34888
|
+
|
|
34889
|
+
// A face belongs to a font, so a scalebar given one names the font it is
|
|
34890
|
+
// drawn in
|
|
34891
|
+
function applyFontStyle(value) {
|
|
34892
|
+
var parts = value.split('|');
|
|
34893
|
+
var changes = getFontStyleValues({style: parts[0], weight: parts[1]});
|
|
34894
|
+
var d = getScalebarRecord();
|
|
34895
|
+
if (!d.font_family && getDefaultFontName()) {
|
|
34896
|
+
changes.font_family = getDefaultFontName();
|
|
34897
|
+
}
|
|
34898
|
+
update(changes);
|
|
34899
|
+
}
|
|
34900
|
+
|
|
34901
|
+
// Regular is stored as no face at all
|
|
34902
|
+
function getFontStyleValues(variant) {
|
|
34903
|
+
if (variantIsRegular(variant)) {
|
|
34904
|
+
return {font_style: '', font_weight: ''};
|
|
34905
|
+
}
|
|
34906
|
+
return {font_style: variant.style, font_weight: variant.weight};
|
|
34907
|
+
}
|
|
34908
|
+
|
|
34909
|
+
// Applies edits to the current settings and replaces the scalebar. The
|
|
34910
|
+
// units menu always applies, so it is written even when it shows the default.
|
|
34911
|
+
function update(changes) {
|
|
34912
|
+
var d = Object.assign({}, getScalebarRecord(), changes);
|
|
34913
|
+
if (changes.style == 'a') d.dual_units = false;
|
|
34914
|
+
if (changes.dual_units) d.style = 'b';
|
|
34915
|
+
if (!d.units) d.units = unitsSelect.node().value;
|
|
34916
|
+
runCommand(getScalebarCommand(d), 'Update scale bar', updateControls);
|
|
34917
|
+
}
|
|
34918
|
+
|
|
34919
|
+
function makeNumberInput(cell, field, className) {
|
|
34920
|
+
var input = makeInput(cell, className).attr('placeholder', 'auto');
|
|
34921
|
+
input.on('change', function() {
|
|
34922
|
+
var str = input.node().value.trim();
|
|
34923
|
+
var o = {};
|
|
34924
|
+
if (str !== '' && !(Number(str) >= 0)) {
|
|
34925
|
+
updateControls();
|
|
34926
|
+
return;
|
|
34927
|
+
}
|
|
34928
|
+
o[field] = str === '' ? '' : Number(str);
|
|
34929
|
+
update(o);
|
|
34930
|
+
});
|
|
34931
|
+
return input;
|
|
34932
|
+
}
|
|
34933
|
+
|
|
34934
|
+
function getFrameProblem() {
|
|
34935
|
+
var frame = internal.getActiveFrame(gui.model);
|
|
34936
|
+
var crs, data;
|
|
34937
|
+
if (!frame) return 'there is no map frame';
|
|
34938
|
+
crs = frame.layer.gui && frame.layer.gui.dynamic_crs ||
|
|
34939
|
+
internal.getDatasetCRS(frame.dataset);
|
|
34940
|
+
data = internal.getFrameLayerData(frame.layer, frame.dataset.arcs, crs);
|
|
34941
|
+
return internal.getFurnitureFrameProblem(data);
|
|
34942
|
+
}
|
|
34943
|
+
|
|
34944
|
+
function getScalebarRecord() {
|
|
34945
|
+
var frame = internal.getActiveFrame(gui.model);
|
|
34946
|
+
var lyr = frame && internal.findFrameFurnitureLayer(frame.dataset, 'scalebar');
|
|
34947
|
+
return lyr ? lyr.data.getReadOnlyRecordAt(0) : null;
|
|
34948
|
+
}
|
|
34949
|
+
|
|
34950
|
+
function runCommand(cmd, title, done) {
|
|
34951
|
+
runGuiEditCommand(gui, cmd, {
|
|
34952
|
+
title: title,
|
|
34953
|
+
onSuccess: done,
|
|
34954
|
+
onError: updateControls
|
|
34955
|
+
});
|
|
34956
|
+
}
|
|
34957
|
+
}
|
|
34958
|
+
|
|
34959
|
+
function makeRow(parent, className) {
|
|
34960
|
+
return El('div').addClass('label-style-row scalebar-row')
|
|
34961
|
+
.addClass(className || 'scalebar-two-col').appendTo(parent);
|
|
34962
|
+
}
|
|
34963
|
+
|
|
34964
|
+
function makeCell(row, label) {
|
|
34965
|
+
var cell = El('label').addClass('frame-input-cell').appendTo(row);
|
|
34966
|
+
El('span').appendTo(cell).text(label);
|
|
34967
|
+
return cell;
|
|
34968
|
+
}
|
|
34969
|
+
|
|
34970
|
+
function makeInput(parent, className) {
|
|
34971
|
+
return El('input').attr('type', 'text').addClass(className).appendTo(parent);
|
|
34972
|
+
}
|
|
34973
|
+
|
|
34974
|
+
function makeSelect(parent, className, options) {
|
|
34975
|
+
var select = El('select').addClass(className).appendTo(parent);
|
|
34976
|
+
options.forEach(function(o) {
|
|
34977
|
+
El('option').attr('value', o[0]).appendTo(select).text(o[1]);
|
|
34978
|
+
});
|
|
34979
|
+
return select;
|
|
34980
|
+
}
|
|
34981
|
+
|
|
34982
|
+
function makeCheckbox(parent, label, className) {
|
|
34983
|
+
var box = El('input').attr('type', 'checkbox').addClass(className);
|
|
34984
|
+
var wrapper = El('label').addClass('scalebar-checkbox').appendTo(parent);
|
|
34985
|
+
box.appendTo(wrapper);
|
|
34986
|
+
El('span').appendTo(wrapper).text(label);
|
|
34987
|
+
return box;
|
|
34988
|
+
}
|
|
34989
|
+
|
|
34990
|
+
// Shows a value that came from the command line even if the menu lacks it
|
|
34991
|
+
function setSelectValue(select, value) {
|
|
34992
|
+
var node = select.node();
|
|
34993
|
+
var found = Array.prototype.some.call(node.options, function(o) {
|
|
34994
|
+
return o.value == value;
|
|
34995
|
+
});
|
|
34996
|
+
if (!found) {
|
|
34997
|
+
El('option').attr('value', value).appendTo(select).text(value);
|
|
34998
|
+
}
|
|
34999
|
+
node.value = value;
|
|
35000
|
+
}
|
|
35001
|
+
|
|
35002
|
+
// The number of a single-distance label, e.g. "100" from "100 km", so the
|
|
35003
|
+
// units menu can apply to it
|
|
35004
|
+
function getBareDistance(label) {
|
|
35005
|
+
var match = /^([\d\s.,/]*\d)\s*[^\d\s.,/]+$/.exec(String(label || '').trim());
|
|
35006
|
+
return match ? match[1].trim() : '';
|
|
35007
|
+
}
|
|
35008
|
+
|
|
35009
|
+
function getUnitsMenuValue(units) {
|
|
35010
|
+
var parsed = internal.parseLabelUnits(units);
|
|
35011
|
+
return parsed == 'mile' || !parsed ? 'miles' :
|
|
35012
|
+
parsed == 'm' ? 'meters' :
|
|
35013
|
+
parsed == 'ft' ? 'feet' : 'km';
|
|
35014
|
+
}
|
|
35015
|
+
|
|
35016
|
+
function formatValue(val) {
|
|
35017
|
+
return val === undefined || val === null ? '' : String(val);
|
|
35018
|
+
}
|
|
35019
|
+
|
|
34309
35020
|
// This is a new way to handle compatibility problems between
|
|
34310
35021
|
// interactive editing modes and other interface modes
|
|
34311
35022
|
// (by default, interactive modes stay on when, e.g., the user clicks
|
|
@@ -35108,6 +35819,7 @@
|
|
|
35108
35819
|
new PreviewMode(gui);
|
|
35109
35820
|
new FrameResizeTool(gui);
|
|
35110
35821
|
new FrameProperties(gui);
|
|
35822
|
+
new ScalebarProperties(gui);
|
|
35111
35823
|
gui.editToolbar = new EditToolbar(gui);
|
|
35112
35824
|
gui.labelTool = new LabelTool(gui);
|
|
35113
35825
|
gui.layerStyleTool = new LayerStyleTool(gui);
|