College Park Crime Reports

Implementing a Google Maps Instance

It's all fine to read about setting up Rails, but the whole point of this application is to employ Google Maps to show where crimes have been reported. How do I get the map on the page?

Quick and Dirty

The Google Maps API specifies that embedded elements should be instantiated by javascript methods called in the BODY tag's onload and onunload event attributes, like so:

<body onload="load()" onunload="GUnload()">

It turns out, though, that adding something to the body tag in a Rails app isn't as simple as it is when you're coding flat HTML. The RHTML file that Rails generated for the Show View is only a fragment:

$ vi app/views/crime_reports/show.rhtml
<% for column in CrimeReport.content_columns %>
<p>
  <b><%= column.human_name %>:</b> <%=h @crime_report.send(column.name) %>
</p>
<% end %>

<%= link_to 'Edit', :action => 'edit', :id => @crime_report %> |
<%= link_to 'Back', :action => 'list' %>

So Rails takes care of the structure of the page with separate files, called layouts. When I was first writing this app, and this little article here, I thought I'd put off digging into these layouts and break the rules by putting the javascript calls right in the view's fragment. Later on I'd clean this file up and put things where they belong. Well, that was a BAD IDEA, because it turns out that although Firefox is cool with placing script tags with JS code anywhere you want, IE definitely isn't. Here's what I saw when I tried to view the page with my spiffy embedded Google map on it in IE 7.0.

screenshot of IE's extremely unhelpful error message

"Can't open the site" -- not an especially helpful error message. Fortunately, I found the solution quickly: my javascript must be enclosed in functions either linked or included within the head element and called in the opening body tag. I owe a massive shout-out to Ryan Grant for explaining the problem in an article on his web site.

First, thinking of the Google map as an object I will create and then interact with, what information do I need to send to it? I want to create an instance of the GMap2 class, create a marker object, and place it on the map as an overlay. To create the map, I need to specify a set of coordinates for the center and a zoom factor. Here's the critical code:

map.setCenter(new GLatLng(lat, lng), zoom);

For now, I will plan to center the map on the coordinates 38.98837 latitude by -76.93511 longitude: more or less the center of the area. That only leaves the zoom function. By playing around with maps.google.com and the examples in the API documentation, I find that 13 is an adequate zoom level--it displays most of the area in one map.

Here is the code that makes the marker and adds it as an overlay:

var marker = new GMarker(new GLatLng(lat, lng), icon);
map.addOverlay(marker);

The icon variable can be set by some boilerplate code. That leaves me with setting the latitude and longitude. As mentioned, I can't just insert javascript code into the View RHTML: the functions need to be in the head element, and the initiation must be in the body tag. So here's what I do. I open the layout file, automatically generated earlier with the rest of the View files, and I start adding in javascript.

$ vi app/views/layouts/crime_reports.rhtml
<!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>
  <title>CrimeReports: <%= controller.action_name %></title>
  <%= stylesheet_link_tag 'scaffold' %>
  <script src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=aRe411yLONGString0fL3tt3r5AndNumb3rS"
  type="text/javascript"></script>
    <script type="text/javascript">
//<![CDATA[
  function gLoad() {
  if (GBrowserIsCompatible()) {
    var map = new GMap2(document.getElementById("map"));
    var icon = new GIcon();
    icon.image = "http://labs.google.com/ridefinder/images/mm_20_red.png";
    icon.iconSize = new GSize(12, 20);
    icon.iconAnchor = new GPoint(6, 20);
    icon.infoWindowAnchor = new GPoint(5, 1);
    map.setCenter(new GLatLng(38.988369, -76.93511), 13);
    
    var lat = document.getElementById("lat").firstChild.nodeValue;
    var lng = document.getElementById("long").firstChild.nodeValue;
    var marker = new GMarker(new GLatLng(lat, lng), icon);
    map.addOverlay(marker);
  }
}
  //]]>
  </script>
</head>
<body onload="gLoad()" onunload="GUnload()">
<p style="color: green"><%= flash[:notice] %></p>

<%= yield  %>

</body>
</html>

I need to enable the Google map. Following the API documentation, I'll add the script tag that contacts Google: http://maps.google.com/maps (it's the script with the long ungainly Maps API key at the end of it). Then I implement the gLoad function to expect to find the latitude and longitude values wrapped in identifying elements. So now I'd better add some code to the show.rhtml view file that will wrap the lat and long values supplied by Rails into these elements. I'll use span elements for it:

<% for column in CrimeReport.content_columns -%>
<p>
  <b><%= column.human_name %>:</b> <span id="<%=column.name %>"><%=h @crime_report.send(column.name) %></span>
</p>
<% end -%>
<%= link_to 'Edit', :action => 'edit', :id => @crime_report %> |
<%= link_to 'Back', :action => 'list' %>

OK, now the final piece: a place in the HTML for the map to appear. Notice how I passed the GMap2 constructor a reference to an element with a "map" ID? That's a requirement: there needs to be some element on the page that the GMap object can appear within. In my case (and I think in most cases, too) it will be a DIV element. I need to restructure my HTML somewhat, to allow for a map to appear at the right of the page. Here's what I add right at the top and bottom of show.rhtml:

<div id="container">
<div id="map" 
     style="width:300px; height:250px; position:absolute; right:10px; "></div>
<div id="data" style="margin-right:320px;">

... existing RHTML ...

</div>
</div>

Now I point my browser again at the show view, and I see my map.

screenshot of Show View page

Cleaning Up a Bit

After this proof of concept, I tidy up by refactoring my code and making a few common sense additions. For one thing, it makes sense to break out the javascript code creating a new icon into its own function:

function generateIcon() {
    var icon = new GIcon();
    icon.image = "http://labs.google.com/ridefinder/images/mm_20_red.png";
    icon.iconSize = new GSize(12, 20);
    icon.iconAnchor = new GPoint(6, 20);
    icon.infoWindowAnchor = new GPoint(5, 1);
    return icon;
}

After adding some embellishments to the GMap2 object, like zoom controls, panning buttons, and map type buttons, I realize that I'd like these to be on every map I create. So the map generation goes into its own function too:

function generateMap(id) {
    var map = new GMap2(document.getElementById(id));
    map.addControl(new GSmallMapControl());
    map.addControl(new GMapTypeControl());
    return map;
}

It also occurs to me that if I'm just showing one marker why not center the map on the marker's location?

Finally, I clean up the file by moving all the javascript into a separate file: public/javascripts/google_maps.js. I can include this file with an RHTML helper tag inside app/views/layouts/crime_reports.rthml.

<!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>
  <title>CrimeReports: <%= controller.action_name %></title>
  <%= stylesheet_link_tag 'scaffold' %>
  <script src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=aRe411yLONGString0fL3tt3r5AndNumb3rS"
  type="text/javascript"></script>
  <%= javascript_include_tag 'google_maps.js' %>
  <%= javascript_include_tag 'cr_effects.js' %>
</head>
<body onload="gLoad()"onunload="GUnload()">

<p style="color: green"><%= flash[:notice] %></p>

<%= yield  %>

</body>
</html>

There we are. By the magic of narrative writing, I have made this all seem deceptively obvious and easy. In fact, it took hours of trial-and-error hacking, not to mention the Why-Didn't-I-Check-IE-Earlier near-disaster. One lovely thing about web services like Google Maps' is that you can make incremental progress that is reflected in your web site's steadily looking better and better.