dreamcat4-geokit 1.3.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,576 @@
1
+ require 'net/http'
2
+ require 'rexml/document'
3
+ require 'yaml'
4
+ require 'timeout'
5
+ require 'logger'
6
+
7
+ module Geokit
8
+ module Inflector
9
+
10
+ extend self
11
+
12
+ def titleize(word)
13
+ humanize(underscore(word)).gsub(/\b([a-z])/u) { $1.capitalize }
14
+ end
15
+
16
+ def underscore(camel_cased_word)
17
+ camel_cased_word.to_s.gsub(/::/, '/').
18
+ gsub(/([A-Z]+)([A-Z][a-z])/u,'\1_\2').
19
+ gsub(/([a-z\d])([A-Z])/u,'\1_\2').
20
+ tr("-", "_").
21
+ downcase
22
+ end
23
+
24
+ def humanize(lower_case_and_underscored_word)
25
+ lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
26
+ end
27
+
28
+ def snake_case(s)
29
+ return s.downcase if s =~ /^[A-Z]+$/u
30
+ s.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/u, '_\&') =~ /_*(.*)/
31
+ return $+.downcase
32
+
33
+ end
34
+
35
+ def url_escape(s)
36
+ s.gsub(/([^ a-zA-Z0-9_.-]+)/nu) do
37
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
38
+ end.tr(' ', '+')
39
+ end
40
+ end
41
+
42
+ # Contains a range of geocoders:
43
+ #
44
+ # ### "regular" address geocoders
45
+ # * Yahoo Geocoder - requires an API key.
46
+ # * Geocoder.us - may require authentication if performing more than the free request limit.
47
+ # * Geocoder.ca - for Canada; may require authentication as well.
48
+ # * Geonames - a free geocoder
49
+ #
50
+ # ### address geocoders that also provide reverse geocoding
51
+ # * Google Geocoder - requires an API key.
52
+ #
53
+ # ### IP address geocoders
54
+ # * IP Geocoder - geocodes an IP address using hostip.info's web service.
55
+ # * Geoplugin.net -- another IP address geocoder
56
+ #
57
+ # ### The Multigeocoder
58
+ # * Multi Geocoder - provides failover for the physical location geocoders.
59
+ #
60
+ # Some of these geocoders require configuration. You don't have to provide it here. See the README.
61
+ module Geocoders
62
+ @@proxy_addr = nil
63
+ @@proxy_port = nil
64
+ @@proxy_user = nil
65
+ @@proxy_pass = nil
66
+ @@timeout = nil
67
+ @@yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
68
+ @@google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
69
+ @@geocoder_us = false
70
+ @@geocoder_ca = false
71
+ @@geonames = false
72
+ @@provider_order = [:google,:us]
73
+ @@logger=Logger.new(STDOUT)
74
+ @@logger.level=Logger::INFO
75
+ @@domain = nil
76
+
77
+ def self.__define_accessors
78
+ class_variables.each do |v|
79
+ sym = v.delete("@").to_sym
80
+ unless self.respond_to? sym
81
+ module_eval <<-EOS, __FILE__, __LINE__
82
+ def self.#{sym}
83
+ value = if defined?(#{sym.to_s.upcase})
84
+ #{sym.to_s.upcase}
85
+ else
86
+ @@#{sym}
87
+ end
88
+ if value.is_a?(Hash)
89
+ value = (self.domain.nil? ? nil : value[self.domain]) || value.values.first
90
+ end
91
+ value
92
+ end
93
+
94
+ def self.#{sym}=(obj)
95
+ @@#{sym} = obj
96
+ end
97
+ EOS
98
+ end
99
+ end
100
+ end
101
+
102
+ __define_accessors
103
+
104
+ # Error which is thrown in the event a geocoding error occurs.
105
+ class GeocodeError < StandardError; end
106
+
107
+ # -------------------------------------------------------------------------------------------
108
+ # Geocoder Base class -- every geocoder should inherit from this
109
+ # -------------------------------------------------------------------------------------------
110
+
111
+ # The Geocoder base class which defines the interface to be used by all
112
+ # other geocoders.
113
+ class Geocoder
114
+ # Main method which calls the do_geocode template method which subclasses
115
+ # are responsible for implementing. Returns a populated GeoLoc or an
116
+ # empty one with a failed success code.
117
+ def self.geocode(address)
118
+ res = do_geocode(address)
119
+ return res.success? ? res : GeoLoc.new
120
+ end
121
+
122
+ # Main method which calls the do_reverse_geocode template method which subclasses
123
+ # are responsible for implementing. Returns a populated GeoLoc or an
124
+ # empty one with a failed success code.
125
+ def self.reverse_geocode(latlng)
126
+ res = do_reverse_geocode(latlng)
127
+ return res.success? ? res : GeoLoc.new
128
+ end
129
+
130
+ # Call the geocoder service using the timeout if configured.
131
+ def self.call_geocoder_service(url)
132
+ timeout(Geokit::Geocoders::timeout) { return self.do_get(url) } if Geokit::Geocoders::timeout
133
+ return self.do_get(url)
134
+ rescue TimeoutError
135
+ return nil
136
+ end
137
+
138
+ # Not all geocoders can do reverse geocoding. So, unless the subclass explicitly overrides this method,
139
+ # a call to reverse_geocode will return an empty GeoLoc. If you happen to be using MultiGeocoder,
140
+ # this will cause it to failover to the next geocoder, which will hopefully be one which supports reverse geocoding.
141
+ def self.do_reverse_geocode(latlng)
142
+ return GeoLoc.new
143
+ end
144
+
145
+ protected
146
+
147
+ def self.logger()
148
+ Geokit::Geocoders::logger
149
+ end
150
+
151
+ private
152
+
153
+ # Wraps the geocoder call around a proxy if necessary.
154
+ def self.do_get(url)
155
+ uri = URI.parse(url)
156
+ req = Net::HTTP::Get.new(url)
157
+ req.basic_auth(uri.user, uri.password) if uri.userinfo
158
+ res = Net::HTTP::Proxy(GeoKit::Geocoders::proxy_addr,
159
+ GeoKit::Geocoders::proxy_port,
160
+ GeoKit::Geocoders::proxy_user,
161
+ GeoKit::Geocoders::proxy_pass).start(uri.host, uri.port) { |http| http.request(req) }
162
+
163
+ return res
164
+ end
165
+
166
+ # Adds subclass' geocode method making it conveniently available through
167
+ # the base class.
168
+ def self.inherited(clazz)
169
+ class_name = clazz.name.split('::').last
170
+ src = <<-END_SRC
171
+ def self.#{Geokit::Inflector.underscore(class_name)}(address)
172
+ #{class_name}.geocode(address)
173
+ end
174
+ END_SRC
175
+ class_eval(src)
176
+ end
177
+ end
178
+
179
+ # -------------------------------------------------------------------------------------------
180
+ # "Regular" Address geocoders
181
+ # -------------------------------------------------------------------------------------------
182
+
183
+ # Geocoder CA geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_CA variable to
184
+ # contain true or false based upon whether authentication is to occur. Conforms to the
185
+ # interface set by the Geocoder class.
186
+ #
187
+ # Returns a response like:
188
+ # <?xml version="1.0" encoding="UTF-8" ?>
189
+ # <geodata>
190
+ # <latt>49.243086</latt>
191
+ # <longt>-123.153684</longt>
192
+ # </geodata>
193
+ class CaGeocoder < Geocoder
194
+
195
+ private
196
+
197
+ # Template method which does the geocode lookup.
198
+ def self.do_geocode(address)
199
+ raise ArgumentError('Geocoder.ca requires a GeoLoc argument') unless address.is_a?(GeoLoc)
200
+ url = construct_request(address)
201
+ res = self.call_geocoder_service(url)
202
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
203
+ xml = res.body
204
+ logger.debug "Geocoder.ca geocoding. Address: #{address}. Result: #{xml}"
205
+ # Parse the document.
206
+ doc = REXML::Document.new(xml)
207
+ address.lat = doc.elements['//latt'].text
208
+ address.lng = doc.elements['//longt'].text
209
+ address.success = true
210
+ return address
211
+ rescue
212
+ logger.error "Caught an error during Geocoder.ca geocoding call: "+$!
213
+ return GeoLoc.new
214
+ end
215
+
216
+ # Formats the request in the format acceptable by the CA geocoder.
217
+ def self.construct_request(location)
218
+ url = ""
219
+ url += add_ampersand(url) + "stno=#{location.street_number}" if location.street_address
220
+ url += add_ampersand(url) + "addresst=#{Geokit::Inflector::url_escape(location.street_name)}" if location.street_address
221
+ url += add_ampersand(url) + "city=#{Geokit::Inflector::url_escape(location.city)}" if location.city
222
+ url += add_ampersand(url) + "prov=#{location.state}" if location.state
223
+ url += add_ampersand(url) + "postal=#{location.zip}" if location.zip
224
+ url += add_ampersand(url) + "auth=#{Geokit::Geocoders::geocoder_ca}" if Geokit::Geocoders::geocoder_ca
225
+ url += add_ampersand(url) + "geoit=xml"
226
+ 'http://geocoder.ca/?' + url
227
+ end
228
+
229
+ def self.add_ampersand(url)
230
+ url && url.length > 0 ? "&" : ""
231
+ end
232
+ end
233
+
234
+ # Geocoder Us geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_US variable to
235
+ # contain true or false based upon whether authentication is to occur. Conforms to the
236
+ # interface set by the Geocoder class.
237
+ class UsGeocoder < Geocoder
238
+
239
+ private
240
+ def self.do_geocode(address)
241
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
242
+
243
+ query = (address_str =~ /^\d{5}(?:-\d{4})?$/ ? "zip" : "address") + "=#{Geokit::Inflector::url_escape(address_str)}"
244
+ url = if GeoKit::Geocoders::geocoder_us
245
+ "http://#{GeoKit::Geocoders::geocoder_us}@geocoder.us/member/service/csv/geocode"
246
+ else
247
+ "http://geocoder.us/service/csv/geocode"
248
+ end
249
+
250
+ url = "#{url}?#{query}"
251
+ res = self.call_geocoder_service(url)
252
+
253
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
254
+ data = res.body
255
+ logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}"
256
+ array = data.chomp.split(',')
257
+
258
+ if array.length == 5
259
+ res=GeoLoc.new
260
+ res.lat,res.lng,res.city,res.state,res.zip=array
261
+ res.country_code='US'
262
+ res.success=true
263
+ return res
264
+ elsif array.length == 6
265
+ res=GeoLoc.new
266
+ res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array
267
+ res.country_code='US'
268
+ res.success=true
269
+ return res
270
+ else
271
+ logger.info "geocoder.us was unable to geocode address: "+address
272
+ return GeoLoc.new
273
+ end
274
+ rescue
275
+ logger.error "Caught an error during geocoder.us geocoding call: "+$!
276
+ return GeoLoc.new
277
+
278
+ end
279
+ end
280
+
281
+ # Yahoo geocoder implementation. Requires the Geokit::Geocoders::YAHOO variable to
282
+ # contain a Yahoo API key. Conforms to the interface set by the Geocoder class.
283
+ class YahooGeocoder < Geocoder
284
+
285
+ private
286
+
287
+ # Template method which does the geocode lookup.
288
+ def self.do_geocode(address)
289
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
290
+ url="http://api.local.yahoo.com/MapsService/V1/geocode?appid=#{Geokit::Geocoders::yahoo}&location=#{Geokit::Inflector::url_escape(address_str)}"
291
+ res = self.call_geocoder_service(url)
292
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
293
+ xml = res.body
294
+ doc = REXML::Document.new(xml)
295
+ logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}"
296
+
297
+ if doc.elements['//ResultSet']
298
+ res=GeoLoc.new
299
+
300
+ #basic
301
+ res.lat=doc.elements['//Latitude'].text
302
+ res.lng=doc.elements['//Longitude'].text
303
+ res.country_code=doc.elements['//Country'].text
304
+ res.provider='yahoo'
305
+
306
+ #extended - false if not available
307
+ res.city=doc.elements['//City'].text if doc.elements['//City'] && doc.elements['//City'].text != nil
308
+ res.state=doc.elements['//State'].text if doc.elements['//State'] && doc.elements['//State'].text != nil
309
+ res.zip=doc.elements['//Zip'].text if doc.elements['//Zip'] && doc.elements['//Zip'].text != nil
310
+ res.street_address=doc.elements['//Address'].text if doc.elements['//Address'] && doc.elements['//Address'].text != nil
311
+ res.precision=doc.elements['//Result'].attributes['precision'] if doc.elements['//Result']
312
+ # set the accuracy as google does (added by Andruby)
313
+ res.accuracy=%w{unknown country state state city zip zip+4 street address building}.index(res.precision)
314
+ res.success=true
315
+ return res
316
+ else
317
+ logger.info "Yahoo was unable to geocode address: "+address
318
+ return GeoLoc.new
319
+ end
320
+
321
+ rescue
322
+ logger.info "Caught an error during Yahoo geocoding call: "+$!
323
+ return GeoLoc.new
324
+ end
325
+ end
326
+
327
+ # Another geocoding web service
328
+ # http://www.geonames.org
329
+ class GeonamesGeocoder < Geocoder
330
+
331
+ private
332
+
333
+ # Template method which does the geocode lookup.
334
+ def self.do_geocode(address)
335
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
336
+ # geonames need a space seperated search string
337
+ address_str.gsub!(/,/, " ")
338
+ params = "/postalCodeSearch?placename=#{Geokit::Inflector::url_escape(address_str)}&maxRows=10"
339
+
340
+ if(GeoKit::Geocoders::geonames)
341
+ url = "http://ws.geonames.net#{params}&username=#{GeoKit::Geocoders::geonames}"
342
+ else
343
+ url = "http://ws.geonames.org#{params}"
344
+ end
345
+
346
+ res = self.call_geocoder_service(url)
347
+
348
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
349
+
350
+ xml=res.body
351
+ logger.debug "Geonames geocoding. Address: #{address}. Result: #{xml}"
352
+ doc=REXML::Document.new(xml)
353
+
354
+ if(doc.elements['//geonames/totalResultsCount'].text.to_i > 0)
355
+ res=GeoLoc.new
356
+
357
+ # only take the first result
358
+ res.lat=doc.elements['//code/lat'].text if doc.elements['//code/lat']
359
+ res.lng=doc.elements['//code/lng'].text if doc.elements['//code/lng']
360
+ res.country_code=doc.elements['//code/countryCode'].text if doc.elements['//code/countryCode']
361
+ res.provider='genomes'
362
+ res.city=doc.elements['//code/name'].text if doc.elements['//code/name']
363
+ res.state=doc.elements['//code/adminName1'].text if doc.elements['//code/adminName1']
364
+ res.zip=doc.elements['//code/postalcode'].text if doc.elements['//code/postalcode']
365
+ res.success=true
366
+ return res
367
+ else
368
+ logger.info "Geonames was unable to geocode address: "+address
369
+ return GeoLoc.new
370
+ end
371
+
372
+ rescue
373
+ logger.error "Caught an error during Geonames geocoding call: "+$!
374
+ end
375
+ end
376
+
377
+ # -------------------------------------------------------------------------------------------
378
+ # Address geocoders that also provide reverse geocoding
379
+ # -------------------------------------------------------------------------------------------
380
+
381
+ # Google geocoder implementation. Requires the Geokit::Geocoders::GOOGLE variable to
382
+ # contain a Google API key. Conforms to the interface set by the Geocoder class.
383
+ class GoogleGeocoder < Geocoder
384
+
385
+ private
386
+
387
+ # Template method which does the reverse-geocode lookup.
388
+ def self.do_reverse_geocode(latlng)
389
+ latlng=LatLng.normalize(latlng)
390
+ 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")
391
+ # 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"))
392
+ return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
393
+ xml = res.body
394
+ logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{xml}"
395
+ return self.xml2GeoLoc(xml)
396
+ end
397
+
398
+ # Template method which does the geocode lookup.
399
+ def self.do_geocode(address)
400
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
401
+ 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")
402
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
403
+ xml = res.body
404
+ logger.debug "Google geocoding. Address: #{address}. Result: #{xml}"
405
+ return self.xml2GeoLoc(xml, address)
406
+ end
407
+
408
+ def self.xml2GeoLoc(xml, address="")
409
+ doc=REXML::Document.new(xml)
410
+
411
+ if doc.elements['//kml/Response/Status/code'].text == '200'
412
+ geoloc = nil
413
+ # Google can return multiple results as //Placemark elements.
414
+ # iterate through each and extract each placemark as a geoloc
415
+ doc.each_element('//Placemark') do |e|
416
+ extracted_geoloc = extract_placemark(e) # g is now an instance of GeoLoc
417
+ if geoloc.nil?
418
+ # first time through, geoloc is still nil, so we make it the geoloc we just extracted
419
+ geoloc = extracted_geoloc
420
+ else
421
+ # second (and subsequent) iterations, we push additional
422
+ # geolocs onto "geoloc.all"
423
+ geoloc.all.push(extracted_geoloc)
424
+ end
425
+ end
426
+ return geoloc
427
+ else
428
+ logger.info "Google was unable to geocode address: "+address
429
+ return GeoLoc.new
430
+ end
431
+
432
+ rescue
433
+ logger.error "Caught an error during Google geocoding call: "+$!
434
+ return GeoLoc.new
435
+ end
436
+
437
+ # extracts a single geoloc from a //placemark element in the google results xml
438
+ def self.extract_placemark(doc)
439
+ res = GeoLoc.new
440
+ coordinates=doc.elements['.//coordinates'].text.to_s.split(',')
441
+
442
+ #basics
443
+ res.lat=coordinates[1]
444
+ res.lng=coordinates[0]
445
+ res.country_code=doc.elements['.//CountryNameCode'].text if doc.elements['.//CountryNameCode']
446
+ res.provider='google'
447
+
448
+ #extended -- false if not not available
449
+ res.city = doc.elements['.//LocalityName'].text if doc.elements['.//LocalityName']
450
+ res.state = doc.elements['.//AdministrativeAreaName'].text if doc.elements['.//AdministrativeAreaName']
451
+ res.full_address = doc.elements['.//address'].text if doc.elements['.//address'] # google provides it
452
+ res.zip = doc.elements['.//PostalCodeNumber'].text if doc.elements['.//PostalCodeNumber']
453
+ res.street_address = doc.elements['.//ThoroughfareName'].text if doc.elements['.//ThoroughfareName']
454
+ # Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country
455
+ # For Google, 1=low accuracy, 8=high accuracy
456
+ address_details=doc.elements['.//*[local-name() = "AddressDetails"]']
457
+ res.accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0
458
+ res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy]
459
+ res.success=true
460
+
461
+ return res
462
+ end
463
+ end
464
+
465
+
466
+ # -------------------------------------------------------------------------------------------
467
+ # IP Geocoders
468
+ # -------------------------------------------------------------------------------------------
469
+
470
+ # Provides geocoding based upon an IP address. The underlying web service is geoplugin.net
471
+ class GeoPluginGeocoder < Geocoder
472
+ private
473
+
474
+ def self.do_geocode(ip)
475
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
476
+ response = self.call_geocoder_service("http://www.geoplugin.net/xml.gp?ip=#{ip}")
477
+ return response.is_a?(Net::HTTPSuccess) ? parse_xml(response.body) : GeoLoc.new
478
+ rescue
479
+ logger.error "Caught an error during GeloPluginGeocoder geocoding call: "+$!
480
+ return GeoLoc.new
481
+ end
482
+
483
+ def self.parse_xml(xml)
484
+ xml = REXML::Document.new(xml)
485
+ geo = GeoLoc.new
486
+ geo.provider='geoPlugin'
487
+ geo.city = xml.elements['//geoplugin_city'].text
488
+ geo.state = xml.elements['//geoplugin_region'].text
489
+ geo.country_code = xml.elements['//geoplugin_countryCode'].text
490
+ geo.lat = xml.elements['//geoplugin_latitude'].text.to_f
491
+ geo.lng = xml.elements['//geoplugin_longitude'].text.to_f
492
+ geo.success = !geo.city.empty?
493
+ return geo
494
+ end
495
+ end
496
+
497
+ # Provides geocoding based upon an IP address. The underlying web service is a hostip.info
498
+ # which sources their data through a combination of publicly available information as well
499
+ # as community contributions.
500
+ class IpGeocoder < Geocoder
501
+
502
+ private
503
+
504
+ # Given an IP address, returns a GeoLoc instance which contains latitude,
505
+ # longitude, city, and country code. Sets the success attribute to false if the ip
506
+ # parameter does not match an ip address.
507
+ def self.do_geocode(ip)
508
+ return GeoLoc.new if '0.0.0.0' == ip
509
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
510
+ url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true"
511
+ response = self.call_geocoder_service(url)
512
+ response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new
513
+ rescue
514
+ logger.error "Caught an error during HostIp geocoding call: "+$!
515
+ return GeoLoc.new
516
+ end
517
+
518
+ # Converts the body to YAML since its in the form of:
519
+ #
520
+ # Country: UNITED STATES (US)
521
+ # City: Sugar Grove, IL
522
+ # Latitude: 41.7696
523
+ # Longitude: -88.4588
524
+ #
525
+ # then instantiates a GeoLoc instance to populate with location data.
526
+ def self.parse_body(body) # :nodoc:
527
+ yaml = YAML.load(body)
528
+ res = GeoLoc.new
529
+ res.provider = 'hostip'
530
+ res.city, res.state = yaml['City'].split(', ')
531
+ country, res.country_code = yaml['Country'].split(' (')
532
+ res.lat = yaml['Latitude']
533
+ res.lng = yaml['Longitude']
534
+ res.country_code.chop!
535
+ res.success = !(res.city =~ /\(.+\)/)
536
+ res
537
+ end
538
+ end
539
+
540
+ # -------------------------------------------------------------------------------------------
541
+ # The Multi Geocoder
542
+ # -------------------------------------------------------------------------------------------
543
+
544
+ # Provides methods to geocode with a variety of geocoding service providers, plus failover
545
+ # among providers in the order you configure.
546
+ #
547
+ # Goal:
548
+ # - homogenize the results of multiple geocoders
549
+ #
550
+ # Limitations:
551
+ # - currently only provides the first result. Sometimes geocoders will return multiple results.
552
+ # - currently discards the "accuracy" component of the geocoding calls
553
+ class MultiGeocoder < Geocoder
554
+ private
555
+
556
+ # This method will call one or more geocoders in the order specified in the
557
+ # configuration until one of the geocoders work.
558
+ #
559
+ # The failover approach is crucial for production-grade apps, but is rarely used.
560
+ # 98% of your geocoding calls will be successful with the first call
561
+ def self.do_geocode(address)
562
+ Geokit::Geocoders::provider_order.each do |provider|
563
+ begin
564
+ klass = Geokit::Geocoders.const_get "#{provider.to_s.capitalize}Geocoder"
565
+ res = klass.send :geocode, address
566
+ return res if res.success?
567
+ rescue
568
+ 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}")
569
+ end
570
+ end
571
+ # If we get here, we failed completely.
572
+ GeoLoc.new
573
+ end
574
+ end
575
+ end
576
+ end