GeoLocation for All Browsers

If you did not already know, a new FireFox is out (FF 3.5). This new version has many improvements, including a new service for determining a users geolocation in in JavaScript (via navigator.geolocation). The technology uses Google Location Services, WiFi Access Point Data, and users IP address to calculate the geolocation. See the Using Geolocation article for more details.

Geolocation can be useful for many reasons, but it is of limited usability, being only available for one browser. So while the other browsers attempt to catch up, I have written a utility that emulates the key behavior of the FireFox service, using AJAX, a proxy url, and publicly available APIs. It will default to the FireFox implementation, only using the emulated system when necessary.

Example 1: GeoLocation

(function() {
	// constants
var Y = YAHOO,
	YC = Y.util.Connect,
	YJSON = Y.lang.JSON,
	URL_GEOCODE = http://freegeoip.appspot.com/json/,
	URL_IP = ../proxy.php?url= + encodeURIComponent(http://jsonip.appspot.com/?callback=getip),
	URL_PROXY = ../proxy.php?url=,
	URL_POSTAL = http://ws.geonames.org/findNearbyPostalCodesJSON?lat={0}&lng={1},
	WATCH_TIMEOUT = 10000, // poll IP every 10 seconds, this could get expensive if called to frequently as it requires an AJAX request

	// local variables
	_engine,
	_lastIP = document.getElementById(geolocation.ip) ? document.getElementById(geolocation.ip).value : null,
	_ipToGeoCache = {},

	_reqHandleGeoCodeFailure = function() {
		Y.log(GeoCode request failed);
		// add desired failure code
	},

	_reqHandleGeoCodeSuccess = function(response) {
		var geoCodeObj = {coords: YJSON.parse(response.responseText)};
		_ipToGeoCache[_lastIP] = geoCodeObj;
		response.argument(geoCodeObj);
	},

	_reqHandleIPFailure = function() {
		Y.log(IP request failed);
		// add desired failure code
	},

	_reqHandleIPSuccess = function(response) {
		_lastIP = response.responseText.replace(/.*?"ip": "([0-9\.]+)".*/, $1);
		response.argument(_lastIP);
	},

	_reqHandlePostalFailure = function() {
		Y.log(Postal request failed);
		// add desired failure code
	},

	_reqHandlePostalSuccess = function(response) {
		var postalCodes = YJSON.parse(response.responseText);
		response.argument(postalCodes && postalCodes.postalCodes ? postalCodes.postalCodes : []);
	},

	_reqReadGeoCodeFromIp = function(ip, callback) {
		if (_ipToGeoCache[ip]) {
			callback(_ipToGeoCache[ip]);
		}
		else {
			var url = URL_PROXY + encodeURIComponent(URL_GEOCODE + ip);
			YC.asyncRequest(get, url, {failure: _reqHandleGeoCodeFailure, success: _reqHandleGeoCodeSuccess, argument: callback, cache: false});
		}
	},

	_reqReadIP = function(callback, force) {
		if (_lastIP && ! force) {
			callback(_lastIP);
		}
		else {
			YC.asyncRequest(get, URL_IP, {failure: _reqHandleIPFailure, success: _reqHandleIPSuccess, argument: callback, cache: false});
		}
	};
	
	if (navigator.geolocation) {
		_engine = navigator.geolocation;
	}
	else {
		// implement non-FF geolocation
		_engine = {
			clearWatch: function(watchId) {
				clearInterval(watchId);
			},

			getCurrentPosition: function(callback) {
				_reqReadIP(function(ip) {
					_reqReadGeoCodeFromIp(ip, callback);
				});
			},

			watchPosition: function(callback) {
				var fx = function() {
					var lastIP = _lastIP;
					_reqReadIP(function(ip) {
						if (ip !== lastIP) {
							lastIP = ip;
							_reqReadGeoCodeFromIp(ip, callback);
						}
					}, true);
				};

				setInterval(fx, WATCH_TIMEOUT);
				fx();
			}
		};
	}

	_engine.getNearbyPostalCodes = function(callback) {
		_engine.getCurrentPosition(function(o) {
			var coords = o.coords,
				url = URL_PROXY + encodeURIComponent(URL_POSTAL.replace(/\{0\}/, coords.latitude).replace(/\{1\}/, coords.longitude));
			YC.asyncRequest(get, url, {failure: _reqHandlePostalFailure, success: _reqHandlePostalSuccess, argument: callback});
		});
	};

	Core.Util.Geolocation = _engine;
}());

There is many AJAX requests, as I have designed the GeoLocation service to use AJAX to fetch all data, making it simpler to maintain, if a little less efficient. The functions clearWatch, getCurrentPosition, and watchPosition from the FireFox implementation have been emulated, and a new function getNearbyPostalCodes is added. The getNearbyPostalCodes fetches a list of nearby postal codes based on the geolocation. I use the PHP Proxy to call remote services fetching the IP address, the GeoLocation information, and then ZipCode information. All the URLs I use in this service are free APIs, but most of them have a limit to the number of times they can be called per day (these URLs are not enterprise ready).

However, hosting any of these services on your own servers should not be very difficult, using your own database/server will be faster and more reliable. You can Google search for IP to GeoLocation databases and GeoLocation to ZipCode databases. I strongly recommend at least handling the IP lookup on your server, as it can be done with this very simple script:

Example 2: PHP Script to Get IP

<?php echo $_SERVER[REMOTE_ADDR]; ?>

To determine the geolocation, my implementation first fetches the users IP, then fetches the GPS coordinates of that IP. The initial IP lookup can be prevented, by including a hidden input element with the id "geolocation.ip" on the page and a value of the IP.

See the code in action on this GeoLocation Test Page.

There are some issues with the FireFox implementation, which I hope they smooth out in the near future:

One of the drawbacks with the new FireFox service is that users must opt in, before you can use it. However the information used by FireFox to determine a users location is already available on the server, where it can be easily forwarded to JavaScript. Thus the opt in is a waste of time and causes issues with my implementation, as I have not figured out how to detect the users choice. So on FireFox, if the user opts out, we should failback on my implementation, but it currently does not.

I have have also noticed issues with the FireFox implementation, where it sometimes does not execute callbacks the were provided before the user authorizes the geolocation service. I addition I have seen the callbacks execute twice with different geolocations each time, but have figured out the root cause; my guess is that FireFox uses several techniques for determining the location and they are being executed asynchronously from each other.