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
         |