abuiles-geokit 1.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.bundle/config +2 -0
  2. data/.gitignore +4 -0
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +19 -0
  5. data/History.txt +77 -0
  6. data/Manifest.txt +21 -0
  7. data/README.markdown +273 -0
  8. data/Rakefile +13 -0
  9. data/geokit.gemspec +24 -0
  10. data/lib/geokit.rb +55 -0
  11. data/lib/geokit/bounds.rb +95 -0
  12. data/lib/geokit/geo_loc.rb +115 -0
  13. data/lib/geokit/geocoders.rb +68 -0
  14. data/lib/geokit/geocoders/ca_geocoder.rb +54 -0
  15. data/lib/geokit/geocoders/geo_plugin_geocoder.rb +30 -0
  16. data/lib/geokit/geocoders/geocode_error.rb +7 -0
  17. data/lib/geokit/geocoders/geocoder.rb +75 -0
  18. data/lib/geokit/geocoders/geonames_geocoder.rb +53 -0
  19. data/lib/geokit/geocoders/google_geocoder.rb +145 -0
  20. data/lib/geokit/geocoders/google_premier_geocoder.rb +147 -0
  21. data/lib/geokit/geocoders/ip_geocoder.rb +76 -0
  22. data/lib/geokit/geocoders/multi_geocoder.rb +60 -0
  23. data/lib/geokit/geocoders/us_geocoder.rb +50 -0
  24. data/lib/geokit/geocoders/yahoo_geocoder.rb +49 -0
  25. data/lib/geokit/inflector.rb +39 -0
  26. data/lib/geokit/lat_lng.rb +112 -0
  27. data/lib/geokit/mappable.rb +210 -0
  28. data/lib/geokit/too_many_queries_error.rb +4 -0
  29. data/lib/geokit/version.rb +3 -0
  30. data/test/test_base_geocoder.rb +58 -0
  31. data/test/test_bounds.rb +97 -0
  32. data/test/test_ca_geocoder.rb +39 -0
  33. data/test/test_geoloc.rb +72 -0
  34. data/test/test_geoplugin_geocoder.rb +58 -0
  35. data/test/test_google_geocoder.rb +225 -0
  36. data/test/test_google_premier_geocoder.rb +88 -0
  37. data/test/test_google_reverse_geocoder.rb +47 -0
  38. data/test/test_inflector.rb +24 -0
  39. data/test/test_ipgeocoder.rb +109 -0
  40. data/test/test_latlng.rb +209 -0
  41. data/test/test_multi_geocoder.rb +91 -0
  42. data/test/test_multi_ip_geocoder.rb +36 -0
  43. data/test/test_us_geocoder.rb +54 -0
  44. data/test/test_yahoo_geocoder.rb +103 -0
  45. metadata +141 -0
@@ -0,0 +1,55 @@
1
+ module Geokit
2
+ VERSION = '1.5.0'
3
+
4
+ # These defaults are used in Geokit::Mappable.distance_to and in acts_as_mappable
5
+ @@default_units = :miles
6
+ @@default_formula = :sphere
7
+
8
+ [:default_units, :default_formula].each do |sym|
9
+ class_eval <<-EOS, __FILE__, __LINE__
10
+ def self.#{sym}
11
+ if defined?(#{sym.to_s.upcase})
12
+ #{sym.to_s.upcase}
13
+ else
14
+ @@#{sym}
15
+ end
16
+ end
17
+
18
+ def self.#{sym}=(obj)
19
+ @@#{sym} = obj
20
+ end
21
+ EOS
22
+ end
23
+ end
24
+
25
+ require 'net/http'
26
+ require 'ipaddr'
27
+ require 'rexml/document'
28
+ require 'yaml'
29
+ require 'timeout'
30
+ require 'logger'
31
+ require 'active_support/core_ext/hash'
32
+ require 'active_support/core_ext/object/conversions'
33
+ require 'openssl'
34
+ require 'base64'
35
+ require 'json'
36
+
37
+ require 'geokit/too_many_queries_error'
38
+ require 'geokit/inflector'
39
+ require 'geokit/geocoders'
40
+ require 'geokit/mappable'
41
+ require 'geokit/lat_lng'
42
+ require 'geokit/geo_loc'
43
+ require 'geokit/bounds'
44
+ require 'geokit/geocoders/geocode_error'
45
+ require 'geokit/geocoders/geocoder'
46
+
47
+ require 'geokit/geocoders/ca_geocoder'
48
+ require 'geokit/geocoders/geo_plugin_geocoder'
49
+ require 'geokit/geocoders/geonames_geocoder'
50
+ require 'geokit/geocoders/google_geocoder'
51
+ require 'geokit/geocoders/google_premier_geocoder'
52
+ require 'geokit/geocoders/ip_geocoder'
53
+ require 'geokit/geocoders/multi_geocoder'
54
+ require 'geokit/geocoders/us_geocoder'
55
+ require 'geokit/geocoders/yahoo_geocoder'
@@ -0,0 +1,95 @@
1
+ module Geokit
2
+ # Bounds represents a rectangular bounds, defined by the SW and NE corners
3
+ class Bounds
4
+ # sw and ne are LatLng objects
5
+ attr_accessor :sw, :ne
6
+
7
+ # provide sw and ne to instantiate a new Bounds instance
8
+ def initialize(sw,ne)
9
+ raise ArgumentError if !(sw.is_a?(Geokit::LatLng) && ne.is_a?(Geokit::LatLng))
10
+ @sw,@ne=sw,ne
11
+ end
12
+
13
+ #returns the a single point which is the center of the rectangular bounds
14
+ def center
15
+ @sw.midpoint_to(@ne)
16
+ end
17
+
18
+ # a simple string representation:sw,ne
19
+ def to_s
20
+ "#{@sw.to_s},#{@ne.to_s}"
21
+ end
22
+
23
+ # a two-element array of two-element arrays: sw,ne
24
+ def to_a
25
+ [@sw.to_a, @ne.to_a]
26
+ end
27
+
28
+ # Returns true if the bounds contain the passed point.
29
+ # allows for bounds which cross the meridian
30
+ def contains?(point)
31
+ point=Geokit::LatLng.normalize(point)
32
+ res = point.lat > @sw.lat && point.lat < @ne.lat
33
+ if crosses_meridian?
34
+ res &= point.lng < @ne.lng || point.lng > @sw.lng
35
+ else
36
+ res &= point.lng < @ne.lng && point.lng > @sw.lng
37
+ end
38
+ res
39
+ end
40
+
41
+ # returns true if the bounds crosses the international dateline
42
+ def crosses_meridian?
43
+ @sw.lng > @ne.lng
44
+ end
45
+
46
+ # Returns true if the candidate object is logically equal. Logical equivalence
47
+ # is true if the lat and lng attributes are the same for both objects.
48
+ def ==(other)
49
+ other.is_a?(Bounds) ? self.sw == other.sw && self.ne == other.ne : false
50
+ end
51
+
52
+ # Equivalent to Google Maps API's .toSpan() method on GLatLng's.
53
+ #
54
+ # Returns a LatLng object, whose coordinates represent the size of a rectangle
55
+ # defined by these bounds.
56
+ def to_span
57
+ lat_span = (@ne.lat - @sw.lat).abs
58
+ lng_span = (crosses_meridian? ? 360 + @ne.lng - @sw.lng : @ne.lng - @sw.lng).abs
59
+ Geokit::LatLng.new(lat_span, lng_span)
60
+ end
61
+
62
+ class <<self
63
+
64
+ # returns an instance of bounds which completely encompases the given circle
65
+ def from_point_and_radius(point,radius,options={})
66
+ point=LatLng.normalize(point)
67
+ p0=point.endpoint(0,radius,options)
68
+ p90=point.endpoint(90,radius,options)
69
+ p180=point.endpoint(180,radius,options)
70
+ p270=point.endpoint(270,radius,options)
71
+ sw=Geokit::LatLng.new(p180.lat,p270.lng)
72
+ ne=Geokit::LatLng.new(p0.lat,p90.lng)
73
+ Geokit::Bounds.new(sw,ne)
74
+ end
75
+
76
+ # Takes two main combinations of arguments to create a bounds:
77
+ # point,point (this is the only one which takes two arguments
78
+ # [point,point]
79
+ # . . . where a point is anything LatLng#normalize can handle (which is quite a lot)
80
+ #
81
+ # NOTE: everything combination is assumed to pass points in the order sw, ne
82
+ def normalize (thing,other=nil)
83
+ # maybe this will be simple -- an actual bounds object is passed, and we can all go home
84
+ return thing if thing.is_a? Bounds
85
+
86
+ # no? OK, if there's no "other," the thing better be a two-element array
87
+ thing,other=thing if !other && thing.is_a?(Array) && thing.size==2
88
+
89
+ # Now that we're set with a thing and another thing, let LatLng do the heavy lifting.
90
+ # Exceptions may be thrown
91
+ Bounds.new(Geokit::LatLng.normalize(thing),Geokit::LatLng.normalize(other))
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,115 @@
1
+ module Geokit
2
+ # This class encapsulates the result of a geocoding call.
3
+ # It's primary purpose is to homogenize the results of multiple
4
+ # geocoding providers. It also provides some additional functionality, such as
5
+ # the "full address" method for geocoders that do not provide a
6
+ # full address in their results (for example, Yahoo), and the "is_us" method.
7
+ #
8
+ # Some geocoders can return multple results. Geoloc can capture multiple results through
9
+ # its "all" method.
10
+ #
11
+ # For the geocoder setting the results, it would look something like this:
12
+ # geo=GeoLoc.new(first_result)
13
+ # geo.all.push(second_result)
14
+ # geo.all.push(third_result)
15
+ #
16
+ # Then, for the user of the result:
17
+ #
18
+ # puts geo.full_address # just like usual
19
+ # puts geo.all.size => 3 # there's three results total
20
+ # puts geo.all.first # all is just an array or additional geolocs,
21
+ # so do what you want with it
22
+ class GeoLoc < LatLng
23
+
24
+ # Location attributes. Full address is a concatenation of all values. For example:
25
+ # 100 Spear St, San Francisco, CA, 94101, US
26
+ attr_accessor :street_address, :city, :state, :zip, :country_code, :country, :full_address, :all, :district, :province
27
+ # Attributes set upon return from geocoding. Success will be true for successful
28
+ # geocode lookups. The provider will be set to the name of the providing geocoder.
29
+ # Finally, precision is an indicator of the accuracy of the geocoding.
30
+ attr_accessor :success, :provider, :precision, :suggested_bounds
31
+ # Street number and street name are extracted from the street address attribute.
32
+ attr_reader :street_number, :street_name
33
+ # accuracy is set for Yahoo and Google geocoders, it is a numeric value of the
34
+ # precision. see http://code.google.com/apis/maps/documentation/geocoding/#GeocodingAccuracy
35
+ attr_accessor :accuracy
36
+
37
+ # Constructor expects a hash of symbols to correspond with attributes.
38
+ def initialize(h={})
39
+ @all = [self]
40
+
41
+ @street_address=h[:street_address]
42
+ @city=h[:city]
43
+ @state=h[:state]
44
+ @zip=h[:zip]
45
+ @country_code=h[:country_code]
46
+ @province = h[:province]
47
+ @success=false
48
+ @precision='unknown'
49
+ @full_address=nil
50
+ super(h[:lat],h[:lng])
51
+ end
52
+
53
+ # Returns true if geocoded to the United States.
54
+ def is_us?
55
+ country_code == 'US'
56
+ end
57
+
58
+ def success?
59
+ success == true
60
+ end
61
+
62
+ # full_address is provided by google but not by yahoo. It is intended that the google
63
+ # geocoding method will provide the full address, whereas for yahoo it will be derived
64
+ # from the parts of the address we do have.
65
+ def full_address
66
+ @full_address ? @full_address : to_geocodeable_s
67
+ end
68
+
69
+ # Extracts the street number from the street address if the street address
70
+ # has a value.
71
+ def street_number
72
+ street_address[/(\d*)/] if street_address
73
+ end
74
+
75
+ # Returns the street name portion of the street address.
76
+ def street_name
77
+ street_address[street_number.length, street_address.length].strip if street_address
78
+ end
79
+
80
+ # gives you all the important fields as key-value pairs
81
+ def hash
82
+ res={}
83
+ [:success,:lat,:lng,:country_code,:city,:state,:zip,:street_address,:province,:district,:provider,:full_address,:is_us?,:ll,:precision].each { |s| res[s] = self.send(s.to_s) }
84
+ res
85
+ end
86
+ alias to_hash hash
87
+
88
+ # Sets the city after capitalizing each word within the city name.
89
+ def city=(city)
90
+ @city = Geokit::Inflector::titleize(city) if city
91
+ end
92
+
93
+ # Sets the street address after capitalizing each word within the street address.
94
+ def street_address=(address)
95
+ @street_address = Geokit::Inflector::titleize(address) if address
96
+ end
97
+
98
+ # Returns a comma-delimited string consisting of the street address, city, state,
99
+ # zip, and country code. Only includes those attributes that are non-blank.
100
+ def to_geocodeable_s
101
+ a=[street_address, district, city, province, state, zip, country_code].compact
102
+ a.delete_if { |e| !e || e == '' }
103
+ a.join(', ')
104
+ end
105
+
106
+ def to_yaml_properties
107
+ (instance_variables - ['@all']).sort
108
+ end
109
+
110
+ # Returns a string representation of the instance.
111
+ def to_s
112
+ "Provider: #{provider}\nStreet: #{street_address}\nCity: #{city}\nState: #{state}\nZip: #{zip}\nLatitude: #{lat}\nLongitude: #{lng}\nCountry: #{country_code}\nSuccess: #{success}"
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,68 @@
1
+ module Geokit
2
+ # Contains a range of geocoders:
3
+ #
4
+ # ### "regular" address geocoders
5
+ # * Yahoo Geocoder - requires an API key.
6
+ # * Geocoder.us - may require authentication if performing more than the free request limit.
7
+ # * Geocoder.ca - for Canada; may require authentication as well.
8
+ # * Geonames - a free geocoder
9
+ #
10
+ # ### address geocoders that also provide reverse geocoding
11
+ # * Google Geocoder - requires an API key.
12
+ #
13
+ # ### IP address geocoders
14
+ # * IP Geocoder - geocodes an IP address using hostip.info's web service.
15
+ # * Geoplugin.net -- another IP address geocoder
16
+ #
17
+ # ### The Multigeocoder
18
+ # * Multi Geocoder - provides failover for the physical location geocoders.
19
+ #
20
+ # Some of these geocoders require configuration. You don't have to provide it here. See the README.
21
+ module Geocoders
22
+ @@proxy_addr = nil
23
+ @@proxy_port = nil
24
+ @@proxy_user = nil
25
+ @@proxy_pass = nil
26
+ @@request_timeout = nil
27
+ @@yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
28
+ @@google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
29
+ @@google_client = 'REPLACE_WITH_YOUR_GOOGLE_CLIENT'
30
+ @@google_channel = 'REPLACE_WITH_YOUR_GOOGLE_CHANNEL'
31
+ @@geocoder_us = false
32
+ @@geocoder_ca = false
33
+ @@geonames = false
34
+ @@provider_order = [:google,:us]
35
+ @@ip_provider_order = [:geo_plugin,:ip]
36
+ @@logger=Logger.new(STDOUT)
37
+ @@logger.level=Logger::INFO
38
+ @@domain = nil
39
+
40
+ def self.__define_accessors
41
+ class_variables.each do |v|
42
+ sym = v.to_s.delete("@").to_sym
43
+ unless self.respond_to? sym
44
+ module_eval <<-EOS, __FILE__, __LINE__
45
+ def self.#{sym}
46
+ value = if defined?(#{sym.to_s.upcase})
47
+ #{sym.to_s.upcase}
48
+ else
49
+ @@#{sym}
50
+ end
51
+ if value.is_a?(Hash)
52
+ value = (self.domain.nil? ? nil : value[self.domain]) || value.values.first
53
+ end
54
+ value
55
+ end
56
+
57
+ def self.#{sym}=(obj)
58
+ @@#{sym} = obj
59
+ end
60
+ EOS
61
+ end
62
+ end
63
+ end
64
+
65
+ __define_accessors
66
+
67
+ end
68
+ end
@@ -0,0 +1,54 @@
1
+ module Geokit
2
+ module Geocoders
3
+ # Geocoder CA geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_CA variable to
4
+ # contain true or false based upon whether authentication is to occur. Conforms to the
5
+ # interface set by the Geocoder class.
6
+ #
7
+ # Returns a response like:
8
+ # <?xml version="1.0" encoding="UTF-8" ?>
9
+ # <geodata>
10
+ # <latt>49.243086</latt>
11
+ # <longt>-123.153684</longt>
12
+ # </geodata>
13
+ class CaGeocoder < Geocoder
14
+
15
+ private
16
+
17
+ # Template method which does the geocode lookup.
18
+ def self.do_geocode(address, options = {})
19
+ raise ArgumentError('Geocoder.ca requires a GeoLoc argument') unless address.is_a?(GeoLoc)
20
+ url = construct_request(address)
21
+ res = self.call_geocoder_service(url)
22
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
23
+ xml = res.body
24
+ logger.debug "Geocoder.ca geocoding. Address: #{address}. Result: #{xml}"
25
+ # Parse the document.
26
+ doc = REXML::Document.new(xml)
27
+ address.lat = doc.elements['//latt'].text
28
+ address.lng = doc.elements['//longt'].text
29
+ address.success = true
30
+ return address
31
+ rescue
32
+ logger.error "Caught an error during Geocoder.ca geocoding call: "+$!
33
+ return GeoLoc.new
34
+ end
35
+
36
+ # Formats the request in the format acceptable by the CA geocoder.
37
+ def self.construct_request(location)
38
+ url = ""
39
+ url += add_ampersand(url) + "stno=#{location.street_number}" if location.street_address
40
+ url += add_ampersand(url) + "addresst=#{Geokit::Inflector::url_escape(location.street_name)}" if location.street_address
41
+ url += add_ampersand(url) + "city=#{Geokit::Inflector::url_escape(location.city)}" if location.city
42
+ url += add_ampersand(url) + "prov=#{location.state}" if location.state
43
+ url += add_ampersand(url) + "postal=#{location.zip}" if location.zip
44
+ url += add_ampersand(url) + "auth=#{Geokit::Geocoders::geocoder_ca}" if Geokit::Geocoders::geocoder_ca
45
+ url += add_ampersand(url) + "geoit=xml"
46
+ 'http://geocoder.ca/?' + url
47
+ end
48
+
49
+ def self.add_ampersand(url)
50
+ url && url.length > 0 ? "&" : ""
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,30 @@
1
+ module Geokit
2
+ module Geocoders
3
+ # Provides geocoding based upon an IP address. The underlying web service is geoplugin.net
4
+ class GeoPluginGeocoder < Geocoder
5
+ private
6
+
7
+ def self.do_geocode(ip, options = {})
8
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
9
+ response = self.call_geocoder_service("http://www.geoplugin.net/xml.gp?ip=#{ip}")
10
+ return response.is_a?(Net::HTTPSuccess) ? parse_xml(response.body) : GeoLoc.new
11
+ rescue
12
+ logger.error "Caught an error during GeoPluginGeocoder geocoding call: "+$!
13
+ return GeoLoc.new
14
+ end
15
+
16
+ def self.parse_xml(xml)
17
+ xml = REXML::Document.new(xml)
18
+ geo = GeoLoc.new
19
+ geo.provider='geoPlugin'
20
+ geo.city = xml.elements['//geoplugin_city'].text
21
+ geo.state = xml.elements['//geoplugin_region'].text
22
+ geo.country_code = xml.elements['//geoplugin_countryCode'].text
23
+ geo.lat = xml.elements['//geoplugin_latitude'].text.to_f
24
+ geo.lng = xml.elements['//geoplugin_longitude'].text.to_f
25
+ geo.success = !!geo.city && !geo.city.empty?
26
+ return geo
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,7 @@
1
+ module Geokit
2
+ module Geocoders
3
+ # Error which is thrown in the event a geocoding error occurs.
4
+ class GeocodeError < StandardError
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,75 @@
1
+ module Geokit
2
+ module Geocoders
3
+
4
+
5
+ # -------------------------------------------------------------------------------------------
6
+ # Geocoder Base class -- every geocoder should inherit from this
7
+ # -------------------------------------------------------------------------------------------
8
+
9
+ # The Geocoder base class which defines the interface to be used by all
10
+ # other geocoders.
11
+ class Geocoder
12
+ # Main method which calls the do_geocode template method which subclasses
13
+ # are responsible for implementing. Returns a populated GeoLoc or an
14
+ # empty one with a failed success code.
15
+ def self.geocode(address, options = {})
16
+ res = do_geocode(address, options)
17
+ return res.nil? ? GeoLoc.new : res
18
+ end
19
+ # Main method which calls the do_reverse_geocode template method which subclasses
20
+ # are responsible for implementing. Returns a populated GeoLoc or an
21
+ # empty one with a failed success code.
22
+ def self.reverse_geocode(latlng)
23
+ res = do_reverse_geocode(latlng)
24
+ return res.success? ? res : GeoLoc.new
25
+ end
26
+
27
+ # Call the geocoder service using the timeout if configured.
28
+ def self.call_geocoder_service(url)
29
+ Timeout::timeout(Geokit::Geocoders::request_timeout) { return self.do_get(url) } if Geokit::Geocoders::request_timeout
30
+ return self.do_get(url)
31
+ rescue TimeoutError
32
+ return nil
33
+ end
34
+
35
+ # Not all geocoders can do reverse geocoding. So, unless the subclass explicitly overrides this method,
36
+ # a call to reverse_geocode will return an empty GeoLoc. If you happen to be using MultiGeocoder,
37
+ # this will cause it to failover to the next geocoder, which will hopefully be one which supports reverse geocoding.
38
+ def self.do_reverse_geocode(latlng)
39
+ return GeoLoc.new
40
+ end
41
+
42
+ protected
43
+
44
+ def self.logger()
45
+ Geokit::Geocoders::logger
46
+ end
47
+
48
+ private
49
+
50
+ # Wraps the geocoder call around a proxy if necessary.
51
+ def self.do_get(url)
52
+ uri = URI.parse(url)
53
+ req = Net::HTTP::Get.new(url)
54
+ req.basic_auth(uri.user, uri.password) if uri.userinfo
55
+ res = Net::HTTP::Proxy(Geokit::Geocoders::proxy_addr,
56
+ Geokit::Geocoders::proxy_port,
57
+ Geokit::Geocoders::proxy_user,
58
+ Geokit::Geocoders::proxy_pass).start(uri.host, uri.port) { |http| http.get(uri.path + "?" + uri.query) }
59
+ return res
60
+ end
61
+
62
+ # Adds subclass' geocode method making it conveniently available through
63
+ # the base class.
64
+ def self.inherited(clazz)
65
+ class_name = clazz.name.split('::').last
66
+ src = <<-END_SRC
67
+ def self.#{Geokit::Inflector.underscore(class_name)}(address, options = {})
68
+ #{class_name}.geocode(address, options)
69
+ end
70
+ END_SRC
71
+ class_eval(src)
72
+ end
73
+ end
74
+ end
75
+ end