Creating a provider-agnostic online mapping application - Part 3

Posted : Friday, 15 January 2010 17:43:54

This is the last in a three-post series 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 part two I built on part one to add the functionality to add content to the map. In this post I shall complete the application by creating a simple persistence system whereby when visitors add points to the map, the data is sent back to the server where it will be saved to an xml file such that any time the map loads, any previously added points will be re-plotted.

So in order to add some server-side functionality to this application I created a new ASP.NET MVC Web Application project. I chose MVC as I wanted something quick and easy to implement and more crucially i wanted to be able to wire up jQuery ajax calls to a serside method with the minimum amount of fuss - MVC supplies exactly this. NB If you're unsure or wary of the MVC technology, I cannot recommend highly enough this book by Steven Sanderson, it explains all aspects of the MVC infrastruture in a clear and concise way providing plenty of easy to understand examples. Anyway back to the subject in hand, I created a default MVC project and for the sake of clarity I removed most of the default files created to yield a project structure like that show below.

default MVC project strucure with unneccesary files removed

The next step is to add a method called Map to the Controllers/HomeController.cs (incidentally I deleted all the other methods that were created by default).

   17         public ActionResult Map()

   18         {

   19             return View();

   20         }

Using the default routing configuration of the MVC project, any requests for the URL /Home/Map will be forwarded to this method. The next step is to create a page template (or View to use MVC terminology) for this method. There are a number of ways of doing this, the simplest being to right click anywhere inside the method and select "Add View" from the context menu. You will be presented with a Dialog box, for the top two checkboxes leave the default (unchecked) and uncheck the "Select Master Page" option. This will add an aspx file called "Map.aspx" to the /Views/Home folder. The next step is to add the files created in the previous posts of this series. When everything is added correctly, your project structure should resemble that show below

completed project strucure with all required files added

The last step required to integrate the functionality from the previous posts into this web project is to copy the markup from the default.html file into the new Map.aspx file we just created. Copy everything from the between the <head></head> tags in default.html to the same location in Map.aspx and do the same for everything between the <body></body> tags. To test if everything is working, run the webserver (press F5) and browse to /localhost:xxx/home/map where xxx is the randomly assigned port number that Visual Studio will allocate to this session and is the port under which the development server will be hosted. You should now be able to add pins to the map and tag the locations as outlined in post 2.

So now that everything is running in a hosted we environment we can get to adding the functionality need to save the plotted points. The first thing I did was to add a class to the project to represent an in memory instance of each geotagged point on the map. As this is only a simple blog post I created in inner class of the HomeController class as follows:

   75         public class GeoTag

   76         {

   77             public string Tag { get; set; }

   78             public double Latitude { get; set; }

   79             public double Longitude { get; set; }

   80         }

For speed of implementation, I choose to use Linq to XML for this post. I added a string constant to the controller to represent the location of the xml file that will contain the saved points. I then added the following method which takes a List<GeoTag> and saves them to the specified location:

   57         private void SavePoints(List<GeoTag> points)

   58         {

   59             XDocument xdoc = new XDocument(

   60                 new XDeclaration("1.0", "utf-8", "yes"),

   61                 new XComment("List of plotted points"),

   62                 new XElement("Points",

   63                 from p in points

   64                     select

   65                     new XElement("Point",

   66                     new XElement("Tag", p.Tag),

   67                     new XElement("Latitude", p.Latitude),

   68                     new XElement("Longitude", p.Longitude))

   69                 )

   70             );

   71             //save the document

   72             xdoc.Save(Server.MapPath(POINTS_FILENAME));

   73         }

And then the corresponding method to load all points from the same file:

   38         private List<GeoTag> GetSavedPoints()

   39         {

   40             try

   41             {

   42                 XDocument xdoc = XDocument.Load(Server.MapPath(POINTS_FILENAME));

   43                 return (from xml in xdoc.Elements("Points").Elements("Point")

   44                         select new GeoTag

   45                         {

   46                             Tag = xml.Element("Tag").Value,

   47                             Latitude = double.Parse(xml.Element("Latitude").Value),

   48                             Longitude = double.Parse(xml.Element("Longitude").Value),

   49                         }).ToList();

   50             }

   51             catch (FileNotFoundException fnfe)

   52             {

   53                 return new List<GeoTag>();

   54             }

   55         }

We're nearly done with the server-side code, two more methods are required, Firstly is a publicly accesible method to send all the saved points to the client. This is acheived very simply using the new JsonResult object in MVC - it simply takes an object and returns it to the client as a serialized json string (if you're unaware what json is, more details can be found here) The method is shown below:

   22         public JsonResult GetPins()

   23         {

   24             return this.Json(GetSavedPoints());

   25         }

This method is pretty simple and doesn't warrant any further explanation. As you have probably guessed the last bit of server code is another publicly accessible method that will accept data sent from the browser when a user adds a pin to the map:

   27         [AcceptVerbs(HttpVerbs.Post)]

   28         public void AddPin(GeoTag newTag)

   29         {

   30             //get saved points

   31             var points = GetSavedPoints();

   32             //add this new point to the list

   33             points.Add(newTag);

   34             //then save(overwrite)

   35             SavePoints(points);

   36         }

This is a bit more interesting than the GetPins() method above, firstly the method is decorated with the [AcceptVerbs(HttpVerbs.Post)] attribute, this tells the server that only Post requests should get sent to this method. The second thing to note is that the method takes a instance of the GeoTag class. This is an example of one of the excellent new features that MVC brings, Model Binding. By default the server creates a new instance of the specified object and checks a number of places (Request.Form, Request.Querystring etc) for values matching the name of the class properties, if a match is found the property is assigned that value. Very nifty I'm sure you'll agree! The method then loads the presaved points, adds this new one to that list then resaves. As this is only a demo I haven't paid any consideration to optimisation or performance issues. If you are intending to create something like this in production I would highly recommend using a database rather than xml.

So now we turn our attention to the client side script. All our script is contained with the file /Scripts/geotagger.js so open up that file now. Fistly we need to hook into the process when the pins get added to the map so that the relevant data gets sent to the new AddPin method we created previously. This is quite straightforward and to achieve it we issue an AJAX call to our webserver, jQuery makes this ridiculously easy and its simply a case of adding the following lines of script to be executed when "Save" is clicked in the pin popup (refer to the previous post for a refresher if you are unsure what I mean by this):

   69             .append(

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

   71                     .click(function() {

   72                         var tag = $('#editedComment').val();

   73                         var lat = pin.Latitude;

   74                         var lon = pin.Longitude;

   75                         //send the request data to the server

   76                         $.post('/Home/AddPin', 'tag=' + tag + '&latitude=' + lat + '&longitude=' + lon);

   77                         GT.Map.setPinTitle(pin, tag);

   78                         $('.popup')

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

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

   81                             })

   82                     })

   83             )

The extra lines are shown in bold (the line numbers shown will be slightly different from the earlier posts as there are a few lines of additional script that I will explain next). As you can see, some local variables are initialized and sent to the server. This is done with a single line of script (line #76) and utilises the jQuery.post method - jQuery makes integrating AJAX functionality into your websites a breeze! Incidentally the same thing can be acheived using the MSAJAX library but personally I find jQuery a lot simpler.

Finally we need to hook into the map loading process so that every time the map loads, the GetPins method is called and all the saved points will be plotted on to the map. As you may recall from the first post in this series we created a mapLoaded event thsat is fired once all the required scipts are loaded and the map is initialized, this is the prefect place to add the plot the presaved pins. So add the following script to the bottom of the mapLoaded event in GeoTagger.js:

   51         //load any presaved points

   52         $.getJSON('/home/getpins', function(json) {

   53             for (var i = 0; i < json.length; i++) {

   54                 var tag = json[i];

   55                 var newPin = GT.Map.addPinAtPixel(tag.Latitude, tag.Longitude);

   56                 GT.Map.setPinTitle(newPin, tag.Tag);

   57             }

   58         })

This is an example of using the jQuery.getJSON method to issue a get Request to the specified URL which will, on completion, fire the anonymous call back method supplied as the second parameter. Any data returned from the URL is passed into the anonymous function via the json parameter. Actually adding the points to the map is simply a case of iterating through each point in the returned array and calling a new provider function I created called addPinAtLatLong - this is very similar to the method addPinAtMapXY except that instead of X,Y pixel co-ordinates relative to the current map, it takes latitude and longitude values as the location for the new pin. I haven't detailed these functions but both the Bing and Google implementaions are available in the accompanying download.

The purpose of this series of posts was to illustrate a possible architecture for an online mapping application that maintains a clear seperation of code used internally by the application and the code related to the third party API code. The principal aim of this post was to outline how one might go about creating a mapping application that while dependent on a third party api, is not so tightly coupled that switch 3rd party libary does not result in a total rewrite. There are clearly areas that can be improved upon and in a real world scenario more attention would need to be paid to contention and performance issues but for a simple example this series of post satifies the goals. I hope you find it of some use and please email me with any questions, you can find my contact details on the about page.

  • (This will not appear on the site)