nakarte

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

index.js (16386B)


      1 import L from 'leaflet';
      2 import {makeButton} from '~/lib/leaflet.control.commons';
      3 import './style.css';
      4 import localStorage from '~/lib/safe-localstorage';
      5 
      6 const STATE_DISABLED = 'disabled';
      7 const STATE_LOCATING = 'locating';
      8 const STATE_ENABLED = 'enabled';
      9 const STATE_ENABLED_FOLLOWING = 'enabled_following';
     10 const STATE_MOVING_TO_FOLLOWING = 'moving_to_following';
     11 const STATE_MOVING_TO_FOLLOWING_FIRST = 'moving_to_following_first';
     12 const STATE_UPDATING_FOLLOWING = 'updating_following';
     13 
     14 const EVENT_INIT = 'init';
     15 const EVENT_BUTTON_CLICK = 'button_click';
     16 const EVENT_LOCATION_RECEIVED = 'location_received';
     17 const EVENT_LOCATION_ERROR = 'location_error';
     18 const EVENT_MAP_MOVE = 'user_move';
     19 const EVENT_MAP_MOVE_END = 'map_move_end';
     20 
     21 const LOCALSTORAGE_POSITION = 'leaflet_locate_position';
     22 
     23 const PositionMarker = L.LayerGroup.extend({
     24     initialize: function(options) {
     25         L.LayerGroup.prototype.initialize.call(this, options);
     26         this._locationSet = false;
     27         this._elements = {
     28             accuracyCircle: L.circle([0, 0], {
     29                 radius: 1,
     30                 interactive: false,
     31                 fillColor: '#4271a8',
     32                 color: '#2ba3f7',
     33                 weight: 2
     34             }),
     35             markerCircle: L.circleMarker([0, 0], {
     36                 interactive: false,
     37                 radius: 10,
     38                 color: '#2ba3f7',
     39                 weight: 2.5,
     40                 fill: null,
     41                 opacity: 0.8
     42             }),
     43             markerPoint: L.circleMarker([0, 0], {
     44                 interactive: false,
     45                 radius: 2,
     46                 weight: 0,
     47                 color: '#2ba3f7',
     48                 fillOpacity: 0.8
     49             }),
     50         };
     51         this.addLayer(this._elements.accuracyCircle);
     52     },
     53 
     54     onAdd: function(map) {
     55         L.LayerGroup.prototype.onAdd.call(this, map);
     56         map.on('zoom', this._onZoom, this);
     57         this._updatePrecisionState();
     58     },
     59 
     60     onRemove: function(map) {
     61         map.off('zoom', this._onZoom, this);
     62         L.LayerGroup.prototype.onRemove.call(this, map);
     63     },
     64 
     65     _updatePrecisionState: function() {
     66         if (!this._map || !this._locationSet) {
     67             return;
     68         }
     69         const precise = this._elements.accuracyCircle._radius <= this._elements.markerCircle.options.radius * 0.8;
     70         if (precise !== this._precise) {
     71             if (precise) {
     72                 this._elements.accuracyCircle.setStyle({opacity: 0, fillOpacity: 0});
     73                 this.addLayer(this._elements.markerPoint);
     74                 this.addLayer(this._elements.markerCircle);
     75             } else {
     76                 this._elements.accuracyCircle.setStyle({opacity: 0.8, fillOpacity: 0.4});
     77                 this.removeLayer(this._elements.markerPoint);
     78                 this.removeLayer(this._elements.markerCircle);
     79             }
     80             this._precise = precise;
     81         }
     82     },
     83 
     84     setLocation: function(latlng, accuracy) {
     85         this._elements.accuracyCircle.setLatLng(latlng);
     86         this._elements.accuracyCircle.setRadius(accuracy);
     87         this._elements.markerCircle.setLatLng(latlng);
     88         this._elements.markerPoint.setLatLng(latlng);
     89         this._locationSet = true;
     90         this._updatePrecisionState();
     91     },
     92 
     93     _onZoom: function() {
     94         this._updatePrecisionState();
     95     }
     96 
     97 });
     98 
     99 const LocateControl = L.Control.extend({
    100         // button click behavior:
    101         // if button turned off -- turn on, maps follows marker
    102         // if button turned on
    103         //     if map is following marker -- turn off
    104         //     if map not following marker -- center map at marker, start following
    105 
    106         options: {
    107             locationAcquireTimeoutMS: Infinity,
    108             showError: ({message}) => {
    109                 alert(message);
    110             },
    111             maxAutoZoom: 17,
    112             minAutoZoomDeltaForAuto: 4,
    113             minDistForAutoZoom: 2 // in average screen sizes
    114         },
    115 
    116         initialize: function(options) {
    117             L.Control.prototype.initialize.call(this, options);
    118             this._events = [];
    119         },
    120 
    121         onAdd: function(map) {
    122             this._map = map;
    123             const {container, link} = makeButton('leaflet-control-locate', 'Where am I?', 'icon-position');
    124             this._container = container;
    125             L.DomEvent.on(link, 'click', () => this._handleEvent(EVENT_BUTTON_CLICK));
    126             this._marker = new PositionMarker();
    127             this._handleEvent(EVENT_INIT);
    128             return container;
    129         },
    130 
    131         moveMapToCurrentLocation: function(zoom) {
    132             let storedPosition = null;
    133             try {
    134                 storedPosition = JSON.parse(localStorage.getItem(LOCALSTORAGE_POSITION));
    135                 let {lat, lon} = storedPosition;
    136                 if (lat && lon) {
    137                     storedPosition = L.latLng(lat, lon);
    138                 } else {
    139                     storedPosition = null;
    140                 }
    141             } catch (e) {
    142                 // ignore invalid data from localstorage
    143             }
    144 
    145             if (storedPosition) {
    146                 this._map.setView(storedPosition, zoom, {animate: false});
    147                 if (!('geolocation' in navigator)) {
    148                     return;
    149                 }
    150                 navigator.geolocation.getCurrentPosition(
    151                     (pos) => {
    152                         this._storePositionToLocalStorage(pos);
    153                         this._map.setView(L.latLng(pos.coords.latitude, pos.coords.longitude), zoom, {
    154                             animate: false,
    155                         });
    156                     },
    157                     (e) => {
    158                         if (e.code === 1) {
    159                             localStorage.removeItem(LOCALSTORAGE_POSITION);
    160                         }
    161                     }, {
    162                     enableHighAccuracy: false,
    163                     timeout: 500,
    164                     maximumAge: 0
    165                 });
    166             }
    167         },
    168 
    169         _startLocating: function() {
    170             if (!('geolocation' in navigator) || !('watchPosition' in navigator.geolocation)) {
    171                 const error = {code: 0, message: 'Geolocation not supported'};
    172                 setTimeout(() => {
    173                         this._onLocationError(error);
    174                     }, 0
    175                 );
    176             }
    177             this._watchID = navigator.geolocation.watchPosition(
    178                 this._onLocationSuccess.bind(this), this._onLocationError.bind(this),
    179                 {
    180                     enableHighAccuracy: true,
    181                     timeout: this.options.locationAcquireTimeoutMS,
    182                 }
    183             );
    184         },
    185 
    186         _storePositionToLocalStorage: function(pos) {
    187             const coords = {lat: pos.coords.latitude, lon: pos.coords.longitude};
    188             localStorage.setItem(LOCALSTORAGE_POSITION, JSON.stringify(coords));
    189         },
    190 
    191         _onLocationSuccess: function(pos) {
    192             this._handleEvent(EVENT_LOCATION_RECEIVED, pos);
    193             this._storePositionToLocalStorage(pos);
    194         },
    195 
    196         _onLocationError: function(e) {
    197             this._handleEvent(EVENT_LOCATION_ERROR, e);
    198             if (e.code === 1) {
    199                 localStorage.removeItem(LOCALSTORAGE_POSITION);
    200             }
    201         },
    202 
    203         _stopLocating: function() {
    204             if (this._watchID && navigator.geolocation) {
    205                 navigator.geolocation.clearWatch(this._watchID);
    206             }
    207         },
    208 
    209         _storeLocation: function(position) {
    210             this._latlng = L.latLng(position.coords.latitude, position.coords.longitude);
    211             this._accuracy = position.coords.accuracy;
    212         },
    213 
    214         _updateMarkerLocation: function() {
    215             this._marker.setLocation(this._latlng, this._accuracy);
    216         },
    217 
    218         _updateMapPositionWhileFollowing: function() {
    219             this._updateFollowingStartPosition = this._map.getCenter();
    220             this._updateFollowingDestPosition = this._latlng;
    221             this._map.panTo(this._latlng);
    222         },
    223 
    224         _setViewToLocation: function(preferAutoZoom) {
    225             if (!this._map || !this._latlng) {
    226                 return;
    227             }
    228 
    229             // autoZoom -- to fit accuracy cirlce on screen, but not more then options.maxAutoZoom (17)
    230             // if current zoom more then options.minAutoZoomDeltaForAuto less then autoZoom, set autoZoom
    231             // if map center far from geolocation, set autoZoom
    232             // if map center not far from geolocation
    233             //      if accuracy circle does not fit at current zoom, zoom out to fit
    234             //      if  current zoom is less then minAutoZoomDeltaForAuto less then autoZoom
    235             //          or >= autoZoom and circle fits screen, keep current zoom
    236 
    237             const currentZoom = this._map.getZoom();
    238             let zoomFitAccuracy = this._map.getBoundsZoom(this._latlng.toBounds(this._accuracy * 2));
    239             let autoZoom = zoomFitAccuracy;
    240             let newZoom;
    241             autoZoom = Math.min(autoZoom, this.options.maxAutoZoom);
    242 
    243             if (preferAutoZoom || autoZoom - currentZoom >= this.options.minAutoZoomDeltaForAuto) {
    244                 newZoom = autoZoom;
    245             } else {
    246                 const p1 = this._map.project(this._map.getCenter());
    247                 const p2 = this._map.project(this._latlng);
    248                 const screenSize = this._map.getSize();
    249                 const averageScreenSize = (screenSize.x + screenSize.y) / 2;
    250                 if (p1.distanceTo(p2) > averageScreenSize * this.options.minDistForAutoZoom) {
    251                     newZoom = autoZoom;
    252                 } else {
    253                     newZoom = currentZoom > zoomFitAccuracy ? zoomFitAccuracy : currentZoom;
    254                 }
    255             }
    256             this._map.setView(this._latlng, newZoom);
    257         },
    258 
    259         _onMapMove: function() {
    260             this._handleEvent(EVENT_MAP_MOVE);
    261         },
    262 
    263         _onMapMoveEnd: function() {
    264             const ll = this._map.getCenter();
    265             setTimeout(() => {
    266                     if (this._map.getCenter().equals(ll)) {
    267                         this._handleEvent(EVENT_MAP_MOVE_END);
    268                     }
    269                 }, 100
    270             );
    271         },
    272 
    273         _isMapOffsetFromFollowingSegment: function() {
    274             if (this._updateFollowingStartPosition) {
    275                 const p = this._map.project(this._map.getCenter());
    276                 const p1 = this._map.project(this._updateFollowingStartPosition);
    277                 const p2 = this._map.project(this._updateFollowingDestPosition);
    278                 return L.LineUtil.pointToSegmentDistance(p, p1, p2) > 5;
    279             }
    280             return true;
    281         },
    282 
    283         _isMapCenteredAtLocation: function() {
    284             if (!this._latlng || !this._map) {
    285                 return false;
    286             }
    287             let p1 = this._map.project(this._latlng);
    288             let p2 = this._map.project(this._map.getCenter());
    289             return p1.distanceTo(p2) < 5;
    290         },
    291 
    292         _updateButtonClasses: function(add, remove) {
    293             for (let cls of add) {
    294                 L.DomUtil.addClass(this._container, cls);
    295             }
    296             for (let cls of remove) {
    297                 L.DomUtil.removeClass(this._container, cls);
    298             }
    299         },
    300 
    301         _setEvents: function(on) {
    302             const f = on ? 'on' : 'off';
    303             this._map[f]('move', this._onMapMove, this);
    304             this._map[f]('moveend', this._onMapMoveEnd, this);
    305         },
    306 
    307         _handleEvent: function(event, data) {
    308             this._events.push({event, data});
    309             if (!this._processingEvent) {
    310                 this._processingEvent = true;
    311                 while (this._events.length) {
    312                     this._processEvent(this._events.shift());
    313                 }
    314                 this._processingEvent = false;
    315             }
    316         },
    317 
    318         _processEvent: function({event, data}) { // eslint-disable-line complexity
    319             // console.log('PROCESS EVENT', event);
    320             const state = this._state;
    321             switch (event) {
    322                 case EVENT_INIT:
    323                     this._setState(STATE_DISABLED);
    324                     break;
    325                 case EVENT_BUTTON_CLICK:
    326                     if (state === STATE_DISABLED) {
    327                         this._setState(STATE_LOCATING);
    328                     } else if (state === STATE_ENABLED) {
    329                         this._setState(STATE_MOVING_TO_FOLLOWING);
    330                         this._setViewToLocation();
    331                     } else {
    332                         this._setState(STATE_DISABLED);
    333                     }
    334                     break;
    335                 case EVENT_LOCATION_RECEIVED:
    336                     if (state === STATE_DISABLED) {
    337                         return;
    338                     }
    339                     this._storeLocation(data);
    340                     this._updateMarkerLocation();
    341                     if (state === STATE_LOCATING || state === STATE_MOVING_TO_FOLLOWING_FIRST) {
    342                         this._setViewToLocation(true);
    343                         this._setState(STATE_MOVING_TO_FOLLOWING_FIRST);
    344                     } else if (state === STATE_MOVING_TO_FOLLOWING) {
    345                         this._setViewToLocation();
    346                     } else if (this._state === STATE_ENABLED_FOLLOWING || state === STATE_UPDATING_FOLLOWING) {
    347                         this._updateMapPositionWhileFollowing();
    348                         this._setState(STATE_UPDATING_FOLLOWING);
    349                     }
    350                     break;
    351                 case EVENT_LOCATION_ERROR:
    352                     if (state === STATE_DISABLED) {
    353                         return;
    354                     }
    355                     this.options.showError(data);
    356                     this._setState(STATE_DISABLED);
    357                     break;
    358                 case EVENT_MAP_MOVE:
    359                     if (state === STATE_ENABLED_FOLLOWING) {
    360                         if (!this._isMapCenteredAtLocation() && this._isMapOffsetFromFollowingSegment()) {
    361                             this._setState(STATE_ENABLED);
    362                         }
    363                     }
    364                     break;
    365                 case EVENT_MAP_MOVE_END:
    366                     if (state === STATE_MOVING_TO_FOLLOWING) {
    367                         if (this._isMapCenteredAtLocation()) {
    368                             this._setState(STATE_ENABLED_FOLLOWING);
    369                         } else {
    370                             this._setState(STATE_ENABLED);
    371                         }
    372                     } else if (state === STATE_UPDATING_FOLLOWING || state === STATE_MOVING_TO_FOLLOWING_FIRST) {
    373                         if (this._isMapCenteredAtLocation() || !this._isMapOffsetFromFollowingSegment()) {
    374                             this._setState(STATE_ENABLED_FOLLOWING);
    375                         } else {
    376                             this._setState(STATE_ENABLED);
    377                         }
    378                     }
    379                     break;
    380                 default:
    381             }
    382         },
    383 
    384         _setState: function(newState) {
    385             const oldState = this._state;
    386             if (oldState === newState) {
    387                 return;
    388             }
    389             // console.log(`STATE: ${oldState} -> ${newState}`);
    390             switch (newState) {
    391                 case STATE_LOCATING:
    392                     this._startLocating();
    393                     this._updateButtonClasses(['requesting'], ['active', 'following']);
    394                     this._setEvents(true);
    395                     break;
    396                 case STATE_DISABLED:
    397                     this._stopLocating();
    398                     this._marker.removeFrom(this._map);
    399                     this._setEvents(false);
    400                     this._updateButtonClasses([], ['active', 'highlight', 'following', 'requesting']);
    401                     break;
    402                 case STATE_ENABLED:
    403                     this._updateButtonClasses(['active', 'highlight'], ['following', 'requesting']);
    404                     break;
    405                 case STATE_MOVING_TO_FOLLOWING_FIRST:
    406                     this._marker.addTo(this._map);
    407                     break;
    408                 case STATE_ENABLED_FOLLOWING:
    409                     this._updateButtonClasses(['active', 'highlight', 'following'], ['requesting']);
    410                     break;
    411                 default:
    412             }
    413             this._state = newState;
    414         },
    415     }
    416 );
    417 
    418 export {LocateControl};