sgslib 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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