using System; using System.Collections.Generic; using System.Linq; namespace Eu07.BlackBox; /// How the user's train kept to a station of its timetable. public enum StationOutcome { /// Not reached yet in the recording. Ahead, /// Stopped where the timetable has a stop. Stopped, /// Passed where the timetable has no stop. Passed, /// Left a stop behind without stopping, or before the departure. Missed } /// How late the user's train was at a moment [min, above 0 late], after the station named, standing there or not. public sealed record TrainDelay(string Train, double Minutes, string Station, bool Standing); /// /// A row of the timetable of the user's train, with what happened there. The times are /// minutes of the day, as in the timetable; the delays are in minutes, above 0 late. /// public sealed record StationProgress( int Number, TimetableRow Row, StationOutcome Outcome, double? ArrivedAt, double? DepartedAt, double? ArrivalClock, double? DepartureClock, StationEvent? Event) { public double? ArrivalDelay => Delay(ArrivalClock, Row.Arrival); /// Of the departure, or of the pass for a station passed through. public double? DepartureDelay => Delay(DepartureClock, Row.Departure); /// Above 0 the head stood short of where it was to stop, below 0 past it [m]. public float? StopError => Outcome == StationOutcome.Stopped && Event is { } e ? e.StopError : null; public bool? OnPlatform => Outcome == StationOutcome.Stopped ? Event?.OnPlatform : null; /// How far the tail stood off the platform [m], when it stopped. public double? OffPlatform => Outcome == StationOutcome.Stopped ? Event?.OffPlatform : null; /// The difference of two times of the day in minutes, across midnight the short way. internal static double? Delay(double? actual, float planned) { if (actual is not { } a || planned < 0) return null; double d = a - planned; if (d > 720) d -= 1440; if (d < -720) d += 1440; return d; } /// /// The rows of the timetable the user's train had at t, each with what the train did /// there by the end of the recording read so far. /// public static IReadOnlyList Of(Recording recording, double t) => recording.TimetableAt(t) is { } timetable ? Of(recording, timetable) : Array.Empty(); /// The rows of a timetable of the recording, each with what the train did there. public static IReadOnlyList Of(Recording recording, Timetable timetable) => recording.Memo((typeof(StationProgress), timetable), () => Work(recording, timetable)); private static IReadOnlyList Work(Recording recording, Timetable timetable) { // The records of this train: from its timetable on, until another one is written. A train given its next // timetable may come to the first station of it at once, and a recording made before the simulator wrote the // timetable first has that station up to a look (1 s) before it: it goes with the timetable after. int next = IndexOf(recording.Timetables, timetable) + 1; double until = next < recording.Timetables.Count ? recording.Timetables[next].Time - 1 : double.MaxValue; var events = recording.UserStations() .Where(e => e.Time >= timetable.Time - 1 && e.Time < until) .ToList(); var rows = new List(timetable.Rows.Count); for (int number = 1; number <= timetable.Rows.Count; number++) { var row = timetable.Rows[number - 1]; var mine = events.Where(e => e.Row == number).ToList(); var stopped = mine.FirstOrDefault(e => e.Kind == StationKind.Stopped); var passed = mine.FirstOrDefault(e => e.Kind == StationKind.Passed); var missed = mine.FirstOrDefault(e => e.Kind == StationKind.Missed); if (stopped is not null) { double? departed = DepartureAfter(recording, stopped.Time); rows.Add(new StationProgress(number, row, missed is not null ? StationOutcome.Missed : StationOutcome.Stopped, stopped.Time, departed, Minutes(recording, stopped.Time), departed is { } d ? Minutes(recording, d) : null, stopped)); } else if (passed is not null) rows.Add(new StationProgress(number, row, StationOutcome.Passed, null, passed.Time, null, Minutes(recording, passed.Time), passed)); else if (missed is not null) rows.Add(new StationProgress(number, row, StationOutcome.Missed, null, missed.Time, null, Minutes(recording, missed.Time), missed)); else rows.Add(new StationProgress(number, row, StationOutcome.Ahead, null, null, null, null, null)); } return rows; } /// When the user's train got moving again after a stop, null while it still stood. private static double? DepartureAfter(Recording recording, double stoppedAt) { var cab = recording.Cab; for (int i = Math.Max(0, recording.CabIndexAt(stoppedAt)); i < cab.Count; i++) if (cab[i].Time > stoppedAt && cab[i].AbsSpeedKmh > 2) return cab[i].Time; return null; } /// /// The delay of the user's train at t, from the last station of its timetable it had come to /// by then: of the departure or the pass once it left, of the arrival while it stands, growing /// while it stands past the departure, and growing on the way once it is past the time it was /// to be at the next station. Null before the first station or without a timetable. /// public static TrainDelay? DelayAt(Recording recording, double t) { if (recording.TimetableAt(t) is not { } timetable || t < timetable.Time - 1) return null; double clock = recording.ClockAt(t).TotalMinutes; TrainDelay? delay = null; var rows = Of(recording, timetable); int reached = -1; for (int i = 0; i < rows.Count; i++) { var row = rows[i]; if (row.DepartedAt is { } departed && departed <= t) delay = new TrainDelay(timetable.Train, row.DepartureDelay ?? row.ArrivalDelay ?? delay?.Minutes ?? 0, row.Row.Station, false); else if (row.ArrivedAt is { } arrived && arrived <= t) { double minutes = row.ArrivalDelay ?? delay?.Minutes ?? 0; if (Delay(clock, row.Row.Departure) is { } overdue) minutes = Math.Max(minutes, overdue); delay = new TrainDelay(timetable.Train, minutes, row.Row.Station, true); } else continue; reached = i; } // On the way, past the time it was to be at the next station: at least that late, and more as it stays away. if (delay is { Standing: false } && reached + 1 < rows.Count) { var next = rows[reached + 1].Row; if (Delay(clock, next.Arrival >= 0 ? next.Arrival : next.Departure) is { } overdue && overdue > delay.Minutes) delay = delay with { Minutes = overdue }; } return delay; } /// The delays the user's train had at the stations it had left or stood at by t, by the name shown. public static IReadOnlyDictionary StationDelays(Recording recording, double t) { var delays = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var timetable in recording.Timetables) { if (timetable.Time > t) break; foreach (var row in Of(recording, timetable)) { double? at = row.ArrivedAt ?? row.DepartedAt; double? delay = row.DepartedAt <= t ? row.DepartureDelay ?? row.ArrivalDelay : row.ArrivalDelay; if (at <= t && delay is { } d) delays[StationNames.Display(row.Row.Station)] = d; } } return delays; } private static int IndexOf(IReadOnlyList timetables, Timetable timetable) { for (int i = 0; i < timetables.Count; i++) if (ReferenceEquals(timetables[i], timetable)) return i; return -1; } private static double Minutes(Recording recording, double t) => recording.ClockAt(t).TotalMinutes; }