radiant-location-extension 1.2.1

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.
@@ -0,0 +1,351 @@
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
+
157
+
158
+
159
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
160
+ xml=res.body
161
+ logger.debug "Google geocoding. Address: #{address}. Result: #{xml}"
162
+ doc=REXML::Document.new(xml)
163
+ if doc.elements['//kml/Response/Status/code'].text == '200'
164
+
165
+ res = GeoLoc.new
166
+ coordinates=doc.elements['//coordinates'].text.to_s.split(',')
167
+
168
+ #basics
169
+ res.lat=coordinates[1]
170
+ res.lng=coordinates[0]
171
+ res.country_code=doc.elements['//CountryNameCode'].text
172
+ res.provider='google'
173
+
174
+ #extended -- false if not not available
175
+ res.city = doc.elements['//LocalityName'].text if doc.elements['//LocalityName']
176
+ res.state = doc.elements['//AdministrativeAreaName'].text if doc.elements['//AdministrativeAreaName']
177
+ res.full_address = doc.elements['//address'].text if doc.elements['//address'] # google provides it
178
+ res.zip = doc.elements['//PostalCodeNumber'].text if doc.elements['//PostalCodeNumber']
179
+ res.street_address = doc.elements['//ThoroughfareName'].text if doc.elements['//ThoroughfareName']
180
+ # Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country
181
+ # For Google, 1=low accuracy, 8=high accuracy
182
+ # old way -- address_details=doc.elements['//AddressDetails','urn:oasis:names:tc:ciq:xsdschema:xAL:2.0']
183
+ address_details=doc.elements['//*[local-name() = "AddressDetails"]']
184
+ accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0
185
+ res.precision=%w{unknown country state state city zip zip+4 street address}[accuracy]
186
+ res.success=true
187
+
188
+ return res
189
+ else
190
+ logger.info "Google was unable to geocode address: "+address
191
+ return GeoLoc.new
192
+ end
193
+
194
+ rescue
195
+ logger.error "Caught an error during Google geocoding call: "+$!
196
+ return GeoLoc.new
197
+ end
198
+ end
199
+
200
+ # Provides geocoding based upon an IP address. The underlying web service is a hostip.info
201
+ # which sources their data through a combination of publicly available information as well
202
+ # as community contributions.
203
+ class IpGeocoder < Geocoder
204
+
205
+ private
206
+
207
+ # Given an IP address, returns a GeoLoc instance which contains latitude,
208
+ # longitude, city, and country code. Sets the success attribute to false if the ip
209
+ # parameter does not match an ip address.
210
+ def self.do_geocode(ip)
211
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
212
+ url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true"
213
+ response = self.call_geocoder_service(url)
214
+ response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new
215
+ rescue
216
+ logger.error "Caught an error during HostIp geocoding call: "+$!
217
+ return GeoLoc.new
218
+ end
219
+
220
+ # Converts the body to YAML since its in the form of:
221
+ #
222
+ # Country: UNITED STATES (US)
223
+ # City: Sugar Grove, IL
224
+ # Latitude: 41.7696
225
+ # Longitude: -88.4588
226
+ #
227
+ # then instantiates a GeoLoc instance to populate with location data.
228
+ def self.parse_body(body) # :nodoc:
229
+ yaml = YAML.load(body)
230
+ res = GeoLoc.new
231
+ res.provider = 'hostip'
232
+ res.city, res.state = yaml['City'].split(', ')
233
+ country, res.country_code = yaml['Country'].split(' (')
234
+ res.lat = yaml['Latitude']
235
+ res.lng = yaml['Longitude']
236
+ res.country_code.chop!
237
+ res.success = res.city != "(Private Address)"
238
+ res
239
+ end
240
+ end
241
+
242
+ # Geocoder Us geocoder implementation. Requires the GeoKit::Geocoders::GEOCODER_US variable to
243
+ # contain true or false based upon whether authentication is to occur. Conforms to the
244
+ # interface set by the Geocoder class.
245
+ class UsGeocoder < Geocoder
246
+
247
+ private
248
+
249
+ # For now, the geocoder_method will only geocode full addresses -- not zips or cities in isolation
250
+ def self.do_geocode(address)
251
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
252
+ url = "http://"+(GeoKit::Geocoders::geocoder_us || '')+"geocoder.us/service/csv/geocode?address=#{CGI.escape(address_str)}"
253
+ res = self.call_geocoder_service(url)
254
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
255
+ data = res.body
256
+ logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}"
257
+ array = data.chomp.split(',')
258
+
259
+ if array.length == 6
260
+ res=GeoLoc.new
261
+ res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array
262
+ res.country_code='US'
263
+ res.success=true
264
+ return res
265
+ else
266
+ logger.info "geocoder.us was unable to geocode address: "+address
267
+ return GeoLoc.new
268
+ end
269
+ rescue
270
+ logger.error "Caught an error during geocoder.us geocoding call: "+$!
271
+ return GeoLoc.new
272
+ end
273
+ end
274
+
275
+ # Yahoo geocoder implementation. Requires the GeoKit::Geocoders::YAHOO variable to
276
+ # contain a Yahoo API key. Conforms to the interface set by the Geocoder class.
277
+ class YahooGeocoder < Geocoder
278
+
279
+ private
280
+
281
+ # Template method which does the geocode lookup.
282
+ def self.do_geocode(address)
283
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
284
+ url="http://api.local.yahoo.com/MapsService/V1/geocode?appid=#{GeoKit::Geocoders::yahoo}&location=#{CGI.escape(address_str)}"
285
+ res = self.call_geocoder_service(url)
286
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
287
+ xml = res.body
288
+ doc = REXML::Document.new(xml)
289
+ logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}"
290
+
291
+ if doc.elements['//ResultSet']
292
+ res=GeoLoc.new
293
+
294
+ #basic
295
+ res.lat=doc.elements['//Latitude'].text
296
+ res.lng=doc.elements['//Longitude'].text
297
+ res.country_code=doc.elements['//Country'].text
298
+ res.provider='yahoo'
299
+
300
+ #extended - false if not available
301
+ res.city=doc.elements['//City'].text if doc.elements['//City'] && doc.elements['//City'].text != nil
302
+ res.state=doc.elements['//State'].text if doc.elements['//State'] && doc.elements['//State'].text != nil
303
+ res.zip=doc.elements['//Zip'].text if doc.elements['//Zip'] && doc.elements['//Zip'].text != nil
304
+ res.street_address=doc.elements['//Address'].text if doc.elements['//Address'] && doc.elements['//Address'].text != nil
305
+ res.precision=doc.elements['//Result'].attributes['precision'] if doc.elements['//Result']
306
+ res.success=true
307
+ return res
308
+ else
309
+ logger.info "Yahoo was unable to geocode address: "+address
310
+ return GeoLoc.new
311
+ end
312
+
313
+ rescue
314
+ logger.info "Caught an error during Yahoo geocoding call: "+$!
315
+ return GeoLoc.new
316
+ end
317
+ end
318
+
319
+ # Provides methods to geocode with a variety of geocoding service providers, plus failover
320
+ # among providers in the order you configure.
321
+ #
322
+ # Goal:
323
+ # - homogenize the results of multiple geocoders
324
+ #
325
+ # Limitations:
326
+ # - currently only provides the first result. Sometimes geocoders will return multiple results.
327
+ # - currently discards the "accuracy" component of the geocoding calls
328
+ class MultiGeocoder < Geocoder
329
+ private
330
+
331
+ # This method will call one or more geocoders in the order specified in the
332
+ # configuration until one of the geocoders work.
333
+ #
334
+ # The failover approach is crucial for production-grade apps, but is rarely used.
335
+ # 98% of your geocoding calls will be successful with the first call
336
+ def self.do_geocode(address)
337
+ GeoKit::Geocoders::provider_order.each do |provider|
338
+ begin
339
+ klass = GeoKit::Geocoders.const_get "#{provider.to_s.capitalize}Geocoder"
340
+ res = klass.send :geocode, address
341
+ return res if res.success
342
+ rescue
343
+ 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}")
344
+ end
345
+ end
346
+ # If we get here, we failed completely.
347
+ GeoLoc.new
348
+ end
349
+ end
350
+ end
351
+ end
@@ -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,0 +1,430 @@
1
+ module GeoKit
2
+ # Contains class and instance methods providing distance calcuation services. This
3
+ # module is meant to be mixed into classes containing lat and lng attributes where
4
+ # distance calculation is desired.
5
+ #
6
+ # At present, two forms of distance calculations are provided:
7
+ #
8
+ # * Pythagorean Theory (flat Earth) - which assumes the world is flat and loses accuracy over long distances.
9
+ # * Haversine (sphere) - which is fairly accurate, but at a performance cost.
10
+ #
11
+ # Distance units supported are :miles and :kms.
12
+ module Mappable
13
+ PI_DIV_RAD = 0.0174
14
+ KMS_PER_MILE = 1.609
15
+ EARTH_RADIUS_IN_MILES = 3963.19
16
+ EARTH_RADIUS_IN_KMS = EARTH_RADIUS_IN_MILES * KMS_PER_MILE
17
+ MILES_PER_LATITUDE_DEGREE = 69.1
18
+ KMS_PER_LATITUDE_DEGREE = MILES_PER_LATITUDE_DEGREE * KMS_PER_MILE
19
+ LATITUDE_DEGREES = EARTH_RADIUS_IN_MILES / MILES_PER_LATITUDE_DEGREE
20
+
21
+ # Mix below class methods into the includer.
22
+ def self.included(receiver) # :nodoc:
23
+ receiver.extend ClassMethods
24
+ end
25
+
26
+ module ClassMethods #:nodoc:
27
+ # Returns the distance between two points. The from and to parameters are
28
+ # required to have lat and lng attributes. Valid options are:
29
+ # :units - valid values are :miles or :kms (GeoKit::default_units is the default)
30
+ # :formula - valid values are :flat or :sphere (GeoKit::default_formula is the default)
31
+ def distance_between(from, to, options={})
32
+ from=GeoKit::LatLng.normalize(from)
33
+ to=GeoKit::LatLng.normalize(to)
34
+ return 0.0 if from == to # fixes a "zero-distance" bug
35
+ units = options[:units] || GeoKit::default_units
36
+ formula = options[:formula] || GeoKit::default_formula
37
+ case formula
38
+ when :sphere
39
+ units_sphere_multiplier(units) *
40
+ Math.acos( Math.sin(deg2rad(from.lat)) * Math.sin(deg2rad(to.lat)) +
41
+ Math.cos(deg2rad(from.lat)) * Math.cos(deg2rad(to.lat)) *
42
+ Math.cos(deg2rad(to.lng) - deg2rad(from.lng)))
43
+ when :flat
44
+ Math.sqrt((units_per_latitude_degree(units)*(from.lat-to.lat))**2 +
45
+ (units_per_longitude_degree(from.lat, units)*(from.lng-to.lng))**2)
46
+ end
47
+ end
48
+
49
+ # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
50
+ # from the first point to the second point. Typicaly, the instance methods will be used
51
+ # instead of this method.
52
+ def heading_between(from,to)
53
+ from=GeoKit::LatLng.normalize(from)
54
+ to=GeoKit::LatLng.normalize(to)
55
+
56
+ d_lng=deg2rad(to.lng-from.lng)
57
+ from_lat=deg2rad(from.lat)
58
+ to_lat=deg2rad(to.lat)
59
+ y=Math.sin(d_lng) * Math.cos(to_lat)
60
+ x=Math.cos(from_lat)*Math.sin(to_lat)-Math.sin(from_lat)*Math.cos(to_lat)*Math.cos(d_lng)
61
+ heading=to_heading(Math.atan2(y,x))
62
+ end
63
+
64
+ # Given a start point, distance, and heading (in degrees), provides
65
+ # an endpoint. Returns a LatLng instance. Typically, the instance method
66
+ # will be used instead of this method.
67
+ def endpoint(start,heading, distance, options={})
68
+ units = options[:units] || GeoKit::default_units
69
+ radius = units == :miles ? EARTH_RADIUS_IN_MILES : EARTH_RADIUS_IN_KMS
70
+ start=GeoKit::LatLng.normalize(start)
71
+ lat=deg2rad(start.lat)
72
+ lng=deg2rad(start.lng)
73
+ heading=deg2rad(heading)
74
+ distance=distance.to_f
75
+
76
+ end_lat=Math.asin(Math.sin(lat)*Math.cos(distance/radius) +
77
+ Math.cos(lat)*Math.sin(distance/radius)*Math.cos(heading))
78
+
79
+ end_lng=lng+Math.atan2(Math.sin(heading)*Math.sin(distance/radius)*Math.cos(lat),
80
+ Math.cos(distance/radius)-Math.sin(lat)*Math.sin(end_lat))
81
+
82
+ LatLng.new(rad2deg(end_lat),rad2deg(end_lng))
83
+ end
84
+
85
+ # Returns the midpoint, given two points. Returns a LatLng.
86
+ # Typically, the instance method will be used instead of this method.
87
+ # Valid option:
88
+ # :units - valid values are :miles or :kms (:miles is the default)
89
+ def midpoint_between(from,to,options={})
90
+ from=GeoKit::LatLng.normalize(from)
91
+
92
+ units = options[:units] || GeoKit::default_units
93
+
94
+ heading=from.heading_to(to)
95
+ distance=from.distance_to(to,options)
96
+ midpoint=from.endpoint(heading,distance/2,options)
97
+ end
98
+
99
+ # Geocodes a location using the multi geocoder.
100
+ def geocode(location)
101
+ res = Geocoders::MultiGeocoder.geocode(location)
102
+ return res if res.success
103
+ raise GeoKit::Geocoders::GeocodeError
104
+ end
105
+
106
+ protected
107
+
108
+ def deg2rad(degrees)
109
+ degrees.to_f / 180.0 * Math::PI
110
+ end
111
+
112
+ def rad2deg(rad)
113
+ rad.to_f * 180.0 / Math::PI
114
+ end
115
+
116
+ def to_heading(rad)
117
+ (rad2deg(rad)+360)%360
118
+ end
119
+
120
+ # Returns the multiplier used to obtain the correct distance units.
121
+ def units_sphere_multiplier(units)
122
+ units == :miles ? EARTH_RADIUS_IN_MILES : EARTH_RADIUS_IN_KMS
123
+ end
124
+
125
+ # Returns the number of units per latitude degree.
126
+ def units_per_latitude_degree(units)
127
+ units == :miles ? MILES_PER_LATITUDE_DEGREE : KMS_PER_LATITUDE_DEGREE
128
+ end
129
+
130
+ # Returns the number units per longitude degree.
131
+ def units_per_longitude_degree(lat, units)
132
+ miles_per_longitude_degree = (LATITUDE_DEGREES * Math.cos(lat * PI_DIV_RAD)).abs
133
+ units == :miles ? miles_per_longitude_degree : miles_per_longitude_degree * KMS_PER_MILE
134
+ end
135
+ end
136
+
137
+ # -----------------------------------------------------------------------------------------------
138
+ # Instance methods below here
139
+ # -----------------------------------------------------------------------------------------------
140
+
141
+ # Extracts a LatLng instance. Use with models that are acts_as_mappable
142
+ def to_lat_lng
143
+ return self if instance_of?(GeoKit::LatLng) || instance_of?(GeoKit::GeoLoc)
144
+ return LatLng.new(send(self.class.lat_column_name),send(self.class.lng_column_name)) if self.class.respond_to?(:acts_as_mappable)
145
+ return nil
146
+ end
147
+
148
+ # Returns the distance from another point. The other point parameter is
149
+ # required to have lat and lng attributes. Valid options are:
150
+ # :units - valid values are :miles or :kms (:miles is the default)
151
+ # :formula - valid values are :flat or :sphere (:sphere is the default)
152
+ def distance_to(other, options={})
153
+ self.class.distance_between(self, other, options)
154
+ end
155
+ alias distance_from distance_to
156
+
157
+ # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
158
+ # to the given point. The given point can be a LatLng or a string to be Geocoded
159
+ def heading_to(other)
160
+ self.class.heading_between(self,other)
161
+ end
162
+
163
+ # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
164
+ # FROM the given point. The given point can be a LatLng or a string to be Geocoded
165
+ def heading_from(other)
166
+ self.class.heading_between(other,self)
167
+ end
168
+
169
+ # Returns the endpoint, given a heading (in degrees) and distance.
170
+ # Valid option:
171
+ # :units - valid values are :miles or :kms (:miles is the default)
172
+ def endpoint(heading,distance,options={})
173
+ self.class.endpoint(self,heading,distance,options)
174
+ end
175
+
176
+ # Returns the midpoint, given another point on the map.
177
+ # Valid option:
178
+ # :units - valid values are :miles or :kms (:miles is the default)
179
+ def midpoint_to(other, options={})
180
+ self.class.midpoint_between(self,other,options)
181
+ end
182
+
183
+ end
184
+
185
+ class LatLng
186
+ include Mappable
187
+
188
+ attr_accessor :lat, :lng
189
+
190
+ # Accepts latitude and longitude or instantiates an empty instance
191
+ # if lat and lng are not provided. Converted to floats if provided
192
+ def initialize(lat=nil, lng=nil)
193
+ lat = lat.to_f if lat && !lat.is_a?(Numeric)
194
+ lng = lng.to_f if lng && !lng.is_a?(Numeric)
195
+ @lat = lat
196
+ @lng = lng
197
+ end
198
+
199
+ # Latitude attribute setter; stored as a float.
200
+ def lat=(lat)
201
+ @lat = lat.to_f if lat
202
+ end
203
+
204
+ # Longitude attribute setter; stored as a float;
205
+ def lng=(lng)
206
+ @lng=lng.to_f if lng
207
+ end
208
+
209
+ # Returns the lat and lng attributes as a comma-separated string.
210
+ def ll
211
+ "#{lat},#{lng}"
212
+ end
213
+
214
+ #returns a string with comma-separated lat,lng values
215
+ def to_s
216
+ ll
217
+ end
218
+
219
+ #returns a two-element array
220
+ def to_a
221
+ [lat,lng]
222
+ end
223
+ # Returns true if the candidate object is logically equal. Logical equivalence
224
+ # is true if the lat and lng attributes are the same for both objects.
225
+ def ==(other)
226
+ other.is_a?(LatLng) ? self.lat == other.lat && self.lng == other.lng : false
227
+ end
228
+
229
+ # A *class* method to take anything which can be inferred as a point and generate
230
+ # a LatLng from it. You should use this anything you're not sure what the input is,
231
+ # and want to deal with it as a LatLng if at all possible. Can take:
232
+ # 1) two arguments (lat,lng)
233
+ # 2) a string in the format "37.1234,-129.1234" or "37.1234 -129.1234"
234
+ # 3) a string which can be geocoded on the fly
235
+ # 4) an array in the format [37.1234,-129.1234]
236
+ # 5) a LatLng or GeoLoc (which is just passed through as-is)
237
+ # 6) anything which acts_as_mappable -- a LatLng will be extracted from it
238
+ def self.normalize(thing,other=nil)
239
+ # if an 'other' thing is supplied, normalize the input by creating an array of two elements
240
+ thing=[thing,other] if other
241
+
242
+ if thing.is_a?(String)
243
+ thing.strip!
244
+ if match=thing.match(/(\-?\d+\.?\d*)[, ] ?(\-?\d+\.?\d*)$/)
245
+ return GeoKit::LatLng.new(match[1],match[2])
246
+ else
247
+ res = GeoKit::Geocoders::MultiGeocoder.geocode(thing)
248
+ return res if res.success
249
+ raise GeoKit::Geocoders::GeocodeError
250
+ end
251
+ elsif thing.is_a?(Array) && thing.size==2
252
+ return GeoKit::LatLng.new(thing[0],thing[1])
253
+ elsif thing.is_a?(LatLng) # will also be true for GeoLocs
254
+ return thing
255
+ elsif thing.class.respond_to?(:acts_as_mappable) && thing.class.respond_to?(:distance_column_name)
256
+ return thing.to_lat_lng
257
+ end
258
+
259
+ 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.")
260
+ end
261
+
262
+ end
263
+
264
+ # This class encapsulates the result of a geocoding call
265
+ # It's primary purpose is to homogenize the results of multiple
266
+ # geocoding providers. It also provides some additional functionality, such as
267
+ # the "full address" method for geocoders that do not provide a
268
+ # full address in their results (for example, Yahoo), and the "is_us" method.
269
+ class GeoLoc < LatLng
270
+ # Location attributes. Full address is a concatenation of all values. For example:
271
+ # 100 Spear St, San Francisco, CA, 94101, US
272
+ attr_accessor :street_address, :city, :state, :zip, :country_code, :full_address
273
+ # Attributes set upon return from geocoding. Success will be true for successful
274
+ # geocode lookups. The provider will be set to the name of the providing geocoder.
275
+ # Finally, precision is an indicator of the accuracy of the geocoding.
276
+ attr_accessor :success, :provider, :precision
277
+ # Street number and street name are extracted from the street address attribute.
278
+ attr_reader :street_number, :street_name
279
+
280
+ # Constructor expects a hash of symbols to correspond with attributes.
281
+ def initialize(h={})
282
+ @street_address=h[:street_address]
283
+ @city=h[:city]
284
+ @state=h[:state]
285
+ @zip=h[:zip]
286
+ @country_code=h[:country_code]
287
+ @success=false
288
+ @precision='unknown'
289
+ super(h[:lat],h[:lng])
290
+ end
291
+
292
+ # Returns true if geocoded to the United States.
293
+ def is_us?
294
+ country_code == 'US'
295
+ end
296
+
297
+ # full_address is provided by google but not by yahoo. It is intended that the google
298
+ # geocoding method will provide the full address, whereas for yahoo it will be derived
299
+ # from the parts of the address we do have.
300
+ def full_address
301
+ @full_address ? @full_address : to_geocodeable_s
302
+ end
303
+
304
+ # Extracts the street number from the street address if the street address
305
+ # has a value.
306
+ def street_number
307
+ street_address[/(\d*)/] if street_address
308
+ end
309
+
310
+ # Returns the street name portion of the street address.
311
+ def street_name
312
+ street_address[street_number.length, street_address.length].strip if street_address
313
+ end
314
+
315
+ # gives you all the important fields as key-value pairs
316
+ def hash
317
+ res={}
318
+ [: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) }
319
+ res
320
+ end
321
+ alias to_hash hash
322
+
323
+ # Sets the city after capitalizing each word within the city name.
324
+ def city=(city)
325
+ @city = city.titleize if city
326
+ end
327
+
328
+ # Sets the street address after capitalizing each word within the street address.
329
+ def street_address=(address)
330
+ @street_address = address.titleize if address
331
+ end
332
+
333
+ # Returns a comma-delimited string consisting of the street address, city, state,
334
+ # zip, and country code. Only includes those attributes that are non-blank.
335
+ def to_geocodeable_s
336
+ a=[street_address, city, state, zip, country_code].compact
337
+ a.delete_if { |e| !e || e == '' }
338
+ a.join(', ')
339
+ end
340
+
341
+ # Returns a string representation of the instance.
342
+ def to_s
343
+ "Provider: #{provider}\n Street: #{street_address}\nCity: #{city}\nState: #{state}\nZip: #{zip}\nLatitude: #{lat}\nLongitude: #{lng}\nCountry: #{country_code}\nSuccess: #{success}"
344
+ end
345
+ end
346
+
347
+ # Bounds represents a rectangular bounds, defined by the SW and NE corners
348
+ class Bounds
349
+ # sw and ne are LatLng objects
350
+ attr_accessor :sw, :ne
351
+
352
+ # provide sw and ne to instantiate a new Bounds instance
353
+ def initialize(sw,ne)
354
+ raise ArguementError if !(sw.is_a?(GeoKit::LatLng) && ne.is_a?(GeoKit::LatLng))
355
+ @sw,@ne=sw,ne
356
+ end
357
+
358
+ #returns the a single point which is the center of the rectangular bounds
359
+ def center
360
+ @sw.midpoint_to(@ne)
361
+ end
362
+
363
+ # a simple string representation:sw,ne
364
+ def to_s
365
+ "#{@sw.to_s},#{@ne.to_s}"
366
+ end
367
+
368
+ # a two-element array of two-element arrays: sw,ne
369
+ def to_a
370
+ [@sw.to_a, @ne.to_a]
371
+ end
372
+
373
+ # Returns true if the bounds contain the passed point.
374
+ # allows for bounds which cross the meridian
375
+ def contains?(point)
376
+ point=GeoKit::LatLng.normalize(point)
377
+ res = point.lat > @sw.lat && point.lat < @ne.lat
378
+ if crosses_meridian?
379
+ res &= point.lng < @ne.lng || point.lng > @sw.lng
380
+ else
381
+ res &= point.lng < @ne.lng && point.lng > @sw.lng
382
+ end
383
+ res
384
+ end
385
+
386
+ # returns true if the bounds crosses the international dateline
387
+ def crosses_meridian?
388
+ @sw.lng > @ne.lng
389
+ end
390
+
391
+ # Returns true if the candidate object is logically equal. Logical equivalence
392
+ # is true if the lat and lng attributes are the same for both objects.
393
+ def ==(other)
394
+ other.is_a?(Bounds) ? self.sw == other.sw && self.ne == other.ne : false
395
+ end
396
+
397
+ class <<self
398
+
399
+ # returns an instance of bounds which completely encompases the given circle
400
+ def from_point_and_radius(point,radius,options={})
401
+ point=LatLng.normalize(point)
402
+ p0=point.endpoint(0,radius,options)
403
+ p90=point.endpoint(90,radius,options)
404
+ p180=point.endpoint(180,radius,options)
405
+ p270=point.endpoint(270,radius,options)
406
+ sw=GeoKit::LatLng.new(p180.lat,p270.lng)
407
+ ne=GeoKit::LatLng.new(p0.lat,p90.lng)
408
+ GeoKit::Bounds.new(sw,ne)
409
+ end
410
+
411
+ # Takes two main combinations of arguements to create a bounds:
412
+ # point,point (this is the only one which takes two arguments
413
+ # [point,point]
414
+ # . . . where a point is anything LatLng#normalize can handle (which is quite a lot)
415
+ #
416
+ # NOTE: everything combination is assumed to pass points in the order sw, ne
417
+ def normalize (thing,other=nil)
418
+ # maybe this will be simple -- an actual bounds object is passed, and we can all go home
419
+ return thing if thing.is_a? Bounds
420
+
421
+ # no? OK, if there's no "other," the thing better be a two-element array
422
+ thing,other=thing if !other && thing.is_a?(Array) && thing.size==2
423
+
424
+ # Now that we're set with a thing and another thing, let LatLng do the heavy lifting.
425
+ # Exceptions may be thrown
426
+ Bounds.new(GeoKit::LatLng.normalize(thing),GeoKit::LatLng.normalize(other))
427
+ end
428
+ end
429
+ end
430
+ end