Build Packages / build:windows:nocuda (push) Successful in 17m28s
Build Packages / build:windows:cuda (push) Successful in 19m59s
Build Packages / build:rugnux:windows (push) Successful in 10m55s
Build Packages / Unit tests (push) Successful in 47m41s
Build Packages / build:viewer-tgz:cpu (push) Successful in 7m17s
Build Packages / build:viewer-tgz:cuda (push) Successful in 8m8s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 7m2s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 4m57s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m21s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 9m10s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 10m14s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 8m17s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 10m57s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 10m9s
Build Packages / build:rpm (rocky8) (push) Successful in 11m10s
Build Packages / build:rpm (rocky9) (push) Successful in 10m17s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 10m55s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 8m58s
Build Packages / Create release (push) Skipped
Build Packages / Generate python client (push) Successful in 14s
Build Packages / Build documentation (push) Successful in 43s
Build Packages / DIALS test (push) Successful in 10m29s
Build Packages / XDS test (neggia plugin) (push) Failing after 3h12m41s
Build Packages / XDS test (JFJoch plugin) (push) Failing after 3h12m43s
Build Packages / XDS test (durin plugin) (push) Failing after 3h12m45s
Measured every section's content minimum width against the 330 px the
settings dock gives it (the powder one, fixed yesterday, sat at 278):
geometry 346, unit cell 355, goniometer 322, spot finding 370,
indexing 321, bragg 287, scaling 297, azint 243, reference 149
Five over. Three causes, none of them about the section:
- addRow("", w) reserves the label column and puts the widget in the
field column, so a checkbox's whole width was added to the widest
label. addRow(w) spans both columns instead, which is what a
label-less row means anyway. Spot finding 370 -> 251.
- A spin box is never narrower than its longest possible value, suffix
included, and these rows hold two or three abreast. The unit moves to
the row label, which is how the panel names units elsewhere
("High resolution [Å]"): "a, b, c [Å]", "Beam origin [px]",
"Detector tilt [°]", "Step x, y [μm]", "Radii r1/r2/r3 [px]".
A cell length can still be four digits and three decimals, so those
six fields also divide the row rather than demand their own width.
- A combo is never narrower than its longest entry - the same thing
that pinned the powder panel. "Flex (best per-image refinement)" set
the indexing section's width; it elides now, as the calibrant does.
Every section is now 291 or less, and none of them moves the dock.
Two inspector changes in the same panel:
- An axis that is present but does not turn covers no angular range, so
the image angle is stated as one number rather than "-145° + 0°".
- Sample-head angles that stay put during a run - an SLS Smargon writes
chi and phi - get a row of their own, "chi 0° / phi 0°". Both its
labels stay empty for a file that carries none, so it costs nothing
where it means nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oVDk7aukYqfPZBLHn5nGg
411 lines
17 KiB
C++
411 lines
17 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "JFJochViewerImageStatistics.h"
|
|
#include "../../common/time_utc.h"
|
|
#include <QFormLayout>
|
|
#include <QRegularExpression>
|
|
|
|
static QString mkUnitCell(const UnitCell &uc) {
|
|
return QString("<b>%1</b> Å <b>%2</b> Å <b>%3</b> Å <b>%4</b>° <b>%5</b>° <b>%6</b>°")
|
|
.arg(QString::number(uc.a, 'f', 1))
|
|
.arg(QString::number(uc.b, 'f', 1))
|
|
.arg(QString::number(uc.c, 'f', 1))
|
|
.arg(QString::number(uc.alpha, 'f', 1))
|
|
.arg(QString::number(uc.beta, 'f', 1))
|
|
.arg(QString::number(uc.gamma, 'f', 1));
|
|
}
|
|
|
|
static QString mkSourceInstrumentText(const DiffractionExperiment& exp) {
|
|
QString src, inst;
|
|
const auto &m = exp.GetInstrumentMetadata();
|
|
if (!m.GetSourceName().empty()) src = QString::fromStdString(m.GetSourceName());
|
|
if (!m.GetInstrumentName().empty()) inst = QString::fromStdString(m.GetInstrumentName());
|
|
|
|
if (!src.isEmpty() && !inst.isEmpty()) return src + " / " + inst;
|
|
if (!src.isEmpty()) return src;
|
|
if (!inst.isEmpty()) return inst;
|
|
return QString("-");
|
|
}
|
|
|
|
static QString mkSourceInstrumentTooltip(const DiffractionExperiment &exp) {
|
|
QStringList tooltips;
|
|
|
|
if (exp.GetRingCurrent_mA().has_value()) {
|
|
tooltips << QString("Ring current: <b>%1</b> mA").arg(exp.GetRingCurrent_mA().value(), 0, 'f', 2);
|
|
}
|
|
|
|
if (exp.GetTotalFlux().has_value()) {
|
|
tooltips << QString("Total flux: <b>%1</b> ph/s").arg(exp.GetTotalFlux().value(), 0, 'e', 2);
|
|
}
|
|
|
|
if (exp.GetAttenuatorTransmission().has_value()) {
|
|
tooltips << QString("Attenuation: <b>%1</b>").arg(exp.GetAttenuatorTransmission().value(), 0, 'f', 3);
|
|
}
|
|
|
|
return tooltips.join("<br/>");
|
|
}
|
|
|
|
static QString mkSampleText(const DiffractionExperiment& exp) {
|
|
const auto& name = exp.GetSampleName();
|
|
return name.empty() ? QString("-") : QString::fromStdString(name);
|
|
}
|
|
|
|
static QString mkSampleTooltip(const DiffractionExperiment& exp) {
|
|
if (exp.GetSampleTemperature_K().has_value()) {
|
|
return QString("Sample temperature: <b>%1</b> K").arg(exp.GetSampleTemperature_K().value(), 0, 'f', 2);
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
static QString mkSymmetry(const LatticeMessage& sym) {
|
|
QString text("Bravais lattice: <b>");
|
|
switch (sym.crystal_system) {
|
|
case gemmi::CrystalSystem::Triclinic:
|
|
text += "a";
|
|
break;
|
|
case gemmi::CrystalSystem::Monoclinic:
|
|
text += "m";
|
|
break;
|
|
case gemmi::CrystalSystem::Orthorhombic:
|
|
text += "o";
|
|
break;
|
|
case gemmi::CrystalSystem::Tetragonal:
|
|
text += "t";
|
|
break;
|
|
case gemmi::CrystalSystem::Hexagonal:
|
|
text += "h";
|
|
break;
|
|
case gemmi::CrystalSystem::Cubic:
|
|
text += "c";
|
|
break;
|
|
default: ;
|
|
}
|
|
text += QString(sym.centering) + "</b><br/>";
|
|
text += QString("Niggli class <b>%1</b><br/>").arg(sym.niggli_class);
|
|
return text;
|
|
}
|
|
|
|
JFJochViewerImageStatistics::JFJochViewerImageStatistics(QWidget *parent) : QWidget(parent) {
|
|
QFormLayout* layout = new QFormLayout(this);
|
|
|
|
// Order: what the dataset is, then how it was collected, then what this image contains.
|
|
dataset_name = new QLabel(this);
|
|
dataset_name->setWordWrap(true);
|
|
layout->addRow(new QLabel("Dataset:"), dataset_name);
|
|
|
|
collection_time = new QLabel(this);
|
|
collection_time->setWordWrap(true);
|
|
layout->addRow(new QLabel("Collection time:"), collection_time);
|
|
|
|
source_name = new QLabel(this);
|
|
layout->addRow("Source / Instrument:", source_name);
|
|
|
|
sample_name = new QLabel(this);
|
|
layout->addRow("Sample:", sample_name);
|
|
|
|
detector_name = new QLabel(this);
|
|
layout->addRow(new QLabel("Detector:"), detector_name);
|
|
|
|
detector_distance = new QLabel(this);
|
|
layout->addRow(new QLabel("Detector distance:"), detector_distance);
|
|
|
|
beam_center = new QLabel(this);
|
|
layout->addRow(new QLabel("Beam center:"), beam_center);
|
|
|
|
wavelength = new QLabel(this);
|
|
layout->addRow(new QLabel("Wavelength:"), wavelength);
|
|
|
|
exposure_time = new QLabel(this);
|
|
layout->addRow(new QLabel("Exposure Time:"), exposure_time);
|
|
|
|
rotation_angle_label = new QLabel(this);
|
|
rotation_angle = new QLabel(this);
|
|
layout->addRow(rotation_angle_label, rotation_angle);
|
|
|
|
// Sample-head angles that do not move during a run (an SLS Smargon writes chi and phi). Both
|
|
// labels stay empty for a file that carries none, so the row costs nothing where it means nothing.
|
|
smargon_label = new QLabel(this);
|
|
smargon_angles = new QLabel(this);
|
|
layout->addRow(smargon_label, smargon_angles);
|
|
|
|
valid_values = new QLabel(this);
|
|
layout->addRow(new QLabel("Valid values:"), valid_values);
|
|
|
|
masked_pixels = new QLabel(this);
|
|
layout->addRow(new QLabel("Masked pixels:"), masked_pixels);
|
|
|
|
spots = new QLabel(this);
|
|
layout->addRow(new QLabel("Spots:"), spots);
|
|
|
|
bkg_estimate = new QLabel(this);
|
|
layout->addRow(new QLabel("Background:"), bkg_estimate);
|
|
|
|
indexed = new QLabel(this);
|
|
layout->addRow(new QLabel("Indexing:"), indexed);
|
|
|
|
multiple_lattices = new QLabel(this);
|
|
layout->addRow(multiple_lattices);
|
|
|
|
res_estimate = new QLabel(this);
|
|
layout->addRow(new QLabel("Resolution estimate:"), res_estimate);
|
|
|
|
profile_radius_label = new QLabel("");
|
|
profile_radius = new QLabel(this);
|
|
layout->addRow(profile_radius_label, profile_radius);
|
|
}
|
|
|
|
QString TrimZeros(double number, int precision) {
|
|
auto s = QString::number(number, 'f', precision);
|
|
s.remove(QRegularExpression("\\.?0+$"));
|
|
if (s.isEmpty()) s = "0";
|
|
return s;
|
|
}
|
|
|
|
template <typename Duration>
|
|
QString FormatTime(Duration time, int precision = 6) {
|
|
return TrimZeros(std::chrono::duration<float>(time).count(), precision);
|
|
}
|
|
|
|
void JFJochViewerImageStatistics::loadImage(std::shared_ptr<const JFJochReaderImage> image) {
|
|
if (!image) {
|
|
source_name->setText("");
|
|
sample_name->setText("");
|
|
dataset_name->setText("");
|
|
collection_time->setText("");
|
|
detector_name->setText("");
|
|
detector_name->setToolTip("");
|
|
detector_distance->setText("");
|
|
beam_center->setText("");
|
|
wavelength->setText("");
|
|
wavelength->setToolTip("");
|
|
exposure_time->setText("");
|
|
exposure_time->setToolTip("");
|
|
rotation_angle->setText("");
|
|
rotation_angle->setToolTip("");
|
|
rotation_angle_label->setText("");
|
|
smargon_label->setText("");
|
|
smargon_angles->setText("");
|
|
valid_values->setText("");
|
|
valid_values->setToolTip("");
|
|
spots->setText("");
|
|
bkg_estimate->setText("");
|
|
indexed->setText("");
|
|
indexed->setToolTip("");
|
|
multiple_lattices->setText("");
|
|
profile_radius_label->setText("");
|
|
profile_radius->setText("");
|
|
profile_radius->setToolTip("");
|
|
res_estimate->setText("");
|
|
masked_pixels->setText("");
|
|
masked_pixels->setToolTip("");
|
|
return;
|
|
}
|
|
|
|
QString text;
|
|
|
|
auto &exp = image->Dataset().experiment;
|
|
|
|
// Word-wrap alone would break wherever it likes (mid-directory-name); a zero-width space after
|
|
// every "/" gives it a preferred break point there, so a long path wraps like a path and only
|
|
// falls back to breaking a single component if that component alone doesn't fit.
|
|
QString path = QString::fromStdString(exp.GetFilePrefix());
|
|
path.replace('/', QString("/") + QChar(0x200B)); // U+200B ZERO WIDTH SPACE, a break opportunity
|
|
text = QString("<b>%1</b>").arg(path);
|
|
dataset_name->setText(text);
|
|
|
|
const QString collected = QString::fromStdString(utc_to_local_human_readable(image->Dataset().arm_date));
|
|
collection_time->setText(collected.isEmpty() ? QString("-") : QString("<b>%1</b>").arg(collected));
|
|
|
|
detector_distance->setText(QString("<b>%1</b> mm").arg(QString::number(exp.GetDetectorDistance_mm(), 'f', 3)));
|
|
|
|
beam_center->setText(QString("<b>%1</b> <b>%2</b> pxl")
|
|
.arg(QString::number(exp.GetBeamX_pxl(), 'f', 2))
|
|
.arg(QString::number(exp.GetBeamY_pxl(), 'f', 2)));
|
|
|
|
wavelength->setText(QString("<b>%1</b> Å").arg(QString::number(exp.GetWavelength_A(), 'f', 4)));
|
|
wavelength->setToolTip(QString("Energy: <b>%1</b> eV")
|
|
.arg(QString::number(exp.GetIncidentEnergy_keV() * 1000.0, 'f', 0)));
|
|
|
|
text = QString("<b>%1</b>").arg(QString::fromStdString(exp.GetDetectorDescription()));
|
|
detector_name->setToolTip(QString("Width: <b>%1</b> pxl (<b>%2</b> mm)<br/>Height: <b>%3</b> pxl (<b>%4</b> mm)<br/>Pixel size: <b>%5</b> μm")
|
|
.arg(QString::number(exp.GetXPixelsNum()))
|
|
.arg(QString::number(exp.GetXPixelsNum() * exp.GetPixelSize_mm(), 'f', 3))
|
|
.arg(QString::number(exp.GetYPixelsNum()))
|
|
.arg(QString::number(exp.GetYPixelsNum() * exp.GetPixelSize_mm(), 'f', 3))
|
|
.arg(QString::number(exp.GetPixelSize_mm() * 1000.0, 'f', 0)));
|
|
detector_name->setText(text);
|
|
|
|
source_name->setText(QString("<b>%1</b>").arg(mkSourceInstrumentText(exp)));
|
|
source_name->setToolTip(mkSourceInstrumentTooltip(exp));
|
|
|
|
sample_name->setText(QString("<b>%1</b>").arg(mkSampleText(exp)));
|
|
sample_name->setToolTip(mkSampleTooltip(exp));
|
|
|
|
if (exp.GetGoniometer()) {
|
|
// An axis that is present but does not turn has no wedge to add: state the angle it stands at.
|
|
const float wedge = exp.GetGoniometer()->GetWedge_deg();
|
|
const QString angle = TrimZeros(exp.GetGoniometer()->GetAngle_deg(image->ImageData().number), 3);
|
|
rotation_angle_label->setText("Image angle:");
|
|
rotation_angle->setText(wedge == 0.0f
|
|
? QString("<b>%1°</b>").arg(angle)
|
|
: QString("<b>%1° + %2°</b>").arg(angle).arg(TrimZeros(wedge, 3)));
|
|
rotation_angle->setToolTip(QString("Start angle: <b>%1°</b><br/>Increment: <b>%2°</b>")
|
|
.arg(TrimZeros(exp.GetGoniometer()->GetStart_deg(), 3))
|
|
.arg(TrimZeros(exp.GetGoniometer()->GetIncrement_deg(), 3))
|
|
);
|
|
} else if (exp.GetGridScan()) {
|
|
rotation_angle_label->setText("Grid scan:");
|
|
rotation_angle->setText(QString("<b>%1</b> x <b>%2</b> μm")
|
|
.arg(QString::number(exp.GetGridScan()->GetGridStepX_um(), 'f', 1))
|
|
.arg(QString::number(exp.GetGridScan()->GetGridStepY_um(), 'f', 1)));
|
|
rotation_angle->setToolTip(QString("Grid size: <b>%1</b> x <b>%2</b> μm<br/>"
|
|
"Grid elements: <b>%3</b> x <b>%4</b>")
|
|
.arg(QString::number(exp.GetGridScan()->GetGridSizeX_um()))
|
|
.arg(QString::number(exp.GetGridScan()->GetGridSizeY_um()))
|
|
.arg(QString::number(exp.GetGridScan()->GetGridSizeX_step()))
|
|
.arg(QString::number(exp.GetGridScan()->GetGridSizeY_step())));
|
|
} else {
|
|
rotation_angle_label->setText("");
|
|
rotation_angle->setText(QString(""));
|
|
rotation_angle->setToolTip("");
|
|
}
|
|
|
|
if (const auto head = exp.GetDatasetSettings().GetSmargonPosition()) {
|
|
smargon_label->setText("Smargon:");
|
|
smargon_angles->setText(QString("chi <b>%1</b>° / phi <b>%2</b>°")
|
|
.arg(TrimZeros(head->chi_deg, 3))
|
|
.arg(TrimZeros(head->phi_deg, 3)));
|
|
} else {
|
|
smargon_label->setText("");
|
|
smargon_angles->setText("");
|
|
}
|
|
|
|
exposure_time->setText(QString("<b>%1</b> s").arg(FormatTime(exp.GetImageTime())));
|
|
exposure_time->setToolTip(QString("Count time: <b>%1</b> s<br/>").arg(FormatTime(exp.GetImageCountTime())));
|
|
|
|
text = QString("<b>%1</b>").arg(image->ImageData().spots.size());
|
|
spots->setText(text);
|
|
if (image->ImageData().spot_count.has_value())
|
|
spots->setToolTip(QString("Unfiltered (total): <b>%1</b> <br/>Low resolution: <b>%2</b><br/>Ice ring: <b>%3</b><br/>Indexed <b>%4</b><br/>")
|
|
.arg(image->ImageData().spot_count.value())
|
|
.arg(image->ImageData().spot_count_low_res.value_or(0))
|
|
.arg(image->ImageData().spot_count_ice_rings.value_or(0))
|
|
.arg(image->ImageData().spot_count_indexed.value_or(0)));
|
|
|
|
auto mm = image->ValidMinMax();
|
|
if (mm.has_value()) {
|
|
text = QString("<b>%1</b> - <b>%2</b>")
|
|
.arg(mm->first)
|
|
.arg(mm->second);
|
|
valid_values->setText(text);
|
|
} else {
|
|
valid_values->setText("N/A");
|
|
}
|
|
valid_values->setToolTip(QString("Error pixels: <b>%1</b><br/>Saturated pixels: <b>%2</b>")
|
|
.arg(image->ErrorPixels().size()).arg(image->SaturatedPixels().size()));
|
|
|
|
if (!image->Dataset().bkg_estimate.empty()) {
|
|
text = QString("<b>%1</b>").arg(image->ImageData().bkg_estimate.value_or(0));
|
|
bkg_estimate->setText(text);
|
|
} else {
|
|
bkg_estimate->setText("N/A");
|
|
}
|
|
|
|
auto pr = image->ImageData().profile_radius;
|
|
auto mos = image->ImageData().mosaicity_deg;
|
|
if (mos && std::isfinite(mos.value())) {
|
|
text = QString("<b>%1</b>°").arg(QString::number(mos.value(), 'f', 6));
|
|
profile_radius_label->setText("Mosaicity:");
|
|
profile_radius->setText(text);
|
|
if (pr && std::isfinite(pr.value())) {
|
|
text = QString("Profile radius <b>%1</b> Å<sup>-1</sup>").arg(QString::number(pr.value(), 'f', 6));
|
|
profile_radius->setToolTip(text);
|
|
} else
|
|
profile_radius->setToolTip("");
|
|
} else if (pr && std::isfinite(pr.value())) {
|
|
profile_radius_label->setText("Profile radius:");
|
|
text = QString("<b>%1</b> Å<sup>-1</sup>").arg(QString::number(pr.value(), 'f', 6));
|
|
profile_radius->setText(text);
|
|
profile_radius->setToolTip("");
|
|
} else {
|
|
profile_radius_label->setText("");
|
|
profile_radius->setText("");
|
|
profile_radius->setToolTip("");
|
|
}
|
|
|
|
auto res = image->ImageData().resolution_estimate;
|
|
if (res && std::isfinite(res.value())) {
|
|
text = QString("<b>%1</b> Å").arg(QString::number(res.value(), 'f', 2));
|
|
res_estimate->setText(text);
|
|
} else {
|
|
res_estimate->setText("N/A");
|
|
}
|
|
|
|
float total_pixels = image->Dataset().experiment.GetPixelsNum();
|
|
if (total_pixels == 0)
|
|
total_pixels = 1;
|
|
|
|
auto mask_stats = image->Dataset().pixel_mask->GetStatistics();
|
|
text = QString("<b>%1</b>").arg(mask_stats.total_masked);
|
|
masked_pixels->setText(text);
|
|
masked_pixels->setToolTip(
|
|
QString(
|
|
"Error pixels: <b>%1</b> (%2 %)<br/> Noisy pixels: <b>%3</b> (%4 %) <br/> User mask: <b>%5</b> (%6 %)<br/>"
|
|
"Chip gaps: <b>%7</b> (%8 %)<br/>"
|
|
"Non active area (module gap and fill): <b>%9</b> (%10 %)")
|
|
.arg(mask_stats.error_pixel).arg(QString::number(mask_stats.error_pixel / total_pixels * 100.0, 'f', 1))
|
|
.arg(mask_stats.noisy_pixel).arg(QString::number(mask_stats.noisy_pixel / total_pixels * 100.0, 'f', 1))
|
|
.arg(mask_stats.user_mask).arg(QString::number(mask_stats.user_mask / total_pixels * 100.0, 'f', 1))
|
|
.arg(mask_stats.chip_gap_pixel).arg(QString::number(mask_stats.chip_gap_pixel / total_pixels * 100.0, 'f', 1))
|
|
.arg(mask_stats.module_gap_pixel).arg(QString::number(mask_stats.module_gap_pixel / total_pixels * 100.0, 'f', 1))
|
|
);
|
|
|
|
if (!image->Dataset().indexing_result.empty()) {
|
|
QString tooltip;
|
|
|
|
auto latt = image->ImageData().indexing_lattice;
|
|
if (latt) {
|
|
text = QString("<span style=\"color: purple;\">%1</span>")
|
|
.arg(mkUnitCell(latt->GetUnitCell()));
|
|
|
|
auto vec0 = latt->Vec0();
|
|
auto vec1 = latt->Vec1();
|
|
auto vec2 = latt->Vec2();
|
|
|
|
tooltip = QString("<b>Lattice vectors (Å):</b><br/>"
|
|
"a = (%1, %2, %3)<br/>"
|
|
"b = (%4, %5, %6)<br/>"
|
|
"c = (%7, %8, %9)")
|
|
.arg(vec0.x, 7, 'f', 1)
|
|
.arg(vec0.y, 7, 'f', 1)
|
|
.arg(vec0.z, 7, 'f', 1)
|
|
.arg(vec1.x, 7, 'f', 1)
|
|
.arg(vec1.y, 7, 'f', 1)
|
|
.arg(vec1.z, 7, 'f', 1)
|
|
.arg(vec2.x, 7, 'f', 1)
|
|
.arg(vec2.y, 7, 'f', 1)
|
|
.arg(vec2.z, 7, 'f', 1);
|
|
|
|
if (image->ImageData().lattice_type) {
|
|
tooltip += QString("<br/><br/>");
|
|
tooltip += mkSymmetry(*image->ImageData().lattice_type);
|
|
} else if (image->Dataset().experiment.GetSpaceGroupNumber()) {
|
|
tooltip += QString("<br/><br/>Space group (user-provided): <b>%1</b>")
|
|
.arg(QString::fromStdString(image->Dataset().experiment.GetSpaceGroupName()));
|
|
}
|
|
} else
|
|
text = QString("<span style=\"color: red;\"><b>No lattice</b></span>");
|
|
|
|
indexed->setToolTip(tooltip);
|
|
indexed->setText(text);
|
|
} else {
|
|
indexed->setText("N/A");
|
|
}
|
|
|
|
if (image->ImageData().indexing_lattice_count.value_or(0) > 1)
|
|
multiple_lattices->setText("<span style=\"color: red;\"><b>Multiple lattices detected</b></span>");
|
|
else
|
|
multiple_lattices->setText("");
|
|
}
|