geokit 1.2.5

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