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 +23 -0
- data/README.markdown +276 -0
- data/Rakefile +21 -0
- data/lib/geokit.rb +30 -0
- data/lib/geokit/geocoders.rb +890 -0
- data/lib/geokit/mappable.rb +534 -0
- data/spec/geocoder_spec.rb +73 -0
- data/spec/spec_helper.rb +38 -0
- data/test/test_base_geocoder.rb +58 -0
- data/test/test_bounds.rb +97 -0
- data/test/test_ca_geocoder.rb +41 -0
- data/test/test_geoloc.rb +72 -0
- data/test/test_geoplugin_geocoder.rb +59 -0
- data/test/test_google_geocoder.rb +227 -0
- data/test/test_google_reverse_geocoder.rb +49 -0
- data/test/test_inflector.rb +24 -0
- data/test/test_ipgeocoder.rb +110 -0
- data/test/test_latlng.rb +209 -0
- data/test/test_multi_geocoder.rb +93 -0
- data/test/test_multi_ip_geocoder.rb +38 -0
- data/test/test_us_geocoder.rb +56 -0
- data/test/test_yahoo_geocoder.rb +105 -0
- metadata +105 -0
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
|
+
"®ion=#{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
|