kaupert 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.txt +20 -0
- data/README.rdoc +50 -0
- data/lib/kaupert.rb +28 -0
- data/lib/kaupert/geocoding.rb +567 -0
- data/lib/kaupert/mapcal.rb +539 -0
- metadata +103 -0
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
|
+
"®ion=#{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
|
+
|