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 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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<std::string> SnapshotNames() const;
|
||||
std::string ActiveSnapshot() const;
|
||||
|
||||
@@ -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<std::chrono::milliseconds>(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) {
|
||||
|
||||
@@ -159,6 +159,7 @@ signals:
|
||||
void liveRateChanged(double hz);
|
||||
void snapshotsChanged(QStringList names, QString active);
|
||||
void runsChanged(QVector<RunData> 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")
|
||||
};
|
||||
|
||||
@@ -3,9 +3,24 @@
|
||||
|
||||
#include <QGridLayout>
|
||||
#include <QPushButton>
|
||||
#include <QColor>
|
||||
|
||||
#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<int>(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<float> 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<JFJochDatasetInfoChartView::NamedSeries> 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<int>(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);
|
||||
|
||||
@@ -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<RunData> runs, QString active) {
|
||||
lastRuns = std::move(runs);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QCategoryAxis>
|
||||
#include <QtCharts/QLegendMarker>
|
||||
|
||||
#include "JFJochDatasetInfoChartView.h"
|
||||
|
||||
@@ -130,9 +131,10 @@ void JFJochDatasetInfoChartView::resetZoom() {
|
||||
|
||||
void JFJochDatasetInfoChartView::loadValues(const std::vector<float> &input, int64_t image, bool one_over_d2,
|
||||
const JFJochReaderDataset *dataset, const QString &primaryName,
|
||||
std::vector<NamedSeries> overlays) {
|
||||
std::vector<NamedSeries> 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<float> &in, std::vector<float> &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<double>::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);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <QGraphicsLineItem>
|
||||
#include <QTimer>
|
||||
#include <QPointer>
|
||||
#include <QColor>
|
||||
|
||||
#ifdef JFJOCH_USE_FFTW
|
||||
#include <fftw3.h>
|
||||
@@ -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<float> values; };
|
||||
struct NamedSeries { QString name; std::vector<float> values; QColor color; };
|
||||
private:
|
||||
std::vector<NamedSeries> 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<float> &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<NamedSeries> overlays = {});
|
||||
std::vector<NamedSeries> overlays = {},
|
||||
const QColor &primaryColor = QColor());
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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<int>(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<int>(jobs_.size()))
|
||||
@@ -307,6 +339,17 @@ void JFJochProcessingJobsWindow::onProgress(quint64 done, quint64 total) {
|
||||
running_bar_->setRange(0, static_cast<int>(total));
|
||||
running_bar_->setValue(static_cast<int>(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) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
#include <vector>
|
||||
@@ -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<const JFJochReaderDataset> 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<JobInfo> jobs_;
|
||||
|
||||
Reference in New Issue
Block a user