yannski-geokit 1.2.6
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest.txt +22 -0
- data/README.markdown +189 -0
- data/Rakefile +14 -0
- data/lib/geokit/geocoders.rb +588 -0
- data/lib/geokit/mappable.rb +482 -0
- data/lib/geokit.rb +30 -0
- data/test/test_base_geocoder.rb +57 -0
- data/test/test_bounds.rb +74 -0
- data/test/test_ca_geocoder.rb +41 -0
- data/test/test_fakegeocoder.rb +28 -0
- data/test/test_geoloc.rb +72 -0
- data/test/test_google_geocoder.rb +116 -0
- data/test/test_latlng.rb +132 -0
- data/test/test_multi_geocoder.rb +44 -0
- data/test/test_us_geocoder.rb +56 -0
- data/test/test_yahoo_geocoder.rb +87 -0
- metadata +80 -0
@@ -0,0 +1,482 @@
|
|
1
|
+
#require 'forwardable'
|
2
|
+
|
3
|
+
module Geokit
|
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 Mappable
|
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 (Geokit::default_units is the default)
|
35
|
+
# :formula - valid values are :flat or :sphere (Geokit::default_formula is the default)
|
36
|
+
def distance_between(from, to, options={})
|
37
|
+
from=Geokit::LatLng.normalize(from)
|
38
|
+
to=Geokit::LatLng.normalize(to)
|
39
|
+
return 0.0 if from == to # fixes a "zero-distance" bug
|
40
|
+
units = options[:units] || Geokit::default_units
|
41
|
+
formula = options[:formula] || Geokit::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=Geokit::LatLng.normalize(from)
|
63
|
+
to=Geokit::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] || Geokit::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=Geokit::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=Geokit::LatLng.normalize(from)
|
104
|
+
|
105
|
+
units = options[:units] || Geokit::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)
|
114
|
+
res = Geocoders::MultiGeocoder.geocode(location)
|
115
|
+
return res if res.success?
|
116
|
+
raise Geokit::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?(Geokit::LatLng) || instance_of?(Geokit::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 Mappable
|
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
|
+
# A *class* method to take anything which can be inferred as a point and generate
|
255
|
+
# a LatLng from it. You should use this anything you're not sure what the input is,
|
256
|
+
# and want to deal with it as a LatLng if at all possible. Can take:
|
257
|
+
# 1) two arguments (lat,lng)
|
258
|
+
# 2) a string in the format "37.1234,-129.1234" or "37.1234 -129.1234"
|
259
|
+
# 3) a string which can be geocoded on the fly
|
260
|
+
# 4) an array in the format [37.1234,-129.1234]
|
261
|
+
# 5) a LatLng or GeoLoc (which is just passed through as-is)
|
262
|
+
# 6) anything which acts_as_mappable -- a LatLng will be extracted from it
|
263
|
+
def self.normalize(thing,other=nil)
|
264
|
+
# if an 'other' thing is supplied, normalize the input by creating an array of two elements
|
265
|
+
thing=[thing,other] if other
|
266
|
+
|
267
|
+
if thing.is_a?(String)
|
268
|
+
thing.strip!
|
269
|
+
if match=thing.match(/(\-?\d+\.?\d*)[, ] ?(\-?\d+\.?\d*)$/)
|
270
|
+
return Geokit::LatLng.new(match[1],match[2])
|
271
|
+
else
|
272
|
+
res = Geokit::Geocoders::MultiGeocoder.geocode(thing)
|
273
|
+
return res if res.success?
|
274
|
+
raise Geokit::Geocoders::GeocodeError
|
275
|
+
end
|
276
|
+
elsif thing.is_a?(Array) && thing.size==2
|
277
|
+
return Geokit::LatLng.new(thing[0],thing[1])
|
278
|
+
elsif thing.is_a?(LatLng) # will also be true for GeoLocs
|
279
|
+
return thing
|
280
|
+
elsif thing.class.respond_to?(:acts_as_mappable) && thing.class.respond_to?(:distance_column_name)
|
281
|
+
return thing.to_lat_lng
|
282
|
+
end
|
283
|
+
|
284
|
+
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.")
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
|
289
|
+
# This class encapsulates the result of a geocoding call.
|
290
|
+
# It's primary purpose is to homogenize the results of multiple
|
291
|
+
# geocoding providers. It also provides some additional functionality, such as
|
292
|
+
# the "full address" method for geocoders that do not provide a
|
293
|
+
# full address in their results (for example, Yahoo), and the "is_us" method.
|
294
|
+
#
|
295
|
+
# Some geocoders can return multple results. Geoloc can capture multiple results through
|
296
|
+
# its "all" method.
|
297
|
+
#
|
298
|
+
# For the geocoder setting the results, it would look something like this:
|
299
|
+
# geo=GeoLoc.new(first_result)
|
300
|
+
# geo.all.push(second_result)
|
301
|
+
# geo.all.push(third_result)
|
302
|
+
#
|
303
|
+
# Then, for the user of the result:
|
304
|
+
#
|
305
|
+
# puts geo.full_address # just like usual
|
306
|
+
# puts geo.all.size => 3 # there's three results total
|
307
|
+
# puts geo.all.first # all is just an array or additional geolocs,
|
308
|
+
# so do what you want with it
|
309
|
+
class GeoLoc < LatLng
|
310
|
+
|
311
|
+
# Location attributes. Full address is a concatenation of all values. For example:
|
312
|
+
# 100 Spear St, San Francisco, CA, 94101, US
|
313
|
+
attr_accessor :street_address, :city, :state, :zip, :country_code, :full_address, :all
|
314
|
+
# Attributes set upon return from geocoding. Success will be true for successful
|
315
|
+
# geocode lookups. The provider will be set to the name of the providing geocoder.
|
316
|
+
# Finally, precision is an indicator of the accuracy of the geocoding.
|
317
|
+
attr_accessor :success, :provider, :precision
|
318
|
+
# Street number and street name are extracted from the street address attribute.
|
319
|
+
attr_reader :street_number, :street_name
|
320
|
+
|
321
|
+
# Constructor expects a hash of symbols to correspond with attributes.
|
322
|
+
def initialize(h={})
|
323
|
+
@all = [self]
|
324
|
+
|
325
|
+
@street_address=h[:street_address]
|
326
|
+
@city=h[:city]
|
327
|
+
@state=h[:state]
|
328
|
+
@zip=h[:zip]
|
329
|
+
@country_code=h[:country_code]
|
330
|
+
@success=false
|
331
|
+
@precision='unknown'
|
332
|
+
@full_address=nil
|
333
|
+
super(h[:lat],h[:lng])
|
334
|
+
end
|
335
|
+
|
336
|
+
# Returns true if geocoded to the United States.
|
337
|
+
def is_us?
|
338
|
+
country_code == 'US'
|
339
|
+
end
|
340
|
+
|
341
|
+
def success?
|
342
|
+
success == true
|
343
|
+
end
|
344
|
+
|
345
|
+
# full_address is provided by google but not by yahoo. It is intended that the google
|
346
|
+
# geocoding method will provide the full address, whereas for yahoo it will be derived
|
347
|
+
# from the parts of the address we do have.
|
348
|
+
def full_address
|
349
|
+
@full_address ? @full_address : to_geocodeable_s
|
350
|
+
end
|
351
|
+
|
352
|
+
# Extracts the street number from the street address if the street address
|
353
|
+
# has a value.
|
354
|
+
def street_number
|
355
|
+
street_address[/(\d*)/] if street_address
|
356
|
+
end
|
357
|
+
|
358
|
+
# Returns the street name portion of the street address.
|
359
|
+
def street_name
|
360
|
+
street_address[street_number.length, street_address.length].strip if street_address
|
361
|
+
end
|
362
|
+
|
363
|
+
# gives you all the important fields as key-value pairs
|
364
|
+
def hash
|
365
|
+
res={}
|
366
|
+
[:success,:lat,:lng,:country_code,:city,:state,:zip,:street_address,:provider,:full_address,:is_us?,:ll,:precision].each { |s| res[s] = self.send(s.to_s) }
|
367
|
+
res
|
368
|
+
end
|
369
|
+
alias to_hash hash
|
370
|
+
|
371
|
+
# Sets the city after capitalizing each word within the city name.
|
372
|
+
def city=(city)
|
373
|
+
@city = Geokit::Inflector::titleize(city) if city
|
374
|
+
end
|
375
|
+
|
376
|
+
# Sets the street address after capitalizing each word within the street address.
|
377
|
+
def street_address=(address)
|
378
|
+
@street_address = Geokit::Inflector::titleize(address) if address
|
379
|
+
end
|
380
|
+
|
381
|
+
# Returns a comma-delimited string consisting of the street address, city, state,
|
382
|
+
# zip, and country code. Only includes those attributes that are non-blank.
|
383
|
+
def to_geocodeable_s
|
384
|
+
a=[street_address, city, state, zip, country_code].compact
|
385
|
+
a.delete_if { |e| !e || e == '' }
|
386
|
+
a.join(', ')
|
387
|
+
end
|
388
|
+
|
389
|
+
def to_yaml_properties
|
390
|
+
(instance_variables - ['@all']).sort
|
391
|
+
end
|
392
|
+
|
393
|
+
# Returns a string representation of the instance.
|
394
|
+
def to_s
|
395
|
+
"Provider: #{provider}\n Street: #{street_address}\nCity: #{city}\nState: #{state}\nZip: #{zip}\nLatitude: #{lat}\nLongitude: #{lng}\nCountry: #{country_code}\nSuccess: #{success}"
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
# Bounds represents a rectangular bounds, defined by the SW and NE corners
|
400
|
+
class Bounds
|
401
|
+
# sw and ne are LatLng objects
|
402
|
+
attr_accessor :sw, :ne
|
403
|
+
|
404
|
+
# provide sw and ne to instantiate a new Bounds instance
|
405
|
+
def initialize(sw,ne)
|
406
|
+
raise ArgumentError if !(sw.is_a?(Geokit::LatLng) && ne.is_a?(Geokit::LatLng))
|
407
|
+
@sw,@ne=sw,ne
|
408
|
+
end
|
409
|
+
|
410
|
+
#returns the a single point which is the center of the rectangular bounds
|
411
|
+
def center
|
412
|
+
@sw.midpoint_to(@ne)
|
413
|
+
end
|
414
|
+
|
415
|
+
# a simple string representation:sw,ne
|
416
|
+
def to_s
|
417
|
+
"#{@sw.to_s},#{@ne.to_s}"
|
418
|
+
end
|
419
|
+
|
420
|
+
# a two-element array of two-element arrays: sw,ne
|
421
|
+
def to_a
|
422
|
+
[@sw.to_a, @ne.to_a]
|
423
|
+
end
|
424
|
+
|
425
|
+
# Returns true if the bounds contain the passed point.
|
426
|
+
# allows for bounds which cross the meridian
|
427
|
+
def contains?(point)
|
428
|
+
point=Geokit::LatLng.normalize(point)
|
429
|
+
res = point.lat > @sw.lat && point.lat < @ne.lat
|
430
|
+
if crosses_meridian?
|
431
|
+
res &= point.lng < @ne.lng || point.lng > @sw.lng
|
432
|
+
else
|
433
|
+
res &= point.lng < @ne.lng && point.lng > @sw.lng
|
434
|
+
end
|
435
|
+
res
|
436
|
+
end
|
437
|
+
|
438
|
+
# returns true if the bounds crosses the international dateline
|
439
|
+
def crosses_meridian?
|
440
|
+
@sw.lng > @ne.lng
|
441
|
+
end
|
442
|
+
|
443
|
+
# Returns true if the candidate object is logically equal. Logical equivalence
|
444
|
+
# is true if the lat and lng attributes are the same for both objects.
|
445
|
+
def ==(other)
|
446
|
+
other.is_a?(Bounds) ? self.sw == other.sw && self.ne == other.ne : false
|
447
|
+
end
|
448
|
+
|
449
|
+
class <<self
|
450
|
+
|
451
|
+
# returns an instance of bounds which completely encompases the given circle
|
452
|
+
def from_point_and_radius(point,radius,options={})
|
453
|
+
point=LatLng.normalize(point)
|
454
|
+
p0=point.endpoint(0,radius,options)
|
455
|
+
p90=point.endpoint(90,radius,options)
|
456
|
+
p180=point.endpoint(180,radius,options)
|
457
|
+
p270=point.endpoint(270,radius,options)
|
458
|
+
sw=Geokit::LatLng.new(p180.lat,p270.lng)
|
459
|
+
ne=Geokit::LatLng.new(p0.lat,p90.lng)
|
460
|
+
Geokit::Bounds.new(sw,ne)
|
461
|
+
end
|
462
|
+
|
463
|
+
# Takes two main combinations of arguments to create a bounds:
|
464
|
+
# point,point (this is the only one which takes two arguments
|
465
|
+
# [point,point]
|
466
|
+
# . . . where a point is anything LatLng#normalize can handle (which is quite a lot)
|
467
|
+
#
|
468
|
+
# NOTE: everything combination is assumed to pass points in the order sw, ne
|
469
|
+
def normalize (thing,other=nil)
|
470
|
+
# maybe this will be simple -- an actual bounds object is passed, and we can all go home
|
471
|
+
return thing if thing.is_a? Bounds
|
472
|
+
|
473
|
+
# no? OK, if there's no "other," the thing better be a two-element array
|
474
|
+
thing,other=thing if !other && thing.is_a?(Array) && thing.size==2
|
475
|
+
|
476
|
+
# Now that we're set with a thing and another thing, let LatLng do the heavy lifting.
|
477
|
+
# Exceptions may be thrown
|
478
|
+
Bounds.new(Geokit::LatLng.normalize(thing),Geokit::LatLng.normalize(other))
|
479
|
+
end
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|
data/lib/geokit.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module Geokit
|
2
|
+
VERSION = '1.2.6'
|
3
|
+
# These defaults are used in Geokit::Mappable.distance_to and in acts_as_mappable
|
4
|
+
@@default_units = :miles
|
5
|
+
@@default_formula = :sphere
|
6
|
+
|
7
|
+
[:default_units, :default_formula].each do |sym|
|
8
|
+
class_eval <<-EOS, __FILE__, __LINE__
|
9
|
+
def self.#{sym}
|
10
|
+
if defined?(#{sym.to_s.upcase})
|
11
|
+
#{sym.to_s.upcase}
|
12
|
+
else
|
13
|
+
@@#{sym}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.#{sym}=(obj)
|
18
|
+
@@#{sym} = obj
|
19
|
+
end
|
20
|
+
EOS
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
path = File.expand_path(File.dirname(__FILE__))
|
25
|
+
$: << path unless $:.include?(path)
|
26
|
+
require 'geokit/geocoders'
|
27
|
+
require 'geokit/mappable'
|
28
|
+
|
29
|
+
# make old-style module name "GeoKit" equivalent to new-style "Geokit"
|
30
|
+
GeoKit=Geokit
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'net/http'
|
3
|
+
require 'rubygems'
|
4
|
+
require 'mocha'
|
5
|
+
require 'lib/geokit'
|
6
|
+
|
7
|
+
class MockSuccess < Net::HTTPSuccess #:nodoc: all
|
8
|
+
def initialize
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class MockFailure < Net::HTTPServiceUnavailable #:nodoc: all
|
13
|
+
def initialize
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Base class for testing geocoders.
|
18
|
+
class BaseGeocoderTest < Test::Unit::TestCase #:nodoc: all
|
19
|
+
|
20
|
+
class Geokit::Geocoders::TestGeocoder < Geokit::Geocoders::Geocoder
|
21
|
+
def self.do_get(url)
|
22
|
+
sleep(2)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Defines common test fixtures.
|
27
|
+
def setup
|
28
|
+
@address = 'San Francisco, CA'
|
29
|
+
@full_address = '100 Spear St, San Francisco, CA, 94105-1522, US'
|
30
|
+
@full_address_short_zip = '100 Spear St, San Francisco, CA, 94105, US'
|
31
|
+
|
32
|
+
@success = Geokit::GeoLoc.new({:city=>"SAN FRANCISCO", :state=>"CA", :country_code=>"US", :lat=>37.7742, :lng=>-122.417068})
|
33
|
+
@success.success = true
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_timeout_call_web_service
|
37
|
+
url = "http://www.anything.com"
|
38
|
+
Geokit::Geocoders::timeout = 1
|
39
|
+
assert_nil Geokit::Geocoders::TestGeocoder.call_geocoder_service(url)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_successful_call_web_service
|
43
|
+
url = "http://www.anything.com"
|
44
|
+
Geokit::Geocoders::Geocoder.expects(:do_get).with(url).returns("SUCCESS")
|
45
|
+
assert_equal "SUCCESS", Geokit::Geocoders::Geocoder.call_geocoder_service(url)
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_find_geocoder_methods
|
49
|
+
public_methods = Geokit::Geocoders::Geocoder.public_methods.map { |m| m.to_s }
|
50
|
+
assert public_methods.include?("yahoo_geocoder")
|
51
|
+
assert public_methods.include?("google_geocoder")
|
52
|
+
assert public_methods.include?("ca_geocoder")
|
53
|
+
assert public_methods.include?("us_geocoder")
|
54
|
+
assert public_methods.include?("multi_geocoder")
|
55
|
+
assert public_methods.include?("ip_geocoder")
|
56
|
+
end
|
57
|
+
end
|
data/test/test_bounds.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'lib/geokit'
|
3
|
+
|
4
|
+
class BoundsTest < Test::Unit::TestCase #:nodoc: all
|
5
|
+
|
6
|
+
def setup
|
7
|
+
# This is the area in Texas
|
8
|
+
@sw = Geokit::LatLng.new(32.91663,-96.982841)
|
9
|
+
@ne = Geokit::LatLng.new(32.96302,-96.919495)
|
10
|
+
@bounds=Geokit::Bounds.new(@sw,@ne)
|
11
|
+
@loc_a=Geokit::LatLng.new(32.918593,-96.958444) # inside bounds
|
12
|
+
@loc_b=Geokit::LatLng.new(32.914144,-96.958444) # outside bouds
|
13
|
+
|
14
|
+
# this is a cross-meridan area
|
15
|
+
@cross_meridian=Geokit::Bounds.normalize([30,170],[40,-170])
|
16
|
+
@inside_cm=Geokit::LatLng.new(35,175)
|
17
|
+
@inside_cm_2=Geokit::LatLng.new(35,-175)
|
18
|
+
@east_of_cm=Geokit::LatLng.new(35,-165)
|
19
|
+
@west_of_cm=Geokit::LatLng.new(35,165)
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_equality
|
24
|
+
assert_equal Geokit::Bounds.new(@sw,@ne), Geokit::Bounds.new(@sw,@ne)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_normalize
|
28
|
+
res=Geokit::Bounds.normalize(@sw,@ne)
|
29
|
+
assert_equal res,Geokit::Bounds.new(@sw,@ne)
|
30
|
+
res=Geokit::Bounds.normalize([@sw,@ne])
|
31
|
+
assert_equal res,Geokit::Bounds.new(@sw,@ne)
|
32
|
+
res=Geokit::Bounds.normalize([@sw.lat,@sw.lng],[@ne.lat,@ne.lng])
|
33
|
+
assert_equal res,Geokit::Bounds.new(@sw,@ne)
|
34
|
+
res=Geokit::Bounds.normalize([[@sw.lat,@sw.lng],[@ne.lat,@ne.lng]])
|
35
|
+
assert_equal res,Geokit::Bounds.new(@sw,@ne)
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_point_inside_bounds
|
39
|
+
assert @bounds.contains?(@loc_a)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_point_outside_bounds
|
43
|
+
assert !@bounds.contains?(@loc_b)
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_point_inside_bounds_cross_meridian
|
47
|
+
assert @cross_meridian.contains?(@inside_cm)
|
48
|
+
assert @cross_meridian.contains?(@inside_cm_2)
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_point_outside_bounds_cross_meridian
|
52
|
+
assert !@cross_meridian.contains?(@east_of_cm)
|
53
|
+
assert !@cross_meridian.contains?(@west_of_cm)
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_center
|
57
|
+
assert_in_delta 32.939828,@bounds.center.lat,0.00005
|
58
|
+
assert_in_delta(-96.9511763,@bounds.center.lng,0.00005)
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_center_cross_meridian
|
62
|
+
assert_in_delta 35.41160, @cross_meridian.center.lat,0.00005
|
63
|
+
assert_in_delta 179.38112, @cross_meridian.center.lng,0.00005
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_creation_from_circle
|
67
|
+
bounds=Geokit::Bounds.from_point_and_radius([32.939829, -96.951176],2.5)
|
68
|
+
inside=Geokit::LatLng.new 32.9695270000,-96.9901590000
|
69
|
+
outside=Geokit::LatLng.new 32.8951550000,-96.9584440000
|
70
|
+
assert bounds.contains?(inside)
|
71
|
+
assert !bounds.contains?(outside)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_base_geocoder')
|
2
|
+
|
3
|
+
Geokit::Geocoders::geocoder_ca = "SOMEKEYVALUE"
|
4
|
+
|
5
|
+
class CaGeocoderTest < BaseGeocoderTest #:nodoc: all
|
6
|
+
|
7
|
+
CA_SUCCESS=<<-EOF
|
8
|
+
<?xml version="1.0" encoding="UTF-8" ?>
|
9
|
+
<geodata><latt>49.243086</latt><longt>-123.153684</longt></geodata>
|
10
|
+
EOF
|
11
|
+
|
12
|
+
def setup
|
13
|
+
@ca_full_hash = {:street_address=>"2105 West 32nd Avenue",:city=>"Vancouver", :state=>"BC"}
|
14
|
+
@ca_full_loc = Geokit::GeoLoc.new(@ca_full_hash)
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_geocoder_with_geo_loc_with_account
|
18
|
+
response = MockSuccess.new
|
19
|
+
response.expects(:body).returns(CA_SUCCESS)
|
20
|
+
url = "http://geocoder.ca/?stno=2105&addresst=West+32nd+Avenue&city=Vancouver&prov=BC&auth=SOMEKEYVALUE&geoit=xml"
|
21
|
+
Geokit::Geocoders::CaGeocoder.expects(:call_geocoder_service).with(url).returns(response)
|
22
|
+
verify(Geokit::Geocoders::CaGeocoder.geocode(@ca_full_loc))
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_service_unavailable
|
26
|
+
response = MockFailure.new
|
27
|
+
#Net::HTTP.expects(:get_response).with(URI.parse("http://geocoder.ca/?stno=2105&addresst=West+32nd+Avenue&city=Vancouver&prov=BC&auth=SOMEKEYVALUE&geoit=xml")).returns(response)
|
28
|
+
url = "http://geocoder.ca/?stno=2105&addresst=West+32nd+Avenue&city=Vancouver&prov=BC&auth=SOMEKEYVALUE&geoit=xml"
|
29
|
+
Geokit::Geocoders::CaGeocoder.expects(:call_geocoder_service).with(url).returns(response)
|
30
|
+
assert !Geokit::Geocoders::CaGeocoder.geocode(@ca_full_loc).success
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def verify(location)
|
36
|
+
assert_equal "BC", location.state
|
37
|
+
assert_equal "Vancouver", location.city
|
38
|
+
assert_equal "49.243086,-123.153684", location.ll
|
39
|
+
assert !location.is_us?
|
40
|
+
end
|
41
|
+
end
|