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();
}