Build Packages / Create release (push) Successful in 23s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 10m6s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 9m6s
Build Packages / build:viewer-tgz:cpu (push) Successful in 11m15s
Build Packages / build:viewer-tgz:cuda (push) Successful in 12m21s
Build Packages / build:windows:nocuda (push) Successful in 17m9s
Build Packages / build:windows:cuda (push) Successful in 19m49s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m42s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 16m0s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m54s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 17m7s
Build Packages / build:rugnux:windows (push) Successful in 10m47s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 17m4s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 17m8s
Build Packages / Generate python client (push) Successful in 45s
Build Packages / Build documentation (push) Successful in 1m45s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m23s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 19m20s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m43s
Build Packages / build:rpm (rocky8) (push) Successful in 19m31s
Build Packages / build:rpm (rocky9) (push) Successful in 20m16s
Build Packages / Unit tests (push) Successful in 1h41m19s
* Fixed a `jfjoch_broker` crash during indexing: sorting no longer misbehaves on non-finite values, and GPU FFT indexer kernel launches are now error-checked. * rugnux needs about a third less peak memory to scale, merge and post-refine rotation data, with identical results. * `rugnux --model`: the placed coordinate file carries the space group its own coordinates obey, and says so when that is not the group the reflection files beside it carry. * `jfjoch_viewer`: fixes in the dataset plots, inspector and layout; spot markers lose their black outline by default (a checkbox under "Image features" restores it) and the highest-pixel markers are white boxes around the pixel. Reviewed-on: #80 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
448 lines
19 KiB
C++
448 lines
19 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>
|
|
|
|
// Two aligned rows - the lengths over their angles, so that alpha sits below a, beta below b,
|
|
// gamma below c. Qt's rich text does not inherit a surrounding colour into a table, so the
|
|
// colour is applied per cell.
|
|
static QString mkUnitCell(const UnitCell &uc, const char *color = nullptr) {
|
|
const QString style = color ? QString(" style=\"color: %1;\"").arg(color) : QString();
|
|
auto num = [&style](double v) {
|
|
// Padded to three integer digits with figure spaces (digit-wide), so the columns hold
|
|
// still as values change magnitude; no axis reaches 1000 Å and no angle 1000°.
|
|
QString s = QString::number(v, 'f', 1);
|
|
while (s.size() < 5)
|
|
s.prepend(QChar(0x2007));
|
|
return QString("<td align=\"right\"%1><b>%2</b> </td>")
|
|
.arg(style).arg(s);
|
|
};
|
|
auto unit = [&style](const char *u) {
|
|
return QString("<td%1>%2</td>").arg(style).arg(u);
|
|
};
|
|
return "<table cellspacing=\"0\" cellpadding=\"0\"><tr>"
|
|
+ num(uc.a) + num(uc.b) + num(uc.c) + unit("Å")
|
|
+ "</tr><tr>"
|
|
+ num(uc.alpha) + num(uc.beta) + num(uc.gamma) + unit("°")
|
|
+ "</tr></table>";
|
|
}
|
|
|
|
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.
|
|
// A dataset name is long and wraps: keep three lines of room from the start, so the rows below
|
|
// do not jump as one dataset's name wraps further than the next one's.
|
|
dataset_name = new QLabel(this);
|
|
dataset_name->setWordWrap(true);
|
|
dataset_name->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
|
dataset_name->setMinimumHeight(3 * fontMetrics().lineSpacing());
|
|
auto *dataset_name_label = new QLabel("Dataset:");
|
|
dataset_name_label->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
|
layout->addRow(dataset_name_label, 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 never breaks inside a word, and a dataset name is one long word. A zero-width space
|
|
// is a break opportunity the wrap takes only when it needs one, so marking "/" first, then "_",
|
|
// then every few characters of a run with neither, wraps at a path separator where one fits, at
|
|
// an underscore next, and mid-name only for a component too long to fit a line at all.
|
|
const QChar zwsp(0x200B);
|
|
QString path;
|
|
int run = 0; // characters since the last break opportunity
|
|
for (const QChar c: QString::fromStdString(exp.GetFilePrefix())) {
|
|
path.append(c);
|
|
if (c == '/' || c == '_') {
|
|
path.append(zwsp);
|
|
run = 0;
|
|
} else if (++run >= 12) {
|
|
path.append(zwsp);
|
|
run = 0;
|
|
}
|
|
}
|
|
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("Goniometer 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()) {
|
|
// One decimal, and an angle that rounds to zero reads "0.0" - a stage that sits a hundredth
|
|
// of a degree below zero should not be reported as "-0.0".
|
|
auto head_angle = [](float deg) {
|
|
return QString::number(std::abs(deg) < 0.05f ? 0.0f : deg, 'f', 1);
|
|
};
|
|
smargon_label->setText("Smargon:");
|
|
smargon_angles->setText(QString("chi <b>%1</b>° / phi <b>%2</b>°")
|
|
.arg(head_angle(head->chi_deg))
|
|
.arg(head_angle(head->phi_deg)));
|
|
} 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', 2));
|
|
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 = mkUnitCell(latt->GetUnitCell(), "purple");
|
|
|
|
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
|
|
// Not indexed is a normal state, not an error - grey, so red keeps meaning trouble.
|
|
text = QString("<span style=\"color: gray;\">No lattice</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("");
|
|
}
|