vincenty 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +6 -0
- data/Manifest.txt +16 -0
- data/README.txt +104 -0
- data/Rakefile +15 -0
- data/lib/angle.rb +357 -0
- data/lib/coordinate.rb +29 -0
- data/lib/core_extensions.rb +89 -0
- data/lib/latitude.rb +42 -0
- data/lib/longitude.rb +34 -0
- data/lib/track_and_distance.rb +26 -0
- data/lib/vincenty.rb +187 -0
- data/test/ts_all.rb +4 -0
- data/test/ts_angle.rb +73 -0
- data/test/ts_latitude.rb +31 -0
- data/test/ts_longitude.rb +25 -0
- data/test/ts_vincenty.rb +96 -0
- metadata +81 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -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
|
data/README.txt
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
+
|
data/lib/angle.rb
ADDED
@@ -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
|
+
|
data/lib/coordinate.rb
ADDED
@@ -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
|
data/lib/latitude.rb
ADDED
@@ -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
|
data/lib/longitude.rb
ADDED
@@ -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
|
data/lib/vincenty.rb
ADDED
@@ -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
|
+
|
data/test/ts_all.rb
ADDED
data/test/ts_angle.rb
ADDED
@@ -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
|
data/test/ts_latitude.rb
ADDED
@@ -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
|
data/test/ts_vincenty.rb
ADDED
@@ -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
|
+
|