You are on page 1of 36

This

t
was utorial (v
down ersio
http:
//rai loaded fr n 17.9.07
ls.no o )
mad m:
TUTORIAL -labs
.com

Rails GIS Hacks Berlin | Germany

Shoaib Burq | Kashif Rasul


Monday, September 17, 2007
13:30 – 17:00
Saal Maritim B

Design & title photo by Aleks Herzog


www.nomad-graphics.com

l a b s
info@nomad-labs.com | www.nomad-labs.com
l a b s

Content
01 Using Geocoders . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3

GeoKit http://geokit.rubyforge.org/ by Bill Eisenhauer


and Andre Lewis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3

Graticule and acts_as_geocodable http://graticule.rubyforge.org/


by Brandon Keepers and Daniel Morrison . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8

02 Location data in ActiveRecord (PostGIS/PostgreSQL) ......................... 12

Prerequisites ................................................................................ 12

Some background to the Geospatial Domain ......................................... 13

Setting up our GeoRails Application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

CRUD Location .............................................................................. 22

03 Supporting New Content Types .................................................... 28

KML (Google Earth) ........................................................................ 28

GeoRSS ....................................................................................... 31

Rails GIS Hacks Content | 2


01 l a b s

Using Geocoders
GeoKit http://geokit.rubyforge.org/ by Bill Eisenhauer
and Andre Lewis

We create our GeoKit demo rails (ver. 1.2.3) app and inside it install the GeoKit plugin:

$ rails geokit
$ cd geokit/
$ ruby script/plugin install svn://rubyforge.org/var/svn/geokit/trunk

or, to install it as an external

$ ruby script/plugin install -x svn://rubyforge.org/var/svn/geokit/trunk

Finally we create our databases geokit_development, geokit_test, and geokit_production and


configure it in our config/database.yml file.

Geocoding

The geocoder uses geocoding webservices provided by Google, Yahoo, Geocoder.us or


Geocoder.ca, and to use it, you have to add the respective API key of the service you will use
into the config/envirnoment.rb file. So for example to use the Yahoo service, go to
http://search.yahooapis.com/webservices/register_application and log in with your Yahoo
user account and get an id and add it:

# This is your yahoo application key for the Yahoo Geocoder.


# See http://developer.yahoo.com/faq/index.html#appid
# and http://developer.yahoo.com/maps/rest/V1/geocode.html
GeoKit::Geocoders::yahoo =
'pPDCgjnV34FJ3TxysU9K.FpFYQ3A_QYJ4VrAJQuyFcFv91Hf0r3PU5tr3SYBhMvOoM__'

For the Google Map API, you need to register by going to:
http://www.google.com/apis/maps/signup.html and get a API key for the http://localhost:3000/
url and also add it:

# This is your Google Maps geocoder key.


# See http://www.google.com/apis/maps/signup.html
# and http://www.google.com/apis/maps/documentation/#Geocoding_Examples
GeoKit::Geocoders::google =
'ABQIAAAAwWqh7sPpuhNCdGZ0pieShBTJQa0g3IQ9GZqIMmInSLzwtGDKaBQOJH6Tw4jIlz7bMDU
6qtLF_9TSHQ'

Rails GIS Hacks Using Geocoders | 3


l a b s

And finally add the provider order, so here we use Yahoo before Google.

GeoKit::Geocoders::provider_order = [:yahoo, :google]

and we test it out using the console:

$ ruby script/console
Loading development environment.
> > include GeoKit::Geocoders
=> Object
> > home = MultiGeocoder.geocode("Torstrasse 104, 10119, Berlin, Germany")
=> #<GeoKit::GeoLoc:0x35de820 @lat=52.530051, @state="Germany",
@street_address="Torstrasse 104", @country_code="DE", @provider="yahoo",
@precision="address", @zip=nil, @lng=13.403495, @city="10119 Mitte",
@success=true>
> > home.lat
=> 52.530051
> > home.lng
=> 13.403495

Distance, headings, endpoints, and midpoint example calculations

GeoKit provides in-memory distance calculations for either the LatLng class (GeoKit::LatLng) or
the GeoLoc class. So for example in the console:

> > office = MultiGeocoder.geocode("Lepsiusstrasse 70, Steglitz, Berlin,


Germany")
=> #<GeoKit::GeoLoc:0x341e5f8 @lat=52.460126, @state="Germany",
@street_address="Lepsiusstrasse 70", @country_code="DE", @provider="yahoo",
@precision="address", @zip=nil, @lng=13.316571, @city="12163 Steglitz",
@success=true>
> > office.distance_to(home, :units => :kms)
=> 9.75995820357575
> > heading = home.heading_to(office) # result is in degrees, 0 is north
=> 217.15430202928
> > endpoint = home.endpoint(90, 2) # given a heading (east) and distance
=> #<GeoKit::LatLng:0x33f6878 @lat=52.5300414818178, @lng=13.4510238774836>
> > midpoint = home.midpoint_to(office)
=> #<GeoKit::LatLng:0x33f08b0 @lat=52.4950964615994, @lng=13.3599984433113>

Auto geocoding of location model

The plugin provides distance calculations between two points for both spherical or flat
environments. If you only need the distance calculation services then add the Mappable module

Rails GIS Hacks Using Geocoders | 4


l a b s

into your class making sure that your class has a lat and lng attribute. However another
application might be to automatically geocode a model itself upon creation.

So lets first add an address, lat and lng attribute in a model called Location

$ ruby script/generate model Location

by adding the migrations in db/migrate/001_create_locations.rb:

class C r e a t e L o c a t i o n s < ActiveRecord::Migration


def self.up
create_table :locations do |t|
t.column :address, :string, :limit => 100
t.column :lat, :decimal, :precision => 15, :scale => 10
t.column :lng, :decimal, :precision => 15, :scale => 10
end
end

def self.down
drop_table :locations
end
end

Now we run our migration to create the locations table in our database:

$ rake db:migrate

To tell this model to auto-geocode simply add the following to app/models/location.rb:

class L o c a t i o n < ActiveRecord::Base


acts_as_mappable :auto_geocode => true
end

and lets test it by creating a few Location objects once again in the console:

> > Location.find :all


=> []
> > Location.create(:address => "Torstrasse 104, Berlin, Germany")
=> #<Location:0x344d074 @errors=#<ActiveRecord::Errors:0x341c99c @errors={},
@base=#<Location:0x344d074 ...>>, @attributes={"id"=>4, "lng"=>13.403495,
"lat"=>52.530051, "address"=>"Torstrasse 104, Berlin, Germany"},
@new_record=false>
> > home = Location.find :first
=> #<Location:0x3416e34
@attributes={"lng"=>#<BigDecimal:3416e5c,'0.13403495E2',12(16)>, "id"=>"4",
"lat"=>#<BigDecimal:3416e84,'0.52530051E2',12(16)>, "address"=>"Torstrasse

Rails GIS Hacks Using Geocoders | 5


l a b s

104, Berlin, Germany"}>


> > Location.create(:address => "Lepsiusstrasse 70, Berlin, Germany")
=> #<Location:0x3413608 @errors=#<ActiveRecord::Errors:0x33e52f8 @errors={},
@base=#<Location:0x3413608 ...>>, @attributes={"id"=>5, "lng"=>13.316571,
"lat"=>52.460126, "address"=>"Lepsiusstrasse 70, Berlin, Germany"},
@new_record=false>
> > Location.create(:address => "Crellestrasse 23, Berlin, Germany")
=> #<Location:0x33df704 @errors=#<ActiveRecord::Errors:0x33b13f4 @errors={},
@base=#<Location:0x33df704 ...>>, @attributes={"id"=>6, "lng"=>13.365749,
"lat"=>52.49112, "address"=>"Crellestrasse 23, Berlin, Germany"},
@new_record=false>
> > Location.create(:address => "Mauerstrasse 65, Berlin, Germany")
=> #<Location:0x33ab8a0 @errors=#<ActiveRecord::Errors:0x337d590 @errors={},
@base=#<Location:0x33ab8a0 ...>>, @attributes={"id"=>7, "lng"=>13.386817,
"lat"=>52.510553, "address"=>"Mauerstrasse 65, Berlin, Germany"},
@new_record=false>

Usually, you can do your sorting in the database as part of your find call. If you need to sort
things post-query, you can do so using the sort_by_distance_from():

> > locs = Location.find :all


=> [#<Location:0x3375d90
@attributes={"lng"=>#<BigDecimal:3375f20,'0.13403495E2',12(16)>, "id"=>"4",
"lat"=>#<BigDecimal:3375f48,'0.52530051E2',12(16)>, "address"=>"Torstrasse
104, Berlin, Germany"}>, #<Location:0x3375d68
@attributes={"lng"=>#<BigDecimal:3375e94,'0.13316571E2',12(16)>, "id"=>"5",
"lat"=>#<BigDecimal:3375ea8,'0.52460126E2',12(16)>,
"address"=>"Lepsiusstrasse 70, Berlin, Germany"}>, #<Location:0x3375d40
@attributes={"lng"=>#<BigDecimal:3375e1c,'0.13365749E2',12(16)>, "id"=>"6",
"lat"=>#<BigDecimal:3375e30,'0.5249112E2',12(16)>, "address"=>"Crellestrasse
23, Berlin, Germany"}>, #<Location:0x3375d18
@attributes={"lng"=>#<BigDecimal:3375da4,'0.13386817E2',12(16)>, "id"=>"7",
"lat"=>#<BigDecimal:3375db8,'0.52510553E2',12(16)>, "address"=>"Mauerstrasse
65, Berlin, Germany"}>]
> > locs.sort_by_distance_from(home)
=> [#<Location:0x3375d90 @distance=0.0,
@attributes={"lng"=>#<BigDecimal:3375f20,'0.13403495E2',12(16)>, "id"=>"4",
"lat"=>#<BigDecimal:3375f48,'0.52530051E2',12(16)>, "address"=>"Torstrasse
104, Berlin, Germany"}>, #<Location:0x3375d18 @distance=1.52043248966975,
@attributes={"lng"=>#<BigDecimal:3375da4,'0.13386817E2',12(16)>, "id"=>"7",
"lat"=>#<BigDecimal:3375db8,'0.52510553E2',12(16)>, "address"=>"Mauerstrasse
65, Berlin, Germany"}>, #<Location:0x3375d40 @distance=3.12676959370349,
@attributes={"lng"=>#<BigDecimal:3375e1c,'0.13365749E2',12(16)>, "id"=>"6",
"lat"=>#<BigDecimal:3375e30,'0.5249112E2',12(16)>, "address"=>"Crellestrasse
23, Berlin, Germany"}>, #<Location:0x3375d68 @distance=6.06585345156976,
@attributes={"lng"=>#<BigDecimal:3375e94,'0.13316571E2',12(16)>, "id"=>"5",

Rails GIS Hacks Using Geocoders | 6


l a b s

"lat"=>#<BigDecimal:3375ea8,'0.52460126E2',12(16)>,
"address"=>"Lepsiusstrasse 70, Berlin, Germany"}>]

When doing the database distance calculation like below, ActiveRecord has the calculated
distance column. However, ActiveRecord drops the distance column if you are doing eager
loading in your find call via :include. So if you need to use the distance column, you will have
to do the sort_by_distance_from() after such a find.

> > locs = Location.find :all, :origin=>home, :within => 5, :order =>
'distance'
=> [#<Location:0x3362268
@attributes={"lng"=>#<BigDecimal:3362394,'0.13403495E2',12(16)>, "id"=>"4",
"lat"=>#<BigDecimal:33623bc,'0.52530051E2',12(16)>, "address"=>"Torstrasse
104, Berlin, Germany", "distance"=>"0"}>, #<Location:0x3362240
@attributes={"lng"=>#<BigDecimal:33622f4,'0.13386817E2',12(16)>, "id"=>"7",
"lat"=>#<BigDecimal:3362308,'0.52510553E2',12(16)>, "address"=>"Mauerstrasse
65, Berlin, Germany", "distance"=>"1.52043248966975"}>, #<Location:0x3362218
@attributes={"lng"=>#<BigDecimal:336227c,'0.13365749E2',12(16)>, "id"=>"6",
"lat"=>#<BigDecimal:3362290,'0.5249112E2',12(16)>, "address"=>"Crellestrasse
23, Berlin, Germany", "distance"=>"3.1267695948189"}>]
> > locs.sort_by_distance_from(home, :units => :kms)
=> [#<Location:0x3362268 @distance=0.0,
@attributes={"lng"=>#<BigDecimal:3362394,'0.13403495E2',12(16)>, "id"=>"4",
"lat"=>#<BigDecimal:33623bc,'0.52530051E2',12(16)>, "address"=>"Torstrasse
104, Berlin, Germany", "distance"=>"0"}>, #<Location:0x3362240
@distance=2.44637587587862,
@attributes={"lng"=>#<BigDecimal:33622f4,'0.13386817E2',12(16)>, "id"=>"7",
"lat"=>#<BigDecimal:3362308,'0.52510553E2',12(16)>, "address"=>"Mauerstrasse
65, Berlin, Germany", "distance"=>"1.52043248966975"}>, #<Location:0x3362218
@distance=5.03097227626891,
@attributes={"lng"=>#<BigDecimal:336227c,'0.13365749E2',12(16)>, "id"=>"6",
"lat"=>#<BigDecimal:3362290,'0.5249112E2',12(16)>, "address"=>"Crellestrasse
23, Berlin, Germany", "distance"=>"3.1267695948189"}>]

IP address geocoding

GeoKit uses the Host.ip service to find an IP’s location. An example of the IP geocoder:

> > location = GeoKit::Geocoders::IpGeocoder.geocode('85.178.26.159')


=> #<GeoKit::GeoLoc:0x3756fe0 @lat=52.5, @state=nil, @street_address=nil,
@country_code="DE", @provider="hostip", @precision="unknown", @zip=nil,
@lng=13.4167, @city="Berlin", @success=true>

Rails GIS Hacks Using Geocoders | 7


l a b s

However GeoKit lets you automatically store the geo location of a user's IP in the session and in
the cookie under the :geo_location key. In subsequent visits, the cookie value is used to cache
the location. So in the app/controllers/application.rb we can have:

class A p p l i c a t i o n C o n t r o l l e r < ActionController::Base


# Pick a unique cookie name to distinguish our session data from others'
session :session_key => '_geokit_session_id'

# Auto-geocode the user's ip address and store in the session.


geocode_ip_address

def geokit
@location = session[:geo_location] # @location is a GeoLoc instance.
end
end

Graticule and acts_as_geocodable http://graticule.rubyforge.org/


by Brandon Keepers and Daniel Morrison

Begin by installing Graticule:

$ sudo gem install graticule --include-dependencies

and then we create our Graticule demo rails (ver. 1.2.3) app and inside it install the
acts_as_geocodable companion plugin:

$ rails graticule
$ cd graticule
$ ruby script/plugin install
http://source.collectiveidea.com/public/rails/plugins/acts_as_geocodable/

Finally we create our databases graticule_development, graticule_test, and


graticule_production and configure our config/database.yml.

Geocoding

The plugin automatically geocodes your models when they are saved, giving you the ability to
search by location and calculate distances between records. We start by creating the required
tables:

$ ruby script/generate geocodable_migration add_geocodable_tables


$ rake db:migrate

Rails GIS Hacks Using Geocoders | 8


l a b s

Set the default geocoder in your config/environment.rb file:

Geocode.geocoder = Graticule.service(:yahoo).new
'pPDCgjnV34FJ3TxysU9K.FpFYQ3A_QYJ4VrAJQuyFcFv91Hf0r3PU5tr3SYBhMvOoM__'

or

Geocode.geocoder = Graticule.service(:google).new
'ABQIAAAAwWqh7sPpuhNCdGZ0pieShBTJQa0g3IQ9GZqIMmInSLzwtGDKaBQOJH6Tw4jIlz7bMDU
6qtLF_9TSHQ'

Finally we create a model which must have the required address fields attributes called street,
locality, region, postal_code, and country:

$ ruby script/generate model Location

and add the migrations in db/migrate/002_create_locations.rb:

class C r e a t e L o c a t i o n s < ActiveRecord::Migration


def self.up
create_table :locations do |t|
t.column "street", :string
t.column "locality", :string
t.column "region", :string
t.column "postal_code", :string
t.column "country", :string
end
end

def self.down
drop_table :locations
end
end

Now we run our migration to create the location table in our database:

$ rake db:migrate

Then, to make the location model geocodable, add acts_as_geocodable in the


apps/models/location.rb

class L o c a t i o n < ActiveRecord::Base


acts_as_geocodable
end

Rails GIS Hacks Using Geocoders | 9


l a b s

And in the console we test the automatic geocoding when we save our model and then show a
search by location and calculate distances between records.

$ ruby script/console
Loading development environment.
> > Location.find :all
=> []
> > conf = Location.create :street => "Friedrichstrasse 151", :locality =>
"Berlin"
=> #<Location:0x357ec40 @geocoding=#<Geocoding:0x356e9a8
@errors=#<ActiveRecord::Errors:0x356dd78 @errors={},
@base=#<Geocoding:0x356e9a8 ...>>, @geocode=#<Geocode:0x357490c
@attributes={"postal_code"=>nil,
"latitude"=>#<BigDecimal:35749d4,'0.5251818E2',12(20)>, "region"=>"Germany",
"country"=>"DE", "id"=>"2", "locality"=>"10117 Mitte",
"street"=>"Friedrichstrasse 151", "query"=>"Friedrichstrasse 151\nBerlin, ",
"longitude"=>#<BigDecimal:35749ac,'0.13388423E2',12(20)>}>,
@attributes={"geocodable_type"=>"Location", "id"=>4, "geocodable_id"=>4,
"geocode_id"=>2}, @new_record=false>,
@errors=#<ActiveRecord::Errors:0x357ccd8 @errors={},
@base=#<Location:0x357ec40 ...>>, @attributes={"postal_code"=>nil,
"region"=>"Germany", "country"=>"DE", "id"=>4, "locality"=>"Berlin",
"street"=>"Friedrichstrasse 151"}, @new_record=false>
> > conf.geocode.latitude
=> #<BigDecimal:35749d4,'0.5251818E2',12(20)>
> > conf.geocode.longitude
=> #<BigDecimal:35749ac,'0.13388423E2',12(20)>
> > prevConf = Location.create :street => "777 NE Martin Luther King, Jr.
Blvd.",:locality => "Portland", :region => "Oregon", :postal_code => 97232
=> #<Location:0x355c924 @geocoding=#<Geocoding:0x3555e6c
@errors=#<ActiveRecord::Errors:0x355578c @errors={},
@base=#<Geocoding:0x3555e6c ...>>, @geocode=#<Geocode:0x3557cd0
@attributes={"postal_code"=>"97232-2742",
"latitude"=>#<BigDecimal:3557d98,'0.45528468E2',12(20)>, "region"=>"OR",
"country"=>"US", "id"=>"1", "locality"=>"Portland", "street"=>"777 Ne M L
King Blvd", "query"=>"777 NE Martin Luther King, Jr. Blvd.\nPortland,
Oregon 97232", "longitude"=>#<BigDecimal:3557d70,'-0.122661895E3',12(20)>}>,
@attributes={"geocodable_type"=>"Location", "id"=>5, "geocodable_id"=>5,
"geocode_id"=>1}, @new_record=false>,
@errors=#<ActiveRecord::Errors:0x355b894 @errors={},
@base=#<Location:0x355c924 ...>>, @attributes={"postal_code"=>97232,
"region"=>"Oregon", "country"=>"US", "id"=>5, "locality"=>"Portland",
"street"=>"777 NE Martin Luther King, Jr. Blvd."}, @new_record=false>
> > conf.distance_to prevConf
=> 5185.541406646
> > Location.find(:all, :within => 50, :origin => "Torstrasse 104, Berlin,

Rails GIS Hacks Using Geocoders | 10


l a b s

Germany")
=> [#<Location:0x35239f8 @readonly=true, @attributes={"postal_code"=>nil,
"region"=>"Germany", "country"=>"DE", "id"=>"4", "locality"=>"Berlin",
"street"=>"Friedrichstrasse 151", "distance"=>"1.03758608910963"}>]

IP geocoding

acts_as_geocodable adds a remote_location method in your controllers that uses


http://hostip.info to guess remote users location based on their IP address. So for example

def index
@nearest = Location.find(:nearest, :origin => remote_location) if
remote_location
@locations = Location.find(:all)
end

Rails GIS Hacks Using Geocoders | 11


02 l a b s

Location data in ActiveRecord


(PostGIS/PostgreSQL)
Prerequisites

Installing PostGIS

Windows
Download the PostgreSQL windows installer from http://www.postgresql.org install but do not
include the PostGIS option.

Then download and run the PostGIS installer


http://postgis.refractions.net/download/windows/

UNIX
Follow the instructions here: http://postgis.refractions.net/docs/ch02.html

Mac OS
Download and install the Mac OS ports for PostGIS from
http://www.kyngchaos.com/software/unixport/postgres

Setting up PostGIS databases

Create a template_postgis database


Some might find this useful for creating PostGIS databases without having to be PostgreSQL
super users. The idea is to create a template_postgis database, install plpgsql and postgis into
it, and then use this database as a template when creating new PostGIS databases.

$ psql template1
\c template1
CREATE DATABASE template_postgis with template = template1;

-- set the 'datistemplate' record in the 'pg_database' table for


-- 'template_postgis' to TRUE indicating its a template
UPDATE pg_database SET datistemplate = TRUE where datname =
'template_postgis';
\c template_postgis
CREATE LANGUAGE plpgsql;
\i /usr/share/postgresql/contrib/lwpostgis.sql
\i /usr/share/postgresql/contrib/spatial_ref_sys.sql

Rails GIS Hacks Location data in ActiveRecord | 12


l a b s

-- set role based permissions in production env.


GRANT ALL ON geometry_columns TO PUBLIC;
GRANT ALL ON spatial_ref_sys TO PUBLIC;

-- vacuum freeze: it will guarantee that all rows in the database are
-- "frozen" and will not be subject to transaction ID wraparound
-- problems.
VACUUM FREEZE;

Now non-super user’s can create PostGIS databases using template_postgis:

$ createdb my_gisdb -W -T template_postgis

Installing GeoRuby http://thepochisuperstarmegashow.com/projects/


by Guilhem Vellut

$ sudo gem install georuby --include-dependencies

GeoRuby is our foundation library for bridging ruby to the spatial databases. Its data model is
roughly based on OGC’s simple feature specification
http://portal.opengeospatial.org/files/index.php?artifact_id=829

Some background to the Geospatial Domain

Geospatial Data

Why should we be treating spatial data so differently and why bother with a whole tutorial on
it? That’s a good question and one that we hope to be able to answer through out this tutorial
but first a little background.

Complex data types


Firstly, spatial data-types (also called Geometry Datatype) as defined in the Open Geospatial
Consortium's (OGC) simple features specification are of the following types (or sub-types, if you
like): point, line and polygon (there are more but this will suffice for now, see Figure 1).

Rails GIS Hacks Location data in ActiveRecord | 13


l a b s

Geometry SpatialReferenceSystem

Point Curve Surface GeometryCollection

1+ 2+

LineString Polygon MultiSurface MultiCurve MultiPoint


1+
1+

Line LinearRing MultiPolygon MultiLineString


1+ FIGURE 1
Geometry Object Model
(source OGC Simple Feature
Specification)

Spatial Reference Systems (SRS)


Another thing that makes the geospatial data special is that it has a Spatial Reference System
(SRS). SRS is really a mathematical model for defining the shape of the earth. And every time
we position something on the face of the earth we need to make sure we remember to also
record what SRS was used for that position. The most well known of SRS’s is WGS84, the one
used when deriving a position from a GPS.

Why can’t we just have one SRS you ask? Well you see the shape of the earth is never the
same, we have things like continental shift. And from time-to-time we (humans) try to
approximate the shape of the earth using a sphere. Every time we do this we create a new
Spatial Reference System.

Spatial indices
Yet another thing that makes spatial data special is spatial indexing. Since searching based on
spatial parameters (e.g. all pubs that are within a certain distance from a hospital) requires a
very different lookup compared to the ordered indexing methods used to look for an ID in an
RDBMS. Most spatial databases will implement the R-Tree spatial indexing algorithm. We won’t
go into too much detail but R-Tree creates a hierarchical index based on spatial extents
allowing records that are in close geographic proximity to also be in close proximity in
computer’s memory. Here is a nice paper if you are the curious type:
http://www.sai.msu.su/~megera/postgres/gist/papers/gutman-rtree.pdf

Later we will see how PostGIS specifically stores the geographic data-type, SRS’s and handles
indices.

Rails GIS Hacks Location data in ActiveRecord | 14


l a b s

Displaying Geospatial Data

2D in a 3D world: map projections


Here’s an interesting experiment to try. Go to http://maps.yahoo.com and zoom out to around
country scale. Then centre you map on the equator and prime meridian (see Figure 2).

FIGURE 2
Screenshot of Yahoo Maps
near the equator.

Now use the left and right arrow keys to move the map along the equator while keeping an eye
on the scale bars at the bottom left of the map. You should see no change in the scale bars.

FIGURE 3
Screenshot of Yahoo Maps
near the equator.

Rails GIS Hacks Location data in ActiveRecord | 15


l a b s

Now move using up or down arrow keys while keeping an eye on the scale bar. As you approach
the poles you will notice a huge difference in the scales.

FIGURE 4
Screenshot of Yahoo Maps
near the north pole.

This is because we are looking at the projection of the 3D world on a 2D screen and this always
leads to some distortion. In this instance the projection being used is the Mercator projection
which nicely displays the latitude and longitude lines as a square grid. But the down side is that
as you move away from the equator the distances and areas distort. The reason this project is
so popular is that the bearing of any straight lines drawn on the map are preserved and that’s
helpful if you are using a compass to navigate.

For more information have a look at:


http://www.gsd.harvard.edu/geo/util/arcgis/ESRI_Library/Managing_data_with_ArcGIS/Unders
tanding_Map_Projections.pdf

Free Data

Let look at how to get some free GIS data. You can find a collection of links here
http://freegis.org/database/?cat=1

Some example datasets for you to download:

Vector data
High Resolution Coastline
http://www.ngdc.noaa.gov/mgg/shorelines/data/gshhs/version1.5/shapefiles/ download
gshhs_1.3_shapefiles.tar.gz

Rails GIS Hacks Location data in ActiveRecord | 16


l a b s

Raster data
Elevation from USGS/NASA http://edc.usgs.gov/products/elevation/gtopo30/gtopo30.html

There are some great online sites for downloading raster data seamlessly for your region of
interest. Checkout: http://glcfapp.umiacs.umd.edu:8080/esdi/index.jsp some of these have a
“shopping-cart-for-maps” feel. You can download multi-band satellite imagery via ftp.
ftp://ftp.glcf.umiacs.umd.edu/glcf/Landsat/WRS1/p098/r087/p098r87_1m19730119.MSS-
EarthSat-Orthorectified

Free Desktop GIS

Quite a few options for visualizing and manipulating GIS data exist. Here are some:
UDig (User-friendly Destktop Internet GIS) http://udig.refractions.net It’s eclipse based.

QGIS (QT based) http://www.qgis.org

GRASS (X11/command-line based, very powerful image processing and integration with R
statistical package – sadly no ruby bindings yet. This HAS to change!) http://grass.itc.it

Utility Tools: There also exist some command-line tools and api’s for interacting with GIS data.
The most useful is GDAL/OGR – an Open Source library for Raster/Vector data IO. Some of its
credentials include: Google Earth uses GDAL; ruby bindings for the API (need work though); it
supports over 20 raster and 10 vector formats. Read more at http://gdal.org

Setting up our GeoRails Application

Database Connection

Spatial Adapter for ActiveRecord


How do you make spatial databases part of the rails stack? By the end of this tutorial you will
be able to answer this question. Lets start by creating a new rails project and installing our
first plug-in. This is the SpatialAdapter plugin which extends ActiveRecord to allow the
geographic data type to be managed seamlessly in our models.
$ rails railsconfeu07_gis
$ cd railsconfeu07_gis/
$ ruby script/plugin install
svn://rubyforge.org/var/svn/georuby/SpatialAdapter/trunk/spatial_adapter
$ createdb -O sab -T template_postgis railsconfeu07_gis_development
$ createdb -O sab -T template_postgis railsconfeu07_gis_test

Now we are going to create a mapping application with some data about the city of Karachi.
This will contain locations of points of interest in Karachi.

Rails GIS Hacks Location data in ActiveRecord | 17


l a b s

Lets stay restful and create a resource (Location) and CRUD for planned resource:

$ ruby script/generate scaffold_resource Location geom:point name:string


category:string description:text

N O T E : support for multiple geometries. We could have also made the point into geometry,
the superclass of point (see the OGC simple features diagram). That would allow us to have
support for all geometry types (Point, Lines & Polygons):

$ ruby script/generate scaffold_resource Location geom:geometry name:string


category:string description:text

Migrations in Spatial Adapter


This will create a migration. We will need to modify the migration 001_create_locations.rb to
include the creation parameters for the point geometry column.

def self.up
create_table :locations do |t|
t.column :geom, :point, :null => false, :srid => 4326, :with_z => true
t.column :name, :string, :null => false
t.column :category, :string, :null => false
t.column :description, :text
end
end

N O T E : what is s r i d ? SRID stands for Spatial Reference ID. When you create a PostGIS
database it adds to it a table called spatial_ref_sys contaning over 3000 spatial reference
systems. They define the geometric model for approximating the shape of the earth. The
system used by GPS has an SRID of 4326. You can check it out by doing the following:

$ psql -d template_postgis
template_postgis=# \x -- to turn on expanded display
template_postgis=# SELECT * from spatial_ref_sys where srid = 4326;
-[ RECORD 1 ]----------------------------------------------------------
srid | 4326
auth_name | EPSG
auth_srid | 4326
srtext | GEOGCS["WGS 84",DATUM["WGS_1984", SPHEROID["WGS
84",6378137,298.25722 3563, AUTHORITY["EPSG","7030"]],
TOWGS84[0,0,0,0,0,0,0], AUTHORITY["EPSG","6326"]], PRIMEM["Greenwich",0,
AUTHORITY["EPSG","8901"]], UNIT["degree",0.01745329251994328,
AUTHORITY["EPSG","9122"]], AUTHORITY["EPSG","4326"]] proj4text |
+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs

Rails GIS Hacks Location data in ActiveRecord | 18


l a b s

There is one more thing that is unique to spatial databases that we will need to add, spatial
indicies. Since lookup operations for complex geometries need to be fast, spatial data-
structures and indexing algorithms exist for looking up spatial data in spatial databases.
So lets create another migration.

$ ruby script/generate migration add_index_to_locations

def self.up
add_index :locations, :geom, :spatial => true
end

def self.down
remove_index :locations, :geom
end

… then

$ rake migrate

Lets add some data

$ ruby script/generate migration add_locations_data

def self.up
Location.create(
:geom => Point.from_x_y_z(67.1069627266882, 24.9153581895111, 3, 4326),
:name => "ALLADIN WATER PARK",
:category => "AMUSEMENT PARK",
:description => "A new amusement park built on the main Rashid Minhas
Road is the latest attraction of Karachi. It has the colorful slides, one
of the tallest in Asia. It is spread over an area of 50 acres. Open for the
people in 1996. It has become a valuable tourist attraction of the city. It
has the amusement park, a water park, shopping center and many eating
outlets including the Kentucky Fried Chicken etc There is a full Olympic
size swimming pool, a children pool and a wave pool. Fishermen's village is
being constructed with a separate area of Bar-B-Cue. There are going to be
40 different kinds of rides, boating facilities and mini train" )

Location.create(
:geom => Point.from_x_y_z(67.0457415431788, 24.9006848344289, 3, 4326),
:name => "POLICE STATION",
:category => "POLICE",
:description => "Yet another well regarded police station in Karachi" )

Rails GIS Hacks Location data in ActiveRecord | 19


l a b s

Location.create(
:geom => Point.from_x_y_z(67.0665783392958, 24.9357149717549, 3, 4326),
:name => "GULBERG POLICE STATION",
:category => "POLICE",
:description => "Yet another well regarded police station in Karachi" )

Location.create(
:geom => Point.from_x_y_z(67.0560860072235, 24.9373400445234, 3, 4326),
:name => "TAIMORIA POLICE STATION",
:category => "POLICE",
:description => "Another highly regarded police station in Karachi" )

Location.create(
:geom => Point.from_x_y_z(67.038036851834, 24.838993022744, 3, 4326),
:name => "POLICE STATION",
:category => "POLICE",
:description => "Another highly regarded police station in Karachi" )

Location.create(
:geom => Point.from_x_y_z(67.0646934316687, 24.9272522976814, 3, 4326),
:name => "HABIB BANK",
:category => "BANK",
:description => "A big bank in the heart of Karachi`s wall street ...
police stations, a theme-park and a bank, I wonder where this is going" )
end

def self.down
Location.delete_all
end

Tests

While we are at it lets also add a couple of fixtures for testing. Note the SpatialAdapter’s
to_fixture_format method for converting spatial data into fixture format.

one:
id: 1
geom: <%= Point.from_x_y_z(67.0665783392958, 24.9357149717549, 3,
4326).to_fixture_format %>
name: "GULBERG POLICE STATION"
category: "POLICE"
description: "Yet another well regarded police station in Karach"

Rails GIS Hacks Location data in ActiveRecord | 20


l a b s

two:
id: 2
geom: <%= Point.from_x_y_z(67.0560860072235, 24.9373400445234, 3,
4326).to_fixture_format %>
name: "TAIMORIA POLICE STATION"
category: "POLICE"
description: "Yet another well regarded police station in Karach"

three:
id: 3
geom: <%= Point.from_x_y_z(67.0646934316687, 24.9272522976814, 3,
4326).to_fixture_format %>
name: "HABIB BANK"
category: "BANK"
description: "A big bank in Karach"

Populate the test database

$ rake db:test:prepare

Quick test to see if the testing environment is all setup and working properly.

$ ruby test/unit/location_test.rb

or you can just run the following

$ rake test:units

how about the functional tests

$ rake test:functionals

This blows up! We haven’t given create the necessary parameters so lets just do that:

def test_should_create_location
old_count = Location.count
post :create, :location => {
:geom => Point.from_x_y_z(67.0665783392958, 24.9357149717549, 3, 4326),
:name => "GULBERG POLICE STATION",
:category => "POLICE",
:description => "Yet another well regarded police station in Karachi" }
assert_equal old_count+1, Location.count

assert_redirected_to location_path( assigns(:location) )


end

Rails GIS Hacks Location data in ActiveRecord | 21


l a b s

now lets re-run

$ rake test:functionals

Javascript Tests
Since we expect to be writing some javascript we will also setup the javascript testing
framework. script.aculo.us includes a javascript unit-testing framework which needs to be
installed as a plugin to the rails framework.

$ ruby script/plugin install


http://dev.rubyonrails.org/svn/rails/plugins/javascript_test

To create a javascript unit-test stub for scripts in public/javascript/application.js run the


javascript test generator like:

$ ruby script/generate javascript_test application

This command will generate a test stub under


RAILS_ROOT/test/javascript/application_test.html. Looking at this file we see the inclustion
of some javascript libraries (prototype.js, scriptaculous.js and unittest.js), a div for
displaying the test results and finally a new instance of Test.Unit.Runner. In it we define three
functions: setup (run before the start of every test case – useful for initialising objects for use in
test), teardown (the opposite of setup and used to cleanup after a test case finishes), testTruth
(a trivial test case example to get us started).

new T e s t . U n i t .Runner({// replace this with your real tests


setup: function() {
$('sandbox').innerHTML = "<div id='123_a' style='display:none;'>
</div>";
},

teardown: function() {
},

testTruth: function() { with(this) {


assert(true);
}}
}, "testlog");

Now lets give our JS testing framework a spin:

$ rake test:javascripts

and be amazed at the way it detects the supported browsers and runs them to display the test
results. Cool eh? The Test.Unit.Assertions class in script.aculo.us defines quite a few useful
assertions for testing.

Rails GIS Hacks Location data in ActiveRecord | 22


l a b s

Check out: http://wiki.script.aculo.us/scriptaculous/show/Test.Unit.Assertions

So it looks as though we might be ready for some serious geo-rails development. To see what
we have so far point your browser to http://localhost:3000/locations/.

CSS

Lets just beautify with a nicer CSS (stylesheet) than the one scaffold gave us. Creating
public/stylesheets/simple.css
body {
background-color: #eee;
color: #222;
font-family: trebuchet;
padding: 0;
margin: 25px;
}
h1 {
margin: -25px -25px 20px -25px;
padding: 50px 0 8px 25px;
border-bottom: 3px solid #666;
background-color: #ff7;
color: #0000ff;
font: normal 28pt georgia;
text-shadow: black 0px 0px 5px;
}
a { color: #229; }
.box {
border: 1px solid;
width: 100px; height: 100px;
padding: 5px;
font-size: .6em;
letter-spacing: .1em;
text-transform: uppercase;
margin-bottom: 20px;
}
.pink {
border-color: #f00;
background-color: #fcc;
}
.green {
border-color: #090;
background-color: #cfc;
}
.hover {
border-width: 5px;

Rails GIS Hacks Location data in ActiveRecord | 23


l a b s

padding: 1px;
}
ul {
background-color: #ccc;
padding: 5px 0 5px 30px;
}
In our layout change the stylesheet to our new one app/views/layouts/locations.rhtml

<%= stylesheet_link_tag 'simple' %>

Lets have a look ... http://localhost:3000/locations

CRUD Location

Introducing Guilhem Vellut’s YM4R_GM

In the last section we noted that our geographic data doesn’t look particularly meaningful. We
can get a partial improvement on this by displaying the coordinates of our locations as latitude
and longitude and elevation. Looking at show.rhtml and index.rhtml we can change
<%=h Location.geom %> to <%=h Location.geom.text_representation %> to display the
coordinates.

But what we really want is to show our locations on a nice mapping interface. For this we will
use Guillhem Vellut’s (http://thepochisuperstarmegashow.com/) excellent YM4R_GM plugin. So lets
install it.

$ ruby script/plugin install


svn://rubyforge.org/var/svn/ym4r/Plugins/GM/trunk/ym4r_gm

This will add some javascript files to your RAILS_ROOT/public/javascripts/ folder. Including:
clusterer.js, geoRssOverlay.js, markerGroup.js, wms-gs.js and ym4r-gm.js. In addition it will
add RAILS_ROOT/config/gmaps_api_key.yml for your Google Maps API key.
We’ll start by modifying the show action to display the Point geometry on a Google Maps.
ym4r_gm has some builtin convenience methods to help initialize the map object. The end result
of these methods is creation of javascript in the view. Lets demonstrate:
In my controller I have the show method. This will allow the view to be renderd. In the case of a
Google Maps application some of our views display maps.

With the introduction of the geometry column to our database table we no longer have a one-on-
one mapping between the types in params-hash and our data type. Infact geom being a complex
data-type it would be a bad idea to pass it around in the params-hash. So we will need to modify
our controller slightly to accommodate this new data-type. Other than this we will try to restrict
our modifications to the view and helpers so our controller is nice and clean.

Rails GIS Hacks Location data in ActiveRecord | 24


l a b s

N O T E : ym4r gives us access to a ruby class called GMap. This can be used to modify the map
instance in the view. At times its useful to have access to this object in the controller to
generate javascript. However for the purposes of the CRUD we will not be needing this object in
the controller.

Show
Se we want to plot our Point-geometry attribute on Google Maps. Lets use ym4r_gm’s
initialisation routines. In RAILS_ROOT/app/helpers/locations_helper.rb add:

def show_map rec


@map = GMap.new("map#{rec.id}_div")
@map.control_init(:large_map => true,:map_type => true)
@map.center_zoom_init([rec.geom.y,rec.geom.x],16)
@map.set_map_type_init(GMapType::G_SATELLITE_MAP)
@map.overlay_init(GMarker.new([rec.geom.y,rec.geom.x],
:title => rec.name,
:info_window => rec.description))
end

Here we create a GMap object passing it a name for a div. In ym4r functions that end with _init
are helpers for initilizing some common tasks in Google Maps. In our example we initialise the
map control by asking for a large_map and the controls for choosing map_types. We center the
map on our record’s location. Note that we have to give center coordinates to Google Maps as y
(lat) and then x (lng). We set the map_type by passing the class variable for the satellite map.
Then we add a marker to show the location of our record.

Having created a show_map helper we can now call it from our view template. In
app/views/locations/show.rhtml lets add the necessary calls to ym4r_gm methods to create the
javascript headers and the map div.

<%= GMap.header %>


<% show_map @location %>
<%= @map.to_html %>
<p>
<b>Geom:</b>
<%= @map.div :width => 400, :height => 400 %>
</p>

First we call the class method to include the Google Maps API’s javascript files. Then we call
show_map passing it the instance variable for the record. This instantiates the map instance
variable. The map instance’s div method is then called to create a div for displaying the map.

Restart your server since we have installed a new plugin and point your browser to
http://localhost:3000/locations/1.

So show is working, lets move on to index.

Rails GIS Hacks Location data in ActiveRecord | 25


l a b s

Index

We would now like to list all the maps in our database. Lets make a variant of this show_map
helper called show_maps. This will create a hash of maps when given an array of objects.

def show_maps recs


@maps = Hash.new
recs.each do |rec|
map = GMap.new("map#{rec.id}_div", "map#{rec.id}")
map.control_init(:small_map => true, :map_type => true)
map.center_zoom_init([rec.geom.y, rec.geom.x], 16)
map.set_map_type_init(GMapType::G_SATELLITE_MAP)
map.overlay_init(GMarker.new([rec.geom.y, rec.geom.x],
:title => rec.name,
:info_window => rec.category))
@maps[rec.id] = map
end
end

Here (in location_helper.rb) we make a smaller map and store the map in hash. In the
index.rhtml view we iterate over the hash displaying each map.

<%= GMap.header %>


<% show_maps @locations %>
<% @maps.each_value do |map| %>
<%= map.to_html %>
<% end %>

New

Now the hard part: How do you suppose we should tackle this while remaining restful and
keeping our controller as clean as possible? Since this tutorial is done with a hack mind-set we
will use a trick. Our trick will be to use the standard mechanisms provided in rails for passing
parameters between views and controllers. Namely the params hash.

Some javascript
We’ll need to write some javascript helpers to allow us to capture the coordinates of a location
being created by the user. This will make use of the Google Maps API and ym4r. In our
application.js lets create a new function that will handle the creation of new markers and
their drag events to update the html form fields. The comments in the code are fairly self
explainitory. Reference: http://groups.google.com/group/Google-Maps-
API/browse_thread/thread/c062c81fa8c0e2ac/5913f312f57ed19b

Rails GIS Hacks Location data in ActiveRecord | 26


l a b s

function create_draggable_editable_marker()
{
// intialize the values in form fields to 0
document.getElementById("lng").value = 0;
document.getElementById("lat").value = 0;
var currMarker;

// if the user click on an existing marker remove it


GEvent.addListener(map, "click", function(marker, point) {
if (marker) {
if (confirm("Do you want to delete marker?")) {
map.removeOverlay(marker);
}
}
// if the user clicks somewhere other than existing marker
else {
// remove the previous marker if it exists
if (currMarker) {
map.removeOverlay(currMarker);
}
currMarker = new G M a r k e r (point, {draggable: true});
map.addOverlay(currMarker);
// update the form fields
document.getElementById("lng").value = point.x;
document.getElementById("lat").value = point.y;
}

// Similarly drag event is used to update the form fields


GEvent.addListener(currMarker, "drag", function() {
document.getElementById("lng").value = currMarker.getPoint().lng();
document.getElementById("lat").value = currMarker.getPoint().lat();
});
});
}

Once this is done we need to make sure that application.js is included in our layout
locations.rhtml.

<%= javascript_include_tag :defaults %>

Rails GIS Hacks Location data in ActiveRecord | 27


l a b s

Helper
Next we create a helper called new_map to call the create_draggable_editable_marker() function
in the context of a new map.

def new_map
@map = GMap.new("map_div")
@map.control_init(:large_map => true, :map_type => true)
@map.center_zoom_init([0,0],2)
@map.record_init('create_draggable_editable_marker();')
end

New form templete


Next we can edit our new.rhtml template. Adding the usual header calls and the map_div. The
new items are a couple of text_field_tags for latitude and longitude values.

<%= GMap.header %>


<% new_map %>
<%= @map.to_html %>

(...)

<%= @map.div :width => 600, :height => 400 %>


Lat: <%= text_field_tag :lat -%>
Lng: <%= text_field_tag :lng -%>

Next we can edit our new.rhtml template. Adding the usual header files and map_div. The new
item we add is a couple of text_field_tags for latitude and longitude values.

Controller
Lets now move to the controller and capture our coordinates to create a new location. So in
locations_controller.rb we modify our create action to:

def create
@location = Location.new(params[:location])
geom = Point.from_x_y_z(params[:lng], params[:lat], 3, 4326)
@location.geom = geom
(...)
end

Here we first explicitly create a geom object from the lat, lng params hash. Then we set the geom
attribute of the new location to this new geom object.

Rails GIS Hacks Location data in ActiveRecord | 28


l a b s

Edit

We are nearing the end of CRUD. Edit is really a slight modification of the new.

Javascript
So first some javascript in application.js.

function create_draggable_marker_for_edit(lng, lat) {


// initalize form fields
document.getElementById('lng').value = lng;
document.getElementById('lat').value = lat;

// initalize marker
var currMarker = new GMarker( new GLatLng(lat, lng), {draggable: true} );
map.addOverlay(currMarker);

// Handle drag events to update the form text fields


GEvent.addListener(currMarker, 'drag', function() {
document.getElementById('lng').value = currMarker.getPoint().lng();
document.getElementById('lat').value = currMarker.getPoint().lat();
});
}
Helper
Then the locations_helper.rb.

def edit_map rec


@map = GMap.new("map_div")
@map.control_init(:large_map => true,:map_type => true)
@map.set_map_type_init(GMapType::G_SATELLITE_MAP)
@map.center_zoom_init([rec.geom.y, rec.geom.x],12)
@map.record_init("create_draggable_marker_for_edit(#{rec.geom.x},
#{rec.geom.y});")
end

View template
Next we update the view template with headers, call to the helper, map_div and text_field_tags.

<%= GMap.header %>


<% edit_map @location %>
<%= @map.to_html %>

(...)

<%= @map.div :width => 600, :height => 400 %>


Lat: <%= text_field_tag :lat -%>
Lng: <%= text_field_tag :lng -%>

Rails GIS Hacks Location data in ActiveRecord | 29


l a b s

Controller
Again the controller will simply update the geom attribute along with others. So in
locations_controller.rb we modify our update action to:

def update
@location = Location.find( params[:id] )
geom = Point.from_x_y_z(params[:lng], params[:lat], 3, 4326)
@location.geom = geom
(...)
end

All done with CRUD! Time to give yourself a pat on the back.

Rails GIS Hacks Location data in ActiveRecord | 30


03 l a b s

Supporting New Content Types


One great advantage of RESTful design is the separation of content-representation and the
under-lying resources that make up our business logic. This decoupling of content-type from
business logic allows us to painlessly add support for additional client applications. In this
chapter we will add support for some popular client applications like Google Earth.

KML (Google Earth)

Register new mime-type

We’ll start off by registering a new mime-type in config/environment.rb. This will ensure that
we can use the new mime-type in the respond_to block in controllers.

Mime::Type.register "application/vnd.google-earth.kml+xml", :kml

Make sure you restart the server after making changes to config/environment.rb.

Controller and Action

In our controller locations_controller we will only be adding kml response to the index and
show actions.

def index
(...)
respond_to do |format|
(...)
format.kml { render :action => 'index_kml', :layout => false }
end
end

def show
(...)
respond_to do |format|
(...)
format.kml { render :action => 'show_kml', :layout => false }
end
end

Rails GIS Hacks Supporting New Content Types | 31


l a b s

Rxml template for kml

We will create rxml templates called show_kml.rxml and index_kml.rxml and these will be
rendered by our respond_to block.

First show_kml.rxml

xml.kml("xmlns" => KML_NS) do


xml.tag! "Document" do
xml.tag! "Style", :id => "myStyle" do
xml.tag! "PointStyle" do
xml.color "ffff0000" #format is aabbggrr
xml.outline 0
end
end
xml.tag! "Placemark" do
xml.description @location.description
xml.name @location.name
xml.styleUrl "#myStyle"
xml << @location.geom.as_kml
end
end
end

and then index_kml.rxml

xml.kml("xmlns" => KML_NS) do


xml.tag! "Document" do
xml.tag! "Style", :id => "myStyle" do
xml.tag! "PointStyle" do
xml.color "ffff0000" #format is aabbggrr
xml.outline 0
end
end
@locations.each do |location|
xml.tag! "Placemark" do
xml.description location.description
xml.name location.name
xml.styleUrl "#myStyle"
xml << location.geom.as_kml
end
end
end
end

Rails GIS Hacks Supporting New Content Types | 32


l a b s

Try them out by pointing your browser to http://localhost:3000/locations/1.kml and


http://localhost:3000/locations.kml. If we have a public IP we can view the locations in
Google Maps.

Formatted URL helpers

Thanks to REST support in rails we also get formatted URL helpers for free. Let’s try them
out. We will use a Google Earth icon to indicate a kml link. An icon can be download from
http://www.google.com/earth/images/google_earth_link.gif and placed in your public/images/
folder. In index.rhtml you can modify the header to include a link to all the locations.

<h1>
Listing locations
<%= link_to image_tag("/images/google_earth_link.gif"),
formatted_locations_path(:kml) %>
</h1>

Similarly to show.rhtml you can add the following somewhere in the page

<%= link_to image_tag("/images/google_earth_link.gif"),


formatted_location_path(@location, :kml) %>

Rails GIS Hacks Supporting New Content Types | 33


l a b s

GeoRSS

As before we start with mime-type registration:

Mime::Type.register "application/georss+xml", :georss

Don’t forget to restart the server after adding the mime type.

Recent Entries

Now with GeoRSS we need to be able to distinguish recent location postings from the older ones.
This calls for a migration to add an updated_at and created_at columns to our locations table.

$ ruby script/generate migration add_date_fields

Edit the migration to add the following columns:

add_column :locations, :created_at, :datetime


add_column :locations, :updated_at, :datetime

Unfortunately this also calls for a slight inconvenience of re-ordering the rails migrations.
Since we need to have these new columns populate for our sample data we will decriment this
(add_date_fields) migration and increment add_location_data migration. This will make sure
the data is created after all the fields are there. So first roll back all migration, just to be safe

$ rake db:migrate VERSION=00

Then rename the migrations so that add_date_fields comes after add_location_data and then:

$ rake db:migrate

Model
Next we’ll add a method to our locations model to give us the 5 most recently updated
locations. So in the app/models/location.rb lets add a recent method

def self.recent
self.find(:all,:limit => 5, :order => "updated_at DESC")
end

Controller actions
Now in our controller we add to the index action’s respond_to block:

format.georss { @recent_locations = Location.recent


render :action => 'index_georss', :layout => false }

Rails GIS Hacks Supporting New Content Types | 34


l a b s

and to the show action’s respond_to block:

format.georss { render :action => 'show_georss', :layout => false }

Okay looking good…

View templates
Next we create our rxml templates: index_georss.rxml and show_georss.rxml.

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


xml.channel do
xml.title "Demo feed for RailsConf Europe 2007"
xml.link( locations_url )
xml.description "This is only a demo no big deal!"
xml.item do
xml.title @location.name
xml.link( location_url(@location) )
xml.description @location.description
xml << @location.geom.as_georss
end
end
end

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

xml.channel do
xml.title "Demo feed for RailsConf Europe 2007"
xml.link( locations_url )
xml.description "This is only a demo no big deal!"
xml.pubDate(@location.created_at.strftime("%a, %d %b %Y %H:%M:%S %z"))

@recent_locations.each do |location|
xml.item do
xml.title location.name
xml.link( location_url(location) )
xml.description location.description
xml << location.geom.as_georss
end
end

end
end

And we are done. Point your browser to http://localhost:3000/locations/1.georss and


http://localhost:3000/locations.georss. Also as with KML above, we can add GeoRSS
formatted URL links to our page.

Rails GIS Hacks Supporting New Content Types | 35


l a b s

References

Rails GIS Hacks References | 36

You might also like