kaupert 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Abhishek Gupta
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,50 @@
1
+ = kaupert
2
+
3
+ Description goes here.
4
+ How to use the Kaupert gem?
5
+
6
+ gem install json/pure
7
+ gem install kaupert
8
+
9
+ Once both the gems are installed. The gem can be tested as following:-
10
+
11
+ Open up the Interactive Ruby Terminal
12
+ irb> require 'rubygems'
13
+ irb> require 'kaupert'
14
+
15
+ Create the object for first address using reverse geocoding i.e through latitude and longitude:-
16
+ irb> a= Kaupert::Geocoders::GoogleGeocoder3.reverse_geocode("49.93162,8.64288")
17
+
18
+ Here the lat and long corresponds to Rebusgasse 3, 64291 Darmstadt, Germany
19
+
20
+ To find out whether call was successfull :
21
+ irb> a.success
22
+ => true
23
+
24
+ Create the object for the second address based on address
25
+ irb>b= Kaupert::Geocoders::GoogleGeocoder3.geocode("Untergasse 1,64291 Darmstadt, Germany")
26
+ irb>b.ll
27
+ =>"49.9334,8.64154"
28
+ To check whether the geocoding was successfull
29
+ irb> b.success
30
+ => true
31
+
32
+ To calculate the distance between a and b in miles:
33
+ irb(main):042:0> a.distance_to(b)
34
+ => 0.136817906674354
35
+
36
+ == Contributing to kaupert
37
+
38
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
39
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
40
+ * Fork the project
41
+ * Start a feature/bugfix branch
42
+ * Commit and push until you are happy with your contribution
43
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
44
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
45
+
46
+ == Copyright
47
+
48
+ Copyright (c) 2011 Abhishek Gupta. See LICENSE.txt for
49
+ further details.
50
+
data/lib/kaupert.rb ADDED
@@ -0,0 +1,28 @@
1
+ module Kaupert
2
+
3
+ # These defaults are used in Kaupert::Mapcal.distance_to
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 'kaupert/geocoding'
27
+ require 'kaupert/mapcal'
28
+
@@ -0,0 +1,567 @@
1
+ require 'net/http'
2
+ require 'ipaddr'
3
+ require 'rexml/document'
4
+ require 'yaml'
5
+ require 'timeout'
6
+ require 'logger'
7
+
8
+ # do this just in case
9
+ begin
10
+ ActiveSupport.nil?
11
+ rescue NameError
12
+ require 'json/pure'
13
+ end
14
+
15
+ module Kaupert
16
+
17
+ class TooManyQueriesError < StandardError; end
18
+
19
+ module Inflector
20
+
21
+ extend self
22
+
23
+ def titleize(word)
24
+ humanize(underscore(word)).gsub(/\b([a-z])/u) { $1.capitalize }
25
+ end
26
+
27
+ def underscore(camel_cased_word)
28
+ camel_cased_word.to_s.gsub(/::/, '/').
29
+ gsub(/([A-Z]+)([A-Z][a-z])/u,'\1_\2').
30
+ gsub(/([a-z\d])([A-Z])/u,'\1_\2').
31
+ tr("-", "_").
32
+ downcase
33
+ end
34
+
35
+ def humanize(lower_case_and_underscored_word)
36
+ lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
37
+ end
38
+
39
+ def snake_case(s)
40
+ return s.downcase if s =~ /^[A-Z]+$/u
41
+ s.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/u, '_\&') =~ /_*(.*)/
42
+ return $+.downcase
43
+
44
+ end
45
+
46
+ def url_escape(s)
47
+ s.gsub(/([^ a-zA-Z0-9_.-]+)/nu) do
48
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
49
+ end.tr(' ', '+')
50
+ end
51
+
52
+ def camelize(str)
53
+ str.split('_').map {|w| w.capitalize}.join
54
+ end
55
+ end
56
+
57
+ # Contains a range of geocoders:
58
+ #
59
+ # ### "regular" address geocoders
60
+ # * Yahoo Geocoder - requires an API key.
61
+ #
62
+ #
63
+ # ### address geocoders that also provide reverse geocoding
64
+ # * Google Geocoder - requires an API key.
65
+ #
66
+ # ### IP address geocoders
67
+ # * IP Geocoder - geocodes an IP address using hostip.info's web service.
68
+ # * Geoplugin.net -- another IP address geocoder
69
+ #
70
+ # ### The Multigeocoder
71
+ # * Multi Geocoder - provides failover for the physical location geocoders.
72
+ #
73
+ # Some of these geocoders require configuration. You don't have to provide it here. See the README.
74
+ module Geocoders
75
+ @@proxy_addr = nil
76
+ @@proxy_port = nil
77
+ @@proxy_user = nil
78
+ @@proxy_pass = nil
79
+ @@request_timeout = nil
80
+ @@yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY'
81
+ @@google = 'REPLACE_WITH_YOUR_GOOGLE_KEY'
82
+ @@geocoder_us = false
83
+ @@geocoder_ca = false
84
+ @@geonames = false
85
+ @@provider_order = [:google3]
86
+ @@ip_provider_order = [:geo_plugin,:ip]
87
+ @@logger=Logger.new(STDOUT)
88
+ @@logger.level=Logger::INFO
89
+ @@domain = nil
90
+
91
+ def self.__define_accessors
92
+ class_variables.each do |v|
93
+ sym = v.to_s.delete("@").to_sym
94
+ unless self.respond_to? sym
95
+ module_eval <<-EOS, __FILE__, __LINE__
96
+ def self.#{sym}
97
+ value = if defined?(#{sym.to_s.upcase})
98
+ #{sym.to_s.upcase}
99
+ else
100
+ @@#{sym}
101
+ end
102
+ if value.is_a?(Hash)
103
+ value = (self.domain.nil? ? nil : value[self.domain]) || value.values.first
104
+ end
105
+ value
106
+ end
107
+
108
+ def self.#{sym}=(obj)
109
+ @@#{sym} = obj
110
+ end
111
+ EOS
112
+ end
113
+ end
114
+ end
115
+
116
+ __define_accessors
117
+
118
+ # Error which is thrown in the event a geocoding error occurs.
119
+ class GeocodeError < StandardError; end
120
+
121
+ # -------------------------------------------------------------------------------------------
122
+ # Geocoder Base class -- every geocoder should inherit from this
123
+ # -------------------------------------------------------------------------------------------
124
+
125
+ # The Geocoder base class which defines the interface to be used by all
126
+ # other geocoders.
127
+ class Geocoder
128
+ # Main method which calls the do_geocode template method which subclasses
129
+ # are responsible for implementing. Returns a populated GeoLoc or an
130
+ # empty one with a failed success code.
131
+ def self.geocode(address, options = {})
132
+ res = do_geocode(address, options)
133
+ return res.nil? ? GeoLoc.new : res
134
+ end
135
+ # Main method which calls the do_reverse_geocode template method which subclasses
136
+ # are responsible for implementing. Returns a populated GeoLoc or an
137
+ # empty one with a failed success code.
138
+ def self.reverse_geocode(latlng)
139
+ res = do_reverse_geocode(latlng)
140
+ return res.success? ? res : GeoLoc.new
141
+ end
142
+
143
+ # Call the geocoder service using the timeout if configured.
144
+ def self.call_geocoder_service(url)
145
+ Timeout::timeout(Kaupert::Geocoders::request_timeout) { return self.do_get(url) } if Kaupert::Geocoders::request_timeout
146
+ return self.do_get(url)
147
+ rescue TimeoutError
148
+ return nil
149
+ end
150
+
151
+ # Not all geocoders can do reverse geocoding. So, unless the subclass explicitly overrides this method,
152
+ # a call to reverse_geocode will return an empty GeoLoc. If you happen to be using MultiGeocoder,
153
+ # this will cause it to failover to the next geocoder, which will hopefully be one which supports reverse geocoding.
154
+ def self.do_reverse_geocode(latlng)
155
+ return GeoLoc.new
156
+ end
157
+
158
+ protected
159
+
160
+ def self.logger()
161
+ Kaupert::Geocoders::logger
162
+ end
163
+
164
+ private
165
+
166
+ # Wraps the geocoder call around a proxy if necessary.
167
+ def self.do_get(url)
168
+ uri = URI.parse(url)
169
+ req = Net::HTTP::Get.new(url)
170
+ req.basic_auth(uri.user, uri.password) if uri.userinfo
171
+ res = Net::HTTP::Proxy(Kaupert::Geocoders::proxy_addr,
172
+ Kaupert::Geocoders::proxy_port,
173
+ Kaupert::Geocoders::proxy_user,
174
+ Kaupert::Geocoders::proxy_pass).start(uri.host, uri.port) { |http| http.get(uri.path + "?" + uri.query) }
175
+ return res
176
+ end
177
+
178
+ # Adds subclass' geocode method making it conveniently available through
179
+ # the base class.
180
+ def self.inherited(clazz)
181
+ class_name = clazz.name.split('::').last
182
+ src = <<-END_SRC
183
+ def self.#{Kaupert::Inflector.underscore(class_name)}(address, options = {})
184
+ #{class_name}.geocode(address, options)
185
+ end
186
+ END_SRC
187
+ class_eval(src)
188
+ end
189
+ end
190
+
191
+ # -------------------------------------------------------------------------------------------
192
+ # "Regular" Address geocoders
193
+ # -------------------------------------------------------------------------------------------
194
+
195
+
196
+
197
+
198
+
199
+ # Yahoo geocoder implementation. Requires the Kaupert::Geocoders::YAHOO variable to
200
+ # contain a Yahoo API key. Conforms to the interface set by the Geocoder class.
201
+ class YahooGeocoder < Geocoder
202
+
203
+ private
204
+
205
+ # Template method which does the geocode lookup.
206
+ def self.do_geocode(address, options = {})
207
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
208
+ url="http://api.local.yahoo.com/MapsService/V1/geocode?appid=#{Kaupert::Geocoders::yahoo}&location=#{Kaupert::Inflector::url_escape(address_str)}"
209
+ res = self.call_geocoder_service(url)
210
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
211
+ xml = res.body
212
+ doc = REXML::Document.new(xml)
213
+ logger.debug "Yahoo geocoding. Address: #{address}. Result: #{xml}"
214
+
215
+ if doc.elements['//ResultSet']
216
+ res=GeoLoc.new
217
+
218
+ #basic
219
+ res.lat=doc.elements['//Latitude'].text
220
+ res.lng=doc.elements['//Longitude'].text
221
+ res.country_code=doc.elements['//Country'].text
222
+ res.provider='yahoo'
223
+
224
+ #extended - false if not available
225
+ res.city=doc.elements['//City'].text if doc.elements['//City'] && doc.elements['//City'].text != nil
226
+ res.state=doc.elements['//State'].text if doc.elements['//State'] && doc.elements['//State'].text != nil
227
+ res.zip=doc.elements['//Zip'].text if doc.elements['//Zip'] && doc.elements['//Zip'].text != nil
228
+ res.street_address=doc.elements['//Address'].text if doc.elements['//Address'] && doc.elements['//Address'].text != nil
229
+ res.precision=doc.elements['//Result'].attributes['precision'] if doc.elements['//Result']
230
+ # set the accuracy as google does (added by Andruby)
231
+ res.accuracy=%w{unknown country state state city zip zip+4 street address building}.index(res.precision)
232
+ res.success=true
233
+ return res
234
+ else
235
+ logger.info "Yahoo was unable to geocode address: "+address
236
+ return GeoLoc.new
237
+ end
238
+
239
+ rescue
240
+ logger.info "Caught an error during Yahoo geocoding call: "+$!
241
+ return GeoLoc.new
242
+ end
243
+ end
244
+
245
+
246
+
247
+ class GoogleGeocoder3 < Geocoder
248
+
249
+ private
250
+ # Template method which does the reverse-geocode lookup.
251
+ def self.do_reverse_geocode(latlng)
252
+ latlng=LatLng.normalize(latlng)
253
+ res = self.call_geocoder_service("http://maps.google.com/maps/api/geocode/json?sensor=false&latlng=#{Kaupert::Inflector::url_escape(latlng.ll)}")
254
+ return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
255
+ json = res.body
256
+ logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{json}"
257
+ return self.json2GeoLoc(json)
258
+ end
259
+
260
+ # Template method which does the geocode lookup.
261
+ #
262
+ # Supports viewport/country code biasing
263
+ #
264
+ # ==== OPTIONS
265
+ # * :bias - This option makes the Google Geocoder return results biased to a particular
266
+ # country or viewport. Country code biasing is achieved by passing the ccTLD
267
+ # ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's,
268
+ # look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder
269
+ # will be biased to results within the US (ccTLD .com).
270
+ #
271
+ # If you'd like the Google Geocoder to prefer results within a given viewport,
272
+ # you can pass a Kaupert::Bounds object as the :bias value.
273
+ #
274
+ # ==== EXAMPLES
275
+ # # By default, the geocoder will return Syracuse, NY
276
+ # Kaupert::Geocoders::GoogleGeocoder.geocode('Syracuse').country_code # => 'US'
277
+ # # With country code biasing, it returns Syracuse in Sicily, Italy
278
+ # Kaupert::Geocoders::GoogleGeocoder.geocode('Syracuse', :bias => :it).country_code # => 'IT'
279
+ #
280
+ # # By default, the geocoder will return Winnetka, IL
281
+ # Kaupert::Geocoders::GoogleGeocoder.geocode('Winnetka').state # => 'IL'
282
+ # # When biased to an bounding box around California, it will now return the Winnetka neighbourhood, CA
283
+ # bounds = Kaupert::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487])
284
+ # Kaupert::Geocoders::GoogleGeocoder.geocode('Winnetka', :bias => bounds).state # => 'CA'
285
+ def self.do_geocode(address, options = {})
286
+ bias_str = options[:bias] ? construct_bias_string_from_options(options[:bias]) : ''
287
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
288
+ res = self.call_geocoder_service("http://maps.google.com/maps/api/geocode/json?sensor=false&address=#{Kaupert::Inflector::url_escape(address_str)}#{bias_str}")
289
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
290
+ json = res.body
291
+ logger.debug "Google geocoding. Address: #{address}. Result: #{json}"
292
+ return self.json2GeoLoc(json, address)
293
+ end
294
+
295
+ def self.construct_bias_string_from_options(bias)
296
+ if bias.is_a?(String) or bias.is_a?(Symbol)
297
+ # country code biasing
298
+ "&region=#{bias.to_s.downcase}"
299
+ elsif bias.is_a?(Bounds)
300
+ # viewport biasing
301
+ Kaupert::Inflector::url_escape("&bounds=#{bias.sw.to_s}|#{bias.ne.to_s}")
302
+ end
303
+ end
304
+
305
+ def self.json2GeoLoc(json, address="")
306
+ ret=nil
307
+ begin
308
+ results=::ActiveSupport::JSON.decode(json)
309
+ rescue NameError => e
310
+ results=JSON.parse(json)
311
+ end
312
+
313
+
314
+ if results['status'] == 'OVER_QUERY_LIMIT'
315
+ raise Kaupert::TooManyQueriesError
316
+ end
317
+ if results['status'] == 'ZERO_RESULTS'
318
+ return GeoLoc.new
319
+ end
320
+ # this should probably be smarter.
321
+ if !results['status'] == 'OK'
322
+ raise Kaupert::Geocoders::GeocodeError
323
+ end
324
+ # location_type stores additional data about the specified location.
325
+ # The following values are currently supported:
326
+ # "ROOFTOP" indicates that the returned result is a precise geocode
327
+ # for which we have location information accurate down to street
328
+ # address precision.
329
+ # "RANGE_INTERPOLATED" indicates that the returned result reflects an
330
+ # approximation (usually on a road) interpolated between two precise
331
+ # points (such as intersections). Interpolated results are generally
332
+ # returned when rooftop geocodes are unavailable for a street address.
333
+ # "GEOMETRIC_CENTER" indicates that the returned result is the
334
+ # geometric center of a result such as a polyline (for example, a
335
+ # street) or polygon (region).
336
+ # "APPROXIMATE" indicates that the returned result is approximate
337
+
338
+ # these do not map well. Perhaps we should guess better based on size
339
+ # of bounding box where it exists? Does it really matter?
340
+ accuracy = {
341
+ "ROOFTOP" => 9,
342
+ "RANGE_INTERPOLATED" => 8,
343
+ "GEOMETRIC_CENTER" => 5,
344
+ "APPROXIMATE" => 4
345
+ }
346
+ results['results'].sort_by{|a|accuracy[a['geometry']['location_type']]}.reverse.each do |addr|
347
+ res=GeoLoc.new
348
+ res.provider = 'google3'
349
+ res.success = true
350
+ res.full_address = addr['formatted_address']
351
+ addr['address_components'].each do |comp|
352
+ case
353
+ when comp['types'].include?("street_number")
354
+ res.street_number = comp['short_name']
355
+ when comp['types'].include?("route")
356
+ res.street_name = comp['long_name']
357
+ when comp['types'].include?("locality")
358
+ res.city = comp['long_name']
359
+ when comp['types'].include?("administrative_area_level_1")
360
+ res.state = comp['short_name']
361
+ res.province = comp['short_name']
362
+ when comp['types'].include?("postal_code")
363
+ res.zip = comp['long_name']
364
+ when comp['types'].include?("country")
365
+ res.country_code = comp['short_name']
366
+ res.country = comp['long_name']
367
+ when comp['types'].include?("administrative_area_level_2")
368
+ res.district = comp['long_name']
369
+ end
370
+ end
371
+ if res.street_name
372
+ res.street_address=[res.street_number,res.street_name].join(' ').strip
373
+ end
374
+ res.accuracy = accuracy[addr['geometry']['location_type']]
375
+ res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy]
376
+ # try a few overrides where we can
377
+ if res.street_name && res.precision=='city'
378
+ res.precision = 'street'
379
+ res.accuracy = 7
380
+ end
381
+
382
+ res.lat=addr['geometry']['location']['lat'].to_f
383
+ res.lng=addr['geometry']['location']['lng'].to_f
384
+
385
+ ne=Kaupert::LatLng.new(
386
+ addr['geometry']['viewport']['northeast']['lat'].to_f,
387
+ addr['geometry']['viewport']['northeast']['lng'].to_f
388
+ )
389
+ sw=Kaupert::LatLng.new(
390
+ addr['geometry']['viewport']['southwest']['lat'].to_f,
391
+ addr['geometry']['viewport']['southwest']['lng'].to_f
392
+ )
393
+ res.suggested_bounds = Kaupert::Bounds.new(sw,ne)
394
+
395
+ if ret
396
+ ret.all.push(res)
397
+ else
398
+ ret=res
399
+ end
400
+ end
401
+ return ret
402
+ end
403
+ end
404
+
405
+
406
+ # -------------------------------------------------------------------------------------------
407
+ # IP Geocoders
408
+ # -------------------------------------------------------------------------------------------
409
+
410
+ # Provides geocoding based upon an IP address. The underlying web service is geoplugin.net
411
+ class GeoPluginGeocoder < Geocoder
412
+ private
413
+
414
+ def self.do_geocode(ip, options = {})
415
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
416
+ response = self.call_geocoder_service("http://www.geoplugin.net/xml.gp?ip=#{ip}")
417
+ return response.is_a?(Net::HTTPSuccess) ? parse_xml(response.body) : GeoLoc.new
418
+ rescue
419
+ logger.error "Caught an error during GeoPluginGeocoder geocoding call: "+$!
420
+ return GeoLoc.new
421
+ end
422
+
423
+ def self.parse_xml(xml)
424
+ xml = REXML::Document.new(xml)
425
+ geo = GeoLoc.new
426
+ geo.provider='geoPlugin'
427
+ geo.city = xml.elements['//geoplugin_city'].text
428
+ geo.state = xml.elements['//geoplugin_region'].text
429
+ geo.country_code = xml.elements['//geoplugin_countryCode'].text
430
+ geo.lat = xml.elements['//geoplugin_latitude'].text.to_f
431
+ geo.lng = xml.elements['//geoplugin_longitude'].text.to_f
432
+ geo.success = !!geo.city && !geo.city.empty?
433
+ return geo
434
+ end
435
+ end
436
+
437
+ # Provides geocoding based upon an IP address. The underlying web service is a hostip.info
438
+ # which sources their data through a combination of publicly available information as well
439
+ # as community contributions.
440
+ class IpGeocoder < Geocoder
441
+
442
+ # A number of non-routable IP ranges.
443
+ #
444
+ # --
445
+ # Sources for these:
446
+ # RFC 3330: Special-Use IPv4 Addresses
447
+ # The bogon list: http://www.cymru.com/Documents/bogon-list.html
448
+
449
+ NON_ROUTABLE_IP_RANGES = [
450
+ IPAddr.new('0.0.0.0/8'), # "This" Network
451
+ IPAddr.new('10.0.0.0/8'), # Private-Use Networks
452
+ IPAddr.new('14.0.0.0/8'), # Public-Data Networks
453
+ IPAddr.new('127.0.0.0/8'), # Loopback
454
+ IPAddr.new('169.254.0.0/16'), # Link local
455
+ IPAddr.new('172.16.0.0/12'), # Private-Use Networks
456
+ IPAddr.new('192.0.2.0/24'), # Test-Net
457
+ IPAddr.new('192.168.0.0/16'), # Private-Use Networks
458
+ IPAddr.new('198.18.0.0/15'), # Network Interconnect Device Benchmark Testing
459
+ IPAddr.new('224.0.0.0/4'), # Multicast
460
+ IPAddr.new('240.0.0.0/4') # Reserved for future use
461
+ ].freeze
462
+
463
+ private
464
+
465
+ # Given an IP address, returns a GeoLoc instance which contains latitude,
466
+ # longitude, city, and country code. Sets the success attribute to false if the ip
467
+ # parameter does not match an ip address.
468
+ def self.do_geocode(ip, options = {})
469
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
470
+ return GeoLoc.new if self.private_ip_address?(ip)
471
+ url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true"
472
+ response = self.call_geocoder_service(url)
473
+ response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new
474
+ rescue
475
+ logger.error "Caught an error during HostIp geocoding call: "+$!
476
+ return GeoLoc.new
477
+ end
478
+
479
+ # Converts the body to YAML since its in the form of:
480
+ #
481
+ # Country: UNITED STATES (US)
482
+ # City: Sugar Grove, IL
483
+ # Latitude: 41.7696
484
+ # Longitude: -88.4588
485
+ #
486
+ # then instantiates a GeoLoc instance to populate with location data.
487
+ def self.parse_body(body) # :nodoc:
488
+ yaml = YAML.load(body)
489
+ res = GeoLoc.new
490
+ res.provider = 'hostip'
491
+ res.city, res.state = yaml['City'].split(', ')
492
+ country, res.country_code = yaml['Country'].split(' (')
493
+ res.lat = yaml['Latitude']
494
+ res.lng = yaml['Longitude']
495
+ res.country_code.chop!
496
+ res.success = !(res.city =~ /\(.+\)/)
497
+ res
498
+ end
499
+
500
+ # Checks whether the IP address belongs to a private address range.
501
+ #
502
+ # This function is used to reduce the number of useless queries made to
503
+ # the geocoding service. Such queries can occur frequently during
504
+ # integration tests.
505
+ def self.private_ip_address?(ip)
506
+ return NON_ROUTABLE_IP_RANGES.any? { |range| range.include?(ip) }
507
+ end
508
+ end
509
+
510
+ # -------------------------------------------------------------------------------------------
511
+ # The Multi Geocoder
512
+ # -------------------------------------------------------------------------------------------
513
+
514
+ # Provides methods to geocode with a variety of geocoding service providers, plus failover
515
+ # among providers in the order you configure. When 2nd parameter is set 'true', perform
516
+ # ip location lookup with 'address' as the ip address.
517
+ #
518
+ # Goal:
519
+ # - homogenize the results of multiple geocoders
520
+ #
521
+ # Limitations:
522
+ # - currently only provides the first result. Sometimes geocoders will return multiple results.
523
+ # - currently discards the "accuracy" component of the geocoding calls
524
+ class MultiGeocoder < Geocoder
525
+
526
+ private
527
+ # This method will call one or more geocoders in the order specified in the
528
+ # configuration until one of the geocoders work.
529
+ #
530
+ # The failover approach is crucial for production-grade apps, but is rarely used.
531
+ # 98% of your geocoding calls will be successful with the first call
532
+ def self.do_geocode(address, options = {})
533
+ geocode_ip = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.match(address)
534
+ provider_order = geocode_ip ? Kaupert::Geocoders::ip_provider_order : Kaupert::Geocoders::provider_order
535
+
536
+ provider_order.each do |provider|
537
+ begin
538
+ klass = Kaupert::Geocoders.const_get "#{Kaupert::Inflector::camelize(provider.to_s)}Geocoder"
539
+ res = klass.send :geocode, address, options
540
+ return res if res.success?
541
+ rescue
542
+ logger.error("Something has gone very wrong during geocoding, OR you have configured an invalid class name in Kaupert::Geocoders::provider_order. Address: #{address}. Provider: #{provider}")
543
+ end
544
+ end
545
+ # If we get here, we failed completely.
546
+ GeoLoc.new
547
+ end
548
+
549
+ # This method will call one or more geocoders in the order specified in the
550
+ # configuration until one of the geocoders work, only this time it's going
551
+ # to try to reverse geocode a geographical point.
552
+ def self.do_reverse_geocode(latlng)
553
+ Kaupert::Geocoders::provider_order.each do |provider|
554
+ begin
555
+ klass = Kaupert::Geocoders.const_get "#{Kaupert::Inflector::camelize(provider.to_s)}Geocoder"
556
+ res = klass.send :reverse_geocode, latlng
557
+ return res if res.success?
558
+ rescue
559
+ logger.error("Something has gone very wrong during reverse geocoding, OR you have configured an invalid class name in Kaupert::Geocoders::provider_order. LatLng: #{latlng}. Provider: #{provider}")
560
+ end
561
+ end
562
+ # If we get here, we failed completely.
563
+ GeoLoc.new
564
+ end
565
+ end
566
+ end
567
+ end
@@ -0,0 +1,539 @@
1
+ #require 'forwardable'
2
+
3
+ module Kaupert
4
+ # Contains class and instance methods providing distance calcuation services. This
5
+ # module is meant to be mixed into classes containing lat and lng attributes where
6
+ # distance calculation is desired.
7
+ #
8
+ # At present, two forms of distance calculations are provided:
9
+ #
10
+ # * Pythagorean Theory (flat Earth) - which assumes the world is flat and loses accuracy over long distances.
11
+ # * Haversine (sphere) - which is fairly accurate, but at a performance cost.
12
+ #
13
+ # Distance units supported are :miles, :kms, and :nms.
14
+ module Mapcal
15
+ PI_DIV_RAD = 0.0174
16
+ KMS_PER_MILE = 1.609
17
+ NMS_PER_MILE = 0.868976242
18
+ EARTH_RADIUS_IN_MILES = 3963.19
19
+ EARTH_RADIUS_IN_KMS = EARTH_RADIUS_IN_MILES * KMS_PER_MILE
20
+ EARTH_RADIUS_IN_NMS = EARTH_RADIUS_IN_MILES * NMS_PER_MILE
21
+ MILES_PER_LATITUDE_DEGREE = 69.1
22
+ KMS_PER_LATITUDE_DEGREE = MILES_PER_LATITUDE_DEGREE * KMS_PER_MILE
23
+ NMS_PER_LATITUDE_DEGREE = MILES_PER_LATITUDE_DEGREE * NMS_PER_MILE
24
+ LATITUDE_DEGREES = EARTH_RADIUS_IN_MILES / MILES_PER_LATITUDE_DEGREE
25
+
26
+ # Mix below class methods into the includer.
27
+ def self.included(receiver) # :nodoc:
28
+ receiver.extend ClassMethods
29
+ end
30
+
31
+ module ClassMethods #:nodoc:
32
+ # Returns the distance between two points. The from and to parameters are
33
+ # required to have lat and lng attributes. Valid options are:
34
+ # :units - valid values are :miles, :kms, :nms (Kaupert::default_units is the default)
35
+ # :formula - valid values are :flat or :sphere (Kaupert::default_formula is the default)
36
+ def distance_between(from, to, options={})
37
+ from=Kaupert::LatLng.normalize(from)
38
+ to=Kaupert::LatLng.normalize(to)
39
+ return 0.0 if from == to # fixes a "zero-distance" bug
40
+ units = options[:units] || Kaupert::default_units
41
+ formula = options[:formula] || Kaupert::default_formula
42
+ case formula
43
+ when :sphere
44
+ begin
45
+ units_sphere_multiplier(units) *
46
+ Math.acos( Math.sin(deg2rad(from.lat)) * Math.sin(deg2rad(to.lat)) +
47
+ Math.cos(deg2rad(from.lat)) * Math.cos(deg2rad(to.lat)) *
48
+ Math.cos(deg2rad(to.lng) - deg2rad(from.lng)))
49
+ rescue Errno::EDOM
50
+ 0.0
51
+ end
52
+ when :flat
53
+ Math.sqrt((units_per_latitude_degree(units)*(from.lat-to.lat))**2 +
54
+ (units_per_longitude_degree(from.lat, units)*(from.lng-to.lng))**2)
55
+ end
56
+ end
57
+
58
+ # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
59
+ # from the first point to the second point. Typicaly, the instance methods will be used
60
+ # instead of this method.
61
+ def heading_between(from,to)
62
+ from=Kaupert::LatLng.normalize(from)
63
+ to=Kaupert::LatLng.normalize(to)
64
+
65
+ d_lng=deg2rad(to.lng-from.lng)
66
+ from_lat=deg2rad(from.lat)
67
+ to_lat=deg2rad(to.lat)
68
+ y=Math.sin(d_lng) * Math.cos(to_lat)
69
+ x=Math.cos(from_lat)*Math.sin(to_lat)-Math.sin(from_lat)*Math.cos(to_lat)*Math.cos(d_lng)
70
+ heading=to_heading(Math.atan2(y,x))
71
+ end
72
+
73
+ # Given a start point, distance, and heading (in degrees), provides
74
+ # an endpoint. Returns a LatLng instance. Typically, the instance method
75
+ # will be used instead of this method.
76
+ def endpoint(start,heading, distance, options={})
77
+ units = options[:units] || Kaupert::default_units
78
+ radius = case units
79
+ when :kms; EARTH_RADIUS_IN_KMS
80
+ when :nms; EARTH_RADIUS_IN_NMS
81
+ else EARTH_RADIUS_IN_MILES
82
+ end
83
+ start=Kaupert::LatLng.normalize(start)
84
+ lat=deg2rad(start.lat)
85
+ lng=deg2rad(start.lng)
86
+ heading=deg2rad(heading)
87
+ distance=distance.to_f
88
+
89
+ end_lat=Math.asin(Math.sin(lat)*Math.cos(distance/radius) +
90
+ Math.cos(lat)*Math.sin(distance/radius)*Math.cos(heading))
91
+
92
+ end_lng=lng+Math.atan2(Math.sin(heading)*Math.sin(distance/radius)*Math.cos(lat),
93
+ Math.cos(distance/radius)-Math.sin(lat)*Math.sin(end_lat))
94
+
95
+ LatLng.new(rad2deg(end_lat),rad2deg(end_lng))
96
+ end
97
+
98
+ # Returns the midpoint, given two points. Returns a LatLng.
99
+ # Typically, the instance method will be used instead of this method.
100
+ # Valid option:
101
+ # :units - valid values are :miles, :kms, or :nms (:miles is the default)
102
+ def midpoint_between(from,to,options={})
103
+ from=Kaupert::LatLng.normalize(from)
104
+
105
+ units = options[:units] || Kaupert::default_units
106
+
107
+ heading=from.heading_to(to)
108
+ distance=from.distance_to(to,options)
109
+ midpoint=from.endpoint(heading,distance/2,options)
110
+ end
111
+
112
+ # Geocodes a location using the multi geocoder.
113
+ def geocode(location, options = {})
114
+ res = Geocoders::MultiGeocoder.geocode(location, options)
115
+ return res if res.success?
116
+ raise Kaupert::Geocoders::GeocodeError
117
+ end
118
+
119
+ protected
120
+
121
+ def deg2rad(degrees)
122
+ degrees.to_f / 180.0 * Math::PI
123
+ end
124
+
125
+ def rad2deg(rad)
126
+ rad.to_f * 180.0 / Math::PI
127
+ end
128
+
129
+ def to_heading(rad)
130
+ (rad2deg(rad)+360)%360
131
+ end
132
+
133
+ # Returns the multiplier used to obtain the correct distance units.
134
+ def units_sphere_multiplier(units)
135
+ case units
136
+ when :kms; EARTH_RADIUS_IN_KMS
137
+ when :nms; EARTH_RADIUS_IN_NMS
138
+ else EARTH_RADIUS_IN_MILES
139
+ end
140
+ end
141
+
142
+ # Returns the number of units per latitude degree.
143
+ def units_per_latitude_degree(units)
144
+ case units
145
+ when :kms; KMS_PER_LATITUDE_DEGREE
146
+ when :nms; NMS_PER_LATITUDE_DEGREE
147
+ else MILES_PER_LATITUDE_DEGREE
148
+ end
149
+ end
150
+
151
+ # Returns the number units per longitude degree.
152
+ def units_per_longitude_degree(lat, units)
153
+ miles_per_longitude_degree = (LATITUDE_DEGREES * Math.cos(lat * PI_DIV_RAD)).abs
154
+ case units
155
+ when :kms; miles_per_longitude_degree * KMS_PER_MILE
156
+ when :nms; miles_per_longitude_degree * NMS_PER_MILE
157
+ else miles_per_longitude_degree
158
+ end
159
+ end
160
+ end
161
+
162
+ # -----------------------------------------------------------------------------------------------
163
+ # Instance methods below here
164
+ # -----------------------------------------------------------------------------------------------
165
+
166
+ # Extracts a LatLng instance. Use with models that are acts_as_mappable
167
+ def to_lat_lng
168
+ return self if instance_of?(Kaupert::LatLng) || instance_of?(Kaupert::GeoLoc)
169
+ return LatLng.new(send(self.class.lat_column_name),send(self.class.lng_column_name)) if self.class.respond_to?(:acts_as_mappable)
170
+ nil
171
+ end
172
+
173
+ # Returns the distance from another point. The other point parameter is
174
+ # required to have lat and lng attributes. Valid options are:
175
+ # :units - valid values are :miles, :kms, :or :nms (:miles is the default)
176
+ # :formula - valid values are :flat or :sphere (:sphere is the default)
177
+ def distance_to(other, options={})
178
+ self.class.distance_between(self, other, options)
179
+ end
180
+ alias distance_from distance_to
181
+
182
+ # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
183
+ # to the given point. The given point can be a LatLng or a string to be Geocoded
184
+ def heading_to(other)
185
+ self.class.heading_between(self,other)
186
+ end
187
+
188
+ # Returns heading in degrees (0 is north, 90 is east, 180 is south, etc)
189
+ # FROM the given point. The given point can be a LatLng or a string to be Geocoded
190
+ def heading_from(other)
191
+ self.class.heading_between(other,self)
192
+ end
193
+
194
+ # Returns the endpoint, given a heading (in degrees) and distance.
195
+ # Valid option:
196
+ # :units - valid values are :miles, :kms, or :nms (:miles is the default)
197
+ def endpoint(heading,distance,options={})
198
+ self.class.endpoint(self,heading,distance,options)
199
+ end
200
+
201
+ # Returns the midpoint, given another point on the map.
202
+ # Valid option:
203
+ # :units - valid values are :miles, :kms, or :nms (:miles is the default)
204
+ def midpoint_to(other, options={})
205
+ self.class.midpoint_between(self,other,options)
206
+ end
207
+
208
+ end
209
+
210
+ class LatLng
211
+ include Mapcal
212
+
213
+ attr_accessor :lat, :lng
214
+
215
+ # Accepts latitude and longitude or instantiates an empty instance
216
+ # if lat and lng are not provided. Converted to floats if provided
217
+ def initialize(lat=nil, lng=nil)
218
+ lat = lat.to_f if lat && !lat.is_a?(Numeric)
219
+ lng = lng.to_f if lng && !lng.is_a?(Numeric)
220
+ @lat = lat
221
+ @lng = lng
222
+ end
223
+
224
+ # Latitude attribute setter; stored as a float.
225
+ def lat=(lat)
226
+ @lat = lat.to_f if lat
227
+ end
228
+
229
+ # Longitude attribute setter; stored as a float;
230
+ def lng=(lng)
231
+ @lng=lng.to_f if lng
232
+ end
233
+
234
+ # Returns the lat and lng attributes as a comma-separated string.
235
+ def ll
236
+ "#{lat},#{lng}"
237
+ end
238
+
239
+ #returns a string with comma-separated lat,lng values
240
+ def to_s
241
+ ll
242
+ end
243
+
244
+ #returns a two-element array
245
+ def to_a
246
+ [lat,lng]
247
+ end
248
+ # Returns true if the candidate object is logically equal. Logical equivalence
249
+ # is true if the lat and lng attributes are the same for both objects.
250
+ def ==(other)
251
+ other.is_a?(LatLng) ? self.lat == other.lat && self.lng == other.lng : false
252
+ end
253
+
254
+ def hash
255
+ lat.hash + lng.hash
256
+ end
257
+
258
+ def eql?(other)
259
+ self == other
260
+ end
261
+
262
+ # A *class* method to take anything which can be inferred as a point and generate
263
+ # a LatLng from it. You should use this anything you're not sure what the input is,
264
+ # and want to deal with it as a LatLng if at all possible. Can take:
265
+ # 1) two arguments (lat,lng)
266
+ # 2) a string in the format "37.1234,-129.1234" or "37.1234 -129.1234"
267
+ # 3) a string which can be geocoded on the fly
268
+ # 4) an array in the format [37.1234,-129.1234]
269
+ # 5) a LatLng or GeoLoc (which is just passed through as-is)
270
+ # 6) anything which acts_as_mappable -- a LatLng will be extracted from it
271
+ def self.normalize(thing,other=nil)
272
+ # if an 'other' thing is supplied, normalize the input by creating an array of two elements
273
+ thing=[thing,other] if other
274
+
275
+ if thing.is_a?(String)
276
+ thing.strip!
277
+ if match=thing.match(/(\-?\d+\.?\d*)[, ] ?(\-?\d+\.?\d*)$/)
278
+ return Kaupert::LatLng.new(match[1],match[2])
279
+ else
280
+ res = Kaupert::Geocoders::MultiGeocoder.geocode(thing)
281
+ return res if res.success?
282
+ raise Kaupert::Geocoders::GeocodeError
283
+ end
284
+ elsif thing.is_a?(Array) && thing.size==2
285
+ return Kaupert::LatLng.new(thing[0],thing[1])
286
+ elsif thing.is_a?(LatLng) # will also be true for GeoLocs
287
+ return thing
288
+ elsif thing.class.respond_to?(:acts_as_mappable) && thing.class.respond_to?(:distance_column_name)
289
+ return thing.to_lat_lng
290
+ elsif thing.respond_to? :to_lat_lng
291
+ return thing.to_lat_lng
292
+ end
293
+
294
+ raise ArgumentError.new("#{thing} (#{thing.class}) cannot be normalized to a LatLng. We tried interpreting it as an array, string, Mappable, etc., but no dice.")
295
+ end
296
+
297
+ # Reverse geocodes a LatLng object using the MultiGeocoder (default), or optionally
298
+ # using a geocoder of your choosing. Returns a new Kaupert::GeoLoc object
299
+ #
300
+ # ==== Options
301
+ # * :using - Specifies the geocoder to use for reverse geocoding. Defaults to
302
+ # MultiGeocoder. Can be either the geocoder class (or any class that
303
+ # implements do_reverse_geocode for that matter), or the name of
304
+ # the class without the "Geocoder" part (e.g. :google)
305
+ #
306
+ # ==== Examples
307
+ # LatLng.new(51.4578329, 7.0166848).reverse_geocode # => #<Kaupert::GeoLoc:0x12dac20 @state...>
308
+ # LatLng.new(51.4578329, 7.0166848).reverse_geocode(:using => :google) # => #<Kaupert::GeoLoc:0x12dac20 @state...>
309
+ # LatLng.new(51.4578329, 7.0166848).reverse_geocode(:using => Kaupert::Geocoders::GoogleGeocoder) # => #<Kaupert::GeoLoc:0x12dac20 @state...>
310
+ def reverse_geocode(options = { :using => Kaupert::Geocoders::MultiGeocoder })
311
+ if options[:using].is_a?(String) or options[:using].is_a?(Symbol)
312
+ provider = Kaupert::Geocoders.const_get("#{Kaupert::Inflector::camelize(options[:using].to_s)}Geocoder")
313
+ elsif options[:using].respond_to?(:do_reverse_geocode)
314
+ provider = options[:using]
315
+ else
316
+ raise ArgumentError.new("#{options[:using]} is not a valid geocoder.")
317
+ end
318
+
319
+ provider.send(:reverse_geocode, self)
320
+ end
321
+ end
322
+
323
+ # This class encapsulates the result of a geocoding call.
324
+ # It's primary purpose is to homogenize the results of multiple
325
+ # geocoding providers. It also provides some additional functionality, such as
326
+ # the "full address" method for geocoders that do not provide a
327
+ # full address in their results (for example, Yahoo), and the "is_us" method.
328
+ #
329
+ # Some geocoders can return multple results. Geoloc can capture multiple results through
330
+ # its "all" method.
331
+ #
332
+ # For the geocoder setting the results, it would look something like this:
333
+ # geo=GeoLoc.new(first_result)
334
+ # geo.all.push(second_result)
335
+ # geo.all.push(third_result)
336
+ #
337
+ # Then, for the user of the result:
338
+ #
339
+ # puts geo.full_address # just like usual
340
+ # puts geo.all.size => 3 # there's three results total
341
+ # puts geo.all.first # all is just an array or additional geolocs,
342
+ # so do what you want with it
343
+ class GeoLoc < LatLng
344
+
345
+ # Location attributes. Full address is a concatenation of all values. For example:
346
+ # 100 Spear St, San Francisco, CA, 94101, US
347
+ # Street number and street name are extracted from the street address attribute if they don't exist
348
+ attr_accessor :street_number,:street_name,:street_address, :city, :state, :zip, :country_code, :country, :full_address, :all, :district, :province
349
+ # Attributes set upon return from geocoding. Success will be true for successful
350
+ # geocode lookups. The provider will be set to the name of the providing geocoder.
351
+ # Finally, precision is an indicator of the accuracy of the geocoding.
352
+ attr_accessor :success, :provider, :precision, :suggested_bounds
353
+ # accuracy is set for Yahoo and Google geocoders, it is a numeric value of the
354
+ # precision. see http://code.google.com/apis/maps/documentation/geocoding/#GeocodingAccuracy
355
+ attr_accessor :accuracy
356
+ # FCC Attributes
357
+ attr_accessor :district_fips, :state_fips, :block_fips
358
+
359
+
360
+ # Constructor expects a hash of symbols to correspond with attributes.
361
+ def initialize(h={})
362
+ @all = [self]
363
+
364
+ @street_address=h[:street_address]
365
+ @street_number=nil
366
+ @street_name=nil
367
+ @city=h[:city]
368
+ @state=h[:state]
369
+ @zip=h[:zip]
370
+ @country_code=h[:country_code]
371
+ @province = h[:province]
372
+ @success=false
373
+ @precision='unknown'
374
+ @full_address=nil
375
+ super(h[:lat],h[:lng])
376
+ end
377
+
378
+ # Returns true if geocoded to the United States.
379
+ def is_us?
380
+ country_code == 'US'
381
+ end
382
+
383
+ def success?
384
+ success == true
385
+ end
386
+
387
+ # full_address is provided by google but not by yahoo. It is intended that the google
388
+ # geocoding method will provide the full address, whereas for yahoo it will be derived
389
+ # from the parts of the address we do have.
390
+ def full_address
391
+ @full_address ? @full_address : to_geocodeable_s
392
+ end
393
+
394
+ # Extracts the street number from the street address where possible.
395
+ def street_number
396
+ @street_number ||= street_address[/(\d*)/] if street_address
397
+ @street_number
398
+ end
399
+
400
+ # Returns the street name portion of the street address where possible
401
+ def street_name
402
+ @street_name||=street_address[street_number.length, street_address.length].strip if street_address
403
+ @street_name
404
+ end
405
+
406
+ # gives you all the important fields as key-value pairs
407
+ def hash
408
+ res={}
409
+ [:success,:lat,:lng,:country_code,:city,:state,:zip,:street_address,:province,:district,:provider,:full_address,:is_us?,:ll,:precision,:district_fips,:state_fips,:block_fips].each { |s| res[s] = self.send(s.to_s) }
410
+ res
411
+ end
412
+ alias to_hash hash
413
+
414
+ # Sets the city after capitalizing each word within the city name.
415
+ def city=(city)
416
+ @city = Kaupert::Inflector::titleize(city) if city
417
+ end
418
+
419
+ # Sets the street address after capitalizing each word within the street address.
420
+ def street_address=(address)
421
+ if address and not ['google','google3'].include?(self.provider)
422
+ @street_address = Kaupert::Inflector::titleize(address)
423
+ else
424
+ @street_address = address
425
+ end
426
+ end
427
+
428
+ # Returns a comma-delimited string consisting of the street address, city, state,
429
+ # zip, and country code. Only includes those attributes that are non-blank.
430
+ def to_geocodeable_s
431
+ a=[street_address, district, city, province, state, zip, country_code].compact
432
+ a.delete_if { |e| !e || e == '' }
433
+ a.join(', ')
434
+ end
435
+
436
+ def to_yaml_properties
437
+ (instance_variables - ['@all']).sort
438
+ end
439
+
440
+ # Returns a string representation of the instance.
441
+ def to_s
442
+ "Provider: #{provider}\nStreet: #{street_address}\nCity: #{city}\nState: #{state}\nZip: #{zip}\nLatitude: #{lat}\nLongitude: #{lng}\nCountry: #{country_code}\nSuccess: #{success}"
443
+ end
444
+ end
445
+
446
+ # Bounds represents a rectangular bounds, defined by the SW and NE corners
447
+ class Bounds
448
+ # sw and ne are LatLng objects
449
+ attr_accessor :sw, :ne
450
+
451
+ # provide sw and ne to instantiate a new Bounds instance
452
+ def initialize(sw,ne)
453
+ raise ArgumentError if !(sw.is_a?(Kaupert::LatLng) && ne.is_a?(Kaupert::LatLng))
454
+ @sw,@ne=sw,ne
455
+ end
456
+
457
+ #returns the a single point which is the center of the rectangular bounds
458
+ def center
459
+ @sw.midpoint_to(@ne)
460
+ end
461
+
462
+ # a simple string representation:sw,ne
463
+ def to_s
464
+ "#{@sw.to_s},#{@ne.to_s}"
465
+ end
466
+
467
+ # a two-element array of two-element arrays: sw,ne
468
+ def to_a
469
+ [@sw.to_a, @ne.to_a]
470
+ end
471
+
472
+ # Returns true if the bounds contain the passed point.
473
+ # allows for bounds which cross the meridian
474
+ def contains?(point)
475
+ point=Kaupert::LatLng.normalize(point)
476
+ res = point.lat > @sw.lat && point.lat < @ne.lat
477
+ if crosses_meridian?
478
+ res &= point.lng < @ne.lng || point.lng > @sw.lng
479
+ else
480
+ res &= point.lng < @ne.lng && point.lng > @sw.lng
481
+ end
482
+ res
483
+ end
484
+
485
+ # returns true if the bounds crosses the international dateline
486
+ def crosses_meridian?
487
+ @sw.lng > @ne.lng
488
+ end
489
+
490
+ # Returns true if the candidate object is logically equal. Logical equivalence
491
+ # is true if the lat and lng attributes are the same for both objects.
492
+ def ==(other)
493
+ other.is_a?(Bounds) ? self.sw == other.sw && self.ne == other.ne : false
494
+ end
495
+
496
+ # Equivalent to Google Maps API's .toSpan() method on GLatLng's.
497
+ #
498
+ # Returns a LatLng object, whose coordinates represent the size of a rectangle
499
+ # defined by these bounds.
500
+ def to_span
501
+ lat_span = (@ne.lat - @sw.lat).abs
502
+ lng_span = (crosses_meridian? ? 360 + @ne.lng - @sw.lng : @ne.lng - @sw.lng).abs
503
+ Kaupert::LatLng.new(lat_span, lng_span)
504
+ end
505
+
506
+ class <<self
507
+
508
+ # returns an instance of bounds which completely encompases the given circle
509
+ def from_point_and_radius(point,radius,options={})
510
+ point=LatLng.normalize(point)
511
+ p0=point.endpoint(0,radius,options)
512
+ p90=point.endpoint(90,radius,options)
513
+ p180=point.endpoint(180,radius,options)
514
+ p270=point.endpoint(270,radius,options)
515
+ sw=Kaupert::LatLng.new(p180.lat,p270.lng)
516
+ ne=Kaupert::LatLng.new(p0.lat,p90.lng)
517
+ Kaupert::Bounds.new(sw,ne)
518
+ end
519
+
520
+ # Takes two main combinations of arguments to create a bounds:
521
+ # point,point (this is the only one which takes two arguments
522
+ # [point,point]
523
+ # . . . where a point is anything LatLng#normalize can handle (which is quite a lot)
524
+ #
525
+ # NOTE: everything combination is assumed to pass points in the order sw, ne
526
+ def normalize (thing,other=nil)
527
+ # maybe this will be simple -- an actual bounds object is passed, and we can all go home
528
+ return thing if thing.is_a? Bounds
529
+
530
+ # no? OK, if there's no "other," the thing better be a two-element array
531
+ thing,other=thing if !other && thing.is_a?(Array) && thing.size==2
532
+
533
+ # Now that we're set with a thing and another thing, let LatLng do the heavy lifting.
534
+ # Exceptions may be thrown
535
+ Bounds.new(Kaupert::LatLng.normalize(thing),Kaupert::LatLng.normalize(other))
536
+ end
537
+ end
538
+ end
539
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kaupert
3
+ version: !ruby/object:Gem::Version
4
+ hash: 19
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 1
9
+ - 0
10
+ version: 1.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Abhishek Gupta
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-06-22 00:00:00 +05:30
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: bundler
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 23
30
+ segments:
31
+ - 1
32
+ - 0
33
+ - 0
34
+ version: 1.0.0
35
+ type: :development
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: jeweler
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 11
46
+ segments:
47
+ - 1
48
+ - 6
49
+ - 2
50
+ version: 1.6.2
51
+ type: :development
52
+ version_requirements: *id002
53
+ description: kaupert fare calculator for Kaupert
54
+ email: a.guptagoa@gmail.com
55
+ executables: []
56
+
57
+ extensions: []
58
+
59
+ extra_rdoc_files:
60
+ - LICENSE.txt
61
+ - README.rdoc
62
+ files:
63
+ - lib/kaupert.rb
64
+ - lib/kaupert/geocoding.rb
65
+ - lib/kaupert/mapcal.rb
66
+ - LICENSE.txt
67
+ - README.rdoc
68
+ has_rdoc: true
69
+ homepage: http://github.com/bizfosys/kaupert
70
+ licenses:
71
+ - MIT
72
+ post_install_message:
73
+ rdoc_options: []
74
+
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ hash: 3
83
+ segments:
84
+ - 0
85
+ version: "0"
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ hash: 3
92
+ segments:
93
+ - 0
94
+ version: "0"
95
+ requirements: []
96
+
97
+ rubyforge_project:
98
+ rubygems_version: 1.5.2
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: kaupert fare calculator
102
+ test_files: []
103
+