loco2-geokit 0.0.1

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