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