Thursday, October 09, 2008

Using Google Maps with the MVC Framework

For the last week or so I've been having a lot fun at work adding some Google map functionality to our application. It's an internal application for my clients, so I can't show you any of the code, but I've put together a little demo to demonstrate some of the techniques. The core message here is that it's easy. Taking a some geographic information from your model and putting some markers on a google map with a bit of interactive functionality is really only a matter of a few lines of code. You can download a zip file of the solution here:

http://static.mikehadlow.com/Mike.GoogleMaps.zip

Here's the application. It's pretty simple, it just displays a map with a collection of locations. For fun I've made it show some of the places I've been lucky enough to live in during my fun-packed life :) Each location has a latitude, longitude, name and an image. You can click on the marker for a location and its name is displayed in a speech bubble (known as an 'info window' by Google) and it's image is displayed to the right of the map.

googleMapDemo

I build the map object graph in memory with a simple 'repository' (this is just a demo, your repository would normally talk to a database).

using Mike.GoogleMaps.Models;
namespace Mike.GoogleMaps.Repositories
{
    public class MapRepository
    {
        public Map GetById(int id)
        {
            return new Map
                   {
                       Name = "Places Mike has lived",
                       Zoom = 1,
                       LatLng = new LatLng { Latitude = 0.0, Longitude = 0.0, },
                       Locations =
                           {
                               new Location
                               {
                                   Name = "St. Julians, Sevenoaks, UK",
                                   LatLng = new LatLng { Latitude = 51.25136, Longitude = 0.21992 },
                                   Image = "st_julians.jpg"
                               },
                               new Location
                               {
                                   Name = "Kadasali, Gujerat, India",
                                   LatLng = new LatLng { Latitude = 21.235142, Longitude = 71.4462 },
                                   Image = "india.jpg"
                               },
                               // ...
                           }
                   };
        }
    }
}

Next we have a controller action that returns the object graph serialized as JSON:

public ActionResult Map()
{
    var mapRepository = new MapRepository();
    var map = mapRepository.GetById(1);
    return Json(map);
}

On the home controller's index view we have some simple HTML that has div placeholders for the content. One for the map name, another for the map itself and two more for the dynamic location name and image. Please forgive the inline CSS :(

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /><title>
 Mike's Google Maps Demo
</title><link href="Content/Site.css" rel="stylesheet" type="text/css" />
    <script src="../../Content/jquery-1.2.6.min.js" type="text/javascript"></script>
    <script src="http://www.google.com/jsapi?key="<key>" type="text/javascript"></script>
    <script src="../../Scripts/LocationsMap.js" type="text/javascript" ></script>
</head>
<body>
    <div class="page">
...
  <h2 id="mapName"></h2>
  <div id="map" style="width : 700px; height : 400px; margin : 0px; padding : 
   0px; float : left; margin-right:20px;"></div>
     
  <p id="info"></p>
  <img id="image" src="" />
  <div style="clear:both;"></div>
...
    </div>
</body>
</html>

Note that this is the HTML as rendered and is a combination of the master view and the home controller's index view. Also note the script references for the Google maps API, jQuery and the LocationMap.js script which controls the page.

jQuery makes writing Javascript a dream. I am a Javascript novice, but I found it blissfully easy to write this code. Here's the javascript which does all the work:

google.load("maps", "2");
// make a json request to get the map data from the Map action
$(function() {
    if (google.maps.BrowserIsCompatible()) {
        $.getJSON("/Home/Map", initialise);
    }
});
function initialise(mapData) {
    $("#mapName").text(mapData.Name);
    // create the map
    var map = new google.maps.Map2($("#map")[0]);
    map.addControl(new google.maps.SmallMapControl());
    map.addControl(new google.maps.MapTypeControl());
    map.setMapType(G_SATELLITE_MAP);
    var latlng = new google.maps.LatLng(mapData.LatLng.Latitude, mapData.LatLng.Longitude);
    var zoom = mapData.Zoom;
    map.setCenter(latlng, zoom);
    // set the marker for each location
    $.each(mapData.Locations, function(i, location) {
        setupLocationMarker(map, location);
    });
}
function setupLocationMarker(map, location) {
    // create a marker
    var latlng = new google.maps.LatLng(location.LatLng.Latitude, location.LatLng.Longitude);
    var marker = new google.maps.Marker(latlng);
    map.addOverlay(marker);
    // add a marker click event
    google.maps.Event.addListener(marker, "click", function(latlng) {
        
        // show the name and image on the page
        $("#info").text(location.Name);
        $("#image")[0].src = "../../Content/" + location.Image;
        
        // open the info window with the location name
        map.openInfoWindow(latlng, $("<p></p>").text(location.Name)[0]);
    });    
    
}

When the page loads we make an ajax request 'getJSON' to the HomeController's Map action listed above. When the call completes, it fires the callback function 'initialise'. This creates the map and binds it to the map div. We set the centre of the map to the map object's LatLng and the zoom level to the map's Zoom value.

Next we iterate (using jQuery's $.each()) through the locations and call setupLocationMarker for each one. This creates a new Marker object for each location and adds it to the map. It also adds a click event handler to each marker to set the name and image url, and popup the info window.

Simple and easy. I've been really impressed by the power of jQuery. It's very good news that Microsoft have adopted it. With the Firebug plugin for Firefox doing javascript development is a real pleasure.  As for the Google maps API, it is nicely conceived and has excellent documentation.

So what's stopping you? Get mapping!

16 comments:

Paul Lockwood said...

You rock Mike. Google Maps with MVC is next on my to-do list (we are refactoring one of our apps to MVC)

Rem said...

Your blog is very nice...
visit my blog asp.net example

Mike said...

Hi Mike, very pleased with this post, solution works fine from default 'localhost' but when I try and run it as a virtual directory e.g. http://localhost/MikeGoogleMaps/ the map doesn't display. I have tried every IIS trick I know, but no luck... any sugestions? (NB: the MVC routing and everything is working fine). Thanks in advance! Mike

Mike Hadlow said...

Hi Mike,

It should be easy enough to debug with the right tools. Make sure you've got Firebug installed (on Firefox) and step through the Javascript to see what's failing. Just a thought, but could it be the relative paths in the HTML for the external scripts?

Good luck!

Mike said...

Thanks Mike, thats done the trick :) The webresource.axd references were being calculated relative to the root ("/webresource*") changed this to accept a "rootPath" and now works like a charm. I have another problem now, nothing to do with your code I don't think, but maybe you can help? I'm rendering the maps into an MVC PartialView which I am loading into a specified DIV as part of an Ajax callback... all the surrounding stuff renders in the right places, but the map image itself is offset. It looks like the center point of the map image is positioned in the top left hand corner. I have checked out the DOM, CSS and scripts and the positioning of the image does seem to be left:-vr, top:-ve... but looks like that is coming from google. Any ideas on how to fix? Should the container div be setup in a particular way?? Thanks for your help with the other problem , much appreciated! Cheers Mike

Mike Hadlow said...

Hi Mike,

The center of the map is controlled by the initial latitude and longitude values of the Map class. If you make both of these zero, the center will be just off the coast of West Africa as you'd expect.

This was true when I did this work, but I haven't run this code recently and it may be that the Google maps API has changed in some way.

tim said...

Hi Mike
I know this is a very old example, but I wanted to have a play with Google maps and MVC so downloaded the zip, loaded it into VS2008 with both mvc1 and mvc2 installed and get the following errors:

Error 1 'System.Web.Mvc.HtmlHelper' does not contain a definition for 'ActionLink' and the best extension method overload 'System.Web.Mvc.Ajax.AjaxExtensions.ActionLink(System.Web.Mvc.AjaxHelper, string, string, System.Web.Mvc.Ajax.AjaxOptions)' has some invalid arguments
\Mike.GoogleMaps\Views\Shared\Site.Master 27 34 Mike.GoogleMaps

Error 2 Instance argument: cannot convert from 'System.Web.Mvc.HtmlHelper' to 'System.Web.Mvc.AjaxHelper'
\Mike.GoogleMaps\Views\Shared\Site.Master 27 34 Mike.GoogleMaps

My guess is that it has something to do with you using a pre release version of MVC - But I can't see what is going wrong here ( a noob with MVC, so be gentle )

Thanks Tim

tim said...

I found the answer...

while reseaching a totally unrelated problem.
on http://stackoverflow.com/questions/598289/using-google-maps-api-in-net-3-5-mvc-app
@maayank
said:
In order to fix it I had to replace the following line in the project's Web.Config:

with the following line:


It worked for me too.
hope this helps someone else.

Tim

tim said...

blogger ate the HTML code...
replace the following line in the project's Web.Config:

add namespace="Microsoft.Web.Mvc"

with the following line:

add namespace="System.Web.Mvc.Html"

Tim

Mike Hadlow said...

Thanks Tim!

jprice said...

Thank you! ! I am Japanese. Able that!

rice said...

Thank you! ! I am Japanese. Able that!

canim said...

Hi,
I know that the post is old.. but I have the same problem to take markers from database and put on the map.
I have copied (I fought the same_ to my page but it is not working. It is seems to like it is not going to the Home/Map action as I put break point there. I use MVC2 in VS2010 and I have jquery-1.3.2.js. I can not understand how it know that it should call $(function () {
if (google.maps.BrowserIsCompatible()) {
//$.getJSON("http://" + window.location.host + "/Home/Map", initialise);
$.getJSON("/Home/Map", initialise);
}
});
?????

Thanks, Iwona

P.S. Yours example is working on my PC but when I tried to addopt this code it is not working for me :/

Pablo Miguel said...

Hi! THANKS THANKS THANKS
This post has been very helpful for me! I have learnt a lot!
Just one thing: at first it didnt work for me, I had to change a line of code:

I changed
return Json(map)
for
return Json(map, JsonRequestBehavior.AllowGet);

Amit said...

HI mike...please tell me one thing i want to place multiple line in the openifowindow like i want to post string like this
HotelName
Avg Price/night
Distance

can u please tell me how to do that

hank1962 said...

Thank you! This is the best example that I could find. I am new to both the MVC and google maps. To get it to work, I had to change

return Json(map);

to

return Json(map,JsonRequestBehavior.AllowGet);

in the HomeController Map() method before the map would show up