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 CHANGED
@@ -1,24 +1,10 @@
1
- # -*- ruby -*-
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
2
3
 
3
- require "rake/gempackagetask"
4
- require 'rubygems'
5
- require 'hoe'
6
- require './lib/geokit.rb'
4
+ task :default => :test
7
5
 
8
-
9
- # undefined method `empty?' for nil:NilClass
10
- # /Library/Ruby/Site/1.8/rubygems/specification.rb:886:in `validate'
11
- class NilClass
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
@@ -1,7 +1,6 @@
1
1
  module Geokit
2
- VERSION = '1.6.0'
3
2
  # These defaults are used in Geokit::Mappable.distance_to and in acts_as_mappable
4
- @@default_units = :miles
3
+ @@default_units = :miles
5
4
  @@default_formula = :sphere
6
5
 
7
6
  [:default_units, :default_formula].each do |sym|
@@ -5,25 +5,20 @@ require 'yaml'
5
5
  require 'timeout'
6
6
  require 'logger'
7
7
 
8
- # do this just in case
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://api.local.yahoo.com/MapsService/V1/geocode?appid=#{Geokit::Geocoders::yahoo}&location=#{Geokit::Inflector::url_escape(address_str)}"
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
- xml = res.body
308
- doc = REXML::Document.new(xml)
309
- logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}"
302
+ json = res.body
303
+ logger.debug "Yahoo geocoding. Address: #{address}. Result: #{json}"
304
+ return self.json2GeoLoc(json, address)
305
+ end
310
306
 
311
- if doc.elements['//ResultSet']
312
- res=GeoLoc.new
307
+ def self.json2GeoLoc(json, address)
308
+ results = MultiJson.decode(json)
313
309
 
314
- #basic
315
- res.lat=doc.elements['//Latitude'].text
316
- res.lng=doc.elements['//Longitude'].text
317
- res.country_code=doc.elements['//Country'].text
318
- res.provider='yahoo'
319
-
320
- #extended - false if not available
321
- res.city=doc.elements['//City'].text if doc.elements['//City'] && doc.elements['//City'].text != nil
322
- res.state=doc.elements['//State'].text if doc.elements['//State'] && doc.elements['//State'].text != nil
323
- res.zip=doc.elements['//Zip'].text if doc.elements['//Zip'] && doc.elements['//Zip'].text != nil
324
- res.street_address=doc.elements['//Address'].text if doc.elements['//Address'] && doc.elements['//Address'].text != nil
325
- res.precision=doc.elements['//Result'].attributes['precision'] if doc.elements['//Result']
326
- # set the accuracy as google does (added by Andruby)
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
- rescue
336
- logger.info "Caught an error during Yahoo geocoding call: "+$!
337
- return GeoLoc.new
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
- return self.json2GeoLoc(json, address)
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
- begin
591
- results=::ActiveSupport::JSON.decode(json)
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
- results['results'].sort_by{|a|accuracy[a['geometry']['location_type']]}.reverse.each do |addr|
630
- res=GeoLoc.new
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
- if ret
679
- ret.all.push(res)
680
- else
681
- ret=res
682
- end
707
+ @unsorted << res
683
708
  end
684
- return ret
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
- begin
717
- results=::ActiveSupport::JSON.decode(json)
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
- IPAddr.new('0.0.0.0/8'), # "This" Network
789
- IPAddr.new('10.0.0.0/8'), # Private-Use Networks
790
- IPAddr.new('14.0.0.0/8'), # Public-Data Networks
791
- IPAddr.new('127.0.0.0/8'), # Loopback
792
- IPAddr.new('169.254.0.0/16'), # Link local
793
- IPAddr.new('172.16.0.0/12'), # Private-Use Networks
794
- IPAddr.new('192.0.2.0/24'), # Test-Net
795
- IPAddr.new('192.168.0.0/16'), # Private-Use Networks
796
- IPAddr.new('198.18.0.0/15'), # Network Interconnect Device Benchmark Testing
797
- IPAddr.new('224.0.0.0/4'), # Multicast
798
- IPAddr.new('240.0.0.0/4') # Reserved for future use
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
- return NON_ROUTABLE_IP_RANGES.any? { |range| range.include?(ip) }
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