File: types.go

package info (click to toggle)
golang-github-adrianmo-go-nmea 1.10.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 900 kB
  • sloc: makefile: 15
file content (432 lines) | stat: -rw-r--r-- 13,251 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
package nmea

// Latitude / longitude representation.

import (
	"errors"
	"fmt"
	"math"
	"regexp"
	"strconv"
	"strings"
	"time"
	"unicode"
)

const (
	// StatusValid indicated status having valid value
	StatusValid = "A"
	// StatusInvalid indicated status having invalid value
	StatusInvalid = "V"
)

const (
	// UnitAmpere is unit for current in Amperes
	UnitAmpere = "A"
	// UnitBars is unit for pressure in Bars
	UnitBars = "B"
	// UnitBinary is unit for binary data
	UnitBinary = "B"
	// UnitCelsius is unit for temperature in Celsius
	UnitCelsius = TemperatureCelsius
	// UnitFahrenheit is unit for temperature in Fahrenheit
	UnitFahrenheit = TemperatureFahrenheit
	// UnitDegrees is unit for angular displacement in Degrees
	UnitDegrees = "D"
	// UnitHertz is unit for frequency in Hertz
	UnitHertz = "H"
	// UnitLitresPerSecond is unit for volumetric flow in Litres per second
	UnitLitresPerSecond = "I"
	// UnitKelvin is unit of temperature in Kelvin
	UnitKelvin = TemperatureKelvin
	// UnitKilogramPerCubicMetre is unit of density in kilogram per cubic metre
	UnitKilogramPerCubicMetre = "K"
	// UnitMeters is unit of distance in Meters
	UnitMeters = DistanceUnitMetre
	// UnitNewtons is unit of force in Newtons (1 kg*m/s2)
	UnitNewtons = "N"
	// UnitCubicMeters is unit of volume in cubic meters
	UnitCubicMeters = "M"
	// UnitRevolutionsPerMinute is unit of rotational speed or the frequency of rotation around a fixed axis in revolutions per minute (RPM)
	UnitRevolutionsPerMinute = "R"
	// UnitPercent is percent of full range
	UnitPercent = "P"
	// UnitPascal is unit of pressure in Pascals
	UnitPascal = "P"
	// UnitPartsPerThousand is in parts-per notation set of pseudo-unit to describe small values of miscellaneous dimensionless quantities, e.g. mole fraction or mass fraction.
	UnitPartsPerThousand = "S"
	// UnitVolts is unit of voltage in Volts
	UnitVolts = "V"
)

const (
	// SpeedKnots is a unit of speed equal to one nautical mile per hour, exactly 1.852 km/h (approximately 1.151 mph or 0.514 m/s)
	SpeedKnots = "N"
	// SpeedMeterPerSecond is unit of speed of 1 meter per second
	SpeedMeterPerSecond = "M"
	// SpeedKilometerPerHour is unit of speed of 1 kilometer per hour
	SpeedKilometerPerHour = "K"
)

const (
	// TemperatureCelsius is unit of temperature measured in celsius. °C = (°F − 32) / 1,8
	TemperatureCelsius = "C"
	// TemperatureFahrenheit is unit of temperature measured in fahrenheits. °F = °C * 1,8 + 32
	TemperatureFahrenheit = "F"
	// TemperatureKelvin is unit of temperature measured in kelvins. K = °C + 273,15
	TemperatureKelvin = "K"
)

// In navigation, the heading of a vessel or object is the compass direction in which the craft's bow or nose is pointed.
// Note that the heading may not necessarily be the direction that the vehicle actually travels, which is known as
// its course or track.
// https://en.wikipedia.org/wiki/Heading_(navigation)
const (
	// HeadingMagnetic - Magnetic heading is your direction relative to magnetic north, read from your magnetic compass.
	// Magnetic north is the point on the Earth's surface where its magnetic field points directly downwards.
	HeadingMagnetic = "M"
	// HeadingTrue - True heading is your direction relative to true north, or the geographic north pole.
	// True north is the northern axis of rotation of the Earth. It is the point where the lines of longitude converge
	// on maps.
	HeadingTrue = "T"
)

// In nautical navigation the absolute bearing is the clockwise angle between north and an object observed from the vessel.
// https://en.wikipedia.org/wiki/Bearing_(angle)
const (
	// BearingMagnetic is the clockwise angle between Earth's magnetic north and an object observed from the vessel.
	BearingMagnetic = "M"
	// BearingTrue  is the clockwise angle between Earth's true (geographical) north and an object observed from the vessel.
	BearingTrue = "T"
)

// FAAMode is type for FAA mode indicator (NMEA 2.3 and later).
// In NMEA 2.3, several sentences (APB, BWC, BWR, GLL, RMA, RMB, RMC, VTG, WCV, and XTE) got a new last field carrying
// the signal integrity information needed by the FAA.
// Source: https://www.xj3.nl/dokuwiki/doku.php?id=nmea
// Note: there can be other values (proprietary).
const (
	// FAAModeAutonomous is Autonomous mode
	FAAModeAutonomous = "A"
	// FAAModeDifferential is Differential Mode
	FAAModeDifferential = "D"
	// FAAModeEstimated is Estimated (dead-reckoning) mode
	FAAModeEstimated = "E"
	// FAAModeRTKFloat is RTK Float mode
	FAAModeRTKFloat = "F"
	// FAAModeManualInput is Manual Input Mode
	FAAModeManualInput = "M"
	// FAAModeDataNotValid is Data Not Valid
	FAAModeDataNotValid = "N"
	// FAAModePrecise is Precise (NMEA4.00+)
	FAAModePrecise = "P"
	// FAAModeRTKInteger is RTK Integer mode
	FAAModeRTKInteger = "R"
	// FAAModeSimulated is Simulated Mode
	FAAModeSimulated = "S"
)

// Navigation Status (NMEA 4.1 and later)
const (
	// NavStatusSimulated is a deprecated placeholder for backwards
	// compatibility. There is no such status in NMEA.
	// Deprecated: use NavStatusSafe
	NavStatusSimulated = "S"
	// NavStatusDataValid is a deprecated placeholder for backwards
	// compatibility. There is no such status in NMEA.
	// Deprecated: use NavStatusNotValid
	NavStatusDataValid = "V"
	// NavStatusSafe is Safe (within selected accuracy level)
	NavStatusSafe = "S"
	// NavStatusCaution is Caution (integrity not available)
	NavStatusCaution = "C"
	// NavStatusUnsafe is Unsafe (outside selected accuracy level)
	NavStatusUnsafe = "U"
	// NavStatusNotValid is Not Valid (equipment does not provide navigation status information)
	NavStatusNotValid = "V"
)

const (
	// DistanceUnitKilometre is unit for distance in kilometres (1km = 1000m)
	DistanceUnitKilometre = "K"
	// DistanceUnitNauticalMile is unit for distance in nautical miles (1nmi = 1852m)
	DistanceUnitNauticalMile = "N"
	// DistanceUnitStatuteMile is unit for distance in statute miles (1smi = 5,280 feet = 1609.344m)
	DistanceUnitStatuteMile = "S"
	// DistanceUnitMetre is unit for distance in metres
	DistanceUnitMetre = "M"
	// DistanceUnitFeet is unit for distance in feets (1f = 0.3048m)
	DistanceUnitFeet = "f"
	// DistanceUnitFathom is unit for distance in fathoms (1fm = 6ft = 1,8288m)
	DistanceUnitFathom = "F"
)

const (
	// Degrees value
	Degrees = '\u00B0'
	// Minutes value
	Minutes = '\''
	// Seconds value
	Seconds = '"'
	// Point value
	Point = '.'
	// North value
	North = "N"
	// South value
	South = "S"
	// East value
	East = "E"
	// West value
	West = "W"
	// Left value
	Left = "L"
	// Right value
	Right = "R"
)

// ParseLatLong parses the supplied string into the LatLong.
//
// Supported formats are:
// - DMS (e.g. 33° 23' 22")
// - Decimal (e.g. 33.23454)
// - GPS (e.g 15113.4322 S)
func ParseLatLong(s string) (float64, error) {
	var l float64
	if v, err := ParseDMS(s); err == nil {
		l = v
	} else if v, err := ParseGPS(s); err == nil {
		l = v
	} else if v, err := ParseDecimal(s); err == nil {
		l = v
	} else {
		return 0, fmt.Errorf("cannot parse [%s], unknown format", s)
	}
	return l, nil
}

// ParseGPS parses a GPS/NMEA coordinate.
// e.g `15113.4322 S`
func ParseGPS(s string) (float64, error) {
	parts := strings.Split(s, " ")
	if len(parts) != 2 {
		return 0, fmt.Errorf("invalid format: %s", s)
	}
	dir := parts[1]
	value, err := strconv.ParseFloat(parts[0], 64)
	if err != nil {
		return 0, fmt.Errorf("parse error: %s", err.Error())
	}

	degrees := math.Floor(value / 100)
	minutes := value - (degrees * 100)
	value = degrees + minutes/60

	if dir == North || dir == East {
		return value, nil
	} else if dir == South || dir == West {
		return 0 - value, nil
	} else {
		return 0, fmt.Errorf("invalid direction [%s]", dir)
	}
}

// FormatGPS formats a GPS/NMEA coordinate
func FormatGPS(l float64) string {
	padding := ""
	degrees := math.Floor(math.Abs(l))
	fraction := (math.Abs(l) - degrees) * 60
	if fraction < 10 {
		padding = "0"
	}
	return fmt.Sprintf("%d%s%.4f", int(degrees), padding, fraction)
}

// ParseDecimal parses a decimal format coordinate.
// e.g: 151.196019
func ParseDecimal(s string) (float64, error) {
	// Make sure it parses as a float.
	l, err := strconv.ParseFloat(s, 64)
	if err != nil || s[0] != '-' && len(strings.Split(s, ".")[0]) > 3 {
		return 0.0, errors.New("parse error (not decimal coordinate)")
	}
	return l, nil
}

// ParseDMS parses a coordinate in degrees, minutes, seconds.
// - e.g. 33° 23' 22"
func ParseDMS(s string) (float64, error) {
	degrees := 0
	minutes := 0
	seconds := 0.0
	// Whether a number has finished parsing (i.e whitespace after it)
	endNumber := false
	// Temporary parse buffer.
	tmpBytes := []byte{}
	var err error

	for i, r := range s {
		switch {
		case unicode.IsNumber(r) || r == '.':
			if !endNumber {
				tmpBytes = append(tmpBytes, s[i])
			} else {
				return 0, errors.New("parse error (no delimiter)")
			}
		case unicode.IsSpace(r) && len(tmpBytes) > 0:
			endNumber = true
		case r == Degrees:
			if degrees, err = strconv.Atoi(string(tmpBytes)); err != nil {
				return 0, errors.New("parse error (degrees)")
			}
			tmpBytes = tmpBytes[:0]
			endNumber = false
		case s[i] == Minutes:
			if minutes, err = strconv.Atoi(string(tmpBytes)); err != nil {
				return 0, errors.New("parse error (minutes)")
			}
			tmpBytes = tmpBytes[:0]
			endNumber = false
		case s[i] == Seconds:
			if seconds, err = strconv.ParseFloat(string(tmpBytes), 64); err != nil {
				return 0, errors.New("parse error (seconds)")
			}
			tmpBytes = tmpBytes[:0]
			endNumber = false
		case unicode.IsSpace(r) && len(tmpBytes) == 0:
			continue
		default:
			return 0, fmt.Errorf("parse error (unknown symbol [%d])", s[i])
		}
	}
	if len(tmpBytes) > 0 {
		return 0, fmt.Errorf("parse error (trailing data [%s])", string(tmpBytes))
	}
	val := float64(degrees) + (float64(minutes) / 60.0) + (float64(seconds) / 60.0 / 60.0)
	return val, nil
}

// FormatDMS returns the degrees, minutes, seconds format for the given LatLong.
func FormatDMS(l float64) string {
	val := math.Abs(l)
	degrees := int(math.Floor(val))
	minutes := int(math.Floor(60 * (val - float64(degrees))))
	seconds := 3600 * (val - float64(degrees) - (float64(minutes) / 60))
	return fmt.Sprintf("%d\u00B0 %d' %f\"", degrees, minutes, seconds)
}

// Time type
type Time struct {
	Valid       bool
	Hour        int
	Minute      int
	Second      int
	Millisecond int
}

// String representation of Time
func (t Time) String() string {
	seconds := float64(t.Second) + float64(t.Millisecond)/1000
	return fmt.Sprintf("%02d:%02d:%07.4f", t.Hour, t.Minute, seconds)
}

// timeRe is used to validate time strings
var timeRe = regexp.MustCompile(`^\d{6}(\.\d*)?$`)

// ParseTime parses wall clock time.
// e.g. hhmmss.ssss
// An empty time string will result in an invalid time.
func ParseTime(s string) (Time, error) {
	if s == "" {
		return Time{}, nil
	}
	if !timeRe.MatchString(s) {
		return Time{}, fmt.Errorf("parse time: expected hhmmss.ss format, got '%s'", s)
	}
	hour, _ := strconv.Atoi(s[:2])
	minute, _ := strconv.Atoi(s[2:4])
	second, _ := strconv.ParseFloat(s[4:], 64)
	whole, frac := math.Modf(second)
	return Time{true, hour, minute, int(whole), int(math.Round(frac * 1000))}, nil
}

// Date type
type Date struct {
	Valid bool
	DD    int
	MM    int
	YY    int
}

// String representation of date
func (d Date) String() string {
	return fmt.Sprintf("%02d/%02d/%02d", d.DD, d.MM, d.YY)
}

// ParseDate field ddmmyy format
func ParseDate(ddmmyy string) (Date, error) {
	if ddmmyy == "" {
		return Date{}, nil
	}
	if len(ddmmyy) != 6 {
		return Date{}, fmt.Errorf("parse date: exptected ddmmyy format, got '%s'", ddmmyy)
	}
	dd, err := strconv.Atoi(ddmmyy[0:2])
	if err != nil {
		return Date{}, errors.New(ddmmyy)
	}
	mm, err := strconv.Atoi(ddmmyy[2:4])
	if err != nil {
		return Date{}, errors.New(ddmmyy)
	}
	yy, err := strconv.Atoi(ddmmyy[4:6])
	if err != nil {
		return Date{}, errors.New(ddmmyy)
	}
	return Date{true, dd, mm, yy}, nil
}

// DateTime converts the provided Date and Time values to a standard UTC time.Time.
// The referenceYear parameter is used to determine the offset (century) for the two-digit year in Date.
// For example, if the referenceYear is 2024, the offset used is 2000; and the input date's year is prepended with 20.
// If referenceYear is 0, the current UTC year is used.
// If either Date or Time is not valid, DateTime returns the zero time.Time.
func DateTime(referenceYear int, d Date, t Time) time.Time {
	if !d.Valid || !t.Valid {
		return time.Time{}
	}
	if referenceYear == 0 {
		referenceYear = time.Now().UTC().Year()
	}
	century := referenceYear / 100 * 100 // truncate the last two digits (year within century); keep first two digits of the full year
	return time.Date(century+d.YY, time.Month(d.MM), d.DD,
		t.Hour, t.Minute, t.Second, t.Millisecond*1e6,
		time.UTC)
}

// LatDir returns the latitude direction symbol
func LatDir(l float64) string {
	if l < 0.0 {
		return South
	}
	return North
}

// LonDir returns the longitude direction symbol
func LonDir(l float64) string {
	if l < 0.0 {
		return West
	}
	return East
}

// Float64 is a nullable float64 value
type Float64 struct {
	Value float64
	Valid bool
}

// Int64 is a nullable int64 value
type Int64 struct {
	Value int64
	Valid bool
}