]> git.openstreetmap.org Git - rails.git/blob - vendor/assets/leaflet/leaflet.locate.js
Merge branch 'p' of https://github.com/jfirebaugh/openstreetmap-website into jfirebaugh-p
[rails.git] / vendor / assets / leaflet / leaflet.locate.js
1 /*!
2 Copyright (c) 2016 Dominik Moritz
3
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
6 */
7 (function (factory, window) {
8      // see https://github.com/Leaflet/Leaflet/blob/master/PLUGIN-GUIDE.md#module-loaders
9      // for details on how to structure a leaflet plugin.
10
11     // define an AMD module that relies on 'leaflet'
12     if (typeof define === 'function' && define.amd) {
13         define(['leaflet'], factory);
14
15     // define a Common JS module that relies on 'leaflet'
16     } else if (typeof exports === 'object') {
17         if (typeof window !== 'undefined' && window.L) {
18             module.exports = factory(L);
19         } else {
20             module.exports = factory(require('leaflet'));
21         }
22     }
23
24     // attach your plugin to the global 'L' variable
25     if (typeof window !== 'undefined' && window.L){
26         window.L.Control.Locate = factory(L);
27     }
28 } (function (L) {
29     var LDomUtilApplyClassesMethod = function(method, element, classNames) {
30         classNames = classNames.split(' ');
31         classNames.forEach(function(className) {
32             L.DomUtil[method].call(this, element, className);
33         });
34     };
35
36     var addClasses = function(el, names) { LDomUtilApplyClassesMethod('addClass', el, names); };
37     var removeClasses = function(el, names) { LDomUtilApplyClassesMethod('removeClass', el, names); };
38
39     var LocateControl = L.Control.extend({
40         options: {
41             /** Position of the control */
42             position: 'topleft',
43             /** The layer that the user's location should be drawn on. By default creates a new layer. */
44             layer: undefined,
45             /**
46              * Automatically sets the map view (zoom and pan) to the user's location as it updates.
47              * While the map is following the user's location, the control is in the `following` state,
48              * which changes the style of the control and the circle marker.
49              *
50              * Possible values:
51              *  - false: never updates the map view when location changes.
52              *  - 'once': set the view when the location is first determined
53              *  - 'always': always updates the map view when location changes.
54              *              The map view follows the users location.
55              *  - 'untilPan': (default) like 'always', except stops updating the
56              *                view if the user has manually panned the map.
57              *                The map view follows the users location until she pans.
58              */
59             setView: 'untilPan',
60             /** Keep the current map zoom level when setting the view and only pan. */
61             keepCurrentZoomLevel: false,
62             /** Smooth pan and zoom to the location of the marker. Only works in Leaflet 1.0+. */
63             flyTo: false,
64             /**
65              * The user location can be inside and outside the current view when the user clicks on the
66              * control that is already active. Both cases can be configures separately.
67              * Possible values are:
68              *  - 'setView': zoom and pan to the current location
69              *  - 'stop': stop locating and remove the location marker
70              */
71             clickBehavior: {
72                 /** What should happen if the user clicks on the control while the location is within the current view. */
73                 inView: 'stop',
74                 /** What should happen if the user clicks on the control while the location is outside the current view. */
75                 outOfView: 'setView',
76             },
77             /**
78              * If set, save the map bounds just before centering to the user's
79              * location. When control is disabled, set the view back to the
80              * bounds that were saved.
81              */
82             returnToPrevBounds: false,
83             /**
84              * Keep a cache of the location after the user deactivates the control. If set to false, the user has to wait
85              * until the locate API returns a new location before they see where they are again.
86              */
87             cacheLocation: true,
88             /** If set, a circle that shows the location accuracy is drawn. */
89             drawCircle: true,
90             /** If set, the marker at the users' location is drawn. */
91             drawMarker: true,
92             /** The class to be used to create the marker. For example L.CircleMarker or L.Marker */
93             markerClass: L.CircleMarker,
94             /** Accuracy circle style properties. */
95             circleStyle: {
96                 color: '#136AEC',
97                 fillColor: '#136AEC',
98                 fillOpacity: 0.15,
99                 weight: 2,
100                 opacity: 0.5
101             },
102             /** Inner marker style properties. Only works if your marker class supports `setStyle`. */
103             markerStyle: {
104                 color: '#136AEC',
105                 fillColor: '#2A93EE',
106                 fillOpacity: 0.7,
107                 weight: 2,
108                 opacity: 0.9,
109                 radius: 5
110             },
111             /**
112              * Changes to accuracy circle and inner marker while following.
113              * It is only necessary to provide the properties that should change.
114              */
115             followCircleStyle: {},
116             followMarkerStyle: {
117                 // color: '#FFA500',
118                 // fillColor: '#FFB000'
119             },
120             /** The CSS class for the icon. For example fa-location-arrow or fa-map-marker */
121             icon: 'fa fa-map-marker',
122             iconLoading: 'fa fa-spinner fa-spin',
123             /** The element to be created for icons. For example span or i */
124             iconElementTag: 'span',
125             /** Padding around the accuracy circle. */
126             circlePadding: [0, 0],
127             /** Use metric units. */
128             metric: true,
129             /**
130              * This callback can be used in case you would like to override button creation behavior.
131              * This is useful for DOM manipulation frameworks such as angular etc.
132              * This function should return an object with HtmlElement for the button (link property) and the icon (icon property).
133              */
134             createButtonCallback: function (container, options) {
135                 var link = L.DomUtil.create('a', 'leaflet-bar-part leaflet-bar-part-single', container);
136                 link.title = options.strings.title;
137                 var icon = L.DomUtil.create(options.iconElementTag, options.icon, link);
138                 return { link: link, icon: icon };
139             },
140             /** This event is called in case of any location error that is not a time out error. */
141             onLocationError: function(err, control) {
142                 alert(err.message);
143             },
144             /**
145              * This even is called when the user's location is outside the bounds set on the map.
146              * The event is called repeatedly when the location changes.
147              */
148             onLocationOutsideMapBounds: function(control) {
149                 control.stop();
150                 alert(control.options.strings.outsideMapBoundsMsg);
151             },
152             /** Display a pop-up when the user click on the inner marker. */
153             showPopup: true,
154             strings: {
155                 title: "Show me where I am",
156                 metersUnit: "meters",
157                 feetUnit: "feet",
158                 popup: "You are within {distance} {unit} from this point",
159                 outsideMapBoundsMsg: "You seem located outside the boundaries of the map"
160             },
161             /** The default options passed to leaflets locate method. */
162             locateOptions: {
163                 maxZoom: Infinity,
164                 watch: true,  // if you overwrite this, visualization cannot be updated
165                 setView: false // have to set this to false because we have to
166                                // do setView manually
167             }
168         },
169
170         initialize: function (options) {
171             // set default options if nothing is set (merge one step deep)
172             for (var i in options) {
173                 if (typeof this.options[i] === 'object') {
174                     L.extend(this.options[i], options[i]);
175                 } else {
176                     this.options[i] = options[i];
177                 }
178             }
179
180             // extend the follow marker style and circle from the normal style
181             this.options.followMarkerStyle = L.extend({}, this.options.markerStyle, this.options.followMarkerStyle);
182             this.options.followCircleStyle = L.extend({}, this.options.circleStyle, this.options.followCircleStyle);
183         },
184
185         /**
186          * Add control to map. Returns the container for the control.
187          */
188         onAdd: function (map) {
189             var container = L.DomUtil.create('div',
190                 'leaflet-control-locate leaflet-bar leaflet-control');
191
192             this._layer = this.options.layer || new L.LayerGroup();
193             this._layer.addTo(map);
194             this._event = undefined;
195             this._prevBounds = null;
196
197             var linkAndIcon = this.options.createButtonCallback(container, this.options);
198             this._link = linkAndIcon.link;
199             this._icon = linkAndIcon.icon;
200
201             L.DomEvent
202                 .on(this._link, 'click', L.DomEvent.stopPropagation)
203                 .on(this._link, 'click', L.DomEvent.preventDefault)
204                 .on(this._link, 'click', this._onClick, this)
205                 .on(this._link, 'dblclick', L.DomEvent.stopPropagation);
206
207             this._resetVariables();
208
209             this._map.on('unload', this._unload, this);
210
211             return container;
212         },
213
214         /**
215          * This method is called when the user clicks on the control.
216          */
217         _onClick: function() {
218             this._justClicked = true;
219             this._userPanned = false;
220
221             if (this._active && !this._event) {
222                 // click while requesting
223                 this.stop();
224             } else if (this._active && this._event !== undefined) {
225                 var behavior = this._map.getBounds().contains(this._event.latlng) ?
226                     this.options.clickBehavior.inView : this.options.clickBehavior.outOfView;
227                 switch (behavior) {
228                     case 'setView':
229                         this.setView();
230                         break;
231                     case 'stop':
232                         this.stop();
233                         if (this.options.returnToPrevBounds) {
234                             var f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds;
235                             f.bind(this._map)(this._prevBounds);
236                         }
237                         break;
238                 }
239             } else {
240                 if (this.options.returnToPrevBounds) {
241                   this._prevBounds = this._map.getBounds();
242                 }
243                 this.start();
244             }
245
246             this._updateContainerStyle();
247         },
248
249         /**
250          * Starts the plugin:
251          * - activates the engine
252          * - draws the marker (if coordinates available)
253          */
254         start: function() {
255             this._activate();
256
257             if (this._event) {
258                 this._drawMarker(this._map);
259
260                 // if we already have a location but the user clicked on the control
261                 if (this.options.setView) {
262                     this.setView();
263                 }
264             }
265             this._updateContainerStyle();
266         },
267
268         /**
269          * Stops the plugin:
270          * - deactivates the engine
271          * - reinitializes the button
272          * - removes the marker
273          */
274         stop: function() {
275             this._deactivate();
276
277             this._cleanClasses();
278             this._resetVariables();
279
280             this._removeMarker();
281         },
282
283         /**
284          * This method launches the location engine.
285          * It is called before the marker is updated,
286          * event if it does not mean that the event will be ready.
287          *
288          * Override it if you want to add more functionalities.
289          * It should set the this._active to true and do nothing if
290          * this._active is true.
291          */
292         _activate: function() {
293             if (!this._active) {
294                 this._map.locate(this.options.locateOptions);
295                 this._active = true;
296
297                 // bind event listeners
298                 this._map.on('locationfound', this._onLocationFound, this);
299                 this._map.on('locationerror', this._onLocationError, this);
300                 this._map.on('dragstart', this._onDrag, this);
301             }
302         },
303
304         /**
305          * Called to stop the location engine.
306          *
307          * Override it to shutdown any functionalities you added on start.
308          */
309         _deactivate: function() {
310             this._map.stopLocate();
311             this._active = false;
312
313             if (!this.options.cacheLocation) {
314                 this._event = undefined;
315             }
316
317             // unbind event listeners
318             this._map.off('locationfound', this._onLocationFound, this);
319             this._map.off('locationerror', this._onLocationError, this);
320             this._map.off('dragstart', this._onDrag, this);
321         },
322
323         /**
324          * Zoom (unless we should keep the zoom level) and an to the current view.
325          */
326         setView: function() {
327             this._drawMarker();
328             if (this._isOutsideMapBounds()) {
329                 this._event = undefined;  // clear the current location so we can get back into the bounds
330                 this.options.onLocationOutsideMapBounds(this);
331             } else {
332                 if (this.options.keepCurrentZoomLevel) {
333                     var f = this.options.flyTo ? this._map.flyTo : this._map.panTo;
334                     f.bind(this._map)([this._event.latitude, this._event.longitude]);
335                 } else {
336                     var f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds;
337                     f.bind(this._map)(this._event.bounds, {
338                         padding: this.options.circlePadding,
339                         maxZoom: this.options.locateOptions.maxZoom
340                     });
341                 }
342             }
343         },
344
345         /**
346          * Draw the marker and accuracy circle on the map.
347          *
348          * Uses the event retrieved from onLocationFound from the map.
349          */
350         _drawMarker: function() {
351             if (this._event.accuracy === undefined) {
352                 this._event.accuracy = 0;
353             }
354
355             var radius = this._event.accuracy;
356             var latlng = this._event.latlng;
357
358             // circle with the radius of the location's accuracy
359             if (this.options.drawCircle) {
360                 var style = this._isFollowing() ? this.options.followCircleStyle : this.options.circleStyle;
361
362                 if (!this._circle) {
363                     this._circle = L.circle(latlng, radius, style).addTo(this._layer);
364                 } else {
365                     this._circle.setLatLng(latlng).setRadius(radius).setStyle(style);
366                 }
367             }
368
369             var distance, unit;
370             if (this.options.metric) {
371                 distance = radius.toFixed(0);
372                 unit =  this.options.strings.metersUnit;
373             } else {
374                 distance = (radius * 3.2808399).toFixed(0);
375                 unit = this.options.strings.feetUnit;
376             }
377
378             // small inner marker
379             if (this.options.drawMarker) {
380                 var mStyle = this._isFollowing() ? this.options.followMarkerStyle : this.options.markerStyle;
381                 if (!this._marker) {
382                     this._marker = new this.options.markerClass(latlng, mStyle).addTo(this._layer);
383                 } else {
384                     this._marker.setLatLng(latlng);
385                     // If the markerClass can be updated with setStyle, update it.
386                     if (this._marker.setStyle) {
387                         this._marker.setStyle(mStyle);
388                     }
389                 }
390             }
391
392             var t = this.options.strings.popup;
393             if (this.options.showPopup && t && this._marker) {
394                 this._marker
395                     .bindPopup(L.Util.template(t, {distance: distance, unit: unit}))
396                     ._popup.setLatLng(latlng);
397             }
398         },
399
400         /**
401          * Remove the marker from map.
402          */
403         _removeMarker: function() {
404             this._layer.clearLayers();
405             this._marker = undefined;
406             this._circle = undefined;
407         },
408
409         /**
410          * Unload the plugin and all event listeners.
411          * Kind of the opposite of onAdd.
412          */
413         _unload: function() {
414             this.stop();
415             this._map.off('unload', this._unload, this);
416         },
417
418         /**
419          * Calls deactivate and dispatches an error.
420          */
421         _onLocationError: function(err) {
422             // ignore time out error if the location is watched
423             if (err.code == 3 && this.options.locateOptions.watch) {
424                 return;
425             }
426
427             this.stop();
428             this.options.onLocationError(err, this);
429         },
430
431         /**
432          * Stores the received event and updates the marker.
433          */
434         _onLocationFound: function(e) {
435             // no need to do anything if the location has not changed
436             if (this._event &&
437                 (this._event.latlng.lat === e.latlng.lat &&
438                  this._event.latlng.lng === e.latlng.lng &&
439                      this._event.accuracy === e.accuracy)) {
440                 return;
441             }
442
443             if (!this._active) {
444                 // we may have a stray event
445                 return;
446             }
447
448             this._event = e;
449
450             this._drawMarker();
451             this._updateContainerStyle();
452
453             switch (this.options.setView) {
454                 case 'once':
455                     if (this._justClicked) {
456                         this.setView();
457                     }
458                     break;
459                 case 'untilPan':
460                     if (!this._userPanned) {
461                         this.setView();
462                     }
463                     break;
464                 case 'always':
465                     this.setView();
466                     break;
467                 case false:
468                     // don't set the view
469                     break;
470             }
471
472             this._justClicked = false;
473         },
474
475         /**
476          * When the user drags. Need a separate even so we can bind and unbind even listeners.
477          */
478         _onDrag: function() {
479             // only react to drags once we have a location
480             if (this._event) {
481                 this._userPanned = true;
482                 this._updateContainerStyle();
483                 this._drawMarker();
484             }
485         },
486
487         /**
488          * Compute whether the map is following the user location with pan and zoom.
489          */
490         _isFollowing: function() {
491             if (!this._active) {
492                 return false;
493             }
494
495             if (this.options.setView === 'always') {
496                 return true;
497             } else if (this.options.setView === 'untilPan') {
498                 return !this._userPanned;
499             }
500         },
501
502         /**
503          * Check if location is in map bounds
504          */
505         _isOutsideMapBounds: function() {
506             if (this._event === undefined) {
507                 return false;
508             }
509             return this._map.options.maxBounds &&
510                 !this._map.options.maxBounds.contains(this._event.latlng);
511         },
512
513         /**
514          * Toggles button class between following and active.
515          */
516         _updateContainerStyle: function() {
517             if (!this._container) {
518                 return;
519             }
520
521             if (this._active && !this._event) {
522                 // active but don't have a location yet
523                 this._setClasses('requesting');
524             } else if (this._isFollowing()) {
525                 this._setClasses('following');
526             } else if (this._active) {
527                 this._setClasses('active');
528             } else {
529                 this._cleanClasses();
530             }
531         },
532
533         /**
534          * Sets the CSS classes for the state.
535          */
536         _setClasses: function(state) {
537             if (state == 'requesting') {
538                 removeClasses(this._container, "active following");
539                 addClasses(this._container, "requesting");
540
541                 removeClasses(this._icon, this.options.icon);
542                 addClasses(this._icon, this.options.iconLoading);
543             } else if (state == 'active') {
544                 removeClasses(this._container, "requesting following");
545                 addClasses(this._container, "active");
546
547                 removeClasses(this._icon, this.options.iconLoading);
548                 addClasses(this._icon, this.options.icon);
549             } else if (state == 'following') {
550                 removeClasses(this._container, "requesting");
551                 addClasses(this._container, "active following");
552
553                 removeClasses(this._icon, this.options.iconLoading);
554                 addClasses(this._icon, this.options.icon);
555             }
556         },
557
558         /**
559          * Removes all classes from button.
560          */
561         _cleanClasses: function() {
562             L.DomUtil.removeClass(this._container, "requesting");
563             L.DomUtil.removeClass(this._container, "active");
564             L.DomUtil.removeClass(this._container, "following");
565
566             removeClasses(this._icon, this.options.iconLoading);
567             addClasses(this._icon, this.options.icon);
568         },
569
570         /**
571          * Reinitializes state variables.
572          */
573         _resetVariables: function() {
574             // whether locate is active or not
575             this._active = false;
576
577             // true if the control was clicked for the first time
578             // we need this so we can pan and zoom once we have the location
579             this._justClicked = false;
580
581             // true if the user has panned the map after clicking the control
582             this._userPanned = false;
583         }
584     });
585
586     L.control.locate = function (options) {
587         return new L.Control.Locate(options);
588     };
589
590     return LocateControl;
591 }, window));