public void CompactPositionReportingCoordinate_Constructor_Initialises_To_Known_State()
 {
     var coordinate = new CompactPositionReportingCoordinate(1, 2, true, 4);
     Assert.AreEqual(1, coordinate.Latitude);
     Assert.AreEqual(2, coordinate.Longitude);
     Assert.AreEqual(true, coordinate.OddFormat);
     Assert.AreEqual((byte)4, coordinate.NumberOfBits);
 }
 public void CompactPositionReportingCoordinate_Default_Constructor_Initialises_To_Known_State_And_Properties_Work()
 {
     var coordinate = new CompactPositionReportingCoordinate();
     TestUtilities.TestProperty(coordinate, r => r.Latitude, 0, 0x10000);
     TestUtilities.TestProperty(coordinate, r => r.Longitude, 0, 0x10000);
     TestUtilities.TestProperty(coordinate, r => r.OddFormat, false);
     TestUtilities.TestProperty(coordinate, r => r.NumberOfBits, (byte)0, (byte)19);
 }
        /// <summary>
        /// Extracts the fields describing a Compact Position Reporting coordinate in a message.
        /// </summary>
        /// <param name="message"></param>
        /// <param name="encodingBits"></param>
        /// <returns></returns>
        private CompactPositionReportingCoordinate ExtractCprCoordinate(AdsbMessage message, byte encodingBits)
        {
            CompactPositionReportingCoordinate result = null;

            if(message.Type != 0) {
                bool isOddFormat = _BitStream.ReadBit();
                int latitude = (int)_BitStream.ReadUInt32(17);
                int longitude = (int)_BitStream.ReadUInt32(17);
                result = new CompactPositionReportingCoordinate(latitude, longitude, isOddFormat, encodingBits);
            }

            return result;
        }
        /// <summary>
        /// See interface docs.
        /// </summary>
        /// <param name="cprCoordinate"></param>
        /// <param name="referenceCoordinate"></param>
        /// <returns></returns>
        /// <remarks><para>
        /// It was found in testing that encoded latitudes of zero are incredibly sensitive to rounding errors introduced by the use of doubles. This
        /// can be fixed by using decimals, which have much greater precision, and rounding to a very large number of decimal places - but using decimals
        /// is bad news, they are a lot slower than doubles because all of the operations are done in software, there is no hardware FPU support for them.
        /// 10 million divisions in LINQPad took 58 seconds on my development machine when using decimals and less than a second using doubles.
        /// </para><para>
        /// However it's only on the boundary between zones where rounding errors cause huge problems (e.g. returning a longitude of -87 instead of -90).
        /// The problem revolves around the selection of the wrong value of m, rounding errors in doubles at boundaries between zones can push m out by
        /// one, placing the longitude into the wrong zone and getting a longitude that it out by miles. Once the longitude is above zero rounding errors
        /// can have an effect but the correct m is still selected and they only produce inaccuracies of a few feet. So the compromise was to use decimals
        /// when accuracy is paramount - i.e. when the encoded longitude is 0 - and doubles for the rest of the time.
        /// </para></remarks>
        public GlobalCoordinate LocalDecode(CompactPositionReportingCoordinate cprCoordinate, GlobalCoordinate referenceCoordinate)
        {
            var result = new GlobalCoordinate();

            var maxResult = MaxResultSize(cprCoordinate.NumberOfBits == 19 ? 17 : cprCoordinate.NumberOfBits);
            var dlat = cprCoordinate.NumberOfBits == 19 ? cprCoordinate.OddFormat ? DLatOdd19Bit : DLatEven19Bit : cprCoordinate.OddFormat ? DLatOdd : DLatEven;
            var dlonNumerator = cprCoordinate.NumberOfBits == 19 ? 90.0 : 360.0;

            var refLat = referenceCoordinate.Latitude;
            var encodedLatitudeSlice = (double)cprCoordinate.Latitude / maxResult;
            var j = Math.Floor(refLat / dlat) + Math.Floor(0.5 + (CircleModulus(refLat, dlat) / dlat) - encodedLatitudeSlice);
            result.Latitude = dlat * (j + encodedLatitudeSlice);

            var oddEvenNL = NL(result.Latitude) - (cprCoordinate.OddFormat ? 1 : 0);
            if(cprCoordinate.Longitude != 0) {
                // The version that uses doubles for speed at the expense of potential rounding errors. Any changes here need duplicating below!
                var refLon = referenceCoordinate.Longitude;
                var dlon = oddEvenNL == 0 ? dlonNumerator : dlonNumerator / oddEvenNL;
                var encodedLongitudeSlice = (double)cprCoordinate.Longitude / maxResult;
                var m = Math.Floor(refLon / dlon) + Math.Floor(0.5 + (CircleModulus(refLon, dlon) / dlon) - encodedLongitudeSlice);
                result.Longitude = dlon * (m + encodedLongitudeSlice);
            } else {
                // Same as the block above but uses decimals for accuracy at the expense of speed. It would be nice if I could use C# generic functions
                // to remove this redundancy, but they're not very good for that. Sometimes I miss C++, I'd have 1000 ways to make the code common :)
                // Anyway, any changes here need duplicating above!
                var refLon = (decimal)referenceCoordinate.Longitude;
                var dlon = oddEvenNL == 0 ? (decimal)dlonNumerator : (decimal)dlonNumerator / oddEvenNL;
                var encodedLongitudeSlice = (decimal)cprCoordinate.Longitude / maxResult;
                var m = Math.Floor(Math.Round(refLon / dlon, 20)) + Math.Floor(0.5M + (CircleModulus(refLon, dlon) / dlon) - encodedLongitudeSlice);
                result.Longitude = (double)(dlon * (m + encodedLongitudeSlice));
            }

            return result;
        }
            public TrackedAircraftState State; // The current stage of position acquisition for the vehicle.

            #endregion Fields

            #region Methods

            public void RecordMessage(DateTime time, CompactPositionReportingCoordinate cpr)
            {
                EarlierCpr = LaterCpr;
                LaterCpr = new PositionMessage() { Time = time, Cpr = cpr };
            }
        public void CompactPositionReporting_LocalDecode_Produces_Correct_Results_For_CPR101_Tables()
        {
            // The values for this test are all taken directly from the transition latitude test tables in 1090-WP30-12 Proposed New Appendix CPR101
            var worksheet = new ExcelWorksheetData(TestContext);

            var numberOfBits = worksheet.Byte("Bits");
            var oddFormat = worksheet.Bool("OddFormat");
            var encodedLatitude = Convert.ToInt32(worksheet.String("ExpectedLatitude"), 16);
            var encodedLongitude = Convert.ToInt32(worksheet.String("ExpectedLongitude"), 16);
            var expectedLatitude = worksheet.Double("Latitude");
            var expectedLongitude = worksheet.Double("Longitude");
            var cprCoordinate = new CompactPositionReportingCoordinate(encodedLatitude, encodedLongitude, oddFormat, numberOfBits);

            // The reference latitude and longitude is set to roughly 50km of the expected latitude and longitude
            double? referenceLatitude, referenceLongitude;
            GreatCircleMaths.Destination(expectedLatitude, expectedLongitude, 45, 50, out referenceLatitude, out referenceLongitude);
            var referenceCoordinate = new GlobalCoordinate(referenceLatitude.Value, referenceLongitude.Value);

            var dataRow = worksheet.Int("DataRow"); // helps set conditional breakpoints, VSTS doesn't always process rows in ascending order as they appear in the worksheet

            var decodedCoordinate = _Cpr.LocalDecode(cprCoordinate, referenceCoordinate);

            // We need to accept 180 and -180 as being the same longitude, taking into account rounding errors
            if(expectedLongitude == -180.0 && decodedCoordinate.Longitude > 179.9999999999) expectedLongitude = 180.0;
            else if(expectedLongitude == 180.0 && decodedCoordinate.Longitude < -179.9999999999) expectedLongitude = -180.0;

            Assert.AreEqual(expectedLatitude, decodedCoordinate.Latitude, 0.0008);    // The CPR tables cover all latitudes, sometimes the rounding introduced by selecting the midpoint of a zone can be quite large
            Assert.AreEqual(expectedLongitude, decodedCoordinate.Longitude, 0.000000000001);  // This can have a lower tolerance as the CPR101 tables aren't testing longitude zone boundaries so much
        }
        /// <summary>
        /// See interface docs.
        /// </summary>
        /// <param name="earlyCpr"></param>
        /// <param name="laterCpr"></param>
        /// <param name="receiverLocation"></param>
        /// <returns></returns>
        public GlobalCoordinate GlobalDecode(CompactPositionReportingCoordinate earlyCpr, CompactPositionReportingCoordinate laterCpr, GlobalCoordinate receiverLocation)
        {
            GlobalCoordinate result = null;
            if(earlyCpr != null && laterCpr != null && earlyCpr.OddFormat != laterCpr.OddFormat && earlyCpr.NumberOfBits == laterCpr.NumberOfBits && (laterCpr.NumberOfBits < 19 || receiverLocation != null)) {
                var numberOfBits = earlyCpr.NumberOfBits;
                var surfaceEncoding = numberOfBits == 19;
                var dlatOdd = surfaceEncoding ? DLatOdd19Bit : DLatOdd;
                var dlatEven = surfaceEncoding ? DLatEven19Bit : DLatEven;
                var maxResult = MaxResultSize(surfaceEncoding ? 17 : numberOfBits);

                var oddCoordinate = earlyCpr.OddFormat ? earlyCpr : laterCpr;
                var evenCoordinate = oddCoordinate == earlyCpr ? laterCpr : earlyCpr;

                var j = Math.Floor(((59 * (double)evenCoordinate.Latitude - 60 * (double)oddCoordinate.Latitude) / maxResult) + 0.5);

                var rlatEven = dlatEven * (CircleModulus(j, 60) + ((double)evenCoordinate.Latitude / maxResult));
                var rlatOdd = dlatOdd * (CircleModulus(j, 59) + ((double)oddCoordinate.Latitude / maxResult));
                if(rlatEven >= 270.0) rlatEven -= 360.0;
                if(rlatOdd >= 270.0) rlatOdd -= 360.0;
                var rlat = laterCpr.OddFormat ? rlatOdd : rlatEven;
                if(surfaceEncoding && AbsoluteDifference(rlat, receiverLocation.Latitude) > AbsoluteDifference(rlat - 90.0, receiverLocation.Latitude)) {
                    rlat -= 90.0;
                    rlatEven -= 90.0;
                    rlatOdd -= 90.0;
                }

                var nlEven = NL(rlatEven);
                var nlOdd = NL(rlatOdd);
                if(nlEven == nlOdd) {
                    var nl = nlEven;

                    var mNumerator = (double)evenCoordinate.Longitude * (nl - 1) - (double)oddCoordinate.Longitude * nl;
                    var m = Math.Floor((mNumerator / maxResult) + 0.5);
                    var ni = Math.Max(nl - (laterCpr.OddFormat ? 1 : 0), 1);
                    var dlon = (surfaceEncoding ? 90.0 : 360.0) / ni;
                    var rlon = dlon * (CircleModulus(m, ni) + ((double)laterCpr.Longitude / maxResult));
                    if(!surfaceEncoding) rlon = CircleModulus(rlon + 180.0, 360.0) - 180.0;
                    else {
                        var deltaDegrees = double.MaxValue;
                        var receiverBearing = LongitudeToBearing(receiverLocation.Longitude);
                        foreach(var possibleRlon in new double[] { rlon, rlon - 90, rlon - 180, rlon - 270 }) {
                            var adjustedRlon = CircleModulus(possibleRlon + 180.0, 360.0) - 180.0;

                            var rlonBearing = LongitudeToBearing(adjustedRlon);
                            var delta = SmallestDegreesBetweenBearings(rlonBearing, receiverBearing);
                            if(delta < 0.0) delta += 360.0;
                            if(delta < deltaDegrees) {
                                rlon = adjustedRlon;
                                deltaDegrees = delta;
                            }
                        }
                    }

                    result = new GlobalCoordinate(rlat, rlon);
                }
            }

            return result;
        }
        public void CompactPositionReporting_GlobalDecode_Returns_Null_If_No_Receiver_Location_Is_Supplied_For_19Bit_Format()
        {
            var message1 = new CompactPositionReportingCoordinate(130929, 23302, false, 19);  // {38.998363494873/-73.9999953560207}
            var message2 = new CompactPositionReportingCoordinate(74133, 0, true, 19);        // {38.998357, -74.0}

            Assert.IsNull(_Cpr.GlobalDecode(message1, message2, null));
        }
        public void CompactPositionReporting_GlobalDecode_Calculates_1090_WP30_17_Test_Surface_Positions()
        {
            var message1 = new CompactPositionReportingCoordinate(130929, 23302, false, 19);  // {38.998363494873/-73.9999953560207}
            var message2 = new CompactPositionReportingCoordinate(74133, 0, true, 19);        // {38.998357, -74.0}

            var decoded = _Cpr.GlobalDecode(message1, message2, new GlobalCoordinate(38.99836, -74));
            Assert.AreEqual(38.998357, decoded.Latitude, 0.000001);
            Assert.AreEqual(-74.0, decoded.Longitude, 0.000001);

            var message3 = new CompactPositionReportingCoordinate(0, 23302, false, 19);
            decoded = _Cpr.GlobalDecode(message2, message3, new GlobalCoordinate(39.0, -74.0));
            Assert.AreEqual(39.0, decoded.Latitude, 0.000001);
            Assert.AreEqual(-73.999995, decoded.Longitude, 0.000001);
        }
        public void CompactPositionReporting_GlobalDecode_Calculates_Correct_Position_For_Internet_Example_1()
        {
            var earlyCpr = new CompactPositionReportingCoordinate(92095, 39846, false, 17);
            var laterCpr = new CompactPositionReportingCoordinate(88385, 125818, true, 17);

            var decoded = _Cpr.GlobalDecode(earlyCpr, laterCpr, null);

            Assert.AreEqual(10.2162144547802, decoded.Latitude, 0.00001);
            Assert.AreEqual(123.889128586342 , decoded.Longitude, 0.00001);
        }
 public void CompactPositionReporting_GlobalDecode_Returns_Null_If_Coordinate_Bits_Are_Different()
 {
     var earlier = new CompactPositionReportingCoordinate(1234, 0, true, 17);
     var later   = new CompactPositionReportingCoordinate(1235, 0, false, 19);
     Assert.IsNull(_Cpr.GlobalDecode(earlier, later, null));
 }
 public void CompactPositionReporting_GlobalDecode_Returns_Null_If_Either_Coordinate_Is_Null()
 {
     var coordinate = new CompactPositionReportingCoordinate(0x10000, 0, true, 17);
     Assert.IsNull(_Cpr.GlobalDecode(null, coordinate, null));
     Assert.IsNull(_Cpr.GlobalDecode(coordinate, null, null));
 }