YM4R/GM + GeoRuby+ Spatial Adapter Demo

This tutorial will show how the YM4R/GM plugin can be used to easily manipulate Google Maps from your Rails application, via the Google Map API v2. The map will be updated through AJAX (using the RJS functionality) without writing a single line of JavaScript. I will also show the integration between GeoRuby, the Spatial Adapter and YM4R/GM. This tutorial can use both MySQL and PostGIS as the target DB.

Setting up the environment

The first thing is to create a Rails project:

rails zipcode

Then you need to install the libraries we are going to need in this tutorial. First install GeoRuby:

gem install georuby

Then we need to install the YM4R/GM and the Spatial Adapter plugins. First the YM4R/GM plugin. In the root of your project type:

ruby script/plugin install svn://rubyforge.org/var/svn/ym4r/Plugins/GM/trunk/ym4r_gm

You will need to configure the Google Maps API key to use in the file RAILS_ROOT/config/gmaps_api_key.yml. If you use the WEBRick default settings, the key for the development and test environments should work all right (host localhost:3000). If you have other settings, visit this page and apply for a key for your host.

The last step is the installation of the Spatial Adapter for Rails. In the root of your project, type:

ruby script/plugin install svn://rubyforge.org/var/svn/georuby/SpatialAdapter/trunk/spatial_adapter

The only thing left is to configure the database to be used by Rails in config/database.yml. And you should be ready for what follows.

Importing the data in the database

First, get the data. I am going to use the Census 2000 5-Digit ZIP Code Tabulation Areas (ZCTAs) in Shapefile (.shp) format. To keep things simple, I will only use the data for one state, Alabama, but feel free to pick any other. If you want, you can visualize the file in about any open source GIS viewer, for example QGIS. To import this file into the database, we are going to use a generic import script called shp2sql.rb, which can be found in the tools directory of the GeoRuby gem. You can take all this directory and put it anywhere but be sure not to take only the script. Copy the zt01_d00.shp, zt01_d00.shx and zt01_d00.dbf in the same directory as the script. Then, before running the script, you need to edit the db.yml file in order to change your database connection information in it. Then you can run the script like this:

ruby shp2sql.rb zt01_d00.shp

Inside your database, you will get a zt01_d00 table with, among others, 3 interesting columns:

This is to show how to use the script in a generic way. I think it can be customized easily to enable only some columns, change the name of the table or the type of the geometry but that will do for us.

The model

Generate a model. I will call it Alabama :

ruby script/generate model Alabama

I then need to tie it explicitly to the zt01_d00 table, for rails to be able to make the link between the 2:

class Alabama < ActiveRecord::Base
  set_table_name "zt01_d00"
end

And that’s it for the model. No need to do anything special for the geometric column: The Spatial Adapter will take care of it.

Initialization of the map

Create a Zip controller with an index action:

ruby script\generate controller Zip index

In this action we are going to take care of the initialization of a Google map. Here is the code for the whole Zip controller, followed by an explanation of what is happening:

class ZipController < ApplicationController
	
  def index
    @map = GMap.new("map_div")

    @map.control_init(:large_map => true, :map_type => true)

    @map.center_zoom_init([33, -87],6)
  end

end

The YM4R/GM being loaded automatically by Rails, there is nothing to load explicitly from our controller. In the index method, the first line initializes the Google map. The argument in the GMap constructor is the id of the DIV element that will contain the map, in this case “map_div”. This line is followed by calls to convenience methods, written to ease frequent initialization needs:

We then need to make a template for the index action:

<!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" <%= GMap::VML_NAMESPACE %> >
<head>
  <title>Alabama Zipcode Areas</title>
  <%= javascript_include_tag :defaults %>

  <%= stylesheet_link_tag 'zipcode' %>
  <%= GMap.header %>
  <%= @map.to_html %>
</head>
	
<body>
  <div id="main">

    <%= form_remote_tag(:url => {:action => :find}) %>
      <div id="form" >
	<label for="zip">Zip:</label>

	<%= text_field_tag "zip" %>
	<%= submit_tag "Find" %>
      </div>
    <%= end_form_tag %>

    <%= @map.div %>
    <div id="notice" style="display:none;"></div>
  </div>
  </body>

</html>

The GMap::VML_NAMESPACE in the html element is to make sure the display of polylines and polygons (via VML) works correctly under Internet Explorer. We then include the default JavaScript libraries, since we are going to need Prototype to perform Ajax requests and Scriptaculous for some effects on the error message. Next is the inclusion of the stylesheet. Download it and put it in your public/stylesheet folder. Among other things, inside this stylesheet is set the dimension of the Google map. Then the header: It includes the JavaScript files from Google needed to use the Maps API, as well as a stylesheet declaration for the display of VML elements under IE. Finally, the initialization with default parameters of the Map itself.

Inside the body, we have first a standard form. Note the use of form_remote_tag to make an Ajax request to a find controller (that we will build very soon). Then the DIV that will contain the map. Finally, a place to display error messages.

We are now ready to test the initialization of the map. Start your WEBRick server and direct your browser to localhost:3000/zip. You should see a map centered on Alabama, like this:

Ajax request to update the map

Then we need an action to respond to requests from the form. When such a request is performed we will either respond by sending the Polygon data and centering the map on it or, if the requested zip code is not found, we will display an error message. Here is the find method of the Zip controller:

def find

  zcta = Alabama.find(:first,:conditions => ["name = ?", @params[:zip]])

  if zcta.nil?
    @message = "#{@params[:zip]} not in Alabama"
  else

    poly = zcta.the_geom[0]
    envelope = poly.envelope

	
    @id = zcta.id
    @map = Variable.new("map")

    @polygon = GPolygon.from_georuby(poly,"#000000",0,0.0,"#ff0000",0.6)

    @center = GLatLng.from_georuby(envelope.center)
    @zoom = @map.get_bounds_zoom_level(GLatLngBounds.from_georuby(envelope))

  end
end

And here is an explanation of what we do. The first line is to find the zipcode row corresponding to the zip requested by the user. If found, we get the geometry (actually the first polygon of the multi-polygon geometry, hence the [0]): We copy the geometry of the row in a local variable. We do this to prevent multiple conversions from the string (returned from the database) to a GeoRuby Geometry object, since ActiveRecord performs this conversion everytime the attribute is accessed. This is OK for simple types (like dates or integers) but, since polygons can be quite big, it would be inefficient if it were done every time. Then we get the envelope of the geometry.

Once this is done we get or create all the data we need to update the view. First, the id of the zipcode tabulation area. Then we create a Variable object for the map. The "map" string passed as argument to the constructor is the name of the global JavaScript variable referencing the Google map created at initialization. More simply: We have lost the reference to the map from the Ruby code but in JavaScript, the map is still globally accessible with the map variable. We use the @map = Variable.new("map") code to indicate that all the methods called on the @map variable in the RJS code must be called on the map variable in the JavaScript code. If it is cloudy, it will become clearer very soon when we look at the RJS code to update the map.

Next, we construct a GPolygon directly from the polygon. Note that at this stage, since the GPolygon class is not documented by Google, it is not very clear how we could have holes in the polygon. This means that only the outer ring will be used to create GPolygons. Then we get the center of the bounding box of the polygon and set the @zoom variable to an expression that when evaluated on the browser will give the best zoom value for the bounding box of the geometry, so we don’t have to indicate an arbitrary zoom at this stage.

Update of the map through RJS

Next the RJS update. RJS works by sending JavaScript code to the browser, where it is then interpreted. We can send arbitrary code and this is what we are going to do in order to update the map! Here is the content of the find.rjs code:

unless @message

  page << @map.clear_overlays
  page << @map.add_overlay(@polygon)

  page << @map.set_center(@center,@zoom)
  notice = georss_ge(@id)

else
  notice = @message
end
	
page.hide notice

page.replace_html notice‘, notice
page.visual_effect :appear, notice‘, :duration => 0.5

When there is no error, we first clear all the overlays from the map (this includes markers, polylines and polygons). Note how this is done: We use the page << method, which records any JavaScript code sent to it. The @map.clear_overlays code returns the following JavaScript code: map.clearOverlays();, where map is the string passed as argument to the Variable constructor (remember what I wrote before?). The method clear_overlays does not appear anywhere in the YM4R library: The missing_method technique is used to transform the unknown methods (such as clear_overlays) to a javascriptified version of it. This lets me implement most of the functionalities of the Google Maps API with very minimal effort. On the other hand, if you make a typo when calling the method, the error won’t be caught in the Ruby code. But I believe the trade off is worth it.

Anyway, next we set the center and the zoom (with the zoom not yet known at this stage, only an expression that gives its value when evaluated on the browser) and add the polygon. Then we generate the HTML code (see the helper code below) that will display the GeoRSS and KML icons, as well as links to the GeoRSS and KML files for the current zipcode tabulation area. And we make it appear.

module ZipHelper
  def georss_ge(id)
    link_to(image_tag("georss.gif"),:controller => zip‘, :action => georss‘,:id => id) +

      " " +
      link_to(image_tag("ge.gif"),:controller => zip‘, :action => kml‘,:id => id)

  end
end

Here is the archive with the RSS and Gogole Earth icons. Put the content in the public/images folder of the Rails app.

Generating the GeoRSS and KML

This is a new part of the tutorial added on January 2007. It shows how to generate GeoRSS and KML geometry strings directly from GeoRuby geometries and how to integrate them with the XML Builder of Rails.

We are going to start with GeoRSS. The example is a bit contrived since there will be only one item but it is enough to show the feature. Here is the georss action in the Zip controller:

def georss
  zcta = Alabama.find(params[:id])

  @id = zcta.id
  @polygon = zcta.the_geom[0]

  @envelope = @polygon.envelope
  @name = zcta.name

end

Basically, we get all the data we need to generate the RSS feed and then we render it. Here is the RXML file that does that:

xml.rss(:version => "2.0", "xmlns:georss" => GEORSS_NS) do

  xml.channel do
    xml.title Alabama Zipcode
    xml.link(url_for(:action => index‘,:only_path => false))

    xml.description Alabama Zipcode tutorial
    xml << @envelope.as_georss
    xml.item do

      xml.title @name
      xml.description "Zipcode Tabulation Area : #{@name}"
      xml << @polygon.as_georss

    end
  end
end

It looks exactly like any other RXML you have ever seen. Note the namespace declaration: it is so the GeoRSS geometry is recognized by parsers. GeoRuby will by default output the GeoRSS elements with the georss prefix, the GML elements with the gml prefix and the W3CGeo elements with the geo prefix, as they are described in the GeoRSS documentation (but of course, it is not a mandatory requirement). These defaults can be changed by passing :georss_ns, :gml_ns and :w3cgeo_ns options to the as_georss method. In any case, the prefix must correspond to the namespace declaration at the top. Since by default, the GeoRSS simple dialect is used for the GeoRSS output, in our case, only the GeoRSS namespace needs to be declared. Helpers that give you the URI for a namespace are available: Here GEORSS_NS is used. Finally, the geometry output per se is done through the xml << ..., in order not to escape the < and > of the string that follows.

For KML, here is the the content of the kml method in the Zip controller:

def kml
  zcta = Alabama.find(params[:id])

  @polygon = zcta.the_geom[0]
  @name = zcta.name

  render :content_type => "application/vnd.google-earth.kml+xml"
end

Nothing special, except we set the MIME output to application/vnd.google-earth.kml+xml so it can be picked up by Google Earth. Here is the content of the kml.rxml file:

xml.kml("xmlns" => KML_NS) do
  xml.tag! "Document" do

    xml.tag! "Style", :id => "myStyle" do
      xml.tag! "PolyStyle" do

        xml.color "ffff0000" #format is aabbggrr
        xml.outline 0
      end

    end
    xml.tag! "Placemark" do
      xml.description "Zipcode Tabulation Area : #{@name}"

      xml.name @name
      xml.styleUrl "#myStyle"
      xml << @polygon.as_kml(:altitude => 2000, :altitude_mode => :relativeToGround)

    end
  end
end

Again not very spectacular… However note the options to the as_kml method: :altitude will give the geometry an altitude when none is initially defined for it. The :altitude_mode indicates how the z component of the geometry of the string will be interpreted by Google Earth. Finally, here is a screenshot of the KML output: The polygon floats in the air 2km above ground.

KML

The finished app

The app is complete. Therefore we are going to test it. Again get your browser to localhost:3000/zip/index. If you enter a zipcode, for example 35221, the map will center somewhere near Birmingham and display a translucent polygon.

35221

Conclusion

As can be seen from this demo application, it is not very difficult to add a Google map to a Rails application. Of course, for advanced applications, you will not escape writing JavaScript, but it can at least get you started.