rails gis hacks tutorial 170907

36
l a b s [email protected] | www.nomad-labs.com TUTORIAL Rails GIS Hacks Berlin | Germany Shoaib Burq | Kashif Rasul Monday, September 17, 2007 13:30 – 17:00 Saal Maritim B This tutorial (version 17.9.07) was downloaded from: http://rails.nomad-labs.com Design & title photo by Aleks Herzog www.nomad-graphics.com

Upload: pius

Post on 12-Nov-2014

1.296 views

Category:

Documents


6 download

TRANSCRIPT

Page 1: rails gis hacks tutorial 170907

l a b [email protected] | www.nomad-labs.com

T U T O R I A L

Rails GIS Hacks Berlin | Germany

Shoaib Burq | Kashif RasulMonday, September 17, 200713:30 – 17:00Saal Maritim B

This tutorial (version 17.9.07)

was downloaded from:

http://rails.nomad-labs.com

Des

ign

& ti

tle p

hoto

by

Alek

s H

erzo

gw

ww

.nom

ad-g

raph

ics.

com

Page 2: rails gis hacks tutorial 170907

Content | 2Rails GIS Hacks

l a b s

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

GeoKit http://geokit.rubyforge.org/ by Bill Eisenhauerand 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

Content

Page 3: rails gis hacks tutorial 170907

Using Geocoders | 3Rails GIS Hacks

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:

$ 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 andconfigure it in our config/database.yml file.

Geocoding

The geocoder uses geocoding webservices provided by Google, Yahoo, Geocoder.us orGeocoder.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 tohttp://search.yahooapis.com/webservices/register_application and log in with your Yahoouser 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.htmlGeoKit::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_ExamplesGeoKit::Geocoders::google ='ABQIAAAAwWqh7sPpuhNCdGZ0pieShBTJQa0g3IQ9GZqIMmInSLzwtGDKaBQOJH6Tw4jIlz7bMDU6qtLF_9TSHQ'

Using Geocoders

01

Page 4: rails gis hacks tutorial 170907

Using Geocoders | 4Rails GIS Hacks

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/consoleLoading 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) orthe 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 flatenvironments. If you only need the distance calculation services then add the Mappable module

Page 5: rails gis hacks tutorial 170907

Using Geocoders | 5Rails GIS Hacks

l a b s

into your class making sure that your class has a lat and lng attribute. However anotherapplication 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 CreateLocations < ActiveRecord::Migrationdef self.up

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

endend

def self.downdrop_table :locations

endend

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 Location < ActiveRecord::Baseacts_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

Page 6: rails gis hacks tutorial 170907

Using Geocoders | 6Rails GIS Hacks

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 sortthings 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"=>"Torstrasse104, 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"=>"Crellestrasse23, Berlin, Germany"}>, #<Location:0x3375d18@attributes={"lng"=>#<BigDecimal:3375da4,'0.13386817E2',12(16)>, "id"=>"7","lat"=>#<BigDecimal:3375db8,'0.52510553E2',12(16)>, "address"=>"Mauerstrasse65, 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"=>"Torstrasse104, 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"=>"Mauerstrasse65, 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"=>"Crellestrasse23, Berlin, Germany"}>, #<Location:0x3375d68 @distance=6.06585345156976,@attributes={"lng"=>#<BigDecimal:3375e94,'0.13316571E2',12(16)>, "id"=>"5",

Page 7: rails gis hacks tutorial 170907

Using Geocoders | 7Rails GIS Hacks

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 calculateddistance column. However, ActiveRecord drops the distance column if you are doing eagerloading in your find call via :include. So if you need to use the distance column, you will haveto 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"=>"Torstrasse104, Berlin, Germany", "distance"=>"0"}>, #<Location:0x3362240@attributes={"lng"=>#<BigDecimal:33622f4,'0.13386817E2',12(16)>, "id"=>"7","lat"=>#<BigDecimal:3362308,'0.52510553E2',12(16)>, "address"=>"Mauerstrasse65, 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"=>"Crellestrasse23, 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"=>"Torstrasse104, 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"=>"Mauerstrasse65, 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"=>"Crellestrasse23, 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>

Page 8: rails gis hacks tutorial 170907

l a b s

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

class ApplicationController < 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.

endend

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 theacts_as_geocodable companion plugin:

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

Finally we create our databases graticule_development, graticule_test, andgraticule_production and configure our config/database.yml.

Geocoding

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

$ ruby script/generate geocodable_migration add_geocodable_tables$ rake db:migrate

Rails GIS Hacks Using Geocoders | 8

Page 9: rails gis hacks tutorial 170907

Using Geocoders | 9Rails GIS Hacks

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'ABQIAAAAwWqh7sPpuhNCdGZ0pieShBTJQa0g3IQ9GZqIMmInSLzwtGDKaBQOJH6Tw4jIlz7bMDU6qtLF_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 CreateLocations < ActiveRecord::Migrationdef self.up

create_table :locations do |t|t.column "street", :stringt.column "locality", :stringt.column "region", :stringt.column "postal_code", :stringt.column "country", :string

endend

def self.downdrop_table :locations

endend

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 theapps/models/location.rb

class Location < ActiveRecord::Baseacts_as_geocodable

end

Page 10: rails gis hacks tutorial 170907

Using Geocoders | 10Rails GIS Hacks

l a b s

And in the console we test the automatic geocoding when we save our model and then show asearch 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 LKing 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,

Page 11: rails gis hacks tutorial 170907

Using Geocoders | 11Rails GIS Hacks

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 useshttp://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

Page 12: rails gis hacks tutorial 170907

Prerequisites

Installing PostGIS

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

Then download and run the PostGIS installerhttp://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 fromhttp://www.kyngchaos.com/software/unixport/postgres

Setting up PostGIS databases

Create a template_postgis databaseSome might find this useful for creating PostGIS databases without having to be PostgreSQLsuper users. The idea is to create a template_postgis database, install plpgsql and postgis intoit, 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

Location data in ActiveRecord | 12Rails GIS Hacks

l a b s

Location data in ActiveRecord(PostGIS/PostgreSQL)

02

Page 13: rails gis hacks tutorial 170907

-- 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 isroughly 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 onit? That’s a good question and one that we hope to be able to answer through out this tutorialbut first a little background.

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

Location data in ActiveRecord | 13Rails GIS Hacks

l a b s

Page 14: rails gis hacks tutorial 170907

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 timewe position something on the face of the earth we need to make sure we remember to alsorecord what SRS was used for that position. The most well known of SRS’s is WGS84, the oneused 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 thesame, we have things like continental shift. And from time-to-time we (humans) try toapproximate the shape of the earth using a sphere. Every time we do this we create a newSpatial Reference System.

Spatial indicesYet another thing that makes spatial data special is spatial indexing. Since searching based onspatial parameters (e.g. all pubs that are within a certain distance from a hospital) requires avery different lookup compared to the ordered indexing methods used to look for an ID in anRDBMS. Most spatial databases will implement the R-Tree spatial indexing algorithm. We won’tgo into too much detail but R-Tree creates a hierarchical index based on spatial extentsallowing records that are in close geographic proximity to also be in close proximity incomputer’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 handlesindices.

Location data in ActiveRecord | 14Rails GIS Hacks

l a b s

MultiPoint

SurfaceCurvePoint

LinearRing

LineString

Line

GeometryCollection

Geometry SpatialReferenceSystem

MultiCurve

MultiLineString

MultiSurface

MultiPolygon

Polygon

1+

1+

1+

1+ 2+

F I G U R E 1 Geometry Object Model(source OGC Simple FeatureSpecification)

Page 15: rails gis hacks tutorial 170907

Displaying Geospatial Data

2D in a 3D world: map projectionsHere’s an interesting experiment to try. Go to http://maps.yahoo.com and zoom out to aroundcountry scale. Then centre you map on the equator and prime meridian (see Figure 2).

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

Location data in ActiveRecord | 15Rails GIS Hacks

l a b s

F I G U R E 2Screenshot of Yahoo Mapsnear the equator.

F I G U R E 3Screenshot of Yahoo Mapsnear the equator.

Page 16: rails gis hacks tutorial 170907

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

This is because we are looking at the projection of the 3D world on a 2D screen and this alwaysleads to some distortion. In this instance the projection being used is the Mercator projectionwhich nicely displays the latitude and longitude lines as a square grid. But the down side is thatas you move away from the equator the distances and areas distort. The reason this project isso popular is that the bearing of any straight lines drawn on the map are preserved and that’shelpful 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/Understanding_Map_Projections.pdf

Free Data

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

Some example datasets for you to download:

Vector dataHigh Resolution Coastlinehttp://www.ngdc.noaa.gov/mgg/shorelines/data/gshhs/version1.5/shapefiles/ downloadgshhs_1.3_shapefiles.tar.gz

Location data in ActiveRecord | 16Rails GIS Hacks

l a b s

F I G U R E 4Screenshot of Yahoo Mapsnear the north pole.

Page 17: rails gis hacks tutorial 170907

Raster dataElevation 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 ofinterest. 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 Rstatistical 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 itscredentials include: Google Earth uses GDAL; ruby bindings for the API (need work though); itsupports over 20 raster and 10 vector formats. Read more at http://gdal.org

Setting up our GeoRails Application

Database Connection

Spatial Adapter for ActiveRecordHow do you make spatial databases part of the rails stack? By the end of this tutorial you willbe able to answer this question. Lets start by creating a new rails project and installing ourfirst plug-in. This is the SpatialAdapter plugin which extends ActiveRecord to allow thegeographic data type to be managed seamlessly in our models.

$ rails railsconfeu07_gis $ cd railsconfeu07_gis/$ ruby script/plugin installsvn://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.

Location data in ActiveRecord | 17Rails GIS Hacks

l a b s

Page 18: rails gis hacks tutorial 170907

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

$ ruby script/generate scaffold_resource Location geom:point name:stringcategory: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 havesupport for all geometry types (Point, Lines & Polygons):

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

Migrations in Spatial Adapter This will create a migration. We will need to modify the migration 001_create_locations.rb toinclude the creation parameters for the point geometry column.

def self.upcreate_table :locations do |t|

t.column :geom, :point, :null => false, :srid => 4326, :with_z => truet.column :name, :string, :null => falset.column :category, :string, :null => falset.column :description, :text

endend

N O T E : what is srid? SRID stands for Spatial Reference ID. When you create a PostGISdatabase it adds to it a table called spatial_ref_sys contaning over 3000 spatial referencesystems. They define the geometric model for approximating the shape of the earth. Thesystem 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 displaytemplate_postgis=# SELECT * from spatial_ref_sys where srid = 4326; -[ RECORD 1 ]----------------------------------------------------------srid | 4326auth_name | EPSG auth_srid | 4326srtext | GEOGCS["WGS 84",DATUM["WGS_1984", SPHEROID["WGS84",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

Location data in ActiveRecord | 18Rails GIS Hacks

l a b s

Page 19: rails gis hacks tutorial 170907

There is one more thing that is unique to spatial databases that we will need to add, spatialindicies. 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.upadd_index :locations, :geom, :spatial => true

end

def self.downremove_index :locations, :geom

end

… then

$ rake migrate

Lets add some data

$ ruby script/generate migration add_locations_data

def self.upLocation.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, oneof the tallest in Asia. It is spread over an area of 50 acres. Open for thepeople in 1996. It has become a valuable tourist attraction of the city. Ithas the amusement park, a water park, shopping center and many eatingoutlets including the Kentucky Fried Chicken etc There is a full Olympicsize swimming pool, a children pool and a wave pool. Fishermen's village isbeing constructed with a separate area of Bar-B-Cue. There are going to be40 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" )

Location data in ActiveRecord | 19Rails GIS Hacks

l a b s

Page 20: rails gis hacks tutorial 170907

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.downLocation.delete_all

end

Tests

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

one:id: 1geom: <%= 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"

Location data in ActiveRecord | 20Rails GIS Hacks

l a b s

Page 21: rails gis hacks tutorial 170907

two:id: 2geom: <%= 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: 3geom: <%= 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_locationold_count = Location.countpost :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

Location data in ActiveRecord | 21Rails GIS Hacks

l a b s

Page 22: rails gis hacks tutorial 170907

now lets re-run

$ rake test:functionals

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

$ ruby script/plugin installhttp://dev.rubyonrails.org/svn/rails/plugins/javascript_test

To create a javascript unit-test stub for scripts in public/javascript/application.js run thejavascript test generator like:

$ ruby script/generate javascript_test application

This command will generate a test stub underRAILS_ROOT/test/javascript/application_test.html. Looking at this file we see the inclustionof some javascript libraries (prototype.js, scriptaculous.js and unittest.js), a div fordisplaying the test results and finally a new instance of Test.Unit.Runner. In it we define threefunctions: setup (run before the start of every test case – useful for initialising objects for use intest), 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 Test.Unit.Runner({// replace this with your real testssetup: 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 testresults. Cool eh? The Test.Unit.Assertions class in script.aculo.us defines quite a few usefulassertions for testing.

Location data in ActiveRecord | 22Rails GIS Hacks

l a b s

Page 23: rails gis hacks tutorial 170907

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 whatwe 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. Creatingpublic/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;

Location data in ActiveRecord | 23Rails GIS Hacks

l a b s

Page 24: rails gis hacks tutorial 170907

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. Wecan get a partial improvement on this by displaying the coordinates of our locations as latitudeand longitude and elevation. Looking at show.rhtml and index.rhtml we can change <%=h Location.geom %> to <%=h Location.geom.text_representation %> to display thecoordinates.

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

$ ruby script/plugin installsvn://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 willadd 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 resultof 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 aGoogle 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 complexdata-type it would be a bad idea to pass it around in the params-hash. So we will need to modifyour controller slightly to accommodate this new data-type. Other than this we will try to restrictour modifications to the view and helpers so our controller is nice and clean.

Location data in ActiveRecord | 24Rails GIS Hacks

l a b s

Page 25: rails gis hacks tutorial 170907

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

ShowSe we want to plot our Point-geometry attribute on Google Maps. Lets use ym4r_gm’sinitialisation 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 _initare helpers for initilizing some common tasks in Google Maps. In our example we initialise themap control by asking for a large_map and the controls for choosing map_types. We center themap 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. Inapp/views/locations/show.rhtml lets add the necessary calls to ym4r_gm methods to create thejavascript 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 callshow_map passing it the instance variable for the record. This instantiates the map instancevariable. 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 tohttp://localhost:3000/locations/1.

So show is working, lets move on to index.

Location data in ActiveRecord | 25Rails GIS Hacks

l a b s

Page 26: rails gis hacks tutorial 170907

Index

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

def show_maps recs@maps = Hash.newrecs.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] = mapend

end

Here (in location_helper.rb) we make a smaller map and store the map in hash. In theindex.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 andkeeping our controller as clean as possible? Since this tutorial is done with a hack mind-set wewill use a trick. Our trick will be to use the standard mechanisms provided in rails for passingparameters 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 locationbeing created by the user. This will make use of the Google Maps API and ym4r. In ourapplication.js lets create a new function that will handle the creation of new markers andtheir drag events to update the html form fields. The comments in the code are fairly selfexplainitory. Reference: http://groups.google.com/group/Google-Maps-API/browse_thread/thread/c062c81fa8c0e2ac/5913f312f57ed19b

Location data in ActiveRecord | 26Rails GIS Hacks

l a b s

Page 27: rails gis hacks tutorial 170907

function create_draggable_editable_marker(){

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

// if the user click on an existing marker remove itGEvent.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 markerelse {

// remove the previous marker if it existsif (currMarker) {

map.removeOverlay(currMarker);}currMarker = new GMarker(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 fieldsGEvent.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 layoutlocations.rhtml.

<%= javascript_include_tag :defaults %>

Location data in ActiveRecord | 27Rails GIS Hacks

l a b s

Page 28: rails gis hacks tutorial 170907

Helper Next we create a helper called new_map to call the create_draggable_editable_marker() functionin 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. Thenew 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 newitem 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 inlocations_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 geomattribute of the new location to this new geom object.

Location data in ActiveRecord | 28Rails GIS Hacks

l a b s

Page 29: rails gis hacks tutorial 170907

Edit

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

JavascriptSo first some javascript in application.js.

function create_draggable_marker_for_edit(lng, lat) {// initalize form fieldsdocument.getElementById('lng').value = lng;document.getElementById('lat').value = lat;

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

// Handle drag events to update the form text fieldsGEvent.addListener(currMarker, 'drag', function() {

document.getElementById('lng').value = currMarker.getPoint().lng();document.getElementById('lat').value = currMarker.getPoint().lat();

}); }

HelperThen 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 -%>

Location data in ActiveRecord | 29Rails GIS Hacks

l a b s

Page 30: rails gis hacks tutorial 170907

Controller Again the controller will simply update the geom attribute along with others. So inlocations_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.

Location data in ActiveRecord | 30Rails GIS Hacks

l a b s

Page 31: rails gis hacks tutorial 170907

One great advantage of RESTful design is the separation of content-representation and theunder-lying resources that make up our business logic. This decoupling of content-type frombusiness logic allows us to painlessly add support for additional client applications. In thischapter 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 thatwe 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 andshow actions.

def index(...)respond_to do |format|

(...)format.kml { render :action => 'index_kml', :layout => false }

endend

def show(...)respond_to do |format|

(...)format.kml { render :action => 'show_kml', :layout => false }

endend

Supporting New Content Types | 31Rails GIS Hacks

l a b s

Supporting New Content Types

03

Page 32: rails gis hacks tutorial 170907

Rxml template for kml

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

First show_kml.rxml

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

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

xml.color "ffff0000" #format is aabbggrrxml.outline 0

endendxml.tag! "Placemark" do

xml.description @location.descriptionxml.name @location.namexml.styleUrl "#myStyle"xml << @location.geom.as_kml

endend

end

and then index_kml.rxml

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

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

xml.color "ffff0000" #format is aabbggrrxml.outline 0

[email protected] do |location|

xml.tag! "Placemark" doxml.description location.descriptionxml.name location.namexml.styleUrl "#myStyle"xml << location.geom.as_kml

endend

endend

Supporting New Content Types | 32Rails GIS Hacks

l a b s

Page 33: rails gis hacks tutorial 170907

Try them out by pointing your browser to http://localhost:3000/locations/1.kml andhttp://localhost:3000/locations.kml. If we have a public IP we can view the locations inGoogle Maps.

Formatted URL helpers

Thanks to REST support in rails we also get formatted URL helpers for free. Let’s try themout. We will use a Google Earth icon to indicate a kml link. An icon can be download fromhttp://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) %>

Supporting New Content Types | 33Rails GIS Hacks

l a b s

Page 34: rails gis hacks tutorial 170907

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, :datetimeadd_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 surethe 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 updatedlocations. So in the app/models/location.rb lets add a recent method

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

end

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

format.georss { @recent_locations = Location.recentrender :action => 'index_georss', :layout => false }

Supporting New Content Types | 34Rails GIS Hacks

l a b s

Page 35: rails gis hacks tutorial 170907

and to the show action’s respond_to block:

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

Okay looking good…

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

xml.rss(:version => "2.0", "xmlns:georss" => GEORSS_NS) doxml.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.namexml.link( location_url(@location) )xml.description @location.descriptionxml << @location.geom.as_georss

endend

end

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

xml.channel doxml.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.namexml.link( location_url(location) )xml.description location.descriptionxml << location.geom.as_georss

endend

endend

And we are done. Point your browser to http://localhost:3000/locations/1.georss andhttp://localhost:3000/locations.georss. Also as with KML above, we can add GeoRSSformatted URL links to our page.

Supporting New Content Types | 35Rails GIS Hacks

l a b s

Page 36: rails gis hacks tutorial 170907

l a b s

References

References | 36Rails GIS Hacks