Pr0d1r2-geokit 1.3.2

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