+/*
+Copyright (c) 2014 Dominik Moritz
+
+This file is part of the leaflet locate control. It is licensed under the MIT license.
+You can find the project at: https://github.com/domoritz/leaflet-locatecontrol
+*/
L.Control.Locate = L.Control.extend({
options: {
position: 'topleft',
drawCircle: true,
follow: false, // follow with zoom and pan the user's location
+ stopFollowingOnDrag: false, // if follow is true, stop following when map is dragged (deprecated)
// range circle
circleStyle: {
- color: '#136AEC',
- fillColor: '#136AEC',
- fillOpacity: 0.15,
- weight: 2,
- opacity: 0.5
- },
+ color: '#136AEC',
+ fillColor: '#136AEC',
+ fillOpacity: 0.15,
+ weight: 2,
+ opacity: 0.5
+ },
// inner marker
markerStyle: {
color: '#136AEC',
fillOpacity: 0.7,
weight: 2,
opacity: 0.9,
- radius: 4
+ radius: 5
+ },
+ // changes to range circle and inner marker while following
+ // it is only necessary to provide the things that should change
+ followCircleStyle: {},
+ followMarkerStyle: {
+ //color: '#FFA500',
+ //fillColor: '#FFB000'
},
+ icon: 'icon-location', // icon-location or icon-direction
+ iconLoading: 'icon-spinner animate-spin',
+ circlePadding: [0, 0],
metric: true,
- debug: false,
onLocationError: function(err) {
+ // this event is called in case of any location error
+ // that is not a time out error.
alert(err.message);
},
- title: "Show me where I am",
- popupText: ["You are within ", " from this point"],
+ onLocationOutsideMapBounds: function(control) {
+ // this event is repeatedly called when the location changes
+ control.stopLocate();
+ alert(context.options.strings.outsideMapBoundsMsg);
+ },
setView: true, // automatically sets the map view to the user's location
- locateOptions: {}
+ // keep the current map zoom level when displaying the user's location. (if 'false', use maxZoom)
+ keepCurrentZoomLevel: false,
+ strings: {
+ title: "Show me where I am",
+ popup: "You are within {distance} {unit} from this point",
+ outsideMapBoundsMsg: "You seem located outside the boundaries of the map"
+ },
+ locateOptions: {
+ maxZoom: Infinity,
+ watch: true // if you overwrite this, visualization cannot be updated
+ }
},
onAdd: function (map) {
- var className = 'control-locate',
- container = L.DomUtil.create('div', className);
+ var container = L.DomUtil.create('div', 'control-locate');
var self = this;
this._layer = new L.LayerGroup();
this._layer.addTo(map);
this._event = undefined;
- // nested extend so that the first can overwrite the second
- // and the second can overwrite the third
- this._locateOptions = L.extend(L.extend({
- 'setView': false // have to set this to false because we have to
- // do setView manually
- }, this.options.locateOptions), {
- 'watch': true // if you overwrite this, visualization cannot be updated
+
+ this._locateOptions = this.options.locateOptions;
+ L.extend(this._locateOptions, this.options.locateOptions);
+ L.extend(this._locateOptions, {
+ setView: false // have to set this to false because we have to
+ // do setView manually
});
- var link = L.DomUtil.create('a', 'control-button', container);
+ // extend the follow marker style and circle from the normal style
+ var tmp = {};
+ L.extend(tmp, this.options.markerStyle, this.options.followMarkerStyle);
+ this.options.followMarkerStyle = tmp;
+ tmp = {};
+ L.extend(tmp, this.options.circleStyle, this.options.followCircleStyle);
+ this.options.followCircleStyle = tmp;
+
+ var link = L.DomUtil.create('a', 'control-button ' + this.options.icon, container);
link.innerHTML = "<span class='icon geolocate'></span>";
link.href = '#';
- link.title = this.options.title;
-
- var _log = function(data) {
- if (self.options.debug) {
- console.log(data);
- }
- };
+ link.title = this.options.strings.title;
L.DomEvent
.on(link, 'click', L.DomEvent.stopPropagation)
.on(link, 'click', L.DomEvent.preventDefault)
.on(link, 'click', function() {
- if (self._active && (map.getBounds().contains(self._event.latlng) || !self.options.setView)) {
+ if (self._active && (self._event === undefined || map.getBounds().contains(self._event.latlng) || !self.options.setView ||
+ isOutsideMapBounds())) {
stopLocate();
} else {
- if (self.options.setView) {
- self._locateOnNextLocationFound = true;
- }
- if(!self._active) {
- map.locate(self._locateOptions);
- }
- self._active = true;
- if (!self._event) {
- L.DomUtil.addClass(self._container, "requesting");
- } else {
- visualizeLocation();
- }
+ locate();
}
})
.on(link, 'dblclick', L.DomEvent.stopPropagation);
- var onLocationFound = function (e) {
- _log('onLocationFound');
-
+ var locate = function () {
+ if (self.options.setView) {
+ self._locateOnNextLocationFound = true;
+ }
+ if(!self._active) {
+ map.locate(self._locateOptions);
+ }
self._active = true;
+ if (self.options.follow) {
+ startFollowing();
+ }
+ if (!self._event) {
+ setClasses('requesting');
+ } else {
+ visualizeLocation();
+ }
+ };
+ var onLocationFound = function (e) {
+ // no need to do anything if the location has not changed
if (self._event &&
- (self._event.latlng.lat != e.latlng.lat ||
- self._event.latlng.lng != e.latlng.lng)) {
- _log('location has changed');
+ (self._event.latlng.lat === e.latlng.lat &&
+ self._event.latlng.lng === e.latlng.lng &&
+ self._event.accuracy === e.accuracy)) {
+ return;
+ }
+
+ if (!self._active) {
+ return;
}
self._event = e;
- if (self.options.follow) {
+ if (self.options.follow && self._following) {
self._locateOnNextLocationFound = true;
}
visualizeLocation();
};
- var visualizeLocation = function() {
- _log('visualizeLocation,' + 'setView:' + self._locateOnNextLocationFound);
+ var startFollowing = function() {
+ map.fire('startfollowing', self);
+ self._following = true;
+ if (self.options.stopFollowingOnDrag) {
+ map.on('dragstart', stopFollowing);
+ }
+ };
- var radius = self._event.accuracy / 2;
+ var stopFollowing = function() {
+ map.fire('stopfollowing', self);
+ self._following = false;
+ if (self.options.stopFollowingOnDrag) {
+ map.off('dragstart', stopFollowing);
+ }
+ visualizeLocation();
+ };
+
+ var isOutsideMapBounds = function () {
+ if (self._event === undefined)
+ return false;
+ return map.options.maxBounds &&
+ !map.options.maxBounds.contains(self._event.latlng);
+ };
+
+ var visualizeLocation = function() {
+ if (self._event.accuracy === undefined)
+ self._event.accuracy = 0;
+ var radius = self._event.accuracy;
if (self._locateOnNextLocationFound) {
- map.fitBounds(self._event.bounds);
+ if (isOutsideMapBounds()) {
+ self.options.onLocationOutsideMapBounds(self);
+ } else {
+ map.fitBounds(self._event.bounds, {
+ padding: self.options.circlePadding,
+ maxZoom: self.options.keepCurrentZoomLevel ? map.getZoom() : self._locateOptions.maxZoom
+ });
+ }
self._locateOnNextLocationFound = false;
}
- self._layer.clearLayers();
-
// circle with the radius of the location's accuracy
+ var style, o;
if (self.options.drawCircle) {
- L.circle(self._event.latlng, radius, self.options.circleStyle)
- .addTo(self._layer);
+ if (self._following) {
+ style = self.options.followCircleStyle;
+ } else {
+ style = self.options.circleStyle;
+ }
+
+ if (!self._circle) {
+ self._circle = L.circle(self._event.latlng, radius, style)
+ .addTo(self._layer);
+ } else {
+ self._circle.setLatLng(self._event.latlng).setRadius(radius);
+ for (o in style) {
+ self._circle.options[o] = style[o];
+ }
+ }
}
var distance, unit;
}
// small inner marker
- var t = self.options.popupText;
- L.circleMarker(self._event.latlng, self.options.markerStyle)
- .bindPopup(t[0] + distance + " " + unit + t[1])
- .addTo(self._layer);
+ var mStyle;
+ if (self._following) {
+ mStyle = self.options.followMarkerStyle;
+ } else {
+ mStyle = self.options.markerStyle;
+ }
+
+ var t = self.options.strings.popup;
+ if (!self._circleMarker) {
+ self._circleMarker = L.circleMarker(self._event.latlng, mStyle)
+ .bindPopup(L.Util.template(t, {distance: distance, unit: unit}))
+ .addTo(self._layer);
+ } else {
+ self._circleMarker.setLatLng(self._event.latlng)
+ .bindPopup(L.Util.template(t, {distance: distance, unit: unit}))
+ ._popup.setLatLng(self._event.latlng);
+ for (o in mStyle) {
+ self._circleMarker.options[o] = mStyle[o];
+ }
+ }
if (!self._container)
return;
-
- L.DomUtil.removeClass(self._container, "requesting");
- L.DomUtil.addClass(self._container, "active");
+ if (self._following) {
+ setClasses('following');
+ } else {
+ setClasses('active');
+ }
};
+ var setClasses = function(state) {
+ if (state == 'requesting') {
+ L.DomUtil.removeClasses(self._container, "active following");
+ L.DomUtil.addClasses(self._container, "requesting");
+
+ L.DomUtil.removeClasses(link, self.options.icon);
+ L.DomUtil.addClasses(link, self.options.iconLoading);
+ } else if (state == 'active') {
+ L.DomUtil.removeClasses(self._container, "requesting following");
+ L.DomUtil.addClasses(self._container, "active");
+
+ L.DomUtil.removeClasses(link, self.options.iconLoading);
+ L.DomUtil.addClasses(link, self.options.icon);
+ } else if (state == 'following') {
+ L.DomUtil.removeClasses(self._container, "requesting");
+ L.DomUtil.addClasses(self._container, "active following");
+
+ L.DomUtil.removeClasses(link, self.options.iconLoading);
+ L.DomUtil.addClasses(link, self.options.icon);
+ }
+ }
+
var resetVariables = function() {
self._active = false;
- self._locateOnNextLocationFound = true;
+ self._locateOnNextLocationFound = self.options.setView;
+ self._following = false;
};
resetVariables();
var stopLocate = function() {
- _log('stopLocate');
map.stopLocate();
+ map.off('dragstart', stopFollowing);
+ if (self.options.follow && self._following) {
+ stopFollowing();
+ }
L.DomUtil.removeClass(self._container, "requesting");
L.DomUtil.removeClass(self._container, "active");
-
+ L.DomUtil.removeClass(self._container, "following");
resetVariables();
self._layer.clearLayers();
+ self._circleMarker = undefined;
+ self._circle = undefined;
};
-
var onLocationError = function (err) {
- _log('onLocationError');
-
- // ignore timeout error if the location is watched
- if (err.code==3 && this._locateOptions.watch) {
+ // ignore time out error if the location is watched
+ if (err.code == 3 && self._locateOptions.watch) {
return;
}
map.on('locationfound', onLocationFound, self);
map.on('locationerror', onLocationError, self);
+ // make locate functions available to outside world
+ this.locate = locate;
+ this.stopLocate = stopLocate;
+ this.stopFollowing = stopFollowing;
+
return container;
}
});
L.control.locate = function (options) {
return new L.Control.Locate(options);
};
+
+(function(){
+ // leaflet.js raises bug when trying to addClass / removeClass multiple classes at once
+ // Let's create a wrapper on it which fixes it.
+ var LDomUtilApplyClassesMethod = function(method, element, classNames) {
+ classNames = classNames.split(' ');
+ classNames.forEach(function(className) {
+ L.DomUtil[method].call(this, element, className);
+ });
+ };
+
+ L.DomUtil.addClasses = function(el, names) { LDomUtilApplyClassesMethod('addClass', el, names); }
+ L.DomUtil.removeClasses = function(el, names) { LDomUtilApplyClassesMethod('removeClass', el, names); }
+})();