eugenebolshakov-geokit 1.5.0

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