commit 863de76fb6cb97d6b908a63d57f956c2da13fa0f
parent 3a3ca8e702a546e49a891fe1c7a1fabd8d4165dd
Author: Sergej Orlov <wladimirych@gmail.com>
Date: Sat, 10 Dec 2016 21:39:33 +0300
[layers configure] added custom layers
Diffstat:
3 files changed, 391 insertions(+), 46 deletions(-)
diff --git a/src/lib/leaflet.control.layers.configure/index.js b/src/lib/leaflet.control.layers.configure/index.js
@@ -2,11 +2,13 @@ import L from 'leaflet';
import './style.css';
import enableTopRow from 'lib/leaflet.control.layers.top-row';
import ko from 'knockout';
+import {notify} from 'lib/notifications';
function enableConfig(control, layers) {
const originalOnAdd = control.onAdd;
const originalUnserializeState = control.unserializeState;
+ const originalAddItem = control._addItem;
if (control._configEnabled) {
return;
}
@@ -16,6 +18,7 @@ function enableConfig(control, layers) {
_configEnabled: true,
_allLayersGroups: layers,
_allLayers: [].concat(...layers.map(group => group.layers)),
+ _customLayers: ko.observableArray(),
onAdd: function(map) {
const container = originalOnAdd.call(this, map);
@@ -24,10 +27,15 @@ function enableConfig(control, layers) {
},
__injectConfigButton: function() {
- const configButton = L.DomUtil.create('div', 'button-config');
+ const configButton = L.DomUtil.create('div', 'button icon-settings');
configButton.title = 'Configure layers';
this._topRow.appendChild(configButton);
L.DomEvent.on(configButton, 'click', this._onConfigButtonClick, this);
+
+ const newCustomLayerButton = L.DomUtil.create('div', 'button icon-edit');
+ newCustomLayerButton.title = 'Add custom layer';
+ this._topRow.appendChild(newCustomLayerButton);
+ L.DomEvent.on(newCustomLayerButton, 'click', this.onCustomLayerCreateClicked, this);
},
_initializeLayersState: function() {
@@ -41,8 +49,10 @@ function enableConfig(control, layers) {
}
}
}
- for (let layer of this._allLayers) {
- // TODO: check if state is stored in localStorage, else
+ // restore custom layers
+ Object.keys(storedLayersEnabled).forEach(code => this.loadCustomLayerFromString(code));
+
+ for (let layer of [...this._allLayers, ...this._customLayers()]) {
let enabled = storedLayersEnabled[layer.layer.options.code];
// if storage is empty enable only default layers
// if new default layer appears it will be enabled
@@ -53,7 +63,6 @@ function enableConfig(control, layers) {
layer.checked = ko.observable(enabled);
layer.description = layer.description || '';
}
- this.storeEnabledLayers();
this.updateEnabledLayers();
},
@@ -67,23 +76,32 @@ function enableConfig(control, layers) {
}
const container = this._configWindow =
- L.DomUtil.create('div', 'leaflet-layers-select-window-wrapper');
+ L.DomUtil.create('div', 'leaflet-layers-dialog-wrapper');
L.DomEvent.disableClickPropagation(container);
if (!L.Browser.touch) {
L.DomEvent.disableScrollPropagation(container);
}
container.innerHTML = `
<div class="leaflet-layers-select-window">
- <form data-bind="foreach: _allLayersGroups">
- <div class="section-header" data-bind="html: group"></div>
- <!-- ko foreach: layers -->
- <label>
- <input type="checkbox" data-bind="checked: checked"/>
- <span data-bind="text: title">
- </span><!-- ko if: description -->:
- <span data-bind="html: description || ''"></span>
- <!-- /ko -->
- </label>
+ <form>
+ <!-- ko foreach: _allLayersGroups -->
+ <div class="section-header" data-bind="html: group"></div>
+ <!-- ko foreach: layers -->
+ <label>
+ <input type="checkbox" data-bind="checked: checked"/>
+ <span data-bind="text: title">
+ </span><!-- ko if: description -->
+ <span data-bind="html: description || ''"></span>
+ <!-- /ko -->
+ </label>
+ <!-- /ko -->
+ <!-- /ko -->
+ <div data-bind="if: _customLayers().length" class="section-header">Custom layers</div>
+ <!-- ko foreach: _customLayers -->
+ <label>
+ <input type="checkbox" data-bind="checked: checked"/>
+ <span data-bind="text: title"></span>
+ </label>
<!-- /ko -->
</form>
<div class="buttons-row">
@@ -100,7 +118,7 @@ function enableConfig(control, layers) {
if (this._configWindowVisible) {
return;
}
- this._allLayers.forEach(layer => layer.checked(layer.enabled));
+ [...this._allLayers, ...this._customLayers()].forEach(layer => layer.checked(layer.enabled));
this._initLayersSelectWindow();
this._map._controlContainer.appendChild(this._configWindow);
this._configWindowVisible = true;
@@ -122,40 +140,46 @@ function enableConfig(control, layers) {
if (!this._configWindow) {
return;
}
- this._allLayers.forEach(layer => layer.checked(layer.isDefault))
+ [...this._allLayers, ...this._customLayers()].forEach(layer => layer.checked(layer.isDefault));
},
onSelectWindowOkClicked: function() {
- this._allLayers.forEach(layer => layer.enabled = layer.checked())
+ [...this._allLayers, ...this._customLayers()].forEach(layer => layer.enabled = layer.checked());
this.updateEnabledLayers();
this.hideSelectWindow();
- this.storeEnabledLayers();
},
- updateEnabledLayers: function() {
- const layersOnMap = [];
- while (this._layers.length) {
- let layer = this._layers[0];
- if (this._map.hasLayer(layer.layer)) {
- layersOnMap.push(layer.layer);
+ onCustomLayerCreateClicked: function() {
+ this.showCustomLayerForm([
+ {
+ caption: 'Add layer',
+ callback: (fieldValues) => this.onCustomLayerAddClicked(fieldValues)
+ }, {
+ caption: 'Cancel',
+ callback: () => this.onCustomLayerCancelClicked()
+ }], {
+ name: 'Custom layer',
+ url: '',
+ tms: false,
+ maxZoom: 18,
+ isOverlay: false,
+ scaleDependent: false
}
- this.removeLayer(layer.layer);
- this._map.removeLayer(layer.layer);
- }
+ );
+ },
+
+ updateEnabledLayers: function() {
+ [...this._layers].forEach((l) => this.removeLayer(l.layer));
+ const disabledLayers = [...this._allLayers, ...this._customLayers()].filter(l => !l.enabled);
+ disabledLayers.forEach((l) => this._map.removeLayer(l.layer));
let hasBaselayerOnMap = false;
- const enabledLayers = [];
- for (let layer of this._allLayers) {
- if (layer.enabled) {
- enabledLayers.push(layer);
- }
- }
+ const enabledLayers = [...this._allLayers, ...this._customLayers()].filter(l => l.enabled);
enabledLayers.sort((l1, l2) => l1.order - l2.order);
enabledLayers.forEach((l) => {
l.isOverlay ? this.addOverlay(l.layer, l.title) : this.addBaseLayer(l.layer, l.title);
- if (layersOnMap.includes(l.layer)) {
- this._map.addLayer(l.layer);
- hasBaselayerOnMap = hasBaselayerOnMap || !l.isOverlay;
+ if (!l.isOverlay && this._map.hasLayer(l.layer)) {
+ hasBaselayerOnMap = true;
}
}
);
@@ -168,7 +192,7 @@ function enableConfig(control, layers) {
}
}
}
-
+ this.storeEnabledLayers();
},
storeEnabledLayers: function() {
@@ -176,8 +200,8 @@ function enableConfig(control, layers) {
return;
}
const layersState = {};
- for (let layer of this._allLayers) {
- if (layer.isDefault || layer.enabled) {
+ for (let layer of [...this._allLayers, ...this._customLayers()]) {
+ if (layer.isDefault || layer.enabled || layer.isCustom) {
layersState[layer.layer.options.code] = layer.enabled;
}
}
@@ -187,6 +211,7 @@ function enableConfig(control, layers) {
unserializeState: function(values) {
if (values) {
+ values.forEach((code) => this.loadCustomLayerFromString(code));
for (let layer of this._allLayers) {
if (layer.layer.options && values.includes(layer.layer.options.code)) {
layer.enabled = true;
@@ -196,8 +221,261 @@ function enableConfig(control, layers) {
}
this.storeEnabledLayers();
return originalUnserializeState.call(this, values);
+ },
+
+ showCustomLayerForm: function(buttons, fieldValues) {
+ if (this._customLayerWindow) {
+ return;
+ }
+ this._customLayerWindow =
+ L.DomUtil.create('div', 'leaflet-layers-dialog-wrapper', this._map._controlContainer);
+
+ if (!L.Browser.touch) {
+ L.DomEvent
+ .disableClickPropagation(this._customLayerWindow)
+ .disableScrollPropagation(this._customLayerWindow);
+ } else {
+ L.DomEvent.on(this._customLayerWindow, 'click', L.DomEvent.stopPropagation);
+ }
+
+ let customLayerWindow = L.DomUtil.create('div', 'custom-layers-window', this._customLayerWindow);
+ let form = L.DomUtil.create('form', '', customLayerWindow);
+
+ let buttonsHtml = '';
+ for (let [i, button] of buttons.entries()) {
+ buttonsHtml += `<a class="button" name="btn-${i}">${button.caption}</a>`;
+ }
+ L.DomEvent.on(form, 'submit', L.DomEvent.preventDefault);
+ form.innerHTML = `
+<p><a href="http://leafletjs.com/reference-1.0.2.html#tilelayer" target="_blank">See Leaflet TileLayer documentation for url format</a></p>
+<label>Layer name<br/>
+<input name="name"/></label><br/>
+<label>Tile url template<br/>
+<textarea name="url" style="width: 100%"></textarea></label><br/>
+<label><input type="radio" name="overlay" value="no">Base layer</label><br/>
+<label><input type="radio" name="overlay" value="yes">Overlay</label><br/>
+<label><input type="checkbox" name="scaleDependent"/>Content depends on scale(like OSM or Google maps)</label><br/>
+<label><input type="checkbox" name="tms" />TMS rows order</label><br />
+
+<label>Max zoom<br>
+<select name="maxZoom">
+<option value="9">9</option>
+<option value="10">10</option>
+<option value="11">11</option>
+<option value="12">12</option>
+<option value="13">13</option>
+<option value="14">14</option>
+<option value="15">15</option>
+<option value="16">16</option>
+<option value="17">17</option>
+<option value="18" selected>18</option>
+</select></label>
+<br />
+${buttonsHtml}`;
+
+ form.name.value = fieldValues.name;
+ form.url.value = fieldValues.url;
+ form.tms.checked = fieldValues.tms;
+ form.scaleDependent.checked = fieldValues.scaleDependent;
+ form.maxZoom.value = fieldValues.maxZoom;
+ form.overlay[fieldValues.isOverlay ? 1 : 0].checked = true;
+
+ function buttonClicked(callback) {
+ var fieldValues = {
+ name: form.name.value.trim(),
+ url: form.url.value.trim(),
+ tms: form.tms.checked,
+ scaleDependent: form.scaleDependent.checked,
+ maxZoom: form.maxZoom.value,
+ isOverlay: form.overlay.value === 'yes'
+ };
+ callback(fieldValues);
+ }
+
+ for (let [i, button] of buttons.entries()) {
+
+ let buttonEl = form.querySelector(`[name="btn-${i}"]`);
+ L.DomEvent.on(buttonEl, 'click', buttonClicked.bind(this, button.callback));
+ }
+
+ },
+
+ _addItem: function(obj) {
+ var label = originalAddItem.call(this, obj);
+ if (obj.layer.__customLayer) {
+ const editButton = L.DomUtil.create('div', 'custom-layer-edit-button icon-edit', label.children[0]);
+ editButton.title = 'Edit layer';
+ L.DomEvent.on(editButton, 'click', (e) => this.onCustomLayerEditClicked(obj.layer.__customLayer, e));
+ }
+ return label;
+ },
+
+ serializeCustomLayer: function(fieldValues) {
+ let s = JSON.stringify(fieldValues);
+ s = s.replace(/[\u007f-\uffff]/g,
+ function(c) {
+ return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
+ }
+ );
+
+ function encodeUrlSafeBase64(s) {
+ return btoa(s)
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_');
+ }
+
+ return encodeUrlSafeBase64(s);
+ },
+
+ customLayerExists: function(fieldValues, ignoreLayer) {
+ const serialized = this.serializeCustomLayer(fieldValues);
+ for (let layer of this._customLayers()) {
+ if (layer !== ignoreLayer && layer.serialized === serialized) {
+ return layer;
+ }
+ }
+ return false;
+ },
+
+ checkCustomLayerValues: function(fieldValues) {
+ if (!fieldValues.url) {
+ return {'error': 'Url is empty'}
+ }
+ if (!fieldValues.name) {
+ return {'error': 'Name is empty'}
+ }
+ return {};
+ },
+
+ onCustomLayerAddClicked: function(fieldValues) {
+ const error = this.checkCustomLayerValues(fieldValues).error;
+ if (error) {
+ notify(error);
+ return;
+ }
+
+ const duplicateLayer = this.customLayerExists(fieldValues);
+ if (duplicateLayer) {
+ let msg = 'Same layer already exists';
+ if (!duplicateLayer.enabled) {
+ msg += ' but it is hidden. You can enable it in layers setting.';
+ }
+ notify(msg);
+ return;
+ }
+
+ const layer = this.createCustomLayer(fieldValues);
+ layer.enabled = true;
+ layer.checked = ko.observable(true);
+ this._customLayers.push(layer);
+ this.hideCustomLayerForm();
+ this.updateEnabledLayers();
+ },
+
+
+ createCustomLayer: function(fieldValues, position, ignoreExists) {
+ const serialized = this.serializeCustomLayer(fieldValues);
+ const tileLayer = L.tileLayer(fieldValues.url, {
+ tms: fieldValues.tms,
+ maxNativeZoom: fieldValues.maxZoom,
+ scaleDependent: fieldValues.scaleDependent,
+ print: true,
+ jnx: true,
+ code: '-cs' + serialized,
+ noCors: true
+ }
+ );
+
+ const customLayer = {
+ title: fieldValues.name,
+ isOverlay: fieldValues.isOverlay,
+ isDefault: false,
+ isCustom: true,
+ serialized: serialized,
+ layer: tileLayer,
+ order: 10000,
+ fieldValues: fieldValues,
+ enabled: true,
+ checked: ko.observable(true)
+ };
+ tileLayer.__customLayer = customLayer;
+ return customLayer;
+ },
+
+ onCustomLayerCancelClicked: function() {
+ this.hideCustomLayerForm();
+ },
+
+ hideCustomLayerForm: function() {
+ if (!this._customLayerWindow) {
+ return;
+ }
+ this._customLayerWindow.parentNode.removeChild(this._customLayerWindow);
+ this._customLayerWindow = null;
+ },
+
+ onCustomLayerEditClicked: function(layer, e) {
+ L.DomEvent.stop(e);
+ this.showCustomLayerForm([
+ {caption: 'Save', callback: (fieldValues) => this.onCustomLayerChangeClicked(layer, fieldValues)},
+ {caption: 'Delete', callback: (fieldValues) => this.onCustomLayerDeletelClicked(layer)},
+ {caption: 'Cancel', callback: () => this.onCustomLayerCancelClicked()}
+ ], layer.fieldValues
+ );
+ },
+
+ onCustomLayerChangeClicked: function(layer, newFieldValues) {
+ const error = this.checkCustomLayerValues(newFieldValues).error;
+ if (error) {
+ notify(error);
+ return;
+ }
+ const duplicateLayer = this.customLayerExists(newFieldValues, layer);
+ if (duplicateLayer) {
+ let msg = 'Same layer already exists';
+ if (!duplicateLayer.enabled) {
+ msg += ' but it is hidden. You can enable it in layers setting.';
+ }
+ notify(msg);
+ return;
+ }
+
+ const layerPos = this._customLayers.indexOf(layer);
+ this._customLayers.remove(layer);
+
+ const newLayer = this.createCustomLayer(newFieldValues);
+ this._customLayers.splice(layerPos, 0, newLayer);
+ if (this._map.hasLayer(layer.layer) && (!layer.isOverlay || newLayer.isOverlay)) {
+ this._map.addLayer(newLayer.layer);
+ }
+ this._map.removeLayer(layer.layer);
+ this.updateEnabledLayers();
+ this.hideCustomLayerForm();
+ },
+
+ onCustomLayerDeletelClicked: function(layer) {
+ this._customLayers.remove(layer);
+ this.updateEnabledLayers();
+ this.hideCustomLayerForm();
+ },
+
+ loadCustomLayerFromString: function(s) {
+ var m, fieldValues;
+ m = s.match(/^-cs(.+)$/);
+ if (m) {
+ s = m[1].replace(/-/g, '+').replace(/_/g, '/');
+ try {
+ s = atob(s);
+ fieldValues = JSON.parse(s);
+ if (!this.customLayerExists(fieldValues)) {
+ this._customLayers.push(this.createCustomLayer(fieldValues));
+ }
+ } catch (e) {
+ }
+ }
}
+
}
);
if (control._map) {
diff --git a/src/lib/leaflet.control.layers.configure/pencil.svg b/src/lib/leaflet.control.layers.configure/pencil.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" ?><svg height="24px" version="1.1" viewBox="0 0 24 24" width="24px" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><desc/><defs/><g fill="none" fill-rule="evenodd" id="miu" stroke="none" stroke-width="1"><g id="Artboard-1" transform="translate(-899.000000, -227.000000)"><g id="slice" transform="translate(215.000000, 119.000000)"/>
+ <path d="M914.000027,248.002414 L914.000464,232.002414 L914.000464,232.002414 L907.001354,232.002414 L907.000079,248.002414 L914.000027,248.002414 Z M913.998311,249.002414 L910.501672,254 L907.00169,249.002414 L913.998311,249.002414 Z M914.000492,231.002414 L914.000574,228.002414 C914.000574,227 912.99816,227 912.99816,227 L908.004086,227 C908.004086,227 907.001672,227 907.001672,228.002414 L907.001433,231.002414 L914.000492,231.002414 L914.000492,231.002414 Z" fill="#707070" id="editor-pencil-pen-edit-write-glyph" transform="translate(910.500326, 240.500000) rotate(45.000000) translate(-910.500326, -240.500000) " /></g></g></svg>
+\ No newline at end of file
diff --git a/src/lib/leaflet.control.layers.configure/style.css b/src/lib/leaflet.control.layers.configure/style.css
@@ -1,27 +1,34 @@
-.leaflet-control-layers .button-config {
+.leaflet-control-layers-top-row .button {
cursor: pointer;
width: 18px;
height: 18px;
margin-right: auto;
-
border-radius: 4px;
border: 1px solid #ccc;
background-color: white;
float: left;
- background-image: url("settings.svg");
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: 16px 16px;
+ margin-right: 4px;
+}
+
+.icon-settings {
+ background-image: url("settings.svg");
+}
+
+.icon-edit {
+ background-image: url("pencil.svg");
}
.leaflet-control-layers .button-config:hover {
background-color: #f4f4f4;
}
-.leaflet-layers-select-window-wrapper {
+.leaflet-layers-dialog-wrapper {
position: absolute;
top: 25px;
bottom: 25px;
@@ -41,7 +48,7 @@
overflow: hidden;
height: 100%;
padding: 6px 0 40px 6px;
- min-width: 250px;
+ min-width: 290px;
}
.leaflet-layers-select-window form {
@@ -50,13 +57,14 @@
padding-right: 18px;
max-height: 100%;
margin-bottom: 50px;
- width: auto ;
+ /*width: auto ;*/
}
.leaflet-layers-select-window .buttons-row {
position: absolute;
bottom: 0;
margin-bottom: 6px;
+ white-space: nowrap;
}
.leaflet-layers-select-window label {
@@ -91,4 +99,60 @@
.leaflet-layers-select-window .section-header a {
color: #666;
+}
+
+.custom-layers-window {
+ position: relative;
+ left: -50%;
+ width: 290px;
+ /*margin-left: -150px;*/
+ margin-top: 15px;
+ background-color: white;
+ border-radius: 5px 5px 5px 5px;
+ box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
+ padding: 3mm 3mm;
+ color: #333;
+ z-index: 3001;
+}
+
+.custom-layers-window textarea,
+.custom-layers-window input,
+.custom-layers-window select {
+ padding: 0.5mm 5px;
+ margin: 1mm 1mm 1mm 0;
+ border-radius: 3px;
+ border: 1px solid #aaa;
+ vertical-align: middle;
+ box-sizing: border-box;
+}
+
+.custom-layers-window .button {
+ display: inline-block;
+ height: 26px;
+ border-radius: 4px 4px 4px 4px;
+ border: 1px solid #ccc;
+ cursor: pointer;
+ margin-right: 6px;
+ padding: 0 1em;
+ line-height: 26px;
+ font-weight: bold;
+ color: #333;
+}
+
+.custom-layers-window .button:hover {
+ background-color: #f4f4f4;
+}
+
+.custom-layer-edit-button {
+ width: 16px;
+ height: 16px;
+ background-size: 16px 16px;
+ display: inline-block;
+ vertical-align: text-bottom;
+ margin-left: 2px;
+ cursor: pointer;
+}
+
+.custom-layers-window a {
+ color: #777;
}
\ No newline at end of file