using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Layout; using Avalonia.Media; namespace Eu07.BlackBox.Views; /// One line of the list of events. public sealed record BlackBoxEvent(double Time, string Text, IBrush Brush, bool Important); /// /// What happened to the user's train, by the clock: the commands, the autopilot, the /// signals passed, the stations, the horn points, the couplings, the train protection, /// the cab and the camera of the user. A click goes to its moment. /// public sealed class EventList : ListBox { private Recording? _recording; private int _shownRevision = -1; private List _events = new(); private bool _picking; public event Action? Seek; // The look of a list box: a control of a type of its own has no theme otherwise, and shows nothing. protected override Type StyleKeyOverride => typeof(ListBox); public IBlackBoxStrings Strings { get; set; } = new DefaultBlackBoxStrings(); public EventList() { SelectionMode = SelectionMode.Single; Padding = new Thickness(4, 0, 4, 6); // The picked line in the starter's green. Resources["ListBoxItemBackgroundSelected"] = Palette.AccentFill; Resources["ListBoxItemBackgroundSelectedPointerOver"] = Palette.AccentFillHover; Resources["ListBoxItemBackgroundSelectedPressed"] = Palette.AccentFill; Resources["ListBoxItemBackgroundPointerOver"] = Palette.CardHover; SelectionChanged += (_, _) => { if (!_picking && SelectedIndex >= 0 && SelectedIndex < _events.Count) Seek?.Invoke(_events[SelectedIndex].Time); }; } public Recording? Recording { get => _recording; set { _recording = value; _shownRevision = -1; Refresh(); } } public IReadOnlyList Events => _events; /// Takes in what the recording gained; the list grows at its end, so what is scrolled to stays. public void Refresh() { if (_recording is not { } recording) { _events.Clear(); Items.Clear(); return; } if (recording.Revision == _shownRevision) return; _shownRevision = recording.Revision; var events = Collect(recording, Strings); bool grown = events.Count >= _events.Count && _events.Select((e, i) => e == events[i]).All(same => same); if (!grown) Items.Clear(); for (int i = grown ? _events.Count : 0; i < events.Count; i++) Items.Add(new ListBoxItem { Content = Line(recording, events[i]), Padding = new Thickness(8, 4), MinHeight = 0, CornerRadius = new CornerRadius(6), Margin = new Thickness(0, 1) }); _events = events; } /// The event at or just before t picked, without going to it. public void ShowTime(double t) { int index = -1; for (int i = 0; i < _events.Count && _events[i].Time <= t + 0.05; i++) index = i; if (index == SelectedIndex) return; _picking = true; try { SelectedIndex = index; if (index >= 0) ScrollIntoView(index); } finally { _picking = false; } } private static Control Line(Recording recording, BlackBoxEvent e) { var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("58,14,*") }; var time = new TextBlock { Text = recording.ClockAt(e.Time).ToString(@"hh\:mm\:ss", CultureInfo.InvariantCulture), Foreground = Palette.FaintText, FontSize = 11.5, VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(0, 1, 0, 0) }; var dot = new Border { Width = e.Important ? 8 : 6, Height = e.Important ? 8 : 6, CornerRadius = new CornerRadius(4), Background = e.Brush, VerticalAlignment = VerticalAlignment.Top, HorizontalAlignment = HorizontalAlignment.Left, Margin = new Thickness(0, e.Important ? 5 : 6, 0, 0) }; var text = new TextBlock { Text = e.Text, Foreground = e.Important ? Palette.Text : Palette.DimText, FontSize = 12.5, FontWeight = e.Important ? FontWeight.SemiBold : FontWeight.Normal, TextWrapping = TextWrapping.Wrap, VerticalAlignment = VerticalAlignment.Center }; Grid.SetColumn(dot, 1); Grid.SetColumn(text, 2); grid.Children.Add(time); grid.Children.Add(dot); grid.Children.Add(text); return grid; } /// The events of the user's train, in the order they happened. public static List Collect(Recording recording, IBlackBoxStrings strings) { var culture = CultureInfo.CurrentCulture; var events = new List(); foreach (var c in recording.UserCommands()) events.Add(new BlackBoxEvent(c.Time, string.Format(culture, strings["EventCommand"], c.Name), Palette.Command, false)); foreach (var d in recording.DriverChanges) { if (recording.CabAt(d.Time + 0.5) is not { } s || s.Vehicle != d.Vehicle) continue; events.Add(new BlackBoxEvent(d.Time, strings[d.Driving == Driver.Ai ? "EventAutopilotOn" : "EventAutopilotOff"], Palette.Autopilot, true)); } foreach (var p in recording.UserPassings()) { string name = recording.Signal(p.Signal)?.Name ?? p.Event; events.Add(p.AtDanger ? new BlackBoxEvent(p.Time, string.Format(culture, strings["EventDanger"], name, Math.Abs(p.SpeedKmh), p.Overrun), Palette.Over, true) : new BlackBoxEvent(p.Time, string.Format(culture, strings["EventPassed"], name, MapView.AspectText(strings, p.Speed1, p.Speed2), Math.Abs(p.SpeedKmh)), Palette.Of(new AspectChange(p.Time, p.Signal, p.Speed1, p.Speed2, "")), false)); } AddUserVehicles(recording, strings, events, culture); AddStations(recording, strings, events, culture); foreach (var horn in recording.UserHornChecks()) events.Add(new BlackBoxEvent(horn.Request.Time, strings[horn.Sounded ? "EventHornSounded" : "EventHornMissed"], horn.Sounded ? Palette.HornPoint : Palette.Bad, !horn.Sounded)); foreach (var span in recording.SecurityBrakings()) { // Which of them was not acknowledged: the lamps blinking when it braked. string who = recording.CabAt(span.Start) is { } s ? s.Has(SecurityFlags.CabSignal) && s.Has(SecurityFlags.Vigilance) ? "CA/SHP" : s.Has(SecurityFlags.CabSignal) ? "SHP" : "CA" : "CA"; events.Add(new BlackBoxEvent(span.Start, string.Format(culture, strings["EventSecurityBraking"], who), Palette.Bad, true)); } AddCameras(recording, strings, events, culture); AddCouplings(recording, strings, events, culture); AddCabChanges(recording, strings, events, culture); foreach (var t in recording.Timetables) events.Add(new BlackBoxEvent(t.Time, string.Format(culture, strings["EventTimetable"], $"{t.Category} {t.Train}".Trim(), string.Format(culture, strings["Relation"], t.From, t.To)), Palette.Station, false)); if (recording.EndTime is { } end) events.Add(new BlackBoxEvent(end, strings["EventEnd"], Palette.FaintText, false)); // Stable: what happened at the same moment stays in the order of its kind. return events.OrderBy(e => e.Time).ToList(); } private static void AddUserVehicles(Recording recording, IBlackBoxStrings strings, List events, CultureInfo culture) { UserVehicleChange? before = null; foreach (var u in recording.UserVehicles) { string vehicle = recording.Vehicle(u.Vehicle)?.Name ?? ""; // Only the cab changed: told as such. if (before is { } b && b.Vehicle == u.Vehicle && b.InCab == u.InCab && b.CabEnd != u.CabEnd && u.CabEnd != 0) events.Add(new BlackBoxEvent(u.Time, string.Format(culture, strings["EventCab"], strings[u.CabEnd > 0 ? "CabEndFront" : "CabEndRear"], vehicle), Palette.UserTrain, true)); else { string text = u.Vehicle == 0 ? strings["EventNoVehicle"] : string.Format(culture, strings[u.InCab ? "EventInVehicle" : "EventOutside"], vehicle); events.Add(new BlackBoxEvent(u.Time, text, u.InCab ? Palette.UserTrain : Palette.DimText, true)); } before = u; } } private static void AddStations(Recording recording, IBlackBoxStrings strings, List events, CultureInfo culture) { foreach (var st in recording.UserStations()) { switch (st.Kind) { case StationKind.Stopped: { float error = st.StopError; string where = Math.Abs(error) < 1 ? strings["TtStopExact"] : string.Format(culture, strings[error > 0 ? "TtStopShort" : "TtStopPast"], Math.Abs(error)); events.Add(new BlackBoxEvent(st.Time, string.Format(culture, strings["EventStopped"], StationNames.Display(st.Station), where), Palette.Station, true)); break; } case StationKind.Passed: events.Add(new BlackBoxEvent(st.Time, string.Format(culture, strings["EventStationPassed"], StationNames.Display(st.Station)), Palette.Station, false)); break; case StationKind.Missed: events.Add(new BlackBoxEvent(st.Time, string.Format(culture, strings["EventStationMissed"], StationNames.Display(st.Station)), Palette.Bad, true)); break; case StationKind.Ready: events.Add(new BlackBoxEvent(st.Time, string.Format(culture, strings["EventReady"], StationNames.Display(st.Station)), Palette.Station, false)); break; } } } /// The camera of the user going to another mode: the cab, outside, free. private static void AddCameras(Recording recording, IBlackBoxStrings strings, List events, CultureInfo culture) { CameraMode? mode = null; foreach (var c in recording.Cameras) { if (c.Mode == mode) continue; if (mode is not null) events.Add(new BlackBoxEvent(c.Time, string.Format(culture, strings["EventCamera"], strings[CameraKey(c.Mode)]), Palette.DimText, false)); mode = c.Mode; } } public static string CameraKey(CameraMode mode) => mode switch { CameraMode.Cab => "CameraCab", CameraMode.External => "CameraExternal", CameraMode.Debug => "CameraDebug", _ => "CameraFree" }; /// The switches the driver worked, as the cab tells them: what came on and went off. private static readonly (CabDevices Device, string Key)[] Switched = [ (CabDevices.MainBreaker, "DevMainBreaker"), (CabDevices.PantographFront, "DevPantographFront"), (CabDevices.PantographRear, "DevPantographRear"), (CabDevices.FuelPump, "DevFuelPump"), (CabDevices.OilPump, "DevOilPump"), (CabDevices.Converter, "DevConverter"), (CabDevices.Compressor, "DevCompressor"), (CabDevices.MotorBlowers, "DevMotorBlowers"), (CabDevices.Heating, "DevHeating"), (CabDevices.Radio, "DevRadio"), (CabDevices.ShuntMode, "DevShuntMode"), (CabDevices.SpringBrake, "DevSpringBrake"), (CabDevices.BrakeReleaser, "DevBrakeReleaser"), (CabDevices.DoorPermitLeft, "DevDoorPermitLeft"), (CabDevices.DoorPermitRight, "DevDoorPermitRight"), (CabDevices.DoorStep, "DevDoorStep"), (CabDevices.DoorLock, "DevDoorLock"), (CabDevices.SpeedControl, "DevSpeedControl") ]; /// /// What the driver did in the cab, from one look to the next in the same vehicle: the switches on and off, the /// doors opened and closed, the departure signal, the hand brake, the brakes' setting, the radio's channel, the /// speed the cruise control is set to; and, as faults, moving with the doors open and the engine overheating. /// private static void AddCabChanges(Recording recording, IBlackBoxStrings strings, List events, CultureInfo culture) { CabSample? last = null; bool movingOpen = false; foreach (var s in recording.Cab) { if (last is not { } b || b.Vehicle != s.Vehicle) { last = s; movingOpen = false; continue; } bool Came(CabDevices device) => !b.Has(device) && s.Has(device); bool Went(CabDevices device) => b.Has(device) && !s.Has(device); foreach (var (device, key) in Switched) { if (Came(device)) events.Add(new BlackBoxEvent(s.Time, string.Format(culture, strings["EventDeviceOn"], strings[key]), Palette.LampOn, false)); else if (Went(device)) events.Add(new BlackBoxEvent(s.Time, string.Format(culture, strings["EventDeviceOff"], strings[key]), Palette.FaintText, false)); } foreach (var (device, open, closed) in new[] { (CabDevices.DoorsLeft, "DevDoorsLeft", "EventDoorsLeftClosed"), (CabDevices.DoorsRight, "DevDoorsRight", "EventDoorsRightClosed") }) { if (Came(device)) events.Add(new BlackBoxEvent(s.Time, strings[open], Palette.LampWarning, false)); else if (Went(device)) events.Add(new BlackBoxEvent(s.Time, strings[closed], Palette.FaintText, false)); } if (Came(CabDevices.DepartureSignal)) events.Add(new BlackBoxEvent(s.Time, strings["DevDepartureSignal"], Palette.LampWarning, false)); if (Came(CabDevices.EngineHot)) events.Add(new BlackBoxEvent(s.Time, strings["DevEngineHot"], Palette.Bad, true)); // moving with the doors open: once, until they are closed or the train stands bool doorsOpen = s.Has(CabDevices.DoorsLeft) || s.Has(CabDevices.DoorsRight); if (doorsOpen && s.AbsSpeedKmh > 2 && !movingOpen) { events.Add(new BlackBoxEvent(s.Time, string.Format(culture, strings["EventMovingDoorsOpen"], s.AbsSpeedKmh), Palette.Bad, true)); movingOpen = true; } else if (!doorsOpen || s.AbsSpeedKmh < 0.5) movingOpen = false; if (b.Extra is { } x && s.Extra is { } y) { if (x.HandBrake <= 0 && y.HandBrake > 0) events.Add(new BlackBoxEvent(s.Time, strings["EventHandBrakeOn"], Palette.LampWarning, false)); else if (x.HandBrake > 0 && y.HandBrake <= 0) events.Add(new BlackBoxEvent(s.Time, strings["EventHandBrakeOff"], Palette.FaintText, false)); if (x.BrakeDelay != y.BrakeDelay || x.BrakeMode != y.BrakeMode) { string setting = string.Join(" ยท ", new[] { CabPanel.BrakeSetting(y.BrakeDelay), CabPanel.BrakeMode(y.BrakeMode) }.Where(t => t.Length > 0)); events.Add(new BlackBoxEvent(s.Time, string.Format(culture, strings["EventBrakeSetting"], setting), Palette.Brakes, false)); } if (x.RadioChannel != y.RadioChannel && y.RadioChannel > 0) events.Add(new BlackBoxEvent(s.Time, string.Format(culture, strings["EventRadioChannel"], y.RadioChannel), Palette.LampOn, false)); // the speed set, while the cruise control holds one if (s.Has(CabDevices.SpeedControl) && (Came(CabDevices.SpeedControl) || Math.Abs(x.SpeedControlKmh - y.SpeedControlKmh) >= 1)) events.Add(new BlackBoxEvent(s.Time, string.Format(culture, strings["EventSpeedControlSet"], y.SpeedControlKmh), Palette.Target, false)); } last = s; } } private static void AddCouplings(Recording recording, IBlackBoxStrings strings, List events, CultureInfo culture) { string Name(int id) => recording.Vehicle(id)?.Name ?? id.ToString(culture); foreach (var c in recording.UserCouplings()) { if (c.Coupled) events.Add(new BlackBoxEvent(c.Time, string.Format(culture, strings["EventCoupled"], Name(c.Vehicle), Name(c.Other)), Palette.Good, true)); else if (c.Uncoupled) events.Add(new BlackBoxEvent(c.Time, string.Format(culture, strings["EventUncoupled"], Name(c.Vehicle), Name(c.PreviousOther)), Palette.Warn, true)); else events.Add(new BlackBoxEvent(c.Time, string.Format(culture, strings["EventCouplingChanged"], Name(c.Vehicle), Name(c.Other), CouplingText(strings, c.Flags)), Palette.DimText, false)); } } /// What a coupling joins, in words. public static string CouplingText(IBlackBoxStrings strings, CouplingFlags flags) { var parts = new List(); foreach (var (flag, key) in new[] { (CouplingFlags.Coupler, "CouplingCoupler"), (CouplingFlags.BrakePipe, "CouplingBrakePipe"), (CouplingFlags.MainPipe, "CouplingMainPipe"), (CouplingFlags.Control, "CouplingControl"), (CouplingFlags.Power, "CouplingPower"), (CouplingFlags.Heating, "CouplingHeating"), (CouplingFlags.Gangway, "CouplingGangway") }) if ((flags & flag) != 0) parts.Add(strings[key]); return parts.Count == 0 ? strings["CouplingNothing"] : string.Join(", ", parts); } }