nakarte

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

index.js (19869B)


      1 import L from 'leaflet';
      2 import ko from 'knockout';
      3 import googleProvider from './lib/google';
      4 import '~/lib/leaflet.hashState/leaflet.hashState';
      5 
      6 import './style.css';
      7 import {Events} from './lib/common';
      8 import '~/lib/controls-styles/controls-styles.css';
      9 import {makeButtonWithBar} from '~/lib/leaflet.control.commons';
     10 import mapillaryProvider from './lib/mapillary';
     11 import wikimediaProvider from './lib/wikimedia';
     12 import {DragEvents} from '~/lib/leaflet.events.drag';
     13 import {onElementResize} from '~/lib/anyElementResizeEvent';
     14 import safeLocalStorage from '~/lib/safe-localstorage';
     15 import mapyczProvider from './lib/mapycz';
     16 
     17 const PanoMarker = L.Marker.extend({
     18     options: {
     19         zIndexOffset: 10000
     20     },
     21 
     22     initialize: function() {
     23         const icon = L.divIcon({
     24                 className: 'leaflet-panorama-marker-wraper',
     25                 html: '<div class="leaflet-panorama-marker"></div>'
     26             }
     27         );
     28         L.Marker.prototype.initialize.call(this, [0, 0], {icon, interactive: false});
     29         this._postponeType = null;
     30         this._postponeHeading = null;
     31     },
     32 
     33     onAdd: function(map) {
     34         L.Marker.prototype.onAdd.call(this, map);
     35         if (this._postponeType !== null) {
     36             this.setType(this._postponeType);
     37         }
     38         if (this._postponeHeading !== null) {
     39             this.setHeading(this._postponeHeading);
     40         }
     41     },
     42 
     43     onRemove: function(map) {
     44         L.Marker.prototype.onRemove.call(this, map);
     45     },
     46 
     47     getIcon: function() {
     48         let markerIcon = this.getElement();
     49         markerIcon = markerIcon.children[0];
     50         return markerIcon;
     51     },
     52 
     53     setHeading: function(angle) {
     54         this._postponeHeading = angle;
     55         if (!this._map) {
     56             return;
     57         }
     58         const markerIcon = this.getIcon();
     59         markerIcon.style.transform = `rotate(${angle || 0}deg)`;
     60     },
     61 
     62     setType: function(markerType) {
     63         this._postponeType = markerType;
     64         if (!this._map) {
     65             return;
     66         }
     67         const className = {
     68             slim: 'leaflet-panorama-marker-circle',
     69             normal: 'leaflet-panorama-marker-binocular'
     70         }[markerType];
     71         this.getIcon().className = className;
     72     }
     73 });
     74 
     75 L.Control.Panoramas = L.Control.extend({
     76         includes: L.Mixin.Events,
     77 
     78         options: {
     79             position: 'topleft',
     80             splitVerically: true,
     81             splitSizeFraction: 0.5,
     82             minViewerSize: 30,
     83         },
     84 
     85         getProviders: function() {
     86             return [
     87                 {
     88                     name: 'google',
     89                     title: 'Google street view',
     90                     provider: googleProvider,
     91                     layerOptions: {zIndex: 10},
     92                     code: 'g',
     93                     selected: ko.observable(true),
     94                     mapMarkerType: 'normal'
     95                 },
     96                 {
     97                     name: 'wikimedia',
     98                     title: 'Wikimedia commons',
     99                     provider: wikimediaProvider,
    100                     layerOptions: {opacity: 0.7, zIndex: 9},
    101                     code: 'w',
    102                     selected: ko.observable(false),
    103                     mapMarkerType: 'slim'
    104                 },
    105                 {
    106                     name: 'mapillary',
    107                     title: 'Mapillary',
    108                     provider: mapillaryProvider,
    109                     layerOptions: {opacity: 0.7, zIndex: 8},
    110                     code: 'm',
    111                     selected: ko.observable(false),
    112                     mapMarkerType: 'normal'
    113                 },
    114                 {
    115                     name: 'mapycz',
    116                     title: 'mapy.cz',
    117                     provider: mapyczProvider,
    118                     layerOptions: {opacity: 0.7, zIndex: 8},
    119                     code: 'c',
    120                     selected: ko.observable(false),
    121                     mapMarkerType: 'normal'
    122                 }
    123             ];
    124         },
    125 
    126         initialize: function(options) {
    127             L.Control.prototype.initialize.call(this, options);
    128             this.loadSettings();
    129             this._panoramasContainer = L.DomUtil.create('div', 'panoramas-container');
    130             onElementResize(
    131                 this._panoramasContainer,
    132                 L.Util.requestAnimFrame.bind(null, this.onContainerResize.bind(this))
    133             );
    134             this.providers = this.getProviders();
    135             for (let provider of this.providers) {
    136                 provider.selected.subscribe(this.updateCoverageVisibility, this);
    137                 provider.container = L.DomUtil.create('div', 'panorama-container', this._panoramasContainer);
    138             }
    139             this.nearbyPoints = [];
    140             this.marker = new PanoMarker();
    141         },
    142 
    143         loadSettings: function() {
    144             let storedSettings;
    145             try {
    146                 storedSettings = JSON.parse(safeLocalStorage.panoramaSettings);
    147             } catch {
    148                 // ignore
    149             }
    150             this._splitVerically = storedSettings?.spitVertically ?? this.options.splitVerically;
    151             const fraction = storedSettings?.splitSizeFraction;
    152             this._splitSizeFraction = isNaN(fraction) ? this.options.splitSizeFraction : fraction;
    153         },
    154 
    155         saveSetting: function() {
    156             safeLocalStorage.panoramaSettings = JSON.stringify({
    157                 spitVertically: this._splitVerically,
    158                 splitSizeFraction: this._splitSizeFraction
    159             });
    160         },
    161 
    162         onAdd: function(map) {
    163             this._map = map;
    164 
    165             this._splitterDragging = false;
    166             const splitter = L.DomUtil.create('div', 'panorama-splitter', this._panoramasContainer);
    167             L.DomUtil.create('div', 'splitter-border', splitter);
    168             const splitterButton = L.DomUtil.create('div', 'button', splitter);
    169             new DragEvents(splitter, null, {trackOutsideElement: true}).on({
    170                 dragstart: this.onSplitterDragStart,
    171                 dragend: this.onSplitterDragEnd,
    172                 drag: this.onSplitterDrag,
    173             }, this);
    174             new DragEvents(splitterButton, null, {trackOutsideElement: true}).on({
    175                 click: this.onSplitterClick
    176             }, this);
    177             this.setupViewerLayout();
    178             const {container, link, barContainer} = makeButtonWithBar(
    179                 'leaflet-contol-panoramas', 'Show panoramas (Alt-P)', 'icon-panoramas');
    180             map.on('resize', this.onMapResize, this);
    181             this._container = container;
    182             L.DomEvent.on(link, 'click', this.onButtonClick, this);
    183             L.DomEvent.on(document, 'keyup', this.onKeyUp, this);
    184             barContainer.innerHTML = `
    185                  <div class="panoramas-list" data-bind="foreach: providers">
    186                      <div>
    187                          <label>
    188                              <input type="checkbox" data-bind="checked: selected"><span data-bind="text: title"></span>
    189                          </label>
    190                      </div>
    191                  </div>
    192              `;
    193 
    194             ko.applyBindings(this, container);
    195             map.createPane('rasterOverlay').style.zIndex = 300;
    196             return container;
    197         },
    198 
    199         onSplitterDrag: function(e) {
    200             const minSize = this.options.minViewerSize;
    201             const container = this._panoramasContainer;
    202             const oldSize = container[this._splitVerically ? 'offsetWidth' : 'offsetHeight'];
    203             let newSize = oldSize + e.dragMovement[this._splitVerically ? 'x' : 'y'];
    204             const mapSize = this._map._container[this._splitVerically ? 'offsetWidth' : 'offsetHeight'];
    205             if (newSize < minSize) {
    206                 newSize = this.options.minViewerSize;
    207             }
    208             const maxSize = oldSize + mapSize - minSize;
    209             if (newSize > maxSize) {
    210                 newSize = maxSize;
    211             }
    212             this.setContainerSizePixels(newSize);
    213         },
    214 
    215         onSplitterClick: function() {
    216             this._splitVerically = !this._splitVerically;
    217             this.saveSetting();
    218             this.setupViewerLayout();
    219         },
    220 
    221         onButtonClick: function() {
    222             this.switchControl();
    223         },
    224 
    225         switchControl: function() {
    226             if (this.controlEnabled) {
    227                 this.disableControl();
    228             } else {
    229                 this.enableControl();
    230             }
    231         },
    232 
    233         onKeyUp: function(e) {
    234             if (e.keyCode === 'P'.codePointAt(0) && e.altKey) {
    235                 this.switchControl();
    236             }
    237         },
    238 
    239         enableControl: function() {
    240             if (this.controlEnabled) {
    241                 return;
    242             }
    243             this.controlEnabled = true;
    244             L.DomUtil.addClass(this._container, 'active');
    245             this.updateCoverageVisibility();
    246             this._map.on('click', this.onMapClick, this);
    247             L.DomUtil.addClass(this._map._container, 'panoramas-control-active');
    248             this.notifyChange();
    249         },
    250 
    251         disableControl: function() {
    252             if (!this.controlEnabled) {
    253                 return;
    254             }
    255             this.controlEnabled = false;
    256             L.DomUtil.removeClass(this._container, 'active');
    257             this.updateCoverageVisibility();
    258             this._map.off('click', this.onMapClick, this);
    259             this.hidePanoViewer();
    260             L.DomUtil.removeClass(this._map._container, 'panoramas-control-active');
    261             this.notifyChange();
    262         },
    263 
    264         updateCoverageVisibility: function() {
    265             if (!this._map) {
    266                 return;
    267             }
    268             for (let provider of this.providers) {
    269                 if (this.controlEnabled && provider.selected()) {
    270                     if (!provider.coverageLayer) {
    271                         const options = L.extend({pane: 'rasterOverlay'}, provider.layerOptions);
    272                         provider.coverageLayer = provider.provider.getCoverageLayer(options);
    273                     }
    274                     provider.coverageLayer.addTo(this._map);
    275                 } else {
    276                     if (provider.coverageLayer) {
    277                         this._map.removeLayer(provider.coverageLayer);
    278                     }
    279                 }
    280             }
    281             this.notifyChange();
    282         },
    283 
    284         showPanoramaContainer: function() {
    285             L.DomUtil.addClass(this._panoramasContainer, 'enabled');
    286         },
    287 
    288         panoramaVisible: function() {
    289             if (L.DomUtil.hasClass(this._panoramasContainer, 'enabled')) {
    290                 for (let provider of this.providers) {
    291                     if (L.DomUtil.hasClass(provider.container, 'enabled')) {
    292                         return provider;
    293                     }
    294                 }
    295             }
    296             return false;
    297         },
    298 
    299         setupNearbyPoints: function(points) {
    300             for (let point of this.nearbyPoints) {
    301                 this._map.removeLayer(point);
    302             }
    303             this.nearbyPoints = [];
    304             if (points) {
    305                 const icon = L.divIcon({className: 'leaflet-panorama-marker-point'});
    306                 for (let latlng of points) {
    307                     this.nearbyPoints.push(L.marker(latlng, {icon}).addTo(this._map));
    308                 }
    309             }
    310         },
    311 
    312         hidePano: function(provider) {
    313             L.DomUtil.removeClass(provider.container, 'enabled');
    314             if (provider.viewer) {
    315                 provider.viewer.deactivate();
    316             }
    317             this.setupNearbyPoints();
    318         },
    319 
    320         showPano: async function(provider, data) {
    321             this.showPanoramaContainer();
    322             for (let otherProvider of this.providers) {
    323                 if (otherProvider !== provider) {
    324                     this.hidePano(otherProvider);
    325                 }
    326             }
    327             L.DomUtil.addClass(provider.container, 'enabled');
    328             if (!provider.viewer) {
    329                 // eslint-disable-next-line require-atomic-updates
    330                 provider.viewer = await provider.provider.getViewer(provider.container);
    331                 this.setupViewerEvents(provider);
    332             }
    333             if (data) {
    334                 // wait for panorama container become of right size, needed for viewer setup
    335                 setTimeout(() => provider.viewer.showPano(data), 0);
    336             }
    337             provider.viewer.activate();
    338             this.marker.setType(provider.mapMarkerType);
    339             this.notifyChange();
    340         },
    341 
    342         setupViewerEvents: function(provider) {
    343             provider.viewer.on({
    344                 [Events.ImageChange]: this.onViewerImageChange,
    345                 [Events.BearingChange]: this.onViewerBearingChange,
    346                 [Events.YawPitchZoomChangeEnd]: this.onViewerZoomYawPitchChangeEnd,
    347                 closeclick: this.onPanoramaCloseClick
    348             }, this);
    349         },
    350 
    351         hidePanoViewer: function() {
    352             for (let provider of this.providers) {
    353                 this.hidePano(provider);
    354             }
    355             L.DomUtil.removeClass(this._panoramasContainer, 'enabled');
    356             this._map.removeLayer(this.marker);
    357             this.notifyChange();
    358         },
    359 
    360         notifyChange: function() {
    361             this.fire('change');
    362         },
    363 
    364         onViewerImageChange: function(e) {
    365             if (!this._map.getBounds().pad(-0.05).contains(e.latlng)) {
    366                 this._map.panTo(e.latlng);
    367             }
    368             this._map.addLayer(this.marker);
    369             this.marker.setLatLng(e.latlng);
    370             this.notifyChange();
    371         },
    372 
    373         onViewerBearingChange: function(e) {
    374             this.marker.setHeading(e.bearing);
    375         },
    376 
    377         onViewerZoomYawPitchChangeEnd: function() {
    378             this.notifyChange();
    379         },
    380 
    381         onPanoramaCloseClick: function() {
    382             this.hidePanoViewer();
    383         },
    384 
    385         onMapClick: async function(e) {
    386             const
    387                 searchRadiusPx = 24,
    388                 p = this._map.project(e.latlng).add([searchRadiusPx, 0]),
    389                 searchRadiusMeters = e.latlng.distanceTo(this._map.unproject(p)),
    390                 promises = [];
    391             for (let provider of this.providers) {
    392                 if (provider.selected()) {
    393                     promises.push({
    394                         promise: provider.provider.getPanoramaAtPos(e.latlng, searchRadiusMeters),
    395                         provider: provider
    396                     });
    397                 }
    398             }
    399 
    400             for (let {promise, provider} of promises) {
    401                 let searchResult = await promise;
    402                 if (searchResult.found) {
    403                     this._map.removeLayer(this.marker);
    404                     this.provider = provider;
    405                     this.showPano(provider, searchResult.data);
    406                     return;
    407                 }
    408             }
    409         },
    410 
    411         setupViewerLayout: function() {
    412             let sidebar;
    413             if (this._splitVerically) {
    414                 L.DomUtil.addClass(this._panoramasContainer, 'split-vertical');
    415                 L.DomUtil.removeClass(this._panoramasContainer, 'split-horizontal');
    416                 sidebar = 'left';
    417                 this._panoramasContainer.style.height = '100%';
    418             } else {
    419                 L.DomUtil.addClass(this._panoramasContainer, 'split-horizontal');
    420                 L.DomUtil.removeClass(this._panoramasContainer, 'split-vertical');
    421                 sidebar = 'top';
    422                 this._panoramasContainer.style.width = '100%';
    423             }
    424             this._map.addElementToSidebar(sidebar, this._panoramasContainer);
    425             this.updateContainerSize();
    426         },
    427 
    428         setContainerSizePixels: function(size) {
    429             size = Math.round(size);
    430             this._panoramasContainer.style[this._splitVerically ? 'width' : 'height'] = `${size}px`;
    431             setTimeout(() => { // map size has not updated yet
    432                 const mapSize = this._map._container[this._splitVerically ? 'offsetWidth' : 'offsetHeight'];
    433                 this._splitSizeFraction = size / (mapSize + size);
    434                 this.saveSetting();
    435             }, 0);
    436         },
    437 
    438         updateContainerSize: function() {
    439             const fraction = this._splitSizeFraction;
    440             const container = this._panoramasContainer;
    441             const containerSize = container[this._splitVerically ? 'offsetWidth' : 'offsetHeight'];
    442             const mapSize = this._map._container[this._splitVerically ? 'offsetWidth' : 'offsetHeight'];
    443             const newSize = Math.round(fraction * (mapSize + containerSize));
    444             container.style[this._splitVerically ? 'width' : 'height'] = `${newSize}px`;
    445         },
    446 
    447         onContainerResize: function() {
    448             const provider = this.panoramaVisible();
    449             if (provider && provider.viewer) {
    450                 provider.viewer.invalidateSize();
    451             }
    452         },
    453 
    454         onMapResize: function() {
    455             if (!this._splitterDragging) {
    456                 this.updateContainerSize();
    457             }
    458         },
    459 
    460         onSplitterDragStart: function() {
    461             this._splitterDragging = true;
    462         },
    463 
    464         onSplitterDragEnd: function() {
    465             this._splitterDragging = false;
    466         },
    467     },
    468 );
    469 
    470 L.Control.Panoramas.include(L.Mixin.HashState);
    471 L.Control.Panoramas.include({
    472         stateChangeEvents: ['change'],
    473 
    474         serializeState: function() {
    475             let state = null;
    476             if (this.controlEnabled) {
    477                 state = [];
    478                 let coverageCode = '_';
    479                 for (let provider of this.providers) {
    480                     if (provider.selected()) {
    481                         coverageCode += provider.code;
    482                     }
    483                 }
    484                 state.push(coverageCode);
    485                 const provider = this.panoramaVisible();
    486                 if (provider && provider.viewer) {
    487                     let viewerState = provider.viewer.getState();
    488                     if (viewerState) {
    489                         state.push(provider.code);
    490                         state.push(...viewerState);
    491                     }
    492                 }
    493             }
    494             return state;
    495         },
    496 
    497         unserializeState: function(state) {
    498             if (!state) {
    499                 this.disableControl();
    500                 return true;
    501             }
    502 
    503             const coverageCode = state[0];
    504             if (!coverageCode || coverageCode[0] !== '_') {
    505                 return false;
    506             }
    507             this.enableControl();
    508             for (let provider of this.providers) {
    509                 provider.selected(coverageCode.includes(provider.code));
    510             }
    511             if (state.length > 2) {
    512                 const panoramaVisible = state[1];
    513                 for (let provider of this.providers) {
    514                     if (panoramaVisible === provider.code) {
    515                         this.showPano(provider).then(() => {
    516                             const success = provider.viewer.setState(state.slice(2));
    517                             if (!success) {
    518                                 this.hidePanoViewer();
    519                             }
    520                         });
    521                         break;
    522                     }
    523                 }
    524             }
    525             return true;
    526         }
    527     }
    528 );
    529 
    530 L.Control.Panoramas.hashStateUpgrader = function(panoramasControl) {
    531     return L.Util.extend({}, L.Mixin.HashState, {
    532         unserializeState: function(oldState) {
    533             if (oldState) {
    534                 const upgradedState = ['_g'];
    535                 if (oldState.length) {
    536                     upgradedState.push('g', ...oldState);
    537                 }
    538                 setTimeout(() => panoramasControl.unserializeState(upgradedState), 0);
    539             }
    540             return false;
    541         },
    542 
    543         serializeState: function() {
    544             return null;
    545         },
    546     });
    547 };