sgslib 0.1.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/sgs/course.rb +178 -0
- data/lib/sgs/location.rb +41 -34
- data/lib/sgs/mission.rb +231 -222
- data/lib/sgs/nmea.rb +4 -4
- data/lib/sgs/redis_base.rb +1 -0
- data/lib/sgs/version.rb +1 -1
- data/lib/sgs/waypoint.rb +69 -38
- data/lib/sgslib.rb +2 -2
- metadata +4 -4
- data/lib/sgs/polar.rb +0 -86
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a552ab707d5da785deb99f2b4118c2e3df9ebfe2
|
4
|
+
data.tar.gz: 36f82fa588da1882b67f32a8cff7709e4f4ca3c9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1aec2712b93140cb27373d170dcbc5a9371ff7d451055752ca8c9c323d59080591f7012b790179c7abd07bd87dd9ec809b72033f374e172a25a54f67f88d74e8
|
7
|
+
data.tar.gz: 991805459fe5a66e8c2efc0be306a1d368abb1bedb76ab20bbfc73139aa4bfc0f7ca216844ede153e8529e5aebf6093fe99b4b91548db0dce501fb36d48eb6b9
|
data/lib/sgs/course.rb
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2013, Kalopa Research. All rights reserved. This is free
|
3
|
+
# software; you can redistribute it and/or modify it under the terms of the
|
4
|
+
# GNU General Public License as published by the Free Software Foundation;
|
5
|
+
# either version 2, or (at your option) any later version.
|
6
|
+
#
|
7
|
+
# It is distributed in the hope that it will be useful, but WITHOUT
|
8
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
9
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
10
|
+
# for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU General Public License along
|
13
|
+
# with this product; see the file COPYING. If not, write to the Free
|
14
|
+
# Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
|
15
|
+
#
|
16
|
+
# THIS SOFTWARE IS PROVIDED BY KALOPA RESEARCH "AS IS" AND ANY EXPRESS OR
|
17
|
+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
18
|
+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
19
|
+
# IN NO EVENT SHALL KALOPA RESEARCH BE LIABLE FOR ANY DIRECT, INDIRECT,
|
20
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
21
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
22
|
+
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
23
|
+
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
24
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
25
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
26
|
+
#
|
27
|
+
|
28
|
+
##
|
29
|
+
# Routines for handling sailboat navigation and route planning.
|
30
|
+
#
|
31
|
+
# The code on this page was derived from formulae on the Movable Type site:
|
32
|
+
# http://www.movable-type.co.uk/scripts/latlong.html
|
33
|
+
#
|
34
|
+
|
35
|
+
require 'date'
|
36
|
+
|
37
|
+
module SGS
|
38
|
+
##
|
39
|
+
#
|
40
|
+
# A class to handle the course sailed, as well as polar speed calculations.
|
41
|
+
# For speed calculations, it takes a range of polars as polynomials and
|
42
|
+
# then applies them.
|
43
|
+
class Course
|
44
|
+
attr_reader :awa, :speed
|
45
|
+
attr_writer :polar_curve
|
46
|
+
|
47
|
+
TACK_NAME = ["Starboard", "Port"].freeze
|
48
|
+
STARBOARD = 0
|
49
|
+
PORT = 1
|
50
|
+
|
51
|
+
#
|
52
|
+
# Right now, we have one polar - from a Catalina 22.
|
53
|
+
# Note that the speed is the same, regardless of the tack.
|
54
|
+
STANDARD = [
|
55
|
+
-3.15994,
|
56
|
+
23.8741,
|
57
|
+
-27.4595,
|
58
|
+
16.4868,
|
59
|
+
-5.15663,
|
60
|
+
0.743936,
|
61
|
+
-0.0344716
|
62
|
+
].freeze
|
63
|
+
|
64
|
+
#
|
65
|
+
# Set up the default values
|
66
|
+
def initialize(wind = nil)
|
67
|
+
@polar_curve = STANDARD
|
68
|
+
@awa = 0.0
|
69
|
+
@speed = 0.0
|
70
|
+
@wind = wind || Bearing.new(0.0, 10.0)
|
71
|
+
@heading = nil
|
72
|
+
self.heading = 0
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# Return the current heading
|
77
|
+
def heading
|
78
|
+
@heading
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Return the heading in degrees
|
83
|
+
def heading_d
|
84
|
+
Bearing.rtod @heading
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Return the wind direction/speed
|
89
|
+
def wind
|
90
|
+
@wind
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Return the Apparent Wind Angle (AWA) in degrees
|
95
|
+
def awa_d
|
96
|
+
Bearing.rtod @awa
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Return the current tack
|
101
|
+
def tack
|
102
|
+
(@awa and @awa < 0.0) ? PORT : STARBOARD
|
103
|
+
end
|
104
|
+
|
105
|
+
#
|
106
|
+
# Return the tack name
|
107
|
+
def tack_name
|
108
|
+
TACK_NAME[tack]
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Set the heading
|
113
|
+
def heading=(new_heading)
|
114
|
+
return if @heading and @heading == new_heading
|
115
|
+
if new_heading > 2*Math::PI
|
116
|
+
new_heading -= 2*Math::PI
|
117
|
+
elsif new_heading < 0.0
|
118
|
+
new_heading += 2*Math::PI
|
119
|
+
end
|
120
|
+
@heading = new_heading
|
121
|
+
self.awa = @wind.angle - @heading
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# Set the wind direction and recompute the AWA if appropriate. Note
|
126
|
+
# that we don't care about wind speed (for now)
|
127
|
+
def wind=(new_wind)
|
128
|
+
return if @wind and @wind.angle == new_wind.angle
|
129
|
+
@wind = new_wind
|
130
|
+
self.awa = @wind.angle - @heading
|
131
|
+
end
|
132
|
+
|
133
|
+
#
|
134
|
+
# Calculate the AWA based on our heading and wind direction
|
135
|
+
def awa=(new_awa)
|
136
|
+
if new_awa < -Math::PI
|
137
|
+
new_awa += 2*Math::PI
|
138
|
+
elsif new_awa > Math::PI
|
139
|
+
new_awa -= 2*Math::PI
|
140
|
+
end
|
141
|
+
return if @awa == new_awa
|
142
|
+
@awa = new_awa
|
143
|
+
compute_speed
|
144
|
+
end
|
145
|
+
|
146
|
+
#
|
147
|
+
# Compute a relative VMG based on the waypoint
|
148
|
+
def relative_vmg(waypt)
|
149
|
+
relvmg = @speed * Math::cos(waypt.bearing.angle - @heading) / waypt.distance
|
150
|
+
puts "Relative VMG to WPT: #{waypt.name} is #{relvmg}"
|
151
|
+
relvmg
|
152
|
+
end
|
153
|
+
|
154
|
+
#
|
155
|
+
# Compute the hull speed from the polar. This is just a guestimate of how
|
156
|
+
# fast the boat will travel at the particular apparent wind angle.
|
157
|
+
def compute_speed
|
158
|
+
awa = @awa.abs
|
159
|
+
return 0.0 if awa < 0.75
|
160
|
+
ap = 1.0
|
161
|
+
@speed = 0.0
|
162
|
+
@polar_curve.each do |poly_val|
|
163
|
+
@speed += poly_val * ap
|
164
|
+
ap *= awa
|
165
|
+
end
|
166
|
+
@speed /= 2.5 # Fudge for small boat
|
167
|
+
if @speed < 0.0
|
168
|
+
@speed = 0.0
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
#
|
173
|
+
# Convert to a string
|
174
|
+
def to_s
|
175
|
+
"Heading %dd (wind %.1f@%dd, AWA:%dd, speed=%.2fknots)" % [heading_d, wind.distance, wind.angle_d, awa_d, speed]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
data/lib/sgs/location.rb
CHANGED
@@ -32,6 +32,11 @@ require 'date'
|
|
32
32
|
require 'json'
|
33
33
|
|
34
34
|
module SGS
|
35
|
+
#
|
36
|
+
# Nominal radius of the planet, in nautical miles.
|
37
|
+
# http://en.wikipedia.org/wiki/Earth_radius#Mean_radii
|
38
|
+
EARTH_RADIUS = 3440.069528437724
|
39
|
+
|
35
40
|
##
|
36
41
|
#
|
37
42
|
# Class for dealing with latitude/longitude. Includes methods for parsing,
|
@@ -43,11 +48,6 @@ module SGS
|
|
43
48
|
class Location
|
44
49
|
attr_accessor :latitude, :longitude
|
45
50
|
|
46
|
-
#
|
47
|
-
# Nominal radius of the planet, in nautical miles.
|
48
|
-
# http://en.wikipedia.org/wiki/Earth_radius#Mean_radii
|
49
|
-
EARTH_RADIUS = 3440.069528437724
|
50
|
-
|
51
51
|
#
|
52
52
|
# Create the Location instance.
|
53
53
|
def initialize(lat = nil, long = nil)
|
@@ -55,6 +55,13 @@ module SGS
|
|
55
55
|
@longitude = long.to_f if long
|
56
56
|
end
|
57
57
|
|
58
|
+
#
|
59
|
+
# The difference between two locations is a Bearing
|
60
|
+
def -(loc)
|
61
|
+
puts "Distance from #{self} to #{loc}"
|
62
|
+
Bearing.compute(self, loc)
|
63
|
+
end
|
64
|
+
|
58
65
|
#
|
59
66
|
# Calculate a new position from the current position
|
60
67
|
# given a bearing (angle and distance)
|
@@ -66,18 +73,18 @@ module SGS
|
|
66
73
|
# Math.cos(lat1)*Math.sin(d/R)*Math.cos(angle) );
|
67
74
|
# var lon2 = lon1 + Math.atan2(Math.sin(angle)*Math.sin(d/R)*Math.cos(lat1),
|
68
75
|
# Math.cos(d/R)-Math.sin(lat1)*Math.sin(lat2));
|
69
|
-
def
|
76
|
+
def +(bearing)
|
70
77
|
loc = Location.new
|
71
78
|
sin_angle = Math.sin(bearing.angle)
|
72
79
|
cos_angle = Math.cos(bearing.angle)
|
73
|
-
sin_dstr = Math.sin(bearing.distance / EARTH_RADIUS)
|
74
|
-
cos_dstr = Math.cos(bearing.distance / EARTH_RADIUS)
|
80
|
+
sin_dstr = Math.sin(bearing.distance / SGS::EARTH_RADIUS)
|
81
|
+
cos_dstr = Math.cos(bearing.distance / SGS::EARTH_RADIUS)
|
75
82
|
sin_lat1 = Math.sin(@latitude)
|
76
83
|
cos_lat1 = Math.cos(@latitude)
|
77
84
|
loc.latitude = Math.asin(sin_lat1*cos_dstr + cos_lat1*sin_dstr*cos_angle)
|
78
|
-
sin_lat2 = Math.sin(
|
85
|
+
sin_lat2 = Math.sin(@latitude)
|
79
86
|
loc.longitude = @longitude + Math.atan2(sin_angle*sin_dstr*cos_lat1,
|
80
|
-
|
87
|
+
cos_dstr - sin_lat1*sin_lat2)
|
81
88
|
loc
|
82
89
|
end
|
83
90
|
|
@@ -139,42 +146,42 @@ module SGS
|
|
139
146
|
|
140
147
|
#
|
141
148
|
# Display the lat/long as it would appear in a KML file.
|
142
|
-
def to_kml(sep = '
|
149
|
+
def to_kml(sep = ',')
|
143
150
|
vals = [@longitude, @latitude, 0.0]
|
144
|
-
str_vals = vals.map {|val| "%.
|
151
|
+
str_vals = vals.map {|val| "%.8f" % Bearing.rtod(val)}
|
145
152
|
str_vals.join(sep)
|
146
153
|
end
|
147
154
|
|
148
155
|
#
|
149
156
|
# Helper functions for working in degrees.
|
150
|
-
def
|
151
|
-
Bearing.
|
157
|
+
def latitude_d
|
158
|
+
Bearing.rtod @latitude
|
152
159
|
end
|
153
160
|
|
154
|
-
def
|
155
|
-
@latitude = Bearing.
|
161
|
+
def latitude_d=(val)
|
162
|
+
@latitude = Bearing.dtor val
|
156
163
|
end
|
157
164
|
|
158
165
|
def latitude_array(fmt = nil)
|
159
|
-
make_ll_array
|
166
|
+
make_ll_array latitude_d, "NS", fmt
|
160
167
|
end
|
161
168
|
|
162
|
-
def
|
163
|
-
Bearing.
|
169
|
+
def longitude_d
|
170
|
+
Bearing.rtod @longitude
|
164
171
|
end
|
165
172
|
|
166
|
-
def
|
167
|
-
@longitude = Bearing.
|
173
|
+
def longitude_d=(val)
|
174
|
+
@longitude = Bearing.dtor val
|
168
175
|
end
|
169
176
|
|
170
177
|
def longitude_array(fmt = nil)
|
171
|
-
make_ll_array
|
178
|
+
make_ll_array longitude_d, "EW", fmt
|
172
179
|
end
|
173
180
|
|
174
181
|
#
|
175
182
|
# Subtract one location from another, returning a bearing
|
176
183
|
def -(loc)
|
177
|
-
Bearing.
|
184
|
+
Bearing.compute(self, loc)
|
178
185
|
end
|
179
186
|
|
180
187
|
private
|
@@ -186,7 +193,7 @@ module SGS
|
|
186
193
|
val = args.shift
|
187
194
|
val = val + args.shift / 60.0 if args.length > 0
|
188
195
|
val = val + args.shift / 3600.0 if args.length > 0
|
189
|
-
Bearing.
|
196
|
+
Bearing.dtor val * ((nsew.index(dir) == 1) ? -1 : 1)
|
190
197
|
end
|
191
198
|
|
192
199
|
#
|
@@ -198,7 +205,7 @@ module SGS
|
|
198
205
|
else
|
199
206
|
chr = str[0]
|
200
207
|
end
|
201
|
-
"%8.6f%c" % [Bearing.
|
208
|
+
"%8.6f%c" % [Bearing.rtod(val), chr]
|
202
209
|
end
|
203
210
|
|
204
211
|
#
|
@@ -235,18 +242,18 @@ module SGS
|
|
235
242
|
#
|
236
243
|
# Create a bearing from an angle in degrees.
|
237
244
|
def self.degrees(angle, distance)
|
238
|
-
new(Bearing.
|
245
|
+
new(Bearing.dtor(angle), distance)
|
239
246
|
end
|
240
247
|
|
241
248
|
#
|
242
249
|
# Handy function to translate degrees to radians
|
243
|
-
def self.
|
250
|
+
def self.dtor(deg)
|
244
251
|
deg.to_f * Math::PI / 180.0
|
245
252
|
end
|
246
253
|
|
247
254
|
#
|
248
255
|
# Handy function to translate radians to degrees
|
249
|
-
def self.
|
256
|
+
def self.rtod(rad)
|
250
257
|
rad.to_f * 180.0 / Math::PI
|
251
258
|
end
|
252
259
|
|
@@ -259,7 +266,7 @@ module SGS
|
|
259
266
|
#
|
260
267
|
# Another handy function to re-adjust an angle (in degrees) away from
|
261
268
|
# negative.
|
262
|
-
def self.
|
269
|
+
def self.absolute_d(angle)
|
263
270
|
(angle + 360) % 360
|
264
271
|
end
|
265
272
|
|
@@ -279,7 +286,7 @@ module SGS
|
|
279
286
|
# var x = Math.cos(lat1)*Math.sin(lat2) -
|
280
287
|
# Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
|
281
288
|
# var angle = Math.atan2(y, x).toDeg();
|
282
|
-
def self.
|
289
|
+
def self.compute(loc1, loc2)
|
283
290
|
bearing = new
|
284
291
|
sin_lat1 = Math.sin(loc1.latitude)
|
285
292
|
sin_lat2 = Math.sin(loc2.latitude)
|
@@ -288,7 +295,7 @@ module SGS
|
|
288
295
|
sin_dlon = Math.sin(loc2.longitude - loc1.longitude)
|
289
296
|
cos_dlon = Math.cos(loc2.longitude - loc1.longitude)
|
290
297
|
bearing.distance = Math.acos(sin_lat1*sin_lat2 + cos_lat1*cos_lat2*cos_dlon) *
|
291
|
-
|
298
|
+
SGS::EARTH_RADIUS
|
292
299
|
y = sin_dlon * cos_lat2
|
293
300
|
x = cos_lat1 * sin_lat2 - sin_lat1 * cos_lat2 * cos_dlon
|
294
301
|
bearing.angle = Math.atan2(y, x)
|
@@ -309,8 +316,8 @@ module SGS
|
|
309
316
|
|
310
317
|
#
|
311
318
|
# Return the angle (in degrees)
|
312
|
-
def
|
313
|
-
Bearing.
|
319
|
+
def angle_d
|
320
|
+
Bearing.rtod(@angle).to_i
|
314
321
|
end
|
315
322
|
|
316
323
|
#
|
@@ -322,7 +329,7 @@ module SGS
|
|
322
329
|
#
|
323
330
|
# Convert to a string
|
324
331
|
def to_s
|
325
|
-
"BRNG %03dd,%.3fnm" % [
|
332
|
+
"BRNG %03dd,%.3fnm" % [angle_d, @distance]
|
326
333
|
end
|
327
334
|
end
|
328
335
|
end
|
data/lib/sgs/mission.rb
CHANGED
@@ -29,319 +29,328 @@
|
|
29
29
|
# Routines for handling sailboat navigation and route planning.
|
30
30
|
#
|
31
31
|
require 'date'
|
32
|
+
require 'nokogiri'
|
32
33
|
|
33
34
|
module SGS
|
34
35
|
#
|
35
36
|
# Handle a specific mission.
|
36
37
|
class Mission
|
37
|
-
attr_accessor :
|
38
|
-
attr_accessor :
|
39
|
-
attr_accessor :waypoints, :repellors, :track
|
40
|
-
attr_accessor :root_dir
|
38
|
+
attr_accessor :attractors, :repellors, :track
|
39
|
+
attr_accessor :where, :time, :course, :distance
|
41
40
|
|
42
41
|
#
|
43
|
-
# Create the attractors and repellors
|
44
|
-
|
45
|
-
|
46
|
-
|
42
|
+
# Create the attractors and repellors as well as the track array
|
43
|
+
# and other items. @where is our current TrackPoint, @current_wpt is
|
44
|
+
# the waypoint we're working (-1 if none), @course is the heading/speed
|
45
|
+
# the boat is on.
|
46
|
+
def initialize
|
47
|
+
@attractors = []
|
47
48
|
@repellors = []
|
48
|
-
@track =
|
49
|
-
@
|
50
|
-
@time =
|
51
|
-
@
|
52
|
-
@
|
53
|
-
@
|
54
|
-
@
|
49
|
+
@track = nil
|
50
|
+
@current_wpt = -1
|
51
|
+
@start_time = @time = nil
|
52
|
+
@where = nil
|
53
|
+
@course = Course.new
|
54
|
+
@distance = 0
|
55
|
+
@swing = 60
|
55
56
|
end
|
56
57
|
|
57
58
|
#
|
58
59
|
# Load a new mission from the missions directory.
|
59
|
-
def self.load(
|
60
|
+
def self.load(filename)
|
60
61
|
mission = new
|
61
|
-
mission.
|
62
|
-
mission.read(File.open(mission.filename))
|
62
|
+
mission.read(File.open(filename))
|
63
63
|
mission
|
64
64
|
end
|
65
65
|
|
66
|
-
#
|
67
|
-
# Parse a mission file.
|
68
|
-
def read(file)
|
69
|
-
file.each do |line|
|
70
|
-
unless line =~ /^#/
|
71
|
-
args = line.split(':')
|
72
|
-
code = args[0]
|
73
|
-
loc = Location.parse_str(args[1])
|
74
|
-
vec = Bearing.degrees(args[2], args[3])
|
75
|
-
name = args[4].strip
|
76
|
-
case code
|
77
|
-
when /\d/
|
78
|
-
@waypoints[code.to_i] = Waypoint.new(loc, vec, name)
|
79
|
-
when /[Xx]/
|
80
|
-
@where = loc
|
81
|
-
@wind = vec
|
82
|
-
compute_heading if active?
|
83
|
-
when /[Rr]/
|
84
|
-
@repellors << Waypoint.new(loc, vec, name, Waypoint::REPELLOR)
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
@wpt_index = -1
|
89
|
-
end
|
90
|
-
|
91
|
-
#
|
92
|
-
# Save the mission.
|
93
|
-
def save
|
94
|
-
write(File.open(filename, 'w'))
|
95
|
-
end
|
96
|
-
|
97
|
-
#
|
98
|
-
# Write a mission to a file.
|
99
|
-
def write(file)
|
100
|
-
file.puts "#\n# My starting position."
|
101
|
-
file.puts "X:%s:0:12:Starting position" % @where
|
102
|
-
file.puts "#\n# Waypoints."
|
103
|
-
@waypoints.each_with_index do |wpt, i|
|
104
|
-
file.puts "%d:%s:%d:%f:%s" % [i,
|
105
|
-
wpt.location,
|
106
|
-
wpt.chord.angle_degrees,
|
107
|
-
wpt.chord.distance,
|
108
|
-
wpt.name]
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
66
|
#
|
113
67
|
# Commence a mission...
|
114
68
|
def commence(time = nil)
|
115
|
-
@start_time = time ||
|
116
|
-
@
|
117
|
-
@
|
118
|
-
@have_new_waypoint = true
|
119
|
-
@have_new_heading = true
|
120
|
-
@have_new_tack = false
|
69
|
+
@start_time = @time = time || Time.now
|
70
|
+
@track = [TrackPoint.new(time, @where)]
|
71
|
+
@current_wpt = 0
|
121
72
|
end
|
122
73
|
|
123
74
|
#
|
124
75
|
# Terminate a mission.
|
125
76
|
def terminate
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
#
|
130
|
-
# On-mission means we have something to do. In other words, we have a
|
131
|
-
# waypoint to get to.
|
132
|
-
def active?
|
133
|
-
@wpt_index >= 0 and @wpt_index < @waypoints.count
|
77
|
+
puts "***** Mission terminated! *****"
|
78
|
+
@current_wpt = -1
|
134
79
|
end
|
135
80
|
|
136
81
|
#
|
137
|
-
#
|
138
|
-
|
139
|
-
|
140
|
-
|
82
|
+
# Compute the best heading based on our current position and the position
|
83
|
+
# of the current attractor. This is where the heavy-lifting happens
|
84
|
+
def navigate
|
85
|
+
return unless active?
|
86
|
+
puts "Attempting to navigate to #{waypoint}"
|
141
87
|
#
|
142
|
-
#
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
@wpt_index += 1
|
153
|
-
@have_new_waypoint = again = true
|
154
|
-
else
|
155
|
-
again = false
|
156
|
-
end
|
157
|
-
end while again and active?
|
88
|
+
# First off, compute distance and bearing from our current location
|
89
|
+
# to every attractor and repellor.
|
90
|
+
@attractors[@current_wpt..-1].each do |waypt|
|
91
|
+
waypt.compute_bearing(@where)
|
92
|
+
puts "Angle: #{waypt.bearing.angle_d}, Distance: #{waypt.bearing.distance} (adj:#{waypt.distance})"
|
93
|
+
end
|
94
|
+
@repellors.each do |waypt|
|
95
|
+
waypt.compute_bearing(@where)
|
96
|
+
puts "Angle: #{waypt.bearing.angle_d}, Distance: #{waypt.bearing.distance} (adj:#{waypt.distance})"
|
97
|
+
end
|
158
98
|
#
|
159
|
-
#
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
puts "New course: #{newc}"
|
164
|
-
@course = newc
|
165
|
-
compute_heading
|
166
|
-
end
|
99
|
+
# Right. Now look to see if we've achieved the current waypoint and
|
100
|
+
# adjust, accordingly
|
101
|
+
while active? and reached? do
|
102
|
+
next_waypoint!
|
167
103
|
end
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
# Because I tend to forget, here's a run-down of the terminology...
|
172
|
-
# @course is where we're trying to get to. It is our compass bearing to the
|
173
|
-
# next mark or finish or whatever. @heading is the compass bearing we're
|
174
|
-
# going to sail to, which may not be the same as our course (we can't sail
|
175
|
-
# upwind, for example). alpha is the difference between our heading and the
|
176
|
-
# course. @twa is the true wind angle, relative to the front of the boat.
|
177
|
-
# Negative values mean a port tack, positive values are starboard. @vmg is
|
178
|
-
# the velocity made-good. In other words, the speed at which we're heading
|
179
|
-
# to the mark. It's defined as Vcos(alpha) where V is the velocity through
|
180
|
-
# the water.
|
181
|
-
def compute_heading
|
182
|
-
puts "Computing Heading and TWA..."
|
183
|
-
wad = @wind.angle_degrees.to_i
|
184
|
-
puts "Wind angle:%03dd" % wad
|
185
|
-
puts "Required course:%03dd" % @course
|
186
|
-
polar = Polar.new
|
187
|
-
@twa ||= 0
|
104
|
+
return unless active?
|
105
|
+
puts "Angle to next waypoint: #{waypoint.bearing.angle_d}d"
|
106
|
+
puts "Adjusted distance to waypoint is #{@distance}"
|
188
107
|
#
|
189
|
-
#
|
190
|
-
#
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
curr_tack = @twa < 0.0 ? -1 : 1
|
199
|
-
puts "Current tack is %d" % curr_tack
|
200
|
-
(-60..60).each do |alpha|
|
201
|
-
newh = Bearing.absolute_degrees(@course + alpha)
|
202
|
-
twa = 180 - (720 + wad - @course - alpha) % 360
|
203
|
-
puts "Trial heading of %03dd (alpha=%d, TWA=%d)" % [newh, alpha, twa]
|
204
|
-
speed = polar.speed(Bearing.degrees_to_radians(twa))
|
205
|
-
vmg = polar.vmg(Bearing.degrees_to_radians(alpha))
|
108
|
+
# Now, start the vector field analysis by examining headings either side
|
109
|
+
# of the bearing to the waypoint.
|
110
|
+
best_course = @course
|
111
|
+
best_relvmg = 0.0
|
112
|
+
puts "Currently on a #{@course.tack_name} tack (heading is #{@course.heading_d} degrees)"
|
113
|
+
(-@swing..@swing).each do |alpha_d|
|
114
|
+
puts ">> Computing swing of #{alpha_d} degrees"
|
115
|
+
new_course = Course.new(@course.wind)
|
116
|
+
new_course.heading = waypoint.bearing.angle + Bearing.dtor(alpha_d)
|
206
117
|
#
|
207
|
-
#
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
118
|
+
# Ignore head-to-wind cases, as they're pointless.
|
119
|
+
next if new_course.speed < 0.001
|
120
|
+
puts "AWA:#{new_course.awa_d}, heading:#{new_course.heading_d}, speed:#{new_course.speed}"
|
121
|
+
relvmg = 0.0
|
122
|
+
relvmg = new_course.relative_vmg(@attractors[@current_wpt])
|
123
|
+
@attractors[@current_wpt..-1].each do |waypt|
|
124
|
+
relvmg += new_course.relative_vmg(waypt)
|
212
125
|
end
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
126
|
+
@repellors.each do |waypt|
|
127
|
+
relvmg -= new_course.relative_vmg(waypt)
|
128
|
+
end
|
129
|
+
relvmg *= 0.1 if new_course.tack != @course.tack
|
130
|
+
puts "Relative VMG: #{relvmg}"
|
131
|
+
if relvmg > best_relvmg
|
132
|
+
puts "Best heading (so far)"
|
133
|
+
best_relvmg = relvmg
|
134
|
+
best_course = new_course
|
217
135
|
end
|
218
136
|
end
|
219
|
-
#
|
220
|
-
|
221
|
-
|
222
|
-
@
|
223
|
-
@vmg = ideal_vmg
|
224
|
-
@have_new_tack = (@twa * curr_tack) < 0
|
225
|
-
puts "> Best TWA is %d, VMG is %.6fknots" % [@twa, @vmg]
|
226
|
-
#
|
227
|
-
# Check to see if we have a new heading.
|
228
|
-
newh = Bearing::absolute_degrees(wad - @twa)
|
229
|
-
puts "> HDG:%03dd, Err:%03dd" % [newh, newh - @course]
|
230
|
-
if newh != @heading
|
231
|
-
puts "New Heading! %03dd" % newh
|
232
|
-
@have_new_heading = true
|
233
|
-
@heading = newh
|
234
|
-
end
|
137
|
+
puts "Best RELVMG: #{best_relvmg}"
|
138
|
+
puts "TACKING!" if best_course.tack != @course.tack
|
139
|
+
puts "New HDG: #{best_course.heading_d} (AWA:#{best_course.awa_d}), WPT:#{waypoint.name}"
|
140
|
+
@course = best_course
|
235
141
|
end
|
236
142
|
|
237
143
|
#
|
238
|
-
# Set
|
239
|
-
def
|
240
|
-
@
|
241
|
-
|
242
|
-
|
144
|
+
# Set new position
|
145
|
+
def set_position(time, loc)
|
146
|
+
@where = loc
|
147
|
+
@time = time
|
148
|
+
@track << TrackPoint.new(@time, @where)
|
243
149
|
end
|
244
150
|
|
245
151
|
#
|
246
|
-
#
|
247
|
-
|
248
|
-
def
|
249
|
-
|
152
|
+
# Advance the mission by a number of seconds (computing the new location
|
153
|
+
# in the process). Fake out the speed and thus the location.
|
154
|
+
def simulated_movement(how_long = 60)
|
155
|
+
puts "Advancing mission by #{how_long}s"
|
156
|
+
distance = @course.speed * how_long.to_f / 3600.0
|
157
|
+
puts "Travelled #{distance * 1852.0} metres in that time."
|
158
|
+
set_position(@time + how_long, @where + Bearing.new(@course.heading, distance))
|
159
|
+
end
|
160
|
+
|
161
|
+
#
|
162
|
+
# On-mission means we have something to do. In other words, we have a
|
163
|
+
# waypoint to get to.
|
164
|
+
def active?
|
165
|
+
@current_wpt >= 0 and @current_wpt < @attractors.count
|
250
166
|
end
|
251
167
|
|
252
168
|
#
|
253
|
-
#
|
254
|
-
def
|
255
|
-
|
256
|
-
@have_new_waypoint = false
|
257
|
-
active? and hnw
|
169
|
+
# How long has the mission been active?
|
170
|
+
def elapsed
|
171
|
+
@time - @start_time
|
258
172
|
end
|
259
173
|
|
260
174
|
#
|
261
|
-
#
|
262
|
-
def
|
263
|
-
|
264
|
-
@have_new_heading = false
|
265
|
-
active? and hnh
|
175
|
+
# Return the current waypoint.
|
176
|
+
def waypoint
|
177
|
+
active? ? @attractors[@current_wpt] : nil
|
266
178
|
end
|
267
179
|
|
268
180
|
#
|
269
|
-
#
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
181
|
+
# Have we reached the waypoint? Note that even though the waypoints have
|
182
|
+
# a "reached" circle, we discard the last 10m on the basis that it is
|
183
|
+
# within the GPS error.
|
184
|
+
def reached?
|
185
|
+
@distance = @attractors[@current_wpt].distance
|
186
|
+
puts "ARE WE THERE YET? (dist=#{@distance})"
|
187
|
+
return true if @distance <= 0.0027
|
188
|
+
#
|
189
|
+
# Check to see if the next WPT is nearer than the current one
|
190
|
+
#if @current_wpt < (@attractors.count - 1)
|
191
|
+
# next_wpt = @attractors[@current_wpt + 1]
|
192
|
+
# brng = @attractors[@current_wpt].location - next_wpt.location
|
193
|
+
# angle = Bearing.absolute(waypoint.bearing.angle - next_wpt.bearing.angle)
|
194
|
+
# return true if brng.distance > next_wpt.distance and
|
195
|
+
# angle > (0.25 * Math::PI) and
|
196
|
+
# angle < (0.75 * Math::PI)
|
197
|
+
#end
|
198
|
+
puts "... Sadly, no."
|
199
|
+
return false
|
274
200
|
end
|
275
201
|
|
276
202
|
#
|
277
|
-
#
|
278
|
-
|
279
|
-
|
203
|
+
# Advance to the next waypoint. Return TRUE if
|
204
|
+
# there actually is one...
|
205
|
+
def next_waypoint!
|
206
|
+
raise "No mission currently active" unless active?
|
207
|
+
@current_wpt += 1
|
208
|
+
puts "Attempting to navigate to #{waypoint.name}" if active?
|
280
209
|
end
|
210
|
+
|
281
211
|
#
|
282
212
|
# Return the mission status as a string
|
283
213
|
def status_str
|
284
|
-
mins =
|
285
|
-
hours =
|
214
|
+
mins = elapsed / 60
|
215
|
+
hours = mins / 60
|
216
|
+
mins %= 60
|
286
217
|
days = hours / 24
|
287
|
-
mins = (mins % 60.0).to_i
|
288
218
|
hours %= 24
|
289
|
-
str = "
|
219
|
+
str = ">>> #{@time}, "
|
290
220
|
if days < 1
|
291
|
-
str += "%dh%
|
221
|
+
str += "%dh%02dm" % [hours, mins]
|
292
222
|
else
|
293
|
-
str += "
|
223
|
+
str += "+%dd%%02dh%02dm" % [days, hours, mins]
|
294
224
|
end
|
295
225
|
str + ": My position is #{@where}"
|
296
226
|
end
|
297
227
|
|
298
228
|
#
|
299
|
-
#
|
300
|
-
def
|
301
|
-
|
229
|
+
# Compute the remaining distance from the current location
|
230
|
+
def overall_distance
|
231
|
+
start_wpt = active? ? @current_wpt : 0
|
232
|
+
dist = 0.0
|
233
|
+
loc = @where
|
234
|
+
@attractors[start_wpt..-1].each do |wpt|
|
235
|
+
wpt.compute_bearing(loc)
|
236
|
+
dist += wpt.bearing.distance
|
237
|
+
loc = wpt.location
|
238
|
+
end
|
239
|
+
dist
|
240
|
+
end
|
241
|
+
|
242
|
+
#
|
243
|
+
# Parse a mission file.
|
244
|
+
def read(file)
|
245
|
+
file.each do |line|
|
246
|
+
unless line =~ /^#/
|
247
|
+
args = line.split(':')
|
248
|
+
code = args[0]
|
249
|
+
loc = Location.parse_str(args[1])
|
250
|
+
nrml = Bearing.dtor args[2].to_i
|
251
|
+
dist = args[3].to_f
|
252
|
+
name = args[4].chomp
|
253
|
+
case code
|
254
|
+
when /[Xx]/
|
255
|
+
@where = loc
|
256
|
+
@course.wind = Bearing.new(nrml, dist)
|
257
|
+
when /\d/
|
258
|
+
@attractors[code.to_i] = Waypoint.new(loc, nrml, dist, name)
|
259
|
+
when /[Rr]/
|
260
|
+
@repellors << Waypoint.new(loc, nrml, dist, name, true)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
@current_wpt = -1
|
302
265
|
end
|
303
266
|
|
304
267
|
#
|
305
|
-
#
|
306
|
-
def
|
307
|
-
|
268
|
+
# Write a mission to a file.
|
269
|
+
def write(filename)
|
270
|
+
File.open(filename, 'w') do |file|
|
271
|
+
file.puts "#\n# My starting position."
|
272
|
+
file.puts ["X", @where.to_s, @course.wind.angle_d.to_i, @course.wind.distance.to_i, "Starting position"].join(':')
|
273
|
+
file.puts "#\n# Attractors."
|
274
|
+
@attractors.each_with_index do |wpt, i|
|
275
|
+
file.puts "%d:%s:%d:%f:%s" % [i,
|
276
|
+
wpt.location,
|
277
|
+
wpt.normal_d,
|
278
|
+
wpt.radius,
|
279
|
+
wpt.name]
|
280
|
+
end
|
281
|
+
file.puts "#\n# Repellors."
|
282
|
+
@repellors.each do |wpt|
|
283
|
+
file.puts "r:%s:%d:%f:%s" % [wpt.location,
|
284
|
+
wpt.normal_d,
|
285
|
+
wpt.radius,
|
286
|
+
wpt.name]
|
287
|
+
end
|
288
|
+
end
|
308
289
|
end
|
309
290
|
|
310
291
|
#
|
311
|
-
#
|
312
|
-
def
|
313
|
-
|
292
|
+
# Save the track.
|
293
|
+
def track_save(filename)
|
294
|
+
kml_write(File.open(filename, 'w'))
|
314
295
|
end
|
315
296
|
|
316
297
|
#
|
317
298
|
# Write the track data as a KML file.
|
299
|
+
# xml.LineString {
|
300
|
+
# xml.extrude 1
|
301
|
+
# xml.tessellate 1
|
302
|
+
# xml.coordinates wpt.to_kml
|
303
|
+
# }
|
304
|
+
# }
|
305
|
+
# xml.Placemark {
|
306
|
+
# xml.styleUrl "#attractorLine"
|
307
|
+
# xml.LineString {
|
308
|
+
# xml.extrude 1
|
309
|
+
# xml.tessellate 1
|
310
|
+
# xml.coordinates wpt.to_axis_kml
|
311
|
+
# }
|
312
|
+
# }
|
318
313
|
def kml_write(file)
|
319
314
|
builder = Nokogiri::XML::Builder.new do |xml|
|
320
315
|
xml.kml('xmlns' => 'http://www.opengis.net/kml/2.2',
|
321
316
|
'xmlns:gx' => 'http://www.google.com/kml/ext/2.2') {
|
322
317
|
xml.Folder {
|
323
|
-
xml_line_style(xml, "
|
318
|
+
xml_line_style(xml, "attractorLine", "0xffcf0000", 4)
|
324
319
|
xml_line_style(xml, "repellorLine", "0xff00007f", 4)
|
325
320
|
xml_line_style(xml, "trackLine")
|
326
|
-
@
|
321
|
+
@attractors.each do |wpt|
|
327
322
|
xml.Placemark {
|
328
323
|
xml.name wpt.name
|
329
|
-
xml.styleUrl "#
|
330
|
-
xml.
|
331
|
-
xml.
|
332
|
-
xml.tessellate 1
|
333
|
-
xml.coordinates wpt.to_kml
|
324
|
+
xml.styleUrl "#attractorLine"
|
325
|
+
xml.Point {
|
326
|
+
xml.coordinates wpt.location.to_kml
|
334
327
|
}
|
335
328
|
}
|
329
|
+
end
|
330
|
+
@repellors.each do |wpt|
|
336
331
|
xml.Placemark {
|
337
|
-
xml.
|
338
|
-
xml.
|
339
|
-
|
340
|
-
xml.
|
341
|
-
xml.coordinates wpt.to_axis_kml
|
332
|
+
xml.name wpt.name
|
333
|
+
xml.styleUrl "#repellorLine"
|
334
|
+
xml.Point {
|
335
|
+
xml.coordinates wpt.location.to_kml
|
342
336
|
}
|
343
337
|
}
|
344
338
|
end
|
339
|
+
xml.Placemark {
|
340
|
+
xml.name "Track"
|
341
|
+
xml.styleUrl "#trackLine"
|
342
|
+
xml.GX_Track {
|
343
|
+
@track.each do |pt|
|
344
|
+
xml.when pt.time.strftime('%Y-%m-%dT%H:%M:%S+00:00')
|
345
|
+
end
|
346
|
+
@track.each do |pt|
|
347
|
+
xml.GX_coord pt.location.to_kml(' ')
|
348
|
+
end
|
349
|
+
}
|
350
|
+
}
|
351
|
+
}
|
352
|
+
}
|
353
|
+
end
|
345
354
|
# Requires a hack to get rid of the 'gx:' for the when tag.
|
346
355
|
file.puts builder.to_xml.gsub(/GX_/, 'gx:')
|
347
356
|
end
|
data/lib/sgs/nmea.rb
CHANGED
@@ -55,7 +55,7 @@ module SGS
|
|
55
55
|
# Parse an NMEA string into its component parts.
|
56
56
|
def parse(str)
|
57
57
|
str.chomp!
|
58
|
-
if str[0] !=
|
58
|
+
if str[0] != "$"
|
59
59
|
return -1
|
60
60
|
end
|
61
61
|
str, sum = str[1..-1].split('*')
|
@@ -80,7 +80,7 @@ module SGS
|
|
80
80
|
return nil
|
81
81
|
end
|
82
82
|
gps = SGS::GPS.new
|
83
|
-
gps.
|
83
|
+
gps.is_valid if @args[2] == "A"
|
84
84
|
hh = @args[1][0..1].to_i
|
85
85
|
mm = @args[1][2..3].to_i
|
86
86
|
ss = @args[1][4..-1].to_f
|
@@ -92,7 +92,7 @@ module SGS
|
|
92
92
|
gps.time = Time.gm(yy, mn, dd, hh, mm, ss, us)
|
93
93
|
gps.location = Location.parse ll_nmea(@args[3,4]), ll_nmea(@args[5,6])
|
94
94
|
gps.sog = @args[7].to_f
|
95
|
-
gps.cmg = Bearing.
|
95
|
+
gps.cmg = Bearing.dtor @args[8].to_f
|
96
96
|
gps
|
97
97
|
end
|
98
98
|
|
@@ -107,7 +107,7 @@ module SGS
|
|
107
107
|
@args.concat gps.location.latitude_array
|
108
108
|
@args.concat gps.location.longitude_array("%03d%07.4f")
|
109
109
|
@args[7] = "%.2f" % gps.sog
|
110
|
-
@args[8] = "%.2f" % Bearing.
|
110
|
+
@args[8] = "%.2f" % Bearing.radians_to_d(gps.cmg)
|
111
111
|
@args[9] = gps.time.strftime("%d%m%y")
|
112
112
|
@args.concat ['', '']
|
113
113
|
@args << 'A'
|
data/lib/sgs/redis_base.rb
CHANGED
data/lib/sgs/version.rb
CHANGED
data/lib/sgs/waypoint.rb
CHANGED
@@ -34,82 +34,113 @@ module SGS
|
|
34
34
|
#
|
35
35
|
# Waypoint, Attractor, and Repellor definitions
|
36
36
|
class Waypoint < RedisBase
|
37
|
-
attr_accessor :location, :
|
37
|
+
attr_accessor :location, :normal, :radius, :name, :repellor, :bearing
|
38
|
+
attr_reader :bearing, :distance
|
38
39
|
|
39
40
|
#
|
40
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
def initialize(location = nil, chord = nil, name = "", type = WAYPOINT)
|
41
|
+
# Define a new Attractor or Repellor, based on certain parameters.
|
42
|
+
# The location is the centre of the waypoint. The normal is the compass
|
43
|
+
# angle of the start of the semicircle, and the radius is the size of
|
44
|
+
# the arc. You can specify an optional name for the waypoint and also
|
45
|
+
# indicate if we should be attracted or repelled by it.
|
46
|
+
def initialize(location = nil, normal = 0.0, radius = 0.1, name = "", repellor = false)
|
47
47
|
@location = location || Location.new
|
48
|
-
@
|
49
|
-
@
|
48
|
+
@normal = normal
|
49
|
+
@radius = radius
|
50
50
|
@name = name
|
51
|
+
@repellor = repellor
|
52
|
+
@bearing = nil
|
53
|
+
@distance = 0
|
51
54
|
end
|
52
55
|
|
53
56
|
#
|
54
|
-
# Calculate the back-vector from the waypoint to the specified position
|
55
|
-
# Calculate the adjusted distance between the
|
57
|
+
# Calculate the back-vector from the waypoint to the specified position.
|
58
|
+
# Calculate the adjusted distance between the position and the mark.
|
56
59
|
# Check to see if our back-bearing from the waypoint to our location is
|
57
|
-
# inside the chord of the waypoint
|
58
|
-
#
|
60
|
+
# inside the chord of the waypoint, which is a semicircle commencing at
|
61
|
+
# the normal. If so, reduce the distance to the waypoint by the length
|
62
|
+
# of the chord. @distance is the adjusted distance to the location
|
59
63
|
def compute_bearing(loc)
|
60
|
-
@bearing =
|
64
|
+
@bearing = loc - @location
|
61
65
|
@distance = @bearing.distance
|
62
|
-
d = Bearing.new(@bearing.back_angle - @
|
63
|
-
# A chord angle of 0 gives a semicircle from
|
64
|
-
#
|
65
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
end
|
66
|
+
d = Bearing.new(@bearing.back_angle - @normal, @bearing.distance)
|
67
|
+
# A chord angle of 0 gives a semicircle from 0 to 180 degrees. If our
|
68
|
+
# approach angle is within range, then reduce our distance to the mark
|
69
|
+
# by the chord distance (radius).
|
70
|
+
@distance -= @radius if d.angle >= 0.0 and d.angle < Math::PI
|
71
|
+
@distance = 0.0 if @distance < 0.0
|
69
72
|
@distance
|
70
73
|
end
|
71
74
|
|
75
|
+
#
|
76
|
+
# Is this an attractor?
|
77
|
+
def attractor?
|
78
|
+
@repellor == false
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Is this a repellor?
|
83
|
+
def repellor?
|
84
|
+
@repellor == true
|
85
|
+
end
|
86
|
+
|
72
87
|
#
|
73
88
|
# Is the waypoint in scope? In other words, is our angle inside the chord.
|
74
89
|
def in_scope?
|
75
90
|
puts "In-scope distance is %f..." % @distance
|
76
|
-
@distance
|
91
|
+
@distance == 0.0
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
# Convert the waypoint normal to/from degrees
|
96
|
+
def normal_d
|
97
|
+
Bearing.rtod @normal
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Convert the waypoint normal to/from degrees
|
102
|
+
def normal_d=(val)
|
103
|
+
@normal = Bearing.dtor val
|
77
104
|
end
|
78
105
|
|
79
106
|
#
|
80
107
|
# Pretty version of the waypoint.
|
81
108
|
def to_s
|
82
|
-
"'#{@name}' at #{@location} => #{
|
109
|
+
"'#{@name}' at #{@location} => #{normal_d}%#{@radius}"
|
83
110
|
end
|
84
111
|
|
85
112
|
#
|
86
113
|
# Display a string for a KML file
|
87
114
|
def to_kml
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
115
|
+
puts "TO KML!"
|
116
|
+
#p self
|
117
|
+
#c2 = @chord.clone
|
118
|
+
#c2.angle += Math::PI
|
119
|
+
#pos1 = @location.calculate(@chord)
|
120
|
+
#pos2 = @location.calculate(c2)
|
121
|
+
#"#{pos1.to_kml(',')} #{pos2.to_kml(',')}"
|
93
122
|
end
|
94
123
|
|
95
124
|
#
|
96
125
|
# Show the axis line for the waypoint (as a KML)
|
97
126
|
def to_axis_kml
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
127
|
+
puts "TO_AXIS_KML!"
|
128
|
+
#::FIXME::
|
129
|
+
#c2 = @chord.clone
|
130
|
+
#c2.angle += 1.5 * Math::PI
|
131
|
+
#pos1 = @location.calculate(c2)
|
132
|
+
#"#{@location.to_kml(',')} #{pos1.to_kml(',')}"
|
102
133
|
end
|
103
134
|
end
|
104
135
|
|
105
136
|
#
|
106
|
-
# Store
|
107
|
-
class
|
137
|
+
# Store an individual track point.
|
138
|
+
class TrackPoint
|
108
139
|
attr_accessor :time, :location
|
109
140
|
|
110
|
-
def initialize(time, location)
|
111
|
-
@time = time
|
112
|
-
@location = location
|
141
|
+
def initialize(time = nil, location = nil)
|
142
|
+
@time = time ? time.clone : nil
|
143
|
+
@location = location ? location.clone : nil
|
113
144
|
end
|
114
145
|
end
|
115
146
|
end
|
data/lib/sgslib.rb
CHANGED
@@ -33,13 +33,13 @@ require 'sgs/location'
|
|
33
33
|
require 'sgs/nmea'
|
34
34
|
require 'sgs/gps'
|
35
35
|
require 'sgs/waypoint'
|
36
|
-
require 'sgs/polar'
|
37
36
|
require 'sgs/alarm'
|
38
37
|
require 'sgs/timing'
|
39
38
|
require 'sgs/command'
|
40
39
|
require 'sgs/otto'
|
40
|
+
require 'sgs/course'
|
41
41
|
require 'sgs/navigate'
|
42
|
-
|
42
|
+
require 'sgs/mission'
|
43
43
|
|
44
44
|
module SGS
|
45
45
|
# Your code goes here...
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sgslib
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dermot Tynan
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-06-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -86,6 +86,7 @@ files:
|
|
86
86
|
- bin/setup
|
87
87
|
- lib/sgs/alarm.rb
|
88
88
|
- lib/sgs/command.rb
|
89
|
+
- lib/sgs/course.rb
|
89
90
|
- lib/sgs/gps.rb
|
90
91
|
- lib/sgs/location.rb
|
91
92
|
- lib/sgs/logger.rb
|
@@ -93,7 +94,6 @@ files:
|
|
93
94
|
- lib/sgs/navigate.rb
|
94
95
|
- lib/sgs/nmea.rb
|
95
96
|
- lib/sgs/otto.rb
|
96
|
-
- lib/sgs/polar.rb
|
97
97
|
- lib/sgs/redis_base.rb
|
98
98
|
- lib/sgs/setup.rb
|
99
99
|
- lib/sgs/timing.rb
|
@@ -122,7 +122,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
122
|
version: '0'
|
123
123
|
requirements: []
|
124
124
|
rubyforge_project:
|
125
|
-
rubygems_version: 2.5.1
|
125
|
+
rubygems_version: 2.5.2.1
|
126
126
|
signing_key:
|
127
127
|
specification_version: 4
|
128
128
|
summary: Sailboat Guidance System
|
data/lib/sgs/polar.rb
DELETED
@@ -1,86 +0,0 @@
|
|
1
|
-
#
|
2
|
-
# Copyright (c) 2013, Kalopa Research. All rights reserved. This is free
|
3
|
-
# software; you can redistribute it and/or modify it under the terms of the
|
4
|
-
# GNU General Public License as published by the Free Software Foundation;
|
5
|
-
# either version 2, or (at your option) any later version.
|
6
|
-
#
|
7
|
-
# It is distributed in the hope that it will be useful, but WITHOUT
|
8
|
-
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
9
|
-
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
10
|
-
# for more details.
|
11
|
-
#
|
12
|
-
# You should have received a copy of the GNU General Public License along
|
13
|
-
# with this product; see the file COPYING. If not, write to the Free
|
14
|
-
# Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
|
15
|
-
#
|
16
|
-
# THIS SOFTWARE IS PROVIDED BY KALOPA RESEARCH "AS IS" AND ANY EXPRESS OR
|
17
|
-
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
18
|
-
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
19
|
-
# IN NO EVENT SHALL KALOPA RESEARCH BE LIABLE FOR ANY DIRECT, INDIRECT,
|
20
|
-
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
21
|
-
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
22
|
-
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
23
|
-
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
24
|
-
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
25
|
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
26
|
-
#
|
27
|
-
|
28
|
-
##
|
29
|
-
# Routines for handling sailboat navigation and route planning.
|
30
|
-
#
|
31
|
-
# The code on this page was derived from formulae on the Movable Type site:
|
32
|
-
# http://www.movable-type.co.uk/scripts/latlong.html
|
33
|
-
#
|
34
|
-
|
35
|
-
require 'date'
|
36
|
-
|
37
|
-
module SGS
|
38
|
-
##
|
39
|
-
#
|
40
|
-
# A class to handle boat polar calculations. It takes a range of polars
|
41
|
-
# as polynomials and then applies them.
|
42
|
-
class Polar
|
43
|
-
# Right now, we have one polar - from a Catalina 22.
|
44
|
-
# Note that the speed is the same, regardless of the tack.
|
45
|
-
STANDARD = [
|
46
|
-
-3.15994,
|
47
|
-
23.8741,
|
48
|
-
-27.4595,
|
49
|
-
16.4868,
|
50
|
-
-5.15663,
|
51
|
-
0.743936,
|
52
|
-
-0.0344716
|
53
|
-
].freeze
|
54
|
-
|
55
|
-
#
|
56
|
-
# set up the default values
|
57
|
-
def initialize
|
58
|
-
@curve = STANDARD
|
59
|
-
end
|
60
|
-
|
61
|
-
#
|
62
|
-
# Compute the hull speed from the polar
|
63
|
-
# :awa: is the apparent wind angle (in radians)
|
64
|
-
# :wspeed: is the wind speed (in knots)
|
65
|
-
def speed(awa, wspeed = 0.0)
|
66
|
-
awa = awa.to_f.abs
|
67
|
-
ap = 1.0
|
68
|
-
@speed = 0.0
|
69
|
-
@curve.each do |poly_val|
|
70
|
-
@speed += poly_val * ap
|
71
|
-
ap *= awa
|
72
|
-
end
|
73
|
-
@speed /= 1.529955
|
74
|
-
if @speed < 0.0
|
75
|
-
@speed = 0.0
|
76
|
-
end
|
77
|
-
@speed
|
78
|
-
end
|
79
|
-
|
80
|
-
#
|
81
|
-
# Calculate the VMG from the angle to the mark.
|
82
|
-
def vmg(alpha)
|
83
|
-
Math::cos(alpha) * @speed
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|