diff --git a/StarterNG/Views/Tapes/HaslerTapeWindow.cs b/StarterNG/Views/Tapes/HaslerTapeWindow.cs index e9ed6a4..90adf8a 100644 --- a/StarterNG/Views/Tapes/HaslerTapeWindow.cs +++ b/StarterNG/Views/Tapes/HaslerTapeWindow.cs @@ -62,6 +62,8 @@ public sealed class HaslerTapeWindow : Window private readonly Dictionary _blackBoxes = new(StringComparer.OrdinalIgnoreCase); private HashSet _liveBlackBoxes = new(StringComparer.OrdinalIgnoreCase); private IReadOnlyList _sessions = Array.Empty(); + private readonly Dictionary _wantedAt = new(StringComparer.OrdinalIgnoreCase); + private readonly Action _picked; private TapeFile? _shown; private string? _followed; @@ -89,8 +91,10 @@ public sealed class HaslerTapeWindow : Window _scale.Value = DefaultKmScale; _scale.PropertyChanged += (_, e) => { - if (e.Property == RangeBase.ValueProperty) - _view.Scale = _scale.Value; + if (e.Property != RangeBase.ValueProperty) + return; + _view.Scale = _scale.Value; + ShowMark(scrollTo: false); }; _live.Content = App.Loc["HaslerLive"]; ToolTip.SetShowOnDisabled(_live, true); @@ -139,7 +143,8 @@ public sealed class HaslerTapeWindow : Window _view = NewView(RecorderKind.Hasler); _view.Scale = DefaultKmScale; _view.Hovered += ShowCursor; - _view.Picked += PickOnTape; + _picked = async sample => await PickOnTapeAsync(sample); + _view.Picked += _picked; _scroll = new ScrollViewer { HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, @@ -288,15 +293,16 @@ public sealed class HaslerTapeWindow : Window var old = _view; old.Hovered -= ShowCursor; - old.Picked -= PickOnTape; + old.Picked -= _picked; _view = NewView(Recorder); _view.CopySettingsFrom(old); _view.Hovered += ShowCursor; - _view.Picked += PickOnTape; + _view.Picked += _picked; _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); _cursor.Text = ""; + ShowMark(scrollTo: 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 (at is { } moment) - open.SeekTo(moment); + SeekBlackBox(box, open, moment); open.Activate(); return; } @@ -438,15 +444,30 @@ public sealed class HaslerTapeWindow : Window Live = _liveBlackBoxes.Contains(box.Path) }; _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.TimeChanged -= Follow; _blackBoxes.Remove(box.Path); + _wantedAt.Remove(box.Path); ShowMark(scrollTo: false); }; window.Show(this); 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); + } + + /// A window still reading its file has no moment to go to yet: it goes there once it has read it. + private void SeekBlackBox(BlackBoxRecording box, BlackBoxWindow window, double t) + { + if (window.Recording is null) + _wantedAt[box.Path] = t; + else window.SeekTo(t); } @@ -458,40 +479,45 @@ public sealed class HaslerTapeWindow : Window /// /// 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. /// - private void PickOnTape(TapeSample sample) + private async Task PickOnTapeAsync(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); + await OpenBlackBoxAsync(session.BlackBoxes[0], sample.Time); return; } foreach (var box in open) - _blackBoxes[box.Path].SeekTo(sample.Time); + SeekBlackBox(box, _blackBoxes[box.Path], sample.Time); } /// Where a black box of the session shown plays, on its tape, brought into view as it moves on. - 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; // 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) + if (x == was || (x is { } now && was is { } before && Math.Abs(now - before) < 0.5)) return; _view.Mark = t; BringMarkIntoView(); } - /// The mark of an open black box of the session shown, or none, on a tape just shown or changed. + /// + /// 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. + /// private void ShowMark(bool scrollTo) { 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; if (scrollTo) BringMarkIntoView(); @@ -567,6 +593,7 @@ public sealed class HaslerTapeWindow : Window try { var tape = await Task.Run(() => _tapes.Load(file)); + bool another = file.Path != _shown?.Path; _shown = file; if (tape is null) { @@ -577,7 +604,7 @@ public sealed class HaslerTapeWindow : Window } _view.Tape = tape; - ShowMark(scrollTo: !scrollToEnd); + ShowMark(scrollTo: another && !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, @@ -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. if (string.IsNullOrWhiteSpace(_startClock.Text)) _summary.Text += " · " + App.Loc["HaslerClockUnknown"]; + if (ShownSession is { BlackBoxes.Count: > 0 }) + _summary.Text += " · " + App.Loc["HaslerClickBlackBox"]; if (scrollToEnd) Dispatcher.UIThread.Post(() => _scroll.Offset = new Vector(double.MaxValue, _scroll.Offset.Y), DispatcherPriority.Background); @@ -645,6 +674,7 @@ public sealed class HaslerTapeWindow : Window _view.ByDistance = byDistance; _scale.Value = byDistance ? DefaultKmScale : DefaultMinuteScale; _scale.Maximum = byDistance ? 600 : 300; + ShowMark(scrollTo: false); } /// The whole tape, not just what is scrolled into view, drawn afresh at the chosen scale. @@ -710,8 +740,6 @@ 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"]; diff --git a/StarterNG/startercfg/lang/cz.xml b/StarterNG/startercfg/lang/cz.xml index efda283..b692ad0 100644 --- a/StarterNG/startercfg/lang/cz.xml +++ b/StarterNG/startercfg/lang/cz.xml @@ -852,7 +852,7 @@ běží Černá skříňka Celá relace na mapě: jízda oproti AI, povely, návěstidla, kabina - klikněte: černá skříňka v tomto okamžiku + klikněte na pásku: černá skříňka v tom okamžiku Ve skladu: {0} Vyberte soupravu s vozidly Úroveň jen pro tuto část grafiky diff --git a/StarterNG/startercfg/lang/en.xml b/StarterNG/startercfg/lang/en.xml index e58483e..e2a479b 100644 --- a/StarterNG/startercfg/lang/en.xml +++ b/StarterNG/startercfg/lang/en.xml @@ -1081,7 +1081,7 @@ running Black box The whole session on a map: the driving against the AI, the commands, the signals, the cab - click: the black box at this moment + click the tape: the black box at that moment In the warehouse: {0} Pick a consist with vehicles Level for this part of the graphics only diff --git a/StarterNG/startercfg/lang/hu.xml b/StarterNG/startercfg/lang/hu.xml index 4c1d5a7..105376f 100644 --- a/StarterNG/startercfg/lang/hu.xml +++ b/StarterNG/startercfg/lang/hu.xml @@ -852,7 +852,7 @@ folyamatban Fekete doboz A teljes menet a térképen: vezetés az AI-hoz képest, parancsok, jelzők, vezetőfülke - kattintás: a fekete doboz ebben a pillanatban + kattintson a szalagra: a fekete doboz abban a pillanatban A raktárban: {0} Válassz járműveket tartalmazó szerelvényt Csak a grafika ezen részének szintje diff --git a/StarterNG/startercfg/lang/pl.xml b/StarterNG/startercfg/lang/pl.xml index 902329a..b1b1884 100644 --- a/StarterNG/startercfg/lang/pl.xml +++ b/StarterNG/startercfg/lang/pl.xml @@ -903,7 +903,7 @@ trwa Czarna skrzynka Cała sesja na mapie: jazda względem AI, komendy, sygnalizatory, kabina - kliknij: czarna skrzynka w tej chwili + kliknij taśmę: czarna skrzynka w tym momencie W magazynie: {0} Wybierz skład z pojazdami Poziom tylko tej części grafiki diff --git a/StarterNG/startercfg/lang/ru.xml b/StarterNG/startercfg/lang/ru.xml index 6eca9ac..00d9070 100644 --- a/StarterNG/startercfg/lang/ru.xml +++ b/StarterNG/startercfg/lang/ru.xml @@ -852,7 +852,7 @@ идёт Чёрный ящик Вся сессия на карте: езда по сравнению с ИИ, команды, светофоры, кабина - щелчок: чёрный ящик в этот момент + щёлкните по ленте: чёрный ящик в этот момент На складе: {0} Выберите состав с транспортом Уровень только для этой части графики