Creating a provider-agnostic online mapping application - Part 2

Posted : Tuesday, 08 December 2009 19:49:04

This is the second in a series of posts where I exemplify a possible architecture for a web based provider-agnostic mapping application. In part one I detailed the basic functionality required to load and display a map as well as toggle between different providers. In this post I shall build on that example and add the functionality to add content to the map.

For this example I considered a few different approaches to address the question of how best to present to the site visitor an intuitive way to add content. The option I selected was to have a pin icon above the map that the user is able to drag onto the map and release at the desired location. While this may not be the best method, it is fairly simple and quick to implement so ideal for the purpose of this post.

The first step in order to add interactivity to the map we created in part one is to add the pin image above the map div, this will act as a template which users will be able to drag into place on the map. In keeping with the style of the application this pin image is implemented via a combination of some very simple css and html so doesn't warrant any further explanation.

In order to make the pin image draggable I am using the jQuery UI plugins Draggable and Droppable. I have added a mapLoaded() function to the prototype object of the GeoTagger object. This will, when invoked, add the requisite behaviour to the pin in order to make it draggable.

   25 GeoTagger.prototype = {

   26     mapLoaded: function() {

   27         //once map is loaded, make the pin image draggable

   28         $('.pin').draggable({

   29             appendTo: 'body',

   30             zIndex: 100000,

   31             scope: 'droppablemap',

   32             revert: 'invalid',

   33             start: function(event, ui) {

   34                 ui.helper.clickoffsetx = (2 + $('.pin').offset().left - event.pageX) * -1;

   35                 ui.helper.clickoffsety = (29 + $('.pin').offset().top - event.pageY) * -1;

   36             },

   37             helper: 'clone'


   39         });

   40         //make the map droppable so it can accept dropped pins

   41         //also create drop event handler to add pin to the map where pin is dropped

   42         $('#' + this.MapDivId).droppable({

   43             scope: 'droppablemap',

   44             drop: function(event, ui) {

   45                 var mapX = event.pageX - $('#' + GT.MapDivId).offset().left - ui.helper.clickoffsetx;

   46                 var mapY = event.pageY - $('#' + GT.MapDivId).offset().top - ui.helper.clickoffsety;

   47                 var newPin = GT.Map.addPinAtMapXY(mapX, mapY);

   48                 GT.showPopUp(event.pageX, event.pageY, newPin);

   49             }


   51         });

   52     }

There are a few things of note here. Firstly we are applying a draggable behavior to the pin and a droppable behavior to the map. Doing so means that certain events (as well as the actual draggable behviour itself) become available. In the start event of the pin's draggable behaviour, the pixel location on the image that the click occurred (relative to the top left corner) is calculated and stored. We are then able to retrieve this value later and use it to calculate the location on the map that corresponds to the tip of the pin in the dragged image when the mouse button is released. We can then ensure that the new data added to the map will be located at that point rather then the pixel location at which the mouse button was released. This second calculation is performed in the drop event of the droppable behaviour that was applied to the map. Another thing worth mentioning here is that in the both the draggable and droppable behaviours, the scope value is set to the same value, doing this means that the pin can only be dropped onto the map - releasing the mouse anywhere else will cause the dropped pin to revert to its original location. Lastly notable is the setting of the helper property to clone, this means that the dragged object is copied when dragging begins so further pins are able to be dragged from the template.

On line 47 a call is made to the function addPinAtMapXY(mapX, mapY) which adds a pin to the map. Like the loadMap() function from the previous post this function must be implemented by the provider specific script. It basically takes x and y pixel co-ordinates (relative to the top left corner of the map) and adds a pin to that location. In the interests of balance I will detail the Google implementation in this post as I focused on Bing maps in part one.

  215 /* Google specific script */

  216 function Google() { }

  217 Google.prototype = {


  260     addPinAtMapXY: function(mapX, mapY) {

  261         var _Google = this;

  262         var newPx = new GPoint(mapX, mapY);

  263         var x = G_NORMAL_MAP.getProjection();

  264         var customIcon = new GIcon(G_DEFAULT_ICON);

  265         customIcon.image = "http://localhost/pin.png";

  266         customIcon.iconSize = new GSize(32, 39);

  267         markerOptions = { icon: customIcon };

  268         var newLatLon = _Google._map.fromContainerPixelToLatLng(newPx, _Google._map.getZoom());

  269         var newPin = new GMarker(newLatLon, markerOptions);

  270         _Google._map.addOverlay(newPin);

  271         return newPin;


  273     }

This doesn't require much explanation really - the API is available here. The Bing implementation is similar and is included in the download. The function should return to a reference to the newly created pin.

After the pin has been added, the provider specific script is complete (for now) and the next step is to call a generic function to open a popup on the map to receive some content from the user. This function, like the mapLoaded() function explained above, is a property of the GeoTagger prototype so available from the page level variable GT (an instance of GeoTagger). The showPopup(pageX, pageY, pin) function is called from inside the drop event of the map so the event co-ordinates are still available.

   53     showPopUp: function(pageX, pageY, pin) {

   54         $("<div class='popup' align='center'></div>")

   55         .hide()

   56         .css({

   57             top: pageY,

   58             left: pageX

   59         })

   60         .append("<div>enter a tag</div>")

   61         .append("<textarea id='editedComment'></textarea>")

   62         .append(

   63             $("<input type='button' value='Save'></input>")

   64                 .click(function() {

   65                     GT.Map.setPinTitle(pin, $('#editedComment').val());

   66                     $('.popup')

   67                         .fadeOut('fast', function() {

   68                             $('.popup').remove();

   69                         })

   70                 })

   71         )

   72         .append(

   73             $("<input type='button' value='Canel'></input>")

   74                 .click(function() {

   75                     //remove pin   

   76                     GT.Map.removePin(pin);

   77                     $('.popup')

   78                         .fadeOut('fast', function() {

   79                             $('.popup').remove();

   80                         })

   81                 })

   82         )

   83         .appendTo('body')

   84         .fadeIn('fast');

   85     }

This function takes as parameters, the point on the page at which the event occurred and a reference to the newly added pin. Via a bit of jQuery magic, an event laden popup is created which contains all the necessary information and events to either persist the pin and comment to the map or remove it. Passing the reference to the newly created pin into the showPopup function means that this reference is available both inside the save and cancel click events and can be passed into two remaining provider specific functions, setPinTitle(pin, title) and removePin(pin), shown below.

  204     setPinTitle: function(pin, title) {

  205         pin.SetTitle(title);

  206     },

  207     removePin: function(pin) {

  208         this._map.DeleteShape(pin);

  209     }

The last change necessary to incorporate this new functionality is to alter the loadMap() provider specific function to accept a callback function that can be executed when the map is loaded. In the case of Google maps this is a bit more complicated than for Bing maps and is available to view in the download. Finally the initial call to create the Map is amended to pass a reference to the function that should be executed once loading is complete.

  286 GT.Map.loadMap(GT.mapLoaded);

In this post I expanded on the basic architecture from the first post to add interactivity to the map. I maintained the same approach of keeping all generic functionality seperate from the provider specific functionality such that it is easy to swap out providers or implement a new one without needing to rewrite the whole application. In the last post of this series I will complete the application by persisting the added data to a permanent store.

  • (This will not appear on the site)