geokit 1.7.1 → 1.8.0

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 (78) hide show
  1. checksums.yaml +6 -14
  2. data/.travis.yml +1 -0
  3. data/CHANGELOG.md +11 -0
  4. data/Gemfile +2 -1
  5. data/MIT-LICENSE +20 -0
  6. data/README.markdown +44 -39
  7. data/Rakefile +15 -0
  8. data/fixtures/vcr_cassettes/bing_full.yml +102 -0
  9. data/fixtures/vcr_cassettes/bing_full_au.yml +91 -0
  10. data/fixtures/vcr_cassettes/bing_full_de.yml +91 -0
  11. data/fixtures/vcr_cassettes/fcc_reverse_geocode.yml +37 -0
  12. data/fixtures/vcr_cassettes/free_geo_ip_geocode.yml +36 -0
  13. data/fixtures/vcr_cassettes/geo_plugin_geocode.yml +38 -0
  14. data/fixtures/vcr_cassettes/geonames_geocode.yml +304 -0
  15. data/fixtures/vcr_cassettes/{google3_city.yml → google_city.yml} +0 -0
  16. data/fixtures/vcr_cassettes/{google3_country_code_biased_result.yml → google_country_code_biased_result.yml} +0 -0
  17. data/fixtures/vcr_cassettes/{google3_full.yml → google_full.yml} +0 -0
  18. data/fixtures/vcr_cassettes/{google3_full_short.yml → google_full_short.yml} +0 -0
  19. data/fixtures/vcr_cassettes/{google3_language_response_fr.yml → google_language_response_fr.yml} +0 -0
  20. data/fixtures/vcr_cassettes/{google3_multi.yml → google_multi.yml} +0 -0
  21. data/fixtures/vcr_cassettes/{google3_reverse_madrid.yml → google_reverse_madrid.yml} +0 -0
  22. data/fixtures/vcr_cassettes/ripe_geocode.yml +66 -0
  23. data/fixtures/vcr_cassettes/ripe_geocode_au.yml +66 -0
  24. data/geokit.gemspec +1 -1
  25. data/lib/geokit.rb +5 -0
  26. data/lib/geokit/bounds.rb +96 -0
  27. data/lib/geokit/core_ext.rb +17 -0
  28. data/lib/geokit/geo_loc.rb +134 -0
  29. data/lib/geokit/geocoders.rb +48 -35
  30. data/lib/geokit/geocoders/base_ip.rb +43 -0
  31. data/lib/geokit/geocoders/bing.rb +101 -0
  32. data/lib/geokit/geocoders/ca_geocoder.rb +50 -0
  33. data/lib/geokit/{services → geocoders}/fcc.rb +17 -20
  34. data/lib/geokit/geocoders/free_geo_ip.rb +34 -0
  35. data/lib/geokit/geocoders/geo_plugin.rb +33 -0
  36. data/lib/geokit/geocoders/geonames.rb +53 -0
  37. data/lib/geokit/{services/google3.rb → geocoders/google.rb} +59 -57
  38. data/lib/geokit/geocoders/ip.rb +69 -0
  39. data/lib/geokit/geocoders/mapquest.rb +72 -0
  40. data/lib/geokit/geocoders/maxmind.rb +29 -0
  41. data/lib/geokit/geocoders/openstreetmap.rb +119 -0
  42. data/lib/geokit/geocoders/ripe.rb +41 -0
  43. data/lib/geokit/{services → geocoders}/us_geocoder.rb +15 -20
  44. data/lib/geokit/{services → geocoders}/yahoo.rb +52 -55
  45. data/lib/geokit/geocoders/yandex.rb +61 -0
  46. data/lib/geokit/inflectors.rb +1 -2
  47. data/lib/geokit/lat_lng.rb +129 -0
  48. data/lib/geokit/mappable.rb +41 -424
  49. data/lib/geokit/multi_geocoder.rb +6 -2
  50. data/lib/geokit/polygon.rb +46 -0
  51. data/lib/geokit/version.rb +1 -1
  52. data/test/helper.rb +2 -12
  53. data/test/test_base_geocoder.rb +0 -10
  54. data/test/test_bing_geocoder.rb +60 -0
  55. data/test/test_fcc_geocoder.rb +23 -0
  56. data/test/test_free_geo_ip_geocoder.rb +23 -0
  57. data/test/test_geo_plugin_geocoder.rb +23 -0
  58. data/test/test_geonames_geocoder.rb +23 -0
  59. data/test/test_google_geocoder.rb +208 -235
  60. data/test/test_maxmind_geocoder.rb +35 -4
  61. data/test/test_multi_geocoder.rb +3 -1
  62. data/test/test_ripe_geocoder.rb +35 -0
  63. data/test/test_yahoo_geocoder.rb +0 -12
  64. metadata +78 -52
  65. data/LICENSE +0 -25
  66. data/Manifest.txt +0 -21
  67. data/data/GeoLiteCity.dat +0 -0
  68. data/lib/geokit/services/ca_geocoder.rb +0 -55
  69. data/lib/geokit/services/geo_plugin.rb +0 -31
  70. data/lib/geokit/services/geonames.rb +0 -53
  71. data/lib/geokit/services/google.rb +0 -158
  72. data/lib/geokit/services/ip.rb +0 -103
  73. data/lib/geokit/services/maxmind.rb +0 -39
  74. data/lib/geokit/services/openstreetmap.rb +0 -119
  75. data/lib/geokit/services/ripe.rb +0 -32
  76. data/lib/geokit/services/yandex.rb +0 -51
  77. data/test/test_google_geocoder3.rb +0 -238
  78. data/test/test_google_reverse_geocoder.rb +0 -49
@@ -0,0 +1,61 @@
1
+ module Geokit
2
+ module Geocoders
3
+ # Yandex geocoder implementation. Expects the Geokit::Geocoders::YANDEX variable to
4
+ # contain a Yandex API key (optional). Conforms to the interface set by the Geocoder class.
5
+ class YandexGeocoder < Geocoder
6
+ private
7
+
8
+ # Template method which does the geocode lookup.
9
+ def self.do_geocode(address)
10
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
11
+ url = submit_url(address_str)
12
+ res = call_geocoder_service(url)
13
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
14
+ parse :json, res.body
15
+ end
16
+
17
+ def self.submit_url(address_str)
18
+ url = "http://geocode-maps.yandex.ru/1.x/?geocode=#{Geokit::Inflector::url_escape(address_str)}&format=json"
19
+ url += "&key=#{Geokit::Geocoders::yandex}" if Geokit::Geocoders::yandex
20
+ url
21
+ end
22
+
23
+ def self.parse_json(result)
24
+ loc = GeoLoc.new
25
+
26
+ coll = result["response"]["GeoObjectCollection"]
27
+ return loc unless coll["metaDataProperty"]["GeocoderResponseMetaData"]["found"].to_i > 0
28
+
29
+ l = coll["featureMember"][0]["GeoObject"]
30
+
31
+ loc.success = true
32
+ loc.provider = "yandex"
33
+ loc.lng = l["Point"]["pos"].split(" ").first
34
+ loc.lat = l["Point"]["pos"].split(" ").last
35
+
36
+ country = l["metaDataProperty"]["GeocoderMetaData"]["AddressDetails"]["Country"]
37
+ locality = country["Locality"] || country["AdministrativeArea"]["Locality"] || country["AdministrativeArea"]["SubAdministrativeArea"]["Locality"] rescue nil
38
+ set_address_components(loc, l, country, locality)
39
+ set_precision(loc, l, locality)
40
+
41
+ loc
42
+ end
43
+
44
+ def self.set_address_components(loc, l, country, locality)
45
+ loc.country_code = country["CountryNameCode"]
46
+ loc.full_address = country["AddressLine"]
47
+ loc.street_address = l["name"]
48
+ loc.street_number = locality["Thoroughfare"]["Premise"]["PremiseNumber"] rescue nil
49
+ loc.street_name = locality["Thoroughfare"]["ThoroughfareName"] rescue nil
50
+ loc.city = locality["LocalityName"] rescue nil
51
+ loc.state = country["AdministrativeArea"]["AdministrativeAreaName"] rescue nil
52
+ loc.state ||= country["Locality"]["LocalityName"] rescue nil
53
+ end
54
+
55
+ def self.set_precision(loc, l, locality)
56
+ loc.precision = l["metaDataProperty"]["GeocoderMetaData"]["precision"].sub(/exact/, "building").sub(/number|near/, "address").sub(/other/, "city")
57
+ loc.precision = "country" unless locality
58
+ end
59
+ end
60
+ end
61
+ end
@@ -23,8 +23,7 @@ module Geokit
23
23
  def snake_case(s)
24
24
  return s.downcase if s =~ /^[A-Z]+$/u
25
25
  s.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/u, '_\&') =~ /_*(.*)/
26
- return $+.downcase
27
-
26
+ $+.downcase
28
27
  end
29
28
 
30
29
  def url_escape(s)
@@ -0,0 +1,129 @@
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
+ def self.from_json(json)
17
+ new(json['lat'], json['lng'])
18
+ end
19
+
20
+ # Latitude attribute setter; stored as a float.
21
+ def lat=(lat)
22
+ @lat = lat.to_f if lat
23
+ end
24
+
25
+ # Longitude attribute setter; stored as a float;
26
+ def lng=(lng)
27
+ @lng=lng.to_f if lng
28
+ end
29
+
30
+ # Returns the lat and lng attributes as a comma-separated string.
31
+ def ll
32
+ "#{lat},#{lng}"
33
+ end
34
+
35
+ #returns a string with comma-separated lat,lng values
36
+ def to_s
37
+ ll
38
+ end
39
+
40
+ #returns a two-element array
41
+ def to_a
42
+ [lat,lng]
43
+ end
44
+ # Returns true if the candidate object is logically equal. Logical equivalence
45
+ # is true if the lat and lng attributes are the same for both objects.
46
+ def ==(other)
47
+ return false unless other.is_a?(LatLng)
48
+ lat == other.lat && lng == other.lng
49
+ end
50
+
51
+ def hash
52
+ lat.hash + lng.hash
53
+ end
54
+
55
+ def eql?(other)
56
+ self == other
57
+ end
58
+
59
+ # Returns true if both lat and lng attributes are defined
60
+ def valid?
61
+ lat && lng
62
+ end
63
+
64
+ # A *class* method to take anything which can be inferred as a point and generate
65
+ # a LatLng from it. You should use this anything you're not sure what the input is,
66
+ # and want to deal with it as a LatLng if at all possible. Can take:
67
+ # 1) two arguments (lat,lng)
68
+ # 2) a string in the format "37.1234,-129.1234" or "37.1234 -129.1234"
69
+ # 3) a string which can be geocoded on the fly
70
+ # 4) an array in the format [37.1234,-129.1234]
71
+ # 5) a LatLng or GeoLoc (which is just passed through as-is)
72
+ # 6) anything responding to to_lat_lng -- a LatLng will be extracted from it
73
+ def self.normalize(thing,other=nil)
74
+ return Geokit::LatLng.new(thing, other) if other
75
+
76
+ case thing
77
+ when String
78
+ from_string(thing)
79
+ when Array
80
+ thing.size == 2 or raise ArgumentError.new("Must initialize with an Array with both latitude and longitude")
81
+ Geokit::LatLng.new(thing[0],thing[1])
82
+ when LatLng # will also be true for GeoLocs
83
+ thing
84
+ else
85
+ if thing.respond_to? :to_lat_lng
86
+ thing.to_lat_lng
87
+ else
88
+ raise ArgumentError.new("#{thing} (#{thing.class}) cannot be normalized to a LatLng. We tried interpreting it as an array, string, etc., but no dice.")
89
+ end
90
+ end
91
+ end
92
+
93
+ def self.from_string(thing)
94
+ thing.strip!
95
+ if match=thing.match(/(\-?\d+\.?\d*)[, ] ?(\-?\d+\.?\d*)$/)
96
+ Geokit::LatLng.new(match[1],match[2])
97
+ else
98
+ res = Geokit::Geocoders::MultiGeocoder.geocode(thing)
99
+ return res if res.success?
100
+ raise Geokit::Geocoders::GeocodeError
101
+ end
102
+ end
103
+
104
+ # Reverse geocodes a LatLng object using the MultiGeocoder (default), or optionally
105
+ # using a geocoder of your choosing. Returns a new Geokit::GeoLoc object
106
+ #
107
+ # ==== Options
108
+ # * :using - Specifies the geocoder to use for reverse geocoding. Defaults to
109
+ # MultiGeocoder. Can be either the geocoder class (or any class that
110
+ # implements do_reverse_geocode for that matter), or the name of
111
+ # the class without the "Geocoder" part (e.g. :google)
112
+ #
113
+ # ==== Examples
114
+ # LatLng.new(51.4578329, 7.0166848).reverse_geocode # => #<Geokit::GeoLoc:0x12dac20 @state...>
115
+ # LatLng.new(51.4578329, 7.0166848).reverse_geocode(:using => :google) # => #<Geokit::GeoLoc:0x12dac20 @state...>
116
+ # LatLng.new(51.4578329, 7.0166848).reverse_geocode(:using => Geokit::Geocoders::GoogleGeocoder) # => #<Geokit::GeoLoc:0x12dac20 @state...>
117
+ def reverse_geocode(options = { :using => Geokit::Geocoders::MultiGeocoder })
118
+ if options[:using].is_a?(String) || options[:using].is_a?(Symbol)
119
+ provider = Geokit::Geocoders.const_get("#{Geokit::Inflector::camelize(options[:using].to_s)}Geocoder")
120
+ elsif options[:using].respond_to?(:do_reverse_geocode)
121
+ provider = options[:using]
122
+ else
123
+ raise ArgumentError.new("#{options[:using]} is not a valid geocoder.")
124
+ end
125
+
126
+ provider.send(:reverse_geocode, self)
127
+ end
128
+ end
129
+ end
@@ -40,27 +40,34 @@ module Geokit
40
40
  units = options[:units] || Geokit::default_units
41
41
  formula = options[:formula] || Geokit::default_formula
42
42
  case formula
43
- when :sphere
44
- error_classes = [Errno::EDOM]
45
-
46
- # Ruby 1.9 raises {Math::DomainError}, but it is not defined in Ruby
47
- # 1.8. Backwards-compatibly rescue both errors.
48
- error_classes << Math::DomainError if defined?(Math::DomainError)
49
-
50
- begin
51
- units_sphere_multiplier(units) *
52
- Math.acos( Math.sin(deg2rad(from.lat)) * Math.sin(deg2rad(to.lat)) +
53
- Math.cos(deg2rad(from.lat)) * Math.cos(deg2rad(to.lat)) *
54
- Math.cos(deg2rad(to.lng) - deg2rad(from.lng)))
55
- rescue *error_classes
56
- 0.0
57
- end
58
- when :flat
59
- Math.sqrt((units_per_latitude_degree(units)*(from.lat-to.lat))**2 +
60
- (units_per_longitude_degree(from.lat, units)*(from.lng-to.lng))**2)
43
+ when :sphere then distance_between_sphere(from, to, units)
44
+ when :flat then distance_between_flat(from, to, units)
61
45
  end
62
46
  end
63
47
 
48
+ def distance_between_sphere(from, to, units)
49
+ lat_sin = Math.sin(deg2rad(from.lat)) * Math.sin(deg2rad(to.lat))
50
+ lat_cos = Math.cos(deg2rad(from.lat)) * Math.cos(deg2rad(to.lat))
51
+ lng_cos = Math.cos(deg2rad(to.lng) - deg2rad(from.lng))
52
+ units_sphere_multiplier(units) * Math.acos(lat_sin + lat_cos * lng_cos)
53
+ rescue *math_error_classes
54
+ 0.0
55
+ end
56
+
57
+ def distance_between_flat(from, to, units)
58
+ lat_length = units_per_latitude_degree(units) * (from.lat - to.lat)
59
+ lng_length = units_per_longitude_degree(from.lat, units) * (from.lng - to.lng)
60
+ Math.sqrt(lat_length ** 2 + lng_length ** 2)
61
+ end
62
+
63
+ def math_error_classes
64
+ error_classes = [Errno::EDOM]
65
+
66
+ # Ruby 1.9 raises {Math::DomainError}, but it is not defined in Ruby
67
+ # 1.8. Backwards-compatibly rescue both errors.
68
+ error_classes << Math::DomainError if defined?(Math::DomainError)
69
+ end
70
+
64
71
  # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
65
72
  # from the first point to the second point. Typicaly, the instance methods will be used
66
73
  # instead of this method.
@@ -80,23 +87,23 @@ module Geokit
80
87
  # an endpoint. Returns a LatLng instance. Typically, the instance method
81
88
  # will be used instead of this method.
82
89
  def endpoint(start,heading, distance, options={})
83
- units = options[:units] || Geokit::default_units
84
- radius = case units
85
- when :kms; EARTH_RADIUS_IN_KMS
86
- when :nms; EARTH_RADIUS_IN_NMS
87
- else EARTH_RADIUS_IN_MILES
88
- end
89
- start=Geokit::LatLng.normalize(start)
90
- lat=deg2rad(start.lat)
91
- lng=deg2rad(start.lng)
92
- heading=deg2rad(heading)
93
- distance=distance.to_f
90
+ units = options[:units] || Geokit::default_units
91
+ ratio = distance.to_f / units_sphere_multiplier(units)
92
+ start = Geokit::LatLng.normalize(start)
93
+ lat = deg2rad(start.lat)
94
+ lng = deg2rad(start.lng)
95
+ heading = deg2rad(heading)
96
+
97
+ sin_ratio = Math.sin(ratio)
98
+ cos_ratio = Math.cos(ratio)
99
+ sin_lat = Math.sin(lat)
100
+ cos_lat = Math.cos(lat)
94
101
 
95
- end_lat=Math.asin(Math.sin(lat)*Math.cos(distance/radius) +
96
- Math.cos(lat)*Math.sin(distance/radius)*Math.cos(heading))
102
+ end_lat = Math.asin(sin_lat * cos_ratio +
103
+ cos_lat * sin_ratio * Math.cos(heading))
97
104
 
98
- end_lng=lng+Math.atan2(Math.sin(heading)*Math.sin(distance/radius)*Math.cos(lat),
99
- Math.cos(distance/radius)-Math.sin(lat)*Math.sin(end_lat))
105
+ end_lng = lng + Math.atan2(Math.sin(heading) * sin_ratio * cos_lat,
106
+ cos_ratio - sin_lat * Math.sin(end_lat))
100
107
 
101
108
  LatLng.new(rad2deg(end_lat),rad2deg(end_lng))
102
109
  end
@@ -172,7 +179,7 @@ module Geokit
172
179
  # Extracts a LatLng instance. Use with models that are acts_as_mappable
173
180
  def to_lat_lng
174
181
  return self if instance_of?(Geokit::LatLng) || instance_of?(Geokit::GeoLoc)
175
- return LatLng.new(send(self.class.lat_column_name),send(self.class.lng_column_name)) if self.class.respond_to?(:acts_as_mappable)
182
+ return LatLng.new(send(self.class.lat_column_name), send(self.class.lng_column_name))
176
183
  nil
177
184
  end
178
185
 
@@ -213,394 +220,4 @@ module Geokit
213
220
 
214
221
  end
215
222
 
216
- class LatLng
217
- include Mappable
218
-
219
- attr_accessor :lat, :lng
220
-
221
- # Accepts latitude and longitude or instantiates an empty instance
222
- # if lat and lng are not provided. Converted to floats if provided
223
- def initialize(lat=nil, lng=nil)
224
- lat = lat.to_f if lat && !lat.is_a?(Numeric)
225
- lng = lng.to_f if lng && !lng.is_a?(Numeric)
226
- @lat = lat
227
- @lng = lng
228
- end
229
-
230
- # Latitude attribute setter; stored as a float.
231
- def lat=(lat)
232
- @lat = lat.to_f if lat
233
- end
234
-
235
- # Longitude attribute setter; stored as a float;
236
- def lng=(lng)
237
- @lng=lng.to_f if lng
238
- end
239
-
240
- # Returns the lat and lng attributes as a comma-separated string.
241
- def ll
242
- "#{lat},#{lng}"
243
- end
244
-
245
- #returns a string with comma-separated lat,lng values
246
- def to_s
247
- ll
248
- end
249
-
250
- #returns a two-element array
251
- def to_a
252
- [lat,lng]
253
- end
254
- # Returns true if the candidate object is logically equal. Logical equivalence
255
- # is true if the lat and lng attributes are the same for both objects.
256
- def ==(other)
257
- other.is_a?(LatLng) ? self.lat == other.lat && self.lng == other.lng : false
258
- end
259
-
260
- def hash
261
- lat.hash + lng.hash
262
- end
263
-
264
- def eql?(other)
265
- self == other
266
- end
267
-
268
- # Returns true if both lat and lng attributes are defined
269
- def valid?
270
- self.lat and self.lng
271
- end
272
-
273
- # A *class* method to take anything which can be inferred as a point and generate
274
- # a LatLng from it. You should use this anything you're not sure what the input is,
275
- # and want to deal with it as a LatLng if at all possible. Can take:
276
- # 1) two arguments (lat,lng)
277
- # 2) a string in the format "37.1234,-129.1234" or "37.1234 -129.1234"
278
- # 3) a string which can be geocoded on the fly
279
- # 4) an array in the format [37.1234,-129.1234]
280
- # 5) a LatLng or GeoLoc (which is just passed through as-is)
281
- # 6) anything which acts_as_mappable -- a LatLng will be extracted from it
282
- def self.normalize(thing,other=nil)
283
- # if an 'other' thing is supplied, normalize the input by creating an array of two elements
284
- thing=[thing,other] if other
285
-
286
- if thing.is_a?(String)
287
- thing.strip!
288
- if match=thing.match(/(\-?\d+\.?\d*)[, ] ?(\-?\d+\.?\d*)$/)
289
- return Geokit::LatLng.new(match[1],match[2])
290
- else
291
- res = Geokit::Geocoders::MultiGeocoder.geocode(thing)
292
- return res if res.success?
293
- raise Geokit::Geocoders::GeocodeError
294
- end
295
- elsif thing.is_a?(Array) && thing.size==2
296
- return Geokit::LatLng.new(thing[0],thing[1])
297
- elsif thing.is_a?(LatLng) # will also be true for GeoLocs
298
- return thing
299
- elsif thing.class.respond_to?(:acts_as_mappable) && thing.class.respond_to?(:distance_column_name)
300
- return thing.to_lat_lng
301
- elsif thing.respond_to? :to_lat_lng
302
- return thing.to_lat_lng
303
- end
304
-
305
- 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.")
306
- end
307
-
308
- # Reverse geocodes a LatLng object using the MultiGeocoder (default), or optionally
309
- # using a geocoder of your choosing. Returns a new Geokit::GeoLoc object
310
- #
311
- # ==== Options
312
- # * :using - Specifies the geocoder to use for reverse geocoding. Defaults to
313
- # MultiGeocoder. Can be either the geocoder class (or any class that
314
- # implements do_reverse_geocode for that matter), or the name of
315
- # the class without the "Geocoder" part (e.g. :google)
316
- #
317
- # ==== Examples
318
- # LatLng.new(51.4578329, 7.0166848).reverse_geocode # => #<Geokit::GeoLoc:0x12dac20 @state...>
319
- # LatLng.new(51.4578329, 7.0166848).reverse_geocode(:using => :google) # => #<Geokit::GeoLoc:0x12dac20 @state...>
320
- # LatLng.new(51.4578329, 7.0166848).reverse_geocode(:using => Geokit::Geocoders::GoogleGeocoder) # => #<Geokit::GeoLoc:0x12dac20 @state...>
321
- def reverse_geocode(options = { :using => Geokit::Geocoders::MultiGeocoder })
322
- if options[:using].is_a?(String) or options[:using].is_a?(Symbol)
323
- provider = Geokit::Geocoders.const_get("#{Geokit::Inflector::camelize(options[:using].to_s)}Geocoder")
324
- elsif options[:using].respond_to?(:do_reverse_geocode)
325
- provider = options[:using]
326
- else
327
- raise ArgumentError.new("#{options[:using]} is not a valid geocoder.")
328
- end
329
-
330
- provider.send(:reverse_geocode, self)
331
- end
332
- end
333
-
334
- # This class encapsulates the result of a geocoding call.
335
- # It's primary purpose is to homogenize the results of multiple
336
- # geocoding providers. It also provides some additional functionality, such as
337
- # the "full address" method for geocoders that do not provide a
338
- # full address in their results (for example, Yahoo), and the "is_us" method.
339
- #
340
- # Some geocoders can return multple results. Geoloc can capture multiple results through
341
- # its "all" method.
342
- #
343
- # For the geocoder setting the results, it would look something like this:
344
- # geo=GeoLoc.new(first_result)
345
- # geo.all.push(second_result)
346
- # geo.all.push(third_result)
347
- #
348
- # Then, for the user of the result:
349
- #
350
- # puts geo.full_address # just like usual
351
- # puts geo.all.size => 3 # there's three results total
352
- # puts geo.all.first # all is just an array or additional geolocs,
353
- # so do what you want with it
354
- class GeoLoc < LatLng
355
-
356
- # Location attributes. Full address is a concatenation of all values. For example:
357
- # 100 Spear St, San Francisco, CA, 94101, US
358
- # Street number and street name are extracted from the street address attribute if they don't exist
359
- attr_accessor :street_number, :street_name, :street_address, :city, :state, :zip, :country_code, :country
360
- attr_accessor :full_address, :all, :district, :province, :sub_premise, :neighborhood
361
- # Attributes set upon return from geocoding. Success will be true for successful
362
- # geocode lookups. The provider will be set to the name of the providing geocoder.
363
- # Finally, precision is an indicator of the accuracy of the geocoding.
364
- attr_accessor :success, :provider, :precision, :suggested_bounds
365
- # accuracy is set for Yahoo and Google geocoders, it is a numeric value of the
366
- # precision. see http://code.google.com/apis/maps/documentation/geocoding/#GeocodingAccuracy
367
- attr_accessor :accuracy
368
- # FCC Attributes
369
- attr_accessor :district_fips, :state_fips, :block_fips
370
-
371
-
372
- # Constructor expects a hash of symbols to correspond with attributes.
373
- def initialize(h={})
374
- @all = [self]
375
-
376
- @street_address=h[:street_address]
377
- @sub_premise=nil
378
- @street_number=nil
379
- @street_name=nil
380
- @city=h[:city]
381
- @state=h[:state]
382
- @zip=h[:zip]
383
- @country_code=h[:country_code]
384
- @province = h[:province]
385
- @success=false
386
- @precision='unknown'
387
- @full_address=nil
388
- super(h[:lat],h[:lng])
389
- end
390
-
391
- # Returns true if geocoded to the United States.
392
- def is_us?
393
- country_code == 'US'
394
- end
395
-
396
- def success?
397
- success == true
398
- end
399
-
400
- # full_address is provided by google but not by yahoo. It is intended that the google
401
- # geocoding method will provide the full address, whereas for yahoo it will be derived
402
- # from the parts of the address we do have.
403
- def full_address
404
- @full_address ? @full_address : to_geocodeable_s
405
- end
406
-
407
- # Extracts the street number from the street address where possible.
408
- def street_number
409
- @street_number ||= street_address[/(\d*)/] if street_address
410
- @street_number
411
- end
412
-
413
- # Returns the street name portion of the street address where possible
414
- def street_name
415
- @street_name||=street_address[street_number.length, street_address.length].strip if street_address
416
- @street_name
417
- end
418
-
419
- # gives you all the important fields as key-value pairs
420
- def hash
421
- res={}
422
- [:success, :lat, :lng, :country_code, :city, :state, :zip, :street_address, :province,
423
- :district, :provider, :full_address, :is_us?, :ll, :precision, :district_fips, :state_fips,
424
- :block_fips, :sub_premise].each { |s| res[s] = self.send(s.to_s) }
425
- res
426
- end
427
- alias to_hash hash
428
-
429
- # Sets the city after capitalizing each word within the city name.
430
- def city=(city)
431
- @city = Geokit::Inflector::titleize(city) if city
432
- end
433
-
434
- # Sets the street address after capitalizing each word within the street address.
435
- def street_address=(address)
436
- if address and not ['google','google3'].include?(self.provider)
437
- @street_address = Geokit::Inflector::titleize(address)
438
- else
439
- @street_address = address
440
- end
441
- end
442
-
443
- # Returns a comma-delimited string consisting of the street address, city, state,
444
- # zip, and country code. Only includes those attributes that are non-blank.
445
- def to_geocodeable_s
446
- a=[street_address, district, city, province, state, zip, country_code].compact
447
- a.delete_if { |e| !e || e == '' }
448
- a.join(', ')
449
- end
450
-
451
- def to_yaml_properties
452
- (instance_variables - ['@all', :@all]).sort
453
- end
454
-
455
- def encode_with(coder)
456
- to_yaml_properties.each do |name|
457
- coder[name[1..-1].to_s] = instance_variable_get(name.to_s)
458
- end
459
- end
460
-
461
- # Returns a string representation of the instance.
462
- def to_s
463
- "Provider: #{provider}\nStreet: #{street_address}\nCity: #{city}\nState: #{state}\nZip: #{zip}\nLatitude: #{lat}\nLongitude: #{lng}\nCountry: #{country_code}\nSuccess: #{success}"
464
- end
465
- end
466
-
467
- # Bounds represents a rectangular bounds, defined by the SW and NE corners
468
- class Bounds
469
- # sw and ne are LatLng objects
470
- attr_accessor :sw, :ne
471
-
472
- # provide sw and ne to instantiate a new Bounds instance
473
- def initialize(sw,ne)
474
- raise ArgumentError if !(sw.is_a?(Geokit::LatLng) && ne.is_a?(Geokit::LatLng))
475
- @sw,@ne=sw,ne
476
- end
477
-
478
- #returns the a single point which is the center of the rectangular bounds
479
- def center
480
- @sw.midpoint_to(@ne)
481
- end
482
-
483
- # a simple string representation:sw,ne
484
- def to_s
485
- "#{@sw.to_s},#{@ne.to_s}"
486
- end
487
-
488
- # a two-element array of two-element arrays: sw,ne
489
- def to_a
490
- [@sw.to_a, @ne.to_a]
491
- end
492
-
493
- # Returns true if the bounds contain the passed point.
494
- # allows for bounds which cross the meridian
495
- def contains?(point)
496
- point=Geokit::LatLng.normalize(point)
497
- res = point.lat > @sw.lat && point.lat < @ne.lat
498
- if crosses_meridian?
499
- res &= point.lng < @ne.lng || point.lng > @sw.lng
500
- else
501
- res &= point.lng < @ne.lng && point.lng > @sw.lng
502
- end
503
- res
504
- end
505
-
506
- # returns true if the bounds crosses the international dateline
507
- def crosses_meridian?
508
- @sw.lng > @ne.lng
509
- end
510
-
511
- # Returns true if the candidate object is logically equal. Logical equivalence
512
- # is true if the lat and lng attributes are the same for both objects.
513
- def ==(other)
514
- other.is_a?(Bounds) ? self.sw == other.sw && self.ne == other.ne : false
515
- end
516
-
517
- # Equivalent to Google Maps API's .toSpan() method on GLatLng's.
518
- #
519
- # Returns a LatLng object, whose coordinates represent the size of a rectangle
520
- # defined by these bounds.
521
- def to_span
522
- lat_span = (@ne.lat - @sw.lat).abs
523
- lng_span = (crosses_meridian? ? 360 + @ne.lng - @sw.lng : @ne.lng - @sw.lng).abs
524
- Geokit::LatLng.new(lat_span, lng_span)
525
- end
526
-
527
- class <<self
528
-
529
- # returns an instance of bounds which completely encompases the given circle
530
- def from_point_and_radius(point,radius,options={})
531
- point=LatLng.normalize(point)
532
- p0=point.endpoint(0,radius,options)
533
- p90=point.endpoint(90,radius,options)
534
- p180=point.endpoint(180,radius,options)
535
- p270=point.endpoint(270,radius,options)
536
- sw=Geokit::LatLng.new(p180.lat,p270.lng)
537
- ne=Geokit::LatLng.new(p0.lat,p90.lng)
538
- Geokit::Bounds.new(sw,ne)
539
- end
540
-
541
- # Takes two main combinations of arguments to create a bounds:
542
- # point,point (this is the only one which takes two arguments
543
- # [point,point]
544
- # . . . where a point is anything LatLng#normalize can handle (which is quite a lot)
545
- #
546
- # NOTE: everything combination is assumed to pass points in the order sw, ne
547
- def normalize (thing,other=nil)
548
- # maybe this will be simple -- an actual bounds object is passed, and we can all go home
549
- return thing if thing.is_a? Bounds
550
-
551
- # no? OK, if there's no "other," the thing better be a two-element array
552
- thing,other=thing if !other && thing.is_a?(Array) && thing.size==2
553
-
554
- # Now that we're set with a thing and another thing, let LatLng do the heavy lifting.
555
- # Exceptions may be thrown
556
- Bounds.new(Geokit::LatLng.normalize(thing),Geokit::LatLng.normalize(other))
557
- end
558
- end
559
- end
560
-
561
- # A complex polygon made of multiple points. End point must equal start point to close the poly.
562
- class Polygon
563
-
564
- attr_accessor :poly_y, :poly_x
565
-
566
- def initialize(points)
567
- # Pass in an array of Geokit::LatLng
568
- @poly_x = []
569
- @poly_y = []
570
-
571
- points.each do |point|
572
- @poly_x << point.lng
573
- @poly_y << point.lat
574
- end
575
-
576
- # A Polygon must be 'closed', the last point equal to the first point
577
- if not @poly_x[0] == @poly_x[-1] or not @poly_y[0] == @poly_y[-1]
578
- # Append the first point to the array to close the polygon
579
- @poly_x << @poly_x[0]
580
- @poly_y << @poly_y[0]
581
- end
582
-
583
- end
584
-
585
- def contains?(point)
586
- j = @poly_x.length - 1
587
- oddNodes = false
588
- x = point.lng
589
- y = point.lat
590
-
591
- for i in (0..j)
592
- if (@poly_y[i] < y && @poly_y[j] >= y ||
593
- @poly_y[j] < y && @poly_y[i] >= y)
594
- if (@poly_x[i] + (y - @poly_y[i]) / (@poly_y[j] - @poly_y[i]) * (@poly_x[j] - @poly_x[i]) < x)
595
- oddNodes = !oddNodes
596
- end
597
- end
598
-
599
- j=i
600
- end
601
-
602
- oddNodes
603
- end # contains?
604
- end # class Polygon
605
-
606
223
  end