Kstars

aberrationinspector.cpp
1/*
2 SPDX-FileCopyrightText: 2023 John Evans <john.e.evans.email@googlemail.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "aberrationinspector.h"
8#include "aberrationinspectorplot.h"
9#include "sensorgraphic.h"
10#include <kstars_debug.h>
11#include "kstars.h"
12#include "Options.h"
13#include <QSplitter>
14
15const float RADIANS2DEGREES = 360.0f / (2.0f * M_PI);
16
17namespace Ekos
18{
19
20AberrationInspector::AberrationInspector(const abInsData &data, const QVector<int> &positions,
21 const QVector<QVector<double>> &measures, const QVector<QVector<double>> &weights,
22 const QVector<QVector<int>> &numStars, const QVector<QPoint> &tileCenterOffsets) :
23 m_data(data), m_positions(positions), m_measures(measures), m_weights(weights),
24 m_numStars(numStars), m_tileOffsets(tileCenterOffsets)
25{
26#ifdef Q_OS_MACOS
27 setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
28#endif
29
30 // 1. Setup the GUI
31 setupUi(this);
32 setupGUI();
33
34 // 2. Initialise the widget
35 initAberrationInspector();
36
37 // 3. Curve fit the data for each tile and update Aberration Inspector with results
38 fitCurves();
39
40 // 4. Initialise the 3D graphic
41 initGraphic();
42
43 // 5. Restore persisted settings
44 loadSettings();
45
46 // 6. connect signals to persist changes to user settings
47 connectSettings();
48
49 // Display data appropriate to the tile selection
50 setTileSelection(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
51}
52
53AberrationInspector::~AberrationInspector()
54{
55}
56
57void AberrationInspector::setupGUI()
58{
59 // Set the title. Use run number to differentiate runs
60 this->setWindowTitle(i18n("Aberration Inspector - Run %1", m_data.run));
61
62 // Connect up button callbacks
63 connect(aberrationInspectorButtonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, [this]()
64 {
65 this->done(QDialog::Accepted);
66 });
67
68 connect(abInsTileSelection, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [&](int index)
69 {
70 setTileSelection(static_cast<TileSelection>(index));
71 });
72
73 connect(abInsShowLabels, &QCheckBox::toggled, this, [&](bool setting)
74 {
75 setShowLabels(setting);
76 });
77
78 connect(abInsShowCFZ, &QCheckBox::toggled, this, [&](bool setting)
79 {
80 setShowCFZ(setting);
81 });
82
83 connect(abInsOptCentres, &QCheckBox::toggled, this, [&](bool setting)
84 {
85 setOptCentres(setting);
86 });
87
88 // Create a plot widget and add to the top of the dialog
89 m_plot = new AberrationInspectorPlot(this);
90 abInsPlotLayout->addWidget(m_plot);
91
92 // Setup the results table
93 QStringList Headers { i18n("Tile"), i18n("Description"), i18n("Solution"), i18n("Delta (ticks)"), i18n("Delta (μm)"), i18n("Num Stars"), i18n("R²"), i18n("Exclude")};
94 abInsTable->setColumnCount(Headers.count());
95 abInsTable->setHorizontalHeaderLabels(Headers);
96
97 // Setup tooltips on column headers
98 abInsTable->horizontalHeaderItem(0)->setToolTip(i18n("Tile"));
99 abInsTable->horizontalHeaderItem(1)->setToolTip(i18n("Description"));
100 abInsTable->horizontalHeaderItem(2)->setToolTip(i18n("Focuser Solution"));
101 abInsTable->horizontalHeaderItem(3)->setToolTip(i18n("Delta from central tile in ticks"));
102 abInsTable->horizontalHeaderItem(4)->setToolTip(i18n("Delta from central tile in micrometers"));
103 abInsTable->horizontalHeaderItem(5)->setToolTip(i18n("Min / max number of stars detected in the focus run"));
104 abInsTable->horizontalHeaderItem(6)->setToolTip(i18n("R²"));
105 abInsTable->horizontalHeaderItem(7)->setToolTip(i18n("Check to exclude row from calculations"));
106
107 // Prevent editing of table widget (except the exclude checkboxes) unless in production support mode
110 abInsTable->setEditTriggers(editTrigger);
111
112 // Connect up table widget events: cellEntered when mouse moves over a cell
113 connect(abInsTable, &AbInsTableWidget::cellEntered, this, &AberrationInspector::newMousePos);
114 // leaveTableEvent is signalled when the mouse is moved away from "table".
115 connect(abInsTable, &AbInsTableWidget::leaveTableEvent, this, &AberrationInspector::leaveTableEvent);
116
117 // Setup a SensorGraphic minimal window that acts like a visual tooltip
118 sensorGraphic = new SensorGraphic(abInsTable, m_data.sensorWidth, m_data.sensorHeight, m_data.tileWidth);
119}
120
121// Mouse over table row, col. Show the sensor graphic
122void AberrationInspector::newMousePos(int row, int column)
123{
124 if (column <= 1)
125 {
126 QTableWidgetItem *tile = abInsTable->item(row, 0);
127 for (int i = 0; i < NUM_TILES; i++)
128 {
129 if (TILE_NAME[i] == tile->text())
130 {
131 // Set the highlight row
132 sensorGraphic->setHighlight(i);
133 // Move the graphic to the mouse position
134 sensorGraphic->move(QCursor::pos());
135 // If the graphic is hidden then show it; if details have changed then repaint it
136 if (m_HighlightedRow != -1 && m_HighlightedRow != i)
137 sensorGraphic->repaint();
138 else
139 sensorGraphic->show();
140 m_HighlightedRow = row;
141 return;
142 }
143 }
144 }
145 m_HighlightedRow = -1;
146 sensorGraphic->hide();
147}
148
149// Called when table widget gets a mouse leave event; hide the sensor graphic
150void AberrationInspector::leaveTableEvent()
151{
152 m_HighlightedRow = -1;
153 if (sensorGraphic)
154 sensorGraphic->hide();
155}
156
157// Called when the "Exclude" checkbox state is changed by the user
158void AberrationInspector::onStateChanged(int state)
159{
160 Q_UNUSED(state);
161
162 // Get the row and state of the checkbox
163 QCheckBox* cb = qobject_cast<QCheckBox *>(QObject::sender());
164 int row = cb->property("row").toInt();
165 bool checked = cb->isChecked();
166
167 // Set the exclude flag for the excluded tile
168 setExcludeTile(row, checked, static_cast<TileSelection>(abInsTileSelection->currentIndex()));
169 // Rerun the calcs so the newly changed exclude flag is taken into account
170 setTileSelection(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
171}
172
173// Called when table cell has been changed
174// This is a production support debug feature that is contolled by ABINS_DEBUG flag
175// For normal use table editing is disabled and this function not called.
176void AberrationInspector::onCellChanged(int row, int column)
177{
178 if (column != 3)
179 return;
180
181 // Get the new value
182 QTableWidgetItem *item = abInsTable->item(row, column);
183 int value = item->text().toInt();
184
185 int tile = getTileFromRow(static_cast<TileSelection>(abInsTileSelection->currentIndex()), row);
186
187 if (tile >= 0 && tile < NUM_TILES)
188 {
189 m_minimum[tile] = value + m_minimum[TILE_CM];
190 // Rerun the calcs so the newly changed exclude flag is taken into account
191 setTileSelection(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
192 }
193}
194
195int AberrationInspector::getTileFromRow(TileSelection tileSelection, int row)
196{
197 int tile = -1;
198
199 if (row < 0 || row >= NUM_TILES)
200 {
201 qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid row %2").arg(__FUNCTION__).arg(row);
202 return tile;
203 }
204
205 switch(tileSelection)
206 {
207 case TileSelection::TILES_ALL:
208 // Use all tiles
209 tile = row;
210 break;
211
212 case TileSelection::TILES_OUTER_CORNERS:
213 // Use tiles 0, 2, 4, 6, 8
214 tile = row * 2;
215 break;
216
217 case TileSelection::TILES_INNER_DIAMOND:
218 // Use tiles 1, 3, 4, 5, 7
219 if (row < 2)
220 tile = (row * 2) + 1;
221 else if (row == 2)
222 tile = 4;
223 else
224 tile = (row * 2) - 1;
225 break;
226
227 default:
228 qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called but TileSelection invalid").arg(__FUNCTION__);
229 break;
230 }
231 return tile;
232}
233
234void AberrationInspector::setExcludeTile(int row, bool checked, TileSelection tileSelection)
235{
236 if (row < 0 || row >= NUM_TILES)
237 {
238 qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid row %2").arg(__FUNCTION__).arg(row);
239 return;
240 }
241
242 int tile = getTileFromRow(tileSelection, row);
243 if (tile >= 0 && tile < NUM_TILES)
244 m_excludeTile[tile] = checked;
245}
246
247void AberrationInspector::connectSettings()
248{
249 // All Combo Boxes
250 for (auto &oneWidget : findChildren<QComboBox*>())
251 connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::AberrationInspector::syncSettings);
252
253 // All Checkboxes, except Sim mode checkbox - this should be defaulted to off when Aberratio Inspector starts
254 for (auto &oneWidget : findChildren<QCheckBox*>())
255 if (oneWidget != abInsSimMode)
256 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::AberrationInspector::syncSettings);
257
258 // All Splitters
259 for (auto &oneWidget : findChildren<QSplitter*>())
260 connect(oneWidget, &QSplitter::splitterMoved, this, &Ekos::AberrationInspector::syncSettings);
261}
262
263void AberrationInspector::loadSettings()
264{
265 QString key;
266 QVariant value;
267
268 // All Combo Boxes
269 for (auto &oneWidget : findChildren<QComboBox*>())
270 {
271 key = oneWidget->objectName();
272 value = Options::self()->property(key.toLatin1());
273 if (value.isValid())
274 oneWidget->setCurrentText(value.toString());
275 else
276 qCDebug(KSTARS_EKOS_FOCUS) << "ComboBox Option" << key << "not found!";
277 }
278
279 // All Checkboxes
280 for (auto &oneWidget : findChildren<QCheckBox*>())
281 {
282 key = oneWidget->objectName();
283 if (key == abInsSimMode->objectName() || key == "")
284 // Sim mode setting isn't persisted as it is always off on Aberration Inspector start.
285 // Also the table widget has a column of checkboxes whose value is data dependent so these aren't persisted
286 continue;
287
288 value = Options::self()->property(key.toLatin1());
289 if (value.isValid())
290 oneWidget->setChecked(value.toBool());
291 else
292 qCDebug(KSTARS_EKOS_FOCUS) << "Checkbox Option" << key << "not found!";
293 }
294
295 // All Splitters
296 for (auto &oneWidget : findChildren<QSplitter*>())
297 {
298 key = oneWidget->objectName();
299 value = Options::self()->property(key.toLatin1());
300 if (value.isValid())
301 {
302 // Convert the saved QString to a QByteArray using Base64
303 auto valueBA = QByteArray::fromBase64(value.toString().toUtf8());
304 oneWidget->restoreState(valueBA);
305 }
306 else
307 qCDebug(KSTARS_EKOS_FOCUS) << "Splitter Option" << key << "not found!";
308 }
309}
310
311void AberrationInspector::syncSettings()
312{
313 QComboBox *cbox = nullptr;
314 QCheckBox *cb = nullptr;
315 QSplitter *s = nullptr;
316
317 QString key;
318 QVariant value;
319
320 if ( (cbox = qobject_cast<QComboBox*>(sender())))
321 {
322 key = cbox->objectName();
323 value = cbox->currentText();
324 }
325 else if ( (cb = qobject_cast<QCheckBox*>(sender())))
326 {
327 key = cb->objectName();
328 value = cb->isChecked();
329 }
330 else if ( (s = qobject_cast<QSplitter*>(sender())))
331 {
332 key = s->objectName();
333 // Convert from the QByteArray to QString using Base64
334 value = QString::fromUtf8(s->saveState().toBase64());
335 }
336
337 // Save changed setting
338 Options::self()->setProperty(key.toLatin1(), value);
339 Options::self()->save();
340}
341
342// Setup display widgets to match tileSelection
343void AberrationInspector::setTileSelection(TileSelection tileSelection)
344{
345 // Setup array of tiles to use, based on user selection
346 setupTiles(tileSelection);
347 // Redraw the v-curves based on user selection
348 m_plot->redrawCurve(m_useTile);
349 // Update the table widget based on user selection
350 updateTable();
351 // Update the results based on user selection
352 analyseResults();
353 // Update the 3D graphic based on user selection
354 updateGraphic(tileSelection);
355 // resize table widget based on contents
356 tableResize();
357 // Updates changes to the v-curves
358 m_plot->replot();
359}
360
361void AberrationInspector::setupTiles(TileSelection tileSelection)
362{
363 switch(tileSelection)
364 {
365 case TileSelection::TILES_ALL:
366 // Use all tiles
367 for (int i = 0; i < NUM_TILES; i++)
368 m_useTile[i] = true;
369 break;
370
371 case TileSelection::TILES_OUTER_CORNERS:
372 // Use tiles TL, TR, CM, BL, BR
373 m_useTile[TILE_TL] = m_useTile[TILE_TR] = m_useTile[TILE_CM] = m_useTile[TILE_BL] = m_useTile[TILE_BR] = true;
374 m_useTile[TILE_TM] = m_useTile[TILE_CL] = m_useTile[TILE_CR] = m_useTile[TILE_BM] = false;
375 break;
376
377 case TileSelection::TILES_INNER_DIAMOND:
378 // Use tiles TM, CL, CM, CR, BM
379 m_useTile[TILE_TL] = m_useTile[TILE_TR] = m_useTile[TILE_BL] = m_useTile[TILE_BR] = false;
380 m_useTile[TILE_TM] = m_useTile[TILE_CL] = m_useTile[TILE_CM] = m_useTile[TILE_CR] = m_useTile[TILE_BM] = true;
381 break;
382
383 default:
384 qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid tile selection %2").arg(__FUNCTION__).arg(tileSelection);
385 break;
386 }
387}
388
389void AberrationInspector::setShowLabels(bool setting)
390{
391 m_plot->setShowLabels(setting);
392 m_plot->replot();
393}
394
395void AberrationInspector::setShowCFZ(bool setting)
396{
397 m_plot->setShowCFZ(setting);
398 m_plot->replot();
399}
400
401// Rerun calculations to take the Optimise Tile Centres setting into account
402void AberrationInspector::setOptCentres(bool setting)
403{
404 Q_UNUSED(setting);
405 setTileSelection(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
406}
407
408void AberrationInspector::initAberrationInspector()
409{
410 // Initialise the plot widget
411 m_plot->init(m_data.yAxisLabel, m_data.starUnits, m_data.useWeights, abInsShowLabels->isChecked(),
412 abInsShowCFZ->isChecked());
413}
414
415// Resize the dialog to the data
416void AberrationInspector::tableResize()
417{
418 // Resize the table columns to fit the data on display in it.
419 abInsTable->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);
420 abInsTable->verticalHeader()->resizeSections(QHeaderView::ResizeToContents);
421}
422
423// Run curve fitting on the collected data for each tile, updating other widgets as we go
424void AberrationInspector::fitCurves()
425{
426 curveFitting.reset(new CurveFitting());
427
428 const double expected = 0.0;
429 int minPos, maxPos;
430
431 QVector<bool> outliers;
432 for (int i = 0; i < m_positions.count(); i++)
433 {
434 outliers.append(false);
435 if (i == 0)
436 minPos = maxPos = m_positions[i];
437 else
438 {
439 minPos = std::min(minPos, m_positions[i]);
440 maxPos = std::max(maxPos, m_positions[i]);
441 }
442 }
443
444 for (int tile = 0; tile < m_measures.count(); tile++)
445 {
446 curveFitting->fitCurve(CurveFitting::FittingGoal::BEST, m_positions, m_measures[tile], m_weights[tile], outliers,
447 m_data.curveFit, m_data.useWeights, m_data.optDir);
448
449 double position = 0.0;
450 double measure = 0.0;
451 double R2 = 0.0;
452 bool foundFit = curveFitting->findMinMax(expected, static_cast<double>(minPos), static_cast<double>(maxPos), &position,
453 &measure, m_data.curveFit, m_data.optDir);
454 if (foundFit)
455 R2 = curveFitting->calculateR2(m_data.curveFit);
456
457 position = round(position);
458 m_minimum.append(position);
459 m_minMeasure.append(measure);
460 m_fit.append(foundFit);
461 m_R2.append(R2);
462
463 // Add the datapoints to the plot for the current tile
464 // JEE Need to sort out what to do with outliers... for now ignore them
465 QVector<bool> outliers;
466 for (int i = 0; i < m_measures[tile].count(); i++)
467 {
468 outliers.append(false);
469 }
470
471 m_plot->addData(m_positions, m_measures[tile], m_weights[tile], outliers);
472 // Fit the curve - note this needs curveFitting with the parameters for the current solution
473 m_plot->drawCurve(tile, curveFitting.get(), position, measure, foundFit, R2);
474 // Draw solutions on the plot
475 m_plot->drawMaxMin(tile, position, measure);
476 // Draw the CFZ for the central tile
477 if (tile == TILE_CM)
478 m_plot->drawCFZ(position, measure, m_data.cfzSteps);
479 }
480}
481
482// Update the results table
483void AberrationInspector::updateTable()
484{
485 // Disconnect the cell changed callback to stop it firing whilst the table is updated
486 disconnect(abInsTable, &AbInsTableWidget::cellChanged, this, &AberrationInspector::onCellChanged);
487
488 if (abInsTileSelection->currentIndex() == TILES_ALL)
489 abInsTable->setRowCount(NUM_TILES);
490 else
491 abInsTable->setRowCount(5);
492
493 int rowCounter = -1;
494 for (int i = 0; i < NUM_TILES; i++)
495 {
496 if (!m_useTile[i])
497 continue;
498
499 ++rowCounter;
500
501 QTableWidgetItem *tile = new QTableWidgetItem(TILE_NAME[i]);
502 tile->setForeground(QColor(TILE_COLOUR[i]));
503 abInsTable->setItem(rowCounter, 0, tile);
504
505 QTableWidgetItem *description = new QTableWidgetItem(TILE_LONGNAME[i]);
506 abInsTable->setItem(rowCounter, 1, description);
507
508 QTableWidgetItem *solution = new QTableWidgetItem(QString::number(m_minimum[i]));
510 abInsTable->setItem(rowCounter, 2, solution);
511
512 int ticks = m_minimum[i] - m_minimum[TILE_CM];
513 QTableWidgetItem *deltaTicks = new QTableWidgetItem(QString::number(ticks));
514 deltaTicks->setTextAlignment(Qt::AlignRight);
515 abInsTable->setItem(rowCounter, 3, deltaTicks);
516
517 int microns = ticks * m_data.focuserStepMicrons;
518 QTableWidgetItem *deltaMicrons = new QTableWidgetItem(QString::number(microns));
519 deltaMicrons->setTextAlignment(Qt::AlignRight);
520 abInsTable->setItem(rowCounter, 4, deltaMicrons);
521
522 int minNumStars = *std::min_element(m_numStars[i].constBegin(), m_numStars[i].constEnd());
523 int maxNumStars = *std::max_element(m_numStars[i].constBegin(), m_numStars[i].constEnd());
524 QTableWidgetItem *numStars = new QTableWidgetItem(QString("%1 / %2").arg(minNumStars).arg(maxNumStars));
526 abInsTable->setItem(rowCounter, 5, numStars);
527
528 QTableWidgetItem *R2 = new QTableWidgetItem(QString("%1").arg(m_R2[i], 0, 'f', 2));
530 abInsTable->setItem(rowCounter, 6, R2);
531
532 QWidget *checkBoxWidget = new QWidget(abInsTable);
533 QCheckBox *checkBox = new QCheckBox();
534 // Set the checkbox based on whether curve fitting worked
535 checkBox->setChecked(!m_fit[i] || m_excludeTile[i]);
536 checkBox->setEnabled(m_fit[i]);
537 // Add a property to identify the row when the user changes the check state.
538 checkBox->setProperty("row", rowCounter);
539 // In order to centre the widget, we need to insert it into a layout and align that
540 QHBoxLayout *layoutCheckBox = new QHBoxLayout(checkBoxWidget);
541 layoutCheckBox->addWidget(checkBox);
542 layoutCheckBox->setAlignment(Qt::AlignCenter);
543
544 abInsTable->setCellWidget(rowCounter, 7, checkBoxWidget);
545 // The tableWidget cellChanged event doesn't fire when the checkbox state is changed.
546 // Seems like the only way to get the event is to connect up directly to the checkbox
547 connect(checkBox, &QCheckBox::stateChanged, this, &AberrationInspector::onStateChanged);
548 }
549 connect(abInsTable, &AbInsTableWidget::cellChanged, this, &AberrationInspector::onCellChanged);
550
551 // Update the sensor graphic with the current tile selection
552 sensorGraphic->setTiles(m_useTile);
553}
554
555// Analyse the results for backfocus delta and tilt based on tile selection.
556void AberrationInspector::analyseResults()
557{
558 // Backfocus
559 // +result = move sensor nearer field flattener
560 // -result = move sensor further from field flattener
561 bool backfocusOK = calcBackfocusDelta(static_cast<TileSelection>(abInsTileSelection->currentIndex()), m_backfocus);
562
563 // Update backfocus screen widgets
564 if (!backfocusOK)
565 backfocus->setText(i18n("N/A"));
566 else
567 {
568 QString side = "";
569 if (m_backfocus < -0.9)
570 side = i18n("Move sensor nearer flattener");
571 else if (m_backfocus > 0.9)
572 side = i18n("Move sensor away from flattener");
573 backfocus->setText(QString("%1μm. %2").arg(m_backfocus, 0, 'f', 0).arg(side));
574 }
575
576 // Tilt
577 bool tiltOK = calcTilt();
578
579 // Update tilt screen widgets
580 if (!tiltOK)
581 {
582 lrTilt->setText(i18n("N/A"));
583 tbTilt->setText(i18n("N/A"));
584 totalTilt->setText(i18n("N/A"));
585 }
586 else
587 {
588 lrTilt->setText(QString("%1μm / %2%").arg(m_LRMicrons, 0, 'f', 0).arg(m_LRTilt, 0, 'f', 2));
589 tbTilt->setText(QString("%1μm / %2%").arg(m_TBMicrons, 0, 'f', 0).arg(m_TBTilt, 0, 'f', 2));
590 totalTilt->setText(QString("%1μm / %2%").arg(m_diagonalMicrons, 0, 'f', 0)
591 .arg(m_diagonalTilt, 0, 'f', 2));
592 }
593
594 // Set m_resultsOK dependent on whether calcs were successful or not
595 m_resultsOK = backfocusOK && tiltOK;
596}
597
598// Calculate the backfocus in microns
599// Note that the tile positions are at different distances from the sensor centre so we need to weight
600// values by the distance to tile centre. Also, we only include tiles for which curve fitting worked
601// and for which the user hasn't elected to exclude
602bool AberrationInspector::calcBackfocusDelta(TileSelection tileSelection, double &backfocusDelta)
603{
604 backfocusDelta = 0.0;
605
606 // Firstly check that we have a valid central tile - we can't do anything without that
607 if (!m_fit[TILE_CM] || m_excludeTile[TILE_CM])
608 return false;
609
610 double dist = 0.0, sum = 0.0, counter = 0.0;
611
612 switch(tileSelection)
613 {
614 case TileSelection::TILES_ALL:
615 // Use all useable tiles weighted by their distance from the centre
616 for (int i = 0; i < NUM_TILES; i++)
617 {
618 if (i == TILE_CM)
619 continue;
620
621 if (m_fit[i] && !m_excludeTile[i])
622 {
623 dist = getXYTileCentre(static_cast<tileID>(i)).length();
624 sum += m_minimum[i] * dist;
625 counter += dist;
626 }
627 }
628 if (counter == 0)
629 // No valid tiles so can't complete the calc
630 return false;
631 break;
632
633 case TileSelection::TILES_OUTER_CORNERS:
634 // All tiles are diagonal from centre... so no need to weight the calc
635 // Use tiles 0, 2, 6, 8
636 if (m_fit[0] && !m_excludeTile[0])
637 {
638 sum += m_minimum[0];
639 counter++;
640 }
641 if (m_fit[2] && !m_excludeTile[2])
642 {
643 sum += m_minimum[2];
644 counter++;
645 }
646 if (m_fit[6] && !m_excludeTile[6])
647 {
648 sum += m_minimum[6];
649 counter++;
650 }
651 if (m_fit[8] && !m_excludeTile[8])
652 {
653 sum += m_minimum[8];
654 counter++;
655 }
656
657 if (counter == 0)
658 // No valid tiles so can't complete the calc
659 return false;
660 break;
661
662 case TileSelection::TILES_INNER_DIAMOND:
663 // Tiles are different distances from centre... so need to weight the calc
664 // Use tiles 1, 3, 5, 7
665 if (m_fit[TILE_TM] && !m_excludeTile[TILE_TM])
666 {
667 dist = getXYTileCentre(TILE_TM).length();
668 sum += m_minimum[TILE_TM] * dist;
669 counter += dist;
670 }
671 if (m_fit[TILE_CL] && !m_excludeTile[TILE_CL])
672 {
673 dist = getXYTileCentre(TILE_CL).length();
674 sum += m_minimum[TILE_CL] * dist;
675 counter += dist;
676 }
677 if (m_fit[TILE_CR] && !m_excludeTile[TILE_CR])
678 {
679 dist = getXYTileCentre(TILE_CR).length();
680 sum += m_minimum[TILE_CR] * dist;
681 counter += dist;
682 }
683 if (m_fit[TILE_BM] && !m_excludeTile[TILE_BM])
684 {
685 dist = getXYTileCentre(TILE_BM).length();
686 sum += m_minimum[TILE_BM] * dist;
687 counter += dist;
688 }
689
690 if (counter == 0)
691 // No valid tiles so can't complete the calc
692 return false;
693 break;
694
695 default:
696 qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid tile selection %2").arg(__FUNCTION__).arg(tileSelection);
697 return false;
698 break;
699 }
700 backfocusDelta = (m_minimum[TILE_CM] - (sum / counter)) * m_data.focuserStepMicrons;
701 return true;
702}
703
704bool AberrationInspector::calcTilt()
705{
706 // Firstly check that we have a valid central tile - we can't do anything without that
707 if (!m_fit[TILE_CM] || m_excludeTile[TILE_CM])
708 return false;
709
710 // Calculate the deltas relative to the centre tile in microns
711 m_deltas.clear();
712 for (int tile = 0; tile < NUM_TILES; tile++)
713 m_deltas.append((m_minimum[TILE_CM] - m_minimum[tile]) * m_data.focuserStepMicrons);
714
715 // Calculate the average tile position for left, right, top and bottom. If any of these cannot be
716 // calculated then fail the whole calculation.
717 double avLeft, avRight, avTop, avBottom;
718 int tilesL[3] = { 0, 3, 6 };
719 if (!avTiles(tilesL, avLeft))
720 return false;
721 int tilesR[3] = { 2, 5, 8 };
722 if (!avTiles(tilesR, avRight))
723 return false;
724 int tilesT[3] = { 0, 1, 2 };
725 if (!avTiles(tilesT, avTop))
726 return false;
727 int tilesB[3] = { 6, 7, 8 };
728 if(!avTiles(tilesB, avBottom))
729 return false;
730
731 m_LRMicrons = avLeft - avRight;
732 m_TBMicrons = avTop - avBottom;
733 m_diagonalMicrons = std::hypot(m_LRMicrons, m_TBMicrons);
734
735 // Calculate the sensor spans in microns
736 const double LRSpan = (m_data.sensorWidth - m_data.tileWidth) * m_data.pixelSize;
737 const double TBSpan = (m_data.sensorHeight - m_data.tileWidth) * m_data.pixelSize;
738
739 // Calculate the tilt as a % slope
740 m_LRTilt = m_LRMicrons / LRSpan * 100.0;
741 m_TBTilt = m_TBMicrons / TBSpan * 100.0;
742 m_diagonalTilt = std::hypot(m_LRTilt, m_TBTilt);
743 return true;
744}
745
746// Averages upto 3 passed in tile values.
747bool AberrationInspector::avTiles(int tiles[3], double &average)
748{
749 double sum = 0.0;
750 int counter = 0;
751 for (int i = 0; i < 3; i++)
752 {
753 if (m_useTile[tiles[i]] && m_fit[tiles[i]] && !m_excludeTile[tiles[i]])
754 {
755 sum += m_deltas[tiles[i]];
756 counter++;
757 }
758 }
759 if (counter > 0)
760 average = sum / counter;
761 return (counter > 0);
762}
763
764// Initialise the 3D graphic
765void AberrationInspector::initGraphic()
766{
767 // Create a 3D Surface widget and add to the dialog
768 m_graphic = new Q3DSurface();
769 QWidget *container = QWidget::createWindowContainer(m_graphic);
770
771 // abInsHSplitter is created in the .ui file but, by default, doesn't work - don't know why
772 // Workaround is to create a new QSplitter object and use that.
773 abInsHSplitter = new QSplitter(abInsVSplitter);
774 abInsHSplitter->setObjectName(QString::fromUtf8("abInsHSplitter"));
775 abInsHSplitter->addWidget(tableAndResultsWidget);
776 abInsHSplitter->addWidget(widget);
777 hLayout->insertWidget(0, container, 1);
778 auto value = Options::abInsHSplitter();
779 // Convert the saved QString to a QByteArray using Base64
780 auto valueBA = QByteArray::fromBase64(value.toUtf8());
781 abInsHSplitter->restoreState(valueBA);
782 connect(abInsHSplitter, &QSplitter::splitterMoved, this, &Ekos::AberrationInspector::syncSettings);
783
784 connect(abInsSelection, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [&](int index)
785 {
786 if (index == 0)
787 m_graphic->setSelectionMode(QAbstract3DGraph::SelectionNone);
788 else if (index == 1)
789 m_graphic->setSelectionMode(QAbstract3DGraph::SelectionItem);
790 else if (index == 2)
791 m_graphic->setSelectionMode(QAbstract3DGraph::SelectionItemAndColumn | QAbstract3DGraph::SelectionSlice |
792 QAbstract3DGraph::SelectionMultiSeries);
793 });
794
795 connect(abInsTheme, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [&](int index)
796 {
797 m_graphic->activeTheme()->setType(Q3DTheme::Theme(index));
798 });
799
800 connect(abInsLabels, &QCheckBox::toggled, this, [&](bool setting)
801 {
802 m_graphicLabels = setting;
803 updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
804 });
805
806 connect(abInsSensor, &QCheckBox::toggled, this, [&](bool setting)
807 {
808 m_graphicSensor = setting;
809 updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
810 });
811
812 connect(abInsPetzvalWire, &QCheckBox::toggled, this, [&](bool setting)
813 {
814 m_graphicPetzvalWire = setting;
815 updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
816 });
817 connect(abInsPetzvalSurface, &QCheckBox::toggled, this, [&](bool setting)
818 {
819 m_graphicPetzvalSurface = setting;
820 updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
821 });
822
823 // Simulation on/off
824 connect(abInsSimMode, &QCheckBox::toggled, this, &AberrationInspector::simModeToggled);
825
826 m_sensor = new QSurface3DSeries;
827 m_sensorProxy = new QSurfaceDataProxy();
828 m_sensor->setDataProxy(m_sensorProxy);
829 m_graphic->addSeries(m_sensor);
830
831 m_petzval = new QSurface3DSeries;
832 m_petzvalProxy = new QSurfaceDataProxy();
833 m_petzval->setDataProxy(m_petzvalProxy);
834 m_graphic->addSeries(m_petzval);
835
836 m_graphic->axisX()->setTitle("X Axis (μm) - Sensor Left to Right");
837 m_graphic->axisY()->setTitle("Y Axis (μm) - Sensor Top to Bottom");
838 m_graphic->axisZ()->setTitle("Z Axis (μm) - Axis of Telescope");
839
840 m_graphic->axisX()->setLabelFormat("%.0f");
841 m_graphic->axisY()->setLabelFormat("%.0f");
842 m_graphic->axisZ()->setLabelFormat("%.0f");
843
844 m_graphic->axisX()->setTitleVisible(true);
845 m_graphic->axisY()->setTitleVisible(true);
846 m_graphic->axisZ()->setTitleVisible(true);
847
848 m_graphic->activeTheme()->setType(Q3DTheme::ThemePrimaryColors);
849
850 // Set projection and shadows
851 m_graphic->setOrthoProjection(false);
852 m_graphic->setShadowQuality(QAbstract3DGraph::ShadowQualityNone);
853 // Balance the z-axis with the x-y plane. Without this the z-axis is crushed to a very small scale
854 m_graphic->setHorizontalAspectRatio(2.0);
855}
856
857// Simulation on/off switch toggled
858void AberrationInspector::simModeToggled(bool setting)
859{
860 m_simMode = setting;
861 abInsBackfocusSlider->setEnabled(setting);
862 abInsTiltLRSlider->setEnabled(setting);
863 abInsTiltTBSlider->setEnabled(setting);
864 if (!m_simMode)
865 {
866 // Reset the sliders
867 abInsBackfocusSlider->setRange(0, 10);
868 abInsBackfocusSlider->setValue(0);
869 abInsTiltLRSlider->setRange(0, 10);
870 abInsTiltLRSlider->setValue(0);
871 abInsTiltTBSlider->setRange(0, 10);
872 abInsTiltTBSlider->setValue(0);
873
874 // Reset the curve fitting object in case Sim mode has caused any problems. This will ensure the graphic
875 // can always return to its original state after using sim mode.
876 curveFitting.reset(new CurveFitting());
877 }
878 else
879 {
880 // Disconnect slider signals which initialising sliders
881 disconnect(abInsBackfocusSlider, &QSlider::valueChanged, this, &AberrationInspector::simBackfocusChanged);
882 disconnect(abInsTiltLRSlider, &QSlider::valueChanged, this, &AberrationInspector::simLRTiltChanged);
883 disconnect(abInsTiltTBSlider, &QSlider::valueChanged, this, &AberrationInspector::simTBTiltChanged);
884
885 // Setup backfocus slider.
886 int range = 10;
887 abInsBackfocusSlider->setRange(-range, range);
888 int sign = m_backfocus < 0.0 ? -1 : 1;
889 abInsBackfocusSlider->setValue(sign * 5);
890 abInsBackfocusSlider->setTickInterval(1);
891 m_simBackfocus = m_backfocus;
892
893 // Setup Left-to-Right tilt slider.
894 abInsTiltLRSlider->setRange(-range, range);
895 sign = m_LRTilt < 0.0 ? -1 : 1;
896 abInsTiltLRSlider->setValue(sign * 5);
897 abInsTiltLRSlider->setTickInterval(1);
898 m_simLRTilt = m_LRTilt;
899
900 // Setup Top-to-Bottom tilt slider
901 abInsTiltTBSlider->setRange(-range, range);
902 sign = m_TBTilt < 0.0 ? -1 : 1;
903 abInsTiltTBSlider->setValue(sign * 5);
904 abInsTiltTBSlider->setTickInterval(1);
905 m_simTBTilt = m_TBTilt;
906
907 // Now that the sliders have been initialised, connect up signals
908 connect(abInsBackfocusSlider, &QSlider::valueChanged, this, &AberrationInspector::simBackfocusChanged);
909 connect(abInsTiltLRSlider, &QSlider::valueChanged, this, &AberrationInspector::simLRTiltChanged);
910 connect(abInsTiltTBSlider, &QSlider::valueChanged, this, &AberrationInspector::simTBTiltChanged);
911 }
912 updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
913}
914
915// Update the 3D graphic based on user selections
916void AberrationInspector::updateGraphic(TileSelection tileSelection)
917{
918 // If we don't have good results then don't display the 3D graphic
919 bool ok = m_resultsOK;
920
921 if (ok)
922 // Display thw sensor
923 ok = processSensor();
924
925 if (ok)
926 {
927 // Add labels to the sensor
928 processSensorLabels();
929
930 // Draw the Petzval surface (from the field flattener
931 ok = processPetzval(tileSelection);
932 }
933
934 if (ok)
935 {
936 // Setup axes
937 m_graphic->axisX()->setRange(-m_maxX, m_maxX);
938 m_graphic->axisY()->setRange(-m_maxY, m_maxY);
939 m_graphic->axisZ()->setRange(m_minZ * 1.1, m_maxZ * 1.1);
940
941 // Display / don't display the sensor
942 m_graphicSensor ? m_graphic->addSeries(m_sensor) : m_graphic->removeSeries(m_sensor);
943
944 // Display Petzval curve
945 QSurface3DSeries::DrawFlags petzvalDrawMode { 0 };
946 if (m_graphicPetzvalWire)
947 petzvalDrawMode = petzvalDrawMode | QSurface3DSeries::DrawWireframe;
948 if (m_graphicPetzvalSurface)
949 petzvalDrawMode = petzvalDrawMode | QSurface3DSeries::DrawSurface;
950
951 if (petzvalDrawMode)
952 {
953 m_petzval->setDrawMode(petzvalDrawMode);
954 m_graphic->addSeries(m_petzval);
955 }
956 else
957 m_graphic->removeSeries(m_petzval);
958 }
959
960 // Display / don't display the graphic
961 ok ? m_graphic->show() : m_graphic->hide();
962}
963
964// Draw the sensor on the graphic
965bool AberrationInspector::processSensor()
966{
967 // If we are in Sim mode we have previously solved the equation for the plane. All movements in
968 // Sim mode are rotations.
969 // If we are not in Sim mode then resolve, e.g. when tile selection changes
970 if (!m_simMode)
971 {
972 // Fit a plane to the datapoints for the selected tiles. Fit is unweighted
973 CurveFitting::DataPoint3DT plane;
974 plane.useWeights = false;
975 for (int tile = 0; tile < NUM_TILES; tile++)
976 {
977 if (m_useTile[tile])
978 {
979 QVector3D tileCentre = QVector3D(getXYTileCentre(static_cast<Ekos::tileID>(tile)),
980 getBSDelta(static_cast<Ekos::tileID>(tile)));
981 plane.push_back(tileCentre.x(), tileCentre.y(), tileCentre.z());
982 // Update the graph range to accomodate the data
983 m_maxX = std::max(m_maxX, tileCentre.x());
984 m_maxY = std::max(m_maxY, tileCentre.y());
985 m_maxZ = std::max(m_maxZ, tileCentre.z());
986 m_minZ = std::min(m_minZ, tileCentre.z());
987 }
988 }
989 curveFitting->fitCurve3D(plane, CurveFitting::FOCUS_PLANE);
990 double R2 = curveFitting->calculateR2(CurveFitting::FOCUS_PLANE);
991 // JEE need to think about how to handle failure to solve versus boundary condition of R2=0
992 qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 sensor plane solved R2=%2").arg(__FUNCTION__).arg(R2);
993 }
994
995 // We've successfully solved the plane of the sensor so load the sensor vertices in the 3D Surface
996 // getSensorVertex will perform the necessary rotations
997 QSurfaceDataArray *data = new QSurfaceDataArray;
998 QSurfaceDataRow *dataRow1 = new QSurfaceDataRow;
999 QSurfaceDataRow *dataRow2 = new QSurfaceDataRow;
1000 *dataRow1 << getSensorVertex(TILE_TL) << getSensorVertex(TILE_TR);
1001 *dataRow2 << getSensorVertex(TILE_BL) << getSensorVertex(TILE_BR);
1002 *data << dataRow1 << dataRow2;
1003 m_sensorProxy->resetArray(data);
1004 m_sensor->setDrawMode(QSurface3DSeries::DrawSurface);
1005 return true;
1006}
1007
1008void AberrationInspector::processSensorLabels()
1009{
1010 // Now sort out the labels on the sensor
1011 for (int tile = 0; tile < NUM_TILES; tile++)
1012 {
1013 if (m_label[tile] == nullptr)
1014 m_label[tile] = new QCustom3DLabel();
1015 else
1016 {
1017 m_graphic->removeCustomItem(m_label[tile]);
1018 m_label[tile] = new QCustom3DLabel();
1019 }
1020 m_label[tile]->setText(TILE_NAME[tile]);
1021 m_label[tile]->setTextColor(TILE_COLOUR[tile]);
1022 QVector3D pos = getLabelCentre(static_cast<tileID>(tile));
1023 m_label[tile]->setPosition(pos);
1024
1025 m_maxX = std::max(m_maxX, pos.x());
1026 m_maxY = std::max(m_maxY, pos.y());
1027 m_maxZ = std::max(m_maxZ, pos.z());
1028 m_minZ = std::min(m_minZ, pos.z());
1029
1030 QFont font = m_label[tile]->font();
1031 font.setPointSize(500);
1032 m_label[tile]->setFont(font);
1033 m_label[tile]->setFacingCamera(true);
1034 }
1035
1036 for (int tile = 0; tile < NUM_TILES; tile++)
1037 {
1038 if (m_useTile[tile] && m_graphicLabels)
1039 m_graphic->addCustomItem(m_label[tile]);
1040 }
1041}
1042
1043// Draw the Petzval surface on the graphic. Assume a parabolid surface
1044// z = x^2/a^2 + y^2/b^2. Assume symmetry where a = b
1045// a^2 = (x^2 + y^2) / z
1046//
1047// We know that at the measured datapoints (tile centres) the z value = backfocus
1048// This is complicated by sensor tilt, but the previously calculated backfocus is an average value
1049// So we can use this to calculate "a" in the above equation
1050// Note that there are 2 solutions: one giving positive z, the other negative
1051bool AberrationInspector::processPetzval(TileSelection tileSelection)
1052{
1053 float a = 1.0;
1054 double sum = 0.0;
1055 double backfocus = m_simMode ? m_simBackfocus : m_backfocus;
1056 double sign = (backfocus < 0.0) ? -1.0 : 1.0;
1057 backfocus = std::abs(backfocus);
1058 switch (tileSelection)
1059 {
1060 case TileSelection::TILES_ALL:
1061 // Use all tiles
1062 for (int i = 0; i < NUM_TILES; i++)
1063 {
1064 if (i == TILE_CM)
1065 continue;
1066 sum += getXYTileCentre(static_cast<tileID>(i)).lengthSquared();
1067 }
1068 a = sqrt(sum / (8 * backfocus));
1069 break;
1070
1071 case TileSelection::TILES_OUTER_CORNERS:
1072 // Use tiles 0, 2, 6, 8
1073 sum += getXYTileCentre(TILE_TL).lengthSquared() +
1074 getXYTileCentre(TILE_TR).lengthSquared() +
1075 getXYTileCentre(TILE_BL).lengthSquared() +
1076 getXYTileCentre(TILE_BR).lengthSquared();
1077 a = sqrt(sum / (4 * backfocus));
1078 break;
1079
1080 case TileSelection::TILES_INNER_DIAMOND:
1081 // Use tiles 1, 3, 5, 7
1082 sum += getXYTileCentre(TILE_TM).lengthSquared() +
1083 getXYTileCentre(TILE_CL).lengthSquared() +
1084 getXYTileCentre(TILE_CR).lengthSquared() +
1085 getXYTileCentre(TILE_BM).lengthSquared();
1086 a = sqrt(sum / (4 * backfocus));
1087 break;
1088
1089 default:
1090 qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid tile selection %2").arg(__FUNCTION__).arg(tileSelection);
1091 return false;
1092 }
1093
1094 // Now we have the Petzval equation load a data array of 21 x 21 points
1095 auto *dataArray = new QSurfaceDataArray;
1096 float x_step = m_data.sensorWidth * m_data.pixelSize * 2.0 / 21.0;
1097 float y_step = m_data.sensorHeight * m_data.pixelSize * 2.0 / 21.0;
1098
1099 m_maxX = std::max(m_maxX, static_cast<float>(m_data.sensorWidth));
1100 m_maxY = std::max(m_maxY, static_cast<float>(m_data.sensorHeight));
1101
1102 // Seems like the x values in the data row need to be increasing / descreasing for the dataProxy to work
1103 for (int j = -10; j < 11; j++)
1104 {
1105 auto *newRow = new QSurfaceDataRow;
1106 float y = y_step * j;
1107 for (int i = -10; i < 11; i++)
1108 {
1109 float x = x_step * i;
1110 float z = sign * (pow(x / a, 2.0) + pow(y / a, 2.0));
1111 newRow->append(QSurfaceDataItem({x, y, z}));
1112 if (i == 10 && j == 10)
1113 {
1114 m_maxZ = std::max(m_maxZ, z);
1115 m_minZ = std::min(m_minZ, z);
1116 }
1117 }
1118 dataArray->append(newRow);
1119 }
1120 m_petzvalProxy->resetArray(dataArray);
1121 return true;
1122}
1123
1124// Returns the X, Y centre of the tile in microns
1125QVector2D AberrationInspector::getXYTileCentre(tileID tile)
1126{
1127 const double halfSW = m_data.sensorWidth * m_data.pixelSize / 2.0;
1128 const double halfSH = m_data.sensorHeight * m_data.pixelSize / 2.0;
1129 const double halfTS = m_data.tileWidth * m_data.pixelSize / 2.0;
1130
1131 // Focus calculates the average star position in each tile and passes this to Aberration Inspector as
1132 // an x, y offset from the center of the tile. If stars are homogenously distributed then the offset would
1133 // be 0, 0. If they aren't, then offset represents how much to add to the tile centre.
1134 // A user option (abInsOptCentres) specifies whether to use the offsets.
1135 double xOffset = 0.0;
1136 double yOffset = 0.0;
1137 if (abInsOptCentres->isChecked())
1138 {
1139 xOffset = m_tileOffsets[tile].x() * m_data.pixelSize;
1140 yOffset = m_tileOffsets[tile].y() * m_data.pixelSize;
1141 }
1142
1143 switch (tile)
1144 {
1145 case TILE_TL:
1146 return QVector2D(-(halfSW - halfTS) + xOffset, halfSH - halfTS + yOffset);
1147 break;
1148
1149 case TILE_TM:
1150 return QVector2D(xOffset, halfSH - halfTS + yOffset);
1151 break;
1152
1153 case TILE_TR:
1154 return QVector2D(halfSW - halfTS + xOffset, halfSH - halfTS + yOffset);
1155 break;
1156
1157 case TILE_CL:
1158 return QVector2D(-(halfSW - halfTS) + xOffset, yOffset);
1159 break;
1160
1161 case TILE_CM:
1162 return QVector2D(xOffset, yOffset);
1163 break;
1164
1165 case TILE_CR:
1166 return QVector2D(halfSW - halfTS + xOffset, yOffset);
1167 break;
1168
1169 case TILE_BL:
1170 return QVector2D(-(halfSW - halfTS) + xOffset, -(halfSH - halfTS) + yOffset);
1171 break;
1172
1173 case TILE_BM:
1174 return QVector2D(xOffset, -(halfSH - halfTS) + yOffset);
1175 break;
1176
1177 case TILE_BR:
1178 return QVector2D(halfSW - halfTS + xOffset, -(halfSH - halfTS) + yOffset);
1179 break;
1180
1181 default:
1182 qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid tile %2").arg(__FUNCTION__).arg(tile);
1183 return QVector2D(0.0, 0.0);
1184 break;
1185 }
1186}
1187
1188// Position the labels just outside the sensor so they are always visible
1189QVector3D AberrationInspector::getLabelCentre(tileID tile)
1190{
1191 const double halfSW = m_data.sensorWidth * m_data.pixelSize / 2.0;
1192 const double halfSH = m_data.sensorHeight * m_data.pixelSize / 2.0;
1193 const double halfTS = m_data.tileWidth * m_data.pixelSize / 2.0;
1194
1195 QVector3D point;
1196 point.setZ(0.0);
1197
1198 switch (tile)
1199 {
1200 case TILE_TL:
1201 point.setX(-halfSW - halfTS);
1202 point.setY(halfSH + halfTS);
1203 break;
1204
1205 case TILE_TM:
1206 point.setX(0.0);
1207 point.setY(halfSH + halfTS);
1208 break;
1209
1210 case TILE_TR:
1211 point.setX(halfSW + halfTS);
1212 point.setY(halfSH + halfTS);
1213 break;
1214
1215 case TILE_CL:
1216 point.setX(-halfSW - halfTS);
1217 point.setY(0.0);
1218 break;
1219
1220 case TILE_CM:
1221 point.setX(0.0);
1222 point.setY(0.0);
1223 break;
1224
1225 case TILE_CR:
1226 point.setX(halfSW + halfTS);
1227 point.setY(0.0);
1228 break;
1229
1230 case TILE_BL:
1231 point.setX(-halfSW - halfTS);
1232 point.setY(-halfSH - halfTS);
1233 break;
1234
1235 case TILE_BM:
1236 point.setX(0.0);
1237 point.setY(-halfSH - halfTS);
1238 break;
1239
1240 case TILE_BR:
1241 point.setX(halfSW + halfTS);
1242 point.setY(-halfSH - halfTS);
1243 break;
1244
1245 default:
1246 qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid tile %2").arg(__FUNCTION__).arg(tile);
1247 point.setX(0.0);
1248 point.setY(0.0);
1249 break;
1250 }
1251 // We have the coordinates of the point, so now rotate it...
1252 return rotatePoint(point);
1253}
1254
1255// Returns the vertex of the sensor
1256// x, y are the sensor corner
1257// z is calculated from the curve fit of the sensor to a plane
1258QVector3D AberrationInspector::getSensorVertex(tileID tile)
1259{
1260 const double halfSW = m_data.sensorWidth * m_data.pixelSize / 2.0;
1261 const double halfSH = m_data.sensorHeight * m_data.pixelSize / 2.0;
1262
1263 QVector3D point;
1264 point.setZ(0.0);
1265
1266 switch (tile)
1267 {
1268 case TILE_TL:
1269 point.setX(-halfSW);
1270 point.setY(halfSH);
1271 break;
1272
1273 case TILE_TR:
1274 point.setX(halfSW);
1275 point.setY(halfSH);
1276 break;
1277
1278 case TILE_BL:
1279 point.setX(-halfSW);
1280 point.setY(-halfSH);
1281 break;
1282
1283 case TILE_BR:
1284 point.setX(halfSW);
1285 point.setY(-halfSH);
1286 break;
1287
1288 default:
1289 qCDebug(KSTARS_EKOS_FOCUS) << QString("%1 called with invalid tile %2").arg(__FUNCTION__).arg(tile);
1290 point.setX(0.0);
1291 point.setY(0.0);
1292 break;
1293 }
1294 // We have the coordinates of the point, so now rotate it...
1295 return rotatePoint(point);
1296}
1297
1298// Calculate the backfocus subtracted delta of the passed in tile versus the central tile
1299// This is really just a translation of the datapoints by the backfocus delta
1300double AberrationInspector::getBSDelta(tileID tile)
1301{
1302 if (tile == TILE_CM)
1303 return 0.0;
1304 else
1305 return m_deltas[tile] - m_backfocus;
1306}
1307
1308// Rotate the passed in 3D point based on:
1309// Sim mode: use the sim slider values for Left-to-Right and Top-to-Bottom tilt
1310// !Sim mode: use the Left-to-Right and Top-to-Bottom tilt calculated from the focus position deltas
1311//
1312// Qt provides the QQuaternion class, although the documentation is very basic.
1313// More info: https://en.wikipedia.org/wiki/Quaternion
1314// The QQuaternion class provides a way to do 3D rotations. This is simpler than doing all the 3D
1315// multiplication manually, although the results would be the same.
1316// Be careful that multiplication is NOT commutative!
1317//
1318// Left-to-Right tilt is a rotation about the y-axis
1319// Top-to-bottom tilt is a rotation about the x-axis
1320//
1321QVector3D AberrationInspector::rotatePoint(QVector3D point)
1322{
1323 float LtoRAngle, TtoBAngle;
1324
1325 if (m_simMode)
1326 {
1327 LtoRAngle = std::asin(m_simLRTilt / 100.0);
1328 TtoBAngle = std::asin(m_simTBTilt / 100.0);
1329 }
1330 else
1331 {
1332 LtoRAngle = std::asin(m_LRTilt / 100.0);
1333 TtoBAngle = std::asin(m_TBTilt / 100.0);
1334 }
1335 QQuaternion xRotation = QQuaternion::fromAxisAndAngle(1.0f, 0.0f, 0.0f, TtoBAngle * RADIANS2DEGREES);
1336 QQuaternion yRotation = QQuaternion::fromAxisAndAngle(0.0f, 1.0f, 0.0f, LtoRAngle * RADIANS2DEGREES);
1337 QQuaternion totalRotation = yRotation * xRotation;
1338 return totalRotation.rotatedVector(point);
1339}
1340
1341// Backfocus simulation slider has changed
1342void AberrationInspector::simBackfocusChanged(int value)
1343{
1344 m_simBackfocus = abs(m_backfocus) * static_cast<double>(value) / 5.0;
1345 updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
1346}
1347
1348// Left-to-Right tilt simulation slider has changed
1349void AberrationInspector::simLRTiltChanged(int value)
1350{
1351 m_simLRTilt = abs(m_LRTilt) * static_cast<double>(value) / 5.0;
1352 updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
1353}
1354
1355// Top-to-Bottom tilt simulation slider has changed
1356void AberrationInspector::simTBTiltChanged(int value)
1357{
1358 m_simTBTilt = abs(m_TBTilt) * static_cast<double>(value) / 5.0;
1359 updateGraphic(static_cast<TileSelection>(abInsTileSelection->currentIndex()));
1360}
1361
1362}
QString i18n(const char *text, const TYPE &arg...)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:83
bool isChecked() const const
void clicked(bool checked)
void toggled(bool checked)
void valueChanged(int value)
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
QByteArray fromBase64(const QByteArray &base64, Base64Options options)
QByteArray toBase64(Base64Options options) const const
void stateChanged(int state)
void activated(int index)
void currentIndexChanged(int index)
QPoint pos()
void setPointSize(int pointSize)
bool setAlignment(QLayout *l, Qt::Alignment alignment)
void append(QList< T > &&value)
QVariant property(const char *name) const const
QObject * sender() const const
bool setProperty(const char *name, QVariant &&value)
QQuaternion fromAxisAndAngle(const QVector3D &axis, float angle)
QVector3D rotatedVector(const QVector3D &vector) const const
QByteArray saveState() const const
void splitterMoved(int pos, int index)
QString arg(Args &&... args) const const
QString fromUtf8(QByteArrayView str)
QString number(double n, char format, int precision)
int toInt(bool *ok, int base) const const
QByteArray toLatin1() const const
QByteArray toUtf8() const const
AlignRight
void cellChanged(int row, int column)
void cellEntered(int row, int column)
void setForeground(const QBrush &brush)
void setTextAlignment(Qt::Alignment alignment)
QString text() const const
QTestData & newRow(const char *dataTag)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
bool isValid() const const
bool toBool() const const
int toInt(bool *ok) const const
QString toString() const const
void setX(float x)
void setY(float y)
void setZ(float z)
float x() const const
float y() const const
float z() const const
QWidget * createWindowContainer(QWindow *window, QWidget *parent, Qt::WindowFlags flags)
void setEnabled(bool)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:04:46 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.