sgslib 0.1.0

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.
@@ -0,0 +1,328 @@
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 location and bearings.
30
+ #
31
+ require 'date'
32
+ require 'json'
33
+
34
+ module SGS
35
+ ##
36
+ #
37
+ # Class for dealing with latitude/longitude. Includes methods for parsing,
38
+ # converting to a printable string, and so forth.
39
+ #
40
+ # Note that for convenience, we retain latitude and longitude in Radians
41
+ # rather than degrees.
42
+ #
43
+ class Location
44
+ attr_accessor :latitude, :longitude
45
+
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
+ #
52
+ # Create the Location instance.
53
+ def initialize(lat = nil, long = nil)
54
+ @latitude = lat.to_f if lat
55
+ @longitude = long.to_f if long
56
+ end
57
+
58
+ #
59
+ # Calculate a new position from the current position
60
+ # given a bearing (angle and distance)
61
+ #
62
+ # This code was derived from formulae on the Movable Type site:
63
+ # http://www.movable-type.co.uk/scripts/latlong.html
64
+ #
65
+ # var lat2 = Math.asin( Math.sin(lat1)*Math.cos(d/R) +
66
+ # Math.cos(lat1)*Math.sin(d/R)*Math.cos(angle) );
67
+ # var lon2 = lon1 + Math.atan2(Math.sin(angle)*Math.sin(d/R)*Math.cos(lat1),
68
+ # Math.cos(d/R)-Math.sin(lat1)*Math.sin(lat2));
69
+ def calculate(bearing)
70
+ loc = Location.new
71
+ sin_angle = Math.sin(bearing.angle)
72
+ cos_angle = Math.cos(bearing.angle)
73
+ sin_dstr = Math.sin(bearing.distance / EARTH_RADIUS)
74
+ cos_dstr = Math.cos(bearing.distance / EARTH_RADIUS)
75
+ sin_lat1 = Math.sin(@latitude)
76
+ cos_lat1 = Math.cos(@latitude)
77
+ loc.latitude = Math.asin(sin_lat1*cos_dstr + cos_lat1*sin_dstr*cos_angle)
78
+ sin_lat2 = Math.sin(loc.latitude)
79
+ loc.longitude = @longitude + Math.atan2(sin_angle*sin_dstr*cos_lat1,
80
+ cos_dstr - sin_lat1*sin_lat2)
81
+ loc
82
+ end
83
+
84
+ #
85
+ # Move to the new location
86
+ def move!(bearing)
87
+ loc = calculate(bearing)
88
+ self.latitude = loc.latitude
89
+ self.longitude = loc.longitude
90
+ end
91
+
92
+ #
93
+ # Create a new location from a string.
94
+ # Uses the instance method to parse.
95
+ def self.parse_str(str)
96
+ loc = new
97
+ loc.parse_str(str)
98
+ loc
99
+ end
100
+
101
+ #
102
+ # Create a new location from a lat/long string pair
103
+ # Uses the instance method to parse.
104
+ def self.parse(latstr, longstr)
105
+ loc = new
106
+ loc.parse(latstr, longstr)
107
+ loc
108
+ end
109
+
110
+ #
111
+ # Parse a lat/long value (in degrees)
112
+ def parse_str(str)
113
+ latstr, longstr = str.split(',')
114
+ parse(latstr, longstr)
115
+ end
116
+
117
+ #
118
+ # Parse a lat/long value pair (in degrees)
119
+ def parse(latstr, longstr)
120
+ @latitude = ll_parse(latstr.split, "NS")
121
+ @longitude = ll_parse(longstr.split, "EW")
122
+ end
123
+
124
+ #
125
+ # Is this location valid?
126
+ def valid?
127
+ @latitude and @longitude
128
+ end
129
+
130
+ #
131
+ # Display the lat/long as a useful string (in degrees).
132
+ def to_s
133
+ if valid?
134
+ "%s, %s" % [ll_to_s(@latitude, "NS"), ll_to_s(@longitude, "EW")]
135
+ else
136
+ "unknown"
137
+ end
138
+ end
139
+
140
+ #
141
+ # Display the lat/long as it would appear in a KML file.
142
+ def to_kml(sep = ' ')
143
+ vals = [@longitude, @latitude, 0.0]
144
+ str_vals = vals.map {|val| "%.6f" % Bearing.radians_to_degrees(val)}
145
+ str_vals.join(sep)
146
+ end
147
+
148
+ #
149
+ # Helper functions for working in degrees.
150
+ def latitude_degrees
151
+ Bearing.radians_to_degrees @latitude
152
+ end
153
+
154
+ def latitude_degrees=(val)
155
+ @latitude = Bearing.degrees_to_radians val
156
+ end
157
+
158
+ def latitude_array(fmt = nil)
159
+ make_ll_array latitude_degrees, "NS", fmt
160
+ end
161
+
162
+ def longitude_degrees
163
+ Bearing.radians_to_degrees @longitude
164
+ end
165
+
166
+ def longitude_degrees=(val)
167
+ @longitude = Bearing.degrees_to_radians val
168
+ end
169
+
170
+ def longitude_array(fmt = nil)
171
+ make_ll_array longitude_degrees, "EW", fmt
172
+ end
173
+
174
+ #
175
+ # Subtract one location from another, returning a bearing
176
+ def -(loc)
177
+ Bearing.calculate(self, loc)
178
+ end
179
+
180
+ private
181
+ #
182
+ # Parse a string into a lat or long.
183
+ def ll_parse(args, nsew)
184
+ dir = args[-1].gsub(/[\d\. ]+/, '').upcase
185
+ args.map! {|val| val.to_f}
186
+ val = args.shift
187
+ val = val + args.shift / 60.0 if args.length > 0
188
+ val = val + args.shift / 3600.0 if args.length > 0
189
+ Bearing.degrees_to_radians val * ((nsew.index(dir) == 1) ? -1 : 1)
190
+ end
191
+
192
+ #
193
+ #
194
+ def ll_to_s(val, str)
195
+ if val < 0.0
196
+ chr = str[1]
197
+ val = -val
198
+ else
199
+ chr = str[0]
200
+ end
201
+ "%8.6f%c" % [Bearing.radians_to_degrees(val), chr]
202
+ end
203
+
204
+ #
205
+ # Create a Lat/Long array suitable for an NMEA output
206
+ def make_ll_array(val, nsew, fmt = nil)
207
+ fmt ||= "%02d%07.4f"
208
+ if (val < 0)
209
+ val = -val
210
+ ne = nsew[1]
211
+ else
212
+ ne = nsew[0]
213
+ end
214
+ deg = val.to_i
215
+ val = (val - deg) * 60
216
+ [fmt % [deg, val], ne.chr]
217
+ end
218
+ end
219
+
220
+ ##
221
+ # Class for dealing with the angle/distance vector.
222
+ #
223
+ # Note that for convenience, we retain the angle in Radians. The
224
+ # distance is in nautical miles.
225
+ class Bearing
226
+ attr_accessor :distance
227
+
228
+ #
229
+ # Create the Bearing instance.
230
+ def initialize(angle = 0.0, distance = 0.0)
231
+ self.angle = angle.to_f
232
+ self.distance = distance.to_f
233
+ end
234
+
235
+ #
236
+ # Create a bearing from an angle in degrees.
237
+ def self.degrees(angle, distance)
238
+ new(Bearing.degrees_to_radians(angle), distance)
239
+ end
240
+
241
+ #
242
+ # Handy function to translate degrees to radians
243
+ def self.degrees_to_radians(deg)
244
+ deg.to_f * Math::PI / 180.0
245
+ end
246
+
247
+ #
248
+ # Handy function to translate radians to degrees
249
+ def self.radians_to_degrees(rad)
250
+ rad.to_f * 180.0 / Math::PI
251
+ end
252
+
253
+ #
254
+ # Handy function to re-adjust an angle away from negative
255
+ def self.absolute(angle)
256
+ (angle + 2.0 * Math::PI) % (2.0 * Math::PI)
257
+ end
258
+
259
+ #
260
+ # Another handy function to re-adjust an angle (in degrees) away from
261
+ # negative.
262
+ def self.absolute_degrees(angle)
263
+ (angle + 360) % 360
264
+ end
265
+
266
+ #
267
+ # Haversine formula for calculating distance and angle, given two
268
+ # locations.
269
+ #
270
+ # To calculate an angle and distance from two positions:
271
+ #
272
+ # This code was derived from formulae on the Movable Type site:
273
+ # http://www.movable-type.co.uk/scripts/latlong.html
274
+ #
275
+ # var d = Math.acos(Math.sin(lat1)*Math.sin(lat2) +
276
+ # Math.cos(lat1)*Math.cos(lat2) *
277
+ # Math.cos(lon2-lon1)) * R;
278
+ # var y = Math.sin(dLon) * Math.cos(lat2);
279
+ # var x = Math.cos(lat1)*Math.sin(lat2) -
280
+ # Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
281
+ # var angle = Math.atan2(y, x).toDeg();
282
+ def self.calculate(loc1, loc2)
283
+ bearing = new
284
+ sin_lat1 = Math.sin(loc1.latitude)
285
+ sin_lat2 = Math.sin(loc2.latitude)
286
+ cos_lat1 = Math.cos(loc1.latitude)
287
+ cos_lat2 = Math.cos(loc2.latitude)
288
+ sin_dlon = Math.sin(loc2.longitude - loc1.longitude)
289
+ cos_dlon = Math.cos(loc2.longitude - loc1.longitude)
290
+ bearing.distance = Math.acos(sin_lat1*sin_lat2 + cos_lat1*cos_lat2*cos_dlon) *
291
+ Location::EARTH_RADIUS
292
+ y = sin_dlon * cos_lat2
293
+ x = cos_lat1 * sin_lat2 - sin_lat1 * cos_lat2 * cos_dlon
294
+ bearing.angle = Math.atan2(y, x)
295
+ bearing
296
+ end
297
+
298
+ #
299
+ # Set the angle
300
+ def angle=(angle)
301
+ @angle = Bearing.absolute(angle)
302
+ end
303
+
304
+ #
305
+ # Get the angle
306
+ def angle
307
+ @angle
308
+ end
309
+
310
+ #
311
+ # Return the angle (in degrees)
312
+ def angle_degrees
313
+ Bearing.radians_to_degrees(@angle).to_i
314
+ end
315
+
316
+ #
317
+ # Get the back-angle (the angle viewed from the opposite end of the line)
318
+ def back_angle
319
+ Bearing.absolute(@angle - Math::PI)
320
+ end
321
+
322
+ #
323
+ # Convert to a string
324
+ def to_s
325
+ "BRNG %03dd,%.3fnm" % [angle_degrees, @distance]
326
+ end
327
+ end
328
+ end
data/lib/sgs/logger.rb ADDED
@@ -0,0 +1,96 @@
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 logging.
30
+ #
31
+ require 'date'
32
+
33
+ module SGS
34
+ #
35
+ # Waypoint, Attractor, and Repellor definitions
36
+ class Logger < RedisBase
37
+ attr_accessor :watch
38
+
39
+ #
40
+ # Watch names and definitions. The first watch is from 8PM until
41
+ # midnight (local time), the middle watch is from midnight until
42
+ # 4AM. The morning watch is from 4AM until 8AM. The forenoon watch
43
+ # runs from 8AM until noon and the afternoon watch runs from noon
44
+ # until 4PM. The dog watches run from 4PM until 8PM and around we
45
+ # go again.
46
+ #
47
+ # Logs are sent back to base every four hours (6 per day) starting
48
+ # at midnight UTC. However, some of the reporting is based on
49
+ # the watch system rather than UTC. For example, reporting battery
50
+ # voltage is most useful at the start of the forenoon watch, because
51
+ # this represents the lowest voltage point after driving the boat all
52
+ # night. As a result, the watch ID is computed based on the longitude,
53
+ # which is a rough approximation of the timezone.
54
+ FIRST_WATCH = 0
55
+ MIDDLE_WATCH = 1
56
+ MORNING_WATCH = 2
57
+ FORENOON_WATCH = 3
58
+ AFTERNOON_WATCH = 4
59
+ DOG_WATCH = 5
60
+ ALARM_REPORT = 7
61
+
62
+ WATCH_NAMES = [
63
+ "First Watch", "Middle Watch", "Morning Watch",
64
+ "Forenoon Watch", "Afternoon Watch", "Dog Watch",
65
+ "", "** ALARM REPORT **"
66
+ ].freeze
67
+
68
+ def initialize()
69
+ @watch = FIRST_WATCH
70
+ end
71
+
72
+ #
73
+ # Convert the watch ID to a name
74
+ def watch_name
75
+ WATCH_NAMES[@watch]
76
+ end
77
+
78
+ #
79
+ # Determine the watch report. This takes account our actual latitude
80
+ # and the current time. It does a rudimentary timezone conversion and
81
+ # calculates the watch. Note that what we're calculating here is what
82
+ # was the previous watch. So any time after 23:45 (local), we'll report
83
+ # from the First Watch. Right up until 03:45 (local) when we'll start
84
+ # reporting from the Middle Watch.
85
+ def determine_watch(longitude)
86
+ utc = Time.now
87
+ local_hour = utc.hour + longitude * 12.0 / Math::PI
88
+ p utc
89
+ p local_hour
90
+ local_hour += 24.0 if local_hour < 0.0
91
+ local_hour -= 24.0 if local_hour >= 24.0
92
+ @watch = ((local_hour / 4.0) + 0.25).to_i
93
+ @watch = FIRST_WATCH if @watch == 6
94
+ end
95
+ end
96
+ end