sgslib 0.1.0 → 0.2.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.
- 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
|