vincenty 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ === 1.0.1 / 2009-03-03
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
@@ -0,0 +1,16 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/angle.rb
6
+ lib/coordinate.rb
7
+ lib/core_extensions.rb
8
+ lib/latitude.rb
9
+ lib/longitude.rb
10
+ lib/track_and_distance.rb
11
+ lib/vincenty.rb
12
+ test/ts_all.rb
13
+ test/ts_angle.rb
14
+ test/ts_latitude.rb
15
+ test/ts_longitude.rb
16
+ test/ts_vincenty.rb
@@ -0,0 +1,104 @@
1
+ = Vincenty
2
+
3
+ * "http://rubyforge.org/projects/vincenty/"
4
+
5
+ == DESCRIPTION:
6
+
7
+ * Vincenty wrote an algorithm for calculating the bearing and distance between two coordinates on the earth
8
+ and an algorithm for finding a second coordinate, given a starting coordinate, bearing and destination.
9
+ The algorithms model the earth as an ellipsoid, using the WGS-84 model. This is the common GPS model for
10
+ mapping to latitudes and longitudes.
11
+
12
+ This is a Ruby implementation of Vincenty's algorithms, and the Vincenty class includes two methods for
13
+ modeling the earth as a sphere. These were added as a reference for testing the Vincenty algorithm, but
14
+ could be used on their own.
15
+
16
+ The package also makes use of several other classes that may be useful in their own Right. These include
17
+ class Angle, class Latitude (subclass of Angle), class Longitude (subclass of Angle),
18
+ class TrackAndBearing and class coordinate (which class Vincenty is a subclass)
19
+
20
+ Angle requires extensions to Numeric and String to provide to_radians (to_r) and to_degrees (to_d). String also includes a to_decimal_degrees(), which converts most string forms of Latitude and Longitude to decimal form. These extensions are included in the package in core_extensions.rb. Float has also been extended to change round to have an optional argument specifying the number of decimal places to round to. This is fully compatible with the Float.round, as the default is to round to 0 decimal places.
21
+
22
+ * The Vincenty code is based on the wikipedia presentation of the Vincenty algorithm http://en.wikipedia.org/wiki/Vincenty%27s_formulae .
23
+ * The algorithm was modified to include changes I found at http://www.movable-type.co.uk/scripts/latlong-vincenty-direct.html.
24
+ * I also altered the formulae to correctly return the bearing for angles greater than 180.
25
+
26
+ * Vincenty's original publication
27
+
28
+ ** T Vincenty, "Direct and Inverse Solutions of Geodesics on the Ellipsoid with application of nested equations", Survey Review, vol XXII no 176, 1975 http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf
29
+
30
+ == FEATURES/PROBLEMS:
31
+
32
+ * None that I yet know of :)
33
+
34
+ == SYNOPSIS:
35
+
36
+ FIX (code sample of usage)
37
+
38
+ == REQUIREMENTS:
39
+
40
+ * require 'rubygems'
41
+
42
+ == INSTALL:
43
+
44
+ * sudo gem install vincenty
45
+
46
+ == LICENSE:
47
+
48
+ Code unique to this implementation of Vincentys algrithm is distributed under the Ruby License.
49
+
50
+ Copyright (c) 2009 FIX
51
+
52
+ 1. You may make and give away verbatim copies of the source form of the
53
+ software without restriction, provided that you duplicate all of the
54
+ original copyright notices and associated disclaimers.
55
+
56
+ 2. You may modify your copy of the software in any way, provided that
57
+ you do at least ONE of the following:
58
+
59
+ a) place your modifications in the Public Domain or otherwise
60
+ make them Freely Available, such as by posting said
61
+ modifications to Usenet or an equivalent medium, or by allowing
62
+ the author to include your modifications in the software.
63
+
64
+ b) use the modified software only within your corporation or
65
+ organization.
66
+
67
+ c) rename any non-standard executables so the names do not conflict
68
+ with standard executables, which must also be provided.
69
+
70
+ d) make other distribution arrangements with the author.
71
+
72
+ 3. You may distribute the software in object code or executable
73
+ form, provided that you do at least ONE of the following:
74
+
75
+ a) distribute the executables and library files of the software,
76
+ together with instructions (in the manual page or equivalent)
77
+ on where to get the original distribution.
78
+
79
+ b) accompany the distribution with the machine-readable source of
80
+ the software.
81
+
82
+ c) give non-standard executables non-standard names, with
83
+ instructions on where to get the original software distribution.
84
+
85
+ d) make other distribution arrangements with the author.
86
+
87
+ 4. You may modify and include the part of the software into any other
88
+ software (possibly commercial). But some files in the distribution
89
+ are not written by the author, so that they are not under this terms.
90
+
91
+ They are gc.c(partly), utils.c(partly), regex.[ch], st.[ch] and some
92
+ files under the ./missing directory. See each file for the copying
93
+ condition.
94
+
95
+ 5. The scripts and library files supplied as input to or produced as
96
+ output from the software do not automatically fall under the
97
+ copyright of the software, but belong to whomever generated them,
98
+ and may be sold commercially, and may be aggregated with this
99
+ software.
100
+
101
+ 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
102
+ IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
103
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
104
+ PURPOSE.
@@ -0,0 +1,15 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require 'lib/vincenty.rb'
6
+
7
+
8
+ Hoe.new('vincenty', Vincenty::VERSION) do |s|
9
+ s.rubyforge_name = "vincenty"
10
+ s.developer( "Rob Burrowes","rob@burrowes.org")
11
+ #s.url = "http://rubyforge.org/projects/vincenty/"
12
+ #s.summary = "Vincenty Algorithm for Distance, Bearing between Map Coordinates."
13
+ #s.description = s.paragraphs_of('README.txt', 1..4).join("\n\n")
14
+ end
15
+
@@ -0,0 +1,357 @@
1
+ #Class Angle is a utility class that allows
2
+ # * Angle arithmetic
3
+ # * Angle comparison
4
+ # * Conversion to and from degrees and radians
5
+ # * Conversion to string as radians or DMS format
6
+
7
+ require 'core_extensions.rb'
8
+
9
+ class Angle
10
+ include Comparable
11
+
12
+ #Provides test for Module Comparable
13
+ def <=>(v)
14
+ if v.class == Angle
15
+ @value <=> v.value
16
+ else
17
+ @value <=> v
18
+ end
19
+ end
20
+
21
+ attr_accessor :value #stored in radians
22
+
23
+ #v may be anything that has a to_f and to_radians. The Default for v is degrees.
24
+ #if radians == true then v is in radians, not degrees.
25
+ def initialize(v=0, radians=false)
26
+ #assumes that we are getting a value in degrees.
27
+ if radians
28
+ @value = v.to_f #works for String, Fixed, other Angles and for Float.
29
+ else
30
+ if v.class == Array
31
+ @value = self.class.decimal_deg(*v).to_radians #Wild assumption that the array is deg,min,sec.
32
+ else
33
+ @value = v.to_radians #we have a String and Numeric class version of this. Another Angle will work too.
34
+ end
35
+ end
36
+ end
37
+
38
+ #Class level function that converts an array of up to 4 values into decimal degrees.
39
+ #a[0..3] is degrees, minutes, seconds, direction. Dircection is one of 'N', 'S', 'E', 'W'.
40
+ #nil values in the array are set to 0
41
+ #if the a.length < 4,t then a is extended to be length 4 with 0
42
+ #A 0 as the direction has no effect on the sign of the result
43
+ #Returns: signed decimal degress.
44
+ def self.decimal_deg(*a)
45
+ (0..3).each { |x| a[x] = 0 if a[x] == nil } #convert nil arguments to 0 and ensure 4 values.
46
+ s = { 'N'=>1, 'S'=>-1, 'E'=>1, 'W'=>-1, 0=>1 }
47
+ a[0].sign * (a[0].abs + a[1]/60.0 + a[2]/3600.0) * s[a[3]]
48
+ end
49
+
50
+ #Class level utility function to return the value as deg,min,sec
51
+ #Assumes decimal degress unless radians == true
52
+ #returns an array of signed deg, min, sec.
53
+ def self.dms(v, radians = false)
54
+ v = v.to_d if radians
55
+ deg = v.floor
56
+ min = ((v-@deg)*60).floor
57
+ sec = ((v-@deg-min/60.0)*3600.0)
58
+
59
+ if v < 0 && deg == 0
60
+ if min == 0
61
+ sec = -sec
62
+ else
63
+ min = -min
64
+ end
65
+ end
66
+ return deg,min,sec
67
+ end
68
+
69
+ #Class level function equivalent to Angle.new(r, true)
70
+ #Returns: new Angle
71
+ def self.radians(r=0) #passed in radians.
72
+ self.new(r.to_f, true) #Nb. self is Angle, be we don't Angle.new, as we want subclasses to return their class, not Angle.
73
+ end
74
+
75
+ #Class level function equivalent to Angle.new(d, false) or just Angle.new(d)
76
+ #Returns: new Angle
77
+ def self.degrees(d=0) #passed in degrees.
78
+ self.new(d.to_radians, true)
79
+ end
80
+
81
+ #unary +
82
+ #Returns: new Angle
83
+ def +@
84
+ self.class.radians(@value) #Nb. Not Angle.new, as we want subclasses to return their class, not Angle.
85
+ end
86
+
87
+ #Unary -
88
+ #Returns: new Angle
89
+ def -@
90
+ self.class.radians(-@value)
91
+ end
92
+
93
+ #Returns :new Angle
94
+ def +(v)
95
+ self.class.radians(@value + v)
96
+ end
97
+
98
+ #Returns: new Angle
99
+ def -(v)
100
+ self.class.radians(@value - v)
101
+ end
102
+
103
+ #Returns :new Angle
104
+ def *(v)
105
+ self.class.radians(@value * v)
106
+ end
107
+
108
+ #Returns: new Angle
109
+ def **(v)
110
+ self.class.radians(@value ** v)
111
+ end
112
+
113
+ #Returns: new Angle
114
+ def /(v)
115
+ self.class.radians(@value / v)
116
+ end
117
+
118
+ #Returns: new Angle
119
+ def %(v)
120
+ self.class.radians(@value % v)
121
+ end
122
+
123
+ #Returns: angle in degrees
124
+ def to_degrees
125
+ @value.to_degrees
126
+ end
127
+
128
+ alias to_d to_degrees
129
+
130
+ #Returns: angle in radians
131
+ def to_radians
132
+ @value
133
+ end
134
+
135
+ alias to_r to_radians
136
+
137
+ #Returns: [deg,min,sec]
138
+ #Nb. * That min will be negative if the angle is negative and deg == 0
139
+ # * That sec will be negative if the angle is negative and deg == 0 && min == 0
140
+ def to_dms
141
+ d = to_degrees.abs
142
+ deg = d.floor
143
+ min = ((d-deg)*60).floor
144
+ sec = ((d-deg-min/60.0)*3600.0)
145
+
146
+ if @value < 0
147
+ if deg == 0
148
+ if min == 0
149
+ sec = -sec
150
+ else
151
+ min = -min
152
+ end
153
+ else
154
+ deg = -deg
155
+ end
156
+ end
157
+
158
+ return deg, min, sec
159
+ end
160
+
161
+ #Returns: the angle in radians as a float (equivalent to to_radians)
162
+ alias to_f to_radians
163
+
164
+ #Returns the angle truncated to an integer, in radians.
165
+ def to_i
166
+ to_radians.to_i
167
+ end
168
+
169
+ alias to_int to_i
170
+
171
+ def coerce(v)
172
+ [Float(v), @value]
173
+ end
174
+
175
+ #Returns: the sign of the angle. 1 for positive, -1 for negative.
176
+ def sign
177
+ @value.sign
178
+ end
179
+
180
+ #Returns: the absolute value of the angle in radians
181
+ def abs
182
+ @value.abs
183
+ end
184
+
185
+ #Returns: angle as compass bearing in radians.
186
+ #Compass bearings are clockwise, Math angles are counter clockwise.
187
+ def to_bearing
188
+ self.class.new(Math::PI * 2 - @value,true)
189
+ end
190
+
191
+ #Returns: the reverse angle in radians. i.e. angle + PI (or angle + 180 degrees)
192
+ def reverse
193
+ if (v = @value + Math::PI) > Math::PI * 2
194
+ v -= Math::PI * 2
195
+ end
196
+ return self.class.new(v,true)
197
+ end
198
+
199
+
200
+ #Returns: angle in radians as a string.
201
+ def to_s(fmt = nil)
202
+ return to_radians.to_s if(fmt == nil)
203
+ return strf(fmt)
204
+ end
205
+
206
+ #formated output of the angle.
207
+ #The default format is a signed deg°minutes′seconds″ with leading 0's in the minutes and seconds and 4 decimal places for seconds.
208
+ #formats are:
209
+ # * %wd output the degrees as an integer.
210
+ # ** where w is 0, 1, 2 or 3 and represents the field width.
211
+ # *** 1 is the default, which indicates that at least 1 digit is displayed
212
+ # *** 2 indicates that at least 2 digits are displayed. 1 to 9 will be displayed as 01° to 09°
213
+ # *** 3 indicates that at least 4 digits are displayed. 10 to 99 will be displayed as 010° to 099°
214
+ #
215
+ # * %w.pD outputs degrees as a float.
216
+ # ** p is the number of decimal places.
217
+ #
218
+ # * %wm output minutes as an integer.
219
+ # ** where the width w is 0, 1 , 2 with similar meaning to %d. p is again the number of decimal places.
220
+ #
221
+ # * %w.pM outputs minutes as a float .e.g. 01.125′.
222
+ # ** p is the number of decimal places.
223
+ #
224
+ # * %wW outputs secs/60 as a float without the leading '0.'.
225
+ # Used with %m like this %2m′%4W , to get minute marker before the decimal places.
226
+ # e.g. -37°01′.1167 rather than -37°01.1167′
227
+ # ** p is the number of decimal places.
228
+ #
229
+ # * %w.ps output seconds as a float.
230
+ # ** where the width w is 1 , 2 with similar meaning to %d. p is again the number of decimal places.
231
+ #
232
+ # * %N outputs N if the angle is positive and S if the angle is negative.
233
+ #
234
+ # * %E outputs E if the angle is positive and W if the angle is negative.
235
+ #
236
+ # * %r outputs Radians
237
+ #
238
+ # * %% outputs %
239
+ #
240
+ # Other strings in the format are printed as is.
241
+ def strf(fmt="%d°%2m′%2.4s″")
242
+ tokens = fmt.scan(/%[0-9\.]*[%dmsDMNErW]|[^%]*/)
243
+ have_dir = have_dms = false
244
+ tokens.collect! do |t|
245
+ if t[0,1] == '%' #format
246
+ had_dot = false
247
+ decimals = -1
248
+ width = -1
249
+ format = nil
250
+ t[1..-1].scan(/[0-9]+|\.|[0-9]+|[%dmsDMNErW]/) do |t2|
251
+ case t2
252
+ when /[0-9]+/
253
+ if had_dot
254
+ decimals = t2.to_i
255
+ else
256
+ width = t2.to_i
257
+ end
258
+ when '%'
259
+ format = t2
260
+ when /[dmsMwW]/
261
+ have_dms = true
262
+ format = t2
263
+ when /[NE]/
264
+ have_dir = true
265
+ format = t2
266
+ when '.'
267
+ had_dot = true
268
+ when /[Dr]/
269
+ format = t2
270
+ else
271
+ raise "unknown format character '#{t2}'" #shouldn't be able to get here.
272
+ end
273
+ end
274
+ [:format, width, decimals, format]
275
+ else
276
+ [:filler, t]
277
+ end
278
+ end
279
+
280
+ deg,min,sec = to_dms if have_dms
281
+
282
+ s = ""
283
+ tokens.each do |t|
284
+ if(t[0] == :format)
285
+ case t[3]
286
+ when '%'
287
+ s += '%'
288
+ when 'd'
289
+ s += s_int(deg, t[1], have_dir)
290
+ when 'D'
291
+ s += s_float(@value.to_d, t[1], t[2], have_dir)
292
+ when 'm'
293
+ s += s_int(min, t[1], have_dir)
294
+ when 'M'
295
+ s += s_float(min + sec/60, t[1], t[2], have_dir)
296
+ when 'W'
297
+ s += s_only_places(sec/60, t[1])
298
+ when 's'
299
+ s += s_float(sec, t[1], t[2], have_dir)
300
+ when 'r'
301
+ s += s_float(@value, t[1], t[2], have_dir)
302
+ when 'N'
303
+ s += (@value < 0 ? 'S' : 'N')
304
+ when 'E'
305
+ s += (@value < 0 ? 'W' : 'E')
306
+ end
307
+ else
308
+ s += t[1] #the fillers.
309
+ end
310
+ end
311
+
312
+ return s
313
+ end
314
+
315
+ private
316
+ def s_places(v, places)
317
+ if places != -1
318
+ dec_p = (v * 10 ** places).round
319
+ f = ".%0#{places > 0 ? places : ''}d"
320
+ f % dec_p
321
+ else
322
+ '.' + v_dec.to_s[2..-1]
323
+ end
324
+ end
325
+
326
+ def s_only_places(v, places)
327
+ v_int, v_dec = v.abs.divmod(1)
328
+ s_places(v_dec, places)
329
+ end
330
+
331
+ #Prints fixed width decimal portion with leading 0s to get at least the width specified
332
+ #Prints the number of places after the decimal point rounded to places places.
333
+ #-1 width means no width format
334
+ #-1 places means print all decimal places.
335
+ #abs means print the absolute value.
336
+ def s_float(v, width, places, abs)
337
+ v_int, v_dec = v.abs.divmod(1)
338
+ f = "%0#{width > 0 ? width : ''}d"
339
+ s = (abs == false && v.sign == -1) ? '-' : '' #catch the case of -0
340
+ s += f % v.abs + s_places(v_dec, places)
341
+ end
342
+
343
+ def s_int(v, width, abs)
344
+ f = "%0#{width > 0 ? width : ''}d"
345
+ s = (abs == false && v.sign == -1) ? '-' : '' #catch the case of -0
346
+ s += f % v.abs
347
+ end
348
+ end
349
+
350
+
351
+
352
+
353
+
354
+
355
+
356
+
357
+
@@ -0,0 +1,29 @@
1
+ #Holds both latitude and longitude, and the altitude at that point
2
+
3
+ require 'angle.rb'
4
+
5
+ class Coordinate
6
+ attr_accessor :latitude, :longitude, :altitude
7
+ #latitude and longitude can be Strings or Numeric, or anything else with to_radians and to_f
8
+ #latitude and longitude are in degrees unless radians == true
9
+ def initialize(latitude=0, longitude=0, altitude=0, radians = false)
10
+ @latitude = Latitude.new(latitude,radians)
11
+ @longitude = Longitude.new(longitude,radians)
12
+ @altitude = altitude.to_f
13
+ end
14
+
15
+ #Returns: Latitude longitude and altitude as a single string.
16
+ def to_s
17
+ "#{@latitude.to_s } #{@longitude.to_s} #{@altitude}m"
18
+ end
19
+
20
+ def to_ary
21
+ [ @latitude, @longitude, @altitude ]
22
+ end
23
+
24
+ alias to_a to_ary
25
+
26
+ def to_hash
27
+ { :latitude => @latitude, :longitude => @longitude, :altitude => @altitude }
28
+ end
29
+ end
@@ -0,0 +1,89 @@
1
+ require 'scanf'
2
+
3
+ #Extends Numeric, hence Fixed & Float to_r & to_d
4
+ #Also adds in sign.
5
+ class Numeric
6
+ #Convert Radians to Degrees
7
+ #if optional argument mod == true, then applies % 360
8
+ #Returns: degrees
9
+ def to_degrees(mod=false)
10
+ if mod
11
+ (self * 180 / Math::PI) % 360
12
+ else
13
+ self * 180 / Math::PI
14
+ end
15
+ end
16
+ #Converts degrees to Radians
17
+ #if optional argument mod == true, then applies % Math::PI
18
+ #Returns: radians
19
+ def to_radians(mod=false)
20
+ if mod
21
+ (self * Math::PI / 180) % Math::PI
22
+ else
23
+ self * Math::PI / 180
24
+ end
25
+ end
26
+
27
+ alias to_r to_radians
28
+ alias to_d to_degrees
29
+
30
+ #Returns: 1 if number is positive, -1 if negative.
31
+ def sign
32
+ self < 0 ? -1 : 1
33
+ end
34
+
35
+ end
36
+
37
+ #Alters round method to have an optional number of decimal places.
38
+ class Float
39
+ alias round0 round
40
+ #Compatible Replacement for Float.round
41
+ #Optional argument n is the number of decimal places to round to.
42
+ #Returns: float rounded to n decimal places.
43
+ def round(n = 0)
44
+ m = 10.0**n
45
+ (self * m).round0 / m
46
+ end
47
+ end
48
+
49
+ #Extends String to to_dec_degrees, add to_r and to_d
50
+ class String
51
+ #string expected to be degrees, returns decimal degrees.
52
+ #common forms are S37°01′7.5″, 37°01′7.5″S , -37°01′7.5″, -37° 1.512′. -37.01875°, 37°01′.512S, S37°01′.512, ...
53
+ #Returns: angle in decimal degrees
54
+ def to_dec_degrees
55
+ #reorder 37°01′.512S, S37°01′.512 into 37°01.512′S, S37°01.512′ respectively
56
+ s = self.gsub(/([0-9])([′'])\.([0-9]+)/, '\1.\3\2')
57
+ #add in minutes and seconds to get 3 values 'deg 0 0'from S37°, 37°S
58
+ s.gsub!(/^([^0-9\.\-]*)([0-9\-\.]+)([^0-9\-\.]*)$/, '\1\2\3 0 0\5')
59
+ #add in seconds get 3 values 'deg min 0' from S37°1.512′, 37°1.512′S
60
+ s.gsub!(/^([^0-9\.\-]*)([0-9\-\.]+)([^0-9\-\.]+)([0-9\-\.]+)([^0-9\-\.]*)$/, '\1\2\3\4 0\5')
61
+
62
+ #look for anything of the form S37°01′7.5″, S37°1.512′, S37.01875°, ...
63
+ s.scanf("%[NSEW]%f%[^0-9-]%f%[^0-9-]%f") do |direction, deg, sep1, min, sep2, sec|
64
+ return Angle.decimal_deg( deg, min, sec, direction)
65
+ end
66
+
67
+ #look for anything of the form 37°01′7.5″S , -37°01′7.5″, -37° 1.512′. -37.01875°, ...
68
+ s.scanf("%f%[^0-9-]%f%[^0-9-]%f%[^NSEW]%[NSEW]") do |deg, sep1, min, sep2, sec, sep3, direction|
69
+ return Angle.decimal_deg( deg, min, sec, direction)
70
+ end
71
+ end
72
+
73
+ #Convert Radians to Degrees
74
+ #if optional argument mod == true, then applies % 360
75
+ #Returns: degrees
76
+ def to_degrees(mod=false) #string expected to be radians, returns degrees
77
+ self.to_f.to_degrees(mod)
78
+ end
79
+
80
+ #Converts string degrees to to_decimal_degrees, then to Radians
81
+ #if optional argument mod == true, then applies % Math::PI
82
+ #Returns: radians
83
+ def to_radians(mod=false) #string expected to be degrees, returns radians
84
+ self.to_dec_degrees.to_radians(mod)
85
+ end
86
+
87
+ alias to_r to_radians
88
+ alias to_d to_degrees
89
+ end
@@ -0,0 +1,42 @@
1
+ #Subclass of Angle to add in special treatment of to_d, to_r , to_s
2
+
3
+ require 'angle.rb'
4
+
5
+ class Latitude < Angle
6
+ #Latitude degrees are between -90 and 90, South to North
7
+ #Returns angle as degrees in range -90 and 90
8
+ def to_degrees
9
+ #longitude's are -180 to 180 for west to east
10
+ degrees = super
11
+ case
12
+ when degrees > 270 : -(360 - degrees)
13
+ when degrees > 180 : 180 - degrees
14
+ when degrees > 90 : 180 - degrees
15
+ when degrees < -90 : 180 - degrees
16
+ else degrees
17
+ end
18
+ end
19
+
20
+ #Latitude degrees are between -PI and PI, South to North
21
+ #Returns: angle as degrees in range -PI and PI
22
+ def to_radians
23
+ #longitude's are -180 to 180 for west to east
24
+ case
25
+ when @value > 3*Math::PI/2 : @value - Math::PI * 2
26
+ when @value > Math::PI : Math::PI - @value
27
+ when @value > Math::PI/2 : Math::PI - @value
28
+ when @value < -Math::PI/2 : -Math::PI - @value
29
+ else @value
30
+ end
31
+ end
32
+
33
+ #Returns: angle as string in degrees minutes seconds direction.
34
+ #A South angle is negative, North is Positive.
35
+ def to_s(fmt='%2d°%2m′%2.4s″%N')
36
+ super(fmt)
37
+ end
38
+
39
+ alias to_r to_radians
40
+ alias to_d to_degrees
41
+
42
+ end
@@ -0,0 +1,34 @@
1
+ #Subclass of Angle to add in special treatment of to_d, to_r and to_s
2
+
3
+ require 'angle.rb'
4
+
5
+ class Longitude < Angle
6
+ #Longitude degrees are between -180 and 180 West to East
7
+ #Returns angle as degrees in range -180 and 180
8
+ def to_degrees
9
+ degrees = super
10
+ case
11
+ when degrees > 180 : degrees - 360
12
+ else degrees
13
+ end
14
+ end
15
+
16
+ #Longitude degrees are between -2PI and 2PI, West to East
17
+ #Returns: angle as degrees in range -2PI and 2PI
18
+ def to_radians
19
+ case
20
+ when @value > Math::PI : @value - 2 * Math::PI
21
+ else @value
22
+ end
23
+ end
24
+
25
+ #Returns: angle as string in degrees minutes seconds direction.
26
+ #A West angle is negative, East is Positive.
27
+ def to_s(fmt='%3d°%2m′%2.4s″%E')
28
+ super(fmt)
29
+ end
30
+
31
+ alias to_r to_radians
32
+ alias to_d to_degrees
33
+
34
+ end
@@ -0,0 +1,26 @@
1
+ #Holds a bearing and distance
2
+
3
+ require 'angle.rb'
4
+
5
+ class TrackAndDistance
6
+ attr_accessor :bearing, :distance
7
+ #Bearing is in degrees unless radians == true.
8
+ #Bearing can be a String or Numeric or any object with to_radians and to_f
9
+ def initialize(bearing, distance, radians=false)
10
+ @bearing = Angle.new(bearing, radians)
11
+ @distance = distance
12
+ end
13
+
14
+ #Returns: Bearing angle and distance in a string.
15
+ def to_s
16
+ "#{@bearing.to_d.round(4)} #{distance.round(4)}m"
17
+ end
18
+
19
+ def to_ary
20
+ [ @bearing, @distance ]
21
+ end
22
+
23
+ def to_hash
24
+ { :bearing => @bearing, :distance => @distance }
25
+ end
26
+ end
@@ -0,0 +1,187 @@
1
+ #Vincenty's algorithms for finding the bearing and distance between two coordinates and
2
+ #for finding the latitude and longitude, given a start coordinate, distance and bearing.
3
+ #
4
+ # Coded from formulae from Wikipedia http://en.wikipedia.org/wiki/Vincenty%27s_formulae
5
+ # Modified to incorporate corrections to formulae as found in script on http://www.movable-type.co.uk/scripts/LatLongVincenty.html
6
+ # Added my Modification of the distanceAndAngle formulae to correct the compass bearing.
7
+ require 'core_extensions.rb'
8
+ require 'angle.rb'
9
+ require 'latitude.rb'
10
+ require 'longitude.rb'
11
+ require 'track_and_distance.rb'
12
+ require 'coordinate.rb'
13
+
14
+ class Vincenty < Coordinate
15
+ VERSION = '1.0.1'
16
+ #Great Circle formulae http://en.wikipedia.org/wiki/Great-circle_distance
17
+ #Reference calculation for testing, assumes the earth is a sphere, which it isn't.
18
+ #This gives us an approximation to verify Vincenty algorithm.
19
+ #Takes: argument p2 is target coordinate that we want the bearing to.
20
+ #Returns: TrackAndDistance object with the compass bearing and distance in meters to P2
21
+ def sphericalDistanceAndAngle( p2 )
22
+ a = 6378137 #equatorial radius in meters (±2 m)
23
+ b = 6356752.31424518 #polar radius in meters
24
+ r = (a+b)/2 #average diametre as a rough estimate for our tests.
25
+
26
+ sin_lat1 = Math.sin(@latitude.to_r)
27
+ sin_lat2 = Math.sin(p2.latitude.to_r)
28
+ cos_lat1 = Math.cos(@latitude.to_r)
29
+ atan1_2 = Math.atan(1) * 2
30
+ t1 = cos_lat1 * Math.cos(p2.latitude.to_r) * ( Math.cos(@longitude.to_r - p2.longitude.to_r) ) + sin_lat1 * sin_lat2
31
+ angular_distance = Math.atan(-t1/Math.sqrt(-t1 * t1 +1)) + atan1_2 #central angle in radians so we can calculate the arc length.
32
+
33
+ t2 = (sin_lat2 - sin_lat1 * Math.cos(angular_distance)) / (cos_lat1 * Math.sin(angular_distance))
34
+ if(Math.sin(p2.longitude.to_r - @longitude.to_r) < 0)
35
+ bearing = 2 * Math::PI - (Math.atan(-t2 / Math.sqrt(-t2 * t2 + 1)) + atan1_2) #Compass Bearing in radians (clockwise)
36
+ else
37
+ bearing = Math.atan(-t2 / Math.sqrt(-t2 * t2 + 1)) + atan1_2 #Compass Bearing in radians (clockwise)
38
+ end
39
+
40
+ #Note that the bearing is a compass angle. That is angles are positive clockwise.
41
+ return TrackAndDistance.new(bearing, angular_distance * r, true)
42
+ end
43
+
44
+ #Vincenty's algorithm for finding bearing and distance between to coordinates.
45
+ #Assumes earth is a WGS-84 Ellipsod.
46
+ #Takes: argument p2 is target coordinate that we want the bearing to.
47
+ #Returns: TrackAndDistance object with the compass bearing and distance in meters to P2
48
+ def distanceAndAngle( p2 )
49
+ # a, b = major & minor semiaxes of the ellipsoid
50
+ a = 6378137 #equatorial radius in meters (±2 m)
51
+ b = 6356752.31424518 #polar radius in meters
52
+ f = (a-b)/a # flattening
53
+
54
+ lat1 = @latitude.to_r
55
+ lon1 = @longitude.to_r
56
+ lat2 = p2.latitude.to_r
57
+ lon2 = p2.longitude.to_r
58
+ lat1 = lat1.sign * (Math::PI/2-(1e-10)) if (Math::PI/2-lat1.abs).abs < 1.0e-10
59
+ lat2 = lat2.sign * (Math::PI/2-(1e-10)) if (Math::PI/2-lat2.abs).abs < 1.0e-10
60
+
61
+ # lat1, lat2 = geodetic latitude
62
+
63
+ l = (lon2 - lon1).abs #difference in longitude
64
+ l = 2*Math::PI - l if l > Math::PI
65
+ u1 = Math.atan( ( 1 - f) * Math.tan( lat1 ) ) #U is ‘reduced latitude’
66
+ u2 = Math.atan( ( 1 - f) * Math.tan( lat2 ) )
67
+ sin_u1 = Math.sin(u1)
68
+ cos_u1 = Math.cos(u1)
69
+ sin_u2 = Math.sin(u2)
70
+ cos_u2 = Math.cos(u2)
71
+
72
+ lambda_v = l
73
+ lambda_dash = Math::PI * 2
74
+ while( (lambda_v - lambda_dash).abs > 1.0e-12 ) #i.e. 0.06 mm error
75
+ sin_lambda_v = Math.sin(lambda_v)
76
+ cos_lambda_v = Math.cos(lambda_v)
77
+ sin_sigma = Math.sqrt( ( cos_u2 * sin_lambda_v ) ** 2 + ( cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda_v ) ** 2 )
78
+ cos_sigma = sin_u1 * sin_u2 + cos_u1 * cos_u2 * cos_lambda_v
79
+ sigma = Math.atan2(sin_sigma, cos_sigma)
80
+ sin_alpha= cos_u1 * cos_u2 * sin_lambda_v / sin_sigma
81
+ cos_2_alpha = 1 - sin_alpha * sin_alpha #trig identity
82
+ cos_2_sigma_m = cos_sigma - 2 * sin_u1 * sin_u2/cos_2_alpha
83
+ c = f / 16 * cos_2_alpha * (4 + f*(4-3*cos_2_alpha))
84
+ lambda_dash = lambda_v
85
+ lambda_v = l + (1-c) * f * sin_alpha * (sigma + c * sin_sigma * (cos_2_sigma_m + c * cos_sigma * (-1 + 2 * cos_2_sigma_m * cos_2_sigma_m) ) ) # use cos_2_sigma_m=0 when over equatorial lines
86
+ if lambda_v > Math::PI
87
+ lambda_v = Math::PI
88
+ break
89
+ end
90
+ end
91
+
92
+ u_2 = cos_2_alpha * (a * a - b * b) / (b * b)
93
+ a1 = 1 + u_2 / 16384 * (4096 + u_2 * (-768 + u_2 * (320 - 175 * u_2)))
94
+ b1 = u_2 / 1024 * (256 + u_2 * (-128 + u_2 * (74 - 47 * u_2)))
95
+ delta_sigma = b1 * sin_sigma * (cos_2_sigma_m + b1 / 4 * (cos_sigma * (-1 + 2 * cos_2_sigma_m * cos_2_sigma_m) - b1 / 6 * cos_2_sigma_m * (-3 + 4 * sin_sigma * sin_sigma) * (-3 + 4 * cos_2_sigma_m * cos_2_sigma_m)))
96
+ s = b * a1 * (sigma - delta_sigma)
97
+ sin_lambda_v = Math.sin(lambda_v)
98
+ cos_lambda_v = Math.cos(lambda_v)
99
+
100
+ #This test isn't in original formulae, and fixes the problem of all angles returned being between 0 - PI (0-180)
101
+ #Also converts the result to compass bearing, rather than the mathmatical anticlockwise angles.
102
+ if(Math.sin(p2.longitude.to_r - @longitude.to_r) < 0)
103
+ alpha_1 = Math::PI*2-Math.atan2( cos_u2 * sin_lambda_v, cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda_v)
104
+ #alpha_2 = Math::PI*2-Math.atan2(cos_u1 * sin_lambda_v, -sin_u1 * cos_u2 + cos_u1 * sin_u2 * cos_lambda_v)
105
+ else
106
+ alpha_1 = Math.atan2( cos_u2 * sin_lambda_v, cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda_v)
107
+ #alpha_2 = Math.atan2(cos_u1 * sin_lambda_v, -sin_u1 * cos_u2 + cos_u1 * sin_u2 * cos_lambda_v)
108
+ end
109
+
110
+ #Note that the bearing is a compass (i.e. clockwise) angle.
111
+ return TrackAndDistance.new(alpha_1, s, true) #What to do with alpha_2?
112
+ end
113
+
114
+ #spherical earth estimate of calculation for finding target coordinate from start coordinate, bearing and distance
115
+ #Used to run checks on the Vincenty algorithm
116
+ #Takes: TrackAndDistance object with bearing and distance.
117
+ #Returns new Vincenty object with the destination coordinates.
118
+ def sphereDestination( track_and_distance )
119
+ a = 6378137 #equatorial radius in meters (±2 m)
120
+ b = 6356752.31424518 #polar radius in meters
121
+ r = (a+b)/2 #average diametre as a rough estimate for our tests.
122
+
123
+ d = track_and_distance.distance.abs
124
+ sin_dor = Math.sin(d/r)
125
+ cos_dor = Math.cos(d/r)
126
+ sin_lat1 = Math.sin(@latitude.to_r)
127
+ cos_lat1 = Math.cos(@latitude.to_r)
128
+ lat2 = Math.asin( sin_lat1 * cos_dor + cos_lat1 * sin_dor * Math.cos(track_and_distance.bearing.to_r) )
129
+ lon2 = @longitude.to_r + Math.atan2(Math.sin(track_and_distance.bearing.to_r) * sin_dor * cos_lat1, cos_dor-sin_lat1 * Math.sin(lat2))
130
+
131
+ Vincenty.new(lat2, lon2, 0, true);
132
+ end
133
+
134
+ #
135
+ # Calculate destination point given start point lat/long, bearing and distance.
136
+ #Assumes earth is a WGS-84 Ellipsod.
137
+ #Takes: TrackAndDistance object with bearing and distance.
138
+ #Returns: new Vincenty object with the destination coordinates.
139
+
140
+ def destination( track_and_distance )
141
+ # a, b = major & minor semiaxes of the ellipsoid
142
+ a = 6378137 #equatorial radius in meters (±2 m)
143
+ b = 6356752.31424518 #polar radius in meters
144
+ f = (a-b)/a # flattening
145
+
146
+ s = track_and_distance.distance.abs;
147
+ alpha1 = track_and_distance.bearing.to_r
148
+ sin_alpha1 = Math.sin(alpha1)
149
+ cos_alpha1 = Math.cos(alpha1)
150
+
151
+ tanU1 = (1-f) * Math.tan(@latitude.to_r);
152
+ cosU1 = 1 / Math.sqrt((1 + tanU1 * tanU1))
153
+ sinU1 = tanU1 * cosU1
154
+ sigma1 = Math.atan2(tanU1, cos_alpha1)
155
+ sin_alpha = cosU1 * sin_alpha1
156
+ cos_2_alpha = 1 - sin_alpha * sin_alpha #Trig identity
157
+ u_2 = cos_2_alpha * (a * a - b * b) / (b * b)
158
+ a1 = 1 + u_2/16384 * (4096 + u_2 * (-768 + u_2 * (320-175 * u_2)))
159
+ b1 = u_2/1024 * (256 + u_2 * (-128 + u_2 * (74-47 * u_2)))
160
+
161
+ sigma = s / (b * a1)
162
+ sigma_dash = 2 * Math::PI
163
+ while ((sigma-sigma_dash).abs > 1.0e-12) #i.e 0.06mm
164
+ cos_2_sigma_m = Math.cos(2 * sigma1 + sigma)
165
+ sin_sigma = Math.sin(sigma)
166
+ cos_sigma = Math.cos(sigma)
167
+ delta_sigma = b1 * sin_sigma * (cos_2_sigma_m + b1/4 * (cos_sigma * (-1 + 2 * cos_2_sigma_m * cos_2_sigma_m) - b1/6 * cos_2_sigma_m * (-3 + 4 * sin_sigma * sin_sigma) * (-3 + 4 * cos_2_sigma_m * cos_2_sigma_m)))
168
+ sigma_dash = sigma
169
+ sigma = s / (b * a1) + delta_sigma
170
+ end
171
+
172
+ tmp = sinU1 * sin_sigma - cosU1 * cos_sigma * cos_alpha1
173
+ lat2 = Math.atan2(sinU1 * cos_sigma + cosU1 * sin_sigma * cos_alpha1, (1-f) * Math.sqrt(sin_alpha * sin_alpha + tmp * tmp))
174
+ lambda_v = Math.atan2(sin_sigma * sin_alpha1, cosU1 * cos_sigma - sinU1 * sin_sigma * cos_alpha1)
175
+ c = f/16 * cos_2_alpha * (4 + f * (4-3 * cos_2_alpha))
176
+ l = lambda_v - (1-c) * f * sin_alpha * (sigma + c * sin_sigma * (cos_2_sigma_m + c * cos_sigma * (-1 + 2 * cos_2_sigma_m * cos_2_sigma_m))) #difference in longitude
177
+
178
+ #sigma2 = Math.atan2(sin_alpha, -tmp) # reverse azimuth
179
+
180
+ return Vincenty.new(lat2, @longitude + l, 0, true);
181
+ end
182
+
183
+
184
+ end
185
+
186
+
187
+
@@ -0,0 +1,4 @@
1
+ require 'ts_angle.rb'
2
+ require 'ts_vincenty.rb'
3
+ require 'latitude.rb'
4
+ require 'longitude.rb'
@@ -0,0 +1,73 @@
1
+ require 'test/unit'
2
+ require 'vincenty.rb'
3
+
4
+ class TestAngle< Test::Unit::TestCase
5
+ #test Angle creation
6
+ def test_angle
7
+ assert_equal(Angle.new(), 0)
8
+ assert_equal(Angle.new("S37°01′7.5″").to_d, -37.01875) #Leading NSEW
9
+ assert_equal(Angle.new("37°01′7.5″S").to_d , -37.01875) #Trailing NSEW
10
+ assert_equal(Angle.new("-37°01′7.5″").to_d, -37.01875) #Use of - rather than S or W
11
+ assert_equal(Angle.new("-37° 1.125′").to_d, -37.01875) #Decimal minutes, rather than minutes and seconds.
12
+ assert_equal(Angle.new("-37°01′.125").to_d, -37.01875) #Nb. the minute marker ' between the minutes and fraction
13
+ assert_equal(Angle.new("S37°01′.125").to_d, -37.01875) #Nb. the minute marker ' between the minutes and fraction
14
+ assert_equal(Angle.new("37°01′.125S").to_d, -37.01875) #Nb. the minute marker ' between the minutes and fraction
15
+ assert_equal(Angle.new("-37.01875°").to_d, -37.01875) #decimal degrees, rather than deg, min, sec.
16
+ assert_equal(Angle.new([-37, 1, 7.5]).to_d, -37.01875) #an array of deg,min,sec
17
+ assert_equal(Angle.new(-37.01875).to_d, -37.01875)
18
+ assert_equal(Angle.degrees(-37.01875).to_d, -37.01875)
19
+ assert_equal(Angle.degrees(-37.01875).to_r.round(15), -0.646099072472651)
20
+ assert_equal(Angle.new(-0.646099072472651, true).to_d.round(5), -37.01875)
21
+ assert_equal(Angle.new(-0.646099072472651, true).to_r, -0.646099072472651)
22
+ assert_equal(Angle.radians(-0.646099072472651).to_d.round(5), -37.01875)
23
+ end
24
+
25
+ def test_strf
26
+ a = Angle.new("S37°01′7.5″")
27
+ assert_equal("37°01′07.50000″S", a.strf( "%d°%2m′%2.5s″%N" ))
28
+ assert_equal("37°01′07.50000″W", a.strf("%d°%2m′%2.5s″%E" ))
29
+ assert_equal("-37°01′07.5000″", a.strf("%d°%2m′%2.4s″" ))
30
+ assert_equal("-37°01.1250′\n", a.strf("%d°%2.4M′\n" ))
31
+ assert_equal("*** -37°01′.1250", a.strf( "*** %d°%2m′%4W" )) #puting the minute ' before decimal point.
32
+ assert_equal("-37.01875°", a.strf("%0.5D°" ))
33
+ assert_equal("-0.64610 radians\n", a.strf("%0.5r radians\n" ))
34
+
35
+ assert_equal("-037°01′7.5000″", Angle.new("-37°01′7.5″").to_s('%3d°%2m′%1.4s″')) #testing leading 0 with -deg, no leading 0 %s
36
+ assert_equal("00°01′07.5000″S", Angle.new("0°01′7.5″S").to_s('%2d°%2m′%2.4s″%N')) #testing 0 degrees and leading 0 %s
37
+ assert_equal("00°-01′07.5000″", Angle.new("0°01′7.5″S").to_s('%2d°%2m′%2.4s″')) #testing 0 degrees and -min
38
+ assert_equal("00°-01′07.5000″", Angle.new("0°01′7.5″S").to_s('%2d°%2m′%2.4s″') ) #test of 0 degrees, -min, no NSEW
39
+ assert_equal("000°00′07.5000″W", Angle.new("0°0′7.5″W").to_s('%3d°%2m′%2.4s″%E') ) #testing E W 0 deg and 0 min and -sec
40
+ assert_equal("00°00′-07.5000″", Angle.new("0°0′7.5″S").to_s('%2d°%2m′%2.4s″') ) #testing 0 deg and 0 min and -sec no NSEW
41
+ end
42
+
43
+ def test_operators
44
+ #Comparable.
45
+ assert_equal(Angle.radians(-0.646099072472651), Angle.radians(-0.646099072472651)) #<=>
46
+ #unary-op Angle
47
+ assert_equal(+Angle.radians(-0.646099072472651), Angle.radians(-0.646099072472651)) #unary +
48
+ assert_equal(-Angle.radians(-0.646099072472651), Angle.radians(0.646099072472651)) #unary -
49
+ #Angle op Numeric
50
+ assert_equal(5, Angle.radians(2) + 3) # +
51
+ assert_equal(-1, Angle.radians(2) - 3) # -
52
+ assert_equal(6, Angle.radians(2) * 3) # *
53
+ assert_equal(2, Angle.radians(4) /2) # /
54
+ assert_equal(1, Angle.radians(4) % 3) # %
55
+ assert_equal(64, Angle.radians(4) ** 3) # **
56
+ #Numeric op Angle
57
+ assert_equal(5.1, 3.1 + Angle.radians(2) ) # +
58
+ assert_equal(2.646099072472651, 2 - Angle.radians(-0.646099072472651) ) # -
59
+ assert_equal(6, 3 * Angle.radians(2) ) # *
60
+ assert_equal(2, 4 / Angle.radians(2) ) # /
61
+ #Angle op Angle
62
+ assert_equal(Angle.radians(3.2+2.1), Angle.radians(3.2) + Angle.radians(2.1) ) # +
63
+ #Sign method.
64
+ assert_equal(1, Angle.radians(3).sign)
65
+ assert_equal(-1, Angle.radians(-3).sign)
66
+ #abs
67
+ assert_equal(3, Angle.radians(-3).abs)
68
+ #reverse
69
+ assert_equal(Angle.degrees(90), Angle.degrees(270).reverse)
70
+ #bearing
71
+ assert_equal(Angle.degrees(340), Angle.degrees(20).to_bearing)
72
+ end
73
+ end
@@ -0,0 +1,31 @@
1
+ require 'test/unit'
2
+ require 'vincenty.rb'
3
+
4
+ class TestAngle< Test::Unit::TestCase
5
+ def test_strf
6
+ assert_equal("37°01′07.5000″S", Latitude.new("S37°01′7.5″").to_s)
7
+ assert_equal("37°01′07.5000″S", Latitude.new("-37°01′7.5″").to_s)
8
+ assert_equal("37°01′07.5000″S", Latitude.new("37°01′7.5″S").to_s)
9
+ assert_equal("37°01′07.5000″N", Latitude.new("N37°01′7.5″").to_s)
10
+ assert_equal("37°01′07.5000″N", Latitude.new("37°01′7.5″").to_s)
11
+ assert_equal("37°01′07.5000″N", Latitude.new("37°01′7.5″N").to_s)
12
+ end
13
+ def test_to_radians
14
+ assert_equal(Math::PI/4, Latitude.degrees(45).to_r)
15
+ assert_equal(Math::PI/4, Latitude.degrees(135).to_r)
16
+ assert_equal(-Math::PI/4, Latitude.degrees(225).to_r)
17
+ assert_equal(-Math::PI/4, Latitude.degrees(315).to_r)
18
+ end
19
+ def test_to_degrees
20
+ assert_equal(45, Latitude.degrees(45).to_d)
21
+ assert_equal(45, Latitude.degrees(135).to_d)
22
+ assert_equal(-45, Latitude.degrees(225).to_d)
23
+ assert_equal(-45, Latitude.degrees(315).to_d)
24
+ assert_equal(1, Latitude.degrees(179).to_d)
25
+ assert_equal(-1, Latitude.degrees(181).to_d)
26
+ assert_equal(-89, Latitude.degrees(269).to_d)
27
+ assert_equal(-89, Latitude.degrees(271).to_d)
28
+ assert_equal(89, Latitude.degrees(89).to_d)
29
+ assert_equal(89, Latitude.degrees(91).to_d)
30
+ end
31
+ end
@@ -0,0 +1,25 @@
1
+ require 'test/unit'
2
+ require 'vincenty.rb'
3
+
4
+ class TestAngle< Test::Unit::TestCase
5
+ def test_strf
6
+ assert_equal("037°01′07.5000″W", Longitude.new("W37°01′7.5″").to_s)
7
+ assert_equal("037°01′07.5000″W", Longitude.new("-37°01′7.5″").to_s)
8
+ assert_equal("037°01′07.5000″W", Longitude.new("37°01′7.5″W").to_s)
9
+ assert_equal("037°01′07.5000″E", Longitude.new("E37°01′7.5″").to_s)
10
+ assert_equal("037°01′07.5000″E", Longitude.new("37°01′7.5″").to_s)
11
+ assert_equal("037°01′07.5000″E", Longitude.new("37°01′7.5″E").to_s)
12
+ end
13
+ def test_to_radians
14
+ assert_equal(Math::PI/4, Longitude.degrees(45).to_r)
15
+ assert_equal(3*Math::PI/4, Longitude.degrees(135).to_r)
16
+ assert_equal(-3*Math::PI/4, Longitude.degrees(225).to_r)
17
+ assert_equal(-Math::PI/4, Longitude.degrees(315).to_r)
18
+ end
19
+ def test_to_degrees
20
+ assert_equal(45, Longitude.degrees(45).to_d)
21
+ assert_equal(135, Longitude.degrees(135).to_d)
22
+ assert_equal(-135, Longitude.degrees(225).to_d)
23
+ assert_equal(-45, Longitude.degrees(315).to_d)
24
+ end
25
+ end
@@ -0,0 +1,96 @@
1
+ require 'test/unit'
2
+ require 'vincenty'
3
+
4
+ class TestVincenty< Test::Unit::TestCase
5
+
6
+ def initialize(x)
7
+ super(x)
8
+
9
+ @path = [ #Path starting at peg by kanaka at end of drive
10
+ TrackAndDistance.new("215,3,0", 19.73 ) ,
11
+ TrackAndDistance.new(Angle.new("320,14,10").reverse, 12.0), #Note don't need to add the radians=true argument as Angle has to_radians function
12
+ TrackAndDistance.new(Angle.new("281,44,40").reverse, 35.23 ),
13
+ TrackAndDistance.new(Angle.new("247,24,0").reverse, 40.23 ),
14
+ TrackAndDistance.new(Angle.new("218,19,0").reverse, 378.98 ),
15
+ TrackAndDistance.new(Angle.new("158,25,0").reverse, 128.39 ),
16
+ TrackAndDistance.new(Angle.new("17,7,40").reverse, 122.41 ),
17
+ TrackAndDistance.new(Angle.new("51,1,0").reverse, 288.89 ),
18
+ TrackAndDistance.new("158,47,30", 61.78 ),
19
+ TrackAndDistance.new("189,16,10", 26.26 ),
20
+ TrackAndDistance.new("217,14,0", 21.87 ),
21
+ ]
22
+
23
+ @waypoints = [
24
+ Vincenty.new(-36.9923293459124, 174.485341187381),
25
+ Vincenty.new(-36.992412464006, 174.485427409127),
26
+ Vincenty.new(-36.9924770796644, 174.485814875954),
27
+ Vincenty.new(-36.9923377696042, 174.486232091137),
28
+ Vincenty.new(-36.9896584018239, 174.488871503953),
29
+ Vincenty.new(-36.988582616694, 174.488340992344),
30
+ Vincenty.new(-36.9896367145752, 174.487936042043),
31
+ Vincenty.new(-36.9912743090293, 174.48541348615),
32
+ Vincenty.new(-36.9917932943506, 174.485664544705),
33
+ Vincenty.new(-36.9920268289562, 174.485617028991),
34
+ Vincenty.new(-36.9921837292671, 174.485468381511),
35
+ ]
36
+ end
37
+
38
+ #The path in @path was entered from the property survey map, with distance and bearings which should form a closed loop
39
+ #verified on google map of my property by creating a KML file and loading the map over the satellite image and checking the
40
+ #coordinates in google earth, and visually checking the route created was a closed loop (it was with a tiny error).
41
+ def test_vincenty_destination
42
+ start = Vincenty.new(-36.9921838030711, 174.485468469841)
43
+
44
+ next_p = start
45
+ # print "Start at coordinate #{next_p.longitude.to_d}, #{next_p.latitude.to_d}\n"
46
+ @path.each_with_index do |leg,i|
47
+ next_p, spherical_ans = next_p.destination( leg ) , next_p.sphereDestination(leg)
48
+
49
+ assert_equal(@waypoints[i].longitude.to_d.round(12), next_p.longitude.to_d.round(12))
50
+ assert_equal(@waypoints[i].latitude.to_d.round(12), next_p.latitude.to_d.round(12))
51
+ # print "Expect #{waypoints[i].longitude.to_d.round(4)}, #{waypoints[i].latitude.to_d.round(4)}\n"
52
+ # print "Moved #{leg.bearing.to_d.round(4)} #{leg.distance.round(4)}m to #{next_p.longitude.to_d.round(4)}, #{next_p.latitude.to_d.round(4)}\n"
53
+ # print "Spherical #{leg.bearing.to_d.round(4)} #{leg.distance.round(4)}m to #{spherical_ans.longitude.to_d.round(4)}, #{spherical_ans.latitude.to_d.round(4)}\n"
54
+ # puts
55
+ end
56
+ # assert_equal(0, next_p.distanceAndAngle(start).distance)
57
+ # puts "distance from end to start should be 0. Actual #{next_p.distanceAndAngle(start)}"
58
+ end
59
+
60
+ #The waypoints are the latitudes and longitudes of the corners of my property.
61
+ #The resulting bearing and distances between them should match those in @path.
62
+ def test_vincenty_distance_and_angle
63
+ start = Vincenty.new(-36.9921838030711, 174.485468469841)
64
+ next_p = start
65
+ # print "\nReverse test, c\n"
66
+ # print "Start at coordinate #{next_p.longitude.to_d}, #{next_p.latitude.to_d}\n"
67
+ @waypoints.each_with_index do |point,i|
68
+ vtrack_and_bearing = next_p.distanceAndAngle( point )
69
+ # strack_and_bearing = next_p.sphericalDistanceAndAngle( point )
70
+
71
+ assert_equal(@path[i].bearing.to_d.round(4), vtrack_and_bearing.bearing.to_d.round(4))
72
+ assert_equal(@path[i].distance.round(4), vtrack_and_bearing.distance.round(4))
73
+ # print "Expected #{path[i].bearing.to_d.round(4)}(#{((path[i].bearing.to_d+180)%360).round(4)}), #{path[i].distance.round(4)}m\n"
74
+ # print "WGS-84 track #{vtrack_and_bearing.bearing.to_d.round(4)} #{vtrack_and_bearing.distance.round(4)}m from #{next_p.longitude.to_d.round(4)}, #{next_p.latitude.to_d.round(4)} to #{point.longitude.to_d.round(4)}, #{point.latitude.to_d.round(4)}\n"
75
+ # print "Spherical track #{strack_and_bearing.bearing.to_d.round(4)} #{strack_and_bearing.distance.round(4)}m from #{next_p.longitude.to_d.round(4)}, #{next_p.latitude.to_d.round(4)} to #{point.longitude.to_d.round(4)}, #{point.latitude.to_d.round(4)}\n"
76
+ # puts
77
+ next_p = point
78
+ end
79
+ # assert_equal(0, next_p.distanceAndAngle(start).distance)
80
+ # puts "distance from end to start should be 0. Actual #{next_p.distanceAndAngle(start)}\n"
81
+ end
82
+
83
+ #Run the Australian Geoscience site example.
84
+ def test_geoscience_au
85
+ flindersPeak = Vincenty.new("-37°57'3.72030″", "144°25'29.52440″" )
86
+ buninyong = Vincenty.new("-37 ° 39 ' 10.15610 ''", "143 ° 55 ' 35.38390 ''") #Buninyong
87
+ track_and_bearing = flindersPeak.distanceAndAngle( buninyong )
88
+ assert_equal(Angle.new("306 ° 52 ' 5.37 ''").to_d.round(4), track_and_bearing.bearing.to_d.round(4))
89
+ assert_equal(54972.271, track_and_bearing.distance.round(3))
90
+
91
+ destination = flindersPeak.destination(TrackAndDistance.new("306 ° 52 ' 5.37 ''", 54972.271))
92
+ assert_equal(buninyong.latitude.to_d.round(4), destination.latitude.to_d.round(4))
93
+ assert_equal(buninyong.longitude.to_d.round(4), destination.longitude.to_d.round(4))
94
+ end
95
+
96
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vincenty
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Rob Burrowes
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-03 00:00:00 +13:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.9.0
24
+ version:
25
+ description: "* Vincenty wrote an algorithm for calculating the bearing and distance between two coordinates on the earth and an algorithm for finding a second coordinate, given a starting coordinate, bearing and destination. The algorithms model the earth as an ellipsoid, using the WGS-84 model. This is the common GPS model for mapping to latitudes and longitudes. This is a Ruby implementation of Vincenty's algorithms, and the Vincenty class includes two methods for modeling the earth as a sphere. These were added as a reference for testing the Vincenty algorithm, but could be used on their own. The package also makes use of several other classes that may be useful in their own Right. These include class Angle, class Latitude (subclass of Angle), class Longitude (subclass of Angle), class TrackAndBearing and class coordinate (which class Vincenty is a subclass) Angle requires extensions to Numeric and String to provide to_radians (to_r) and to_degrees (to_d). String also includes a to_decimal_degrees(), which converts most string forms of Latitude and Longitude to decimal form. These extensions are included in the package in core_extensions.rb. Float has also been extended to change round to have an optional argument specifying the number of decimal places to round to. This is fully compatible with the Float.round, as the default is to round to 0 decimal places. * The Vincenty code is based on the wikipedia presentation of the Vincenty algorithm http://en.wikipedia.org/wiki/Vincenty%27s_formulae . * The algorithm was modified to include changes I found at http://www.movable-type.co.uk/scripts/latlong-vincenty-direct.html. * I also altered the formulae to correctly return the bearing for angles greater than 180. * Vincenty's original publication ** T Vincenty, \"Direct and Inverse Solutions of Geodesics on the Ellipsoid with application of nested equations\", Survey Review, vol XXII no 176, 1975 http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf"
26
+ email:
27
+ - rob@burrowes.org
28
+ executables: []
29
+
30
+ extensions: []
31
+
32
+ extra_rdoc_files:
33
+ - History.txt
34
+ - Manifest.txt
35
+ - README.txt
36
+ files:
37
+ - History.txt
38
+ - Manifest.txt
39
+ - README.txt
40
+ - Rakefile
41
+ - lib/angle.rb
42
+ - lib/coordinate.rb
43
+ - lib/core_extensions.rb
44
+ - lib/latitude.rb
45
+ - lib/longitude.rb
46
+ - lib/track_and_distance.rb
47
+ - lib/vincenty.rb
48
+ - test/ts_all.rb
49
+ - test/ts_angle.rb
50
+ - test/ts_latitude.rb
51
+ - test/ts_longitude.rb
52
+ - test/ts_vincenty.rb
53
+ has_rdoc: true
54
+ homepage: "\"http://rubyforge.org/projects/vincenty/\""
55
+ post_install_message:
56
+ rdoc_options:
57
+ - --main
58
+ - README.txt
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: "0"
72
+ version:
73
+ requirements: []
74
+
75
+ rubyforge_project: vincenty
76
+ rubygems_version: 1.3.1
77
+ signing_key:
78
+ specification_version: 2
79
+ summary: "* Vincenty wrote an algorithm for calculating the bearing and distance between two coordinates on the earth and an algorithm for finding a second coordinate, given a starting coordinate, bearing and destination"
80
+ test_files: []
81
+