loco2-geokit 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,30 @@
1
+ module Geokit
2
+ VERSION = '1.5.0'
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
+ $:.unshift path unless $:.include?(path)
26
+ require 'geokit/geocoders'
27
+ require 'geokit/mappable'
28
+
29
+ # make old-style module name "GeoKit" equivalent to new-style "Geokit"
30
+ GeoKit=Geokit
@@ -0,0 +1,777 @@
1
+ require 'net/http'
2
+ require 'ipaddr'
3
+ require 'rexml/document'
4
+ require 'yaml'
5
+ require 'timeout'
6
+ require 'logger'
7
+
8
+ module Geokit
9
+
10
+ class TooManyQueriesError < StandardError; end
11
+
12
+ module Inflector
13
+
14
+ extend self
15
+
16
+ def titleize(word)
17
+ humanize(underscore(word)).gsub(/\b([a-z])/u) { $1.capitalize }
18
+ end
19
+
20
+ def underscore(camel_cased_word)
21
+ camel_cased_word.to_s.gsub(/::/, '/').
22
+ gsub(/([A-Z]+)([A-Z][a-z])/u,'\1_\2').
23
+ gsub(/([a-z\d])([A-Z])/u,'\1_\2').
24
+ tr("-", "_").
25
+ downcase
26
+ end
27
+
28
+ def humanize(lower_case_and_underscored_word)
29
+ lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
30
+ end
31
+
32
+ def snake_case(s)
33
+ return s.downcase if s =~ /^[A-Z]+$/u
34
+ s.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/u, '_\&') =~ /_*(.*)/
35
+ return $+.downcase
36
+
37
+ end
38
+
39
+ def url_escape(s)
40
+ s.gsub(/([^ a-zA-Z0-9_.-]+)/nu) do
41
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
42
+ end.tr(' ', '+')
43
+ end
44
+
45
+ def camelize(str)
46
+ str.split('_').map {|w| w.capitalize}.join
47
+ end
48
+ end
49
+
50
+ # Contains a range of geocoders:
51
+ #
52
+ # ### "regular" address geocoders
53
+ # * Yahoo Geocoder - requires an API key.
54
+ # * Geocoder.us - may require authentication if performing more than the free request limit.
55
+ # * Geocoder.ca - for Canada; may require authentication as well.
56
+ # * Geonames - a free geocoder
57
+ #
58
+ # ### address geocoders that also provide reverse geocoding
59
+ # * Google Geocoder - requires an API key.
60
+ #
61
+ # ### IP address geocoders
62
+ # * IP Geocoder - geocodes an IP address using hostip.info's web service.
63
+ # * Geoplugin.net -- another IP address geocoder
64
+ #
65
+ # ### The Multigeocoder
66
+ # * Multi Geocoder - provides failover for the physical location geocoders.
67
+ #
68
+ # Some of these geocoders require configuration. You don't have to provide it here. See the README.
69
+ module Geocoders
70
+ @@proxy_addr = nil
71
+ @@proxy_port = nil
72
+ @@proxy_user = nil
73
+ @@proxy_pass = nil
74
+ @@request_timeout = nil
75
+ @@yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
76
+ @@google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
77
+ @@geocoder_us = false
78
+ @@geocoder_ca = false
79
+ @@geonames_host = 'ws.geonames.org'
80
+ @@geonames_username = false
81
+ @@provider_order = [:google,:us]
82
+ @@ip_provider_order = [:geo_plugin,:ip]
83
+ @@logger=Logger.new(STDOUT)
84
+ @@logger.level=Logger::INFO
85
+ @@domain = nil
86
+
87
+ def self.__define_accessors
88
+ class_variables.each do |v|
89
+ sym = v.to_s.delete("@").to_sym
90
+ unless self.respond_to? sym
91
+ module_eval <<-EOS, __FILE__, __LINE__
92
+ def self.#{sym}
93
+ value = if defined?(#{sym.to_s.upcase})
94
+ #{sym.to_s.upcase}
95
+ else
96
+ @@#{sym}
97
+ end
98
+ if value.is_a?(Hash)
99
+ value = (self.domain.nil? ? nil : value[self.domain]) || value.values.first
100
+ end
101
+ value
102
+ end
103
+
104
+ def self.#{sym}=(obj)
105
+ @@#{sym} = obj
106
+ end
107
+ EOS
108
+ end
109
+ end
110
+ end
111
+
112
+ __define_accessors
113
+
114
+ # Error which is thrown in the event a geocoding error occurs.
115
+ class GeocodeError < StandardError; end
116
+
117
+ # -------------------------------------------------------------------------------------------
118
+ # Geocoder Base class -- every geocoder should inherit from this
119
+ # -------------------------------------------------------------------------------------------
120
+
121
+ # The Geocoder base class which defines the interface to be used by all
122
+ # other geocoders.
123
+ class Geocoder
124
+ # Main method which calls the do_geocode template method which subclasses
125
+ # are responsible for implementing. Returns a populated GeoLoc or an
126
+ # empty one with a failed success code.
127
+ def self.geocode(address, options = {})
128
+ res = do_geocode(address, options)
129
+ return res.nil? ? GeoLoc.new : res
130
+ end
131
+ # Main method which calls the do_reverse_geocode template method which subclasses
132
+ # are responsible for implementing. Returns a populated GeoLoc or an
133
+ # empty one with a failed success code.
134
+ def self.reverse_geocode(latlng)
135
+ res = do_reverse_geocode(latlng)
136
+ return res.success? ? res : GeoLoc.new
137
+ end
138
+
139
+ # Call the geocoder service using the timeout if configured.
140
+ def self.call_geocoder_service(url)
141
+ Timeout::timeout(Geokit::Geocoders::request_timeout) { return self.do_get(url) } if Geokit::Geocoders::request_timeout
142
+ return self.do_get(url)
143
+ rescue TimeoutError
144
+ return nil
145
+ end
146
+
147
+ # Not all geocoders can do reverse geocoding. So, unless the subclass explicitly overrides this method,
148
+ # a call to reverse_geocode will return an empty GeoLoc. If you happen to be using MultiGeocoder,
149
+ # this will cause it to failover to the next geocoder, which will hopefully be one which supports reverse geocoding.
150
+ def self.do_reverse_geocode(latlng)
151
+ return GeoLoc.new
152
+ end
153
+
154
+ protected
155
+
156
+ def self.logger()
157
+ Geokit::Geocoders::logger
158
+ end
159
+
160
+ private
161
+
162
+ # Wraps the geocoder call around a proxy if necessary.
163
+ def self.do_get(url)
164
+ uri = URI.parse(url)
165
+ req = Net::HTTP::Get.new(url)
166
+ req.basic_auth(uri.user, uri.password) if uri.userinfo
167
+ res = Net::HTTP::Proxy(GeoKit::Geocoders::proxy_addr,
168
+ GeoKit::Geocoders::proxy_port,
169
+ GeoKit::Geocoders::proxy_user,
170
+ GeoKit::Geocoders::proxy_pass).start(uri.host, uri.port) { |http| http.get(uri.path + "?" + uri.query) }
171
+ return res
172
+ end
173
+
174
+ # Adds subclass' geocode method making it conveniently available through
175
+ # the base class.
176
+ def self.inherited(clazz)
177
+ class_name = clazz.name.split('::').last
178
+ src = <<-END_SRC
179
+ def self.#{Geokit::Inflector.underscore(class_name)}(address, options = {})
180
+ #{class_name}.geocode(address, options)
181
+ end
182
+ END_SRC
183
+ class_eval(src)
184
+ end
185
+ end
186
+
187
+ # -------------------------------------------------------------------------------------------
188
+ # "Regular" Address geocoders
189
+ # -------------------------------------------------------------------------------------------
190
+
191
+ # Geocoder CA geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_CA variable to
192
+ # contain true or false based upon whether authentication is to occur. Conforms to the
193
+ # interface set by the Geocoder class.
194
+ #
195
+ # Returns a response like:
196
+ # <?xml version="1.0" encoding="UTF-8" ?>
197
+ # <geodata>
198
+ # <latt>49.243086</latt>
199
+ # <longt>-123.153684</longt>
200
+ # </geodata>
201
+ class CaGeocoder < Geocoder
202
+
203
+ private
204
+
205
+ # Template method which does the geocode lookup.
206
+ def self.do_geocode(address, options = {})
207
+ raise ArgumentError('Geocoder.ca requires a GeoLoc argument') unless address.is_a?(GeoLoc)
208
+ url = construct_request(address)
209
+ res = self.call_geocoder_service(url)
210
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
211
+ xml = res.body
212
+ logger.debug "Geocoder.ca geocoding. Address: #{address}. Result: #{xml}"
213
+ # Parse the document.
214
+ doc = REXML::Document.new(xml)
215
+ address.lat = doc.elements['//latt'].text
216
+ address.lng = doc.elements['//longt'].text
217
+ address.success = true
218
+ return address
219
+ rescue
220
+ logger.error "Caught an error during Geocoder.ca geocoding call: "+$!
221
+ return GeoLoc.new
222
+ end
223
+
224
+ # Formats the request in the format acceptable by the CA geocoder.
225
+ def self.construct_request(location)
226
+ url = ""
227
+ url += add_ampersand(url) + "stno=#{location.street_number}" if location.street_address
228
+ url += add_ampersand(url) + "addresst=#{Geokit::Inflector::url_escape(location.street_name)}" if location.street_address
229
+ url += add_ampersand(url) + "city=#{Geokit::Inflector::url_escape(location.city)}" if location.city
230
+ url += add_ampersand(url) + "prov=#{location.state}" if location.state
231
+ url += add_ampersand(url) + "postal=#{location.zip}" if location.zip
232
+ url += add_ampersand(url) + "auth=#{Geokit::Geocoders::geocoder_ca}" if Geokit::Geocoders::geocoder_ca
233
+ url += add_ampersand(url) + "geoit=xml"
234
+ 'http://geocoder.ca/?' + url
235
+ end
236
+
237
+ def self.add_ampersand(url)
238
+ url && url.length > 0 ? "&" : ""
239
+ end
240
+ end
241
+
242
+ # Geocoder Us geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_US variable to
243
+ # contain true or false based upon whether authentication is to occur. Conforms to the
244
+ # interface set by the Geocoder class.
245
+ class UsGeocoder < Geocoder
246
+
247
+ private
248
+ def self.do_geocode(address, options = {})
249
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
250
+
251
+ query = (address_str =~ /^\d{5}(?:-\d{4})?$/ ? "zip" : "address") + "=#{Geokit::Inflector::url_escape(address_str)}"
252
+ url = if GeoKit::Geocoders::geocoder_us
253
+ "http://#{GeoKit::Geocoders::geocoder_us}@geocoder.us/member/service/csv/geocode"
254
+ else
255
+ "http://geocoder.us/service/csv/geocode"
256
+ end
257
+
258
+ url = "#{url}?#{query}"
259
+ res = self.call_geocoder_service(url)
260
+
261
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
262
+ data = res.body
263
+ logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}"
264
+ array = data.chomp.split(',')
265
+
266
+ if array.length == 5
267
+ res=GeoLoc.new
268
+ res.lat,res.lng,res.city,res.state,res.zip=array
269
+ res.country_code='US'
270
+ res.success=true
271
+ return res
272
+ elsif array.length == 6
273
+ res=GeoLoc.new
274
+ res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array
275
+ res.country_code='US'
276
+ res.success=true
277
+ return res
278
+ else
279
+ logger.info "geocoder.us was unable to geocode address: "+address
280
+ return GeoLoc.new
281
+ end
282
+ rescue
283
+ logger.error "Caught an error during geocoder.us geocoding call: "+$!
284
+ return GeoLoc.new
285
+
286
+ end
287
+ end
288
+
289
+ # Yahoo geocoder implementation. Requires the Geokit::Geocoders::YAHOO variable to
290
+ # contain a Yahoo API key. Conforms to the interface set by the Geocoder class.
291
+ class YahooGeocoder < Geocoder
292
+
293
+ private
294
+
295
+ # Template method which does the geocode lookup.
296
+ def self.do_geocode(address, options = {})
297
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
298
+ url="http://api.local.yahoo.com/MapsService/V1/geocode?appid=#{Geokit::Geocoders::yahoo}&location=#{Geokit::Inflector::url_escape(address_str)}"
299
+ res = self.call_geocoder_service(url)
300
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
301
+ xml = res.body
302
+ doc = REXML::Document.new(xml)
303
+ logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}"
304
+
305
+ if doc.elements['//ResultSet']
306
+ res=GeoLoc.new
307
+
308
+ #basic
309
+ res.lat=doc.elements['//Latitude'].text
310
+ res.lng=doc.elements['//Longitude'].text
311
+ res.country_code=doc.elements['//Country'].text
312
+ res.provider='yahoo'
313
+
314
+ #extended - false if not available
315
+ res.city=doc.elements['//City'].text if doc.elements['//City'] && doc.elements['//City'].text != nil
316
+ res.state=doc.elements['//State'].text if doc.elements['//State'] && doc.elements['//State'].text != nil
317
+ res.zip=doc.elements['//Zip'].text if doc.elements['//Zip'] && doc.elements['//Zip'].text != nil
318
+ res.street_address=doc.elements['//Address'].text if doc.elements['//Address'] && doc.elements['//Address'].text != nil
319
+ res.precision=doc.elements['//Result'].attributes['precision'] if doc.elements['//Result']
320
+ # set the accuracy as google does (added by Andruby)
321
+ res.accuracy=%w{unknown country state state city zip zip+4 street address building}.index(res.precision)
322
+ res.success=true
323
+ return res
324
+ else
325
+ logger.info "Yahoo was unable to geocode address: "+address
326
+ return GeoLoc.new
327
+ end
328
+
329
+ rescue
330
+ logger.info "Caught an error during Yahoo geocoding call: "+$!
331
+ return GeoLoc.new
332
+ end
333
+ end
334
+
335
+ # Another geocoding web service
336
+ # http://www.geonames.org
337
+ class GeonamesGeocoder < Geocoder
338
+
339
+ # Returns result from geonames' search webservice
340
+ def self.search(address, options = {})
341
+ params = "/search?q=#{Geokit::Inflector::url_escape(address_string(address))}"
342
+
343
+ if options[:continent]
344
+ Array(options[:continent]).each do |c|
345
+ params << "&continentCode=#{c}"
346
+ end
347
+ end
348
+
349
+ if options[:feature_class]
350
+ Array(options[:feature_class]).each do |fc|
351
+ params << "&featureClass=#{fc}"
352
+ end
353
+ end
354
+
355
+ if options[:feature_code]
356
+ Array(options[:feature_code]).each do |fc|
357
+ params << "&featureCode=#{fc}"
358
+ end
359
+ end
360
+
361
+ if options[:operator].to_s.upcase == 'OR'
362
+ params << "&operator=OR"
363
+ end
364
+
365
+ params << "&style=FULL"
366
+ params << "&maxRows=1"
367
+
368
+ do_call_geocoder_service(address, params)
369
+ end
370
+
371
+ def self.cities_in_bounds(bounds)
372
+ params = "/cities?north=#{bounds.ne.lat}&south=#{bounds.sw.lat}&east=#{bounds.ne.lng}&west=#{bounds.sw.lng}"
373
+
374
+ do_call_geocoder_service(bounds.to_s, params)
375
+ end
376
+
377
+ def self.find_nearby(ll, options)
378
+ params = "/findNearby?lat=#{ll.lat}&lng=#{ll.lng}"
379
+
380
+ params << "&radius=#{options[:radius]}" if options[:radius]
381
+
382
+ if options[:feature_class]
383
+ Array(options[:feature_class]).each do |fc|
384
+ params << "&featureClass=#{fc}"
385
+ end
386
+ end
387
+ if options[:feature_code]
388
+ Array(options[:feature_code]).each do |fc|
389
+ params << "&featureCode=#{fc}"
390
+ end
391
+ end
392
+ params << "&style=FULL"
393
+ params << "&maxRows=1"
394
+
395
+ do_call_geocoder_service(ll.to_s, params)
396
+ end
397
+
398
+ private
399
+
400
+ # Template method which does the geocode lookup.
401
+ def self.do_geocode(address, options = {})
402
+ params = "/postalCodeSearch?placename=#{Geokit::Inflector::url_escape(address_string(address))}&maxRows=1"
403
+
404
+ self.do_call_geocoder_service(address, params) do |res, doc|
405
+ if(doc.elements['//geonames/totalResultsCount'].text.to_i > 0)
406
+ # only take the first result
407
+ res.lat=doc.elements['//code/lat'].text if doc.elements['//code/lat']
408
+ res.lng=doc.elements['//code/lng'].text if doc.elements['//code/lng']
409
+ res.country_code=doc.elements['//code/countryCode'].text if doc.elements['//code/countryCode']
410
+ res.provider='geonames'
411
+ res.city=doc.elements['//code/name'].text if doc.elements['//code/name']
412
+ res.state=doc.elements['//code/adminName1'].text if doc.elements['//code/adminName1']
413
+ res.zip=doc.elements['//code/postalcode'].text if doc.elements['//code/postalcode']
414
+ res.success=true
415
+ end
416
+ end
417
+ end
418
+
419
+ def self.address_string(address)
420
+ # geonames need a space seperated search string
421
+ (address.is_a?(GeoLoc) ? address.to_geocodeable_s : address).gsub(/,/, ' ')
422
+ end
423
+
424
+ def self.url(params)
425
+ if(GeoKit::Geocoders::geonames_username)
426
+ url = "http://ws.geonames.net#{params}&username=#{GeoKit::Geocoders::geonames_username}"
427
+ else
428
+ url = "http://#{GeoKit::Geocoders::geonames_host}#{params}"
429
+ end
430
+ end
431
+
432
+ def self.do_call_geocoder_service(request, params)
433
+ res = self.call_geocoder_service(url(params))
434
+
435
+ raise GeocodeError.new("HTTP request failed: #{res.class}") if !res.is_a?(Net::HTTPSuccess)
436
+
437
+ xml=res.body
438
+ logger.debug "Geonames gecoder. Request: #{request}. Result: #{xml}"
439
+ doc=REXML::Document.new(xml)
440
+
441
+ res=GeoLoc.new
442
+
443
+ if block_given?
444
+ yield res, doc
445
+ else
446
+ parse_result res, doc
447
+ end
448
+
449
+ if !res.success?
450
+ logger.info "Geonames was unable to process request: #{request}"
451
+ end
452
+
453
+ res
454
+ end
455
+
456
+ def self.parse_result(res, doc)
457
+ if(doc.elements['//geonames/geoname'])
458
+ # only take the first result
459
+ res.lat=doc.elements['//geoname/lat'].text if doc.elements['//geoname/lat']
460
+ res.lng=doc.elements['//geoname/lng'].text if doc.elements['//geoname/lng']
461
+ res.name = doc.elements['//geoname/name'].text if doc.elements['//geoname/name']
462
+ res.country_code=doc.elements['//geoname/countryCode'].text if doc.elements['//geoname/countryCode']
463
+ res.provider='geonames'
464
+ res.provider_id = doc.elements['//geoname/geonameId'].text if doc.elements['//geoname/geonameId']
465
+ # if the location is a city or village
466
+ if doc.elements['//geoname/fcl'] && doc.elements['//geoname/fcl'].text == 'P'
467
+ res.city=res.name
468
+ end
469
+ res.state=doc.elements['//geoname/adminCode1'].text if doc.elements['//geoname/adminCode1']
470
+ res.timezone = doc.elements['//geoname/timezone'].text if doc.elements['//geoname/timezone']
471
+ res.success=true
472
+ end
473
+ end
474
+ end
475
+
476
+ # -------------------------------------------------------------------------------------------
477
+ # Address geocoders that also provide reverse geocoding
478
+ # -------------------------------------------------------------------------------------------
479
+
480
+ # Google geocoder implementation. Requires the Geokit::Geocoders::GOOGLE variable to
481
+ # contain a Google API key. Conforms to the interface set by the Geocoder class.
482
+ class GoogleGeocoder < Geocoder
483
+
484
+ private
485
+
486
+ # Template method which does the reverse-geocode lookup.
487
+ def self.do_reverse_geocode(latlng)
488
+ latlng=LatLng.normalize(latlng)
489
+ 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")
490
+ # 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"))
491
+ return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
492
+ xml = res.body
493
+ logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{xml}"
494
+ return self.xml2GeoLoc(xml)
495
+ end
496
+
497
+ # Template method which does the geocode lookup.
498
+ #
499
+ # Supports viewport/country code biasing
500
+ #
501
+ # ==== OPTIONS
502
+ # * :bias - This option makes the Google Geocoder return results biased to a particular
503
+ # country or viewport. Country code biasing is achieved by passing the ccTLD
504
+ # ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's,
505
+ # look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder
506
+ # will be biased to results within the US (ccTLD .com).
507
+ #
508
+ # If you'd like the Google Geocoder to prefer results within a given viewport,
509
+ # you can pass a Geokit::Bounds object as the :bias value.
510
+ #
511
+ # ==== EXAMPLES
512
+ # # By default, the geocoder will return Syracuse, NY
513
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse').country_code # => 'US'
514
+ # # With country code biasing, it returns Syracuse in Sicily, Italy
515
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse', :bias => :it).country_code # => 'IT'
516
+ #
517
+ # # By default, the geocoder will return Winnetka, IL
518
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka').state # => 'IL'
519
+ # # When biased to an bounding box around California, it will now return the Winnetka neighbourhood, CA
520
+ # bounds = Geokit::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487])
521
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka', :bias => bounds).state # => 'CA'
522
+ def self.do_geocode(address, options = {})
523
+ bias_str = options[:bias] ? construct_bias_string_from_options(options[:bias]) : ''
524
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
525
+ res = self.call_geocoder_service("http://maps.google.com/maps/geo?q=#{Geokit::Inflector::url_escape(address_str)}&output=xml#{bias_str}&key=#{Geokit::Geocoders::google}&oe=utf-8")
526
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
527
+ xml = res.body
528
+ logger.debug "Google geocoding. Address: #{address}. Result: #{xml}"
529
+ return self.xml2GeoLoc(xml, address)
530
+ end
531
+
532
+ def self.construct_bias_string_from_options(bias)
533
+ if bias.is_a?(String) or bias.is_a?(Symbol)
534
+ # country code biasing
535
+ "&gl=#{bias.to_s.downcase}"
536
+ elsif bias.is_a?(Bounds)
537
+ # viewport biasing
538
+ "&ll=#{bias.center.ll}&spn=#{bias.to_span.ll}"
539
+ end
540
+ end
541
+
542
+ def self.xml2GeoLoc(xml, address="")
543
+ doc=REXML::Document.new(xml)
544
+
545
+ if doc.elements['//kml/Response/Status/code'].text == '200'
546
+ geoloc = nil
547
+ # Google can return multiple results as //Placemark elements.
548
+ # iterate through each and extract each placemark as a geoloc
549
+ doc.each_element('//Placemark') do |e|
550
+ extracted_geoloc = extract_placemark(e) # g is now an instance of GeoLoc
551
+ if geoloc.nil?
552
+ # first time through, geoloc is still nil, so we make it the geoloc we just extracted
553
+ geoloc = extracted_geoloc
554
+ else
555
+ # second (and subsequent) iterations, we push additional
556
+ # geolocs onto "geoloc.all"
557
+ geoloc.all.push(extracted_geoloc)
558
+ end
559
+ end
560
+ return geoloc
561
+ elsif doc.elements['//kml/Response/Status/code'].text == '620'
562
+ raise Geokit::TooManyQueriesError
563
+ else
564
+ logger.info "Google was unable to geocode address: "+address
565
+ return GeoLoc.new
566
+ end
567
+
568
+ rescue Geokit::TooManyQueriesError
569
+ # re-raise because of other rescue
570
+ raise Geokit::TooManyQueriesError, "Google returned a 620 status, too many queries. The given key has gone over the requests limit in the 24 hour period or has submitted too many requests in too short a period of time. If you're sending multiple requests in parallel or in a tight loop, use a timer or pause in your code to make sure you don't send the requests too quickly."
571
+ rescue
572
+ logger.error "Caught an error during Google geocoding call: "+$!
573
+ return GeoLoc.new
574
+ end
575
+
576
+ # extracts a single geoloc from a //placemark element in the google results xml
577
+ def self.extract_placemark(doc)
578
+ res = GeoLoc.new
579
+ coordinates=doc.elements['.//coordinates'].text.to_s.split(',')
580
+
581
+ #basics
582
+ res.lat=coordinates[1]
583
+ res.lng=coordinates[0]
584
+ res.country_code=doc.elements['.//CountryNameCode'].text if doc.elements['.//CountryNameCode']
585
+ res.provider='google'
586
+
587
+ #extended -- false if not not available
588
+ res.city = doc.elements['.//LocalityName'].text if doc.elements['.//LocalityName']
589
+ res.state = doc.elements['.//AdministrativeAreaName'].text if doc.elements['.//AdministrativeAreaName']
590
+ res.province = doc.elements['.//SubAdministrativeAreaName'].text if doc.elements['.//SubAdministrativeAreaName']
591
+ res.full_address = doc.elements['.//address'].text if doc.elements['.//address'] # google provides it
592
+ res.zip = doc.elements['.//PostalCodeNumber'].text if doc.elements['.//PostalCodeNumber']
593
+ res.street_address = doc.elements['.//ThoroughfareName'].text if doc.elements['.//ThoroughfareName']
594
+ res.country = doc.elements['.//CountryName'].text if doc.elements['.//CountryName']
595
+ res.district = doc.elements['.//DependentLocalityName'].text if doc.elements['.//DependentLocalityName']
596
+ # Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country
597
+ # For Google, 1=low accuracy, 8=high accuracy
598
+ address_details=doc.elements['.//*[local-name() = "AddressDetails"]']
599
+ res.accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0
600
+ res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy]
601
+
602
+ # google returns a set of suggested boundaries for the geocoded result
603
+ if suggested_bounds = doc.elements['//LatLonBox']
604
+ res.suggested_bounds = Bounds.normalize(
605
+ [suggested_bounds.attributes['south'], suggested_bounds.attributes['west']],
606
+ [suggested_bounds.attributes['north'], suggested_bounds.attributes['east']])
607
+ end
608
+
609
+ res.success=true
610
+
611
+ return res
612
+ end
613
+ end
614
+
615
+
616
+ # -------------------------------------------------------------------------------------------
617
+ # IP Geocoders
618
+ # -------------------------------------------------------------------------------------------
619
+
620
+ # Provides geocoding based upon an IP address. The underlying web service is geoplugin.net
621
+ class GeoPluginGeocoder < Geocoder
622
+ private
623
+
624
+ def self.do_geocode(ip, options = {})
625
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
626
+ response = self.call_geocoder_service("http://www.geoplugin.net/xml.gp?ip=#{ip}")
627
+ return response.is_a?(Net::HTTPSuccess) ? parse_xml(response.body) : GeoLoc.new
628
+ rescue
629
+ logger.error "Caught an error during GeoPluginGeocoder geocoding call: "+$!
630
+ return GeoLoc.new
631
+ end
632
+
633
+ def self.parse_xml(xml)
634
+ xml = REXML::Document.new(xml)
635
+ geo = GeoLoc.new
636
+ geo.provider='geoPlugin'
637
+ geo.city = xml.elements['//geoplugin_city'].text
638
+ geo.state = xml.elements['//geoplugin_region'].text
639
+ geo.country_code = xml.elements['//geoplugin_countryCode'].text
640
+ geo.lat = xml.elements['//geoplugin_latitude'].text.to_f
641
+ geo.lng = xml.elements['//geoplugin_longitude'].text.to_f
642
+ geo.success = !!geo.city && !geo.city.empty?
643
+ return geo
644
+ end
645
+ end
646
+
647
+ # Provides geocoding based upon an IP address. The underlying web service is a hostip.info
648
+ # which sources their data through a combination of publicly available information as well
649
+ # as community contributions.
650
+ class IpGeocoder < Geocoder
651
+
652
+ # A number of non-routable IP ranges.
653
+ #
654
+ # --
655
+ # Sources for these:
656
+ # RFC 3330: Special-Use IPv4 Addresses
657
+ # The bogon list: http://www.cymru.com/Documents/bogon-list.html
658
+
659
+ NON_ROUTABLE_IP_RANGES = [
660
+ IPAddr.new('0.0.0.0/8'), # "This" Network
661
+ IPAddr.new('10.0.0.0/8'), # Private-Use Networks
662
+ IPAddr.new('14.0.0.0/8'), # Public-Data Networks
663
+ IPAddr.new('127.0.0.0/8'), # Loopback
664
+ IPAddr.new('169.254.0.0/16'), # Link local
665
+ IPAddr.new('172.16.0.0/12'), # Private-Use Networks
666
+ IPAddr.new('192.0.2.0/24'), # Test-Net
667
+ IPAddr.new('192.168.0.0/16'), # Private-Use Networks
668
+ IPAddr.new('198.18.0.0/15'), # Network Interconnect Device Benchmark Testing
669
+ IPAddr.new('224.0.0.0/4'), # Multicast
670
+ IPAddr.new('240.0.0.0/4') # Reserved for future use
671
+ ].freeze
672
+
673
+ private
674
+
675
+ # Given an IP address, returns a GeoLoc instance which contains latitude,
676
+ # longitude, city, and country code. Sets the success attribute to false if the ip
677
+ # parameter does not match an ip address.
678
+ def self.do_geocode(ip, options = {})
679
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
680
+ return GeoLoc.new if self.private_ip_address?(ip)
681
+ url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true"
682
+ response = self.call_geocoder_service(url)
683
+ response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new
684
+ rescue
685
+ logger.error "Caught an error during HostIp geocoding call: "+$!
686
+ return GeoLoc.new
687
+ end
688
+
689
+ # Converts the body to YAML since its in the form of:
690
+ #
691
+ # Country: UNITED STATES (US)
692
+ # City: Sugar Grove, IL
693
+ # Latitude: 41.7696
694
+ # Longitude: -88.4588
695
+ #
696
+ # then instantiates a GeoLoc instance to populate with location data.
697
+ def self.parse_body(body) # :nodoc:
698
+ yaml = YAML.load(body)
699
+ res = GeoLoc.new
700
+ res.provider = 'hostip'
701
+ res.city, res.state = yaml['City'].split(', ')
702
+ country, res.country_code = yaml['Country'].split(' (')
703
+ res.lat = yaml['Latitude']
704
+ res.lng = yaml['Longitude']
705
+ res.country_code.chop!
706
+ res.success = !(res.city =~ /\(.+\)/)
707
+ res
708
+ end
709
+
710
+ # Checks whether the IP address belongs to a private address range.
711
+ #
712
+ # This function is used to reduce the number of useless queries made to
713
+ # the geocoding service. Such queries can occur frequently during
714
+ # integration tests.
715
+ def self.private_ip_address?(ip)
716
+ return NON_ROUTABLE_IP_RANGES.any? { |range| range.include?(ip) }
717
+ end
718
+ end
719
+
720
+ # -------------------------------------------------------------------------------------------
721
+ # The Multi Geocoder
722
+ # -------------------------------------------------------------------------------------------
723
+
724
+ # Provides methods to geocode with a variety of geocoding service providers, plus failover
725
+ # among providers in the order you configure. When 2nd parameter is set 'true', perform
726
+ # ip location lookup with 'address' as the ip address.
727
+ #
728
+ # Goal:
729
+ # - homogenize the results of multiple geocoders
730
+ #
731
+ # Limitations:
732
+ # - currently only provides the first result. Sometimes geocoders will return multiple results.
733
+ # - currently discards the "accuracy" component of the geocoding calls
734
+ class MultiGeocoder < Geocoder
735
+
736
+ private
737
+ # This method will call one or more geocoders in the order specified in the
738
+ # configuration until one of the geocoders work.
739
+ #
740
+ # The failover approach is crucial for production-grade apps, but is rarely used.
741
+ # 98% of your geocoding calls will be successful with the first call
742
+ def self.do_geocode(address, options = {})
743
+ geocode_ip = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.match(address)
744
+ provider_order = geocode_ip ? Geokit::Geocoders::ip_provider_order : Geokit::Geocoders::provider_order
745
+
746
+ provider_order.each do |provider|
747
+ begin
748
+ klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder"
749
+ res = klass.send :geocode, address, options
750
+ return res if res.success?
751
+ rescue
752
+ 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}")
753
+ end
754
+ end
755
+ # If we get here, we failed completely.
756
+ GeoLoc.new
757
+ end
758
+
759
+ # This method will call one or more geocoders in the order specified in the
760
+ # configuration until one of the geocoders work, only this time it's going
761
+ # to try to reverse geocode a geographical point.
762
+ def self.do_reverse_geocode(latlng)
763
+ Geokit::Geocoders::provider_order.each do |provider|
764
+ begin
765
+ klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder"
766
+ res = klass.send :reverse_geocode, latlng
767
+ return res if res.success?
768
+ rescue
769
+ logger.error("Something has gone very wrong during reverse geocoding, OR you have configured an invalid class name in Geokit::Geocoders::provider_order. LatLng: #{latlng}. Provider: #{provider}")
770
+ end
771
+ end
772
+ # If we get here, we failed completely.
773
+ GeoLoc.new
774
+ end
775
+ end
776
+ end
777
+ end