diff --git a/reader/JFJochHDF5Reader.cpp b/reader/JFJochHDF5Reader.cpp index de90417d..e5b60f71 100644 --- a/reader/JFJochHDF5Reader.cpp +++ b/reader/JFJochHDF5Reader.cpp @@ -178,3 +178,17 @@ std::string JFJochHDF5Reader::ActiveSnapshot() const { std::unique_lock ul(hdf5_mutex); return active_snapshot_; } + +std::vector>> +JFJochHDF5Reader::AllSnapshotDatasets() const { + std::unique_lock ul(hdf5_mutex); + std::vector>> out; + out.reserve(snapshots_.size()); + // "Original" first, then the rest, so overlay colours stay stable across updates. + if (auto it = snapshots_.find("Original"); it != snapshots_.end()) + out.emplace_back(it->first, it->second->Dataset()); + for (const auto &[name, source]: snapshots_) + if (name != "Original") + out.emplace_back(name, source->Dataset()); + return out; +} diff --git a/reader/JFJochHDF5Reader.h b/reader/JFJochHDF5Reader.h index 3c69cc0e..1af4a689 100644 --- a/reader/JFJochHDF5Reader.h +++ b/reader/JFJochHDF5Reader.h @@ -63,4 +63,7 @@ public: void SetActiveSnapshot(const std::string &name); std::vector SnapshotNames() const; std::string ActiveSnapshot() const; + + // All snapshot datasets (name -> dataset), for overlaying every run's plots at once. + std::vector>> AllSnapshotDatasets() const; }; diff --git a/viewer/CMakeLists.txt b/viewer/CMakeLists.txt index 8bddcb3f..2d41295f 100644 --- a/viewer/CMakeLists.txt +++ b/viewer/CMakeLists.txt @@ -29,6 +29,7 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView image_viewer/JFJochSimpleImage.h JFJochImageReadingWorker.cpp JFJochImageReadingWorker.h + RunData.h JFJochProcessController.cpp JFJochProcessController.h JFJochViewerDatasetInfo.cpp diff --git a/viewer/JFJochImageReadingWorker.cpp b/viewer/JFJochImageReadingWorker.cpp index 7ed4f76f..f7a1d18b 100644 --- a/viewer/JFJochImageReadingWorker.cpp +++ b/viewer/JFJochImageReadingWorker.cpp @@ -97,6 +97,8 @@ JFJochImageReadingWorker::JFJochImageReadingWorker(const SpotFindingSettings &se qRegisterMetaType("ROIDefinition"); qRegisterMetaType("BraggIntegrationSettings"); qRegisterMetaType("ScalingSettings"); + qRegisterMetaType("RunData"); + qRegisterMetaType>("QVector"); spot_finding_settings = settings; indexing = std::make_unique(indexing_settings); @@ -287,6 +289,12 @@ void JFJochImageReadingWorker::LoadFile_i(const QString &filename, qint64 image_ emit datasetLoaded(dataset); + // Reset the run collection for the freshly opened file (file mode only has snapshots). + run_labels_.clear(); + if (!http_mode) + run_labels_["Original"] = "Original"; + EmitRuns_i(); + auto end = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast(end - start).count(); logger.Info(fmt::format("Loaded file {} in {} ms", filename.toStdString(), duration)); @@ -948,6 +956,7 @@ void JFJochImageReadingWorker::ActivateSnapshot_i(const QString &name) { for (const auto &n: file_reader.SnapshotNames()) names << QString::fromStdString(n); emit snapshotsChanged(names, QString::fromStdString(file_reader.ActiveSnapshot())); + EmitRuns_i(); if (current_image.has_value()) { // A snapshot carries its own per-image results; just re-read the image against the new @@ -959,17 +968,37 @@ void JFJochImageReadingWorker::ActivateSnapshot_i(const QString &name) { } } -void JFJochImageReadingWorker::RegisterProcessingSnapshot(QString name, QString master_path) { +void JFJochImageReadingWorker::EmitRuns_i() { + // Assumes m locked! Build the run list (every snapshot dataset + its display label). + QVector runs; + if (!http_mode) { + for (const auto &[id, dataset]: file_reader.AllSnapshotDatasets()) { + const auto it = run_labels_.find(id); + const QString label = (it != run_labels_.end()) ? it->second : QString::fromStdString(id); + runs.push_back(RunData{QString::fromStdString(id), label, dataset}); + } + } + emit runsChanged(runs, http_mode ? QString() : QString::fromStdString(file_reader.ActiveSnapshot())); +} + +void JFJochImageReadingWorker::RenameRun(QString id, QString label) { + QMutexLocker ul(&m); + run_labels_[id.toStdString()] = label; + EmitRuns_i(); +} + +void JFJochImageReadingWorker::RegisterProcessingSnapshot(QString id, QString label, QString master_path) { QMutexLocker ul(&m); if (http_mode) { logger.Error("Processing snapshots are only available for files"); return; } try { - file_reader.RegisterSnapshot(name.toStdString(), master_path.toStdString()); - ActivateSnapshot_i(name); + file_reader.RegisterSnapshot(id.toStdString(), master_path.toStdString()); + run_labels_[id.toStdString()] = label; + ActivateSnapshot_i(id); // activates + emits datasetLoaded / snapshotsChanged / runsChanged } catch (const std::exception &e) { - logger.Error("Failed to register snapshot {}: {}", name.toStdString(), e.what()); + logger.Error("Failed to register snapshot {}: {}", id.toStdString(), e.what()); emit fileLoadError("Snapshot error", QString::fromStdString(e.what())); } } diff --git a/viewer/JFJochImageReadingWorker.h b/viewer/JFJochImageReadingWorker.h index beadf56c..9a3092ab 100644 --- a/viewer/JFJochImageReadingWorker.h +++ b/viewer/JFJochImageReadingWorker.h @@ -46,6 +46,8 @@ struct ReprocessingInputs { SpotFindingSettings spot_finding; }; +#include "RunData.h" + class JFJochImageReadingWorker : public QObject { Q_OBJECT @@ -69,6 +71,7 @@ private: // Once the user edits ROIs they override whatever the file carried, for this and // every subsequently loaded image, until a new file is opened. std::optional roi_override_; + std::map run_labels_; // snapshot id -> editable display label IndexingSettings indexing_settings; AzimuthalIntegrationSettings azint_settings; BraggIntegrationSettings bragg_settings; @@ -138,6 +141,7 @@ private: void ApplyROIOverrideToImage_i(); // rewrite current_image_ptr's dataset with roi_override_ void setAutoLoadMode_i(AutoloadMode mode); void ActivateSnapshot_i(const QString &name); + void EmitRuns_i(); // emit runsChanged with every snapshot dataset + its label signals: void datasetLoaded(std::shared_ptr); @@ -154,6 +158,7 @@ signals: void httpConnectionChanged(bool connected, QString addr); void liveRateChanged(double hz); void snapshotsChanged(QStringList names, QString active); + void runsChanged(QVector runs, QString active_id); public: JFJochImageReadingWorker(const SpotFindingSettings &settings, const DiffractionExperiment& experiment, QObject *parent = nullptr); @@ -202,6 +207,7 @@ public slots: // Register a reprocessing result (_process.h5) as a metadata snapshot over the same images and // make it the active dataset; SetActiveSnapshot switches between registered snapshots. - void RegisterProcessingSnapshot(QString name, QString master_path); + 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) }; diff --git a/viewer/JFJochViewerDatasetInfo.cpp b/viewer/JFJochViewerDatasetInfo.cpp index 9dfd709a..e4bb366d 100644 --- a/viewer/JFJochViewerDatasetInfo.cpp +++ b/viewer/JFJochViewerDatasetInfo.cpp @@ -83,7 +83,20 @@ void JFJochViewerDatasetInfo::UpdateLabels() { combo_box->addItem("Spot count (ice rings)", 3); combo_box->addItem("Spot count (low res.)", 4); - if (!dataset->indexing_result.empty()) { + // Offer indexing/scaling metrics if the active run, any overlay run, or the live run has them. + bool has_indexing = !dataset->indexing_result.empty(); + bool has_scale = !dataset->image_scale_factor.empty(); + for (const auto &r: runs_) + if (r.dataset) { + has_indexing = has_indexing || !r.dataset->indexing_result.empty(); + has_scale = has_scale || !r.dataset->image_scale_factor.empty(); + } + if (live_run_) { + has_indexing = has_indexing || !live_run_->indexing_result.empty(); + has_scale = has_scale || !live_run_->image_scale_factor.empty(); + } + + if (has_indexing) { combo_box->insertSeparator(1000); combo_box->addItem("Indexing result", 5); combo_box->addItem("Profile radius", 6); @@ -93,7 +106,7 @@ void JFJochViewerDatasetInfo::UpdateLabels() { combo_box->addItem("Indexing lattice count", 13); } - if (!dataset->image_scale_factor.empty()) { + if (has_scale) { combo_box->insertSeparator(1000); combo_box->addItem("Scale factor", 10); combo_box->addItem("CC", 11); @@ -122,7 +135,7 @@ void JFJochViewerDatasetInfo::datasetLoaded(std::shared_ptrsetCurrentIndex(0); UpdatePlot(); } else { - chart_view->loadValues({}, 0, false); + chart_view->loadValues({}, 0, false); grid_scan_image->clear(); UpdateLabels(); } @@ -140,6 +153,46 @@ void JFJochViewerDatasetInfo::imageSelectedInChart(int64_t number) { emit imageSelected(number, 1); } +std::vector JFJochViewerDatasetInfo::ExtractMetric(const JFJochReaderDataset &ds, int val, + bool &one_over_d2) const { + one_over_d2 = false; + std::vector data; + if (val == 0) data = ds.bkg_estimate; + else if (val == 1) data = ds.spot_count; + else if (val == 2) data = ds.spot_count_indexed; + else if (val == 3) data = ds.spot_count_ice_rings; + else if (val == 4) data = ds.spot_count_low_res; + else if (val == 5) data = ds.indexing_result; + else if (val == 6) data = ds.profile_radius; + else if (val == 7) { data = ds.resolution_estimate; one_over_d2 = true; } + else if (val == 8) data = ds.b_factor; + else if (val == 9) data = ds.mosaicity_deg; + else if (val == 10) data = ds.image_scale_factor; + else if (val == 11) data = ds.image_scale_cc; + else if (val == 12) data = ds.integrated_reflections; + else if (val == 13) data = ds.indexing_lattice_count; + else if (val >= 100) { + const int roi_index = (val - 100) / 4; + if (val % 4 == 0) { + if (roi_index < ds.roi_sum.size() && ds.roi_sum.size() == ds.roi_npixel.size()) + for (int i = 0; i < ds.roi_sum[roi_index].size(); i++) + data.push_back(static_cast(ds.roi_sum[roi_index][i]) + / static_cast(ds.roi_npixel[roi_index][i])); + } else if (val % 4 == 1) { + if (roi_index < ds.roi_sum.size()) { + data.reserve(ds.roi_sum[roi_index].size()); + for (auto &v: ds.roi_sum[roi_index]) + data.push_back(v); + } + } else if (val % 4 == 2) { + if (roi_index < ds.roi_x.size()) data = ds.roi_x[roi_index]; + } else if (val % 4 == 3) { + if (roi_index < ds.roi_y.size()) data = ds.roi_y[roi_index]; + } + } + return data; +} + void JFJochViewerDatasetInfo::UpdatePlot() { int index = combo_box->currentIndex(); if (combo_box->count() == 0 || index < 0) @@ -147,83 +200,58 @@ void JFJochViewerDatasetInfo::UpdatePlot() { int val = combo_box->itemData(index).toInt(); - if (dataset) { - int64_t image_number = 0; - if (image) - image_number = image->ImageData().number; + if (!dataset) { + grid_button->setEnabled(false); + return; + } - bool one_over_d2 = false; - std::vector data; - if (val == 0) - data = dataset->bkg_estimate; - else if (val == 1) - data = dataset->spot_count; - else if (val == 2) - data = dataset->spot_count_indexed; - else if (val == 3) - data = dataset->spot_count_ice_rings; - else if (val == 4) - data = dataset->spot_count_low_res; - else if (val == 5) - data = dataset->indexing_result; - else if (val == 6) - data = dataset->profile_radius; - else if (val == 7) { - data = dataset->resolution_estimate; - one_over_d2 = true; - } else if (val == 8) { - data = dataset->b_factor; - } else if (val == 9) { - data = dataset->mosaicity_deg; - } else if (val == 10) { - data = dataset->image_scale_factor; - } else if (val == 11) { - data = dataset->image_scale_cc; - } else if (val == 12) { - data = dataset->integrated_reflections; - } else if (val == 13) { - data = dataset->indexing_lattice_count; - } else if (val >= 100) { - int roi_index = (val - 100) / 4; + const int64_t image_number = image ? image->ImageData().number : 0; - if (val % 4 == 0) { - if (roi_index < dataset->roi_sum.size() && dataset->roi_sum.size() == dataset->roi_npixel.size()) { - for(int i = 0; i < dataset->roi_sum[roi_index].size(); i++) { - data.push_back(static_cast(dataset->roi_sum[roi_index][i]) - / static_cast(dataset->roi_npixel[roi_index][i])); - } - } - } else if (val % 4 == 1) { - if (roi_index < dataset->roi_sum.size()) { - data.reserve(dataset->roi_sum[roi_index].size()); - for (auto &v: dataset->roi_sum[roi_index]) - data.push_back(v); - } - } else if (val % 4 == 2) { - if (roi_index < dataset->roi_x.size()) - data = dataset->roi_x[roi_index]; - } else if (val % 4 == 3) { - if (roi_index < dataset->roi_y.size()) - data = dataset->roi_y[roi_index]; - } - } + bool one_over_d2 = false; + std::vector data = ExtractMetric(*dataset, val, one_over_d2); - chart_view->loadValues(data, image_number, one_over_d2, dataset.get()); - if (dataset->experiment.GetGridScan()) { - stack->setCurrentWidget(grid_scan_image); - grid_scan_image->loadData(data, dataset->experiment.GetGridScan().value(), one_over_d2); - grid_button->setEnabled(true); - grid_button->setChecked(true); - } else { - grid_scan_image->clear(); - stack->setCurrentWidget(chart_view); - grid_button->setEnabled(false); - } + // One overlay line per other run, plus the in-progress live run. + std::vector overlays; + QString primary_name; + for (const auto &run: runs_) { + if (run.id == active_id_) { primary_name = run.label; 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)}); + } + if (live_run_ && live_run_ != dataset) { + bool ignore = false; + overlays.push_back({QStringLiteral("Live"), ExtractMetric(*live_run_, val, ignore)}); + } + + chart_view->loadValues(data, image_number, one_over_d2, dataset.get(), primary_name, std::move(overlays)); + + if (dataset->experiment.GetGridScan()) { + stack->setCurrentWidget(grid_scan_image); + grid_scan_image->loadData(data, dataset->experiment.GetGridScan().value(), one_over_d2); + grid_button->setEnabled(true); + grid_button->setChecked(true); } else { + grid_scan_image->clear(); + stack->setCurrentWidget(chart_view); grid_button->setEnabled(false); } } +void JFJochViewerDatasetInfo::runsChanged(QVector runs, QString active_id) { + runs_ = std::move(runs); + active_id_ = std::move(active_id); + UpdateLabels(); + if (last_selection < combo_box->count()) + combo_box->setCurrentIndex(last_selection); + UpdatePlot(); +} + +void JFJochViewerDatasetInfo::liveRunUpdated(std::shared_ptr in_dataset) { + live_run_ = std::move(in_dataset); + UpdatePlot(); // lightweight refresh; no combo rebuild on every live tick +} + void JFJochViewerDatasetInfo::comboBoxSelected(int index) { UpdatePlot(); } diff --git a/viewer/JFJochViewerDatasetInfo.h b/viewer/JFJochViewerDatasetInfo.h index f6eb8be5..1e84f557 100644 --- a/viewer/JFJochViewerDatasetInfo.h +++ b/viewer/JFJochViewerDatasetInfo.h @@ -10,21 +10,28 @@ #include "charts/JFJochDatasetInfoChartView.h" #include "../reader/JFJochReader.h" #include "image_viewer/JFJochGridScanImage.h" +#include "RunData.h" class JFJochViewerDatasetInfo : public QWidget { Q_OBJECT QComboBox *combo_box; JFJochDatasetInfoChartView *chart_view; const std::vector *GetDataset(); - std::shared_ptr dataset; + std::shared_ptr dataset; // the active (primary) run std::shared_ptr image; JFJochGridScanImage *grid_scan_image = nullptr; + QVector runs_; // all runs, for overlay lines + QString active_id_; // which run is primary + std::shared_ptr live_run_; // in-progress reprocessing, if any + QStackedWidget *stack = nullptr; QPushButton *grid_button = nullptr; int last_selection; void UpdatePlot(); + // Pull the metric selected by combo value `val` out of one run's dataset. + std::vector ExtractMetric(const JFJochReaderDataset &ds, int val, bool &one_over_d2) const; void UpdateLabels(); signals: @@ -41,6 +48,10 @@ public slots: void datasetLoaded(std::shared_ptr dataset); void imageLoaded(std::shared_ptr image); void setColorMap(int color_map); + // All runs (original + reprocessing snapshots) and which one is active/primary. + void runsChanged(QVector runs, QString active_id); + // Live results of the in-progress reprocessing run (updated as it goes), or nullptr to clear. + void liveRunUpdated(std::shared_ptr dataset); }; diff --git a/viewer/JFJochViewerWindow.cpp b/viewer/JFJochViewerWindow.cpp index 9c5491cc..56a4ad1f 100644 --- a/viewer/JFJochViewerWindow.cpp +++ b/viewer/JFJochViewerWindow.cpp @@ -401,8 +401,15 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString reading_worker, &JFJochImageReadingWorker::SetActiveSnapshot); connect(processingJobsWindow, &JFJochProcessingJobsWindow::writeStatusBar, statusbar, &JFJochViewerStatusBar::display); + connect(processingJobsWindow, &JFJochProcessingJobsWindow::renameRun, + reading_worker, &JFJochImageReadingWorker::RenameRun); connect(reading_worker, &JFJochImageReadingWorker::httpConnectionChanged, processingJobsWindow, &JFJochProcessingJobsWindow::onHttpConnectionChanged); + connect(reading_worker, &JFJochImageReadingWorker::runsChanged, this, + [this](QVector runs, QString active) { + lastRuns = std::move(runs); + lastActiveRunId = std::move(active); + }); // Dock the processing panel in the bottom-right corner, next to the dataset-info plots. auto processingDock = new QDockWidget("Processing", this); @@ -442,6 +449,7 @@ void JFJochViewerWindow::NewDatasetInfo() { auto info = new JFJochViewerDatasetInfo(this); info->datasetLoaded(lastDataset); info->imageLoaded(lastImage); + info->runsChanged(lastRuns, lastActiveRunId); auto dock = new QDockWidget(QString("Dataset info"), this); dock->setAllowedAreas(Qt::BottomDockWidgetArea); @@ -458,9 +466,12 @@ void JFJochViewerWindow::NewDatasetInfo() { info, &JFJochViewerDatasetInfo::datasetLoaded); connect(reading_worker, &JFJochImageReadingWorker::imageLoaded, info, &JFJochViewerDatasetInfo::imageLoaded); - // Live processing results: refresh the plots while a job runs. + // All runs (original + reprocessing snapshots) overlay as separate lines. + connect(reading_worker, &JFJochImageReadingWorker::runsChanged, + info, &JFJochViewerDatasetInfo::runsChanged); + // Live processing results: the in-progress run as its own overlay line. connect(processingJobsWindow, &JFJochProcessingJobsWindow::liveDataset, - info, &JFJochViewerDatasetInfo::datasetLoaded); + info, &JFJochViewerDatasetInfo::liveRunUpdated); connect(info, &JFJochViewerDatasetInfo::imageSelected, reading_worker, &JFJochImageReadingWorker::LoadImage); connect(toolBarDisplay, &JFJochViewerToolbarDisplay::colorMapChanged, diff --git a/viewer/JFJochViewerWindow.h b/viewer/JFJochViewerWindow.h index 38d5153a..c1ce10e5 100644 --- a/viewer/JFJochViewerWindow.h +++ b/viewer/JFJochViewerWindow.h @@ -32,6 +32,8 @@ private: std::shared_ptr lastDataset; // added std::shared_ptr lastImage; // added + QVector lastRuns; // for dataset-info docks opened later + QString lastActiveRunId; QProgressDialog *retryDialog = nullptr; diff --git a/viewer/RunData.h b/viewer/RunData.h new file mode 100644 index 00000000..1731832e --- /dev/null +++ b/viewer/RunData.h @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include +#include +#include +#include + +#include "../reader/JFJochReaderDataset.h" + +// One processing run (the original file or a reprocessing snapshot), for overlaying its plots. +// id is the stable reader-snapshot key; label is the editable display name shown in the legend. +struct RunData { + QString id; + QString label; + std::shared_ptr dataset; +}; + +Q_DECLARE_METATYPE(RunData) +Q_DECLARE_METATYPE(QVector) diff --git a/viewer/charts/JFJochDatasetInfoChartView.cpp b/viewer/charts/JFJochDatasetInfoChartView.cpp index c1f33399..bd73362a 100644 --- a/viewer/charts/JFJochDatasetInfoChartView.cpp +++ b/viewer/charts/JFJochDatasetInfoChartView.cpp @@ -128,6 +128,71 @@ void JFJochDatasetInfoChartView::resetZoom() { chart()->zoomReset(); } +void JFJochDatasetInfoChartView::loadValues(const std::vector &input, int64_t image, bool one_over_d2, + const JFJochReaderDataset *dataset, const QString &primaryName, + std::vector overlays) { + m_yOneOverD = one_over_d2; + primaryName_ = primaryName; + + // 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) { + out.resize(in.size()); + for (size_t i = 0; i < in.size(); i++) { + if (one_over_d2) { + const float d = in[i]; + out[i] = std::isfinite(d) ? 1.0f / (d * d) : 0.0f; + } else + out[i] = in[i]; + } + }; + + transform(input, values); + overlays_ = std::move(overlays); + for (auto &ov: overlays_) + transform(ov.values, ov.values); // in-place + + if (dataset != nullptr) { + goniometer_axis = dataset->experiment.GetGoniometer(); + image_time_us = dataset->experiment.GetImageTime().count(); + } else { + goniometer_axis = {}; + image_time_us = {}; + } + + curr_image = image; + updateChart(); +} + +void JFJochDatasetInfoChartView::appendSeries(QLineSeries *s, const std::vector &vals, + double &mn, double &mx) const { + if (binning == 1) { + for (int i = 0; i < static_cast(vals.size()); i++) { + const double v = vals[static_cast(i)]; + if (!std::isfinite(v)) continue; + s->append(i, v); + mn = std::min(mn, v); + mx = std::max(mx, v); + } + } else { + for (int i = 0; i < static_cast(vals.size() / static_cast(binning)); i++) { + double tmp = 0.0; + int64_t count = 0; + for (int b = 0; b < binning; b++) { + const int64_t idx = static_cast(i) * binning + b; + if (idx >= static_cast(vals.size())) break; + const double v = vals[static_cast(idx)]; + if (std::isfinite(v)) { tmp += v; count++; } + } + if (count > 0) { + const double mean = tmp / static_cast(count); + s->append((i + 0.5) * binning, mean); + mn = std::min(mn, mean); + mx = std::max(mx, mean); + } + } + } +} + void JFJochDatasetInfoChartView::updateChart() { // Important: drop any stale QObject pointers BEFORE rebuilding the chart. series = nullptr; @@ -156,44 +221,23 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() { // At least one full point series = new QLineSeries(this); + if (!primaryName_.isEmpty()) + series->setName(primaryName_); currentSeries = new QScatterSeries(this); double dispMin = std::numeric_limits::infinity(); double dispMax = -std::numeric_limits::infinity(); - if (binning == 1) { - for (int i = 0; i < static_cast(values.size()); i++) { - if (!std::isfinite(values[static_cast(i)])) continue; - const double disp = values[static_cast(i)]; - if (!std::isfinite(disp)) continue; - series->append(i, disp); - if (disp < dispMin) dispMin = disp; - if (disp > dispMax) dispMax = disp; - } - } else { - for (int i = 0; i < static_cast(values.size() / static_cast(binning)); i++) { - double tmp = 0.0; - int64_t count = 0; - for (int b = 0; b < binning; b++) { - const int64_t idx = static_cast(i) * binning + b; - if (idx >= static_cast(values.size())) - break; - const double v = values[static_cast(idx)]; - if (std::isfinite(v)) { - tmp += v; - count++; - } - } - if (count > 0) { - const double mean = tmp / static_cast(count); - const double disp = mean; - if (std::isfinite(disp)) { - series->append((i + 0.5) * binning, disp); - if (disp < dispMin) dispMin = disp; - if (disp > dispMax) dispMax = disp; - } - } - } + appendSeries(series, values, dispMin, dispMax); + + // Overlay runs share the primary's axes and range; build them now so the Y range fits all. + std::vector overlayLines; + overlayLines.reserve(overlays_.size()); + for (const auto &ov: overlays_) { + auto *line = new QLineSeries(this); + line->setName(ov.name); + appendSeries(line, ov.values, dispMin, dispMax); + overlayLines.push_back(line); } // ---- current point marker as binned value when binning > 1 ---- @@ -366,6 +410,16 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() { axisY->setLabelsVisible(true); } } + + // Attach overlay lines to the primary series' final axes; show the legend when overlaying. + const auto finalAxes = series->attachedAxes(); + for (auto *line: overlayLines) { + chart()->addSeries(line); + for (auto *ax: finalAxes) + line->attachAxis(ax); + } + chart()->legend()->setVisible(!overlays_.empty()); + chart()->legend()->setAlignment(Qt::AlignBottom); } } diff --git a/viewer/charts/JFJochDatasetInfoChartView.h b/viewer/charts/JFJochDatasetInfoChartView.h index f26e5bea..62fa0ca6 100644 --- a/viewer/charts/JFJochDatasetInfoChartView.h +++ b/viewer/charts/JFJochDatasetInfoChartView.h @@ -29,6 +29,7 @@ class JFJochDatasetInfoChartView : public QChartView { int64_t m_hoverPendingIdx = -1; std::vector values; + QString primaryName_; // legend name of the active (primary) run int64_t binning = 1; int64_t curr_image = 0; @@ -44,6 +45,14 @@ class JFJochDatasetInfoChartView : public QChartView { // Build regular (index or goniometer) chart void buildTimeDomainChart(); +public: + // One overlay line (a non-active run), drawn alongside the primary series with a legend entry. + struct NamedSeries { QString name; std::vector values; }; +private: + std::vector overlays_; + // 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; + // FFT view toggle bool m_showFFT = false; @@ -76,36 +85,14 @@ public slots: void resetZoom(); public: - template - void loadValues(const std::vector &input, + // Load the primary (active) run's values plus any overlay runs. one_over_d2 applies the same + // d -> 1/d^2 transform to every series; primaryName/overlay names drive the legend. + void loadValues(const std::vector &input, int64_t image, bool one_over_d2, - const JFJochReaderDataset *dataset = nullptr) { - m_yOneOverD = one_over_d2; - - values.resize(input.size()); - - if (dataset != nullptr) { - goniometer_axis = dataset->experiment.GetGoniometer(); - image_time_us = dataset->experiment.GetImageTime().count(); - } else { - goniometer_axis = {}; - image_time_us = {}; - } - - for (int i = 0; i < input.size(); i++) { - if (one_over_d2) { - float d = static_cast(input[i]); - if (!std::isfinite(d)) - values[i] = 0.0f; - else - values[i] = 1.0f / (d * d); - } else - values[i] = static_cast(input[i]); - } - curr_image = image; - updateChart(); - } + const JFJochReaderDataset *dataset = nullptr, + const QString &primaryName = QString(), + std::vector overlays = {}); }; diff --git a/viewer/windows/JFJochProcessingJobsWindow.cpp b/viewer/windows/JFJochProcessingJobsWindow.cpp index 4ed1805f..0546bb09 100644 --- a/viewer/windows/JFJochProcessingJobsWindow.cpp +++ b/viewer/windows/JFJochProcessingJobsWindow.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -27,7 +28,7 @@ #include namespace { - enum Column { COL_NAME = 0, COL_MODE, COL_IMAGES, COL_STATUS, COL_INDEX, COL_CELL, COL_COUNT }; + enum Column { COL_NAME = 0, COL_STARTED, COL_MODE, COL_IMAGES, COL_STATUS, COL_INDEX, COL_CELL, COL_COUNT }; int default_threads() { const unsigned hc = std::thread::hardware_concurrency(); @@ -55,11 +56,21 @@ JFJochProcessingJobsWindow::JFJochProcessingJobsWindow(JFJochImageReadingWorker toolbar_->addAction("Show original", this, &JFJochProcessingJobsWindow::showOriginal); table_ = new QTableWidget(0, COL_COUNT, this); - table_->setHorizontalHeaderLabels({"Name", "Mode", "Images", "Status", "Index %", "Unit cell"}); + table_->setHorizontalHeaderLabels({"Name", "Started", "Mode", "Images", "Status", "Index %", "Unit cell"}); table_->horizontalHeader()->setStretchLastSection(true); table_->setSelectionBehavior(QAbstractItemView::SelectRows); table_->setSelectionMode(QAbstractItemView::SingleSelection); - table_->setEditTriggers(QAbstractItemView::NoEditTriggers); + // Only the Name column is editable (per-item flags); editing it renames the run's legend label. + table_->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed); + connect(table_, &QTableWidget::itemChanged, this, [this](QTableWidgetItem *item) { + if (item->column() != COL_NAME) + return; + const int row = item->row(); + if (row < 0 || row >= static_cast(jobs_.size()) || item->text() == jobs_[row].label) + return; + jobs_[row].label = item->text(); + emit renameRun(jobs_[row].id, jobs_[row].label); + }); auto *message = new QLabel("Dataset re-processing is currently available only in File mode.\n\n" "Open a stored HDF5 file to run processing jobs.", this); @@ -210,16 +221,33 @@ void JFJochProcessingJobsWindow::newJob() { } const bool full = spec.mode == ProcessMode::FullAnalysis; - const QString name = QStringLiteral("%1 %2").arg(full ? "Full" : "AzInt").arg(++job_counter_); + const int run_number = ++job_counter_; + const QString label = QStringLiteral("%1 %2").arg(full ? "Full" : "AzInt").arg(run_number); + const QString id = QStringLiteral("run-%1").arg(run_number); + + JobInfo info; + info.id = id; + info.label = label; + if (spec.save) + info.snapshot_path = QString::fromStdString(config.output_prefix) + "_process.h5"; const int row = table_->rowCount(); table_->insertRow(row); - table_->setItem(row, COL_NAME, new QTableWidgetItem(name)); - table_->setItem(row, COL_MODE, new QTableWidgetItem(full ? "Full" : "AzInt")); - table_->setItem(row, COL_IMAGES, new QTableWidgetItem(spec.images > 0 ? QString::number(spec.images) : "all")); - table_->setItem(row, COL_STATUS, new QTableWidgetItem("queued")); - table_->setItem(row, COL_INDEX, new QTableWidgetItem("-")); - table_->setItem(row, COL_CELL, new QTableWidgetItem("-")); + jobs_.push_back(info); // jobs_[row] must exist before setItem(COL_NAME) fires itemChanged + + auto fixed = [](const QString &text) { // a non-editable cell + auto *cell = new QTableWidgetItem(text); + cell->setFlags(cell->flags() & ~Qt::ItemIsEditable); + return cell; + }; + + table_->setItem(row, COL_NAME, new QTableWidgetItem(label)); // editable (default flags) + table_->setItem(row, COL_STARTED, fixed(QDateTime::currentDateTime().toString("HH:mm:ss"))); + table_->setItem(row, COL_MODE, fixed(full ? "Full" : "AzInt")); + table_->setItem(row, COL_IMAGES, fixed(spec.images > 0 ? QString::number(spec.images) : "all")); + table_->setItem(row, COL_STATUS, fixed("queued")); + table_->setItem(row, COL_INDEX, fixed("-")); + table_->setItem(row, COL_CELL, fixed("-")); // A progress bar lives in the Status cell while the job runs; it shows the phase as text until // image processing starts, then " / " with the bar filling in the background. @@ -230,15 +258,10 @@ void JFJochProcessingJobsWindow::newJob() { running_bar_->setFormat("queued"); table_->setCellWidget(row, COL_STATUS, running_bar_); - JobInfo info; - info.name = name; - if (spec.save) - info.snapshot_path = QString::fromStdString(config.output_prefix) + "_process.h5"; - jobs_.push_back(info); running_row_ = row; controller_->start(inputs.file, experiment, inputs.pixel_mask, config); - emit writeStatusBar("Started processing job " + name); + emit writeStatusBar("Started processing job " + label); } void JFJochProcessingJobsWindow::cancelJob() { @@ -256,7 +279,7 @@ void JFJochProcessingJobsWindow::viewResults() { QMessageBox::information(this, "Processing", "This job has no saved results to view."); return; } - emit activateSnapshot(jobs_[row].name); + emit activateSnapshot(jobs_[row].id); } void JFJochProcessingJobsWindow::showOriginal() { @@ -287,6 +310,7 @@ void JFJochProcessingJobsWindow::onProgress(quint64 done, quint64 total) { } void JFJochProcessingJobsWindow::onFinished(ProcessResult result) { + emit liveDataset(nullptr); // the finished run takes over from the live overlay const int row = running_row_; running_row_ = -1; if (row >= 0) @@ -308,12 +332,13 @@ void JFJochProcessingJobsWindow::onFinished(ProcessResult result) { if (!result.cancelled && result.written_master_path.has_value() && !jobs_[row].snapshot_path.isEmpty()) { jobs_[row].has_result = true; - emit registerSnapshot(jobs_[row].name, jobs_[row].snapshot_path); // also activates it + emit registerSnapshot(jobs_[row].id, jobs_[row].label, jobs_[row].snapshot_path); // also activates it } emit writeStatusBar(result.cancelled ? "Processing cancelled" : "Processing finished"); } void JFJochProcessingJobsWindow::onFailed(QString error) { + emit liveDataset(nullptr); // clear the live overlay const int row = running_row_; running_row_ = -1; if (row >= 0) diff --git a/viewer/windows/JFJochProcessingJobsWindow.h b/viewer/windows/JFJochProcessingJobsWindow.h index 4394cbcf..c8f0eaa7 100644 --- a/viewer/windows/JFJochProcessingJobsWindow.h +++ b/viewer/windows/JFJochProcessingJobsWindow.h @@ -26,10 +26,11 @@ public: explicit JFJochProcessingJobsWindow(JFJochImageReadingWorker *worker, QWidget *parent = nullptr); signals: - void registerSnapshot(QString name, QString master_path); - void activateSnapshot(QString name); + void registerSnapshot(QString id, QString label, QString master_path); + void activateSnapshot(QString id); + void renameRun(QString id, QString label); void writeStatusBar(QString message, int timeout_ms = 0); - // Live per-image results while a job runs, for the dataset-info plots. + // Live per-image results while a job runs, for the dataset-info plots (nullptr clears it). void liveDataset(std::shared_ptr dataset); public slots: @@ -47,7 +48,8 @@ private slots: private: struct JobInfo { - QString name; + QString id; // stable reader-snapshot key + QString label; // editable display name (legend / table) QString snapshot_path; // empty unless saved bool has_result = false; };