// W3C Geolocation API (Level 1) "Polyfill" Implementation // This uses freegeoip.org to estimate location from IP address // by Joshua Bell - https://github.com/inexorabletash.polyfill // PUBLIC DOMAIN (function() { 'use strict'; if (!navigator || !window || !document) { return; } if (!('now' in Date)) Date.now = function() { return Number(new Date); }; var GEOIP_SERVICE_JSONP = 'https://freegeoip.net/json/google.com?callback='; var SERVICE_THROTTLE_QPS = 1000 / (60 * 60); // 1000/hour var POLITENESS_FACTOR = 2; var POLL_TIMER_MS = (1000 / SERVICE_THROTTLE_QPS) * POLITENESS_FACTOR; var DISTANCE_THRESHOLD_M = 20; var EARTH_RADIUS_M = 6.384e6; // TODO: Implement user prompt and store preference w/ cookies function PositionError(code, message) { this.code = code; this.message = message; } PositionError.PERMISSION_DENIED = 1; PositionError.POSITION_UNAVAILABLE = 2; PositionError.TIMEOUT = 3; PositionError.prototype = new Error(); function Coordinates(data) { this.accuracy = EARTH_RADIUS_M * Math.PI; this.altitude = null; this.altitudeAccuracy = null; this.heading = null; this.latitude = Number(data.latitude); this.longitude = Number(data.longitude); this.speed = null; } function Geoposition(data) { this.timestamp = Date.now(); this.coords = new Coordinates(data); } Geoposition.distance = function(p1, p2) { if (p1 === p2) { return 0; } if (!p1 || !p2) { return Infinity; } // c/o http://jsp.vs19.net/lr/sphere-distance.php function angle(b1, l1, b2, l2) { function d2r(d) { return d * Math.PI / 180; } var p1 = Math.cos(d2r(l1 - l2)), p2 = Math.cos(d2r(b1 - b2)), p3 = Math.cos(d2r(b1 + b2)); return Math.acos(((p1 * (p2 + p3)) + (p2 - p3)) / 2); } return EARTH_RADIUS_M * angle(p1.coords.latitude, p1.coords.longitude, p2.coords.latitude, p2.coords.longitude); }; function GeolocationPolyfill() { var cached = null; function dispatch(handler, data) { if (typeof handler === 'function') { setTimeout(function() { handler(data); }, 0); } else if (typeof handler === 'object' && handler && 'handleEvent' in handler) { handler = handler.handleEvent; setTimeout(function() { handler(data); }, 0); } } function acquireLocation(onSuccess, onFailure, enableHighAccuracy) { var script = document.createElement('SCRIPT'), cbname = '_geoip_callback_' + Math.floor(Math.random() * (1<<30)); function cleanup() { if (script.parentNode) { script.parentNode.removeChild(script); } try { delete window[cbname]; } catch (ex) { window[cbname] = (void 0); /*IE8-*/ } } window[cbname] = function(data) { cleanup(); onSuccess(new Geoposition(data)); }; script.onerror = function(e) { cleanup(); onFailure(e); }; script.src = GEOIP_SERVICE_JSONP + encodeURIComponent(cbname); (document.head || document.body || document.documentElement).appendChild(script); return cleanup; } this.getCurrentPosition = function(successCallback, errorCallback, options) { if (!successCallback) { throw TypeError('The successCallback parameter is null.'); } if (options && 'maximumAge' in options) { var maximumAge = options.maximumAge|0; if (maximumAge < 0) maximumAge = 0; } else { maximumAge = 0; } if (options && 'timeout' in options) { var timeout = options.timeout|0; if (timeout < 0) timeout = 0; } else { timeout = Infinity; } if (options && 'enableHighAccuracy' in options) { var enableHighAccuracy = Boolean(enableHighAccuracy); } else { enableHighAccuracy = false; } if (cached && ((Date.now() - cached.timestamp) <= maximumAge)) { dispatch(successCallback, cached); return; } if (timeout === 0) { dispatch(errorCallback, new PositionError(PositionError.TIMEOUT, 'Timed out')); return; } var cancelOperation = acquireLocation(onSuccess, onFailure, enableHighAccuracy); var timedOut = false, timerId = 0; if (isFinite(timeout)) { timerId = setTimeout(function() { timedOut = true; cancelOperation(); dispatch(errorCallback, new PositionError(PositionError.TIMEOUT, 'Timed out')); }, timeout); } function onSuccess(position) { cached = position; if (timedOut) return; if (timerId) clearTimeout(timerId); dispatch(successCallback, position); } function onFailure() { if (timedOut) return; if (timerId) clearTimeout(timerId); dispatch(errorCallback, new PositionError(PositionError.POSITION_UNAVAILABLE, 'Position unavailable')); } }; var timers = [], counter = 0; this.watchPosition = function(successCallback, errorCallback, options) { if (!successCallback) { throw TypeError('The successCallback parameter is null.'); } if (options && 'maximumAge' in options) { var maximumAge = options.maximumAge|0; if (maximumAge < 0) maximumAge = 0; } else { maximumAge = 0; } if (options && 'timeout' in options) { var timeout = options.timeout|0; if (timeout < 0) timeout = 0; } else { timeout = Infinity; } if (options && 'enableHighAccuracy' in options) { var enableHighAccuracy = Boolean(enableHighAccuracy); } else { enableHighAccuracy = false; } if (cached && ((Date.now() - cached.timestamp) < maximumAge)) { dispatch(successCallback, cached); } var intervalId = setInterval(systemEvent, POLL_TIMER_MS); var timerDetails = { intervalId: intervalId, cleared: false }; var lastPosition = null, timerId = 0; acquisitionSteps(); function acquisitionSteps() { var cancelOperation = acquireLocation(onSuccess, onFailure, enableHighAccuracy); var timedOut = false; if (isFinite(timeout) && !timerId) { timerId = setTimeout(function() { timedOut = true; timerId = 0; cancelOperation(); if (!timerDetails.cleared) dispatch(errorCallback, new PositionError(PositionError.TIMEOUT, 'Timed out')); }, timeout); } function onSuccess(position) { cached = position; if (timedOut || timerDetails.cleared) return; if (timerId) timerId = clearTimeout(timerId); if (Geoposition.distance(lastPosition, position) >= DISTANCE_THRESHOLD_M) { lastPosition = position; dispatch(successCallback, position); } } function onFailure() { if (timedOut || timerDetails.cleared) return; if (timerId) timerId = clearTimeout(timerId); dispatch(errorCallback, new PositionError(PositionError.POSITION_UNAVAILABLE, 'Position unavailable')); } } function systemEvent() { acquisitionSteps(); } var watchId = ++counter; timers[watchId] = timerDetails; return watchId; }; this.clearWatch = function(watchId) { watchId = watchId|0; if (!(watchId in timers)) { return; } var timerDetails = timers[watchId]; delete timers[watchId]; clearInterval(timerDetails.intervalId); timerDetails.cleared = true; }; } // Exports if (!navigator.geolocation) { navigator.geolocation = new GeolocationPolyfill(); } }());