misc: updated go-nmea dependency and refactored code for v1.1.0

This commit is contained in:
evilsocket 2018-07-31 16:19:59 +02:00
parent f8ede4ddbe
commit 3d1936ef61
No known key found for this signature in database
GPG key ID: 1564D7F30393A456
22 changed files with 262 additions and 205 deletions

5
Gopkg.lock generated
View file

@ -2,11 +2,12 @@
[[projects]]
digest = "1:fa526d5f6ec66a1833c687768639251d6db3bc3b7f32abd0265ae9625a9233de"
digest = "1:4132a4623657c2ba93a7cf83dccc6869b3e3bb91dc2afefa7c7032e10ceeaa12"
name = "github.com/adrianmo/go-nmea"
packages = ["."]
pruneopts = "UT"
revision = "22095aa1b48050243d3eb9a001ca80eb91a0c6fa"
revision = "a32116e4989e2b0e17c057ee378b4d5246add74e"
version = "v1.1.0"
[[projects]]
branch = "master"

View file

@ -93,6 +93,10 @@
branch = "master"
name = "github.com/tarm/serial"
[[constraint]]
name = "github.com/adrianmo/go-nmea"
version = "1.1.0"
[prune]
go-tests = true
unused-packages = true

View file

@ -129,13 +129,10 @@ func (gps *GPS) Start() error {
for gps.Running() {
if line, err := gps.readLine(); err == nil {
if info, err := nmea.Parse(line); err == nil {
s := info.Sentence()
if s, err := nmea.Parse(line); err == nil {
// http://aprs.gids.nl/nmea/#gga
if s.Type == "GNGGA" {
gps.Session.GPS = info.(nmea.GNGGA)
} else {
log.Debug("Skipping message %s: %v", s.Type, s)
if m, ok := s.(nmea.GNGGA); ok {
gps.Session.GPS = m
}
} else {
log.Debug("Error parsing line '%s': %s", line, err)

View file

@ -5,7 +5,10 @@
language: go
go:
- 1.7
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- tip
matrix:

View file

@ -27,6 +27,7 @@ At this moment, this library supports the following sentence types:
- [GPVTG](http://aprs.gids.nl/nmea/#vtg) - Track Made Good and Ground Speed
- [GPZDA](http://aprs.gids.nl/nmea/#zda) - Date & time data
- [PGRME](http://aprs.gids.nl/nmea/#rme) - Estimated Position Error (Garmin proprietary sentence)
- [GPHDT](http://aprs.gids.nl/nmea/#hdt) - Actual vessel heading in degrees True
## Example
@ -36,17 +37,49 @@ package main
import (
"fmt"
"log"
"github.com/adrianmo/go-nmea"
)
func main() {
m, err := nmea.Parse("$GPRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*70")
if err == nil {
fmt.Printf("%+v\n", m)
sentence := "$GPRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*70"
s, err := nmea.Parse(sentence)
if err != nil {
log.Fatal(err)
}
m := s.(nmea.GPRMC)
fmt.Printf("Raw sentence: %v\n", m)
fmt.Printf("Time: %s\n", m.Time)
fmt.Printf("Validity: %s\n", m.Validity)
fmt.Printf("Latitude GPS: %s\n", nmea.FormatGPS(m.Latitude))
fmt.Printf("Latitude DMS: %s\n", nmea.FormatDMS(m.Latitude))
fmt.Printf("Longitude GPS: %s\n", nmea.FormatGPS(m.Longitude))
fmt.Printf("Longitude DMS: %s\n", nmea.FormatDMS(m.Longitude))
fmt.Printf("Speed: %f\n", m.Speed)
fmt.Printf("Course: %f\n", m.Course)
fmt.Printf("Date: %s\n", m.Date)
fmt.Printf("Variation: %f\n", m.Variation)
}
```
Output:
```
$ go run main/main.go
Raw sentence: $GPRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*70
Time: 22:05:16.0000
Validity: A
Latitude GPS: 5133.8200
Latitude DMS: 51° 33' 49.200000"
Longitude GPS: 042.2400
Longitude DMS: 0° 42' 14.400000"
Speed: 173.800000
Course: 231.800000
Date: 13/06/94
Variation: -4.200000
```
## Contributions
Please, feel free to implement support for new sentences, fix bugs, refactor code, etc. and send a pull-request to update the library.

1
vendor/github.com/adrianmo/go-nmea/VERSION generated vendored Normal file
View file

@ -0,0 +1 @@
v1.1.0

View file

@ -8,7 +8,7 @@ const (
// GLGSV represents the GPS Satellites in view
// http://aprs.gids.nl/nmea/#glgsv
type GLGSV struct {
Sent
BaseSentence
TotalMessages int64 // Total number of messages of this type in this cycle
MessageNumber int64 // Message number
NumberSVsInView int64 // Total number of SVs in view
@ -23,11 +23,11 @@ type GLGSVInfo struct {
SNR int64 // SNR, 00-99 dB (null when not tracking)
}
// NewGLGSV constructor
func NewGLGSV(s Sent) (GLGSV, error) {
// newGLGSV constructor
func newGLGSV(s BaseSentence) (GLGSV, error) {
p := newParser(s, PrefixGLGSV)
m := GLGSV{
Sent: s,
BaseSentence: s,
TotalMessages: p.Int64(0, "total number of messages"),
MessageNumber: p.Int64(1, "message number"),
NumberSVsInView: p.Int64(2, "number of SVs in view"),

View file

@ -7,10 +7,10 @@ const (
// GNGGA is the Time, position, and fix related data of the receiver.
type GNGGA struct {
Sent
BaseSentence
Time Time // Time of fix.
Latitude LatLong // Latitude.
Longitude LatLong // Longitude.
Latitude float64 // Latitude.
Longitude float64 // Longitude.
FixQuality string // Quality of fix.
NumSatellites int64 // Number of satellites in use.
HDOP float64 // Horizontal dilution of precision.
@ -20,16 +20,16 @@ type GNGGA struct {
DGPSId string // DGPS reference station ID.
}
// NewGNGGA constructor
func NewGNGGA(s Sent) (GNGGA, error) {
// newGNGGA constructor
func newGNGGA(s BaseSentence) (GNGGA, error) {
p := newParser(s, PrefixGNGGA)
return GNGGA{
Sent: s,
BaseSentence: s,
Time: p.Time(0, "time"),
Latitude: p.LatLong(1, 2, "latitude"),
Longitude: p.LatLong(3, 4, "longitude"),
FixQuality: p.EnumString(5, "fix quality", Invalid, GPS, DGPS),
NumSatellites: p.Int64(6, "number of satelites"),
FixQuality: p.EnumString(5, "fix quality", Invalid, GPS, DGPS, PPS, RTK, FRTK),
NumSatellites: p.Int64(6, "number of satellites"),
HDOP: p.Float64(7, "hdop"),
Altitude: p.Float64(8, "altitude"),
Separation: p.Float64(10, "separation"),

View file

@ -8,22 +8,22 @@ const (
// GNRMC is the Recommended Minimum Specific GNSS data.
// http://aprs.gids.nl/nmea/#rmc
type GNRMC struct {
Sent
BaseSentence
Time Time // Time Stamp
Validity string // validity - A-ok, V-invalid
Latitude LatLong // Latitude
Longitude LatLong // Longitude
Latitude float64 // Latitude
Longitude float64 // Longitude
Speed float64 // Speed in knots
Course float64 // True course
Date Date // Date
Variation float64 // Magnetic variation
}
// NewGNRMC constructor
func NewGNRMC(s Sent) (GNRMC, error) {
// newGNRMC constructor
func newGNRMC(s BaseSentence) (GNRMC, error) {
p := newParser(s, PrefixGNRMC)
m := GNRMC{
Sent: s,
BaseSentence: s,
Time: p.Time(0, "time"),
Validity: p.EnumString(1, "validity", ValidRMC, InvalidRMC),
Latitude: p.LatLong(2, 3, "latitude"),

3
vendor/github.com/adrianmo/go-nmea/go.mod generated vendored Normal file
View file

@ -0,0 +1,3 @@
module github.com/adrianmo/go-nmea
require github.com/stretchr/testify v1.2.1

View file

@ -9,15 +9,21 @@ const (
GPS = "1"
// DGPS fix quality
DGPS = "2"
// PPS fix
PPS = "3"
// RTK real time kinematic fix
RTK = "4"
// FRTK float RTK fix
FRTK = "5"
)
// GPGGA represents fix data.
// http://aprs.gids.nl/nmea/#gga
type GPGGA struct {
Sent
BaseSentence
Time Time // Time of fix.
Latitude LatLong // Latitude.
Longitude LatLong // Longitude.
Latitude float64 // Latitude.
Longitude float64 // Longitude.
FixQuality string // Quality of fix.
NumSatellites int64 // Number of satellites in use.
HDOP float64 // Horizontal dilution of precision.
@ -27,17 +33,17 @@ type GPGGA struct {
DGPSId string // DGPS reference station ID.
}
// NewGPGGA parses the GPGGA sentence into this struct.
// newGPGGA parses the GPGGA sentence into this struct.
// e.g: $GPGGA,034225.077,3356.4650,S,15124.5567,E,1,03,9.7,-25.0,M,21.0,M,,0000*58
func NewGPGGA(s Sent) (GPGGA, error) {
func newGPGGA(s BaseSentence) (GPGGA, error) {
p := newParser(s, PrefixGPGGA)
return GPGGA{
Sent: s,
BaseSentence: s,
Time: p.Time(0, "time"),
Latitude: p.LatLong(1, 2, "latitude"),
Longitude: p.LatLong(3, 4, "longitude"),
FixQuality: p.EnumString(5, "fix quality", Invalid, GPS, DGPS),
NumSatellites: p.Int64(6, "number of satelites"),
FixQuality: p.EnumString(5, "fix quality", Invalid, GPS, DGPS, PPS, RTK, FRTK),
NumSatellites: p.Int64(6, "number of satellites"),
HDOP: p.Float64(7, "hdap"),
Altitude: p.Float64(8, "altitude"),
Separation: p.Float64(10, "separation"),

View file

@ -12,18 +12,18 @@ const (
// GPGLL is Geographic Position, Latitude / Longitude and time.
// http://aprs.gids.nl/nmea/#gll
type GPGLL struct {
Sent
Latitude LatLong // Latitude
Longitude LatLong // Longitude
BaseSentence
Latitude float64 // Latitude
Longitude float64 // Longitude
Time Time // Time Stamp
Validity string // validity - A-valid
}
// NewGPGLL constructor
func NewGPGLL(s Sent) (GPGLL, error) {
// newGPGLL constructor
func newGPGLL(s BaseSentence) (GPGLL, error) {
p := newParser(s, PrefixGPGLL)
return GPGLL{
Sent: s,
BaseSentence: s,
Latitude: p.LatLong(0, 1, "latitude"),
Longitude: p.LatLong(2, 3, "longitude"),
Time: p.Time(4, "time"),

View file

@ -18,7 +18,7 @@ const (
// GPGSA represents overview satellite data.
// http://aprs.gids.nl/nmea/#gsa
type GPGSA struct {
Sent
BaseSentence
Mode string // The selection mode.
FixType string // The fix type.
SV []string // List of satellite PRNs used for this fix.
@ -27,17 +27,17 @@ type GPGSA struct {
VDOP float64 // Vertical dilution of precision.
}
// NewGPGSA parses the GPGSA sentence into this struct.
func NewGPGSA(s Sent) (GPGSA, error) {
// newGPGSA parses the GPGSA sentence into this struct.
func newGPGSA(s BaseSentence) (GPGSA, error) {
p := newParser(s, PrefixGPGSA)
m := GPGSA{
Sent: s,
BaseSentence: s,
Mode: p.EnumString(0, "selection mode", Auto, Manual),
FixType: p.EnumString(1, "fix type", FixNone, Fix2D, Fix3D),
}
// Satellites in view.
for i := 2; i < 14; i++ {
if v := p.String(i, "satelite in view"); v != "" {
if v := p.String(i, "satellite in view"); v != "" {
m.SV = append(m.SV, v)
}
}

View file

@ -8,7 +8,7 @@ const (
// GPGSV represents the GPS Satellites in view
// http://aprs.gids.nl/nmea/#gpgsv
type GPGSV struct {
Sent
BaseSentence
TotalMessages int64 // Total number of messages of this type in this cycle
MessageNumber int64 // Message number
NumberSVsInView int64 // Total number of SVs in view
@ -23,11 +23,11 @@ type GPGSVInfo struct {
SNR int64 // SNR, 00-99 dB (null when not tracking)
}
// NewGPGSV constructor
func NewGPGSV(s Sent) (GPGSV, error) {
// newGPGSV constructor
func newGPGSV(s BaseSentence) (GPGSV, error) {
p := newParser(s, PrefixGPGSV)
m := GPGSV{
Sent: s,
BaseSentence: s,
TotalMessages: p.Int64(0, "total number of messages"),
MessageNumber: p.Int64(1, "message number"),
NumberSVsInView: p.Int64(2, "number of SVs in view"),

25
vendor/github.com/adrianmo/go-nmea/gphdt.go generated vendored Normal file
View file

@ -0,0 +1,25 @@
package nmea
const (
// PrefixGPHDT prefix of GPHDT sentence type
PrefixGPHDT = "GPHDT"
)
// GPHDT is the Actual vessel heading in degrees True.
// http://aprs.gids.nl/nmea/#hdt
type GPHDT struct {
BaseSentence
Heading float64 // Heading in degrees
True bool // Heading is relative to true north
}
// newGPHDT constructor
func newGPHDT(s BaseSentence) (GPHDT, error) {
p := newParser(s, PrefixGPHDT)
m := GPHDT{
BaseSentence: s,
Heading: p.Float64(0, "heading"),
True: p.EnumString(1, "true", "T") == "T",
}
return m, p.Err()
}

View file

@ -12,22 +12,22 @@ const (
// GPRMC is the Recommended Minimum Specific GNSS data.
// http://aprs.gids.nl/nmea/#rmc
type GPRMC struct {
Sent
BaseSentence
Time Time // Time Stamp
Validity string // validity - A-ok, V-invalid
Latitude LatLong // Latitude
Longitude LatLong // Longitude
Latitude float64 // Latitude
Longitude float64 // Longitude
Speed float64 // Speed in knots
Course float64 // True course
Date Date // Date
Variation float64 // Magnetic variation
}
// NewGPRMC constructor
func NewGPRMC(s Sent) (GPRMC, error) {
// newGPRMC constructor
func newGPRMC(s BaseSentence) (GPRMC, error) {
p := newParser(s, PrefixGPRMC)
m := GPRMC{
Sent: s,
BaseSentence: s,
Time: p.Time(0, "time"),
Validity: p.EnumString(1, "validity", ValidRMC, InvalidRMC),
Latitude: p.LatLong(2, 3, "latitude"),

View file

@ -8,19 +8,19 @@ const (
// GPVTG represents track & speed data.
// http://aprs.gids.nl/nmea/#vtg
type GPVTG struct {
Sent
BaseSentence
TrueTrack float64
MagneticTrack float64
GroundSpeedKnots float64
GroundSpeedKPH float64
}
// NewGPVTG parses the GPVTG sentence into this struct.
// newGPVTG parses the GPVTG sentence into this struct.
// e.g: $GPVTG,360.0,T,348.7,M,000.0,N,000.0,K*43
func NewGPVTG(s Sent) (GPVTG, error) {
func newGPVTG(s BaseSentence) (GPVTG, error) {
p := newParser(s, PrefixGPVTG)
return GPVTG{
Sent: s,
BaseSentence: s,
TrueTrack: p.Float64(0, "true track"),
MagneticTrack: p.Float64(2, "magnetic track"),
GroundSpeedKnots: p.Float64(4, "ground speed (knots)"),

View file

@ -8,7 +8,7 @@ const (
// GPZDA represents date & time data.
// http://aprs.gids.nl/nmea/#zda
type GPZDA struct {
Sent
BaseSentence
Time Time
Day int64
Month int64
@ -17,11 +17,11 @@ type GPZDA struct {
OffsetMinutes int64 // Local time zone offset from GMT, minutes
}
// NewGPZDA constructor
func NewGPZDA(s Sent) (GPZDA, error) {
// newGPZDA constructor
func newGPZDA(s BaseSentence) (GPZDA, error) {
p := newParser(s, PrefixGPZDA)
return GPZDA{
Sent: s,
BaseSentence: s,
Time: p.Time(0, "time"),
Day: p.Int64(1, "day"),
Month: p.Int64(2, "month"),

View file

@ -8,21 +8,21 @@ import (
// parser provides a simple way of accessing and parsing
// sentence fields
type parser struct {
Sent
BaseSentence
prefix string
err error
}
// newParser constructor
func newParser(s Sent, prefix string) *parser {
p := &parser{Sent: s, prefix: prefix}
func newParser(s BaseSentence, prefix string) *parser {
p := &parser{BaseSentence: s, prefix: prefix}
if p.Type != prefix {
p.SetErr("prefix", p.Type)
}
return p
}
// Err returns the first error encounterd during the parser's usage.
// Err returns the first error encountered during the parser's usage.
func (p *parser) Err() error {
return p.err
}
@ -48,10 +48,10 @@ func (p *parser) String(i int, context string) string {
}
// EnumString returns the field value at the specified index.
// An error occurs if the value is not one of the options.
// An error occurs if the value is not one of the options and not empty.
func (p *parser) EnumString(i int, context string, options ...string) string {
s := p.String(i, context)
if p.err != nil {
if p.err != nil || s == "" {
return ""
}
for _, o := range options {
@ -64,7 +64,7 @@ func (p *parser) EnumString(i int, context string, options ...string) string {
}
// Int64 returns the int64 value at the specified index.
// If the value is an emtpy string, 0 is returned.
// If the value is an empty string, 0 is returned.
func (p *parser) Int64(i int, context string) int64 {
s := p.String(i, context)
if p.err != nil {
@ -126,7 +126,7 @@ func (p *parser) Date(i int, context string) Date {
}
// LatLong returns the coordinate value of the specified fields.
func (p *parser) LatLong(i, j int, context string) LatLong {
func (p *parser) LatLong(i, j int, context string) float64 {
a := p.String(i, context)
b := p.String(j, context)
if p.err != nil {

View file

@ -10,14 +10,14 @@ const (
// PGRME is Estimated Position Error (Garmin proprietary sentence)
// http://aprs.gids.nl/nmea/#rme
type PGRME struct {
Sent
BaseSentence
Horizontal float64 // Estimated horizontal position error (HPE) in metres
Vertical float64 // Estimated vertical position error (VPE) in metres
Spherical float64 // Overall spherical equivalent position error in meters
}
// NewPGRME constructor
func NewPGRME(s Sent) (PGRME, error) {
// newPGRME constructor
func newPGRME(s BaseSentence) (PGRME, error) {
p := newParser(s, PrefixPGRME)
horizontal := p.Float64(0, "horizontal error")
@ -30,7 +30,7 @@ func NewPGRME(s Sent) (PGRME, error) {
_ = p.EnumString(5, "spherical error unit", ErrorUnit)
return PGRME{
Sent: s,
BaseSentence: s,
Horizontal: horizontal,
Vertical: vertial,
Spherical: spherical,

View file

@ -16,43 +16,35 @@ const (
ChecksumSep = "*"
)
// Message interface for all NMEA sentence
type Message interface {
// Sentence interface for all NMEA sentence
type Sentence interface {
fmt.Stringer
Sentence() Sent
Prefix() string
Validate() error
}
// Sent contains the information about the NMEA sentence
type Sent struct {
// BaseSentence contains the information about the NMEA sentence
type BaseSentence struct {
Type string // The sentence type (e.g $GPGSA)
Fields []string // Array of fields
Checksum string // The Checksum
Raw string // The raw NMEA sentence received
}
// Sentence returns the Messages Sent
func (s Sent) Sentence() Sent { return s }
// Prefix returns the type of the message
func (s Sent) Prefix() string { return s.Type }
func (s BaseSentence) Prefix() string { return s.Type }
// String formats the sentence into a string
func (s Sent) String() string { return s.Raw }
func (s BaseSentence) String() string { return s.Raw }
// Validate returns an error if the sentence is not valid
func (s Sent) Validate() error { return nil }
// ParseSentence parses a raw message into it's fields
func ParseSentence(raw string) (Sent, error) {
// parseSentence parses a raw message into it's fields
func parseSentence(raw string) (BaseSentence, error) {
startIndex := strings.Index(raw, SentenceStart)
if startIndex != 0 {
return Sent{}, fmt.Errorf("nmea: sentence does not start with a '$'")
return BaseSentence{}, fmt.Errorf("nmea: sentence does not start with a '$'")
}
sumSepIndex := strings.Index(raw, ChecksumSep)
if sumSepIndex == -1 {
return Sent{}, fmt.Errorf("nmea: sentence does not contain checksum separator")
return BaseSentence{}, fmt.Errorf("nmea: sentence does not contain checksum separator")
}
var (
fieldsRaw = raw[startIndex+1 : sumSepIndex]
@ -62,10 +54,10 @@ func ParseSentence(raw string) (Sent, error) {
)
// Validate the checksum
if checksum != checksumRaw {
return Sent{}, fmt.Errorf(
return BaseSentence{}, fmt.Errorf(
"nmea: sentence checksum mismatch [%s != %s]", checksum, checksumRaw)
}
return Sent{
return BaseSentence{
Type: fields[0],
Fields: fields[1:],
Checksum: checksumRaw,
@ -84,34 +76,36 @@ func xorChecksum(s string) string {
}
// Parse parses the given string into the correct sentence type.
func Parse(raw string) (Message, error) {
s, err := ParseSentence(raw)
func Parse(raw string) (Sentence, error) {
s, err := parseSentence(raw)
if err != nil {
return nil, err
}
switch s.Type {
case PrefixGPRMC:
return NewGPRMC(s)
return newGPRMC(s)
case PrefixGNRMC:
return NewGNRMC(s)
return newGNRMC(s)
case PrefixGPGGA:
return NewGPGGA(s)
return newGPGGA(s)
case PrefixGNGGA:
return NewGNGGA(s)
return newGNGGA(s)
case PrefixGPGSA:
return NewGPGSA(s)
return newGPGSA(s)
case PrefixGPGLL:
return NewGPGLL(s)
return newGPGLL(s)
case PrefixGPVTG:
return NewGPVTG(s)
return newGPVTG(s)
case PrefixGPZDA:
return NewGPZDA(s)
return newGPZDA(s)
case PrefixPGRME:
return NewPGRME(s)
return newPGRME(s)
case PrefixGPGSV:
return NewGPGSV(s)
return newGPGSV(s)
case PrefixGLGSV:
return NewGLGSV(s)
return newGLGSV(s)
case PrefixGPHDT:
return newGPHDT(s)
default:
return nil, fmt.Errorf("nmea: sentence type '%s' not implemented", s.Type)
}

View file

@ -9,7 +9,6 @@ import (
"strconv"
"strings"
"unicode"
// "unicode/utf8"
)
const (
@ -31,42 +30,6 @@ const (
West = "W"
)
// LatLong type
type LatLong float64
// PrintGPS returns the GPS format for the given LatLong.
func (l LatLong) PrintGPS() string {
padding := ""
value := float64(l)
degrees := math.Floor(math.Abs(value))
fraction := (math.Abs(value) - degrees) * 60
if fraction < 10 {
padding = "0"
}
return fmt.Sprintf("%d%s%.4f", int(degrees), padding, fraction)
}
// PrintDMS returns the degrees, minutes, seconds format for the given LatLong.
func (l LatLong) PrintDMS() string {
val := math.Abs(float64(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)
}
//ValidRange validates if the range is between -180 and +180.
func (l LatLong) ValidRange() bool {
return -180.0 <= l && l <= 180.0
}
// IsNear returns whether the coordinate is near the other coordinate,
// by no further than the given distance away.
func (l LatLong) IsNear(o LatLong, max float64) bool {
return math.Abs(float64(l-o)) <= max
}
// ParseLatLong parses the supplied string into the LatLong.
//
// Supported formats are:
@ -74,27 +37,30 @@ func (l LatLong) IsNear(o LatLong, max float64) bool {
// - Decimal (e.g. 33.23454)
// - GPS (e.g 15113.4322S)
//
func ParseLatLong(s string) (LatLong, error) {
var l LatLong
var err error
invalid := LatLong(0.0) // The invalid value to return.
if l, err = ParseDMS(s); err == nil {
return l, nil
} else if l, err = ParseGPS(s); err == nil {
return l, nil
} else if l, err = ParseDecimal(s); err == nil {
return l, nil
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)
}
if !l.ValidRange() {
return invalid, errors.New("coordinate is not in range -180, 180")
if l < -180.0 || 180.0 < l {
return 0, errors.New("coordinate is not in range -180, 180")
}
return invalid, fmt.Errorf("cannot parse [%s], unknown format", s)
return l, nil
}
// ParseGPS parses a GPS/NMEA coordinate.
// e.g 15113.4322S
func ParseGPS(s string) (LatLong, error) {
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 {
@ -106,28 +72,39 @@ func ParseGPS(s string) (LatLong, error) {
value = degrees + minutes/60
if dir == North || dir == East {
return LatLong(value), nil
return value, nil
} else if dir == South || dir == West {
return LatLong(0 - value), nil
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) (LatLong, error) {
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 LatLong(0.0), errors.New("parse error (not decimal coordinate)")
return 0.0, errors.New("parse error (not decimal coordinate)")
}
return LatLong(l), nil
return l, nil
}
// ParseDMS parses a coordinate in degrees, minutes, seconds.
// - e.g. 33° 23' 22"
func ParseDMS(s string) (LatLong, error) {
func ParseDMS(s string) (float64, error) {
degrees := 0
minutes := 0
seconds := 0.0
@ -138,42 +115,55 @@ func ParseDMS(s string) (LatLong, error) {
var err error
for i, r := range s {
if unicode.IsNumber(r) || r == '.' {
switch {
case unicode.IsNumber(r) || r == '.':
if !endNumber {
tmpBytes = append(tmpBytes, s[i])
} else {
return 0, errors.New("parse error (no delimiter)")
}
} else if unicode.IsSpace(r) && len(tmpBytes) > 0 {
case unicode.IsSpace(r) && len(tmpBytes) > 0:
endNumber = true
} else if r == Degrees {
case r == Degrees:
if degrees, err = strconv.Atoi(string(tmpBytes)); err != nil {
return 0, errors.New("parse error (degrees)")
}
tmpBytes = tmpBytes[:0]
endNumber = false
} else if s[i] == Minutes {
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
} else if s[i] == Seconds {
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
} else if unicode.IsSpace(r) && len(tmpBytes) == 0 {
case unicode.IsSpace(r) && len(tmpBytes) == 0:
continue
} else {
default:
return 0, fmt.Errorf("parse error (unknown symbol [%d])", s[i])
}
}
val := LatLong(float64(degrees) + (float64(minutes) / 60.0) + (float64(seconds) / 60.0 / 60.0))
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