Académique Documents
Professionnel Documents
Culture Documents
t
was utorial (v
down ersio
http:
//rai loaded fr n 17.9.07
ls.no o )
mad m:
TUTORIAL -labs
.com
l a b s
info@nomad-labs.com | www.nomad-labs.com
l a b s
Content
01 Using Geocoders . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Prerequisites ................................................................................ 12
GeoRSS ....................................................................................... 31
Using Geocoders
GeoKit http://geokit.rubyforge.org/ by Bill Eisenhauer
and Andre Lewis
We create our GeoKit demo rails (ver. 1.2.3) app and inside it install the GeoKit plugin:
$ rails geokit
$ cd geokit/
$ ruby script/plugin install svn://rubyforge.org/var/svn/geokit/trunk
Geocoding
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:
And finally add the provider order, so here we use Yahoo before Google.
$ 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
GeoKit provides in-memory distance calculations for either the LatLng class (GeoKit::LatLng) or
the GeoLoc class. So for example in the console:
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
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
def self.down
drop_table :locations
end
end
Now we run our migration to create the locations table in our database:
$ rake db:migrate
and lets test it by creating a few Location objects once again in the console:
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():
"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:
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:
def geokit
@location = session[:geo_location] # @location is a GeoLoc instance.
end
end
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/
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:
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:
def self.down
drop_table :locations
end
end
Now we run our migration to create the location table in our database:
$ rake db:migrate
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,
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
def index
@nearest = Location.find(:nearest, :origin => remote_location) if
remote_location
@locations = Location.find(:all)
end
Installing PostGIS
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
$ psql template1
\c template1
CREATE DATABASE template_postgis with template = template1;
-- 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;
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
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.
Geometry SpatialReferenceSystem
1+ 2+
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.
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.
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.
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
Vector data
High Resolution Coastline
http://www.ngdc.noaa.gov/mgg/shorelines/data/gshhs/version1.5/shapefiles/ download
gshhs_1.3_shapefiles.tar.gz
Raster data
Elevation from USGS/NASA http://edc.usgs.gov/products/elevation/gtopo30/gtopo30.html
There are some great online sites for downloading raster data seamlessly for your region of
interest. Checkout: http://glcfapp.umiacs.umd.edu:8080/esdi/index.jsp some of these have a
“shopping-cart-for-maps” feel. You can download multi-band satellite imagery via ftp.
ftp://ftp.glcf.umiacs.umd.edu/glcf/Landsat/WRS1/p098/r087/p098r87_1m19730119.MSS-
EarthSat-Orthorectified
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.
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
Database Connection
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.
Lets stay restful and create a resource (Location) and CRUD for planned resource:
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):
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
There is one more thing that is unique to spatial databases that we will need to add, spatial
indicies. Since lookup operations for complex geometries need to be fast, spatial data-
structures and indexing algorithms exist for looking up spatial data in spatial databases.
So lets create another migration.
def self.up
add_index :locations, :geom, :spatial => true
end
def self.down
remove_index :locations, :geom
end
… then
$ rake migrate
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" )
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"
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"
$ rake db:test:prepare
Quick test to see if the testing environment is all setup and working properly.
$ ruby test/unit/location_test.rb
$ rake test:units
$ 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
$ 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.
teardown: function() {
},
$ 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.
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;
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
CRUD Location
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.
This will add some javascript files to your RAILS_ROOT/public/javascripts/ folder. Including:
clusterer.js, geoRssOverlay.js, markerGroup.js, wms-gs.js and ym4r-gm.js. In addition it will
add RAILS_ROOT/config/gmaps_api_key.yml for your Google Maps API key.
We’ll start by modifying the show action to display the Point geometry on a Google Maps.
ym4r_gm has some builtin convenience methods to help initialize the map object. The end result
of these methods is creation of javascript in the view. Lets demonstrate:
In my controller I have the show method. This will allow the view to be renderd. In the case of a
Google Maps application some of our views display maps.
With the introduction of the geometry column to our database table we no longer have a one-on-
one mapping between the types in params-hash and our data type. Infact geom being a complex
data-type it would be a bad idea to pass it around in the params-hash. So we will need to modify
our controller slightly to accommodate this new data-type. Other than this we will try to restrict
our modifications to the view and helpers so our controller is nice and clean.
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:
Here we create a GMap object passing it a name for a div. In ym4r functions that end with _init
are helpers for initilizing some common tasks in Google Maps. In our example we initialise the
map control by asking for a large_map and the controls for choosing map_types. We center the
map on our record’s location. Note that we have to give center coordinates to Google Maps as y
(lat) and then x (lng). We set the map_type by passing the class variable for the satellite map.
Then we add a marker to show the location of our record.
Having created a show_map helper we can now call it from our view template. In
app/views/locations/show.rhtml lets add the necessary calls to ym4r_gm methods to create the
javascript headers and the map div.
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.
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.
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.
New
Now the hard part: How do you suppose we should tackle this while remaining restful and
keeping our controller as clean as possible? Since this tutorial is done with a hack mind-set we
will use a trick. Our trick will be to use the standard mechanisms provided in rails for passing
parameters between views and controllers. Namely the params hash.
Some javascript
We’ll need to write some javascript helpers to allow us to capture the coordinates of a location
being created by the user. This will make use of the Google Maps API and ym4r. In our
application.js lets create a new function that will handle the creation of new markers and
their drag events to update the html form fields. The comments in the code are fairly self
explainitory. Reference: http://groups.google.com/group/Google-Maps-
API/browse_thread/thread/c062c81fa8c0e2ac/5913f312f57ed19b
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;
Once this is done we need to make sure that application.js is included in our layout
locations.rhtml.
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
(...)
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.
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.
// initalize marker
var currMarker = new GMarker( new GLatLng(lat, lng), {draggable: true} );
map.addOverlay(currMarker);
View template
Next we update the view template with headers, call to the helper, map_div and text_field_tags.
(...)
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.
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.
Make sure you restart the server after making changes to config/environment.rb.
In our controller locations_controller we will only be adding kml response to the index and
show actions.
def index
(...)
respond_to do |format|
(...)
format.kml { render :action => 'index_kml', :layout => false }
end
end
def show
(...)
respond_to do |format|
(...)
format.kml { render :action => 'show_kml', :layout => false }
end
end
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
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
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.
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
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:
View templates
Next we create our rxml templates: index_georss.rxml and show_georss.rxml.
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
References