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:
- id: The table key used by Rails
- name: The zipcode number
- the_geom: The zipcode area, which comes as a multi-polygon, since the Shapefile polygons are in fact multi-polygons. In our case, we know the ZCAT have only one contour so when reading data from the database, we will take only the first polygon of the multi-polygon.
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:
- Addition of controls: Here the map is created with a large map control (the zoom slider + panning cross) and a map type control (to choose between normal, satellite and hybrid types of maps).
- Center and zoom: The map will be initialized centered on Alabama with a zoom level of 6.
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.

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.

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.