nakarte

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

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;