private void SubscribeContextEventHandlers(FlightContext context) { // Subscribe to the events so we can propagate 'em via the factory context.OnTakeoff += (sender, args) => OnTakeoff?.Invoke(sender, args); context.OnLaunchCompleted += (sender, args) => OnLaunchCompleted?.Invoke(sender, args); context.OnLanding += (sender, args) => OnLanding?.Invoke(sender, args); context.OnRadarContact += (sender, args) => OnRadarContact?.Invoke(sender, args); context.OnCompletedWithErrors += (sender, args) => OnCompletedWithErrors?.Invoke(sender, args); }
internal static void Initialize(this FlightContext context) { context.LatestTimeStamp = DateTime.MinValue; context.Flight = new Flight { Aircraft = context.Options.AircraftId }; context.StateMachine.Fire(FlightContext.Trigger.Next); }
internal static AircraftRelation WhatAmI(this FlightContext context1, FlightContext context2) { var c2Position = context2.GetPositionAt(context1.CurrentPosition.TimeStamp); if (c2Position == null && Math.Abs((context1.Flight.DepartureTime - context2.Flight.DepartureTime)?.TotalSeconds ?? 21) < 30) { return(AircraftRelation.Indeterministic); } else if (c2Position == null || context2.CurrentPosition == null || (context1.CurrentPosition.TimeStamp - context2.CurrentPosition.TimeStamp).TotalSeconds > 30 || context1.CurrentPosition.Location.DistanceTo(c2Position.Location) > 200) { // In this case we conclusively know there's nothing to be found return(AircraftRelation.None); } var bearings = new List <double>(); for (var i = context1.Flight.PositionUpdates.Count - 1; i >= 0; i--) { var p1 = context1.Flight.PositionUpdates[i]; var p2 = context2.GetPositionAt(p1.TimeStamp); // If this statement is true, the changes that our interpolation is right are getting more slim by the tick. if (p1.TimeStamp < context2.Flight.PositionUpdates[0].TimeStamp) { break; } if (Math.Abs(p1.Speed - p2.Speed) > 20 || Math.Abs(p1.Altitude - p2.Altitude) > 100 || p1.Location.DistanceTo(p2.Location) > 200) { return(AircraftRelation.None); } var angle = (p1.Location.DegreeBearing(p2.Location) - p1.Heading + 360) % 360; bearings.Add(angle); } var bearing = Geo.MeanAngle(bearings.ToArray()); return(90 < bearing && bearing < 270 ? AircraftRelation.Towplane : AircraftRelation.OnTow); }
public IEnumerable <FlightContext> FindNearby(FlightContext context, double distance = 0.2) { if (context == null) { throw new ArgumentException($"{nameof(context)} should not be null"); } var nearbyPositions = _map.Nearby(new PositionUpdate(null, DateTime.MinValue, context.CurrentPosition.Location.Y, context.CurrentPosition.Location.X), distance); foreach (var position in nearbyPositions) { if (position.Aircraft != context.Options.AircraftId) { yield return(_flightContextDictionary[position.Aircraft]); } } }
internal static IEnumerable <Encounter> TowEncounter(this FlightContext context) { var nearbyAircraft = context.Options.NearbyAircraftAccessor?.Invoke( context.CurrentPosition.Location, 0.5) .ToList(); if (nearbyAircraft == null) { yield break; } foreach (var aircraft in nearbyAircraft) { var iAm = context.WhatAmI(aircraft); if (iAm == AircraftRelation.OnTow) { yield return(new Encounter { Aircraft = aircraft.Options.AircraftId, Start = aircraft.Flight.DepartureTime, Type = EncounterType.Tug }); } else if (iAm == AircraftRelation.Towplane) { yield return(new Encounter { Aircraft = aircraft.Options.AircraftId, Start = aircraft.Flight.DepartureTime, Type = EncounterType.Tow }); } else if (iAm == AircraftRelation.Indeterministic) { yield return(null); } } }
internal static void TrackAerotow(this FlightContext context) { // ToDo: Think about the possibility of a paired landing. if (context.Options.AircraftAccessor == null) { throw new Exception($"Unable to track tow without {nameof(FlightContextFactory)}"); } var target = context.Flight.Encounters .FirstOrDefault(q => q.Type == Models.EncounterType.Tow || q.Type == Models.EncounterType.Tug); if (target == null) { // Note of caution; this situation should ideally never happen. If it does it would be a design flaw in the state machine? context.StateMachine.Fire(FlightContext.Trigger.LaunchCompleted); return; } var otherContext = context.Options.AircraftAccessor(target.Aircraft); var iAm = context.WhatAmI(otherContext); if (iAm == null) { return; // (I'm nothing. Try again next time) } if (iAm == Internal.Geo.AircraftRelation.None || (target.Type == Models.EncounterType.Tow && iAm != Internal.Geo.AircraftRelation.Towplane) || (target.Type == Models.EncounterType.Tug && iAm != Internal.Geo.AircraftRelation.OnTow)) { target.End = context.CurrentPosition.TimeStamp; context.StateMachine.Fire(FlightContext.Trigger.LaunchCompleted); context.InvokeOnLaunchCompletedEvent(); return; } }
internal static void Stationary(this FlightContext context) { if (context.CurrentPosition == null) { return; } if (context.CurrentPosition.Speed > 30) { double groundElevation = 0; if (context.Options.NearbyRunwayAccessor != null) { groundElevation = context.Options.NearbyRunwayAccessor( context.CurrentPosition.Location, Constants.RunwayQueryRadius)? .OrderBy(q => q.Sides .Min(w => Geo.DistanceTo(w, context.CurrentPosition.Location)) ).FirstOrDefault() ?.Sides .Average(q => q.Z) ?? 0; } // Walk back to when the speed was 0 var start = context.Flight.PositionUpdates .Where(q => (q.Speed == 0 || double.IsNaN(q.Speed)) && (context.CurrentPosition.TimeStamp - q.TimeStamp).TotalSeconds < 30) .OrderByDescending(q => q.TimeStamp) .FirstOrDefault(); if (start == null && context.CurrentPosition.Altitude > (groundElevation + Constants.ArrivalHeight)) { // The flight was already in progress, or we could not find the starting point (trees in line of sight?) // Create an estimation about the departure time. Unless contact happens high in the sky context.Flight.DepartureInfoFound = false; context.InvokeOnRadarContactEvent(); context.StateMachine.Fire(FlightContext.Trigger.TrackMovements); return; } else if (start == null && context.CurrentPosition.Altitude <= (groundElevation + Constants.ArrivalHeight)) { // ToDo: Try to estimate the departure time context.Flight.DepartureTime = context.CurrentPosition.TimeStamp; context.Flight.DepartureLocation = context.CurrentPosition.Location; context.Flight.PositionUpdates .Where(q => q.TimeStamp < context.Flight.DepartureTime.Value) .ToList() .ForEach(q => context.Flight.PositionUpdates.Remove(q)); context.Flight.DepartureInfoFound = false; } else if (start != null) { context.Flight.DepartureTime = start.TimeStamp; context.Flight.DepartureLocation = context.CurrentPosition.Location; // Remove points not related to this flight context.Flight.PositionUpdates .Where(q => q.TimeStamp < context.Flight.DepartureTime.Value) .ToList() .ForEach(q => context.Flight.PositionUpdates.Remove(q)); context.Flight.DepartureInfoFound = false; } context.Flight.DepartureHeading = (short)context.CurrentPosition.Heading; context.InvokeOnTakeoffEvent(); context.StateMachine.Fire(FlightContext.Trigger.Depart); } }
internal static void Departing(this FlightContext context) { /* * First check plausible scenarios. The easiest to track is an aerotow. * * If not, wait until the launch is completed. */ if (context.Flight.LaunchMethod == LaunchMethods.None) { var departure = context.Flight.PositionUpdates .Where(q => q.Heading != 0 && !double.IsNaN(q.Heading)) .OrderBy(q => q.TimeStamp) .Take(5) .ToList(); if (departure.Count > 4) { context.Flight.DepartureHeading = Convert.ToInt16(departure.Average(q => q.Heading)); if (context.Flight.DepartureHeading == 0) { context.Flight.DepartureHeading = 360; } // Only start method recognition after the heading has been determined context.Flight.LaunchMethod = LaunchMethods.Unknown | LaunchMethods.Aerotow | LaunchMethods.Winch | LaunchMethods.Self; } else { return; } } if (context.Flight.DepartureTime != null && (context.CurrentPosition.TimeStamp - (context.Flight.PositionUpdates.FirstOrDefault(q => q.Speed > 30)?.TimeStamp ?? context.CurrentPosition.TimeStamp)).TotalSeconds < 10) { return; } // We can safely try to extract the correct path if (context.Flight.LaunchMethod.HasFlag(LaunchMethods.Unknown | LaunchMethods.Aerotow)) { var encounters = context.TowEncounter().ToList(); if (encounters.Count(q => q?.Type == EncounterType.Tug || q?.Type == EncounterType.Tow) > 1) { return; } var encounter = encounters.SingleOrDefault(q => q?.Type == EncounterType.Tug || q?.Type == EncounterType.Tow); if (encounter != null) { context.Flight.LaunchMethod = LaunchMethods.Aerotow | (encounter.Type == EncounterType.Tug ? LaunchMethods.OnTow : LaunchMethods.TowPlane ); context.Flight.Encounters.Add(encounter); context.StateMachine.Fire(FlightContext.Trigger.TrackAerotow); return; } else if (encounters.Any(q => q == null)) { return; } context.Flight.LaunchMethod &= ~LaunchMethods.Aerotow; } if (context.Flight.LaunchMethod.HasFlag(LaunchMethods.Unknown)) { var x = new DenseVector(context.Flight.PositionUpdates.Select(w => (w.TimeStamp - context.Flight.DepartureTime.Value).TotalSeconds).ToArray()); var y = new DenseVector(context.Flight.PositionUpdates.Select(w => w.Altitude).ToArray()); var interpolation = CubicSpline.InterpolateNatural(x, y); var r = new List <double>(); var r2 = new List <double>(); for (var i = 0; i < (context.CurrentPosition.TimeStamp - context.Flight.DepartureTime.Value).TotalSeconds; i++) { r.Add(interpolation.Differentiate(i)); r2.Add(interpolation.Differentiate2(i)); } // When the initial climb has completed if (interpolation.Differentiate((context.CurrentPosition.TimeStamp - context.Flight.DepartureTime.Value).TotalSeconds) < 0) { // Skip the first element because heading is 0 when in rest var averageHeading = context.Flight.PositionUpdates.Skip(1).Average(q => q.Heading); // ToDo: Add check to see whether there is another aircraft nearby if (context.Flight.PositionUpdates .Skip(1) .Where(q => interpolation.Differentiate((context.CurrentPosition.TimeStamp - context.Flight.DepartureTime.Value).TotalSeconds) > 0) .Select(q => Geo.GetHeadingError(averageHeading, q.Heading)) .Any(q => q > 20) || Geo.DistanceTo( context.Flight.PositionUpdates.First().Location, context.CurrentPosition.Location) > 3000) { context.Flight.LaunchMethod = LaunchMethods.Self; } else { context.Flight.LaunchMethod = LaunchMethods.Winch; } context.Flight.LaunchFinished = context.CurrentPosition.TimeStamp; context.InvokeOnLaunchCompletedEvent(); context.StateMachine.Fire(FlightContext.Trigger.LaunchCompleted); } } }
internal static AircraftRelation TrackTow(this FlightContext context1, FlightContext context2) { if (context1.Flight.PositionUpdates.Count < 5 || context2.Flight.PositionUpdates.Count < 5) { return(AircraftRelation.None); } var interpolation = Interpolation.Interpolate( context1.Flight.PositionUpdates, context2.Flight.PositionUpdates, q => q.TimeStamp.Ticks, (object1, object2, time) => { var dX = object2.Location.X - object1.Location.X; var dY = object2.Location.Y - object1.Location.Y; var dT = (object2.TimeStamp - object1.TimeStamp).Ticks; if (dT == 0) { return(null); } double factor = (time - object1.TimeStamp.Ticks) / (double)dT; return(new PositionUpdate( "", new DateTime((long)(object1.TimeStamp.Ticks + dT * factor)), object1.Location.Y + factor * dY, object1.Location.X + factor * dX)); }); /* * We only care about the following two characteristics; * 1. In relation to the glider, the towplane is always in front -90 to +90 bearing * 2. The aircraft should be no more than 200 meters apart for any interpolated point * * As soon as any of these is false, the tow has ended. */ bool?isTowed = null; foreach (var dataPoint in interpolation) { if (dataPoint.T1.Location.DistanceTo(dataPoint.T2.Location) > 200) { break; } // Determine the position irt to the other aircraft var bearing = dataPoint.T1.Location.DegreeBearing(dataPoint.T2.Location); if (90 < bearing && bearing < 270) { // We're being towed if (isTowed == null) { isTowed = true; } else if (isTowed == false) { return(AircraftRelation.None); } } else { // We're towing if (isTowed == null) { isTowed = false; } else if (isTowed == true) { return(AircraftRelation.None); } } } if (isTowed == null) { return(AircraftRelation.None); } return(isTowed.Value ? AircraftRelation.OnTow : AircraftRelation.Towplane); }
/// <summary> /// The attach method can be used to add an already existing context instance to this factory. /// This method will overwrite any FlightContext instance with the same aircraft identifier already /// tracked by this FlightContextFactory. /// </summary> /// <param name="context"></param> public void Attach(FlightContext context) => Attach(context?.Flight);
internal static void Arriving(this FlightContext context) { /* * - Create an estimate for the arrival time * - When data shows a landing, use that data * - When no data is received anymore, use the estimation */ double groundElevation = 0; if (context.Options.NearbyRunwayAccessor != null) { groundElevation = context.Options.NearbyRunwayAccessor( context.CurrentPosition.Location, Constants.RunwayQueryRadius)? .OrderBy(q => q.Sides .Min(w => Geo.DistanceTo(w, context.CurrentPosition.Location)) ).FirstOrDefault() ?.Sides .Average(q => q.Z) ?? 0; } if (context.CurrentPosition.Altitude > (groundElevation + Constants.ArrivalHeight)) { context.Flight.ArrivalTime = null; context.Flight.ArrivalInfoFound = null; context.Flight.ArrivalHeading = 0; context.StateMachine.Fire(FlightContext.Trigger.LandingAborted); return; } var arrival = context.Flight.PositionUpdates .Where(q => q.Heading != 0 && !double.IsNaN(q.Heading)) .OrderByDescending(q => q.TimeStamp) .Take(5) .ToList(); if (!arrival.Any()) { return; } if (context.CurrentPosition.Speed == 0) { /* * If a flight has been in progress, end the flight. * * When the aircraft has been registered mid flight the departure * location is unknown, and so is the time. Therefore look at the * flag which is set to indicate whether the departure location has * been found. * * ToDo: Also check the vertical speed as it might be an indication * that the flight is still in progress! (Aerobatic stuff and so) */ context.Flight.ArrivalTime = context.CurrentPosition.TimeStamp; context.Flight.ArrivalInfoFound = true; context.Flight.ArrivalHeading = Convert.ToInt16(arrival.Average(q => q.Heading)); context.Flight.ArrivalLocation = arrival.First().Location; if (context.Flight.ArrivalHeading == 0) { context.Flight.ArrivalHeading = 360; } context.InvokeOnLandingEvent(); context.StateMachine.Fire(FlightContext.Trigger.Arrived); } else if (!(context.Flight.ArrivalInfoFound ?? true) && context.CurrentPosition.TimeStamp > context.Flight.ArrivalTime.Value.AddSeconds(Constants.ArrivalTimeout)) { // Our theory needs to be finalized context.InvokeOnLandingEvent(); context.StateMachine.Fire(FlightContext.Trigger.Arrived); } else { var previousPoint = context.Flight.PositionUpdates.LastOrDefault(); if (previousPoint == null) { return; } // Take the average climbrate over the last few points var climbrates = new List <double>(); var speeds = new List <double>(); for (var i = context.Flight.PositionUpdates.Count - 1; i > Math.Max(context.Flight.PositionUpdates.Count - 15, 0); i--) { var p1 = context.Flight.PositionUpdates[i]; var p2 = context.Flight.PositionUpdates[i - 1]; var deltaAltitude = p1.Altitude - p2.Altitude; var deltaTime = p1.TimeStamp - p2.TimeStamp; speeds.Add(p1.Speed); climbrates.Add(deltaAltitude / deltaTime.TotalSeconds); } if (!climbrates.Any()) { context.Flight.ArrivalTime = null; context.Flight.ArrivalInfoFound = null; context.Flight.ArrivalHeading = 0; context.Flight.ArrivalLocation = null; return; } var average = climbrates.Average(); double ETUA = context.CurrentPosition.Altitude / -average; if (double.IsInfinity(ETUA) || ETUA > (60 * 10) || ETUA < 0) { context.Flight.ArrivalTime = null; context.Flight.ArrivalInfoFound = null; context.Flight.ArrivalHeading = 0; context.Flight.ArrivalLocation = null; return; } var averageHeading = arrival.Average(q => q.Heading); context.Flight.ArrivalTime = context.CurrentPosition.TimeStamp.AddSeconds(ETUA); context.Flight.ArrivalInfoFound = false; context.Flight.ArrivalHeading = Convert.ToInt16(averageHeading); context.Flight.ArrivalLocation = context.CurrentPosition.Location.HaversineExtrapolation( averageHeading, speeds.Average() * 0.514444444 * ETUA); // Knots to m/s times the estimated time until arrival } }