Rails Geo Plugins: GeoKit

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  :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  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)

Tags: , , , , ,

4 comments

  1. Out of interest, I’m curious about your comment on the default_units accessor and the idea of localization. There are two ways you could reset the default_units value:

    # Less likely to be used; but definitely changes your default:
    BusStop.default_units = :kms

    # More likely to be used; enables you to request the units you want used.
    BusStop.find(:nearest, :origin => ‘1319 Fulton St. East, Grand Rapids MI 49503’, :units => :kms)

    Is there a better, more convenient way that you have in mind?

  2. Ah, okay. I hadn’t seen those options and either of those would do what I wanted. I thought it had to be specified in the acts_as_mappable declaration in the model. What I was thinking would be nice would be a ‘distance’ method like:

    stops = BusStop.find(:all, :origin => ‘1319 Fulton St. East, Grand Rapids MI 49503’, :order => ‘distance’)
    distance_in_km = stops.first.distance(:km)
    distance_in_miles = stops.first.distance(:miles)

  3. […] been a while since I tinkered with GeoKit, but when I read this post, I thought I’d take a look at a couple of things that were […]

  4. FYI, GeoKit still does not have :include support. Just ran across this myself while trying it out. FANTASTIC plugin though. I hope :include support arrives soon… my database is going to have to take a beating in the meantime otherwise.