2 Copyright (c) 2014 Dominik Moritz
4 This file is part of the leaflet locate control. It is licensed under the MIT license.
5 You can find the project at: https://github.com/domoritz/leaflet-locatecontrol
7 L.Control.Locate = L.Control.extend({
11 follow: false, // follow with zoom and pan the user's location
12 stopFollowingOnDrag: false, // if follow is true, stop following when map is dragged (deprecated)
30 // changes to range circle and inner marker while following
31 // it is only necessary to provide the things that should change
32 followCircleStyle: {},
35 //fillColor: '#FFB000'
37 icon: 'icon-location', // icon-location or icon-direction
38 iconLoading: 'icon-spinner animate-spin',
39 circlePadding: [0, 0],
41 onLocationError: function(err) {
42 // this event is called in case of any location error
43 // that is not a time out error.
46 onLocationOutsideMapBounds: function(control) {
47 // this event is repeatedly called when the location changes
49 alert(context.options.strings.outsideMapBoundsMsg);
51 setView: true, // automatically sets the map view to the user's location
52 // keep the current map zoom level when displaying the user's location. (if 'false', use maxZoom)
53 keepCurrentZoomLevel: false,
55 title: "Show me where I am",
56 popup: "You are within {distance} {unit} from this point",
57 outsideMapBoundsMsg: "You seem located outside the boundaries of the map"
61 watch: true // if you overwrite this, visualization cannot be updated
65 onAdd: function (map) {
66 var container = L.DomUtil.create('div', 'control-locate');
69 this._layer = new L.LayerGroup();
70 this._layer.addTo(map);
71 this._event = undefined;
73 this._locateOptions = this.options.locateOptions;
74 L.extend(this._locateOptions, this.options.locateOptions);
75 L.extend(this._locateOptions, {
76 setView: false // have to set this to false because we have to
77 // do setView manually
80 // extend the follow marker style and circle from the normal style
82 L.extend(tmp, this.options.markerStyle, this.options.followMarkerStyle);
83 this.options.followMarkerStyle = tmp;
85 L.extend(tmp, this.options.circleStyle, this.options.followCircleStyle);
86 this.options.followCircleStyle = tmp;
88 var link = L.DomUtil.create('a', 'control-button ' + this.options.icon, container);
89 link.innerHTML = "<span class='icon geolocate'></span>";
91 link.title = this.options.strings.title;
94 .on(link, 'click', L.DomEvent.stopPropagation)
95 .on(link, 'click', L.DomEvent.preventDefault)
96 .on(link, 'click', function() {
97 if (self._active && (self._event === undefined || map.getBounds().contains(self._event.latlng) || !self.options.setView ||
98 isOutsideMapBounds())) {
104 .on(link, 'dblclick', L.DomEvent.stopPropagation);
106 var locate = function () {
107 if (self.options.setView) {
108 self._locateOnNextLocationFound = true;
111 map.locate(self._locateOptions);
114 if (self.options.follow) {
118 setClasses('requesting');
124 var onLocationFound = function (e) {
125 // no need to do anything if the location has not changed
127 (self._event.latlng.lat === e.latlng.lat &&
128 self._event.latlng.lng === e.latlng.lng &&
129 self._event.accuracy === e.accuracy)) {
139 if (self.options.follow && self._following) {
140 self._locateOnNextLocationFound = true;
146 var startFollowing = function() {
147 map.fire('startfollowing', self);
148 self._following = true;
149 if (self.options.stopFollowingOnDrag) {
150 map.on('dragstart', stopFollowing);
154 var stopFollowing = function() {
155 map.fire('stopfollowing', self);
156 self._following = false;
157 if (self.options.stopFollowingOnDrag) {
158 map.off('dragstart', stopFollowing);
163 var isOutsideMapBounds = function () {
164 if (self._event === undefined)
166 return map.options.maxBounds &&
167 !map.options.maxBounds.contains(self._event.latlng);
170 var visualizeLocation = function() {
171 if (self._event.accuracy === undefined)
172 self._event.accuracy = 0;
174 var radius = self._event.accuracy;
175 if (self._locateOnNextLocationFound) {
176 if (isOutsideMapBounds()) {
177 self.options.onLocationOutsideMapBounds(self);
179 map.fitBounds(self._event.bounds, {
180 padding: self.options.circlePadding,
181 maxZoom: self.options.keepCurrentZoomLevel ? map.getZoom() : self._locateOptions.maxZoom
184 self._locateOnNextLocationFound = false;
187 // circle with the radius of the location's accuracy
189 if (self.options.drawCircle) {
190 if (self._following) {
191 style = self.options.followCircleStyle;
193 style = self.options.circleStyle;
197 self._circle = L.circle(self._event.latlng, radius, style)
200 self._circle.setLatLng(self._event.latlng).setRadius(radius);
202 self._circle.options[o] = style[o];
208 if (self.options.metric) {
209 distance = radius.toFixed(0);
212 distance = (radius * 3.2808399).toFixed(0);
216 // small inner marker
218 if (self._following) {
219 mStyle = self.options.followMarkerStyle;
221 mStyle = self.options.markerStyle;
224 var t = self.options.strings.popup;
225 if (!self._circleMarker) {
226 self._circleMarker = L.circleMarker(self._event.latlng, mStyle)
227 .bindPopup(L.Util.template(t, {distance: distance, unit: unit}))
230 self._circleMarker.setLatLng(self._event.latlng)
231 .bindPopup(L.Util.template(t, {distance: distance, unit: unit}))
232 ._popup.setLatLng(self._event.latlng);
234 self._circleMarker.options[o] = mStyle[o];
238 if (!self._container)
240 if (self._following) {
241 setClasses('following');
243 setClasses('active');
247 var setClasses = function(state) {
248 if (state == 'requesting') {
249 L.DomUtil.removeClasses(self._container, "active following");
250 L.DomUtil.addClasses(self._container, "requesting");
252 L.DomUtil.removeClasses(link, self.options.icon);
253 L.DomUtil.addClasses(link, self.options.iconLoading);
254 } else if (state == 'active') {
255 L.DomUtil.removeClasses(self._container, "requesting following");
256 L.DomUtil.addClasses(self._container, "active");
258 L.DomUtil.removeClasses(link, self.options.iconLoading);
259 L.DomUtil.addClasses(link, self.options.icon);
260 } else if (state == 'following') {
261 L.DomUtil.removeClasses(self._container, "requesting");
262 L.DomUtil.addClasses(self._container, "active following");
264 L.DomUtil.removeClasses(link, self.options.iconLoading);
265 L.DomUtil.addClasses(link, self.options.icon);
269 var resetVariables = function() {
270 self._active = false;
271 self._locateOnNextLocationFound = self.options.setView;
272 self._following = false;
277 var stopLocate = function() {
279 map.off('dragstart', stopFollowing);
280 if (self.options.follow && self._following) {
284 L.DomUtil.removeClass(self._container, "requesting");
285 L.DomUtil.removeClass(self._container, "active");
286 L.DomUtil.removeClass(self._container, "following");
289 self._layer.clearLayers();
290 self._circleMarker = undefined;
291 self._circle = undefined;
294 var onLocationError = function (err) {
295 // ignore time out error if the location is watched
296 if (err.code == 3 && self._locateOptions.watch) {
301 self.options.onLocationError(err);
305 map.on('locationfound', onLocationFound, self);
306 map.on('locationerror', onLocationError, self);
308 // make locate functions available to outside world
309 this.locate = locate;
310 this.stopLocate = stopLocate;
311 this.stopFollowing = stopFollowing;
317 L.Map.addInitHook(function () {
318 if (this.options.locateControl) {
319 this.locateControl = L.control.locate();
320 this.addControl(this.locateControl);
324 L.control.locate = function (options) {
325 return new L.Control.Locate(options);
329 // leaflet.js raises bug when trying to addClass / removeClass multiple classes at once
330 // Let's create a wrapper on it which fixes it.
331 var LDomUtilApplyClassesMethod = function(method, element, classNames) {
332 classNames = classNames.split(' ');
333 classNames.forEach(function(className) {
334 L.DomUtil[method].call(this, element, className);
338 L.DomUtil.addClasses = function(el, names) { LDomUtilApplyClassesMethod('addClass', el, names); }
339 L.DomUtil.removeClasses = function(el, names) { LDomUtilApplyClassesMethod('removeClass', el, names); }