fix(tapes): no mark from a closed black box, the mark kept right through a view changed or a stop, the moment asked while a file is read kept, no jump back on a live re-read, the hint where it fits

This commit is contained in:
maj00r
2026-11-11 18:04:43 +01:00
parent fa36284de1
commit cdc35c44e5
6 changed files with 53 additions and 25 deletions

View File

@@ -62,6 +62,8 @@ public sealed class HaslerTapeWindow : Window
private readonly Dictionary<string, BlackBoxWindow> _blackBoxes = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, BlackBoxWindow> _blackBoxes = new(StringComparer.OrdinalIgnoreCase);
private HashSet<string> _liveBlackBoxes = new(StringComparer.OrdinalIgnoreCase); private HashSet<string> _liveBlackBoxes = new(StringComparer.OrdinalIgnoreCase);
private IReadOnlyList<TapeSession> _sessions = Array.Empty<TapeSession>(); private IReadOnlyList<TapeSession> _sessions = Array.Empty<TapeSession>();
private readonly Dictionary<string, double> _wantedAt = new(StringComparer.OrdinalIgnoreCase);
private readonly Action<TapeSample> _picked;
private TapeFile? _shown; private TapeFile? _shown;
private string? _followed; private string? _followed;
@@ -89,8 +91,10 @@ public sealed class HaslerTapeWindow : Window
_scale.Value = DefaultKmScale; _scale.Value = DefaultKmScale;
_scale.PropertyChanged += (_, e) => _scale.PropertyChanged += (_, e) =>
{ {
if (e.Property == RangeBase.ValueProperty) if (e.Property != RangeBase.ValueProperty)
return;
_view.Scale = _scale.Value; _view.Scale = _scale.Value;
ShowMark(scrollTo: false);
}; };
_live.Content = App.Loc["HaslerLive"]; _live.Content = App.Loc["HaslerLive"];
ToolTip.SetShowOnDisabled(_live, true); ToolTip.SetShowOnDisabled(_live, true);
@@ -139,7 +143,8 @@ public sealed class HaslerTapeWindow : Window
_view = NewView(RecorderKind.Hasler); _view = NewView(RecorderKind.Hasler);
_view.Scale = DefaultKmScale; _view.Scale = DefaultKmScale;
_view.Hovered += ShowCursor; _view.Hovered += ShowCursor;
_view.Picked += PickOnTape; _picked = async sample => await PickOnTapeAsync(sample);
_view.Picked += _picked;
_scroll = new ScrollViewer _scroll = new ScrollViewer
{ {
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
@@ -288,15 +293,16 @@ public sealed class HaslerTapeWindow : Window
var old = _view; var old = _view;
old.Hovered -= ShowCursor; old.Hovered -= ShowCursor;
old.Picked -= PickOnTape; old.Picked -= _picked;
_view = NewView(Recorder); _view = NewView(Recorder);
_view.CopySettingsFrom(old); _view.CopySettingsFrom(old);
_view.Hovered += ShowCursor; _view.Hovered += ShowCursor;
_view.Picked += PickOnTape; _view.Picked += _picked;
_scroll.Content = _view; _scroll.Content = _view;
// The other view is not as tall; an offset kept from it would hide its top rows. // 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); _scroll.Offset = new Vector(_scroll.Offset.X, 0);
_cursor.Text = ""; _cursor.Text = "";
ShowMark(scrollTo: false);
} }
private async Task RefreshListAsync(bool toDriven = false) => private async Task RefreshListAsync(bool toDriven = false) =>
@@ -428,7 +434,7 @@ public sealed class HaslerTapeWindow : Window
if (_blackBoxes.TryGetValue(box.Path, out var open)) if (_blackBoxes.TryGetValue(box.Path, out var open))
{ {
if (at is { } moment) if (at is { } moment)
open.SeekTo(moment); SeekBlackBox(box, open, moment);
open.Activate(); open.Activate();
return; return;
} }
@@ -438,15 +444,30 @@ public sealed class HaslerTapeWindow : Window
Live = _liveBlackBoxes.Contains(box.Path) Live = _liveBlackBoxes.Contains(box.Path)
}; };
_blackBoxes[box.Path] = window; _blackBoxes[box.Path] = window;
window.TimeChanged += t => FollowBlackBox(box, t); if (at is { } first)
_wantedAt[box.Path] = first;
void Follow(double t) => FollowBlackBox(box, window, t);
window.TimeChanged += Follow;
window.Closed += (_, _) => window.Closed += (_, _) =>
{ {
window.TimeChanged -= Follow;
_blackBoxes.Remove(box.Path); _blackBoxes.Remove(box.Path);
_wantedAt.Remove(box.Path);
ShowMark(scrollTo: false); ShowMark(scrollTo: false);
}; };
window.Show(this); window.Show(this);
await window.OpenAsync(box.Path); await window.OpenAsync(box.Path);
if (at is { } t) // The last moment asked for while the file was read.
if (_wantedAt.Remove(box.Path, out double wanted))
window.SeekTo(wanted);
}
/// <summary>A window still reading its file has no moment to go to yet: it goes there once it has read it.</summary>
private void SeekBlackBox(BlackBoxRecording box, BlackBoxWindow window, double t)
{
if (window.Recording is null)
_wantedAt[box.Path] = t;
else
window.SeekTo(t); window.SeekTo(t);
} }
@@ -458,40 +479,45 @@ public sealed class HaslerTapeWindow : Window
/// <summary> /// <summary>
/// A click on the tape takes the black box of its session to the same moment, opening it if no window has it. /// 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. /// Both count seconds of the simulation from its start, so a tape's own time is the black box's. Also the click
/// that brings this window back from the black box's: going back to the tape is to look at another moment.
/// </summary> /// </summary>
private void PickOnTape(TapeSample sample) private async Task PickOnTapeAsync(TapeSample sample)
{ {
if (ShownSession is not { BlackBoxes.Count: > 0 } session) if (ShownSession is not { BlackBoxes.Count: > 0 } session)
return; return;
var open = session.BlackBoxes.Where(box => _blackBoxes.ContainsKey(box.Path)).ToList(); var open = session.BlackBoxes.Where(box => _blackBoxes.ContainsKey(box.Path)).ToList();
if (open.Count == 0) if (open.Count == 0)
{ {
_ = OpenBlackBoxAsync(session.BlackBoxes[0], sample.Time); await OpenBlackBoxAsync(session.BlackBoxes[0], sample.Time);
return; return;
} }
foreach (var box in open) foreach (var box in open)
_blackBoxes[box.Path].SeekTo(sample.Time); SeekBlackBox(box, _blackBoxes[box.Path], sample.Time);
} }
/// <summary>Where a black box of the session shown plays, on its tape, brought into view as it moves on.</summary> /// <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) private void FollowBlackBox(BlackBoxRecording box, BlackBoxWindow window, double t)
{ {
if (ShownSession?.BlackBoxes.Any(b => b.Path == box.Path) != true) if (!_blackBoxes.TryGetValue(box.Path, out var open) || open != window
|| ShownSession?.BlackBoxes.Any(b => b.Path == box.Path) != true)
return; return;
// Played, it moves every frame: the tape is drawn again only when the mark moves on it. // 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; double? x = _view.XAt(t), was = _view.MarkX;
if (x is { } now && was is { } before && Math.Abs(now - before) < 0.5) if (x == was || (x is { } now && was is { } before && Math.Abs(now - before) < 0.5))
return; return;
_view.Mark = t; _view.Mark = t;
BringMarkIntoView(); BringMarkIntoView();
} }
/// <summary>The mark of an open black box of the session shown, or none, on a tape just shown or changed.</summary> /// <summary>
/// The mark of an open black box of the session shown, or none: on a tape just shown, and on a view changed,
/// where a mark held back while it stood still on the old one would stand wrong.
/// </summary>
private void ShowMark(bool scrollTo) private void ShowMark(bool scrollTo)
{ {
var window = ShownSession?.BlackBoxes.Select(box => _blackBoxes.GetValueOrDefault(box.Path)) var window = ShownSession?.BlackBoxes.Select(box => _blackBoxes.GetValueOrDefault(box.Path))
.FirstOrDefault(w => w is not null); .FirstOrDefault(w => w?.Recording is not null);
_view.Mark = window?.CurrentTime; _view.Mark = window?.CurrentTime;
if (scrollTo) if (scrollTo)
BringMarkIntoView(); BringMarkIntoView();
@@ -567,6 +593,7 @@ public sealed class HaslerTapeWindow : Window
try try
{ {
var tape = await Task.Run(() => _tapes.Load(file)); var tape = await Task.Run(() => _tapes.Load(file));
bool another = file.Path != _shown?.Path;
_shown = file; _shown = file;
if (tape is null) if (tape is null)
{ {
@@ -577,7 +604,7 @@ public sealed class HaslerTapeWindow : Window
} }
_view.Tape = tape; _view.Tape = tape;
ShowMark(scrollTo: !scrollToEnd); ShowMark(scrollTo: another && !scrollToEnd);
_summary.Text = string.Format(App.Loc["HaslerSummary"], file.Vehicle, TapeFigures.Clock(tape.Duration), _summary.Text = string.Format(App.Loc["HaslerSummary"], file.Vehicle, TapeFigures.Clock(tape.Duration),
TapeFigures.Clock(tape.RunningTime), TapeFigures.Clock(tape.StandingTime), TapeFigures.Clock(tape.RunningTime), TapeFigures.Clock(tape.StandingTime),
tape.DistanceM / 1000.0, tape.MaxSpeedKmh, tape.AverageSpeedKmh, tape.DistanceM / 1000.0, tape.MaxSpeedKmh, tape.AverageSpeedKmh,
@@ -589,6 +616,8 @@ public sealed class HaslerTapeWindow : Window
// The log counts seconds from the start: an hour comes only from what the starter noted. // The log counts seconds from the start: an hour comes only from what the starter noted.
if (string.IsNullOrWhiteSpace(_startClock.Text)) if (string.IsNullOrWhiteSpace(_startClock.Text))
_summary.Text += " · " + App.Loc["HaslerClockUnknown"]; _summary.Text += " · " + App.Loc["HaslerClockUnknown"];
if (ShownSession is { BlackBoxes.Count: > 0 })
_summary.Text += " · " + App.Loc["HaslerClickBlackBox"];
if (scrollToEnd) if (scrollToEnd)
Dispatcher.UIThread.Post(() => _scroll.Offset = new Vector(double.MaxValue, _scroll.Offset.Y), Dispatcher.UIThread.Post(() => _scroll.Offset = new Vector(double.MaxValue, _scroll.Offset.Y),
DispatcherPriority.Background); DispatcherPriority.Background);
@@ -645,6 +674,7 @@ public sealed class HaslerTapeWindow : Window
_view.ByDistance = byDistance; _view.ByDistance = byDistance;
_scale.Value = byDistance ? DefaultKmScale : DefaultMinuteScale; _scale.Value = byDistance ? DefaultKmScale : DefaultMinuteScale;
_scale.Maximum = byDistance ? 600 : 300; _scale.Maximum = byDistance ? 600 : 300;
ShowMark(scrollTo: false);
} }
/// <summary>The whole tape, not just what is scrolled into view, drawn afresh at the chosen scale.</summary> /// <summary>The whole tape, not just what is scrolled into view, drawn afresh at the chosen scale.</summary>
@@ -710,8 +740,6 @@ public sealed class HaslerTapeWindow : Window
s.BrakePressBar, YesNo(s.Current), YesNo(s.SecurityAlarm)) s.BrakePressBar, YesNo(s.Current), YesNo(s.SecurityAlarm))
: string.Format(App.Loc["HaslerCursor"], time, s.DistanceM / 1000.0, s.SpeedKmh, s.BrakePressBar, : string.Format(App.Loc["HaslerCursor"], time, s.DistanceM / 1000.0, s.SpeedKmh, s.BrakePressBar,
YesNo(s.Current)); 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"]; private static string YesNo(bool on) => on ? App.Loc["HaslerYes"] : App.Loc["HaslerNo"];

View File

@@ -852,7 +852,7 @@
<String key="HaslerSessionRunning">běží</String> <String key="HaslerSessionRunning">běží</String>
<String key="HaslerBlackBox">Černá skříňka</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="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="HaslerClickBlackBox">klikněte na pásku: černá skříňka v tom okamžiku</String>
<String key="WarehouseSaved">Ve skladu: {0}</String> <String key="WarehouseSaved">Ve skladu: {0}</String>
<String key="WarehouseNothingToAdd">Vyberte soupravu s vozidly</String> <String key="WarehouseNothingToAdd">Vyberte soupravu s vozidly</String>
<String key="GfxGroupLevelTip">Úroveň jen pro tuto část grafiky</String> <String key="GfxGroupLevelTip">Úroveň jen pro tuto část grafiky</String>

View File

@@ -1081,7 +1081,7 @@
<String key="HaslerSessionRunning">running</String> <String key="HaslerSessionRunning">running</String>
<String key="HaslerBlackBox">Black box</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="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="HaslerClickBlackBox">click the tape: the black box at that moment</String>
<String key="WarehouseSaved">In the warehouse: {0}</String> <String key="WarehouseSaved">In the warehouse: {0}</String>
<String key="WarehouseNothingToAdd">Pick a consist with vehicles</String> <String key="WarehouseNothingToAdd">Pick a consist with vehicles</String>
<String key="GfxGroupLevelTip">Level for this part of the graphics only</String> <String key="GfxGroupLevelTip">Level for this part of the graphics only</String>

View File

@@ -852,7 +852,7 @@
<String key="HaslerSessionRunning">folyamatban</String> <String key="HaslerSessionRunning">folyamatban</String>
<String key="HaslerBlackBox">Fekete doboz</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="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="HaslerClickBlackBox">kattintson a szalagra: a fekete doboz abban a pillanatban</String>
<String key="WarehouseSaved">A raktárban: {0}</String> <String key="WarehouseSaved">A raktárban: {0}</String>
<String key="WarehouseNothingToAdd">Válassz járműveket tartalmazó szerelvényt</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> <String key="GfxGroupLevelTip">Csak a grafika ezen részének szintje</String>

View File

@@ -903,7 +903,7 @@
<String key="HaslerSessionRunning">trwa</String> <String key="HaslerSessionRunning">trwa</String>
<String key="HaslerBlackBox">Czarna skrzynka</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="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="HaslerClickBlackBox">kliknij taśmę: czarna skrzynka w tym momencie</String>
<String key="WarehouseSaved">W magazynie: {0}</String> <String key="WarehouseSaved">W magazynie: {0}</String>
<String key="WarehouseNothingToAdd">Wybierz skład z pojazdami</String> <String key="WarehouseNothingToAdd">Wybierz skład z pojazdami</String>
<String key="GfxGroupLevelTip">Poziom tylko tej części grafiki</String> <String key="GfxGroupLevelTip">Poziom tylko tej części grafiki</String>

View File

@@ -852,7 +852,7 @@
<String key="HaslerSessionRunning">идёт</String> <String key="HaslerSessionRunning">идёт</String>
<String key="HaslerBlackBox">Чёрный ящик</String> <String key="HaslerBlackBox">Чёрный ящик</String>
<String key="HaslerBlackBoxTip">Вся сессия на карте: езда по сравнению с ИИ, команды, светофоры, кабина</String> <String key="HaslerBlackBoxTip">Вся сессия на карте: езда по сравнению с ИИ, команды, светофоры, кабина</String>
<String key="HaslerClickBlackBox">щелчок: чёрный ящик в этот момент</String> <String key="HaslerClickBlackBox">щёлкните по ленте: чёрный ящик в этот момент</String>
<String key="WarehouseSaved">На складе: {0}</String> <String key="WarehouseSaved">На складе: {0}</String>
<String key="WarehouseNothingToAdd">Выберите состав с транспортом</String> <String key="WarehouseNothingToAdd">Выберите состав с транспортом</String>
<String key="GfxGroupLevelTip">Уровень только для этой части графики</String> <String key="GfxGroupLevelTip">Уровень только для этой части графики</String>