geokit 1.7.1 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
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