graticule 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/CHANGELOG.txt +4 -0
  2. data/Manifest.txt +9 -0
  3. data/Rakefile +69 -0
  4. data/lib/graticule.rb +2 -0
  5. data/lib/graticule/cli.rb +6 -4
  6. data/lib/graticule/geocoder/base.rb +112 -0
  7. data/lib/graticule/geocoder/geocoder_us.rb +1 -1
  8. data/lib/graticule/geocoder/google.rb +5 -5
  9. data/lib/graticule/geocoder/host_ip.rb +9 -18
  10. data/lib/graticule/geocoder/local_search_maps.rb +4 -4
  11. data/lib/graticule/geocoder/meta_carta.rb +1 -6
  12. data/lib/graticule/geocoder/rest.rb +6 -98
  13. data/lib/graticule/geocoder/yahoo.rb +1 -13
  14. data/lib/graticule/version.rb +1 -1
  15. data/site/index.html +114 -0
  16. data/site/plugin.html +82 -0
  17. data/site/stylesheets/style.css +73 -0
  18. data/test/config.yml.default +36 -0
  19. data/test/fixtures/responses/geocoder_us/success.xml +13 -5
  20. data/test/fixtures/responses/geocoder_us/unknown.xml +1 -1
  21. data/test/fixtures/responses/google/badkey.xml +1 -10
  22. data/test/fixtures/responses/google/missing_address.xml +1 -10
  23. data/test/fixtures/responses/google/partial.xml +1 -0
  24. data/test/fixtures/responses/google/success.xml +1 -37
  25. data/test/fixtures/responses/google/unavailable.xml +1 -10
  26. data/test/fixtures/responses/google/unknown_address.xml +1 -10
  27. data/test/fixtures/responses/host_ip/private.txt +1 -1
  28. data/test/fixtures/responses/host_ip/success.txt +1 -1
  29. data/test/fixtures/responses/host_ip/unknown.txt +1 -1
  30. data/test/fixtures/responses/local_search_maps/empty.txt +1 -0
  31. data/test/fixtures/responses/local_search_maps/not_found.txt +1 -0
  32. data/test/fixtures/responses/local_search_maps/success.txt +1 -1
  33. data/test/fixtures/responses/meta_carta/bad_address.xml +15 -7
  34. data/test/fixtures/responses/meta_carta/success.xml +29 -21
  35. data/test/fixtures/responses/yahoo/success.xml +2 -2
  36. data/test/fixtures/responses/yahoo/unknown_address.xml +6 -6
  37. data/test/mocks/uri.rb +3 -2
  38. data/test/unit/graticule/geocoder/google_test.rb +14 -0
  39. data/test/unit/graticule/geocoder/host_ip_test.rb +1 -0
  40. data/test/unit/graticule/geocoder/meta_carta_test.rb +1 -1
  41. data/test/unit/graticule/location_test.rb +1 -1
  42. metadata +10 -3
  43. data/test/unit/graticule/cli_test.rb +0 -26
data/CHANGELOG.txt CHANGED
@@ -1,3 +1,7 @@
1
+ 0.2.3 (2007-04-27)
2
+ * fixed Google for less precise queries
3
+ * added User-Agent to coerce Google into returning UTF-8 (Jonathan Tron)
4
+
1
5
  0.2.2 (2007-03-27)
2
6
  * fixed LocalSearchMaps
3
7
 
data/Manifest.txt CHANGED
@@ -12,6 +12,7 @@ lib/graticule/distance/haversine.rb
12
12
  lib/graticule/distance/spherical.rb
13
13
  lib/graticule/distance/vincenty.rb
14
14
  lib/graticule/geocoder.rb
15
+ lib/graticule/geocoder/base.rb
15
16
  lib/graticule/geocoder/bogus.rb
16
17
  lib/graticule/geocoder/geocoder_ca.rb
17
18
  lib/graticule/geocoder/geocoder_us.rb
@@ -24,11 +25,16 @@ lib/graticule/geocoder/rest.rb
24
25
  lib/graticule/geocoder/yahoo.rb
25
26
  lib/graticule/location.rb
26
27
  lib/graticule/version.rb
28
+ site/index.html
29
+ site/plugin.html
30
+ site/stylesheets/style.css
31
+ test/config.yml.default
27
32
  test/fixtures/responses/geocoder_us/success.xml
28
33
  test/fixtures/responses/geocoder_us/unknown.xml
29
34
  test/fixtures/responses/google/badkey.xml
30
35
  test/fixtures/responses/google/limit.xml
31
36
  test/fixtures/responses/google/missing_address.xml
37
+ test/fixtures/responses/google/partial.xml
32
38
  test/fixtures/responses/google/server_error.xml
33
39
  test/fixtures/responses/google/success.xml
34
40
  test/fixtures/responses/google/unavailable.xml
@@ -36,6 +42,8 @@ test/fixtures/responses/google/unknown_address.xml
36
42
  test/fixtures/responses/host_ip/private.txt
37
43
  test/fixtures/responses/host_ip/success.txt
38
44
  test/fixtures/responses/host_ip/unknown.txt
45
+ test/fixtures/responses/local_search_maps/empty.txt
46
+ test/fixtures/responses/local_search_maps/not_found.txt
39
47
  test/fixtures/responses/local_search_maps/success.txt
40
48
  test/fixtures/responses/meta_carta/bad_address.xml
41
49
  test/fixtures/responses/meta_carta/multiple.xml
@@ -59,3 +67,4 @@ test/unit/graticule/geocoder/postcode_anywhere_test.rb
59
67
  test/unit/graticule/geocoder/yahoo_test.rb
60
68
  test/unit/graticule/geocoder_test.rb
61
69
  test/unit/graticule/location_test.rb
70
+
data/Rakefile CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'rubygems'
2
+ require 'active_support'
2
3
  require 'hoe'
3
4
  require File.join(File.dirname(__FILE__), 'lib', 'graticule', 'version.rb')
4
5
 
@@ -15,3 +16,71 @@ Hoe.new("graticule", Graticule::Version::STRING) do |p|
15
16
  p.changes = p.paragraphs_of('CHANGELOG.txt', 0..1).join("\n\n")
16
17
  p.extra_deps << ['activesupport']
17
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/lib/graticule.rb CHANGED
@@ -2,8 +2,10 @@ $:.unshift(File.dirname(__FILE__))
2
2
 
3
3
  require 'active_support'
4
4
 
5
+ require 'graticule/version'
5
6
  require 'graticule/location'
6
7
  require 'graticule/geocoder'
8
+ require 'graticule/geocoder/base'
7
9
  require 'graticule/geocoder/bogus'
8
10
  require 'graticule/geocoder/rest'
9
11
  require 'graticule/geocoder/google'
data/lib/graticule/cli.rb CHANGED
@@ -20,7 +20,7 @@ module Graticule
20
20
  # -h, --help Help
21
21
  class Cli
22
22
 
23
- def self.start(args)
23
+ def self.start(args, out = STDOUT)
24
24
  options = { :service => :yahoo, :api_key => 'YahooDemo' }
25
25
 
26
26
  OptionParser.new do |opts|
@@ -42,12 +42,14 @@ module Graticule
42
42
 
43
43
  options[:location] = args.join(" ")
44
44
 
45
- result = Graticule.service(options[:service]).new(options[:api_key]).locate(options[:location])
45
+ result = Graticule.service(options[:service]).new(*options[:api_key].split(',')).locate(options[:location])
46
46
  location = (result.is_a?(Array) ? result.first : result)
47
47
  if location
48
- puts location.to_s(:coordinates => true)
48
+ out << location.to_s(:coordinates => true)
49
+ exit 0
49
50
  else
50
- puts "Location not found"
51
+ out << "Location not found"
52
+ exit 1
51
53
  end
52
54
  rescue Graticule::CredentialsError
53
55
  $stderr.puts "Invalid API key. Pass your #{options[:service]} API key using the -a option. "
@@ -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
@@ -39,7 +39,7 @@ module Graticule #:nodoc:
39
39
  end
40
40
 
41
41
  def check_error(xml) #:nodoc:
42
- raise AddressError, xml.text if xml.text == 'couldn\'t find this address! sorry'
42
+ raise AddressError, xml.text if xml.text =~ /couldn't find this address! sorry/
43
43
  end
44
44
 
45
45
  end
@@ -51,11 +51,11 @@ module Graticule #:nodoc:
51
51
  longitude, latitude, = xml.elements['/kml/Response/Placemark/Point/coordinates'].text.split(',').map { |v| v.to_f }
52
52
 
53
53
  Location.new \
54
- :street => value(address.elements['Country/AdministrativeArea/SubAdministrativeArea/Locality/Thoroughfare/ThoroughfareName/text()']),
55
- :locality => value(address.elements['Country/AdministrativeArea/SubAdministrativeArea/Locality/LocalityName/text()']),
56
- :region => value(address.elements['Country/AdministrativeArea/AdministrativeAreaName/text()']),
57
- :postal_code => value(address.elements['Country/AdministrativeArea/SubAdministrativeArea/Locality/PostalCode/PostalCodeNumber/text()']),
58
- :country => value(address.elements['Country/CountryNameCode/text()']),
54
+ :street => value(address.elements['//ThoroughfareName/text()']),
55
+ :locality => value(address.elements['//LocalityName/text()']),
56
+ :region => value(address.elements['//AdministrativeAreaName/text()']),
57
+ :postal_code => value(address.elements['//PostalCodeNumber/text()']),
58
+ :country => value(address.elements['//CountryNameCode/text()']),
59
59
  :latitude => latitude,
60
60
  :longitude => longitude,
61
61
  :precision => PRECISION[address.attribute('Accuracy').value.to_i] || :unknown
@@ -3,7 +3,7 @@ require 'yaml'
3
3
  module Graticule #:nodoc:
4
4
  module Geocoder #:nodoc:
5
5
 
6
- class HostIp
6
+ class HostIp < Base
7
7
 
8
8
  def initialize
9
9
  @url = URI.parse 'http://api.hostip.info/get_html.php'
@@ -11,15 +11,15 @@ module Graticule #:nodoc:
11
11
 
12
12
  # Geocode an IP address using http://hostip.info
13
13
  def locate(address)
14
- make_url(:ip => address, :position => true).open do |response|
15
- # add new line so YAML.load doesn't puke
16
- result = response.read + "\n"
17
- check_error(result)
18
- parse_response(YAML.load(result))
19
- end
14
+ get :ip => address, :position => true
20
15
  end
21
16
 
22
17
  private
18
+
19
+ def read_response(response)
20
+ # add new line so YAML.load doesn't puke
21
+ YAML.load(response + "\n")
22
+ end
23
23
 
24
24
  def parse_response(response) #:nodoc:
25
25
  returning Location.new do |location|
@@ -32,19 +32,10 @@ module Graticule #:nodoc:
32
32
  end
33
33
 
34
34
  def check_error(response) #:nodoc:
35
- raise AddressError, 'Unknown' if response =~ /Unknown City/
36
- raise AddressError, 'Private Address' if response =~ /Private Address/
35
+ raise AddressError, 'Unknown' if response['City'] =~ /Unknown City/
36
+ raise AddressError, 'Private Address' if response['City'] =~ /Private Address/
37
37
  end
38
38
 
39
- def make_url(params) #:nodoc:
40
- returning @url.dup do |url|
41
- url.query = params.map do |k,v|
42
- "#{URI.escape k.to_s}=#{URI.escape v.to_s}"
43
- end.join('&')
44
- end
45
- end
46
-
47
-
48
39
  end
49
40
  end
50
41
  end
@@ -4,7 +4,7 @@ module Graticule #:nodoc:
4
4
  # A library for lookup of coordinates with http://geo.localsearchmaps.com/
5
5
  #
6
6
  # See http://emad.fano.us/blog/?p=277
7
- class LocalSearchMaps < Rest
7
+ class LocalSearchMaps < Base
8
8
 
9
9
  def initialize
10
10
  @url = URI.parse 'http://geo.localsearchmaps.com/'
@@ -28,13 +28,13 @@ module Graticule #:nodoc:
28
28
  end
29
29
 
30
30
  def check_error(js)
31
- raise AddressError, "Empty Response" if js.nil? or js.text.nil?
32
- raise AddressError, 'Location not found' if js.text =~ /location not found/
31
+ raise AddressError, "Empty Response" if js.nil?
32
+ raise AddressError, 'Location not found' if js =~ /location not found/
33
33
  end
34
34
 
35
35
  def parse_response(js)
36
36
  returning Location.new do |location|
37
- coordinates = js.text.match(/map.centerAndZoom\(new GPoint\((.+?), (.+?)\)/)
37
+ coordinates = js.match(/map.centerAndZoom\(new GPoint\((.+?), (.+?)\)/)
38
38
  location.longitude = coordinates[1].to_f
39
39
  location.latitude = coordinates[2].to_f
40
40
  end
@@ -13,7 +13,7 @@ module Graticule
13
13
 
14
14
  # Finds +location+ and returns a Location object.
15
15
  def locate(location)
16
- get :q => location.is_a?(String) ? location : location_from_params(location).to_s
16
+ get :q => location.is_a?(String) ? location : location_from_params(location).to_s, :output => 'locations'
17
17
  end
18
18
 
19
19
  private
@@ -22,11 +22,6 @@ module Graticule
22
22
  raise AddressError, 'bad location' unless xml.elements['Locations/Location']
23
23
  end
24
24
 
25
- def make_url(params) # :nodoc:
26
- params[:output] = 'locations'
27
- super params
28
- end
29
-
30
25
  def parse_response(xml) # :nodoc:
31
26
  result = xml.elements['/Locations/Location[1]']
32
27
  coords = result.elements['Centroid/gml:Point/gml:coordinates'].text.split ','
@@ -1,110 +1,18 @@
1
- require 'open-uri'
2
1
  require 'rexml/document'
3
2
 
4
3
  module Graticule #:nodoc:
5
4
  module Geocoder #:nodoc:
6
5
 
7
- # Abstract class for implementing REST APIs.
8
- #
9
- # === Example
10
- #
11
- # The following methods must be implemented in sublcasses:
12
- #
13
- # +initialize+:: Sets @url to the service enpoint.
14
- # +check_error+:: Checks for errors in the server response.
15
- # +parse_response+:: Extracts information from the server response.
16
- #
17
- # If you have extra URL paramaters (application id, output type) or need to
18
- # perform URL customization, override +make_url+.
19
- #
20
- # class FakeService < RCRest
21
- #
22
- # class Error < RCRest::Error; end
23
- #
24
- # def initialize(appid)
25
- # @appid = appid
26
- # @url = URI.parse 'http://example.com/test'
27
- # end
28
- #
29
- # def check_error(xml)
30
- # raise Error, xml.elements['error'].text if xml.elements['error']
31
- # end
32
- #
33
- # def make_url(params)
34
- # params[:appid] = @appid
35
- # super params
36
- # end
37
- #
38
- # def parse_response(xml)
39
- # # return Location
40
- # end
41
- #
42
- # def test(query)
43
- # get :q => query
44
- # end
45
- #
46
- # end
47
- class Rest
6
+ # Abstract class for implementing REST geocoders. Passes on a REXML::Document
7
+ # to +check_errors+ and +parse_response+.
8
+ class Rest < Base
48
9
 
49
- # Web services initializer.
50
- #
51
- # Concrete web services implementations must set the +url+ instance
52
- # variable which must be a URI.
53
- def initialize
54
- raise NotImplementedError
55
- end
56
-
57
10
  private
58
11
 
59
- def location_from_params(params)
60
- case params
61
- when Location then params
62
- when Hash then Location.new params
63
- else
64
- raise ArgumentError, "Expected a Graticule::Location or a hash with :street, :locality, :region, :postal_code, and :country attributes"
65
- end
66
- end
67
-
68
- # Must extract and raise an error from +xml+, an REXML::Document, if any.
69
- # Must returns if no error could be found.
70
- def check_error(xml)
71
- raise NotImplementedError
72
- end
73
-
74
- # Performs a GET request with +params+. Calls the parse_response method on
75
- # the concrete class with an REXML::Document instance and returns its
76
- # result.
77
- def get(params = {})
78
- url = make_url params
79
-
80
- url.open do |response|
81
- res = REXML::Document.new response.read
82
- check_error(res)
83
- return parse_response(res)
84
- end
85
- rescue OpenURI::HTTPError => e
86
- response = REXML::Document.new e.io.read
87
- check_error response
88
- raise
12
+ def prepare_response(response)
13
+ REXML::Document.new(response)
89
14
  end
90
-
91
- # Creates a URI from the Hash +params+. Override this then call super if
92
- # you need to add extra params like an application id or output type.
93
- def make_url(params)
94
- escaped_params = params.sort_by { |k,v| k.to_s }.map do |k,v|
95
- "#{URI.escape k.to_s}=#{URI.escape v.to_s}"
96
- end
97
-
98
- url = @url.dup
99
- url.query = escaped_params.join '&'
100
- return url
101
- end
102
-
103
- # Must parse results from +xml+, an REXML::Document, into a Location.
104
- def parse_response(xml)
105
- raise NotImplementedError
106
- end
107
-
15
+
108
16
  end
109
17
  end
110
18
  end