rails-geocoder 0.9.6 → 0.9.7
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +6 -1
- data/README.rdoc +43 -5
- data/lib/geocoder.rb +31 -339
- data/lib/geocoder/active_record.rb +235 -0
- data/lib/geocoder/calculations.rb +94 -0
- data/lib/geocoder/lookup.rb +78 -0
- data/test/geocoder_test.rb +10 -3
- data/test/test_helper.rb +26 -3
- metadata +7 -4
data/CHANGELOG.rdoc
CHANGED
@@ -2,10 +2,15 @@
|
|
2
2
|
|
3
3
|
Per-release changes to Geocoder.
|
4
4
|
|
5
|
+
== 0.9.7 (2011 Feb 1)
|
6
|
+
|
7
|
+
* Add reverse geocoding (+reverse_geocoded_by+).
|
8
|
+
* Prevent exception (uninitialized constant Geocoder::Net) when net/http not already required (sleepycat).
|
9
|
+
|
5
10
|
== 0.9.6 (2011 Jan 19)
|
6
11
|
|
7
12
|
* Fix incompatibility with will_paginate gem.
|
8
|
-
* Include table names in GROUP BY clause of nearby scope to avoid ambiguity in joins.
|
13
|
+
* Include table names in GROUP BY clause of nearby scope to avoid ambiguity in joins (matchu).
|
9
14
|
|
10
15
|
== 0.9.5 (2010 Oct 15)
|
11
16
|
|
data/README.rdoc
CHANGED
@@ -68,13 +68,13 @@ To find objects by location, use the following scopes:
|
|
68
68
|
Some utility methods are also available:
|
69
69
|
|
70
70
|
# distance (in miles) between Eiffel Tower and Empire State Building
|
71
|
-
Geocoder.distance_between( 48.858205,2.294359, 40.748433,-73.985655 )
|
71
|
+
Geocoder::Calculations.distance_between( 48.858205,2.294359, 40.748433,-73.985655 )
|
72
72
|
|
73
73
|
# look up coordinates of some location (like searching Google Maps)
|
74
74
|
Geocoder.fetch_coordinates("25 Main St, Cooperstown, NY")
|
75
75
|
|
76
76
|
# find the geographic center (aka center of gravity) of objects or points
|
77
|
-
Geocoder.geographic_center([ city1, city2, city3, [40.22,-73.99], city4 ])
|
77
|
+
Geocoder::Calculations.geographic_center([ city1, city2, city3, [40.22,-73.99], city4 ])
|
78
78
|
|
79
79
|
|
80
80
|
== More On Configuration
|
@@ -101,6 +101,45 @@ If your model has +address+, +city+, +state+, and +country+ attributes you might
|
|
101
101
|
Please see the code for more methods and detailed information about arguments (eg, working with kilometers).
|
102
102
|
|
103
103
|
|
104
|
+
== Reverse Geocoding
|
105
|
+
|
106
|
+
If you need reverse geocoding (lat/long coordinates to address), do something like the following in your model:
|
107
|
+
|
108
|
+
reverse_geocoded_by :latitude, :longitude
|
109
|
+
after_validation :fetch_address
|
110
|
+
|
111
|
+
and make sure it has +latitude+ and +longitude+ attributes, as well as an +address+ attribute. As with regular geocoding, you can specify alternate names for all of these attributes, for example:
|
112
|
+
|
113
|
+
reverse_geocoded_by :lat, :lon, :address => :location
|
114
|
+
|
115
|
+
|
116
|
+
== Forward and Reverse Geocoding in the Same Model
|
117
|
+
|
118
|
+
If you apply both forward and reverse geocoding functionality to the same model, you can provide different methods for storing the fetched address (reverse geocoding) and providing an address to use when fetching coordinates (forward geocoding), for example:
|
119
|
+
|
120
|
+
class Venue
|
121
|
+
|
122
|
+
# build an address from street, city, and state attributes
|
123
|
+
geocoded_by :address_from_components
|
124
|
+
|
125
|
+
# store the Google-provided address in the full_address attribute
|
126
|
+
reverse_geocoded_by :latitude, :longitude, :address => :full_address
|
127
|
+
end
|
128
|
+
|
129
|
+
However, there can be only one set of latitude/longitude attributes, and whichever you specify last will be used. For example:
|
130
|
+
|
131
|
+
class Venue
|
132
|
+
|
133
|
+
geocoded_by :address,
|
134
|
+
:latitude => :fetched_latitude, # this will be overridden by the below
|
135
|
+
:longitude => :fetched_longitude # same here
|
136
|
+
|
137
|
+
reverse_geocoded_by :latitude, :longitude
|
138
|
+
end
|
139
|
+
|
140
|
+
The reason for this is that we don't want ambiguity when doing distance calculations. We need a single, authoritative source for coordinates!
|
141
|
+
|
142
|
+
|
104
143
|
== SQLite
|
105
144
|
|
106
145
|
SQLite's lack of trigonometric functions requires an alternate implementation of the +near+ method (scope). When using SQLite, Geocoder will automatically use a less accurate algorithm for finding objects near a given point. Results of this algorithm should not be trusted too much as it will return objects that are outside the given radius.
|
@@ -114,9 +153,9 @@ There are few options for finding objects near a given point in SQLite without i
|
|
114
153
|
|
115
154
|
1. Use a square instead of a circle for finding nearby points. For example, if you want to find points near 40.71, 100.23, search for objects with latitude between 39.71 and 41.71 and longitude between 99.23 and 101.23. One degree of latitude or longitude is at most 69 miles so divide your radius (in miles) by 69.0 to get the amount to add and subtract from your center coordinates to get the upper and lower bounds. The results will not be very accurate (you'll get points outside the desired radius--at worst 29% farther away), but you will get all the points within the required radius.
|
116
155
|
|
117
|
-
2. Load all objects into memory and compute distances between them using the <tt>Geocoder.distance_between</tt> method. This will produce accurate results but will be very slow (and use a lot of memory) if you have a lot of objects in your database.
|
156
|
+
2. Load all objects into memory and compute distances between them using the <tt>Geocoder::Calculations.distance_between</tt> method. This will produce accurate results but will be very slow (and use a lot of memory) if you have a lot of objects in your database.
|
118
157
|
|
119
|
-
3. If you have a large number of objects (so you can't use approach #2) and you need accurate results (better than approach #1 will give), you can use a combination of the two. Get all the objects within a square around your center point, and then eliminate the ones that are too far away using <tt>Geocoder.distance_between</tt>.
|
158
|
+
3. If you have a large number of objects (so you can't use approach #2) and you need accurate results (better than approach #1 will give), you can use a combination of the two. Get all the objects within a square around your center point, and then eliminate the ones that are too far away using <tt>Geocoder::Calculations.distance_between</tt>.
|
120
159
|
|
121
160
|
Because Geocoder needs to provide this functionality as a scope, we must go with option #1, but feel free to implement #2 or #3 if you need more accuracy.
|
122
161
|
|
@@ -136,7 +175,6 @@ If anyone has a more elegant solution to this problem I am very interested in se
|
|
136
175
|
* use completely separate "drivers" for different AR adapters?
|
137
176
|
* seems reasonable since we're using very DB-specific features
|
138
177
|
* also need to make sure 'mysql2' is supported
|
139
|
-
* add reverse geocoding
|
140
178
|
* make 'near' scope work with AR associations
|
141
179
|
* http://stackoverflow.com/questions/3266358/geocoder-rails-plugin-near-search-problem-with-activerecord
|
142
180
|
* prepend table names to column names in SQL distance expression (required
|
data/lib/geocoder.rb
CHANGED
@@ -1,361 +1,53 @@
|
|
1
|
+
require "geocoder/calculations"
|
2
|
+
require "geocoder/lookup"
|
3
|
+
require "geocoder/active_record"
|
4
|
+
|
1
5
|
##
|
2
|
-
# Add
|
6
|
+
# Add geocoded_by method to ActiveRecord::Base so Geocoder is accessible.
|
3
7
|
#
|
4
|
-
|
5
|
-
|
6
|
-
##
|
7
|
-
# Implementation of 'included' hook method.
|
8
|
-
#
|
9
|
-
def self.included(base)
|
10
|
-
base.extend ClassMethods
|
11
|
-
base.class_eval do
|
12
|
-
|
13
|
-
# scope: geocoded objects
|
14
|
-
scope :geocoded,
|
15
|
-
:conditions => "#{geocoder_options[:latitude]} IS NOT NULL " +
|
16
|
-
"AND #{geocoder_options[:longitude]} IS NOT NULL"
|
17
|
-
|
18
|
-
# scope: not-geocoded objects
|
19
|
-
scope :not_geocoded,
|
20
|
-
:conditions => "#{geocoder_options[:latitude]} IS NULL " +
|
21
|
-
"OR #{geocoder_options[:longitude]} IS NULL"
|
22
|
-
|
23
|
-
##
|
24
|
-
# Find all objects within a radius (in miles) of the given location
|
25
|
-
# (address string). Location (the first argument) may be either a string
|
26
|
-
# to geocode or an array of coordinates (<tt>[lat,long]</tt>).
|
27
|
-
#
|
28
|
-
scope :near, lambda{ |location, *args|
|
29
|
-
latitude, longitude = location.is_a?(Array) ?
|
30
|
-
location : Geocoder.fetch_coordinates(location)
|
31
|
-
if latitude and longitude
|
32
|
-
near_scope_options(latitude, longitude, *args)
|
33
|
-
else
|
34
|
-
{}
|
35
|
-
end
|
36
|
-
}
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
##
|
41
|
-
# Methods which will be class methods of the including class.
|
42
|
-
#
|
43
|
-
module ClassMethods
|
44
|
-
|
45
|
-
##
|
46
|
-
# Get options hash suitable for passing to ActiveRecord.find to get
|
47
|
-
# records within a radius (in miles) of the given point.
|
48
|
-
# Options hash may include:
|
49
|
-
#
|
50
|
-
# +units+ :: <tt>:mi</tt> (default) or <tt>:km</tt>
|
51
|
-
# +exclude+ :: an object to exclude (used by the #nearbys method)
|
52
|
-
# +order+ :: column(s) for ORDER BY SQL clause
|
53
|
-
# +limit+ :: number of records to return (for LIMIT SQL clause)
|
54
|
-
# +offset+ :: number of records to skip (for OFFSET SQL clause)
|
55
|
-
# +select+ :: string with the SELECT SQL fragment (e.g. “id, name”)
|
56
|
-
#
|
57
|
-
def near_scope_options(latitude, longitude, radius = 20, options = {})
|
58
|
-
radius *= km_in_mi if options[:units] == :km
|
59
|
-
if ActiveRecord::Base.connection.adapter_name == "SQLite"
|
60
|
-
approx_near_scope_options(latitude, longitude, radius, options)
|
61
|
-
else
|
62
|
-
full_near_scope_options(latitude, longitude, radius, options)
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
|
67
|
-
private # ----------------------------------------------------------------
|
68
|
-
|
69
|
-
##
|
70
|
-
# Scope options hash for use with a database that supports POWER(),
|
71
|
-
# SQRT(), PI(), and trigonometric functions (SIN(), COS(), and ASIN()).
|
72
|
-
#
|
73
|
-
# Taken from the excellent tutorial at:
|
74
|
-
# http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
|
75
|
-
#
|
76
|
-
def full_near_scope_options(latitude, longitude, radius, options)
|
77
|
-
lat_attr = geocoder_options[:latitude]
|
78
|
-
lon_attr = geocoder_options[:longitude]
|
79
|
-
distance = "3956 * 2 * ASIN(SQRT(" +
|
80
|
-
"POWER(SIN((#{latitude} - #{lat_attr}) * " +
|
81
|
-
"PI() / 180 / 2), 2) + COS(#{latitude} * PI()/180) * " +
|
82
|
-
"COS(#{lat_attr} * PI() / 180) * " +
|
83
|
-
"POWER(SIN((#{longitude} - #{lon_attr}) * " +
|
84
|
-
"PI() / 180 / 2), 2) ))"
|
85
|
-
options[:order] ||= "#{distance} ASC"
|
86
|
-
default_near_scope_options(latitude, longitude, radius, options).merge(
|
87
|
-
:select => "#{options[:select] || '*'}, #{distance} AS distance",
|
88
|
-
:having => "#{distance} <= #{radius}"
|
89
|
-
)
|
90
|
-
end
|
91
|
-
|
92
|
-
##
|
93
|
-
# Scope options hash for use with a database without trigonometric
|
94
|
-
# functions, like SQLite. Approach is to find objects within a square
|
95
|
-
# rather than a circle, so results are very approximate (will include
|
96
|
-
# objects outside the given radius).
|
97
|
-
#
|
98
|
-
def approx_near_scope_options(latitude, longitude, radius, options)
|
99
|
-
default_near_scope_options(latitude, longitude, radius, options).merge(
|
100
|
-
:select => options[:select] || nil
|
101
|
-
)
|
102
|
-
end
|
103
|
-
|
104
|
-
##
|
105
|
-
# Options used for any near-like scope.
|
106
|
-
#
|
107
|
-
def default_near_scope_options(latitude, longitude, radius, options)
|
108
|
-
lat_attr = geocoder_options[:latitude]
|
109
|
-
lon_attr = geocoder_options[:longitude]
|
110
|
-
conditions = \
|
111
|
-
["#{lat_attr} BETWEEN ? AND ? AND #{lon_attr} BETWEEN ? AND ?"] +
|
112
|
-
coordinate_bounds(latitude, longitude, radius)
|
113
|
-
if obj = options[:exclude]
|
114
|
-
conditions[0] << " AND id != ?"
|
115
|
-
conditions << obj.id
|
116
|
-
end
|
117
|
-
{
|
118
|
-
:group => columns.map{ |c| "#{table_name}.#{c.name}" }.join(','),
|
119
|
-
:order => options[:order],
|
120
|
-
:limit => options[:limit],
|
121
|
-
:offset => options[:offset],
|
122
|
-
:conditions => conditions
|
123
|
-
}
|
124
|
-
end
|
125
|
-
|
126
|
-
##
|
127
|
-
# Get the rough high/low lat/long bounds for a geographic point and
|
128
|
-
# radius. Returns an array: <tt>[lat_lo, lat_hi, lon_lo, lon_hi]</tt>.
|
129
|
-
# Used to constrain search to a (radius x radius) square.
|
130
|
-
#
|
131
|
-
def coordinate_bounds(latitude, longitude, radius)
|
132
|
-
radius = radius.to_f
|
133
|
-
factor = (Math::cos(latitude * Math::PI / 180.0) * 69.0).abs
|
134
|
-
[
|
135
|
-
latitude - (radius / 69.0),
|
136
|
-
latitude + (radius / 69.0),
|
137
|
-
longitude - (radius / factor),
|
138
|
-
longitude + (radius / factor)
|
139
|
-
]
|
140
|
-
end
|
141
|
-
|
142
|
-
##
|
143
|
-
# Conversion factor: km to mi.
|
144
|
-
#
|
145
|
-
def km_in_mi
|
146
|
-
0.621371192
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
##
|
151
|
-
# Read the coordinates [lat,lon] of an object. This is not great but it
|
152
|
-
# seems cleaner than polluting the instance method namespace.
|
153
|
-
#
|
154
|
-
def read_coordinates
|
155
|
-
[:latitude, :longitude].map{ |i| send self.class.geocoder_options[i] }
|
156
|
-
end
|
157
|
-
|
158
|
-
##
|
159
|
-
# Is this object geocoded? (Does it have latitude and longitude?)
|
160
|
-
#
|
161
|
-
def geocoded?
|
162
|
-
read_coordinates.compact.size > 0
|
163
|
-
end
|
164
|
-
|
165
|
-
##
|
166
|
-
# Calculate the distance from the object to a point (lat,lon).
|
167
|
-
# Valid units are defined in <tt>distance_between</tt> class method.
|
168
|
-
#
|
169
|
-
def distance_to(lat, lon, units = :mi)
|
170
|
-
return nil unless geocoded?
|
171
|
-
mylat,mylon = read_coordinates
|
172
|
-
Geocoder.distance_between(mylat, mylon, lat, lon, :units => units)
|
173
|
-
end
|
174
|
-
|
175
|
-
##
|
176
|
-
# Get other geocoded objects within a given radius.
|
177
|
-
# Valid units are defined in <tt>distance_between</tt> class method.
|
178
|
-
#
|
179
|
-
def nearbys(radius = 20, units = :mi)
|
180
|
-
return [] unless geocoded?
|
181
|
-
options = {:exclude => self, :units => units}
|
182
|
-
self.class.near(read_coordinates, radius, options)
|
183
|
-
end
|
8
|
+
ActiveRecord::Base.class_eval do
|
184
9
|
|
185
10
|
##
|
186
|
-
#
|
187
|
-
# coordinates as an array: <tt>[lat, lon]</tt>.
|
11
|
+
# Set attribute names and include the Geocoder module.
|
188
12
|
#
|
189
|
-
def
|
190
|
-
|
191
|
-
|
13
|
+
def self.geocoded_by(address_attr, options = {})
|
14
|
+
_geocoder_init(
|
15
|
+
:user_address => address_attr,
|
16
|
+
:latitude => options[:latitude] || :latitude,
|
17
|
+
:longitude => options[:longitude] || :longitude
|
192
18
|
)
|
193
|
-
unless coords.blank?
|
194
|
-
method = (save ? "update" : "write") + "_attribute"
|
195
|
-
send method, self.class.geocoder_options[:latitude], coords[0]
|
196
|
-
send method, self.class.geocoder_options[:longitude], coords[1]
|
197
|
-
end
|
198
|
-
coords
|
199
|
-
end
|
200
|
-
|
201
|
-
##
|
202
|
-
# Fetch coordinates and update (save) +latitude+ and +longitude+ data.
|
203
|
-
#
|
204
|
-
def fetch_coordinates!
|
205
|
-
fetch_coordinates(true)
|
206
19
|
end
|
207
20
|
|
208
21
|
##
|
209
|
-
#
|
210
|
-
# Takes two sets of coordinates and an options hash:
|
211
|
-
#
|
212
|
-
# <tt>:units</tt> :: <tt>:mi</tt> (default) or <tt>:km</tt>
|
22
|
+
# Set attribute names and include the Geocoder module.
|
213
23
|
#
|
214
|
-
def self.
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
conversions = { :mi => 3956, :km => 6371 }
|
221
|
-
|
222
|
-
# convert degrees to radians
|
223
|
-
lat1 = to_radians(lat1)
|
224
|
-
lon1 = to_radians(lon1)
|
225
|
-
lat2 = to_radians(lat2)
|
226
|
-
lon2 = to_radians(lon2)
|
227
|
-
|
228
|
-
# compute distances
|
229
|
-
dlat = (lat1 - lat2).abs
|
230
|
-
dlon = (lon1 - lon2).abs
|
231
|
-
|
232
|
-
a = (Math.sin(dlat / 2))**2 + Math.cos(lat1) *
|
233
|
-
(Math.sin(dlon / 2))**2 * Math.cos(lat2)
|
234
|
-
c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a))
|
235
|
-
c * conversions[options[:units]]
|
24
|
+
def self.reverse_geocoded_by(latitude_attr, longitude_attr, options = {})
|
25
|
+
_geocoder_init(
|
26
|
+
:fetched_address => options[:address] || :address,
|
27
|
+
:latitude => latitude_attr,
|
28
|
+
:longitude => longitude_attr
|
29
|
+
)
|
236
30
|
end
|
237
31
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
# the procedure documented at http://www.geomidpoint.com/calculation.html.
|
243
|
-
#
|
244
|
-
def self.geographic_center(points)
|
245
|
-
|
246
|
-
# convert objects to [lat,lon] arrays and remove nils
|
247
|
-
points = points.map{ |p|
|
248
|
-
p.is_a?(Array) ? p : (p.geocoded?? p.read_coordinates : nil)
|
249
|
-
}.compact
|
250
|
-
|
251
|
-
# convert degrees to radians
|
252
|
-
points.map!{ |p| [to_radians(p[0]), to_radians(p[1])] }
|
253
|
-
|
254
|
-
# convert to Cartesian coordinates
|
255
|
-
x = []; y = []; z = []
|
256
|
-
points.each do |p|
|
257
|
-
x << Math.cos(p[0]) * Math.cos(p[1])
|
258
|
-
y << Math.cos(p[0]) * Math.sin(p[1])
|
259
|
-
z << Math.sin(p[0])
|
32
|
+
def self._geocoder_init(options)
|
33
|
+
unless _geocoder_initialized?
|
34
|
+
class_inheritable_reader :geocoder_options
|
35
|
+
class_inheritable_hash_writer :geocoder_options
|
260
36
|
end
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
c.inject(0){ |tot,i| tot += i } / c.size.to_f
|
37
|
+
self.geocoder_options = options
|
38
|
+
unless _geocoder_initialized?
|
39
|
+
include Geocoder::ActiveRecord
|
265
40
|
end
|
266
|
-
|
267
|
-
# convert back to latitude/longitude
|
268
|
-
lon = Math.atan2(ya, xa)
|
269
|
-
hyp = Math.sqrt(xa**2 + ya**2)
|
270
|
-
lat = Math.atan2(za, hyp)
|
271
|
-
|
272
|
-
# return answer in degrees
|
273
|
-
[to_degrees(lat), to_degrees(lon)]
|
274
|
-
end
|
275
|
-
|
276
|
-
##
|
277
|
-
# Convert degrees to radians.
|
278
|
-
#
|
279
|
-
def self.to_radians(degrees)
|
280
|
-
degrees * (Math::PI / 180)
|
281
|
-
end
|
282
|
-
|
283
|
-
##
|
284
|
-
# Convert radians to degrees.
|
285
|
-
#
|
286
|
-
def self.to_degrees(radians)
|
287
|
-
(radians * 180.0) / Math::PI
|
288
|
-
end
|
289
|
-
|
290
|
-
##
|
291
|
-
# Query Google for geographic information about the given phrase.
|
292
|
-
# Returns a hash representing a valid geocoder response.
|
293
|
-
# Returns nil if non-200 HTTP response, timeout, or other error.
|
294
|
-
#
|
295
|
-
def self.search(query)
|
296
|
-
doc = _fetch_parsed_response(query)
|
297
|
-
doc && doc['status'] == "OK" ? doc : nil
|
298
41
|
end
|
299
42
|
|
300
|
-
|
301
|
-
|
302
|
-
# Returns array [lat,lon] if found, nil if not found or if network error.
|
303
|
-
#
|
304
|
-
def self.fetch_coordinates(query)
|
305
|
-
return nil unless doc = self.search(query)
|
306
|
-
# blindly use the first results (assume they are most accurate)
|
307
|
-
place = doc['results'].first['geometry']['location']
|
308
|
-
['lat', 'lng'].map{ |i| place[i] }
|
43
|
+
def self._geocoder_initialized?
|
44
|
+
included_modules.include? Geocoder::ActiveRecord
|
309
45
|
end
|
46
|
+
end
|
310
47
|
|
311
|
-
##
|
312
|
-
# Returns a parsed Google geocoder search result (hash).
|
313
|
-
# This method is not intended for general use (prefer Geocoder.search).
|
314
|
-
#
|
315
|
-
def self._fetch_parsed_response(query)
|
316
|
-
if doc = _fetch_raw_response(query)
|
317
|
-
ActiveSupport::JSON.decode(doc)
|
318
|
-
end
|
319
|
-
end
|
320
|
-
|
321
|
-
##
|
322
|
-
# Returns a raw Google geocoder search result (JSON).
|
323
|
-
# This method is not intended for general use (prefer Geocoder.search).
|
324
|
-
#
|
325
|
-
def self._fetch_raw_response(query)
|
326
|
-
return nil if query.blank?
|
327
|
-
|
328
|
-
# build URL
|
329
|
-
params = { :address => query, :sensor => "false" }
|
330
|
-
url = "http://maps.google.com/maps/api/geocode/json?" + params.to_query
|
331
48
|
|
332
|
-
|
333
|
-
begin
|
334
|
-
resp = nil
|
335
|
-
timeout(3) do
|
336
|
-
Net::HTTP.get_response(URI.parse(url)).body
|
337
|
-
end
|
338
|
-
rescue SocketError, TimeoutError
|
339
|
-
return nil
|
340
|
-
end
|
341
|
-
end
|
49
|
+
class GeocoderError < StandardError
|
342
50
|
end
|
343
51
|
|
344
|
-
|
345
|
-
# Add geocoded_by method to ActiveRecord::Base so Geocoder is accessible.
|
346
|
-
#
|
347
|
-
ActiveRecord::Base.class_eval do
|
348
|
-
|
349
|
-
##
|
350
|
-
# Set attribute names and include the Geocoder module.
|
351
|
-
#
|
352
|
-
def self.geocoded_by(method_name = :location, options = {})
|
353
|
-
class_inheritable_reader :geocoder_options
|
354
|
-
write_inheritable_attribute :geocoder_options, {
|
355
|
-
:method_name => method_name,
|
356
|
-
:latitude => options[:latitude] || :latitude,
|
357
|
-
:longitude => options[:longitude] || :longitude
|
358
|
-
}
|
359
|
-
include Geocoder
|
360
|
-
end
|
52
|
+
class GeocoderConfigurationError < GeocoderError
|
361
53
|
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
##
|
2
|
+
# Add geocoding functionality to any ActiveRecord object.
|
3
|
+
#
|
4
|
+
module Geocoder
|
5
|
+
module ActiveRecord
|
6
|
+
|
7
|
+
##
|
8
|
+
# Implementation of 'included' hook method.
|
9
|
+
#
|
10
|
+
def self.included(base)
|
11
|
+
base.extend ClassMethods
|
12
|
+
base.class_eval do
|
13
|
+
|
14
|
+
# scope: geocoded objects
|
15
|
+
scope :geocoded,
|
16
|
+
:conditions => "#{geocoder_options[:latitude]} IS NOT NULL " +
|
17
|
+
"AND #{geocoder_options[:longitude]} IS NOT NULL"
|
18
|
+
|
19
|
+
# scope: not-geocoded objects
|
20
|
+
scope :not_geocoded,
|
21
|
+
:conditions => "#{geocoder_options[:latitude]} IS NULL " +
|
22
|
+
"OR #{geocoder_options[:longitude]} IS NULL"
|
23
|
+
|
24
|
+
##
|
25
|
+
# Find all objects within a radius (in miles) of the given location
|
26
|
+
# (address string). Location (the first argument) may be either a string
|
27
|
+
# to geocode or an array of coordinates (<tt>[lat,long]</tt>).
|
28
|
+
#
|
29
|
+
scope :near, lambda{ |location, *args|
|
30
|
+
latitude, longitude = location.is_a?(Array) ?
|
31
|
+
location : Geocoder::Lookup.coordinates(location)
|
32
|
+
if latitude and longitude
|
33
|
+
near_scope_options(latitude, longitude, *args)
|
34
|
+
else
|
35
|
+
{}
|
36
|
+
end
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Methods which will be class methods of the including class.
|
43
|
+
#
|
44
|
+
module ClassMethods
|
45
|
+
|
46
|
+
##
|
47
|
+
# Get options hash suitable for passing to ActiveRecord.find to get
|
48
|
+
# records within a radius (in miles) of the given point.
|
49
|
+
# Options hash may include:
|
50
|
+
#
|
51
|
+
# +units+ :: <tt>:mi</tt> (default) or <tt>:km</tt>
|
52
|
+
# +exclude+ :: an object to exclude (used by the #nearbys method)
|
53
|
+
# +order+ :: column(s) for ORDER BY SQL clause
|
54
|
+
# +limit+ :: number of records to return (for LIMIT SQL clause)
|
55
|
+
# +offset+ :: number of records to skip (for OFFSET SQL clause)
|
56
|
+
# +select+ :: string with the SELECT SQL fragment (e.g. “id, name”)
|
57
|
+
#
|
58
|
+
def near_scope_options(latitude, longitude, radius = 20, options = {})
|
59
|
+
radius *= Geocoder::Calculations.km_in_mi if options[:units] == :km
|
60
|
+
if ::ActiveRecord::Base.connection.adapter_name == "SQLite"
|
61
|
+
approx_near_scope_options(latitude, longitude, radius, options)
|
62
|
+
else
|
63
|
+
full_near_scope_options(latitude, longitude, radius, options)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
private # ----------------------------------------------------------------
|
69
|
+
|
70
|
+
##
|
71
|
+
# Scope options hash for use with a database that supports POWER(),
|
72
|
+
# SQRT(), PI(), and trigonometric functions (SIN(), COS(), and ASIN()).
|
73
|
+
#
|
74
|
+
# Taken from the excellent tutorial at:
|
75
|
+
# http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
|
76
|
+
#
|
77
|
+
def full_near_scope_options(latitude, longitude, radius, options)
|
78
|
+
lat_attr = geocoder_options[:latitude]
|
79
|
+
lon_attr = geocoder_options[:longitude]
|
80
|
+
distance = "3956 * 2 * ASIN(SQRT(" +
|
81
|
+
"POWER(SIN((#{latitude} - #{lat_attr}) * " +
|
82
|
+
"PI() / 180 / 2), 2) + COS(#{latitude} * PI()/180) * " +
|
83
|
+
"COS(#{lat_attr} * PI() / 180) * " +
|
84
|
+
"POWER(SIN((#{longitude} - #{lon_attr}) * " +
|
85
|
+
"PI() / 180 / 2), 2) ))"
|
86
|
+
options[:order] ||= "#{distance} ASC"
|
87
|
+
default_near_scope_options(latitude, longitude, radius, options).merge(
|
88
|
+
:select => "#{options[:select] || '*'}, #{distance} AS distance",
|
89
|
+
:having => "#{distance} <= #{radius}"
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Scope options hash for use with a database without trigonometric
|
95
|
+
# functions, like SQLite. Approach is to find objects within a square
|
96
|
+
# rather than a circle, so results are very approximate (will include
|
97
|
+
# objects outside the given radius).
|
98
|
+
#
|
99
|
+
def approx_near_scope_options(latitude, longitude, radius, options)
|
100
|
+
default_near_scope_options(latitude, longitude, radius, options).merge(
|
101
|
+
:select => options[:select] || nil
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Options used for any near-like scope.
|
107
|
+
#
|
108
|
+
def default_near_scope_options(latitude, longitude, radius, options)
|
109
|
+
lat_attr = geocoder_options[:latitude]
|
110
|
+
lon_attr = geocoder_options[:longitude]
|
111
|
+
conditions = \
|
112
|
+
["#{lat_attr} BETWEEN ? AND ? AND #{lon_attr} BETWEEN ? AND ?"] +
|
113
|
+
coordinate_bounds(latitude, longitude, radius)
|
114
|
+
if obj = options[:exclude]
|
115
|
+
conditions[0] << " AND id != ?"
|
116
|
+
conditions << obj.id
|
117
|
+
end
|
118
|
+
{
|
119
|
+
:group => columns.map{ |c| "#{table_name}.#{c.name}" }.join(','),
|
120
|
+
:order => options[:order],
|
121
|
+
:limit => options[:limit],
|
122
|
+
:offset => options[:offset],
|
123
|
+
:conditions => conditions
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
##
|
128
|
+
# Get the rough high/low lat/long bounds for a geographic point and
|
129
|
+
# radius. Returns an array: <tt>[lat_lo, lat_hi, lon_lo, lon_hi]</tt>.
|
130
|
+
# Used to constrain search to a (radius x radius) square.
|
131
|
+
#
|
132
|
+
def coordinate_bounds(latitude, longitude, radius)
|
133
|
+
radius = radius.to_f
|
134
|
+
factor = (Math::cos(latitude * Math::PI / 180.0) * 69.0).abs
|
135
|
+
[
|
136
|
+
latitude - (radius / 69.0),
|
137
|
+
latitude + (radius / 69.0),
|
138
|
+
longitude - (radius / factor),
|
139
|
+
longitude + (radius / factor)
|
140
|
+
]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Read the coordinates [lat,lon] of an object. This is not great but it
|
146
|
+
# seems cleaner than polluting the instance method namespace.
|
147
|
+
#
|
148
|
+
def read_coordinates
|
149
|
+
[:latitude, :longitude].map{ |i| send self.class.geocoder_options[i] }
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
153
|
+
# Is this object geocoded? (Does it have latitude and longitude?)
|
154
|
+
#
|
155
|
+
def geocoded?
|
156
|
+
read_coordinates.compact.size > 0
|
157
|
+
end
|
158
|
+
|
159
|
+
##
|
160
|
+
# Calculate the distance from the object to a point (lat,lon).
|
161
|
+
#
|
162
|
+
# <tt>:units</tt> :: <tt>:mi</tt> (default) or <tt>:km</tt>
|
163
|
+
#
|
164
|
+
def distance_to(lat, lon, units = :mi)
|
165
|
+
return nil unless geocoded?
|
166
|
+
mylat,mylon = read_coordinates
|
167
|
+
Geocoder::Calculations.distance_between(mylat, mylon, lat, lon, :units => units)
|
168
|
+
end
|
169
|
+
|
170
|
+
##
|
171
|
+
# Get other geocoded objects within a given radius.
|
172
|
+
#
|
173
|
+
# <tt>:units</tt> :: <tt>:mi</tt> (default) or <tt>:km</tt>
|
174
|
+
#
|
175
|
+
def nearbys(radius = 20, units = :mi)
|
176
|
+
return [] unless geocoded?
|
177
|
+
options = {:exclude => self, :units => units}
|
178
|
+
self.class.near(read_coordinates, radius, options)
|
179
|
+
end
|
180
|
+
|
181
|
+
##
|
182
|
+
# Fetch coordinates and assign +latitude+ and +longitude+. Also returns
|
183
|
+
# coordinates as an array: <tt>[lat, lon]</tt>.
|
184
|
+
#
|
185
|
+
def fetch_coordinates(save = false)
|
186
|
+
address_method = self.class.geocoder_options[:user_address]
|
187
|
+
unless address_method.is_a? Symbol
|
188
|
+
raise GeocoderConfigurationError,
|
189
|
+
"You are attempting to fetch coordinates but have not specified " +
|
190
|
+
"a method which provides an address for the object."
|
191
|
+
end
|
192
|
+
coords = Geocoder::Lookup.coordinates(send(address_method))
|
193
|
+
unless coords.blank?
|
194
|
+
method = (save ? "update" : "write") + "_attribute"
|
195
|
+
send method, self.class.geocoder_options[:latitude], coords[0]
|
196
|
+
send method, self.class.geocoder_options[:longitude], coords[1]
|
197
|
+
end
|
198
|
+
coords
|
199
|
+
end
|
200
|
+
|
201
|
+
##
|
202
|
+
# Fetch coordinates and update (save) +latitude+ and +longitude+ data.
|
203
|
+
#
|
204
|
+
def fetch_coordinates!
|
205
|
+
fetch_coordinates(true)
|
206
|
+
end
|
207
|
+
|
208
|
+
##
|
209
|
+
# Fetch address and assign +address+ attribute. Also returns
|
210
|
+
# address as a string.
|
211
|
+
#
|
212
|
+
def fetch_address(save = false)
|
213
|
+
lat_attr = self.class.geocoder_options[:latitude]
|
214
|
+
lon_attr = self.class.geocoder_options[:longitude]
|
215
|
+
unless lat_attr.is_a?(Symbol) and lon_attr.is_a?(Symbol)
|
216
|
+
raise GeocoderConfigurationError,
|
217
|
+
"You are attempting to fetch an address but have not specified " +
|
218
|
+
"attributes which provide coordinates for the object."
|
219
|
+
end
|
220
|
+
address = Geocoder::Lookup.address(send(lat_attr), send(lon_attr))
|
221
|
+
unless address.blank?
|
222
|
+
method = (save ? "update" : "write") + "_attribute"
|
223
|
+
send method, self.class.geocoder_options[:fetched_address], address
|
224
|
+
end
|
225
|
+
address
|
226
|
+
end
|
227
|
+
|
228
|
+
##
|
229
|
+
# Fetch address and update (save) +address+ data.
|
230
|
+
#
|
231
|
+
def fetch_address!
|
232
|
+
fetch_address(true)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Geocoder
|
2
|
+
module Calculations
|
3
|
+
extend self
|
4
|
+
|
5
|
+
##
|
6
|
+
# Calculate the distance between two points on Earth (Haversine formula).
|
7
|
+
# Takes two sets of coordinates and an options hash:
|
8
|
+
#
|
9
|
+
# <tt>:units</tt> :: <tt>:mi</tt> (default) or <tt>:km</tt>
|
10
|
+
#
|
11
|
+
def distance_between(lat1, lon1, lat2, lon2, options = {})
|
12
|
+
|
13
|
+
# set default options
|
14
|
+
options[:units] ||= :mi
|
15
|
+
|
16
|
+
# define conversion factors
|
17
|
+
conversions = { :mi => 3956, :km => 6371 }
|
18
|
+
|
19
|
+
# convert degrees to radians
|
20
|
+
lat1 = to_radians(lat1)
|
21
|
+
lon1 = to_radians(lon1)
|
22
|
+
lat2 = to_radians(lat2)
|
23
|
+
lon2 = to_radians(lon2)
|
24
|
+
|
25
|
+
# compute distances
|
26
|
+
dlat = (lat1 - lat2).abs
|
27
|
+
dlon = (lon1 - lon2).abs
|
28
|
+
|
29
|
+
a = (Math.sin(dlat / 2))**2 + Math.cos(lat1) *
|
30
|
+
(Math.sin(dlon / 2))**2 * Math.cos(lat2)
|
31
|
+
c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a))
|
32
|
+
c * conversions[options[:units]]
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Compute the geographic center (aka geographic midpoint, center of
|
37
|
+
# gravity) for an array of geocoded objects and/or [lat,lon] arrays
|
38
|
+
# (can be mixed). Any objects missing coordinates are ignored. Follows
|
39
|
+
# the procedure documented at http://www.geomidpoint.com/calculation.html.
|
40
|
+
#
|
41
|
+
def geographic_center(points)
|
42
|
+
|
43
|
+
# convert objects to [lat,lon] arrays and remove nils
|
44
|
+
points = points.map{ |p|
|
45
|
+
p.is_a?(Array) ? p : (p.geocoded?? p.read_coordinates : nil)
|
46
|
+
}.compact
|
47
|
+
|
48
|
+
# convert degrees to radians
|
49
|
+
points.map!{ |p| [to_radians(p[0]), to_radians(p[1])] }
|
50
|
+
|
51
|
+
# convert to Cartesian coordinates
|
52
|
+
x = []; y = []; z = []
|
53
|
+
points.each do |p|
|
54
|
+
x << Math.cos(p[0]) * Math.cos(p[1])
|
55
|
+
y << Math.cos(p[0]) * Math.sin(p[1])
|
56
|
+
z << Math.sin(p[0])
|
57
|
+
end
|
58
|
+
|
59
|
+
# compute average coordinate values
|
60
|
+
xa, ya, za = [x,y,z].map do |c|
|
61
|
+
c.inject(0){ |tot,i| tot += i } / c.size.to_f
|
62
|
+
end
|
63
|
+
|
64
|
+
# convert back to latitude/longitude
|
65
|
+
lon = Math.atan2(ya, xa)
|
66
|
+
hyp = Math.sqrt(xa**2 + ya**2)
|
67
|
+
lat = Math.atan2(za, hyp)
|
68
|
+
|
69
|
+
# return answer in degrees
|
70
|
+
[to_degrees(lat), to_degrees(lon)]
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Convert degrees to radians.
|
75
|
+
#
|
76
|
+
def to_radians(degrees)
|
77
|
+
degrees * (Math::PI / 180)
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Convert radians to degrees.
|
82
|
+
#
|
83
|
+
def to_degrees(radians)
|
84
|
+
(radians * 180.0) / Math::PI
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Conversion factor: km to mi.
|
89
|
+
#
|
90
|
+
def km_in_mi
|
91
|
+
0.621371192
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module Geocoder
|
4
|
+
module Lookup
|
5
|
+
extend self
|
6
|
+
|
7
|
+
##
|
8
|
+
# Query Google for the coordinates of the given address.
|
9
|
+
# Returns array [lat,lon] if found, nil if not found or if network error.
|
10
|
+
#
|
11
|
+
def coordinates(address)
|
12
|
+
return nil if address.blank?
|
13
|
+
return nil unless doc = search(address, false)
|
14
|
+
# blindly use first result (assume it is most accurate)
|
15
|
+
place = doc['results'].first['geometry']['location']
|
16
|
+
['lat', 'lng'].map{ |i| place[i] }
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Query Google for the address of the given coordinates.
|
21
|
+
# Returns string if found, nil if not found or if network error.
|
22
|
+
#
|
23
|
+
def address(latitude, longitude)
|
24
|
+
return nil if latitude.blank? || longitude.blank?
|
25
|
+
return nil unless doc = search("#{latitude},#{longitude}", true)
|
26
|
+
# blindly use first result (assume it is most accurate)
|
27
|
+
doc['results'].first['formatted_address']
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
private # ---------------------------------------------------------------
|
32
|
+
|
33
|
+
##
|
34
|
+
# Query Google for geographic information about the given phrase.
|
35
|
+
# Returns a hash representing a valid geocoder response.
|
36
|
+
# Returns nil if non-200 HTTP response, timeout, or other error.
|
37
|
+
#
|
38
|
+
def search(query, reverse = false)
|
39
|
+
doc = fetch_parsed_response(query, reverse)
|
40
|
+
doc && doc['status'] == "OK" ? doc : nil
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Returns a parsed Google geocoder search result (hash).
|
45
|
+
# This method is not intended for general use (prefer Geocoder.search).
|
46
|
+
#
|
47
|
+
def fetch_parsed_response(query, reverse = false)
|
48
|
+
if doc = fetch_raw_response(query, reverse)
|
49
|
+
ActiveSupport::JSON.decode(doc)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Returns a raw Google geocoder search result (JSON).
|
55
|
+
# This method is not intended for general use (prefer Geocoder.search).
|
56
|
+
#
|
57
|
+
def fetch_raw_response(query, reverse = false)
|
58
|
+
return nil if query.blank?
|
59
|
+
|
60
|
+
# name parameter based on forward/reverse geocoding
|
61
|
+
param = reverse ? :latlng : :address
|
62
|
+
|
63
|
+
# build URL
|
64
|
+
params = { param => query, :sensor => "false" }
|
65
|
+
url = "http://maps.google.com/maps/api/geocode/json?" + params.to_query
|
66
|
+
|
67
|
+
# query geocoder and make sure it responds quickly
|
68
|
+
begin
|
69
|
+
resp = nil
|
70
|
+
timeout(3) do
|
71
|
+
Net::HTTP.get_response(URI.parse(url)).body
|
72
|
+
end
|
73
|
+
rescue SocketError, TimeoutError
|
74
|
+
return nil
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/test/geocoder_test.rb
CHANGED
@@ -10,14 +10,21 @@ class GeocoderTest < Test::Unit::TestCase
|
|
10
10
|
|
11
11
|
# sanity check
|
12
12
|
def test_distance_between
|
13
|
-
assert_equal 69, Geocoder.distance_between(0,0, 0,1).round
|
13
|
+
assert_equal 69, Geocoder::Calculations.distance_between(0,0, 0,1).round
|
14
14
|
end
|
15
15
|
|
16
16
|
# sanity check
|
17
17
|
def test_geographic_center
|
18
18
|
assert_equal [0.0, 0.5],
|
19
|
-
Geocoder.geographic_center([[0,0], [0,1]])
|
19
|
+
Geocoder::Calculations.geographic_center([[0,0], [0,1]])
|
20
20
|
assert_equal [0.0, 1.0],
|
21
|
-
Geocoder.geographic_center([[0,0], [0,1], [0,2]])
|
21
|
+
Geocoder::Calculations.geographic_center([[0,0], [0,1], [0,2]])
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_exception_raised_for_unconfigured_geocoding
|
25
|
+
l = Landmark.new("Mount Rushmore", 43.88, -103.46)
|
26
|
+
assert_raises GeocoderConfigurationError do
|
27
|
+
l.fetch_coordinates
|
28
|
+
end
|
22
29
|
end
|
23
30
|
end
|
data/test/test_helper.rb
CHANGED
@@ -35,11 +35,13 @@ end
|
|
35
35
|
require 'geocoder'
|
36
36
|
|
37
37
|
##
|
38
|
-
# Mock HTTP request to
|
38
|
+
# Mock HTTP request to geocoding service.
|
39
39
|
#
|
40
40
|
module Geocoder
|
41
|
-
|
42
|
-
|
41
|
+
module Lookup
|
42
|
+
def self.fetch_raw_response(query, reverse = false)
|
43
|
+
File.read(File.join("test", "fixtures", "madison_square_garden.json"))
|
44
|
+
end
|
43
45
|
end
|
44
46
|
end
|
45
47
|
|
@@ -63,6 +65,27 @@ class Venue < ActiveRecord::Base
|
|
63
65
|
end
|
64
66
|
end
|
65
67
|
|
68
|
+
##
|
69
|
+
# Reverse geocoded model.
|
70
|
+
#
|
71
|
+
class Landmark < ActiveRecord::Base
|
72
|
+
reverse_geocoded_by :latitude, :longitude
|
73
|
+
|
74
|
+
def initialize(name, latitude, longitude)
|
75
|
+
super()
|
76
|
+
write_attribute :name, name
|
77
|
+
write_attribute :latitude, latitude
|
78
|
+
write_attribute :longitude, longitude
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# If method not found, assume it's an ActiveRecord attribute reader.
|
83
|
+
#
|
84
|
+
def method_missing(name, *args, &block)
|
85
|
+
@attributes[name]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
66
89
|
class Test::Unit::TestCase
|
67
90
|
def venue_params(abbrev)
|
68
91
|
{
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails-geocoder
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 53
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 9
|
9
|
-
-
|
10
|
-
version: 0.9.
|
9
|
+
- 7
|
10
|
+
version: 0.9.7
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Alex Reisner
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-01
|
18
|
+
date: 2011-02-01 00:00:00 -05:00
|
19
19
|
default_executable:
|
20
20
|
dependencies: []
|
21
21
|
|
@@ -30,6 +30,9 @@ extra_rdoc_files: []
|
|
30
30
|
|
31
31
|
files:
|
32
32
|
- lib/geocoder.rb
|
33
|
+
- lib/geocoder/lookup.rb
|
34
|
+
- lib/geocoder/calculations.rb
|
35
|
+
- lib/geocoder/active_record.rb
|
33
36
|
- test/test_helper.rb
|
34
37
|
- test/geocoder_test.rb
|
35
38
|
- CHANGELOG.rdoc
|