andre-geokit 1.1.0

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