Commit 26982e37628b31c14f7701a3b92b9fe4b5b31af4
1 parent
76644e68
Exists in
master
and in
28 other branches
ActionItem261: added the geokit plugin
git-svn-id: https://svn.colivre.coop.br/svn/noosfero/trunk@1650 3f533792-8f58-4932-b0fe-aaf55b0a4547
Showing
32 changed files
with
3287 additions
and
0 deletions
Show diff stats
... | ... | @@ -0,0 +1,20 @@ |
1 | +Copyright (c) 2007 Bill Eisenhauer & Andre Lewis | |
2 | + | |
3 | +Permission is hereby granted, free of charge, to any person obtaining | |
4 | +a copy of this software and associated documentation files (the | |
5 | +"Software"), to deal in the Software without restriction, including | |
6 | +without limitation the rights to use, copy, modify, merge, publish, | |
7 | +distribute, sublicense, and/or sell copies of the Software, and to | |
8 | +permit persons to whom the Software is furnished to do so, subject to | |
9 | +the following conditions: | |
10 | + | |
11 | +The above copyright notice and this permission notice shall be | |
12 | +included in all copies or substantial portions of the Software. | |
13 | + | |
14 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
15 | +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
16 | +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
17 | +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |
18 | +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
19 | +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
20 | +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
0 | 21 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,451 @@ |
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 | +Database Compatability | |
413 | +================================================================================= | |
414 | +GeoKit does *not* work with SQLite, as it lacks the necessary geometry functions. | |
415 | +GeoKit works with MySQL (tested with version 5.0.41) or PostgreSQL (tested with version 8.2.6) | |
416 | +GeoKit is known to *not* work with Postgres <8.1 -- it uses the least() funciton. | |
417 | + | |
418 | + | |
419 | +HIGH-LEVEL NOTES ON WHAT'S WHERE | |
420 | +================================================================================= | |
421 | + | |
422 | +acts_as_mappable.rb, as you'd expect, contains the ActsAsMappable | |
423 | +module which gets mixed into your models to provide the | |
424 | +location-based finder goodness. | |
425 | + | |
426 | +mappable.rb contains the Mappable module, which provides basic | |
427 | +distance calculation methods, i.e., calculating the distance | |
428 | +between two points. | |
429 | + | |
430 | +mappable.rb also contains LatLng, GeoLoc, and Bounds. | |
431 | +LatLng is a simple container for latitude and longitude, but | |
432 | +it's made more powerful by mixing in the above-mentioned Mappable | |
433 | +module -- therefore, you can calculate easily the distance between two | |
434 | +LatLng ojbects with distance = first.distance_to(other) | |
435 | + | |
436 | +GeoLoc (also in mappable.rb) represents an address or location which | |
437 | +has been geocoded. You can get the city, zipcode, street address, etc. | |
438 | +from a GeoLoc object. GeoLoc extends LatLng, so you also get lat/lng | |
439 | +AND the Mappable modeule goodness for free. | |
440 | + | |
441 | +geocoders.rb contains the geocoder classes. | |
442 | + | |
443 | +ip_geocode_lookup.rb contains the before_filter helper method which | |
444 | +enables auto lookup of the requesting IP address. | |
445 | + | |
446 | +## IMPORTANT NOTE: We have appended to your environment.rb file | |
447 | + | |
448 | +Installation of this plugin has appended an API key template | |
449 | +to your environment.rb file. You *must* add your own keys for the various | |
450 | +geocoding services if you want to use geocoding. If you need to refer to the original | |
451 | +template again, see the api_keys_template file in the root of the plugin. | ... | ... |
... | ... | @@ -0,0 +1,22 @@ |
1 | +require 'rake' | |
2 | +require 'rake/testtask' | |
3 | +require 'rake/rdoctask' | |
4 | + | |
5 | +desc 'Default: run unit tests.' | |
6 | +task :default => :test | |
7 | + | |
8 | +desc 'Test the GeoKit plugin.' | |
9 | +Rake::TestTask.new(:test) do |t| | |
10 | + t.libs << 'lib' | |
11 | + t.pattern = 'test/**/*_test.rb' | |
12 | + t.verbose = true | |
13 | +end | |
14 | + | |
15 | +desc 'Generate documentation for the GeoKit plugin.' | |
16 | +Rake::RDocTask.new(:rdoc) do |rdoc| | |
17 | + rdoc.rdoc_dir = 'rdoc' | |
18 | + rdoc.title = 'GeoKit' | |
19 | + rdoc.options << '--line-numbers' << '--inline-source' | |
20 | + rdoc.rdoc_files.include('README') | |
21 | + rdoc.rdoc_files.include('lib/**/*.rb') | |
22 | +end | |
0 | 23 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,4 @@ |
1 | +01/20/08 Version 1.0.1. Further fix of distance calculation, this time in SQL. Now uses least() function, which is available in MySQL version 3.22.5+ and postgres versions 8.1+ | |
2 | +01/16/08 fixed the "zero-distance" bug (calculating between two points that are the same) | |
3 | +12/11/07 fixed a small but with queries crossing meridian, and also fixed find(:closest) | |
4 | +10/11/07 Fixed Rails2/Edge compatability | |
0 | 5 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,9 @@ |
1 | +author: | |
2 | + name_1: Bill Eisenhauer | |
3 | + homepage_1: http://blog.billeisenhauer.com | |
4 | + name_2: Andre Lewis | |
5 | + homepage_2: http://www.earthcode.com | |
6 | +summary: Geo distance calculations, distance calculation query support, geocoding for physical and ip addresses. | |
7 | +version: 1.0.1 | |
8 | +rails_version: 1.0+ | |
9 | +license: MIT | |
0 | 10 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,50 @@ |
1 | +# These defaults are used in GeoKit::Mappable.distance_to and in acts_as_mappable | |
2 | +GeoKit::default_units = :miles | |
3 | +GeoKit::default_formula = :sphere | |
4 | + | |
5 | +# This is the timeout value in seconds to be used for calls to the geocoder web | |
6 | +# services. For no timeout at all, comment out the setting. The timeout unit | |
7 | +# is in seconds. | |
8 | +GeoKit::Geocoders::timeout = 3 | |
9 | + | |
10 | +# These settings are used if web service calls must be routed through a proxy. | |
11 | +# These setting can be nil if not needed, otherwise, addr and port must be | |
12 | +# filled in at a minimum. If the proxy requires authentication, the username | |
13 | +# and password can be provided as well. | |
14 | +GeoKit::Geocoders::proxy_addr = nil | |
15 | +GeoKit::Geocoders::proxy_port = nil | |
16 | +GeoKit::Geocoders::proxy_user = nil | |
17 | +GeoKit::Geocoders::proxy_pass = nil | |
18 | + | |
19 | +# This is your yahoo application key for the Yahoo Geocoder. | |
20 | +# See http://developer.yahoo.com/faq/index.html#appid | |
21 | +# and http://developer.yahoo.com/maps/rest/V1/geocode.html | |
22 | +GeoKit::Geocoders::yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY' | |
23 | + | |
24 | +# This is your Google Maps geocoder key. | |
25 | +# See http://www.google.com/apis/maps/signup.html | |
26 | +# and http://www.google.com/apis/maps/documentation/#Geocoding_Examples | |
27 | +GeoKit::Geocoders::google = 'REPLACE_WITH_YOUR_GOOGLE_KEY' | |
28 | + | |
29 | +# This is your username and password for geocoder.us. | |
30 | +# To use the free service, the value can be set to nil or false. For | |
31 | +# usage tied to an account, the value should be set to username:password. | |
32 | +# See http://geocoder.us | |
33 | +# and http://geocoder.us/user/signup | |
34 | +GeoKit::Geocoders::geocoder_us = false | |
35 | + | |
36 | +# This is your authorization key for geocoder.ca. | |
37 | +# To use the free service, the value can be set to nil or false. For | |
38 | +# usage tied to an account, set the value to the key obtained from | |
39 | +# Geocoder.ca. | |
40 | +# See http://geocoder.ca | |
41 | +# and http://geocoder.ca/?register=1 | |
42 | +GeoKit::Geocoders::geocoder_ca = false | |
43 | + | |
44 | +# This is the order in which the geocoders are called in a failover scenario | |
45 | +# If you only want to use a single geocoder, put a single symbol in the array. | |
46 | +# Valid symbols are :google, :yahoo, :us, and :ca. | |
47 | +# Be aware that there are Terms of Use restrictions on how you can use the | |
48 | +# various geocoders. Make sure you read up on relevant Terms of Use for each | |
49 | +# geocoder you are going to use. | |
50 | +GeoKit::Geocoders::provider_order = [:google,:us] | |
0 | 51 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,13 @@ |
1 | +# Load modules and classes needed to automatically mix in ActiveRecord and | |
2 | +# ActionController helpers. All other functionality must be explicitly | |
3 | +# required. | |
4 | +require 'geo_kit/defaults' | |
5 | +require 'geo_kit/mappable' | |
6 | +require 'geo_kit/acts_as_mappable' | |
7 | +require 'geo_kit/ip_geocode_lookup' | |
8 | + | |
9 | +# Automatically mix in distance finder support into ActiveRecord classes. | |
10 | +ActiveRecord::Base.send :include, GeoKit::ActsAsMappable | |
11 | + | |
12 | +# Automatically mix in ip geocoding helpers into ActionController classes. | |
13 | +ActionController::Base.send :include, GeoKit::IpGeocodeLookup | ... | ... |
... | ... | @@ -0,0 +1,7 @@ |
1 | +# Display to the console the contents of the README file. | |
2 | +puts IO.read(File.join(File.dirname(__FILE__), 'README')) | |
3 | + | |
4 | +# Append the contents of api_keys_template to the application's environment.rb file | |
5 | +environment_rb = File.open(File.expand_path(File.join(File.dirname(__FILE__), '../../../config/environment.rb')), "a") | |
6 | +environment_rb.puts IO.read(File.join(File.dirname(__FILE__), '/assets/api_keys_template')) | |
7 | +environment_rb.close | ... | ... |
... | ... | @@ -0,0 +1,436 @@ |
1 | +module GeoKit | |
2 | + # Contains the class method acts_as_mappable targeted to be mixed into ActiveRecord. | |
3 | + # When mixed in, augments find services such that they provide distance calculation | |
4 | + # query services. The find method accepts additional options: | |
5 | + # | |
6 | + # * :origin - can be | |
7 | + # 1. a two-element array of latititude/longitude -- :origin=>[37.792,-122.393] | |
8 | + # 2. a geocodeable string -- :origin=>'100 Spear st, San Francisco, CA' | |
9 | + # 3. an object which responds to lat and lng methods, or latitude and longitude methods, | |
10 | + # or whatever methods you have specified for lng_column_name and lat_column_name | |
11 | + # | |
12 | + # Other finder methods are provided for specific queries. These are: | |
13 | + # | |
14 | + # * find_within (alias: find_inside) | |
15 | + # * find_beyond (alias: find_outside) | |
16 | + # * find_closest (alias: find_nearest) | |
17 | + # * find_farthest | |
18 | + # | |
19 | + # Counter methods are available and work similarly to finders. | |
20 | + # | |
21 | + # If raw SQL is desired, the distance_sql method can be used to obtain SQL appropriate | |
22 | + # to use in a find_by_sql call. | |
23 | + module ActsAsMappable | |
24 | + # Mix below class methods into ActiveRecord. | |
25 | + def self.included(base) # :nodoc: | |
26 | + base.extend ClassMethods | |
27 | + end | |
28 | + | |
29 | + # Class method to mix into active record. | |
30 | + module ClassMethods # :nodoc: | |
31 | + # Class method to bring distance query support into ActiveRecord models. By default | |
32 | + # uses :miles for distance units and performs calculations based upon the Haversine | |
33 | + # (sphere) formula. These can be changed by setting GeoKit::default_units and | |
34 | + # GeoKit::default_formula. Also, by default, uses lat, lng, and distance for respective | |
35 | + # column names. All of these can be overridden using the :default_units, :default_formula, | |
36 | + # :lat_column_name, :lng_column_name, and :distance_column_name hash keys. | |
37 | + # | |
38 | + # Can also use to auto-geocode a specific column on create. Syntax; | |
39 | + # | |
40 | + # acts_as_mappable :auto_geocode=>true | |
41 | + # | |
42 | + # By default, it tries to geocode the "address" field. Or, for more customized behavior: | |
43 | + # | |
44 | + # acts_as_mappable :auto_geocode=>{:field=>:address,:error_message=>'bad address'} | |
45 | + # | |
46 | + # In both cases, it creates a before_validation_on_create callback to geocode the given column. | |
47 | + # For anything more customized, we recommend you forgo the auto_geocode option | |
48 | + # and create your own AR callback to handle geocoding. | |
49 | + def acts_as_mappable(options = {}) | |
50 | + # Mix in the module, but ensure to do so just once. | |
51 | + return if self.included_modules.include?(GeoKit::ActsAsMappable::InstanceMethods) | |
52 | + send :include, GeoKit::ActsAsMappable::InstanceMethods | |
53 | + # include the Mappable module. | |
54 | + send :include, Mappable | |
55 | + | |
56 | + # Handle class variables. | |
57 | + cattr_accessor :distance_column_name, :default_units, :default_formula, :lat_column_name, :lng_column_name, :qualified_lat_column_name, :qualified_lng_column_name | |
58 | + self.distance_column_name = options[:distance_column_name] || 'distance' | |
59 | + self.default_units = options[:default_units] || GeoKit::default_units | |
60 | + self.default_formula = options[:default_formula] || GeoKit::default_formula | |
61 | + self.lat_column_name = options[:lat_column_name] || 'lat' | |
62 | + self.lng_column_name = options[:lng_column_name] || 'lng' | |
63 | + self.qualified_lat_column_name = "#{table_name}.#{lat_column_name}" | |
64 | + self.qualified_lng_column_name = "#{table_name}.#{lng_column_name}" | |
65 | + if options.include?(:auto_geocode) && options[:auto_geocode] | |
66 | + # if the form auto_geocode=>true is used, let the defaults take over by suppling an empty hash | |
67 | + options[:auto_geocode] = {} if options[:auto_geocode] == true | |
68 | + cattr_accessor :auto_geocode_field, :auto_geocode_error_message | |
69 | + self.auto_geocode_field = options[:auto_geocode][:field] || 'address' | |
70 | + self.auto_geocode_error_message = options[:auto_geocode][:error_message] || 'could not locate address' | |
71 | + | |
72 | + # set the actual callback here | |
73 | + before_validation_on_create :auto_geocode_address | |
74 | + end | |
75 | + end | |
76 | + end | |
77 | + | |
78 | + # this is the callback for auto_geocoding | |
79 | + def auto_geocode_address | |
80 | + address=self.send(auto_geocode_field) | |
81 | + geo=GeoKit::Geocoders::MultiGeocoder.geocode(address) | |
82 | + | |
83 | + if geo.success | |
84 | + self.send("#{lat_column_name}=", geo.lat) | |
85 | + self.send("#{lng_column_name}=", geo.lng) | |
86 | + else | |
87 | + errors.add(auto_geocode_field, auto_geocode_error_message) | |
88 | + end | |
89 | + | |
90 | + geo.success | |
91 | + end | |
92 | + | |
93 | + # Instance methods to mix into ActiveRecord. | |
94 | + module InstanceMethods #:nodoc: | |
95 | + # Mix class methods into module. | |
96 | + def self.included(base) # :nodoc: | |
97 | + base.extend SingletonMethods | |
98 | + end | |
99 | + | |
100 | + # Class singleton methods to mix into ActiveRecord. | |
101 | + module SingletonMethods # :nodoc: | |
102 | + # Extends the existing find method in potentially two ways: | |
103 | + # - If a mappable instance exists in the options, adds a distance column. | |
104 | + # - If a mappable instance exists in the options and the distance column exists in the | |
105 | + # conditions, substitutes the distance sql for the distance column -- this saves | |
106 | + # having to write the gory SQL. | |
107 | + def find(*args) | |
108 | + prepare_for_find_or_count(:find, args) | |
109 | + super(*args) | |
110 | + end | |
111 | + | |
112 | + # Extends the existing count method by: | |
113 | + # - If a mappable instance exists in the options and the distance column exists in the | |
114 | + # conditions, substitutes the distance sql for the distance column -- this saves | |
115 | + # having to write the gory SQL. | |
116 | + def count(*args) | |
117 | + prepare_for_find_or_count(:count, args) | |
118 | + super(*args) | |
119 | + end | |
120 | + | |
121 | + # Finds within a distance radius. | |
122 | + def find_within(distance, options={}) | |
123 | + options[:within] = distance | |
124 | + find(:all, options) | |
125 | + end | |
126 | + alias find_inside find_within | |
127 | + | |
128 | + # Finds beyond a distance radius. | |
129 | + def find_beyond(distance, options={}) | |
130 | + options[:beyond] = distance | |
131 | + find(:all, options) | |
132 | + end | |
133 | + alias find_outside find_beyond | |
134 | + | |
135 | + # Finds according to a range. Accepts inclusive or exclusive ranges. | |
136 | + def find_by_range(range, options={}) | |
137 | + options[:range] = range | |
138 | + find(:all, options) | |
139 | + end | |
140 | + | |
141 | + # Finds the closest to the origin. | |
142 | + def find_closest(options={}) | |
143 | + find(:nearest, options) | |
144 | + end | |
145 | + alias find_nearest find_closest | |
146 | + | |
147 | + # Finds the farthest from the origin. | |
148 | + def find_farthest(options={}) | |
149 | + find(:farthest, options) | |
150 | + end | |
151 | + | |
152 | + # Finds within rectangular bounds (sw,ne). | |
153 | + def find_within_bounds(bounds, options={}) | |
154 | + options[:bounds] = bounds | |
155 | + find(:all, options) | |
156 | + end | |
157 | + | |
158 | + # counts within a distance radius. | |
159 | + def count_within(distance, options={}) | |
160 | + options[:within] = distance | |
161 | + count(options) | |
162 | + end | |
163 | + alias count_inside count_within | |
164 | + | |
165 | + # Counts beyond a distance radius. | |
166 | + def count_beyond(distance, options={}) | |
167 | + options[:beyond] = distance | |
168 | + count(options) | |
169 | + end | |
170 | + alias count_outside count_beyond | |
171 | + | |
172 | + # Counts according to a range. Accepts inclusive or exclusive ranges. | |
173 | + def count_by_range(range, options={}) | |
174 | + options[:range] = range | |
175 | + count(options) | |
176 | + end | |
177 | + | |
178 | + # Finds within rectangular bounds (sw,ne). | |
179 | + def count_within_bounds(bounds, options={}) | |
180 | + options[:bounds] = bounds | |
181 | + count(options) | |
182 | + end | |
183 | + | |
184 | + # Returns the distance calculation to be used as a display column or a condition. This | |
185 | + # is provide for anyone wanting access to the raw SQL. | |
186 | + def distance_sql(origin, units=default_units, formula=default_formula) | |
187 | + case formula | |
188 | + when :sphere | |
189 | + sql = sphere_distance_sql(origin, units) | |
190 | + when :flat | |
191 | + sql = flat_distance_sql(origin, units) | |
192 | + end | |
193 | + sql | |
194 | + end | |
195 | + | |
196 | + private | |
197 | + | |
198 | + # Prepares either a find or a count action by parsing through the options and | |
199 | + # conditionally adding to the select clause for finders. | |
200 | + def prepare_for_find_or_count(action, args) | |
201 | + options = defined?(args.extract_options!) ? args.extract_options! : extract_options_from_args!(args) | |
202 | + # Obtain items affecting distance condition. | |
203 | + origin = extract_origin_from_options(options) | |
204 | + units = extract_units_from_options(options) | |
205 | + formula = extract_formula_from_options(options) | |
206 | + bounds = extract_bounds_from_options(options) | |
207 | + # if no explicit bounds were given, try formulating them from the point and distance given | |
208 | + bounds = formulate_bounds_from_distance(options, origin, units) unless bounds | |
209 | + # Apply select adjustments based upon action. | |
210 | + add_distance_to_select(options, origin, units, formula) if origin && action == :find | |
211 | + # Apply the conditions for a bounding rectangle if applicable | |
212 | + apply_bounds_conditions(options,bounds) if bounds | |
213 | + # Apply distance scoping and perform substitutions. | |
214 | + apply_distance_scope(options) | |
215 | + substitute_distance_in_conditions(options, origin, units, formula) if origin && options.has_key?(:conditions) | |
216 | + # Order by scoping for find action. | |
217 | + apply_find_scope(args, options) if action == :find | |
218 | + # Unfortunatley, we need to do extra work if you use an :include. See the method for more info. | |
219 | + handle_order_with_include(options,origin,units,formula) if options.include?(:include) && options.include?(:order) && origin | |
220 | + # Restore options minus the extra options that we used for the | |
221 | + # GeoKit API. | |
222 | + args.push(options) | |
223 | + end | |
224 | + | |
225 | + # If we're here, it means that 1) an origin argument, 2) an :include, 3) an :order clause were supplied. | |
226 | + # Now we have to sub some SQL into the :order clause. The reason is that when you do an :include, | |
227 | + # ActiveRecord drops the psuedo-column (specificically, distance) which we supplied for :select. | |
228 | + # So, the 'distance' column isn't available for the :order clause to reference when we use :include. | |
229 | + def handle_order_with_include(options, origin, units, formula) | |
230 | + # replace the distance_column_name with the distance sql in order clause | |
231 | + options[:order].sub!(distance_column_name, distance_sql(origin, units, formula)) | |
232 | + end | |
233 | + | |
234 | + # Looks for mapping-specific tokens and makes appropriate translations so that the | |
235 | + # original finder has its expected arguments. Resets the the scope argument to | |
236 | + # :first and ensures the limit is set to one. | |
237 | + def apply_find_scope(args, options) | |
238 | + case args.first | |
239 | + when :nearest, :closest | |
240 | + args[0] = :first | |
241 | + options[:limit] = 1 | |
242 | + options[:order] = "#{distance_column_name} ASC" | |
243 | + when :farthest | |
244 | + args[0] = :first | |
245 | + options[:limit] = 1 | |
246 | + options[:order] = "#{distance_column_name} DESC" | |
247 | + end | |
248 | + end | |
249 | + | |
250 | + # If it's a :within query, add a bounding box to improve performance. | |
251 | + # This only gets called if a :bounds argument is not otherwise supplied. | |
252 | + def formulate_bounds_from_distance(options, origin, units) | |
253 | + distance = options[:within] if options.has_key?(:within) | |
254 | + distance = options[:range].last-(options[:range].exclude_end?? 1 : 0) if options.has_key?(:range) | |
255 | + if distance | |
256 | + res=GeoKit::Bounds.from_point_and_radius(origin,distance,:units=>units) | |
257 | + else | |
258 | + nil | |
259 | + end | |
260 | + end | |
261 | + | |
262 | + # Replace :within, :beyond and :range distance tokens with the appropriate distance | |
263 | + # where clauses. Removes these tokens from the options hash. | |
264 | + def apply_distance_scope(options) | |
265 | + distance_condition = "#{distance_column_name} <= #{options[:within]}" if options.has_key?(:within) | |
266 | + distance_condition = "#{distance_column_name} > #{options[:beyond]}" if options.has_key?(:beyond) | |
267 | + distance_condition = "#{distance_column_name} >= #{options[:range].first} AND #{distance_column_name} <#{'=' unless options[:range].exclude_end?} #{options[:range].last}" if options.has_key?(:range) | |
268 | + [:within, :beyond, :range].each { |option| options.delete(option) } if distance_condition | |
269 | + | |
270 | + options[:conditions]=augment_conditions(options[:conditions],distance_condition) if distance_condition | |
271 | + end | |
272 | + | |
273 | + # This method lets you transparently add a new condition to a query without | |
274 | + # worrying about whether it currently has conditions, or what kind of conditions they are | |
275 | + # (string or array). | |
276 | + # | |
277 | + # Takes the current conditions (which can be an array or a string, or can be nil/false), | |
278 | + # and a SQL string. It inserts the sql into the existing conditions, and returns new conditions | |
279 | + # (which can be a string or an array | |
280 | + def augment_conditions(current_conditions,sql) | |
281 | + if current_conditions && current_conditions.is_a?(String) | |
282 | + res="#{current_conditions} AND #{sql}" | |
283 | + elsif current_conditions && current_conditions.is_a?(Array) | |
284 | + current_conditions[0]="#{current_conditions[0]} AND #{sql}" | |
285 | + res=current_conditions | |
286 | + else | |
287 | + res=sql | |
288 | + end | |
289 | + res | |
290 | + end | |
291 | + | |
292 | + # Alters the conditions to include rectangular bounds conditions. | |
293 | + def apply_bounds_conditions(options,bounds) | |
294 | + sw,ne=bounds.sw,bounds.ne | |
295 | + lng_sql= bounds.crosses_meridian? ? "(#{qualified_lng_column_name}<#{sw.lng} OR #{qualified_lng_column_name}>#{ne.lng})" : "#{qualified_lng_column_name}>#{sw.lng} AND #{qualified_lng_column_name}<#{ne.lng}" | |
296 | + bounds_sql="#{qualified_lat_column_name}>#{sw.lat} AND #{qualified_lat_column_name}<#{ne.lat} AND #{lng_sql}" | |
297 | + options[:conditions]=augment_conditions(options[:conditions],bounds_sql) | |
298 | + end | |
299 | + | |
300 | + # Extracts the origin instance out of the options if it exists and returns | |
301 | + # it. If there is no origin, looks for latitude and longitude values to | |
302 | + # create an origin. The side-effect of the method is to remove these | |
303 | + # option keys from the hash. | |
304 | + def extract_origin_from_options(options) | |
305 | + origin = options.delete(:origin) | |
306 | + res = normalize_point_to_lat_lng(origin) if origin | |
307 | + res | |
308 | + end | |
309 | + | |
310 | + # Extract the units out of the options if it exists and returns it. If | |
311 | + # there is no :units key, it uses the default. The side effect of the | |
312 | + # method is to remove the :units key from the options hash. | |
313 | + def extract_units_from_options(options) | |
314 | + units = options[:units] || default_units | |
315 | + options.delete(:units) | |
316 | + units | |
317 | + end | |
318 | + | |
319 | + # Extract the formula out of the options if it exists and returns it. If | |
320 | + # there is no :formula key, it uses the default. The side effect of the | |
321 | + # method is to remove the :formula key from the options hash. | |
322 | + def extract_formula_from_options(options) | |
323 | + formula = options[:formula] || default_formula | |
324 | + options.delete(:formula) | |
325 | + formula | |
326 | + end | |
327 | + | |
328 | + def extract_bounds_from_options(options) | |
329 | + bounds = options.delete(:bounds) | |
330 | + bounds = GeoKit::Bounds.normalize(bounds) if bounds | |
331 | + end | |
332 | + | |
333 | + # Geocode IP address. | |
334 | + def geocode_ip_address(origin) | |
335 | + geo_location = GeoKit::Geocoders::IpGeocoder.geocode(origin) | |
336 | + return geo_location if geo_location.success | |
337 | + raise GeoKit::Geocoders::GeocodeError | |
338 | + end | |
339 | + | |
340 | + | |
341 | + # Given a point in a variety of (an address to geocode, | |
342 | + # an array of [lat,lng], or an object with appropriate lat/lng methods, an IP addres) | |
343 | + # this method will normalize it into a GeoKit::LatLng instance. The only thing this | |
344 | + # method adds on top of LatLng#normalize is handling of IP addresses | |
345 | + def normalize_point_to_lat_lng(point) | |
346 | + res = geocode_ip_address(point) if point.is_a?(String) && /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(point) | |
347 | + res = GeoKit::LatLng.normalize(point) unless res | |
348 | + res | |
349 | + end | |
350 | + | |
351 | + # Augments the select with the distance SQL. | |
352 | + def add_distance_to_select(options, origin, units=default_units, formula=default_formula) | |
353 | + if origin | |
354 | + distance_selector = distance_sql(origin, units, formula) + " AS #{distance_column_name}" | |
355 | + selector = options.has_key?(:select) && options[:select] ? options[:select] : "*" | |
356 | + options[:select] = "#{selector}, #{distance_selector}" | |
357 | + end | |
358 | + end | |
359 | + | |
360 | + # Looks for the distance column and replaces it with the distance sql. If an origin was not | |
361 | + # passed in and the distance column exists, we leave it to be flagged as bad SQL by the database. | |
362 | + # Conditions are either a string or an array. In the case of an array, the first entry contains | |
363 | + # the condition. | |
364 | + def substitute_distance_in_conditions(options, origin, units=default_units, formula=default_formula) | |
365 | + original_conditions = options[:conditions] | |
366 | + condition = original_conditions.is_a?(String) ? original_conditions : original_conditions.first | |
367 | + pattern = Regexp.new("\s*#{distance_column_name}(\s<>=)*") | |
368 | + condition = condition.gsub(pattern, distance_sql(origin, units, formula)) | |
369 | + original_conditions = condition if original_conditions.is_a?(String) | |
370 | + original_conditions[0] = condition if original_conditions.is_a?(Array) | |
371 | + options[:conditions] = original_conditions | |
372 | + end | |
373 | + | |
374 | + # Returns the distance SQL using the spherical world formula (Haversine). The SQL is tuned | |
375 | + # to the database in use. | |
376 | + def sphere_distance_sql(origin, units) | |
377 | + lat = deg2rad(origin.lat) | |
378 | + lng = deg2rad(origin.lng) | |
379 | + multiplier = units_sphere_multiplier(units) | |
380 | + case connection.adapter_name.downcase | |
381 | + when "mysql" | |
382 | + sql=<<-SQL_END | |
383 | + (ACOS(least(1,COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*COS(RADIANS(#{qualified_lng_column_name}))+ | |
384 | + COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*SIN(RADIANS(#{qualified_lng_column_name}))+ | |
385 | + SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column_name}))))*#{multiplier}) | |
386 | + SQL_END | |
387 | + when "postgresql" | |
388 | + sql=<<-SQL_END | |
389 | + (ACOS(least(1,COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*COS(RADIANS(#{qualified_lng_column_name}))+ | |
390 | + COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*SIN(RADIANS(#{qualified_lng_column_name}))+ | |
391 | + SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column_name}))))*#{multiplier}) | |
392 | + SQL_END | |
393 | + else | |
394 | + sql = "unhandled #{connection.adapter_name.downcase} adapter" | |
395 | + end | |
396 | + end | |
397 | + | |
398 | + # Returns the distance SQL using the flat-world formula (Phythagorean Theory). The SQL is tuned | |
399 | + # to the database in use. | |
400 | + def flat_distance_sql(origin, units) | |
401 | + lat_degree_units = units_per_latitude_degree(units) | |
402 | + lng_degree_units = units_per_longitude_degree(origin.lat, units) | |
403 | + case connection.adapter_name.downcase | |
404 | + when "mysql" | |
405 | + sql=<<-SQL_END | |
406 | + SQRT(POW(#{lat_degree_units}*(#{origin.lat}-#{qualified_lat_column_name}),2)+ | |
407 | + POW(#{lng_degree_units}*(#{origin.lng}-#{qualified_lng_column_name}),2)) | |
408 | + SQL_END | |
409 | + when "postgresql" | |
410 | + sql=<<-SQL_END | |
411 | + SQRT(POW(#{lat_degree_units}*(#{origin.lat}-#{qualified_lat_column_name}),2)+ | |
412 | + POW(#{lng_degree_units}*(#{origin.lng}-#{qualified_lng_column_name}),2)) | |
413 | + SQL_END | |
414 | + else | |
415 | + sql = "unhandled #{connection.adapter_name.downcase} adapter" | |
416 | + end | |
417 | + end | |
418 | + end | |
419 | + end | |
420 | + end | |
421 | +end | |
422 | + | |
423 | +# Extend Array with a sort_by_distance method. | |
424 | +# This method creates a "distance" attribute on each object, | |
425 | +# calculates the distance from the passed origin, | |
426 | +# and finally sorts the array by the resulting distance. | |
427 | +class Array | |
428 | + def sort_by_distance_from(origin, opts={}) | |
429 | + distance_attribute_name = opts.delete(:distance_attribute_name) || 'distance' | |
430 | + self.each do |e| | |
431 | + e.class.send(:attr_accessor, distance_attribute_name) if !e.respond_to? "#{distance_attribute_name}=" | |
432 | + e.send("#{distance_attribute_name}=", origin.distance_to(e,opts)) | |
433 | + end | |
434 | + self.sort!{|a,b|a.send(distance_attribute_name) <=> b.send(distance_attribute_name)} | |
435 | + end | |
436 | +end | |
0 | 437 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,21 @@ |
1 | +module GeoKit | |
2 | + # These defaults are used in GeoKit::Mappable.distance_to and in acts_as_mappable | |
3 | + @@default_units = :miles | |
4 | + @@default_formula = :sphere | |
5 | + | |
6 | + [:default_units, :default_formula].each do |sym| | |
7 | + class_eval <<-EOS, __FILE__, __LINE__ | |
8 | + def self.#{sym} | |
9 | + if defined?(#{sym.to_s.upcase}) | |
10 | + #{sym.to_s.upcase} | |
11 | + else | |
12 | + @@#{sym} | |
13 | + end | |
14 | + end | |
15 | + | |
16 | + def self.#{sym}=(obj) | |
17 | + @@#{sym} = obj | |
18 | + end | |
19 | + EOS | |
20 | + end | |
21 | +end | ... | ... |
... | ... | @@ -0,0 +1,348 @@ |
1 | +require 'net/http' | |
2 | +require 'rexml/document' | |
3 | +require 'yaml' | |
4 | +require 'timeout' | |
5 | + | |
6 | +module GeoKit | |
7 | + # Contains a set of geocoders which can be used independently if desired. The list contains: | |
8 | + # | |
9 | + # * Google Geocoder - requires an API key. | |
10 | + # * Yahoo Geocoder - requires an API key. | |
11 | + # * Geocoder.us - may require authentication if performing more than the free request limit. | |
12 | + # * Geocoder.ca - for Canada; may require authentication as well. | |
13 | + # * IP Geocoder - geocodes an IP address using hostip.info's web service. | |
14 | + # * Multi Geocoder - provides failover for the physical location geocoders. | |
15 | + # | |
16 | + # Some configuration is required for these geocoders and can be located in the environment | |
17 | + # configuration files. | |
18 | + module Geocoders | |
19 | + @@proxy_addr = nil | |
20 | + @@proxy_port = nil | |
21 | + @@proxy_user = nil | |
22 | + @@proxy_pass = nil | |
23 | + @@timeout = nil | |
24 | + @@yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY' | |
25 | + @@google = 'REPLACE_WITH_YOUR_GOOGLE_KEY' | |
26 | + @@geocoder_us = false | |
27 | + @@geocoder_ca = false | |
28 | + @@provider_order = [:google,:us] | |
29 | + | |
30 | + [:yahoo, :google, :geocoder_us, :geocoder_ca, :provider_order, :timeout, | |
31 | + :proxy_addr, :proxy_port, :proxy_user, :proxy_pass].each do |sym| | |
32 | + class_eval <<-EOS, __FILE__, __LINE__ | |
33 | + def self.#{sym} | |
34 | + if defined?(#{sym.to_s.upcase}) | |
35 | + #{sym.to_s.upcase} | |
36 | + else | |
37 | + @@#{sym} | |
38 | + end | |
39 | + end | |
40 | + | |
41 | + def self.#{sym}=(obj) | |
42 | + @@#{sym} = obj | |
43 | + end | |
44 | + EOS | |
45 | + end | |
46 | + | |
47 | + # Error which is thrown in the event a geocoding error occurs. | |
48 | + class GeocodeError < StandardError; end | |
49 | + | |
50 | + # The Geocoder base class which defines the interface to be used by all | |
51 | + # other geocoders. | |
52 | + class Geocoder | |
53 | + # Main method which calls the do_geocode template method which subclasses | |
54 | + # are responsible for implementing. Returns a populated GeoLoc or an | |
55 | + # empty one with a failed success code. | |
56 | + def self.geocode(address) | |
57 | + res = do_geocode(address) | |
58 | + return res.success ? res : GeoLoc.new | |
59 | + end | |
60 | + | |
61 | + # Call the geocoder service using the timeout if configured. | |
62 | + def self.call_geocoder_service(url) | |
63 | + timeout(GeoKit::Geocoders::timeout) { return self.do_get(url) } if GeoKit::Geocoders::timeout | |
64 | + return self.do_get(url) | |
65 | + rescue TimeoutError | |
66 | + return nil | |
67 | + end | |
68 | + | |
69 | + protected | |
70 | + | |
71 | + def self.logger() RAILS_DEFAULT_LOGGER; end | |
72 | + | |
73 | + private | |
74 | + | |
75 | + # Wraps the geocoder call around a proxy if necessary. | |
76 | + def self.do_get(url) | |
77 | + return Net::HTTP::Proxy(GeoKit::Geocoders::proxy_addr, GeoKit::Geocoders::proxy_port, | |
78 | + GeoKit::Geocoders::proxy_user, GeoKit::Geocoders::proxy_pass).get_response(URI.parse(url)) | |
79 | + end | |
80 | + | |
81 | + # Adds subclass' geocode method making it conveniently available through | |
82 | + # the base class. | |
83 | + def self.inherited(clazz) | |
84 | + class_name = clazz.name.split('::').last | |
85 | + src = <<-END_SRC | |
86 | + def self.#{class_name.underscore}(address) | |
87 | + #{class_name}.geocode(address) | |
88 | + end | |
89 | + END_SRC | |
90 | + class_eval(src) | |
91 | + end | |
92 | + end | |
93 | + | |
94 | + # Geocoder CA geocoder implementation. Requires the GeoKit::Geocoders::GEOCODER_CA variable to | |
95 | + # contain true or false based upon whether authentication is to occur. Conforms to the | |
96 | + # interface set by the Geocoder class. | |
97 | + # | |
98 | + # Returns a response like: | |
99 | + # <?xml version="1.0" encoding="UTF-8" ?> | |
100 | + # <geodata> | |
101 | + # <latt>49.243086</latt> | |
102 | + # <longt>-123.153684</longt> | |
103 | + # </geodata> | |
104 | + class CaGeocoder < Geocoder | |
105 | + | |
106 | + private | |
107 | + | |
108 | + # Template method which does the geocode lookup. | |
109 | + def self.do_geocode(address) | |
110 | + raise ArgumentError('Geocoder.ca requires a GeoLoc argument') unless address.is_a?(GeoLoc) | |
111 | + url = construct_request(address) | |
112 | + res = self.call_geocoder_service(url) | |
113 | + return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) | |
114 | + xml = res.body | |
115 | + logger.debug "Geocoder.ca geocoding. Address: #{address}. Result: #{xml}" | |
116 | + # Parse the document. | |
117 | + doc = REXML::Document.new(xml) | |
118 | + address.lat = doc.elements['//latt'].text | |
119 | + address.lng = doc.elements['//longt'].text | |
120 | + address.success = true | |
121 | + return address | |
122 | + rescue | |
123 | + logger.error "Caught an error during Geocoder.ca geocoding call: "+$! | |
124 | + return GeoLoc.new | |
125 | + end | |
126 | + | |
127 | + # Formats the request in the format acceptable by the CA geocoder. | |
128 | + def self.construct_request(location) | |
129 | + url = "" | |
130 | + url += add_ampersand(url) + "stno=#{location.street_number}" if location.street_address | |
131 | + url += add_ampersand(url) + "addresst=#{CGI.escape(location.street_name)}" if location.street_address | |
132 | + url += add_ampersand(url) + "city=#{CGI.escape(location.city)}" if location.city | |
133 | + url += add_ampersand(url) + "prov=#{location.state}" if location.state | |
134 | + url += add_ampersand(url) + "postal=#{location.zip}" if location.zip | |
135 | + url += add_ampersand(url) + "auth=#{GeoKit::Geocoders::geocoder_ca}" if GeoKit::Geocoders::geocoder_ca | |
136 | + url += add_ampersand(url) + "geoit=xml" | |
137 | + 'http://geocoder.ca/?' + url | |
138 | + end | |
139 | + | |
140 | + def self.add_ampersand(url) | |
141 | + url && url.length > 0 ? "&" : "" | |
142 | + end | |
143 | + end | |
144 | + | |
145 | + # Google geocoder implementation. Requires the GeoKit::Geocoders::GOOGLE variable to | |
146 | + # contain a Google API key. Conforms to the interface set by the Geocoder class. | |
147 | + class GoogleGeocoder < Geocoder | |
148 | + | |
149 | + private | |
150 | + | |
151 | + # Template method which does the geocode lookup. | |
152 | + def self.do_geocode(address) | |
153 | + address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address | |
154 | + res = self.call_geocoder_service("http://maps.google.com/maps/geo?q=#{CGI.escape(address_str)}&output=xml&key=#{GeoKit::Geocoders::google}&oe=utf-8") | |
155 | +# res = Net::HTTP.get_response(URI.parse("http://maps.google.com/maps/geo?q=#{CGI.escape(address_str)}&output=xml&key=#{GeoKit::Geocoders::google}&oe=utf-8")) | |
156 | + return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) | |
157 | + xml=res.body | |
158 | + logger.debug "Google geocoding. Address: #{address}. Result: #{xml}" | |
159 | + doc=REXML::Document.new(xml) | |
160 | + | |
161 | + if doc.elements['//kml/Response/Status/code'].text == '200' | |
162 | + res = GeoLoc.new | |
163 | + coordinates=doc.elements['//coordinates'].text.to_s.split(',') | |
164 | + | |
165 | + #basics | |
166 | + res.lat=coordinates[1] | |
167 | + res.lng=coordinates[0] | |
168 | + res.country_code=doc.elements['//CountryNameCode'].text | |
169 | + res.provider='google' | |
170 | + | |
171 | + #extended -- false if not not available | |
172 | + res.city = doc.elements['//LocalityName'].text if doc.elements['//LocalityName'] | |
173 | + res.state = doc.elements['//AdministrativeAreaName'].text if doc.elements['//AdministrativeAreaName'] | |
174 | + res.full_address = doc.elements['//address'].text if doc.elements['//address'] # google provides it | |
175 | + res.zip = doc.elements['//PostalCodeNumber'].text if doc.elements['//PostalCodeNumber'] | |
176 | + res.street_address = doc.elements['//ThoroughfareName'].text if doc.elements['//ThoroughfareName'] | |
177 | + # Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country | |
178 | + # For Google, 1=low accuracy, 8=high accuracy | |
179 | + # old way -- address_details=doc.elements['//AddressDetails','urn:oasis:names:tc:ciq:xsdschema:xAL:2.0'] | |
180 | + address_details=doc.elements['//*[local-name() = "AddressDetails"]'] | |
181 | + accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0 | |
182 | + res.precision=%w{unknown country state state city zip zip+4 street address}[accuracy] | |
183 | + res.success=true | |
184 | + | |
185 | + return res | |
186 | + else | |
187 | + logger.info "Google was unable to geocode address: "+address | |
188 | + return GeoLoc.new | |
189 | + end | |
190 | + | |
191 | + rescue | |
192 | + logger.error "Caught an error during Google geocoding call: "+$! | |
193 | + return GeoLoc.new | |
194 | + end | |
195 | + end | |
196 | + | |
197 | + # Provides geocoding based upon an IP address. The underlying web service is a hostip.info | |
198 | + # which sources their data through a combination of publicly available information as well | |
199 | + # as community contributions. | |
200 | + class IpGeocoder < Geocoder | |
201 | + | |
202 | + private | |
203 | + | |
204 | + # Given an IP address, returns a GeoLoc instance which contains latitude, | |
205 | + # longitude, city, and country code. Sets the success attribute to false if the ip | |
206 | + # parameter does not match an ip address. | |
207 | + def self.do_geocode(ip) | |
208 | + return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip) | |
209 | + url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true" | |
210 | + response = self.call_geocoder_service(url) | |
211 | + response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new | |
212 | + rescue | |
213 | + logger.error "Caught an error during HostIp geocoding call: "+$! | |
214 | + return GeoLoc.new | |
215 | + end | |
216 | + | |
217 | + # Converts the body to YAML since its in the form of: | |
218 | + # | |
219 | + # Country: UNITED STATES (US) | |
220 | + # City: Sugar Grove, IL | |
221 | + # Latitude: 41.7696 | |
222 | + # Longitude: -88.4588 | |
223 | + # | |
224 | + # then instantiates a GeoLoc instance to populate with location data. | |
225 | + def self.parse_body(body) # :nodoc: | |
226 | + yaml = YAML.load(body) | |
227 | + res = GeoLoc.new | |
228 | + res.provider = 'hostip' | |
229 | + res.city, res.state = yaml['City'].split(', ') | |
230 | + country, res.country_code = yaml['Country'].split(' (') | |
231 | + res.lat = yaml['Latitude'] | |
232 | + res.lng = yaml['Longitude'] | |
233 | + res.country_code.chop! | |
234 | + res.success = res.city != "(Private Address)" | |
235 | + res | |
236 | + end | |
237 | + end | |
238 | + | |
239 | + # Geocoder Us geocoder implementation. Requires the GeoKit::Geocoders::GEOCODER_US variable to | |
240 | + # contain true or false based upon whether authentication is to occur. Conforms to the | |
241 | + # interface set by the Geocoder class. | |
242 | + class UsGeocoder < Geocoder | |
243 | + | |
244 | + private | |
245 | + | |
246 | + # For now, the geocoder_method will only geocode full addresses -- not zips or cities in isolation | |
247 | + def self.do_geocode(address) | |
248 | + address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address | |
249 | + url = "http://"+(GeoKit::Geocoders::geocoder_us || '')+"geocoder.us/service/csv/geocode?address=#{CGI.escape(address_str)}" | |
250 | + res = self.call_geocoder_service(url) | |
251 | + return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) | |
252 | + data = res.body | |
253 | + logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}" | |
254 | + array = data.chomp.split(',') | |
255 | + | |
256 | + if array.length == 6 | |
257 | + res=GeoLoc.new | |
258 | + res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array | |
259 | + res.country_code='US' | |
260 | + res.success=true | |
261 | + return res | |
262 | + else | |
263 | + logger.info "geocoder.us was unable to geocode address: "+address | |
264 | + return GeoLoc.new | |
265 | + end | |
266 | + rescue | |
267 | + logger.error "Caught an error during geocoder.us geocoding call: "+$! | |
268 | + return GeoLoc.new | |
269 | + end | |
270 | + end | |
271 | + | |
272 | + # Yahoo geocoder implementation. Requires the GeoKit::Geocoders::YAHOO variable to | |
273 | + # contain a Yahoo API key. Conforms to the interface set by the Geocoder class. | |
274 | + class YahooGeocoder < Geocoder | |
275 | + | |
276 | + private | |
277 | + | |
278 | + # Template method which does the geocode lookup. | |
279 | + def self.do_geocode(address) | |
280 | + address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address | |
281 | + url="http://api.local.yahoo.com/MapsService/V1/geocode?appid=#{GeoKit::Geocoders::yahoo}&location=#{CGI.escape(address_str)}" | |
282 | + res = self.call_geocoder_service(url) | |
283 | + return GeoLoc.new if !res.is_a?(Net::HTTPSuccess) | |
284 | + xml = res.body | |
285 | + doc = REXML::Document.new(xml) | |
286 | + logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}" | |
287 | + | |
288 | + if doc.elements['//ResultSet'] | |
289 | + res=GeoLoc.new | |
290 | + | |
291 | + #basic | |
292 | + res.lat=doc.elements['//Latitude'].text | |
293 | + res.lng=doc.elements['//Longitude'].text | |
294 | + res.country_code=doc.elements['//Country'].text | |
295 | + res.provider='yahoo' | |
296 | + | |
297 | + #extended - false if not available | |
298 | + res.city=doc.elements['//City'].text if doc.elements['//City'] && doc.elements['//City'].text != nil | |
299 | + res.state=doc.elements['//State'].text if doc.elements['//State'] && doc.elements['//State'].text != nil | |
300 | + res.zip=doc.elements['//Zip'].text if doc.elements['//Zip'] && doc.elements['//Zip'].text != nil | |
301 | + res.street_address=doc.elements['//Address'].text if doc.elements['//Address'] && doc.elements['//Address'].text != nil | |
302 | + res.precision=doc.elements['//Result'].attributes['precision'] if doc.elements['//Result'] | |
303 | + res.success=true | |
304 | + return res | |
305 | + else | |
306 | + logger.info "Yahoo was unable to geocode address: "+address | |
307 | + return GeoLoc.new | |
308 | + end | |
309 | + | |
310 | + rescue | |
311 | + logger.info "Caught an error during Yahoo geocoding call: "+$! | |
312 | + return GeoLoc.new | |
313 | + end | |
314 | + end | |
315 | + | |
316 | + # Provides methods to geocode with a variety of geocoding service providers, plus failover | |
317 | + # among providers in the order you configure. | |
318 | + # | |
319 | + # Goal: | |
320 | + # - homogenize the results of multiple geocoders | |
321 | + # | |
322 | + # Limitations: | |
323 | + # - currently only provides the first result. Sometimes geocoders will return multiple results. | |
324 | + # - currently discards the "accuracy" component of the geocoding calls | |
325 | + class MultiGeocoder < Geocoder | |
326 | + private | |
327 | + | |
328 | + # This method will call one or more geocoders in the order specified in the | |
329 | + # configuration until one of the geocoders work. | |
330 | + # | |
331 | + # The failover approach is crucial for production-grade apps, but is rarely used. | |
332 | + # 98% of your geocoding calls will be successful with the first call | |
333 | + def self.do_geocode(address) | |
334 | + GeoKit::Geocoders::provider_order.each do |provider| | |
335 | + begin | |
336 | + klass = GeoKit::Geocoders.const_get "#{provider.to_s.capitalize}Geocoder" | |
337 | + res = klass.send :geocode, address | |
338 | + return res if res.success | |
339 | + rescue | |
340 | + logger.error("Something has gone very wrong during geocoding, OR you have configured an invalid class name in GeoKit::Geocoders::provider_order. Address: #{address}. Provider: #{provider}") | |
341 | + end | |
342 | + end | |
343 | + # If we get here, we failed completely. | |
344 | + GeoLoc.new | |
345 | + end | |
346 | + end | |
347 | + end | |
348 | +end | |
0 | 349 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,46 @@ |
1 | +require 'yaml' | |
2 | + | |
3 | +module GeoKit | |
4 | + # Contains a class method geocode_ip_address which can be used to enable automatic geocoding | |
5 | + # for request IP addresses. The geocoded information is stored in a cookie and in the | |
6 | + # session to minimize web service calls. The point of the helper is to enable location-based | |
7 | + # websites to have a best-guess for new visitors. | |
8 | + module IpGeocodeLookup | |
9 | + # Mix below class methods into ActionController. | |
10 | + def self.included(base) # :nodoc: | |
11 | + base.extend ClassMethods | |
12 | + end | |
13 | + | |
14 | + # Class method to mix into active record. | |
15 | + module ClassMethods # :nodoc: | |
16 | + def geocode_ip_address(filter_options = {}) | |
17 | + before_filter :store_ip_location, filter_options | |
18 | + end | |
19 | + end | |
20 | + | |
21 | + private | |
22 | + | |
23 | + # Places the IP address' geocode location into the session if it | |
24 | + # can be found. Otherwise, looks for a geo location cookie and | |
25 | + # uses that value. The last resort is to call the web service to | |
26 | + # get the value. | |
27 | + def store_ip_location | |
28 | + session[:geo_location] ||= retrieve_location_from_cookie_or_service | |
29 | + cookies[:geo_location] = { :value => session[:geo_location].to_yaml, :expires => 30.days.from_now } if session[:geo_location] | |
30 | + end | |
31 | + | |
32 | + # Uses the stored location value from the cookie if it exists. If | |
33 | + # no cookie exists, calls out to the web service to get the location. | |
34 | + def retrieve_location_from_cookie_or_service | |
35 | + return YAML.load(cookies[:geo_location]) if cookies[:geo_location] | |
36 | + location = Geocoders::IpGeocoder.geocode(get_ip_address) | |
37 | + return location.success ? location : nil | |
38 | + end | |
39 | + | |
40 | + # Returns the real ip address, though this could be the localhost ip | |
41 | + # address. No special handling here anymore. | |
42 | + def get_ip_address | |
43 | + request.remote_ip | |
44 | + end | |
45 | + end | |
46 | +end | |
0 | 47 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,432 @@ |
1 | +require 'geo_kit/defaults' | |
2 | + | |
3 | +module GeoKit | |
4 | + # Contains class and instance methods providing distance calcuation services. This | |
5 | + # module is meant to be mixed into classes containing lat and lng attributes where | |
6 | + # distance calculation is desired. | |
7 | + # | |
8 | + # At present, two forms of distance calculations are provided: | |
9 | + # | |
10 | + # * Pythagorean Theory (flat Earth) - which assumes the world is flat and loses accuracy over long distances. | |
11 | + # * Haversine (sphere) - which is fairly accurate, but at a performance cost. | |
12 | + # | |
13 | + # Distance units supported are :miles and :kms. | |
14 | + module Mappable | |
15 | + PI_DIV_RAD = 0.0174 | |
16 | + KMS_PER_MILE = 1.609 | |
17 | + EARTH_RADIUS_IN_MILES = 3963.19 | |
18 | + EARTH_RADIUS_IN_KMS = EARTH_RADIUS_IN_MILES * KMS_PER_MILE | |
19 | + MILES_PER_LATITUDE_DEGREE = 69.1 | |
20 | + KMS_PER_LATITUDE_DEGREE = MILES_PER_LATITUDE_DEGREE * KMS_PER_MILE | |
21 | + LATITUDE_DEGREES = EARTH_RADIUS_IN_MILES / MILES_PER_LATITUDE_DEGREE | |
22 | + | |
23 | + # Mix below class methods into the includer. | |
24 | + def self.included(receiver) # :nodoc: | |
25 | + receiver.extend ClassMethods | |
26 | + end | |
27 | + | |
28 | + module ClassMethods #:nodoc: | |
29 | + # Returns the distance between two points. The from and to parameters are | |
30 | + # required to have lat and lng attributes. Valid options are: | |
31 | + # :units - valid values are :miles or :kms (GeoKit::default_units is the default) | |
32 | + # :formula - valid values are :flat or :sphere (GeoKit::default_formula is the default) | |
33 | + def distance_between(from, to, options={}) | |
34 | + from=GeoKit::LatLng.normalize(from) | |
35 | + to=GeoKit::LatLng.normalize(to) | |
36 | + return 0.0 if from == to # fixes a "zero-distance" bug | |
37 | + units = options[:units] || GeoKit::default_units | |
38 | + formula = options[:formula] || GeoKit::default_formula | |
39 | + case formula | |
40 | + when :sphere | |
41 | + units_sphere_multiplier(units) * | |
42 | + Math.acos( Math.sin(deg2rad(from.lat)) * Math.sin(deg2rad(to.lat)) + | |
43 | + Math.cos(deg2rad(from.lat)) * Math.cos(deg2rad(to.lat)) * | |
44 | + Math.cos(deg2rad(to.lng) - deg2rad(from.lng))) | |
45 | + when :flat | |
46 | + Math.sqrt((units_per_latitude_degree(units)*(from.lat-to.lat))**2 + | |
47 | + (units_per_longitude_degree(from.lat, units)*(from.lng-to.lng))**2) | |
48 | + end | |
49 | + end | |
50 | + | |
51 | + # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc) | |
52 | + # from the first point to the second point. Typicaly, the instance methods will be used | |
53 | + # instead of this method. | |
54 | + def heading_between(from,to) | |
55 | + from=GeoKit::LatLng.normalize(from) | |
56 | + to=GeoKit::LatLng.normalize(to) | |
57 | + | |
58 | + d_lng=deg2rad(to.lng-from.lng) | |
59 | + from_lat=deg2rad(from.lat) | |
60 | + to_lat=deg2rad(to.lat) | |
61 | + y=Math.sin(d_lng) * Math.cos(to_lat) | |
62 | + x=Math.cos(from_lat)*Math.sin(to_lat)-Math.sin(from_lat)*Math.cos(to_lat)*Math.cos(d_lng) | |
63 | + heading=to_heading(Math.atan2(y,x)) | |
64 | + end | |
65 | + | |
66 | + # Given a start point, distance, and heading (in degrees), provides | |
67 | + # an endpoint. Returns a LatLng instance. Typically, the instance method | |
68 | + # will be used instead of this method. | |
69 | + def endpoint(start,heading, distance, options={}) | |
70 | + units = options[:units] || GeoKit::default_units | |
71 | + radius = units == :miles ? EARTH_RADIUS_IN_MILES : EARTH_RADIUS_IN_KMS | |
72 | + start=GeoKit::LatLng.normalize(start) | |
73 | + lat=deg2rad(start.lat) | |
74 | + lng=deg2rad(start.lng) | |
75 | + heading=deg2rad(heading) | |
76 | + distance=distance.to_f | |
77 | + | |
78 | + end_lat=Math.asin(Math.sin(lat)*Math.cos(distance/radius) + | |
79 | + Math.cos(lat)*Math.sin(distance/radius)*Math.cos(heading)) | |
80 | + | |
81 | + end_lng=lng+Math.atan2(Math.sin(heading)*Math.sin(distance/radius)*Math.cos(lat), | |
82 | + Math.cos(distance/radius)-Math.sin(lat)*Math.sin(end_lat)) | |
83 | + | |
84 | + LatLng.new(rad2deg(end_lat),rad2deg(end_lng)) | |
85 | + end | |
86 | + | |
87 | + # Returns the midpoint, given two points. Returns a LatLng. | |
88 | + # Typically, the instance method will be used instead of this method. | |
89 | + # Valid option: | |
90 | + # :units - valid values are :miles or :kms (:miles is the default) | |
91 | + def midpoint_between(from,to,options={}) | |
92 | + from=GeoKit::LatLng.normalize(from) | |
93 | + | |
94 | + units = options[:units] || GeoKit::default_units | |
95 | + | |
96 | + heading=from.heading_to(to) | |
97 | + distance=from.distance_to(to,options) | |
98 | + midpoint=from.endpoint(heading,distance/2,options) | |
99 | + end | |
100 | + | |
101 | + # Geocodes a location using the multi geocoder. | |
102 | + def geocode(location) | |
103 | + res = Geocoders::MultiGeocoder.geocode(location) | |
104 | + return res if res.success | |
105 | + raise GeoKit::Geocoders::GeocodeError | |
106 | + end | |
107 | + | |
108 | + protected | |
109 | + | |
110 | + def deg2rad(degrees) | |
111 | + degrees.to_f / 180.0 * Math::PI | |
112 | + end | |
113 | + | |
114 | + def rad2deg(rad) | |
115 | + rad.to_f * 180.0 / Math::PI | |
116 | + end | |
117 | + | |
118 | + def to_heading(rad) | |
119 | + (rad2deg(rad)+360)%360 | |
120 | + end | |
121 | + | |
122 | + # Returns the multiplier used to obtain the correct distance units. | |
123 | + def units_sphere_multiplier(units) | |
124 | + units == :miles ? EARTH_RADIUS_IN_MILES : EARTH_RADIUS_IN_KMS | |
125 | + end | |
126 | + | |
127 | + # Returns the number of units per latitude degree. | |
128 | + def units_per_latitude_degree(units) | |
129 | + units == :miles ? MILES_PER_LATITUDE_DEGREE : KMS_PER_LATITUDE_DEGREE | |
130 | + end | |
131 | + | |
132 | + # Returns the number units per longitude degree. | |
133 | + def units_per_longitude_degree(lat, units) | |
134 | + miles_per_longitude_degree = (LATITUDE_DEGREES * Math.cos(lat * PI_DIV_RAD)).abs | |
135 | + units == :miles ? miles_per_longitude_degree : miles_per_longitude_degree * KMS_PER_MILE | |
136 | + end | |
137 | + end | |
138 | + | |
139 | + # ----------------------------------------------------------------------------------------------- | |
140 | + # Instance methods below here | |
141 | + # ----------------------------------------------------------------------------------------------- | |
142 | + | |
143 | + # Extracts a LatLng instance. Use with models that are acts_as_mappable | |
144 | + def to_lat_lng | |
145 | + return self if instance_of?(GeoKit::LatLng) || instance_of?(GeoKit::GeoLoc) | |
146 | + return LatLng.new(send(self.class.lat_column_name),send(self.class.lng_column_name)) if self.class.respond_to?(:acts_as_mappable) | |
147 | + return nil | |
148 | + end | |
149 | + | |
150 | + # Returns the distance from another point. The other point parameter is | |
151 | + # required to have lat and lng attributes. Valid options are: | |
152 | + # :units - valid values are :miles or :kms (:miles is the default) | |
153 | + # :formula - valid values are :flat or :sphere (:sphere is the default) | |
154 | + def distance_to(other, options={}) | |
155 | + self.class.distance_between(self, other, options) | |
156 | + end | |
157 | + alias distance_from distance_to | |
158 | + | |
159 | + # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc) | |
160 | + # to the given point. The given point can be a LatLng or a string to be Geocoded | |
161 | + def heading_to(other) | |
162 | + self.class.heading_between(self,other) | |
163 | + end | |
164 | + | |
165 | + # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc) | |
166 | + # FROM the given point. The given point can be a LatLng or a string to be Geocoded | |
167 | + def heading_from(other) | |
168 | + self.class.heading_between(other,self) | |
169 | + end | |
170 | + | |
171 | + # Returns the endpoint, given a heading (in degrees) and distance. | |
172 | + # Valid option: | |
173 | + # :units - valid values are :miles or :kms (:miles is the default) | |
174 | + def endpoint(heading,distance,options={}) | |
175 | + self.class.endpoint(self,heading,distance,options) | |
176 | + end | |
177 | + | |
178 | + # Returns the midpoint, given another point on the map. | |
179 | + # Valid option: | |
180 | + # :units - valid values are :miles or :kms (:miles is the default) | |
181 | + def midpoint_to(other, options={}) | |
182 | + self.class.midpoint_between(self,other,options) | |
183 | + end | |
184 | + | |
185 | + end | |
186 | + | |
187 | + class LatLng | |
188 | + include Mappable | |
189 | + | |
190 | + attr_accessor :lat, :lng | |
191 | + | |
192 | + # Accepts latitude and longitude or instantiates an empty instance | |
193 | + # if lat and lng are not provided. Converted to floats if provided | |
194 | + def initialize(lat=nil, lng=nil) | |
195 | + lat = lat.to_f if lat && !lat.is_a?(Numeric) | |
196 | + lng = lng.to_f if lng && !lng.is_a?(Numeric) | |
197 | + @lat = lat | |
198 | + @lng = lng | |
199 | + end | |
200 | + | |
201 | + # Latitude attribute setter; stored as a float. | |
202 | + def lat=(lat) | |
203 | + @lat = lat.to_f if lat | |
204 | + end | |
205 | + | |
206 | + # Longitude attribute setter; stored as a float; | |
207 | + def lng=(lng) | |
208 | + @lng=lng.to_f if lng | |
209 | + end | |
210 | + | |
211 | + # Returns the lat and lng attributes as a comma-separated string. | |
212 | + def ll | |
213 | + "#{lat},#{lng}" | |
214 | + end | |
215 | + | |
216 | + #returns a string with comma-separated lat,lng values | |
217 | + def to_s | |
218 | + ll | |
219 | + end | |
220 | + | |
221 | + #returns a two-element array | |
222 | + def to_a | |
223 | + [lat,lng] | |
224 | + end | |
225 | + # Returns true if the candidate object is logically equal. Logical equivalence | |
226 | + # is true if the lat and lng attributes are the same for both objects. | |
227 | + def ==(other) | |
228 | + other.is_a?(LatLng) ? self.lat == other.lat && self.lng == other.lng : false | |
229 | + end | |
230 | + | |
231 | + # A *class* method to take anything which can be inferred as a point and generate | |
232 | + # a LatLng from it. You should use this anything you're not sure what the input is, | |
233 | + # and want to deal with it as a LatLng if at all possible. Can take: | |
234 | + # 1) two arguments (lat,lng) | |
235 | + # 2) a string in the format "37.1234,-129.1234" or "37.1234 -129.1234" | |
236 | + # 3) a string which can be geocoded on the fly | |
237 | + # 4) an array in the format [37.1234,-129.1234] | |
238 | + # 5) a LatLng or GeoLoc (which is just passed through as-is) | |
239 | + # 6) anything which acts_as_mappable -- a LatLng will be extracted from it | |
240 | + def self.normalize(thing,other=nil) | |
241 | + # if an 'other' thing is supplied, normalize the input by creating an array of two elements | |
242 | + thing=[thing,other] if other | |
243 | + | |
244 | + if thing.is_a?(String) | |
245 | + thing.strip! | |
246 | + if match=thing.match(/(\-?\d+\.?\d*)[, ] ?(\-?\d+\.?\d*)$/) | |
247 | + return GeoKit::LatLng.new(match[1],match[2]) | |
248 | + else | |
249 | + res = GeoKit::Geocoders::MultiGeocoder.geocode(thing) | |
250 | + return res if res.success | |
251 | + raise GeoKit::Geocoders::GeocodeError | |
252 | + end | |
253 | + elsif thing.is_a?(Array) && thing.size==2 | |
254 | + return GeoKit::LatLng.new(thing[0],thing[1]) | |
255 | + elsif thing.is_a?(LatLng) # will also be true for GeoLocs | |
256 | + return thing | |
257 | + elsif thing.class.respond_to?(:acts_as_mappable) && thing.class.respond_to?(:distance_column_name) | |
258 | + return thing.to_lat_lng | |
259 | + end | |
260 | + | |
261 | + throw ArgumentError.new("#{thing} (#{thing.class}) cannot be normalized to a LatLng. We tried interpreting it as an array, string, Mappable, etc., but no dice.") | |
262 | + end | |
263 | + | |
264 | + end | |
265 | + | |
266 | + # This class encapsulates the result of a geocoding call | |
267 | + # It's primary purpose is to homogenize the results of multiple | |
268 | + # geocoding providers. It also provides some additional functionality, such as | |
269 | + # the "full address" method for geocoders that do not provide a | |
270 | + # full address in their results (for example, Yahoo), and the "is_us" method. | |
271 | + class GeoLoc < LatLng | |
272 | + # Location attributes. Full address is a concatenation of all values. For example: | |
273 | + # 100 Spear St, San Francisco, CA, 94101, US | |
274 | + attr_accessor :street_address, :city, :state, :zip, :country_code, :full_address | |
275 | + # Attributes set upon return from geocoding. Success will be true for successful | |
276 | + # geocode lookups. The provider will be set to the name of the providing geocoder. | |
277 | + # Finally, precision is an indicator of the accuracy of the geocoding. | |
278 | + attr_accessor :success, :provider, :precision | |
279 | + # Street number and street name are extracted from the street address attribute. | |
280 | + attr_reader :street_number, :street_name | |
281 | + | |
282 | + # Constructor expects a hash of symbols to correspond with attributes. | |
283 | + def initialize(h={}) | |
284 | + @street_address=h[:street_address] | |
285 | + @city=h[:city] | |
286 | + @state=h[:state] | |
287 | + @zip=h[:zip] | |
288 | + @country_code=h[:country_code] | |
289 | + @success=false | |
290 | + @precision='unknown' | |
291 | + super(h[:lat],h[:lng]) | |
292 | + end | |
293 | + | |
294 | + # Returns true if geocoded to the United States. | |
295 | + def is_us? | |
296 | + country_code == 'US' | |
297 | + end | |
298 | + | |
299 | + # full_address is provided by google but not by yahoo. It is intended that the google | |
300 | + # geocoding method will provide the full address, whereas for yahoo it will be derived | |
301 | + # from the parts of the address we do have. | |
302 | + def full_address | |
303 | + @full_address ? @full_address : to_geocodeable_s | |
304 | + end | |
305 | + | |
306 | + # Extracts the street number from the street address if the street address | |
307 | + # has a value. | |
308 | + def street_number | |
309 | + street_address[/(\d*)/] if street_address | |
310 | + end | |
311 | + | |
312 | + # Returns the street name portion of the street address. | |
313 | + def street_name | |
314 | + street_address[street_number.length, street_address.length].strip if street_address | |
315 | + end | |
316 | + | |
317 | + # gives you all the important fields as key-value pairs | |
318 | + def hash | |
319 | + res={} | |
320 | + [:success,:lat,:lng,:country_code,:city,:state,:zip,:street_address,:provider,:full_address,:is_us?,:ll,:precision].each { |s| res[s] = self.send(s.to_s) } | |
321 | + res | |
322 | + end | |
323 | + alias to_hash hash | |
324 | + | |
325 | + # Sets the city after capitalizing each word within the city name. | |
326 | + def city=(city) | |
327 | + @city = city.titleize if city | |
328 | + end | |
329 | + | |
330 | + # Sets the street address after capitalizing each word within the street address. | |
331 | + def street_address=(address) | |
332 | + @street_address = address.titleize if address | |
333 | + end | |
334 | + | |
335 | + # Returns a comma-delimited string consisting of the street address, city, state, | |
336 | + # zip, and country code. Only includes those attributes that are non-blank. | |
337 | + def to_geocodeable_s | |
338 | + a=[street_address, city, state, zip, country_code].compact | |
339 | + a.delete_if { |e| !e || e == '' } | |
340 | + a.join(', ') | |
341 | + end | |
342 | + | |
343 | + # Returns a string representation of the instance. | |
344 | + def to_s | |
345 | + "Provider: #{provider}\n Street: #{street_address}\nCity: #{city}\nState: #{state}\nZip: #{zip}\nLatitude: #{lat}\nLongitude: #{lng}\nCountry: #{country_code}\nSuccess: #{success}" | |
346 | + end | |
347 | + end | |
348 | + | |
349 | + # Bounds represents a rectangular bounds, defined by the SW and NE corners | |
350 | + class Bounds | |
351 | + # sw and ne are LatLng objects | |
352 | + attr_accessor :sw, :ne | |
353 | + | |
354 | + # provide sw and ne to instantiate a new Bounds instance | |
355 | + def initialize(sw,ne) | |
356 | + raise ArguementError if !(sw.is_a?(GeoKit::LatLng) && ne.is_a?(GeoKit::LatLng)) | |
357 | + @sw,@ne=sw,ne | |
358 | + end | |
359 | + | |
360 | + #returns the a single point which is the center of the rectangular bounds | |
361 | + def center | |
362 | + @sw.midpoint_to(@ne) | |
363 | + end | |
364 | + | |
365 | + # a simple string representation:sw,ne | |
366 | + def to_s | |
367 | + "#{@sw.to_s},#{@ne.to_s}" | |
368 | + end | |
369 | + | |
370 | + # a two-element array of two-element arrays: sw,ne | |
371 | + def to_a | |
372 | + [@sw.to_a, @ne.to_a] | |
373 | + end | |
374 | + | |
375 | + # Returns true if the bounds contain the passed point. | |
376 | + # allows for bounds which cross the meridian | |
377 | + def contains?(point) | |
378 | + point=GeoKit::LatLng.normalize(point) | |
379 | + res = point.lat > @sw.lat && point.lat < @ne.lat | |
380 | + if crosses_meridian? | |
381 | + res &= point.lng < @ne.lng || point.lng > @sw.lng | |
382 | + else | |
383 | + res &= point.lng < @ne.lng && point.lng > @sw.lng | |
384 | + end | |
385 | + res | |
386 | + end | |
387 | + | |
388 | + # returns true if the bounds crosses the international dateline | |
389 | + def crosses_meridian? | |
390 | + @sw.lng > @ne.lng | |
391 | + end | |
392 | + | |
393 | + # Returns true if the candidate object is logically equal. Logical equivalence | |
394 | + # is true if the lat and lng attributes are the same for both objects. | |
395 | + def ==(other) | |
396 | + other.is_a?(Bounds) ? self.sw == other.sw && self.ne == other.ne : false | |
397 | + end | |
398 | + | |
399 | + class <<self | |
400 | + | |
401 | + # returns an instance of bounds which completely encompases the given circle | |
402 | + def from_point_and_radius(point,radius,options={}) | |
403 | + point=LatLng.normalize(point) | |
404 | + p0=point.endpoint(0,radius,options) | |
405 | + p90=point.endpoint(90,radius,options) | |
406 | + p180=point.endpoint(180,radius,options) | |
407 | + p270=point.endpoint(270,radius,options) | |
408 | + sw=GeoKit::LatLng.new(p180.lat,p270.lng) | |
409 | + ne=GeoKit::LatLng.new(p0.lat,p90.lng) | |
410 | + GeoKit::Bounds.new(sw,ne) | |
411 | + end | |
412 | + | |
413 | + # Takes two main combinations of arguements to create a bounds: | |
414 | + # point,point (this is the only one which takes two arguments | |
415 | + # [point,point] | |
416 | + # . . . where a point is anything LatLng#normalize can handle (which is quite a lot) | |
417 | + # | |
418 | + # NOTE: everything combination is assumed to pass points in the order sw, ne | |
419 | + def normalize (thing,other=nil) | |
420 | + # maybe this will be simple -- an actual bounds object is passed, and we can all go home | |
421 | + return thing if thing.is_a? Bounds | |
422 | + | |
423 | + # no? OK, if there's no "other," the thing better be a two-element array | |
424 | + thing,other=thing if !other && thing.is_a?(Array) && thing.size==2 | |
425 | + | |
426 | + # Now that we're set with a thing and another thing, let LatLng do the heavy lifting. | |
427 | + # Exceptions may be thrown | |
428 | + Bounds.new(GeoKit::LatLng.normalize(thing),GeoKit::LatLng.normalize(other)) | |
429 | + end | |
430 | + end | |
431 | + end | |
432 | +end | |
0 | 433 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,481 @@ |
1 | +require 'rubygems' | |
2 | +require 'mocha' | |
3 | +require File.join(File.dirname(__FILE__), 'test_helper') | |
4 | + | |
5 | +GeoKit::Geocoders::provider_order=[:google,:us] | |
6 | + | |
7 | +# Uses defaults | |
8 | +class Company < ActiveRecord::Base #:nodoc: all | |
9 | + has_many :locations | |
10 | +end | |
11 | + | |
12 | +# Configures everything. | |
13 | +class Location < ActiveRecord::Base #:nodoc: all | |
14 | + belongs_to :company | |
15 | + acts_as_mappable | |
16 | +end | |
17 | + | |
18 | +# for auto_geocode | |
19 | +class Store < ActiveRecord::Base | |
20 | + acts_as_mappable :auto_geocode=>true | |
21 | +end | |
22 | + | |
23 | +# Uses deviations from conventions. | |
24 | +class CustomLocation < ActiveRecord::Base #:nodoc: all | |
25 | + belongs_to :company | |
26 | + acts_as_mappable :distance_column_name => 'dist', | |
27 | + :default_units => :kms, | |
28 | + :default_formula => :flat, | |
29 | + :lat_column_name => 'latitude', | |
30 | + :lng_column_name => 'longitude' | |
31 | + | |
32 | + def to_s | |
33 | + "lat: #{latitude} lng: #{longitude} dist: #{dist}" | |
34 | + end | |
35 | +end | |
36 | + | |
37 | +class ActsAsMappableTest < Test::Unit::TestCase #:nodoc: all | |
38 | + | |
39 | + LOCATION_A_IP = "217.10.83.5" | |
40 | + | |
41 | + #self.fixture_path = File.dirname(__FILE__) + '/fixtures' | |
42 | + #self.fixture_path = RAILS_ROOT + '/test/fixtures/' | |
43 | + #puts "Rails Path #{RAILS_ROOT}" | |
44 | + #puts "Fixture Path: #{self.fixture_path}" | |
45 | + #self.fixture_path = ' /Users/bill_eisenhauer/Projects/geokit_test/test/fixtures/' | |
46 | + fixtures :companies, :locations, :custom_locations, :stores | |
47 | + | |
48 | + def setup | |
49 | + @location_a = GeoKit::GeoLoc.new | |
50 | + @location_a.lat = 32.918593 | |
51 | + @location_a.lng = -96.958444 | |
52 | + @location_a.city = "Irving" | |
53 | + @location_a.state = "TX" | |
54 | + @location_a.country_code = "US" | |
55 | + @location_a.success = true | |
56 | + | |
57 | + @sw = GeoKit::LatLng.new(32.91663,-96.982841) | |
58 | + @ne = GeoKit::LatLng.new(32.96302,-96.919495) | |
59 | + @bounds_center=GeoKit::LatLng.new((@sw.lat+@ne.lat)/2,(@sw.lng+@ne.lng)/2) | |
60 | + | |
61 | + @starbucks = companies(:starbucks) | |
62 | + @loc_a = locations(:a) | |
63 | + @custom_loc_a = custom_locations(:a) | |
64 | + @loc_e = locations(:e) | |
65 | + @custom_loc_e = custom_locations(:e) | |
66 | + end | |
67 | + | |
68 | + def test_override_default_units_the_hard_way | |
69 | + Location.default_units = :kms | |
70 | + locations = Location.find(:all, :origin => @loc_a, :conditions => "distance < 3.97") | |
71 | + assert_equal 5, locations.size | |
72 | + locations = Location.count(:origin => @loc_a, :conditions => "distance < 3.97") | |
73 | + assert_equal 5, locations | |
74 | + Location.default_units = :miles | |
75 | + end | |
76 | + | |
77 | + def test_include | |
78 | + locations = Location.find(:all, :origin => @loc_a, :include => :company, :conditions => "company_id = 1") | |
79 | + assert !locations.empty? | |
80 | + assert_equal 1, locations[0].company.id | |
81 | + assert_equal 'Starbucks', locations[0].company.name | |
82 | + end | |
83 | + | |
84 | + def test_distance_between_geocoded | |
85 | + GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("Irving, TX").returns(@location_a) | |
86 | + GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("San Francisco, CA").returns(@location_a) | |
87 | + assert_equal 0, Location.distance_between("Irving, TX", "San Francisco, CA") | |
88 | + end | |
89 | + | |
90 | + def test_distance_to_geocoded | |
91 | + GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("Irving, TX").returns(@location_a) | |
92 | + assert_equal 0, @custom_loc_a.distance_to("Irving, TX") | |
93 | + end | |
94 | + | |
95 | + def test_distance_to_geocoded_error | |
96 | + GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("Irving, TX").returns(GeoKit::GeoLoc.new) | |
97 | + assert_raise(GeoKit::Geocoders::GeocodeError) { @custom_loc_a.distance_to("Irving, TX") } | |
98 | + end | |
99 | + | |
100 | + def test_custom_attributes_distance_calculations | |
101 | + assert_equal 0, @custom_loc_a.distance_to(@loc_a) | |
102 | + assert_equal 0, CustomLocation.distance_between(@custom_loc_a, @loc_a) | |
103 | + end | |
104 | + | |
105 | + def test_distance_column_in_select | |
106 | + locations = Location.find(:all, :origin => @loc_a, :order => "distance ASC") | |
107 | + assert_equal 6, locations.size | |
108 | + assert_equal 0, @loc_a.distance_to(locations.first) | |
109 | + assert_in_delta 3.97, @loc_a.distance_to(locations.last, :units => :miles, :formula => :sphere), 0.01 | |
110 | + end | |
111 | + | |
112 | + def test_find_with_distance_condition | |
113 | + locations = Location.find(:all, :origin => @loc_a, :conditions => "distance < 3.97") | |
114 | + assert_equal 5, locations.size | |
115 | + locations = Location.count(:origin => @loc_a, :conditions => "distance < 3.97") | |
116 | + assert_equal 5, locations | |
117 | + end | |
118 | + | |
119 | + def test_find_with_distance_condition_with_units_override | |
120 | + locations = Location.find(:all, :origin => @loc_a, :units => :kms, :conditions => "distance < 6.387") | |
121 | + assert_equal 5, locations.size | |
122 | + locations = Location.count(:origin => @loc_a, :units => :kms, :conditions => "distance < 6.387") | |
123 | + assert_equal 5, locations | |
124 | + end | |
125 | + | |
126 | + def test_find_with_distance_condition_with_formula_override | |
127 | + locations = Location.find(:all, :origin => @loc_a, :formula => :flat, :conditions => "distance < 6.387") | |
128 | + assert_equal 6, locations.size | |
129 | + locations = Location.count(:origin => @loc_a, :formula => :flat, :conditions => "distance < 6.387") | |
130 | + assert_equal 6, locations | |
131 | + end | |
132 | + | |
133 | + def test_find_within | |
134 | + locations = Location.find_within(3.97, :origin => @loc_a) | |
135 | + assert_equal 5, locations.size | |
136 | + locations = Location.count_within(3.97, :origin => @loc_a) | |
137 | + assert_equal 5, locations | |
138 | + end | |
139 | + | |
140 | + def test_find_within_with_token | |
141 | + locations = Location.find(:all, :within => 3.97, :origin => @loc_a) | |
142 | + assert_equal 5, locations.size | |
143 | + locations = Location.count(:within => 3.97, :origin => @loc_a) | |
144 | + assert_equal 5, locations | |
145 | + end | |
146 | + | |
147 | + def test_find_within_with_coordinates | |
148 | + locations = Location.find_within(3.97, :origin =>[@loc_a.lat,@loc_a.lng]) | |
149 | + assert_equal 5, locations.size | |
150 | + locations = Location.count_within(3.97, :origin =>[@loc_a.lat,@loc_a.lng]) | |
151 | + assert_equal 5, locations | |
152 | + end | |
153 | + | |
154 | + def test_find_with_compound_condition | |
155 | + locations = Location.find(:all, :origin => @loc_a, :conditions => "distance < 5 and city = 'Coppell'") | |
156 | + assert_equal 2, locations.size | |
157 | + locations = Location.count(:origin => @loc_a, :conditions => "distance < 5 and city = 'Coppell'") | |
158 | + assert_equal 2, locations | |
159 | + end | |
160 | + | |
161 | + def test_find_with_secure_compound_condition | |
162 | + locations = Location.find(:all, :origin => @loc_a, :conditions => ["distance < ? and city = ?", 5, 'Coppell']) | |
163 | + assert_equal 2, locations.size | |
164 | + locations = Location.count(:origin => @loc_a, :conditions => ["distance < ? and city = ?", 5, 'Coppell']) | |
165 | + assert_equal 2, locations | |
166 | + end | |
167 | + | |
168 | + def test_find_beyond | |
169 | + locations = Location.find_beyond(3.95, :origin => @loc_a) | |
170 | + assert_equal 1, locations.size | |
171 | + locations = Location.count_beyond(3.95, :origin => @loc_a) | |
172 | + assert_equal 1, locations | |
173 | + end | |
174 | + | |
175 | + def test_find_beyond_with_token | |
176 | + locations = Location.find(:all, :beyond => 3.95, :origin => @loc_a) | |
177 | + assert_equal 1, locations.size | |
178 | + locations = Location.count(:beyond => 3.95, :origin => @loc_a) | |
179 | + assert_equal 1, locations | |
180 | + end | |
181 | + | |
182 | + def test_find_beyond_with_coordinates | |
183 | + locations = Location.find_beyond(3.95, :origin =>[@loc_a.lat, @loc_a.lng]) | |
184 | + assert_equal 1, locations.size | |
185 | + locations = Location.count_beyond(3.95, :origin =>[@loc_a.lat, @loc_a.lng]) | |
186 | + assert_equal 1, locations | |
187 | + end | |
188 | + | |
189 | + def test_find_range_with_token | |
190 | + locations = Location.find(:all, :range => 0..10, :origin => @loc_a) | |
191 | + assert_equal 6, locations.size | |
192 | + locations = Location.count(:range => 0..10, :origin => @loc_a) | |
193 | + assert_equal 6, locations | |
194 | + end | |
195 | + | |
196 | + def test_find_range_with_token_with_conditions | |
197 | + locations = Location.find(:all, :origin => @loc_a, :range => 0..10, :conditions => ["city = ?", 'Coppell']) | |
198 | + assert_equal 2, locations.size | |
199 | + locations = Location.count(:origin => @loc_a, :range => 0..10, :conditions => ["city = ?", 'Coppell']) | |
200 | + assert_equal 2, locations | |
201 | + end | |
202 | + | |
203 | + def test_find_range_with_token_excluding_end | |
204 | + locations = Location.find(:all, :range => 0...10, :origin => @loc_a) | |
205 | + assert_equal 6, locations.size | |
206 | + locations = Location.count(:range => 0...10, :origin => @loc_a) | |
207 | + assert_equal 6, locations | |
208 | + end | |
209 | + | |
210 | + def test_find_nearest | |
211 | + assert_equal @loc_a, Location.find_nearest(:origin => @loc_a) | |
212 | + end | |
213 | + | |
214 | + def test_find_nearest_through_find | |
215 | + assert_equal @loc_a, Location.find(:nearest, :origin => @loc_a) | |
216 | + end | |
217 | + | |
218 | + def test_find_nearest_with_coordinates | |
219 | + assert_equal @loc_a, Location.find_nearest(:origin =>[@loc_a.lat, @loc_a.lng]) | |
220 | + end | |
221 | + | |
222 | + def test_find_farthest | |
223 | + assert_equal @loc_e, Location.find_farthest(:origin => @loc_a) | |
224 | + end | |
225 | + | |
226 | + def test_find_farthest_through_find | |
227 | + assert_equal @loc_e, Location.find(:farthest, :origin => @loc_a) | |
228 | + end | |
229 | + | |
230 | + def test_find_farthest_with_coordinates | |
231 | + assert_equal @loc_e, Location.find_farthest(:origin =>[@loc_a.lat, @loc_a.lng]) | |
232 | + end | |
233 | + | |
234 | + def test_scoped_distance_column_in_select | |
235 | + locations = @starbucks.locations.find(:all, :origin => @loc_a, :order => "distance ASC") | |
236 | + assert_equal 5, locations.size | |
237 | + assert_equal 0, @loc_a.distance_to(locations.first) | |
238 | + assert_in_delta 3.97, @loc_a.distance_to(locations.last, :units => :miles, :formula => :sphere), 0.01 | |
239 | + end | |
240 | + | |
241 | + def test_scoped_find_with_distance_condition | |
242 | + locations = @starbucks.locations.find(:all, :origin => @loc_a, :conditions => "distance < 3.97") | |
243 | + assert_equal 4, locations.size | |
244 | + locations = @starbucks.locations.count(:origin => @loc_a, :conditions => "distance < 3.97") | |
245 | + assert_equal 4, locations | |
246 | + end | |
247 | + | |
248 | + def test_scoped_find_within | |
249 | + locations = @starbucks.locations.find_within(3.97, :origin => @loc_a) | |
250 | + assert_equal 4, locations.size | |
251 | + locations = @starbucks.locations.count_within(3.97, :origin => @loc_a) | |
252 | + assert_equal 4, locations | |
253 | + end | |
254 | + | |
255 | + def test_scoped_find_with_compound_condition | |
256 | + locations = @starbucks.locations.find(:all, :origin => @loc_a, :conditions => "distance < 5 and city = 'Coppell'") | |
257 | + assert_equal 2, locations.size | |
258 | + locations = @starbucks.locations.count( :origin => @loc_a, :conditions => "distance < 5 and city = 'Coppell'") | |
259 | + assert_equal 2, locations | |
260 | + end | |
261 | + | |
262 | + def test_scoped_find_beyond | |
263 | + locations = @starbucks.locations.find_beyond(3.95, :origin => @loc_a) | |
264 | + assert_equal 1, locations.size | |
265 | + locations = @starbucks.locations.count_beyond(3.95, :origin => @loc_a) | |
266 | + assert_equal 1, locations | |
267 | + end | |
268 | + | |
269 | + def test_scoped_find_nearest | |
270 | + assert_equal @loc_a, @starbucks.locations.find_nearest(:origin => @loc_a) | |
271 | + end | |
272 | + | |
273 | + def test_scoped_find_farthest | |
274 | + assert_equal @loc_e, @starbucks.locations.find_farthest(:origin => @loc_a) | |
275 | + end | |
276 | + | |
277 | + def test_ip_geocoded_distance_column_in_select | |
278 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) | |
279 | + locations = Location.find(:all, :origin => LOCATION_A_IP, :order => "distance ASC") | |
280 | + assert_equal 6, locations.size | |
281 | + assert_equal 0, @loc_a.distance_to(locations.first) | |
282 | + assert_in_delta 3.97, @loc_a.distance_to(locations.last, :units => :miles, :formula => :sphere), 0.01 | |
283 | + end | |
284 | + | |
285 | + def test_ip_geocoded_find_with_distance_condition | |
286 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) | |
287 | + locations = Location.find(:all, :origin => LOCATION_A_IP, :conditions => "distance < 3.97") | |
288 | + assert_equal 5, locations.size | |
289 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) | |
290 | + locations = Location.count(:origin => LOCATION_A_IP, :conditions => "distance < 3.97") | |
291 | + assert_equal 5, locations | |
292 | + end | |
293 | + | |
294 | + def test_ip_geocoded_find_within | |
295 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) | |
296 | + locations = Location.find_within(3.97, :origin => LOCATION_A_IP) | |
297 | + assert_equal 5, locations.size | |
298 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) | |
299 | + locations = Location.count_within(3.97, :origin => LOCATION_A_IP) | |
300 | + assert_equal 5, locations | |
301 | + end | |
302 | + | |
303 | + def test_ip_geocoded_find_with_compound_condition | |
304 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) | |
305 | + locations = Location.find(:all, :origin => LOCATION_A_IP, :conditions => "distance < 5 and city = 'Coppell'") | |
306 | + assert_equal 2, locations.size | |
307 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) | |
308 | + locations = Location.count(:origin => LOCATION_A_IP, :conditions => "distance < 5 and city = 'Coppell'") | |
309 | + assert_equal 2, locations | |
310 | + end | |
311 | + | |
312 | + def test_ip_geocoded_find_with_secure_compound_condition | |
313 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) | |
314 | + locations = Location.find(:all, :origin => LOCATION_A_IP, :conditions => ["distance < ? and city = ?", 5, 'Coppell']) | |
315 | + assert_equal 2, locations.size | |
316 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) | |
317 | + locations = Location.count(:origin => LOCATION_A_IP, :conditions => ["distance < ? and city = ?", 5, 'Coppell']) | |
318 | + assert_equal 2, locations | |
319 | + end | |
320 | + | |
321 | + def test_ip_geocoded_find_beyond | |
322 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) | |
323 | + locations = Location.find_beyond(3.95, :origin => LOCATION_A_IP) | |
324 | + assert_equal 1, locations.size | |
325 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) | |
326 | + locations = Location.count_beyond(3.95, :origin => LOCATION_A_IP) | |
327 | + assert_equal 1, locations | |
328 | + end | |
329 | + | |
330 | + def test_ip_geocoded_find_nearest | |
331 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) | |
332 | + assert_equal @loc_a, Location.find_nearest(:origin => LOCATION_A_IP) | |
333 | + end | |
334 | + | |
335 | + def test_ip_geocoded_find_farthest | |
336 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) | |
337 | + assert_equal @loc_e, Location.find_farthest(:origin => LOCATION_A_IP) | |
338 | + end | |
339 | + | |
340 | + def test_ip_geocoder_exception | |
341 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with('127.0.0.1').returns(GeoKit::GeoLoc.new) | |
342 | + assert_raises GeoKit::Geocoders::GeocodeError do | |
343 | + Location.find_farthest(:origin => '127.0.0.1') | |
344 | + end | |
345 | + end | |
346 | + | |
347 | + def test_address_geocode | |
348 | + GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with('Irving, TX').returns(@location_a) | |
349 | + locations = Location.find(:all, :origin => 'Irving, TX', :conditions => ["distance < ? and city = ?", 5, 'Coppell']) | |
350 | + assert_equal 2, locations.size | |
351 | + end | |
352 | + | |
353 | + def test_find_with_custom_distance_condition | |
354 | + locations = CustomLocation.find(:all, :origin => @loc_a, :conditions => "dist < 3.97") | |
355 | + assert_equal 5, locations.size | |
356 | + locations = CustomLocation.count(:origin => @loc_a, :conditions => "dist < 3.97") | |
357 | + assert_equal 5, locations | |
358 | + end | |
359 | + | |
360 | + def test_find_with_custom_distance_condition_using_custom_origin | |
361 | + locations = CustomLocation.find(:all, :origin => @custom_loc_a, :conditions => "dist < 3.97") | |
362 | + assert_equal 5, locations.size | |
363 | + locations = CustomLocation.count(:origin => @custom_loc_a, :conditions => "dist < 3.97") | |
364 | + assert_equal 5, locations | |
365 | + end | |
366 | + | |
367 | + def test_find_within_with_custom | |
368 | + locations = CustomLocation.find_within(3.97, :origin => @loc_a) | |
369 | + assert_equal 5, locations.size | |
370 | + locations = CustomLocation.count_within(3.97, :origin => @loc_a) | |
371 | + assert_equal 5, locations | |
372 | + end | |
373 | + | |
374 | + def test_find_within_with_coordinates_with_custom | |
375 | + locations = CustomLocation.find_within(3.97, :origin =>[@loc_a.lat, @loc_a.lng]) | |
376 | + assert_equal 5, locations.size | |
377 | + locations = CustomLocation.count_within(3.97, :origin =>[@loc_a.lat, @loc_a.lng]) | |
378 | + assert_equal 5, locations | |
379 | + end | |
380 | + | |
381 | + def test_find_with_compound_condition_with_custom | |
382 | + locations = CustomLocation.find(:all, :origin => @loc_a, :conditions => "dist < 5 and city = 'Coppell'") | |
383 | + assert_equal 1, locations.size | |
384 | + locations = CustomLocation.count(:origin => @loc_a, :conditions => "dist < 5 and city = 'Coppell'") | |
385 | + assert_equal 1, locations | |
386 | + end | |
387 | + | |
388 | + def test_find_with_secure_compound_condition_with_custom | |
389 | + locations = CustomLocation.find(:all, :origin => @loc_a, :conditions => ["dist < ? and city = ?", 5, 'Coppell']) | |
390 | + assert_equal 1, locations.size | |
391 | + locations = CustomLocation.count(:origin => @loc_a, :conditions => ["dist < ? and city = ?", 5, 'Coppell']) | |
392 | + assert_equal 1, locations | |
393 | + end | |
394 | + | |
395 | + def test_find_beyond_with_custom | |
396 | + locations = CustomLocation.find_beyond(3.95, :origin => @loc_a) | |
397 | + assert_equal 1, locations.size | |
398 | + locations = CustomLocation.count_beyond(3.95, :origin => @loc_a) | |
399 | + assert_equal 1, locations | |
400 | + end | |
401 | + | |
402 | + def test_find_beyond_with_coordinates_with_custom | |
403 | + locations = CustomLocation.find_beyond(3.95, :origin =>[@loc_a.lat, @loc_a.lng]) | |
404 | + assert_equal 1, locations.size | |
405 | + locations = CustomLocation.count_beyond(3.95, :origin =>[@loc_a.lat, @loc_a.lng]) | |
406 | + assert_equal 1, locations | |
407 | + end | |
408 | + | |
409 | + def test_find_nearest_with_custom | |
410 | + assert_equal @custom_loc_a, CustomLocation.find_nearest(:origin => @loc_a) | |
411 | + end | |
412 | + | |
413 | + def test_find_nearest_with_coordinates_with_custom | |
414 | + assert_equal @custom_loc_a, CustomLocation.find_nearest(:origin =>[@loc_a.lat, @loc_a.lng]) | |
415 | + end | |
416 | + | |
417 | + def test_find_farthest_with_custom | |
418 | + assert_equal @custom_loc_e, CustomLocation.find_farthest(:origin => @loc_a) | |
419 | + end | |
420 | + | |
421 | + def test_find_farthest_with_coordinates_with_custom | |
422 | + assert_equal @custom_loc_e, CustomLocation.find_farthest(:origin =>[@loc_a.lat, @loc_a.lng]) | |
423 | + end | |
424 | + | |
425 | + def test_find_with_array_origin | |
426 | + locations = Location.find(:all, :origin =>[@loc_a.lat,@loc_a.lng], :conditions => "distance < 3.97") | |
427 | + assert_equal 5, locations.size | |
428 | + locations = Location.count(:origin =>[@loc_a.lat,@loc_a.lng], :conditions => "distance < 3.97") | |
429 | + assert_equal 5, locations | |
430 | + end | |
431 | + | |
432 | + | |
433 | + # Bounding box tests | |
434 | + | |
435 | + def test_find_within_bounds | |
436 | + locations = Location.find_within_bounds([@sw,@ne]) | |
437 | + assert_equal 2, locations.size | |
438 | + locations = Location.count_within_bounds([@sw,@ne]) | |
439 | + assert_equal 2, locations | |
440 | + end | |
441 | + | |
442 | + def test_find_within_bounds_ordered_by_distance | |
443 | + locations = Location.find_within_bounds([@sw,@ne], :origin=>@bounds_center, :order=>'distance asc') | |
444 | + assert_equal locations[0], locations(:d) | |
445 | + assert_equal locations[1], locations(:a) | |
446 | + end | |
447 | + | |
448 | + def test_find_within_bounds_with_token | |
449 | + locations = Location.find(:all, :bounds=>[@sw,@ne]) | |
450 | + assert_equal 2, locations.size | |
451 | + locations = Location.count(:bounds=>[@sw,@ne]) | |
452 | + assert_equal 2, locations | |
453 | + end | |
454 | + | |
455 | + def test_find_within_bounds_with_string_conditions | |
456 | + locations = Location.find(:all, :bounds=>[@sw,@ne], :conditions=>"id !=#{locations(:a).id}") | |
457 | + assert_equal 1, locations.size | |
458 | + end | |
459 | + | |
460 | + def test_find_within_bounds_with_array_conditions | |
461 | + locations = Location.find(:all, :bounds=>[@sw,@ne], :conditions=>["id != ?", locations(:a).id]) | |
462 | + assert_equal 1, locations.size | |
463 | + end | |
464 | + | |
465 | + def test_auto_geocode | |
466 | + GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("Irving, TX").returns(@location_a) | |
467 | + store=Store.new(:address=>'Irving, TX') | |
468 | + store.save | |
469 | + assert_equal store.lat,@location_a.lat | |
470 | + assert_equal store.lng,@location_a.lng | |
471 | + assert_equal 0, store.errors.size | |
472 | + end | |
473 | + | |
474 | + def test_auto_geocode_failure | |
475 | + GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("BOGUS").returns(GeoKit::GeoLoc.new) | |
476 | + store=Store.new(:address=>'BOGUS') | |
477 | + store.save | |
478 | + assert store.new_record? | |
479 | + assert_equal 1, store.errors.size | |
480 | + end | |
481 | +end | ... | ... |
... | ... | @@ -0,0 +1,57 @@ |
1 | +require 'test/unit' | |
2 | +require 'net/http' | |
3 | +require 'rubygems' | |
4 | +require 'mocha' | |
5 | +require File.join(File.dirname(__FILE__), '../../../../config/environment') | |
6 | + | |
7 | + | |
8 | +class MockSuccess < Net::HTTPSuccess #:nodoc: all | |
9 | + def initialize | |
10 | + end | |
11 | +end | |
12 | + | |
13 | +class MockFailure < Net::HTTPServiceUnavailable #:nodoc: all | |
14 | + def initialize | |
15 | + end | |
16 | +end | |
17 | + | |
18 | +# Base class for testing geocoders. | |
19 | +class BaseGeocoderTest < Test::Unit::TestCase #:nodoc: all | |
20 | + | |
21 | + # Defines common test fixtures. | |
22 | + def setup | |
23 | + @address = 'San Francisco, CA' | |
24 | + @full_address = '100 Spear St, San Francisco, CA, 94105-1522, US' | |
25 | + @full_address_short_zip = '100 Spear St, San Francisco, CA, 94105, US' | |
26 | + | |
27 | + @success = GeoKit::GeoLoc.new({:city=>"SAN FRANCISCO", :state=>"CA", :country_code=>"US", :lat=>37.7742, :lng=>-122.417068}) | |
28 | + @success.success = true | |
29 | + end | |
30 | + | |
31 | + def test_timeout_call_web_service | |
32 | + GeoKit::Geocoders::Geocoder.class_eval do | |
33 | + def self.do_get(url) | |
34 | + sleep(2) | |
35 | + end | |
36 | + end | |
37 | + url = "http://www.anything.com" | |
38 | + GeoKit::Geocoders::timeout = 1 | |
39 | + assert_nil GeoKit::Geocoders::Geocoder.call_geocoder_service(url) | |
40 | + end | |
41 | + | |
42 | + def test_successful_call_web_service | |
43 | + url = "http://www.anything.com" | |
44 | + GeoKit::Geocoders::Geocoder.expects(:do_get).with(url).returns("SUCCESS") | |
45 | + assert_equal "SUCCESS", GeoKit::Geocoders::Geocoder.call_geocoder_service(url) | |
46 | + end | |
47 | + | |
48 | + def test_find_geocoder_methods | |
49 | + public_methods = GeoKit::Geocoders::Geocoder.public_methods | |
50 | + assert public_methods.include?("yahoo_geocoder") | |
51 | + assert public_methods.include?("google_geocoder") | |
52 | + assert public_methods.include?("ca_geocoder") | |
53 | + assert public_methods.include?("us_geocoder") | |
54 | + assert public_methods.include?("multi_geocoder") | |
55 | + assert public_methods.include?("ip_geocoder") | |
56 | + end | |
57 | +end | |
0 | 58 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,75 @@ |
1 | +$LOAD_PATH.unshift File.join('..', 'lib') | |
2 | +require 'geo_kit/mappable' | |
3 | +require 'test/unit' | |
4 | + | |
5 | +class BoundsTest < Test::Unit::TestCase #:nodoc: all | |
6 | + | |
7 | + def setup | |
8 | + # This is the area in Texas | |
9 | + @sw = GeoKit::LatLng.new(32.91663,-96.982841) | |
10 | + @ne = GeoKit::LatLng.new(32.96302,-96.919495) | |
11 | + @bounds=GeoKit::Bounds.new(@sw,@ne) | |
12 | + @loc_a=GeoKit::LatLng.new(32.918593,-96.958444) # inside bounds | |
13 | + @loc_b=GeoKit::LatLng.new(32.914144,-96.958444) # outside bouds | |
14 | + | |
15 | + # this is a cross-meridan area | |
16 | + @cross_meridian=GeoKit::Bounds.normalize([30,170],[40,-170]) | |
17 | + @inside_cm=GeoKit::LatLng.new(35,175) | |
18 | + @inside_cm_2=GeoKit::LatLng.new(35,-175) | |
19 | + @east_of_cm=GeoKit::LatLng.new(35,-165) | |
20 | + @west_of_cm=GeoKit::LatLng.new(35,165) | |
21 | + | |
22 | + end | |
23 | + | |
24 | + def test_equality | |
25 | + assert_equal GeoKit::Bounds.new(@sw,@ne), GeoKit::Bounds.new(@sw,@ne) | |
26 | + end | |
27 | + | |
28 | + def test_normalize | |
29 | + res=GeoKit::Bounds.normalize(@sw,@ne) | |
30 | + assert_equal res,GeoKit::Bounds.new(@sw,@ne) | |
31 | + res=GeoKit::Bounds.normalize([@sw,@ne]) | |
32 | + assert_equal res,GeoKit::Bounds.new(@sw,@ne) | |
33 | + res=GeoKit::Bounds.normalize([@sw.lat,@sw.lng],[@ne.lat,@ne.lng]) | |
34 | + assert_equal res,GeoKit::Bounds.new(@sw,@ne) | |
35 | + res=GeoKit::Bounds.normalize([[@sw.lat,@sw.lng],[@ne.lat,@ne.lng]]) | |
36 | + assert_equal res,GeoKit::Bounds.new(@sw,@ne) | |
37 | + end | |
38 | + | |
39 | + def test_point_inside_bounds | |
40 | + assert @bounds.contains?(@loc_a) | |
41 | + end | |
42 | + | |
43 | + def test_point_outside_bounds | |
44 | + assert !@bounds.contains?(@loc_b) | |
45 | + end | |
46 | + | |
47 | + def test_point_inside_bounds_cross_meridian | |
48 | + assert @cross_meridian.contains?(@inside_cm) | |
49 | + assert @cross_meridian.contains?(@inside_cm_2) | |
50 | + end | |
51 | + | |
52 | + def test_point_outside_bounds_cross_meridian | |
53 | + assert !@cross_meridian.contains?(@east_of_cm) | |
54 | + assert !@cross_meridian.contains?(@west_of_cm) | |
55 | + end | |
56 | + | |
57 | + def test_center | |
58 | + assert_in_delta 32.939828,@bounds.center.lat,0.00005 | |
59 | + assert_in_delta -96.9511763,@bounds.center.lng,0.00005 | |
60 | + end | |
61 | + | |
62 | + def test_center_cross_meridian | |
63 | + assert_in_delta 35.41160, @cross_meridian.center.lat,0.00005 | |
64 | + assert_in_delta 179.38112, @cross_meridian.center.lng,0.00005 | |
65 | + end | |
66 | + | |
67 | + def test_creation_from_circle | |
68 | + bounds=GeoKit::Bounds.from_point_and_radius([32.939829, -96.951176],2.5) | |
69 | + inside=GeoKit::LatLng.new 32.9695270000,-96.9901590000 | |
70 | + outside=GeoKit::LatLng.new 32.8951550000,-96.9584440000 | |
71 | + assert bounds.contains?(inside) | |
72 | + assert !bounds.contains?(outside) | |
73 | + end | |
74 | + | |
75 | +end | |
0 | 76 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,41 @@ |
1 | +require File.join(File.dirname(__FILE__), 'base_geocoder_test') | |
2 | + | |
3 | +GeoKit::Geocoders::geocoder_ca = "SOMEKEYVALUE" | |
4 | + | |
5 | +class CaGeocoderTest < BaseGeocoderTest #:nodoc: all | |
6 | + | |
7 | + CA_SUCCESS=<<-EOF | |
8 | + <?xml version="1.0" encoding="UTF-8" ?> | |
9 | + <geodata><latt>49.243086</latt><longt>-123.153684</longt></geodata> | |
10 | + EOF | |
11 | + | |
12 | + def setup | |
13 | + @ca_full_hash = {:street_address=>"2105 West 32nd Avenue",:city=>"Vancouver", :state=>"BC"} | |
14 | + @ca_full_loc = GeoKit::GeoLoc.new(@ca_full_hash) | |
15 | + end | |
16 | + | |
17 | + def test_geocoder_with_geo_loc_with_account | |
18 | + response = MockSuccess.new | |
19 | + response.expects(:body).returns(CA_SUCCESS) | |
20 | + url = "http://geocoder.ca/?stno=2105&addresst=West+32nd+Avenue&city=Vancouver&prov=BC&auth=SOMEKEYVALUE&geoit=xml" | |
21 | + GeoKit::Geocoders::CaGeocoder.expects(:call_geocoder_service).with(url).returns(response) | |
22 | + verify(GeoKit::Geocoders::CaGeocoder.geocode(@ca_full_loc)) | |
23 | + end | |
24 | + | |
25 | + def test_service_unavailable | |
26 | + response = MockFailure.new | |
27 | + #Net::HTTP.expects(:get_response).with(URI.parse("http://geocoder.ca/?stno=2105&addresst=West+32nd+Avenue&city=Vancouver&prov=BC&auth=SOMEKEYVALUE&geoit=xml")).returns(response) | |
28 | + url = "http://geocoder.ca/?stno=2105&addresst=West+32nd+Avenue&city=Vancouver&prov=BC&auth=SOMEKEYVALUE&geoit=xml" | |
29 | + GeoKit::Geocoders::CaGeocoder.expects(:call_geocoder_service).with(url).returns(response) | |
30 | + assert !GeoKit::Geocoders::CaGeocoder.geocode(@ca_full_loc).success | |
31 | + end | |
32 | + | |
33 | + private | |
34 | + | |
35 | + def verify(location) | |
36 | + assert_equal "BC", location.state | |
37 | + assert_equal "Vancouver", location.city | |
38 | + assert_equal "49.243086,-123.153684", location.ll | |
39 | + assert !location.is_us? | |
40 | + end | |
41 | +end | |
0 | 42 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,12 @@ |
1 | +mysql: | |
2 | + adapter: mysql | |
3 | + host: localhost | |
4 | + username: root | |
5 | + password: | |
6 | + database: geokit_plugin_test | |
7 | +postgresql: | |
8 | + adapter: postgresql | |
9 | + host: localhost | |
10 | + username: root | |
11 | + password: | |
12 | + database: geokit_plugin_test | |
0 | 13 | \ No newline at end of file | ... | ... |
vendor/plugins/geokit/test/fixtures/custom_locations.yml
0 → 100644
... | ... | @@ -0,0 +1,54 @@ |
1 | +a: | |
2 | + id: 1 | |
3 | + company_id: 1 | |
4 | + street: 7979 N MacArthur Blvd | |
5 | + city: Irving | |
6 | + state: TX | |
7 | + postal_code: 75063 | |
8 | + latitude: 32.918593 | |
9 | + longitude: -96.958444 | |
10 | +b: | |
11 | + id: 2 | |
12 | + company_id: 1 | |
13 | + street: 7750 N Macarthur Blvd # 160 | |
14 | + city: Irving | |
15 | + state: TX | |
16 | + postal_code: 75063 | |
17 | + latitude: 32.914144 | |
18 | + longitude: -96.958444 | |
19 | +c: | |
20 | + id: 3 | |
21 | + company_id: 1 | |
22 | + street: 5904 N Macarthur Blvd # 160 | |
23 | + city: Irving | |
24 | + state: TX | |
25 | + postal_code: 75039 | |
26 | + latitude: 32.895155 | |
27 | + longitude: -96.958444 | |
28 | +d: | |
29 | + id: 4 | |
30 | + company_id: 1 | |
31 | + street: 817 S Macarthur Blvd # 145 | |
32 | + city: Coppell | |
33 | + state: TX | |
34 | + postal_code: 75019 | |
35 | + latitude: 32.951613 | |
36 | + longitude: -96.958444 | |
37 | +e: | |
38 | + id: 5 | |
39 | + company_id: 1 | |
40 | + street: 106 N Denton Tap Rd # 350 | |
41 | + city: Coppell | |
42 | + state: TX | |
43 | + postal_code: 75019 | |
44 | + latitude: 32.969527 | |
45 | + longitude: -96.990159 | |
46 | +f: | |
47 | + id: 6 | |
48 | + company_id: 2 | |
49 | + street: 5904 N Macarthur Blvd # 160 | |
50 | + city: Irving | |
51 | + state: TX | |
52 | + postal_code: 75039 | |
53 | + latitude: 32.895155 | |
54 | + longitude: -96.958444 | |
0 | 55 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,54 @@ |
1 | +a: | |
2 | + id: 1 | |
3 | + company_id: 1 | |
4 | + street: 7979 N MacArthur Blvd | |
5 | + city: Irving | |
6 | + state: TX | |
7 | + postal_code: 75063 | |
8 | + lat: 32.918593 | |
9 | + lng: -96.958444 | |
10 | +b: | |
11 | + id: 2 | |
12 | + company_id: 1 | |
13 | + street: 7750 N Macarthur Blvd # 160 | |
14 | + city: Irving | |
15 | + state: TX | |
16 | + postal_code: 75063 | |
17 | + lat: 32.914144 | |
18 | + lng: -96.958444 | |
19 | +c: | |
20 | + id: 3 | |
21 | + company_id: 1 | |
22 | + street: 5904 N Macarthur Blvd # 160 | |
23 | + city: Irving | |
24 | + state: TX | |
25 | + postal_code: 75039 | |
26 | + lat: 32.895155 | |
27 | + lng: -96.958444 | |
28 | +d: | |
29 | + id: 4 | |
30 | + company_id: 1 | |
31 | + street: 817 S Macarthur Blvd # 145 | |
32 | + city: Coppell | |
33 | + state: TX | |
34 | + postal_code: 75019 | |
35 | + lat: 32.951613 | |
36 | + lng: -96.958444 | |
37 | +e: | |
38 | + id: 5 | |
39 | + company_id: 1 | |
40 | + street: 106 N Denton Tap Rd # 350 | |
41 | + city: Coppell | |
42 | + state: TX | |
43 | + postal_code: 75019 | |
44 | + lat: 32.969527 | |
45 | + lng: -96.990159 | |
46 | +f: | |
47 | + id: 6 | |
48 | + company_id: 2 | |
49 | + street: 5904 N Macarthur Blvd # 160 | |
50 | + city: Irving | |
51 | + state: TX | |
52 | + postal_code: 75039 | |
53 | + lat: 32.895155 | |
54 | + lng: -96.958444 | |
0 | 55 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,49 @@ |
1 | +require 'test/unit' | |
2 | +require File.join(File.dirname(__FILE__), '../../../../config/environment') | |
3 | + | |
4 | +class GeoLocTest < Test::Unit::TestCase #:nodoc: all | |
5 | + | |
6 | + def setup | |
7 | + @loc = GeoKit::GeoLoc.new | |
8 | + end | |
9 | + | |
10 | + def test_is_us | |
11 | + assert !@loc.is_us? | |
12 | + @loc.country_code = 'US' | |
13 | + assert @loc.is_us? | |
14 | + end | |
15 | + | |
16 | + def test_street_number | |
17 | + @loc.street_address = '123 Spear St.' | |
18 | + assert_equal '123', @loc.street_number | |
19 | + end | |
20 | + | |
21 | + def test_street_name | |
22 | + @loc.street_address = '123 Spear St.' | |
23 | + assert_equal 'Spear St.', @loc.street_name | |
24 | + end | |
25 | + | |
26 | + def test_city | |
27 | + @loc.city = "san francisco" | |
28 | + assert_equal 'San Francisco', @loc.city | |
29 | + end | |
30 | + | |
31 | + def test_full_address | |
32 | + @loc.city = 'San Francisco' | |
33 | + @loc.state = 'CA' | |
34 | + @loc.zip = '94105' | |
35 | + @loc.country_code = 'US' | |
36 | + assert_equal 'San Francisco, CA, 94105, US', @loc.full_address | |
37 | + @loc.full_address = 'Irving, TX, 75063, US' | |
38 | + assert_equal 'Irving, TX, 75063, US', @loc.full_address | |
39 | + end | |
40 | + | |
41 | + def test_hash | |
42 | + @loc.city = 'San Francisco' | |
43 | + @loc.state = 'CA' | |
44 | + @loc.zip = '94105' | |
45 | + @loc.country_code = 'US' | |
46 | + @another = GeoKit::GeoLoc.new @loc.to_hash | |
47 | + assert_equal @loc, @another | |
48 | + end | |
49 | +end | |
0 | 50 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,88 @@ |
1 | +require File.join(File.dirname(__FILE__), 'base_geocoder_test') | |
2 | + | |
3 | +GeoKit::Geocoders::google = 'Google' | |
4 | + | |
5 | +class GoogleGeocoderTest < BaseGeocoderTest #:nodoc: all | |
6 | + | |
7 | + GOOGLE_FULL=<<-EOF.strip | |
8 | + <?xml version="1.0" encoding="UTF-8"?><kml xmlns="http://earth.google.com/kml/2.0"><Response><name>100 spear st, san francisco, ca</name><Status><code>200</code><request>geocode</request></Status><Placemark><address>100 Spear St, San Francisco, CA 94105, USA</address><AddressDetails Accuracy="8" xmlns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"><Country><CountryNameCode>US</CountryNameCode><AdministrativeArea><AdministrativeAreaName>CA</AdministrativeAreaName><SubAdministrativeArea><SubAdministrativeAreaName>San Francisco</SubAdministrativeAreaName><Locality><LocalityName>San Francisco</LocalityName><Thoroughfare><ThoroughfareName>100 Spear St</ThoroughfareName></Thoroughfare><PostalCode><PostalCodeNumber>94105</PostalCodeNumber></PostalCode></Locality></SubAdministrativeArea></AdministrativeArea></Country></AddressDetails><Point><coordinates>-122.393985,37.792501,0</coordinates></Point></Placemark></Response></kml> | |
9 | + EOF | |
10 | + | |
11 | + GOOGLE_CITY=<<-EOF.strip | |
12 | + <?xml version="1.0" encoding="UTF-8"?><kml xmlns="http://earth.google.com/kml/2.0"><Response><name>San Francisco</name><Status><code>200</code><request>geocode</request></Status><Placemark><address>San Francisco, CA, USA</address><AddressDetails Accuracy="4" xmlns="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"><Country><CountryNameCode>US</CountryNameCode><AdministrativeArea><AdministrativeAreaName>CA</AdministrativeAreaName><Locality><LocalityName>San Francisco</LocalityName></Locality></AdministrativeArea></Country></AddressDetails><Point><coordinates>-122.418333,37.775000,0</coordinates></Point></Placemark></Response></kml> | |
13 | + EOF | |
14 | + | |
15 | + def setup | |
16 | + super | |
17 | + @google_full_hash = {:street_address=>"100 Spear St", :city=>"San Francisco", :state=>"CA", :zip=>"94105", :country_code=>"US"} | |
18 | + @google_city_hash = {:city=>"San Francisco", :state=>"CA"} | |
19 | + | |
20 | + @google_full_loc = GeoKit::GeoLoc.new(@google_full_hash) | |
21 | + @google_city_loc = GeoKit::GeoLoc.new(@google_city_hash) | |
22 | + end | |
23 | + | |
24 | + def test_google_full_address | |
25 | + response = MockSuccess.new | |
26 | + response.expects(:body).returns(GOOGLE_FULL) | |
27 | + url = "http://maps.google.com/maps/geo?q=#{CGI.escape(@address)}&output=xml&key=Google&oe=utf-8" | |
28 | + GeoKit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) | |
29 | + res=GeoKit::Geocoders::GoogleGeocoder.geocode(@address) | |
30 | + assert_equal "CA", res.state | |
31 | + assert_equal "San Francisco", res.city | |
32 | + assert_equal "37.792501,-122.393985", res.ll # slightly dif from yahoo | |
33 | + assert res.is_us? | |
34 | + assert_equal "100 Spear St, San Francisco, CA 94105, USA", res.full_address #slightly different from yahoo | |
35 | + assert_equal "google", res.provider | |
36 | + end | |
37 | + | |
38 | + def test_google_full_address_with_geo_loc | |
39 | + response = MockSuccess.new | |
40 | + response.expects(:body).returns(GOOGLE_FULL) | |
41 | + url = "http://maps.google.com/maps/geo?q=#{CGI.escape(@full_address_short_zip)}&output=xml&key=Google&oe=utf-8" | |
42 | + GeoKit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) | |
43 | + res=GeoKit::Geocoders::GoogleGeocoder.geocode(@google_full_loc) | |
44 | + assert_equal "CA", res.state | |
45 | + assert_equal "San Francisco", res.city | |
46 | + assert_equal "37.792501,-122.393985", res.ll # slightly dif from yahoo | |
47 | + assert res.is_us? | |
48 | + assert_equal "100 Spear St, San Francisco, CA 94105, USA", res.full_address #slightly different from yahoo | |
49 | + assert_equal "google", res.provider | |
50 | + end | |
51 | + | |
52 | + def test_google_city | |
53 | + response = MockSuccess.new | |
54 | + response.expects(:body).returns(GOOGLE_CITY) | |
55 | + url = "http://maps.google.com/maps/geo?q=#{CGI.escape(@address)}&output=xml&key=Google&oe=utf-8" | |
56 | + GeoKit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) | |
57 | + res=GeoKit::Geocoders::GoogleGeocoder.geocode(@address) | |
58 | + assert_equal "CA", res.state | |
59 | + assert_equal "San Francisco", res.city | |
60 | + assert_equal "37.775,-122.418333", res.ll | |
61 | + assert res.is_us? | |
62 | + assert_equal "San Francisco, CA, USA", res.full_address | |
63 | + assert_nil res.street_address | |
64 | + assert_equal "google", res.provider | |
65 | + end | |
66 | + | |
67 | + def test_google_city_with_geo_loc | |
68 | + response = MockSuccess.new | |
69 | + response.expects(:body).returns(GOOGLE_CITY) | |
70 | + url = "http://maps.google.com/maps/geo?q=#{CGI.escape(@address)}&output=xml&key=Google&oe=utf-8" | |
71 | + GeoKit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) | |
72 | + res=GeoKit::Geocoders::GoogleGeocoder.geocode(@google_city_loc) | |
73 | + assert_equal "CA", res.state | |
74 | + assert_equal "San Francisco", res.city | |
75 | + assert_equal "37.775,-122.418333", res.ll | |
76 | + assert res.is_us? | |
77 | + assert_equal "San Francisco, CA, USA", res.full_address | |
78 | + assert_nil res.street_address | |
79 | + assert_equal "google", res.provider | |
80 | + end | |
81 | + | |
82 | + def test_service_unavailable | |
83 | + response = MockFailure.new | |
84 | + url = "http://maps.google.com/maps/geo?q=#{CGI.escape(@address)}&output=xml&key=Google&oe=utf-8" | |
85 | + GeoKit::Geocoders::GoogleGeocoder.expects(:call_geocoder_service).with(url).returns(response) | |
86 | + assert !GeoKit::Geocoders::GoogleGeocoder.geocode(@google_city_loc).success | |
87 | + end | |
88 | +end | |
0 | 89 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,84 @@ |
1 | +require File.join(File.dirname(__FILE__), '../../../../config/environment') | |
2 | +require 'action_controller/test_process' | |
3 | +require 'test/unit' | |
4 | +require 'rubygems' | |
5 | +require 'mocha' | |
6 | + | |
7 | + | |
8 | +class LocationAwareController < ActionController::Base #:nodoc: all | |
9 | + geocode_ip_address | |
10 | + | |
11 | + def index | |
12 | + render :nothing => true | |
13 | + end | |
14 | +end | |
15 | + | |
16 | +class ActionController::TestRequest #:nodoc: all | |
17 | + attr_accessor :remote_ip | |
18 | +end | |
19 | + | |
20 | +# Re-raise errors caught by the controller. | |
21 | +class LocationAwareController #:nodoc: all | |
22 | + def rescue_action(e) raise e end; | |
23 | +end | |
24 | + | |
25 | +class IpGeocodeLookupTest < Test::Unit::TestCase #:nodoc: all | |
26 | + | |
27 | + def setup | |
28 | + @success = GeoKit::GeoLoc.new | |
29 | + @success.provider = "hostip" | |
30 | + @success.lat = 41.7696 | |
31 | + @success.lng = -88.4588 | |
32 | + @success.city = "Sugar Grove" | |
33 | + @success.state = "IL" | |
34 | + @success.country_code = "US" | |
35 | + @success.success = true | |
36 | + | |
37 | + @failure = GeoKit::GeoLoc.new | |
38 | + @failure.provider = "hostip" | |
39 | + @failure.city = "(Private Address)" | |
40 | + @failure.success = false | |
41 | + | |
42 | + @controller = LocationAwareController.new | |
43 | + @request = ActionController::TestRequest.new | |
44 | + @response = ActionController::TestResponse.new | |
45 | + end | |
46 | + | |
47 | + def test_no_location_in_cookie_or_session | |
48 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with("good ip").returns(@success) | |
49 | + @request.remote_ip = "good ip" | |
50 | + get :index | |
51 | + verify | |
52 | + end | |
53 | + | |
54 | + def test_location_in_cookie | |
55 | + @request.remote_ip = "good ip" | |
56 | + @request.cookies['geo_location'] = CGI::Cookie.new('geo_location', @success.to_yaml) | |
57 | + get :index | |
58 | + verify | |
59 | + end | |
60 | + | |
61 | + def test_location_in_session | |
62 | + @request.remote_ip = "good ip" | |
63 | + @request.session[:geo_location] = @success | |
64 | + @request.cookies['geo_location'] = CGI::Cookie.new('geo_location', @success.to_yaml) | |
65 | + get :index | |
66 | + verify | |
67 | + end | |
68 | + | |
69 | + def test_ip_not_located | |
70 | + GeoKit::Geocoders::IpGeocoder.expects(:geocode).with("bad ip").returns(@failure) | |
71 | + @request.remote_ip = "bad ip" | |
72 | + get :index | |
73 | + assert_nil @request.session[:geo_location] | |
74 | + end | |
75 | + | |
76 | + private | |
77 | + | |
78 | + def verify | |
79 | + assert_response :success | |
80 | + assert_equal @success, @request.session[:geo_location] | |
81 | + assert_not_nil cookies['geo_location'] | |
82 | + assert_equal @success, YAML.load(cookies['geo_location'].join) | |
83 | + end | |
84 | +end | |
0 | 85 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,87 @@ |
1 | +require File.join(File.dirname(__FILE__), 'base_geocoder_test') | |
2 | + | |
3 | +class IpGeocoderTest < BaseGeocoderTest #:nodoc: all | |
4 | + | |
5 | + IP_FAILURE=<<-EOF | |
6 | + Country: (Private Address) (XX) | |
7 | + City: (Private Address) | |
8 | + Latitude: | |
9 | + Longitude: | |
10 | + EOF | |
11 | + | |
12 | + IP_SUCCESS=<<-EOF | |
13 | + Country: UNITED STATES (US) | |
14 | + City: Sugar Grove, IL | |
15 | + Latitude: 41.7696 | |
16 | + Longitude: -88.4588 | |
17 | + EOF | |
18 | + | |
19 | + IP_UNICODED=<<-EOF | |
20 | + Country: SWEDEN (SE) | |
21 | + City: Borås | |
22 | + Latitude: 57.7167 | |
23 | + Longitude: 12.9167 | |
24 | + EOF | |
25 | + | |
26 | + def setup | |
27 | + super | |
28 | + @success.provider = "hostip" | |
29 | + end | |
30 | + | |
31 | + def test_successful_lookup | |
32 | + success = MockSuccess.new | |
33 | + success.expects(:body).returns(IP_SUCCESS) | |
34 | + url = 'http://api.hostip.info/get_html.php?ip=12.215.42.19&position=true' | |
35 | + GeoKit::Geocoders::IpGeocoder.expects(:call_geocoder_service).with(url).returns(success) | |
36 | + location = GeoKit::Geocoders::IpGeocoder.geocode('12.215.42.19') | |
37 | + assert_not_nil location | |
38 | + assert_equal 41.7696, location.lat | |
39 | + assert_equal -88.4588, location.lng | |
40 | + assert_equal "Sugar Grove", location.city | |
41 | + assert_equal "IL", location.state | |
42 | + assert_equal "US", location.country_code | |
43 | + assert_equal "hostip", location.provider | |
44 | + assert location.success | |
45 | + end | |
46 | + | |
47 | + def test_unicoded_lookup | |
48 | + success = MockSuccess.new | |
49 | + success.expects(:body).returns(IP_UNICODED) | |
50 | + url = 'http://api.hostip.info/get_html.php?ip=12.215.42.19&position=true' | |
51 | + GeoKit::Geocoders::IpGeocoder.expects(:call_geocoder_service).with(url).returns(success) | |
52 | + location = GeoKit::Geocoders::IpGeocoder.geocode('12.215.42.19') | |
53 | + assert_not_nil location | |
54 | + assert_equal 57.7167, location.lat | |
55 | + assert_equal 12.9167, location.lng | |
56 | + assert_equal "Borås", location.city | |
57 | + assert_nil location.state | |
58 | + assert_equal "SE", location.country_code | |
59 | + assert_equal "hostip", location.provider | |
60 | + assert location.success | |
61 | + end | |
62 | + | |
63 | + def test_failed_lookup | |
64 | + failure = MockSuccess.new | |
65 | + failure.expects(:body).returns(IP_FAILURE) | |
66 | + url = 'http://api.hostip.info/get_html.php?ip=0.0.0.0&position=true' | |
67 | + GeoKit::Geocoders::IpGeocoder.expects(:call_geocoder_service).with(url).returns(failure) | |
68 | + location = GeoKit::Geocoders::IpGeocoder.geocode("0.0.0.0") | |
69 | + assert_not_nil location | |
70 | + assert !location.success | |
71 | + end | |
72 | + | |
73 | + def test_invalid_ip | |
74 | + location = GeoKit::Geocoders::IpGeocoder.geocode("blah") | |
75 | + assert_not_nil location | |
76 | + assert !location.success | |
77 | + end | |
78 | + | |
79 | + def test_service_unavailable | |
80 | + failure = MockFailure.new | |
81 | + url = 'http://api.hostip.info/get_html.php?ip=0.0.0.0&position=true' | |
82 | + GeoKit::Geocoders::IpGeocoder.expects(:call_geocoder_service).with(url).returns(failure) | |
83 | + location = GeoKit::Geocoders::IpGeocoder.geocode("0.0.0.0") | |
84 | + assert_not_nil location | |
85 | + assert !location.success | |
86 | + end | |
87 | +end | |
0 | 88 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,112 @@ |
1 | +$LOAD_PATH.unshift File.join('..', 'lib') | |
2 | +require 'geo_kit/mappable' | |
3 | +require 'test/unit' | |
4 | + | |
5 | +class LatLngTest < Test::Unit::TestCase #:nodoc: all | |
6 | + | |
7 | + def setup | |
8 | + @loc_a = GeoKit::LatLng.new(32.918593,-96.958444) | |
9 | + @loc_e = GeoKit::LatLng.new(32.969527,-96.990159) | |
10 | + @point = GeoKit::LatLng.new(@loc_a.lat, @loc_a.lng) | |
11 | + end | |
12 | + | |
13 | + def test_distance_between_same_using_defaults | |
14 | + assert_equal 0, GeoKit::LatLng.distance_between(@loc_a, @loc_a) | |
15 | + assert_equal 0, @loc_a.distance_to(@loc_a) | |
16 | + end | |
17 | + | |
18 | + def test_distance_between_same_with_miles_and_flat | |
19 | + assert_equal 0, GeoKit::LatLng.distance_between(@loc_a, @loc_a, :units => :miles, :formula => :flat) | |
20 | + assert_equal 0, @loc_a.distance_to(@loc_a, :units => :miles, :formula => :flat) | |
21 | + end | |
22 | + | |
23 | + def test_distance_between_same_with_kms_and_flat | |
24 | + assert_equal 0, GeoKit::LatLng.distance_between(@loc_a, @loc_a, :units => :kms, :formula => :flat) | |
25 | + assert_equal 0, @loc_a.distance_to(@loc_a, :units => :kms, :formula => :flat) | |
26 | + end | |
27 | + | |
28 | + def test_distance_between_same_with_miles_and_sphere | |
29 | + assert_equal 0, GeoKit::LatLng.distance_between(@loc_a, @loc_a, :units => :miles, :formula => :sphere) | |
30 | + assert_equal 0, @loc_a.distance_to(@loc_a, :units => :miles, :formula => :sphere) | |
31 | + end | |
32 | + | |
33 | + def test_distance_between_same_with_kms_and_sphere | |
34 | + assert_equal 0, GeoKit::LatLng.distance_between(@loc_a, @loc_a, :units => :kms, :formula => :sphere) | |
35 | + assert_equal 0, @loc_a.distance_to(@loc_a, :units => :kms, :formula => :sphere) | |
36 | + end | |
37 | + | |
38 | + def test_distance_between_diff_using_defaults | |
39 | + assert_in_delta 3.97, GeoKit::LatLng.distance_between(@loc_a, @loc_e), 0.01 | |
40 | + assert_in_delta 3.97, @loc_a.distance_to(@loc_e), 0.01 | |
41 | + end | |
42 | + | |
43 | + def test_distance_between_diff_with_miles_and_flat | |
44 | + assert_in_delta 3.97, GeoKit::LatLng.distance_between(@loc_a, @loc_e, :units => :miles, :formula => :flat), 0.2 | |
45 | + assert_in_delta 3.97, @loc_a.distance_to(@loc_e, :units => :miles, :formula => :flat), 0.2 | |
46 | + end | |
47 | + | |
48 | + def test_distance_between_diff_with_kms_and_flat | |
49 | + assert_in_delta 6.39, GeoKit::LatLng.distance_between(@loc_a, @loc_e, :units => :kms, :formula => :flat), 0.4 | |
50 | + assert_in_delta 6.39, @loc_a.distance_to(@loc_e, :units => :kms, :formula => :flat), 0.4 | |
51 | + end | |
52 | + | |
53 | + def test_distance_between_diff_with_miles_and_sphere | |
54 | + assert_in_delta 3.97, GeoKit::LatLng.distance_between(@loc_a, @loc_e, :units => :miles, :formula => :sphere), 0.01 | |
55 | + assert_in_delta 3.97, @loc_a.distance_to(@loc_e, :units => :miles, :formula => :sphere), 0.01 | |
56 | + end | |
57 | + | |
58 | + def test_distance_between_diff_with_kms_and_sphere | |
59 | + assert_in_delta 6.39, GeoKit::LatLng.distance_between(@loc_a, @loc_e, :units => :kms, :formula => :sphere), 0.01 | |
60 | + assert_in_delta 6.39, @loc_a.distance_to(@loc_e, :units => :kms, :formula => :sphere), 0.01 | |
61 | + end | |
62 | + | |
63 | + def test_manually_mixed_in | |
64 | + assert_equal 0, GeoKit::LatLng.distance_between(@point, @point) | |
65 | + assert_equal 0, @point.distance_to(@point) | |
66 | + assert_equal 0, @point.distance_to(@loc_a) | |
67 | + assert_in_delta 3.97, @point.distance_to(@loc_e, :units => :miles, :formula => :flat), 0.2 | |
68 | + assert_in_delta 6.39, @point.distance_to(@loc_e, :units => :kms, :formula => :flat), 0.4 | |
69 | + end | |
70 | + | |
71 | + def test_heading_between | |
72 | + assert_in_delta 332, GeoKit::LatLng.heading_between(@loc_a,@loc_e), 0.5 | |
73 | + end | |
74 | + | |
75 | + def test_heading_to | |
76 | + assert_in_delta 332, @loc_a.heading_to(@loc_e), 0.5 | |
77 | + end | |
78 | + | |
79 | + def test_class_endpoint | |
80 | + endpoint=GeoKit::LatLng.endpoint(@loc_a, 332, 3.97) | |
81 | + assert_in_delta @loc_e.lat, endpoint.lat, 0.0005 | |
82 | + assert_in_delta @loc_e.lng, endpoint.lng, 0.0005 | |
83 | + end | |
84 | + | |
85 | + def test_instance_endpoint | |
86 | + endpoint=@loc_a.endpoint(332, 3.97) | |
87 | + assert_in_delta @loc_e.lat, endpoint.lat, 0.0005 | |
88 | + assert_in_delta @loc_e.lng, endpoint.lng, 0.0005 | |
89 | + end | |
90 | + | |
91 | + def test_midpoint | |
92 | + midpoint=@loc_a.midpoint_to(@loc_e) | |
93 | + assert_in_delta 32.944061, midpoint.lat, 0.0005 | |
94 | + assert_in_delta -96.974296, midpoint.lng, 0.0005 | |
95 | + end | |
96 | + | |
97 | + def test_normalize | |
98 | + lat=37.7690 | |
99 | + lng=-122.443 | |
100 | + res=GeoKit::LatLng.normalize(lat,lng) | |
101 | + assert_equal res,GeoKit::LatLng.new(lat,lng) | |
102 | + res=GeoKit::LatLng.normalize("#{lat}, #{lng}") | |
103 | + assert_equal res,GeoKit::LatLng.new(lat,lng) | |
104 | + res=GeoKit::LatLng.normalize("#{lat} #{lng}") | |
105 | + assert_equal res,GeoKit::LatLng.new(lat,lng) | |
106 | + res=GeoKit::LatLng.normalize("#{lat.to_i} #{lng.to_i}") | |
107 | + assert_equal res,GeoKit::LatLng.new(lat.to_i,lng.to_i) | |
108 | + res=GeoKit::LatLng.normalize([lat,lng]) | |
109 | + assert_equal res,GeoKit::LatLng.new(lat,lng) | |
110 | + end | |
111 | + | |
112 | +end | |
0 | 113 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,44 @@ |
1 | +require File.join(File.dirname(__FILE__), 'base_geocoder_test') | |
2 | + | |
3 | +GeoKit::Geocoders::provider_order=[:google,:yahoo,:us] | |
4 | + | |
5 | +class MultiGeocoderTest < BaseGeocoderTest #:nodoc: all | |
6 | + | |
7 | + def setup | |
8 | + super | |
9 | + @failure = GeoKit::GeoLoc.new | |
10 | + end | |
11 | + | |
12 | + def test_successful_first | |
13 | + GeoKit::Geocoders::GoogleGeocoder.expects(:geocode).with(@address).returns(@success) | |
14 | + assert_equal @success, GeoKit::Geocoders::MultiGeocoder.geocode(@address) | |
15 | + end | |
16 | + | |
17 | + def test_failover | |
18 | + GeoKit::Geocoders::GoogleGeocoder.expects(:geocode).with(@address).returns(@failure) | |
19 | + GeoKit::Geocoders::YahooGeocoder.expects(:geocode).with(@address).returns(@success) | |
20 | + assert_equal @success, GeoKit::Geocoders::MultiGeocoder.geocode(@address) | |
21 | + end | |
22 | + | |
23 | + def test_double_failover | |
24 | + GeoKit::Geocoders::GoogleGeocoder.expects(:geocode).with(@address).returns(@failure) | |
25 | + GeoKit::Geocoders::YahooGeocoder.expects(:geocode).with(@address).returns(@failure) | |
26 | + GeoKit::Geocoders::UsGeocoder.expects(:geocode).with(@address).returns(@success) | |
27 | + assert_equal @success, GeoKit::Geocoders::MultiGeocoder.geocode(@address) | |
28 | + end | |
29 | + | |
30 | + def test_failure | |
31 | + GeoKit::Geocoders::GoogleGeocoder.expects(:geocode).with(@address).returns(@failure) | |
32 | + GeoKit::Geocoders::YahooGeocoder.expects(:geocode).with(@address).returns(@failure) | |
33 | + GeoKit::Geocoders::UsGeocoder.expects(:geocode).with(@address).returns(@failure) | |
34 | + assert_equal @failure, GeoKit::Geocoders::MultiGeocoder.geocode(@address) | |
35 | + end | |
36 | + | |
37 | + def test_invalid_provider | |
38 | + temp = GeoKit::Geocoders::provider_order | |
39 | + GeoKit::Geocoders.provider_order = [:bogus] | |
40 | + assert_equal @failure, GeoKit::Geocoders::MultiGeocoder.geocode(@address) | |
41 | + GeoKit::Geocoders.provider_order = temp | |
42 | + end | |
43 | + | |
44 | +end | |
0 | 45 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,31 @@ |
1 | +ActiveRecord::Schema.define(:version => 0) do | |
2 | + create_table :companies, :force => true do |t| | |
3 | + t.column :name, :string | |
4 | + end | |
5 | + | |
6 | + create_table :locations, :force => true do |t| | |
7 | + t.column :company_id, :integer, :default => 0, :null => false | |
8 | + t.column :street, :string, :limit => 60 | |
9 | + t.column :city, :string, :limit => 60 | |
10 | + t.column :state, :string, :limit => 2 | |
11 | + t.column :postal_code, :string, :limit => 16 | |
12 | + t.column :lat, :decimal, :precision => 15, :scale => 10 | |
13 | + t.column :lng, :decimal, :precision => 15, :scale => 10 | |
14 | + end | |
15 | + | |
16 | + create_table :custom_locations, :force => true do |t| | |
17 | + t.column :company_id, :integer, :default => 0, :null => false | |
18 | + t.column :street, :string, :limit => 60 | |
19 | + t.column :city, :string, :limit => 60 | |
20 | + t.column :state, :string, :limit => 2 | |
21 | + t.column :postal_code, :string, :limit => 16 | |
22 | + t.column :latitude, :decimal, :precision => 15, :scale => 10 | |
23 | + t.column :longitude, :decimal, :precision => 15, :scale => 10 | |
24 | + end | |
25 | + | |
26 | + create_table :stores, :force=> true do |t| | |
27 | + t.column :address, :string | |
28 | + t.column :lat, :decimal, :precision => 15, :scale => 10 | |
29 | + t.column :lng, :decimal, :precision => 15, :scale => 10 | |
30 | + end | |
31 | +end | |
0 | 32 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,18 @@ |
1 | +require 'test/unit' | |
2 | + | |
3 | +plugin_test_dir = File.dirname(__FILE__) | |
4 | + | |
5 | +# Load the Rails environment | |
6 | +require File.join(plugin_test_dir, '../../../../config/environment') | |
7 | +require 'active_record/fixtures' | |
8 | +databases = YAML::load(IO.read(plugin_test_dir + '/database.yml')) | |
9 | +ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log") | |
10 | + | |
11 | +# A specific database can be used by setting the DB environment variable | |
12 | +ActiveRecord::Base.establish_connection(databases[ENV['DB'] || 'mysql']) | |
13 | + | |
14 | +# Load the test schema into the database | |
15 | +load(File.join(plugin_test_dir, 'schema.rb')) | |
16 | + | |
17 | +# Load fixtures from the plugin | |
18 | +Test::Unit::TestCase.fixture_path = File.join(plugin_test_dir, 'fixtures/') | |
0 | 19 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,47 @@ |
1 | +require File.join(File.dirname(__FILE__), 'base_geocoder_test') | |
2 | + | |
3 | +GeoKit::Geocoders::geocoder_us = nil | |
4 | + | |
5 | +class UsGeocoderTest < BaseGeocoderTest #:nodoc: all | |
6 | + | |
7 | + GEOCODER_US_FULL='37.792528,-122.393981,100 Spear St,San Francisco,CA,94105' | |
8 | + | |
9 | + def setup | |
10 | + super | |
11 | + @us_full_hash = {:city=>"San Francisco", :state=>"CA"} | |
12 | + @us_full_loc = GeoKit::GeoLoc.new(@us_full_hash) | |
13 | + end | |
14 | + | |
15 | + def test_geocoder_us | |
16 | + response = MockSuccess.new | |
17 | + response.expects(:body).returns(GEOCODER_US_FULL) | |
18 | + url = "http://geocoder.us/service/csv/geocode?address=#{CGI.escape(@address)}" | |
19 | + GeoKit::Geocoders::UsGeocoder.expects(:call_geocoder_service).with(url).returns(response) | |
20 | + verify(GeoKit::Geocoders::UsGeocoder.geocode(@address)) | |
21 | + end | |
22 | + | |
23 | + def test_geocoder_with_geo_loc | |
24 | + response = MockSuccess.new | |
25 | + response.expects(:body).returns(GEOCODER_US_FULL) | |
26 | + url = "http://geocoder.us/service/csv/geocode?address=#{CGI.escape(@address)}" | |
27 | + GeoKit::Geocoders::UsGeocoder.expects(:call_geocoder_service).with(url).returns(response) | |
28 | + verify(GeoKit::Geocoders::UsGeocoder.geocode(@us_full_loc)) | |
29 | + end | |
30 | + | |
31 | + def test_service_unavailable | |
32 | + response = MockFailure.new | |
33 | + url = "http://geocoder.us/service/csv/geocode?address=#{CGI.escape(@address)}" | |
34 | + GeoKit::Geocoders::UsGeocoder.expects(:call_geocoder_service).with(url).returns(response) | |
35 | + assert !GeoKit::Geocoders::UsGeocoder.geocode(@us_full_loc).success | |
36 | + end | |
37 | + | |
38 | + private | |
39 | + | |
40 | + def verify(location) | |
41 | + assert_equal "CA", location.state | |
42 | + assert_equal "San Francisco", location.city | |
43 | + assert_equal "37.792528,-122.393981", location.ll | |
44 | + assert location.is_us? | |
45 | + assert_equal "100 Spear St, San Francisco, CA, 94105, US", location.full_address #slightly different from yahoo | |
46 | + end | |
47 | +end | |
0 | 48 | \ No newline at end of file | ... | ... |
... | ... | @@ -0,0 +1,87 @@ |
1 | +require File.join(File.dirname(__FILE__), 'base_geocoder_test') | |
2 | + | |
3 | +GeoKit::Geocoders::yahoo = 'Yahoo' | |
4 | + | |
5 | +class YahooGeocoderTest < BaseGeocoderTest #:nodoc: all | |
6 | + YAHOO_FULL=<<-EOF.strip | |
7 | + <?xml version="1.0"?> | |
8 | + <ResultSet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:yahoo:maps" xsi:schemaLocation="urn:yahoo:maps http://api.local.yahoo.com/MapsService/V1/GeocodeResponse.xsd"><Result precision="address"><Latitude>37.792406</Latitude><Longitude>-122.39411</Longitude><Address>100 SPEAR ST</Address><City>SAN FRANCISCO</City><State>CA</State><Zip>94105-1522</Zip><Country>US</Country></Result></ResultSet> | |
9 | + <!-- ws01.search.scd.yahoo.com uncompressed/chunked Mon Jan 29 16:23:43 PST 2007 --> | |
10 | + EOF | |
11 | + | |
12 | + YAHOO_CITY=<<-EOF.strip | |
13 | + <?xml version="1.0"?> | |
14 | + <ResultSet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:yahoo:maps" xsi:schemaLocation="urn:yahoo:maps http://api.local.yahoo.com/MapsService/V1/GeocodeResponse.xsd"><Result precision="city"><Latitude>37.7742</Latitude><Longitude>-122.417068</Longitude><Address></Address><City>SAN FRANCISCO</City><State>CA</State><Zip></Zip><Country>US</Country></Result></ResultSet> | |
15 | + <!-- ws02.search.scd.yahoo.com uncompressed/chunked Mon Jan 29 18:00:28 PST 2007 --> | |
16 | + EOF | |
17 | + | |
18 | + def setup | |
19 | + super | |
20 | + @yahoo_full_hash = {:street_address=>"100 Spear St", :city=>"San Francisco", :state=>"CA", :zip=>"94105-1522", :country_code=>"US"} | |
21 | + @yahoo_city_hash = {:city=>"San Francisco", :state=>"CA"} | |
22 | + @yahoo_full_loc = GeoKit::GeoLoc.new(@yahoo_full_hash) | |
23 | + @yahoo_city_loc = GeoKit::GeoLoc.new(@yahoo_city_hash) | |
24 | + end | |
25 | + | |
26 | + # the testing methods themselves | |
27 | + def test_yahoo_full_address | |
28 | + response = MockSuccess.new | |
29 | + response.expects(:body).returns(YAHOO_FULL) | |
30 | + url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{CGI.escape(@address)}" | |
31 | + GeoKit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) | |
32 | + do_full_address_assertions(GeoKit::Geocoders::YahooGeocoder.geocode(@address)) | |
33 | + end | |
34 | + | |
35 | + def test_yahoo_full_address_with_geo_loc | |
36 | + response = MockSuccess.new | |
37 | + response.expects(:body).returns(YAHOO_FULL) | |
38 | + url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{CGI.escape(@full_address)}" | |
39 | + GeoKit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) | |
40 | + do_full_address_assertions(GeoKit::Geocoders::YahooGeocoder.geocode(@yahoo_full_loc)) | |
41 | + end | |
42 | + | |
43 | + def test_yahoo_city | |
44 | + response = MockSuccess.new | |
45 | + response.expects(:body).returns(YAHOO_CITY) | |
46 | + url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{CGI.escape(@address)}" | |
47 | + GeoKit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) | |
48 | + do_city_assertions(GeoKit::Geocoders::YahooGeocoder.geocode(@address)) | |
49 | + end | |
50 | + | |
51 | + def test_yahoo_city_with_geo_loc | |
52 | + response = MockSuccess.new | |
53 | + response.expects(:body).returns(YAHOO_CITY) | |
54 | + url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{CGI.escape(@address)}" | |
55 | + GeoKit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) | |
56 | + do_city_assertions(GeoKit::Geocoders::YahooGeocoder.geocode(@yahoo_city_loc)) | |
57 | + end | |
58 | + | |
59 | + def test_service_unavailable | |
60 | + response = MockFailure.new | |
61 | + url = "http://api.local.yahoo.com/MapsService/V1/geocode?appid=Yahoo&location=#{CGI.escape(@address)}" | |
62 | + GeoKit::Geocoders::YahooGeocoder.expects(:call_geocoder_service).with(url).returns(response) | |
63 | + assert !GeoKit::Geocoders::YahooGeocoder.geocode(@yahoo_city_loc).success | |
64 | + end | |
65 | + | |
66 | + private | |
67 | + | |
68 | + # next two methods do the assertions for both address-level and city-level lookups | |
69 | + def do_full_address_assertions(res) | |
70 | + assert_equal "CA", res.state | |
71 | + assert_equal "San Francisco", res.city | |
72 | + assert_equal "37.792406,-122.39411", res.ll | |
73 | + assert res.is_us? | |
74 | + assert_equal "100 Spear St, San Francisco, CA, 94105-1522, US", res.full_address | |
75 | + assert_equal "yahoo", res.provider | |
76 | + end | |
77 | + | |
78 | + def do_city_assertions(res) | |
79 | + assert_equal "CA", res.state | |
80 | + assert_equal "San Francisco", res.city | |
81 | + assert_equal "37.7742,-122.417068", res.ll | |
82 | + assert res.is_us? | |
83 | + assert_equal "San Francisco, CA, US", res.full_address | |
84 | + assert_nil res.street_address | |
85 | + assert_equal "yahoo", res.provider | |
86 | + end | |
87 | +end | |
0 | 88 | \ No newline at end of file | ... | ... |