geocoder 0.1.1 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of geocoder might be problematic. Click here for more details.

@@ -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 Geocoder::ConfigurationError,
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 Geocoder::ConfigurationError,
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,8 @@
1
+ module Geocoder
2
+ class Configuration
3
+ cattr_accessor :timeout
4
+ end
5
+ end
6
+
7
+ Geocoder::Configuration.timeout = 3
8
+
@@ -0,0 +1,90 @@
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
+ #
10
+ def coordinates(address)
11
+ if (results = search(address)).size > 0
12
+ place = results.first.geometry['location']
13
+ ['lat', 'lng'].map{ |i| place[i] }
14
+ end
15
+ end
16
+
17
+ ##
18
+ # Query Google for the address of the given coordinates.
19
+ #
20
+ def address(latitude, longitude)
21
+ if (results = search(latitude, longitude)).size > 0
22
+ results.first.formatted_address
23
+ end
24
+ end
25
+
26
+ ##
27
+ # Takes a search string (eg: "Mississippi Coast Coliseumf, Biloxi, MS") for
28
+ # geocoding, or coordinates (latitude, longitude) for reverse geocoding.
29
+ # Returns an array of Geocoder::Result objects,
30
+ # or nil if not found or if network error.
31
+ #
32
+ def search(*args)
33
+ return nil if args[0].blank?
34
+ doc = parsed_response(args.join(","), args.size == 2)
35
+ [].tap do |results|
36
+ if doc
37
+ doc['results'].each{ |r| results << Result.new(r) }
38
+ end
39
+ end
40
+ end
41
+
42
+
43
+ private # ---------------------------------------------------------------
44
+
45
+ ##
46
+ # Returns a parsed Google geocoder search result (hash).
47
+ # Returns nil if non-200 HTTP response, timeout, or other error.
48
+ #
49
+ def parsed_response(query, reverse = false)
50
+ begin
51
+ doc = ActiveSupport::JSON.decode(fetch_data(query, reverse))
52
+ rescue SocketError
53
+ warn "Google Geocoding API connection cannot be established."
54
+ rescue TimeoutError
55
+ warn "Google Geocoding API not responding fast enough " +
56
+ "(see Geocoder::Configuration.timeout to set limit)."
57
+ end
58
+
59
+ case doc['status']; when "OK"
60
+ doc
61
+ when "OVER_QUERY_LIMIT"
62
+ warn "Google Geocoding API error: over query limit."
63
+ when "REQUEST_DENIED"
64
+ warn "Google Geocoding API error: request denied."
65
+ when "INVALID_REQUEST"
66
+ warn "Google Geocoding API error: invalid request."
67
+ end
68
+ end
69
+
70
+ ##
71
+ # Fetches a raw Google geocoder search result (JSON string).
72
+ #
73
+ def fetch_data(query, reverse = false)
74
+ return nil if query.blank?
75
+ url = query_url(query, reverse)
76
+ timeout(Geocoder::Configuration.timeout) do
77
+ Net::HTTP.get_response(URI.parse(url)).body
78
+ end
79
+ end
80
+
81
+ def query_url(query, reverse = false)
82
+ params = {
83
+ (reverse ? :latlng : :address) => query,
84
+ :sensor => "false"
85
+ }
86
+ "http://maps.google.com/maps/api/geocode/json?" + params.to_query
87
+ end
88
+ end
89
+ end
90
+
@@ -0,0 +1,68 @@
1
+ require 'geocoder'
2
+
3
+ module Geocoder
4
+ if defined? Rails::Railtie
5
+ require 'rails'
6
+ class Railtie < Rails::Railtie
7
+ initializer 'geocoder.insert_into_active_record' do
8
+ ActiveSupport.on_load :active_record do
9
+ Geocoder::Railtie.insert
10
+ end
11
+ end
12
+ rake_tasks do
13
+ load "tasks/geocoder.rake"
14
+ end
15
+ end
16
+ end
17
+
18
+ class Railtie
19
+ def self.insert
20
+
21
+ return unless defined?(::ActiveRecord)
22
+
23
+ ##
24
+ # Add methods to ActiveRecord::Base so Geocoder is accessible by models.
25
+ #
26
+ ::ActiveRecord::Base.class_eval do
27
+
28
+ ##
29
+ # Set attribute names and include the Geocoder module.
30
+ #
31
+ def self.geocoded_by(address_attr, options = {})
32
+ _geocoder_init(
33
+ :user_address => address_attr,
34
+ :latitude => options[:latitude] || :latitude,
35
+ :longitude => options[:longitude] || :longitude
36
+ )
37
+ end
38
+
39
+ ##
40
+ # Set attribute names and include the Geocoder module.
41
+ #
42
+ def self.reverse_geocoded_by(latitude_attr, longitude_attr, options = {})
43
+ _geocoder_init(
44
+ :fetched_address => options[:address] || :address,
45
+ :latitude => latitude_attr,
46
+ :longitude => longitude_attr
47
+ )
48
+ end
49
+
50
+ def self._geocoder_init(options)
51
+ unless _geocoder_initialized?
52
+ class_inheritable_reader :geocoder_options
53
+ class_inheritable_hash_writer :geocoder_options
54
+ end
55
+ self.geocoder_options = options
56
+ unless _geocoder_initialized?
57
+ include Geocoder::ActiveRecord
58
+ end
59
+ end
60
+
61
+ def self._geocoder_initialized?
62
+ included_modules.include? Geocoder::ActiveRecord
63
+ end
64
+ end
65
+
66
+ end
67
+ end
68
+ end