College Park Crime Reports

Extending Google Maps Use to the List View

It's nice to be able to see the marker for one crime report, but wouldn't it be even better to have a big map on the List View page so I could see where the reports cluster? Of course it would.

Displaying multiple markers

The first step is figuring out how to put all the marker overlays on the map. To enable the map in the Show View, I called javascript in the layout file's body tag. That javascript expects a single record's coordinates to be available in the document. Now I have a lot of reports. I need to get their latitude and longitude information to the code that sets up the map. I can have ruby write out the gLoad function depending on the value of controller.action_name, which is 'show' for the Show View and 'list' for the List view. That way, gLoad could do the right thing with gathering values from the document.

But here's another wrinkle: on the Show View page, I don't want to show latitude and longitude variables on the List View page. The page is too cluttered with all that information. It would make more sense to place the critical data right in the javascript code. It will make my code messier -- I hate using use one programming language to write out code in another programming language -- but for now it seems like the simplest way to a solution. Here's how I think a function to load a bunch of markers should look:

function load_list() {
if (GBrowserIsCompatible()) {
  var map = generateMap("map");
  var icon = generateIcon();
  
  icon.infoWindowAnchor = new GPoint(5, 1);
  map.setCenter(new GLatLng(38.988369, -76.93511), 13);

  // (Use some Ruby to write out declarations of GMarkers and add them as overlays)
  
}
}

Putting the Ruby loop inside the load_list function to write out javascript code would be simple, so of course it won't work. I've "cleaned up" my Javascript by placing it in an external file, and I can't put Ruby in an external javascript file. Dang. I have to keep my Ruby-generated output to rhtml files. For view generation, that means the layout file app/views/layouts/crime_reports.rthml. This isn't the worst news, though, because I was going to have to meddle with the layout file anyway.

Both the List and Show views use the app/views/layouts/crime_reports.rthml layout file to build the wrapper of the HTML pages. So the opening body tag is in one file: there's only one place to call the Javascript with the onload event, only one function, gLoad, that will be called each time.

So the gLoad function is the place to determine what javascript action will take place on this page, and it's also the best place to declare javascript variables, like the ones I'll need in my list view. Here is the change I'll make to app/views/layouts/crime_reports.rthml. To help distinguish the ruby code from the javascript, I've colored the ruby a dark red.

<script type="text/javascript">
//<![CDATA[

// Body of gLoad function is defined by server-side script 
function gLoad() {
<% if (controller.action_name=='list') -%>
  var reports = new Array();
  var report;
  <% count = 0
  @crime_reports.each do |report|  %>
  report = new Array();
  report["lat"] = <%= report.lat %>;
  report["lng"] = <%= report.long %>;
  reports[<%= count %>] = report;
  <% count +=1
  end %>
  load_list(reports);
<% else -%>
    load_one();
<% end -%>
}  
 
  //]]>
  </script>

Here's how I implement the load_list function.

function load_list(reports) {
if (GBrowserIsCompatible()) {
  var map = generateMap("map");
  var icon = generateIcon();
  map.setCenter(new GLatLng(38.988369, -76.93511), 13);
  
  var r;
  for (r in reports) {
    var marker = new GMarker(new GLatLng(reports[r]["lat"], 
                                         reports[r]["lng"]),
                             icon);
    map.addOverlay(marker);
  }
}
}

Finally, the current contents of the gLoad function move to a new function named load_one inside my consolidated Javascript file public/javascripts/google_maps.js:

function load_one() { 
if (GBrowserIsCompatible()) {
  var map = generateMap("map");
  var icon = generateIcon();
  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);
}
}

I would like to add automatic centering to the list view. If I average the latitude values and the longitude values, that should give me a good center point. I'll add an averaging function into my google_maps.js file:

$ vi public/javascripts/google_maps.js
DEFAULT_LAT = 38.988369;
DEFAULT_LNG = -76.93511;

function find_center(reports) {
  var lat = 0.0;
  var lng = 0.0;
  
  var length = reports.length;
  if (length == 0) {
	return new GLatLng(DEFAULT_LAT, DEFAULT_LNG);
  }
  for (i=0; i<length; i++) {
    lat += reports[i]["lat"];
    lng += reports[i]["lng"];
  }
  lat = lat/length;
  lng = lng/length;
  return new GLatLng(lat,lng);
}

... 

function load_list(reports) {
if (GBrowserIsCompatible()) {
  var map = generateMap("map");
  var icon = generateIcon();
  var center_latlng = find_center(reports);
  map.setCenter(center_latlng, 13);
  
  var r;
  for (r in reports) {
    var marker = new GMarker(new GLatLng(reports[r]["lat"], 
                                         reports[r]["lng"]),
                             icon);
    map.addOverlay(marker);
  }
}
}

Finally, I'd like to add the zoom controls and map type buttons ("Map", "Satellite", "Hybrid") that Google puts on its own maps. I can do that by adding these instructions:

Success. With some sample data added in the form, here's how my new List View map looks:

screenshot of List View map

Opening a Window onto More Information

Google Map markers come with a function called openInfoWindowHtml that produces that "speech bubble" over the marker that you may be familiar with from using Google Maps. I'd like to have one of these windows open when I click on a marker on the list page. It could display a description of the report it marks and a link to the report's Show View. To accomplish this, one should be able to add an event listener to each marker as it's created. The listener should intercept a click event and then fire a function that displays some HTML over that marker.

GEvent.addListener(marker, "click", function() {
    marker.openInfoWindowHtml("Some HTML");
  });
Easier said than done, it turned out. I tried adding the code above into my load_list function:

for (r in reports) {
  var marker = new GMarker(new GLatLng(reports[r]["lat"], 
                                       reports[r]["lng"]),
                           icon);
  GEvent.addListener(marker, "click", function() {
    marker.openInfoWindowHtml("Some HTML for testing");
  });
  map.addOverlay(marker);
}

But I got some strange behavior. Whichever marker I clicked in the resulting map, the same marker reacted: the marker that was added last. In other words, when I clicked on the first or second marker, the map would scroll to center the last marker and then open up the window over that marker. I suspect that in the loop, the variable marker was not being reset each time, even though I was using the GMarker constructor. Somehow, old references were sticking around.

I fancy myself to be pretty adept with object-oriented programming, and I rarely get flummoxed by these kinds of issues with references to objects, but this problem was a real flummoxer. So, I tried to imagine how I could eliminate the lingering reference to the marker object. One way would be to use javascript to enforce a tighter scope right around the constructor and the object's use. Moving the marker creation to a new function, with its built-in scope, might be the way to go: I'd essentially be squeezing a function declaration into the for-loop.

I'd have to restructure my code to move the marker creation into its own function, but I began to feel like this would be the right thing to do anyway. I create markers in a number of places; why not put that code in one place anyway?

As with the function I wrote to handle icon creation, I wrote my createMarker function to expect an object holding attribute values, a point on the map (a GLatLng object), an icon, and a message:

function createMarker(marker_info) {
  var marker = new GMarker(marker_info.point, marker_info.icon);
  if (marker_info.msg && marker_info.msg != '') {
	  GEvent.addListener(marker, "click", function() {
      marker.openInfoWindowHtml(marker_info.msg);
    });
  }
  return marker;
}

Why did I put in the conditional code if (marker_info.msg && marker_info.msg != '')? I confess I'm already thinking ahead to using this new function in other contexts in which I will want a marker but not an info window. In those cases, there will be no msg declared for the marker.

These attributes need to be defined in load_list. Here's the for-loop in that function now. Notice that I'm passing the counter variable r in the msg variable so I can be sure that different marker's messages are appearing when I click different messages.

  for (r in reports) {
    
    var marker_info = new Object();
    marker_info.icon = icon;
    marker_info.point = new GLatLng(reports[r]["lat"], reports[r]["lng"]);
    marker_info.msg = "HTML for marker number " + r;
    map.addOverlay(createMarker(marker_info));
  }

It works! Now I can open the marker I want to. Here's how it looks:

screenshot of List View map with correct marker displaying HTML window

This seems like a good place to comment on the advantages to using an object to contain the values you want to send to a function, as I did with the marker_info object, above. First, as I begin passing more information to the function, I don't want to have to remember the order in which the parameters are expected. Javascript objects will happily allow you set the value of any "property" you like, and they'll return them when accessed.

Another advantage to an object is that in javascript you can test for the value of a property that may not exist in the following manner:

if (marker_info.msg)

If I've never defined a value for this parameter, javascript will return false. If I used a bare variable that hadn't been defined, e.g. if (msg), javascript would register an error, and my script would stop dead. So this use of the object gets us around another ugly wrinkle of javascript's. I'm indebted to ESQ Software's page on trapping undefined javascript variables for this method.

How about some more interesting HTML for the info window? I'll put the crime report's id and description values into the marker_info.msg variable, like this:

  marker_info.msg = "Report #" + reports[r]["id"] + ": " +
  reports[r]["description"];

Now I have to make sure I'm putting such values into the report arrays in my gLoad function in app/views/crime_reports/layouts/crime_report.rhtml.

  ...
  report["lat"] = <%= report.lat %>;
  report["lng"] = <%= report.long %>;
  report["id"] = <%= report.id %>;
  report["description"] = "<%= h report.description %>";
  ...

Some users might want to use the map as the listing: they might want to identify a report by its location and then get the details for that report. I will link the detail view for the report to the window's HTML:

  marker_info.msg = "Report #" + reports[r]["id"] + ": " +
  reports[r]["description"] + "
" + reports[r]["detail_link"];

How do I get the detail link? With an awkward but functional hack, in app/views/crime_reports/layouts/crime_report.rhtml.

...
report["detail_link"] = '<%= link_to "Show Details", {:action => "show", :id => report} %>';
...

I'm using a helper tag that's meant to produce a human-viewable output but instead squeezing that output into a javascript command. Like I said, it's a hack. But I wouldn't want to try constructing links in the javascript. Rails manages the creation of URLs for you so that you don't have to hard-code any paths into your app.

Let's see how it works.

screenshot of List View map with info window displaying ID, description, and link

A (hopefully useful) discussion about loose coupling and the separation of concerns

Working with Javascript and Ruby in such close proximity to each other leads me to reflect on the way I've divided functionality into RHTML and Javascript. I really like Javascript's promise of moving evaluation and computation onto the client side. I really hate Javascript the language, however, with its terrible debugging support in browsers. Why wouldn't I do everything in RHTML, including computing the center point of the locations of the reports I want to display?

Well, that sounds like I'd be tightly coupling the server-side app with the javascript. I am very unlikely to change from javascript to some other client-side technology any time soon. However, I might decide I want to use google_maps.js in a PHP or Java project. It's simple to establish the interface to the javascript functions this way: "Have your server-side code determine which javascript function will execute and output a data structure, which is organized in a given way, for the function to use." This becomes the interface between client-side and server-side. All you have to do in a new server-side implementation is provide the execution determination and necessary data structure.

The list of reports seems like the smallest, most basic set of data possible. It all comes out of the model. This quantitatively suggests the loosest coupling.

Also, if I decide to enhance the features of the map display, I only have to change load_more() in some way, I shouldn't have to update the server-side stuff. This leads to another conclusion: the proper separation of concerns is to put manipulation of reports data for mapping solidly on the client-side.)

Of course, the code itself could be more elegant. Rails implements a helper class strategy: you can create classes that encapsulate a lot of ugly HTML and javascript code production. I could (probably should) build one of these for my layout file, to reduce the clutter and confusion of alternating languages. I'll put that on my to-do list.