geokit-premier 0.0.4

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