314 lines
7.6 KiB
Go
314 lines
7.6 KiB
Go
package aprs
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.maze.io/go/ham/util/maidenhead"
|
|
)
|
|
|
|
var (
|
|
// Position ambiguity replacement
|
|
disambiguation = []int{2, 3, 5, 6, 12, 13, 15, 16}
|
|
|
|
miceCodes = map[rune]map[int]string{
|
|
'0': {0: "0", 1: "0", 2: "S", 3: "0", 4: "E"},
|
|
'1': {0: "1", 1: "0", 2: "S", 3: "0", 4: "E"},
|
|
'2': {0: "2", 1: "0", 2: "S", 3: "0", 4: "E"},
|
|
'3': {0: "3", 1: "0", 2: "S", 3: "0", 4: "E"},
|
|
'4': {0: "4", 1: "0", 2: "S", 3: "0", 4: "E"},
|
|
'5': {0: "5", 1: "0", 2: "S", 3: "0", 4: "E"},
|
|
'6': {0: "6", 1: "0", 2: "S", 3: "0", 4: "E"},
|
|
'7': {0: "7", 1: "0", 2: "S", 3: "0", 4: "E"},
|
|
'8': {0: "8", 1: "0", 2: "S", 3: "0", 4: "E"},
|
|
'9': {0: "9", 1: "0", 2: "S", 3: "0", 4: "E"},
|
|
'A': {0: "0", 1: "1 (Custom)"},
|
|
'B': {0: "1", 1: "1 (Custom)"},
|
|
'C': {0: "2", 1: "1 (Custom)"},
|
|
'D': {0: "3", 1: "1 (Custom)"},
|
|
'E': {0: "4", 1: "1 (Custom)"},
|
|
'F': {0: "5", 1: "1 (Custom)"},
|
|
'G': {0: "6", 1: "1 (Custom)"},
|
|
'H': {0: "7", 1: "1 (Custom)"},
|
|
'I': {0: "8", 1: "1 (Custom)"},
|
|
'J': {0: "9", 1: "1 (Custom)"},
|
|
'K': {0: " ", 1: "1 (Custom)"},
|
|
'L': {0: " ", 1: "0", 2: "S", 3: "0", 4: "E"},
|
|
'P': {0: "0", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
|
'Q': {0: "1", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
|
'R': {0: "2", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
|
'S': {0: "3", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
|
'T': {0: "4", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
|
'U': {0: "5", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
|
'V': {0: "6", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
|
'W': {0: "7", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
|
'X': {0: "8", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
|
'Y': {0: "9", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
|
'Z': {0: " ", 1: "1 (Std)", 2: "N", 3: "100", 4: "W"},
|
|
}
|
|
|
|
miceMsgTypes = map[string]string{
|
|
"000": "Emergency",
|
|
"001 (Std)": "Priority",
|
|
"001 (Custom)": "Custom-6",
|
|
"010 (Std)": "Special",
|
|
"010 (Custom)": "Custom-5",
|
|
"011 (Std)": "Committed",
|
|
"011 (Custom)": "Custom-4",
|
|
"100 (Std)": "Returning",
|
|
"100 (Custom)": "Custom-3",
|
|
"101 (Std)": "In Service",
|
|
"101 (Custom)": "Custom-2",
|
|
"110 (Std)": "En Route",
|
|
"110 (Custom)": "Custom-1",
|
|
"111 (Std)": "Off Duty",
|
|
"111 (Custom)": "Custom-0",
|
|
}
|
|
)
|
|
|
|
const (
|
|
gridChars = "ABCDEFGHIJKLMNOPQRSTUVWX0123456789"
|
|
|
|
messageTypeStd = "Std"
|
|
messageTypeCustom = "Custom"
|
|
)
|
|
|
|
// Position contains GPS coordinates.
|
|
type Position struct {
|
|
Latitude float64 `json:"latitude"` // Degrees
|
|
Longitude float64 `json:"longitude"` // Degrees
|
|
Ambiguity int `json:"ambiguity,omitempty"`
|
|
Symbol Symbol `json:"symbol"`
|
|
Compressed bool `json:"compressed,omitempty"`
|
|
}
|
|
|
|
func (pos Position) String() string {
|
|
if pos.Ambiguity == 0 {
|
|
return fmt.Sprintf("{%f, %f}", pos.Latitude, pos.Longitude)
|
|
}
|
|
return fmt.Sprintf("{%f, %f}, ambiguity=%d", pos.Latitude, pos.Longitude, pos.Ambiguity)
|
|
}
|
|
|
|
func ParseUncompressedPosition(s string) (Position, string, error) {
|
|
// APRS PROTOCOL REFERENCE 1.0.1 Chapter 8, page 32 (42 in PDF)
|
|
|
|
pos := Position{}
|
|
|
|
if len(s) < 18 {
|
|
return pos, "", ErrInvalidPosition
|
|
}
|
|
|
|
b := []byte(s)
|
|
for _, p := range disambiguation {
|
|
if b[p] == ' ' {
|
|
pos.Ambiguity++
|
|
b[p] = '0'
|
|
}
|
|
}
|
|
s = string(b)
|
|
|
|
var (
|
|
err error
|
|
latDeg, latMin, latMinFrag uint64
|
|
lngDeg, lngMin, lngMinFrag uint64
|
|
latHemi, lngHemi byte
|
|
isSouth, isWest bool
|
|
)
|
|
|
|
if latDeg, err = strconv.ParseUint(s[0:2], 10, 8); err != nil {
|
|
return pos, "", err
|
|
}
|
|
if latMin, err = strconv.ParseUint(s[2:4], 10, 8); err != nil {
|
|
return pos, "", err
|
|
}
|
|
if latMinFrag, err = strconv.ParseUint(s[5:7], 10, 8); err != nil {
|
|
return pos, "", err
|
|
}
|
|
latHemi = s[7]
|
|
pos.Symbol[0] = s[8]
|
|
if lngDeg, err = strconv.ParseUint(s[9:12], 10, 8); err != nil {
|
|
return pos, "", err
|
|
}
|
|
if lngMin, err = strconv.ParseUint(s[12:14], 10, 8); err != nil {
|
|
return pos, "", err
|
|
}
|
|
if lngMinFrag, err = strconv.ParseUint(s[15:17], 10, 8); err != nil {
|
|
return pos, "", err
|
|
}
|
|
lngHemi = s[17]
|
|
pos.Symbol[1] = s[18]
|
|
|
|
if latHemi == 'S' || latHemi == 's' {
|
|
isSouth = true
|
|
} else if latHemi != 'N' && latHemi != 'n' {
|
|
return pos, "", ErrInvalidPosition
|
|
}
|
|
|
|
if lngHemi == 'W' || lngHemi == 'w' {
|
|
isWest = true
|
|
} else if lngHemi != 'E' && lngHemi != 'e' {
|
|
return pos, "", ErrInvalidPosition
|
|
}
|
|
|
|
if latDeg > 89 || lngDeg > 179 {
|
|
return pos, "", ErrInvalidPosition
|
|
}
|
|
|
|
pos.Latitude = float64(latDeg) + float64(latMin)/60.0 + float64(latMinFrag)/6000.0
|
|
pos.Longitude = float64(lngDeg) + float64(lngMin)/60.0 + float64(lngMinFrag)/6000.0
|
|
|
|
if isSouth {
|
|
pos.Latitude = 0.0 - pos.Latitude
|
|
}
|
|
if isWest {
|
|
pos.Longitude = 0.0 - pos.Longitude
|
|
}
|
|
|
|
if pos.Symbol[1] >= 'a' || pos.Symbol[1] <= 'k' {
|
|
pos.Symbol[1] -= 32
|
|
}
|
|
|
|
if len(s) > 19 {
|
|
return pos, s[19:], nil
|
|
}
|
|
return pos, "", nil
|
|
}
|
|
|
|
func ParseCompressedPosition(s string) (Position, string, error) {
|
|
// APRS PROTOCOL REFERENCE 1.0.1 Chapter 9, page 36 (46 in PDF)
|
|
|
|
pos := Position{}
|
|
|
|
if len(s) < 10 {
|
|
return pos, "", ErrInvalidPosition
|
|
}
|
|
|
|
// Base-91 check
|
|
for _, c := range s[1:9] {
|
|
if c < 0x21 || c > 0x7b {
|
|
return pos, "", ErrInvalidPosition
|
|
}
|
|
}
|
|
|
|
var err error
|
|
var lat, lng int
|
|
if lat, err = base91Decode(s[1:5]); err != nil {
|
|
return pos, "", err
|
|
}
|
|
if lng, err = base91Decode(s[5:9]); err != nil {
|
|
return pos, "", err
|
|
}
|
|
|
|
pos.Latitude = 90.0 - float64(lat)/380926.0
|
|
pos.Longitude = -180.0 + float64(lng)/190463.0
|
|
pos.Compressed = true
|
|
|
|
return pos, s[10:], nil
|
|
}
|
|
|
|
func ParseMicE(s, dest string) (Position, error) {
|
|
// APRS PROTOCOL REFERENCE 1.0.1 Chapter 10, page 42 in PDF
|
|
|
|
pos := Position{}
|
|
|
|
if len(s) < 9 || len(dest) != 6 {
|
|
return pos, ErrInvalidPosition
|
|
}
|
|
|
|
ns := miceCodes[rune(dest[3])][2]
|
|
we := miceCodes[rune(dest[5])][4]
|
|
|
|
latF := fmt.Sprintf("%s%s", miceCodes[rune(dest[0])][0], miceCodes[rune(dest[1])][0])
|
|
latF = strings.Trim(latF, ". ")
|
|
latD, err := strconv.ParseFloat(latF, 64)
|
|
if err != nil {
|
|
return pos, ErrInvalidPosition
|
|
}
|
|
lonF := fmt.Sprintf("%s%s.%s%s", miceCodes[rune(dest[2])][0], miceCodes[rune(dest[3])][0], miceCodes[rune(dest[4])][0], miceCodes[rune(dest[5])][0])
|
|
lonF = strings.Trim(lonF, ". ")
|
|
latM, err := strconv.ParseFloat(lonF, 64)
|
|
if err != nil {
|
|
return pos, ErrInvalidPosition
|
|
}
|
|
if latM != 0 {
|
|
latD += latM / 60
|
|
}
|
|
if strings.ToUpper(ns) == "S" {
|
|
latD = -latD
|
|
}
|
|
|
|
lonOff := miceCodes[rune(dest[4])][3]
|
|
lonD := float64(s[1]) - 28
|
|
if lonOff == "100" {
|
|
lonD += 100
|
|
}
|
|
if lonD >= 180 && lonD < 190 {
|
|
lonD -= 80
|
|
}
|
|
if lonD >= 190 && lonD < 200 {
|
|
lonD -= 190
|
|
}
|
|
|
|
lonM := float64(s[2]) - 28
|
|
if lonM >= 60 {
|
|
lonM -= 60
|
|
}
|
|
// adding hundreth of minute then add minute as deg fraction
|
|
lonH := float64(s[3]) - 28
|
|
if lonH != 0 {
|
|
lonM += lonH / 100
|
|
}
|
|
if lonM != 0 {
|
|
lonD += lonM / 60
|
|
}
|
|
if strings.ToUpper(we) == "W" {
|
|
lonD = -lonD
|
|
}
|
|
|
|
pos.Latitude = latD
|
|
pos.Longitude = lonD
|
|
|
|
return pos, nil
|
|
}
|
|
|
|
func ParsePositionGrid(s string) (Position, string, error) {
|
|
var o int
|
|
for o = 0; o < len(s); o++ {
|
|
if strings.IndexByte(gridChars, s[o]) < 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
pos := Position{}
|
|
if o == 2 || o == 4 || o == 6 || o == 8 {
|
|
p, err := maidenhead.ParseLocator(s[:o])
|
|
if err != nil {
|
|
return pos, "", err
|
|
}
|
|
pos.Latitude = p.Latitude
|
|
pos.Longitude = p.Longitude
|
|
}
|
|
|
|
var txt string
|
|
if o < len(s) {
|
|
txt = s[o+1:]
|
|
}
|
|
return pos, txt, nil
|
|
}
|
|
|
|
func ParsePosition(s string, compressed bool) (Position, string, error) {
|
|
if compressed {
|
|
return ParseCompressedPosition(s)
|
|
}
|
|
return ParseUncompressedPosition(s)
|
|
}
|
|
|
|
func ParsePositionBoth(s string) (Position, string, error) {
|
|
pos, txt, err := ParseUncompressedPosition(s)
|
|
if err != nil {
|
|
return ParseCompressedPosition(s)
|
|
}
|
|
return pos, txt, err
|
|
}
|