TUTORIAL

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

Rails GIS Hacks
Shoaib Burq | Kashif Rasul Monday, September 17, 2007 13:30 – 17:00 Saal Maritim B

Berlin | Germany

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

Design & title photo by Aleks Herzog www.nomad-graphics.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) Prerequisites

.........................

12 12 13

................................................................................

Some background to the Geospatial Domain

.........................................

Setting up our GeoRails Application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 CRUD Location
..............................................................................

22

03 Supporting New Content Types KML (Google Earth) GeoRSS

....................................................

28 28 31

........................................................................

.......................................................................................

Rails GIS Hacks

Content | 2

01
Using Geocoders
$ rails geokit $ cd geokit/

l a b s

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:

$ 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

http://www.google.com/apis/maps/signup.html

API, you need to register by going to: 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] end end # @location is a GeoLoc instance.

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 http://hostip.info

adds a remote_location method in your controllers that uses 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
(PostGIS/PostgreSQL)
Prerequisites
Installing PostGIS
Then download and run the PostGIS installer
http://postgis.refractions.net/download/windows/

l a b s

Location data in ActiveRecord

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

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 1+ 2+

Curve

Surface

GeometryCollection

LineString 1+

Polygon 1+

MultiSurface

MultiCurve

MultiPoint

Line

LinearRing 1+

MultiPolygon

MultiLineString

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/ gshhs_1.3_shapefiles.tar.gz

download

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.MSSEarthSat-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 datastructures 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 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"); RAILS_ROOT/test/javascript/application_test.html.

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-onone 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 lets add the necessary calls to ym4r_gm methods to create the javascript headers and the map div.
app/views/locations/show.rhtml <%= 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-MapsAPI/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
KML (Google Earth)
Register new mime-type

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.

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 end end def show (...) respond_to do |format| (...) format.kml end end { render :action => 'show_kml', :layout => false } { render :action => 'index_kml', :layout => false }

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 xml << end end end @location.name @location.geom.as_kml xml.styleUrl "#myStyle"

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 xml << end end end end location.name location.geom.as_kml xml.styleUrl "#myStyle"

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