pepe-graticule 0.2.11

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 (88) hide show
  1. data/.gitignore +4 -0
  2. data/CHANGELOG.txt +61 -0
  3. data/LICENSE.txt +30 -0
  4. data/Manifest.txt +84 -0
  5. data/README.txt +41 -0
  6. data/Rakefile +143 -0
  7. data/VERSION +1 -0
  8. data/bin/geocode +5 -0
  9. data/graticule.gemspec +146 -0
  10. data/init.rb +2 -0
  11. data/lib/graticule.rb +26 -0
  12. data/lib/graticule/cli.rb +64 -0
  13. data/lib/graticule/core_ext.rb +15 -0
  14. data/lib/graticule/distance.rb +18 -0
  15. data/lib/graticule/distance/haversine.rb +40 -0
  16. data/lib/graticule/distance/spherical.rb +52 -0
  17. data/lib/graticule/distance/vincenty.rb +76 -0
  18. data/lib/graticule/geocoder.rb +21 -0
  19. data/lib/graticule/geocoder/base.rb +112 -0
  20. data/lib/graticule/geocoder/bogus.rb +15 -0
  21. data/lib/graticule/geocoder/geocoder_ca.rb +54 -0
  22. data/lib/graticule/geocoder/geocoder_us.rb +51 -0
  23. data/lib/graticule/geocoder/google.rb +100 -0
  24. data/lib/graticule/geocoder/host_ip.rb +41 -0
  25. data/lib/graticule/geocoder/local_search_maps.rb +45 -0
  26. data/lib/graticule/geocoder/mapquest.rb +87 -0
  27. data/lib/graticule/geocoder/meta_carta.rb +33 -0
  28. data/lib/graticule/geocoder/multi.rb +46 -0
  29. data/lib/graticule/geocoder/multimap.rb +73 -0
  30. data/lib/graticule/geocoder/postcode_anywhere.rb +63 -0
  31. data/lib/graticule/geocoder/rest.rb +18 -0
  32. data/lib/graticule/geocoder/yahoo.rb +84 -0
  33. data/lib/graticule/location.rb +82 -0
  34. data/lib/graticule/version.rb +9 -0
  35. data/site/index.html +114 -0
  36. data/site/plugin.html +82 -0
  37. data/site/stylesheets/style.css +69 -0
  38. data/test/config.yml.default +36 -0
  39. data/test/fixtures/responses/geocoder_us/success.xml +18 -0
  40. data/test/fixtures/responses/geocoder_us/unknown.xml +1 -0
  41. data/test/fixtures/responses/google/badkey.xml +1 -0
  42. data/test/fixtures/responses/google/limit.xml +10 -0
  43. data/test/fixtures/responses/google/missing_address.xml +1 -0
  44. data/test/fixtures/responses/google/only_coordinates.xml +1 -0
  45. data/test/fixtures/responses/google/partial.xml +1 -0
  46. data/test/fixtures/responses/google/server_error.xml +10 -0
  47. data/test/fixtures/responses/google/success.xml +1 -0
  48. data/test/fixtures/responses/google/success_multiple_results.xml +88 -0
  49. data/test/fixtures/responses/google/unavailable.xml +1 -0
  50. data/test/fixtures/responses/google/unknown_address.xml +1 -0
  51. data/test/fixtures/responses/host_ip/private.txt +4 -0
  52. data/test/fixtures/responses/host_ip/success.txt +4 -0
  53. data/test/fixtures/responses/host_ip/unknown.txt +4 -0
  54. data/test/fixtures/responses/local_search_maps/empty.txt +1 -0
  55. data/test/fixtures/responses/local_search_maps/not_found.txt +1 -0
  56. data/test/fixtures/responses/local_search_maps/success.txt +1 -0
  57. data/test/fixtures/responses/mapquest/multi_result.xml +1 -0
  58. data/test/fixtures/responses/mapquest/success.xml +1 -0
  59. data/test/fixtures/responses/meta_carta/bad_address.xml +17 -0
  60. data/test/fixtures/responses/meta_carta/multiple.xml +33 -0
  61. data/test/fixtures/responses/meta_carta/success.xml +31 -0
  62. data/test/fixtures/responses/multimap/missing_params.xml +4 -0
  63. data/test/fixtures/responses/multimap/no_matches.xml +4 -0
  64. data/test/fixtures/responses/multimap/success.xml +19 -0
  65. data/test/fixtures/responses/postcode_anywhere/badkey.xml +9 -0
  66. data/test/fixtures/responses/postcode_anywhere/canada.xml +16 -0
  67. data/test/fixtures/responses/postcode_anywhere/empty.xml +16 -0
  68. data/test/fixtures/responses/postcode_anywhere/success.xml +16 -0
  69. data/test/fixtures/responses/postcode_anywhere/uk.xml +18 -0
  70. data/test/fixtures/responses/yahoo/success.xml +3 -0
  71. data/test/fixtures/responses/yahoo/unknown_address.xml +6 -0
  72. data/test/mocks/uri.rb +52 -0
  73. data/test/test_helper.rb +31 -0
  74. data/test/unit/graticule/distance_test.rb +58 -0
  75. data/test/unit/graticule/geocoder/geocoder_us_test.rb +43 -0
  76. data/test/unit/graticule/geocoder/geocoders.rb +56 -0
  77. data/test/unit/graticule/geocoder/google_test.rb +112 -0
  78. data/test/unit/graticule/geocoder/host_ip_test.rb +40 -0
  79. data/test/unit/graticule/geocoder/local_search_maps_test.rb +30 -0
  80. data/test/unit/graticule/geocoder/mapquest_test.rb +47 -0
  81. data/test/unit/graticule/geocoder/meta_carta_test.rb +44 -0
  82. data/test/unit/graticule/geocoder/multi_test.rb +43 -0
  83. data/test/unit/graticule/geocoder/multimap_test.rb +52 -0
  84. data/test/unit/graticule/geocoder/postcode_anywhere_test.rb +50 -0
  85. data/test/unit/graticule/geocoder/yahoo_test.rb +48 -0
  86. data/test/unit/graticule/geocoder_test.rb +27 -0
  87. data/test/unit/graticule/location_test.rb +79 -0
  88. metadata +167 -0
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+
2
+ require 'graticule'
data/lib/graticule.rb ADDED
@@ -0,0 +1,26 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ require 'active_support'
4
+
5
+ require 'graticule/version'
6
+ require 'graticule/core_ext'
7
+ require 'graticule/location'
8
+ require 'graticule/geocoder'
9
+ require 'graticule/geocoder/base'
10
+ require 'graticule/geocoder/bogus'
11
+ require 'graticule/geocoder/rest'
12
+ require 'graticule/geocoder/google'
13
+ require 'graticule/geocoder/host_ip'
14
+ require 'graticule/geocoder/multi'
15
+ require 'graticule/geocoder/yahoo'
16
+ require 'graticule/geocoder/geocoder_ca'
17
+ require 'graticule/geocoder/geocoder_us'
18
+ require 'graticule/geocoder/local_search_maps'
19
+ require 'graticule/geocoder/meta_carta'
20
+ require 'graticule/geocoder/postcode_anywhere'
21
+ require 'graticule/geocoder/multimap'
22
+ require 'graticule/geocoder/mapquest'
23
+ require 'graticule/distance'
24
+ require 'graticule/distance/haversine'
25
+ require 'graticule/distance/spherical'
26
+ require 'graticule/distance/vincenty'
@@ -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,15 @@
1
+ module Graticule
2
+ module RadiansAndDegrees
3
+ # Convert from degrees to radians
4
+ def to_radians
5
+ ( self / 360.0 ) * Math::PI * 2
6
+ end
7
+
8
+ # Convert from radians to degrees
9
+ def to_degrees
10
+ ( self * 360.0 ) / Math::PI / 2
11
+ end
12
+ end
13
+ end
14
+
15
+ Numeric.send :include, Graticule::RadiansAndDegrees
@@ -0,0 +1,18 @@
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
+ end
17
+ end
18
+ end
@@ -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 = from.longitude.to_radians
21
+ from_latitude = from.latitude.to_radians
22
+ to_longitude = to.longitude.to_radians
23
+ to_latitude = to.latitude.to_radians
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 = from.longitude.to_radians
20
+ from_latitude = from.latitude.to_radians
21
+ to_longitude = to.longitude.to_radians
22
+ to_latitude = to.latitude.to_radians
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 = from.longitude.to_radians
22
+ from_latitude = from.latitude.to_radians
23
+ to_longitude = to.longitude.to_radians
24
+ to_latitude = to.latitude.to_radians
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,21 @@
1
+
2
+ module Graticule
3
+
4
+ # Get a geocoder for the given service
5
+ #
6
+ # geocoder = Graticule.service(:google).new "api_key"
7
+ #
8
+ # See the documentation for your specific geocoder for more information
9
+ #
10
+ def self.service(name)
11
+ Geocoder.const_get name.to_s.camelize
12
+ end
13
+
14
+ # Base error class
15
+ class Error < RuntimeError; end
16
+ class CredentialsError < Error; end
17
+
18
+ # Raised when you try to locate an invalid address.
19
+ class AddressError < Error; end
20
+
21
+ end
@@ -0,0 +1,112 @@
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_response(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(params = {})
79
+ response = prepare_response(make_url(params).open('User-Agent' => USER_AGENT).read)
80
+ check_error(response)
81
+ return parse_response(response)
82
+ rescue OpenURI::HTTPError => e
83
+ check_error(prepare_response(e.io.read))
84
+ raise
85
+ end
86
+
87
+ # Creates a URI from the Hash +params+. Override this then call super if
88
+ # you need to add extra params like an application id or output type.
89
+ def make_url(params)
90
+ escaped_params = params.sort_by { |k,v| k.to_s }.map do |k,v|
91
+ "#{URI.escape k.to_s}=#{URI.escape v.to_s}"
92
+ end
93
+
94
+ url = @url.dup
95
+ url.query = escaped_params.join '&'
96
+ return url
97
+ end
98
+
99
+ # Override to convert the response to something besides a String, which
100
+ # will get passed to +check_error+ and +parse_response+.
101
+ def prepare_response(response)
102
+ response
103
+ end
104
+
105
+ # Must parse results from +response+ into a Location.
106
+ def parse_response(response)
107
+ raise NotImplementedError
108
+ end
109
+
110
+ end
111
+ end
112
+ 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