nakarte

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

index.js (28655B)


      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                             break;
    405                         }
    406                     }
    407                 }
    408             },
    409 
    410             updateLayers: function(addedLayers) {
    411                 this.updateLayersListControl(addedLayers);
    412                 this.saveSettings();
    413             },
    414 
    415             saveSettings: function() {
    416                 const layersSettings = [];
    417 
    418                 for (let layer of this.allLayers()) {
    419                     layersSettings.push({
    420                         code: layer.layer.options.code,
    421                         isCustom: layer.isCustom,
    422                         enabled: layer.enabled,
    423                         hotkey: layer.layer.hotkey,
    424                     });
    425                 }
    426                 const settings = {layers: layersSettings};
    427                 safeLocalStorage.setItem('leafletLayersSettings', JSON.stringify(settings));
    428             },
    429 
    430             unserializeState: function(values) {
    431                 if (values) {
    432                     values = values.map((code) => {
    433                         let newCode = this.loadCustomLayerFromString(code);
    434                         return newCode || code;
    435                     });
    436                     for (let layer of this.allLayers()) {
    437                         if (layer.layer.options && values.includes(layer.layer.options.code)) {
    438                             layer.enabled = true;
    439                         }
    440                     }
    441                     this.updateLayers();
    442                 }
    443                 return originalUnserializeState.call(this, values);
    444             },
    445 
    446             showCustomLayerForm: function(buttons, fieldValues) {
    447                 if (this._customLayerWindow || this._configWindowVisible) {
    448                     return;
    449                 }
    450                 this._customLayerWindow =
    451                     L.DomUtil.create('div', 'leaflet-layers-dialog-wrapper', this._map._controlContainer);
    452 
    453                 L.DomEvent
    454                     .disableClickPropagation(this._customLayerWindow)
    455                     .disableScrollPropagation(this._customLayerWindow);
    456 
    457                 let customLayerWindow = L.DomUtil.create('div', 'custom-layers-window', this._customLayerWindow);
    458                 let form = L.DomUtil.create('form', '', customLayerWindow);
    459                 L.DomEvent.on(form, 'submit', L.DomEvent.preventDefault);
    460 
    461                 const dialogModel = {
    462                     name: ko.observable(fieldValues.name),
    463                     url: ko.observable(fieldValues.url),
    464                     tms: ko.observable(fieldValues.tms),
    465                     scaleDependent: ko.observable(fieldValues.scaleDependent),
    466                     maxZoom: ko.observable(fieldValues.maxZoom),
    467                     isOverlay: ko.observable(fieldValues.isOverlay),
    468                     isTop: ko.observable(fieldValues.isTop),
    469                     buttons: buttons,
    470                     buttonClicked: function buttonClicked(callbackN) {
    471                         const fieldValues = {
    472                             name: dialogModel.name().trim(),
    473                             url: dialogModel.url().trim(),
    474                             tms: dialogModel.tms(),
    475                             scaleDependent: dialogModel.scaleDependent(),
    476                             maxZoom: dialogModel.maxZoom(),
    477                             isOverlay: dialogModel.isOverlay(),
    478                             isTop: dialogModel.isTop()
    479                         };
    480                         buttons[callbackN].callback(fieldValues);
    481                     }
    482                 };
    483 
    484 /* eslint-disable max-len */
    485                 const formHtml = `
    486 <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>
    487 <label>Layer name<br/>
    488 <span class="hint">Maximum 40 characters</span><br/>
    489 <input maxlength="40" class="layer-name" data-bind="value: name"/></label><br/>
    490 <label>Tile url template<br/><textarea data-bind="value: url" class="layer-url"></textarea></label><br/>
    491 <label><input type="radio" name="overlay" data-bind="checked: isOverlay, checkedValue: false">Base layer</label><br/>
    492 <label><input type="radio" name="overlay" data-bind="checked: isOverlay, checkedValue: true">Overlay</label><br/>
    493 <hr/>
    494 <label><input type="radio" name="top-or-bottom"
    495         data-bind="checked: isTop, checkedValue: false, enable: isOverlay">Place below other layers</label><br/>
    496 <label><input type="radio" name="top-or-bottom"
    497         data-bind="checked: isTop, checkedValue: true, enable: isOverlay">Place above other layers</label><br/>
    498 <hr/>
    499 <label><input type="checkbox" data-bind="checked: scaleDependent"/>Content depends on scale(like OSM or Google maps)</label><br/>
    500 <label><input type="checkbox" data-bind="checked: tms" />TMS rows order</label><br />
    501 
    502 <label>Max zoom<br>
    503 <select data-bind="options: [9,10,11,12,13,14,15,16,17,18], value: maxZoom"></select></label>
    504 <div data-bind="foreach: buttons">
    505     <a class="button" data-bind="click: $root.buttonClicked.bind(null, $index()), text: caption"></a>
    506 </div>`;
    507 /* eslint-enable max-len */
    508                 form.innerHTML = formHtml;
    509                 ko.applyBindings(dialogModel, form);
    510             },
    511 
    512             _addItem: function(obj) {
    513                 var label = originalAddItem.call(this, obj);
    514                 if (obj.layer.__customLayer) {
    515                     const editButton = L.DomUtil.create('div', 'custom-layer-edit-button icon-edit', label.children[0]);
    516                     editButton.title = 'Edit layer';
    517                     L.DomEvent.on(editButton, 'click', (e) =>
    518                         this.onCustomLayerEditClicked(obj.layer.__customLayer, e)
    519                     );
    520                 }
    521                 if (obj.layer._justAdded) {
    522                     L.DomUtil.addClass(label, 'leaflet-layers-configure-just-added-1');
    523                     setTimeout(() => {
    524                         L.DomUtil.addClass(label, 'leaflet-layers-configure-just-added-2');
    525                     }, 0);
    526                 }
    527                 return label;
    528             },
    529 
    530             serializeCustomLayer: function(fieldValues) {
    531                 let s = JSON.stringify(fieldValues);
    532                 s = s.replace(/[\u007f-\uffff]/ug,
    533                     function(c) {
    534                         return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
    535                     }
    536                 );
    537 
    538                 function encodeUrlSafeBase64(s) {
    539                     return btoa(s)
    540                         .replace(/\+/ug, '-')
    541                         .replace(/\//ug, '_');
    542                 }
    543 
    544                 return '-cs' + encodeUrlSafeBase64(s);
    545             },
    546 
    547             customLayerExists: function(fieldValues, ignoreLayer) {
    548                 const serialized = this.serializeCustomLayer(fieldValues);
    549                 for (let layer of this._customLayers) {
    550                     if (layer !== ignoreLayer && layer.serialized === serialized) {
    551                         return layer;
    552                     }
    553                 }
    554                 return false;
    555             },
    556 
    557             checkCustomLayerValues: function(fieldValues) {
    558                 if (!fieldValues.url) {
    559                     return {error: 'Url is empty'};
    560                 }
    561                 if (!fieldValues.name) {
    562                     return {error: 'Name is empty'};
    563                 }
    564                 return {};
    565             },
    566 
    567             onCustomLayerAddClicked: function(fieldValues) {
    568                 const error = this.checkCustomLayerValues(fieldValues).error;
    569                 if (error) {
    570                     notify(error);
    571                     return;
    572                 }
    573 
    574                 const duplicateLayer = this.customLayerExists(fieldValues);
    575                 if (duplicateLayer) {
    576                     let msg = 'Same layer already exists';
    577                     if (!duplicateLayer.enabled) {
    578                         msg += ' but it is hidden. You can enable it in layers setting.';
    579                     }
    580                     notify(msg);
    581                     return;
    582                 }
    583 
    584                 const layer = this.createCustomLayer(fieldValues);
    585                 layer.enabled = true;
    586                 this._customLayers.push(layer);
    587                 this.hideCustomLayerForm();
    588                 this.updateLayers();
    589             },
    590 
    591             createCustomLayer: function(fieldValues) {
    592                 const serialized = this.serializeCustomLayer(fieldValues);
    593                 const tileLayer = new L.Layer.CustomLayer(fieldValues.url, {
    594                         isOverlay: fieldValues.isOverlay,
    595                         tms: fieldValues.tms,
    596                         maxNativeZoom: fieldValues.maxZoom,
    597                         scaleDependent: fieldValues.scaleDependent,
    598                         print: true,
    599                         jnx: true,
    600                         code: serialized,
    601                         noCors: true,
    602                         isTop: fieldValues.isTop
    603                     }
    604                 );
    605 
    606                 const customLayer = {
    607                     title: fieldValues.name,
    608                     isDefault: false,
    609                     isCustom: true,
    610                     serialized: serialized,
    611                     layer: tileLayer,
    612                     order:
    613                         (fieldValues.isOverlay && fieldValues.isTop) ? customLayersOrder.top : customLayersOrder.bottom,
    614                     fieldValues: fieldValues,
    615                     enabled: true,
    616                     checked: ko.observable(true)
    617                 };
    618                 tileLayer.__customLayer = customLayer;
    619                 return customLayer;
    620             },
    621 
    622             onCustomLayerCancelClicked: function() {
    623                 this.hideCustomLayerForm();
    624             },
    625 
    626             hideCustomLayerForm: function() {
    627                 if (!this._customLayerWindow) {
    628                     return;
    629                 }
    630                 this._customLayerWindow.parentNode.removeChild(this._customLayerWindow);
    631                 this._customLayerWindow = null;
    632             },
    633 
    634             onCustomLayerEditClicked: function(layer, e) {
    635                 L.DomEvent.stop(e);
    636                 this.showCustomLayerForm([
    637                     {
    638                         caption: 'Save',
    639                         callback: (fieldValues) => this.onCustomLayerChangeClicked(layer, fieldValues),
    640                     },
    641                     {caption: 'Delete', callback: () => this.onCustomLayerDeleteClicked(layer)},
    642                         {caption: 'Cancel', callback: () => this.onCustomLayerCancelClicked()}
    643                     ], layer.fieldValues
    644                 );
    645             },
    646 
    647             onCustomLayerChangeClicked: function(layer, newFieldValues) {
    648                 const error = this.checkCustomLayerValues(newFieldValues).error;
    649                 if (error) {
    650                     notify(error);
    651                     return;
    652                 }
    653                 const duplicateLayer = this.customLayerExists(newFieldValues, layer);
    654                 if (duplicateLayer) {
    655                     let msg = 'Same layer already exists';
    656                     if (!duplicateLayer.enabled) {
    657                         msg += ' but it is hidden. You can enable it in layers setting.';
    658                     }
    659                     notify(msg);
    660                     return;
    661                 }
    662 
    663                 const newLayer = this.createCustomLayer(newFieldValues);
    664                 newLayer.layer.hotkey = layer.layer.hotkey;
    665                 this._customLayers.splice(this._customLayers.indexOf(layer), 1, newLayer);
    666                 const newLayerVisible = (
    667                     this._map.hasLayer(layer.layer) &&
    668                     // turn off layer if changing from overlay to baselayer
    669                     (!layer.layer.options.isOverlay || newLayer.layer.options.isOverlay)
    670                 );
    671                 if (newLayerVisible) {
    672                     this._map.addLayer(newLayer.layer);
    673                 }
    674                 this._map.removeLayer(layer.layer);
    675                 this.updateLayers();
    676                 if (newLayerVisible) {
    677                     newLayer.layer.fire('add');
    678                 }
    679                 this.hideCustomLayerForm();
    680             },
    681 
    682             onCustomLayerDeleteClicked: function(layer) {
    683                 this._map.removeLayer(layer.layer);
    684                 this._customLayers.splice(this._customLayers.indexOf(layer), 1);
    685                 this.updateLayers();
    686                 this.hideCustomLayerForm();
    687             },
    688 
    689             loadCustomLayerFromString: function(s) {
    690                 let fieldValues;
    691                 const m = s.match(/^-cs(.+)$/u);
    692                 if (m) {
    693                     s = m[1].replace(/-/ug, '+').replace(/_/ug, '/');
    694                     try {
    695                         s = atob(s);
    696                         fieldValues = JSON.parse(s);
    697                     } catch (e) {
    698                         // ignore malformed data
    699                     }
    700 
    701                     if (fieldValues) {
    702                         // upgrade
    703                         if (fieldValues.isTop === undefined) {
    704                             fieldValues.isTop = true;
    705                         }
    706                         if (!this.customLayerExists(fieldValues)) {
    707                             this._customLayers.push(this.createCustomLayer(fieldValues));
    708                         }
    709                         return this.serializeCustomLayer(fieldValues);
    710                     }
    711                 }
    712                 return null;
    713             }
    714         }
    715     );
    716 }
    717 
    718 export default enableConfig;