abuiles-geokit 1.6.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.
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