X-Git-Url: https://git.openstreetmap.org./rails.git/blobdiff_plain/1f7bd08f4a8a6a626a0c1d7ed60f2dcd6a6801e8..480eaa1b3638f29fcf05b67f67e31319f596130b:/vendor/assets/leaflet/leaflet.locate.js?ds=sidebyside diff --git a/vendor/assets/leaflet/leaflet.locate.js b/vendor/assets/leaflet/leaflet.locate.js index 822b24da2..615b4654b 100644 --- a/vendor/assets/leaflet/leaflet.locate.js +++ b/vendor/assets/leaflet/leaflet.locate.js @@ -1,340 +1,894 @@ -/* -Copyright (c) 2014 Dominik Moritz +/*! +Copyright (c) 2016 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 - }, - // inner marker - markerStyle: { - color: '#136AEC', - fillColor: '#2A93EE', - fillOpacity: 0.7, - weight: 2, - opacity: 0.9, - 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, - onLocationError: function(err) { - // this event is called in case of any location error - // that is not a time out error. - alert(err.message); - }, - 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 - // 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 +(function (factory, window) { + // see https://github.com/Leaflet/Leaflet/blob/master/PLUGIN-GUIDE.md#module-loaders + // for details on how to structure a leaflet plugin. + + // define an AMD module that relies on 'leaflet' + if (typeof define === 'function' && define.amd) { + define(['leaflet'], factory); + + // define a Common JS module that relies on 'leaflet' + } else if (typeof exports === 'object') { + if (typeof window !== 'undefined' && window.L) { + module.exports = factory(L); + } else { + module.exports = factory(require('leaflet')); } - }, - - onAdd: function (map) { - var container = L.DomUtil.create('div', 'control-locate'); - - var self = this; - this._layer = new L.LayerGroup(); - this._layer.addTo(map); - this._event = undefined; + } - 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 + // attach your plugin to the global 'L' variable + if (typeof window !== 'undefined' && window.L){ + window.L.Control.Locate = factory(L); + } +} (function (L) { + var LDomUtilApplyClassesMethod = function(method, element, classNames) { + classNames = classNames.split(' '); + classNames.forEach(function(className) { + L.DomUtil[method].call(this, element, className); }); + }; + + var addClasses = function(el, names) { LDomUtilApplyClassesMethod('addClass', el, names); }; + var removeClasses = function(el, names) { LDomUtilApplyClassesMethod('removeClass', el, names); }; + + /** + * Compatible with L.Circle but a true marker instead of a path + */ + var LocationMarker = L.Marker.extend({ + initialize: function (latlng, options) { + L.Util.setOptions(this, options); + this._latlng = latlng; + this.createIcon(); + }, - // 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 = ""; - link.href = '#'; - 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 && (self._event === undefined || map.getBounds().contains(self._event.latlng) || !self.options.setView || - isOutsideMapBounds())) { - stopLocate(); - } else { - locate(); - } - }) - .on(link, 'dblclick', L.DomEvent.stopPropagation); + /** + * Create a styled circle location marker + */ + createIcon: function() { + var opt = this.options; + + var style = ''; - var locate = function () { - if (self.options.setView) { - self._locateOnNextLocationFound = true; + if (opt.color !== undefined) { + style += 'stroke:'+opt.color+';'; } - if(!self._active) { - map.locate(self._locateOptions); + if (opt.weight !== undefined) { + style += 'stroke-width:'+opt.weight+';'; } - self._active = true; - if (self.options.follow) { - startFollowing(); + if (opt.fillColor !== undefined) { + style += 'fill:'+opt.fillColor+';'; } - if (!self._event) { - setClasses('requesting'); - } else { - visualizeLocation(); + if (opt.fillOpacity !== undefined) { + style += 'fill-opacity:'+opt.fillOpacity+';'; + } + if (opt.opacity !== undefined) { + style += 'opacity:'+opt.opacity+';'; } - }; - 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 && - self._event.accuracy === e.accuracy)) { - return; + var icon = this._getIconSVG(opt, style); + + this._locationIcon = L.divIcon({ + className: icon.className, + html: icon.svg, + iconSize: [icon.w,icon.h], + }); + + this.setIcon(this._locationIcon); + }, + + /** + * Return the raw svg for the shape + * + * Split so can be easily overridden + */ + _getIconSVG: function(options, style) { + var r = options.radius; + var w = options.weight; + var s = r + w; + var s2 = s * 2; + var svg = '' + + '' + + ''; + return { + className: 'leaflet-control-locate-location', + svg: svg, + w: s2, + h: s2 + }; + }, + + setStyle: function(style) { + L.Util.setOptions(this, style); + this.createIcon(); + } + }); + + var CompassMarker = LocationMarker.extend({ + initialize: function (latlng, heading, options) { + L.Util.setOptions(this, options); + this._latlng = latlng; + this._heading = heading; + this.createIcon(); + }, + + setHeading: function(heading) { + this._heading = heading; + }, + + /** + * Create a styled arrow compass marker + */ + _getIconSVG: function(options, style) { + var r = options.radius; + var w = (options.width + options.weight); + var h = (r+options.depth + options.weight)*2; + var path = 'M0,0 l'+(options.width/2)+','+options.depth+' l-'+(w)+',0 z'; + var svgstyle = 'transform: rotate('+this._heading+'deg)'; + var svg = ''+ + ''+ + ''; + return { + className: 'leafet-control-locate-heading', + svg: svg, + w: w, + h: h + }; + }, + }); + + + var LocateControl = L.Control.extend({ + options: { + /** Position of the control */ + position: 'topleft', + /** The layer that the user's location should be drawn on. By default creates a new layer. */ + layer: undefined, + /** + * Automatically sets the map view (zoom and pan) to the user's location as it updates. + * While the map is following the user's location, the control is in the `following` state, + * which changes the style of the control and the circle marker. + * + * Possible values: + * - false: never updates the map view when location changes. + * - 'once': set the view when the location is first determined + * - 'always': always updates the map view when location changes. + * The map view follows the user's location. + * - 'untilPan': like 'always', except stops updating the + * view if the user has manually panned the map. + * The map view follows the user's location until she pans. + * - 'untilPanOrZoom': (default) like 'always', except stops updating the + * view if the user has manually panned the map. + * The map view follows the user's location until she pans. + */ + setView: 'untilPanOrZoom', + /** Keep the current map zoom level when setting the view and only pan. */ + keepCurrentZoomLevel: false, + /** + * This callback can be used to override the viewport tracking + * This function should return a LatLngBounds object. + * + * For example to extend the viewport to ensure that a particular LatLng is visible: + * + * getLocationBounds: function(locationEvent) { + * return locationEvent.bounds.extend([-33.873085, 151.219273]); + * }, + */ + getLocationBounds: function (locationEvent) { + return locationEvent.bounds; + }, + /** Smooth pan and zoom to the location of the marker. Only works in Leaflet 1.0+. */ + flyTo: false, + /** + * The user location can be inside and outside the current view when the user clicks on the + * control that is already active. Both cases can be configures separately. + * Possible values are: + * - 'setView': zoom and pan to the current location + * - 'stop': stop locating and remove the location marker + */ + clickBehavior: { + /** What should happen if the user clicks on the control while the location is within the current view. */ + inView: 'stop', + /** What should happen if the user clicks on the control while the location is outside the current view. */ + outOfView: 'setView', + /** + * What should happen if the user clicks on the control while the location is within the current view + * and we could be following but are not. Defaults to a special value which inherits from 'inView'; + */ + inViewNotFollowing: 'inView', + }, + /** + * If set, save the map bounds just before centering to the user's + * location. When control is disabled, set the view back to the + * bounds that were saved. + */ + returnToPrevBounds: false, + /** + * Keep a cache of the location after the user deactivates the control. If set to false, the user has to wait + * until the locate API returns a new location before they see where they are again. + */ + cacheLocation: true, + /** If set, a circle that shows the location accuracy is drawn. */ + drawCircle: true, + /** If set, the marker at the users' location is drawn. */ + drawMarker: true, + /** If set and supported then show the compass heading */ + showCompass: true, + /** The class to be used to create the marker. For example L.CircleMarker or L.Marker */ + markerClass: LocationMarker, + /** The class us be used to create the compass bearing arrow */ + compassClass: CompassMarker, + /** Accuracy circle style properties. NOTE these styles should match the css animations styles */ + circleStyle: { + className: 'leaflet-control-locate-circle', + color: '#136AEC', + fillColor: '#136AEC', + fillOpacity: 0.15, + weight: 0 + }, + /** Inner marker style properties. Only works if your marker class supports `setStyle`. */ + markerStyle: { + className: 'leaflet-control-locate-marker', + color: '#fff', + fillColor: '#2A93EE', + fillOpacity: 1, + weight: 3, + opacity: 1, + radius: 9 + }, + /** Compass */ + compassStyle: { + fillColor: '#2A93EE', + fillOpacity: 1, + weight: 0, + color: '#fff', + opacity: 1, + radius: 9, // How far is the arrow is from the center of of the marker + width: 9, // Width of the arrow + depth: 6 // Length of the arrow + }, + /** + * Changes to accuracy circle and inner marker while following. + * It is only necessary to provide the properties that should change. + */ + followCircleStyle: {}, + followMarkerStyle: { + // color: '#FFA500', + // fillColor: '#FFB000' + }, + followCompassStyle: {}, + /** The CSS class for the icon. For example fa-location-arrow or fa-map-marker */ + icon: 'fa fa-map-marker', + iconLoading: 'fa fa-spinner fa-spin', + /** The element to be created for icons. For example span or i */ + iconElementTag: 'span', + /** Padding around the accuracy circle. */ + circlePadding: [0, 0], + /** Use metric units. */ + metric: true, + /** + * This callback can be used in case you would like to override button creation behavior. + * This is useful for DOM manipulation frameworks such as angular etc. + * This function should return an object with HtmlElement for the button (link property) and the icon (icon property). + */ + createButtonCallback: function (container, options) { + var link = L.DomUtil.create('a', 'leaflet-bar-part leaflet-bar-part-single', container); + link.title = options.strings.title; + var icon = L.DomUtil.create(options.iconElementTag, options.icon, link); + return { link: link, icon: icon }; + }, + /** This event is called in case of any location error that is not a time out error. */ + onLocationError: function(err, control) { + alert(err.message); + }, + /** + * This event is called when the user's location is outside the bounds set on the map. + * The event is called repeatedly when the location changes. + */ + onLocationOutsideMapBounds: function(control) { + control.stop(); + alert(control.options.strings.outsideMapBoundsMsg); + }, + /** Display a pop-up when the user click on the inner marker. */ + showPopup: true, + strings: { + title: "Show me where I am", + metersUnit: "meters", + feetUnit: "feet", + popup: "You are within {distance} {unit} from this point", + outsideMapBoundsMsg: "You seem located outside the boundaries of the map" + }, + /** The default options passed to leaflets locate method. */ + locateOptions: { + maxZoom: Infinity, + watch: true, // if you overwrite this, visualization cannot be updated + setView: false // have to set this to false because we have to + // do setView manually } + }, - if (!self._active) { - return; + initialize: function (options) { + // set default options if nothing is set (merge one step deep) + for (var i in options) { + if (typeof this.options[i] === 'object') { + L.extend(this.options[i], options[i]); + } else { + this.options[i] = options[i]; + } } - self._event = e; + // extend the follow marker style and circle from the normal style + this.options.followMarkerStyle = L.extend({}, this.options.markerStyle, this.options.followMarkerStyle); + this.options.followCircleStyle = L.extend({}, this.options.circleStyle, this.options.followCircleStyle); + this.options.followCompassStyle = L.extend({}, this.options.compassStyle, this.options.followCompassStyle); + }, + + /** + * Add control to map. Returns the container for the control. + */ + onAdd: function (map) { + var container = L.DomUtil.create('div', + 'leaflet-control-locate leaflet-bar leaflet-control'); + + this._layer = this.options.layer || new L.LayerGroup(); + this._layer.addTo(map); + this._event = undefined; + this._compassHeading = null; + this._prevBounds = null; + + var linkAndIcon = this.options.createButtonCallback(container, this.options); + this._link = linkAndIcon.link; + this._icon = linkAndIcon.icon; + + L.DomEvent + .on(this._link, 'click', L.DomEvent.stopPropagation) + .on(this._link, 'click', L.DomEvent.preventDefault) + .on(this._link, 'click', this._onClick, this) + .on(this._link, 'dblclick', L.DomEvent.stopPropagation); + + this._resetVariables(); + + this._map.on('unload', this._unload, this); + + return container; + }, - if (self.options.follow && self._following) { - self._locateOnNextLocationFound = true; + /** + * This method is called when the user clicks on the control. + */ + _onClick: function() { + this._justClicked = true; + var wasFollowing = this._isFollowing(); + this._userPanned = false; + this._userZoomed = false; + + if (this._active && !this._event) { + // click while requesting + this.stop(); + } else if (this._active && this._event !== undefined) { + var behaviors = this.options.clickBehavior; + var behavior = behaviors.outOfView; + if (this._map.getBounds().contains(this._event.latlng)) { + behavior = wasFollowing ? behaviors.inView : behaviors.inViewNotFollowing; + } + + // Allow inheriting from another behavior + if (behaviors[behavior]) { + behavior = behaviors[behavior]; + } + + switch (behavior) { + case 'setView': + this.setView(); + break; + case 'stop': + this.stop(); + if (this.options.returnToPrevBounds) { + var f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds; + f.bind(this._map)(this._prevBounds); + } + break; + } + } else { + if (this.options.returnToPrevBounds) { + this._prevBounds = this._map.getBounds(); + } + this.start(); } - visualizeLocation(); - }; + this._updateContainerStyle(); + }, + + /** + * Starts the plugin: + * - activates the engine + * - draws the marker (if coordinates available) + */ + start: function() { + this._activate(); - var startFollowing = function() { - map.fire('startfollowing', self); - self._following = true; - if (self.options.stopFollowingOnDrag) { - map.on('dragstart', stopFollowing); + if (this._event) { + this._drawMarker(this._map); + + // if we already have a location but the user clicked on the control + if (this.options.setView) { + this.setView(); + } } - }; + this._updateContainerStyle(); + }, + + /** + * Stops the plugin: + * - deactivates the engine + * - reinitializes the button + * - removes the marker + */ + stop: function() { + this._deactivate(); + + this._cleanClasses(); + this._resetVariables(); + + this._removeMarker(); + }, - var stopFollowing = function() { - map.fire('stopfollowing', self); - self._following = false; - if (self.options.stopFollowingOnDrag) { - map.off('dragstart', stopFollowing); + /** + * Keep the control active but stop following the location + */ + stopFollowing: function() { + this._userPanned = true; + this._updateContainerStyle(); + this._drawMarker(); + }, + + /** + * This method launches the location engine. + * It is called before the marker is updated, + * event if it does not mean that the event will be ready. + * + * Override it if you want to add more functionalities. + * It should set the this._active to true and do nothing if + * this._active is true. + */ + _activate: function() { + if (!this._active) { + this._map.locate(this.options.locateOptions); + this._active = true; + + // bind event listeners + this._map.on('locationfound', this._onLocationFound, this); + this._map.on('locationerror', this._onLocationError, this); + this._map.on('dragstart', this._onDrag, this); + this._map.on('zoomstart', this._onZoom, this); + this._map.on('zoomend', this._onZoomEnd, this); + if (this.options.showCompass) { + if ('ondeviceorientationabsolute' in window) { + L.DomEvent.on(window, 'deviceorientationabsolute', this._onDeviceOrientation, this); + } else if ('ondeviceorientation' in window) { + L.DomEvent.on(window, 'deviceorientation', this._onDeviceOrientation, this); + } + } } - 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) { - if (isOutsideMapBounds()) { - self.options.onLocationOutsideMapBounds(self); + /** + * Called to stop the location engine. + * + * Override it to shutdown any functionalities you added on start. + */ + _deactivate: function() { + this._map.stopLocate(); + this._active = false; + + if (!this.options.cacheLocation) { + this._event = undefined; + } + + // unbind event listeners + this._map.off('locationfound', this._onLocationFound, this); + this._map.off('locationerror', this._onLocationError, this); + this._map.off('dragstart', this._onDrag, this); + this._map.off('zoomstart', this._onZoom, this); + this._map.off('zoomend', this._onZoomEnd, this); + if (this.options.showCompass) { + this._compassHeading = null; + if ('ondeviceorientationabsolute' in window) { + L.DomEvent.off(window, 'deviceorientationabsolute', this._onDeviceOrientation, this); + } else if ('ondeviceorientation' in window) { + L.DomEvent.off(window, 'deviceorientation', this._onDeviceOrientation, this); + } + } + }, + + /** + * Zoom (unless we should keep the zoom level) and an to the current view. + */ + 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 { + if (this.options.keepCurrentZoomLevel) { + var f = this.options.flyTo ? this._map.flyTo : this._map.panTo; + f.bind(this._map)([this._event.latitude, this._event.longitude]); } else { - map.fitBounds(self._event.bounds, { - padding: self.options.circlePadding, - maxZoom: self.options.keepCurrentZoomLevel ? map.getZoom() : self._locateOptions.maxZoom + var f = this.options.flyTo ? this._map.flyToBounds : this._map.fitBounds; + // Ignore zoom events while setting the viewport as these would stop following + this._ignoreEvent = true; + f.bind(this._map)(this.options.getLocationBounds(this._event), { + padding: this.options.circlePadding, + maxZoom: this.options.locateOptions.maxZoom }); + L.Util.requestAnimFrame(function(){ + // Wait until after the next animFrame because the flyTo can be async + this._ignoreEvent = false; + }, this); + } - self._locateOnNextLocationFound = false; } + }, - // circle with the radius of the location's accuracy - var style, o; - if (self.options.drawCircle) { - if (self._following) { - style = self.options.followCircleStyle; + /** + * + */ + _drawCompass: function() { + var latlng = this._event.latlng; + + if (this.options.showCompass && latlng && this._compassHeading !== null) { + var cStyle = this._isFollowing() ? this.options.followCompassStyle : this.options.compassStyle; + if (!this._compass) { + this._compass = new this.options.compassClass(latlng, this._compassHeading, cStyle).addTo(this._layer); } else { - style = self.options.circleStyle; + this._compass.setLatLng(latlng); + this._compass.setHeading(this._compassHeading); + // If the compassClass can be updated with setStyle, update it. + if (this._compass.setStyle) { + this._compass.setStyle(cStyle); + } } + // + } + if (this._compass && (!this.options.showCompass || this._compassHeading === null)) { + this._compass.removeFrom(this._layer); + this._compass = null; + } + }, + + /** + * Draw the marker and accuracy circle on the map. + * + * Uses the event retrieved from onLocationFound from the map. + */ + _drawMarker: function() { + if (this._event.accuracy === undefined) { + this._event.accuracy = 0; + } + + var radius = this._event.accuracy; + var latlng = this._event.latlng; + + // circle with the radius of the location's accuracy + if (this.options.drawCircle) { + var style = this._isFollowing() ? this.options.followCircleStyle : this.options.circleStyle; - if (!self._circle) { - self._circle = L.circle(self._event.latlng, radius, style) - .addTo(self._layer); + if (!this._circle) { + this._circle = L.circle(latlng, radius, style).addTo(this._layer); } else { - self._circle.setLatLng(self._event.latlng).setRadius(radius); - for (o in style) { - self._circle.options[o] = style[o]; - } + this._circle.setLatLng(latlng).setRadius(radius).setStyle(style); } } var distance, unit; - if (self.options.metric) { + if (this.options.metric) { distance = radius.toFixed(0); - unit = "meters"; + unit = this.options.strings.metersUnit; } else { distance = (radius * 3.2808399).toFixed(0); - unit = "feet"; + unit = this.options.strings.feetUnit; } // small inner marker - var mStyle; - if (self._following) { - mStyle = self.options.followMarkerStyle; - } else { - mStyle = self.options.markerStyle; + if (this.options.drawMarker) { + var mStyle = this._isFollowing() ? this.options.followMarkerStyle : this.options.markerStyle; + if (!this._marker) { + this._marker = new this.options.markerClass(latlng, mStyle).addTo(this._layer); + } else { + this._marker.setLatLng(latlng); + // If the markerClass can be updated with setStyle, update it. + if (this._marker.setStyle) { + this._marker.setStyle(mStyle); + } + } } - var t = self.options.strings.popup; - if (!self._circleMarker) { - self._circleMarker = L.circleMarker(self._event.latlng, mStyle) + this._drawCompass(); + + var t = this.options.strings.popup; + if (this.options.showPopup && t && this._marker) { + this._marker .bindPopup(L.Util.template(t, {distance: distance, unit: unit})) - .addTo(self._layer); - } else { - self._circleMarker.setLatLng(self._event.latlng) + ._popup.setLatLng(latlng); + } + if (this.options.showPopup && t && this._compass) { + this._compass .bindPopup(L.Util.template(t, {distance: distance, unit: unit})) - ._popup.setLatLng(self._event.latlng); - for (o in mStyle) { - self._circleMarker.options[o] = mStyle[o]; - } + ._popup.setLatLng(latlng); } + }, - if (!self._container) - return; - if (self._following) { - setClasses('following'); + /** + * Remove the marker from map. + */ + _removeMarker: function() { + this._layer.clearLayers(); + this._marker = undefined; + this._circle = undefined; + }, + + /** + * Unload the plugin and all event listeners. + * Kind of the opposite of onAdd. + */ + _unload: function() { + this.stop(); + this._map.off('unload', this._unload, this); + }, + + /** + * Sets the compass heading + */ + _setCompassHeading: function(angle) { + if (!isNaN(parseFloat(angle)) && isFinite(angle)) { + angle = Math.round(angle); + + this._compassHeading = angle; + L.Util.requestAnimFrame(this._drawCompass, this); } else { - setClasses('active'); + this._compassHeading = null; } - }; + }, - var setClasses = function(state) { - if (state == 'requesting') { - L.DomUtil.removeClasses(self._container, "active following"); - L.DomUtil.addClasses(self._container, "requesting"); + /** + * If the compass fails calibration just fail safely and remove the compass + */ + _onCompassNeedsCalibration: function() { + this._setCompassHeading(); + }, - 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"); + /** + * Process and normalise compass events + */ + _onDeviceOrientation: function(e) { + if (!this._active) { + return; + } - 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"); + if (e.webkitCompassHeading) { + // iOS + this._setCompassHeading(e.webkitCompassHeading); + } else if (e.absolute && e.alpha) { + // Android + this._setCompassHeading(360 - e.alpha) + } + }, - L.DomUtil.removeClasses(link, self.options.iconLoading); - L.DomUtil.addClasses(link, self.options.icon); + /** + * Calls deactivate and dispatches an error. + */ + _onLocationError: function(err) { + // ignore time out error if the location is watched + if (err.code == 3 && this.options.locateOptions.watch) { + return; } - } - var resetVariables = function() { - self._active = false; - self._locateOnNextLocationFound = self.options.setView; - self._following = false; - }; + this.stop(); + this.options.onLocationError(err, this); + }, - resetVariables(); + /** + * Stores the received event and updates the marker. + */ + _onLocationFound: function(e) { + // no need to do anything if the location has not changed + if (this._event && + (this._event.latlng.lat === e.latlng.lat && + this._event.latlng.lng === e.latlng.lng && + this._event.accuracy === e.accuracy)) { + return; + } - var stopLocate = function() { - map.stopLocate(); - map.off('dragstart', stopFollowing); - if (self.options.follow && self._following) { - stopFollowing(); + if (!this._active) { + // we may have a stray event + return; } - L.DomUtil.removeClass(self._container, "requesting"); - L.DomUtil.removeClass(self._container, "active"); - L.DomUtil.removeClass(self._container, "following"); - resetVariables(); + this._event = e; - self._layer.clearLayers(); - self._circleMarker = undefined; - self._circle = undefined; - }; + this._drawMarker(); + this._updateContainerStyle(); - var onLocationError = function (err) { - // ignore time out error if the location is watched - if (err.code == 3 && self._locateOptions.watch) { + switch (this.options.setView) { + case 'once': + if (this._justClicked) { + this.setView(); + } + break; + case 'untilPan': + if (!this._userPanned) { + this.setView(); + } + break; + case 'untilPanOrZoom': + if (!this._userPanned && !this._userZoomed) { + this.setView(); + } + break; + case 'always': + this.setView(); + break; + case false: + // don't set the view + break; + } + + this._justClicked = false; + }, + + /** + * When the user drags. Need a separate event so we can bind and unbind event listeners. + */ + _onDrag: function() { + // only react to drags once we have a location + if (this._event && !this._ignoreEvent) { + this._userPanned = true; + this._updateContainerStyle(); + this._drawMarker(); + } + }, + + /** + * When the user zooms. Need a separate event so we can bind and unbind event listeners. + */ + _onZoom: function() { + // only react to drags once we have a location + if (this._event && !this._ignoreEvent) { + this._userZoomed = true; + this._updateContainerStyle(); + this._drawMarker(); + } + }, + + /** + * After a zoom ends update the compass and handle sideways zooms + */ + _onZoomEnd: function() { + if (this._event) { + this._drawCompass(); + } + + if (this._event && !this._ignoreEvent) { + // If we have zoomed in and out and ended up sideways treat it as a pan + if (!this._map.getBounds().pad(-.3).contains(this._marker.getLatLng())) { + this._userPanned = true; + this._updateContainerStyle(); + this._drawMarker(); + } + } + }, + + /** + * Compute whether the map is following the user location with pan and zoom. + */ + _isFollowing: function() { + if (!this._active) { + return false; + } + + if (this.options.setView === 'always') { + return true; + } else if (this.options.setView === 'untilPan') { + return !this._userPanned; + } else if (this.options.setView === 'untilPanOrZoom') { + return !this._userPanned && !this._userZoomed; + } + }, + + /** + * Check if location is in map bounds + */ + _isOutsideMapBounds: function() { + if (this._event === undefined) { + return false; + } + return this._map.options.maxBounds && + !this._map.options.maxBounds.contains(this._event.latlng); + }, + + /** + * Toggles button class between following and active. + */ + _updateContainerStyle: function() { + if (!this._container) { return; } - stopLocate(); - self.options.onLocationError(err); - }; + if (this._active && !this._event) { + // active but don't have a location yet + this._setClasses('requesting'); + } else if (this._isFollowing()) { + this._setClasses('following'); + } else if (this._active) { + this._setClasses('active'); + } else { + this._cleanClasses(); + } + }, - // event hooks - map.on('locationfound', onLocationFound, self); - map.on('locationerror', onLocationError, self); + /** + * Sets the CSS classes for the state. + */ + _setClasses: function(state) { + if (state == 'requesting') { + removeClasses(this._container, "active following"); + addClasses(this._container, "requesting"); - // make locate functions available to outside world - this.locate = locate; - this.stopLocate = stopLocate; - this.stopFollowing = stopFollowing; + removeClasses(this._icon, this.options.icon); + addClasses(this._icon, this.options.iconLoading); + } else if (state == 'active') { + removeClasses(this._container, "requesting following"); + addClasses(this._container, "active"); - return container; - } -}); + removeClasses(this._icon, this.options.iconLoading); + addClasses(this._icon, this.options.icon); + } else if (state == 'following') { + removeClasses(this._container, "requesting"); + addClasses(this._container, "active following"); -L.Map.addInitHook(function () { - if (this.options.locateControl) { - this.locateControl = L.control.locate(); - this.addControl(this.locateControl); - } -}); - -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); + removeClasses(this._icon, this.options.iconLoading); + addClasses(this._icon, this.options.icon); + } + }, + + /** + * Removes all classes from button. + */ + _cleanClasses: function() { + L.DomUtil.removeClass(this._container, "requesting"); + L.DomUtil.removeClass(this._container, "active"); + L.DomUtil.removeClass(this._container, "following"); + + removeClasses(this._icon, this.options.iconLoading); + addClasses(this._icon, this.options.icon); + }, + + /** + * Reinitializes state variables. + */ + _resetVariables: function() { + // whether locate is active or not + this._active = false; + + // true if the control was clicked for the first time + // we need this so we can pan and zoom once we have the location + this._justClicked = false; + + // true if the user has panned the map after clicking the control + this._userPanned = false; + + // true if the user has zoomed the map after clicking the control + this._userZoomed = false; + } }); - }; - L.DomUtil.addClasses = function(el, names) { LDomUtilApplyClassesMethod('addClass', el, names); } - L.DomUtil.removeClasses = function(el, names) { LDomUtilApplyClassesMethod('removeClass', el, names); } -})(); + L.control.locate = function (options) { + return new L.Control.Locate(options); + }; + + return LocateControl; +}, window));