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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0fc3371c28ba4e23106c0a6ec7438269c4e127a4
4
- data.tar.gz: 513b37fe44496a798e3c9a332fa7446bd35d7a93
3
+ metadata.gz: a552ab707d5da785deb99f2b4118c2e3df9ebfe2
4
+ data.tar.gz: 36f82fa588da1882b67f32a8cff7709e4f4ca3c9
5
5
  SHA512:
6
- metadata.gz: cc840557ca82d76640c300cd2fb93745da198f5827444ff38afd84c2a11aabfd429dcfd600cc54ea30020c28acecd2a9b5719f820a8d277e8060e5ed97e20d73
7
- data.tar.gz: a574b15c37a867713b01cf4583edfafa4623461c433d30ad87512d47b507d76224abe8b91aaaefed6ad65d4a3e8d57ed46f1921ce8e461aebd41f5fa3a8979b9
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 calculate(bearing)
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(loc.latitude)
85
+ sin_lat2 = Math.sin(@latitude)
79
86
  loc.longitude = @longitude + Math.atan2(sin_angle*sin_dstr*cos_lat1,
80
- cos_dstr - sin_lat1*sin_lat2)
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| "%.6f" % Bearing.radians_to_degrees(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 latitude_degrees
151
- Bearing.radians_to_degrees @latitude
157
+ def latitude_d
158
+ Bearing.rtod @latitude
152
159
  end
153
160
 
154
- def latitude_degrees=(val)
155
- @latitude = Bearing.degrees_to_radians val
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 latitude_degrees, "NS", fmt
166
+ make_ll_array latitude_d, "NS", fmt
160
167
  end
161
168
 
162
- def longitude_degrees
163
- Bearing.radians_to_degrees @longitude
169
+ def longitude_d
170
+ Bearing.rtod @longitude
164
171
  end
165
172
 
166
- def longitude_degrees=(val)
167
- @longitude = Bearing.degrees_to_radians val
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 longitude_degrees, "EW", fmt
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.calculate(self, loc)
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.degrees_to_radians val * ((nsew.index(dir) == 1) ? -1 : 1)
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.radians_to_degrees(val), chr]
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.degrees_to_radians(angle), distance)
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.degrees_to_radians(deg)
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.radians_to_degrees(rad)
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.absolute_degrees(angle)
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.calculate(loc1, loc2)
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
- Location::EARTH_RADIUS
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 angle_degrees
313
- Bearing.radians_to_degrees(@angle).to_i
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" % [angle_degrees, @distance]
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 :id, :where, :time, :start_time
38
- attr_accessor :course, :heading, :twa, :vmg
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
- def initialize(id = 0)
45
- @root_dir = "."
46
- @waypoints = []
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
- @id = id
50
- @time = DateTime.now
51
- @wpt_index = -1
52
- @heading = -1
53
- @course = -1
54
- @tack = 0
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(id)
60
+ def self.load(filename)
60
61
  mission = new
61
- mission.id = id
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 || DateTime.now
116
- @time = @start_time
117
- @wpt_index = 0
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
- @wpt_index = -1
127
- end
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
- # Set our current location (and timestamp).
138
- def set_location(where, time = nil)
139
- @where = where
140
- @time = time || DateTime.now
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
- # Update the track with our new position, and find the next waypoint.
143
- @track << Track.new(@time, @where.clone)
144
- again = false
145
- begin
146
- wpt = @waypoints[@wpt_index]
147
- puts ">>Waypoint is #{wpt}"
148
- wpt.compute_bearing(@where)
149
- puts ">>Course #{wpt.bearing}"
150
- if wpt.in_scope?
151
- puts "We're at the mark. Time to find the next waypoint..."
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
- # OK, now compute our heading...
160
- if active?
161
- newc = wpt.bearing.angle_degrees
162
- if newc != @course
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
- end
169
- #
170
- # Compute the most effective TWA/VMG for the course.
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
- # Try an alpha angle between -60 and +60 degrees either side of the
190
- # course. The point of this exercise is to compute the TWA for that
191
- # heading, compute the speed at that trial TWA, and then compute
192
- # the VMG for it. We compute the maximum VMG (and TWA) and use that
193
- # to drive the boat. The PVMG is the poisoned version of the VMG,
194
- # adjusted so that the opposite tack is less-favoured.
195
- ideal_twa = 0
196
- ideal_vmg = 0.0
197
- max_pvmg = 0.0
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
- # Adjust the speed to favour the current tack.
208
- tack_err = twa < 0.0 ? -curr_tack : curr_tack
209
- pvmg = vmg + vmg * tack_err / 5.0
210
- if vmg > 1.7 and false
211
- puts "Trial TWA: %3d, speed:%.5fkn, VMG:%.6fkn" % [twa, speed, vmg]
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
- if pvmg > max_pvmg
214
- max_pvmg = pvmg
215
- ideal_twa = twa
216
- ideal_vmg = vmg
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
- # For the various angles, we have computed the best TWA and VMG. Now
221
- # adjust our settings, accordingly. Don't use the poisoned VMG.
222
- @twa = ideal_twa
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 the wind data.
239
- def wind_data(angle, speed = 0.0)
240
- @wind = Bearing.degrees(angle, speed)
241
- puts "Wind:#{@wind}"
242
- compute_heading if active?
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
- # Wind speed (Beaufort scale)
247
- BEAUFORT = [0, 1, 4, 7, 11, 17, 22, 28, 34, 41]
248
- def wind_beaufort
249
- ws = @wind.speed
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
- # Are we at a new waypoint?
254
- def new_waypoint?
255
- hnw = @have_new_waypoint
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
- # Do we have a new heading?
262
- def new_heading?
263
- hnh = @have_new_heading
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
- # Do we need to tack?
270
- def tacking?
271
- hnt = @have_new_tack
272
- @have_new_tack = false
273
- active? and hnt
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
- # Return the next (current) waypoint.
278
- def waypoint
279
- active? ? @waypoints[@wpt_index] : nil
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 = (@time - @start_time) * 24.0 * 60.0
285
- hours = (mins / 60.0).to_i
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 = ">>>#{@time}, "
219
+ str = ">>> #{@time}, "
290
220
  if days < 1
291
- str += "%dh%dm" % [hours, mins]
221
+ str += "%dh%02dm" % [hours, mins]
292
222
  else
293
- str += "%dd%dh" % [days, mins]
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
- # Name/path of mission file.
300
- def filename
301
- @root_dir + "/missions/%03d" % id
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
- # Save the track.
306
- def track_save
307
- kml_write(File.open(track_filename, 'w'))
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
- # The name of the track file
312
- def track_filename
313
- @root_dir + "/tracks/output-%03d.kml" % id
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, "waypointLine", "0xffcf0000", 4)
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
- @waypoints.each do |wpt|
321
+ @attractors.each do |wpt|
327
322
  xml.Placemark {
328
323
  xml.name wpt.name
329
- xml.styleUrl "#waypointLine"
330
- xml.LineString {
331
- xml.extrude 1
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.styleUrl "#waypointLine"
338
- xml.LineString {
339
- xml.extrude 1
340
- xml.tessellate 1
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] != 36
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.valid = @args[2] == "A"
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.degrees_to_radians @args[8].to_f
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.radians_to_degrees(gps.cmg)
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'
@@ -139,6 +139,7 @@ module SGS
139
139
  end
140
140
  $redis.incr count_name
141
141
  end
142
+ true
142
143
  end
143
144
 
144
145
  #
data/lib/sgs/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module SGS
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.1"
3
3
  end
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, :chord, :type, :name, :bearing, :distance
37
+ attr_accessor :location, :normal, :radius, :name, :repellor, :bearing
38
+ attr_reader :bearing, :distance
38
39
 
39
40
  #
40
- # Types of waypoints
41
- START_LINE = 0
42
- FINISH_LINE = 1
43
- WAYPOINT = 2
44
- REPELLOR = 3
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
- @chord = chord
49
- @type = type
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 mark and the set location.
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. If so, reduce the distance to the
58
- # waypoint by the length of the chord.
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 = Bearing.calculate(loc, @location)
64
+ @bearing = loc - @location
61
65
  @distance = @bearing.distance
62
- d = Bearing.new(@bearing.back_angle - @chord.angle, @distance)
63
- # A chord angle of 0 gives a semicircle from 180 to 360 degrees
64
- # (inclusive). If our approach angle is within range, then reduce our
65
- # distance to the mark by the chord distance (radius).
66
- unless d.angle > 0.0 and d.angle < Math::PI
67
- @distance -= @chord.distance
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 <= 0.0
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} => #{chord}"
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
- c2 = @chord.clone
89
- c2.angle += Math::PI
90
- pos1 = @location.calculate(@chord)
91
- pos2 = @location.calculate(c2)
92
- "#{pos1.to_kml(',')} #{pos2.to_kml(',')}"
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
- c2 = @chord.clone
99
- c2.angle += 1.5 * Math::PI
100
- pos1 = @location.calculate(c2)
101
- "#{@location.to_kml(',')} #{pos1.to_kml(',')}"
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 the track over the ground.
107
- class Track
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
- #require 'mission'
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.0
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: 2017-03-22 00:00:00.000000000 Z
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