geokit 1.2.5

Sign up to get free protection for your applications and to get access to all the features.
data/.project ADDED
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <projectDescription>
3
+ <name>geokit</name>
4
+ <comment></comment>
5
+ <projects>
6
+ </projects>
7
+ <buildSpec>
8
+ <buildCommand>
9
+ <name>org.rubypeople.rdt.core.rubybuilder</name>
10
+ <arguments>
11
+ </arguments>
12
+ </buildCommand>
13
+ </buildSpec>
14
+ <natures>
15
+ <nature>org.rubypeople.rdt.core.rubynature</nature>
16
+ </natures>
17
+ </projectDescription>
data/History.txt ADDED
@@ -0,0 +1,29 @@
1
+ === 1.2.5 / 2009-02-25
2
+
3
+ * fixed GeoLoc.to_yaml
4
+ * fixed minor google geocoding bug
5
+ * now periodically publishing the Geokit gem to Rubyforge. Still maintaining development and managing contributions at Github
6
+
7
+ === 1.2.4 / 2009-02-25
8
+
9
+ * Improved Google geocoder in the Gem: Support for multiple geocoding results from the Google geocoder. (thanks github/pic)
10
+
11
+ === 1.2.3 / 2009-02-01
12
+
13
+ * Adding GeoPluginGeocoder for IP geocoding (thanks github/xjunior)
14
+ * Ruby 1.9.1 compatibility and Unicode fixes (thanks github/Nielsomat)
15
+ * various bug fixes
16
+
17
+ === 1.2.1 / 2009-01-05
18
+
19
+ * minor bug fixes
20
+ * reverse geocoding added (Google only): res=Geokit::Geocoders::GoogleGeocoder.reverse_geocode "37.791821,-122.394679"
21
+ * nautical miles added (in addition to miles and KM)
22
+
23
+ === 1.2.0 / 2008-12-01
24
+
25
+ * Improved Geocoder.us support -- respects authentication, and can geocode city names or zipcodes alone
26
+ * cross-meridian finds work correctly with bounds conditions
27
+ * fixed a problem with columns with "distance" in their name
28
+ * added Geonames geocoder
29
+ * the gem and plugin are now hosted at Github.
data/Manifest.txt ADDED
@@ -0,0 +1,22 @@
1
+ .project
2
+ History.txt
3
+ Manifest.txt
4
+ README.markdown
5
+ Rakefile
6
+ geokit.gemspec
7
+ lib/geokit.rb
8
+ lib/geokit/geocoders.rb
9
+ lib/geokit/mappable.rb
10
+ test/test_base_geocoder.rb
11
+ test/test_bounds.rb
12
+ test/test_ca_geocoder.rb
13
+ test/test_geoloc.rb
14
+ test/test_geoplugin_geocoder.rb
15
+ test/test_google_geocoder.rb
16
+ test/test_google_reverse_geocoder.rb
17
+ test/test_inflector.rb
18
+ test/test_ipgeocoder.rb
19
+ test/test_latlng.rb
20
+ test/test_multi_geocoder.rb
21
+ test/test_us_geocoder.rb
22
+ test/test_yahoo_geocoder.rb
data/README.markdown ADDED
@@ -0,0 +1,190 @@
1
+ ## GEOKIT GEM DESCRIPTION
2
+
3
+ The Geokit gem provides:
4
+
5
+ * Distance calculations between two points on the earth. Calculate the distance in miles, kilometers, or nautical miles, with all the trigonometry abstracted away by GeoKit.
6
+ * Geocoding from multiple providers. It supports Google, Yahoo, Geocoder.us, and Geocoder.ca geocoders, and others. It provides a uniform response structure from all of them.
7
+ It also provides a fail-over mechanism, in case your input fails to geocode in one service.
8
+ * Rectangular bounds calculations: is a point within a given rectangular bounds?
9
+ * Heading and midpoint calculations
10
+
11
+ Combine this gem with the [geokit-rails plugin](http://github.com/andre/geokit-rails/tree/master) to get location-based finders for your Rails app.
12
+
13
+ * Geokit Documentation at Rubyforge [http://geokit.rubyforge.org](http://geokit.rubyforge.org).
14
+ * Repository at Github: [http://github.com/andre/geokit-gem/tree/master](http://github.com/andre/geokit-gem/tree/master).
15
+ * Follow the Google Group for updates and discussion on Geokit: [http://groups.google.com/group/geokit](http://groups.google.com/group/geokit)
16
+
17
+ ## INSTALL
18
+
19
+ gem sources -a http://gems.github.com
20
+ sudo gem install andre-geokit
21
+
22
+ ## QUICK START
23
+
24
+ irb> require 'rubygems'
25
+ irb> require 'geokit'
26
+ irb> a=Geokit::Geocoders::YahooGeocoder.geocode '140 Market St, San Francisco, CA'
27
+ irb> a.ll
28
+ => 37.79363,-122.396116
29
+ irb> b=Geokit::Geocoders::YahooGeocoder.geocode '789 Geary St, San Francisco, CA'
30
+ irb> b.ll
31
+ => 37.786217,-122.41619
32
+ irb> a.distance_to(b)
33
+ => 1.21120007413626
34
+ irb> a.heading_to(b)
35
+ => 244.959832435678
36
+ irb(main):006:0> c=a.midpoint_to(b) # what's halfway from a to b?
37
+ irb> c.ll
38
+ => "37.7899239257175,-122.406153503469"
39
+ irb(main):008:0> d=c.endpoint(90,10) # what's 10 miles to the east of c?
40
+ irb> d.ll
41
+ => "37.7897825005142,-122.223214776155"
42
+
43
+ FYI, that `.ll` method means "latitude longitude".
44
+
45
+ See the RDOC more more ... there are also operations on rectangular bounds (e.g., determining if a point is within bounds, find the center, etc).
46
+
47
+ ## CONFIGURATION
48
+
49
+ If you're using this gem by itself, here are the configuration options:
50
+
51
+ # These defaults are used in Geokit::Mappable.distance_to and in acts_as_mappable
52
+ Geokit::default_units = :miles
53
+ Geokit::default_formula = :sphere
54
+
55
+ # This is the timeout value in seconds to be used for calls to the geocoder web
56
+ # services. For no timeout at all, comment out the setting. The timeout unit
57
+ # is in seconds.
58
+ Geokit::Geocoders::timeout = 3
59
+
60
+ # These settings are used if web service calls must be routed through a proxy.
61
+ # These setting can be nil if not needed, otherwise, addr and port must be
62
+ # filled in at a minimum. If the proxy requires authentication, the username
63
+ # and password can be provided as well.
64
+ Geokit::Geocoders::proxy_addr = nil
65
+ Geokit::Geocoders::proxy_port = nil
66
+ Geokit::Geocoders::proxy_user = nil
67
+ Geokit::Geocoders::proxy_pass = nil
68
+
69
+ # This is your yahoo application key for the Yahoo Geocoder.
70
+ # See http://developer.yahoo.com/faq/index.html#appid
71
+ # and http://developer.yahoo.com/maps/rest/V1/geocode.html
72
+ Geokit::Geocoders::yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
73
+
74
+ # This is your Google Maps geocoder key.
75
+ # See http://www.google.com/apis/maps/signup.html
76
+ # and http://www.google.com/apis/maps/documentation/#Geocoding_Examples
77
+ Geokit::Geocoders::google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
78
+
79
+ # This is your username and password for geocoder.us.
80
+ # To use the free service, the value can be set to nil or false. For
81
+ # usage tied to an account, the value should be set to username:password.
82
+ # See http://geocoder.us
83
+ # and http://geocoder.us/user/signup
84
+ Geokit::Geocoders::geocoder_us = false
85
+
86
+ # This is your authorization key for geocoder.ca.
87
+ # To use the free service, the value can be set to nil or false. For
88
+ # usage tied to an account, set the value to the key obtained from
89
+ # Geocoder.ca.
90
+ # See http://geocoder.ca
91
+ # and http://geocoder.ca/?register=1
92
+ Geokit::Geocoders::geocoder_ca = false
93
+
94
+ # This is the order in which the geocoders are called in a failover scenario
95
+ # If you only want to use a single geocoder, put a single symbol in the array.
96
+ # Valid symbols are :google, :yahoo, :us, and :ca.
97
+ # Be aware that there are Terms of Use restrictions on how you can use the
98
+ # various geocoders. Make sure you read up on relevant Terms of Use for each
99
+ # geocoder you are going to use.
100
+ Geokit::Geocoders::provider_order = [:google,:us]
101
+
102
+ If you're using this gem with the [geokit-rails plugin](http://github.com/andre/geokit-rails/tree/master), the plugin
103
+ creates a template with these settings and places it in `config/initializers/geokit_config.rb`.
104
+
105
+ ## SUPPORTED GEOCODERS
106
+
107
+ ### "regular" address geocoders
108
+ * Yahoo Geocoder - requires an API key.
109
+ * Geocoder.us - may require authentication if performing more than the free request limit.
110
+ * Geocoder.ca - for Canada; may require authentication as well.
111
+ * Geonames - a free geocoder
112
+
113
+ ### address geocoders that also provide reverse geocoding
114
+ * Google Geocoder - requires an API key. Also supports multiple results.
115
+
116
+ ### IP address geocoders
117
+ * IP Geocoder - geocodes an IP address using hostip.info's web service.
118
+ * Geoplugin.net -- another IP address geocoder
119
+
120
+ ### The Multigeocoder
121
+ * Multi Geocoder - provides failover for the physical location geocoders.
122
+
123
+ ## MULTIPLE RESULTS
124
+ Some geocoding services will return multple results if the there isn't one clear result.
125
+ Geoloc can capture multiple results through its "all" method. Currently only the Google geocoder
126
+ supports multiple results:
127
+
128
+ irb> geo=Geokit::Geocoders::GoogleGeocoder.geocode("900 Sycamore Drive")
129
+ irb> geo.full_address
130
+ => "900 Sycamore Dr, Arkadelphia, AR 71923, USA"
131
+ irb> geo.all.size
132
+ irb> geo.all.each { |e| puts e.full_address }
133
+ 900 Sycamore Dr, Arkadelphia, AR 71923, USA
134
+ 900 Sycamore Dr, Burkburnett, TX 76354, USA
135
+ 900 Sycamore Dr, TN 38361, USA
136
+ ....
137
+
138
+ geo.all is just an array of additional Geolocs, so do what you want with it. If you call .all on a
139
+ geoloc that doesn't have any additional results, you will get an array of one.
140
+
141
+
142
+ ## NOTES ON WHAT'S WHERE
143
+
144
+ mappable.rb contains the Mappable module, which provides basic
145
+ distance calculation methods, i.e., calculating the distance
146
+ between two points.
147
+
148
+ mappable.rb also contains LatLng, GeoLoc, and Bounds.
149
+ LatLng is a simple container for latitude and longitude, but
150
+ it's made more powerful by mixing in the above-mentioned Mappable
151
+ module -- therefore, you can calculate easily the distance between two
152
+ LatLng ojbects with `distance = first.distance_to(other)`
153
+
154
+ GeoLoc (also in mappable.rb) represents an address or location which
155
+ has been geocoded. You can get the city, zipcode, street address, etc.
156
+ from a GeoLoc object. GeoLoc extends LatLng, so you also get lat/lng
157
+ AND the Mappable modeule goodness for free.
158
+
159
+ geocoders.rb contains all the geocoder implemenations. All the gercoders
160
+ inherit from a common base (class Geocoder) and implement the private method
161
+ do_geocode.
162
+
163
+ ## GOOGLE GROUP
164
+
165
+ Follow the Google Group for updates and discussion on Geokit: http://groups.google.com/group/geokit
166
+
167
+ ## LICENSE
168
+
169
+ (The MIT License)
170
+
171
+ Copyright (c) 2007-2009 Andre Lewis and Bill Eisenhauer
172
+
173
+ Permission is hereby granted, free of charge, to any person obtaining
174
+ a copy of this software and associated documentation files (the
175
+ 'Software'), to deal in the Software without restriction, including
176
+ without limitation the rights to use, copy, modify, merge, publish,
177
+ distribute, sublicense, and/or sell copies of the Software, and to
178
+ permit persons to whom the Software is furnished to do so, subject to
179
+ the following conditions:
180
+
181
+ The above copyright notice and this permission notice shall be
182
+ included in all copies or substantial portions of the Software.
183
+
184
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
185
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
186
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
187
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
188
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
189
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
190
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/geokit.rb'
6
+
7
+ Hoe.new('geokit', Geokit::VERSION) do |p|
8
+ #p.rubyforge_name = 'geokit' # if different than lowercase project name
9
+ p.developer('Andre Lewis', 'andre@earthcode.com')
10
+ p.summary="Geokit provides geocoding and distance calculation in an easy-to-use API"
11
+ end
12
+
13
+ # vim: syntax=Ruby
data/geokit.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{geokit}
5
+ s.version = "1.2.5"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Andre Lewis and Bill Eisenhauer"]
9
+ s.date = %q{2009-02-09}
10
+ s.description = %q{Geokit Gem}
11
+ s.email = ["andre@earthcode.com / bill_eisenhauer@yahoo.com"]
12
+ s.extra_rdoc_files = ["Manifest.txt", "README.markdown"]
13
+ s.files = ["Manifest.txt", "README.markdown", "Rakefile", "lib/geokit/geocoders.rb", "lib/geokit.rb", "lib/geokit/mappable.rb", "test/test_base_geocoder.rb", "test/test_bounds.rb", "test/test_ca_geocoder.rb", "test/test_geoloc.rb", "test/test_google_geocoder.rb", "test/test_latlng.rb", "test/test_multi_geocoder.rb", "test/test_us_geocoder.rb", "test/test_yahoo_geocoder.rb"]
14
+ s.has_rdoc = true
15
+ s.homepage = %q{http://geokit.rubyforge.org}
16
+ s.rdoc_options = ["--main", "README.markdown"]
17
+ s.require_paths = ["lib"]
18
+ s.rubyforge_project = %q{geokit}
19
+ s.rubygems_version = %q{1.3.1}
20
+ s.summary = %q{none}
21
+ s.test_files = ["test/test_base_geocoder.rb", "test/test_bounds.rb", "test/test_ca_geocoder.rb", "test/test_geoloc.rb", "test/test_google_geocoder.rb", "test/test_latlng.rb", "test/test_multi_geocoder.rb", "test/test_us_geocoder.rb", "test/test_yahoo_geocoder.rb"]
22
+
23
+ if s.respond_to? :specification_version then
24
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
25
+ s.specification_version = 2
26
+ end
27
+ end
28
+
data/lib/geokit.rb ADDED
@@ -0,0 +1,30 @@
1
+ module Geokit
2
+ VERSION = '1.2.5'
3
+ # These defaults are used in Geokit::Mappable.distance_to and in acts_as_mappable
4
+ @@default_units = :miles
5
+ @@default_formula = :sphere
6
+
7
+ [:default_units, :default_formula].each do |sym|
8
+ class_eval <<-EOS, __FILE__, __LINE__
9
+ def self.#{sym}
10
+ if defined?(#{sym.to_s.upcase})
11
+ #{sym.to_s.upcase}
12
+ else
13
+ @@#{sym}
14
+ end
15
+ end
16
+
17
+ def self.#{sym}=(obj)
18
+ @@#{sym} = obj
19
+ end
20
+ EOS
21
+ end
22
+ end
23
+
24
+ path = File.expand_path(File.dirname(__FILE__))
25
+ $: << path unless $:.include?(path)
26
+ require 'geokit/geocoders'
27
+ require 'geokit/mappable'
28
+
29
+ # make old-style module name "GeoKit" equivilent to new-style "Geokit"
30
+ GeoKit=Geokit
@@ -0,0 +1,563 @@
1
+ require 'net/http'
2
+ require 'rexml/document'
3
+ require 'yaml'
4
+ require 'timeout'
5
+ require 'logger'
6
+
7
+ module Geokit
8
+ module Inflector
9
+
10
+ extend self
11
+
12
+ def titleize(word)
13
+ humanize(underscore(word)).gsub(/\b([a-z])/u) { $1.capitalize }
14
+ end
15
+
16
+ def underscore(camel_cased_word)
17
+ camel_cased_word.to_s.gsub(/::/, '/').
18
+ gsub(/([A-Z]+)([A-Z][a-z])/u,'\1_\2').
19
+ gsub(/([a-z\d])([A-Z])/u,'\1_\2').
20
+ tr("-", "_").
21
+ downcase
22
+ end
23
+
24
+ def humanize(lower_case_and_underscored_word)
25
+ lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
26
+ end
27
+
28
+ def snake_case(s)
29
+ return s.downcase if s =~ /^[A-Z]+$/u
30
+ s.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/u, '_\&') =~ /_*(.*)/
31
+ return $+.downcase
32
+
33
+ end
34
+
35
+ def url_escape(s)
36
+ s.gsub(/([^ a-zA-Z0-9_.-]+)/nu) do
37
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
38
+ end.tr(' ', '+')
39
+ end
40
+ end
41
+
42
+ # Contains a range of geocoders:
43
+ #
44
+ # ### "regular" address geocoders
45
+ # * Yahoo Geocoder - requires an API key.
46
+ # * Geocoder.us - may require authentication if performing more than the free request limit.
47
+ # * Geocoder.ca - for Canada; may require authentication as well.
48
+ # * Geonames - a free geocoder
49
+ #
50
+ # ### address geocoders that also provide reverse geocoding
51
+ # * Google Geocoder - requires an API key.
52
+ #
53
+ # ### IP address geocoders
54
+ # * IP Geocoder - geocodes an IP address using hostip.info's web service.
55
+ # * Geoplugin.net -- another IP address geocoder
56
+ #
57
+ # ### The Multigeocoder
58
+ # * Multi Geocoder - provides failover for the physical location geocoders.
59
+ #
60
+ # Some of these geocoders require configuration. You don't have to provide it here. See the README.
61
+ module Geocoders
62
+ @@proxy_addr = nil
63
+ @@proxy_port = nil
64
+ @@proxy_user = nil
65
+ @@proxy_pass = nil
66
+ @@timeout = nil
67
+ @@yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
68
+ @@google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
69
+ @@geocoder_us = false
70
+ @@geocoder_ca = false
71
+ @@geonames = false
72
+ @@provider_order = [:google,:us]
73
+ @@logger=Logger.new(STDOUT)
74
+ @@logger.level=Logger::INFO
75
+
76
+ [:yahoo, :google, :geocoder_us, :geocoder_ca, :geonames, :provider_order, :timeout,
77
+ :proxy_addr, :proxy_port, :proxy_user, :proxy_pass,:logger].each do |sym|
78
+ class_eval <<-EOS, __FILE__, __LINE__
79
+ def self.#{sym}
80
+ if defined?(#{sym.to_s.upcase})
81
+ #{sym.to_s.upcase}
82
+ else
83
+ @@#{sym}
84
+ end
85
+ end
86
+
87
+ def self.#{sym}=(obj)
88
+ @@#{sym} = obj
89
+ end
90
+ EOS
91
+ end
92
+
93
+ # Error which is thrown in the event a geocoding error occurs.
94
+ class GeocodeError < StandardError; end
95
+
96
+ # -------------------------------------------------------------------------------------------
97
+ # Geocoder Base class -- every geocoder should inherit from this
98
+ # -------------------------------------------------------------------------------------------
99
+
100
+ # The Geocoder base class which defines the interface to be used by all
101
+ # other geocoders.
102
+ class Geocoder
103
+ # Main method which calls the do_geocode template method which subclasses
104
+ # are responsible for implementing. Returns a populated GeoLoc or an
105
+ # empty one with a failed success code.
106
+ def self.geocode(address)
107
+ res = do_geocode(address)
108
+ return res.success ? res : GeoLoc.new
109
+ end
110
+
111
+ # Main method which calls the do_reverse_geocode template method which subclasses
112
+ # are responsible for implementing. Returns a populated GeoLoc or an
113
+ # empty one with a failed success code.
114
+ def self.reverse_geocode(latlng)
115
+ res = do_reverse_geocode(latlng)
116
+ return res.success ? res : GeoLoc.new
117
+ end
118
+
119
+ # Call the geocoder service using the timeout if configured.
120
+ def self.call_geocoder_service(url)
121
+ timeout(Geokit::Geocoders::timeout) { return self.do_get(url) } if Geokit::Geocoders::timeout
122
+ return self.do_get(url)
123
+ rescue TimeoutError
124
+ return nil
125
+ end
126
+
127
+ # Not all geocoders can do reverse geocoding. So, unless the subclass explicitly overrides this method,
128
+ # a call to reverse_geocode will return an empty GeoLoc. If you happen to be using MultiGeocoder,
129
+ # this will cause it to failover to the next geocoder, which will hopefully be one which supports reverse geocoding.
130
+ def self.do_reverse_geocode(latlng)
131
+ return GeoLoc.new
132
+ end
133
+
134
+ protected
135
+
136
+ def self.logger()
137
+ Geokit::Geocoders::logger
138
+ end
139
+
140
+ private
141
+
142
+ # Wraps the geocoder call around a proxy if necessary.
143
+ def self.do_get(url)
144
+ uri = URI.parse(url)
145
+ req = Net::HTTP::Get.new(url)
146
+ req.basic_auth(uri.user, uri.password) if uri.userinfo
147
+ res = Net::HTTP::Proxy(GeoKit::Geocoders::proxy_addr,
148
+ GeoKit::Geocoders::proxy_port,
149
+ GeoKit::Geocoders::proxy_user,
150
+ GeoKit::Geocoders::proxy_pass).start(uri.host, uri.port) { |http| http.request(req) }
151
+
152
+ return res
153
+ end
154
+
155
+ # Adds subclass' geocode method making it conveniently available through
156
+ # the base class.
157
+ def self.inherited(clazz)
158
+ class_name = clazz.name.split('::').last
159
+ src = <<-END_SRC
160
+ def self.#{Geokit::Inflector.underscore(class_name)}(address)
161
+ #{class_name}.geocode(address)
162
+ end
163
+ END_SRC
164
+ class_eval(src)
165
+ end
166
+ end
167
+
168
+ # -------------------------------------------------------------------------------------------
169
+ # "Regular" Address geocoders
170
+ # -------------------------------------------------------------------------------------------
171
+
172
+ # Geocoder CA geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_CA variable to
173
+ # contain true or false based upon whether authentication is to occur. Conforms to the
174
+ # interface set by the Geocoder class.
175
+ #
176
+ # Returns a response like:
177
+ # <?xml version="1.0" encoding="UTF-8" ?>
178
+ # <geodata>
179
+ # <latt>49.243086</latt>
180
+ # <longt>-123.153684</longt>
181
+ # </geodata>
182
+ class CaGeocoder < Geocoder
183
+
184
+ private
185
+
186
+ # Template method which does the geocode lookup.
187
+ def self.do_geocode(address)
188
+ raise ArgumentError('Geocoder.ca requires a GeoLoc argument') unless address.is_a?(GeoLoc)
189
+ url = construct_request(address)
190
+ res = self.call_geocoder_service(url)
191
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
192
+ xml = res.body
193
+ logger.debug "Geocoder.ca geocoding. Address: #{address}. Result: #{xml}"
194
+ # Parse the document.
195
+ doc = REXML::Document.new(xml)
196
+ address.lat = doc.elements['//latt'].text
197
+ address.lng = doc.elements['//longt'].text
198
+ address.success = true
199
+ return address
200
+ rescue
201
+ logger.error "Caught an error during Geocoder.ca geocoding call: "+$!
202
+ return GeoLoc.new
203
+ end
204
+
205
+ # Formats the request in the format acceptable by the CA geocoder.
206
+ def self.construct_request(location)
207
+ url = ""
208
+ url += add_ampersand(url) + "stno=#{location.street_number}" if location.street_address
209
+ url += add_ampersand(url) + "addresst=#{Geokit::Inflector::url_escape(location.street_name)}" if location.street_address
210
+ url += add_ampersand(url) + "city=#{Geokit::Inflector::url_escape(location.city)}" if location.city
211
+ url += add_ampersand(url) + "prov=#{location.state}" if location.state
212
+ url += add_ampersand(url) + "postal=#{location.zip}" if location.zip
213
+ url += add_ampersand(url) + "auth=#{Geokit::Geocoders::geocoder_ca}" if Geokit::Geocoders::geocoder_ca
214
+ url += add_ampersand(url) + "geoit=xml"
215
+ 'http://geocoder.ca/?' + url
216
+ end
217
+
218
+ def self.add_ampersand(url)
219
+ url && url.length > 0 ? "&" : ""
220
+ end
221
+ end
222
+
223
+ # Geocoder Us geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_US variable to
224
+ # contain true or false based upon whether authentication is to occur. Conforms to the
225
+ # interface set by the Geocoder class.
226
+ class UsGeocoder < Geocoder
227
+
228
+ private
229
+ def self.do_geocode(address)
230
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
231
+
232
+ query = (address_str =~ /^\d{5}(?:-\d{4})?$/ ? "zip" : "address") + "=#{Geokit::Inflector::url_escape(address_str)}"
233
+ url = if GeoKit::Geocoders::geocoder_us
234
+ "http://#{GeoKit::Geocoders::geocoder_us}@geocoder.us/member/service/csv/geocode"
235
+ else
236
+ "http://geocoder.us/service/csv/geocode"
237
+ end
238
+
239
+ url = "#{url}?#{query}"
240
+ res = self.call_geocoder_service(url)
241
+
242
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
243
+ data = res.body
244
+ logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}"
245
+ array = data.chomp.split(',')
246
+
247
+ if array.length == 5
248
+ res=GeoLoc.new
249
+ res.lat,res.lng,res.city,res.state,res.zip=array
250
+ res.country_code='US'
251
+ res.success=true
252
+ return res
253
+ elsif array.length == 6
254
+ res=GeoLoc.new
255
+ res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array
256
+ res.country_code='US'
257
+ res.success=true
258
+ return res
259
+ else
260
+ logger.info "geocoder.us was unable to geocode address: "+address
261
+ return GeoLoc.new
262
+ end
263
+ rescue
264
+ logger.error "Caught an error during geocoder.us geocoding call: "+$!
265
+ return GeoLoc.new
266
+
267
+ end
268
+ end
269
+
270
+ # Yahoo geocoder implementation. Requires the Geokit::Geocoders::YAHOO variable to
271
+ # contain a Yahoo API key. Conforms to the interface set by the Geocoder class.
272
+ class YahooGeocoder < Geocoder
273
+
274
+ private
275
+
276
+ # Template method which does the geocode lookup.
277
+ def self.do_geocode(address)
278
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
279
+ url="http://api.local.yahoo.com/MapsService/V1/geocode?appid=#{Geokit::Geocoders::yahoo}&location=#{Geokit::Inflector::url_escape(address_str)}"
280
+ res = self.call_geocoder_service(url)
281
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
282
+ xml = res.body
283
+ doc = REXML::Document.new(xml)
284
+ logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}"
285
+
286
+ if doc.elements['//ResultSet']
287
+ res=GeoLoc.new
288
+
289
+ #basic
290
+ res.lat=doc.elements['//Latitude'].text
291
+ res.lng=doc.elements['//Longitude'].text
292
+ res.country_code=doc.elements['//Country'].text
293
+ res.provider='yahoo'
294
+
295
+ #extended - false if not available
296
+ res.city=doc.elements['//City'].text if doc.elements['//City'] && doc.elements['//City'].text != nil
297
+ res.state=doc.elements['//State'].text if doc.elements['//State'] && doc.elements['//State'].text != nil
298
+ res.zip=doc.elements['//Zip'].text if doc.elements['//Zip'] && doc.elements['//Zip'].text != nil
299
+ res.street_address=doc.elements['//Address'].text if doc.elements['//Address'] && doc.elements['//Address'].text != nil
300
+ res.precision=doc.elements['//Result'].attributes['precision'] if doc.elements['//Result']
301
+ res.success=true
302
+ return res
303
+ else
304
+ logger.info "Yahoo was unable to geocode address: "+address
305
+ return GeoLoc.new
306
+ end
307
+
308
+ rescue
309
+ logger.info "Caught an error during Yahoo geocoding call: "+$!
310
+ return GeoLoc.new
311
+ end
312
+ end
313
+
314
+ # Another geocoding web service
315
+ # http://www.geonames.org
316
+ class GeonamesGeocoder < Geocoder
317
+
318
+ private
319
+
320
+ # Template method which does the geocode lookup.
321
+ def self.do_geocode(address)
322
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
323
+ # geonames need a space seperated search string
324
+ address_str.gsub!(/,/, " ")
325
+ params = "/postalCodeSearch?placename=#{Geokit::Inflector::url_escape(address_str)}&maxRows=10"
326
+
327
+ if(GeoKit::Geocoders::geonames)
328
+ url = "http://ws.geonames.net#{params}&username=#{GeoKit::Geocoders::geonames}"
329
+ else
330
+ url = "http://ws.geonames.org#{params}"
331
+ end
332
+
333
+ res = self.call_geocoder_service(url)
334
+
335
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
336
+
337
+ xml=res.body
338
+ logger.debug "Geonames geocoding. Address: #{address}. Result: #{xml}"
339
+ doc=REXML::Document.new(xml)
340
+
341
+ if(doc.elements['//geonames/totalResultsCount'].text.to_i > 0)
342
+ res=GeoLoc.new
343
+
344
+ # only take the first result
345
+ res.lat=doc.elements['//code/lat'].text if doc.elements['//code/lat']
346
+ res.lng=doc.elements['//code/lng'].text if doc.elements['//code/lng']
347
+ res.country_code=doc.elements['//code/countryCode'].text if doc.elements['//code/countryCode']
348
+ res.provider='genomes'
349
+ res.city=doc.elements['//code/name'].text if doc.elements['//code/name']
350
+ res.state=doc.elements['//code/adminName1'].text if doc.elements['//code/adminName1']
351
+ res.zip=doc.elements['//code/postalcode'].text if doc.elements['//code/postalcode']
352
+ res.success=true
353
+ return res
354
+ else
355
+ logger.info "Geonames was unable to geocode address: "+address
356
+ return GeoLoc.new
357
+ end
358
+
359
+ rescue
360
+ logger.error "Caught an error during Geonames geocoding call: "+$!
361
+ end
362
+ end
363
+
364
+ # -------------------------------------------------------------------------------------------
365
+ # Address geocoders that also provide reverse geocoding
366
+ # -------------------------------------------------------------------------------------------
367
+
368
+ # Google geocoder implementation. Requires the Geokit::Geocoders::GOOGLE variable to
369
+ # contain a Google API key. Conforms to the interface set by the Geocoder class.
370
+ class GoogleGeocoder < Geocoder
371
+
372
+ private
373
+
374
+ # Template method which does the reverse-geocode lookup.
375
+ def self.do_reverse_geocode(latlng)
376
+ latlng=LatLng.normalize(latlng)
377
+ res = self.call_geocoder_service("http://maps.google.com/maps/geo?ll=#{Geokit::Inflector::url_escape(latlng.ll)}&output=xml&key=#{Geokit::Geocoders::google}&oe=utf-8")
378
+ # res = Net::HTTP.get_response(URI.parse("http://maps.google.com/maps/geo?ll=#{Geokit::Inflector::url_escape(address_str)}&output=xml&key=#{Geokit::Geocoders::google}&oe=utf-8"))
379
+ return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
380
+ xml = res.body
381
+ logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{xml}"
382
+ return self.xml2GeoLoc(xml)
383
+ end
384
+
385
+ # Template method which does the geocode lookup.
386
+ def self.do_geocode(address)
387
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
388
+ res = self.call_geocoder_service("http://maps.google.com/maps/geo?q=#{Geokit::Inflector::url_escape(address_str)}&output=xml&key=#{Geokit::Geocoders::google}&oe=utf-8")
389
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
390
+ xml = res.body
391
+ logger.debug "Google geocoding. Address: #{address}. Result: #{xml}"
392
+ return self.xml2GeoLoc(xml, address)
393
+ end
394
+
395
+ def self.xml2GeoLoc(xml, address="")
396
+ doc=REXML::Document.new(xml)
397
+
398
+ if doc.elements['//kml/Response/Status/code'].text == '200'
399
+ geoloc = nil
400
+ # Google can return multiple results as //Placemark elements.
401
+ # iterate through each and extract each placemark as a geoloc
402
+ doc.each_element('//Placemark') do |e|
403
+ extracted_geoloc = extract_placemark(e) # g is now an instance of Geoloc
404
+ if geoloc.nil?
405
+ # first time through, geoloc is still nil, so we make it the geoloc we just extracted
406
+ geoloc = extracted_geoloc
407
+ else
408
+ # second (and subsequent) iterations, we push additional
409
+ # geolocs onto "geoloc.all"
410
+ geoloc.all.push(extracted_geoloc)
411
+ end
412
+ end
413
+ return geoloc
414
+ else
415
+ logger.info "Google was unable to geocode address: "+address
416
+ return GeoLoc.new
417
+ end
418
+
419
+ rescue
420
+ logger.error "Caught an error during Google geocoding call: "+$!
421
+ return GeoLoc.new
422
+ end
423
+
424
+ # extracts a single geoloc from a //placemark element in the google results xml
425
+ def self.extract_placemark(doc)
426
+ res = GeoLoc.new
427
+ coordinates=doc.elements['.//coordinates'].text.to_s.split(',')
428
+
429
+ #basics
430
+ res.lat=coordinates[1]
431
+ res.lng=coordinates[0]
432
+ res.country_code=doc.elements['.//CountryNameCode'].text if doc.elements['.//CountryNameCode']
433
+ res.provider='google'
434
+
435
+ #extended -- false if not not available
436
+ res.city = doc.elements['.//LocalityName'].text if doc.elements['.//LocalityName']
437
+ res.state = doc.elements['.//AdministrativeAreaName'].text if doc.elements['.//AdministrativeAreaName']
438
+ res.full_address = doc.elements['.//address'].text if doc.elements['.//address'] # google provides it
439
+ res.zip = doc.elements['.//PostalCodeNumber'].text if doc.elements['.//PostalCodeNumber']
440
+ res.street_address = doc.elements['.//ThoroughfareName'].text if doc.elements['.//ThoroughfareName']
441
+ # Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country
442
+ # For Google, 1=low accuracy, 8=high accuracy
443
+ address_details=doc.elements['.//*[local-name() = "AddressDetails"]']
444
+ accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0
445
+ res.precision=%w{unknown country state state city zip zip+4 street address building}[accuracy]
446
+ res.success=true
447
+
448
+ return res
449
+ end
450
+ end
451
+
452
+
453
+ # -------------------------------------------------------------------------------------------
454
+ # IP Geocoders
455
+ # -------------------------------------------------------------------------------------------
456
+
457
+ # Provides geocoding based upon an IP address. The underlying web service is geoplugin.net
458
+ class GeoPluginGeocoder < Geocoder
459
+ private
460
+
461
+ def self.do_geocode(ip)
462
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
463
+ response = self.call_geocoder_service("http://www.geoplugin.net/xml.gp?ip=#{ip}")
464
+ return response.is_a?(Net::HTTPSuccess) ? parse_xml(response.body) : GeoLoc.new
465
+ rescue
466
+ logger.error "Caught an error during GeloPluginGeocoder geocoding call: "+$!
467
+ return GeoLoc.new
468
+ end
469
+
470
+ def self.parse_xml(xml)
471
+ xml = REXML::Document.new(xml)
472
+ geo = GeoLoc.new
473
+ geo.provider='geoPlugin'
474
+ geo.city = xml.elements['//geoplugin_city'].text
475
+ geo.state = xml.elements['//geoplugin_region'].text
476
+ geo.country_code = xml.elements['//geoplugin_countryCode'].text
477
+ geo.lat = xml.elements['//geoplugin_latitude'].text.to_f
478
+ geo.lng = xml.elements['//geoplugin_longitude'].text.to_f
479
+ geo.success = !geo.city.empty?
480
+ return geo
481
+ end
482
+ end
483
+
484
+ # Provides geocoding based upon an IP address. The underlying web service is a hostip.info
485
+ # which sources their data through a combination of publicly available information as well
486
+ # as community contributions.
487
+ class IpGeocoder < Geocoder
488
+
489
+ private
490
+
491
+ # Given an IP address, returns a GeoLoc instance which contains latitude,
492
+ # longitude, city, and country code. Sets the success attribute to false if the ip
493
+ # parameter does not match an ip address.
494
+ def self.do_geocode(ip)
495
+ return Geoloc.new if '0.0.0.0' == ip
496
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
497
+ url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true"
498
+ response = self.call_geocoder_service(url)
499
+ response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new
500
+ rescue
501
+ logger.error "Caught an error during HostIp geocoding call: "+$!
502
+ return GeoLoc.new
503
+ end
504
+
505
+ # Converts the body to YAML since its in the form of:
506
+ #
507
+ # Country: UNITED STATES (US)
508
+ # City: Sugar Grove, IL
509
+ # Latitude: 41.7696
510
+ # Longitude: -88.4588
511
+ #
512
+ # then instantiates a GeoLoc instance to populate with location data.
513
+ def self.parse_body(body) # :nodoc:
514
+ yaml = YAML.load(body)
515
+ res = GeoLoc.new
516
+ res.provider = 'hostip'
517
+ res.city, res.state = yaml['City'].split(', ')
518
+ country, res.country_code = yaml['Country'].split(' (')
519
+ res.lat = yaml['Latitude']
520
+ res.lng = yaml['Longitude']
521
+ res.country_code.chop!
522
+ res.success = !(res.city =~ /\(.+\)/)
523
+ res
524
+ end
525
+ end
526
+
527
+ # -------------------------------------------------------------------------------------------
528
+ # The Multi Geocoder
529
+ # -------------------------------------------------------------------------------------------
530
+
531
+ # Provides methods to geocode with a variety of geocoding service providers, plus failover
532
+ # among providers in the order you configure.
533
+ #
534
+ # Goal:
535
+ # - homogenize the results of multiple geocoders
536
+ #
537
+ # Limitations:
538
+ # - currently only provides the first result. Sometimes geocoders will return multiple results.
539
+ # - currently discards the "accuracy" component of the geocoding calls
540
+ class MultiGeocoder < Geocoder
541
+ private
542
+
543
+ # This method will call one or more geocoders in the order specified in the
544
+ # configuration until one of the geocoders work.
545
+ #
546
+ # The failover approach is crucial for production-grade apps, but is rarely used.
547
+ # 98% of your geocoding calls will be successful with the first call
548
+ def self.do_geocode(address)
549
+ Geokit::Geocoders::provider_order.each do |provider|
550
+ begin
551
+ klass = Geokit::Geocoders.const_get "#{provider.to_s.capitalize}Geocoder"
552
+ res = klass.send :geocode, address
553
+ return res if res.success
554
+ rescue
555
+ logger.error("Something has gone very wrong during geocoding, OR you have configured an invalid class name in Geokit::Geocoders::provider_order. Address: #{address}. Provider: #{provider}")
556
+ end
557
+ end
558
+ # If we get here, we failed completely.
559
+ GeoLoc.new
560
+ end
561
+ end
562
+ end
563
+ end