geokit 1.6.5 → 1.6.6

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