3 This plugin provides key functionality for location-oriented Rails applications:
5 - Distance calculations, for both flat and spherical environments. For example,
6 given the location of two points on the earth, you can calculate the miles/KM
8 - ActiveRecord distance-based finders. For example, you can find all the points
9 in your database within a 50-mile radius.
10 - Geocoding from multiple providers. It currently supports Google, Yahoo,
11 Geocoder.us, and Geocoder.ca geocoders, and it provides a uniform response
12 structure from all of them. It also provides a fail-over mechanism, in case
13 your input fails to geocode in one service.
14 - IP-based location lookup utilizing hostip.info. Provide an IP address, and get
15 city name and latitude/longitude in return
16 - A before_filter helper to geocoder the user's location based on IP address,
17 and retain the location in a cookie.
19 The goal of this plugin is to provide the common functionality for location-oriented
20 applications (geocoding, location lookup, distance calculation) in an easy-to-use
23 ## A NOTE ON TERMINOLOGY
25 Throughout the code and API of this, latitude and longitude are referred to as lat
26 and lng. We've found over the long term the abbreviation saves lots of typing time.
28 ## DISTANCE CALCULATIONS AND QUERIES
30 If you want only distance calculation services, you need only mix in the Mappable
34 include GeoKit::Mappable
37 After doing so, you can do things like:
39 Location.distance_between(from, to)
41 with optional parameters :units and :formula. Values for :units can be :miles or
42 :kms with :miles as the default. Values for :formula can be :sphere or :flat with
43 :sphere as the default. :sphere gives you Haversine calculations, while :flat
44 gives the Pythagoreum Theory. These defaults persist through out the plug-in.
48 location.distance_to(other)
50 The real power and utility of the plug-in is in its query support. This is
51 achieved through mixing into an ActiveRecord model object:
53 class Location < ActiveRecord::Base
57 The plug-in uses the above-mentioned defaults, but can be modified to use
58 different units and a different formulae. This is done through the :default_units
59 and :default_formula keys which accept the same values as mentioned above.
61 The plug-in creates a calculated column and potentially a calculated condition.
62 By default, these are known as "distance" but this can be changed through the
63 :distance_field_name key.
65 So, an alternative invocation would look as below:
67 class Location < ActiveRecord::Base
68 acts_as_mappable :default_units => :kms,
69 :default_formula => :flat,
70 :distance_field_name => :distance
73 You can also define alternative column names for latitude and longitude using
74 the :lat_column_name and :lng_column_name keys. The defaults are 'lat' and
77 Thereafter, a set of finder methods are made available. Below are the
78 different combinations:
80 Origin as a two-element array of latititude/longitude:
82 find(:all, :origin => [37.792,-122.393])
84 Origin as a geocodeable string:
86 find(:all, :origin => '100 Spear st, San Francisco, CA')
88 Origin as an object which responds to lat and lng methods,
89 or latitude and longitude methods, or whatever methods you have
90 specified for lng_column_name and lat_column_name:
92 find(:all, :origin=>my_store) # my_store.lat and my_store.lng methods exist
94 Often you will need to find within a certain distance. The prefered syntax is:
96 find(:all, :origin => @somewhere, :within => 5)
98 . . . however these syntaxes will also work:
100 find_within(5, :origin => @somewhere)
101 find(:all, :origin => @somewhere, :conditions => "distance < 5")
103 Note however that the third form should be avoided. With either of the first two,
104 GeoKit automatically adds a bounding box to speed up the radial query in the database.
105 With the third form, it does not.
107 If you need to combine distance conditions with other conditions, you should do
110 find(:all, :origin => @somewhere, :within => 5, :conditions=>['state=?',state])
112 If :origin is not provided in the finder call, the find method
113 works as normal. Further, the key is removed
114 from the :options hash prior to invoking the superclass behavior.
116 Other convenience methods work intuitively and are as follows:
118 find_within(distance, :origin => @somewhere)
119 find_beyond(distance, :origin => @somewhere)
120 find_closest(:origin => @somewhere)
121 find_farthest(:origin => @somewhere)
123 where the options respect the defaults, but can be overridden if
126 Lastly, if all that is desired is the raw SQL for distance
127 calculations, you can use the following:
129 distance_sql(origin, units=default_units, formula=default_formula)
131 Thereafter, you are free to use it in find_by_sql as you wish.
133 There are methods available to enable you to get the count based upon
134 the find condition that you have provided. These all work similarly to
135 the finders. So for instance:
137 count(:origin, :conditions => "distance < 5")
138 count_within(distance, :origin => @somewhere)
139 count_beyond(distance, :origin => @somewhere)
141 ## FINDING WITHIN A BOUNDING BOX
143 If you are displaying points on a map, you probably need to query for whatever falls within the rectangular bounds of the map:
145 Store.find :all, :bounds=>[sw_point,ne_point]
147 The input to :bounds can be array with the two points or a Bounds object. However you provide them, the order should always be the southwest corner, northeast corner of the rectangle. Typically, you will be getting the sw_point and ne_point from a map that is displayed on a web page.
149 If you need to calculate the bounding box from a point and radius, you can do that:
151 bounds=Bounds.from_point_and_radius(home,5)
152 Store.find :all, :bounds=>bounds
156 You can use includes along with your distance finders:
158 stores=Store.find :all, :origin=>home, :include=>[:reviews,:cities] :within=>5, :order=>'distance'
160 *However*, ActiveRecord drops the calculated distance column when you use include. So, if you need to
161 use the distance column, you'll have to re-calculate it post-query in Ruby:
163 stores.sort_by_distance_from(home)
165 In this case, you may want to just use the bounding box
166 condition alone in your SQL (there's no use calculating the distance twice):
168 bounds=Bounds.from_point_and_radius(home,5)
169 stores=Store.find :all, :include=>[:reviews,:cities] :bounds=>bounds
170 stores.sort_by_distance_from(home)
174 You can obtain the location for an IP at any time using the geocoder
175 as in the following example:
177 location = IpGeocoder.geocode('12.215.42.19')
179 where Location is a GeoLoc instance containing the latitude,
180 longitude, city, state, and country code. Also, the success
183 If the IP cannot be geocoded, a GeoLoc instance is returned with a
184 success value of false.
186 It should be noted that the IP address needs to be visible to the
187 Rails application. In other words, you need to ensure that the
188 requesting IP address is forwarded by any front-end servers that
189 are out in front of the Rails app. Otherwise, the IP will always
190 be that of the front-end server.
192 ## IP GEOCODING HELPER
194 A class method called geocode_ip_address has been mixed into the
195 ActionController::Base. This enables before_filter style lookup of
196 the IP address. Since it is a filter, it can accept any of the
197 available filter options.
201 class LocationAwareController < ActionController::Base
205 A first-time lookup will result in the GeoLoc class being stored
206 in the session as :geo_location as well as in a cookie called
207 :geo_session. Subsequent lookups will use the session value if it
208 exists or the cookie value if it doesn't exist. The last resort is
209 to make a call to the web service. Clients are free to manage the
212 The intent of this feature is to be able to provide a good guess as
213 to a new visitor's location.
215 ## INTEGRATED FIND AND GEOCODING
217 Geocoding has been integrated with the finders enabling you to pass
218 a physical address or an IP address. This would look the following:
220 Location.find_farthest(:origin => '217.15.10.9')
221 Location.find_farthest(:origin => 'Irving, TX')
223 where the IP or physical address would be geocoded to a location and
224 then the resulting latitude and longitude coordinates would be used
225 in the find. This is not expected to be common usage, but it can be
230 GeoKit can geocode addresses using multiple geocodeing web services.
231 Currently, GeoKit supports Google, Yahoo, and Geocoder.us geocoding
234 These geocoder services are made available through three classes:
235 GoogleGeocoder, YahooGeocoder, and UsGeocoder. Further, an additional
236 geocoder class called MultiGeocoder incorporates an ordered failover
237 sequence to increase the probability of successful geocoding.
239 All classes are called using the following signature:
241 include GeoKit::Geocoders
242 location = XxxGeocoder.geocode(address)
244 where you replace Xxx Geocoder with the appropriate class. A GeoLoc
245 instance is the result of the call. This class has a "success"
246 attribute which will be true if a successful geocoding occurred.
247 If successful, the lat and lng properties will be populated.
249 Geocoders are named with the naming convention NameGeocoder. This
250 naming convention enables Geocoder to auto-detect its sub-classes
251 in order to create methods called name_geocoder(address) so that
252 all geocoders are called through the base class. This is done
253 purely for convenience; the individual geocoder classes are expected
254 to be used independently.
256 The MultiGeocoder class requires the configuration of a provider
257 order which dictates what order to use the various geocoders. Ordering
258 is done through the PROVIDER_ORDER constant found in environment.rb.
260 On installation, this plugin appends a template for your API keys to
263 Make sure your failover configuration matches the usage characteristics
264 of your application -- for example, if you routinely get bogus input to
265 geocode, your code will be much slower if you have to failover among
266 multiple geocoders before determining that the input was in fact bogus.
268 The Geocoder.geocode method returns a GeoLoc object. Basic usage:
270 loc=Geocoder.geocode('100 Spear St, San Francisco, CA')
274 puts loc.full_address
277 ## INTEGRATED FIND WITH ADDRESS GEOCODING
279 Just has you can pass an IP address directly into an ActiveRecord finder
280 as the origin, you can also pass a physical address as the origin:
282 Location.find_closest(:origin => '100 Spear st, San Francisco, CA')
284 where the physical address would be geocoded to a location and then the
285 resulting latitude and longitude coordinates would be used in the
288 Note that if the address fails to geocode, the find method will raise an
289 ActiveRecord::GeocodeError you must be prepared to catch. Alternatively,
290 You can geocoder the address beforehand, and pass the resulting lat/lng
291 into the finder if successful.
295 If your geocoding needs are simple, you can tell your model to automatically
296 geocode itself on create:
298 class Store < ActiveRecord::Base
299 acts_as_mappable :auto_geocode=>true
302 It takes two optional params:
304 class Store < ActiveRecord::Base
305 acts_as_mappable :auto_geocode=>{:field=>:address, :error_message=>'Could not geocode address'}
308 . . . which is equivilent to:
310 class Store << ActiveRecord::Base
312 before_validation_on_create :geocode_address
316 geo=GeoKit::Geocoders::MultiGeocoder.geocode (address)
317 errors.add(:address, "Could not Geocode address") if !geo.success
318 self.lat, self.lng = geo.lat,geo.lng if geo.success
322 If you need any more complicated geocoding behavior for your model, you should roll your own
323 before_validate callback.
326 ## Distances, headings, endpoints, and midpoints
328 distance=home.distance_from(work, :units=>:miles)
329 heading=home.heading_to(work) # result is in degrees, 0 is north
330 endpoint=home.endpoint(90,2) # two miles due east
331 midpoing=home.midpoint_to(work)
333 ## Cool stuff you can do with bounds
335 bounds=Bounds.new(sw_point,ne_point)
336 bounds.contains?(home)
341 =================================================================================
343 ## How to install the GeoKit plugin
345 ruby script/plugin install svn://rubyforge.org/var/svn/geokit/trunk
346 or, to install as an external (your project must be version controlled):
347 ruby script/plugin install -x svn://rubyforge.org/var/svn/geokit/trunk
349 ## How to find all stores within a 10-mile radius of a given lat/lng
350 1. ensure your stores table has lat and lng columns with numeric or float
351 datatypes to store your latitude/longitude
353 3. use acts_as_mappable on your store model:
354 class Store < ActiveRecord::Base
358 3. finders now have extra capabilities:
359 Store.find(:all, :origin =>[32.951613,-96.958444], :within=>10)
361 ## How to geocode an address
363 1. configure your geocoder key(s) in environment.rb
365 2. also in environment.rb, make sure that PROVIDER_ORDER reflects the
366 geocoder(s). If you only want to use one geocoder, there should
367 be only one symbol in the array. For example:
368 PROVIDER_ORDER=[:google]
370 3. Test it out in script/console
371 include GeoKit::Geocoders
372 res = MultiGeocoder.geocode('100 Spear St, San Francisco, CA')
375 puts res.full_address
376 ... etc. The return type is GeoLoc, see the API for
377 all the methods you can call on it.
379 ## How to find all stores within 10 miles of a given address
381 1. as above, ensure your table has the lat/lng columns, and you've
382 applied acts_as_mappable to the Store model.
384 2. configure and test out your geocoder, as above
386 3. pass the address in under the :origin key
387 Store.find(:all, :origin=>'100 Spear st, San Francisco, CA',
390 4. you can also use a zipcode, or anything else that's geocodable:
391 Store.find(:all, :origin=>'94117',
392 :conditions=>'distance<10')
394 ## How to sort a query by distance from an origin
396 You now have access to a 'distance' column, and you can use it
397 as you would any other column. For example:
398 Store.find(:all, :origin=>'94117', :order=>'distance')
400 ## How to elements of an array according to distance from a common point
402 Usually, you can do your sorting in the database as part of your find call.
403 If you need to sort things post-query, you can do so:
405 stores=Store.find :all
406 stores.sort_by_distance_from(home)
407 puts stores.first.distance
409 Obviously, each of the items in the array must have a latitude/longitude so
410 they can be sorted by distance.
413 HIGH-LEVEL NOTES ON WHAT'S WHERE
414 =================================================================================
416 acts_as_mappable.rb, as you'd expect, contains the ActsAsMappable
417 module which gets mixed into your models to provide the
418 location-based finder goodness.
420 mappable.rb contains the Mappable module, which provides basic
421 distance calculation methods, i.e., calculating the distance
424 mappable.rb also contains LatLng, GeoLoc, and Bounds.
425 LatLng is a simple container for latitude and longitude, but
426 it's made more powerful by mixing in the above-mentioned Mappable
427 module -- therefore, you can calculate easily the distance between two
428 LatLng ojbects with distance = first.distance_to(other)
430 GeoLoc (also in mappable.rb) represents an address or location which
431 has been geocoded. You can get the city, zipcode, street address, etc.
432 from a GeoLoc object. GeoLoc extends LatLng, so you also get lat/lng
433 AND the Mappable modeule goodness for free.
435 geocoders.rb contains the geocoder classes.
437 ip_geocode_lookup.rb contains the before_filter helper method which
438 enables auto lookup of the requesting IP address.
440 ## IMPORTANT NOTE: We have appended to your environment.rb file
442 Installation of this plugin has appended an API key template
443 to your environment.rb file. You *must* add your own keys for the various
444 geocoding services if you want to use geocoding. If you need to refer to the original
445 template again, see the api_keys_template file in the root of the plugin.