// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include "JFJochViewerImageStrip.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include JFJochViewerImageStrip::JFJochViewerImageStrip(QWidget *parent) : QWidget(parent) { auto *layout = new QVBoxLayout(this); layout->setContentsMargins(4, 2, 4, 2); auto *controls = new QHBoxLayout(); controls->addWidget(new QLabel("Show", this)); mode_ = new QComboBox(this); mode_->addItem("Evenly spaced", 0); mode_->addItem("Most spots", 1); mode_->addItem("Indexed", 2); mode_->addItem("Resolution", 3); // representatives across the resolution range mode_->addItem("Background", 4); // representatives across the background range controls->addWidget(mode_); spots_ = new QCheckBox("Spots", this); spots_->setChecked(true); spots_->setToolTip("Overlay found spots — a real pattern reads like a constellation"); controls->addWidget(spots_); auto *refresh = new QToolButton(this); refresh->setIcon(style()->standardIcon(QStyle::SP_BrowserReload)); refresh->setAutoRaise(true); refresh->setCursor(Qt::PointingHandCursor); refresh->setToolTip("Re-roll the representative selection"); controls->addWidget(refresh); controls->addStretch(); layout->addLayout(controls); connect(refresh, &QToolButton::clicked, this, [this] { Rebuild(); }); scroll_ = new QScrollArea(this); scroll_->setWidgetResizable(true); scroll_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scroll_->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); scroll_->setMinimumHeight(48); // let the dock shrink; thumbnails scale to fit auto *stripWidget = new QWidget(scroll_); strip_ = new QHBoxLayout(stripWidget); strip_->setContentsMargins(0, 0, 0, 0); strip_->addStretch(); scroll_->setWidget(stripWidget); layout->addWidget(scroll_); connect(mode_, &QComboBox::currentIndexChanged, this, [this] { Rebuild(); }); connect(spots_, &QCheckBox::toggled, this, [this] { Rebuild(); }); } void JFJochViewerImageStrip::datasetLoaded(std::shared_ptr dataset) { // Store the latest dataset (metrics may update during live sync / re-analysis) but do NOT // rebuild here — that would refetch thumbnails on every HTTP image update. Rebuilding happens // on file open (resetForNewFile, which fires just after this) and on mode / Spots / Refresh. dataset_ = std::move(dataset); } void JFJochViewerImageStrip::resetForNewFile() { Rebuild(); // a new file/stream opened; dataset_ was just set by datasetLoaded } void JFJochViewerImageStrip::thumbnailReady(qint64 image_number, QImage thumb) { auto it = slots_.find(image_number); if (it != slots_.end()) it.value()->setIcon(QPixmap::fromImage(thumb)); } QVector JFJochViewerImageStrip::ComputeRepresentatives() const { QVector result; if (!dataset_) return result; const int64_t total = dataset_->experiment.GetImageNum(); if (total <= 0) return result; const int N = static_cast(std::min(8, total)); const int mode = mode_->currentIndex(); auto *rng = QRandomGenerator::global(); // Pick one random image from each of N equal bins over an ordered candidate list. Stochastic, // so Refresh re-rolls a fresh set and deterministic-spacing artefacts are avoided. auto binnedRandom = [&](const std::vector &candidates) { const size_t M = candidates.size(); if (M == 0) return; const int n = static_cast(std::min(N, M)); for (int i = 0; i < n; ++i) { const size_t lo = static_cast(i) * M / n; size_t hi = static_cast(i + 1) * M / n; if (hi <= lo) hi = lo + 1; result.push_back(candidates[lo + rng->bounded(static_cast(hi - lo))]); } }; // Image indices ordered by a metric (finite values only), low to high. auto orderedByMetric = [](const std::vector &metric) { std::vector> vals; for (size_t i = 0; i < metric.size(); ++i) if (std::isfinite(metric[i])) vals.push_back({metric[i], static_cast(i)}); std::sort(vals.begin(), vals.end()); std::vector idx; idx.reserve(vals.size()); for (const auto &v : vals) idx.push_back(v.second); return idx; }; std::vector allIndices(total); std::iota(allIndices.begin(), allIndices.end(), 0); const auto &sc = dataset_->spot_count; const auto &sci = dataset_->spot_count_indexed; if (mode == 1 && !sc.empty()) { // most spots (deterministic top-N) std::vector idx(sc.size()); std::iota(idx.begin(), idx.end(), 0); const size_t n = std::min(N, idx.size()); std::partial_sort(idx.begin(), idx.begin() + n, idx.end(), [&](int64_t a, int64_t b) { return sc[a] > sc[b]; }); idx.resize(n); std::sort(idx.begin(), idx.end()); for (int64_t i : idx) result.push_back(i); } else if (mode == 2 && !sci.empty()) { // indexed images, spaced random std::vector indexed; for (size_t i = 0; i < sci.size(); ++i) if (sci[i] > 0) indexed.push_back(static_cast(i)); binnedRandom(indexed.empty() ? allIndices : indexed); } else if (mode == 3 && !dataset_->resolution_estimate.empty()) { binnedRandom(orderedByMetric(dataset_->resolution_estimate)); } else if (mode == 4 && !dataset_->bkg_estimate.empty()) { binnedRandom(orderedByMetric(dataset_->bkg_estimate)); } else { // evenly spaced (random within each bin) binnedRandom(allIndices); } return result; } void JFJochViewerImageStrip::Rebuild() { // Clear the strip (keep the trailing stretch). slots_.clear(); while (strip_->count() > 1) { QLayoutItem *item = strip_->takeAt(0); if (item->widget()) item->widget()->deleteLater(); delete item; } if (!dataset_) return; const QVector reps = ComputeRepresentatives(); int pos = 0; for (qint64 n : reps) { auto *btn = new QToolButton(this); btn->setToolButtonStyle(Qt::ToolButtonIconOnly); // the image number is drawn in the bitmap btn->setAutoRaise(true); btn->setCursor(Qt::PointingHandCursor); btn->setToolTip(QStringLiteral("Open image %1").arg(n + 1)); connect(btn, &QToolButton::clicked, this, [this, n] { emit imageSelected(n, 1); }); strip_->insertWidget(pos++, btn); slots_[n] = btn; } UpdateThumbnailSize(); emit requestThumbnails(reps, spots_->isChecked()); } void JFJochViewerImageStrip::UpdateThumbnailSize() { if (!scroll_) return; // Scale the thumbnails to the available height so the strip never needs more room than it has. const int h = std::clamp(scroll_->viewport()->height() - 4, 40, 240); for (auto *btn : std::as_const(slots_)) btn->setIconSize(QSize(h, h)); } void JFJochViewerImageStrip::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); UpdateThumbnailSize(); }