Posts tagged Geography
Rails Geo Plugins: GeoKit
Mar 12th
There’s quite a bit of overlap between GeoKit and acts_as_geocodable/graticule, as the latter pair were based on GeoKit. But it provides at least one feature (IP-based location lookup) that they don’t, so I decided to give it a whirl.
Since my main geographically related projects are both now based on plugins that I’m pretty happy with and which suit them well, I decided to resuscitate an old sample piece. A few months back I wrote about scraping the Grand Rapids bus routes site and put up a toy application utilising the resource features in then-edge Rails. I’ve been meaning to return to that project to test out some features in ActiveResource, but in the meantime it seemed like it might be useful to be able to search for the nearest bus stop.
The plugin comes with an extensive README file and getting up and running is very straightforward:
class BusStop < ActiveRecord::Base acts_as_mappable :default_units => :kms, :default_formula => :flat, :distance_field_name => :distance, :lng_column_name => 'longitude', :lat_column_name => 'latitude'
I had to add the column_name parameters to signify which database columns I was using, as the plugin defaults to using ‘lat’ and ‘lng’ for brevity. The inclusion of the default_units parameter is a nice one, but it would also be nice if the plugin provided an accessor method to convert distance on the fly to help developers localise their apps.
With that done I get access to a suite of methods for doing location searches. So if I wanted to find the nearest bus stop to Common Ground Coffee Shop I could call:
BusStop.find(:nearest, :origin => '1319 Fulton St. East, Grand Rapids MI 49503')
That address actually failed in the google geocoder (probably because I had yet to enter my api key), but GeoKit automatically fell back to geocoder.us and got the co-ordinates.
Where it seemed to fall over was when I tried to limit by bus route:
BusStop.find(:nearest, :origin => '1319 Fulton St. East, Grand Rapids MI 49503', :include => :route_stops, :conditions => 'route_stops.bus_route_id = 6') ActiveRecord::StatementInvalid: Mysql::Error: Unknown column 'distance' in 'field list': SELECT DISTINCT bus_stops.id, distance FROM bus_stops LEFT OUTER JOIN route_stops ON route_stops.bus_stop_id = bus_stops.id WHERE (route_stops.bus_route_id = 6) ORDER BY distance ASC LIMIT 1
Andre Lewis, one of the plugin’s developers (along with Bill Eisenhauer), tells me that GeoKit doesn’t currently support the :include option or conditions on join tables. He’s hoping to add that soon, but for now you’re limited to a single table.
Implementing the IP-based geolocation was very easy. A before_filter is included which can be included in a controller with the declaration:
geocode_ip_address
will return a GeoLoc object containing details of the lookup source, the address and the co-ordinates. I chose to add my own before_filter so I could find the nearest bus stop:
class ApplicationController < ActionController::Base before_filter :find_nearest_stop def find_nearest_stop @nearest = BusStop.find_nearest(:origin => get_ip_address) end end
(where get_ip_address is a method provided by the plugin). I had to deploy to a remote host to actually test this as 127.0.0.1 obviously doesn’t work as a source IP address for these purposes, and it’s likely that a number of users working through proxies of one sort or another will find the information less than accurate. But so long as that’s clear, it’s a nice feature to add to local information services, potentially getting users to relevant information more quickly.
I do like the separation between gem and plugin that acts_as_locateable offers, the fact that geocodes are stored in a separate table, and full join support for queries. But the IP translation from GeoKit is also a nice feature. Both are highly capable solutions, so it’s likely that once GeoKit adds full join support, choosing between them may well come down to personal taste.
(Nb. Those following along with these posts may be interested in GeoKit co-author Andre Lewis’ book Beginning Google Maps Applications with Rails and Ajax. I’m hoping to check it out soon)
Extending acts_as_geocodable
Mar 11th
After writing my review of acts_as_geocodable/graticule earlier in the week, I decided to go searching for geocoding services that might offer data for addresses outside of North America. One that I came across is at Local Search Maps. There’s an introductory blog entry here.
The API is a little different in that it returns its data as javascript strings, but otherwise it’s simple enough to send a GET for a given address and get back the data. To see how easy it is, I decided to code up an extra geocoder for graticule that would use this service.
The main change I had to make was to allow for extra arguments to the locate method. For the geocoders distributed with the gem there’s just one argument: the address. For this service to retrieve data outside the USA it requires more structured data, so I added an optional argument for the locate method and built my query based on its existence. The code turned out to be very simple:
require 'graticule' module Graticule # A library for lookup of coordinates with http://geo.localsearchmaps.com/ class LocalSearchMapsGeocoder < RestGeocoder def initialize @url = URI.parse 'http://geo.localsearchmaps.com/' end # This web service will handle some addresses outside the US # if given more structured arguments than just a string address # So allow input as a hash for the different arguments (:city, :country, :zip) def locate(address, args = {}) if args.empty? get :address => address else get args.merge(:street => address) end end def check_error(js) raise AddressError, "Empty Response" if js.nil? or js.text.nil? raise AddressError, js.text.match(/alert\('(.+)'\)/)[1] if js.text == "alert('location not found');" end def parse_response(js) location = Location.new coords = js.text.match(/map.centerAndZoom\(new GPoint\((.+?), (.+?)\)/) location.longitude = coords[1] location.latitude = coords[2] location end end end
So now I can use graticule and specify an address such as:
g = Graticule::LocalSearchMapsGeocoder.new location = g.locate '14, Avenue Claude Vellefaux', :zip => '75010', :city => 'Paris', :country => 'France'
and find out that that address is at latitude 48.873016, longitude 2.369934. Unfortunately it is still quite a limited service; when I threw in some addresses in China, Malaysia and even New Zealand I got Graticule::AddressError exceptions. But such is the current state of geocoding, and getting some of Western Europe on top of North America is progress.
Another extension for graticule that might be nice would be a way to specify countries that a geocoder does/doesn’t support. That way the gem could intelligently choose a geocoder that will work out for the specified address.
Update (March 21st): This code is now included in the latest release of the graticule gem.
Rails Geo Plugins: acts_as_geocodable
Mar 7th
acts_as_geocodable (blog entry, repository) is the newest kid on the rails geo plugin block. It actually consists of two parts, a gem called graticule which handles the actual geocoding, interacting with external services, etc, and the plugin which offers extensions to your models.
I like that separation. Having the generalised code in a gem and the rails-specific hooks in a plugin makes a lot of sense and makes it much easier to use the core code in non-rails ruby apps, and having a single gem that supports multiple services allows for built-in failover should the preferred geocoder be unavailable.
Much of the functionality of the plugin is already integrated into my application, but not with quite so many options. In such cases I really enjoy installing plugins; there’s something very satisfying about going through my application deleting code.
The plugin adds two tables to your database. The first, geocodes, holds longitudes/latitudes for given addresses, while the other, geocodings, polymorphically links those geocodes to your existing models. In my case, this meant re-geocoding all the locations already in my database, but since I’m operating on a fairly small data set, that was a pretty simple case of iterating across them all and re-saving them. For those operating with very large databases, you may want to write a more sophisticated migration to handle that.
The trickiest thing was re-coding my search queries to use the new database. acts_as_geocodable offers a number of neat methods for running queries such as (from their documentation)
event.distance_to "49423" Event.find(:all, :within => 50, :origin => "97232") Event.find(:nearest, :origin => "Portland, OR")
But I wanted a way to build more sophisticated searches so I could, say, limit by title and order by distance. It turns out that’s pretty easy too:
Location.find(:all, :origin => 'Grand Rapids MI', :conditions => ['title LIKE ?', 'My Title'] :order => 'distance')
The one place where I had a problem was when trying to use the last of the examples.
Location.find(:nearest, :origin => 'Portland, OR')
blew up with:
ActiveRecord::StatementInvalid: Mysql::Error: Incorrect parameter count in the call to native function 'RADIANS': SELECT locations.*, geocodes.*, (ACOS( SIN(RADIANS()) * SIN(RADIANS(geocodes.latitude)) + COS(RADIANS()) * COS(RADIANS(geocodes.latitude)) * COS(RADIANS(geocodes.longitude) - RADIANS()) ) * 3963.1676) AS distance FROM locations JOIN geocodings ON locations.id = geocodings.geocodable_id AND geocodings.geocodable_type = 'Location' JOIN geocodes ON geocodings.geocode_id = geocodes.id ORDER BY distance ASC LIMIT 1
but when I used a full address (my home) in the same query, I got an appropriate result. It looks as though perhaps if it fails to get an appropriate pair of co-ordinates for the specified location, it tries to perform the query anyway, with an exception resulting.
I also found some problems when trying to use the plugin with locations outside North America, but that is a limitation of the geocoding services and not of the gem or plugin themselves. Hasten the day when enough data is open that global geocoding services can become a reality.
Working with acts_as_geocodable has so far been a very straightforward experience and has allowed me to rid my code of some pieces I’d always meant to refactor out. It’d be good to see the error I ran into handled more neatly, and perhaps an obvious API to take advantage of the failover options presented by graticule, but the plugin is still early in its life and shows a lot of promise.
Comparing rails geo-plugins
Mar 4th
There seems to be quite a plethora of Ruby/Rails libraries appearing aiming to simplify handling geography and distances. In some cases these libraries do quite distinct things (zip codes vs. longitude/latitude, map output vs. distance calculations) but they’re frequently lumped together and it’s difficult to tell which will be best to use in your projects.
I’ve used several of these projects and have previously blogged about YM4R and acts_as_locateable, but I’m still not sure which I’d pick for new projects. So I thought it would be helpful to try to put together a comparison of which libraries offer what functionality. Here I’ll just offer a quick chart, but I’m hoping to write them up in a bit more detail over the coming days/weeks. If there’s sufficient interest, I’d consider moving this out to a wiki for more general use.
| Auto-geocoding ActiveRecord models | Multi-provider geo-coding | Distance based finds | Distance based :through finds | Google map output | Yahoo map output | |
|---|---|---|---|---|---|---|
| GeoKit (plugin index) |
Y | Y | Y | N | N | N |
|
acts_as_geocodable ( plugin index) |
Y | Y* | Y | N | N | N |
|
acts_as_geocode (plugin index) |
Website currently down | |||||
|
actsaslocateable (plugin index) |
N | N | Y | Y | N | N |
|
YM4R (plugin index) |
N | N | N | N | Y | Y |
(* through companion gem)
Also worth mentioning are SpatialAdapter and GeoRuby (from the same developer as YM4R) which respectively provide ActiveRecord support for MySql Spatial and PostGIS geometric columns, and ruby data types for that information.
A recent addition is this Ruby library for the Geonames API which provides a nice way to interface with the GeoNames database of 2.2 million populated places. Properly harnessed, that service begins to make it possible to allow your users to describe their location in natural language and convert that into machine-parseable co-ordinates.
Update (8th March): Updated versions of this comparison will now appear on the foss4r wiki
Update (October 2008): The chart now lives on a separate page within this site.