norman-graticule 0.2.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/CHANGELOG.txt +46 -0
  2. data/LICENSE.txt +30 -0
  3. data/Manifest.txt +73 -0
  4. data/README.txt +29 -0
  5. data/Rakefile +86 -0
  6. data/bin/geocode +5 -0
  7. data/init.rb +2 -0
  8. data/lib/graticule/cli.rb +64 -0
  9. data/lib/graticule/distance/haversine.rb +40 -0
  10. data/lib/graticule/distance/spherical.rb +52 -0
  11. data/lib/graticule/distance/vincenty.rb +76 -0
  12. data/lib/graticule/distance.rb +29 -0
  13. data/lib/graticule/geocoder/base.rb +113 -0
  14. data/lib/graticule/geocoder/bogus.rb +15 -0
  15. data/lib/graticule/geocoder/geocoder_ca.rb +54 -0
  16. data/lib/graticule/geocoder/geocoder_us.rb +49 -0
  17. data/lib/graticule/geocoder/google.rb +120 -0
  18. data/lib/graticule/geocoder/host_ip.rb +41 -0
  19. data/lib/graticule/geocoder/local_search_maps.rb +45 -0
  20. data/lib/graticule/geocoder/map_quest.rb +109 -0
  21. data/lib/graticule/geocoder/meta_carta.rb +33 -0
  22. data/lib/graticule/geocoder/multi.rb +46 -0
  23. data/lib/graticule/geocoder/postcode_anywhere.rb +63 -0
  24. data/lib/graticule/geocoder/rest.rb +18 -0
  25. data/lib/graticule/geocoder/yahoo.rb +102 -0
  26. data/lib/graticule/geocoder.rb +21 -0
  27. data/lib/graticule/location.rb +57 -0
  28. data/lib/graticule/version.rb +9 -0
  29. data/lib/graticule.rb +24 -0
  30. data/test/config.yml.default +36 -0
  31. data/test/fixtures/responses/geocoder_us/success.xml +18 -0
  32. data/test/fixtures/responses/geocoder_us/unknown.xml +1 -0
  33. data/test/fixtures/responses/google/badkey.xml +1 -0
  34. data/test/fixtures/responses/google/limit.xml +10 -0
  35. data/test/fixtures/responses/google/missing_address.xml +1 -0
  36. data/test/fixtures/responses/google/only_coordinates.xml +1 -0
  37. data/test/fixtures/responses/google/partial.xml +1 -0
  38. data/test/fixtures/responses/google/server_error.xml +10 -0
  39. data/test/fixtures/responses/google/success.xml +1 -0
  40. data/test/fixtures/responses/google/unavailable.xml +1 -0
  41. data/test/fixtures/responses/google/unknown_address.xml +1 -0
  42. data/test/fixtures/responses/host_ip/private.txt +4 -0
  43. data/test/fixtures/responses/host_ip/success.txt +4 -0
  44. data/test/fixtures/responses/host_ip/unknown.txt +4 -0
  45. data/test/fixtures/responses/local_search_maps/empty.txt +1 -0
  46. data/test/fixtures/responses/local_search_maps/not_found.txt +1 -0
  47. data/test/fixtures/responses/local_search_maps/success.txt +1 -0
  48. data/test/fixtures/responses/meta_carta/bad_address.xml +17 -0
  49. data/test/fixtures/responses/meta_carta/multiple.xml +33 -0
  50. data/test/fixtures/responses/meta_carta/success.xml +31 -0
  51. data/test/fixtures/responses/postcode_anywhere/badkey.xml +9 -0
  52. data/test/fixtures/responses/postcode_anywhere/canada.xml +16 -0
  53. data/test/fixtures/responses/postcode_anywhere/empty.xml +16 -0
  54. data/test/fixtures/responses/postcode_anywhere/success.xml +16 -0
  55. data/test/fixtures/responses/postcode_anywhere/uk.xml +18 -0
  56. data/test/fixtures/responses/yahoo/success.xml +3 -0
  57. data/test/fixtures/responses/yahoo/unknown_address.xml +6 -0
  58. data/test/mocks/uri.rb +52 -0
  59. data/test/test_helper.rb +32 -0
  60. data/test/unit/graticule/distance_test.rb +58 -0
  61. data/test/unit/graticule/geocoder/geocoder_us_test.rb +43 -0
  62. data/test/unit/graticule/geocoder/google_test.rb +107 -0
  63. data/test/unit/graticule/geocoder/host_ip_test.rb +40 -0
  64. data/test/unit/graticule/geocoder/local_search_maps_test.rb +30 -0
  65. data/test/unit/graticule/geocoder/meta_carta_test.rb +44 -0
  66. data/test/unit/graticule/geocoder/multi_test.rb +43 -0
  67. data/test/unit/graticule/geocoder/postcode_anywhere_test.rb +50 -0
  68. data/test/unit/graticule/geocoder/yahoo_test.rb +58 -0
  69. data/test/unit/graticule/geocoder_test.rb +27 -0
  70. data/test/unit/graticule/location_test.rb +66 -0
  71. metadata +123 -0
data/CHANGELOG.txt ADDED
@@ -0,0 +1,46 @@
1
+ 0.2.7
2
+ * Added MapQuest geocoder [Andrew Selder]
3
+ * Fix google geocoder for responses that only return coordinates [Andrew Selder]
4
+
5
+ 0.2.5
6
+ * fixed address mapping for local search maps (again)
7
+
8
+ 0.2.4 (2007-05-15)
9
+ * fixed address mapping for local search maps (Tom Taylor)
10
+
11
+ 0.2.3 (2007-04-27)
12
+ * fixed Google for less precise queries
13
+ * added User-Agent to coerce Google into returning UTF-8 (Jonathan Tron)
14
+
15
+ 0.2.2 (2007-03-27)
16
+ * fixed LocalSearchMaps
17
+
18
+ 0.2.1 (2007-03-19)
19
+ * fixed error in command line interface
20
+
21
+ 0.2.0 (2007-03-17)
22
+ * changed city to locality, state to region, and zip to postal_code
23
+ * added support for PostcodeAnywhere
24
+ * added support for Local Search Maps (James Stewart)
25
+ * added IP-based geocoder
26
+ * moved geocoders to Graticule::Geocoder namespace
27
+ * fixed Google geocoder (again)
28
+ * made Yahoo geocoder consistent with others by returning 1 result
29
+ * geocoders can how take a Hash (:street, :locality, :region, :postal_code, :country)
30
+ or a Graticule::Location for the #locate call
31
+
32
+ 0.1.3 (2007-02-14)
33
+ * fixed Google geocoder
34
+ * fixed CLI
35
+
36
+ 0.1.2 (2007-02-12)
37
+ * added "geocode" executable. See "geocode --help" for more information
38
+ * declared dependency on ActiveSupport
39
+
40
+ 0.1.1 (2006-12-16)
41
+ * fixed bug in Yahoo that raised error when street address not returned
42
+ * migrated to Hoe (http://seattlerb.rubyforge.org/hoe/)
43
+ * added Haversine, Spherical and Vincenty distance calculations
44
+
45
+ 0.1 (2006-10-31)
46
+ * Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,30 @@
1
+ Copyright 2006 Brandon Keepers, Collective Idea. All rights reserved.
2
+
3
+ Original geocoding code:
4
+ Copyright 2006 Eric Hodel, The Robot Co-op. All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions
8
+ are met:
9
+
10
+ 1. Redistributions of source code must retain the above copyright
11
+ notice, this list of conditions and the following disclaimer.
12
+ 2. Redistributions in binary form must reproduce the above copyright
13
+ notice, this list of conditions and the following disclaimer in the
14
+ documentation and/or other materials provided with the distribution.
15
+ 3. Neither the names of the authors nor the names of their contributors
16
+ may be used to endorse or promote products derived from this software
17
+ without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
20
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
23
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
24
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
25
+ OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
26
+ BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
27
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
28
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
29
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
data/Manifest.txt ADDED
@@ -0,0 +1,73 @@
1
+ CHANGELOG.txt
2
+ LICENSE.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ bin/geocode
7
+ init.rb
8
+ lib/graticule.rb
9
+ lib/graticule/cli.rb
10
+ lib/graticule/distance.rb
11
+ lib/graticule/distance/haversine.rb
12
+ lib/graticule/distance/spherical.rb
13
+ lib/graticule/distance/vincenty.rb
14
+ lib/graticule/geocoder.rb
15
+ lib/graticule/geocoder/base.rb
16
+ lib/graticule/geocoder/bogus.rb
17
+ lib/graticule/geocoder/geocoder_ca.rb
18
+ lib/graticule/geocoder/geocoder_us.rb
19
+ lib/graticule/geocoder/google.rb
20
+ lib/graticule/geocoder/host_ip.rb
21
+ lib/graticule/geocoder/local_search_maps.rb
22
+ lib/graticule/geocoder/map_quest.rb
23
+ lib/graticule/geocoder/meta_carta.rb
24
+ lib/graticule/geocoder/multi.rb
25
+ lib/graticule/geocoder/postcode_anywhere.rb
26
+ lib/graticule/geocoder/rest.rb
27
+ lib/graticule/geocoder/yahoo.rb
28
+ lib/graticule/location.rb
29
+ lib/graticule/version.rb
30
+ site/index.html
31
+ site/plugin.html
32
+ site/stylesheets/style.css
33
+ test/config.yml.default
34
+ test/fixtures/responses/geocoder_us/success.xml
35
+ test/fixtures/responses/geocoder_us/unknown.xml
36
+ test/fixtures/responses/google/badkey.xml
37
+ test/fixtures/responses/google/limit.xml
38
+ test/fixtures/responses/google/missing_address.xml
39
+ test/fixtures/responses/google/only_coordinates.xml
40
+ test/fixtures/responses/google/partial.xml
41
+ test/fixtures/responses/google/server_error.xml
42
+ test/fixtures/responses/google/success.xml
43
+ test/fixtures/responses/google/unavailable.xml
44
+ test/fixtures/responses/google/unknown_address.xml
45
+ test/fixtures/responses/host_ip/private.txt
46
+ test/fixtures/responses/host_ip/success.txt
47
+ test/fixtures/responses/host_ip/unknown.txt
48
+ test/fixtures/responses/local_search_maps/empty.txt
49
+ test/fixtures/responses/local_search_maps/not_found.txt
50
+ test/fixtures/responses/local_search_maps/success.txt
51
+ test/fixtures/responses/meta_carta/bad_address.xml
52
+ test/fixtures/responses/meta_carta/multiple.xml
53
+ test/fixtures/responses/meta_carta/success.xml
54
+ test/fixtures/responses/postcode_anywhere/badkey.xml
55
+ test/fixtures/responses/postcode_anywhere/canada.xml
56
+ test/fixtures/responses/postcode_anywhere/empty.xml
57
+ test/fixtures/responses/postcode_anywhere/success.xml
58
+ test/fixtures/responses/postcode_anywhere/uk.xml
59
+ test/fixtures/responses/yahoo/success.xml
60
+ test/fixtures/responses/yahoo/unknown_address.xml
61
+ test/mocks/uri.rb
62
+ test/test_helper.rb
63
+ test/unit/graticule/distance_test.rb
64
+ test/unit/graticule/geocoder/geocoder_us_test.rb
65
+ test/unit/graticule/geocoder/google_test.rb
66
+ test/unit/graticule/geocoder/host_ip_test.rb
67
+ test/unit/graticule/geocoder/local_search_maps_test.rb
68
+ test/unit/graticule/geocoder/meta_carta_test.rb
69
+ test/unit/graticule/geocoder/multi_test.rb
70
+ test/unit/graticule/geocoder/postcode_anywhere_test.rb
71
+ test/unit/graticule/geocoder/yahoo_test.rb
72
+ test/unit/graticule/geocoder_test.rb
73
+ test/unit/graticule/location_test.rb
data/README.txt ADDED
@@ -0,0 +1,29 @@
1
+ = Graticule
2
+
3
+ Graticule is a geocoding API for looking up address coordinates. It supports many popular APIs, including Yahoo, Google, Geocoder.ca, Geocoder.us, PostcodeAnywhere and MetaCarta.
4
+
5
+ = Usage
6
+
7
+ require 'rubygems'
8
+ require 'graticule'
9
+ geocoder = Graticule.service(:google).new "api_key"
10
+ location = geocoder.locate "61 East 9th Street, Holland, MI"
11
+
12
+ = Distance Calculation
13
+
14
+ Graticule includes 3 different distance formulas, Spherical (simplest but least accurate), Vincenty (most accurate and most complicated), and Haversine (somewhere inbetween).
15
+
16
+ geocoder.locate("Holland, MI").distance_to(geocoder.locate("Chicago, IL"))
17
+ #=> 101.997458788177
18
+
19
+ = Command Line
20
+
21
+ Graticule includes a command line interface (CLI).
22
+
23
+ $ geocode -s yahoo -a yahookey Washington, DC
24
+ Washington, DC US
25
+ latitude: 38.895222, longitude: -77.036758
26
+
27
+ = Source
28
+
29
+ The source code for Graticule is available at http://source.collectiveidea.com/public/geocode/trunk
data/Rakefile ADDED
@@ -0,0 +1,86 @@
1
+ require 'rubygems'
2
+ require 'active_support'
3
+ require 'hoe'
4
+ require File.join(File.dirname(__FILE__), 'lib', 'graticule', 'version.rb')
5
+
6
+ Hoe.new("graticule", Graticule::Version::STRING) do |p|
7
+ p.rubyforge_name = "graticule"
8
+ p.author = 'Brandon Keepers'
9
+ p.email = 'brandon@opensoul.org'
10
+ p.summary = "API for using all the popular geocoding services."
11
+ p.description = 'Graticule is a geocoding API that provides a common interface to all the popular services, including Google, Yahoo, Geocoder.us, and MetaCarta.'
12
+ p.url = 'http://graticule.rubyforge.org'
13
+ p.need_tar = true
14
+ p.need_zip = true
15
+ p.test_globs = ['test/**/*_test.rb']
16
+ p.changes = p.paragraphs_of('CHANGELOG.txt', 0..1).join("\n\n")
17
+ p.extra_deps << ['activesupport']
18
+ end
19
+
20
+
21
+
22
+ namespace :test do
23
+ namespace :cache do
24
+ desc 'Cache test responses from all the free geocoders'
25
+ task :free => [:google, :geocoder_us, :host_ip, :local_search_maps, :meta_carta, :yahoo]
26
+
27
+ desc 'Cache test responses from Google'
28
+ task :google do
29
+ cache_responses('google')
30
+ end
31
+
32
+ desc 'Cache test responses from Geocoder.us'
33
+ task :geocoder_us do
34
+ cache_responses('geocoder_us')
35
+ end
36
+
37
+ desc 'Cache test responses from HostIP'
38
+ task :host_ip do
39
+ cache_responses('host_ip')
40
+ end
41
+
42
+ desc 'Cache test responses from Local Search Maps'
43
+ task :local_search_maps do
44
+ cache_responses('local_search_maps')
45
+ end
46
+
47
+ desc 'Cache test responses from Meta Carta'
48
+ task :meta_carta do
49
+ cache_responses('meta_carta')
50
+ end
51
+
52
+ desc 'Cache test responses from Yahoo'
53
+ task :yahoo do
54
+ cache_responses('yahoo')
55
+ end
56
+
57
+ end
58
+ end
59
+
60
+ require 'net/http'
61
+ require 'uri'
62
+ RESPONSES_PATH = File.dirname(__FILE__) + '/test/fixtures/responses'
63
+
64
+ def cache_responses(service)
65
+ test_config[service.to_s]['responses'].each do |file,url|
66
+ File.open("#{RESPONSES_PATH}/#{service}/#{file}", 'w') do |f|
67
+ f.puts Net::HTTP.get(URI.parse(url))
68
+ end
69
+ end
70
+ end
71
+
72
+ def test_config
73
+ file = File.dirname(__FILE__) + '/test/config.yml'
74
+ raise "Copy config.yml.default to config.yml and set the API keys" unless File.exists?(file)
75
+ @test_config ||= returning(YAML.load(File.read(file))) do |config|
76
+ config.each do |service,values|
77
+ values['responses'].each {|file,url| update_placeholders!(values, url) }
78
+ end
79
+ end
80
+ end
81
+
82
+ def update_placeholders!(config, thing)
83
+ config.each do |option, value|
84
+ thing.gsub!(":#{option}", value) if value.is_a?(String)
85
+ end
86
+ end
data/bin/geocode ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'graticule/cli'
4
+
5
+ Graticule::Cli.start ARGV
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+
2
+ require 'graticule'
@@ -0,0 +1,64 @@
1
+ require 'graticule'
2
+ require 'optparse'
3
+
4
+ module Graticule
5
+
6
+ # A command line interface for geocoding. From the command line, run:
7
+ #
8
+ # geocode 49423
9
+ #
10
+ # Outputs:
11
+ #
12
+ # # Holland, MI 49423 US
13
+ # # latitude: 42.7654, longitude: -86.1085
14
+ #
15
+ # == Usage: geocode [options] location
16
+ #
17
+ # Options:
18
+ # -s, --service service Geocoding service
19
+ # -a, --apikey apikey API key for the selected service
20
+ # -h, --help Help
21
+ class Cli
22
+
23
+ def self.start(args, out = STDOUT)
24
+ options = { :service => :yahoo, :api_key => 'YahooDemo' }
25
+
26
+ OptionParser.new do |opts|
27
+ opts.banner = "Usage: geocode [options] location"
28
+ opts.separator ""
29
+ opts.separator "Options: "
30
+
31
+ opts.on("-s service", %w(yahoo google geocoder_us metacarta), "--service service", "Geocoding service") do |service|
32
+ options[:service] = service
33
+ end
34
+
35
+ opts.on("-a apikey", "--apikey apikey", "API key for the selected service")
36
+
37
+ opts.on_tail("-h", "--help", "Help") do
38
+ puts opts
39
+ exit
40
+ end
41
+ end.parse! args
42
+
43
+ options[:location] = args.join(" ")
44
+
45
+ result = Graticule.service(options[:service]).new(*options[:api_key].split(',')).locate(options[:location])
46
+ location = (result.is_a?(Array) ? result.first : result)
47
+ if location
48
+ out << location.to_s(:coordinates => true)
49
+ exit 0
50
+ else
51
+ out << "Location not found"
52
+ exit 1
53
+ end
54
+ rescue Graticule::CredentialsError
55
+ $stderr.puts "Invalid API key. Pass your #{options[:service]} API key using the -a option. "
56
+ rescue OptionParser::InvalidArgument, OptionParser::InvalidOption,
57
+ Graticule::Error => error
58
+ $stderr.puts error.message
59
+ end
60
+
61
+
62
+ end
63
+ end
64
+
@@ -0,0 +1,40 @@
1
+ module Graticule
2
+ module Distance
3
+ #
4
+ # The Haversine Formula works better at small distances than the Spherical Law of Cosines
5
+ #
6
+ # Thanks to Chris Veness (http://www.movable-type.co.uk/scripts/LatLong.html)
7
+ # for distance formulas.
8
+ #
9
+ class Haversine < DistanceFormula
10
+
11
+ # Calculate the distance between two Locations using the Haversine formula
12
+ #
13
+ # Graticule::Distance::Haversine.distance(
14
+ # Graticule::Location.new(:latitude => 42.7654, :longitude => -86.1085),
15
+ # Graticule::Location.new(:latitude => 41.849838, :longitude => -87.648193)
16
+ # )
17
+ # #=> 101.061720831836
18
+ #
19
+ def self.distance(from, to, units = :miles)
20
+ from_longitude = deg2rad(from.longitude)
21
+ from_latitude = deg2rad(from.latitude)
22
+ to_longitude = deg2rad(to.longitude)
23
+ to_latitude = deg2rad(to.latitude)
24
+
25
+ latitude_delta = to_latitude - from_latitude
26
+ longitude_delta = to_longitude - from_longitude
27
+
28
+ a = sin(latitude_delta/2)**2 +
29
+ cos(from_latitude) *
30
+ cos(to_latitude) *
31
+ sin(longitude_delta/2)**2
32
+
33
+ c = 2 * atan2(sqrt(a), sqrt(1-a))
34
+
35
+ d = EARTH_RADIUS[units.to_sym] * c
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,52 @@
1
+ module Graticule
2
+ module Distance
3
+
4
+ #
5
+ # The Spherical Law of Cosines is the simplist though least accurate distance
6
+ # formula (earth isn't a perfect sphere).
7
+ #
8
+ class Spherical < DistanceFormula
9
+
10
+ # Calculate the distance between two Locations using the Spherical formula
11
+ #
12
+ # Graticule::Distance::Spherical.distance(
13
+ # Graticule::Location.new(:latitude => 42.7654, :longitude => -86.1085),
14
+ # Graticule::Location.new(:latitude => 41.849838, :longitude => -87.648193)
15
+ # )
16
+ # #=> 101.061720831853
17
+ #
18
+ def self.distance(from, to, units = :miles)
19
+ from_longitude = deg2rad(from.longitude)
20
+ from_latitude = deg2rad(from.latitude)
21
+ to_longitude = deg2rad(to.longitude)
22
+ to_latitude = deg2rad(to.latitude)
23
+
24
+ Math.acos(
25
+ Math.sin(from_latitude) *
26
+ Math.sin(to_latitude) +
27
+
28
+ Math.cos(from_latitude) *
29
+ Math.cos(to_latitude) *
30
+ Math.cos(to_longitude - from_longitude)
31
+ ) * EARTH_RADIUS[units.to_sym]
32
+ end
33
+
34
+ def self.to_sql(options)
35
+ options = {
36
+ :units => :miles,
37
+ :latitude_column => 'latitude',
38
+ :longitude_column => 'longitude'
39
+ }.merge(options)
40
+ %{(ACOS(
41
+ SIN(RADIANS(#{options[:latitude]})) *
42
+ SIN(RADIANS(#{options[:latitude_column]})) +
43
+ COS(RADIANS(#{options[:latitude]})) *
44
+ COS(RADIANS(#{options[:latitude_column]})) *
45
+ COS(RADIANS(#{options[:longitude_column]}) - RADIANS(#{options[:longitude]}))
46
+ ) * #{Graticule::Distance::EARTH_RADIUS[options[:units].to_sym]})
47
+ }.gsub("\n", '').squeeze(" ")
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,76 @@
1
+ module Graticule
2
+ module Distance
3
+
4
+ #
5
+ # The Vincenty Formula uses an ellipsoidal model of the earth, which is very accurate.
6
+ #
7
+ # Thanks to Chris Veness (http://www.movable-type.co.uk/scripts/LatLongVincenty.html)
8
+ # for distance formulas.
9
+ #
10
+ class Vincenty < DistanceFormula
11
+
12
+ # Calculate the distance between two Locations using the Vincenty formula
13
+ #
14
+ # Graticule::Distance::Vincenty.distance(
15
+ # Graticule::Location.new(:latitude => 42.7654, :longitude => -86.1085),
16
+ # Graticule::Location.new(:latitude => 41.849838, :longitude => -87.648193)
17
+ # )
18
+ # #=> 101.070118000159
19
+ #
20
+ def self.distance(from, to, units = :miles)
21
+ from_longitude = deg2rad(from.longitude)
22
+ from_latitude = deg2rad(from.latitude)
23
+ to_longitude = deg2rad(to.longitude)
24
+ to_latitude = deg2rad(to.latitude)
25
+
26
+ earth_major_axis_radius = EARTH_MAJOR_AXIS_RADIUS[units.to_sym]
27
+ earth_minor_axis_radius = EARTH_MINOR_AXIS_RADIUS[units.to_sym]
28
+
29
+ f = (earth_major_axis_radius - earth_minor_axis_radius) / earth_major_axis_radius
30
+
31
+ l = to_longitude - from_longitude
32
+ u1 = atan((1-f) * tan(from_latitude))
33
+ u2 = atan((1-f) * tan(to_latitude))
34
+ sin_u1 = sin(u1)
35
+ cos_u1 = cos(u1)
36
+ sin_u2 = sin(u2)
37
+ cos_u2 = cos(u2)
38
+
39
+ lambda = l
40
+ lambda_p = 2 * PI
41
+ iteration_limit = 20
42
+ while (lambda-lambda_p).abs > 1e-12 && (iteration_limit -= 1) > 0
43
+ sin_lambda = sin(lambda)
44
+ cos_lambda = cos(lambda)
45
+ sin_sigma = sqrt((cos_u2*sin_lambda) * (cos_u2*sin_lambda) +
46
+ (cos_u1*sin_u2-sin_u1*cos_u2*cos_lambda) * (cos_u1*sin_u2-sin_u1*cos_u2*cos_lambda))
47
+ return 0 if sin_sigma == 0 # co-incident points
48
+ cos_sigma = sin_u1*sin_u2 + cos_u1*cos_u2*cos_lambda
49
+ sigma = atan2(sin_sigma, cos_sigma)
50
+ sin_alpha = cos_u1 * cos_u2 * sin_lambda / sin_sigma
51
+ cosSqAlpha = 1 - sin_alpha*sin_alpha
52
+ cos2SigmaM = cos_sigma - 2*sin_u1*sin_u2/cosSqAlpha
53
+
54
+ cos2SigmaM = 0 if cos2SigmaM.nan? # equatorial line: cosSqAlpha=0 (§6)
55
+
56
+ c = f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha))
57
+ lambda_p = lambda
58
+ lambda = l + (1-c) * f * sin_alpha *
59
+ (sigma + c*sin_sigma*(cos2SigmaM+c*cos_sigma*(-1+2*cos2SigmaM*cos2SigmaM)))
60
+ end
61
+ # formula failed to converge (happens on antipodal points)
62
+ # We'll call Haversine formula instead.
63
+ return Haversine.distance(from, to, units) if iteration_limit == 0
64
+
65
+ uSq = cosSqAlpha * (earth_major_axis_radius**2 - earth_minor_axis_radius**2) / (earth_minor_axis_radius**2)
66
+ a = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)))
67
+ b = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)))
68
+ delta_sigma = b*sin_sigma*(cos2SigmaM+b/4*(cos_sigma*(-1+2*cos2SigmaM*cos2SigmaM)-
69
+ b/6*cos2SigmaM*(-3+4*sin_sigma*sin_sigma)*(-3+4*cos2SigmaM*cos2SigmaM)))
70
+
71
+ earth_minor_axis_radius * a * (sigma-delta_sigma)
72
+ end
73
+
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,29 @@
1
+ module Graticule
2
+ module Distance
3
+
4
+ EARTH_RADIUS = { :kilometers => 6378.135, :miles => 3963.1676 }
5
+ # WGS-84 numbers
6
+ EARTH_MAJOR_AXIS_RADIUS = { :kilometers => 6378.137, :miles => 3963.19059 }
7
+ EARTH_MINOR_AXIS_RADIUS = { :kilometers => 6356.7523142, :miles => 3949.90276 }
8
+
9
+ class DistanceFormula
10
+ include Math
11
+ extend Math
12
+
13
+ def initialize
14
+ raise NotImplementedError
15
+ end
16
+
17
+ # Convert from degrees to radians
18
+ def self.deg2rad(deg)
19
+ (deg * PI / 180)
20
+ end
21
+
22
+ # Convert from radians to degrees
23
+ def self.rad2deg(rad)
24
+ (rad * 180 / PI)
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,113 @@
1
+ require 'open-uri'
2
+
3
+ module Graticule #:nodoc:
4
+ module Geocoder
5
+
6
+ # Abstract class for implementing geocoders.
7
+ #
8
+ # === Example
9
+ #
10
+ # The following methods must be implemented in sublcasses:
11
+ #
12
+ # * +initialize+:: Sets @url to the service enpoint.
13
+ # * +check_error+:: Checks for errors in the server response.
14
+ # * +parse_response+:: Extracts information from the server response.
15
+ #
16
+ # Optionally, you can also override
17
+ #
18
+ # * +prepare_response+:: Convert the string response into a different format
19
+ # that gets passed on to +check_error+ and +parse_response+.
20
+ #
21
+ # If you have extra URL paramaters (application id, output type) or need to
22
+ # perform URL customization, override +make_url+.
23
+ #
24
+ # class FakeGeocoder < Base
25
+ #
26
+ # def initialize(appid)
27
+ # @appid = appid
28
+ # @url = URI.parse 'http://example.com/test'
29
+ # end
30
+ #
31
+ # def locate(query)
32
+ # get :q => query
33
+ # end
34
+ #
35
+ # private
36
+ #
37
+ # def check_error(xml)
38
+ # raise Error, xml.elements['error'].text if xml.elements['error']
39
+ # end
40
+ #
41
+ # def make_url(params)
42
+ # params[:appid] = @appid
43
+ # super params
44
+ # end
45
+ #
46
+ # def parse_first(response)
47
+ # # return Location
48
+ # end
49
+ #
50
+ # end
51
+ #
52
+ class Base
53
+ USER_AGENT = "Mozilla/5.0 (compatible; Graticule/#{Graticule::Version::STRING}; http://graticule.rubyforge.org)"
54
+
55
+ def initialize
56
+ raise NotImplementedError
57
+ end
58
+
59
+ private
60
+
61
+ def location_from_params(params)
62
+ case params
63
+ when Location then params
64
+ when Hash then Location.new params
65
+ else
66
+ raise ArgumentError, "Expected a Graticule::Location or a hash with :street, :locality, :region, :postal_code, and :country attributes"
67
+ end
68
+ end
69
+
70
+ # Check for errors in +response+ and raise appropriate error, if any.
71
+ # Must return if no error could be found.
72
+ def check_error(response)
73
+ raise NotImplementedError
74
+ end
75
+
76
+ # Performs a GET request with +params+. Calls +check_error+ and returns
77
+ # the result of +parse_response+.
78
+ def get(*args)
79
+ params = args.last.is_a?(::Hash) ? args.pop : args
80
+ response = prepare_response(make_url(params).open('User-Agent' => USER_AGENT).read)
81
+ check_error(response)
82
+ parse_response(args.first, response)
83
+ rescue OpenURI::HTTPError => e
84
+ check_error(prepare_response(e.io.read))
85
+ raise
86
+ end
87
+
88
+ # Creates a URI from the Hash +params+. Override this then call super if
89
+ # you need to add extra params like an application id or output type.
90
+ def make_url(params)
91
+ escaped_params = params.sort_by { |k,v| k.to_s }.map do |k,v|
92
+ "#{URI.escape k.to_s}=#{URI.escape v.to_s}"
93
+ end
94
+
95
+ url = @url.dup
96
+ url.query = escaped_params.join '&'
97
+ return url
98
+ end
99
+
100
+ # Override to convert the response to something besides a String, which
101
+ # will get passed to +check_error+ and +parse_response+.
102
+ def prepare_response(response)
103
+ response
104
+ end
105
+
106
+ # Must parse :first or :all results from +response+ into a Location.
107
+ def parse_response(parse_type, response)
108
+ raise NotImplementedError
109
+ end
110
+
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,15 @@
1
+ module Graticule #:nodoc:
2
+ module Geocoder #:nodoc:
3
+
4
+ # Bogus geocoder that can be used for test purposes
5
+ class Bogus
6
+
7
+ # returns a new location with the address set to the original query string
8
+ def locate(address)
9
+ Location.new :street => address
10
+ end
11
+
12
+ end
13
+
14
+ end
15
+ end