abuiles-geokit 1.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.bundle/config +2 -0
  2. data/.gitignore +4 -0
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +19 -0
  5. data/History.txt +77 -0
  6. data/Manifest.txt +21 -0
  7. data/README.markdown +273 -0
  8. data/Rakefile +13 -0
  9. data/geokit.gemspec +24 -0
  10. data/lib/geokit.rb +55 -0
  11. data/lib/geokit/bounds.rb +95 -0
  12. data/lib/geokit/geo_loc.rb +115 -0
  13. data/lib/geokit/geocoders.rb +68 -0
  14. data/lib/geokit/geocoders/ca_geocoder.rb +54 -0
  15. data/lib/geokit/geocoders/geo_plugin_geocoder.rb +30 -0
  16. data/lib/geokit/geocoders/geocode_error.rb +7 -0
  17. data/lib/geokit/geocoders/geocoder.rb +75 -0
  18. data/lib/geokit/geocoders/geonames_geocoder.rb +53 -0
  19. data/lib/geokit/geocoders/google_geocoder.rb +145 -0
  20. data/lib/geokit/geocoders/google_premier_geocoder.rb +147 -0
  21. data/lib/geokit/geocoders/ip_geocoder.rb +76 -0
  22. data/lib/geokit/geocoders/multi_geocoder.rb +60 -0
  23. data/lib/geokit/geocoders/us_geocoder.rb +50 -0
  24. data/lib/geokit/geocoders/yahoo_geocoder.rb +49 -0
  25. data/lib/geokit/inflector.rb +39 -0
  26. data/lib/geokit/lat_lng.rb +112 -0
  27. data/lib/geokit/mappable.rb +210 -0
  28. data/lib/geokit/too_many_queries_error.rb +4 -0
  29. data/lib/geokit/version.rb +3 -0
  30. data/test/test_base_geocoder.rb +58 -0
  31. data/test/test_bounds.rb +97 -0
  32. data/test/test_ca_geocoder.rb +39 -0
  33. data/test/test_geoloc.rb +72 -0
  34. data/test/test_geoplugin_geocoder.rb +58 -0
  35. data/test/test_google_geocoder.rb +225 -0
  36. data/test/test_google_premier_geocoder.rb +88 -0
  37. data/test/test_google_reverse_geocoder.rb +47 -0
  38. data/test/test_inflector.rb +24 -0
  39. data/test/test_ipgeocoder.rb +109 -0
  40. data/test/test_latlng.rb +209 -0
  41. data/test/test_multi_geocoder.rb +91 -0
  42. data/test/test_multi_ip_geocoder.rb +36 -0
  43. data/test/test_us_geocoder.rb +54 -0
  44. data/test/test_yahoo_geocoder.rb +103 -0
  45. metadata +141 -0
@@ -0,0 +1,50 @@
1
+ module Geokit
2
+ module Geocoders
3
+ # Geocoder Us geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_US variable to
4
+ # contain true or false based upon whether authentication is to occur. Conforms to the
5
+ # interface set by the Geocoder class.
6
+ class UsGeocoder < Geocoder
7
+
8
+ private
9
+ def self.do_geocode(address, options = {})
10
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
11
+
12
+ query = (address_str =~ /^\d{5}(?:-\d{4})?$/ ? "zip" : "address") + "=#{Geokit::Inflector::url_escape(address_str)}"
13
+ url = if Geokit::Geocoders::geocoder_us
14
+ "http://#{Geokit::Geocoders::geocoder_us}@geocoder.us/member/service/csv/geocode"
15
+ else
16
+ "http://geocoder.us/service/csv/geocode"
17
+ end
18
+
19
+ url = "#{url}?#{query}"
20
+ res = self.call_geocoder_service(url)
21
+
22
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
23
+ data = res.body
24
+ logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}"
25
+ array = data.chomp.split(',')
26
+
27
+ if array.length == 5
28
+ res=GeoLoc.new
29
+ res.lat,res.lng,res.city,res.state,res.zip=array
30
+ res.country_code='US'
31
+ res.success=true
32
+ return res
33
+ elsif array.length == 6
34
+ res=GeoLoc.new
35
+ res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array
36
+ res.country_code='US'
37
+ res.success=true
38
+ return res
39
+ else
40
+ logger.info "geocoder.us was unable to geocode address: "+address
41
+ return GeoLoc.new
42
+ end
43
+ rescue
44
+ logger.error "Caught an error during geocoder.us geocoding call: "+$!
45
+ return GeoLoc.new
46
+
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,49 @@
1
+ module Geokit
2
+ module Geocoders
3
+ # Yahoo geocoder implementation. Requires the Geokit::Geocoders::YAHOO variable to
4
+ # contain a Yahoo API key. Conforms to the interface set by the Geocoder class.
5
+ class YahooGeocoder < Geocoder
6
+
7
+ private
8
+
9
+ # Template method which does the geocode lookup.
10
+ def self.do_geocode(address, options = {})
11
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
12
+ url="http://api.local.yahoo.com/MapsService/V1/geocode?appid=#{Geokit::Geocoders::yahoo}&location=#{Geokit::Inflector::url_escape(address_str)}"
13
+ res = self.call_geocoder_service(url)
14
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
15
+ xml = res.body
16
+ doc = REXML::Document.new(xml)
17
+ logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}"
18
+
19
+ if doc.elements['//ResultSet']
20
+ res=GeoLoc.new
21
+
22
+ #basic
23
+ res.lat=doc.elements['//Latitude'].text
24
+ res.lng=doc.elements['//Longitude'].text
25
+ res.country_code=doc.elements['//Country'].text
26
+ res.provider='yahoo'
27
+
28
+ #extended - false if not available
29
+ res.city=doc.elements['//City'].text if doc.elements['//City'] && doc.elements['//City'].text != nil
30
+ res.state=doc.elements['//State'].text if doc.elements['//State'] && doc.elements['//State'].text != nil
31
+ res.zip=doc.elements['//Zip'].text if doc.elements['//Zip'] && doc.elements['//Zip'].text != nil
32
+ res.street_address=doc.elements['//Address'].text if doc.elements['//Address'] && doc.elements['//Address'].text != nil
33
+ res.precision=doc.elements['//Result'].attributes['precision'] if doc.elements['//Result']
34
+ # set the accuracy as google does (added by Andruby)
35
+ res.accuracy=%w{unknown country state state city zip zip+4 street address building}.index(res.precision)
36
+ res.success=true
37
+ return res
38
+ else
39
+ logger.info "Yahoo was unable to geocode address: "+address
40
+ return GeoLoc.new
41
+ end
42
+
43
+ rescue
44
+ logger.info "Caught an error during Yahoo geocoding call: "+$!
45
+ return GeoLoc.new
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,39 @@
1
+ module Geokit
2
+ module Inflector
3
+
4
+ extend self
5
+
6
+ def titleize(word)
7
+ humanize(underscore(word)).gsub(/\b([a-z])/u) { $1.capitalize }
8
+ end
9
+
10
+ def underscore(camel_cased_word)
11
+ camel_cased_word.to_s.gsub(/::/, '/').
12
+ gsub(/([A-Z]+)([A-Z][a-z])/u,'\1_\2').
13
+ gsub(/([a-z\d])([A-Z])/u,'\1_\2').
14
+ tr("-", "_").
15
+ downcase
16
+ end
17
+
18
+ def humanize(lower_case_and_underscored_word)
19
+ lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
20
+ end
21
+
22
+ def snake_case(s)
23
+ return s.downcase if s =~ /^[A-Z]+$/u
24
+ s.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/u, '_\&') =~ /_*(.*)/
25
+ return $+.downcase
26
+
27
+ end
28
+
29
+ def url_escape(s)
30
+ s.gsub(/([^ a-zA-Z0-9_.-]+)/nu) do
31
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
32
+ end.tr(' ', '+')
33
+ end
34
+
35
+ def camelize(str)
36
+ str.split('_').map {|w| w.capitalize}.join
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,112 @@
1
+ module Geokit
2
+ class LatLng
3
+ include Mappable
4
+
5
+ attr_accessor :lat, :lng
6
+
7
+ # Accepts latitude and longitude or instantiates an empty instance
8
+ # if lat and lng are not provided. Converted to floats if provided
9
+ def initialize(lat=nil, lng=nil)
10
+ lat = lat.to_f if lat && !lat.is_a?(Numeric)
11
+ lng = lng.to_f if lng && !lng.is_a?(Numeric)
12
+ @lat = lat
13
+ @lng = lng
14
+ end
15
+
16
+ # Latitude attribute setter; stored as a float.
17
+ def lat=(lat)
18
+ @lat = lat.to_f if lat
19
+ end
20
+
21
+ # Longitude attribute setter; stored as a float;
22
+ def lng=(lng)
23
+ @lng=lng.to_f if lng
24
+ end
25
+
26
+ # Returns the lat and lng attributes as a comma-separated string.
27
+ def ll
28
+ "#{lat},#{lng}"
29
+ end
30
+
31
+ #returns a string with comma-separated lat,lng values
32
+ def to_s
33
+ ll
34
+ end
35
+
36
+ #returns a two-element array
37
+ def to_a
38
+ [lat,lng]
39
+ end
40
+ # Returns true if the candidate object is logically equal. Logical equivalence
41
+ # is true if the lat and lng attributes are the same for both objects.
42
+ def ==(other)
43
+ other.is_a?(LatLng) ? self.lat == other.lat && self.lng == other.lng : false
44
+ end
45
+
46
+ def hash
47
+ lat.hash + lng.hash
48
+ end
49
+
50
+ def eql?(other)
51
+ self == other
52
+ end
53
+
54
+ # A *class* method to take anything which can be inferred as a point and generate
55
+ # a LatLng from it. You should use this anything you're not sure what the input is,
56
+ # and want to deal with it as a LatLng if at all possible. Can take:
57
+ # 1) two arguments (lat,lng)
58
+ # 2) a string in the format "37.1234,-129.1234" or "37.1234 -129.1234"
59
+ # 3) a string which can be geocoded on the fly
60
+ # 4) an array in the format [37.1234,-129.1234]
61
+ # 5) a LatLng or GeoLoc (which is just passed through as-is)
62
+ # 6) anything which acts_as_mappable -- a LatLng will be extracted from it
63
+ def self.normalize(thing,other=nil)
64
+ # if an 'other' thing is supplied, normalize the input by creating an array of two elements
65
+ thing=[thing,other] if other
66
+
67
+ if thing.is_a?(String)
68
+ thing.strip!
69
+ if match=thing.match(/(\-?\d+\.?\d*)[, ] ?(\-?\d+\.?\d*)$/)
70
+ return Geokit::LatLng.new(match[1],match[2])
71
+ else
72
+ res = Geokit::Geocoders::MultiGeocoder.geocode(thing)
73
+ return res if res.success?
74
+ raise Geokit::Geocoders::GeocodeError
75
+ end
76
+ elsif thing.is_a?(Array) && thing.size==2
77
+ return Geokit::LatLng.new(thing[0],thing[1])
78
+ elsif thing.is_a?(LatLng) # will also be true for GeoLocs
79
+ return thing
80
+ elsif thing.class.respond_to?(:acts_as_mappable) && thing.class.respond_to?(:distance_column_name)
81
+ return thing.to_lat_lng
82
+ end
83
+
84
+ raise ArgumentError.new("#{thing} (#{thing.class}) cannot be normalized to a LatLng. We tried interpreting it as an array, string, Mappable, etc., but no dice.")
85
+ end
86
+
87
+ # Reverse geocodes a LatLng object using the MultiGeocoder (default), or optionally
88
+ # using a geocoder of your choosing. Returns a new Geokit::GeoLoc object
89
+ #
90
+ # ==== Options
91
+ # * :using - Specifies the geocoder to use for reverse geocoding. Defaults to
92
+ # MultiGeocoder. Can be either the geocoder class (or any class that
93
+ # implements do_reverse_geocode for that matter), or the name of
94
+ # the class without the "Geocoder" part (e.g. :google)
95
+ #
96
+ # ==== Examples
97
+ # LatLng.new(51.4578329, 7.0166848).reverse_geocode # => #<Geokit::GeoLoc:0x12dac20 @state...>
98
+ # LatLng.new(51.4578329, 7.0166848).reverse_geocode(:using => :google) # => #<Geokit::GeoLoc:0x12dac20 @state...>
99
+ # LatLng.new(51.4578329, 7.0166848).reverse_geocode(:using => Geokit::Geocoders::GoogleGeocoder) # => #<Geokit::GeoLoc:0x12dac20 @state...>
100
+ def reverse_geocode(options = { :using => Geokit::Geocoders::MultiGeocoder })
101
+ if options[:using].is_a?(String) or options[:using].is_a?(Symbol)
102
+ provider = Geokit::Geocoders.const_get("#{Geokit::Inflector::camelize(options[:using].to_s)}Geocoder")
103
+ elsif options[:using].respond_to?(:do_reverse_geocode)
104
+ provider = options[:using]
105
+ else
106
+ raise ArgumentError.new("#{options[:using]} is not a valid geocoder.")
107
+ end
108
+
109
+ provider.send(:reverse_geocode, self)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,210 @@
1
+ #require 'forwardable'
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, :kms, and :nms.
14
+ module Mappable
15
+ PI_DIV_RAD = 0.0174
16
+ KMS_PER_MILE = 1.609
17
+ NMS_PER_MILE = 0.868976242
18
+ EARTH_RADIUS_IN_MILES = 3963.19
19
+ EARTH_RADIUS_IN_KMS = EARTH_RADIUS_IN_MILES * KMS_PER_MILE
20
+ EARTH_RADIUS_IN_NMS = EARTH_RADIUS_IN_MILES * NMS_PER_MILE
21
+ MILES_PER_LATITUDE_DEGREE = 69.1
22
+ KMS_PER_LATITUDE_DEGREE = MILES_PER_LATITUDE_DEGREE * KMS_PER_MILE
23
+ NMS_PER_LATITUDE_DEGREE = MILES_PER_LATITUDE_DEGREE * NMS_PER_MILE
24
+ LATITUDE_DEGREES = EARTH_RADIUS_IN_MILES / MILES_PER_LATITUDE_DEGREE
25
+
26
+ # Mix below class methods into the includer.
27
+ def self.included(receiver) # :nodoc:
28
+ receiver.extend ClassMethods
29
+ end
30
+
31
+ module ClassMethods #:nodoc:
32
+ # Returns the distance between two points. The from and to parameters are
33
+ # required to have lat and lng attributes. Valid options are:
34
+ # :units - valid values are :miles, :kms, :nms (Geokit::default_units is the default)
35
+ # :formula - valid values are :flat or :sphere (Geokit::default_formula is the default)
36
+ def distance_between(from, to, options={})
37
+ from=Geokit::LatLng.normalize(from)
38
+ to=Geokit::LatLng.normalize(to)
39
+ return 0.0 if from == to # fixes a "zero-distance" bug
40
+ units = options[:units] || Geokit::default_units
41
+ formula = options[:formula] || Geokit::default_formula
42
+ case formula
43
+ when :sphere
44
+ begin
45
+ units_sphere_multiplier(units) *
46
+ Math.acos( Math.sin(deg2rad(from.lat)) * Math.sin(deg2rad(to.lat)) +
47
+ Math.cos(deg2rad(from.lat)) * Math.cos(deg2rad(to.lat)) *
48
+ Math.cos(deg2rad(to.lng) - deg2rad(from.lng)))
49
+ rescue Errno::EDOM
50
+ 0.0
51
+ end
52
+ when :flat
53
+ Math.sqrt((units_per_latitude_degree(units)*(from.lat-to.lat))**2 +
54
+ (units_per_longitude_degree(from.lat, units)*(from.lng-to.lng))**2)
55
+ end
56
+ end
57
+
58
+ # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
59
+ # from the first point to the second point. Typicaly, the instance methods will be used
60
+ # instead of this method.
61
+ def heading_between(from,to)
62
+ from=Geokit::LatLng.normalize(from)
63
+ to=Geokit::LatLng.normalize(to)
64
+
65
+ d_lng=deg2rad(to.lng-from.lng)
66
+ from_lat=deg2rad(from.lat)
67
+ to_lat=deg2rad(to.lat)
68
+ y=Math.sin(d_lng) * Math.cos(to_lat)
69
+ x=Math.cos(from_lat)*Math.sin(to_lat)-Math.sin(from_lat)*Math.cos(to_lat)*Math.cos(d_lng)
70
+ heading=to_heading(Math.atan2(y,x))
71
+ end
72
+
73
+ # Given a start point, distance, and heading (in degrees), provides
74
+ # an endpoint. Returns a LatLng instance. Typically, the instance method
75
+ # will be used instead of this method.
76
+ def endpoint(start,heading, distance, options={})
77
+ units = options[:units] || Geokit::default_units
78
+ radius = case units
79
+ when :kms; EARTH_RADIUS_IN_KMS
80
+ when :nms; EARTH_RADIUS_IN_NMS
81
+ else EARTH_RADIUS_IN_MILES
82
+ end
83
+ start=Geokit::LatLng.normalize(start)
84
+ lat=deg2rad(start.lat)
85
+ lng=deg2rad(start.lng)
86
+ heading=deg2rad(heading)
87
+ distance=distance.to_f
88
+
89
+ end_lat=Math.asin(Math.sin(lat)*Math.cos(distance/radius) +
90
+ Math.cos(lat)*Math.sin(distance/radius)*Math.cos(heading))
91
+
92
+ end_lng=lng+Math.atan2(Math.sin(heading)*Math.sin(distance/radius)*Math.cos(lat),
93
+ Math.cos(distance/radius)-Math.sin(lat)*Math.sin(end_lat))
94
+
95
+ LatLng.new(rad2deg(end_lat),rad2deg(end_lng))
96
+ end
97
+
98
+ # Returns the midpoint, given two points. Returns a LatLng.
99
+ # Typically, the instance method will be used instead of this method.
100
+ # Valid option:
101
+ # :units - valid values are :miles, :kms, or :nms (:miles is the default)
102
+ def midpoint_between(from,to,options={})
103
+ from=Geokit::LatLng.normalize(from)
104
+
105
+ units = options[:units] || Geokit::default_units
106
+
107
+ heading=from.heading_to(to)
108
+ distance=from.distance_to(to,options)
109
+ midpoint=from.endpoint(heading,distance/2,options)
110
+ end
111
+
112
+ # Geocodes a location using the multi geocoder.
113
+ def geocode(location, options = {})
114
+ res = Geocoders::MultiGeocoder.geocode(location, options)
115
+ return res if res.success?
116
+ raise Geokit::Geocoders::GeocodeError
117
+ end
118
+
119
+ protected
120
+
121
+ def deg2rad(degrees)
122
+ degrees.to_f / 180.0 * Math::PI
123
+ end
124
+
125
+ def rad2deg(rad)
126
+ rad.to_f * 180.0 / Math::PI
127
+ end
128
+
129
+ def to_heading(rad)
130
+ (rad2deg(rad)+360)%360
131
+ end
132
+
133
+ # Returns the multiplier used to obtain the correct distance units.
134
+ def units_sphere_multiplier(units)
135
+ case units
136
+ when :kms; EARTH_RADIUS_IN_KMS
137
+ when :nms; EARTH_RADIUS_IN_NMS
138
+ else EARTH_RADIUS_IN_MILES
139
+ end
140
+ end
141
+
142
+ # Returns the number of units per latitude degree.
143
+ def units_per_latitude_degree(units)
144
+ case units
145
+ when :kms; KMS_PER_LATITUDE_DEGREE
146
+ when :nms; NMS_PER_LATITUDE_DEGREE
147
+ else MILES_PER_LATITUDE_DEGREE
148
+ end
149
+ end
150
+
151
+ # Returns the number units per longitude degree.
152
+ def units_per_longitude_degree(lat, units)
153
+ miles_per_longitude_degree = (LATITUDE_DEGREES * Math.cos(lat * PI_DIV_RAD)).abs
154
+ case units
155
+ when :kms; miles_per_longitude_degree * KMS_PER_MILE
156
+ when :nms; miles_per_longitude_degree * NMS_PER_MILE
157
+ else miles_per_longitude_degree
158
+ end
159
+ end
160
+ end
161
+
162
+ # -----------------------------------------------------------------------------------------------
163
+ # Instance methods below here
164
+ # -----------------------------------------------------------------------------------------------
165
+
166
+ # Extracts a LatLng instance. Use with models that are acts_as_mappable
167
+ def to_lat_lng
168
+ return self if instance_of?(Geokit::LatLng) || instance_of?(Geokit::GeoLoc)
169
+ return LatLng.new(send(self.class.lat_column_name),send(self.class.lng_column_name)) if self.class.respond_to?(:acts_as_mappable)
170
+ nil
171
+ end
172
+
173
+ # Returns the distance from another point. The other point parameter is
174
+ # required to have lat and lng attributes. Valid options are:
175
+ # :units - valid values are :miles, :kms, :or :nms (:miles is the default)
176
+ # :formula - valid values are :flat or :sphere (:sphere is the default)
177
+ def distance_to(other, options={})
178
+ self.class.distance_between(self, other, options)
179
+ end
180
+ alias distance_from distance_to
181
+
182
+ # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
183
+ # to the given point. The given point can be a LatLng or a string to be Geocoded
184
+ def heading_to(other)
185
+ self.class.heading_between(self,other)
186
+ end
187
+
188
+ # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
189
+ # FROM the given point. The given point can be a LatLng or a string to be Geocoded
190
+ def heading_from(other)
191
+ self.class.heading_between(other,self)
192
+ end
193
+
194
+ # Returns the endpoint, given a heading (in degrees) and distance.
195
+ # Valid option:
196
+ # :units - valid values are :miles, :kms, or :nms (:miles is the default)
197
+ def endpoint(heading,distance,options={})
198
+ self.class.endpoint(self,heading,distance,options)
199
+ end
200
+
201
+ # Returns the midpoint, given another point on the map.
202
+ # Valid option:
203
+ # :units - valid values are :miles, :kms, or :nms (:miles is the default)
204
+ def midpoint_to(other, options={})
205
+ self.class.midpoint_between(self,other,options)
206
+ end
207
+
208
+ end
209
+
210
+ end