artofmission-Geokit 1.0.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,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