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 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