/// <summary>
        /// Performs a local decode of the tracked aircraft's position against the previous position of the aircraft.
        /// </summary>
        /// <param name="trackedAircraft"></param>
        /// <returns></returns>
        private GlobalCoordinate LocalCprDecode(TrackedAircraft trackedAircraft)
        {
            var cpr = trackedAircraft.LaterCpr.Cpr;

            var result = _CompactPositionReporting.LocalDecode(cpr, trackedAircraft.LastPosition);
            if(result != null) {
                var distance = GreatCircleMaths.Distance(result.Latitude, result.Longitude, trackedAircraft.LastPosition.Latitude, trackedAircraft.LastPosition.Longitude);
                var countPeriods = (int)Math.Max(1, Math.Ceiling((trackedAircraft.LaterCpr.Time - trackedAircraft.LastPositionTime).TotalSeconds / LocalDecodeMaxSpeedSeconds));
                bool earlierIsSurface = trackedAircraft.LastPositionIsSurface;
                bool laterIsSurface = cpr.NumberOfBits == 19;
                double allowableDistance = 0.0;
                double distancePerPeriod = laterIsSurface ? LocalDecodeMaxSpeedSurface : LocalDecodeMaxSpeedAirborne;
                if(earlierIsSurface != laterIsSurface) {
                    allowableDistance = LocalDecodeMaxSpeedTransition;
                    distancePerPeriod = LocalDecodeMaxSpeedAirborne;
                    --countPeriods;
                }
                allowableDistance += countPeriods * distancePerPeriod;

                if(distance >= allowableDistance) {
                    result = null;
                    if(_Statistics.Lock != null) { lock(_Statistics.Lock) ++_Statistics.AdsbPositionsExceededSpeedCheck; }
                }
            }

            return result;
        }
        /// <summary>
        /// Resets the position tracking state of the aircraft.
        /// </summary>
        /// <param name="trackedAircraft"></param>
        /// <returns></returns>
        private GlobalCoordinate ResetPositionTracking(TrackedAircraft trackedAircraft)
        {
            trackedAircraft.LastPosition = null;
            trackedAircraft.EarlierCpr = trackedAircraft.LaterCpr = trackedAircraft.AcquisitionCpr = null;
            trackedAircraft.InitialLocalDecodingUnsafe = true;
            trackedAircraft.State = TrackedAircraftState.Acquiring;

            if(_Statistics.Lock != null) { lock(_Statistics.Lock) ++_Statistics.AdsbPositionsReset; }

            OnPositionReset(new EventArgs<string>(trackedAircraft.Icao24));

            return null;
        }
        /// <summary>
        /// Decodes the vehicle's position.
        /// </summary>
        /// <param name="result"></param>
        /// <param name="trackedAircraft"></param>
        /// <param name="groundSpeed"></param>
        private void DecodePosition(BaseStationMessage result, TrackedAircraft trackedAircraft, double? groundSpeed)
        {
            GlobalCoordinate position = null;
            TrackedAircraftState newState = trackedAircraft.State;

            if(UseLocalDecodeForInitialPosition && trackedAircraft.EarlierCpr == null && trackedAircraft.LaterCpr != null && ReceiverLocation != null && !trackedAircraft.InitialLocalDecodingUnsafe) {
                position = _CompactPositionReporting.LocalDecode(trackedAircraft.LaterCpr.Cpr, ReceiverLocation);
                newState = TrackedAircraftState.Acquired;
            } else if(trackedAircraft.LaterCpr != null && trackedAircraft.EarlierCpr != null) {
                switch(trackedAircraft.State) {
                    case TrackedAircraftState.Acquiring:
                        position = GlobalCprDecode(trackedAircraft, groundSpeed);
                        newState = TrackedAircraftState.Acquired;
                        break;
                    case TrackedAircraftState.Acquired:
                        position = LocalCprDecode(trackedAircraft);
                        if(position != null && trackedAircraft.AcquisitionCpr != trackedAircraft.EarlierCpr) {
                            var globalPosition = GlobalCprDecode(trackedAircraft, groundSpeed);
                            if(globalPosition != null) {
                                var distance = GreatCircleMaths.Distance(position.Latitude, position.Longitude, globalPosition.Latitude, globalPosition.Longitude);
                                var isValid = true;
                                switch(trackedAircraft.LaterCpr.Cpr.NumberOfBits) {
                                    case 19:    isValid = distance <= SurfaceResolution; break;
                                    default:    isValid = distance <= AirborneResolution; break;
                                }
                                if(!isValid) position = ResetPositionTracking(trackedAircraft);
                                else         newState = TrackedAircraftState.Tracking;
                            }
                        }
                        break;
                    case TrackedAircraftState.Tracking:
                        position = LocalCprDecode(trackedAircraft);
                        break;
                    default:
                        throw new NotImplementedException();
                }
            }

            if(!SuppressReceiverRangeCheck && position != null && ReceiverLocation != null) {
                var distance = GreatCircleMaths.Distance(ReceiverLocation.Latitude, ReceiverLocation.Longitude, position.Latitude, position.Longitude);
                if(distance > ReceiverRangeKilometres) {
                    position = null;
                    if(_Statistics.Lock != null) { lock(_Statistics.Lock) ++_Statistics.AdsbPositionsOutsideRange; }
                }
            }

            if(position != null) {
                result.Latitude = position.Latitude;
                result.Longitude = position.Longitude;
                trackedAircraft.LastPosition = position;
                trackedAircraft.LastPositionTime = trackedAircraft.LaterCpr.Time;
                trackedAircraft.LastPositionIsSurface = trackedAircraft.LaterCpr.Cpr.NumberOfBits == 19;
                if(newState == TrackedAircraftState.Acquired && trackedAircraft.State != newState) trackedAircraft.AcquisitionCpr = trackedAircraft.LaterCpr;
                trackedAircraft.State = newState;
            }
        }
        /// <summary>
        /// Performs a global decode of the tracked aircraft's position when the last two messages are odd and even.
        /// </summary>
        /// <param name="trackedAircraft"></param>
        /// <param name="groundSpeed"></param>
        /// <returns></returns>
        private GlobalCoordinate GlobalCprDecode(TrackedAircraft trackedAircraft, double? groundSpeed)
        {
            GlobalCoordinate result = null;

            var threshold = trackedAircraft.LaterCpr.Cpr.NumberOfBits == 19 ? groundSpeed == null || groundSpeed > 25.0 ? GlobalDecodeFastSurfaceThresholdMilliseconds
                                                                                                                        : GlobalDecodeSlowSurfaceThresholdMilliseconds
                                                                            : GlobalDecodeAirborneThresholdMilliseconds;
            if((trackedAircraft.LaterCpr.Time - trackedAircraft.EarlierCpr.Time).TotalMilliseconds <= threshold) {
                result = _CompactPositionReporting.GlobalDecode(trackedAircraft.EarlierCpr.Cpr, trackedAircraft.LaterCpr.Cpr, ReceiverLocation);
            }

            return result;
        }
        /// <summary>
        /// Applies values from the Mode-S message to the BaseStation message being formed.
        /// </summary>
        /// <param name="modeSMessage"></param>
        /// <param name="trackedAircraft"></param>
        /// <param name="baseStationMessage"></param>
        private void ApplyModeSValues(ModeSMessage modeSMessage, TrackedAircraft trackedAircraft, BaseStationMessage baseStationMessage)
        {
            if(modeSMessage.FlightStatus != null) {
                var flightStatus = modeSMessage.FlightStatus.Value;
                baseStationMessage.SquawkHasChanged = flightStatus == FlightStatus.AirborneAlert || flightStatus == FlightStatus.OnGroundAlert || flightStatus == FlightStatus.SpiWithAlert;
                baseStationMessage.IdentActive = flightStatus == FlightStatus.SpiWithAlert || flightStatus == FlightStatus.SpiWithNoAlert;

                switch(flightStatus) {
                    case FlightStatus.Airborne:
                    case FlightStatus.AirborneAlert: baseStationMessage.OnGround = false; break;
                    case FlightStatus.OnGround:
                    case FlightStatus.OnGroundAlert: baseStationMessage.OnGround = true; break;
                }
            }

            if(modeSMessage.Identity != null) baseStationMessage.Emergency = modeSMessage.Identity == 7500 || modeSMessage.Identity == 7600 || modeSMessage.Identity == 7700;

            if(modeSMessage.Capability != null) {
                if(modeSMessage.Capability == Capability.HasCommACommBAndAirborne) baseStationMessage.OnGround = false;
                else if(modeSMessage.Capability == Capability.HasCommACommBAndOnGround) baseStationMessage.OnGround = true;
            }

            if(modeSMessage.VerticalStatus != null) baseStationMessage.OnGround = modeSMessage.VerticalStatus == VerticalStatus.OnGround;

            if(!String.IsNullOrEmpty(modeSMessage.PossibleCallsign) && !SuppressCallsignsFromBds20 && !trackedAircraft.SeenAdsbCallsign) {
                baseStationMessage.Callsign = modeSMessage.PossibleCallsign.Trim();
                if(baseStationMessage.Supplementary == null) baseStationMessage.Supplementary = new BaseStationSupplementaryMessage();
                baseStationMessage.Supplementary.CallsignIsSuspect = true;
            }
        }
        /// <summary>
        /// Creates a BaseStationMessage from the Mode-S and ADS-B message passed across.
        /// </summary>
        /// <param name="messageReceivedUtc"></param>
        /// <param name="modeSMessage"></param>
        /// <param name="adsbMessage"></param>
        /// <param name="trackedAircraft"></param>
        /// <returns></returns>
        private BaseStationMessage CreateBaseStationMessage(DateTime messageReceivedUtc, ModeSMessage modeSMessage, AdsbMessage adsbMessage, TrackedAircraft trackedAircraft)
        {
            var result = new BaseStationMessage() {
                MessageType = BaseStationMessageType.Transmission,
                TransmissionType = ConvertToTransmissionType(modeSMessage, adsbMessage),
                MessageGenerated = messageReceivedUtc,
                MessageLogged = messageReceivedUtc,
                Icao24 = modeSMessage.FormattedIcao24,
                Altitude = modeSMessage.Altitude,
                Squawk = modeSMessage.Identity,
            };

            ApplyModeSValues(modeSMessage, trackedAircraft, result);
            if(adsbMessage != null) ApplyAdsbValues(messageReceivedUtc, adsbMessage, trackedAircraft, result);

            result.GroundSpeed = Round.GroundSpeed(result.GroundSpeed);
            result.Track = Round.Track(result.Track);
            result.Latitude = Round.Coordinate(result.Latitude);
            result.Longitude = Round.Coordinate(result.Longitude);

            return result;
        }
 /// <summary>
 /// Applies values from the ADS-B message to the BaseStation message being formed.
 /// </summary>
 /// <param name="messageReceivedUtc"></param>
 /// <param name="adsbMessage"></param>
 /// <param name="trackedAircraft"></param>
 /// <param name="baseStationMessage"></param>
 private void ApplyAdsbValues(DateTime messageReceivedUtc, AdsbMessage adsbMessage, TrackedAircraft trackedAircraft, BaseStationMessage baseStationMessage)
 {
     if(adsbMessage.AirbornePosition != null)        ApplyAdsbAirbornePosition(messageReceivedUtc, adsbMessage, trackedAircraft, baseStationMessage);
     if(adsbMessage.AirborneVelocity != null)        ApplyAdsbAirborneVelocity(adsbMessage, baseStationMessage);
     if(adsbMessage.AircraftStatus != null)          ApplyAdsbAircraftStatus(adsbMessage, baseStationMessage);
     if(adsbMessage.IdentifierAndCategory != null)   ApplyAdsbIdentifierAndCategory(adsbMessage, trackedAircraft, baseStationMessage);
     if(adsbMessage.SurfacePosition != null)         ApplyAdsbSurfacePosition(messageReceivedUtc, adsbMessage, trackedAircraft, baseStationMessage);
     if(adsbMessage.TargetStateAndStatus != null)    ApplyAdsbTargetStateAndStatus(adsbMessage, baseStationMessage);
 }
        private void ApplyAdsbSurfacePosition(DateTime messageReceivedUtc, AdsbMessage adsbMessage, TrackedAircraft trackedAircraft, BaseStationMessage baseStationMessage)
        {
            baseStationMessage.OnGround = true;
            baseStationMessage.GroundSpeed = (float?)adsbMessage.SurfacePosition.GroundSpeed;
            baseStationMessage.Track = (float?)adsbMessage.SurfacePosition.GroundTrack;
            if(adsbMessage.SurfacePosition.IsReversing) {
                if(baseStationMessage.Supplementary == null) baseStationMessage.Supplementary = new BaseStationSupplementaryMessage();
                baseStationMessage.Supplementary.SpeedType = SpeedType.GroundSpeedReversing;
            }

            if(adsbMessage.SurfacePosition.CompactPosition != null) {
                trackedAircraft.RecordMessage(messageReceivedUtc, adsbMessage.SurfacePosition.CompactPosition);
                DecodePosition(baseStationMessage, trackedAircraft, adsbMessage.SurfacePosition.GroundSpeed);
            }
        }
 private void ApplyAdsbIdentifierAndCategory(AdsbMessage adsbMessage, TrackedAircraft trackedAircraft, BaseStationMessage baseStationMessage)
 {
     if(adsbMessage.IdentifierAndCategory.Identification != null) {
         trackedAircraft.SeenAdsbCallsign = true;
         baseStationMessage.Callsign = adsbMessage.IdentifierAndCategory.Identification.Trim();
         if(baseStationMessage.Supplementary == null) baseStationMessage.Supplementary = new BaseStationSupplementaryMessage();
         baseStationMessage.Supplementary.CallsignIsSuspect = false;
     }
 }
        private void ApplyAdsbAirbornePosition(DateTime messageReceivedUtc, AdsbMessage adsbMessage, TrackedAircraft trackedAircraft, BaseStationMessage baseStationMessage)
        {
            if(adsbMessage.AirbornePosition.BarometricAltitude != null) baseStationMessage.Altitude = adsbMessage.AirbornePosition.BarometricAltitude;
            else if(adsbMessage.AirbornePosition.GeometricAltitude != null) {
                baseStationMessage.Altitude = adsbMessage.AirbornePosition.GeometricAltitude;
                if(baseStationMessage.Supplementary == null) baseStationMessage.Supplementary = new BaseStationSupplementaryMessage();
                baseStationMessage.Supplementary.AltitudeIsGeometric = true;
            }

            if(adsbMessage.AirbornePosition.CompactPosition != null) {
                trackedAircraft.RecordMessage(messageReceivedUtc, adsbMessage.AirbornePosition.CompactPosition);
                DecodePosition(baseStationMessage, trackedAircraft, null);
            }

            baseStationMessage.SquawkHasChanged = adsbMessage.AirbornePosition.SurveillanceStatus == SurveillanceStatus.TemporaryAlert;
            baseStationMessage.IdentActive = adsbMessage.AirbornePosition.SurveillanceStatus == SurveillanceStatus.SpecialPositionIdentification;
        }
        /// <summary>
        /// See interface docs.
        /// </summary>
        /// <param name="messageReceivedUtc"></param>
        /// <param name="modeSMessage"></param>
        /// <param name="adsbMessage"></param>
        /// <returns></returns>
        public BaseStationMessage Translate(DateTime messageReceivedUtc, ModeSMessage modeSMessage, AdsbMessage adsbMessage)
        {
            BaseStationMessage result = null;

            if(modeSMessage != null && !_Disposed) {
                var isValidMessage = DetermineWhetherValid(modeSMessage, messageReceivedUtc);

                if(isValidMessage) {
                    TrackedAircraft trackedAircraft;
                    lock(_TrackedAircraftLock) {
                        if(!_TrackedAircraft.TryGetValue(modeSMessage.Icao24, out trackedAircraft)) {
                            trackedAircraft = new TrackedAircraft() { Icao24 = modeSMessage.FormattedIcao24, LatestMessageUtc = messageReceivedUtc, };
                            _TrackedAircraft.Add(modeSMessage.Icao24, trackedAircraft);
                        } else {
                            if((messageReceivedUtc - trackedAircraft.LatestMessageUtc).TotalSeconds >= TrackingTimeoutSeconds) {
                                trackedAircraft = new TrackedAircraft() { Icao24 = modeSMessage.FormattedIcao24 };
                                _TrackedAircraft[modeSMessage.Icao24] = trackedAircraft;
                            }
                            trackedAircraft.LatestMessageUtc = messageReceivedUtc;
                        }
                    }

                    if(isValidMessage) {
                        result = CreateBaseStationMessage(messageReceivedUtc, modeSMessage, adsbMessage, trackedAircraft);
                        if(_Statistics.Lock != null) { lock(_Statistics.Lock) _Statistics.AdsbAircraftTracked = _TrackedAircraft.Count; }
                    }
                }
            }

            return result;
        }