index.js (23124B)
1 import L from 'leaflet'; 2 import './style.css'; 3 import enableTopRow from '~/lib/leaflet.control.layers.top-row'; 4 import ko from 'knockout'; 5 import {notify} from '~/lib/notifications'; 6 import * as logging from '~/lib/logging'; 7 import safeLocalStorage from '~/lib/safe-localstorage'; 8 import './customLayer'; 9 10 function enableConfig(control, {layers, customLayersOrder}) { 11 const originalOnAdd = control.onAdd; 12 const originalUnserializeState = control.unserializeState; 13 const originalAddItem = control._addItem; 14 if (control._configEnabled) { 15 return; 16 } 17 enableTopRow(control); 18 19 L.Util.extend(control, { 20 _configEnabled: true, 21 _allLayersGroups: layers, 22 _allLayers: [].concat(...layers.map((group) => group.layers)), 23 _customLayers: ko.observableArray(), 24 25 onAdd: function(map) { 26 const container = originalOnAdd.call(this, map); 27 this.__injectConfigButton(); 28 return container; 29 }, 30 31 __injectConfigButton: function() { 32 const configButton = L.DomUtil.create('div', 'button icon-settings'); 33 configButton.title = 'Configure layers'; 34 this._topRow.appendChild(configButton); 35 L.DomEvent.on(configButton, 'click', this._onConfigButtonClick, this); 36 37 const newCustomLayerButton = L.DomUtil.create('div', 'button icon-edit'); 38 newCustomLayerButton.title = 'Add custom layer'; 39 this._topRow.appendChild(newCustomLayerButton); 40 L.DomEvent.on(newCustomLayerButton, 'click', this.onCustomLayerCreateClicked, this); 41 }, 42 43 _initializeLayersState: function() { 44 let storedLayersEnabled = {}; 45 const serialized = safeLocalStorage.getItem('layersEnabled'); 46 if (serialized) { 47 try { 48 storedLayersEnabled = JSON.parse(serialized); 49 } catch (e) { 50 logging.captureMessage('Failed to load enabled layers from localstorage - invalid json', { 51 "localstorage.layersEnabled": serialized.slice(0, 1000) 52 }); 53 } 54 } 55 // restore custom layers; 56 // custom layers can be upgraded in loadCustomLayerFromString and their code will change 57 const storedLayersEnabled2 = {}; 58 for (let [code, isEnabled] of Object.entries(storedLayersEnabled)) { 59 let newCode = this.loadCustomLayerFromString(code) || code; 60 storedLayersEnabled2[newCode] = isEnabled; 61 } 62 63 for (let layer of [...this._allLayers, ...this._customLayers()]) { 64 let enabled = storedLayersEnabled2[layer.layer.options.code]; 65 // if storage is empty enable only default layers 66 // if new default layer appears it will be enabled 67 if (typeof enabled === 'undefined') { 68 enabled = layer.isDefault; 69 } 70 layer.enabled = enabled; 71 layer.checked = ko.observable(enabled); 72 layer.description = layer.description || ''; 73 } 74 this.updateEnabledLayers(); 75 }, 76 77 _onConfigButtonClick: function() { 78 this.showLayersSelectWindow(); 79 }, 80 81 _initLayersSelectWindow: function() { 82 if (this._configWindow) { 83 return; 84 } 85 86 const container = this._configWindow = 87 L.DomUtil.create('div', 'leaflet-layers-dialog-wrapper'); 88 L.DomEvent 89 .disableClickPropagation(container) 90 .disableScrollPropagation(container); 91 container.innerHTML = ` 92 <div class="leaflet-layers-select-window"> 93 <form> 94 <!-- ko foreach: _allLayersGroups --> 95 <div class="section-header" data-bind="html: group"></div> 96 <!-- ko foreach: layers --> 97 <label> 98 <input type="checkbox" data-bind="checked: checked"/> 99 <span data-bind="text: title"> 100 </span><!-- ko if: description --> 101 <span data-bind="html: description || ''"></span> 102 <!-- /ko --> 103 </label> 104 <!-- /ko --> 105 <!-- /ko --> 106 <div data-bind="if: _customLayers().length" class="section-header">Custom layers</div> 107 <!-- ko foreach: _customLayers --> 108 <label> 109 <input type="checkbox" data-bind="checked: checked"/> 110 <span data-bind="text: title"></span> 111 </label> 112 <!-- /ko --> 113 </form> 114 <div class="buttons-row"> 115 <div href="#" class="button" data-bind="click: onSelectWindowOkClicked">Ok</div> 116 <div href="#" class="button" data-bind="click: onSelectWindowCancelClicked">Cancel</div> 117 <div href="#" class="button" data-bind="click: onSelectWindowResetClicked">Reset</div> 118 </div> 119 </div> 120 `; 121 ko.applyBindings(this, container); 122 }, 123 124 showLayersSelectWindow: function() { 125 if (this._configWindowVisible || this._customLayerWindow) { 126 return; 127 } 128 [...this._allLayers, ...this._customLayers()].forEach((layer) => layer.checked(layer.enabled)); 129 this._initLayersSelectWindow(); 130 this._map._controlContainer.appendChild(this._configWindow); 131 this._configWindowVisible = true; 132 }, 133 134 hideSelectWindow: function() { 135 if (!this._configWindowVisible) { 136 return; 137 } 138 this._map._controlContainer.removeChild(this._configWindow); 139 this._configWindowVisible = false; 140 }, 141 142 onSelectWindowCancelClicked: function() { 143 this.hideSelectWindow(); 144 }, 145 146 onSelectWindowResetClicked: function() { 147 if (!this._configWindow) { 148 return; 149 } 150 [...this._allLayers, ...this._customLayers()].forEach((layer) => layer.checked(layer.isDefault)); 151 }, 152 153 onSelectWindowOkClicked: function() { 154 const newEnabledLayers = []; 155 for (let layer of [...this._allLayers, ...this._customLayers()]) { 156 if (layer.checked()) { 157 if (!layer.enabled) { 158 newEnabledLayers.push(layer); 159 } 160 layer.enabled = true; 161 } else { 162 layer.enabled = false; 163 } 164 } 165 this.updateEnabledLayers(newEnabledLayers); 166 this.hideSelectWindow(); 167 }, 168 169 onCustomLayerCreateClicked: function() { 170 this.showCustomLayerForm( 171 [ 172 { 173 caption: 'Add layer', 174 callback: (fieldValues) => this.onCustomLayerAddClicked(fieldValues) 175 }, 176 { 177 caption: 'Cancel', 178 callback: () => this.onCustomLayerCancelClicked() 179 } 180 ], 181 { 182 name: 'Custom layer', 183 url: '', 184 tms: false, 185 maxZoom: 18, 186 isOverlay: false, 187 scaleDependent: false, 188 isTop: true 189 } 190 ); 191 }, 192 193 updateEnabledLayers: function(addedLayers) { 194 const disabledLayers = [...this._allLayers, ...this._customLayers()].filter((l) => !l.enabled); 195 disabledLayers.forEach((l) => this._map.removeLayer(l.layer)); 196 [...this._layers].forEach((l) => this.removeLayer(l.layer)); 197 198 let hasBaselayerOnMap = false; 199 const enabledLayers = [...this._allLayers, ...this._customLayers()].filter((l) => l.enabled); 200 enabledLayers.sort((l1, l2) => l1.order - l2.order); 201 enabledLayers.forEach((l) => { 202 l.layer._justAdded = addedLayers && addedLayers.includes(l); 203 const {layer: {options: {isOverlay}}} = l; 204 if (isOverlay) { 205 this.addOverlay(l.layer, l.title); 206 } else { 207 this.addBaseLayer(l.layer, l.title); 208 } 209 if (!isOverlay && this._map.hasLayer(l.layer)) { 210 hasBaselayerOnMap = true; 211 } 212 } 213 ); 214 // если нет активного базового слоя, включить первый, если он есть 215 if (!hasBaselayerOnMap) { 216 for (let layer of enabledLayers) { 217 if (!layer.layer.options.isOverlay) { 218 this._map.addLayer(layer.layer); 219 break; 220 } 221 } 222 } 223 this.storeEnabledLayers(); 224 }, 225 226 storeEnabledLayers: function() { 227 const layersState = {}; 228 for (let layer of [...this._allLayers, ...this._customLayers()]) { 229 if (layer.isDefault || layer.enabled || layer.isCustom) { 230 layersState[layer.layer.options.code] = layer.enabled; 231 } 232 } 233 const serialized = JSON.stringify(layersState); 234 safeLocalStorage.setItem('layersEnabled', serialized); 235 }, 236 237 unserializeState: function(values) { 238 if (values) { 239 values = values.map((code) => { 240 let newCode = this.loadCustomLayerFromString(code); 241 return newCode || code; 242 }); 243 for (let layer of [...this._allLayers, ...this._customLayers()]) { 244 if (layer.layer.options && values.includes(layer.layer.options.code)) { 245 layer.enabled = true; 246 } 247 } 248 this.updateEnabledLayers(); 249 } 250 this.storeEnabledLayers(); 251 return originalUnserializeState.call(this, values); 252 }, 253 254 showCustomLayerForm: function(buttons, fieldValues) { 255 if (this._customLayerWindow || this._configWindowVisible) { 256 return; 257 } 258 this._customLayerWindow = 259 L.DomUtil.create('div', 'leaflet-layers-dialog-wrapper', this._map._controlContainer); 260 261 L.DomEvent 262 .disableClickPropagation(this._customLayerWindow) 263 .disableScrollPropagation(this._customLayerWindow); 264 265 let customLayerWindow = L.DomUtil.create('div', 'custom-layers-window', this._customLayerWindow); 266 let form = L.DomUtil.create('form', '', customLayerWindow); 267 L.DomEvent.on(form, 'submit', L.DomEvent.preventDefault); 268 269 const dialogModel = { 270 name: ko.observable(fieldValues.name), 271 url: ko.observable(fieldValues.url), 272 tms: ko.observable(fieldValues.tms), 273 scaleDependent: ko.observable(fieldValues.scaleDependent), 274 maxZoom: ko.observable(fieldValues.maxZoom), 275 isOverlay: ko.observable(fieldValues.isOverlay), 276 isTop: ko.observable(fieldValues.isTop), 277 buttons: buttons, 278 buttonClicked: function buttonClicked(callbackN) { 279 const fieldValues = { 280 name: dialogModel.name().trim(), 281 url: dialogModel.url().trim(), 282 tms: dialogModel.tms(), 283 scaleDependent: dialogModel.scaleDependent(), 284 maxZoom: dialogModel.maxZoom(), 285 isOverlay: dialogModel.isOverlay(), 286 isTop: dialogModel.isTop() 287 }; 288 buttons[callbackN].callback(fieldValues); 289 } 290 }; 291 292 /* eslint-disable max-len */ 293 const formHtml = ` 294 <p><a class="doc-link" href="https://leafletjs.com/reference-1.0.3.html#tilelayer" target="_blank">See Leaflet TileLayer documentation for url format</a></p> 295 <label>Layer name<br/> 296 <span class="hint">Maximum 40 characters</span><br/> 297 <input maxlength="40" class="layer-name" data-bind="value: name"/></label><br/> 298 <label>Tile url template<br/><textarea data-bind="value: url" class="layer-url"></textarea></label><br/> 299 <label><input type="radio" name="overlay" data-bind="checked: isOverlay, checkedValue: false">Base layer</label><br/> 300 <label><input type="radio" name="overlay" data-bind="checked: isOverlay, checkedValue: true">Overlay</label><br/> 301 <hr/> 302 <label><input type="radio" name="top-or-bottom" 303 data-bind="checked: isTop, checkedValue: false, enable: isOverlay">Place below other layers</label><br/> 304 <label><input type="radio" name="top-or-bottom" 305 data-bind="checked: isTop, checkedValue: true, enable: isOverlay">Place above other layers</label><br/> 306 <hr/> 307 <label><input type="checkbox" data-bind="checked: scaleDependent"/>Content depends on scale(like OSM or Google maps)</label><br/> 308 <label><input type="checkbox" data-bind="checked: tms" />TMS rows order</label><br /> 309 310 <label>Max zoom<br> 311 <select data-bind="options: [9,10,11,12,13,14,15,16,17,18], value: maxZoom"></select></label> 312 <div data-bind="foreach: buttons"> 313 <a class="button" data-bind="click: $root.buttonClicked.bind(null, $index()), text: caption"></a> 314 </div>`; 315 /* eslint-enable max-len */ 316 form.innerHTML = formHtml; 317 ko.applyBindings(dialogModel, form); 318 }, 319 320 _addItem: function(obj) { 321 var label = originalAddItem.call(this, obj); 322 if (obj.layer.__customLayer) { 323 const editButton = L.DomUtil.create('div', 'custom-layer-edit-button icon-edit', label.children[0]); 324 editButton.title = 'Edit layer'; 325 L.DomEvent.on(editButton, 'click', (e) => 326 this.onCustomLayerEditClicked(obj.layer.__customLayer, e) 327 ); 328 } 329 if (obj.layer._justAdded) { 330 L.DomUtil.addClass(label, 'leaflet-layers-configure-just-added-1'); 331 setTimeout(() => { 332 L.DomUtil.addClass(label, 'leaflet-layers-configure-just-added-2'); 333 }, 0); 334 } 335 return label; 336 }, 337 338 serializeCustomLayer: function(fieldValues) { 339 let s = JSON.stringify(fieldValues); 340 s = s.replace(/[\u007f-\uffff]/ug, 341 function(c) { 342 return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); 343 } 344 ); 345 346 function encodeUrlSafeBase64(s) { 347 return btoa(s) 348 .replace(/\+/ug, '-') 349 .replace(/\//ug, '_'); 350 } 351 352 return '-cs' + encodeUrlSafeBase64(s); 353 }, 354 355 customLayerExists: function(fieldValues, ignoreLayer) { 356 const serialized = this.serializeCustomLayer(fieldValues); 357 for (let layer of this._customLayers()) { 358 if (layer !== ignoreLayer && layer.serialized === serialized) { 359 return layer; 360 } 361 } 362 return false; 363 }, 364 365 checkCustomLayerValues: function(fieldValues) { 366 if (!fieldValues.url) { 367 return {error: 'Url is empty'}; 368 } 369 if (!fieldValues.name) { 370 return {error: 'Name is empty'}; 371 } 372 return {}; 373 }, 374 375 onCustomLayerAddClicked: function(fieldValues) { 376 const error = this.checkCustomLayerValues(fieldValues).error; 377 if (error) { 378 notify(error); 379 return; 380 } 381 382 const duplicateLayer = this.customLayerExists(fieldValues); 383 if (duplicateLayer) { 384 let msg = 'Same layer already exists'; 385 if (!duplicateLayer.enabled) { 386 msg += ' but it is hidden. You can enable it in layers setting.'; 387 } 388 notify(msg); 389 return; 390 } 391 392 const layer = this.createCustomLayer(fieldValues); 393 layer.enabled = true; 394 layer.checked = ko.observable(true); 395 this._customLayers.push(layer); 396 this.hideCustomLayerForm(); 397 this.updateEnabledLayers(); 398 }, 399 400 createCustomLayer: function(fieldValues) { 401 const serialized = this.serializeCustomLayer(fieldValues); 402 const tileLayer = new L.Layer.CustomLayer(fieldValues.url, { 403 isOverlay: fieldValues.isOverlay, 404 tms: fieldValues.tms, 405 maxNativeZoom: fieldValues.maxZoom, 406 scaleDependent: fieldValues.scaleDependent, 407 print: true, 408 jnx: true, 409 code: serialized, 410 noCors: true, 411 isTop: fieldValues.isTop 412 } 413 ); 414 415 const customLayer = { 416 title: fieldValues.name, 417 isDefault: false, 418 isCustom: true, 419 serialized: serialized, 420 layer: tileLayer, 421 order: 422 (fieldValues.isOverlay && fieldValues.isTop) ? customLayersOrder.top : customLayersOrder.bottom, 423 fieldValues: fieldValues, 424 enabled: true, 425 checked: ko.observable(true) 426 }; 427 tileLayer.__customLayer = customLayer; 428 return customLayer; 429 }, 430 431 onCustomLayerCancelClicked: function() { 432 this.hideCustomLayerForm(); 433 }, 434 435 hideCustomLayerForm: function() { 436 if (!this._customLayerWindow) { 437 return; 438 } 439 this._customLayerWindow.parentNode.removeChild(this._customLayerWindow); 440 this._customLayerWindow = null; 441 }, 442 443 onCustomLayerEditClicked: function(layer, e) { 444 L.DomEvent.stop(e); 445 this.showCustomLayerForm([ 446 { 447 caption: 'Save', 448 callback: (fieldValues) => this.onCustomLayerChangeClicked(layer, fieldValues), 449 }, 450 {caption: 'Delete', callback: () => this.onCustomLayerDeletelClicked(layer)}, 451 {caption: 'Cancel', callback: () => this.onCustomLayerCancelClicked()} 452 ], layer.fieldValues 453 ); 454 }, 455 456 onCustomLayerChangeClicked: function(layer, newFieldValues) { 457 const error = this.checkCustomLayerValues(newFieldValues).error; 458 if (error) { 459 notify(error); 460 return; 461 } 462 const duplicateLayer = this.customLayerExists(newFieldValues, layer); 463 if (duplicateLayer) { 464 let msg = 'Same layer already exists'; 465 if (!duplicateLayer.enabled) { 466 msg += ' but it is hidden. You can enable it in layers setting.'; 467 } 468 notify(msg); 469 return; 470 } 471 472 const layerPos = this._customLayers.indexOf(layer); 473 this._customLayers.remove(layer); 474 475 const newLayer = this.createCustomLayer(newFieldValues); 476 this._customLayers.splice(layerPos, 0, newLayer); 477 const newLayerVisible = ( 478 this._map.hasLayer(layer.layer) && 479 // turn off layer if changing from overlay to baselayer 480 (!layer.layer.options.isOverlay || newLayer.layer.options.isOverlay) 481 ); 482 if (newLayerVisible) { 483 this._map.addLayer(newLayer.layer); 484 } 485 this._map.removeLayer(layer.layer); 486 this.updateEnabledLayers(); 487 if (newLayerVisible) { 488 newLayer.layer.fire('add'); 489 } 490 this.hideCustomLayerForm(); 491 }, 492 493 onCustomLayerDeletelClicked: function(layer) { 494 this._map.removeLayer(layer.layer); 495 this._customLayers.remove(layer); 496 this.updateEnabledLayers(); 497 this.hideCustomLayerForm(); 498 }, 499 500 loadCustomLayerFromString: function(s) { 501 let fieldValues; 502 const m = s.match(/^-cs(.+)$/u); 503 if (m) { 504 s = m[1].replace(/-/ug, '+').replace(/_/ug, '/'); 505 try { 506 s = atob(s); 507 fieldValues = JSON.parse(s); 508 } catch (e) { 509 // ignore malformed data 510 } 511 512 if (fieldValues) { 513 // upgrade 514 if (fieldValues.isTop === undefined) { 515 fieldValues.isTop = true; 516 } 517 if (!this.customLayerExists(fieldValues)) { 518 this._customLayers.push(this.createCustomLayer(fieldValues)); 519 } 520 return this.serializeCustomLayer(fieldValues); 521 } 522 } 523 return null; 524 } 525 526 } 527 ); 528 if (control._map) { 529 control.__injectConfigButton(); 530 } 531 control._initializeLayersState(); 532 } 533 534 export default enableConfig;