nakarte

Source code of https://map.sikmir.ru (fork)
git clone git://git.sikmir.ru/nakarte
Log | Files | Refs | LICENSE

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:
Msrc/lib/leaflet.control.layers.configure/index.js | 358++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Asrc/lib/leaflet.control.layers.configure/pencil.svg | 3+++
Msrc/lib/leaflet.control.layers.configure/style.css | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
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