A generic, lazy loading, self dependency resolving geolocation javascript service object

I've been working on upgrading a client's form autofill options. Right now, they have server side ip sniffing, and forms I'm unable to cache because of the defaulted information. So, blank forms with client side geolocation through the the javascript navigator object is required. I've done this kind of thing before, but it's always been on the page, assuming everything would work. This time around, I'm doing things a little differently, anticipating using this over and over for their many forms. So this time, I'm playing by a few new rules...

  1. Ask the user their location as rarely as possible, once being ideal.
  2. Do not assume dependencies will be met. Load things like google map apis on the fly, but only when required.
  3. Function as a service. Any number of things might need the geolocation information, as soon as its available, handle them all.

To that end, a short explanation of what's completed, then the object for your copying pleasure:

The geolocation.js file will expose three methods you should expect to use: geolocation.getCoordinates(), geolocation.getAddress(), geolocation.reverseGeocode(). More often than not, you'll use coordinates and address, that last one was in there because I needed it for the first two. Usage for all of these involves registering your function to callback to 'whenever the information becomes available.' This whole thing depends on multiple levels of delay and callback. An initial pause is to be expected as the user is asked for their coordinates. Another delay is required if an address is required, while google maps api is loaded, then again as the address is reverse geocoded. If returned, the coordinates and address information are stored in the browser's localStorage and returned immediately to prevent dependency loads in future uses. Please note that I have not concerned myself with backwards compatibility yet. If you care, you may need to add support in case JSON objects are missing in the browser, and a backup localStorage method just as a cookie. I do not encourage the storage of geolocation data in cookies. Your users will not thank you.

  1. geolocation = {
  2.   error: null
  3.   ,location: {
  4.     coordinates: {
  5.       timestamp: null
  6.       ,latitude: null
  7.       ,longitude: null
  8.     }
  9.     ,address: {
  10.       timestamp: null
  11.       ,street_number: null
  12.       ,route: null
  13.       ,locality: null
  14.       ,administrative_area_level_3: null
  15.       ,administrative_area_level_2: null
  16.       ,administrative_area_level_1: null
  17.       ,country: null
  18.       ,postal_code: null
  19.     }
  20.   }
  21.   ,coordinatesQueue: {
  22.     items: []
  23.     ,add: function(callbacks){
  24.       if( callbacks != null && !geolocation.coordinatesQueue.contains(callbacks) ){
  25.         geolocation.coordinatesQueue.items.push(callbacks);
  26.       }
  27.     }
  28.     ,get: function(){ return geolocation.coordinatesQueue.items; }
  29.     ,contains: function(callbacks){
  30.       var i = geolocation.coordinatesQueue.items.length;
  31.       while (i--) {
  32.         if (geolocation.coordinatesQueue.items[i] === callbacks) {
  33.           return true;
  34.         }
  35.       }
  36.       return false;
  37.     }
  38.   }
  39.   ,getCoordinates: function(success, error){
  40.     // Return from local browser cache if set
  41.     try {
  42.       var coordinates = sessionStorage.getItem('geolocation.coordinates');
  43.       if( coordinates != null ){
  44.         geolocation.location.coordinates = JSON.parse(coordinates);
  45.         success(geolocation.location.coordinates);
  46.         return null;
  47.       }
  48.     } catch(err) {}
  49.    
  50.     // If we've hit an error, return it
  51.     if( geolocation.error != null ){
  52.       if( typeof error == 'function' ){
  53.         error(geolocation.error);
  54.       }
  55.       return false;
  56.     }
  57.    
  58.     // Return from window object if already defined
  59.     if( geolocation.location.coordinates.timestamp != null ){
  60.       success(geolocation.location.coordinates);
  61.       return null;
  62.     }
  63.    
  64.     // Add this as a package to the queue to notify when available
  65.     var callbacks = {
  66.       success: success
  67.       ,error: error
  68.     }
  69.     geolocation.coordinatesQueue.add(callbacks);
  70.    
  71.     // Ask for it, store it, and send it along to the submitted callback
  72.     navigator.geolocation.getCurrentPosition(geolocation.setCoordinates, geolocation.setError);
  73.     return null;
  74.   }
  75.   ,setCoordinates: function(position){
  76.     // Prevent race firing this twice
  77.     if( geolocation.location.coordinates.timestamp != null ){
  78.       return;
  79.     }
  80.     geolocation.location.coordinates.latitude = position.coords.latitude;
  81.     geolocation.location.coordinates.longitude = position.coords.longitude;
  82.     geolocation.location.coordinates.timestamp = new Date().getTime();
  83.     try {
  84.       sessionStorage.setItem('geolocation.coordinates', JSON.stringify(geolocation.location.coordinates) );
  85.     } catch(err) {}
  86.     var queue = geolocation.coordinatesQueue.get();
  87.     if( queue.length > 0 ){
  88.       for( var i = 0; i < queue.length; i++ ){
  89.         queue[i].success(geolocation.location.coordinates);
  90.       }
  91.     }
  92.     geolocation.coordinatesQueue = null;
  93.   }
  94.   ,addressQueue: {
  95.     items: []
  96.     ,add: function(callbacks){
  97.       if( callbacks != null && !geolocation.addressQueue.contains(callbacks) ){
  98.         geolocation.addressQueue.items.push(callbacks);
  99.       }
  100.     }
  101.     ,get: function(){ return geolocation.addressQueue.items; }
  102.     ,contains: function(callbacks){
  103.       var i = geolocation.addressQueue.items.length;
  104.       while (i--) {
  105.         if (geolocation.addressQueue.items[i] === callbacks) {
  106.           return true;
  107.         }
  108.       }
  109.       return false;
  110.     }
  111.   }
  112.   ,getAddress: function(success, error){
  113.     // Return from local browser cache if set
  114.     try {
  115.       var address = sessionStorage.getItem('geolocation.address');
  116.       if( address != null ){
  117.         geolocation.location.address = JSON.parse(address);
  118.         success(geolocation.location.address);
  119.         return null;
  120.       }
  121.     } catch(err) {}
  122.  
  123.     // If we've hit an error, return it
  124.     if( geolocation.error != null ){
  125.       if( typeof error == 'function' ){
  126.         error(geolocation.error);
  127.       }
  128.       return false;
  129.     }
  130.    
  131.     // Return from window object if already defined
  132.     if( geolocation.location.address.timestamp != null ){
  133.       success(geolocation.location.address);
  134.       return null;
  135.     }
  136.  
  137.     // Add this to the queue to notify when available
  138.     // Add this as a package to the queue to notify when available
  139.     var callbacks = {
  140.       success: success
  141.       ,error: error
  142.     }
  143.     geolocation.addressQueue.add(callbacks);
  144.    
  145.     // Start the event chain
  146.     geolocation.getCoordinates(geolocation.getCoordinatesAddress, geolocation.setError);
  147.   }
  148.   ,getCoordinatesAddress: function(coordinates){
  149.     geolocation.reverseGeocode(geolocation.setAddress, coordinates);
  150.   }
  151.   ,setAddress: function(address){
  152.     // Prevent race firing this twice
  153.     if( geolocation.location.address.timestamp != null ){
  154.       return;
  155.     }
  156.     geolocation.location.address = address;
  157.     try {
  158.       sessionStorage.setItem('geolocation.address', JSON.stringify(address) );
  159.     } catch(err) {}
  160.     var queue = geolocation.addressQueue.get();
  161.     if( queue.length > 0 ){
  162.       for( var i = 0; i < queue.length; i++ ){
  163.         queue[i].success( address );
  164.       }
  165.     }
  166.     geolocation.addressQueue = null;
  167.   }
  168.   ,reset: function(){
  169.     sessionStorage.removeItem('geolocation.coordinates');
  170.     sessionStorage.removeItem('geolocation.address');
  171.   }
  172.   ,setError: function(error){
  173.     // Clear queues, notify all registered error functions
  174.     geolocation.error = error;
  175.     // Coordinates
  176.     var queue = geolocation.coordinatesQueue.get();
  177.     if( queue.length > 0 ){
  178.       for( var i = 0; i < queue.length; i++ ){
  179.         if( typeof queue[i].error == 'function' ){
  180.           queue[i].error(error);
  181.         }
  182.       }
  183.     }
  184.     geolocation.coordinatesQueue = null;
  185.    
  186.     // Addresses
  187.     queue = geolocation.addressQueue.get();
  188.     if( queue.length > 0 ){
  189.       for( var i = 0; i < queue.length; i++ ){
  190.         if( typeof queue[i].error == 'function' ){
  191.           queue[i].error( error );
  192.         }
  193.       }
  194.     }
  195.     geolocation.addressQueue = null;
  196.   }
  197.   ,geocoder: null
  198.   ,geocodeQueue: {
  199.     items: []
  200.     ,add: function(callback){
  201.       if( callback != null && !geolocation.geocodeQueue.contains(callback) ){
  202.         geolocation.geocodeQueue.items.push(callback);
  203.       }
  204.     }
  205.     ,get: function(){ return geolocation.geocodeQueue.items; }
  206.     ,contains: function(callback){
  207.       var i = geolocation.geocodeQueue.items.length;
  208.       while (i--) {
  209.         if (geolocation.geocodeQueue.items[i] === callback) {
  210.           return true;
  211.         }
  212.       }
  213.       return false;
  214.     }
  215.   }
  216.   ,geocode: function(callback, address){
  217.     if( geolocation.geocoder == null ){
  218.       geolocation.geocodeQueue.add({
  219.         callback: callback
  220.         ,address: address
  221.       });
  222.       geolocation._geocoderLoad();
  223.       return null;
  224.     }
  225.     geolocation.geocoder.geocode( { 'address': address }, function(results, status) {
  226.       if (status == google.maps.GeocoderStatus.OK) {
  227.         coordinates = {};
  228.         coordinates.latitude = results[0].geometry.location.lat();
  229.         coordinates.longitude = results[0].geometry.location.lng();
  230.         coordinates.timestamp = new Date().getTime();
  231.         callback(coordinates);
  232.       } else {
  233.         callback(null);
  234.       }
  235.     });
  236.   }
  237.   ,reverseGeocodeQueue: {
  238.     items: []
  239.     ,add: function(callback){
  240.       if( callback != null && !geolocation.reverseGeocodeQueue.contains(callback) ){
  241.         geolocation.reverseGeocodeQueue.items.push(callback);
  242.       }
  243.     }
  244.     ,get: function(){ return geolocation.reverseGeocodeQueue.items; }
  245.     ,contains: function(callback){
  246.       var i = geolocation.reverseGeocodeQueue.items.length;
  247.       while (i--) {
  248.         if (geolocation.reverseGeocodeQueue.items[i] === callback) {
  249.           return true;
  250.         }
  251.       }
  252.       return false;
  253.     }
  254.   }
  255.   ,reverseGeocode: function(callback, coordinates){
  256.     if( geolocation.geocoder == null ){
  257.       geolocation.reverseGeocodeQueue.add({
  258.         callback: callback
  259.         ,coordinates: coordinates
  260.       });
  261.       geolocation._geocoderLoad();
  262.       return null;
  263.     }
  264.     var latlng = new google.maps.LatLng(coordinates.latitude, coordinates.longitude);
  265.     geolocation.geocoder.geocode({'latLng': latlng}, function(results, status) {
  266.       if (status == google.maps.GeocoderStatus.OK) {
  267.         var address = {
  268.           timestamp: null
  269.           ,street_number: null
  270.           ,route: null
  271.           ,locality: null
  272.           ,administrative_area_level_3: null
  273.           ,administrative_area_level_2: null
  274.           ,administrative_area_level_1: null
  275.           ,country: null
  276.           ,postal_code: null
  277.         };
  278.         for (var i = 0; i < results[0].address_components.length; i++) {
  279.           var _element = results[0].address_components[i];
  280.           switch( _element.types[0] ){
  281.             case 'street_number':
  282.               address.street_number = _element;
  283.               break;
  284.             case 'route':
  285.               address.route = _element;
  286.               break;
  287.             case 'locality':
  288.               address.locality = _element;
  289.               break;
  290.             case 'administrative_area_level_3':
  291.               address.administrative_area_level_3 = _element;
  292.               break;
  293.             case 'administrative_area_level_2':
  294.               address.administrative_area_level_2 = _element;
  295.               break;
  296.             case 'administrative_area_level_1':
  297.               address.administrative_area_level_1 = _element;
  298.               break;
  299.             case 'country':
  300.               address.country = _element;
  301.               break;
  302.             case 'postal_code':
  303.               address.postal_code = _element;
  304.               break;
  305.           }
  306.         }
  307.         address.timestamp = new Date().getTime();
  308.         callback(address);
  309.       } else {
  310.         callback(null);
  311.       }
  312.     });
  313.   }
  314.   /* internal functions */
  315.   ,_geocoderLoad: function(){
  316.     var loaded = false;
  317.     try {
  318.       var geocoder = new google.maps.Geocoder();
  319.       loaded = true;
  320.     } catch (err){}
  321.     if( !loaded ){
  322.       var gm = document.createElement('script');
  323.       gm.type = 'text/javascript';
  324.       gm.async = true;
  325.       var s = document.getElementsByTagName('script')[0];
  326.       s.parentNode.insertBefore(gm, s);
  327.     } else {
  328.       geolocation._geocoderLoaded();
  329.     }
  330.     return null;
  331.   }
  332.   ,_geocoderLoaded: function(){
  333.     geolocation.geocoder = new google.maps.Geocoder();
  334.     var queue = geolocation.reverseGeocodeQueue.get();
  335.     if( queue.length > 0 ){
  336.       for( var i = 0; i < queue.length; i++ ){
  337.         geolocation.reverseGeocode(queue[i].callback, queue[i].coordinates);
  338.       }
  339.     }
  340.     geolocation.reverseGeocodeQueue = null;
  341.     queue = geolocation.geocodeQueue.get();
  342.     if( queue.length > 0 ){
  343.       for( var i = 0; i < queue.length; i++ ){
  344.         geolocation.geocode(queue[i].callback, queue[i].address);
  345.       }
  346.     }
  347.     geolocation.geocodeQueue = null;
  348.   }
  349. };

Usage example:

  1. var feedback = {
  2.   init: function(){
  3.     geolocation.getAddress(feedback.addressAvailable);
  4.   }
  5.   ,addressAvailable: function(address){
  6.     var $element;
  7.     jQuery('#country').val( address.country.short_name.toLowerCase() );
  8.     $element = jQuery('#state');
  9.     if( $element.val() == '' ){
  10.       $element.val( address.administrative_area_level_1.long_name );
  11.     }
  12.     $element = jQuery('#postal_code');
  13.     if( $element.val() == '' ){
  14.       $element.val( address.postal_code.long_name );
  15.     }
  16.     $element = jQuery('#city');
  17.     if( $element.val() == '' ){
  18.       $element.val( address.locality.long_name );
  19.     }
  20.     $element = jQuery('#street');
  21.     if( $element.val() == '' ){
  22.       $element.val( address.street_number.long_name + ' ' + address.route.long_name );
  23.     }
  24.   }
  25. };
  26.  
  27. jQuery(document).ready ( feedback.init );

Comments

I knew I'd need this at some point... I've added geolocation from address to coordinates working exactly the way the rest of the interface does.

Ran into the case that I needed to react to users denying me access. Rebuild the system to take that into account.

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.