artofmission-Geokit 1.0.1

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