bjeanes-geokit 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,708 @@
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
+ private
339
+
340
+ # Template method which does the geocode lookup.
341
+ def self.do_geocode(address, options = {})
342
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
343
+ # geonames need a space seperated search string
344
+ address_str.gsub!(/,/, " ")
345
+ params = "/postalCodeSearch?placename=#{Geokit::Inflector::url_escape(address_str)}&maxRows=10"
346
+
347
+ if(GeoKit::Geocoders::geonames)
348
+ url = "http://ws.geonames.net#{params}&username=#{GeoKit::Geocoders::geonames}"
349
+ else
350
+ url = "http://ws.geonames.org#{params}"
351
+ end
352
+
353
+ res = self.call_geocoder_service(url)
354
+
355
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
356
+
357
+ xml=res.body
358
+ logger.debug "Geonames geocoding. Address: #{address}. Result: #{xml}"
359
+ doc=REXML::Document.new(xml)
360
+
361
+ if(doc.elements['//geonames/totalResultsCount'].text.to_i > 0)
362
+ res=GeoLoc.new
363
+
364
+ # only take the first result
365
+ res.lat=doc.elements['//code/lat'].text if doc.elements['//code/lat']
366
+ res.lng=doc.elements['//code/lng'].text if doc.elements['//code/lng']
367
+ res.country_code=doc.elements['//code/countryCode'].text if doc.elements['//code/countryCode']
368
+ res.provider='genomes'
369
+ res.city=doc.elements['//code/name'].text if doc.elements['//code/name']
370
+ res.state=doc.elements['//code/adminName1'].text if doc.elements['//code/adminName1']
371
+ res.zip=doc.elements['//code/postalcode'].text if doc.elements['//code/postalcode']
372
+ res.success=true
373
+ return res
374
+ else
375
+ logger.info "Geonames was unable to geocode address: "+address
376
+ return GeoLoc.new
377
+ end
378
+
379
+ rescue
380
+ logger.error "Caught an error during Geonames geocoding call: "+$!
381
+ end
382
+ end
383
+
384
+ # -------------------------------------------------------------------------------------------
385
+ # Address geocoders that also provide reverse geocoding
386
+ # -------------------------------------------------------------------------------------------
387
+
388
+ # Google geocoder implementation. Requires the Geokit::Geocoders::GOOGLE variable to
389
+ # contain a Google API key. Conforms to the interface set by the Geocoder class.
390
+ class GoogleGeocoder < Geocoder
391
+
392
+ private
393
+
394
+ # Template method which does the reverse-geocode lookup.
395
+ def self.do_reverse_geocode(latlng)
396
+ latlng=LatLng.normalize(latlng)
397
+ 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")
398
+ # 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"))
399
+ return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
400
+ xml = res.body
401
+ logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{xml}"
402
+ return self.xml2GeoLoc(xml)
403
+ end
404
+
405
+ # Template method which does the geocode lookup.
406
+ #
407
+ # Supports viewport/country code biasing
408
+ #
409
+ # ==== OPTIONS
410
+ # * :bias - This option makes the Google Geocoder return results biased to a particular
411
+ # country or viewport. Country code biasing is achieved by passing the ccTLD
412
+ # ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's,
413
+ # look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder
414
+ # will be biased to results within the US (ccTLD .com).
415
+ #
416
+ # If you'd like the Google Geocoder to prefer results within a given viewport,
417
+ # you can pass a Geokit::Bounds object as the :bias value.
418
+ #
419
+ # ==== EXAMPLES
420
+ # # By default, the geocoder will return Syracuse, NY
421
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse').country_code # => 'US'
422
+ # # With country code biasing, it returns Syracuse in Sicily, Italy
423
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse', :bias => :it).country_code # => 'IT'
424
+ #
425
+ # # By default, the geocoder will return Winnetka, IL
426
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka').state # => 'IL'
427
+ # # When biased to an bounding box around California, it will now return the Winnetka neighbourhood, CA
428
+ # bounds = Geokit::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487])
429
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka', :bias => bounds).state # => 'CA'
430
+ def self.do_geocode(address, options = {})
431
+ bias_str = options[:bias] ? construct_bias_string_from_options(options[:bias]) : ''
432
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
433
+ 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")
434
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
435
+ xml = res.body
436
+ logger.debug "Google geocoding. Address: #{address}. Result: #{xml}"
437
+ return self.xml2GeoLoc(xml, address)
438
+ end
439
+
440
+ def self.construct_bias_string_from_options(bias)
441
+ if bias.is_a?(String) or bias.is_a?(Symbol)
442
+ # country code biasing
443
+ "&gl=#{bias.to_s.downcase}"
444
+ elsif bias.is_a?(Bounds)
445
+ # viewport biasing
446
+ "&ll=#{bias.center.ll}&spn=#{bias.to_span.ll}"
447
+ end
448
+ end
449
+
450
+ def self.xml2GeoLoc(xml, address="")
451
+ doc=REXML::Document.new(xml)
452
+
453
+ if doc.elements['//kml/Response/Status/code'].text == '200'
454
+ geoloc = nil
455
+ # Google can return multiple results as //Placemark elements.
456
+ # iterate through each and extract each placemark as a geoloc
457
+ doc.each_element('//Placemark') do |e|
458
+ extracted_geoloc = extract_placemark(e) # g is now an instance of GeoLoc
459
+ if geoloc.nil?
460
+ # first time through, geoloc is still nil, so we make it the geoloc we just extracted
461
+ geoloc = extracted_geoloc
462
+ else
463
+ # second (and subsequent) iterations, we push additional
464
+ # geolocs onto "geoloc.all"
465
+ geoloc.all.push(extracted_geoloc)
466
+ end
467
+ end
468
+ return geoloc
469
+ elsif doc.elements['//kml/Response/Status/code'].text == '620'
470
+ raise Geokit::TooManyQueriesError
471
+ else
472
+ logger.info "Google was unable to geocode address: "+address
473
+ return GeoLoc.new
474
+ end
475
+
476
+ rescue Geokit::TooManyQueriesError
477
+ # re-raise because of other rescue
478
+ 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."
479
+ rescue
480
+ logger.error "Caught an error during Google geocoding call: "+$!
481
+ return GeoLoc.new
482
+ end
483
+
484
+ # extracts a single geoloc from a //placemark element in the google results xml
485
+ def self.extract_placemark(doc)
486
+ res = GeoLoc.new
487
+ coordinates=doc.elements['.//coordinates'].text.to_s.split(',')
488
+
489
+ #basics
490
+ res.lat=coordinates[1]
491
+ res.lng=coordinates[0]
492
+ res.country_code=doc.elements['.//CountryNameCode'].text if doc.elements['.//CountryNameCode']
493
+ res.provider='google'
494
+
495
+ #extended -- false if not not available
496
+ res.city = doc.elements['.//LocalityName'].text if doc.elements['.//LocalityName']
497
+ res.state = doc.elements['.//AdministrativeAreaName'].text if doc.elements['.//AdministrativeAreaName']
498
+ res.province = doc.elements['.//SubAdministrativeAreaName'].text if doc.elements['.//SubAdministrativeAreaName']
499
+ res.full_address = doc.elements['.//address'].text if doc.elements['.//address'] # google provides it
500
+ res.zip = doc.elements['.//PostalCodeNumber'].text if doc.elements['.//PostalCodeNumber']
501
+ res.street_address = doc.elements['.//ThoroughfareName'].text if doc.elements['.//ThoroughfareName']
502
+ res.country = doc.elements['.//CountryName'].text if doc.elements['.//CountryName']
503
+ res.district = doc.elements['.//DependentLocalityName'].text if doc.elements['.//DependentLocalityName']
504
+ # Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country
505
+ # For Google, 1=low accuracy, 8=high accuracy
506
+ address_details=doc.elements['.//*[local-name() = "AddressDetails"]']
507
+ res.accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0
508
+ res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy]
509
+
510
+ # google returns a set of suggested boundaries for the geocoded result
511
+ if suggested_bounds = doc.elements['//LatLonBox']
512
+ res.suggested_bounds = Bounds.normalize(
513
+ [suggested_bounds.attributes['south'], suggested_bounds.attributes['west']],
514
+ [suggested_bounds.attributes['north'], suggested_bounds.attributes['east']])
515
+ end
516
+
517
+ res.success=true
518
+
519
+ return res
520
+ end
521
+ end
522
+
523
+
524
+ # -------------------------------------------------------------------------------------------
525
+ # IP Geocoders
526
+ # -------------------------------------------------------------------------------------------
527
+
528
+ # Provides geocoding based upon an IP address. The underlying web service is geoplugin.net
529
+ class GeoPluginGeocoder < Geocoder
530
+ private
531
+
532
+ def self.do_geocode(ip, options = {})
533
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
534
+ response = self.call_geocoder_service("http://www.geoplugin.net/xml.gp?ip=#{ip}")
535
+ return response.is_a?(Net::HTTPSuccess) ? parse_xml(response.body) : GeoLoc.new
536
+ rescue
537
+ logger.error "Caught an error during GeoPluginGeocoder geocoding call: "+$!
538
+ return GeoLoc.new
539
+ end
540
+
541
+ def self.parse_xml(xml)
542
+ xml = REXML::Document.new(xml)
543
+ geo = GeoLoc.new
544
+ geo.provider='geoPlugin'
545
+ geo.city = xml.elements['//geoplugin_city'].text
546
+ geo.state = xml.elements['//geoplugin_region'].text
547
+ geo.country_code = xml.elements['//geoplugin_countryCode'].text
548
+ geo.lat = xml.elements['//geoplugin_latitude'].text.to_f
549
+ geo.lng = xml.elements['//geoplugin_longitude'].text.to_f
550
+ geo.success = !!geo.city && !geo.city.empty?
551
+ return geo
552
+ end
553
+ end
554
+
555
+ # Provides geocoding based upon an IP address. The underlying web service is a hostip.info
556
+ # which sources their data through a combination of publicly available information as well
557
+ # as community contributions.
558
+ class IpGeocoder < Geocoder
559
+
560
+ # A number of non-routable IP ranges.
561
+ #
562
+ # --
563
+ # Sources for these:
564
+ # RFC 3330: Special-Use IPv4 Addresses
565
+ # The bogon list: http://www.cymru.com/Documents/bogon-list.html
566
+
567
+ NON_ROUTABLE_IP_RANGES = [
568
+ IPAddr.new('0.0.0.0/8'), # "This" Network
569
+ IPAddr.new('10.0.0.0/8'), # Private-Use Networks
570
+ IPAddr.new('14.0.0.0/8'), # Public-Data Networks
571
+ IPAddr.new('127.0.0.0/8'), # Loopback
572
+ IPAddr.new('169.254.0.0/16'), # Link local
573
+ IPAddr.new('172.16.0.0/12'), # Private-Use Networks
574
+ IPAddr.new('192.0.2.0/24'), # Test-Net
575
+ IPAddr.new('192.168.0.0/16'), # Private-Use Networks
576
+ IPAddr.new('198.18.0.0/15'), # Network Interconnect Device Benchmark Testing
577
+ IPAddr.new('224.0.0.0/4'), # Multicast
578
+ IPAddr.new('240.0.0.0/4') # Reserved for future use
579
+ ].freeze
580
+
581
+ private
582
+
583
+ # Given an IP address, returns a GeoLoc instance which contains latitude,
584
+ # longitude, city, and country code. Sets the success attribute to false if the ip
585
+ # parameter does not match an ip address.
586
+ def self.do_geocode(ip, options = {})
587
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
588
+ return GeoLoc.new if self.private_ip_address?(ip)
589
+ url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true"
590
+ response = self.call_geocoder_service(url)
591
+ response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new
592
+ rescue
593
+ logger.error "Caught an error during HostIp geocoding call: "+$!
594
+ return GeoLoc.new
595
+ end
596
+
597
+ # Converts the body to YAML since its in the form of:
598
+ #
599
+ # Country: UNITED STATES (US)
600
+ # City: Sugar Grove, IL
601
+ # Latitude: 41.7696
602
+ # Longitude: -88.4588
603
+ #
604
+ # then instantiates a GeoLoc instance to populate with location data.
605
+ def self.parse_body(body) # :nodoc:
606
+ yaml = YAML.load(body)
607
+ res = GeoLoc.new
608
+ res.provider = 'hostip'
609
+ res.city, res.state = yaml['City'].split(', ')
610
+ country, res.country_code = yaml['Country'].split(' (')
611
+ res.lat = yaml['Latitude']
612
+ res.lng = yaml['Longitude']
613
+ res.country_code.chop!
614
+ res.success = !(res.city =~ /\(.+\)/)
615
+ res
616
+ end
617
+
618
+ # Checks whether the IP address belongs to a private address range.
619
+ #
620
+ # This function is used to reduce the number of useless queries made to
621
+ # the geocoding service. Such queries can occur frequently during
622
+ # integration tests.
623
+ def self.private_ip_address?(ip)
624
+ return NON_ROUTABLE_IP_RANGES.any? { |range| range.include?(ip) }
625
+ end
626
+ end
627
+
628
+ # -------------------------------------------------------------------------------------------
629
+ # The Multi Geocoder
630
+ # -------------------------------------------------------------------------------------------
631
+
632
+ # Provides methods to geocode with a variety of geocoding service providers, plus failover
633
+ # among providers in the order you configure. When 2nd parameter is set 'true', perform
634
+ # ip location lookup with 'address' as the ip address.
635
+ #
636
+ # Goal:
637
+ # - homogenize the results of multiple geocoders
638
+ #
639
+ # Limitations:
640
+ # - currently only provides the first result. Sometimes geocoders will return multiple results.
641
+ # - currently discards the "accuracy" component of the geocoding calls
642
+ class MultiGeocoder < Geocoder
643
+
644
+ private
645
+
646
+ # This is taken from http://github.com/bluemonk/ipaddress/blob/master/lib/ipaddress/ipbase.rb
647
+ # TODO: Make GeoKit use the ipaddress gem to get these support functions
648
+ def self.valid_ipv4?(addr)
649
+ if /\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\Z/ =~ addr
650
+ return $~.captures.all? {|i| i.to_i < 256}
651
+ end
652
+ false
653
+ end
654
+
655
+ def self.valid_ipv6?(addr)
656
+ # IPv6 (normal)
657
+ return true if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*\Z/ =~ addr
658
+ return true if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*)?\Z/ =~ addr
659
+ return true if /\A::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*)?\Z/ =~ addr
660
+ # IPv6 (IPv4 compat)
661
+ return true if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*:/ =~ addr && valid_ipv4?($')
662
+ return true if /\A[\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*:)?/ =~ addr && valid_ipv4?($')
663
+ return true if /\A::([\dA-Fa-f]{1,4}(:[\dA-Fa-f]{1,4})*:)?/ =~ addr && valid_ipv4?($')
664
+ false
665
+ end
666
+
667
+ # This method will call one or more geocoders in the order specified in the
668
+ # configuration until one of the geocoders work.
669
+ #
670
+ # The failover approach is crucial for production-grade apps, but is rarely used.
671
+ # 98% of your geocoding calls will be successful with the first call
672
+ def self.do_geocode(address, options = {})
673
+ geocode_ip = valid_ipv4?(address) || valid_ipv6?(address)
674
+
675
+ provider_order = geocode_ip ? Geokit::Geocoders::ip_provider_order : Geokit::Geocoders::provider_order
676
+
677
+ provider_order.each do |provider|
678
+ begin
679
+ klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder"
680
+ res = klass.send :geocode, address, options
681
+ return res if res.success?
682
+ rescue NameError => e
683
+ 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}")
684
+ end
685
+ end
686
+ # If we get here, we failed completely.
687
+ GeoLoc.new
688
+ end
689
+
690
+ # This method will call one or more geocoders in the order specified in the
691
+ # configuration until one of the geocoders work, only this time it's going
692
+ # to try to reverse geocode a geographical point.
693
+ def self.do_reverse_geocode(latlng)
694
+ Geokit::Geocoders::provider_order.each do |provider|
695
+ begin
696
+ klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder"
697
+ res = klass.send :reverse_geocode, latlng
698
+ return res if res.success?
699
+ rescue NameError => e
700
+ 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}")
701
+ end
702
+ end
703
+ # If we get here, we failed completely.
704
+ GeoLoc.new
705
+ end
706
+ end
707
+ end
708
+ end