rjaswal-geokit 1.5.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/Manifest.txt ADDED
@@ -0,0 +1,21 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.markdown
4
+ Rakefile
5
+ geokit.gemspec
6
+ lib/geokit.rb
7
+ lib/geokit/geocoders.rb
8
+ lib/geokit/mappable.rb
9
+ test/test_base_geocoder.rb
10
+ test/test_bounds.rb
11
+ test/test_ca_geocoder.rb
12
+ test/test_geoloc.rb
13
+ test/test_geoplugin_geocoder.rb
14
+ test/test_google_geocoder.rb
15
+ test/test_google_reverse_geocoder.rb
16
+ test/test_inflector.rb
17
+ test/test_ipgeocoder.rb
18
+ test/test_latlng.rb
19
+ test/test_multi_geocoder.rb
20
+ test/test_us_geocoder.rb
21
+ test/test_yahoo_geocoder.rb
data/README.markdown ADDED
@@ -0,0 +1,273 @@
1
+ ## GEOKIT GEM DESCRIPTION
2
+
3
+ The Geokit gem provides:
4
+
5
+ * Distance calculations between two points on the earth. Calculate the distance in miles, kilometers, or nautical miles, with all the trigonometry abstracted away by GeoKit.
6
+ * Geocoding from multiple providers. It supports Google, Yahoo, Geocoder.us, and Geocoder.ca geocoders, and others. It provides a uniform response structure from all of them.
7
+ It also provides a fail-over mechanism, in case your input fails to geocode in one service.
8
+ * Rectangular bounds calculations: is a point within a given rectangular bounds?
9
+ * Heading and midpoint calculations
10
+
11
+ Combine this gem with the [geokit-rails plugin](http://github.com/andre/geokit-rails/tree/master) to get location-based finders for your Rails app.
12
+
13
+ * Geokit Documentation at Rubyforge [http://geokit.rubyforge.org](http://geokit.rubyforge.org).
14
+ * Repository at Github: [http://github.com/andre/geokit-gem/tree/master](http://github.com/andre/geokit-gem/tree/master).
15
+ * Follow the Google Group for updates and discussion on Geokit: [http://groups.google.com/group/geokit](http://groups.google.com/group/geokit)
16
+
17
+ ## INSTALL
18
+
19
+ sudo gem install geokit
20
+
21
+ ## QUICK START
22
+
23
+ irb> require 'rubygems'
24
+ irb> require 'geokit'
25
+ irb> a=Geokit::Geocoders::YahooGeocoder.geocode '140 Market St, San Francisco, CA'
26
+ irb> a.ll
27
+ => 37.79363,-122.396116
28
+ irb> b=Geokit::Geocoders::YahooGeocoder.geocode '789 Geary St, San Francisco, CA'
29
+ irb> b.ll
30
+ => 37.786217,-122.41619
31
+ irb> a.distance_to(b)
32
+ => 1.21120007413626
33
+ irb> a.heading_to(b)
34
+ => 244.959832435678
35
+ irb(main):006:0> c=a.midpoint_to(b) # what's halfway from a to b?
36
+ irb> c.ll
37
+ => "37.7899239257175,-122.406153503469"
38
+ irb(main):008:0> d=c.endpoint(90,10) # what's 10 miles to the east of c?
39
+ irb> d.ll
40
+ => "37.7897825005142,-122.223214776155"
41
+
42
+ FYI, that `.ll` method means "latitude longitude".
43
+
44
+ See the RDOC more more ... there are also operations on rectangular bounds (e.g., determining if a point is within bounds, find the center, etc).
45
+
46
+ ## CONFIGURATION
47
+
48
+ If you're using this gem by itself, here are the configuration options:
49
+
50
+ # These defaults are used in Geokit::Mappable.distance_to and in acts_as_mappable
51
+ Geokit::default_units = :miles
52
+ Geokit::default_formula = :sphere
53
+
54
+ # This is the timeout value in seconds to be used for calls to the geocoder web
55
+ # services. For no timeout at all, comment out the setting. The timeout unit
56
+ # is in seconds.
57
+ Geokit::Geocoders::timeout = 3
58
+
59
+ # These settings are used if web service calls must be routed through a proxy.
60
+ # These setting can be nil if not needed, otherwise, addr and port must be
61
+ # filled in at a minimum. If the proxy requires authentication, the username
62
+ # and password can be provided as well.
63
+ Geokit::Geocoders::proxy_addr = nil
64
+ Geokit::Geocoders::proxy_port = nil
65
+ Geokit::Geocoders::proxy_user = nil
66
+ Geokit::Geocoders::proxy_pass = nil
67
+
68
+ # This is your yahoo application key for the Yahoo Geocoder.
69
+ # See http://developer.yahoo.com/faq/index.html#appid
70
+ # and http://developer.yahoo.com/maps/rest/V1/geocode.html
71
+ Geokit::Geocoders::yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
72
+
73
+ # This is your Google Maps geocoder key.
74
+ # See http://www.google.com/apis/maps/signup.html
75
+ # and http://www.google.com/apis/maps/documentation/#Geocoding_Examples
76
+ Geokit::Geocoders::google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
77
+
78
+ # You can also set multiple API KEYS for different domains that may be directed to this same application.
79
+ # The domain from which the current user is being directed will automatically be updated for Geokit via
80
+ # the GeocoderControl class, which gets it's begin filter mixed into the ActionController.
81
+ # You define these keys with a Hash as follows:
82
+ #Geokit::Geocoders::google = { 'rubyonrails.org' => 'RUBY_ON_RAILS_API_KEY', 'ruby-docs.org' => 'RUBY_DOCS_API_KEY' }
83
+
84
+ # This is your username and password for geocoder.us.
85
+ # To use the free service, the value can be set to nil or false. For
86
+ # usage tied to an account, the value should be set to username:password.
87
+ # See http://geocoder.us
88
+ # and http://geocoder.us/user/signup
89
+ Geokit::Geocoders::geocoder_us = false
90
+
91
+ # This is your authorization key for geocoder.ca.
92
+ # To use the free service, the value can be set to nil or false. For
93
+ # usage tied to an account, set the value to the key obtained from
94
+ # Geocoder.ca.
95
+ # See http://geocoder.ca
96
+ # and http://geocoder.ca/?register=1
97
+ Geokit::Geocoders::geocoder_ca = false
98
+
99
+ # require "external_geocoder.rb"
100
+ # Please see the section "writing your own geocoders" for more information.
101
+ # Geokit::Geocoders::external_key = 'REPLACE_WITH_YOUR_API_KEY'
102
+
103
+ # This is the order in which the geocoders are called in a failover scenario
104
+ # If you only want to use a single geocoder, put a single symbol in the array.
105
+ # Valid symbols are :google, :yahoo, :us, and :ca.
106
+ # Be aware that there are Terms of Use restrictions on how you can use the
107
+ # various geocoders. Make sure you read up on relevant Terms of Use for each
108
+ # geocoder you are going to use.
109
+ Geokit::Geocoders::provider_order = [:google,:us]
110
+
111
+ # The IP provider order. Valid symbols are :ip,:geo_plugin.
112
+ # As before, make sure you read up on relevant Terms of Use for each.
113
+ # Geokit::Geocoders::ip_provider_order = [:external,:geo_plugin,:ip]
114
+
115
+ If you're using this gem with the [geokit-rails plugin](http://github.com/andre/geokit-rails/tree/master), the plugin
116
+ creates a template with these settings and places it in `config/initializers/geokit_config.rb`.
117
+
118
+ ## SUPPORTED GEOCODERS
119
+
120
+ ### "regular" address geocoders
121
+ * Yahoo Geocoder - requires an API key.
122
+ * Geocoder.us - may require authentication if performing more than the free request limit.
123
+ * Geocoder.ca - for Canada; may require authentication as well.
124
+ * Geonames - a free geocoder
125
+
126
+ ### address geocoders that also provide reverse geocoding
127
+ * Google Geocoder - requires an API key. Also supports multiple results and bounding box/country code biasing.
128
+
129
+ ### IP address geocoders
130
+ * IP Geocoder - geocodes an IP address using hostip.info's web service.
131
+ * Geoplugin.net -- another IP address geocoder
132
+
133
+ ### Google Geocoder Tricks
134
+
135
+ The Google Geocoder sports a number of useful tricks that elevate it a little bit above the rest of the currently supported geocoders. For starters, it returns a `suggested_bounds` property for all your geocoded results, so you can more easily decide where and how to center a map on the places you geocode. Here's a quick example:
136
+
137
+ irb> res = Geokit::Geocoders::GoogleGeocoder.geocode('140 Market St, San Francisco, CA')
138
+ irb> pp res.suggested_bounds
139
+ #<Geokit::Bounds:0x53b36c
140
+ @ne=#<Geokit::LatLng:0x53b204 @lat=37.7968528, @lng=-122.3926933>,
141
+ @sw=#<Geokit::LatLng:0x53b2b8 @lat=37.7905576, @lng=-122.3989885>>
142
+
143
+ In addition, you can use viewport or country code biasing to make sure the geocoders prefers results within a specific area. Say we wanted to geocode the city of Syracuse in Italy. A normal geocoding query would look like this:
144
+
145
+ irb> res = Geokit::Geocoder::GoogleGeocoder.geocode('Syracuse')
146
+ irb> res.full_address
147
+ => "Syracuse, NY, USA"
148
+
149
+ Not exactly what we were looking for. We know that Syracuse is in Italy, so we can tell the Google Geocoder to prefer results from Italy first, and then wander the Syracuses of the world. To do that, we have to pass Italy's ccTLD (country code top-level domain) to the `:bias` option of the `geocode` method. You can find a comprehensive list of all ccTLDs here: http://en.wikipedia.org/wiki/CcTLD.
150
+
151
+ irb> res = Geokit::Geocoder::GoogleGeocoder.geocode('Syracuse', :bias => 'it')
152
+ irb> res.full_address
153
+ => "Syracuse, Italy"
154
+
155
+ Alternatively, we can speficy the geocoding bias as a bounding box object. Say we wanted to geocode the Winnetka district in Los Angeles.
156
+
157
+ irb> res = Geokit::Geocoder::GoogleGeocoder.geocode('Winnetka')
158
+ irb> res.full_address
159
+ => "Winnetka, IL, USA"
160
+
161
+ Not it. What we can do is tell the geocoder to return results only from in and around LA.
162
+
163
+ irb> la_bounds = Geokit::Geocoder::GoogleGeocoder.geocode('Los Angeles').suggested_bounds
164
+ irb> res = Geokit::Geocoder::GoogleGeocoder.geocode('Winnetka', :bias => la_bounds)
165
+ irb> res.full_address
166
+ => "Winnetka, California, USA"
167
+
168
+
169
+ ### The Multigeocoder
170
+ Multi Geocoder - provides failover for the physical location geocoders, and also IP address geocoders. Its configured by setting Geokit::Geocoders::provider_order, and Geokit::Geocoders::ip_provider_order. You should call the Multi-Geocoder with its :geocode method, supplying one address parameter which is either a real street address, or an ip address. For example:
171
+
172
+ Geokit::Geocoders::MultiGeocoder.geocode("900 Sycamore Drive")
173
+
174
+ Geokit::Geocoders::MultiGeocoder.geocode("12.12.12.12")
175
+
176
+ ## MULTIPLE RESULTS
177
+ Some geocoding services will return multple results if the there isn't one clear result.
178
+ Geoloc can capture multiple results through its "all" method. Currently only the Google geocoder
179
+ supports multiple results:
180
+
181
+ irb> geo=Geokit::Geocoders::GoogleGeocoder.geocode("900 Sycamore Drive")
182
+ irb> geo.full_address
183
+ => "900 Sycamore Dr, Arkadelphia, AR 71923, USA"
184
+ irb> geo.all.size
185
+ irb> geo.all.each { |e| puts e.full_address }
186
+ 900 Sycamore Dr, Arkadelphia, AR 71923, USA
187
+ 900 Sycamore Dr, Burkburnett, TX 76354, USA
188
+ 900 Sycamore Dr, TN 38361, USA
189
+ ....
190
+
191
+ geo.all is just an array of additional Geolocs, so do what you want with it. If you call .all on a
192
+ geoloc that doesn't have any additional results, you will get an array of one.
193
+
194
+
195
+ ## NOTES ON WHAT'S WHERE
196
+
197
+ mappable.rb contains the Mappable module, which provides basic
198
+ distance calculation methods, i.e., calculating the distance
199
+ between two points.
200
+
201
+ mappable.rb also contains LatLng, GeoLoc, and Bounds.
202
+ LatLng is a simple container for latitude and longitude, but
203
+ it's made more powerful by mixing in the above-mentioned Mappable
204
+ module -- therefore, you can calculate easily the distance between two
205
+ LatLng ojbects with `distance = first.distance_to(other)`
206
+
207
+ GeoLoc (also in mappable.rb) represents an address or location which
208
+ has been geocoded. You can get the city, zipcode, street address, etc.
209
+ from a GeoLoc object. GeoLoc extends LatLng, so you also get lat/lng
210
+ AND the Mappable modeule goodness for free.
211
+
212
+ geocoders.rb contains all the geocoder implemenations. All the gercoders
213
+ inherit from a common base (class Geocoder) and implement the private method
214
+ do_geocode.
215
+
216
+ ## WRITING YOUR OWN GEOCODERS
217
+
218
+ If you would like to write your own geocoders, you can do so by requiring 'geokit' or 'geokit/geocoders.rb' in a new file and subclassing the base class (which is class "Geocoder").
219
+ You must then also require such extenal file back in your main geokit configuration.
220
+
221
+ require "geokit"
222
+
223
+ module Geokit
224
+ module Geocoders
225
+
226
+ # Should be overriden as Geokit::Geocoders::external_key in your configuration file
227
+ @@external_key = 'REPLACE_WITH_YOUR_API_KEY'
228
+ __define_accessors
229
+
230
+ # Replace name 'External' (below) with the name of your custom geocoder class
231
+ # and use :external to specify this geocoder in your list of geocoders.
232
+ class ExternalGeocoder < Geocoder
233
+ private
234
+ def self.do_geocode(address, options = {})
235
+ # Main geocoding method
236
+ end
237
+
238
+ def self.parse_http_resp(body) # :nodoc:
239
+ # Helper method to parse http response. See geokit/geocoders.rb.
240
+ end
241
+ end
242
+
243
+ end
244
+ end
245
+
246
+ ## GOOGLE GROUP
247
+
248
+ Follow the Google Group for updates and discussion on Geokit: http://groups.google.com/group/geokit
249
+
250
+ ## LICENSE
251
+
252
+ (The MIT License)
253
+
254
+ Copyright (c) 2007-2009 Andre Lewis and Bill Eisenhauer
255
+
256
+ Permission is hereby granted, free of charge, to any person obtaining
257
+ a copy of this software and associated documentation files (the
258
+ 'Software'), to deal in the Software without restriction, including
259
+ without limitation the rights to use, copy, modify, merge, publish,
260
+ distribute, sublicense, and/or sell copies of the Software, and to
261
+ permit persons to whom the Software is furnished to do so, subject to
262
+ the following conditions:
263
+
264
+ The above copyright notice and this permission notice shall be
265
+ included in all copies or substantial portions of the Software.
266
+
267
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
268
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
269
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
270
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
271
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
272
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
273
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/geokit.rb'
6
+
7
+ # undefined method `empty?' for nil:NilClass
8
+ # /Library/Ruby/Site/1.8/rubygems/specification.rb:886:in `validate'
9
+ class NilClass
10
+ def empty?
11
+ true
12
+ end
13
+ end
14
+
15
+ project=Hoe.new('geokit', Geokit::VERSION) do |p|
16
+ #p.rubyforge_name = 'geokit' # if different than lowercase project name
17
+ p.developer('Andre Lewis', 'andre@earthcode.com')
18
+ p.summary="Geokit provides geocoding and distance calculation in an easy-to-use API"
19
+ end
20
+
21
+
22
+ # vim: syntax=Ruby
data/lib/geokit.rb ADDED
@@ -0,0 +1,30 @@
1
+ module Geokit
2
+ VERSION = '1.5.0.3'
3
+ # These defaults are used in Geokit::Mappable.distance_to and in acts_as_mappable
4
+ @@default_units = :miles
5
+ @@default_formula = :sphere
6
+
7
+ [:default_units, :default_formula].each do |sym|
8
+ class_eval <<-EOS, __FILE__, __LINE__
9
+ def self.#{sym}
10
+ if defined?(#{sym.to_s.upcase})
11
+ #{sym.to_s.upcase}
12
+ else
13
+ @@#{sym}
14
+ end
15
+ end
16
+
17
+ def self.#{sym}=(obj)
18
+ @@#{sym} = obj
19
+ end
20
+ EOS
21
+ end
22
+ end
23
+
24
+ path = File.expand_path(File.dirname(__FILE__))
25
+ $:.unshift path unless $:.include?(path)
26
+ require 'geokit/geocoders'
27
+ require 'geokit/mappable'
28
+
29
+ # make old-style module name "GeoKit" equivalent to new-style "Geokit"
30
+ GeoKit=Geokit
@@ -0,0 +1,948 @@
1
+ require 'net/http'
2
+ require 'ipaddr'
3
+ require 'rexml/document'
4
+ require 'yaml'
5
+ require 'timeout'
6
+ require 'logger'
7
+
8
+ # do this just in case
9
+ begin
10
+ ActiveSupport.nil?
11
+ rescue NameError
12
+ require 'json/pure'
13
+ end
14
+
15
+ module Geokit
16
+
17
+ class TooManyQueriesError < StandardError; end
18
+
19
+ module Inflector
20
+
21
+ extend self
22
+
23
+ def titleize(word)
24
+ humanize(underscore(word)).gsub(/\b([a-z])/u) { $1.capitalize }
25
+ end
26
+
27
+ def underscore(camel_cased_word)
28
+ camel_cased_word.to_s.gsub(/::/, '/').
29
+ gsub(/([A-Z]+)([A-Z][a-z])/u,'\1_\2').
30
+ gsub(/([a-z\d])([A-Z])/u,'\1_\2').
31
+ tr("-", "_").
32
+ downcase
33
+ end
34
+
35
+ def humanize(lower_case_and_underscored_word)
36
+ lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
37
+ end
38
+
39
+ def snake_case(s)
40
+ return s.downcase if s =~ /^[A-Z]+$/u
41
+ s.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/u, '_\&') =~ /_*(.*)/
42
+ return $+.downcase
43
+
44
+ end
45
+
46
+ def url_escape(s)
47
+ s.gsub(/([^ a-zA-Z0-9_.-]+)/nu) do
48
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
49
+ end.tr(' ', '+')
50
+ end
51
+
52
+ def camelize(str)
53
+ str.split('_').map {|w| w.capitalize}.join
54
+ end
55
+ end
56
+
57
+ # Contains a range of geocoders:
58
+ #
59
+ # ### "regular" address geocoders
60
+ # * Yahoo Geocoder - requires an API key.
61
+ # * Geocoder.us - may require authentication if performing more than the free request limit.
62
+ # * Geocoder.ca - for Canada; may require authentication as well.
63
+ # * Geonames - a free geocoder
64
+ #
65
+ # ### address geocoders that also provide reverse geocoding
66
+ # * Google Geocoder - requires an API key.
67
+ #
68
+ # ### IP address geocoders
69
+ # * IP Geocoder - geocodes an IP address using hostip.info's web service.
70
+ # * Geoplugin.net -- another IP address geocoder
71
+ #
72
+ # ### The Multigeocoder
73
+ # * Multi Geocoder - provides failover for the physical location geocoders.
74
+ #
75
+ # Some of these geocoders require configuration. You don't have to provide it here. See the README.
76
+ module Geocoders
77
+ @@proxy_addr = nil
78
+ @@proxy_port = nil
79
+ @@proxy_user = nil
80
+ @@proxy_pass = nil
81
+ @@request_timeout = nil
82
+ @@yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
83
+ @@google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
84
+ @@bing = 'REPLACE_WITH_YOUR_BING_KEY'
85
+ @@geocoder_us = false
86
+ @@geocoder_ca = false
87
+ @@geonames = false
88
+ @@provider_order = [:google_v3,:us]
89
+ @@ip_provider_order = [:geo_plugin,:ip]
90
+ @@logger=Logger.new(STDOUT)
91
+ @@logger.level=Logger::INFO
92
+ @@domain = nil
93
+
94
+ def self.__define_accessors
95
+ class_variables.each do |v|
96
+ sym = v.to_s.delete("@").to_sym
97
+ unless self.respond_to? sym
98
+ module_eval <<-EOS, __FILE__, __LINE__
99
+ def self.#{sym}
100
+ value = if defined?(#{sym.to_s.upcase})
101
+ #{sym.to_s.upcase}
102
+ else
103
+ @@#{sym}
104
+ end
105
+ if value.is_a?(Hash)
106
+ value = (self.domain.nil? ? nil : value[self.domain]) || value.values.first
107
+ end
108
+ value
109
+ end
110
+
111
+ def self.#{sym}=(obj)
112
+ @@#{sym} = obj
113
+ end
114
+ EOS
115
+ end
116
+ end
117
+ end
118
+
119
+ __define_accessors
120
+
121
+ # Error which is thrown in the event a geocoding error occurs.
122
+ class GeocodeError < StandardError; end
123
+
124
+ # -------------------------------------------------------------------------------------------
125
+ # Geocoder Base class -- every geocoder should inherit from this
126
+ # -------------------------------------------------------------------------------------------
127
+
128
+ # The Geocoder base class which defines the interface to be used by all
129
+ # other geocoders.
130
+ class Geocoder
131
+ # Main method which calls the do_geocode template method which subclasses
132
+ # are responsible for implementing. Returns a populated GeoLoc or an
133
+ # empty one with a failed success code.
134
+ def self.geocode(address, options = {})
135
+ res = do_geocode(address, options)
136
+ return res.nil? ? GeoLoc.new : res
137
+ end
138
+ # Main method which calls the do_reverse_geocode template method which subclasses
139
+ # are responsible for implementing. Returns a populated GeoLoc or an
140
+ # empty one with a failed success code.
141
+ def self.reverse_geocode(latlng)
142
+ res = do_reverse_geocode(latlng)
143
+ return res.success? ? res : GeoLoc.new
144
+ end
145
+
146
+ # Call the geocoder service using the timeout if configured.
147
+ def self.call_geocoder_service(url)
148
+ Timeout::timeout(Geokit::Geocoders::request_timeout) { return self.do_get(url) } if Geokit::Geocoders::request_timeout
149
+ return self.do_get(url)
150
+ rescue TimeoutError
151
+ return nil
152
+ end
153
+
154
+ # Not all geocoders can do reverse geocoding. So, unless the subclass explicitly overrides this method,
155
+ # a call to reverse_geocode will return an empty GeoLoc. If you happen to be using MultiGeocoder,
156
+ # this will cause it to failover to the next geocoder, which will hopefully be one which supports reverse geocoding.
157
+ def self.do_reverse_geocode(latlng)
158
+ return GeoLoc.new
159
+ end
160
+
161
+ protected
162
+
163
+ def self.logger()
164
+ Geokit::Geocoders::logger
165
+ end
166
+
167
+ private
168
+
169
+ # Wraps the geocoder call around a proxy if necessary.
170
+ def self.do_get(url)
171
+ uri = URI.parse(url)
172
+ req = Net::HTTP::Get.new(url)
173
+ req.basic_auth(uri.user, uri.password) if uri.userinfo
174
+ res = Net::HTTP::Proxy(GeoKit::Geocoders::proxy_addr,
175
+ GeoKit::Geocoders::proxy_port,
176
+ GeoKit::Geocoders::proxy_user,
177
+ GeoKit::Geocoders::proxy_pass).start(uri.host, uri.port) { |http| http.get(uri.path + "?" + uri.query) }
178
+ return res
179
+ end
180
+
181
+ # Adds subclass' geocode method making it conveniently available through
182
+ # the base class.
183
+ def self.inherited(clazz)
184
+ class_name = clazz.name.split('::').last
185
+ src = <<-END_SRC
186
+ def self.#{Geokit::Inflector.underscore(class_name)}(address, options = {})
187
+ #{class_name}.geocode(address, options)
188
+ end
189
+ END_SRC
190
+ class_eval(src)
191
+ end
192
+ end
193
+
194
+ # -------------------------------------------------------------------------------------------
195
+ # "Regular" Address geocoders
196
+ # -------------------------------------------------------------------------------------------
197
+
198
+ # Geocoder CA geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_CA variable to
199
+ # contain true or false based upon whether authentication is to occur. Conforms to the
200
+ # interface set by the Geocoder class.
201
+ #
202
+ # Returns a response like:
203
+ # <?xml version="1.0" encoding="UTF-8" ?>
204
+ # <geodata>
205
+ # <latt>49.243086</latt>
206
+ # <longt>-123.153684</longt>
207
+ # </geodata>
208
+ class CaGeocoder < Geocoder
209
+
210
+ private
211
+
212
+ # Template method which does the geocode lookup.
213
+ def self.do_geocode(address, options = {})
214
+ raise ArgumentError('Geocoder.ca requires a GeoLoc argument') unless address.is_a?(GeoLoc)
215
+ url = construct_request(address)
216
+ res = self.call_geocoder_service(url)
217
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
218
+ xml = res.body
219
+ logger.debug "Geocoder.ca geocoding. Address: #{address}. Result: #{xml}"
220
+ # Parse the document.
221
+ doc = REXML::Document.new(xml)
222
+ address.lat = doc.elements['//latt'].text
223
+ address.lng = doc.elements['//longt'].text
224
+ address.success = true
225
+ return address
226
+ rescue
227
+ logger.error "Caught an error during Geocoder.ca geocoding call: "+$!
228
+ return GeoLoc.new
229
+ end
230
+
231
+ # Formats the request in the format acceptable by the CA geocoder.
232
+ def self.construct_request(location)
233
+ url = ""
234
+ url += add_ampersand(url) + "stno=#{location.street_number}" if location.street_number
235
+ url += add_ampersand(url) + "addresst=#{Geokit::Inflector::url_escape(location.street_address)}" if location.street_address
236
+ url += add_ampersand(url) + "city=#{Geokit::Inflector::url_escape(location.city)}" if location.city
237
+ url += add_ampersand(url) + "prov=#{location.province}" if location.province
238
+ url += add_ampersand(url) + "postal=#{Geokit::Inflector::url_escape(location.zip)}" if location.zip
239
+ url += add_ampersand(url) + "auth=#{Geokit::Geocoders::geocoder_ca}" if Geokit::Geocoders::geocoder_ca
240
+ url += add_ampersand(url) + "geoit=xml"
241
+ 'http://geocoder.ca/?' + url
242
+ end
243
+
244
+ def self.add_ampersand(url)
245
+ url && url.length > 0 ? "&" : ""
246
+ end
247
+ end
248
+
249
+ # Geocoder Us geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_US variable to
250
+ # contain true or false based upon whether authentication is to occur. Conforms to the
251
+ # interface set by the Geocoder class.
252
+ class UsGeocoder < Geocoder
253
+
254
+ private
255
+ def self.do_geocode(address, options = {})
256
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
257
+
258
+ query = (address_str =~ /^\d{5}(?:-\d{4})?$/ ? "zip" : "address") + "=#{Geokit::Inflector::url_escape(address_str)}"
259
+ url = if GeoKit::Geocoders::geocoder_us
260
+ "http://#{GeoKit::Geocoders::geocoder_us}@geocoder.us/member/service/csv/geocode"
261
+ else
262
+ "http://geocoder.us/service/csv/geocode"
263
+ end
264
+
265
+ url = "#{url}?#{query}"
266
+ res = self.call_geocoder_service(url)
267
+
268
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
269
+ data = res.body
270
+ logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}"
271
+ array = data.chomp.split(',')
272
+
273
+ if array.length == 5
274
+ res=GeoLoc.new
275
+ res.lat,res.lng,res.city,res.state,res.zip=array
276
+ res.country_code='US'
277
+ res.success=true
278
+ return res
279
+ elsif array.length == 6
280
+ res=GeoLoc.new
281
+ res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array
282
+ res.country_code='US'
283
+ res.success=true
284
+ return res
285
+ else
286
+ logger.info "geocoder.us was unable to geocode address: "+address
287
+ return GeoLoc.new
288
+ end
289
+ rescue
290
+ logger.error "Caught an error during geocoder.us geocoding call: "+$!
291
+ return GeoLoc.new
292
+
293
+ end
294
+ end
295
+
296
+ # Yahoo geocoder implementation. Requires the Geokit::Geocoders::YAHOO variable to
297
+ # contain a Yahoo API key. Conforms to the interface set by the Geocoder class.
298
+ class YahooGeocoder < Geocoder
299
+
300
+ private
301
+
302
+ # Template method which does the geocode lookup.
303
+ def self.do_geocode(address, options = {})
304
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
305
+ url="http://where.yahooapis.com/geocode?appid=#{Geokit::Geocoders::yahoo}&count=100&location=#{Geokit::Inflector::url_escape(address_str)}"
306
+ res = self.call_geocoder_service(url)
307
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
308
+ xml = res.body
309
+ doc = REXML::Document.new(xml)
310
+ logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}"
311
+
312
+ if doc.elements['//ResultSet']
313
+ if result = doc.elements['//Result']
314
+ res=GeoLoc.new
315
+
316
+ res.lat=result.elements['latitude'].text
317
+ res.lng=result.elements['longitude'].text
318
+ res.country_code=result.elements['countrycode'].text
319
+ #res.county=result.elements['//county'].text
320
+ res.provider='yahoo'
321
+
322
+ res.city=result.elements['city'].text if result.elements['city'] && result.elements['city'].text != nil
323
+ res.state=result.elements['statecode'].text if result.elements['statecode'] && result.elements['statecode'].text != nil
324
+ res.zip=result.elements['postal'].text if result.elements['postal'] && result.elements['postal'].text != nil
325
+ res.street_address="#{result.elements['house'].text} #{result.elements['street'].text}".squeeze(" ") if result.elements['street'] && result.elements['street'].text != nil
326
+ res.full_address = "#{result.elements['line1'].text}, #{result.elements['line2'].text}".squeeze(" ") if result.elements['line2'] && result.elements['line2'].text != nil && result.elements['line1'] && result.elements['line1'].text != nil
327
+
328
+ res.accuracy = case result.elements['quality'].text.to_i
329
+ when 0 then 0 # unknown
330
+ when 1..10 then 1 # country
331
+ when 11..20 then 2 # state
332
+ when 21..30 then 3 # county
333
+ when 40..59 then 4 # city
334
+ when 60..73 then 5 # zip
335
+ when 74..83 then 6 # zip+4
336
+ when 84..86 then 7 # street
337
+ else 8 # address
338
+ end
339
+
340
+ res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy]
341
+
342
+ res.success=true
343
+ end
344
+
345
+ return res
346
+
347
+ else
348
+ logger.info "Yahoo was unable to geocode address: "+address
349
+ return GeoLoc.new
350
+ end
351
+
352
+ rescue
353
+ logger.info "Caught an error during Yahoo geocoding call: "+$!
354
+ return GeoLoc.new
355
+ end
356
+ end
357
+
358
+ # Another geocoding web service
359
+ # http://www.geonames.org
360
+ class GeonamesGeocoder < Geocoder
361
+
362
+ private
363
+
364
+ # Template method which does the geocode lookup.
365
+ def self.do_geocode(address, options = {})
366
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
367
+ # geonames need a space seperated search string
368
+ address_str.gsub!(/,/, " ")
369
+ params = "/postalCodeSearch?placename=#{Geokit::Inflector::url_escape(address_str)}&maxRows=10"
370
+
371
+ if(GeoKit::Geocoders::geonames)
372
+ url = "http://ws.geonames.net#{params}&username=#{GeoKit::Geocoders::geonames}"
373
+ else
374
+ url = "http://ws.geonames.org#{params}"
375
+ end
376
+
377
+ res = self.call_geocoder_service(url)
378
+
379
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
380
+
381
+ xml=res.body
382
+ logger.debug "Geonames geocoding. Address: #{address}. Result: #{xml}"
383
+ doc=REXML::Document.new(xml)
384
+
385
+ if(doc.elements['//geonames/totalResultsCount'].text.to_i > 0)
386
+ res=GeoLoc.new
387
+
388
+ # only take the first result
389
+ res.lat=doc.elements['//code/lat'].text if doc.elements['//code/lat']
390
+ res.lng=doc.elements['//code/lng'].text if doc.elements['//code/lng']
391
+ res.country_code=doc.elements['//code/countryCode'].text if doc.elements['//code/countryCode']
392
+ res.provider='genomes'
393
+ res.city=doc.elements['//code/name'].text if doc.elements['//code/name']
394
+ res.state=doc.elements['//code/adminName1'].text if doc.elements['//code/adminName1']
395
+ res.zip=doc.elements['//code/postalcode'].text if doc.elements['//code/postalcode']
396
+ res.success=true
397
+ return res
398
+ else
399
+ logger.info "Geonames was unable to geocode address: "+address
400
+ return GeoLoc.new
401
+ end
402
+
403
+ rescue
404
+ logger.error "Caught an error during Geonames geocoding call: "+$!
405
+ end
406
+ end
407
+
408
+ # Bing geocoder implementation. Requires the Geokit::Geocoders::BING variable to
409
+ # contain a Bing Maps API key. Conforms to the interface set by the Geocoder class.
410
+ class BingGeocoder < Geocoder
411
+
412
+ private
413
+
414
+ # Template method which does the geocode lookup.
415
+ def self.do_geocode(address, options = {})
416
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
417
+ url="http://dev.virtualearth.net/REST/v1/Locations/#{URI.escape(address_str)}?key=#{Geokit::Geocoders::bing}&o=xml"
418
+ res = self.call_geocoder_service(url)
419
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
420
+ xml = res.body
421
+ logger.debug "Bing geocoding. Address: #{address}. Result: #{xml}"
422
+ return self.xml2GeoLoc(xml, address)
423
+ end
424
+
425
+ def self.xml2GeoLoc(xml, address="")
426
+ doc=REXML::Document.new(xml)
427
+ if doc.elements['//Response/StatusCode'] && doc.elements['//Response/StatusCode'].text == '200'
428
+ geoloc = nil
429
+ # Bing can return multiple results as //Location elements.
430
+ # iterate through each and extract each location as a geoloc
431
+ doc.each_element('//Location') do |l|
432
+ extracted_geoloc = extract_location(l)
433
+ geoloc.nil? ? geoloc = extracted_geoloc : geoloc.all.push(extracted_geoloc)
434
+ end
435
+ return geoloc
436
+ else
437
+ logger.debug "Bing was unable to geocode address: "+address
438
+ return GeoLoc.new
439
+ end
440
+
441
+ rescue
442
+ logger.error "Caught an error during Bing geocoding call: "+$!
443
+ return GeoLoc.new
444
+ end
445
+
446
+ # extracts a single geoloc from a //Location element in the bing results xml
447
+ def self.extract_location(doc)
448
+ res = GeoLoc.new
449
+ res.provider = 'bing'
450
+ res.lat = doc.elements['.//Latitude'].text if doc.elements['.//Latitude']
451
+ res.lng = doc.elements['.//Longitude'].text if doc.elements['.//Longitude']
452
+ res.city = doc.elements['.//Locality'].text if doc.elements['.//Locality']
453
+ res.state = doc.elements['.//AdminDistrict'].text if doc.elements['.//AdminDistrict']
454
+ res.province = doc.elements['.//AdminDistrict2'].text if doc.elements['.//AdminDistrict2']
455
+ res.full_address = doc.elements['.//FormattedAddress'].text if doc.elements['.//FormattedAddress']
456
+ res.zip = doc.elements['.//PostalCode'].text if doc.elements['.//PostalCode']
457
+ res.street_address = doc.elements['.//AddressLine'].text if doc.elements['.//AddressLine']
458
+ res.country = doc.elements['.//CountryRegion'].text if doc.elements['.//CountryRegion']
459
+ if doc.elements['.//Confidence']
460
+ res.accuracy = case doc.elements['.//Confidence'].text
461
+ when 'High' : 8
462
+ when 'Medium' : 5
463
+ when 'Low' : 2
464
+ else 0
465
+ end
466
+ end
467
+ if doc.elements['.//EntityType']
468
+ res.precision = case doc.elements['.//EntityType'].text
469
+ when 'Sovereign' : 'country'
470
+ when 'AdminDivision1' : 'state'
471
+ when 'AdminDivision2' : 'state'
472
+ when 'PopulatedPlace' : 'city'
473
+ when 'Postcode1' : 'zip'
474
+ when 'Postcode2' : 'zip'
475
+ when 'RoadBlock' : 'street'
476
+ when 'Address' : 'address'
477
+ else 'unkown'
478
+ end
479
+ end
480
+ if suggested_bounds = doc.elements['.//BoundingBox']
481
+ res.suggested_bounds = Bounds.normalize(
482
+ [suggested_bounds.elements['.//SouthLatitude'].text, suggested_bounds.elements['.//WestLongitude'].text],
483
+ [suggested_bounds.elements['.//NorthLatitude'].text, suggested_bounds.elements['.//EastLongitude'].text])
484
+ end
485
+ res.success = true
486
+ return res
487
+ end
488
+
489
+ end
490
+
491
+ # -------------------------------------------------------------------------------------------
492
+ # Address geocoders that also provide reverse geocoding
493
+ # -------------------------------------------------------------------------------------------
494
+
495
+ # Google geocoder implementation. Requires the Geokit::Geocoders::GOOGLE variable to
496
+ # contain a Google API key. Conforms to the interface set by the Geocoder class.
497
+ class GoogleGeocoder < Geocoder
498
+
499
+ private
500
+
501
+ # Template method which does the reverse-geocode lookup.
502
+ def self.do_reverse_geocode(latlng)
503
+ latlng=LatLng.normalize(latlng)
504
+ res = self.call_geocoder_service("http://maps.google.com/maps/geo?ll=#{Geokit::Inflector::url_escape(latlng.ll)}&output=xml&key=#{Geokit::Geocoders::google}&oe=utf-8")
505
+ # res = Net::HTTP.get_response(URI.parse("http://maps.google.com/maps/geo?ll=#{Geokit::Inflector::url_escape(address_str)}&output=xml&key=#{Geokit::Geocoders::google}&oe=utf-8"))
506
+ return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
507
+ xml = res.body
508
+ logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{xml}"
509
+ return self.xml2GeoLoc(xml)
510
+ end
511
+
512
+ # Template method which does the geocode lookup.
513
+ #
514
+ # Supports viewport/country code biasing
515
+ #
516
+ # ==== OPTIONS
517
+ # * :bias - This option makes the Google Geocoder return results biased to a particular
518
+ # country or viewport. Country code biasing is achieved by passing the ccTLD
519
+ # ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's,
520
+ # look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder
521
+ # will be biased to results within the US (ccTLD .com).
522
+ #
523
+ # If you'd like the Google Geocoder to prefer results within a given viewport,
524
+ # you can pass a Geokit::Bounds object as the :bias value.
525
+ #
526
+ # ==== EXAMPLES
527
+ # # By default, the geocoder will return Syracuse, NY
528
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse').country_code # => 'US'
529
+ # # With country code biasing, it returns Syracuse in Sicily, Italy
530
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse', :bias => :it).country_code # => 'IT'
531
+ #
532
+ # # By default, the geocoder will return Winnetka, IL
533
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka').state # => 'IL'
534
+ # # When biased to an bounding box around California, it will now return the Winnetka neighbourhood, CA
535
+ # bounds = Geokit::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487])
536
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka', :bias => bounds).state # => 'CA'
537
+ def self.do_geocode(address, options = {})
538
+ bias_str = options[:bias] ? construct_bias_string_from_options(options[:bias]) : ''
539
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
540
+ res = self.call_geocoder_service("http://maps.google.com/maps/geo?q=#{Geokit::Inflector::url_escape(address_str)}&output=xml#{bias_str}&key=#{Geokit::Geocoders::google}&oe=utf-8")
541
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
542
+ xml = res.body
543
+ logger.debug "Google geocoding. Address: #{address}. Result: #{xml}"
544
+ return self.xml2GeoLoc(xml, address)
545
+ end
546
+
547
+ def self.construct_bias_string_from_options(bias)
548
+ if bias.is_a?(String) or bias.is_a?(Symbol)
549
+ # country code biasing
550
+ "&gl=#{bias.to_s.downcase}"
551
+ elsif bias.is_a?(Bounds)
552
+ # viewport biasing
553
+ "&ll=#{bias.center.ll}&spn=#{bias.to_span.ll}"
554
+ end
555
+ end
556
+
557
+ def self.xml2GeoLoc(xml, address="")
558
+ doc=REXML::Document.new(xml)
559
+
560
+ if doc.elements['//kml/Response/Status/code'].text == '200'
561
+ geoloc = nil
562
+ # Google can return multiple results as //Placemark elements.
563
+ # iterate through each and extract each placemark as a geoloc
564
+ doc.each_element('//Placemark') do |e|
565
+ extracted_geoloc = extract_placemark(e) # g is now an instance of GeoLoc
566
+ if geoloc.nil?
567
+ # first time through, geoloc is still nil, so we make it the geoloc we just extracted
568
+ geoloc = extracted_geoloc
569
+ else
570
+ # second (and subsequent) iterations, we push additional
571
+ # geolocs onto "geoloc.all"
572
+ geoloc.all.push(extracted_geoloc)
573
+ end
574
+ end
575
+ return geoloc
576
+ elsif doc.elements['//kml/Response/Status/code'].text == '620'
577
+ raise Geokit::TooManyQueriesError
578
+ else
579
+ logger.info "Google was unable to geocode address: "+address
580
+ return GeoLoc.new
581
+ end
582
+
583
+ rescue Geokit::TooManyQueriesError
584
+ # re-raise because of other rescue
585
+ raise Geokit::TooManyQueriesError, "Google returned a 620 status, too many queries. The given key has gone over the requests limit in the 24 hour period or has submitted too many requests in too short a period of time. If you're sending multiple requests in parallel or in a tight loop, use a timer or pause in your code to make sure you don't send the requests too quickly."
586
+ rescue
587
+ logger.error "Caught an error during Google geocoding call: "+$!
588
+ return GeoLoc.new
589
+ end
590
+
591
+ # extracts a single geoloc from a //placemark element in the google results xml
592
+ def self.extract_placemark(doc)
593
+ res = GeoLoc.new
594
+ coordinates=doc.elements['.//coordinates'].text.to_s.split(',')
595
+
596
+ #basics
597
+ res.lat=coordinates[1]
598
+ res.lng=coordinates[0]
599
+ res.country_code=doc.elements['.//CountryNameCode'].text if doc.elements['.//CountryNameCode']
600
+ res.provider='google'
601
+
602
+ #extended -- false if not not available
603
+ res.city = doc.elements['.//LocalityName'].text if doc.elements['.//LocalityName']
604
+ res.state = doc.elements['.//AdministrativeAreaName'].text if doc.elements['.//AdministrativeAreaName']
605
+ res.province = doc.elements['.//SubAdministrativeAreaName'].text if doc.elements['.//SubAdministrativeAreaName']
606
+ res.full_address = doc.elements['.//address'].text if doc.elements['.//address'] # google provides it
607
+ res.zip = doc.elements['.//PostalCodeNumber'].text if doc.elements['.//PostalCodeNumber']
608
+ res.street_address = doc.elements['.//ThoroughfareName'].text if doc.elements['.//ThoroughfareName']
609
+ res.country = doc.elements['.//CountryName'].text if doc.elements['.//CountryName']
610
+ res.district = doc.elements['.//DependentLocalityName'].text if doc.elements['.//DependentLocalityName']
611
+ # Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country
612
+ # For Google, 1=low accuracy, 8=high accuracy
613
+ address_details=doc.elements['.//*[local-name() = "AddressDetails"]']
614
+ res.accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0
615
+ res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy]
616
+
617
+ # google returns a set of suggested boundaries for the geocoded result
618
+ if suggested_bounds = doc.elements['//LatLonBox']
619
+ res.suggested_bounds = Bounds.normalize(
620
+ [suggested_bounds.attributes['south'], suggested_bounds.attributes['west']],
621
+ [suggested_bounds.attributes['north'], suggested_bounds.attributes['east']])
622
+ end
623
+
624
+ res.success=true
625
+
626
+ return res
627
+ end
628
+ end
629
+
630
+ class GoogleV3Geocoder < Geocoder
631
+
632
+ private
633
+ # Template method which does the reverse-geocode lookup.
634
+ def self.do_reverse_geocode(latlng)
635
+ latlng=LatLng.normalize(latlng)
636
+ res = self.call_geocoder_service("http://maps.google.com/maps/api/geocode/json?sensor=false&latlng=#{Geokit::Inflector::url_escape(latlng.ll)}")
637
+ return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
638
+ json = res.body
639
+ logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{json}"
640
+ return self.json2GeoLoc(json)
641
+ end
642
+
643
+ # Template method which does the geocode lookup.
644
+ #
645
+ # Supports viewport/country code biasing
646
+ #
647
+ # ==== OPTIONS
648
+ # * :bias - This option makes the Google Geocoder return results biased to a particular
649
+ # country or viewport. Country code biasing is achieved by passing the ccTLD
650
+ # ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's,
651
+ # look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder
652
+ # will be biased to results within the US (ccTLD .com).
653
+ #
654
+ # If you'd like the Google Geocoder to prefer results within a given viewport,
655
+ # you can pass a Geokit::Bounds object as the :bias value.
656
+ #
657
+ # ==== EXAMPLES
658
+ # # By default, the geocoder will return Syracuse, NY
659
+ # Geokit::Geocoders::GoogleV3Geocoder.geocode('Syracuse').country_code # => 'US'
660
+ # # With country code biasing, it returns Syracuse in Sicily, Italy
661
+ # Geokit::Geocoders::GoogleV3Geocoder.geocode('Syracuse', :bias => :it).country_code # => 'IT'
662
+ #
663
+ # # By default, the geocoder will return Winnetka, IL
664
+ # Geokit::Geocoders::GoogleV3Geocoder.geocode('Winnetka').state # => 'IL'
665
+ # # When biased to an bounding box around California, it will now return the Winnetka neighbourhood, CA
666
+ # bounds = Geokit::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487])
667
+ # Geokit::Geocoders::GoogleV3Geocoder.geocode('Winnetka', :bias => bounds).state # => 'CA'
668
+ def self.do_geocode(address, options = {})
669
+ bias_str = options[:bias] ? construct_bias_string_from_options(options[:bias]) : ''
670
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
671
+ res = self.call_geocoder_service("http://maps.google.com/maps/api/geocode/json?sensor=false&address=#{Geokit::Inflector::url_escape(address_str)}#{bias_str}")
672
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
673
+ json = res.body
674
+ logger.debug "Google geocoding. Address: #{address}. Result: #{json}"
675
+ return self.json2GeoLoc(json, address)
676
+ end
677
+
678
+ def self.construct_bias_string_from_options(bias)
679
+ if bias.is_a?(String) or bias.is_a?(Symbol)
680
+ # country code biasing
681
+ "&region=#{bias.to_s.downcase}"
682
+ elsif bias.is_a?(Bounds)
683
+ # viewport biasing
684
+ Geokit::Inflector::url_escape("&bounds=#{bias.sw.to_s}|#{bias.ne.to_s}")
685
+ end
686
+ end
687
+
688
+ def self.json2GeoLoc(json, address="")
689
+ ret=nil
690
+ begin
691
+ results=::ActiveSupport::JSON.decode(json)
692
+ rescue NameError => e
693
+ results=JSON.parse(json)
694
+ end
695
+
696
+
697
+ if results['status'] == 'OVER_QUERY_LIMIT'
698
+ raise Geokit::TooManyQueriesError
699
+ end
700
+ if results['status'] == 'ZERO_RESULTS'
701
+ return GeoLoc.new
702
+ end
703
+ # this should probably be smarter.
704
+ if !results['status'] == 'OK'
705
+ raise Geokit::Geocoders::GeocodeError
706
+ end
707
+ # location_type stores additional data about the specified location.
708
+ # The following values are currently supported:
709
+ # "ROOFTOP" indicates that the returned result is a precise geocode
710
+ # for which we have location information accurate down to street
711
+ # address precision.
712
+ # "RANGE_INTERPOLATED" indicates that the returned result reflects an
713
+ # approximation (usually on a road) interpolated between two precise
714
+ # points (such as intersections). Interpolated results are generally
715
+ # returned when rooftop geocodes are unavailable for a street address.
716
+ # "GEOMETRIC_CENTER" indicates that the returned result is the
717
+ # geometric center of a result such as a polyline (for example, a
718
+ # street) or polygon (region).
719
+ # "APPROXIMATE" indicates that the returned result is approximate
720
+
721
+ # these do not map well. Perhaps we should guess better based on size
722
+ # of bounding box where it exists? Does it really matter?
723
+ accuracy = {
724
+ "ROOFTOP" => 9,
725
+ "RANGE_INTERPOLATED" => 8,
726
+ "GEOMETRIC_CENTER" => 5,
727
+ "APPROXIMATE" => 4
728
+ }
729
+ results['results'].sort_by{|a|accuracy[a['geometry']['location_type']]}.reverse.each do |addr|
730
+ res=GeoLoc.new
731
+ res.provider = 'google_v3'
732
+ res.success = true
733
+ res.full_address = addr['formatted_address']
734
+ addr['address_components'].each do |comp|
735
+ case
736
+ when comp['types'].include?("street_number")
737
+ res.street_number = comp['short_name']
738
+ when comp['types'].include?("route")
739
+ res.street_name = comp['long_name']
740
+ when comp['types'].include?("locality")
741
+ res.city = comp['long_name']
742
+ when comp['types'].include?("administrative_area_level_1")
743
+ res.state = comp['short_name']
744
+ res.province = comp['short_name']
745
+ when comp['types'].include?("postal_code")
746
+ res.zip = comp['long_name']
747
+ when comp['types'].include?("country")
748
+ res.country_code = comp['short_name']
749
+ res.country = comp['long_name']
750
+ when comp['types'].include?("administrative_area_level_2")
751
+ res.district = comp['long_name']
752
+ end
753
+ end
754
+ if res.street_name
755
+ res.street_address=[res.street_number,res.street_name].join(' ').strip
756
+ end
757
+ res.accuracy = accuracy[addr['geometry']['location_type']]
758
+ res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy]
759
+ # try a few overrides where we can
760
+ if res.street_name && res.precision=='city'
761
+ res.precision = 'street'
762
+ res.accuracy = 7
763
+ end
764
+
765
+ res.lat=addr['geometry']['location']['lat'].to_f
766
+ res.lng=addr['geometry']['location']['lng'].to_f
767
+
768
+ ne=Geokit::LatLng.new(
769
+ addr['geometry']['viewport']['northeast']['lat'].to_f,
770
+ addr['geometry']['viewport']['northeast']['lng'].to_f
771
+ )
772
+ sw=Geokit::LatLng.new(
773
+ addr['geometry']['viewport']['southwest']['lat'].to_f,
774
+ addr['geometry']['viewport']['southwest']['lng'].to_f
775
+ )
776
+ res.suggested_bounds = Geokit::Bounds.new(sw,ne)
777
+
778
+ if ret
779
+ ret.all.push(res)
780
+ else
781
+ ret=res
782
+ end
783
+ end
784
+ return ret
785
+ end
786
+ end
787
+ # -------------------------------------------------------------------------------------------
788
+ # IP Geocoders
789
+ # -------------------------------------------------------------------------------------------
790
+
791
+ # Provides geocoding based upon an IP address. The underlying web service is geoplugin.net
792
+ class GeoPluginGeocoder < Geocoder
793
+ private
794
+
795
+ def self.do_geocode(ip, options = {})
796
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
797
+ response = self.call_geocoder_service("http://www.geoplugin.net/xml.gp?ip=#{ip}")
798
+ return response.is_a?(Net::HTTPSuccess) ? parse_xml(response.body) : GeoLoc.new
799
+ rescue
800
+ logger.error "Caught an error during GeoPluginGeocoder geocoding call: "+$!
801
+ return GeoLoc.new
802
+ end
803
+
804
+ def self.parse_xml(xml)
805
+ xml = REXML::Document.new(xml)
806
+ geo = GeoLoc.new
807
+ geo.provider='geoPlugin'
808
+ geo.city = xml.elements['//geoplugin_city'].text
809
+ geo.state = xml.elements['//geoplugin_region'].text
810
+ geo.country_code = xml.elements['//geoplugin_countryCode'].text
811
+ geo.lat = xml.elements['//geoplugin_latitude'].text.to_f
812
+ geo.lng = xml.elements['//geoplugin_longitude'].text.to_f
813
+ geo.success = !!geo.city && !geo.city.empty?
814
+ return geo
815
+ end
816
+ end
817
+
818
+ # Provides geocoding based upon an IP address. The underlying web service is a hostip.info
819
+ # which sources their data through a combination of publicly available information as well
820
+ # as community contributions.
821
+ class IpGeocoder < Geocoder
822
+
823
+ # A number of non-routable IP ranges.
824
+ #
825
+ # --
826
+ # Sources for these:
827
+ # RFC 3330: Special-Use IPv4 Addresses
828
+ # The bogon list: http://www.cymru.com/Documents/bogon-list.html
829
+
830
+ NON_ROUTABLE_IP_RANGES = [
831
+ IPAddr.new('0.0.0.0/8'), # "This" Network
832
+ IPAddr.new('10.0.0.0/8'), # Private-Use Networks
833
+ IPAddr.new('14.0.0.0/8'), # Public-Data Networks
834
+ IPAddr.new('127.0.0.0/8'), # Loopback
835
+ IPAddr.new('169.254.0.0/16'), # Link local
836
+ IPAddr.new('172.16.0.0/12'), # Private-Use Networks
837
+ IPAddr.new('192.0.2.0/24'), # Test-Net
838
+ IPAddr.new('192.168.0.0/16'), # Private-Use Networks
839
+ IPAddr.new('198.18.0.0/15'), # Network Interconnect Device Benchmark Testing
840
+ IPAddr.new('224.0.0.0/4'), # Multicast
841
+ IPAddr.new('240.0.0.0/4') # Reserved for future use
842
+ ].freeze
843
+
844
+ private
845
+
846
+ # Given an IP address, returns a GeoLoc instance which contains latitude,
847
+ # longitude, city, and country code. Sets the success attribute to false if the ip
848
+ # parameter does not match an ip address.
849
+ def self.do_geocode(ip, options = {})
850
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
851
+ return GeoLoc.new if self.private_ip_address?(ip)
852
+ url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true"
853
+ response = self.call_geocoder_service(url)
854
+ response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new
855
+ rescue
856
+ logger.error "Caught an error during HostIp geocoding call: "+$!
857
+ return GeoLoc.new
858
+ end
859
+
860
+ # Converts the body to YAML since its in the form of:
861
+ #
862
+ # Country: UNITED STATES (US)
863
+ # City: Sugar Grove, IL
864
+ # Latitude: 41.7696
865
+ # Longitude: -88.4588
866
+ #
867
+ # then instantiates a GeoLoc instance to populate with location data.
868
+ def self.parse_body(body) # :nodoc:
869
+ yaml = YAML.load(body)
870
+ res = GeoLoc.new
871
+ res.provider = 'hostip'
872
+ res.city, res.state = yaml['City'].split(', ')
873
+ country, res.country_code = yaml['Country'].split(' (')
874
+ res.lat = yaml['Latitude']
875
+ res.lng = yaml['Longitude']
876
+ res.country_code.chop!
877
+ res.success = !(res.city =~ /\(.+\)/)
878
+ res
879
+ end
880
+
881
+ # Checks whether the IP address belongs to a private address range.
882
+ #
883
+ # This function is used to reduce the number of useless queries made to
884
+ # the geocoding service. Such queries can occur frequently during
885
+ # integration tests.
886
+ def self.private_ip_address?(ip)
887
+ return NON_ROUTABLE_IP_RANGES.any? { |range| range.include?(ip) }
888
+ end
889
+ end
890
+
891
+ # -------------------------------------------------------------------------------------------
892
+ # The Multi Geocoder
893
+ # -------------------------------------------------------------------------------------------
894
+
895
+ # Provides methods to geocode with a variety of geocoding service providers, plus failover
896
+ # among providers in the order you configure. When 2nd parameter is set 'true', perform
897
+ # ip location lookup with 'address' as the ip address.
898
+ #
899
+ # Goal:
900
+ # - homogenize the results of multiple geocoders
901
+ #
902
+ # Limitations:
903
+ # - currently only provides the first result. Sometimes geocoders will return multiple results.
904
+ # - currently discards the "accuracy" component of the geocoding calls
905
+ class MultiGeocoder < Geocoder
906
+
907
+ private
908
+ # This method will call one or more geocoders in the order specified in the
909
+ # configuration until one of the geocoders work.
910
+ #
911
+ # The failover approach is crucial for production-grade apps, but is rarely used.
912
+ # 98% of your geocoding calls will be successful with the first call
913
+ def self.do_geocode(address, options = {})
914
+ geocode_ip = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.match(address) if address.is_a?(String)
915
+ provider_order = geocode_ip ? Geokit::Geocoders::ip_provider_order : Geokit::Geocoders::provider_order
916
+
917
+ provider_order.each do |provider|
918
+ begin
919
+ klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder"
920
+ res = klass.send :geocode, address, options
921
+ return res if res.success?
922
+ rescue
923
+ logger.error("Something has gone very wrong during geocoding, OR you have configured an invalid class name in Geokit::Geocoders::provider_order. Address: #{address}. Provider: #{provider}")
924
+ end
925
+ end
926
+ # If we get here, we failed completely.
927
+ GeoLoc.new
928
+ end
929
+
930
+ # This method will call one or more geocoders in the order specified in the
931
+ # configuration until one of the geocoders work, only this time it's going
932
+ # to try to reverse geocode a geographical point.
933
+ def self.do_reverse_geocode(latlng)
934
+ Geokit::Geocoders::provider_order.each do |provider|
935
+ begin
936
+ klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder"
937
+ res = klass.send :reverse_geocode, latlng
938
+ return res if res.success?
939
+ rescue
940
+ logger.error("Something has gone very wrong during reverse geocoding, OR you have configured an invalid class name in Geokit::Geocoders::provider_order. LatLng: #{latlng}. Provider: #{provider}")
941
+ end
942
+ end
943
+ # If we get here, we failed completely.
944
+ GeoLoc.new
945
+ end
946
+ end
947
+ end
948
+ end