From 2af822436eddc20ecd994db86815a2aabe25bad3 Mon Sep 17 00:00:00 2001 From: leonarski_f Date: Mon, 22 Jun 2026 12:00:15 +0200 Subject: [PATCH] viewer: remove/clear runs, stable per-run colours, processing rate + ETA - "Remove result" toolbar action drops a reprocessing run: reader RemoveSnapshot (Original is protected; if the removed run was active the view falls back to Original), worker RemoveRun, and the table row is removed. - Opening a new file clears the jobs table (worker fileOpened -> clearJobs) to match the reader, which already resets snapshots on ReadFile. - Plot fixes: each run keeps a stable colour by its position (Original = blue), so colours no longer swap when the active run changes; the current-image marker is hidden from the legend (was the stray "[Image #N]" entry). - The status bar shows processing rate (Hz) and estimated remaining time while a job runs. Co-Authored-By: Claude Opus 4.8 --- reader/JFJochHDF5Reader.cpp | 12 ++++++ reader/JFJochHDF5Reader.h | 1 + viewer/JFJochImageReadingWorker.cpp | 11 +++++ viewer/JFJochImageReadingWorker.h | 2 + viewer/JFJochViewerDatasetInfo.cpp | 31 ++++++++++--- viewer/JFJochViewerWindow.cpp | 4 ++ viewer/charts/JFJochDatasetInfoChartView.cpp | 11 ++++- viewer/charts/JFJochDatasetInfoChartView.h | 7 ++- viewer/windows/JFJochProcessingJobsWindow.cpp | 43 +++++++++++++++++++ viewer/windows/JFJochProcessingJobsWindow.h | 5 +++ 10 files changed, 119 insertions(+), 8 deletions(-) diff --git a/reader/JFJochHDF5Reader.cpp b/reader/JFJochHDF5Reader.cpp index e5b60f71..3dfcd9cd 100644 --- a/reader/JFJochHDF5Reader.cpp +++ b/reader/JFJochHDF5Reader.cpp @@ -152,6 +152,18 @@ void JFJochHDF5Reader::RegisterSnapshot(const std::string &name, const std::stri snapshots_[name] = metadata; } +void JFJochHDF5Reader::RemoveSnapshot(const std::string &name) { + std::unique_lock ul(hdf5_mutex); + if (name == "Original") + return; // the original file metadata is always kept + snapshots_.erase(name); + if (active_snapshot_ == name) { + active_metadata_ = snapshots_.at("Original"); + active_snapshot_ = "Original"; + SetStartMessage(active_metadata_->Dataset()); + } +} + void JFJochHDF5Reader::SetActiveSnapshot(const std::string &name) { std::unique_lock ul(hdf5_mutex); diff --git a/reader/JFJochHDF5Reader.h b/reader/JFJochHDF5Reader.h index 1af4a689..985ace03 100644 --- a/reader/JFJochHDF5Reader.h +++ b/reader/JFJochHDF5Reader.h @@ -60,6 +60,7 @@ public: // Metadata snapshots over the same images. The original file is registered as "Original" on // ReadFile and is the initial active snapshot; reprocessing results can be added later. void RegisterSnapshot(const std::string &name, const std::string &master_path); + void RemoveSnapshot(const std::string &name); // "Original" cannot be removed void SetActiveSnapshot(const std::string &name); std::vector SnapshotNames() const; std::string ActiveSnapshot() const; diff --git a/viewer/JFJochImageReadingWorker.cpp b/viewer/JFJochImageReadingWorker.cpp index f7a1d18b..b9d6725a 100644 --- a/viewer/JFJochImageReadingWorker.cpp +++ b/viewer/JFJochImageReadingWorker.cpp @@ -294,6 +294,7 @@ void JFJochImageReadingWorker::LoadFile_i(const QString &filename, qint64 image_ if (!http_mode) run_labels_["Original"] = "Original"; EmitRuns_i(); + emit fileOpened(); // listeners (e.g. the jobs table) reset their per-file state auto end = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast(end - start).count(); @@ -987,6 +988,16 @@ void JFJochImageReadingWorker::RenameRun(QString id, QString label) { EmitRuns_i(); } +void JFJochImageReadingWorker::RemoveRun(QString id) { + QMutexLocker ul(&m); + if (http_mode || id == "Original") + return; + file_reader.RemoveSnapshot(id.toStdString()); + run_labels_.erase(id.toStdString()); + // Refresh the (possibly now-Original) active view; ActivateSnapshot_i also re-emits runsChanged. + ActivateSnapshot_i(QString::fromStdString(file_reader.ActiveSnapshot())); +} + void JFJochImageReadingWorker::RegisterProcessingSnapshot(QString id, QString label, QString master_path) { QMutexLocker ul(&m); if (http_mode) { diff --git a/viewer/JFJochImageReadingWorker.h b/viewer/JFJochImageReadingWorker.h index 9a3092ab..6cc50558 100644 --- a/viewer/JFJochImageReadingWorker.h +++ b/viewer/JFJochImageReadingWorker.h @@ -159,6 +159,7 @@ signals: void liveRateChanged(double hz); void snapshotsChanged(QStringList names, QString active); void runsChanged(QVector runs, QString active_id); + void fileOpened(); // a new file/stream was loaded - listeners should reset per-file state public: JFJochImageReadingWorker(const SpotFindingSettings &settings, const DiffractionExperiment& experiment, QObject *parent = nullptr); @@ -210,4 +211,5 @@ public slots: void RegisterProcessingSnapshot(QString id, QString label, QString master_path); void SetActiveSnapshot(QString name); void RenameRun(QString id, QString label); // change a run's display label (legend) + void RemoveRun(QString id); // drop a reprocessing snapshot (not "Original") }; diff --git a/viewer/JFJochViewerDatasetInfo.cpp b/viewer/JFJochViewerDatasetInfo.cpp index e4bb366d..cfb960d2 100644 --- a/viewer/JFJochViewerDatasetInfo.cpp +++ b/viewer/JFJochViewerDatasetInfo.cpp @@ -3,9 +3,24 @@ #include #include +#include #include "JFJochViewerDatasetInfo.h" +namespace { + // Stable colour per run by its position in the run list (Original is index 0 -> blue), so a + // run keeps its colour whether it is the active (primary) line or an overlay. + QColor RunColor(int index) { + static const QColor palette[] = { + QColor(0x1f, 0x77, 0xb4), QColor(0xff, 0x7f, 0x0e), QColor(0x2c, 0xa0, 0x2c), + QColor(0xd6, 0x27, 0x28), QColor(0x94, 0x67, 0xbd), QColor(0x8c, 0x56, 0x4b), + QColor(0xe3, 0x77, 0xc2), QColor(0x7f, 0x7f, 0x7f), + }; + constexpr int n = static_cast(sizeof(palette) / sizeof(palette[0])); + return palette[((index % n) + n) % n]; + } +} + JFJochViewerDatasetInfo::JFJochViewerDatasetInfo(QWidget *parent) : QWidget(parent) { auto layout = new QGridLayout(this); combo_box = new QComboBox(this); @@ -210,21 +225,27 @@ void JFJochViewerDatasetInfo::UpdatePlot() { bool one_over_d2 = false; std::vector data = ExtractMetric(*dataset, val, one_over_d2); - // One overlay line per other run, plus the in-progress live run. + // One overlay line per other run, plus the in-progress live run. Each run keeps a stable colour + // tied to its position in the list, so colours don't swap when the active run changes. std::vector overlays; QString primary_name; + QColor primary_color; + int run_index = 0; for (const auto &run: runs_) { - if (run.id == active_id_) { primary_name = run.label; continue; } + const QColor color = RunColor(run_index++); + if (run.id == active_id_) { primary_name = run.label; primary_color = color; continue; } if (!run.dataset || run.dataset == dataset) continue; // never draw the active run twice bool ignore = false; - overlays.push_back({run.label, ExtractMetric(*run.dataset, val, ignore)}); + overlays.push_back({run.label, ExtractMetric(*run.dataset, val, ignore), color}); } if (live_run_ && live_run_ != dataset) { bool ignore = false; - overlays.push_back({QStringLiteral("Live"), ExtractMetric(*live_run_, val, ignore)}); + overlays.push_back({QStringLiteral("Live"), ExtractMetric(*live_run_, val, ignore), + RunColor(static_cast(runs_.size()))}); } - chart_view->loadValues(data, image_number, one_over_d2, dataset.get(), primary_name, std::move(overlays)); + chart_view->loadValues(data, image_number, one_over_d2, dataset.get(), primary_name, + std::move(overlays), primary_color); if (dataset->experiment.GetGridScan()) { stack->setCurrentWidget(grid_scan_image); diff --git a/viewer/JFJochViewerWindow.cpp b/viewer/JFJochViewerWindow.cpp index 56a4ad1f..e9667d93 100644 --- a/viewer/JFJochViewerWindow.cpp +++ b/viewer/JFJochViewerWindow.cpp @@ -403,8 +403,12 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString statusbar, &JFJochViewerStatusBar::display); connect(processingJobsWindow, &JFJochProcessingJobsWindow::renameRun, reading_worker, &JFJochImageReadingWorker::RenameRun); + connect(processingJobsWindow, &JFJochProcessingJobsWindow::removeRun, + reading_worker, &JFJochImageReadingWorker::RemoveRun); connect(reading_worker, &JFJochImageReadingWorker::httpConnectionChanged, processingJobsWindow, &JFJochProcessingJobsWindow::onHttpConnectionChanged); + connect(reading_worker, &JFJochImageReadingWorker::fileOpened, + processingJobsWindow, &JFJochProcessingJobsWindow::clearJobs); connect(reading_worker, &JFJochImageReadingWorker::runsChanged, this, [this](QVector runs, QString active) { lastRuns = std::move(runs); diff --git a/viewer/charts/JFJochDatasetInfoChartView.cpp b/viewer/charts/JFJochDatasetInfoChartView.cpp index bd73362a..9dc5e79a 100644 --- a/viewer/charts/JFJochDatasetInfoChartView.cpp +++ b/viewer/charts/JFJochDatasetInfoChartView.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "JFJochDatasetInfoChartView.h" @@ -130,9 +131,10 @@ void JFJochDatasetInfoChartView::resetZoom() { void JFJochDatasetInfoChartView::loadValues(const std::vector &input, int64_t image, bool one_over_d2, const JFJochReaderDataset *dataset, const QString &primaryName, - std::vector overlays) { + std::vector overlays, const QColor &primaryColor) { m_yOneOverD = one_over_d2; primaryName_ = primaryName; + primary_color_ = primaryColor; // d -> 1/d^2 for resolution plots; identity otherwise. Applied to every series alike. auto transform = [one_over_d2](const std::vector &in, std::vector &out) { @@ -223,6 +225,8 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() { series = new QLineSeries(this); if (!primaryName_.isEmpty()) series->setName(primaryName_); + if (primary_color_.isValid()) + series->setColor(primary_color_); currentSeries = new QScatterSeries(this); double dispMin = std::numeric_limits::infinity(); @@ -236,6 +240,8 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() { for (const auto &ov: overlays_) { auto *line = new QLineSeries(this); line->setName(ov.name); + if (ov.color.isValid()) + line->setColor(ov.color); appendSeries(line, ov.values, dispMin, dispMax); overlayLines.push_back(line); } @@ -418,6 +424,9 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() { for (auto *ax: finalAxes) line->attachAxis(ax); } + // The current-image marker is not a run - keep it out of the legend. + for (auto *marker: chart()->legend()->markers(currentSeries)) + marker->setVisible(false); chart()->legend()->setVisible(!overlays_.empty()); chart()->legend()->setAlignment(Qt::AlignBottom); } diff --git a/viewer/charts/JFJochDatasetInfoChartView.h b/viewer/charts/JFJochDatasetInfoChartView.h index 62fa0ca6..7a1daf91 100644 --- a/viewer/charts/JFJochDatasetInfoChartView.h +++ b/viewer/charts/JFJochDatasetInfoChartView.h @@ -11,6 +11,7 @@ #include #include #include +#include #ifdef JFJOCH_USE_FFTW #include @@ -47,9 +48,10 @@ class JFJochDatasetInfoChartView : public QChartView { public: // One overlay line (a non-active run), drawn alongside the primary series with a legend entry. - struct NamedSeries { QString name; std::vector values; }; + struct NamedSeries { QString name; std::vector values; QColor color; }; private: std::vector overlays_; + QColor primary_color_; // colour of the active (primary) run, if assigned // Append (binned) finite points of vals to s, extending the [mn, mx] data range. void appendSeries(QLineSeries *s, const std::vector &vals, double &mn, double &mx) const; @@ -92,7 +94,8 @@ public: bool one_over_d2, const JFJochReaderDataset *dataset = nullptr, const QString &primaryName = QString(), - std::vector overlays = {}); + std::vector overlays = {}, + const QColor &primaryColor = QColor()); }; diff --git a/viewer/windows/JFJochProcessingJobsWindow.cpp b/viewer/windows/JFJochProcessingJobsWindow.cpp index 0546bb09..b38e1a80 100644 --- a/viewer/windows/JFJochProcessingJobsWindow.cpp +++ b/viewer/windows/JFJochProcessingJobsWindow.cpp @@ -51,6 +51,7 @@ JFJochProcessingJobsWindow::JFJochProcessingJobsWindow(JFJochImageReadingWorker toolbar_->setMovable(false); toolbar_->addAction("New job…", this, &JFJochProcessingJobsWindow::newJob); toolbar_->addAction("Cancel", this, &JFJochProcessingJobsWindow::cancelJob); + toolbar_->addAction("Remove result", this, &JFJochProcessingJobsWindow::removeResult); toolbar_->addSeparator(); toolbar_->addAction("View results", this, &JFJochProcessingJobsWindow::viewResults); toolbar_->addAction("Show original", this, &JFJochProcessingJobsWindow::showOriginal); @@ -260,6 +261,7 @@ void JFJochProcessingJobsWindow::newJob() { running_row_ = row; + job_timer_.start(); controller_->start(inputs.file, experiment, inputs.pixel_mask, config); emit writeStatusBar("Started processing job " + label); } @@ -271,6 +273,36 @@ void JFJochProcessingJobsWindow::cancelJob() { } } +void JFJochProcessingJobsWindow::removeResult() { + const int row = table_->currentRow(); + if (row < 0 || row >= static_cast(jobs_.size())) + return; + if (row == running_row_) { + QMessageBox::information(this, "Processing", "Cancel the running job before removing it."); + return; + } + emit removeRun(jobs_[row].id); // drops the snapshot from the reader (no-op for Original) + + QSignalBlocker block(table_); // row removal must not look like a rename + table_->removeRow(row); + jobs_.erase(jobs_.begin() + row); + if (running_row_ > row) + --running_row_; +} + +void JFJochProcessingJobsWindow::clearJobs() { + if (controller_->running()) + controller_->cancel(); + emit liveDataset(nullptr); + + QSignalBlocker block(table_); + table_->setRowCount(0); + jobs_.clear(); + job_counter_ = 0; + running_row_ = -1; + running_bar_ = nullptr; +} + void JFJochProcessingJobsWindow::viewResults() { const int row = table_->currentRow(); if (row < 0 || row >= static_cast(jobs_.size())) @@ -307,6 +339,17 @@ void JFJochProcessingJobsWindow::onProgress(quint64 done, quint64 total) { running_bar_->setRange(0, static_cast(total)); running_bar_->setValue(static_cast(done)); running_bar_->setFormat("%v / %m"); + + // Processing rate and ETA in the status bar. + if (job_timer_.isValid() && done > 0) { + const double secs = job_timer_.elapsed() / 1000.0; + if (secs > 0.0) { + const double hz = done / secs; + const double remaining = hz > 0.0 ? (total - done) / hz : 0.0; + emit writeStatusBar(QStringLiteral("Processing %1 Hz — ~%2 s remaining") + .arg(hz, 0, 'f', 1).arg(qRound(remaining))); + } + } } void JFJochProcessingJobsWindow::onFinished(ProcessResult result) { diff --git a/viewer/windows/JFJochProcessingJobsWindow.h b/viewer/windows/JFJochProcessingJobsWindow.h index c8f0eaa7..55d5b56b 100644 --- a/viewer/windows/JFJochProcessingJobsWindow.h +++ b/viewer/windows/JFJochProcessingJobsWindow.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -29,16 +30,19 @@ signals: void registerSnapshot(QString id, QString label, QString master_path); void activateSnapshot(QString id); void renameRun(QString id, QString label); + void removeRun(QString id); void writeStatusBar(QString message, int timeout_ms = 0); // Live per-image results while a job runs, for the dataset-info plots (nullptr clears it). void liveDataset(std::shared_ptr dataset); public slots: void onHttpConnectionChanged(bool connected, QString addr); + void clearJobs(); // reset the table on a new file private slots: void newJob(); void cancelJob(); + void removeResult(); void viewResults(); void showOriginal(); void onPhase(QString phase); @@ -74,6 +78,7 @@ private: QStackedWidget *stack_; // page 0: jobs table, page 1: HTTP-mode message QTableWidget *table_; QProgressBar *running_bar_ = nullptr; // lives in the running row's Status cell + QElapsedTimer job_timer_; // running job wall-clock, for rate + ETA int running_row_ = -1; int job_counter_ = 0; std::vector jobs_;