Kstars

buildfilteroffsets.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 "buildfilteroffsets.h"
8#include <kstars_debug.h>
9#include "kstars.h"
10#include "ekos/auxiliary/tabledelegate.h"
11#include "Options.h"
12
13namespace Ekos
14{
15
16// buildFilterOffsets (BFO) sets up a dialog to manage automatic calculation of filter offsets
17// by performing autofocus runs on user selected filters and working out the relative offsets.
18// The idea is to use the utility once in a while to setup the offsets which them enables the user to avoid having
19// to do an autofocus run when swapping filters. The benefit of this might be to avoid focusing on a difficult-to-
20// focus filter but focusing on a lock filter and then using the offset; or just to avoid an Autofocus run when
21// changing filter.
22//
23// The utility follows the standard Ekos cpp/h/ui file setup. The basic dialog is contained in the .ui file but
24// because the table is constructed from data at runtime some of the GUI is constructed in code in the .cpp.
25// BFO uses the MVC design pattern. The BuildFilterOffset object is a friend of FilterManager because BFO is closely
26// related to FilterManager. This allows several FilterManager methods to be accessed by BFO. In addition, signals
27// with Focus are passed via FilterManager.
28//
29// On launch, BFO displays a grid containing all the filters in the Filter Settings. Relevant information is copied across
30// to BFO and the filters appear in the same order. The 1st filter is suffiixed with a "*", marking it as the reference
31// filter which is the one against which all other relevant offsets are measured.
32//
33// To change the reference filter, the user double clicks another filter.
34//
35// The user needs to set the number of Autofocus (AF) runs to perform on each filter. The default is 5, the maximum is 10.
36// Setting this field to 0, removes the associated filter from further processing.
37//
38// The user presses Run and the utility moves to the processing stage. Extra columns are displayed, one for each AF run,
39// as well as average, new offset and save columns. For each filter the number of requested AF runs is performed. The
40// table cell associated with the in-flight AF run is highlighted. As an AF run completes the results are displayed in
41// the table. When all AF runs for a filter are complete, processing moves to the next filter.
42//
43// Normally, if a lock filter is configured for a particular filter, then when focusing, the lock filter is swapped in
44// AF runs, and then the original filter is swapped back into position. For BFO this model is inappropriate. When an
45// AF run is requested on a filter then AF is always run on that filter so the lock filter policy is not honoured when
46// running AF from BFO.
47//
48// The user can interrupt processing by pressing stop. A confirm dialog then allows the user to stop BFO or resume.
49//
50// BFO can take a while to complete. For example if 5 AF runs are requested on 8 filters and AF takes, say 2 mins per
51// run, then BFO will take over an hour. During this time environmental conditions such as temperature and altitude
52// could change the focus point. For this reaons, it it possible Adapt each focus run back to the temp and alt applicable
53// during the first AF run for more accurate calculation of the offsets. If Adapt Focus is checked, then the adapted
54// vales are used in calculations; if not checked then the raw (unadapted) values are used. The toggle can be used at
55// any time. The tooltip on each AF run provides a tabular explanation of the adaptations and how the raw value is
56// changed to the adapted value. In order to use Adapt Focus, the ticks per temperature and ticks per altitude fields
57// in the Fillter Settings popup need to be filled in appropriately for each filter being processed.
58//
59// If AF fails then the user is prompted to retry or abort the processing.
60//
61// When processing completes the user can review the results. Each processed filter's new offset has an associated save
62// checkbox allowing all or some values to be saved. Saving persists the new offset values in the Filter Settings popup
63// for future use during imaging.
64//
65// The average AF value for a filter is a simple mean. There are typically not enough sample points taken for robust
66// statistical processing to add any value. So the user needs to review the AF values and decide if they want to remove
67// any outliers (set the AF value to 0 to exclude from processing, or adjust the number). In addition, it is possible
68// to override the offset with a manually entered value.
69//
70BuildFilterOffsets::BuildFilterOffsets(QSharedPointer<FilterManager> filterManager)
71{
72#ifdef Q_OS_OSX
74#endif
75
76 if (filterManager.isNull())
77 return;
78
79 m_filterManager = filterManager;
80
81 setupUi(this);
82 setupConnections();
83 initBuildFilterOffsets();
84 setupBuildFilterOffsetsTable();
85 setupGUI();
86
87 // Launch the dialog - synchronous call
88 this->exec();
89}
90
91BuildFilterOffsets::~BuildFilterOffsets()
92{
93}
94
95void BuildFilterOffsets::setupConnections()
96{
97 // Connections to FilterManager
98 connect(this, &BuildFilterOffsets::runAutoFocus, m_filterManager.get(), &FilterManager::signalRunAutoFocus);
99 connect(this, &BuildFilterOffsets::abortAutoFocus, m_filterManager.get(), &FilterManager::signalAbortAutoFocus);
100
101 // Connections from FilterManager
102 connect(m_filterManager.get(), &FilterManager::autoFocusDone, this, &BuildFilterOffsets::autoFocusComplete);
103 connect(m_filterManager.get(), &FilterManager::ready, this, &BuildFilterOffsets::buildTheOffsetsTaskComplete);
104
105 // Connections internal to BuildFilterOffsets
106 connect(this, &BuildFilterOffsets::ready, this, &BuildFilterOffsets::buildTheOffsetsTaskComplete);
107}
108
109void BuildFilterOffsets::setupGUI()
110{
111 // Add action buttons to the button box
112 m_runButton = buildOffsetsButtonBox->addButton("Run", QDialogButtonBox::ActionRole);
113 m_stopButton = buildOffsetsButtonBox->addButton("Stop", QDialogButtonBox::ActionRole);
114
115 // Set tooltips for the buttons
116 m_runButton->setToolTip("Run Build Filter Offsets utility");
117 m_stopButton->setToolTip("Interrupt processing when utility is running");
118 buildOffsetsButtonBox->button(QDialogButtonBox::Save)->setToolTip("Save New Offsets");
119
120 // Set the buttons' state
121 setBuildFilterOffsetsButtons(BFO_INIT);
122
123 // Connect up button callbacks
124 connect(m_runButton, &QPushButton::clicked, this, &BuildFilterOffsets::buildTheOffsets);
125 connect(m_stopButton, &QPushButton::clicked, this, &BuildFilterOffsets::stopProcessing);
127 &BuildFilterOffsets::saveTheOffsets);
129 {
130 // Close the dialog down unless lots of processing has been done. In that case put up an "are you sure" popup
131 if (!m_tableInEditMode)
132 this->done(QDialog::Rejected);
133 else if (KMessageBox::questionYesNo(KStars::Instance(),
134 i18n("Are you sure you want to quit?")) == KMessageBox::Yes)
135 this->done(QDialog::Rejected);
136 });
137
138 // Setup the Adapt Focus checkbox
139 buildOffsetsAdaptFocus->setChecked(Options::adaptFocusBFO());
140 connect(buildOffsetsAdaptFocus, &QCheckBox::toggled, this, [&](bool checked)
141 {
142 Options::setAdaptFocusBFO(checked);
143 reloadPositions(checked);
144 });
145
146 // Connect cell changed callback
147 connect(&m_BFOModel, &QStandardItemModel::itemChanged, this, &BuildFilterOffsets::itemChanged);
148
149 // Connect double click callback
150 connect(buildOffsetsTableView, &QAbstractItemView::doubleClicked, this, &BuildFilterOffsets::refChanged);
151
152 // Display an initial message in the status bar
153 buildOffsetsStatusBar->showMessage(i18n("Idle"));
154
155 // Resize the dialog based on the data
156 buildOffsetsDialogResize();
157}
158
159void BuildFilterOffsets::initBuildFilterOffsets()
160{
161 m_inBuildOffsets = false;
162 m_stopFlag = m_problemFlag = m_abortAFPending = m_tableInEditMode = false;
163 m_filters.clear();
164 m_refFilter = -1;
165 m_rowIdx = m_colIdx = 0;
166
167 // Drain any old queue items
168 m_buildOffsetsQ.clear();
169}
170
171void BuildFilterOffsets::setupBuildFilterOffsetsTable()
172{
173 // Setup MVC
174 buildOffsetsTableView->setModel(&m_BFOModel);
175
176 // Setup the table view
177 QStringList Headers { i18n("Filter"), i18n("Offset"), i18n("Lock Filter"), i18n("# Focus Runs") };
178 m_BFOModel.setColumnCount(Headers.count());
179 m_BFOModel.setHorizontalHeaderLabels(Headers);
180
181 // Setup tooltips on column headers
182 m_BFOModel.setHeaderData(getColumn(BFO_FILTER), Qt::Horizontal,
183 i18n("Filter. * indicates reference filter. Double click to change"),
185 m_BFOModel.setHeaderData(getColumn(BFO_NUM_FOCUS_RUNS), Qt::Horizontal, i18n("# Focus Runs. Set per filter. 0 to ignore"),
187
188 // Setup edit delegates for each column
189 // No Edit delegates for Filter, Offset and Lock Filter
190 NotEditableDelegate *noEditDel = new NotEditableDelegate(buildOffsetsTableView);
191 buildOffsetsTableView->setItemDelegateForColumn(getColumn(BFO_FILTER), noEditDel);
192 buildOffsetsTableView->setItemDelegateForColumn(getColumn(BFO_OFFSET), noEditDel);
193 buildOffsetsTableView->setItemDelegateForColumn(getColumn(BFO_LOCK), noEditDel);
194
195 // # Focus Runs delegate
196 IntegerDelegate *numRunsDel = new IntegerDelegate(buildOffsetsTableView, 0, 10, 1);
197 buildOffsetsTableView->setItemDelegateForColumn(getColumn(BFO_NUM_FOCUS_RUNS), numRunsDel);
198
199 // Setup the table
200 m_BFOModel.setRowCount(m_filterManager->m_ActiveFilters.count());
201
202 // Load the data for each filter
203 for (int row = 0 ; row < m_filterManager->m_ActiveFilters.count(); row++)
204 {
205 // Filter name
206 m_filters.push_back(m_filterManager->m_ActiveFilters[row]->color());
207 QString str;
208 if (row == 0)
209 {
210 str = QString("%1 *").arg(m_filters[row]);
211 m_refFilter = 0;
212 }
213 else
214 str = m_filters[row];
215
217 m_BFOModel.setItem(row, getColumn(BFO_FILTER), item0);
218
219 // Offset
220 QStandardItem* item1 = new QStandardItem(QString::number(m_filterManager->m_ActiveFilters[row]->offset()));
221 m_BFOModel.setItem(row, getColumn(BFO_OFFSET), item1);
222
223 // Lock filter
224 QStandardItem* item2 = new QStandardItem(m_filterManager->m_ActiveFilters[row]->lockedFilter());
225 m_BFOModel.setItem(row, getColumn(BFO_LOCK), item2);
226
227 // Number of AF runs to perform
229 m_BFOModel.setItem(row, getColumn(BFO_NUM_FOCUS_RUNS), item3);
230 }
231}
232
233void BuildFilterOffsets::setBuildFilterOffsetsButtons(const BFOButtonState state)
234{
235 switch (state)
236 {
237 case BFO_INIT:
238 m_runButton->setEnabled(true);
239 m_stopButton->setEnabled(false);
240 buildOffsetsButtonBox->button(QDialogButtonBox::Save)->setEnabled(false);
241 buildOffsetsButtonBox->button(QDialogButtonBox::Close)->setEnabled(true);
242 break;
243
244 case BFO_RUN:
245 m_runButton->setEnabled(false);
246 m_stopButton->setEnabled(true);
247 buildOffsetsButtonBox->button(QDialogButtonBox::Save)->setEnabled(false);
248 buildOffsetsButtonBox->button(QDialogButtonBox::Close)->setEnabled(false);
249 break;
250
251 case BFO_SAVE:
252 m_runButton->setEnabled(false);
253 m_stopButton->setEnabled(false);
254 buildOffsetsButtonBox->button(QDialogButtonBox::Save)->setEnabled(true);
255 buildOffsetsButtonBox->button(QDialogButtonBox::Close)->setEnabled(true);
256 break;
257
258 case BFO_STOP:
259 m_runButton->setEnabled(false);
260 m_stopButton->setEnabled(false);
261 buildOffsetsButtonBox->button(QDialogButtonBox::Save)->setEnabled(false);
262 buildOffsetsButtonBox->button(QDialogButtonBox::Close)->setEnabled(false);
263 break;
264
265 default:
266 break;
267 }
268}
269
270// Loop through all the filters to process, and for each...
271// - set Autofocus to use the filter
272// - Loop for the number of runs chosen by the user for that filter
273// - Run AF
274// - Get the focus solution
275// - Load the focus solution into table widget in the appropriate cell
276// - Calculate the average AF solution for that filter and display it
277void BuildFilterOffsets::buildTheOffsets()
278{
279 buildOffsetsQItem qItem;
280
281 // Set the buttons
282 setBuildFilterOffsetsButtons(BFO_RUN);
283
284 // Make the Number of runs column not editable
285 // No Edit delegates for Filter, Offset and Lock Filter
287 buildOffsetsTableView->setItemDelegateForColumn(getColumn(BFO_NUM_FOCUS_RUNS), noEditDel);
288
289 // Loop through the work to do and load up Queue and extend tableWidget to record AF answers
290 int maxAFRuns = 0;
291 int startRow = -1;
292 for (int row = 0; row < m_filters.count(); row++ )
293 {
294 const int numRuns = m_BFOModel.item(row, getColumn(BFO_NUM_FOCUS_RUNS))->text().toInt();
295 if (numRuns > 0)
296 {
297 if (startRow < 0)
298 startRow = row;
299
300 // Set the filter change
301 qItem.color = m_filters[row];
302 qItem.changeFilter = true;
303 m_buildOffsetsQ.enqueue(qItem);
304
305 // Load up the AF runs based on how many the user requested
306 qItem.changeFilter = false;
307 maxAFRuns = std::max(maxAFRuns, numRuns);
308 for (int runNum = 1; runNum <= numRuns; runNum++)
309 {
310 qItem.numAFRun = runNum;
311 m_buildOffsetsQ.enqueue(qItem);
312 }
313 }
314 }
315
316 // Add columns to the Model for AF runs and set the headers. Each AF run result is editable
317 // but the calculated average is not.
318 int origCols = m_BFOModel.columnCount();
319 m_BFOModel.setColumnCount(origCols + maxAFRuns + 3);
320 for (int col = 0; col < maxAFRuns; col++)
321 {
322 QStandardItem *newItem = new QStandardItem(i18n("AF Run %1", col + 1));
323 m_BFOModel.setHorizontalHeaderItem(origCols + col, newItem);
325 i18n("AF Run %1. Calculated automatically but can be edited. Set to 0 to exclude from average.", col + 1),
327 buildOffsetsTableView->setItemDelegateForColumn(origCols + col, noEditDel);
328 }
329
330 // Add 3 more columns for the average of the AF runs, the offset and whether to save the offset
331 QStandardItem *averageItem = new QStandardItem(i18n("Average"));
333 m_BFOModel.setHeaderData(origCols + maxAFRuns, Qt::Horizontal, i18n("AF Average (mean)."), Qt::ToolTipRole);
334 buildOffsetsTableView->setItemDelegateForColumn(origCols + maxAFRuns, noEditDel);
335
336 QStandardItem *offsetItem = new QStandardItem(i18n("New Offset"));
339 i18n("New Offset. Calculated relative to Filter with *. Can be edited."), Qt::ToolTipRole);
340 buildOffsetsTableView->setItemDelegateForColumn(origCols + maxAFRuns + 1, noEditDel);
341
342 QPointer<ToggleDelegate> saveDelegate = new ToggleDelegate(&m_BFOModel);
343 QStandardItem *saveItem = new QStandardItem(i18n("Save"));
344 m_BFOModel.setHorizontalHeaderItem(origCols + maxAFRuns + 2, saveItem);
346 i18n("Save. Check to save the New Offset for the associated Filter."), Qt::ToolTipRole);
347 buildOffsetsTableView->setItemDelegateForColumn(origCols + maxAFRuns + 2, saveDelegate);
348
349 // Resize the dialog
350 buildOffsetsDialogResize();
351
352 // Set the selected cell to the first AF run of the ref filter
353 m_rowIdx = startRow;
354 m_colIdx = getColumn(BFO_AF_RUN_1);
355 QModelIndex index = buildOffsetsTableView->model()->index(m_rowIdx, m_colIdx);
356 buildOffsetsTableView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect);
357
358 // Initialise the progress bar
360 buildOffsetsProgressBar->setRange(0, m_buildOffsetsQ.count());
361 buildOffsetsProgressBar->setValue(0);
362
363 // The Q has been loaded with all required actions so lets start processing
364 m_inBuildOffsets = true;
365 runBuildOffsets();
366}
367
368// This is signalled when an asynchronous task has been completed, either
369// a filter change or an autofocus run
370void BuildFilterOffsets::buildTheOffsetsTaskComplete()
371{
372 if (m_stopFlag)
373 {
374 // User hit the stop button, so see what they want to do
376 i18n("Are you sure you want to stop Build Filter Offsets?"), i18n("Stop Build Filter Offsets"),
378 {
379 // User wants to retry processing
380 m_stopFlag = false;
381 setBuildFilterOffsetsButtons(BFO_RUN);
382
383 if (m_abortAFPending)
384 {
385 // If the in-flight task was aborted then retry - don't take the next task off the Q
386 m_abortAFPending = false;
387 processQItem(m_qItemInProgress);
388 }
389 else
390 {
391 // No tasks were aborted so we can just start the next task in the queue
392 buildOffsetsProgressBar->setValue(buildOffsetsProgressBar->value() + 1);
393 runBuildOffsets();
394 }
395 }
396 else
397 {
398 // User wants to abort
399 m_stopFlag = m_abortAFPending = false;
400 this->done(QDialog::Rejected);
401 }
402 }
403 else if (m_problemFlag)
404 {
405 // The in flight task had a problem so see what the user wants to do
407 i18n("An unexpected problem occurred.\nStop Build Filter Offsets, or Cancel to retry?"),
408 i18n("Build Filter Offsets Unexpected Problem"),
410 {
411 // User wants to retry
412 m_problemFlag = false;
413 processQItem(m_qItemInProgress);
414 }
415 else
416 {
417 // User wants to abort
418 m_problemFlag = false;
419 this->done(QDialog::Rejected);
420 }
421 }
422 else
423 {
424 // All good so update the progress bar and process the next task
425 buildOffsetsProgressBar->setValue(buildOffsetsProgressBar->value() + 1);
426 runBuildOffsets();
427 }
428}
429
430// Resize the dialog to the data
431void BuildFilterOffsets::buildOffsetsDialogResize()
432{
433 // Resize the columns to the data
434 buildOffsetsTableView->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);
435
436 // Resize the dialog to the width and height of the table widget
437 const int width = buildOffsetsTableView->horizontalHeader()->length() + 40;
438 const int height = buildOffsetsTableView->verticalHeader()->length() + buildOffsetsButtonBox->height() +
439 buildOffsetsProgressBar->height() + buildOffsetsStatusBar->height() + 60;
440 this->resize(width, height);
441}
442
443void BuildFilterOffsets::runBuildOffsets()
444{
445 if (m_buildOffsetsQ.isEmpty())
446 {
447 // All tasks have been actioned so allow the user to edit the results, save them or quit.
448 setCellsEditable();
449 setBuildFilterOffsetsButtons(BFO_SAVE);
450 m_tableInEditMode = true;
451 buildOffsetsStatusBar->showMessage(i18n("Processing complete."));
452 }
453 else
454 {
455 // Take the next item off the queue
456 m_qItemInProgress = m_buildOffsetsQ.dequeue();
457 processQItem(m_qItemInProgress);
458 }
459}
460
461void BuildFilterOffsets::processQItem(const buildOffsetsQItem currentItem)
462{
463 if (currentItem.changeFilter)
464 {
465 // Need to change filter
466 buildOffsetsStatusBar->showMessage(i18n("Changing filter to %1...", currentItem.color));
467
468 auto pos = m_filterManager->m_currentFilterLabels.indexOf(currentItem.color) + 1;
469 if (!m_filterManager->setFilterPosition(pos, m_filterManager->CHANGE_POLICY))
470 {
471 // Filter wheel position change failed.
472 buildOffsetsStatusBar->showMessage(i18n("Problem changing filter to %1...", currentItem.color));
473 m_problemFlag = true;
474 }
475 }
476 else
477 {
478 // Signal an AF run with an arg of "build offsets"
479 const int run = m_colIdx - getColumn(BFO_AF_RUN_1) + 1;
480 const int numRuns = m_BFOModel.item(m_rowIdx, getColumn(BFO_NUM_FOCUS_RUNS))->text().toInt();
481 buildOffsetsStatusBar->showMessage(i18n("Running Autofocus on %1 (%2/%3)...", currentItem.color, run, numRuns));
482 emit runAutoFocus(AutofocusReason::FOCUS_FILTER_OFFSETS, "");
483 }
484}
485
486// This is called at the end of an AF run
487void BuildFilterOffsets::autoFocusComplete(FocusState completionState, int position, double temperature, double altitude)
488{
489 if (!m_inBuildOffsets)
490 return;
491
492 if (completionState != FOCUS_COMPLETE)
493 {
494 // The AF run has failed. If the user aborted the run then this is an expected signal
495 if (!m_abortAFPending)
496 // In this case the failure is a genuine problem so set a problem flag
497 m_problemFlag = true;
498 }
499 else
500 {
501 // AF run was successful so load the solution results
502 processAFcomplete(position, temperature, altitude);
503
504 // Load the result into the table. The Model update will trigger further updates
505 loadPosition(buildOffsetsAdaptFocus->isChecked(), m_rowIdx, m_colIdx);
506
507 // Now see what's next, another AF run on this filter or are we moving to the next filter
508 if (m_colIdx - getColumn(BFO_NUM_FOCUS_RUNS) < getNumRuns(m_rowIdx))
509 m_colIdx++;
510 else
511 {
512 // Move the active cell to the next AF run in the table
513 // Usually this will be the next row, but if this row has zero AF runs skip to the next
514 for (int nextRow = m_rowIdx + 1; nextRow < m_filters.count(); nextRow++)
515 {
516 if (getNumRuns(nextRow) > 0)
517 {
518 // Found the next filter to process
519 m_rowIdx = nextRow;
520 m_colIdx = getColumn(BFO_AF_RUN_1);
521 break;
522 }
523 }
524 }
525 // Highlight the next cell...
526 const QModelIndex index = buildOffsetsTableView->model()->index(m_rowIdx, m_colIdx);
527 buildOffsetsTableView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect);
528 }
529 // Signal the next processing step
530 emit ready();
531}
532
533// Called to store the AF position details. The raw AF position is passed in (from Focus)
534// The Adapted position (based on temperature and altitude) is calculated
535// The sense of the adaptation is to take the current position and adapt it to the
536// conditions (temp and alt) of the reference.
537void BuildFilterOffsets::processAFcomplete(const int position, const double temperature, const double altitude)
538{
539 AFSolutionDetail solution;
540
541 solution.position = position;
542 solution.temperature = temperature;
543 solution.altitude = altitude;
544 solution.ticksPerTemp = m_filterManager->getFilterTicksPerTemp(m_filters[m_rowIdx]);
545 solution.ticksPerAlt = m_filterManager->getFilterTicksPerAlt(m_filters[m_rowIdx]);
546
547 if (m_rowIdx == 0 && m_colIdx == getColumn(BFO_AF_RUN_1))
548 {
549 // Set the reference temp and alt to be the first AF run
550 m_refTemperature = temperature;
551 m_refAltitude = altitude;
552 }
553
554 // Calculate the temperature adaptation
555 if (temperature == INVALID_VALUE || m_refTemperature == INVALID_VALUE)
556 {
557 solution.deltaTemp = 0.0;
558 solution.deltaTicksTemperature = 0.0;
559 }
560 else
561 {
562 solution.deltaTemp = m_refTemperature - temperature;
563 solution.deltaTicksTemperature = solution.ticksPerTemp * solution.deltaTemp;
564 }
565
566 // Calculate the altitude adaptation
567 if (altitude == INVALID_VALUE || m_refAltitude == INVALID_VALUE)
568 {
569 solution.deltaAlt = 0.0;
570 solution.deltaTicksAltitude = 0.0;
571 }
572 else
573 {
574 solution.deltaAlt = m_refAltitude - altitude;
575 solution.deltaTicksAltitude = solution.ticksPerAlt * solution.deltaAlt;
576 }
577
578 // Calculate the total adaptation
579 solution.deltaTicksTotal = static_cast<int>(round(solution.deltaTicksTemperature + solution.deltaTicksAltitude));
580
581 // Calculate the Adapted position
582 solution.adaptedPosition = position + solution.deltaTicksTotal;
583
584 m_AFSolutions.push_back(solution);
585}
586
587// Load the focus position depending on the setting of the adaptPos checkbox. The Model update will trigger further updates
588void BuildFilterOffsets::loadPosition(const bool checked, const int row, const int col)
589{
590 int idx = 0;
591 // Work out the array index for m_AFSolutions
592 for (int i = 0; i < row; i++)
593 idx += getNumRuns(i);
594
595 idx += col - getColumn(BFO_AF_RUN_1);
596
597 // Check that the passed in row, col has been processed. If not, nothing to do
598 if (idx < m_AFSolutions.count())
599 {
600 // Get the AF position to use based on the setting of 'checked'
601 int pos = (checked) ? m_AFSolutions[idx].adaptedPosition : m_AFSolutions[idx].position;
602
603 // Present a tooltip explanation of how the original position is changed by adaptation, e.g.
604 // Adapt Focus Explainer
605 // Position Temperature (°C) Altitude (°Alt)
606 // Measured Pos: 36704 T: 0.9°C (ΔT=0.7) Alt: 40.1° (ΔAlt=-4.2)
607 // Adaptations: 4 T: 7.10 ticks Alt: -3.20 ticks
608 // Adapted Pos: 36708
609 //
610 const QString temp = QString("%1").arg(m_AFSolutions[idx].temperature, 0, 'f', 1);
611 const QString deltaTemp = i18n("(ΔT=%1)", QString("%1").arg(m_AFSolutions[idx].deltaTemp, 0, 'f', 1));
612 const QString ticksTemp = i18n("(%1 ticks)", QString("%1").arg(m_AFSolutions[idx].deltaTicksTemperature, 0, 'f', 1));
613 const QString alt = QString("%1").arg(m_AFSolutions[idx].altitude, 0, 'f', 1);
614 const QString deltaAlt = i18n("(ΔAlt=%1)", QString("%1").arg(m_AFSolutions[idx].deltaAlt, 0, 'f', 1));
615 const QString ticksAlt = i18n("(%1 ticks)", QString("%1").arg(m_AFSolutions[idx].deltaTicksAltitude, 0, 'f', 1));
616
618 const QString toolTip =
619 i18nc("Graphics tooltip; colume 1 is a header, column 2 is focus position, column 3 is temperature in °C, colunm 4 is altitude in °Alt"
620 "Row 1 is the headers, row 2 is the measured position, row 3 are the adaptations for temperature and altitude, row 4 is adapted position",
621 "<head><style>"
622 " th, td, caption {white-space: nowrap; padding-left: 5px; padding-right: 5px;}"
623 " th { text-align: left;}"
624 " td { text-align: right;}"
625 " caption { text-align: center; vertical-align: top; font-weight: bold; margin: 0px; padding-bottom: 5px;}"
626 "</head></style>"
627 "<body><table>"
628 "<caption align=top>Adapt Focus Explainer</caption>"
629 "<tr><th></th><th>Position</th><th>Temperature (°C)</th><th>Altitude (°Alt)</th></tr>"
630 "<tr><th>Measured Pos</th><td>%1</td><td>%2 %3</td><td>%4 %5</td></tr>"
631 "<tr><th>Adaptations</th><td>%6</td><td>%7</td><td>%8</td></tr>"
632 "<tr><th>Adapted Pos</th><td>%9</td></tr>"
633 "</table></body>",
634 m_AFSolutions[idx].position, temp, deltaTemp, alt, deltaAlt,
635 m_AFSolutions[idx].deltaTicksTotal, ticksTemp, ticksAlt,
636 m_AFSolutions[idx].adaptedPosition);
637
638 posItem->setToolTip(toolTip);
639 m_BFOModel.setItem(row, col, posItem);
640 }
641}
642
643// Reload the focus position grid depending on the setting of the adaptPos checkbox.
644void BuildFilterOffsets::reloadPositions(const bool checked)
645{
646 for (int row = 0; row <= m_rowIdx; row++)
647 {
648 const int numRuns = getNumRuns(row);
649 const int maxCol = (row < m_rowIdx) ? numRuns : m_colIdx - getColumn(BFO_AF_RUN_1) + 1;
650 for (int col = 0; col < maxCol; col++)
651 loadPosition(checked, row, col + getColumn(BFO_AF_RUN_1));
652 }
653}
654
655// Called when the user wants to persist the calculated offsets
656void BuildFilterOffsets::saveTheOffsets()
657{
658 for (int row = 0; row < m_filters.count(); row++)
659 {
660 // Check there's an item set for the current row before accessing
661 if (m_BFOModel.item(row, getColumn(BFO_SAVE_CHECK))->text().toInt())
662 {
663 // Save item is set so persist the offset
664 const int offset = m_BFOModel.item(row, getColumn(BFO_NEW_OFFSET))->text().toInt();
665 if (!m_filterManager->setFilterOffset(m_filters[row], offset))
666 qCDebug(KSTARS) << "Unable to save calculated offset for filter " << m_filters[row];
667 }
668 }
669 // All done so close the dialog
670 this->done(QDialog::Accepted);
671}
672
673// Processing done so make certain cells editable for the user
674void BuildFilterOffsets::setCellsEditable()
675{
676 // Enable an edit delegate on the AF run result columns so the user can adjust as necessary
677 // The delegates operate at the row or column level so some cells need to be manually disabled
678 for (int col = getColumn(BFO_AF_RUN_1); col < getColumn(BFO_AF_RUN_1) + getMaxRuns(); col++)
679 {
680 IntegerDelegate *AFDel = new IntegerDelegate(buildOffsetsTableView, 0, 1000000, 1);
681 buildOffsetsTableView->setItemDelegateForColumn(col, AFDel);
682
683 // Disable any cells where for that filter less AF runs were requested
684 for (int row = 0; row < m_BFOModel.rowCount(); row++)
685 {
686 const int numRuns = getNumRuns(row);
687 if ((numRuns > 0) && (col > numRuns + getColumn(BFO_AF_RUN_1) - 1))
688 {
689 QStandardItem *currentItem = new QStandardItem();
690 currentItem->setEditable(false);
691 m_BFOModel.setItem(row, col, currentItem);
692 }
693 }
694 }
695
696 // Offset column
697 IntegerDelegate *offsetDel = new IntegerDelegate(buildOffsetsTableView, -10000, 10000, 1);
698 buildOffsetsTableView->setItemDelegateForColumn(getColumn(BFO_NEW_OFFSET), offsetDel);
699
700 // Save column
701 ToggleDelegate *saveDel = new ToggleDelegate(buildOffsetsTableView);
702 buildOffsetsTableView->setItemDelegateForColumn(getColumn(BFO_SAVE_CHECK), saveDel);
703
704 // Check filters where user requested zero AF runs
705 for (int row = 0; row < m_filters.count(); row++)
706 {
707 if (getNumRuns(row) <= 0)
708 {
709 // Uncheck save just in case user activated it
710 QStandardItem *saveItem = new QStandardItem("");
711 m_BFOModel.setItem(row, getColumn(BFO_SAVE_CHECK), saveItem);
712 NotEditableDelegate *newDelegate = new NotEditableDelegate(buildOffsetsTableView);
713 buildOffsetsTableView->setItemDelegateForRow(row, newDelegate);
714 }
715 }
716}
717
718void BuildFilterOffsets::stopProcessing()
719{
720 m_stopFlag = true;
721 setBuildFilterOffsetsButtons(BFO_STOP);
722
723 if (m_qItemInProgress.changeFilter)
724 {
725 // Change filter in progress. Let it run to completion
726 m_abortAFPending = false;
727 }
728 else
729 {
730 // AF run is currently in progress so signal an abort
731 buildOffsetsStatusBar->showMessage(i18n("Aborting Autofocus..."));
732 m_abortAFPending = true;
733 emit abortAutoFocus();
734 }
735}
736
737// Callback when an item in the Model is changed
738void BuildFilterOffsets::itemChanged(QStandardItem *item)
739{
740 if (item->column() == getColumn(BFO_NUM_FOCUS_RUNS))
741 {
742 if (item->row() == m_refFilter && m_BFOModel.item(item->row(), item->column())->text().toInt() == 0)
743 // If user is trying to set the num AF runs of the ref filter to 0, set to 1
744 m_BFOModel.setItem(item->row(), item->column(), new QStandardItem(QString::number(1)));
745 }
746 else if ((item->column() >= getColumn(BFO_AF_RUN_1)) && (item->column() < getColumn(BFO_AVERAGE)))
747 {
748 // One of the AF runs has changed so recalc the Average and Offset
749 calculateAFAverage(item->row(), item->column());
750 calculateOffset(item->row());
751 }
752}
753
754// Callback when a row in the Model is changed
755void BuildFilterOffsets::refChanged(QModelIndex index)
756{
757 if (m_inBuildOffsets)
758 return;
759
760 const int row = index.row();
761 const int col = index.column();
762 if (col == 0 && row >= 0 && row < m_filters.count() && row != m_refFilter)
763 {
764 // User double clicked the filter column in a different cell to the current ref filter
765 QStandardItem* itemSelect = new QStandardItem(QString("%1 *").arg(m_filters[row]));
766 m_BFOModel.setItem(row, getColumn(BFO_FILTER), itemSelect);
767
768 // The ref filter needs to have at least 1 AF run so that there is an average
769 // solution to base the offsets of other filters against... so force this condition
770 if (getNumRuns(row) == 0)
771 m_BFOModel.setItem(row, getColumn(BFO_NUM_FOCUS_RUNS), new QStandardItem(QString::number(1)));
772
773 // Reset the previous selection
774 QStandardItem* itemDeselect = new QStandardItem(m_filters[m_refFilter]);
775 m_BFOModel.setItem(m_refFilter, getColumn(BFO_FILTER), itemDeselect);
776
777 m_refFilter = row;
778 // Resize the cols and dialog
779 buildOffsetsDialogResize();
780 }
781}
782
783// This routine calculates the average of the AF runs. Given that the number of runs is likely to be low
784// a simple average is used. The user may manually adjust the values.
785void BuildFilterOffsets::calculateAFAverage(const int row, const int col)
786{
787 int numRuns;
788 if (m_tableInEditMode)
789 numRuns = getNumRuns(row);
790 else
791 numRuns = col - getColumn(BFO_AF_RUN_1) + 1;
792
793 // Firstly, the average of the AF runs
794 double total = 0;
795 int useableRuns = numRuns;
796 for(int i = 0; i < numRuns; i++)
797 {
798 int j = m_BFOModel.item(row, getColumn(BFO_AF_RUN_1) + i)->text().toInt();
799 if (j > 0)
800 total += j;
801 else
802 useableRuns--;
803 }
804
805 const int average = (useableRuns > 0) ? static_cast<int>(round(total / useableRuns)) : 0;
806
807 // Update the Model with the newly calculated average
809 m_BFOModel.setItem(row, getColumn(BFO_AVERAGE), averageItem);
810}
811
812// calculateOffset updates new offsets when AF averages have been calculated. There are 2 posibilities:
813// 1. The updated row is the reference filter in the list so update the offset of other filters
814// 2. The updated row is another filter in which case just update its offset
815void BuildFilterOffsets::calculateOffset(const int row)
816{
817 if (row == m_refFilter)
818 {
819 // The first filter has been changed so loop through the other filters and adjust the offsets
820 if (m_tableInEditMode)
821 {
822 for (int i = 0; i < m_filters.count(); i++)
823 {
824 if (i != m_refFilter && getNumRuns(i) > 0)
825 calculateOffset(i);
826 }
827 }
828 else
829 {
830 // If there are some filters higher in the table than ref filter then these can only be
831 // proessed now that the ref filter has been processed.
832 for (int i = 0; i < m_refFilter; i++)
833 {
834 if (getNumRuns(i) > 0)
835 calculateOffset(i);
836 }
837 }
838 }
839
840 // If we haven't processed the ref filter yet then skip over
841 if (m_rowIdx >= m_refFilter)
842 {
843 // The ref filter has been processed so we can calculate the offset from it
844 const int average = m_BFOModel.item(row, getColumn(BFO_AVERAGE))->text().toInt();
845 const int refFilterAverage = m_BFOModel.item(m_refFilter, getColumn(BFO_AVERAGE))->text().toInt();
846
847 // Calculate the offset and set it in the model
848 const int offset = average - refFilterAverage;
850 m_BFOModel.setItem(row, getColumn(BFO_NEW_OFFSET), offsetItem);
851
852 // Set the save checkbox
853 QStandardItem *saveItem = new QStandardItem(QString::number(1));
854 m_BFOModel.setItem(row, getColumn(BFO_SAVE_CHECK), saveItem);
855 }
856}
857
858// Returns the column in the table for the passed in id. The structure is:
859// Col 0 -- BFO_FILTER -- Filter name
860// Col 1 -- BFO_OFFSET -- Current offset value
861// Col 2 -- BFO_LOCK -- Lock filter name
862// Col 3 -- BFO_NUM_FOCUS_RUNS -- Number of AF runs
863// Col 4 -- BFO_AF_RUN_1 -- 1st AF run
864// Col x -- BFO_AVERAGE -- Average AF run. User selects the number of AF runs at run time
865// Col y -- BFO_NEW_OFFSET -- New offset.
866// Col z -- BFO_SAVE_CHECK -- Save checkbox
867int BuildFilterOffsets::getColumn(const BFOColID id)
868{
869 switch (id)
870 {
871 case BFO_FILTER:
872 case BFO_OFFSET:
873 case BFO_LOCK:
874 case BFO_NUM_FOCUS_RUNS:
875 case BFO_AF_RUN_1:
876 break;
877
878 case BFO_AVERAGE:
879 return m_BFOModel.columnCount() - 3;
880 break;
881
882 case BFO_NEW_OFFSET:
883 return m_BFOModel.columnCount() - 2;
884 break;
885
886 case BFO_SAVE_CHECK:
887 return m_BFOModel.columnCount() - 1;
888 break;
889
890 default:
891 break;
892 }
893 return id;
894}
895
896// Get the number of AF runs for the passed in row
897int BuildFilterOffsets::getNumRuns(const int row)
898{
899 return m_BFOModel.item(row, getColumn(BFO_NUM_FOCUS_RUNS))->text().toInt();
900}
901
902// Get the maximum number of AF runs
903int BuildFilterOffsets::getMaxRuns()
904{
905 return getColumn(BFO_AVERAGE) - getColumn(BFO_AF_RUN_1);
906}
907
908}
static KStars * Instance()
Definition kstars.h:123
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:79
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
KGuiItem stop()
KGuiItem cancel()
void clicked(bool checked)
void toggled(bool checked)
void doubleClicked(const QModelIndex &index)
virtual void done(int r)
virtual int exec()
int column() const const
int row() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
T dequeue()
void enqueue(const T &t)
T * get() const const
int column() const const
int row() const const
void setEditable(bool editable)
QString text() const const
virtual int columnCount(const QModelIndex &parent) const const override
QStandardItem * item(int row, int column) const const
void itemChanged(QStandardItem *item)
virtual int rowCount(const QModelIndex &parent) const const override
void setColumnCount(int columns)
virtual bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) override
void setHorizontalHeaderItem(int column, QStandardItem *item)
void setHorizontalHeaderLabels(const QStringList &labels)
void setItem(int row, QStandardItem *item)
void setRowCount(int rows)
QString arg(Args &&... args) const const
QString number(double n, char format, int precision)
int toInt(bool *ok, int base) const const
ToolTipRole
Horizontal
QFuture< T > run(Function function,...)
void setEnabled(bool)
void setupUi(QWidget *widget)
void resize(const QSize &)
void setToolTip(const QString &)
void setWindowFlags(Qt::WindowFlags type)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 3 2024 11:49:50 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.