radiant-location-extension 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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