geokit 1.6.0 → 1.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Rakefile +7 -21
- data/lib/geokit.rb +1 -2
- data/lib/geokit/geocoders.rb +219 -194
- data/lib/geokit/mappable.rb +98 -89
- data/test/test_base_geocoder.rb +11 -11
- data/test/test_bounds.rb +22 -22
- data/test/test_ca_geocoder.rb +17 -17
- data/test/test_geoloc.rb +15 -15
- data/test/test_geoplugin_geocoder.rb +5 -5
- data/test/test_google_geocoder.rb +30 -30
- data/test/test_google_geocoder3.rb +107 -86
- data/test/test_google_reverse_geocoder.rb +16 -16
- data/test/test_inflector.rb +3 -3
- data/test/test_ipgeocoder.rb +13 -13
- data/test/test_latlng.rb +48 -36
- data/test/test_multi_geocoder.rb +17 -17
- data/test/test_multi_ip_geocoder.rb +5 -5
- data/test/test_us_geocoder.rb +17 -17
- data/test/test_yahoo_geocoder.rb +27 -31
- metadata +29 -38
- data/History.txt +0 -77
- data/Manifest.txt +0 -21
- data/geokit.gemspec +0 -38
data/Rakefile
CHANGED
@@ -1,24 +1,10 @@
|
|
1
|
-
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rake/testtask'
|
2
3
|
|
3
|
-
|
4
|
-
require 'rubygems'
|
5
|
-
require 'hoe'
|
6
|
-
require './lib/geokit.rb'
|
4
|
+
task :default => :test
|
7
5
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
def empty?
|
13
|
-
true
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
project=Hoe.new('geokit', Geokit::VERSION) do |p|
|
18
|
-
#p.rubyforge_name = 'geokit' # if different than lowercase project name
|
19
|
-
p.developer('Andre Lewis', 'andre@earthcode.com')
|
20
|
-
p.summary="Geokit provides geocoding and distance calculation in an easy-to-use API"
|
6
|
+
Rake::TestTask.new do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.test_files = FileList['test/test*.rb']
|
9
|
+
t.verbose = true
|
21
10
|
end
|
22
|
-
|
23
|
-
|
24
|
-
# vim: syntax=Ruby
|
data/lib/geokit.rb
CHANGED
data/lib/geokit/geocoders.rb
CHANGED
@@ -5,25 +5,20 @@ require 'yaml'
|
|
5
5
|
require 'timeout'
|
6
6
|
require 'logger'
|
7
7
|
|
8
|
-
|
9
|
-
begin
|
10
|
-
ActiveSupport.nil?
|
11
|
-
rescue NameError
|
12
|
-
require 'json/pure'
|
13
|
-
end
|
8
|
+
require 'multi_json'
|
14
9
|
|
15
10
|
module Geokit
|
16
11
|
|
17
12
|
class TooManyQueriesError < StandardError; end
|
18
13
|
|
19
14
|
module Inflector
|
20
|
-
|
15
|
+
|
21
16
|
extend self
|
22
|
-
|
17
|
+
|
23
18
|
def titleize(word)
|
24
19
|
humanize(underscore(word)).gsub(/\b([a-z])/u) { $1.capitalize }
|
25
20
|
end
|
26
|
-
|
21
|
+
|
27
22
|
def underscore(camel_cased_word)
|
28
23
|
camel_cased_word.to_s.gsub(/::/, '/').
|
29
24
|
gsub(/([A-Z]+)([A-Z][a-z])/u,'\1_\2').
|
@@ -31,54 +26,54 @@ module Geokit
|
|
31
26
|
tr("-", "_").
|
32
27
|
downcase
|
33
28
|
end
|
34
|
-
|
29
|
+
|
35
30
|
def humanize(lower_case_and_underscored_word)
|
36
31
|
lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
|
37
32
|
end
|
38
|
-
|
33
|
+
|
39
34
|
def snake_case(s)
|
40
35
|
return s.downcase if s =~ /^[A-Z]+$/u
|
41
36
|
s.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/u, '_\&') =~ /_*(.*)/
|
42
37
|
return $+.downcase
|
43
|
-
|
38
|
+
|
44
39
|
end
|
45
|
-
|
40
|
+
|
46
41
|
def url_escape(s)
|
47
42
|
s.gsub(/([^ a-zA-Z0-9_.-]+)/nu) do
|
48
43
|
'%' + $1.unpack('H2' * $1.size).join('%').upcase
|
49
44
|
end.tr(' ', '+')
|
50
45
|
end
|
51
|
-
|
46
|
+
|
52
47
|
def camelize(str)
|
53
48
|
str.split('_').map {|w| w.capitalize}.join
|
54
49
|
end
|
55
|
-
end
|
56
|
-
|
50
|
+
end
|
51
|
+
|
57
52
|
# Contains a range of geocoders:
|
58
|
-
#
|
59
|
-
# ### "regular" address geocoders
|
53
|
+
#
|
54
|
+
# ### "regular" address geocoders
|
60
55
|
# * Yahoo Geocoder - requires an API key.
|
61
56
|
# * Geocoder.us - may require authentication if performing more than the free request limit.
|
62
57
|
# * Geocoder.ca - for Canada; may require authentication as well.
|
63
58
|
# * Geonames - a free geocoder
|
64
59
|
#
|
65
|
-
# ### address geocoders that also provide reverse geocoding
|
60
|
+
# ### address geocoders that also provide reverse geocoding
|
66
61
|
# * Google Geocoder - requires an API key.
|
67
|
-
#
|
68
|
-
# ### IP address geocoders
|
62
|
+
#
|
63
|
+
# ### IP address geocoders
|
69
64
|
# * IP Geocoder - geocodes an IP address using hostip.info's web service.
|
70
65
|
# * Geoplugin.net -- another IP address geocoder
|
71
66
|
#
|
72
67
|
# ### The Multigeocoder
|
73
68
|
# * Multi Geocoder - provides failover for the physical location geocoders.
|
74
|
-
#
|
69
|
+
#
|
75
70
|
# Some of these geocoders require configuration. You don't have to provide it here. See the README.
|
76
71
|
module Geocoders
|
77
72
|
@@proxy_addr = nil
|
78
73
|
@@proxy_port = nil
|
79
74
|
@@proxy_user = nil
|
80
75
|
@@proxy_pass = nil
|
81
|
-
@@request_timeout = nil
|
76
|
+
@@request_timeout = nil
|
82
77
|
@@yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
|
83
78
|
@@google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
|
84
79
|
@@geocoder_us = false
|
@@ -89,9 +84,9 @@ module Geokit
|
|
89
84
|
@@logger=Logger.new(STDOUT)
|
90
85
|
@@logger.level=Logger::INFO
|
91
86
|
@@domain = nil
|
92
|
-
|
87
|
+
|
93
88
|
def self.__define_accessors
|
94
|
-
class_variables.each do |v|
|
89
|
+
class_variables.each do |v|
|
95
90
|
sym = v.to_s.delete("@").to_sym
|
96
91
|
unless self.respond_to? sym
|
97
92
|
module_eval <<-EOS, __FILE__, __LINE__
|
@@ -106,7 +101,7 @@ module Geokit
|
|
106
101
|
end
|
107
102
|
value
|
108
103
|
end
|
109
|
-
|
104
|
+
|
110
105
|
def self.#{sym}=(obj)
|
111
106
|
@@#{sym} = obj
|
112
107
|
end
|
@@ -116,38 +111,38 @@ module Geokit
|
|
116
111
|
end
|
117
112
|
|
118
113
|
__define_accessors
|
119
|
-
|
114
|
+
|
120
115
|
# Error which is thrown in the event a geocoding error occurs.
|
121
116
|
class GeocodeError < StandardError; end
|
122
117
|
|
123
118
|
# -------------------------------------------------------------------------------------------
|
124
119
|
# Geocoder Base class -- every geocoder should inherit from this
|
125
|
-
# -------------------------------------------------------------------------------------------
|
126
|
-
|
120
|
+
# -------------------------------------------------------------------------------------------
|
121
|
+
|
127
122
|
# The Geocoder base class which defines the interface to be used by all
|
128
123
|
# other geocoders.
|
129
|
-
class Geocoder
|
124
|
+
class Geocoder
|
130
125
|
# Main method which calls the do_geocode template method which subclasses
|
131
126
|
# are responsible for implementing. Returns a populated GeoLoc or an
|
132
127
|
# empty one with a failed success code.
|
133
|
-
def self.geocode(address, options = {})
|
128
|
+
def self.geocode(address, options = {})
|
134
129
|
res = do_geocode(address, options)
|
135
130
|
return res.nil? ? GeoLoc.new : res
|
136
|
-
end
|
131
|
+
end
|
137
132
|
# Main method which calls the do_reverse_geocode template method which subclasses
|
138
133
|
# are responsible for implementing. Returns a populated GeoLoc or an
|
139
134
|
# empty one with a failed success code.
|
140
135
|
def self.reverse_geocode(latlng)
|
141
136
|
res = do_reverse_geocode(latlng)
|
142
|
-
return res.success? ? res : GeoLoc.new
|
137
|
+
return res.success? ? res : GeoLoc.new
|
143
138
|
end
|
144
|
-
|
139
|
+
|
145
140
|
# Call the geocoder service using the timeout if configured.
|
146
141
|
def self.call_geocoder_service(url)
|
147
|
-
Timeout::timeout(Geokit::Geocoders::request_timeout) { return self.do_get(url) } if Geokit::Geocoders::request_timeout
|
142
|
+
Timeout::timeout(Geokit::Geocoders::request_timeout) { return self.do_get(url) } if Geokit::Geocoders::request_timeout
|
148
143
|
return self.do_get(url)
|
149
144
|
rescue TimeoutError
|
150
|
-
return nil
|
145
|
+
return nil
|
151
146
|
end
|
152
147
|
|
153
148
|
# Not all geocoders can do reverse geocoding. So, unless the subclass explicitly overrides this method,
|
@@ -159,14 +154,14 @@ module Geokit
|
|
159
154
|
|
160
155
|
protected
|
161
156
|
|
162
|
-
def self.logger()
|
157
|
+
def self.logger()
|
163
158
|
Geokit::Geocoders::logger
|
164
159
|
end
|
165
|
-
|
160
|
+
|
166
161
|
private
|
167
|
-
|
162
|
+
|
168
163
|
# Wraps the geocoder call around a proxy if necessary.
|
169
|
-
def self.do_get(url)
|
164
|
+
def self.do_get(url)
|
170
165
|
uri = URI.parse(url)
|
171
166
|
req = Net::HTTP::Get.new(url)
|
172
167
|
req.basic_auth(uri.user, uri.password) if uri.userinfo
|
@@ -176,8 +171,8 @@ module Geokit
|
|
176
171
|
GeoKit::Geocoders::proxy_pass).start(uri.host, uri.port) { |http| http.get(uri.path + "?" + uri.query) }
|
177
172
|
return res
|
178
173
|
end
|
179
|
-
|
180
|
-
# Adds subclass' geocode method making it conveniently available through
|
174
|
+
|
175
|
+
# Adds subclass' geocode method making it conveniently available through
|
181
176
|
# the base class.
|
182
177
|
def self.inherited(clazz)
|
183
178
|
class_name = clazz.name.split('::').last
|
@@ -192,10 +187,10 @@ module Geokit
|
|
192
187
|
|
193
188
|
# -------------------------------------------------------------------------------------------
|
194
189
|
# "Regular" Address geocoders
|
195
|
-
# -------------------------------------------------------------------------------------------
|
196
|
-
|
190
|
+
# -------------------------------------------------------------------------------------------
|
191
|
+
|
197
192
|
# Geocoder CA geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_CA variable to
|
198
|
-
# contain true or false based upon whether authentication is to occur. Conforms to the
|
193
|
+
# contain true or false based upon whether authentication is to occur. Conforms to the
|
199
194
|
# interface set by the Geocoder class.
|
200
195
|
#
|
201
196
|
# Returns a response like:
|
@@ -217,15 +212,15 @@ module Geokit
|
|
217
212
|
xml = res.body
|
218
213
|
logger.debug "Geocoder.ca geocoding. Address: #{address}. Result: #{xml}"
|
219
214
|
# Parse the document.
|
220
|
-
doc = REXML::Document.new(xml)
|
215
|
+
doc = REXML::Document.new(xml)
|
221
216
|
address.lat = doc.elements['//latt'].text
|
222
217
|
address.lng = doc.elements['//longt'].text
|
223
218
|
address.success = true
|
224
219
|
return address
|
225
220
|
rescue
|
226
221
|
logger.error "Caught an error during Geocoder.ca geocoding call: "+$!
|
227
|
-
return GeoLoc.new
|
228
|
-
end
|
222
|
+
return GeoLoc.new
|
223
|
+
end
|
229
224
|
|
230
225
|
# Formats the request in the format acceptable by the CA geocoder.
|
231
226
|
def self.construct_request(location)
|
@@ -243,98 +238,124 @@ module Geokit
|
|
243
238
|
def self.add_ampersand(url)
|
244
239
|
url && url.length > 0 ? "&" : ""
|
245
240
|
end
|
246
|
-
end
|
247
|
-
|
241
|
+
end
|
242
|
+
|
248
243
|
# Geocoder Us geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_US variable to
|
249
|
-
# contain true or false based upon whether authentication is to occur. Conforms to the
|
244
|
+
# contain true or false based upon whether authentication is to occur. Conforms to the
|
250
245
|
# interface set by the Geocoder class.
|
251
246
|
class UsGeocoder < Geocoder
|
252
247
|
|
253
248
|
private
|
254
249
|
def self.do_geocode(address, options = {})
|
255
250
|
address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
|
256
|
-
|
251
|
+
|
257
252
|
query = (address_str =~ /^\d{5}(?:-\d{4})?$/ ? "zip" : "address") + "=#{Geokit::Inflector::url_escape(address_str)}"
|
258
|
-
url = if GeoKit::Geocoders::geocoder_us
|
253
|
+
url = if GeoKit::Geocoders::geocoder_us
|
259
254
|
"http://#{GeoKit::Geocoders::geocoder_us}@geocoder.us/member/service/csv/geocode"
|
260
255
|
else
|
261
256
|
"http://geocoder.us/service/csv/geocode"
|
262
257
|
end
|
263
|
-
|
264
|
-
url = "#{url}?#{query}"
|
258
|
+
|
259
|
+
url = "#{url}?#{query}"
|
265
260
|
res = self.call_geocoder_service(url)
|
266
|
-
|
261
|
+
|
267
262
|
return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
|
268
263
|
data = res.body
|
269
264
|
logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}"
|
270
265
|
array = data.chomp.split(',')
|
271
|
-
|
266
|
+
|
272
267
|
if array.length == 5
|
273
268
|
res=GeoLoc.new
|
274
269
|
res.lat,res.lng,res.city,res.state,res.zip=array
|
275
270
|
res.country_code='US'
|
276
271
|
res.success=true
|
277
272
|
return res
|
278
|
-
elsif array.length == 6
|
279
|
-
res=GeoLoc.new
|
273
|
+
elsif array.length == 6
|
274
|
+
res=GeoLoc.new
|
280
275
|
res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array
|
281
276
|
res.country_code='US'
|
282
|
-
res.success=true
|
277
|
+
res.success=true
|
283
278
|
return res
|
284
|
-
else
|
279
|
+
else
|
285
280
|
logger.info "geocoder.us was unable to geocode address: "+address
|
286
|
-
return GeoLoc.new
|
281
|
+
return GeoLoc.new
|
287
282
|
end
|
288
|
-
rescue
|
283
|
+
rescue
|
289
284
|
logger.error "Caught an error during geocoder.us geocoding call: "+$!
|
290
285
|
return GeoLoc.new
|
291
286
|
|
292
287
|
end
|
293
288
|
end
|
294
|
-
|
289
|
+
|
295
290
|
# Yahoo geocoder implementation. Requires the Geokit::Geocoders::YAHOO variable to
|
296
291
|
# contain a Yahoo API key. Conforms to the interface set by the Geocoder class.
|
297
292
|
class YahooGeocoder < Geocoder
|
298
293
|
|
299
|
-
private
|
294
|
+
private
|
300
295
|
|
301
296
|
# Template method which does the geocode lookup.
|
302
297
|
def self.do_geocode(address, options = {})
|
303
298
|
address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
|
304
|
-
url="http://
|
299
|
+
url="http://where.yahooapis.com/geocode?flags=J&appid=#{Geokit::Geocoders::yahoo}&q=#{Geokit::Inflector::url_escape(address_str)}"
|
305
300
|
res = self.call_geocoder_service(url)
|
306
301
|
return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
|
307
|
-
|
308
|
-
|
309
|
-
|
302
|
+
json = res.body
|
303
|
+
logger.debug "Yahoo geocoding. Address: #{address}. Result: #{json}"
|
304
|
+
return self.json2GeoLoc(json, address)
|
305
|
+
end
|
310
306
|
|
311
|
-
|
312
|
-
|
307
|
+
def self.json2GeoLoc(json, address)
|
308
|
+
results = MultiJson.decode(json)
|
313
309
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
res.accuracy=%w{unknown country state state city zip zip+4 street address building}.index(res.precision)
|
328
|
-
res.success=true
|
329
|
-
return res
|
330
|
-
else
|
331
|
-
logger.info "Yahoo was unable to geocode address: "+address
|
310
|
+
if results['ResultSet']['Error'] == 0
|
311
|
+
geoloc = nil
|
312
|
+
results['ResultSet']['Results'].each do |result|
|
313
|
+
extracted_geoloc = extract_geoloc(result)
|
314
|
+
if geoloc.nil?
|
315
|
+
geoloc = extracted_geoloc
|
316
|
+
else
|
317
|
+
geoloc.all.push(extracted_geoloc)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
return geoloc
|
321
|
+
else
|
322
|
+
logger.info "Yahoo was unable to geocode address: " + address
|
332
323
|
return GeoLoc.new
|
333
|
-
end
|
324
|
+
end
|
325
|
+
end
|
334
326
|
|
335
|
-
|
336
|
-
|
337
|
-
|
327
|
+
def self.extract_geoloc(result_json)
|
328
|
+
geoloc = GeoLoc.new
|
329
|
+
|
330
|
+
# basic
|
331
|
+
geoloc.lat = result_json['latitude']
|
332
|
+
geoloc.lng = result_json['longitude']
|
333
|
+
geoloc.country_code = result_json['countrycode']
|
334
|
+
geoloc.provider = 'yahoo'
|
335
|
+
|
336
|
+
# extended
|
337
|
+
geoloc.street_address = result_json['line1'].to_s.empty? ? nil : result_json['line1']
|
338
|
+
geoloc.city = result_json['city']
|
339
|
+
geoloc.state = geoloc.is_us? ? result_json['statecode'] : result_json['state']
|
340
|
+
geoloc.zip = result_json['postal']
|
341
|
+
|
342
|
+
geoloc.precision = case result_json['quality']
|
343
|
+
when 9,10 then 'country'
|
344
|
+
when 19..30 then 'state'
|
345
|
+
when 39,40 then 'city'
|
346
|
+
when 49,50 then 'neighborhood'
|
347
|
+
when 59,60,64 then 'zip'
|
348
|
+
when 74,75 then 'zip+4'
|
349
|
+
when 70..72 then 'street'
|
350
|
+
when 80..87 then 'address'
|
351
|
+
when 62,63,90,99 then 'building'
|
352
|
+
else 'unknown'
|
353
|
+
end
|
354
|
+
|
355
|
+
geoloc.accuracy = %w{unknown country state state city zip zip+4 street address building}.index(geoloc.precision)
|
356
|
+
geoloc.success = true
|
357
|
+
|
358
|
+
return geoloc
|
338
359
|
end
|
339
360
|
end
|
340
361
|
|
@@ -342,47 +363,47 @@ module Geokit
|
|
342
363
|
# http://www.geonames.org
|
343
364
|
class GeonamesGeocoder < Geocoder
|
344
365
|
|
345
|
-
private
|
346
|
-
|
366
|
+
private
|
367
|
+
|
347
368
|
# Template method which does the geocode lookup.
|
348
369
|
def self.do_geocode(address, options = {})
|
349
370
|
address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
|
350
371
|
# geonames need a space seperated search string
|
351
372
|
address_str.gsub!(/,/, " ")
|
352
373
|
params = "/postalCodeSearch?placename=#{Geokit::Inflector::url_escape(address_str)}&maxRows=10"
|
353
|
-
|
374
|
+
|
354
375
|
if(GeoKit::Geocoders::geonames)
|
355
376
|
url = "http://ws.geonames.net#{params}&username=#{GeoKit::Geocoders::geonames}"
|
356
377
|
else
|
357
378
|
url = "http://ws.geonames.org#{params}"
|
358
379
|
end
|
359
|
-
|
380
|
+
|
360
381
|
res = self.call_geocoder_service(url)
|
361
|
-
|
382
|
+
|
362
383
|
return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
|
363
|
-
|
384
|
+
|
364
385
|
xml=res.body
|
365
386
|
logger.debug "Geonames geocoding. Address: #{address}. Result: #{xml}"
|
366
387
|
doc=REXML::Document.new(xml)
|
367
|
-
|
388
|
+
|
368
389
|
if(doc.elements['//geonames/totalResultsCount'].text.to_i > 0)
|
369
390
|
res=GeoLoc.new
|
370
|
-
|
391
|
+
|
371
392
|
# only take the first result
|
372
393
|
res.lat=doc.elements['//code/lat'].text if doc.elements['//code/lat']
|
373
394
|
res.lng=doc.elements['//code/lng'].text if doc.elements['//code/lng']
|
374
395
|
res.country_code=doc.elements['//code/countryCode'].text if doc.elements['//code/countryCode']
|
375
|
-
res.provider='genomes'
|
396
|
+
res.provider='genomes'
|
376
397
|
res.city=doc.elements['//code/name'].text if doc.elements['//code/name']
|
377
398
|
res.state=doc.elements['//code/adminName1'].text if doc.elements['//code/adminName1']
|
378
399
|
res.zip=doc.elements['//code/postalcode'].text if doc.elements['//code/postalcode']
|
379
400
|
res.success=true
|
380
401
|
return res
|
381
|
-
else
|
402
|
+
else
|
382
403
|
logger.info "Geonames was unable to geocode address: "+address
|
383
404
|
return GeoLoc.new
|
384
405
|
end
|
385
|
-
|
406
|
+
|
386
407
|
rescue
|
387
408
|
logger.error "Caught an error during Geonames geocoding call: "+$!
|
388
409
|
end
|
@@ -396,18 +417,18 @@ module Geokit
|
|
396
417
|
# contain a Google API key. Conforms to the interface set by the Geocoder class.
|
397
418
|
class GoogleGeocoder < Geocoder
|
398
419
|
|
399
|
-
private
|
400
|
-
|
420
|
+
private
|
421
|
+
|
401
422
|
# Template method which does the reverse-geocode lookup.
|
402
|
-
def self.do_reverse_geocode(latlng)
|
423
|
+
def self.do_reverse_geocode(latlng)
|
403
424
|
latlng=LatLng.normalize(latlng)
|
404
425
|
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")
|
405
426
|
# 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"))
|
406
427
|
return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
|
407
428
|
xml = res.body
|
408
429
|
logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{xml}"
|
409
|
-
return self.xml2GeoLoc(xml)
|
410
|
-
end
|
430
|
+
return self.xml2GeoLoc(xml)
|
431
|
+
end
|
411
432
|
|
412
433
|
# Template method which does the geocode lookup.
|
413
434
|
#
|
@@ -416,7 +437,7 @@ module Geokit
|
|
416
437
|
# ==== OPTIONS
|
417
438
|
# * :bias - This option makes the Google Geocoder return results biased to a particular
|
418
439
|
# country or viewport. Country code biasing is achieved by passing the ccTLD
|
419
|
-
# ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's,
|
440
|
+
# ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's,
|
420
441
|
# look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder
|
421
442
|
# will be biased to results within the US (ccTLD .com).
|
422
443
|
#
|
@@ -441,9 +462,9 @@ module Geokit
|
|
441
462
|
return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
|
442
463
|
xml = res.body
|
443
464
|
logger.debug "Google geocoding. Address: #{address}. Result: #{xml}"
|
444
|
-
return self.xml2GeoLoc(xml, address)
|
465
|
+
return self.xml2GeoLoc(xml, address)
|
445
466
|
end
|
446
|
-
|
467
|
+
|
447
468
|
def self.construct_bias_string_from_options(bias)
|
448
469
|
if bias.is_a?(String) or bias.is_a?(Symbol)
|
449
470
|
# country code biasing
|
@@ -453,24 +474,24 @@ module Geokit
|
|
453
474
|
"&ll=#{bias.center.ll}&spn=#{bias.to_span.ll}"
|
454
475
|
end
|
455
476
|
end
|
456
|
-
|
477
|
+
|
457
478
|
def self.xml2GeoLoc(xml, address="")
|
458
479
|
doc=REXML::Document.new(xml)
|
459
480
|
|
460
481
|
if doc.elements['//kml/Response/Status/code'].text == '200'
|
461
482
|
geoloc = nil
|
462
|
-
# Google can return multiple results as //Placemark elements.
|
483
|
+
# Google can return multiple results as //Placemark elements.
|
463
484
|
# iterate through each and extract each placemark as a geoloc
|
464
485
|
doc.each_element('//Placemark') do |e|
|
465
486
|
extracted_geoloc = extract_placemark(e) # g is now an instance of GeoLoc
|
466
|
-
if geoloc.nil?
|
487
|
+
if geoloc.nil?
|
467
488
|
# first time through, geoloc is still nil, so we make it the geoloc we just extracted
|
468
|
-
geoloc = extracted_geoloc
|
489
|
+
geoloc = extracted_geoloc
|
469
490
|
else
|
470
|
-
# second (and subsequent) iterations, we push additional
|
471
|
-
# geolocs onto "geoloc.all"
|
472
|
-
geoloc.all.push(extracted_geoloc)
|
473
|
-
end
|
491
|
+
# second (and subsequent) iterations, we push additional
|
492
|
+
# geolocs onto "geoloc.all"
|
493
|
+
geoloc.all.push(extracted_geoloc)
|
494
|
+
end
|
474
495
|
end
|
475
496
|
return geoloc
|
476
497
|
elsif doc.elements['//kml/Response/Status/code'].text == '620'
|
@@ -486,7 +507,7 @@ module Geokit
|
|
486
507
|
rescue
|
487
508
|
logger.error "Caught an error during Google geocoding call: "+$!
|
488
509
|
return GeoLoc.new
|
489
|
-
end
|
510
|
+
end
|
490
511
|
|
491
512
|
# extracts a single geoloc from a //placemark element in the google results xml
|
492
513
|
def self.extract_placemark(doc)
|
@@ -513,14 +534,14 @@ module Geokit
|
|
513
534
|
address_details=doc.elements['.//*[local-name() = "AddressDetails"]']
|
514
535
|
res.accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0
|
515
536
|
res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy]
|
516
|
-
|
537
|
+
|
517
538
|
# google returns a set of suggested boundaries for the geocoded result
|
518
|
-
if suggested_bounds = doc.elements['//LatLonBox']
|
539
|
+
if suggested_bounds = doc.elements['//LatLonBox']
|
519
540
|
res.suggested_bounds = Bounds.normalize(
|
520
|
-
[suggested_bounds.attributes['south'], suggested_bounds.attributes['west']],
|
541
|
+
[suggested_bounds.attributes['south'], suggested_bounds.attributes['west']],
|
521
542
|
[suggested_bounds.attributes['north'], suggested_bounds.attributes['east']])
|
522
543
|
end
|
523
|
-
|
544
|
+
|
524
545
|
res.success=true
|
525
546
|
|
526
547
|
return res
|
@@ -529,16 +550,16 @@ module Geokit
|
|
529
550
|
|
530
551
|
class GoogleGeocoder3 < Geocoder
|
531
552
|
|
532
|
-
private
|
553
|
+
private
|
533
554
|
# Template method which does the reverse-geocode lookup.
|
534
|
-
def self.do_reverse_geocode(latlng)
|
555
|
+
def self.do_reverse_geocode(latlng)
|
535
556
|
latlng=LatLng.normalize(latlng)
|
536
557
|
res = self.call_geocoder_service("http://maps.google.com/maps/api/geocode/json?sensor=false&latlng=#{Geokit::Inflector::url_escape(latlng.ll)}")
|
537
558
|
return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
|
538
559
|
json = res.body
|
539
560
|
logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{json}"
|
540
|
-
return self.json2GeoLoc(json)
|
541
|
-
end
|
561
|
+
return self.json2GeoLoc(json)
|
562
|
+
end
|
542
563
|
|
543
564
|
# Template method which does the geocode lookup.
|
544
565
|
#
|
@@ -547,7 +568,7 @@ module Geokit
|
|
547
568
|
# ==== OPTIONS
|
548
569
|
# * :bias - This option makes the Google Geocoder return results biased to a particular
|
549
570
|
# country or viewport. Country code biasing is achieved by passing the ccTLD
|
550
|
-
# ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's,
|
571
|
+
# ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's,
|
551
572
|
# look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder
|
552
573
|
# will be biased to results within the US (ccTLD .com).
|
553
574
|
#
|
@@ -568,13 +589,16 @@ module Geokit
|
|
568
589
|
def self.do_geocode(address, options = {})
|
569
590
|
bias_str = options[:bias] ? construct_bias_string_from_options(options[:bias]) : ''
|
570
591
|
address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
|
592
|
+
|
571
593
|
res = self.call_geocoder_service("http://maps.google.com/maps/api/geocode/json?sensor=false&address=#{Geokit::Inflector::url_escape(address_str)}#{bias_str}")
|
572
594
|
return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
|
595
|
+
|
573
596
|
json = res.body
|
574
597
|
logger.debug "Google geocoding. Address: #{address}. Result: #{json}"
|
575
|
-
|
598
|
+
|
599
|
+
return self.json2GeoLoc(json, address)
|
576
600
|
end
|
577
|
-
|
601
|
+
|
578
602
|
def self.construct_bias_string_from_options(bias)
|
579
603
|
if bias.is_a?(String) or bias.is_a?(Symbol)
|
580
604
|
# country code biasing
|
@@ -587,13 +611,8 @@ module Geokit
|
|
587
611
|
|
588
612
|
def self.json2GeoLoc(json, address="")
|
589
613
|
ret=nil
|
590
|
-
|
591
|
-
|
592
|
-
rescue NameError => e
|
593
|
-
results=JSON.parse(json)
|
594
|
-
end
|
595
|
-
|
596
|
-
|
614
|
+
results = MultiJson.decode(json)
|
615
|
+
|
597
616
|
if results['status'] == 'OVER_QUERY_LIMIT'
|
598
617
|
raise Geokit::TooManyQueriesError
|
599
618
|
end
|
@@ -626,13 +645,19 @@ module Geokit
|
|
626
645
|
"GEOMETRIC_CENTER" => 5,
|
627
646
|
"APPROXIMATE" => 4
|
628
647
|
}
|
629
|
-
|
630
|
-
|
648
|
+
|
649
|
+
@unsorted = []
|
650
|
+
|
651
|
+
results['results'].each do |addr|
|
652
|
+
res = GeoLoc.new
|
631
653
|
res.provider = 'google3'
|
632
654
|
res.success = true
|
633
655
|
res.full_address = addr['formatted_address']
|
656
|
+
|
634
657
|
addr['address_components'].each do |comp|
|
635
658
|
case
|
659
|
+
when comp['types'].include?("subpremise")
|
660
|
+
res.sub_premise = comp['short_name']
|
636
661
|
when comp['types'].include?("street_number")
|
637
662
|
res.street_number = comp['short_name']
|
638
663
|
when comp['types'].include?("route")
|
@@ -657,16 +682,20 @@ module Geokit
|
|
657
682
|
res.accuracy = accuracy[addr['geometry']['location_type']]
|
658
683
|
res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy]
|
659
684
|
# try a few overrides where we can
|
685
|
+
if res.sub_premise
|
686
|
+
res.accuracy = 9
|
687
|
+
res.precision = 'building'
|
688
|
+
end
|
660
689
|
if res.street_name && res.precision=='city'
|
661
690
|
res.precision = 'street'
|
662
691
|
res.accuracy = 7
|
663
692
|
end
|
664
|
-
|
693
|
+
|
665
694
|
res.lat=addr['geometry']['location']['lat'].to_f
|
666
695
|
res.lng=addr['geometry']['location']['lng'].to_f
|
667
696
|
|
668
697
|
ne=Geokit::LatLng.new(
|
669
|
-
addr['geometry']['viewport']['northeast']['lat'].to_f,
|
698
|
+
addr['geometry']['viewport']['northeast']['lat'].to_f,
|
670
699
|
addr['geometry']['viewport']['northeast']['lng'].to_f
|
671
700
|
)
|
672
701
|
sw=Geokit::LatLng.new(
|
@@ -675,34 +704,34 @@ module Geokit
|
|
675
704
|
)
|
676
705
|
res.suggested_bounds = Geokit::Bounds.new(sw,ne)
|
677
706
|
|
678
|
-
|
679
|
-
ret.all.push(res)
|
680
|
-
else
|
681
|
-
ret=res
|
682
|
-
end
|
707
|
+
@unsorted << res
|
683
708
|
end
|
684
|
-
|
709
|
+
|
710
|
+
all = @unsorted.sort_by { |a| a.accuracy }.reverse
|
711
|
+
encoded = all.first
|
712
|
+
encoded.all = all
|
713
|
+
return encoded
|
685
714
|
end
|
686
715
|
end
|
687
|
-
|
716
|
+
|
688
717
|
class FCCGeocoder < Geocoder
|
689
718
|
|
690
|
-
private
|
719
|
+
private
|
691
720
|
# Template method which does the reverse-geocode lookup.
|
692
|
-
def self.do_reverse_geocode(latlng)
|
721
|
+
def self.do_reverse_geocode(latlng)
|
693
722
|
latlng=LatLng.normalize(latlng)
|
694
723
|
res = self.call_geocoder_service("http://data.fcc.gov/api/block/find?format=json&latitude=#{Geokit::Inflector::url_escape(latlng.lat.to_s)}&longitude=#{Geokit::Inflector::url_escape(latlng.lng.to_s)}")
|
695
724
|
return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
|
696
725
|
json = res.body
|
697
726
|
logger.debug "FCC reverse-geocoding. LL: #{latlng}. Result: #{json}"
|
698
|
-
return self.json2GeoLoc(json)
|
699
|
-
end
|
727
|
+
return self.json2GeoLoc(json)
|
728
|
+
end
|
700
729
|
|
701
730
|
# Template method which does the geocode lookup.
|
702
731
|
#
|
703
732
|
# ==== EXAMPLES
|
704
733
|
# ll=GeoKit::LatLng.new(40, -85)
|
705
|
-
# Geokit::Geocoders::FCCGeocoder.geocode(ll) #
|
734
|
+
# Geokit::Geocoders::FCCGeocoder.geocode(ll) #
|
706
735
|
|
707
736
|
# JSON result looks like this
|
708
737
|
# => {"County"=>{"name"=>"Wayne", "FIPS"=>"18177"},
|
@@ -712,13 +741,9 @@ module Geokit
|
|
712
741
|
# "status"=>"OK"}
|
713
742
|
|
714
743
|
def self.json2GeoLoc(json, address="")
|
715
|
-
ret=nil
|
716
|
-
|
717
|
-
|
718
|
-
rescue NameError => e
|
719
|
-
results=JSON.parse(json)
|
720
|
-
end
|
721
|
-
|
744
|
+
ret = nil
|
745
|
+
results = MultiJson.decode(json)
|
746
|
+
|
722
747
|
if results.has_key?('Err') and results['Err']["msg"] == 'There are no results for this location'
|
723
748
|
return GeoLoc.new
|
724
749
|
end
|
@@ -744,11 +769,11 @@ module Geokit
|
|
744
769
|
# -------------------------------------------------------------------------------------------
|
745
770
|
# IP Geocoders
|
746
771
|
# -------------------------------------------------------------------------------------------
|
747
|
-
|
772
|
+
|
748
773
|
# Provides geocoding based upon an IP address. The underlying web service is geoplugin.net
|
749
774
|
class GeoPluginGeocoder < Geocoder
|
750
775
|
private
|
751
|
-
|
776
|
+
|
752
777
|
def self.do_geocode(ip, options = {})
|
753
778
|
return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
|
754
779
|
response = self.call_geocoder_service("http://www.geoplugin.net/xml.gp?ip=#{ip}")
|
@@ -775,7 +800,7 @@ module Geokit
|
|
775
800
|
# Provides geocoding based upon an IP address. The underlying web service is a hostip.info
|
776
801
|
# which sources their data through a combination of publicly available information as well
|
777
802
|
# as community contributions.
|
778
|
-
class IpGeocoder < Geocoder
|
803
|
+
class IpGeocoder < Geocoder
|
779
804
|
|
780
805
|
# A number of non-routable IP ranges.
|
781
806
|
#
|
@@ -785,24 +810,24 @@ module Geokit
|
|
785
810
|
# The bogon list: http://www.cymru.com/Documents/bogon-list.html
|
786
811
|
|
787
812
|
NON_ROUTABLE_IP_RANGES = [
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
813
|
+
IPAddr.new('0.0.0.0/8'), # "This" Network
|
814
|
+
IPAddr.new('10.0.0.0/8'), # Private-Use Networks
|
815
|
+
IPAddr.new('14.0.0.0/8'), # Public-Data Networks
|
816
|
+
IPAddr.new('127.0.0.0/8'), # Loopback
|
817
|
+
IPAddr.new('169.254.0.0/16'), # Link local
|
818
|
+
IPAddr.new('172.16.0.0/12'), # Private-Use Networks
|
819
|
+
IPAddr.new('192.0.2.0/24'), # Test-Net
|
820
|
+
IPAddr.new('192.168.0.0/16'), # Private-Use Networks
|
821
|
+
IPAddr.new('198.18.0.0/15'), # Network Interconnect Device Benchmark Testing
|
822
|
+
IPAddr.new('224.0.0.0/4'), # Multicast
|
823
|
+
IPAddr.new('240.0.0.0/4') # Reserved for future use
|
799
824
|
].freeze
|
800
825
|
|
801
|
-
private
|
826
|
+
private
|
802
827
|
|
803
828
|
# Given an IP address, returns a GeoLoc instance which contains latitude,
|
804
|
-
# longitude, city, and country code. Sets the success attribute to false if the ip
|
805
|
-
# parameter does not match an ip address.
|
829
|
+
# longitude, city, and country code. Sets the success attribute to false if the ip
|
830
|
+
# parameter does not match an ip address.
|
806
831
|
def self.do_geocode(ip, options = {})
|
807
832
|
return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
|
808
833
|
return GeoLoc.new if self.private_ip_address?(ip)
|
@@ -828,7 +853,7 @@ module Geokit
|
|
828
853
|
res.provider = 'hostip'
|
829
854
|
res.city, res.state = yaml['City'].split(', ')
|
830
855
|
country, res.country_code = yaml['Country'].split(' (')
|
831
|
-
res.lat = yaml['Latitude']
|
856
|
+
res.lat = yaml['Latitude']
|
832
857
|
res.lng = yaml['Longitude']
|
833
858
|
res.country_code.chop!
|
834
859
|
res.success = !(res.city =~ /\(.+\)/)
|
@@ -841,36 +866,36 @@ module Geokit
|
|
841
866
|
# the geocoding service. Such queries can occur frequently during
|
842
867
|
# integration tests.
|
843
868
|
def self.private_ip_address?(ip)
|
844
|
-
|
869
|
+
return NON_ROUTABLE_IP_RANGES.any? { |range| range.include?(ip) }
|
845
870
|
end
|
846
871
|
end
|
847
|
-
|
872
|
+
|
848
873
|
# -------------------------------------------------------------------------------------------
|
849
874
|
# The Multi Geocoder
|
850
|
-
# -------------------------------------------------------------------------------------------
|
851
|
-
|
875
|
+
# -------------------------------------------------------------------------------------------
|
876
|
+
|
852
877
|
# Provides methods to geocode with a variety of geocoding service providers, plus failover
|
853
878
|
# among providers in the order you configure. When 2nd parameter is set 'true', perform
|
854
879
|
# ip location lookup with 'address' as the ip address.
|
855
|
-
#
|
880
|
+
#
|
856
881
|
# Goal:
|
857
882
|
# - homogenize the results of multiple geocoders
|
858
|
-
#
|
883
|
+
#
|
859
884
|
# Limitations:
|
860
885
|
# - currently only provides the first result. Sometimes geocoders will return multiple results.
|
861
886
|
# - currently discards the "accuracy" component of the geocoding calls
|
862
|
-
class MultiGeocoder < Geocoder
|
887
|
+
class MultiGeocoder < Geocoder
|
863
888
|
|
864
889
|
private
|
865
|
-
# This method will call one or more geocoders in the order specified in the
|
890
|
+
# This method will call one or more geocoders in the order specified in the
|
866
891
|
# configuration until one of the geocoders work.
|
867
|
-
#
|
892
|
+
#
|
868
893
|
# The failover approach is crucial for production-grade apps, but is rarely used.
|
869
|
-
# 98% of your geocoding calls will be successful with the first call
|
894
|
+
# 98% of your geocoding calls will be successful with the first call
|
870
895
|
def self.do_geocode(address, options = {})
|
871
896
|
geocode_ip = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.match(address)
|
872
897
|
provider_order = geocode_ip ? Geokit::Geocoders::ip_provider_order : Geokit::Geocoders::provider_order
|
873
|
-
|
898
|
+
|
874
899
|
provider_order.each do |provider|
|
875
900
|
begin
|
876
901
|
klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder"
|
@@ -883,8 +908,8 @@ module Geokit
|
|
883
908
|
# If we get here, we failed completely.
|
884
909
|
GeoLoc.new
|
885
910
|
end
|
886
|
-
|
887
|
-
# This method will call one or more geocoders in the order specified in the
|
911
|
+
|
912
|
+
# This method will call one or more geocoders in the order specified in the
|
888
913
|
# configuration until one of the geocoders work, only this time it's going
|
889
914
|
# to try to reverse geocode a geographical point.
|
890
915
|
def self.do_reverse_geocode(latlng)
|
@@ -900,6 +925,6 @@ module Geokit
|
|
900
925
|
# If we get here, we failed completely.
|
901
926
|
GeoLoc.new
|
902
927
|
end
|
903
|
-
end
|
928
|
+
end
|
904
929
|
end
|
905
930
|
end
|