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 }