diff --git a/Eu07.BlackBox.Views/BlackBoxStrings.cs b/Eu07.BlackBox.Views/BlackBoxStrings.cs index d897004..c0474d4 100644 --- a/Eu07.BlackBox.Views/BlackBoxStrings.cs +++ b/Eu07.BlackBox.Views/BlackBoxStrings.cs @@ -48,14 +48,36 @@ public sealed class DefaultBlackBoxStrings : IBlackBoxStrings ["AspectStop"] = "stop", ["AspectFree"] = "go", ["AspectSpeed"] = "{0:0}/{1}", - ["LegendWithin"] = "within the AI's speed", - ["LegendSlightly"] = "up to {0:0} km/h over", - ["LegendOver"] = "more over", - ["LegendAutopilot"] = "autopilot", - ["LegendNoData"] = "no AI data", + ["LegendTitle"] = "The train's way: its speed against the AI's", + ["LegendWithin"] = "no faster than the AI", + ["LegendSlightly"] = "up to {0:0} km/h faster than the AI", + ["LegendOver"] = "over {0:0} km/h faster than the AI", + ["LegendAutopilot"] = "the autopilot drove", + ["LegendNoData"] = "no AI to compare with", + ["LegendMarks"] = "Signs on the map", ["TimelineSpeed"] = "speed", ["TimelineTarget"] = "AI", ["TimelineLimit"] = "next limit", + ["BandEvents"] = "events", + ["KeyStation"] = "station", + ["KeyHorn"] = "W6 horn", + ["KeySignal"] = "signal passed", + ["KeyCoupling"] = "coupling", + ["KeyDriver"] = "autopilot on/off", + ["KeyCommand"] = "command", + ["TlSignal"] = "signal {0} passed: {1}", + ["TlSignalDanger"] = "signal {0} passed at stop", + ["TlCoupled"] = "coupled", + ["TlUncoupled"] = "uncoupled", + ["TlCouplingChanged"] = "a coupling changed", + ["TlAutopilotOn"] = "the autopilot switched on", + ["TlAutopilotOff"] = "the user takes over", + ["TlStationStopped"] = "{0}: stopped", + ["TlStationPassed"] = "{0}: passed", + ["TlStationMissed"] = "{0}: missed", + ["TlHornSounded"] = "W6: the horn sounded", + ["TlHornSilent"] = "W6: no horn", + ["TlCommand"] = "command: {0}", ["BandDriving"] = "driving", ["BandBrakes"] = "brakes", ["BandAlerter"] = "alerter", @@ -173,6 +195,12 @@ public sealed class DefaultBlackBoxStrings : IBlackBoxStrings ["CabLightsTail"] = "tail", ["CabSectionBrakes"] = "Brakes", ["CabSectionDrive"] = "Traction", + ["CabSectionControls"] = "Controls", + ["CabSectionDevices"] = "Devices", + ["CabGroupPower"] = "Power", + ["CabGroupDriving"] = "Driving", + ["CabGroupSafety"] = "Safety", + ["CabLightsWant"] = "{0}, should be {1}", ["DevVigilance"] = "CA", ["DevCabSignal"] = "SHP", ["DevSecurityBraking"] = "CA/SHP braking", @@ -247,14 +275,36 @@ public sealed class DefaultBlackBoxStrings : IBlackBoxStrings ["AspectStop"] = "stój", ["AspectFree"] = "wolna droga", ["AspectSpeed"] = "{0:0}/{1}", - ["LegendWithin"] = "w granicach prędkości AI", - ["LegendSlightly"] = "do {0:0} km/h ponad", - ["LegendOver"] = "więcej ponad", - ["LegendAutopilot"] = "autopilot", - ["LegendNoData"] = "brak danych AI", + ["LegendTitle"] = "Ślad pociągu: prędkość wobec AI", + ["LegendWithin"] = "nie szybciej niż AI", + ["LegendSlightly"] = "do {0:0} km/h szybciej niż AI", + ["LegendOver"] = "ponad {0:0} km/h szybciej niż AI", + ["LegendAutopilot"] = "prowadził autopilot", + ["LegendNoData"] = "brak AI do porównania", + ["LegendMarks"] = "Znaki na mapie", ["TimelineSpeed"] = "prędkość", ["TimelineTarget"] = "AI", ["TimelineLimit"] = "następne ograniczenie", + ["BandEvents"] = "zdarzenia", + ["KeyStation"] = "stacja", + ["KeyHorn"] = "W6 trąbienie", + ["KeySignal"] = "minięty semafor", + ["KeyCoupling"] = "sprzęg", + ["KeyDriver"] = "autopilot wł./wył.", + ["KeyCommand"] = "polecenie", + ["TlSignal"] = "semafor {0} minięty: {1}", + ["TlSignalDanger"] = "semafor {0} minięty na „Stój”", + ["TlCoupled"] = "sprzęg połączony", + ["TlUncoupled"] = "sprzęg rozłączony", + ["TlCouplingChanged"] = "zmiana na sprzęgu", + ["TlAutopilotOn"] = "autopilot włączony", + ["TlAutopilotOff"] = "prowadzenie przejmuje użytkownik", + ["TlStationStopped"] = "{0}: postój", + ["TlStationPassed"] = "{0}: przejazd", + ["TlStationMissed"] = "{0}: pominięta", + ["TlHornSounded"] = "W6: zatrąbiono", + ["TlHornSilent"] = "W6: bez trąbienia", + ["TlCommand"] = "polecenie: {0}", ["BandDriving"] = "jazda", ["BandBrakes"] = "hamulce", ["BandAlerter"] = "czuwak", @@ -372,6 +422,12 @@ public sealed class DefaultBlackBoxStrings : IBlackBoxStrings ["CabLightsTail"] = "koniec", ["CabSectionBrakes"] = "Hamulce", ["CabSectionDrive"] = "Napęd", + ["CabSectionControls"] = "Nastawniki", + ["CabSectionDevices"] = "Urządzenia", + ["CabGroupPower"] = "Zasilanie", + ["CabGroupDriving"] = "Jazda", + ["CabGroupSafety"] = "Bezpieczeństwo", + ["CabLightsWant"] = "{0}, a ma być {1}", ["DevVigilance"] = "CA", ["DevCabSignal"] = "SHP", ["DevSecurityBraking"] = "Hamowanie CA/SHP", diff --git a/Eu07.BlackBox.Views/CabPanel.cs b/Eu07.BlackBox.Views/CabPanel.cs index 3cfb307..2f65eeb 100644 --- a/Eu07.BlackBox.Views/CabPanel.cs +++ b/Eu07.BlackBox.Views/CabPanel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Media; @@ -10,14 +11,14 @@ namespace Eu07.BlackBox.Views; /// /// The cab of the user's train at the moment played: the speed against the AI's, the /// accelerations, the controllers, the pressures of the brakes as gauges, the traction, -/// the lights at the head and the tail of the train, and a lamp for every device, the -/// alerter (CA) and the cab signalling (SHP) apart. +/// the lights at the head and the tail of the train with what they are to show, and a +/// lamp for every device in three groups (power, driving, safety), the alerter (CA) and +/// the cab signalling (SHP) apart. /// public sealed class CabPanel : Control { private const double Pad = 14; - private const double LampRow = 16; - private const int LampColumns = 3; + private const double LampRow = 15; private Recording? _recording; private double _time; @@ -27,7 +28,7 @@ public sealed class CabPanel : Control public CabPanel() { ClipToBounds = true; - Height = 428; + Height = 506; } public IBlackBoxStrings Strings { get; set; } = new DefaultBlackBoxStrings(); @@ -56,7 +57,7 @@ public sealed class CabPanel : Control public override void Render(DrawingContext context) { var size = Bounds.Size; - double width = size.Width - 2 * Pad; + double width = size.Width - 2 * Pad, right = size.Width - Pad; // The last sample, unless the train was out of sight for longer than a moment. if (_recording is not { } recording || recording.CabAt(_time) is not { } s || _time - s.Time > 1.5) @@ -67,44 +68,40 @@ public sealed class CabPanel : Control Measure(recording); var culture = CultureInfo.CurrentCulture; - // Who drives, from which cab of which vehicle. - double chipX = size.Width - Pad; - string name = recording.Vehicle(s.Vehicle)?.Name ?? ""; + // The vehicle at the left; who drives, from which cab, at the right. + double chipX = right; if (s.CabEnd != 0) - chipX -= ChipLeft(context, Strings[s.CabEnd > 0 ? "CabEndFront" : "CabEndRear"], Palette.Text, chipX, 10) + 5; + chipX -= ChipLeft(context, Strings[s.CabEnd > 0 ? "CabEndFront" : "CabEndRear"], Palette.Text, chipX, 4) + 5; var (driverText, driverBrush) = !s.InCab ? (Strings["CabOutside"], Palette.DimText) : s.Driving == Driver.Ai ? (Strings["CabDriverAi"], Palette.Autopilot) : (Strings["CabDriverUser"], Palette.UserTrain); - chipX -= ChipLeft(context, driverText, driverBrush, chipX, 10) + 8; - var vehicle = MapView.Text(name, Palette.FaintText, 11); - if (chipX - vehicle.Width > Pad + 70) - context.DrawText(vehicle, new Point(chipX - vehicle.Width, 13)); + chipX -= ChipLeft(context, driverText, driverBrush, chipX, 4) + 10; + using (context.PushClip(new Rect(Pad, 0, Math.Max(0, chipX - Pad), 28))) + context.DrawText(MapView.Text(recording.Vehicle(s.Vehicle)?.Name ?? "", Palette.Text, 12, FontWeight.SemiBold), new Point(Pad, 6)); - // The speed, large, and what the AI would do. - double top = 38; + // The speed, large, what the AI would do under it, the accelerations at the right. var judgement = DrivingSummary.Judge(s); var speed = MapView.Text(s.AbsSpeedKmh.ToString("0", culture), judgement is SpeedJudgement.Over ? Palette.Bad - : judgement is SpeedJudgement.Slightly ? Palette.Warn : Palette.Text, 38, FontWeight.SemiBold); - context.DrawText(speed, new Point(Pad, top - 4)); - context.DrawText(MapView.Text("km/h", Palette.DimText, 12), new Point(Pad + speed.Width + 4, top + speed.Height - 24)); + : judgement is SpeedJudgement.Slightly ? Palette.Warn : Palette.Text, 36, FontWeight.SemiBold); + context.DrawText(speed, new Point(Pad, 24)); + context.DrawText(MapView.Text("km/h", Palette.DimText, 12), new Point(Pad + speed.Width + 4, 24 + speed.Height - 21)); + double gaugeX = Pad + Math.Max(150, width * 0.42); if (s.AiValid) { - context.DrawText(MapView.Text(string.Format(culture, Strings["CabAiTarget"], s.Ai.TargetKmh), Palette.Target, 12), - new Point(Pad, top + speed.Height - 4)); - if (s.Ai.HasNextLimit) - context.DrawText(MapView.Text(string.Format(culture, Strings["LimitAt"], s.Ai.NextLimitKmh, s.Ai.NextLimitDistance), Palette.Limit, 11), - new Point(Pad, top + speed.Height + 12)); + using (context.PushClip(new Rect(Pad, 76, gaugeX - Pad - 8, 40))) + { + context.DrawText(MapView.Text(string.Format(culture, Strings["CabAiTarget"], s.Ai.TargetKmh), Palette.Target, 12), new Point(Pad, 76)); + if (s.Ai.HasNextLimit) + context.DrawText(MapView.Text(string.Format(culture, Strings["LimitAt"], s.Ai.NextLimitKmh, s.Ai.NextLimitDistance), Palette.Limit, 11), + new Point(Pad, 93)); + } } - - // The accelerations, as needles from the middle: along the track and across it. - double gaugeX = Pad + Math.Max(150, width * 0.42); - double gaugeWidth = size.Width - Pad - gaugeX; - Balance(context, Strings["CabAcceleration"], s.Acceleration, 1.5, gaugeX, top + 2, gaugeWidth); - Balance(context, Strings["CabLateral"], s.LateralAcceleration, 1.5, gaugeX, top + 34, gaugeWidth); + double gaugeWidth = right - gaugeX; + Balance(context, Strings["CabAcceleration"], s.Acceleration, 1.5, gaugeX, 34, gaugeWidth); + Balance(context, Strings["CabLateral"], s.LateralAcceleration, 1.5, gaugeX, 70, gaugeWidth); // The controllers. - top = 112; - Divider(context, top - 8, size.Width); + double top = Section(context, Strings["CabSectionControls"], Pad, 118, right); double column = width / 4; Value(context, Strings["CabController"], s.MainController.ToString(culture), Pad, top); Steps(context, s.MainController, _controllerTop, Pad, top + 34, column - 14); @@ -113,54 +110,83 @@ public sealed class CabPanel : Control Value(context, Strings["CabTrainBrake"], s.TrainBrake.ToString("0.0", culture), Pad + 3 * column, top); // The brakes, as gauges. - top = 160; - Divider(context, top - 6, size.Width); + top = Section(context, Strings["CabSectionBrakes"], Pad, 180, right); Gauge(context, Strings["CabPipe"], s.BrakePipe, 6, 5, $"{s.BrakePipe:0.00} bar", Palette.Station, Pad, top, width); Gauge(context, Strings["CabCylinder"], s.BrakeCylinder, 6, null, $"{s.BrakeCylinder:0.0} bar", Palette.Brakes, Pad, top + 18, width); Gauge(context, Strings["CabReservoir"], s.MainReservoir, 10, null, $"{s.MainReservoir:0.0} bar", Palette.Controller, Pad, top + 36, width); Gauge(context, Strings["CabLocalBrake"], s.LocalBrake, 1, null, $"{s.LocalBrake * 100:0} %", Palette.Brakes, Pad, top + 54, width); - // The traction and the lights at both ends of the train. - top = 242; - Divider(context, top - 6, size.Width); - double third = width * 0.62 / 3; + // The traction at the left, the lights of both ends of the train at the right, one under the other. + double half = Pad + width / 2; + top = Section(context, Strings["CabSectionDrive"], Pad, 272, half - 10); + Section(context, Strings["CabLights"], half + 4, 272, right); + double third = (half - 10 - Pad) / 3; Value(context, Strings["CabVoltage"], $"{s.Voltage:0} V", Pad, top); Value(context, Strings["CabCurrent"], $"{s.MotorCurrent:0} A", Pad + third, top); Value(context, Strings["CabRpm"], $"{s.EngineRpm:0}", Pad + 2 * third, top); - double lightsX = Pad + 3 * third + 4; - context.DrawText(MapView.Text(Strings["CabLights"], Palette.DimText, 10), new Point(lightsX, top)); - double faceX = lightsX; - foreach (var (key, lights, required, right) in new[] + double row = top; + foreach (var (key, lights, required, isRight) in new[] { ("CabLightsHead", s.LightsHead, HeadCodes.RequiredHead(s), HeadCodes.HeadRight(s)), ("CabLightsTail", s.LightsTail, HeadCodes.RequiredTail(s), HeadCodes.TailRight(s)) }) { - DrawFace(context, lights, new Point(faceX + 18, top + 24)); - // the end, and the signal it shows: green as it is to be (with the lamps the vehicle has), red otherwise - // with the one it is to show + DrawFace(context, lights, new Point(half + 4 + 17, row + 15)); + // the end, and under it the signal it shows: green as it is to be (with the lamps the vehicle has), + // red otherwise with the one it is to show var shown = HeadCodes.Of(lights); - var text = right == false && required is { } want - ? $"{HeadCodes.Name(shown)} ≠ {HeadCodes.Name(want)}" - : right == true && required is { } met ? HeadCodes.Name(met) : HeadCodes.Name(shown); - var brush = right is null ? Palette.FaintText : right.Value ? Palette.Good : Palette.Bad; - var label = MapView.Text($"{Strings[key]} {text}", brush, 9); - context.DrawText(label, new Point(faceX + 18 - label.Width / 2, top + 34)); - faceX += 62; + var text = isRight == false && required is { } want + ? string.Format(culture, Strings["CabLightsWant"], HeadCodes.Name(shown), HeadCodes.Name(want)) + : isRight == true && required is { } met ? HeadCodes.Name(met) : HeadCodes.Name(shown); + var brush = isRight is null ? Palette.DimText : isRight.Value ? Palette.Good : Palette.Bad; + using (context.PushClip(new Rect(half + 44, row, right - half - 44, 28))) + { + context.DrawText(MapView.Text(Strings[key], Palette.DimText, 10), new Point(half + 46, row)); + context.DrawText(MapView.Text(text, brush, 12, FontWeight.SemiBold), new Point(half + 46, row + 11)); + } + row += 27; } - // The lamps. - top = 292; - Divider(context, top - 6, size.Width); - int i = 0; - double lampColumn = width / LampColumns; - foreach (var (key, brush) in Lamps(s)) + // The devices, in three groups side by side: lit as their lamps are in the cab, the ones off faint. + top = Section(context, Strings["CabSectionDevices"], Pad, 350, right); + var groups = Devices(s); + var widths = new double[groups.Length]; + for (int g = 0; g < groups.Length; g++) { - var at = new Point(Pad + i % LampColumns * lampColumn, top + i / LampColumns * LampRow); - Led(context, at + new Vector(5, 7), brush); - context.DrawText(MapView.Text(Strings[key], brush is null ? Palette.FaintText : Palette.Text, 10.5), at + new Vector(14, 0)); - i++; + widths[g] = MapView.Text(Strings[groups[g].Title], Palette.DimText, 10, FontWeight.SemiBold).Width; + foreach (var (key, _) in groups[g].Lamps) + widths[g] = Math.Max(widths[g], 14 + MapView.Text(Strings[key], Palette.Text, 10.5).Width); } + // each group as wide as its longest name, what is left shared between them + double spare = (width - widths.Sum()) / groups.Length; + double x = Pad; + for (int g = 0; g < groups.Length; g++) + { + double groupWidth = Math.Max(20, widths[g] + spare); + using (context.PushClip(new Rect(x, top, groupWidth - 4, size.Height - top))) + { + context.DrawText(MapView.Text(Strings[groups[g].Title], Palette.DimText, 10, FontWeight.SemiBold), new Point(x, top)); + double y = top + 16; + foreach (var (key, brush) in groups[g].Lamps) + { + Led(context, new Point(x + 4, y + 7), brush); + context.DrawText(MapView.Text(Strings[key], brush is null ? Palette.FaintText : Palette.Text, 10.5), new Point(x + 14, y)); + y += LampRow; + } + } + x += groupWidth; + } + } + + /// The title of a part of the panel, with a line from it to the right; the top of the part returned. + private static double Section(DrawingContext context, string title, double x, double y, double right) + { + var text = MapView.Text(title.ToUpper(CultureInfo.CurrentCulture), Palette.DimText, 9, FontWeight.SemiBold); + context.DrawText(text, new Point(x, y)); + double line = Math.Round(y + text.Height / 2) + 0.5; + if (x + text.Width + 8 < right) + context.DrawLine(new Pen(Palette.Grid, 1), new Point(x + text.Width + 8, line), new Point(right, line)); + return y + 17; } /// The highest step of the controller used in the recording, for its bar. @@ -175,9 +201,6 @@ public sealed class CabPanel : Control _measured = recording.Revision; } - private static void Divider(DrawingContext context, double y, double width) => - context.DrawLine(new Pen(Palette.Grid, 1), new Point(Pad, y + 0.5), new Point(width - Pad, y + 0.5)); - private static void Value(DrawingContext context, string label, string value, double x, double y) { context.DrawText(MapView.Text(label, Palette.DimText, 10), new Point(x, y)); @@ -269,40 +292,48 @@ public sealed class CabPanel : Control context.DrawEllipse(lit ?? Palette.LampOff, null, center, 3.5, 3.5); } - /// Every device, with the colour of its lamp if it was on. - private static IEnumerable<(string Key, IBrush? Lit)> Lamps(CabSample s) + /// Every device, with the colour of its lamp if it was on, in the groups the panel shows. + private static (string Title, (string Key, IBrush? Lit)[] Lamps)[] Devices(CabSample s) { IBrush? On(CabDevices device, IBrush colour) => s.Has(device) ? colour : null; IBrush? Flag(CabFlags flag, IBrush colour) => (s.Flags & flag) != 0 ? colour : null; IBrush? Security(SecurityFlags flag, IBrush colour) => s.Has(flag) ? colour : null; - yield return ("DevBattery", On(CabDevices.Battery, Palette.LampOn)); - yield return ("DevMainBreaker", On(CabDevices.MainBreaker, Palette.LampOn)); - yield return ("DevConverter", On(CabDevices.Converter, Palette.LampOn)); - yield return ("DevPantographFront", On(CabDevices.PantographFront, Palette.LampOn)); - yield return ("DevPantographRear", On(CabDevices.PantographRear, Palette.LampOn)); - yield return ("DevCompressor", On(CabDevices.Compressor, Palette.LampOn)); - yield return ("DevLineContactors", On(CabDevices.LineContactors, Palette.LampOn)); - yield return ("DevHeating", On(CabDevices.Heating, Palette.LampOn)); - yield return ("DevRadio", On(CabDevices.Radio, Palette.LampOn)); - yield return ("DevShuntMode", On(CabDevices.ShuntMode, Palette.LampOn)); - yield return ("DevDoorsLeft", On(CabDevices.DoorsLeft, Palette.LampWarning)); - yield return ("DevDoorsRight", On(CabDevices.DoorsRight, Palette.LampWarning)); - yield return ("DevSanding", On(CabDevices.Sanding, Palette.LampWarning)); - yield return ("DevHorn", On(CabDevices.Horn, Palette.LampWarning)); - yield return ("DevSpringBrake", On(CabDevices.SpringBrake, Palette.LampWarning)); + var power = new[] + { + ("DevBattery", On(CabDevices.Battery, Palette.LampOn)), + ("DevMainBreaker", On(CabDevices.MainBreaker, Palette.LampOn)), + ("DevPantographFront", On(CabDevices.PantographFront, Palette.LampOn)), + ("DevPantographRear", On(CabDevices.PantographRear, Palette.LampOn)), + ("DevConverter", On(CabDevices.Converter, Palette.LampOn)), + ("DevCompressor", On(CabDevices.Compressor, Palette.LampOn)), + ("DevLineContactors", On(CabDevices.LineContactors, Palette.LampOn)), + ("DevHeating", On(CabDevices.Heating, Palette.LampOn)) + }; + var driving = new[] + { + ("DevShuntMode", On(CabDevices.ShuntMode, Palette.LampOn)), + ("DevRadio", On(CabDevices.Radio, Palette.LampOn)), + ("DevSanding", On(CabDevices.Sanding, Palette.LampWarning)), + ("DevHorn", On(CabDevices.Horn, Palette.LampWarning)), + ("DevDoorsLeft", On(CabDevices.DoorsLeft, Palette.LampWarning)), + ("DevDoorsRight", On(CabDevices.DoorsRight, Palette.LampWarning)), + ("DevSpringBrake", On(CabDevices.SpringBrake, Palette.LampWarning)) + }; + var safety = new List<(string, IBrush?)>(); if (s.Security != SecurityFlags.None || s.CabEnd != 0) { // A recording which tells them apart. - yield return ("DevVigilance", Security(SecurityFlags.VigilanceBeeping, Palette.LampAlarm) ?? Security(SecurityFlags.Vigilance, Palette.Vigilance)); - yield return ("DevCabSignal", Security(SecurityFlags.CabSignalBeeping, Palette.LampAlarm) ?? Security(SecurityFlags.CabSignal, Palette.CabSignal)); - yield return ("DevSecurityBraking", Security(SecurityFlags.Braking, Palette.LampAlarm)); + safety.Add(("DevVigilance", Security(SecurityFlags.VigilanceBeeping, Palette.LampAlarm) ?? Security(SecurityFlags.Vigilance, Palette.Vigilance))); + safety.Add(("DevCabSignal", Security(SecurityFlags.CabSignalBeeping, Palette.LampAlarm) ?? Security(SecurityFlags.CabSignal, Palette.CabSignal))); + safety.Add(("DevSecurityBraking", Security(SecurityFlags.Braking, Palette.LampAlarm))); } else - yield return ("DevAlerter", Flag(CabFlags.AlerterBeeping, Palette.LampAlarm) ?? Flag(CabFlags.AlerterBlinking, Palette.LampWarning)); - yield return ("DevWheelSlip", Flag(CabFlags.WheelSlip, Palette.LampAlarm)); - yield return ("DevOverloadRelay", On(CabDevices.OverloadRelay, Palette.LampAlarm)); - yield return ("DevRadioStop", Flag(CabFlags.RadioStop, Palette.LampAlarm)); - yield return ("DevAlarmChain", Flag(CabFlags.AlarmChain, Palette.LampAlarm)); + safety.Add(("DevAlerter", Flag(CabFlags.AlerterBeeping, Palette.LampAlarm) ?? Flag(CabFlags.AlerterBlinking, Palette.LampWarning))); + safety.Add(("DevWheelSlip", Flag(CabFlags.WheelSlip, Palette.LampAlarm))); + safety.Add(("DevOverloadRelay", On(CabDevices.OverloadRelay, Palette.LampAlarm))); + safety.Add(("DevRadioStop", Flag(CabFlags.RadioStop, Palette.LampAlarm))); + safety.Add(("DevAlarmChain", Flag(CabFlags.AlarmChain, Palette.LampAlarm))); + return [("CabGroupPower", power), ("CabGroupDriving", driving), ("CabGroupSafety", safety.ToArray())]; } } diff --git a/Eu07.BlackBox.Views/MapView.cs b/Eu07.BlackBox.Views/MapView.cs index 189aa40..d1d0f23 100644 --- a/Eu07.BlackBox.Views/MapView.cs +++ b/Eu07.BlackBox.Views/MapView.cs @@ -412,7 +412,7 @@ public sealed class MapView : Control } } - private static StreamGeometry Diamond(Point p, double size) + internal static StreamGeometry Diamond(Point p, double size) { var geometry = new StreamGeometry(); using (var g = geometry.Open()) @@ -546,7 +546,7 @@ public sealed class MapView : Control { (Palette.Within, Strings["LegendWithin"]), (Palette.Slightly, string.Format(CultureInfo.CurrentCulture, Strings["LegendSlightly"], DrivingSummary.DefaultTolerance)), - (Palette.Over, Strings["LegendOver"]), + (Palette.Over, string.Format(CultureInfo.CurrentCulture, Strings["LegendOver"], DrivingSummary.DefaultTolerance)), (Palette.Autopilot, Strings["LegendAutopilot"]), (Palette.NoData, Strings["LegendNoData"]) }; @@ -562,25 +562,33 @@ public sealed class MapView : Control if (r.Cameras.Count > 0) marks.Add((Palette.HornPoint, Strings["LegendCamera"], 2)); } - var texts = rows.Select(row => Text(row.Text, Palette.DimText, 11)).Concat(marks.Select(m => Text(m.Text, Palette.DimText, 11))).ToList(); - const double line = 17; - double width = texts.Max(t => t.Width) + 44, height = texts.Count * line + 14 + (marks.Count > 0 ? 6 : 0); + // Each part under a title of its own, in the text's colour: the dim one is hard to read on the map. + var title = Text(Strings["LegendTitle"], Palette.DimText, 11); + var marksTitle = Text(Strings["LegendMarks"], Palette.DimText, 11); + var texts = rows.Select(row => Text(row.Text, Palette.Text, 12)).Concat(marks.Select(m => Text(m.Text, Palette.Text, 12))).ToList(); + const double line = 19, heading = 18, gap = 8; + double width = Math.Max(texts.Max(t => t.Width) + 44, Math.Max(title.Width, marks.Count > 0 ? marksTitle.Width : 0) + 24); + double height = 10 + heading + rows.Length * line + (marks.Count > 0 ? gap + heading + marks.Count * line : 0) + 6; var box = new Rect(10, size.Height - height - 10, width, height); - context.DrawRectangle(Palette.Faded(Palette.Card, 0.9), new Pen(Palette.CardBorder, 1), box); - double y = box.Top + 7; + context.DrawRectangle(Palette.Faded(Palette.Card, 0.96), new Pen(Palette.CardBorder, 1), box, 3, 3); + double y = box.Top + 8; + context.DrawText(title, new Point(box.Left + 12, y)); + y += heading; for (int i = 0; i < rows.Length; i++, y += line) { - context.DrawLine(new Pen(rows[i].Brush, 4, lineCap: PenLineCap.Round), new Point(box.Left + 12, y + 8), new Point(box.Left + 26, y + 8)); - context.DrawText(texts[i], new Point(box.Left + 34, y)); + context.DrawLine(new Pen(rows[i].Brush, 5, lineCap: PenLineCap.Round), new Point(box.Left + 13, y + 9), new Point(box.Left + 27, y + 9)); + context.DrawText(texts[i], new Point(box.Left + 36, y)); } if (marks.Count > 0) { - context.DrawLine(new Pen(Palette.Grid, 1), new Point(box.Left + 10, y + 3), new Point(box.Right - 10, y + 3)); - y += 6; + y += gap; + context.DrawLine(new Pen(Palette.Grid, 1), new Point(box.Left + 10, y - gap / 2), new Point(box.Right - 10, y - gap / 2)); + context.DrawText(marksTitle, new Point(box.Left + 12, y)); + y += heading; } for (int i = 0; i < marks.Count; i++, y += line) { - var c = new Point(box.Left + 19, y + 8); + var c = new Point(box.Left + 20, y + 9); if (marks[i].Shape == 0) context.DrawRectangle(marks[i].Brush, null, new Rect(c.X - 3, c.Y - 4, 6, 8), 1.5, 1.5); else if (marks[i].Shape == 3) @@ -589,7 +597,7 @@ public sealed class MapView : Control context.DrawGeometry(marks[i].Brush, null, Diamond(c, 4.5)); else context.DrawEllipse(marks[i].Brush, null, c, 4, 4); - context.DrawText(texts[rows.Length + i], new Point(box.Left + 34, y)); + context.DrawText(texts[rows.Length + i], new Point(box.Left + 36, y)); } } diff --git a/Eu07.BlackBox.Views/TimelineView.cs b/Eu07.BlackBox.Views/TimelineView.cs index 4e86af2..3a9c435 100644 --- a/Eu07.BlackBox.Views/TimelineView.cs +++ b/Eu07.BlackBox.Views/TimelineView.cs @@ -20,7 +20,7 @@ public sealed class TimelineView : Control { private const double Left = 56; private const double Right = 12; - private const double Top = 22; + private const double Top = 36; private const double BandHeight = 8; private const double BandGap = 3; private const double MarksHeight = 16; @@ -35,6 +35,7 @@ public sealed class TimelineView : Control private double _pressedStart; private bool _dragged; private double? _hover; + private Point? _pointer; private int _measured = -1; private Recording? _measuredOf; private double _speedTop = 40; @@ -134,6 +135,8 @@ public sealed class TimelineView : Control DrawSpeedLabels(context, top, Y); DrawBandLabels(context, chartBottom + 6); DrawKey(context); + if (MarkUnderPointer(recording, chartBottom) is { } mark) + DrawMarkLabel(context, mark, size); } /// The top of the speed scale and the highest step of the controller, once a reading. @@ -330,6 +333,9 @@ public sealed class TimelineView : Control var text = MapView.Text(names[i], Palette.FaintText, 9); context.DrawText(text, new Point(Left - 6 - text.Width, y + (BandHeight - text.Height) / 2)); } + var events = MapView.Text(Strings["BandEvents"], Palette.FaintText, 9); + double marks = top + Bands * (BandHeight + BandGap); + context.DrawText(events, new Point(Left - 6 - events.Width, marks + (MarksHeight - events.Height) / 2)); } /// @@ -427,14 +433,111 @@ public sealed class TimelineView : Control private void DrawKey(DrawingContext context) { - double x = Left + 4; - foreach (var (brush, key) in new[] { (Palette.Speed, "TimelineSpeed"), (Palette.Target, "TimelineTarget"), (Palette.Limit, "TimelineLimit") }) + const double y = 10; + double x = Left + 4, limit = Bounds.Width - Right; + bool Room(string key) => x + 18 + MapView.Text(Strings[key], Palette.DimText, 10).Width <= limit; + void Name(string key) { - context.DrawLine(new Pen(brush, 2), new Point(x, 10), new Point(x + 14, 10)); var text = MapView.Text(Strings[key], Palette.DimText, 10); - context.DrawText(text, new Point(x + 18, 10 - text.Height / 2)); + context.DrawText(text, new Point(x + 18, y - text.Height / 2)); x += 18 + text.Width + 14; } + foreach (var (brush, key) in new[] { (Palette.Speed, "TimelineSpeed"), (Palette.Target, "TimelineTarget"), (Palette.Limit, "TimelineLimit") }) + { + if (!Room(key)) + return; + context.DrawLine(new Pen(brush, 2), new Point(x, y), new Point(x + 14, y)); + Name(key); + } + // the marks, as they are drawn above the chart and in the row of the events + if (Room("KeyStation")) + { + context.DrawRectangle(Palette.Station, null, new Rect(x + 4, y - 4, 6, 8), 1.5, 1.5); + Name("KeyStation"); + } + if (Room("KeyHorn")) + { + context.DrawGeometry(Palette.Good, null, MapView.Diamond(new Point(x + 7, y), 4)); + Name("KeyHorn"); + } + foreach (var (brush, key) in new[] { (Palette.Within, "KeySignal"), (Palette.Warn, "KeyCoupling"), (Palette.Autopilot, "KeyDriver"), (Palette.Command, "KeyCommand") }) + { + if (!Room(key)) + return; + context.DrawLine(new Pen(brush, 2), new Point(x + 7, y - 5), new Point(x + 7, y + 5)); + Name(key); + } + } + + /// What the mark under the pointer is: the nearest within a few pixels, in the row of the events or the plates above. + private string? MarkUnderPointer(Recording recording, double chartBottom) + { + if (_pointer is not { } pointer) + return null; + double marks = chartBottom + 6 + Bands * (BandHeight + BandGap); + bool inPlates = pointer.Y >= Top - 16 && pointer.Y <= Top - 2; + bool inMarks = pointer.Y >= marks - 2 && pointer.Y <= marks + MarksHeight + 2; + if (!inPlates && !inMarks) + return null; + var culture = CultureInfo.CurrentCulture; + string? found = null; + double best = 5; + void Consider(double time, Func text) + { + double d = Math.Abs(X(time) - pointer.X); + if (d < best) + { + best = d; + found = text(); + } + } + if (inPlates) + { + foreach (var station in recording.UserStations()) + if (station.Kind != StationKind.Ready) + Consider(station.Time, () => string.Format(culture, Strings[station.Kind switch + { + StationKind.Stopped => "TlStationStopped", + StationKind.Missed => "TlStationMissed", + _ => "TlStationPassed" + }], StationNames.Display(station.Station))); + foreach (var horn in recording.UserHornChecks()) + Consider(horn.Request.Time, () => Strings[horn.Sounded ? "TlHornSounded" : "TlHornSilent"]); + } + foreach (var p in recording.UserPassings()) + { + if (!inMarks && !p.AtDanger) + continue; + Consider(p.Time, () => + { + string name = recording.Signal(p.Signal)?.Name ?? p.Event; + return p.AtDanger + ? string.Format(culture, Strings["TlSignalDanger"], name) + : string.Format(culture, Strings["TlSignal"], name, MapView.AspectText(Strings, p.Speed1, p.Speed2)); + }); + } + if (inMarks) + { + foreach (var c in recording.UserCouplings()) + Consider(c.Time, () => Strings[c.Coupled ? "TlCoupled" : c.Uncoupled ? "TlUncoupled" : "TlCouplingChanged"]); + foreach (var d in recording.DriverChanges) + Consider(d.Time, () => Strings[d.Driving == Driver.Ai ? "TlAutopilotOn" : "TlAutopilotOff"]); + foreach (var c in recording.UserCommands()) + Consider(c.Time, () => string.Format(culture, Strings["TlCommand"], c.Name)); + } + return found; + } + + private void DrawMarkLabel(DrawingContext context, string text, Size size) + { + if (_pointer is not { } pointer) + return; + var label = MapView.Text(text, Palette.Text, 11); + double x = Math.Clamp(pointer.X + 10, Left, size.Width - Right - label.Width - 12); + double y = Math.Max(2, pointer.Y - label.Height - 12); + var box = new Rect(x, y, label.Width + 12, label.Height + 6); + context.DrawRectangle(Palette.Card, new Pen(Palette.CardBorder, 1), box, 3, 3); + context.DrawText(label, new Point(x + 6, y + 3)); } protected override void OnPointerPressed(PointerPressedEventArgs e) @@ -468,6 +571,7 @@ public sealed class TimelineView : Control _start = Math.Clamp(_pressedStart - moved * _secondsPerPixel, 0, Math.Max(0, _recording.Duration - PlotWidth * _secondsPerPixel * 0.5)); } _hover = position.X >= Left ? Math.Clamp(TimeAt(position.X), 0, _recording.Duration) : null; + _pointer = position; Hovered?.Invoke(_hover); InvalidateVisual(); } @@ -485,6 +589,7 @@ public sealed class TimelineView : Control { base.OnPointerExited(e); _hover = null; + _pointer = null; Hovered?.Invoke(null); InvalidateVisual(); }