sgslib 0.3.1 → 1.5.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.
data/lib/sgs/location.rb CHANGED
@@ -1,34 +1,41 @@
1
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.
2
+ # Copyright (c) 2013-2023, Kalopa Robotics Limited. All rights
3
+ # reserved.
6
4
  #
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.
5
+ # This program is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU General Public License as
7
+ # published by the Free Software Foundation; either version 2 of
8
+ # the License, or (at your option) any later version.
11
9
  #
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.
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
15
14
  #
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.
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program; if not, write to the Free Software
17
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18
+ # 02110-1301, USA.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY KALOPA ROBOTICS LIMITED "AS IS" AND
21
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22
+ # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
23
+ # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KALOPA
24
+ # ROBOTICS LIMITED BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
27
+ # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
29
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
31
+ # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
+ #
33
+ # ABSTRACT
26
34
  #
27
35
 
28
36
  ##
29
- # Routines for handling sailboat location and bearings.
37
+ # Routines for handling sailboat location.
30
38
  #
31
- require 'date'
32
39
  require 'json'
33
40
 
34
41
  module SGS
@@ -97,35 +104,19 @@ module SGS
97
104
  end
98
105
 
99
106
  #
100
- # Create a new location from a string.
101
- # Uses the instance method to parse.
102
- def self.parse_str(str)
103
- loc = new
104
- loc.parse_str(str)
105
- loc
106
- end
107
-
108
- #
109
- # Create a new location from a lat/long string pair
107
+ # Create a new location from a lat/long hash.
110
108
  # Uses the instance method to parse.
111
- def self.parse(latstr, longstr)
109
+ def self.parse(data)
112
110
  loc = new
113
- loc.parse(latstr, longstr)
111
+ loc.parse(data)
114
112
  loc
115
113
  end
116
114
 
117
115
  #
118
- # Parse a lat/long value (in degrees)
119
- def parse_str(str)
120
- latstr, longstr = str.split(',')
121
- parse(latstr, longstr)
122
- end
123
-
124
- #
125
- # Parse a lat/long value pair (in degrees)
126
- def parse(latstr, longstr)
127
- @latitude = ll_parse(latstr.split, "NS")
128
- @longitude = ll_parse(longstr.split, "EW")
116
+ # Set the lat/long from a hash.
117
+ def parse(data)
118
+ @latitude = ll_parse(data["latitude"], "NS")
119
+ @longitude = ll_parse(data["longitude"], "EW")
129
120
  end
130
121
 
131
122
  #
@@ -134,6 +125,13 @@ module SGS
134
125
  @latitude and @longitude
135
126
  end
136
127
 
128
+ #
129
+ # Convert the lat/long to a hash.
130
+ def to_hash
131
+ {"latitude" => ll_to_s(latitude, "NS"),
132
+ "longitude" => ll_to_s(longitude, "EW")}
133
+ end
134
+
137
135
  #
138
136
  # Display the lat/long as a useful string (in degrees).
139
137
  def to_s
@@ -186,8 +184,11 @@ module SGS
186
184
 
187
185
  private
188
186
  #
189
- # Parse a string into a lat or long.
190
- def ll_parse(args, nsew)
187
+ # Parse a string into a lat or long. The latitude or longitude string
188
+ # should be of the format dd mm ss.sssssC (where C is NS or EW). Note
189
+ # that latitude and longitude are saved internally in radians.
190
+ def ll_parse(value, nsew)
191
+ args = value.split
191
192
  dir = args[-1].gsub(/[\d\. ]+/, '').upcase
192
193
  args.map! {|val| val.to_f}
193
194
  val = args.shift
@@ -197,15 +198,22 @@ module SGS
197
198
  end
198
199
 
199
200
  #
200
- #
201
+ # Convert a latitude or longitude into an ASCII string of the form:
202
+ # dd mm ss.ssssssC (where C is NS or EW). The value should be in
203
+ # radians.
201
204
  def ll_to_s(val, str)
205
+ val = Bearing.rtod val
202
206
  if val < 0.0
203
207
  chr = str[1]
204
208
  val = -val
205
209
  else
206
210
  chr = str[0]
207
211
  end
208
- "%8.6f%c" % [Bearing.rtod(val), chr]
212
+ deg = val.to_i
213
+ val = (val - deg.to_f) * 60.0
214
+ min = val.to_i
215
+ sec = (val - min.to_f) * 60.0
216
+ "%d %d %8.6f%c" % [deg, min, sec, chr]
209
217
  end
210
218
 
211
219
  #
@@ -223,113 +231,4 @@ module SGS
223
231
  [fmt % [deg, val], ne.chr]
224
232
  end
225
233
  end
226
-
227
- ##
228
- # Class for dealing with the angle/distance vector.
229
- #
230
- # Note that for convenience, we retain the angle in Radians. The
231
- # distance is in nautical miles.
232
- class Bearing
233
- attr_accessor :distance
234
-
235
- #
236
- # Create the Bearing instance.
237
- def initialize(angle = 0.0, distance = 0.0)
238
- self.angle = angle.to_f
239
- self.distance = distance.to_f
240
- end
241
-
242
- #
243
- # Create a bearing from an angle in degrees.
244
- def self.degrees(angle, distance)
245
- new(Bearing.dtor(angle), distance)
246
- end
247
-
248
- #
249
- # Handy function to translate degrees to radians
250
- def self.dtor(deg)
251
- deg.to_f * Math::PI / 180.0
252
- end
253
-
254
- #
255
- # Handy function to translate radians to degrees
256
- def self.rtod(rad)
257
- rad.to_f * 180.0 / Math::PI
258
- end
259
-
260
- #
261
- # Handy function to re-adjust an angle away from negative
262
- def self.absolute(angle)
263
- (angle + 2.0 * Math::PI) % (2.0 * Math::PI)
264
- end
265
-
266
- #
267
- # Another handy function to re-adjust an angle (in degrees) away from
268
- # negative.
269
- def self.absolute_d(angle)
270
- (angle + 360) % 360
271
- end
272
-
273
- #
274
- # Haversine formula for calculating distance and angle, given two
275
- # locations.
276
- #
277
- # To calculate an angle and distance from two positions:
278
- #
279
- # This code was derived from formulae on the Movable Type site:
280
- # http://www.movable-type.co.uk/scripts/latlong.html
281
- #
282
- # var d = Math.acos(Math.sin(lat1)*Math.sin(lat2) +
283
- # Math.cos(lat1)*Math.cos(lat2) *
284
- # Math.cos(lon2-lon1)) * R;
285
- # var y = Math.sin(dLon) * Math.cos(lat2);
286
- # var x = Math.cos(lat1)*Math.sin(lat2) -
287
- # Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
288
- # var angle = Math.atan2(y, x).toDeg();
289
- def self.compute(loc1, loc2)
290
- bearing = new
291
- sin_lat1 = Math.sin(loc1.latitude)
292
- sin_lat2 = Math.sin(loc2.latitude)
293
- cos_lat1 = Math.cos(loc1.latitude)
294
- cos_lat2 = Math.cos(loc2.latitude)
295
- sin_dlon = Math.sin(loc2.longitude - loc1.longitude)
296
- cos_dlon = Math.cos(loc2.longitude - loc1.longitude)
297
- bearing.distance = Math.acos(sin_lat1*sin_lat2 + cos_lat1*cos_lat2*cos_dlon) *
298
- SGS::EARTH_RADIUS
299
- y = sin_dlon * cos_lat2
300
- x = cos_lat1 * sin_lat2 - sin_lat1 * cos_lat2 * cos_dlon
301
- bearing.angle = Math.atan2(y, x)
302
- bearing
303
- end
304
-
305
- #
306
- # Set the angle
307
- def angle=(angle)
308
- @angle = Bearing.absolute(angle)
309
- end
310
-
311
- #
312
- # Get the angle
313
- def angle
314
- @angle
315
- end
316
-
317
- #
318
- # Return the angle (in degrees)
319
- def angle_d
320
- Bearing.rtod(@angle).to_i
321
- end
322
-
323
- #
324
- # Get the back-angle (the angle viewed from the opposite end of the line)
325
- def back_angle
326
- Bearing.absolute(@angle - Math::PI)
327
- end
328
-
329
- #
330
- # Convert to a string
331
- def to_s
332
- "BRNG %03dd,%.3fnm" % [angle_d, @distance]
333
- end
334
- end
335
234
  end
data/lib/sgs/logger.rb CHANGED
@@ -1,96 +1,49 @@
1
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.
2
+ # Copyright (c) 2013-2023, Kalopa Robotics Limited. All rights
3
+ # reserved.
6
4
  #
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.
5
+ # This program is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU General Public License as
7
+ # published by the Free Software Foundation; either version 2 of
8
+ # the License, or (at your option) any later version.
11
9
  #
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.
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
15
14
  #
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.
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program; if not, write to the Free Software
17
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18
+ # 02110-1301, USA.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY KALOPA ROBOTICS LIMITED "AS IS" AND
21
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22
+ # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
23
+ # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KALOPA
24
+ # ROBOTICS LIMITED BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
27
+ # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
29
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
31
+ # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
+ #
33
+ # ABSTRACT
26
34
  #
27
35
 
28
36
  ##
29
- # Routines for handling sailboat logging.
37
+ # Routines for handling sailboat navigation and route planning.
30
38
  #
31
- require 'date'
32
-
33
39
  module SGS
34
- #
35
- # Waypoint, Attractor, and Repellor definitions
36
40
  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
41
  #
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
42
+ # Main daemon function (called from executable)
43
+ def self.daemon
44
+ loop do
45
+ sleep 300
46
+ end
94
47
  end
95
48
  end
96
49
  end