Creating a provider-agnostic online mapping application - Part 1

Posted : Sunday, 18 October 2009 22:56:15

Not so long ago I was tasked with writing a new online mapping application that would take a number of locations stored in a database and plot them onto a map. The project brief didn't mention which map provider would be used in production as that would be a business decision made closer to the end of the develoment cycle. In any event it would most likely be GoogleMaps so I began to build my first prototype of the application. The initial prototype was well received so I continued to develop the product and extend the initial functionality to fulfill the brief. The functionality of the application was duly signed off and I handed the project over to the design department for final amends and tweaks. Can you guess whats coming?

Yep sure enough, around the time the project was nearing final sign off, the decision was made and a contract with Microsoft was signed meaning that the mapping components of the application would be served using Bing(at that time known as VirtualEarth) as the provider. I had to rewrite the application to use a completely different API. Tsk! Shame on me - had I devoted more time/thought during the architecture phase of the project then the impending rewrite would have been far less arduous. The purpose of this post is to explain the basic archictecture I came up with to achieve a provider-agnostic mapping application. I will create a basic online mapping application called GeoTagger that will allow users to plot points on a map and add captions - these locations will be persisted to an online datastore. The application will also allow users to switch between providers and have their GeoTags replotted. Although a simple concept it is a reasonably extensive application so I will break it into chunks and cover the entire application over the next few posts. This first post will cover the basic javascript architecture.

Looking at the project logically we will need a core library of functions that will be required to make the application work, these will be present regardless of which provider is being used. There will also need to be a set of provider-specific functions that will implement a common interface for the core library to use. Another component that will be required is a map-manager object that can be used initialize and manage whatever map provider is currently in use as well as switch providers if required. The last piece of the puzzle is a principle application script object that will be used to manage all the other components. Given the wholly fantastic nature of jQuery I will be using it extensively for this sample application. For those who have yet to be converted, I suggest you go to the jQuery site to check it out - there are loads of examples to help you get started.

The first thing we need to get things going is a html page - I created a simple a page and added to it a div that will contain the map tiles, a couple of radio buttons to facilitate switching bewteen different map providers, some css code and a couple of script references. The html is pretty simple so I wont say anymore than that. Things start to get interesting when we look at the script so lets jump in. There are two script references, one is the jQuery library, the second contains most of the functionality of our application. In subsequent posts I will refactor this script and split it out into multiple files but for now we'll keep it in one file.

So I think the best place to start is the core function library that will be reponsible for all common map related functionality that does not involve any provider-specific script. For this first post there isn't a great deal to this object:

   48 /* MapObject constructor - mapDivId is id of html div */

   49 function MapObject(mapDivId) {

   50     this.MapDivId = mapDivId;

   51     this.MapDiv = $('#'+this.MapDivId);

   52 };

   53 /* MapObject.prototype - contains all common functionality  */

   54 MapObject.prototype = {

   55     DefaultZoomLevel: 4,

   56     DefaultLat: 53.800651,

   57     DefaultLon: -4.064941,

   58     addOverlay: function() {

   59         $('<div class="loadingOverlay"></div>')

   60         .css({

   61             top: 0,

   62             left: 0,

   63             width: this.MapDiv.width(),

   64             height: this.MapDiv.height(),

   65             zIndex: 1000000,

   66             textAlign:'center'

   67         })

   68         .append($('<div>Loading..</div>')

   69             .css({

   70                 position:'relative',

   71                 top:100,

   72                 backgroundColor:'white'

   73             }))

   74         .appendTo($('#mapContainer'));

   75     },

   76     removeOverlay: function() {

   77         $('.loadingOverlay').fadeOut(function() {

   78             $('.loadingOverlay').remove();

   79         });

   80     }

   81 }

The constructor takes the ID of the html div that will contain the html tiles. The prototype of the MapObject has a few simple properties that determine where the map is positioned and at what zoom level - these properties are common to all map providers. The addOverlay method will simply create a new div the same size as the map div and append it to the DOM and position it directly above the map and serves to provide a slightly more friendly UI by informing the user that the map is loading.

So now that we have our basic map object, lets extend it to add some proper mapping functionality. I am going to use Bing as the provider for this example (the source code for this project also contains the Google implementation) so lets take a closer look:

   84 /* Bing specific script */

   85 function Bing() {}

   86 Bing.prototype = {

   87     CurrentProvider: MapProvider.ProviderEnum.Bing,

   88     loadMap: function() {

   89         var _Bing = this;

   90         // Is this browser supported by VE? If not why bother loading a broken control?

   91         if (!($.browser.msie) && !($.browser.mozilla) && !($.browser.safari)) {

   92             $(_Bing.MapDivId).html("Virtual Earth is not supported by your browser.")

   93         } else {

   94             if (!($.browser.msie)) {

   95                 //work around for non ie

   96                 $.getScript('http://dev.virtualearth.net/mapcontrol/v6.2/js/atlascompat.js');

   97             }

   98             $.getScript(

   99                 'http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2',

  100                 function() {

  101                     var _map = new VEMap(_Bing.MapDivId);

  102                     _map.onLoadMap = function() {

  103                         _Bing.removeOverlay();

  104                     };

  105                     $(window).unload(function() {

  106                         _Bing.unloadMap();

  107                     });

  108                     _map.LoadMap(new VELatLong(_Bing.DefaultLat, _Bing.DefaultLon), _Bing.DefaultZoomLevel);

  109                     _Bing._map = _map;

  110                 }

  111             );

  112         }

  113     },

  114     unloadMap: function() {

  115         if (this._map) this._map.Dispose();

  116     }

  117 }

As you can see a basic object called Bing is defined, the prototype of which is then modified as follows. First a property representing the identity of this provider is added (I will cover the details of MapProvider.ProviderEnum shortly). Next added is a method called loadMap(), the first line of this function explicity creates a reference to the Bing object itself such that the reference is still available in closures defined later in the function. The purpose of the loadMap function is to utilise the jQuery ajax functionality and dynamically load the Bing API script then fire a callback function once the script has loaded. The callback function is what actually loads the map, and the details of it whats going on can be found in the Bing API documentation. As you can from line #108 - the map is initialized using the properties defined in the MapOject earlier - I show you the details of how the two objects are joined next. A function called unloadMap() is added to make sure that we clean up any resources as recommended in the Bing API documentation.

Next comes the map-manager object:

   21 /* object to handle logic of determining which provider to load */

   22 var MapManager = {

   23     ProviderEnum: new EnumeratedType('Google', 'Bing'),

   24     createMap: function(mapDivId, eProvider) {

   25         _Map = new MapObject(mapDivId);

   26         switch(eProvider) {

   27         case MapManager.ProviderEnum.Bing:

   28             _Bing = new Bing();

   29             jQuery.extend(_Map, _Bing);

   30         break;

   31         case MapManager.ProviderEnum.Google:

   32             _Google = new Google();

   33             jQuery.extend(_Map, _Google);

   34         break;

   35         default:

   36             alert('Unknown map provider: '+eProvider);

   37         };

   38         return _Map;

   39     }

   40 }

So the MapManager object reqires a bit of explanation. The first member to the prototype ProviderEnum, is an enumeration representing all the currently registered map providers - it is basically an instance of a simple Javascript enumeration object:

   43 /* simple javascript enumeration */

   44 function EnumeratedType(){

   45     for (var i = 0; i < arguments.length; i++) {

   46         this[arguments[i]] = i;

   47     }

   48 };

As you can see it simply creates an associative array of all the members.

The second member of the MapManager object is the createMap() function which takes as parameters, the Id of the map div and an instance of the ProviderEnum enumeration. The first line of this function creates an instance of the MapObject we defined earlier. Next a switch statement is used to determine which provider specific implementation will be used, an instance of the specified provider is then created and this object is then merged with the MapObject just created by using the extend() method of jQuery. The resulting object is then returned to the calling function.

Last of all is the main application script object:

    1 /* main GeoTagger object through which all script objects/functions are accessed */

    2 function GeoTagger(mapDivId, eProvider){

    3     this.Name = "GeoTagger"

    4     MapDivId = mapDivId;

    5     Map = MapManager.createMap(mapDivId, eProvider);

    6     Map.addOverlay();

    7     //set up behaviour for radio buttons

    8     $('input[name=rdoMapManager]:radio').change(function() {

    9         //only load if the current map is different from what is currently loaded

   10         if(Map.CurrentProvider != MapManager.ProviderEnum[this.value]) {

   11             Map.addOverlay();

   12             Map.unloadMap();

   13             Map = MapManager.createMap(MapDivId, MapManager.ProviderEnum[this.value]);

   14             Map.loadMap();

   15         }

   16     });

   17     this.Map = Map;

   18     this.MapDivId = mapDivId;

   19 };

This is also a fairly simple object - it initializes a couple of properties, loads the map based on the supplied parameters and adds an event handler to the provider radio buttons so the correct map is loaded if either is selected. The only other bit of script is the one to kick off the whole process - for the purposes of this post, the process is initialized with a simple bit of script at the bottom of the file:

  167 $(document).ready(function() {

  168     var GT = new GeoTagger('mapDiv', MapManager.ProviderEnum.Bing);

  169     GT.Map.loadMap();

  170     $('input[name=rdoMapManager]:radio:first').attr("checked",true);

  171 });

This doesn't warrant much of an explanation suffice to say an instance of the GeoTagger is created supplying the ID of the map div and a instance of the ProviderEnum meaning that Bing will be the provider that is first loaded. As previously stated the Google implementation is available in the download for this post./p>

What we have now is our basic provider-agnostic online mapping application. As you can see, the provider specific functionality has been seperated from the rest of the script so the task of implementing a new provider version is a simple case of implementing only those functions specific to the map provider, the rest of the application can remain unchanged. In the next part of this post I will expand upon this basic structure to make the maps a bit more interesting by allowing users to plot points on the map.

  • (This will not appear on the site)