diff --git a/viewer/JFJochImageReadingWorker.cpp b/viewer/JFJochImageReadingWorker.cpp index a1ce8a26d..55661f511 100644 --- a/viewer/JFJochImageReadingWorker.cpp +++ b/viewer/JFJochImageReadingWorker.cpp @@ -352,8 +352,17 @@ void JFJochImageReadingWorker::LoadFile_i(const QString &filename, qint64 image_ } current_image.reset(); + current_image_ptr.reset(); // the previous file's image must not be re-emitted against this dataset current_summation = 1; current_file = filename; + // An image number carried over from the previous dataset (e.g. a D-Bus caller keeping its + // position) may not exist in this one: open the file at its first image instead of showing none. + if (!http_mode && image_number >= 0 && image_number + summation > total_images) { + logger.Warning("Image {} out of range for {} ({} images), showing image 0", + image_number, filename.toStdString(), total_images); + image_number = 0; + summation = 1; + } if (dataset) { curr_experiment = dataset->experiment; roi_override_.reset(); // new file: use its ROIs, forget any earlier edits @@ -581,7 +590,11 @@ void JFJochImageReadingWorker::UpdateDataset_i(const std::optional(*azint_mapping); - image_analysis->Analyze(new_image->ImageData(), *last_profile_, spot_finding_settings); + try { + image_analysis->Analyze(new_image->ImageData(), *last_profile_, spot_finding_settings); + } catch (const std::exception &e) { + logger.Error("Analysis of image failed: {}", e.what()); + return; + } current_image_ptr = new_image; auto end_time = std::chrono::high_resolution_clock::now(); @@ -624,27 +642,28 @@ void JFJochImageReadingWorker::FindCenter(QVector ring_q, bool guess, boo DiffractionGeometry geom = current_image_ptr->Dataset().experiment.GetDiffractionGeometry(); const std::vector ring_Q(ring_q.begin(), ring_q.end()); + // A diverged fit is rejected by the setters (NaN beam, sub-mm distance), so they sit in the try too. try { if (guess) GuessGeometry(geom, current_image_ptr->ImageData().spots, ring_Q, refine_tilt); else OptimizeGeometry(geom, current_image_ptr->ImageData().spots, ring_Q, refine_tilt); - } catch (const JFJochException &e) { - logger.ErrorException(e); + + logger.Info("Geometry found X: {} pxl Y: {} pxl Dist: {} mm", geom.GetBeamX_pxl(), geom.GetBeamY_pxl(), + geom.GetDetectorDistance_mm()); + + DiffractionExperiment new_experiment = current_image_ptr->Dataset().experiment; + new_experiment.BeamX_pxl(geom.GetBeamX_pxl()).BeamY_pxl(geom.GetBeamY_pxl()) + .DetectorDistance_mm(geom.GetDetectorDistance_mm()) + .PoniRot1_rad(geom.GetPoniRot1_rad()) + .PoniRot2_rad(geom.GetPoniRot2_rad()) + .PoniRot3_rad(geom.GetPoniRot3_rad()); + UpdateDataset_i(new_experiment); + } catch (const std::exception &e) { + logger.Error("Finding beam center failed: {}", e.what()); return; } - logger.Info("Geometry found X: {} pxl Y: {} pxl Dist: {} mm", geom.GetBeamX_pxl(), geom.GetBeamY_pxl(), - geom.GetDetectorDistance_mm()); - - DiffractionExperiment new_experiment = current_image_ptr->Dataset().experiment; - new_experiment.BeamX_pxl(geom.GetBeamX_pxl()).BeamY_pxl(geom.GetBeamY_pxl()) - .DetectorDistance_mm(geom.GetDetectorDistance_mm()) - .PoniRot1_rad(geom.GetPoniRot1_rad()) - .PoniRot2_rad(geom.GetPoniRot2_rad()) - .PoniRot3_rad(geom.GetPoniRot3_rad()); - UpdateDataset_i(new_experiment); - QVector rings; for (int i = 0; i < 15 && i < ring_Q.size(); i++) { rings.push_back(2 * PI / ring_Q[i]); @@ -695,14 +714,29 @@ void JFJochImageReadingWorker::ReanalyzeImages(bool input) { void JFJochImageReadingWorker::UpdateAzintSettings(const AzimuthalIntegrationSettings &settings) { QMutexLocker locker(&m); + // Checked here, not only where the mapping is built: settings that cannot be mapped would + // otherwise be kept and fail every later file open and dataset update. + if (settings.GetBinCount() >= UINT16_MAX) { + logger.Error("Azimuthal integration settings ignored: {} bins, at most 65534 supported", + settings.GetBinCount()); + return; + } azint_settings = settings; - UpdateDataset_i(std::nullopt); + try { + UpdateDataset_i(std::nullopt); + } catch (const std::exception &e) { + logger.Error("Azimuthal integration update failed: {}", e.what()); + } } void JFJochImageReadingWorker::UpdateBraggIntegrationSettings(BraggIntegrationSettings settings) { QMutexLocker locker(&m); bragg_settings = settings; - UpdateDataset_i(std::nullopt); + try { + UpdateDataset_i(std::nullopt); + } catch (const std::exception &e) { + logger.Error("Bragg integration update failed: {}", e.what()); + } } void JFJochImageReadingWorker::UpdateScalingSettings(ScalingSettings settings) { @@ -778,7 +812,11 @@ void JFJochImageReadingWorker::EmitReferenceInfo_i() { void JFJochImageReadingWorker::SetROIDefinition(const ROIDefinition &rois) { QMutexLocker locker(&m); - SetROIDefinition_i(rois); + try { + SetROIDefinition_i(rois); + } catch (const std::exception &e) { + logger.Error("ROI update failed: {}", e.what()); + } } void JFJochImageReadingWorker::MaskFromSelectedROI(QString name, bool add) { @@ -879,25 +917,30 @@ void JFJochImageReadingWorker::ApplyROIOverrideToImage_i() { void JFJochImageReadingWorker::UpdateUserMask_i(const std::vector &mask) { std::shared_ptr dataset; - if (http_mode) { - http_reader.UpdateUserMask(mask); - dataset = http_reader.GetDataset(); - } else { - file_reader->UpdateUserMask(mask); - dataset = file_reader->GetDataset(); - } + try { + if (http_mode) { + http_reader.UpdateUserMask(mask); + dataset = http_reader.GetDataset(); + } else { + file_reader->UpdateUserMask(mask); + dataset = file_reader->GetDataset(); + } - if (!dataset) { - logger.Error("UpdateUserMask_i: dataset is null (http_mode={}) - skipping update to avoid crash", http_mode); + if (!dataset) { + logger.Error("UpdateUserMask_i: dataset is null (http_mode={}) - skipping update to avoid crash", http_mode); + return; + } + + UpdateAzint_i(dataset.get()); + + EmitDatasetLoaded_i(dataset); + + current_image_ptr = std::make_shared(current_image_ptr->ImageData(), dataset); + } catch (const std::exception &e) { + logger.Error("User mask update failed: {}", e.what()); return; } - UpdateAzint_i(dataset.get()); - - EmitDatasetLoaded_i(dataset); - - current_image_ptr = std::make_shared(current_image_ptr->ImageData(), dataset); - if (current_image.has_value()) LoadImage_i(current_image.value(), current_summation); } @@ -918,7 +961,11 @@ void JFJochImageReadingWorker::SaveUserMaskTIFF(QString filename) { return; auto user_mask = current_image_ptr->Dataset().pixel_mask->GetUserMask(); CompressedImage mask_image(user_mask, current_image_ptr->Dataset().experiment.GetXPixelsNum(), current_image_ptr->Dataset().experiment.GetYPixelsNum()); - WriteTIFFToFile(filename.toStdString(), mask_image); + try { + WriteTIFFToFile(filename.toStdString(), mask_image); + } catch (const std::exception &e) { + logger.Error("Could not write mask TIFF {}: {}", filename.toStdString(), e.what()); + } } void JFJochImageReadingWorker::LoadUserMaskTIFF(QString filename, bool replace) { @@ -961,8 +1008,15 @@ void JFJochImageReadingWorker::UploadUserMask() { return; auto user_mask = current_image_ptr->Dataset().pixel_mask->GetUserMask(); - if (http_mode) + if (!http_mode) + return; + try { http_reader.UploadUserMask(user_mask); + } catch (const HttpUnauthorized &) { + HandleUnauthorized_i(current_file); + } catch (const std::exception &e) { + logger.Error("Upload of user mask failed: {}", e.what()); + } } void JFJochImageReadingWorker::LoadCalibration(QString dataset) { @@ -1265,7 +1319,11 @@ void JFJochImageReadingWorker::RemoveRun(QString id) { } run_labels_.erase(id.toStdString()); // Refresh the (possibly now-Original) active view; ActivateSnapshot_i also re-emits runsChanged. - ActivateSnapshot_i(QString::fromStdString(ActiveRun_i())); + try { + ActivateSnapshot_i(QString::fromStdString(ActiveRun_i())); + } catch (const std::exception &e) { + logger.Error("Could not activate run after removal: {}", e.what()); + } } void JFJochImageReadingWorker::RegisterProcessingSnapshot(QString id, QString label, QString master_path) { diff --git a/viewer/JFJochViewerMenu.cpp b/viewer/JFJochViewerMenu.cpp index ac5699c89..f25a4f804 100644 --- a/viewer/JFJochViewerMenu.cpp +++ b/viewer/JFJochViewerMenu.cpp @@ -327,7 +327,7 @@ void JFJochViewerMenu::openHttpSelected() { char *port_std = std::getenv("JUNGFRAUJOCH_HTTP_PORT"); if (port_std != nullptr) { - const int tmp = std::stoi(std::string(port_std)); + const int tmp = QString(port_std).toInt(); // 0 for a non-number, unlike std::stoi which throws if (tmp < 65536 && tmp > 0) portSpinBox->setValue(tmp); } else diff --git a/viewer/image_viewer/JFJochAzIntImage.cpp b/viewer/image_viewer/JFJochAzIntImage.cpp index 7ccf3ce45..ac69df000 100644 --- a/viewer/image_viewer/JFJochAzIntImage.cpp +++ b/viewer/image_viewer/JFJochAzIntImage.cpp @@ -107,7 +107,11 @@ void JFJochAzIntImage::mouseDoubleClickEvent(QMouseEvent *event) { float phi = image->Dataset().az_int_bin_to_phi[idx]; auto geom = image->Dataset().experiment.GetDiffractionGeometry(); - auto coord = geom.ResPhiToPxl(2 * PI / q, phi / 180.0 * PI); - emit zoomOnBin(QPointF(coord.first, coord.second)); + try { + auto coord = geom.ResPhiToPxl(2 * PI / q, phi / 180.0 * PI); + emit zoomOnBin(QPointF(coord.first, coord.second)); + } catch (const std::exception &) { + // a bin beyond 2 theta = 180 deg (manual High Q above 4 pi / lambda) is on no pixel + } } } \ No newline at end of file diff --git a/viewer/image_viewer/JFJochDiffractionImage.cpp b/viewer/image_viewer/JFJochDiffractionImage.cpp index 479fec07d..37a14a8c1 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.cpp +++ b/viewer/image_viewer/JFJochDiffractionImage.cpp @@ -834,11 +834,14 @@ void JFJochDiffractionImage::roiEditMove(const QPointF &scenePos) { case RoiEdit::ResizeCircle: edit_radius_ = std::max(1.0, QLineF(edit_center_, scenePos).length()); break; + // PxlToRes is infinite on the beam centre, which ROIAzimuthal rejects: keep the last finite value case RoiEdit::AzimInner: - edit_d_max_ = geom.PxlToRes(cursor_r); + if (const float d = geom.PxlToRes(cursor_r); std::isfinite(d)) + edit_d_max_ = d; break; case RoiEdit::AzimOuter: - edit_d_min_ = geom.PxlToRes(cursor_r); + if (const float d = geom.PxlToRes(cursor_r); std::isfinite(d)) + edit_d_min_ = d; break; case RoiEdit::RotatePhiMin: edit_phi_min_ = geom.Phi_rad(scenePos.x(), scenePos.y()) * 180.0f / static_cast(PI); diff --git a/viewer/image_viewer/JFJochGridScanImage.cpp b/viewer/image_viewer/JFJochGridScanImage.cpp index ea6601f90..cfcf5adb0 100644 --- a/viewer/image_viewer/JFJochGridScanImage.cpp +++ b/viewer/image_viewer/JFJochGridScanImage.cpp @@ -155,7 +155,10 @@ void JFJochGridScanImage::mouseDoubleClickEvent(QMouseEvent *event) { } void JFJochGridScanImage::setImage(int64_t val) { - if (settings) { + // The image can lie outside the grid: live, the broker's image and dataset are fetched apart and + // can belong to different collections, and a re-emitted image can meet a newer dataset's grid. + // Such an image has no cell to mark (GetElementPos*_step would throw on the GUI thread). + if (settings && val >= 0 && val < settings->GetNElem()) { current_image_W = settings->GetElementPosX_step(val); current_image_H = settings->GetElementPosY_step(val); } else { diff --git a/viewer/widgets/JFJochViewerROIList.cpp b/viewer/widgets/JFJochViewerROIList.cpp index ab4a701b3..cf3dbf23d 100644 --- a/viewer/widgets/JFJochViewerROIList.cpp +++ b/viewer/widgets/JFJochViewerROIList.cpp @@ -114,29 +114,35 @@ void JFJochViewerROIList::ApplyEditor() { const std::string sel = SelectedName().toStdString(); bool changed = false; - for (auto &b : rois_.boxes) - if (b.GetName() == sel) { - b = ROIBox(sel, fedit_[0]->text().toLongLong(), fedit_[1]->text().toLongLong(), - fedit_[2]->text().toLongLong(), fedit_[3]->text().toLongLong()); - changed = true; break; - } - for (auto &c : rois_.circles) - if (c.GetName() == sel) { - c = ROICircle(sel, fedit_[0]->text().toDouble(), fedit_[1]->text().toDouble(), - std::max(0.1, fedit_[2]->text().toDouble())); - changed = true; break; - } - for (auto &a : rois_.azimuthal) - if (a.GetName() == sel) { - const float qmin = fedit_[0]->text().toFloat(); - const float qmax = fedit_[1]->text().toFloat(); - const float phimin = fedit_[2]->text().toFloat(); - const float phimax = fedit_[3]->text().toFloat(); - const float d_min = (qmax > 0) ? 2.0f * static_cast(PI) / qmax : a.GetDMin_A(); - const float d_max = (qmin > 0) ? 2.0f * static_cast(PI) / qmin : a.GetDMax_A(); - a = ROIAzimuthal(sel, d_min, d_max, phimin, phimax); - changed = true; break; - } + // The ROI constructors reject what QString::toDouble lets through ("inf", "nan"); an exception + // escaping a slot terminates the viewer, so such an entry is just not applied. + try { + for (auto &b : rois_.boxes) + if (b.GetName() == sel) { + b = ROIBox(sel, fedit_[0]->text().toLongLong(), fedit_[1]->text().toLongLong(), + fedit_[2]->text().toLongLong(), fedit_[3]->text().toLongLong()); + changed = true; break; + } + for (auto &c : rois_.circles) + if (c.GetName() == sel) { + c = ROICircle(sel, fedit_[0]->text().toDouble(), fedit_[1]->text().toDouble(), + std::max(0.1, fedit_[2]->text().toDouble())); + changed = true; break; + } + for (auto &a : rois_.azimuthal) + if (a.GetName() == sel) { + const float qmin = fedit_[0]->text().toFloat(); + const float qmax = fedit_[1]->text().toFloat(); + const float phimin = fedit_[2]->text().toFloat(); + const float phimax = fedit_[3]->text().toFloat(); + const float d_min = (qmax > 0) ? 2.0f * static_cast(PI) / qmax : a.GetDMin_A(); + const float d_max = (qmin > 0) ? 2.0f * static_cast(PI) / qmin : a.GetDMax_A(); + a = ROIAzimuthal(sel, d_min, d_max, phimin, phimax); + changed = true; break; + } + } catch (const std::exception &) { + return; + } if (changed) emit roisChanged(rois_); diff --git a/viewer/widgets/JFJochViewerSettingsDock.cpp b/viewer/widgets/JFJochViewerSettingsDock.cpp index 2b0364779..ea037df2b 100644 --- a/viewer/widgets/JFJochViewerSettingsDock.cpp +++ b/viewer/widgets/JFJochViewerSettingsDock.cpp @@ -230,9 +230,9 @@ QWidget *JFJochViewerSettingsDock::BuildMXPage() { cellA_ = new NumberLineEdit(1.0, 2000.0, 79.0, 3, "", page); cellB_ = new NumberLineEdit(1.0, 2000.0, 79.0, 3, "", page); cellC_ = new NumberLineEdit(1.0, 2000.0, 38.0, 3, "", page); - cellAlpha_ = new NumberLineEdit(1.0, 179.0, 90.0, 2, "", page); - cellBeta_ = new NumberLineEdit(1.0, 179.0, 90.0, 2, "", page); - cellGamma_ = new NumberLineEdit(1.0, 179.0, 90.0, 2, "", page); + cellAlpha_ = new NumberLineEdit(5.0, 179.0, 90.0, 2, "", page); + cellBeta_ = new NumberLineEdit(5.0, 179.0, 90.0, 2, "", page); + cellGamma_ = new NumberLineEdit(5.0, 179.0, 90.0, 2, "", page); spaceGroup_ = new NumberLineEdit(0.0, 230.0, 0.0, 0, "", page); spaceGroup_->setToolTip("Space group number (1–230); 0 = unset."); spaceGroupName_ = new QLabel("—", page); @@ -682,12 +682,20 @@ QWidget *JFJochViewerSettingsDock::BuildAzIntSection() { az->addRow("Azimuthal bins", azimBins); azSection->setContentLayout(az); + // A combination can still be invalid (Low Q above High Q): the setters throw, and an exception + // escaping a slot terminates the viewer. Build on a copy and keep the previous settings instead. auto emitAz = [=, this] { - azint_.QRange_recipA(static_cast(lowQ->value()), - autoHighQ->isChecked() ? std::nullopt - : std::optional(highQ->value())); - azint_.QSpacing_recipA(static_cast(spacing->value())); - azint_.AzimuthalBinCount(azimBins->currentData().toInt()); + AzimuthalIntegrationSettings s = azint_; + try { + s.QRange_recipA(static_cast(lowQ->value()), + autoHighQ->isChecked() ? std::nullopt + : std::optional(highQ->value())); + s.QSpacing_recipA(static_cast(spacing->value())); + s.AzimuthalBinCount(azimBins->currentData().toInt()); + } catch (const std::exception &) { + return; + } + azint_ = s; emit azintChanged(azint_); }; connect(lowQ, &SliderPlusBox::valueChanged, this, [emitAz] { emitAz(); }); @@ -792,7 +800,7 @@ QWidget *JFJochViewerSettingsDock::BuildBraggSection() { auto *gaussian = new QCheckBox("Gaussian profile fit", this); gaussian->setChecked(bragg_.GetIntegrator() != IntegratorMode::BoxSum); gaussian->setToolTip("Profile-fit the spots (more accurate intensities); off = classical box-sum."); - auto *r1 = new NumberLineEdit(1.0f, 30.0f, bragg_.GetR1(), 1, "", this); + auto *r1 = new NumberLineEdit(1.0f, 20.0f, bragg_.GetR1(), 1, "", this); auto *r2 = new NumberLineEdit(1.0f, 30.0f, bragg_.GetR2(), 1, "", this); auto *r3 = new NumberLineEdit(1.0f, 30.0f, bragg_.GetR3(), 1, "", this); auto *radii = new QHBoxLayout(); @@ -817,15 +825,23 @@ QWidget *JFJochViewerSettingsDock::BuildBraggSection() { form->addRow(trimRow); section->setContentLayout(form); + // The radii must grow r1 < r2 < r3, which a single typed value can break: the setters throw, and an + // exception escaping a slot terminates the viewer. Build on a copy and keep the previous settings. auto emitBragg = [=, this] { - bragg_.Integrator(gaussian->isChecked() ? IntegratorMode::ProfileGaussian : IntegratorMode::BoxSum); - bragg_.R1(static_cast(r1->value())).R2(static_cast(r2->value())) - .R3(static_cast(r3->value())); - // Either estimator clears the other, so unchecking has to put the default clip back explicitly. - if (bkgTrim->isChecked()) - bragg_.BackgroundTrimFraction(static_cast(bkgTrimFrac->value())); - else - bragg_.BackgroundClipNSigma(BraggIntegrationSettings().GetBackgroundClipNSigma()); + BraggIntegrationSettings s = bragg_; + try { + s.Integrator(gaussian->isChecked() ? IntegratorMode::ProfileGaussian : IntegratorMode::BoxSum); + s.R1(static_cast(r1->value())).R2(static_cast(r2->value())) + .R3(static_cast(r3->value())); + // Either estimator clears the other, so unchecking has to put the default clip back explicitly. + if (bkgTrim->isChecked()) + s.BackgroundTrimFraction(static_cast(bkgTrimFrac->value())); + else + s.BackgroundClipNSigma(BraggIntegrationSettings().GetBackgroundClipNSigma()); + } catch (const std::exception &) { + return; + } + bragg_ = s; emit braggChanged(bragg_); }; connect(gaussian, &QCheckBox::toggled, this, [emitBragg] { emitBragg(); }); @@ -909,27 +925,35 @@ QWidget *JFJochViewerSettingsDock::BuildScalingSection() { void JFJochViewerSettingsDock::EmitExperiment() { if (!have_experiment_) return; - experiment_.IncidentEnergy_keV(static_cast(energy_->value())); - experiment_.DetectorDistance_mm(static_cast(distance_->value())); - experiment_.BeamX_pxl(static_cast(beamX_->value())); - experiment_.BeamY_pxl(static_cast(beamY_->value())); - experiment_.PoniRot1_rad(static_cast(rot1_->value() * PI / 180.0)); - experiment_.PoniRot2_rad(static_cast(rot2_->value() * PI / 180.0)); - experiment_.PolarizationFactor(polarizationOn_->isChecked() - ? std::optional(static_cast(polarization_->value())) : std::nullopt); - experiment_.Goniometer(GoniometerFromFields()); - experiment_.GridScan(GridScanFromFields()); - if (cellKnown_->isChecked()) { - experiment_.SetUnitCell(UnitCell{ - static_cast(cellA_->value()), static_cast(cellB_->value()), - static_cast(cellC_->value()), static_cast(cellAlpha_->value()), - static_cast(cellBeta_->value()), static_cast(cellGamma_->value())}); - const int sg = static_cast(std::lround(spaceGroup_->value())); - experiment_.SpaceGroupNumber(sg > 0 ? std::optional(sg) : std::nullopt); - } else { - experiment_.SetUnitCell(std::nullopt); - experiment_.SpaceGroupNumber(std::nullopt); + // The setters validate and throw; an exception escaping a slot terminates the viewer, so + // an invalid entry keeps the previous experiment instead. + DiffractionExperiment e = experiment_; + try { + e.IncidentEnergy_keV(static_cast(energy_->value())); + e.DetectorDistance_mm(static_cast(distance_->value())); + e.BeamX_pxl(static_cast(beamX_->value())); + e.BeamY_pxl(static_cast(beamY_->value())); + e.PoniRot1_rad(static_cast(rot1_->value() * PI / 180.0)); + e.PoniRot2_rad(static_cast(rot2_->value() * PI / 180.0)); + e.PolarizationFactor(polarizationOn_->isChecked() + ? std::optional(static_cast(polarization_->value())) : std::nullopt); + e.Goniometer(GoniometerFromFields()); + e.GridScan(GridScanFromFields()); + if (cellKnown_->isChecked()) { + e.SetUnitCell(UnitCell{ + static_cast(cellA_->value()), static_cast(cellB_->value()), + static_cast(cellC_->value()), static_cast(cellAlpha_->value()), + static_cast(cellBeta_->value()), static_cast(cellGamma_->value())}); + const int sg = static_cast(std::lround(spaceGroup_->value())); + e.SpaceGroupNumber(sg > 0 ? std::optional(sg) : std::nullopt); + } else { + e.SetUnitCell(std::nullopt); + e.SpaceGroupNumber(std::nullopt); + } + } catch (const std::exception &) { + return; } + experiment_ = e; emit experimentChanged(experiment_); }