From 24c5bf503f4ba4c18093bdd41f967baa60d45f56 Mon Sep 17 00:00:00 2001 From: Addison Schuhardt Date: Mon, 15 Sep 2025 09:52:46 -0700 Subject: [PATCH 1/2] Decoding compressed latitude and longitude --- src/AprsParser/Position.cs | 53 ++++++++++++++++--- src/AprsParser/RegexStrings.cs | 12 +++++ test/AprsParserUnitTests/PositionUnitTests.cs | 1 + 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/AprsParser/Position.cs b/src/AprsParser/Position.cs index c92b844..d420ad4 100644 --- a/src/AprsParser/Position.cs +++ b/src/AprsParser/Position.cs @@ -1,6 +1,7 @@ namespace AprsSharp.AprsParser { using System; + using System.Diagnostics; using System.Globalization; using System.Text; using System.Text.RegularExpressions; @@ -167,16 +168,54 @@ public void Decode(string coords) throw new ArgumentNullException(nameof(coords)); } + Ambiguity = 0; + + // first try uncompressed Match match = Regex.Match(coords, RegexStrings.PositionLatLongWithSymbols); - match.AssertSuccess("Coordinates", nameof(coords)); - Ambiguity = 0; - double latitude = DecodeLatitude(match.Groups[1].Value); - double longitude = DecodeLongitude(match.Groups[3].Value); + if (match.Success) + { + double latitude = DecodeLatitude(match.Groups[1].Value); + double longitude = DecodeLongitude(match.Groups[3].Value); - SymbolTableIdentifier = match.Groups[2].Value[0]; - SymbolCode = match.Groups[4].Value[0]; - Coordinates = new GeoCoordinate(latitude, longitude); + SymbolTableIdentifier = match.Groups[2].Value[0]; + SymbolCode = match.Groups[4].Value[0]; + Coordinates = new GeoCoordinate(latitude, longitude); + return; + } + + // next try compressed + match = Regex.Match(coords, RegexStrings.CompressedPosition); + + if (match.Success) + { + double latitude = DecodeCompressedLatitude(match.Groups[2].Value); + double longitude = DecodeCompressedLongitude(match.Groups[3].Value); + SymbolTableIdentifier = match.Groups[1].Value[0]; + SymbolCode = match.Groups[4].Value[0]; + Coordinates = new GeoCoordinate(latitude, longitude); + return; + } + } + + private static double DecodeCompressedLatitude(string coords) + { + Debug.Assert(coords.Length == 4, "Compressed latitude must be 4 characters"); + var latitude = 90 - ((((Convert.ToByte(coords[0]) - 33) * 753571) + + ((Convert.ToByte(coords[1]) - 33) * 8281) + + ((Convert.ToByte(coords[2]) - 33) * 91) + + (Convert.ToByte(coords[3]) - 33)) / 380926.0); + return Math.Round(latitude, 4); + } + + private static double DecodeCompressedLongitude(string coords) + { + Debug.Assert(coords.Length == 4, "Compressed longitude must be 4 characters"); + var longitude = -180 + ((((Convert.ToByte(coords[0]) - 33) * 753571) + + ((Convert.ToByte(coords[1]) - 33) * 8281) + + ((Convert.ToByte(coords[2]) - 33) * 91) + + (Convert.ToByte(coords[3]) - 33)) / 190463.0); + return Math.Round(longitude, 4); } /// diff --git a/src/AprsParser/RegexStrings.cs b/src/AprsParser/RegexStrings.cs index 8023807..512a99a 100644 --- a/src/AprsParser/RegexStrings.cs +++ b/src/AprsParser/RegexStrings.cs @@ -34,6 +34,18 @@ internal static class RegexStrings /// public const string PositionLatLongWithSymbols = @"([0-9 \.NS]{8})(.)([0-9 \.EW]{9})(.)"; + /// + /// Matchdes a compressed latitude and longitude with optional additional data + /// Six matches: + /// Symbol table ID + /// Compressed Latitude + /// Compressed Longitude + /// Symbol code + /// Compressed one of: course/speed, radio range, or altitude + /// Compressed data type + /// + public const string CompressedPosition = @"(.)(.{4})(.{4})(.)(.{2})(.)"; + /// /// Same as but forces full line match. /// diff --git a/test/AprsParserUnitTests/PositionUnitTests.cs b/test/AprsParserUnitTests/PositionUnitTests.cs index c88ea86..f187d2c 100644 --- a/test/AprsParserUnitTests/PositionUnitTests.cs +++ b/test/AprsParserUnitTests/PositionUnitTests.cs @@ -293,6 +293,7 @@ public void DecodeLongitudeWrongDecimalPointLocation() [InlineData(null, '\\', '.', 0, 0)] // defaults [InlineData("4903.50N/07201.75W-", '/', '-', 49.0583, -72.0292)] // from APRS spec [InlineData("4903.50S/07201.75E-", '/', '-', -49.0583, 72.0292)] // Ensure south and east work + [InlineData("/5L!!<*e7>7P[", '/', '>', 49.5, -72.7500)] // from the APRS spec public void Decode( string? encodedPosition, char expectedSymbolTable, From d832a60b66959b948721a7c35ca0fee219b3a26a Mon Sep 17 00:00:00 2001 From: Addison Schuhardt Date: Tue, 16 Sep 2025 13:01:04 -0700 Subject: [PATCH 2/2] move Base91 decoding into its own method to clean-up compressed coordinates parsing --- src/AprsParser/Position.cs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/AprsParser/Position.cs b/src/AprsParser/Position.cs index d420ad4..b51d2e7 100644 --- a/src/AprsParser/Position.cs +++ b/src/AprsParser/Position.cs @@ -198,23 +198,32 @@ public void Decode(string coords) } } + private static int DecodeBase91(string encoded) + { + var bytes = Encoding.ASCII.GetBytes(encoded); + + var result = 0; + for (var i = 0; i < bytes.Length; i++) + { + result += i == bytes.Length - 1 + ? bytes[i] - 33 + : (bytes[i] - 33) * (int)Math.Pow(91, bytes.Length - i - 1); + } + + return result; + } + private static double DecodeCompressedLatitude(string coords) { Debug.Assert(coords.Length == 4, "Compressed latitude must be 4 characters"); - var latitude = 90 - ((((Convert.ToByte(coords[0]) - 33) * 753571) + - ((Convert.ToByte(coords[1]) - 33) * 8281) + - ((Convert.ToByte(coords[2]) - 33) * 91) + - (Convert.ToByte(coords[3]) - 33)) / 380926.0); + var latitude = 90 - (DecodeBase91(coords) / 380926.0); return Math.Round(latitude, 4); } private static double DecodeCompressedLongitude(string coords) { Debug.Assert(coords.Length == 4, "Compressed longitude must be 4 characters"); - var longitude = -180 + ((((Convert.ToByte(coords[0]) - 33) * 753571) + - ((Convert.ToByte(coords[1]) - 33) * 8281) + - ((Convert.ToByte(coords[2]) - 33) * 91) + - (Convert.ToByte(coords[3]) - 33)) / 190463.0); + var longitude = -180 + (DecodeBase91(coords) / 190463.0); return Math.Round(longitude, 4); }