mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-11-12 09:04:09 +01:00
feat(tapes): a click on the tape takes the black box of its session to that moment, and where the black box plays is marked on the tape
This commit is contained in:
@@ -291,6 +291,9 @@ public sealed class BlackBoxWindow : Window
|
||||
/// <summary>Goes to a moment of the recording, in seconds from its start.</summary>
|
||||
public void SeekTo(double t) => SetTime(t);
|
||||
|
||||
/// <summary>The moment shown has changed: played on, sought or read further.</summary>
|
||||
public event Action<double>? TimeChanged;
|
||||
|
||||
/// <summary>Keeps the user's train in the middle of the map.</summary>
|
||||
public bool FollowTrain
|
||||
{
|
||||
@@ -453,6 +456,7 @@ public sealed class BlackBoxWindow : Window
|
||||
_events.ShowTime(_time);
|
||||
ShowReadout(null);
|
||||
ShowDelay(recording);
|
||||
TimeChanged?.Invoke(_time);
|
||||
}
|
||||
|
||||
/// <summary>How late the user's train was at the moment played, by the clock.</summary>
|
||||
|
||||
@@ -61,6 +61,7 @@ public sealed class HaslerTapeWindow : Window
|
||||
private readonly Dictionary<DateTime, bool> _expanded = new();
|
||||
private readonly Dictionary<string, BlackBoxWindow> _blackBoxes = new(StringComparer.OrdinalIgnoreCase);
|
||||
private HashSet<string> _liveBlackBoxes = new(StringComparer.OrdinalIgnoreCase);
|
||||
private IReadOnlyList<TapeSession> _sessions = Array.Empty<TapeSession>();
|
||||
|
||||
private TapeFile? _shown;
|
||||
private string? _followed;
|
||||
@@ -138,6 +139,7 @@ public sealed class HaslerTapeWindow : Window
|
||||
_view = NewView(RecorderKind.Hasler);
|
||||
_view.Scale = DefaultKmScale;
|
||||
_view.Hovered += ShowCursor;
|
||||
_view.Picked += PickOnTape;
|
||||
_scroll = new ScrollViewer
|
||||
{
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
@@ -286,9 +288,11 @@ public sealed class HaslerTapeWindow : Window
|
||||
|
||||
var old = _view;
|
||||
old.Hovered -= ShowCursor;
|
||||
old.Picked -= PickOnTape;
|
||||
_view = NewView(Recorder);
|
||||
_view.CopySettingsFrom(old);
|
||||
_view.Hovered += ShowCursor;
|
||||
_view.Picked += PickOnTape;
|
||||
_scroll.Content = _view;
|
||||
// The other view is not as tall; an offset kept from it would hide its top rows.
|
||||
_scroll.Offset = new Vector(_scroll.Offset.X, 0);
|
||||
@@ -313,6 +317,7 @@ public sealed class HaslerTapeWindow : Window
|
||||
private void ShowSessions(IReadOnlyList<TapeSession> sessions, bool toDriven)
|
||||
{
|
||||
_running = sessions.Any(session => session.Running);
|
||||
_sessions = sessions;
|
||||
_listed = Fingerprint(sessions);
|
||||
ShowWhatIsKnown();
|
||||
|
||||
@@ -346,6 +351,7 @@ public sealed class HaslerTapeWindow : Window
|
||||
{
|
||||
_shown = null;
|
||||
_view.Tape = null;
|
||||
_view.Mark = null;
|
||||
_summary.Text = "";
|
||||
return;
|
||||
}
|
||||
@@ -413,11 +419,16 @@ public sealed class HaslerTapeWindow : Window
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>The black box in its window, read on while the session runs; one window a file.</summary>
|
||||
private async Task OpenBlackBoxAsync(BlackBoxRecording box)
|
||||
/// <summary>
|
||||
/// The black box in its window, read on while the session runs; one window a file. Given a moment, it opens
|
||||
/// there. Where it plays is marked on a tape of the same session.
|
||||
/// </summary>
|
||||
private async Task OpenBlackBoxAsync(BlackBoxRecording box, double? at = null)
|
||||
{
|
||||
if (_blackBoxes.TryGetValue(box.Path, out var open))
|
||||
{
|
||||
if (at is { } moment)
|
||||
open.SeekTo(moment);
|
||||
open.Activate();
|
||||
return;
|
||||
}
|
||||
@@ -427,9 +438,73 @@ public sealed class HaslerTapeWindow : Window
|
||||
Live = _liveBlackBoxes.Contains(box.Path)
|
||||
};
|
||||
_blackBoxes[box.Path] = window;
|
||||
window.Closed += (_, _) => _blackBoxes.Remove(box.Path);
|
||||
window.TimeChanged += t => FollowBlackBox(box, t);
|
||||
window.Closed += (_, _) =>
|
||||
{
|
||||
_blackBoxes.Remove(box.Path);
|
||||
ShowMark(scrollTo: false);
|
||||
};
|
||||
window.Show(this);
|
||||
await window.OpenAsync(box.Path);
|
||||
if (at is { } t)
|
||||
window.SeekTo(t);
|
||||
}
|
||||
|
||||
/// <summary>The session of the tape shown, with its black boxes; null for a tape no list has any more.</summary>
|
||||
private TapeSession? ShownSession =>
|
||||
_shown is { } shown
|
||||
? _sessions.FirstOrDefault(session => session.Tapes.Any(tape => tape.Path == shown.Path))
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// A click on the tape takes the black box of its session to the same moment, opening it if no window has it.
|
||||
/// Both count seconds of the simulation from its start, so a tape's own time is the black box's.
|
||||
/// </summary>
|
||||
private void PickOnTape(TapeSample sample)
|
||||
{
|
||||
if (ShownSession is not { BlackBoxes.Count: > 0 } session)
|
||||
return;
|
||||
var open = session.BlackBoxes.Where(box => _blackBoxes.ContainsKey(box.Path)).ToList();
|
||||
if (open.Count == 0)
|
||||
{
|
||||
_ = OpenBlackBoxAsync(session.BlackBoxes[0], sample.Time);
|
||||
return;
|
||||
}
|
||||
foreach (var box in open)
|
||||
_blackBoxes[box.Path].SeekTo(sample.Time);
|
||||
}
|
||||
|
||||
/// <summary>Where a black box of the session shown plays, on its tape, brought into view as it moves on.</summary>
|
||||
private void FollowBlackBox(BlackBoxRecording box, double t)
|
||||
{
|
||||
if (ShownSession?.BlackBoxes.Any(b => b.Path == box.Path) != true)
|
||||
return;
|
||||
// Played, it moves every frame: the tape is drawn again only when the mark moves on it.
|
||||
double? x = _view.XAt(t), was = _view.MarkX;
|
||||
if (x is { } now && was is { } before && Math.Abs(now - before) < 0.5)
|
||||
return;
|
||||
_view.Mark = t;
|
||||
BringMarkIntoView();
|
||||
}
|
||||
|
||||
/// <summary>The mark of an open black box of the session shown, or none, on a tape just shown or changed.</summary>
|
||||
private void ShowMark(bool scrollTo)
|
||||
{
|
||||
var window = ShownSession?.BlackBoxes.Select(box => _blackBoxes.GetValueOrDefault(box.Path))
|
||||
.FirstOrDefault(w => w is not null);
|
||||
_view.Mark = window?.CurrentTime;
|
||||
if (scrollTo)
|
||||
BringMarkIntoView();
|
||||
}
|
||||
|
||||
private void BringMarkIntoView()
|
||||
{
|
||||
if (_view.MarkX is not { } x)
|
||||
return;
|
||||
double left = _scroll.Offset.X, width = _scroll.Viewport.Width;
|
||||
if (width <= 0 || (x >= left + 16 && x <= left + width - 16))
|
||||
return;
|
||||
_scroll.Offset = new Vector(Math.Max(0, x - width / 2), _scroll.Offset.Y);
|
||||
}
|
||||
|
||||
/// <summary>The newest black box of a running session is still written; once the session ends, its last part is read and that is all.</summary>
|
||||
@@ -496,11 +571,13 @@ public sealed class HaslerTapeWindow : Window
|
||||
if (tape is null)
|
||||
{
|
||||
_view.Tape = null;
|
||||
_view.Mark = null;
|
||||
_summary.Text = string.Format(App.Loc["HaslerUnreadable"], file.Path);
|
||||
return;
|
||||
}
|
||||
|
||||
_view.Tape = tape;
|
||||
ShowMark(scrollTo: !scrollToEnd);
|
||||
_summary.Text = string.Format(App.Loc["HaslerSummary"], file.Vehicle, TapeFigures.Clock(tape.Duration),
|
||||
TapeFigures.Clock(tape.RunningTime), TapeFigures.Clock(tape.StandingTime),
|
||||
tape.DistanceM / 1000.0, tape.MaxSpeedKmh, tape.AverageSpeedKmh,
|
||||
@@ -588,6 +665,8 @@ public sealed class HaslerTapeWindow : Window
|
||||
|
||||
var copy = NewView(Recorder);
|
||||
copy.CopySettingsFrom(_view);
|
||||
// The paper alone, not where a black box happens to be.
|
||||
copy.Mark = null;
|
||||
// The trip it was asked for: a reload may have come in behind the file picker.
|
||||
copy.Tape = tape;
|
||||
copy.Measure(Size.Infinity);
|
||||
@@ -631,6 +710,8 @@ public sealed class HaslerTapeWindow : Window
|
||||
s.BrakePressBar, YesNo(s.Current), YesNo(s.SecurityAlarm))
|
||||
: string.Format(App.Loc["HaslerCursor"], time, s.DistanceM / 1000.0, s.SpeedKmh, s.BrakePressBar,
|
||||
YesNo(s.Current));
|
||||
if (ShownSession is { BlackBoxes.Count: > 0 })
|
||||
_cursor.Text += " " + App.Loc["HaslerClickBlackBox"];
|
||||
}
|
||||
|
||||
private static string YesNo(bool on) => on ? App.Loc["HaslerYes"] : App.Loc["HaslerNo"];
|
||||
|
||||
@@ -31,12 +31,21 @@ public abstract class TapeView : Control
|
||||
public static readonly StyledProperty<TimeSpan> StartClockProperty =
|
||||
AvaloniaProperty.Register<TapeView, TimeSpan>(nameof(StartClock));
|
||||
|
||||
/// <summary>
|
||||
/// The moment a black box of the same run is at, in the tape's own seconds: both count the simulation from
|
||||
/// its start, paused or not. Null draws nothing.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<double?> MarkProperty =
|
||||
AvaloniaProperty.Register<TapeView, double?>(nameof(Mark));
|
||||
|
||||
private static readonly IPen CursorPen = new Pen(new SolidColorBrush(Color.Parse("#FFB000")), 1);
|
||||
private static readonly IBrush MarkBrush = new SolidColorBrush(Color.Parse("#4FC3F7"));
|
||||
private static readonly IPen MarkPen = new Pen(MarkBrush, 2);
|
||||
|
||||
static TapeView()
|
||||
{
|
||||
AffectsMeasure<TapeView>(TapeProperty, ByDistanceProperty, ScaleProperty);
|
||||
AffectsRender<TapeView>(TapeProperty, ByDistanceProperty, ScaleProperty, StartClockProperty);
|
||||
AffectsRender<TapeView>(TapeProperty, ByDistanceProperty, ScaleProperty, StartClockProperty, MarkProperty);
|
||||
}
|
||||
|
||||
public HaslerTape? Tape
|
||||
@@ -63,9 +72,18 @@ public abstract class TapeView : Control
|
||||
set => SetValue(StartClockProperty, value);
|
||||
}
|
||||
|
||||
public double? Mark
|
||||
{
|
||||
get => GetValue(MarkProperty);
|
||||
set => SetValue(MarkProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>The sample under the pointer, or null once it leaves the tape.</summary>
|
||||
public event Action<TapeSample?>? Hovered;
|
||||
|
||||
/// <summary>The sample clicked on.</summary>
|
||||
public event Action<TapeSample>? Picked;
|
||||
|
||||
protected abstract double LeftMargin { get; }
|
||||
|
||||
private const double RightMargin = 24;
|
||||
@@ -86,6 +104,23 @@ public abstract class TapeView : Control
|
||||
private double PositionOf(double x) =>
|
||||
ByDistance ? (x - LeftMargin) / Scale * 1000.0 : (x - LeftMargin) / Scale * 60.0 + (Tape?.StartTime ?? 0);
|
||||
|
||||
/// <summary>
|
||||
/// Where the mark stands across the tape; null with no mark or outside the trip. By distance it is where the
|
||||
/// train was at that moment, so through a stop the mark stands still.
|
||||
/// </summary>
|
||||
public double? MarkX => Mark is { } t ? XAt(t) : null;
|
||||
|
||||
/// <summary>Where a moment of the trip stands across the tape; null outside the trip.</summary>
|
||||
public double? XAt(double t)
|
||||
{
|
||||
var tape = Tape;
|
||||
if (tape is null || tape.IsEmpty || t < tape.StartTime || t > tape.Samples[^1].Time)
|
||||
return null;
|
||||
return ByDistance
|
||||
? XOf(tape.Samples[tape.IndexAt(t, byDistance: false)])
|
||||
: LeftMargin + (t - tape.StartTime) / 60.0 * Scale;
|
||||
}
|
||||
|
||||
/// <summary>Seconds since midnight at the sample, by the clock the trip started at.</summary>
|
||||
public double ClockAt(TapeSample s) => StartClock.TotalSeconds + s.Time - (Tape?.StartTime ?? 0);
|
||||
|
||||
@@ -96,6 +131,7 @@ public abstract class TapeView : Control
|
||||
ByDistance = other.ByDistance;
|
||||
Scale = other.Scale;
|
||||
StartClock = other.StartClock;
|
||||
Mark = other.Mark;
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize) =>
|
||||
@@ -110,6 +146,7 @@ public abstract class TapeView : Control
|
||||
return;
|
||||
|
||||
DrawTape(context, tape);
|
||||
DrawMark(context);
|
||||
DrawCursor(context);
|
||||
}
|
||||
|
||||
@@ -133,6 +170,19 @@ public abstract class TapeView : Control
|
||||
Hovered?.Invoke(index < 0 ? null : tape.Samples[index]);
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
var tape = Tape;
|
||||
if (tape is null || tape.IsEmpty || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
return;
|
||||
|
||||
double x = Math.Clamp(e.GetPosition(this).X, LeftMargin, TapeRight);
|
||||
int index = tape.IndexAt(PositionOf(x), ByDistance);
|
||||
if (index >= 0)
|
||||
Picked?.Invoke(tape.Samples[index]);
|
||||
}
|
||||
|
||||
protected override void OnPointerExited(PointerEventArgs e)
|
||||
{
|
||||
base.OnPointerExited(e);
|
||||
@@ -141,6 +191,23 @@ public abstract class TapeView : Control
|
||||
Hovered?.Invoke(null);
|
||||
}
|
||||
|
||||
/// <summary>A line across the tape with a notch on top, in a colour of its own: not the pointer's.</summary>
|
||||
private void DrawMark(DrawingContext context)
|
||||
{
|
||||
if (MarkX is not { } x)
|
||||
return;
|
||||
context.DrawLine(MarkPen, new Point(x, 0), new Point(x, TapeHeight));
|
||||
var notch = new StreamGeometry();
|
||||
using (var g = notch.Open())
|
||||
{
|
||||
g.BeginFigure(new Point(x - 5, 0), true);
|
||||
g.LineTo(new Point(x + 5, 0));
|
||||
g.LineTo(new Point(x, 7));
|
||||
g.EndFigure(true);
|
||||
}
|
||||
context.DrawGeometry(MarkBrush, null, notch);
|
||||
}
|
||||
|
||||
private void DrawCursor(DrawingContext context)
|
||||
{
|
||||
if (CursorX is { } x)
|
||||
|
||||
@@ -852,6 +852,7 @@
|
||||
<String key="HaslerSessionRunning">běží</String>
|
||||
<String key="HaslerBlackBox">Černá skříňka</String>
|
||||
<String key="HaslerBlackBoxTip">Celá relace na mapě: jízda oproti AI, povely, návěstidla, kabina</String>
|
||||
<String key="HaslerClickBlackBox">klikněte: černá skříňka v tomto okamžiku</String>
|
||||
<String key="WarehouseSaved">Ve skladu: {0}</String>
|
||||
<String key="WarehouseNothingToAdd">Vyberte soupravu s vozidly</String>
|
||||
<String key="GfxGroupLevelTip">Úroveň jen pro tuto část grafiky</String>
|
||||
|
||||
@@ -1081,6 +1081,7 @@
|
||||
<String key="HaslerSessionRunning">running</String>
|
||||
<String key="HaslerBlackBox">Black box</String>
|
||||
<String key="HaslerBlackBoxTip">The whole session on a map: the driving against the AI, the commands, the signals, the cab</String>
|
||||
<String key="HaslerClickBlackBox">click: the black box at this moment</String>
|
||||
<String key="WarehouseSaved">In the warehouse: {0}</String>
|
||||
<String key="WarehouseNothingToAdd">Pick a consist with vehicles</String>
|
||||
<String key="GfxGroupLevelTip">Level for this part of the graphics only</String>
|
||||
|
||||
@@ -852,6 +852,7 @@
|
||||
<String key="HaslerSessionRunning">folyamatban</String>
|
||||
<String key="HaslerBlackBox">Fekete doboz</String>
|
||||
<String key="HaslerBlackBoxTip">A teljes menet a térképen: vezetés az AI-hoz képest, parancsok, jelzők, vezetőfülke</String>
|
||||
<String key="HaslerClickBlackBox">kattintás: a fekete doboz ebben a pillanatban</String>
|
||||
<String key="WarehouseSaved">A raktárban: {0}</String>
|
||||
<String key="WarehouseNothingToAdd">Válassz járműveket tartalmazó szerelvényt</String>
|
||||
<String key="GfxGroupLevelTip">Csak a grafika ezen részének szintje</String>
|
||||
|
||||
@@ -903,6 +903,7 @@
|
||||
<String key="HaslerSessionRunning">trwa</String>
|
||||
<String key="HaslerBlackBox">Czarna skrzynka</String>
|
||||
<String key="HaslerBlackBoxTip">Cała sesja na mapie: jazda względem AI, komendy, sygnalizatory, kabina</String>
|
||||
<String key="HaslerClickBlackBox">kliknij: czarna skrzynka w tej chwili</String>
|
||||
<String key="WarehouseSaved">W magazynie: {0}</String>
|
||||
<String key="WarehouseNothingToAdd">Wybierz skład z pojazdami</String>
|
||||
<String key="GfxGroupLevelTip">Poziom tylko tej części grafiki</String>
|
||||
|
||||
@@ -852,6 +852,7 @@
|
||||
<String key="HaslerSessionRunning">идёт</String>
|
||||
<String key="HaslerBlackBox">Чёрный ящик</String>
|
||||
<String key="HaslerBlackBoxTip">Вся сессия на карте: езда по сравнению с ИИ, команды, светофоры, кабина</String>
|
||||
<String key="HaslerClickBlackBox">щелчок: чёрный ящик в этот момент</String>
|
||||
<String key="WarehouseSaved">На складе: {0}</String>
|
||||
<String key="WarehouseNothingToAdd">Выберите состав с транспортом</String>
|
||||
<String key="GfxGroupLevelTip">Уровень только для этой части графики</String>
|
||||
|
||||
Reference in New Issue
Block a user