Pr0d1r2-geokit 1.3.2

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