a work on process

Viewing posts tagged: acts_as_locateable

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.

Rails Geo Plugins
  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

Recommend this post:

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

 

The Array Argument (aka. *)

9 February 2007 (9:47 am)

By James Stewart
Filed under: Notes
Tagged: , , , ,

Wednesday’s post on acts_as_locateable didn’t do much to explain what the patch to the plugin’s methods was doing to allow us to pass extra arguments to ActiveRecord#find. The secret is in the *, or array argument.

A normal method will have a fixed number of arguments:

def simple_method(first, second, third)
  puts "#{first} : #{second} : #{third}"
end
 
simple_method('one', 'two', 'three')
 
>> one : two : three

and sometimes we can develop that by allowing default values for those arguments:

def simple_method(first, second, third = 'four')
  puts "#{first} : #{second} : #{third}"
end
 
simple_method('one', 'two')
 
>> one : two : four

but sometimes we want to allow an arbitrary number of arguments, and that’s where * (the Array Argument) comes in.

def simple_method(first, *others)
  puts "#{first} : " + others.join(' : ')
end
 
simple_method('one', 'two', 'three')
 
>> one : two : three

If you pass in keyed parameters then you will get a hash within the others array:

def simple_method(*others)
  puts others.inspect
end
 
simple_method('one', :two => 'three')
>> ["one", {:two=>"three"}]

So in the case of acts_as_locateable, the find_within_radius can take an arbitrary number of parameters to allow for different types of input:

def find_within_radius(radius_in_miles, *args)

All we needed to do to allow in more parameters was to extract our extra arguments which I knew would be grouped together in a Hash, so that the rest of the internals would still get the input they’re expecting:

# standard_args will be the args input minus any arguments of type Hash
# (our addition to the input)
standard_args = args.reject { |arg| arg.kind_of?(Hash) }
 
# query_args will then be the first Hash found in the arguments
query_args = (args - standard_args).first

and then we can pass our query_args hash to the find call later on:

find(:all, query_args)

Recommend this post:

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

 

Extending acts_as_locateable

7 February 2007 (4:16 pm)

By James Stewart
Filed under: Notes
Tagged: , , , ,

There have been quite a few geographically-themed Rails plugins emerging over the past few months and I decided it was time to try out acts_as_locateable.

Acts_as_locateable is based on ZipCodeSearch. It loads in a database mapping US zip codes to coordinates and then adds convenience methods to ActiveRecord objects that let you search by distance. eg.

Event.find_within_radius(50, '49503')

will return all events within 50 miles of me.

What the standard plugin doesn’t allow is the passing in of more search parameters. So if I wanted to limit that search to future events I’d have to retrieve all the results and iterate over them. In a large system that could be very inefficient.

It turns out it’s not too hard to hack the plugin to make it accept other ActiveRecord parameters. By just adding a couple more checks to the parameter decoding, and adding an extra argument to the call to find. For my use it turned out to be easier to patch the original than re-open the class and add extra methods, though that would obviously be possible. For anyone who may want it, the patch is:

Index: lib/acts_as_locateable.rb
===================================================================
--- lib/acts_as_locateable.rb   (revision 29)
+++ lib/acts_as_locateable.rb   (working copy)
@@ -48,9 +48,13 @@
         # Find instances of the locatable class within radius_in_miles of your target.
         # Target can be specified in a number of ways, see expand_radius_args for options
         def find_within_radius(radius_in_miles, *args)
-          lat, lon = expand_radius_args(args)
+          standard_args = args.reject { |arg| arg.kind_of?(Hash) }
+          query_args = (args - standard_args).first
+          
+          lat, lon = expand_radius_args(standard_args)
+          # query_args = args.find { |arg| arg.kind_of?(Hash) }
           within_radius_scope(radius_in_miles, lat, lon) do
-            find(:all)
+            find(:all, query_args)
           end
         end

Now I can use:

Event.find_within_radius(50, '49503', 
	:conditions => ['starts_at > ?', DateTime.now], 
	:order => 'starts_at')

and get much more useful results.

Recommend this post:

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]