abuiles-geokit 1.6.1

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 (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