commit a6dc4a31d9498dd633f860ad5576e33d87069ce6
parent 7ae72a8901dc9ecb001359e4b005684291472605
Author: Sergej Orlov <wladimirych@gmail.com>
Date: Fri, 27 Apr 2018 17:09:12 +0300
replaced geo locating plugin with own one #59
Diffstat:
8 files changed, 383 insertions(+), 179 deletions(-)
diff --git a/package.json b/package.json
@@ -61,7 +61,6 @@
"image-promise": "^4.0.1",
"knockout": "^3.4.0",
"leaflet": "1.0.3",
- "leaflet.locatecontrol": "^0.62.0",
"load-script": "^1.0.0",
"mapillary-js": "2.5.2",
"pbf": "^3.0.5",
diff --git a/src/App.js b/src/App.js
@@ -26,7 +26,7 @@ import 'lib/leaflet.control.jnx';
import 'lib/leaflet.control.jnx/hash-state';
import 'lib/leaflet.control.azimuth';
import {hashState, bindHashStateReadOnly} from 'lib/leaflet.hashState/hashState';
-import {MyLocate} from 'lib/leaflet.control.mylocation';
+import {LocateControl} from 'lib/leaflet.control.locate';
function setUp() {
@@ -67,8 +67,7 @@ function setUp() {
const azimuthControl = new L.Control.Azimuth({position: 'topleft'}).addTo(map);
- const location = new MyLocate();
- location.addTo(map);
+ new LocateControl({position: 'topleft'}).addTo(map);
/////////// controls top-right corner
diff --git a/src/lib/leaflet.control.locate/index.js b/src/lib/leaflet.control.locate/index.js
@@ -0,0 +1,348 @@
+import L from 'leaflet';
+import 'lib/leaflet.control.commons'
+import './style.css';
+
+const STATE_DISABLED = 'disabled';
+const STATE_LOCATING = 'locating';
+const STATE_ENABLED = 'enabled';
+const STATE_ENABLED_FOLLOWING = 'enabled_following';
+const STATE_MOVING_TO_FOLLOWING = 'moving_to_following';
+const STATE_MOVING_TO_FOLLOWING_FIRST = 'moving_to_following_first';
+const STATE_UPDATING_FOLLOWING = 'updating_following';
+
+const EVENT_INIT = 'init';
+const EVENT_BUTTON_CLICK = 'button_click';
+const EVENT_LOCATION_RECEIVED = 'location_received';
+const EVENT_LOCATION_ERROR = 'location_error';
+const EVENT_MAP_MOVE = 'user_move';
+const EVENT_MAP_MOVE_END = 'map_move_end';
+
+
+const PositionMarker = L.LayerGroup.extend({
+ initialize: function(options) {
+ L.LayerGroup.prototype.initialize.call(this, options);
+ this._locationSet = false;
+ this._elements = {
+ accuracyCircle: L.circle([0, 0], {
+ radius: 1,
+ interactive: false,
+ fillColor: '#4271a8',
+ color: '#2ba3f7',
+ weight: 2
+ }),
+ markerCircle: L.circleMarker([0, 0], {
+ interactive: false,
+ radius: 10,
+ color: '#2ba3f7',
+ weight: 2.5,
+ fill: null,
+ opacity: 0.8
+ }),
+ markerPoint: L.circleMarker([0, 0], {
+ interactive: false,
+ radius: 2,
+ weight: 0,
+ color: '#2ba3f7',
+ fillOpacity: 0.8
+ }),
+ };
+ this.addLayer(this._elements.accuracyCircle);
+
+ },
+
+ onAdd: function(map) {
+ L.LayerGroup.prototype.onAdd.call(this, map);
+ map.on('zoom', this._onZoom, this);
+ this._updatePrecisionState();
+ },
+
+ onRemove: function(map) {
+ map.off('zoom', this._onZoom, this);
+ L.LayerGroup.prototype.onRemove.call(this, map);
+ },
+
+ _updatePrecisionState: function() {
+ if (!this._map || !this._locationSet) {
+ return;
+ }
+ const precise = this._elements.accuracyCircle._radius <= this._elements.markerCircle.options.radius * 0.8;
+ if (precise !== this._precise) {
+ if (precise) {
+ this._elements.accuracyCircle.setStyle({opacity: 0, fillOpacity: 0});
+ this.addLayer(this._elements.markerPoint);
+ this.addLayer(this._elements.markerCircle);
+ } else {
+ this._elements.accuracyCircle.setStyle({opacity: 0.8, fillOpacity: 0.4});
+ this.removeLayer(this._elements.markerPoint);
+ this.removeLayer(this._elements.markerCircle);
+
+ }
+ this._precise = precise;
+ }
+ },
+
+ setLocation: function(latlng, accuracy) {
+ this._elements.accuracyCircle.setLatLng(latlng);
+ this._elements.accuracyCircle.setRadius(accuracy);
+ this._elements.markerCircle.setLatLng(latlng);
+ this._elements.markerPoint.setLatLng(latlng);
+ this._locationSet = true;
+ this._updatePrecisionState();
+ },
+
+ _onZoom: function(e) {
+ this._updatePrecisionState();
+ }
+
+});
+
+const LocateControl = L.Control.extend({
+ // button click behavior:
+ // if button turned off -- turn on, maps follows marker
+ // if button turned on
+ // if map is following marker -- turn off
+ // if map not following marker -- center map at marker, start following
+
+ options: {
+ locationAcquireTimeoutMS: 10000,
+ showError: ({message}) => {
+ alert(message);
+ },
+ maxAutoZoom: 17,
+ minAutoZoomDeltaForAuto: 4,
+ minDistForAutoZoom: 2 // in average screen sizes
+ },
+
+ initialize: function(options) {
+ L.Control.prototype.initialize.call(this, options);
+ this._events = [];
+ },
+
+ onAdd: function(map) {
+ this._map = map;
+ const container = this._container = L.DomUtil.create(
+ 'div', 'leaflet-bar leaflet-control leaflet-control-locate'
+ );
+ this._stopContainerEvents();
+ const link = L.DomUtil.create('a', '', container);
+ L.DomUtil.create('div', 'icon-position', link);
+ L.DomEvent.on(container, 'click', () => this._handleEvent(EVENT_BUTTON_CLICK));
+ this._marker = new PositionMarker();
+ this._handleEvent(EVENT_INIT);
+ return container;
+ },
+
+ _startLocating: function() {
+ if (!('geolocation' in navigator)) {
+ const error = {code: 0, message: 'Geolocation not supported'};
+ setTimeout(() => {
+ this._onLocationError(error);
+ }, 0
+ )
+ }
+ this._watchID = navigator.geolocation.watchPosition(
+ (pos) => this._handleEvent(EVENT_LOCATION_RECEIVED, pos),
+ (e) => this._handleEvent(EVENT_LOCATION_ERROR, e),
+ {
+ enableHighAccuracy: true,
+ timeout: this.options.locationAcquireTimeoutMS,
+ }
+ );
+ },
+
+ _stopLocating: function() {
+ if (this._watchID && navigator.geolocation) {
+ navigator.geolocation.clearWatch(this._watchID);
+ }
+ },
+
+ _storeLocation: function(position) {
+ this._latlng = L.latLng(position.coords.latitude, position.coords.longitude);
+ this._accuracy = position.coords.accuracy;
+ },
+
+ _updateMarkerLocation: function() {
+ this._marker.setLocation(this._latlng, this._accuracy);
+ },
+
+ _updateMapPosition: function() {
+ this._map.panTo(this._latlng, {noMoveStart: true});
+ },
+
+ _setViewToLocation: function(preferAutoZoom) {
+ if (!this._map || !this._latlng) {
+ return;
+ }
+
+ // autoZoom -- to fit accuracy cirlce on screen, but not more then options.maxAutoZoom (17)
+ // if current zoom more then options.minAutoZoomDeltaForAuto less then autoZoom, set autoZoom
+ // if map center far from geolocation, set autoZoom
+ // if map center not far from geolocation
+ // if accuracy circle does not fit at current zoom, zoom out to fit
+ // if current zoom is less then minAutoZoomDeltaForAuto less then autoZoom or >= autoZoom and circle fits screen, keep current zoom
+
+ const currentZoom = this._map.getZoom();
+ let zoomFitAccuracy = this._map.getBoundsZoom(this._latlng.toBounds(this._accuracy * 2));
+ let autoZoom = zoomFitAccuracy;
+ let newZoom;
+ autoZoom = Math.min(autoZoom, this.options.maxAutoZoom);
+
+ if (preferAutoZoom || autoZoom - currentZoom >= this.options.minAutoZoomDeltaForAuto) {
+ newZoom = autoZoom;
+ } else {
+ const p1 = this._map.project(this._map.getCenter());
+ const p2 = this._map.project(this._latlng);
+ const screenSize = this._map.getSize();
+ const averageScreenSize = (screenSize.x + screenSize.y) / 2;
+ if (p1.distanceTo(p2) > averageScreenSize * this.options.minDistForAutoZoom) {
+ newZoom = autoZoom
+ } else {
+ newZoom = currentZoom > zoomFitAccuracy ? zoomFitAccuracy : currentZoom;
+ }
+ }
+ this._map.setView(this._latlng, newZoom);
+ },
+
+ _onMapMove: function() {
+ this._handleEvent(EVENT_MAP_MOVE)
+ },
+
+ _onMapMoveEnd: function() {
+ const ll = this._map.getCenter();
+ setTimeout(() => {
+ if (this._map.getCenter().equals(ll)) {
+ this._handleEvent(EVENT_MAP_MOVE_END)
+ }
+ }, 100);
+ },
+
+ _isMapCenteredAtLocation: function() {
+ if (!this._latlng || !this._map) {
+ return false;
+ }
+ let p1 = this._map.project(this._latlng);
+ let p2 = this._map.project(this._map.getCenter());
+ return p1.distanceTo(p2) < 5;
+ },
+
+ _updateButtonClasses: function(add, remove) {
+ for (let cls of add) {
+ L.DomUtil.addClass(this._container, cls);
+ }
+ for (let cls of remove) {
+ L.DomUtil.removeClass(this._container, cls);
+ }
+ },
+
+ _setEvents: function(on) {
+ const f = on ? 'on' : 'off';
+ this._map[f]('move', this._onMapMove, this);
+ this._map[f]('moveend', this._onMapMoveEnd, this);
+ },
+
+ _handleEvent: function(event, data) {
+ this._events.push({event, data});
+ if (!this._processingEvent) {
+ this._processingEvent = true;
+ while (this._events.length) {
+ this._processEvent(this._events.shift());
+ }
+ this._processingEvent = false;
+ }
+ },
+
+ _processEvent: function({event, data}) {
+ // console.log('PROCESS EVENT', event);
+ const state = this._state;
+ switch (event) {
+ case EVENT_INIT:
+ this._setState(STATE_DISABLED);
+ break;
+ case EVENT_BUTTON_CLICK:
+ if (state === STATE_DISABLED) {
+ this._setState(STATE_LOCATING);
+ } else if (state === STATE_ENABLED) {
+ this._setState(STATE_MOVING_TO_FOLLOWING);
+ this._setViewToLocation();
+ } else {
+ this._setState(STATE_DISABLED);
+ }
+ break;
+ case EVENT_LOCATION_RECEIVED:
+ if (state === STATE_DISABLED) {
+ return;
+ }
+ this._storeLocation(data);
+ this._updateMarkerLocation();
+ if (state === STATE_LOCATING || state === STATE_MOVING_TO_FOLLOWING_FIRST) {
+ this._setViewToLocation(true);
+ this._setState(STATE_MOVING_TO_FOLLOWING_FIRST);
+ } else if (state === STATE_MOVING_TO_FOLLOWING) {
+ this._setViewToLocation();
+ } else if (this._state === STATE_ENABLED_FOLLOWING || state === STATE_UPDATING_FOLLOWING) {
+ this._updateMapPosition();
+ this._setState(STATE_UPDATING_FOLLOWING)
+ }
+ break;
+ case EVENT_LOCATION_ERROR:
+ if (state === STATE_DISABLED) {
+ return
+ }
+ this.options.showError(data);
+ this._setState(STATE_DISABLED);
+ break;
+ case EVENT_MAP_MOVE:
+ if (state === STATE_ENABLED_FOLLOWING) {
+ if (!this._isMapCenteredAtLocation()) {
+ this._setState(STATE_ENABLED);
+ }
+ }
+ break;
+ case EVENT_MAP_MOVE_END:
+ if (state === STATE_MOVING_TO_FOLLOWING || state === STATE_UPDATING_FOLLOWING || state === STATE_MOVING_TO_FOLLOWING_FIRST) {
+ if (this._isMapCenteredAtLocation()) {
+ this._setState(STATE_ENABLED_FOLLOWING);
+ } else {
+ this._setState(STATE_ENABLED);
+ }
+ }
+ break;
+ default:
+ }
+ },
+
+ _setState: function(newState) {
+ const oldState = this._state;
+ if (oldState === newState) {
+ return;
+ }
+ console.log(`STATE: ${oldState} -> ${newState}`);
+ switch (newState) {
+ case STATE_LOCATING:
+ this._startLocating();
+ this._updateButtonClasses(['requesting'], ['active', 'following']);
+ this._setEvents(true);
+ break;
+ case STATE_DISABLED:
+ this._stopLocating();
+ this._marker.removeFrom(this._map);
+ this._setEvents(false);
+ this._updateButtonClasses([], ['active', 'following', 'requesting']);
+ break;
+ case STATE_ENABLED:
+ this._updateButtonClasses(['active'], ['following', 'requesting']);
+ break;
+ case STATE_MOVING_TO_FOLLOWING_FIRST:
+ this._marker.addTo(this._map);
+ break;
+ case STATE_ENABLED_FOLLOWING:
+ this._updateButtonClasses(['active', 'following'], ['requesting']);
+ break;
+ default:
+ }
+ this._state = newState;
+ },
+ }
+);
+
+export {LocateControl};
diff --git a/src/lib/leaflet.control.mylocation/location-arrow-active.svg b/src/lib/leaflet.control.locate/location-arrow-active.svg
diff --git a/src/lib/leaflet.control.mylocation/location-arrow.svg b/src/lib/leaflet.control.locate/location-arrow.svg
diff --git a/src/lib/leaflet.control.locate/style.css b/src/lib/leaflet.control.locate/style.css
@@ -0,0 +1,33 @@
+.icon-position {
+ background-image: url("location-arrow.svg");
+ background-size: 61%;
+ background-repeat: no-repeat;
+ background-position: 50% 50%;
+ width: 100%;
+ height: 100%;
+}
+
+.following .icon-position {
+ background-image: url("location-arrow-active.svg");
+}
+
+.requesting .icon-position {
+ animation-name: spin;
+ animation-duration: 500ms;
+ animation-iteration-count: infinite;
+ animation-timing-function: linear;
+ animation-delay: 100ms;
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.leaflet-control-locate.active a {
+ background-color: #cce8ff;
+}
diff --git a/src/lib/leaflet.control.mylocation/index.js b/src/lib/leaflet.control.mylocation/index.js
@@ -1,137 +0,0 @@
-import L from 'leaflet';
-import 'leaflet.locatecontrol';
-import './style.css';
-
-
-const MyLocate = L.Control.Locate.extend({
- options: {
- icon: 'icon-position',
- iconLoading: 'icon-position',
- setView: 'untilPan',
- flyTo: true,
- cacheLocation: false,
- showPopup: false,
- locateOptions: {
- enableHighAccuracy: true,
- watch: true,
- setView: false
- },
- maxZoom: 16,
-
- circleStyle: {
- interactive: false,
- color: '#4271a8',
- fillOpacity: 0.3,
- weight: 0,
- },
- markerStyle: {
- color: '#2a85d4',
- weight: 2.5,
- opacity: 0.8,
- fillOpacity: 0.4,
- radius: 8
- },
- minCirclePixelRadius: 50
-
- },
-
- start: function() {
- this.options.keepCurrentZoomLevel = false;
- L.Control.Locate.prototype.start.call(this);
- },
-
- _onDrag: function() {
- if (this._settingView) {
- return;
- }
- L.Control.Locate.prototype._onDrag.call(this);
- },
-
- _activate: function() {
- if (!this._active) {
- this._map.on('movestart', this._onDrag, this);
- this._map.on('zoom', this._onZoom, this);
- }
- L.Control.Locate.prototype._activate.call(this);
- },
-
- _deactivate: function() {
- L.Control.Locate.prototype._deactivate.call(this);
- this._map.off('movestart', this._onDrag, this);
- this._map.off('zoom', this._onZoom, this);
- },
-
- _onZoom: function() {
- if (!this._circle || !this.options.minCirclePixelRadius) {
- return;
- }
- if (typeof this._circle._origFillOpacity === 'undefined') {
- this._circle._origFillOpacity = this._circle.options.fillOpacity;
- }
- L.Util.requestAnimFrame(() => {
- const opacity = this._circle._radius < this.options.minCirclePixelRadius ?
- 0 : this._circle._origFillOpacity;
- console.log(this._circle._radius, this.options.minCirclePixelRadius, this._circle._origFillOpacity);
- this._circle.setStyle({fillOpacity: opacity});
- });
- },
-
- _onClick: function() {
- this.options.keepCurrentZoomLevel = false;
- L.Control.Locate.prototype._onClick.call(this);
- },
-
- _onMarkerClick: function() {
- this._userPanned = false;
- this._updateContainerStyle();
- this.options.keepCurrentZoomLevel = false;
- this.setView();
- },
-
- _drawMarker: function() {
- var newMarker = !this._marker;
- L.Control.Locate.prototype._drawMarker.call(this);
- if (newMarker) {
- this._marker.on('click', this._onMarkerClick.bind(this));
- }
- },
-
- setView: function() {
- this._drawMarker();
- if (this._isOutsideMapBounds()) {
- this._event = undefined; // clear the current location so we can get back into the bounds
- this.options.onLocationOutsideMapBounds(this);
- } else {
- var coords = this._event,
- lat = coords.latitude,
- lng = coords.longitude,
- latlng = new L.LatLng(lat, lng),
- zoom;
- if (!this.options.keepCurrentZoomLevel) {
- // fix for leaflet issue #6139
- var bounds = latlng.toBounds(coords.accuracy * 2);
- zoom = this._map.getBoundsZoom(bounds);
- zoom = this.options.maxZoom ? Math.min(zoom, this.options.maxZoom) : zoom;
- }
- this._settingView = true;
- this._map.once('moveend', () => {
- this._settingView = false;
- });
- var f = this.options.flyTo ? this._map.flyTo : this._map.setView;
- f.call(this._map, latlng, zoom);
-
- this.options.keepCurrentZoomLevel = true;
- }
- },
-
- }
-);
-
-export {MyLocate};
-
-
-// + при включение зумить, но не ближе ~16 уровня и не ближе круга точности.
-// + зум можно уменьшать только если круг точности большой и на 16 уровень не помещается
-// + при обновлении позиции зум не менять
-// + при плавном приближении маркер сильно зумится
-// + а что с сохранением состоямия в localStorage? -- ничего
-\ No newline at end of file
diff --git a/src/lib/leaflet.control.mylocation/style.css b/src/lib/leaflet.control.mylocation/style.css
@@ -1,37 +0,0 @@
-.icon-position {
- display: block;
- position: absolute;
- width: 60%;
- height: 60%;
- top: 20%;
- left: 20%;
- background-image: url("location-arrow.svg");
- background-size: 100%;
- background-repeat: no-repeat;
- background-position: 50% 50%;
-}
-
-.following .icon-position {
- background-image: url("location-arrow-active.svg");
-}
-
-.requesting .icon-position {
- animation-name: spin;
- animation-duration: 500ms;
- animation-iteration-count: infinite;
- animation-timing-function: linear;
- animation-delay: 100ms;
-}
-
-@keyframes spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-.leaflet-control-locate.active a {
- background-color: #cce8ff;
-}