geokit 1.6.5 → 1.6.6

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.
Files changed (45) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +13 -0
  4. data/CHANGELOG.md +92 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +25 -0
  7. data/Manifest.txt +21 -0
  8. data/README.markdown +179 -176
  9. data/Rakefile +8 -1
  10. data/ext/mkrf_conf.rb +15 -0
  11. data/geokit.gemspec +33 -0
  12. data/lib/geokit/geocoders.rb +7 -774
  13. data/lib/geokit/inflectors.rb +38 -0
  14. data/lib/geokit/mappable.rb +61 -3
  15. data/lib/geokit/multi_geocoder.rb +61 -0
  16. data/lib/geokit/services/ca_geocoder.rb +55 -0
  17. data/lib/geokit/services/fcc.rb +57 -0
  18. data/lib/geokit/services/geo_plugin.rb +31 -0
  19. data/lib/geokit/services/geonames.rb +53 -0
  20. data/lib/geokit/services/google.rb +158 -0
  21. data/lib/geokit/services/google3.rb +202 -0
  22. data/lib/geokit/services/ip.rb +103 -0
  23. data/lib/geokit/services/openstreetmap.rb +119 -0
  24. data/lib/geokit/services/us_geocoder.rb +50 -0
  25. data/lib/geokit/services/yahoo.rb +75 -0
  26. data/lib/geokit/version.rb +3 -0
  27. data/test/helper.rb +92 -0
  28. data/test/test_base_geocoder.rb +1 -15
  29. data/test/test_bounds.rb +1 -2
  30. data/test/test_ca_geocoder.rb +1 -1
  31. data/test/test_geoloc.rb +35 -5
  32. data/test/test_geoplugin_geocoder.rb +1 -2
  33. data/test/test_google_geocoder.rb +39 -2
  34. data/test/test_google_geocoder3.rb +55 -3
  35. data/test/test_google_reverse_geocoder.rb +1 -1
  36. data/test/test_inflector.rb +5 -3
  37. data/test/test_ipgeocoder.rb +25 -1
  38. data/test/test_latlng.rb +1 -3
  39. data/test/test_multi_geocoder.rb +1 -1
  40. data/test/test_multi_ip_geocoder.rb +1 -1
  41. data/test/test_openstreetmap_geocoder.rb +161 -0
  42. data/test/test_polygon_contains.rb +101 -0
  43. data/test/test_us_geocoder.rb +1 -1
  44. data/test/test_yahoo_geocoder.rb +18 -1
  45. metadata +164 -83
data/Rakefile CHANGED
@@ -1,10 +1,17 @@
1
1
  require "bundler/gem_tasks"
2
2
  require 'rake/testtask'
3
3
 
4
- task :default => :test
4
+ task :default do
5
+ end
5
6
 
6
7
  Rake::TestTask.new do |t|
7
8
  t.libs << "test"
8
9
  t.test_files = FileList['test/test*.rb']
9
10
  t.verbose = true
10
11
  end
12
+
13
+ desc "Generate SimpleCov test coverage and open in your browser"
14
+ task :coverage do
15
+ ENV['COVERAGE'] = 'true'
16
+ Rake::Task['test'].invoke
17
+ end
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'rubygems/command.rb'
3
+ require 'rubygems/dependency_installer.rb'
4
+ begin
5
+ Gem::Command.build_args = ARGV
6
+ rescue NoMethodError
7
+ end
8
+ inst = Gem::DependencyInstaller.new
9
+ begin
10
+ if RUBY_VERSION < "1.9"
11
+ inst.install "iconv"
12
+ end
13
+ rescue
14
+ exit(1)
15
+ end
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'geokit/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "geokit"
8
+ spec.version = Geokit::VERSION
9
+ spec.authors = ["Michael Noack, James Cox, Andre Lewis & Bill Eisenhauer"]
10
+ spec.email = ["michael+geokit@noack.com.au"]
11
+ spec.description = %q{Geokit provides geocoding and distance calculation in an easy-to-use API}
12
+ spec.summary = %q{Geokit: encoding and distance calculation gem}
13
+ spec.homepage = "http://github.com/geokit/geokit"
14
+ spec.license = "MIT"
15
+
16
+ spec.has_rdoc = true
17
+ spec.rdoc_options = ["--main", "README.markdown"]
18
+ spec.extra_rdoc_files = ["README.markdown"]
19
+ spec.extensions = 'ext/mkrf_conf.rb'
20
+
21
+ spec.files = `git ls-files`.split($/)
22
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_dependency "multi_json", ">= 1.3.2"
27
+ spec.add_development_dependency "bundler", "> 1.0"
28
+ spec.add_development_dependency 'simplecov'
29
+ spec.add_development_dependency "simplecov-rcov"
30
+ spec.add_development_dependency 'rake'
31
+ spec.add_development_dependency 'mocha'
32
+ spec.add_development_dependency 'coveralls'
33
+ end
@@ -8,46 +8,10 @@ require 'logger'
8
8
  require 'multi_json'
9
9
 
10
10
  module Geokit
11
+ require File.join(File.dirname(__FILE__), 'inflectors')
11
12
 
12
13
  class TooManyQueriesError < StandardError; end
13
14
 
14
- module Inflector
15
-
16
- extend self
17
-
18
- def titleize(word)
19
- humanize(underscore(word)).gsub(/\b([a-z])/u) { $1.capitalize }
20
- end
21
-
22
- def underscore(camel_cased_word)
23
- camel_cased_word.to_s.gsub(/::/, '/').
24
- gsub(/([A-Z]+)([A-Z][a-z])/u,'\1_\2').
25
- gsub(/([a-z\d])([A-Z])/u,'\1_\2').
26
- tr("-", "_").
27
- downcase
28
- end
29
-
30
- def humanize(lower_case_and_underscored_word)
31
- lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
32
- end
33
-
34
- def snake_case(s)
35
- return s.downcase if s =~ /^[A-Z]+$/u
36
- s.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/u, '_\&') =~ /_*(.*)/
37
- return $+.downcase
38
-
39
- end
40
-
41
- def url_escape(s)
42
- s.gsub(/([^ a-zA-Z0-9_.-]+)/nu) do
43
- '%' + $1.unpack('H2' * $1.size).join('%').upcase
44
- end.tr(' ', '+')
45
- end
46
-
47
- def camelize(str)
48
- str.split('_').map {|w| w.capitalize}.join
49
- end
50
- end
51
15
 
52
16
  # Contains a range of geocoders:
53
17
  #
@@ -76,6 +40,9 @@ module Geokit
76
40
  @@request_timeout = nil
77
41
  @@yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
78
42
  @@google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
43
+ @@google_client_id = nil
44
+ @@google_cryptographic_key = nil
45
+ @@google_channel = nil
79
46
  @@geocoder_us = false
80
47
  @@geocoder_ca = false
81
48
  @@geonames = false
@@ -84,6 +51,7 @@ module Geokit
84
51
  @@logger=Logger.new(STDOUT)
85
52
  @@logger.level=Logger::INFO
86
53
  @@domain = nil
54
+ @@osm = 'REPLACE_WITH_YOUR_OSM_KEY' #if needed
87
55
 
88
56
  def self.__define_accessors
89
57
  class_variables.each do |v|
@@ -188,743 +156,8 @@ module Geokit
188
156
  # -------------------------------------------------------------------------------------------
189
157
  # "Regular" Address geocoders
190
158
  # -------------------------------------------------------------------------------------------
159
+ Dir[File.join(File.dirname(__FILE__), "/services/*.rb")].each {|f| require f}
191
160
 
192
- # Geocoder CA geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_CA variable to
193
- # contain true or false based upon whether authentication is to occur. Conforms to the
194
- # interface set by the Geocoder class.
195
- #
196
- # Returns a response like:
197
- # <?xml version="1.0" encoding="UTF-8" ?>
198
- # <geodata>
199
- # <latt>49.243086</latt>
200
- # <longt>-123.153684</longt>
201
- # </geodata>
202
- class CaGeocoder < Geocoder
203
-
204
- private
205
-
206
- # Template method which does the geocode lookup.
207
- def self.do_geocode(address, options = {})
208
- raise ArgumentError('Geocoder.ca requires a GeoLoc argument') unless address.is_a?(GeoLoc)
209
- url = construct_request(address)
210
- res = self.call_geocoder_service(url)
211
- return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
212
- xml = res.body
213
- logger.debug "Geocoder.ca geocoding. Address: #{address}. Result: #{xml}"
214
- # Parse the document.
215
- doc = REXML::Document.new(xml)
216
- address.lat = doc.elements['//latt'].text
217
- address.lng = doc.elements['//longt'].text
218
- address.success = true
219
- return address
220
- rescue
221
- logger.error "Caught an error during Geocoder.ca geocoding call: "+$!
222
- return GeoLoc.new
223
- end
224
-
225
- # Formats the request in the format acceptable by the CA geocoder.
226
- def self.construct_request(location)
227
- url = ""
228
- url += add_ampersand(url) + "stno=#{location.street_number}" if location.street_address
229
- url += add_ampersand(url) + "addresst=#{Geokit::Inflector::url_escape(location.street_name)}" if location.street_address
230
- url += add_ampersand(url) + "city=#{Geokit::Inflector::url_escape(location.city)}" if location.city
231
- url += add_ampersand(url) + "prov=#{location.state}" if location.state
232
- url += add_ampersand(url) + "postal=#{location.zip}" if location.zip
233
- url += add_ampersand(url) + "auth=#{Geokit::Geocoders::geocoder_ca}" if Geokit::Geocoders::geocoder_ca
234
- url += add_ampersand(url) + "geoit=xml"
235
- 'http://geocoder.ca/?' + url
236
- end
237
-
238
- def self.add_ampersand(url)
239
- url && url.length > 0 ? "&" : ""
240
- end
241
- end
242
-
243
- # Geocoder Us geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_US variable to
244
- # contain true or false based upon whether authentication is to occur. Conforms to the
245
- # interface set by the Geocoder class.
246
- class UsGeocoder < Geocoder
247
-
248
- private
249
- def self.do_geocode(address, options = {})
250
- address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
251
-
252
- query = (address_str =~ /^\d{5}(?:-\d{4})?$/ ? "zip" : "address") + "=#{Geokit::Inflector::url_escape(address_str)}"
253
- url = if GeoKit::Geocoders::geocoder_us
254
- "http://#{GeoKit::Geocoders::geocoder_us}@geocoder.us/member/service/csv/geocode"
255
- else
256
- "http://geocoder.us/service/csv/geocode"
257
- end
258
-
259
- url = "#{url}?#{query}"
260
- res = self.call_geocoder_service(url)
261
-
262
- return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
263
- data = res.body
264
- logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}"
265
- array = data.chomp.split(',')
266
-
267
- if array.length == 5
268
- res=GeoLoc.new
269
- res.lat,res.lng,res.city,res.state,res.zip=array
270
- res.country_code='US'
271
- res.success=true
272
- return res
273
- elsif array.length == 6
274
- res=GeoLoc.new
275
- res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array
276
- res.country_code='US'
277
- res.success=true
278
- return res
279
- else
280
- logger.info "geocoder.us was unable to geocode address: "+address
281
- return GeoLoc.new
282
- end
283
- rescue
284
- logger.error "Caught an error during geocoder.us geocoding call: "+$!
285
- return GeoLoc.new
286
-
287
- end
288
- end
289
-
290
- # Yahoo geocoder implementation. Requires the Geokit::Geocoders::YAHOO variable to
291
- # contain a Yahoo API key. Conforms to the interface set by the Geocoder class.
292
- class YahooGeocoder < Geocoder
293
-
294
- private
295
-
296
- # Template method which does the geocode lookup.
297
- def self.do_geocode(address, options = {})
298
- address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
299
- url="http://where.yahooapis.com/geocode?flags=J&appid=#{Geokit::Geocoders::yahoo}&q=#{Geokit::Inflector::url_escape(address_str)}"
300
- res = self.call_geocoder_service(url)
301
- return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
302
- json = res.body
303
- logger.debug "Yahoo geocoding. Address: #{address}. Result: #{json}"
304
- return self.json2GeoLoc(json, address)
305
- end
306
-
307
- def self.json2GeoLoc(json, address)
308
- results = MultiJson.decode(json)
309
-
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
323
- return GeoLoc.new
324
- end
325
- end
326
-
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
359
- end
360
- end
361
-
362
- # Another geocoding web service
363
- # http://www.geonames.org
364
- class GeonamesGeocoder < Geocoder
365
-
366
- private
367
-
368
- # Template method which does the geocode lookup.
369
- def self.do_geocode(address, options = {})
370
- address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
371
- # geonames need a space seperated search string
372
- address_str.gsub!(/,/, " ")
373
- params = "/postalCodeSearch?placename=#{Geokit::Inflector::url_escape(address_str)}&maxRows=10"
374
-
375
- if(GeoKit::Geocoders::geonames)
376
- url = "http://ws.geonames.net#{params}&username=#{GeoKit::Geocoders::geonames}"
377
- else
378
- url = "http://ws.geonames.org#{params}"
379
- end
380
-
381
- res = self.call_geocoder_service(url)
382
-
383
- return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
384
-
385
- xml=res.body
386
- logger.debug "Geonames geocoding. Address: #{address}. Result: #{xml}"
387
- doc=REXML::Document.new(xml)
388
-
389
- if(doc.elements['//geonames/totalResultsCount'].text.to_i > 0)
390
- res=GeoLoc.new
391
-
392
- # only take the first result
393
- res.lat=doc.elements['//code/lat'].text if doc.elements['//code/lat']
394
- res.lng=doc.elements['//code/lng'].text if doc.elements['//code/lng']
395
- res.country_code=doc.elements['//code/countryCode'].text if doc.elements['//code/countryCode']
396
- res.provider='genomes'
397
- res.city=doc.elements['//code/name'].text if doc.elements['//code/name']
398
- res.state=doc.elements['//code/adminName1'].text if doc.elements['//code/adminName1']
399
- res.zip=doc.elements['//code/postalcode'].text if doc.elements['//code/postalcode']
400
- res.success=true
401
- return res
402
- else
403
- logger.info "Geonames was unable to geocode address: "+address
404
- return GeoLoc.new
405
- end
406
-
407
- rescue
408
- logger.error "Caught an error during Geonames geocoding call: "+$!
409
- end
410
- end
411
-
412
- # -------------------------------------------------------------------------------------------
413
- # Address geocoders that also provide reverse geocoding
414
- # -------------------------------------------------------------------------------------------
415
-
416
- # Google geocoder implementation. Requires the Geokit::Geocoders::GOOGLE variable to
417
- # contain a Google API key. Conforms to the interface set by the Geocoder class.
418
- class GoogleGeocoder < Geocoder
419
-
420
- private
421
-
422
- # Template method which does the reverse-geocode lookup.
423
- def self.do_reverse_geocode(latlng)
424
- latlng=LatLng.normalize(latlng)
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")
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"))
427
- return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
428
- xml = res.body
429
- logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{xml}"
430
- return self.xml2GeoLoc(xml)
431
- end
432
-
433
- # Template method which does the geocode lookup.
434
- #
435
- # Supports viewport/country code biasing
436
- #
437
- # ==== OPTIONS
438
- # * :bias - This option makes the Google Geocoder return results biased to a particular
439
- # country or viewport. Country code biasing is achieved by passing the ccTLD
440
- # ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's,
441
- # look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder
442
- # will be biased to results within the US (ccTLD .com).
443
- #
444
- # If you'd like the Google Geocoder to prefer results within a given viewport,
445
- # you can pass a Geokit::Bounds object as the :bias value.
446
- #
447
- # ==== EXAMPLES
448
- # # By default, the geocoder will return Syracuse, NY
449
- # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse').country_code # => 'US'
450
- # # With country code biasing, it returns Syracuse in Sicily, Italy
451
- # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse', :bias => :it).country_code # => 'IT'
452
- #
453
- # # By default, the geocoder will return Winnetka, IL
454
- # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka').state # => 'IL'
455
- # # When biased to an bounding box around California, it will now return the Winnetka neighbourhood, CA
456
- # bounds = Geokit::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487])
457
- # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka', :bias => bounds).state # => 'CA'
458
- def self.do_geocode(address, options = {})
459
- bias_str = options[:bias] ? construct_bias_string_from_options(options[:bias]) : ''
460
- address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
461
- res = self.call_geocoder_service("http://maps.google.com/maps/geo?q=#{Geokit::Inflector::url_escape(address_str)}&output=xml#{bias_str}&key=#{Geokit::Geocoders::google}&oe=utf-8")
462
- return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
463
- xml = res.body
464
- logger.debug "Google geocoding. Address: #{address}. Result: #{xml}"
465
- return self.xml2GeoLoc(xml, address)
466
- end
467
-
468
- def self.construct_bias_string_from_options(bias)
469
- if bias.is_a?(String) or bias.is_a?(Symbol)
470
- # country code biasing
471
- "&gl=#{bias.to_s.downcase}"
472
- elsif bias.is_a?(Bounds)
473
- # viewport biasing
474
- "&ll=#{bias.center.ll}&spn=#{bias.to_span.ll}"
475
- end
476
- end
477
-
478
- def self.xml2GeoLoc(xml, address="")
479
- doc=REXML::Document.new(xml)
480
-
481
- if doc.elements['//kml/Response/Status/code'].text == '200'
482
- geoloc = nil
483
- # Google can return multiple results as //Placemark elements.
484
- # iterate through each and extract each placemark as a geoloc
485
- doc.each_element('//Placemark') do |e|
486
- extracted_geoloc = extract_placemark(e) # g is now an instance of GeoLoc
487
- if geoloc.nil?
488
- # first time through, geoloc is still nil, so we make it the geoloc we just extracted
489
- geoloc = extracted_geoloc
490
- else
491
- # second (and subsequent) iterations, we push additional
492
- # geolocs onto "geoloc.all"
493
- geoloc.all.push(extracted_geoloc)
494
- end
495
- end
496
- return geoloc
497
- elsif doc.elements['//kml/Response/Status/code'].text == '620'
498
- raise Geokit::TooManyQueriesError
499
- else
500
- logger.info "Google was unable to geocode address: "+address
501
- return GeoLoc.new
502
- end
503
-
504
- rescue Geokit::TooManyQueriesError
505
- # re-raise because of other rescue
506
- raise Geokit::TooManyQueriesError, "Google returned a 620 status, too many queries. The given key has gone over the requests limit in the 24 hour period or has submitted too many requests in too short a period of time. If you're sending multiple requests in parallel or in a tight loop, use a timer or pause in your code to make sure you don't send the requests too quickly."
507
- rescue
508
- logger.error "Caught an error during Google geocoding call: "+$!
509
- return GeoLoc.new
510
- end
511
-
512
- # extracts a single geoloc from a //placemark element in the google results xml
513
- def self.extract_placemark(doc)
514
- res = GeoLoc.new
515
- coordinates=doc.elements['.//coordinates'].text.to_s.split(',')
516
-
517
- #basics
518
- res.lat=coordinates[1]
519
- res.lng=coordinates[0]
520
- res.country_code=doc.elements['.//CountryNameCode'].text if doc.elements['.//CountryNameCode']
521
- res.provider='google'
522
-
523
- #extended -- false if not not available
524
- res.city = doc.elements['.//LocalityName'].text if doc.elements['.//LocalityName']
525
- res.state = doc.elements['.//AdministrativeAreaName'].text if doc.elements['.//AdministrativeAreaName']
526
- res.province = doc.elements['.//SubAdministrativeAreaName'].text if doc.elements['.//SubAdministrativeAreaName']
527
- res.full_address = doc.elements['.//address'].text if doc.elements['.//address'] # google provides it
528
- res.zip = doc.elements['.//PostalCodeNumber'].text if doc.elements['.//PostalCodeNumber']
529
- res.street_address = doc.elements['.//ThoroughfareName'].text if doc.elements['.//ThoroughfareName']
530
- res.country = doc.elements['.//CountryName'].text if doc.elements['.//CountryName']
531
- res.district = doc.elements['.//DependentLocalityName'].text if doc.elements['.//DependentLocalityName']
532
- # Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country
533
- # For Google, 1=low accuracy, 8=high accuracy
534
- address_details=doc.elements['.//*[local-name() = "AddressDetails"]']
535
- res.accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0
536
- res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy]
537
-
538
- # google returns a set of suggested boundaries for the geocoded result
539
- if suggested_bounds = doc.elements['//LatLonBox']
540
- res.suggested_bounds = Bounds.normalize(
541
- [suggested_bounds.attributes['south'], suggested_bounds.attributes['west']],
542
- [suggested_bounds.attributes['north'], suggested_bounds.attributes['east']])
543
- end
544
-
545
- res.success=true
546
-
547
- return res
548
- end
549
- end
550
-
551
- class GoogleGeocoder3 < Geocoder
552
-
553
- private
554
- # Template method which does the reverse-geocode lookup.
555
- def self.do_reverse_geocode(latlng)
556
- latlng=LatLng.normalize(latlng)
557
- res = self.call_geocoder_service("http://maps.google.com/maps/api/geocode/json?sensor=false&latlng=#{Geokit::Inflector::url_escape(latlng.ll)}")
558
- return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
559
- json = res.body
560
- logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{json}"
561
- return self.json2GeoLoc(json)
562
- end
563
-
564
- # Template method which does the geocode lookup.
565
- #
566
- # Supports viewport/country code biasing
567
- #
568
- # ==== OPTIONS
569
- # * :bias - This option makes the Google Geocoder return results biased to a particular
570
- # country or viewport. Country code biasing is achieved by passing the ccTLD
571
- # ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's,
572
- # look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder
573
- # will be biased to results within the US (ccTLD .com).
574
- #
575
- # If you'd like the Google Geocoder to prefer results within a given viewport,
576
- # you can pass a Geokit::Bounds object as the :bias value.
577
- #
578
- # ==== EXAMPLES
579
- # # By default, the geocoder will return Syracuse, NY
580
- # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse').country_code # => 'US'
581
- # # With country code biasing, it returns Syracuse in Sicily, Italy
582
- # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse', :bias => :it).country_code # => 'IT'
583
- #
584
- # # By default, the geocoder will return Winnetka, IL
585
- # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka').state # => 'IL'
586
- # # When biased to an bounding box around California, it will now return the Winnetka neighbourhood, CA
587
- # bounds = Geokit::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487])
588
- # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka', :bias => bounds).state # => 'CA'
589
- def self.do_geocode(address, options = {})
590
- bias_str = options[:bias] ? construct_bias_string_from_options(options[:bias]) : ''
591
- address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
592
-
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}")
594
- return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
595
-
596
- json = res.body
597
- logger.debug "Google geocoding. Address: #{address}. Result: #{json}"
598
-
599
- return self.json2GeoLoc(json, address)
600
- end
601
-
602
- def self.construct_bias_string_from_options(bias)
603
- if bias.is_a?(String) or bias.is_a?(Symbol)
604
- # country code biasing
605
- "&region=#{bias.to_s.downcase}"
606
- elsif bias.is_a?(Bounds)
607
- # viewport biasing
608
- Geokit::Inflector::url_escape("&bounds=#{bias.sw.to_s}|#{bias.ne.to_s}")
609
- end
610
- end
611
-
612
- def self.json2GeoLoc(json, address="")
613
- ret=nil
614
- results = MultiJson.decode(json)
615
-
616
- if results['status'] == 'OVER_QUERY_LIMIT'
617
- raise Geokit::TooManyQueriesError
618
- end
619
- if results['status'] == 'ZERO_RESULTS'
620
- return GeoLoc.new
621
- end
622
- # this should probably be smarter.
623
- if !results['status'] == 'OK'
624
- raise Geokit::Geocoders::GeocodeError
625
- end
626
- # location_type stores additional data about the specified location.
627
- # The following values are currently supported:
628
- # "ROOFTOP" indicates that the returned result is a precise geocode
629
- # for which we have location information accurate down to street
630
- # address precision.
631
- # "RANGE_INTERPOLATED" indicates that the returned result reflects an
632
- # approximation (usually on a road) interpolated between two precise
633
- # points (such as intersections). Interpolated results are generally
634
- # returned when rooftop geocodes are unavailable for a street address.
635
- # "GEOMETRIC_CENTER" indicates that the returned result is the
636
- # geometric center of a result such as a polyline (for example, a
637
- # street) or polygon (region).
638
- # "APPROXIMATE" indicates that the returned result is approximate
639
-
640
- # these do not map well. Perhaps we should guess better based on size
641
- # of bounding box where it exists? Does it really matter?
642
- accuracy = {
643
- "ROOFTOP" => 9,
644
- "RANGE_INTERPOLATED" => 8,
645
- "GEOMETRIC_CENTER" => 5,
646
- "APPROXIMATE" => 4
647
- }
648
-
649
- @unsorted = []
650
-
651
- results['results'].each do |addr|
652
- res = GeoLoc.new
653
- res.provider = 'google3'
654
- res.success = true
655
- res.full_address = addr['formatted_address']
656
-
657
- addr['address_components'].each do |comp|
658
- case
659
- when comp['types'].include?("subpremise")
660
- res.sub_premise = comp['short_name']
661
- when comp['types'].include?("street_number")
662
- res.street_number = comp['short_name']
663
- when comp['types'].include?("route")
664
- res.street_name = comp['long_name']
665
- when comp['types'].include?("locality")
666
- res.city = comp['long_name']
667
- when comp['types'].include?("administrative_area_level_1")
668
- res.state = comp['short_name']
669
- res.province = comp['short_name']
670
- when comp['types'].include?("postal_code")
671
- res.zip = comp['long_name']
672
- when comp['types'].include?("country")
673
- res.country_code = comp['short_name']
674
- res.country = comp['long_name']
675
- when comp['types'].include?("administrative_area_level_2")
676
- res.district = comp['long_name']
677
- end
678
- end
679
- if res.street_name
680
- res.street_address=[res.street_number,res.street_name].join(' ').strip
681
- end
682
- res.accuracy = accuracy[addr['geometry']['location_type']]
683
- res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy]
684
- # try a few overrides where we can
685
- if res.sub_premise
686
- res.accuracy = 9
687
- res.precision = 'building'
688
- end
689
- if res.street_name && res.precision=='city'
690
- res.precision = 'street'
691
- res.accuracy = 7
692
- end
693
-
694
- res.lat=addr['geometry']['location']['lat'].to_f
695
- res.lng=addr['geometry']['location']['lng'].to_f
696
-
697
- ne=Geokit::LatLng.new(
698
- addr['geometry']['viewport']['northeast']['lat'].to_f,
699
- addr['geometry']['viewport']['northeast']['lng'].to_f
700
- )
701
- sw=Geokit::LatLng.new(
702
- addr['geometry']['viewport']['southwest']['lat'].to_f,
703
- addr['geometry']['viewport']['southwest']['lng'].to_f
704
- )
705
- res.suggested_bounds = Geokit::Bounds.new(sw,ne)
706
-
707
- @unsorted << res
708
- end
709
-
710
- all = @unsorted.sort_by { |a| a.accuracy }.reverse
711
- encoded = all.first
712
- encoded.all = all
713
- return encoded
714
- end
715
- end
716
-
717
- class FCCGeocoder < Geocoder
718
-
719
- private
720
- # Template method which does the reverse-geocode lookup.
721
- def self.do_reverse_geocode(latlng)
722
- latlng=LatLng.normalize(latlng)
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)}")
724
- return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
725
- json = res.body
726
- logger.debug "FCC reverse-geocoding. LL: #{latlng}. Result: #{json}"
727
- return self.json2GeoLoc(json)
728
- end
729
-
730
- # Template method which does the geocode lookup.
731
- #
732
- # ==== EXAMPLES
733
- # ll=GeoKit::LatLng.new(40, -85)
734
- # Geokit::Geocoders::FCCGeocoder.geocode(ll) #
735
-
736
- # JSON result looks like this
737
- # => {"County"=>{"name"=>"Wayne", "FIPS"=>"18177"},
738
- # "Block"=>{"FIPS"=>"181770103002004"},
739
- # "executionTime"=>"0.099",
740
- # "State"=>{"name"=>"Indiana", "code"=>"IN", "FIPS"=>"18"},
741
- # "status"=>"OK"}
742
-
743
- def self.json2GeoLoc(json, address="")
744
- ret = nil
745
- results = MultiJson.decode(json)
746
-
747
- if results.has_key?('Err') and results['Err']["msg"] == 'There are no results for this location'
748
- return GeoLoc.new
749
- end
750
- # this should probably be smarter.
751
- if !results['status'] == 'OK'
752
- raise Geokit::Geocoders::GeocodeError
753
- end
754
-
755
- res = GeoLoc.new
756
- res.provider = 'fcc'
757
- res.success = true
758
- res.precision = 'block'
759
- res.country_code = 'US'
760
- res.district = results['County']['name']
761
- res.district_fips = results['County']['FIPS']
762
- res.state = results['State']['code']
763
- res.state_fips = results['State']['FIPS']
764
- res.block_fips = results['Block']['FIPS']
765
-
766
- res
767
- end
768
- end
769
- # -------------------------------------------------------------------------------------------
770
- # IP Geocoders
771
- # -------------------------------------------------------------------------------------------
772
-
773
- # Provides geocoding based upon an IP address. The underlying web service is geoplugin.net
774
- class GeoPluginGeocoder < Geocoder
775
- private
776
-
777
- def self.do_geocode(ip, options = {})
778
- return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
779
- response = self.call_geocoder_service("http://www.geoplugin.net/xml.gp?ip=#{ip}")
780
- return response.is_a?(Net::HTTPSuccess) ? parse_xml(response.body) : GeoLoc.new
781
- rescue
782
- logger.error "Caught an error during GeoPluginGeocoder geocoding call: "+$!
783
- return GeoLoc.new
784
- end
785
-
786
- def self.parse_xml(xml)
787
- xml = REXML::Document.new(xml)
788
- geo = GeoLoc.new
789
- geo.provider='geoPlugin'
790
- geo.city = xml.elements['//geoplugin_city'].text
791
- geo.state = xml.elements['//geoplugin_region'].text
792
- geo.country_code = xml.elements['//geoplugin_countryCode'].text
793
- geo.lat = xml.elements['//geoplugin_latitude'].text.to_f
794
- geo.lng = xml.elements['//geoplugin_longitude'].text.to_f
795
- geo.success = !!geo.city && !geo.city.empty?
796
- return geo
797
- end
798
- end
799
-
800
- # Provides geocoding based upon an IP address. The underlying web service is a hostip.info
801
- # which sources their data through a combination of publicly available information as well
802
- # as community contributions.
803
- class IpGeocoder < Geocoder
804
-
805
- # A number of non-routable IP ranges.
806
- #
807
- # --
808
- # Sources for these:
809
- # RFC 3330: Special-Use IPv4 Addresses
810
- # The bogon list: http://www.cymru.com/Documents/bogon-list.html
811
-
812
- NON_ROUTABLE_IP_RANGES = [
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
824
- ].freeze
825
-
826
- private
827
-
828
- # Given an IP address, returns a GeoLoc instance which contains latitude,
829
- # longitude, city, and country code. Sets the success attribute to false if the ip
830
- # parameter does not match an ip address.
831
- def self.do_geocode(ip, options = {})
832
- return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
833
- return GeoLoc.new if self.private_ip_address?(ip)
834
- url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true"
835
- response = self.call_geocoder_service(url)
836
- response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new
837
- rescue
838
- logger.error "Caught an error during HostIp geocoding call: "+$!
839
- return GeoLoc.new
840
- end
841
-
842
- # Converts the body to YAML since its in the form of:
843
- #
844
- # Country: UNITED STATES (US)
845
- # City: Sugar Grove, IL
846
- # Latitude: 41.7696
847
- # Longitude: -88.4588
848
- #
849
- # then instantiates a GeoLoc instance to populate with location data.
850
- def self.parse_body(body) # :nodoc:
851
- yaml = YAML.load(body)
852
- res = GeoLoc.new
853
- res.provider = 'hostip'
854
- res.city, res.state = yaml['City'].split(', ')
855
- country, res.country_code = yaml['Country'].split(' (')
856
- res.lat = yaml['Latitude']
857
- res.lng = yaml['Longitude']
858
- res.country_code.chop!
859
- res.success = !(res.city =~ /\(.+\)/)
860
- res
861
- end
862
-
863
- # Checks whether the IP address belongs to a private address range.
864
- #
865
- # This function is used to reduce the number of useless queries made to
866
- # the geocoding service. Such queries can occur frequently during
867
- # integration tests.
868
- def self.private_ip_address?(ip)
869
- return NON_ROUTABLE_IP_RANGES.any? { |range| range.include?(ip) }
870
- end
871
- end
872
-
873
- # -------------------------------------------------------------------------------------------
874
- # The Multi Geocoder
875
- # -------------------------------------------------------------------------------------------
876
-
877
- # Provides methods to geocode with a variety of geocoding service providers, plus failover
878
- # among providers in the order you configure. When 2nd parameter is set 'true', perform
879
- # ip location lookup with 'address' as the ip address.
880
- #
881
- # Goal:
882
- # - homogenize the results of multiple geocoders
883
- #
884
- # Limitations:
885
- # - currently only provides the first result. Sometimes geocoders will return multiple results.
886
- # - currently discards the "accuracy" component of the geocoding calls
887
- class MultiGeocoder < Geocoder
888
-
889
- private
890
- # This method will call one or more geocoders in the order specified in the
891
- # configuration until one of the geocoders work.
892
- #
893
- # The failover approach is crucial for production-grade apps, but is rarely used.
894
- # 98% of your geocoding calls will be successful with the first call
895
- def self.do_geocode(address, options = {})
896
- geocode_ip = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.match(address)
897
- provider_order = geocode_ip ? Geokit::Geocoders::ip_provider_order : Geokit::Geocoders::provider_order
898
-
899
- provider_order.each do |provider|
900
- begin
901
- klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder"
902
- res = klass.send :geocode, address, options
903
- return res if res.success?
904
- rescue
905
- logger.error("Something has gone very wrong during geocoding, OR you have configured an invalid class name in Geokit::Geocoders::provider_order. Address: #{address}. Provider: #{provider}")
906
- end
907
- end
908
- # If we get here, we failed completely.
909
- GeoLoc.new
910
- end
911
-
912
- # This method will call one or more geocoders in the order specified in the
913
- # configuration until one of the geocoders work, only this time it's going
914
- # to try to reverse geocode a geographical point.
915
- def self.do_reverse_geocode(latlng)
916
- Geokit::Geocoders::provider_order.each do |provider|
917
- begin
918
- klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder"
919
- res = klass.send :reverse_geocode, latlng
920
- return res if res.success?
921
- rescue
922
- logger.error("Something has gone very wrong during reverse geocoding, OR you have configured an invalid class name in Geokit::Geocoders::provider_order. LatLng: #{latlng}. Provider: #{provider}")
923
- end
924
- end
925
- # If we get here, we failed completely.
926
- GeoLoc.new
927
- end
928
- end
161
+ require File.join(File.dirname(__FILE__), 'multi_geocoder')
929
162
  end
930
163
  end