]> projects.mako.cc - selectricity-live/blob - vendor/plugins/geokit/README
Merged from John.
[selectricity-live] / vendor / plugins / geokit / README
1 ## FEATURE SUMMARY
2
3 This plugin provides key functionality for location-oriented Rails applications:
4
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 
7   between them.
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.
18   
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 
21 package.
22
23 ## A NOTE ON TERMINOLOGY
24
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.
27
28 ## DISTANCE CALCULATIONS AND QUERIES
29
30 If you want only distance calculation services, you need only mix in the Mappable 
31 module like so:
32
33     class Location
34         include GeoKit::Mappable
35     end
36
37 After doing so, you can do things like:
38
39     Location.distance_between(from, to) 
40
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.
45
46 You can also do:
47
48     location.distance_to(other)
49
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:
52
53     class Location < ActiveRecord::Base
54         acts_as_mappable
55     end
56
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.
60
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.  
64
65 So, an alternative invocation would look as below:
66
67     class Location < ActiveRecord::Base
68        acts_as_mappable :default_units => :kms, 
69                         :default_formula => :flat, 
70                         :distance_field_name => :distance
71     end
72
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
75 'lng' respectively.
76
77 Thereafter, a set of finder methods are made available.  Below are the 
78 different combinations:
79
80 Origin as a two-element array of latititude/longitude:
81
82                 find(:all, :origin => [37.792,-122.393])
83
84 Origin as a geocodeable string:
85
86                 find(:all, :origin => '100 Spear st, San Francisco, CA')
87
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:
91
92                 find(:all, :origin=>my_store) # my_store.lat and my_store.lng methods exist
93
94 Often you will need to find within a certain distance. The prefered syntax is:
95
96     find(:all, :origin => @somewhere, :within => 5)
97     
98 . . . however these syntaxes will also work:
99
100     find_within(5, :origin => @somewhere)
101     find(:all, :origin => @somewhere, :conditions => "distance < 5")
102     
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.
106
107 If you need to combine distance conditions with other conditions, you should do
108 so like this:
109
110     find(:all, :origin => @somewhere, :within => 5, :conditions=>['state=?',state])
111
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.
115
116 Other convenience methods work intuitively and are as follows:
117
118     find_within(distance, :origin => @somewhere)
119     find_beyond(distance, :origin => @somewhere)
120     find_closest(:origin => @somewhere)
121     find_farthest(:origin => @somewhere)
122
123 where the options respect the defaults, but can be overridden if 
124 desired.  
125
126 Lastly, if all that is desired is the raw SQL for distance 
127 calculations, you can use the following:
128
129     distance_sql(origin, units=default_units, formula=default_formula)
130
131 Thereafter, you are free to use it in find_by_sql as you wish.
132
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:
136
137     count(:origin, :conditions => "distance < 5")
138     count_within(distance, :origin => @somewhere)
139     count_beyond(distance, :origin => @somewhere)
140
141 ## FINDING WITHIN A BOUNDING BOX
142  
143 If you are displaying points on a map, you probably need to query for whatever falls within the rectangular bounds of the map:
144
145     Store.find :all, :bounds=>[sw_point,ne_point]
146
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.
148
149 If you need to calculate the bounding box from a point and radius, you can do that:
150
151     bounds=Bounds.from_point_and_radius(home,5)
152     Store.find :all, :bounds=>bounds
153
154 ## USING INCLUDES
155
156 You can use includes along with your distance finders:
157
158     stores=Store.find :all, :origin=>home, :include=>[:reviews,:cities] :within=>5, :order=>'distance'
159
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:
162
163     stores.sort_by_distance_from(home)
164
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):
167
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)
171
172 ## IP GEOCODING
173
174 You can obtain the location for an IP at any time using the geocoder
175 as in the following example:
176
177     location = IpGeocoder.geocode('12.215.42.19')
178
179 where Location is a GeoLoc instance containing the latitude, 
180 longitude, city, state, and country code.  Also, the success 
181 value is true.
182
183 If the IP cannot be geocoded, a GeoLoc instance is returned with a
184 success value of false.  
185
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.
191
192 ## IP GEOCODING HELPER
193
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.
198
199 Usage is as below:
200
201     class LocationAwareController < ActionController::Base
202       geocode_ip_address
203     end
204
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
210 cookie as they wish.
211
212 The intent of this feature is to be able to provide a good guess as
213 to a new visitor's location.
214
215 ## INTEGRATED FIND AND GEOCODING
216
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:
219
220     Location.find_farthest(:origin => '217.15.10.9')
221     Location.find_farthest(:origin => 'Irving, TX')
222
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
226 done nevertheless.
227
228 ## ADDRESS GEOCODING
229
230 GeoKit can geocode addresses using multiple geocodeing web services.
231 Currently, GeoKit supports Google, Yahoo, and Geocoder.us geocoding 
232 services. 
233
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.
238
239 All classes are called using the following signature:
240
241     include GeoKit::Geocoders
242     location = XxxGeocoder.geocode(address)
243
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.
248
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.
255
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.
259
260 On installation, this plugin appends a template for your API keys to 
261 your environment.rb. 
262
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. 
267
268 The Geocoder.geocode method returns a GeoLoc object. Basic usage:
269
270     loc=Geocoder.geocode('100 Spear St, San Francisco, CA')
271     if loc.success
272       puts loc.lat
273       puts loc.lng
274       puts loc.full_address
275     end
276
277 ## INTEGRATED FIND WITH ADDRESS GEOCODING
278
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:
281
282     Location.find_closest(:origin => '100 Spear st, San Francisco, CA')
283
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 
286 find. 
287
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.
292
293 ## Auto Geocoding
294
295 If your geocoding needs are simple, you can tell your model to automatically
296 geocode itself on create:
297
298     class Store < ActiveRecord::Base
299       acts_as_mappable :auto_geocode=>true
300     end
301
302 It takes two optional params:
303
304     class Store < ActiveRecord::Base
305       acts_as_mappable :auto_geocode=>{:field=>:address, :error_message=>'Could not geocode address'}
306     end
307
308 . . . which is equivilent to:
309
310     class Store << ActiveRecord::Base
311       acts_as_mappable
312       before_validation_on_create :geocode_address
313
314       private
315       def 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
319       end
320     end
321     
322 If you need any more complicated geocoding behavior for your model, you should roll your own 
323 before_validate callback.
324
325
326 ## Distances, headings, endpoints, and midpoints
327
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)
332
333 ## Cool stuff you can do with bounds
334     
335     bounds=Bounds.new(sw_point,ne_point)
336     bounds.contains?(home)
337     puts bounds.center
338     
339
340 HOW TO . . .
341 =================================================================================
342
343 ## How to install the GeoKit plugin 
344     cd [APP_ROOT]
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
348
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
352
353 3. use acts_as_mappable on your store model:
354     class Store < ActiveRecord::Base
355        acts_as_mappable
356        ...
357     end
358 3. finders now have extra capabilities:
359     Store.find(:all, :origin =>[32.951613,-96.958444], :within=>10)
360
361 ## How to geocode an address
362
363 1. configure your geocoder key(s) in environment.rb
364
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]
369    
370 3. Test it out in script/console
371     include GeoKit::Geocoders
372     res = MultiGeocoder.geocode('100 Spear St, San Francisco, CA')
373     puts res.lat
374     puts res.lng
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.
378
379 ## How to find all stores within 10 miles of a given address
380
381 1. as above, ensure your table has the lat/lng columns, and you've
382    applied acts_as_mappable to the Store model.
383
384 2. configure and test out your geocoder, as above
385
386 3. pass the address in under the :origin key
387                 Store.find(:all, :origin=>'100 Spear st, San Francisco, CA', 
388                            :within=>10)
389
390 4. you can also use a zipcode, or anything else that's geocodable:
391                 Store.find(:all, :origin=>'94117', 
392                            :conditions=>'distance<10')
393
394 ## How to sort a query by distance from an origin
395
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')
399
400 ## How to elements of an array according to distance from a common point
401
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:
404
405     stores=Store.find :all
406     stores.sort_by_distance_from(home)
407     puts stores.first.distance
408     
409 Obviously, each of the items in the array must have a latitude/longitude so
410 they can be sorted by distance.
411
412
413 HIGH-LEVEL NOTES ON WHAT'S WHERE
414 =================================================================================
415
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.
419
420 mappable.rb contains the Mappable module, which provides basic
421 distance calculation methods, i.e., calculating the distance
422 between two points. 
423
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)
429
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.
434
435 geocoders.rb contains the geocoder classes.
436
437 ip_geocode_lookup.rb contains the before_filter helper method which
438 enables auto lookup of the requesting IP address.
439
440 ## IMPORTANT NOTE: We have appended to your environment.rb file
441
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.

Benjamin Mako Hill || Want to submit a patch?