andre-geokit 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ === 1.0.0 / 2008-11-30
2
+
3
+ * Extracted geocoding and mappable functionality and tests from original GeoKit Rails plugin.
4
+
5
+
@@ -0,0 +1,20 @@
1
+ .loadpath
2
+ .project
3
+ History.txt
4
+ Manifest.txt
5
+ README.markdown
6
+ Rakefile
7
+ geokit.gemspec
8
+ lib/geocoders.rb
9
+ lib/geokit.rb
10
+ lib/mappable.rb
11
+ test/test_base_geocoder.rb
12
+ test/test_bounds.rb
13
+ test/test_ca_geocoder.rb
14
+ test/test_geoloc.rb
15
+ test/test_google_geocoder.rb
16
+ test/test_ipgeocoder.rb
17
+ test/test_latlng.rb
18
+ test/test_multi_geocoder.rb
19
+ test/test_us_geocoder.rb
20
+ test/test_yahoo_geocoder.rb
@@ -0,0 +1,66 @@
1
+ # Geokit gem
2
+
3
+ [http://geokit.rubyforge.org](http://geokit.rubyforge.org)
4
+
5
+ ## DESCRIPTION:
6
+
7
+ The Geokit gem provides the following:
8
+
9
+ * Distance calculations between two points on the earth. Calculate the distance in miles or KM, with all the trigonometry abstracted away by GeoKit.
10
+ * Geocoding from multiple providers. It currently supports Google, Yahoo, Geocoder.us, and Geocoder.ca geocoders, and it provides a uniform response structure from all of them. It also provides a fail-over mechanism, in case your input fails to geocode in one service.
11
+
12
+ Combine this with gem with the geokit-rails plugin to get location-based finders for your Rails app. Plugins for other web frameworks and ORMs will provide similar functionality.
13
+
14
+
15
+ ## FEATURES/PROBLEMS:
16
+
17
+ * none currently
18
+
19
+ ## SYNOPSIS:
20
+
21
+ irb> require 'rubygems'
22
+ irb> require 'geokit'
23
+ irb> a=Geokit::Geocoders::YahooGeocoder.geocode '140 Market St, San Francisco, CA'
24
+ irb> a.ll
25
+ => 37.79363,-122.396116
26
+ irb> b=Geokit::Geocoders::YahooGeocoder.geocode '789 Geary St, San Francisco, CA'
27
+ irb> b.ll
28
+ => 37.786217,-122.41619
29
+ irb> a.distance_to(b)
30
+ => 1.21120007413626
31
+ irb> a.heading_to(b)
32
+ => 244.959832435678
33
+
34
+
35
+ ## REQUIREMENTS:
36
+
37
+
38
+ ## INSTALL:
39
+
40
+ * gem sources -a http://gems.github.com
41
+ * sudo gem install
42
+
43
+ ## LICENSE:
44
+
45
+ (The MIT License)
46
+
47
+ Copyright (c) 2007-2008 Andre Lewis and Bill Eisenhauer
48
+
49
+ Permission is hereby granted, free of charge, to any person obtaining
50
+ a copy of this software and associated documentation files (the
51
+ 'Software'), to deal in the Software without restriction, including
52
+ without limitation the rights to use, copy, modify, merge, publish,
53
+ distribute, sublicense, and/or sell copies of the Software, and to
54
+ permit persons to whom the Software is furnished to do so, subject to
55
+ the following conditions:
56
+
57
+ The above copyright notice and this permission notice shall be
58
+ included in all copies or substantial portions of the Software.
59
+
60
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
61
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
62
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
63
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
64
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
65
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
66
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,20 @@
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 = 'Geokitx' # if different than lowercase project name
9
+ p.developer('Andre Lewis and Bill Eisenhauer', 'andre@earthcode.com / bill_eisenhauer@yahoo.com')
10
+ end
11
+
12
+ task :generate_gemspec do
13
+ system "rake debug_gem | grep -v \"(in \" > `basename \\`pwd\\``.gemspec"
14
+ end
15
+
16
+ task :update_manifest do
17
+ system "touch Manifest.txt; rake check_manifest | grep -v \"(in \" | patch"
18
+ end
19
+
20
+ # vim: syntax=Ruby
@@ -0,0 +1,386 @@
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])/) { $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])/,'\1_\2').
19
+ gsub(/([a-z\d])([A-Z])/,'\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]+$/
30
+ s.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/, '_\&') =~ /_*(.*)/
31
+ return $+.downcase
32
+
33
+ end
34
+
35
+ def url_escape(s)
36
+ s.gsub(/([^ a-zA-Z0-9_.-]+)/n) do
37
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
38
+ end.tr(' ', '+')
39
+ end
40
+ end
41
+ # Contains a set of geocoders which can be used independently if desired. The list contains:
42
+ #
43
+ # * Google Geocoder - requires an API key.
44
+ # * Yahoo Geocoder - requires an API key.
45
+ # * Geocoder.us - may require authentication if performing more than the free request limit.
46
+ # * Geocoder.ca - for Canada; may require authentication as well.
47
+ # * IP Geocoder - geocodes an IP address using hostip.info's web service.
48
+ # * Multi Geocoder - provides failover for the physical location geocoders.
49
+ #
50
+ # Some configuration is required for these geocoders and can be located in the environment
51
+ # configuration files.
52
+ module Geocoders
53
+ @@proxy_addr = nil
54
+ @@proxy_port = nil
55
+ @@proxy_user = nil
56
+ @@proxy_pass = nil
57
+ @@timeout = nil
58
+ @@yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
59
+ @@google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
60
+ @@geocoder_us = false
61
+ @@geocoder_ca = false
62
+ @@provider_order = [:google,:us]
63
+ @@logger=Logger.new(STDOUT)
64
+ @@logger.level=Logger::INFO
65
+
66
+ [:yahoo, :google, :geocoder_us, :geocoder_ca, :provider_order, :timeout,
67
+ :proxy_addr, :proxy_port, :proxy_user, :proxy_pass,:logger].each do |sym|
68
+ class_eval <<-EOS, __FILE__, __LINE__
69
+ def self.#{sym}
70
+ if defined?(#{sym.to_s.upcase})
71
+ #{sym.to_s.upcase}
72
+ else
73
+ @@#{sym}
74
+ end
75
+ end
76
+
77
+ def self.#{sym}=(obj)
78
+ @@#{sym} = obj
79
+ end
80
+ EOS
81
+ end
82
+
83
+ # Error which is thrown in the event a geocoding error occurs.
84
+ class GeocodeError < StandardError; end
85
+
86
+ # The Geocoder base class which defines the interface to be used by all
87
+ # other geocoders.
88
+ class Geocoder
89
+ # Main method which calls the do_geocode template method which subclasses
90
+ # are responsible for implementing. Returns a populated GeoLoc or an
91
+ # empty one with a failed success code.
92
+ def self.geocode(address)
93
+ res = do_geocode(address)
94
+ return res.success ? res : GeoLoc.new
95
+ end
96
+
97
+ # Call the geocoder service using the timeout if configured.
98
+ def self.call_geocoder_service(url)
99
+ timeout(Geokit::Geocoders::timeout) { return self.do_get(url) } if Geokit::Geocoders::timeout
100
+ return self.do_get(url)
101
+ rescue TimeoutError
102
+ return nil
103
+ end
104
+
105
+ protected
106
+
107
+ def self.logger()
108
+ Geokit::Geocoders::logger
109
+ end
110
+
111
+ private
112
+
113
+ # Wraps the geocoder call around a proxy if necessary.
114
+ def self.do_get(url)
115
+ return Net::HTTP::Proxy(Geokit::Geocoders::proxy_addr, Geokit::Geocoders::proxy_port,
116
+ Geokit::Geocoders::proxy_user, Geokit::Geocoders::proxy_pass).get_response(URI.parse(url))
117
+ end
118
+
119
+ # Adds subclass' geocode method making it conveniently available through
120
+ # the base class.
121
+ def self.inherited(clazz)
122
+ class_name = clazz.name.split('::').last
123
+ src = <<-END_SRC
124
+ def self.#{Geokit::Inflector.underscore(class_name)}(address)
125
+ #{class_name}.geocode(address)
126
+ end
127
+ END_SRC
128
+ class_eval(src)
129
+ end
130
+ end
131
+
132
+ # Geocoder CA geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_CA variable to
133
+ # contain true or false based upon whether authentication is to occur. Conforms to the
134
+ # interface set by the Geocoder class.
135
+ #
136
+ # Returns a response like:
137
+ # <?xml version="1.0" encoding="UTF-8" ?>
138
+ # <geodata>
139
+ # <latt>49.243086</latt>
140
+ # <longt>-123.153684</longt>
141
+ # </geodata>
142
+ class CaGeocoder < Geocoder
143
+
144
+ private
145
+
146
+ # Template method which does the geocode lookup.
147
+ def self.do_geocode(address)
148
+ raise ArgumentError('Geocoder.ca requires a GeoLoc argument') unless address.is_a?(GeoLoc)
149
+ url = construct_request(address)
150
+ res = self.call_geocoder_service(url)
151
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
152
+ xml = res.body
153
+ logger.debug "Geocoder.ca geocoding. Address: #{address}. Result: #{xml}"
154
+ # Parse the document.
155
+ doc = REXML::Document.new(xml)
156
+ address.lat = doc.elements['//latt'].text
157
+ address.lng = doc.elements['//longt'].text
158
+ address.success = true
159
+ return address
160
+ rescue
161
+ logger.error "Caught an error during Geocoder.ca geocoding call: "+$!
162
+ return GeoLoc.new
163
+ end
164
+
165
+ # Formats the request in the format acceptable by the CA geocoder.
166
+ def self.construct_request(location)
167
+ url = ""
168
+ url += add_ampersand(url) + "stno=#{location.street_number}" if location.street_address
169
+ url += add_ampersand(url) + "addresst=#{Geokit::Inflector::url_escape(location.street_name)}" if location.street_address
170
+ url += add_ampersand(url) + "city=#{Geokit::Inflector::url_escape(location.city)}" if location.city
171
+ url += add_ampersand(url) + "prov=#{location.state}" if location.state
172
+ url += add_ampersand(url) + "postal=#{location.zip}" if location.zip
173
+ url += add_ampersand(url) + "auth=#{Geokit::Geocoders::geocoder_ca}" if Geokit::Geocoders::geocoder_ca
174
+ url += add_ampersand(url) + "geoit=xml"
175
+ 'http://geocoder.ca/?' + url
176
+ end
177
+
178
+ def self.add_ampersand(url)
179
+ url && url.length > 0 ? "&" : ""
180
+ end
181
+ end
182
+
183
+ # Google geocoder implementation. Requires the Geokit::Geocoders::GOOGLE variable to
184
+ # contain a Google API key. Conforms to the interface set by the Geocoder class.
185
+ class GoogleGeocoder < Geocoder
186
+
187
+ private
188
+
189
+ # Template method which does the geocode lookup.
190
+ def self.do_geocode(address)
191
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
192
+ 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")
193
+ # res = Net::HTTP.get_response(URI.parse("http://maps.google.com/maps/geo?q=#{Geokit::Inflector::url_escape(address_str)}&output=xml&key=#{Geokit::Geocoders::google}&oe=utf-8"))
194
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
195
+ xml=res.body
196
+ logger.debug "Google geocoding. Address: #{address}. Result: #{xml}"
197
+ doc=REXML::Document.new(xml)
198
+
199
+ if doc.elements['//kml/Response/Status/code'].text == '200'
200
+ res = GeoLoc.new
201
+ coordinates=doc.elements['//coordinates'].text.to_s.split(',')
202
+
203
+ #basics
204
+ res.lat=coordinates[1]
205
+ res.lng=coordinates[0]
206
+ res.country_code=doc.elements['//CountryNameCode'].text
207
+ res.provider='google'
208
+
209
+ #extended -- false if not not available
210
+ res.city = doc.elements['//LocalityName'].text if doc.elements['//LocalityName']
211
+ res.state = doc.elements['//AdministrativeAreaName'].text if doc.elements['//AdministrativeAreaName']
212
+ res.full_address = doc.elements['//address'].text if doc.elements['//address'] # google provides it
213
+ res.zip = doc.elements['//PostalCodeNumber'].text if doc.elements['//PostalCodeNumber']
214
+ res.street_address = doc.elements['//ThoroughfareName'].text if doc.elements['//ThoroughfareName']
215
+ # Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country
216
+ # For Google, 1=low accuracy, 8=high accuracy
217
+ # old way -- address_details=doc.elements['//AddressDetails','urn:oasis:names:tc:ciq:xsdschema:xAL:2.0']
218
+ address_details=doc.elements['//*[local-name() = "AddressDetails"]']
219
+ accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0
220
+ res.precision=%w{unknown country state state city zip zip+4 street address}[accuracy]
221
+ res.success=true
222
+
223
+ return res
224
+ else
225
+ logger.info "Google was unable to geocode address: "+address
226
+ return GeoLoc.new
227
+ end
228
+
229
+ rescue
230
+ logger.error "Caught an error during Google geocoding call: "+$!
231
+ return GeoLoc.new
232
+ end
233
+ end
234
+
235
+ # Provides geocoding based upon an IP address. The underlying web service is a hostip.info
236
+ # which sources their data through a combination of publicly available information as well
237
+ # as community contributions.
238
+ class IpGeocoder < Geocoder
239
+
240
+ private
241
+
242
+ # Given an IP address, returns a GeoLoc instance which contains latitude,
243
+ # longitude, city, and country code. Sets the success attribute to false if the ip
244
+ # parameter does not match an ip address.
245
+ def self.do_geocode(ip)
246
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
247
+ url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true"
248
+ response = self.call_geocoder_service(url)
249
+ response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new
250
+ rescue
251
+ logger.error "Caught an error during HostIp geocoding call: "+$!
252
+ return GeoLoc.new
253
+ end
254
+
255
+ # Converts the body to YAML since its in the form of:
256
+ #
257
+ # Country: UNITED STATES (US)
258
+ # City: Sugar Grove, IL
259
+ # Latitude: 41.7696
260
+ # Longitude: -88.4588
261
+ #
262
+ # then instantiates a GeoLoc instance to populate with location data.
263
+ def self.parse_body(body) # :nodoc:
264
+ yaml = YAML.load(body)
265
+ res = GeoLoc.new
266
+ res.provider = 'hostip'
267
+ res.city, res.state = yaml['City'].split(', ')
268
+ country, res.country_code = yaml['Country'].split(' (')
269
+ res.lat = yaml['Latitude']
270
+ res.lng = yaml['Longitude']
271
+ res.country_code.chop!
272
+ res.success = res.city != "(Private Address)"
273
+ res
274
+ end
275
+ end
276
+
277
+ # Geocoder Us geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_US variable to
278
+ # contain true or false based upon whether authentication is to occur. Conforms to the
279
+ # interface set by the Geocoder class.
280
+ class UsGeocoder < Geocoder
281
+
282
+ private
283
+
284
+ # For now, the geocoder_method will only geocode full addresses -- not zips or cities in isolation
285
+ def self.do_geocode(address)
286
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
287
+ url = "http://"+(Geokit::Geocoders::geocoder_us || '')+"geocoder.us/service/csv/geocode?address=#{Geokit::Inflector::url_escape(address_str)}"
288
+ res = self.call_geocoder_service(url)
289
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
290
+ data = res.body
291
+ logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}"
292
+ array = data.chomp.split(',')
293
+
294
+ if array.length == 6
295
+ res=GeoLoc.new
296
+ res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array
297
+ res.country_code='US'
298
+ res.success=true
299
+ return res
300
+ else
301
+ logger.info "geocoder.us was unable to geocode address: "+address
302
+ return GeoLoc.new
303
+ end
304
+ rescue
305
+ logger.error "Caught an error during geocoder.us geocoding call: "+$!
306
+ return GeoLoc.new
307
+ end
308
+ end
309
+
310
+ # Yahoo geocoder implementation. Requires the Geokit::Geocoders::YAHOO variable to
311
+ # contain a Yahoo API key. Conforms to the interface set by the Geocoder class.
312
+ class YahooGeocoder < Geocoder
313
+
314
+ private
315
+
316
+ # Template method which does the geocode lookup.
317
+ def self.do_geocode(address)
318
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
319
+ url="http://api.local.yahoo.com/MapsService/V1/geocode?appid=#{Geokit::Geocoders::yahoo}&location=#{Geokit::Inflector::url_escape(address_str)}"
320
+ res = self.call_geocoder_service(url)
321
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
322
+ xml = res.body
323
+ doc = REXML::Document.new(xml)
324
+ logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}"
325
+
326
+ if doc.elements['//ResultSet']
327
+ res=GeoLoc.new
328
+
329
+ #basic
330
+ res.lat=doc.elements['//Latitude'].text
331
+ res.lng=doc.elements['//Longitude'].text
332
+ res.country_code=doc.elements['//Country'].text
333
+ res.provider='yahoo'
334
+
335
+ #extended - false if not available
336
+ res.city=doc.elements['//City'].text if doc.elements['//City'] && doc.elements['//City'].text != nil
337
+ res.state=doc.elements['//State'].text if doc.elements['//State'] && doc.elements['//State'].text != nil
338
+ res.zip=doc.elements['//Zip'].text if doc.elements['//Zip'] && doc.elements['//Zip'].text != nil
339
+ res.street_address=doc.elements['//Address'].text if doc.elements['//Address'] && doc.elements['//Address'].text != nil
340
+ res.precision=doc.elements['//Result'].attributes['precision'] if doc.elements['//Result']
341
+ res.success=true
342
+ return res
343
+ else
344
+ logger.info "Yahoo was unable to geocode address: "+address
345
+ return GeoLoc.new
346
+ end
347
+
348
+ rescue
349
+ logger.info "Caught an error during Yahoo geocoding call: "+$!
350
+ return GeoLoc.new
351
+ end
352
+ end
353
+
354
+ # Provides methods to geocode with a variety of geocoding service providers, plus failover
355
+ # among providers in the order you configure.
356
+ #
357
+ # Goal:
358
+ # - homogenize the results of multiple geocoders
359
+ #
360
+ # Limitations:
361
+ # - currently only provides the first result. Sometimes geocoders will return multiple results.
362
+ # - currently discards the "accuracy" component of the geocoding calls
363
+ class MultiGeocoder < Geocoder
364
+ private
365
+
366
+ # This method will call one or more geocoders in the order specified in the
367
+ # configuration until one of the geocoders work.
368
+ #
369
+ # The failover approach is crucial for production-grade apps, but is rarely used.
370
+ # 98% of your geocoding calls will be successful with the first call
371
+ def self.do_geocode(address)
372
+ Geokit::Geocoders::provider_order.each do |provider|
373
+ begin
374
+ klass = Geokit::Geocoders.const_get "#{provider.to_s.capitalize}Geocoder"
375
+ res = klass.send :geocode, address
376
+ return res if res.success
377
+ rescue
378
+ 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}")
379
+ end
380
+ end
381
+ # If we get here, we failed completely.
382
+ GeoLoc.new
383
+ end
384
+ end
385
+ end
386
+ end