Source: leaf/locate.js maps.nyc.gov

var nyc = nyc || {};
nyc.leaf = nyc.leaf || {};

/**
 * @desc Class for providing geocoding and device geolocation functionality
 * @public
 * @class
 * @implements {nyc.Locate}
 * @extends {nyc.EventHandling}
 * @constructor
 * @param {L.Map} map The Leaflet map for use in geolocation 
 * @param {nyc.Geocoder} geocoder A geocoder implementation
 * @param {L.LatLngBounds=} extentLimit Geolocation coordinates outside of this bounding box are ignored  
 * @fires nyc.Locate#geocode
 * @fires nyc.Locate#ambiguous
 * @fires nyc.Locate#geolocation
 * @fires nyc.Locate#error
 */
nyc.leaf.Locate = function(map, geocoder, extentLimit){	
	var me = this;	
	me.map = map;
	me.geocoder = geocoder;
	me.extentLimit = extentLimit;
	geocoder.on(nyc.Locate.EventType.GEOCODE, function(data){
		me.proxyEvent(nyc.Locate.EventType.GEOCODE, data);
	});
	geocoder.on(nyc.Locate.EventType.AMBIGUOUS, function(data){
		me.proxyEvent(nyc.Locate.EventType.AMBIGUOUS, data);
	});
	geocoder.on(nyc.Locate.EventType.ERROR, function(data){
		me.proxyEvent(nyc.Locate.EventType.ERROR, data);
	});
	me.geocoder = geocoder;
	map.on('locationfound', $.proxy(me.geolocated, me));
	map.on('locationerror', $.proxy(me.geoError, me));
};
	
nyc.leaf.Locate.prototype = {
	/**
	 * @private
	 * @member {L.Map}
	 */
	map: null,
	/**
	 * @private
	 * @member {nyc.Geocoder}
	 */
	geocoder: null,
	/**
	 * @private
	 * @member {L.LatLngBounds}
	 */
	extentLimit: null,
	/**
	 * @private
	 * @member {boolean}
	 */
	isGeolocateAllowed: true,
	/**
	 * @desc Locate once using device geolocation
	 * @public
	 * @override
	 * @method
	 */
	locate: function(){
		if (this.isGeolocateAllowed){
			this.map.locate({
				enableHighAccuracy: true,
				setView: false, 
				maxZoom: nyc.leaf.Locate.ZOOM_LEVEL
			});
		}else{
			this.trigger(nyc.Locate.EventType.ERROR, 'You have disabled location services for this page. To use this feature you must enable location services.');
		}
	},
	/**
	 * @desc Geocode an input string representing a location
	 * @public
	 * @method
	 * @param {string} input The value to geocode
	 */
	search: function(input){
		this.geocoder.search(input);
	},
	/** 
	 * @private 
	 * @method
	 * @param {string} evt
	 * @param {(nyc.Locate.Result|nyc.Locate.Ambiguous)} data
	 */
	proxyEvent: function(evt, data){
		this.trigger(evt, data);
	},
	/**
	 * @private
	 * @method
	 * @param {Object} e
	 */
	geoError: function(e) {
		if (e.code == 1) this.isGeolocateAllowed = false;
	},
	/**
	 * @private
	 * @method
	 * @param {Object} e
	 */
	geolocated: function(e) {
	    if (!this.extentLimit || this.extentLimit.contains(e.latlng)){
			this.trigger(nyc.Locate.EventType.GEOLOCATION, {
				 name: this.dmsString(e.latlng),
				 coordinates: [e.latlng.lng, e.latlng.lat],
				 accuracy: e.accuracy,
				 type: nyc.Locate.ResultType.GEOLOCATION
			});
	    }				
	},
	/** 
	 * @private 
	 * @method
	 * @return {string}
	 */
	dmsString: function(latLng){
		var lat = latLng.lat, lng = latLng.lng, 
			dmsLat = this.dms(Math.abs(lat)), dmsLng = this.dms(Math.abs(lng));
		dmsLat += lat < 0 ? " S " : " N ";
		dmsLng += lng < 0 ? " W" : " E";
		return dmsLat + dmsLng;
	},
	/**
	 * @private 
	 * @method
	 * @param {number} deg
	 * @return {string}
	 */
	dms: function(deg){
	   var d = Math.floor(deg),
	   	minfloat = (deg-d) * 60,
	   	m = Math.floor(minfloat),
	   	secfloat = (minfloat-m) * 60,
	   	s = secfloat.toFixed(0);
	   return "" + d + "° " + m + "' " + s + '"';
	}
};

nyc.inherits(nyc.leaf.Locate, nyc.EventHandling);

/**
 * @desc The recommended zoom level to use when centering a map on a the result of a call to a locate method  
 * @public
 * @const
 * @type {number}
 */
nyc.leaf.Locate.ZOOM_LEVEL = 16;