nakarte

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

index.js (29047B)


      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 getLayerDefaultHotkey(layer) {
     11     const layerOptions = layer?.layer.options;
     12     if (!layerOptions) {
     13         return null;
     14     }
     15     if (layerOptions.hotkey) {
     16         return layerOptions.hotkey;
     17     }
     18     if (layerOptions.code?.length === 1) {
     19         return layerOptions.code;
     20     }
     21     return null;
     22 }
     23 
     24 class LayersConfigDialog {
     25     constructor(builtInLayers, customLayers, withHotkeys, cbOk) {
     26         this.builtInLayers = builtInLayers;
     27         this.customLayers = customLayers;
     28         this.withHotkeys = withHotkeys;
     29         this.cbOk = cbOk;
     30 
     31         this.visible = ko.observable(false);
     32         this.layerGroups = ko.observableArray([]);
     33 
     34         this.initWindow();
     35     }
     36 
     37     allLayers() {
     38         return [
     39             ...([].concat(...this.builtInLayers.map((group) => group.layers))),
     40             ...this.customLayers
     41         ];
     42     }
     43 
     44     allLayerModels() {
     45         return [].concat(...this.layerGroups().map((group) => group.layers()));
     46     }
     47 
     48     initWindow() {
     49         const container = this.window =
     50             L.DomUtil.create('div', 'leaflet-layers-dialog-wrapper');
     51         L.DomEvent
     52             .disableClickPropagation(container)
     53             .disableScrollPropagation(container);
     54         container.setAttribute('data-bind', "visible: visible");
     55         container.innerHTML = `
     56 <div class="leaflet-layers-config-window">
     57     <form>
     58         <!-- ko foreach: layerGroups -->
     59             <div class="section-header" data-bind="html: group"></div>
     60             <!-- ko foreach: layers -->
     61                 <label class="layer-label">
     62                     <input type="checkbox" class="layer-enabled-checkbox" data-bind="checked: enabled"/>
     63                     <span data-bind="text: title"></span>
     64                     <!-- ko if: $root.withHotkeys -->
     65                     <div class="hotkey-input"
     66                         tabindex="0"
     67                         data-bind="
     68                             text: hotkey,
     69                             attr: {title: $root.getHotkeyTooltip($data)},
     70                             event: {
     71                                 keyup: $root.onHotkeyInput.bind($root),
     72                                 click: function(_, e) {e.target.focus()},
     73                                 blur: function() {error(null)},
     74                             },
     75                             clickBubble: false,
     76                             keyupBubble: false">
     77                     ></div>
     78                     <div class="error" data-bind="text: error, visible: error"></div>
     79                     <!-- /ko -->
     80                 </label>
     81             <!-- /ko -->
     82         <!-- /ko -->
     83     </form>
     84     <div class="buttons-row">
     85         <div href="#" class="button" data-bind="click: onOkClicked">Ok</div>
     86         <div href="#" class="button" data-bind="click: onCancelClicked">Cancel</div>
     87         <div href="#" class="button" data-bind="click: onResetClicked">Reset</div>
     88     </div>
     89 </div>
     90                 `;
     91         ko.applyBindings(this, container);
     92     }
     93 
     94     getWindow() {
     95         return this.window;
     96     }
     97 
     98     showDialog() {
     99         this.updateModelFromLayers();
    100         this.visible(true);
    101     }
    102 
    103     updateModelFromLayers() {
    104         this.layerGroups.removeAll();
    105         for (const group of this.builtInLayers) {
    106             this.layerGroups.push({
    107                 group: group.group,
    108                 layers: ko.observableArray(
    109                     group.layers.map((l) => ({
    110                         title: l.title,
    111                         enabled: ko.observable(l.enabled),
    112                         hotkey: ko.observable(l.layer.hotkey),
    113                         origLayer: l,
    114                         error: ko.observable(null),
    115                     }))
    116                 ),
    117             });
    118         }
    119         if (this.customLayers.length) {
    120             this.layerGroups.push({
    121                 group: 'Custom layers',
    122                 layers: ko.observableArray(
    123                     this.customLayers.map((l) => ({
    124                         title: l.title,
    125                         enabled: ko.observable(l.enabled),
    126                         hotkey: ko.observable(l.layer.hotkey),
    127                         origLayer: l,
    128                         error: ko.observable(null),
    129                     }))
    130                 ),
    131             });
    132         }
    133     }
    134 
    135     updateLayersFromModel() {
    136         for (const layer of this.allLayerModels()) {
    137             layer.origLayer.enabled = layer.enabled();
    138             layer.origLayer.layer.hotkey = layer.hotkey();
    139         }
    140     }
    141 
    142     getLayersEnabledOnlyInModel() {
    143         const newLayers = [];
    144         for (const layer of this.allLayerModels()) {
    145             if (layer.enabled() && !layer.origLayer.enabled) {
    146                 newLayers.push(layer.origLayer);
    147             }
    148         }
    149         return newLayers;
    150     }
    151 
    152     onOkClicked() {
    153         const newEnabledLayers = this.getLayersEnabledOnlyInModel();
    154         this.updateLayersFromModel();
    155         this.visible(false);
    156         this.cbOk(newEnabledLayers);
    157     }
    158 
    159     onCancelClicked() {
    160         this.visible(false);
    161     }
    162 
    163     onResetClicked() {
    164         for (const layer of this.allLayerModels()) {
    165             layer.enabled(layer.origLayer.isDefault);
    166             layer.hotkey(getLayerDefaultHotkey(layer.origLayer));
    167         }
    168     }
    169 
    170     displayError(message, layerModel, event) {
    171         layerModel.error(message);
    172 
    173         setTimeout(() => {
    174             event.target.parentNode.querySelector('.error')
    175                 .scrollIntoView({block: 'nearest', behavior: 'smooth'});
    176         }, 0);
    177     }
    178 
    179     onHotkeyInput(layerModel, event) {
    180         layerModel.error(null);
    181         if (['Delete', 'Backspace', 'Space'].includes(event.code)) {
    182             layerModel.hotkey(null);
    183             return;
    184         }
    185 
    186         if (/Enter|Escape/u.test(event.code)) {
    187             event.target.blur();
    188             return;
    189         }
    190 
    191         if (/Alt|Shift|Control|Tab|Lock|Meta|ContextMenu|Lang|Arrow/u.test(event.code)) {
    192             return;
    193         }
    194         const match = /^(Key|Digit)(.)/u.exec(event.code);
    195         if (!match) {
    196             this.displayError('Only keys A-Z and 0-9 can be used for hotkeys.', layerModel, event);
    197             return;
    198         }
    199         const newHotkey = match[2];
    200         for (const layer of this.allLayerModels()) {
    201             if (layer !== layerModel && layer.hotkey() === newHotkey) {
    202                 this.displayError(`Hotkey "${newHotkey}" is already used by layer "${layer.title}"`, layerModel, event);
    203                 return;
    204             }
    205         }
    206         layerModel.hotkey(newHotkey);
    207     }
    208 
    209     getHotkeyTooltip(layer) {
    210         return layer.hotkey() ? 'Change or remove hotkey' : 'Set hotkey';
    211     }
    212 }
    213 
    214 function enableConfig(control, {layers, customLayersOrder}, options = {withHotkeys: true}) {
    215     if (control._configEnabled) {
    216         return;
    217     }
    218 
    219     enableTopRow(control);
    220 
    221     const originalOnAdd = control.onAdd;
    222     const originalUnserializeState = control.unserializeState;
    223     const originalAddItem = control._addItem;
    224 
    225     L.Util.extend(control, {
    226             _configEnabled: true,
    227             _builtinLayersByGroup: layers,
    228             _builtinLayers: [].concat(...layers.map((group) => group.layers)),
    229             _customLayers: [],
    230             _withHotkeys: options.withHotkeys,
    231 
    232             onAdd: function(map) {
    233                 const container = originalOnAdd.call(this, map);
    234                 this.__injectConfigButton();
    235                 this.initLayersConfigWindow();
    236                 this.loadSettings();
    237                 return container;
    238             },
    239 
    240             allLayers: function() {
    241                 return [...this._builtinLayers, ...this._customLayers];
    242             },
    243 
    244             __injectConfigButton: function() {
    245                 const configButton = L.DomUtil.create('div', 'button icon-settings');
    246                 configButton.title = 'Configure layers';
    247                 this._topRow.appendChild(configButton);
    248                 L.DomEvent.on(configButton, 'click', this._onConfigButtonClick, this);
    249 
    250                 const newCustomLayerButton = L.DomUtil.create('div', 'button icon-edit');
    251                 newCustomLayerButton.title = 'Add custom layer';
    252                 this._topRow.appendChild(newCustomLayerButton);
    253                 L.DomEvent.on(newCustomLayerButton, 'click', this.onCustomLayerCreateClicked, this);
    254             },
    255 
    256             migrateSetting: function() {
    257                 let oldSettings;
    258                 let newSettings;
    259                 try {
    260                     oldSettings = JSON.parse(safeLocalStorage.getItem('layersEnabled'));
    261                 } catch {
    262                     // consider empty
    263                 }
    264                 try {
    265                     newSettings = JSON.parse(safeLocalStorage.getItem('leafletLayersSettings'));
    266                 } catch {
    267                     // consider empty
    268                 }
    269 
    270                 if (!oldSettings || newSettings) {
    271                     return;
    272                 }
    273 
    274                 const layersSettings = [];
    275                 for (let [code, isEnabled] of Object.entries(oldSettings)) {
    276                     layersSettings.push({
    277                         code,
    278                         isCustom: Boolean(code.match(/^-cs(.+)$/u)),
    279                         enabled: isEnabled,
    280                     });
    281                 }
    282                 const settings = {layers: layersSettings};
    283                 safeLocalStorage.setItem('leafletLayersSettings', JSON.stringify(settings));
    284             },
    285 
    286         loadSettings: function() {
    287                 this.migrateSetting();
    288                 // load settings from storage
    289                 const serialized = safeLocalStorage.getItem('leafletLayersSettings');
    290                 let settings = {};
    291                 if (serialized) {
    292                     try {
    293                         settings = JSON.parse(serialized);
    294                     } catch (e) {
    295                         logging.captureMessage('Failed to load layers settings from localstorage - invalid json', {
    296                             "localstorage.leafletLayersSettings": serialized.slice(0, 1000)
    297                         });
    298                     }
    299                 }
    300                 const layersSettings = settings.layers ?? [];
    301 
    302                 // load custom layers;
    303                 for (const layerSettings of layersSettings) {
    304                     if (layerSettings.isCustom) {
    305                         // custom layers can be upgraded in loadCustomLayerFromString and their code will change
    306                         const newCode = this.loadCustomLayerFromString(String(layerSettings.code));
    307                         if (newCode) {
    308                             layerSettings.code = newCode;
    309                         } else {
    310                             logging.captureMessage(
    311                                 `Failed to load custom layer from local storage record: "${layerSettings.code}"`
    312                             );
    313                         }
    314                     }
    315                 }
    316 
    317                 // apply settings to layers
    318                 const layersSettingsByCode = {};
    319                 layersSettings.forEach((it) => {
    320                     layersSettingsByCode[it.code] = it;
    321                 });
    322 
    323                 for (let layer of this.allLayers()) {
    324                     const layerSettings = layersSettingsByCode[layer.layer.options.code] ?? {};
    325                     // if storage is empty enable only default layers
    326                     // if new default layer appears it will be enabled
    327                     layer.enabled = layerSettings.enabled ?? layer.isDefault;
    328                     layer.layer.hotkey = layerSettings.hotkey || getLayerDefaultHotkey(layer);
    329                 }
    330                 this.updateLayers();
    331             },
    332 
    333             _onConfigButtonClick: function() {
    334                 if (this._layersConfigDialog.visible() || this._customLayerWindow) {
    335                     return;
    336                 }
    337                 this._layersConfigDialog.showDialog();
    338             },
    339 
    340             initLayersConfigWindow: function() {
    341                 this._layersConfigDialog = new LayersConfigDialog(
    342                     this._builtinLayersByGroup,
    343                     this._customLayers,
    344                     this._withHotkeys,
    345                     this.onConfigDialogOkClicked.bind(this),
    346                 );
    347                 this._map._controlContainer.appendChild(this._layersConfigDialog.getWindow());
    348             },
    349 
    350             onConfigDialogOkClicked: function(addedLayers) {
    351                 this.updateLayers(addedLayers);
    352             },
    353 
    354             onCustomLayerCreateClicked: function() {
    355                 this.showCustomLayerForm(
    356                     [
    357                         {
    358                             caption: 'Add layer',
    359                             callback: (fieldValues) => this.onCustomLayerAddClicked(fieldValues)
    360                         },
    361                         {
    362                             caption: 'Cancel',
    363                             callback: () => this.onCustomLayerCancelClicked()
    364                         }
    365                     ],
    366                     {
    367                         name: 'Custom layer',
    368                         url: '',
    369                         tms: false,
    370                         maxZoom: 18,
    371                         isOverlay: false,
    372                         scaleDependent: false,
    373                         isTop: true
    374                     }
    375                 );
    376             },
    377 
    378             updateLayersListControl: function(addedLayers) {
    379                 const disabledLayers = this.allLayers().filter((l) => !l.enabled);
    380                 disabledLayers.forEach((l) => this._map.removeLayer(l.layer));
    381                 [...this._layers].forEach((l) => this.removeLayer(l.layer));
    382 
    383                 let hasBaselayerOnMap = false;
    384                 const enabledLayers = this.allLayers().filter((l) => l.enabled);
    385                 enabledLayers.sort((l1, l2) => l1.order - l2.order);
    386                 enabledLayers.forEach((l) => {
    387                         l.layer._justAdded = addedLayers && addedLayers.includes(l);
    388                         const {layer: {options: {isOverlay}}} = l;
    389                         if (isOverlay) {
    390                             this.addOverlay(l.layer, l.title);
    391                         } else {
    392                             this.addBaseLayer(l.layer, l.title);
    393                         }
    394                         if (!isOverlay && this._map.hasLayer(l.layer)) {
    395                             hasBaselayerOnMap = true;
    396                         }
    397                     }
    398                 );
    399                 // если нет активного базового слоя, включить первый, если он есть
    400                 if (!hasBaselayerOnMap) {
    401                     for (let layer of enabledLayers) {
    402                         if (!layer.layer.options.isOverlay) {
    403                             this._map.addLayer(layer.layer);
    404                             hasBaselayerOnMap = true;
    405                             break;
    406                         }
    407                     }
    408                 }
    409                 // Not quite correct - the event will be fired even if there was no base layer before the update.
    410                 // Still it is better than not firing event at all when there is no base layer after update.
    411                 if (!hasBaselayerOnMap) {
    412                     this._map.fire('baselayerchange');
    413                 }
    414             },
    415 
    416             updateLayers: function(addedLayers) {
    417                 this.updateLayersListControl(addedLayers);
    418                 this.saveSettings();
    419             },
    420 
    421             saveSettings: function() {
    422                 const layersSettings = [];
    423 
    424                 for (let layer of this.allLayers()) {
    425                     layersSettings.push({
    426                         code: layer.layer.options.code,
    427                         isCustom: layer.isCustom,
    428                         enabled: layer.enabled,
    429                         hotkey: layer.layer.hotkey,
    430                     });
    431                 }
    432                 const settings = {layers: layersSettings};
    433                 safeLocalStorage.setItem('leafletLayersSettings', JSON.stringify(settings));
    434             },
    435 
    436             unserializeState: function(values) {
    437                 if (values) {
    438                     values = values.map((code) => {
    439                         let newCode = this.loadCustomLayerFromString(code);
    440                         return newCode || code;
    441                     });
    442                     for (let layer of this.allLayers()) {
    443                         if (layer.layer.options && values.includes(layer.layer.options.code)) {
    444                             layer.enabled = true;
    445                         }
    446                     }
    447                     this.updateLayers();
    448                 }
    449                 return originalUnserializeState.call(this, values);
    450             },
    451 
    452             showCustomLayerForm: function(buttons, fieldValues) {
    453                 if (this._customLayerWindow || this._configWindowVisible) {
    454                     return;
    455                 }
    456                 this._customLayerWindow =
    457                     L.DomUtil.create('div', 'leaflet-layers-dialog-wrapper', this._map._controlContainer);
    458 
    459                 L.DomEvent
    460                     .disableClickPropagation(this._customLayerWindow)
    461                     .disableScrollPropagation(this._customLayerWindow);
    462 
    463                 let customLayerWindow = L.DomUtil.create('div', 'custom-layers-window', this._customLayerWindow);
    464                 let form = L.DomUtil.create('form', '', customLayerWindow);
    465                 L.DomEvent.on(form, 'submit', L.DomEvent.preventDefault);
    466 
    467                 const dialogModel = {
    468                     name: ko.observable(fieldValues.name),
    469                     url: ko.observable(fieldValues.url),
    470                     tms: ko.observable(fieldValues.tms),
    471                     scaleDependent: ko.observable(fieldValues.scaleDependent),
    472                     maxZoom: ko.observable(fieldValues.maxZoom),
    473                     isOverlay: ko.observable(fieldValues.isOverlay),
    474                     isTop: ko.observable(fieldValues.isTop),
    475                     buttons: buttons,
    476                     buttonClicked: function buttonClicked(callbackN) {
    477                         const fieldValues = {
    478                             name: dialogModel.name().trim(),
    479                             url: dialogModel.url().trim(),
    480                             tms: dialogModel.tms(),
    481                             scaleDependent: dialogModel.scaleDependent(),
    482                             maxZoom: dialogModel.maxZoom(),
    483                             isOverlay: dialogModel.isOverlay(),
    484                             isTop: dialogModel.isTop()
    485                         };
    486                         buttons[callbackN].callback(fieldValues);
    487                     }
    488                 };
    489 
    490 /* eslint-disable max-len */
    491                 const formHtml = `
    492 <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>
    493 <label>Layer name<br/>
    494 <span class="hint">Maximum 40 characters</span><br/>
    495 <input maxlength="40" class="layer-name" data-bind="value: name"/></label><br/>
    496 <label>Tile url template<br/><textarea data-bind="value: url" class="layer-url"></textarea></label><br/>
    497 <label><input type="radio" name="overlay" data-bind="checked: isOverlay, checkedValue: false">Base layer</label><br/>
    498 <label><input type="radio" name="overlay" data-bind="checked: isOverlay, checkedValue: true">Overlay</label><br/>
    499 <hr/>
    500 <label><input type="radio" name="top-or-bottom"
    501         data-bind="checked: isTop, checkedValue: false, enable: isOverlay">Place below other layers</label><br/>
    502 <label><input type="radio" name="top-or-bottom"
    503         data-bind="checked: isTop, checkedValue: true, enable: isOverlay">Place above other layers</label><br/>
    504 <hr/>
    505 <label><input type="checkbox" data-bind="checked: scaleDependent"/>Content depends on scale(like OSM or Google maps)</label><br/>
    506 <label><input type="checkbox" data-bind="checked: tms" />TMS rows order</label><br />
    507 
    508 <label>Max zoom<br>
    509 <select data-bind="options: [9,10,11,12,13,14,15,16,17,18], value: maxZoom"></select></label>
    510 <div data-bind="foreach: buttons">
    511     <a class="button" data-bind="click: $root.buttonClicked.bind(null, $index()), text: caption"></a>
    512 </div>`;
    513 /* eslint-enable max-len */
    514                 form.innerHTML = formHtml;
    515                 ko.applyBindings(dialogModel, form);
    516             },
    517 
    518             _addItem: function(obj) {
    519                 var label = originalAddItem.call(this, obj);
    520                 if (obj.layer.__customLayer) {
    521                     const editButton = L.DomUtil.create('div', 'custom-layer-edit-button icon-edit', label.children[0]);
    522                     editButton.title = 'Edit layer';
    523                     L.DomEvent.on(editButton, 'click', (e) =>
    524                         this.onCustomLayerEditClicked(obj.layer.__customLayer, e)
    525                     );
    526                 }
    527                 if (obj.layer._justAdded) {
    528                     L.DomUtil.addClass(label, 'leaflet-layers-configure-just-added-1');
    529                     setTimeout(() => {
    530                         L.DomUtil.addClass(label, 'leaflet-layers-configure-just-added-2');
    531                     }, 0);
    532                 }
    533                 return label;
    534             },
    535 
    536             serializeCustomLayer: function(fieldValues) {
    537                 let s = JSON.stringify(fieldValues);
    538                 s = s.replace(/[\u007f-\uffff]/ug,
    539                     function(c) {
    540                         return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
    541                     }
    542                 );
    543 
    544                 function encodeUrlSafeBase64(s) {
    545                     return btoa(s)
    546                         .replace(/\+/ug, '-')
    547                         .replace(/\//ug, '_');
    548                 }
    549 
    550                 return '-cs' + encodeUrlSafeBase64(s);
    551             },
    552 
    553             customLayerExists: function(fieldValues, ignoreLayer) {
    554                 const serialized = this.serializeCustomLayer(fieldValues);
    555                 for (let layer of this._customLayers) {
    556                     if (layer !== ignoreLayer && layer.serialized === serialized) {
    557                         return layer;
    558                     }
    559                 }
    560                 return false;
    561             },
    562 
    563             checkCustomLayerValues: function(fieldValues) {
    564                 if (!fieldValues.url) {
    565                     return {error: 'Url is empty'};
    566                 }
    567                 if (!fieldValues.name) {
    568                     return {error: 'Name is empty'};
    569                 }
    570                 return {};
    571             },
    572 
    573             onCustomLayerAddClicked: function(fieldValues) {
    574                 const error = this.checkCustomLayerValues(fieldValues).error;
    575                 if (error) {
    576                     notify(error);
    577                     return;
    578                 }
    579 
    580                 const duplicateLayer = this.customLayerExists(fieldValues);
    581                 if (duplicateLayer) {
    582                     let msg = 'Same layer already exists';
    583                     if (!duplicateLayer.enabled) {
    584                         msg += ' but it is hidden. You can enable it in layers setting.';
    585                     }
    586                     notify(msg);
    587                     return;
    588                 }
    589 
    590                 const layer = this.createCustomLayer(fieldValues);
    591                 layer.enabled = true;
    592                 this._customLayers.push(layer);
    593                 this.hideCustomLayerForm();
    594                 this.updateLayers();
    595             },
    596 
    597             createCustomLayer: function(fieldValues) {
    598                 const serialized = this.serializeCustomLayer(fieldValues);
    599                 const tileLayer = new L.Layer.CustomLayer(fieldValues.url, {
    600                         isOverlay: fieldValues.isOverlay,
    601                         tms: fieldValues.tms,
    602                         maxNativeZoom: fieldValues.maxZoom,
    603                         scaleDependent: fieldValues.scaleDependent,
    604                         print: true,
    605                         jnx: true,
    606                         code: serialized,
    607                         noCors: true,
    608                         isTop: fieldValues.isTop
    609                     }
    610                 );
    611 
    612                 const customLayer = {
    613                     title: fieldValues.name,
    614                     isDefault: false,
    615                     isCustom: true,
    616                     serialized: serialized,
    617                     layer: tileLayer,
    618                     order:
    619                         (fieldValues.isOverlay && fieldValues.isTop) ? customLayersOrder.top : customLayersOrder.bottom,
    620                     fieldValues: fieldValues,
    621                     enabled: true,
    622                     checked: ko.observable(true)
    623                 };
    624                 tileLayer.__customLayer = customLayer;
    625                 return customLayer;
    626             },
    627 
    628             onCustomLayerCancelClicked: function() {
    629                 this.hideCustomLayerForm();
    630             },
    631 
    632             hideCustomLayerForm: function() {
    633                 if (!this._customLayerWindow) {
    634                     return;
    635                 }
    636                 this._customLayerWindow.parentNode.removeChild(this._customLayerWindow);
    637                 this._customLayerWindow = null;
    638             },
    639 
    640             onCustomLayerEditClicked: function(layer, e) {
    641                 L.DomEvent.stop(e);
    642                 this.showCustomLayerForm([
    643                     {
    644                         caption: 'Save',
    645                         callback: (fieldValues) => this.onCustomLayerChangeClicked(layer, fieldValues),
    646                     },
    647                     {caption: 'Delete', callback: () => this.onCustomLayerDeleteClicked(layer)},
    648                         {caption: 'Cancel', callback: () => this.onCustomLayerCancelClicked()}
    649                     ], layer.fieldValues
    650                 );
    651             },
    652 
    653             onCustomLayerChangeClicked: function(layer, newFieldValues) {
    654                 const error = this.checkCustomLayerValues(newFieldValues).error;
    655                 if (error) {
    656                     notify(error);
    657                     return;
    658                 }
    659                 const duplicateLayer = this.customLayerExists(newFieldValues, layer);
    660                 if (duplicateLayer) {
    661                     let msg = 'Same layer already exists';
    662                     if (!duplicateLayer.enabled) {
    663                         msg += ' but it is hidden. You can enable it in layers setting.';
    664                     }
    665                     notify(msg);
    666                     return;
    667                 }
    668 
    669                 const newLayer = this.createCustomLayer(newFieldValues);
    670                 newLayer.layer.hotkey = layer.layer.hotkey;
    671                 this._customLayers.splice(this._customLayers.indexOf(layer), 1, newLayer);
    672                 const newLayerVisible = (
    673                     this._map.hasLayer(layer.layer) &&
    674                     // turn off layer if changing from overlay to baselayer
    675                     (!layer.layer.options.isOverlay || newLayer.layer.options.isOverlay)
    676                 );
    677                 if (newLayerVisible) {
    678                     this._map.addLayer(newLayer.layer);
    679                 }
    680                 this._map.removeLayer(layer.layer);
    681                 this.updateLayers();
    682                 if (newLayerVisible) {
    683                     newLayer.layer.fire('add');
    684                 }
    685                 this.hideCustomLayerForm();
    686             },
    687 
    688             onCustomLayerDeleteClicked: function(layer) {
    689                 this._map.removeLayer(layer.layer);
    690                 this._customLayers.splice(this._customLayers.indexOf(layer), 1);
    691                 this.updateLayers();
    692                 this.hideCustomLayerForm();
    693             },
    694 
    695             loadCustomLayerFromString: function(s) {
    696                 let fieldValues;
    697                 const m = s.match(/^-cs(.+)$/u);
    698                 if (m) {
    699                     s = m[1].replace(/-/ug, '+').replace(/_/ug, '/');
    700                     try {
    701                         s = atob(s);
    702                         fieldValues = JSON.parse(s);
    703                     } catch (e) {
    704                         // ignore malformed data
    705                     }
    706 
    707                     if (fieldValues) {
    708                         // upgrade
    709                         if (fieldValues.isTop === undefined) {
    710                             fieldValues.isTop = true;
    711                         }
    712                         if (!this.customLayerExists(fieldValues)) {
    713                             this._customLayers.push(this.createCustomLayer(fieldValues));
    714                         }
    715                         return this.serializeCustomLayer(fieldValues);
    716                     }
    717                 }
    718                 return null;
    719             }
    720         }
    721     );
    722 }
    723 
    724 export default enableConfig;