File indexing completed on 2024-04-28 15:09:08

0001 /*
0002     SPDX-FileCopyrightText: 2023 John Evans <john.e.evans.email@googlemail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "buildfilteroffsets.h"
0008 #include <kstars_debug.h>
0009 #include "kstars.h"
0010 #include "ekos/auxiliary/tabledelegate.h"
0011 #include "Options.h"
0012 
0013 namespace Ekos
0014 {
0015 
0016 // buildFilterOffsets (BFO) sets up a dialog to manage automatic calculation of filter offsets
0017 // by performing autofocus runs on user selected filters and working out the relative offsets.
0018 // The idea is to use the utility once in a while to setup the offsets which them enables the user to avoid having
0019 // to do an autofocus run when swapping filters. The benefit of this might be to avoid focusing on a difficult-to-
0020 // focus filter but focusing on a lock filter and then using the offset; or just to avoid an Autofocus run when
0021 // changing filter.
0022 //
0023 // The utility follows the standard Ekos cpp/h/ui file setup. The basic dialog is contained in the .ui file but
0024 // because the table is constructed from data at runtime some of the GUI is constructed in code in the .cpp.
0025 // BFO uses the MVC design pattern. The BuildFilterOffset object is a friend of FilterManager because BFO is closely
0026 // related to FilterManager. This allows several FilterManager methods to be accessed by BFO. In addition, signals
0027 // with Focus are passed via FilterManager.
0028 //
0029 // On launch, BFO displays a grid containing all the filters in the Filter Settings. Relevant information is copied across
0030 // to BFO and the filters appear in the same order. The 1st filter is suffiixed with a "*", marking it as the reference
0031 // filter which is the one against which all other relevant offsets are measured.
0032 //
0033 // To change the reference filter, the user double clicks another filter.
0034 //
0035 // The user needs to set the number of Autofocus (AF) runs to perform on each filter. The default is 5, the maximum is 10.
0036 // Setting this field to 0, removes the associated filter from further processing.
0037 //
0038 // The user presses Run and the utility moves to the processing stage. Extra columns are displayed, one for each AF run,
0039 // as well as average, new offset and save columns. For each filter the number of requested AF runs is performed. The
0040 // table cell associated with the in-flight AF run is highlighted. As an AF run completes the results are displayed in
0041 // the table. When all AF runs for a filter are complete, processing moves to the next filter.
0042 //
0043 // Normally, if a lock filter is configured for a particular filter, then when focusing, the lock filter is swapped in
0044 // AF runs, and then the original filter is swapped back into position. For BFO this model is inappropriate. When an
0045 // AF run is requested on a filter then AF is always run on that filter so the lock filter policy is not honoured when
0046 // running AF from BFO.
0047 //
0048 // The user can interrupt processing by pressing stop. A confirm dialog then allows the user to stop BFO or resume.
0049 //
0050 // 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
0051 // run, then BFO will take over an hour. During this time environmental conditions such as temperature and altitude
0052 // could change the focus point. For this reaons, it it possible Adapt each focus run back to the temp and alt applicable
0053 // during the first AF run for more accurate calculation of the offsets. If Adapt Focus is checked, then the adapted
0054 // vales are used in calculations; if not checked then the raw (unadapted) values are used. The toggle can be used at
0055 // any time. The tooltip on each AF run provides a tabular explanation of the adaptations and how the raw value is
0056 // changed to the adapted value. In order to use Adapt Focus, the ticks per temperature and ticks per altitude fields
0057 // in the Fillter Settings popup need to be filled in appropriately for each filter being processed.
0058 //
0059 // If AF fails then the user is prompted to retry or abort the processing.
0060 //
0061 // When processing completes the user can review the results. Each processed filter's new offset has an associated save
0062 // checkbox allowing all or some values to be saved. Saving persists the new offset values in the Filter Settings popup
0063 // for future use during imaging.
0064 //
0065 // The average AF value for a filter is a simple mean. There are typically not enough sample points taken for robust
0066 // statistical processing to add any value. So the user needs to review the AF values and decide if they want to remove
0067 // any outliers (set the AF value to 0 to exclude from processing, or adjust the number). In addition, it is possible
0068 // to override the offset with a manually entered value.
0069 //
0070 BuildFilterOffsets::BuildFilterOffsets(QSharedPointer<FilterManager> filterManager)
0071 {
0072 #ifdef Q_OS_OSX
0073     setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
0074 #endif
0075 
0076     if (filterManager.isNull())
0077         return;
0078 
0079     m_filterManager = filterManager;
0080 
0081     setupUi(this);
0082     setupConnections();
0083     initBuildFilterOffsets();
0084     setupBuildFilterOffsetsTable();
0085     setupGUI();
0086 
0087     // Launch the dialog - synchronous call
0088     this->exec();
0089 }
0090 
0091 BuildFilterOffsets::~BuildFilterOffsets()
0092 {
0093 }
0094 
0095 void BuildFilterOffsets::setupConnections()
0096 {
0097     // Connections to FilterManager
0098     connect(this, &BuildFilterOffsets::runAutoFocus, m_filterManager.get(), &FilterManager::signalRunAutoFocus);
0099     connect(this, &BuildFilterOffsets::abortAutoFocus, m_filterManager.get(), &FilterManager::signalAbortAutoFocus);
0100 
0101     // Connections from FilterManager
0102     connect(m_filterManager.get(), &FilterManager::autoFocusDone, this, &BuildFilterOffsets::autoFocusComplete);
0103     connect(m_filterManager.get(), &FilterManager::ready, this, &BuildFilterOffsets::buildTheOffsetsTaskComplete);
0104 
0105     // Connections internal to BuildFilterOffsets
0106     connect(this, &BuildFilterOffsets::ready, this, &BuildFilterOffsets::buildTheOffsetsTaskComplete);
0107 }
0108 
0109 void BuildFilterOffsets::setupGUI()
0110 {
0111     // Add action buttons to the button box
0112     m_runButton = buildOffsetsButtonBox->addButton("Run", QDialogButtonBox::ActionRole);
0113     m_stopButton = buildOffsetsButtonBox->addButton("Stop", QDialogButtonBox::ActionRole);
0114 
0115     // Set tooltips for the buttons
0116     m_runButton->setToolTip("Run Build Filter Offsets utility");
0117     m_stopButton->setToolTip("Interrupt processing when utility is running");
0118     buildOffsetsButtonBox->button(QDialogButtonBox::Save)->setToolTip("Save New Offsets");
0119 
0120     // Set the buttons' state
0121     setBuildFilterOffsetsButtons(BFO_INIT);
0122 
0123     // Connect up button callbacks
0124     connect(m_runButton, &QPushButton::clicked, this, &BuildFilterOffsets::buildTheOffsets);
0125     connect(m_stopButton, &QPushButton::clicked, this, &BuildFilterOffsets::stopProcessing);
0126     connect(buildOffsetsButtonBox->button(QDialogButtonBox::Save), &QPushButton::clicked, this,
0127             &BuildFilterOffsets::saveTheOffsets);
0128     connect(buildOffsetsButtonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, [this]()
0129     {
0130         // Close the dialog down unless lots of processing has been done. In that case put up an "are you sure" popup
0131         if (!m_tableInEditMode)
0132             this->done(QDialog::Rejected);
0133         else if (KMessageBox::questionYesNo(KStars::Instance(),
0134                                             i18n("Are you sure you want to quit?")) == KMessageBox::Yes)
0135             this->done(QDialog::Rejected);
0136     });
0137 
0138     // Setup the Adapt Focus checkbox
0139     buildOffsetsAdaptFocus->setChecked(Options::adaptFocusBFO());
0140     connect(buildOffsetsAdaptFocus, &QCheckBox::toggled, this, [&](bool checked)
0141     {
0142         Options::setAdaptFocusBFO(checked);
0143         reloadPositions(checked);
0144     });
0145 
0146     // Connect cell changed callback
0147     connect(&m_BFOModel, &QStandardItemModel::itemChanged, this, &BuildFilterOffsets::itemChanged);
0148 
0149     // Connect double click callback
0150     connect(buildOffsetsTableView, &QAbstractItemView::doubleClicked, this, &BuildFilterOffsets::refChanged);
0151 
0152     // Display an initial message in the status bar
0153     buildOffsetsStatusBar->showMessage(i18n("Idle"));
0154 
0155     // Resize the dialog based on the data
0156     buildOffsetsDialogResize();
0157 }
0158 
0159 void BuildFilterOffsets::initBuildFilterOffsets()
0160 {
0161     m_inBuildOffsets = false;
0162     m_stopFlag = m_problemFlag = m_abortAFPending = m_tableInEditMode = false;
0163     m_filters.clear();
0164     m_refFilter = -1;
0165     m_rowIdx = m_colIdx = 0;
0166 
0167     // Drain any old queue items
0168     m_buildOffsetsQ.clear();
0169 }
0170 
0171 void BuildFilterOffsets::setupBuildFilterOffsetsTable()
0172 {
0173     // Setup MVC
0174     buildOffsetsTableView->setModel(&m_BFOModel);
0175 
0176     // Setup the table view
0177     QStringList Headers { i18n("Filter"), i18n("Offset"), i18n("Lock Filter"), i18n("# Focus Runs") };
0178     m_BFOModel.setColumnCount(Headers.count());
0179     m_BFOModel.setHorizontalHeaderLabels(Headers);
0180 
0181     // Setup tooltips on column headers
0182     m_BFOModel.setHeaderData(getColumn(BFO_FILTER), Qt::Horizontal,
0183                              i18n("Filter. * indicates reference filter. Double click to change"),
0184                              Qt::ToolTipRole);
0185     m_BFOModel.setHeaderData(getColumn(BFO_NUM_FOCUS_RUNS), Qt::Horizontal, i18n("# Focus Runs. Set per filter. 0 to ignore"),
0186                              Qt::ToolTipRole);
0187 
0188     // Setup edit delegates for each column
0189     // No Edit delegates for Filter, Offset and Lock Filter
0190     NotEditableDelegate *noEditDel = new NotEditableDelegate(buildOffsetsTableView);
0191     buildOffsetsTableView->setItemDelegateForColumn(getColumn(BFO_FILTER), noEditDel);
0192     buildOffsetsTableView->setItemDelegateForColumn(getColumn(BFO_OFFSET), noEditDel);
0193     buildOffsetsTableView->setItemDelegateForColumn(getColumn(BFO_LOCK), noEditDel);
0194 
0195     // # Focus Runs delegate
0196     IntegerDelegate *numRunsDel = new IntegerDelegate(buildOffsetsTableView, 0, 10, 1);
0197     buildOffsetsTableView->setItemDelegateForColumn(getColumn(BFO_NUM_FOCUS_RUNS), numRunsDel);
0198 
0199     // Setup the table
0200     m_BFOModel.setRowCount(m_filterManager->m_ActiveFilters.count());
0201 
0202     // Load the data for each filter
0203     for (int row = 0 ; row < m_filterManager->m_ActiveFilters.count(); row++)
0204     {
0205         // Filter name
0206         m_filters.push_back(m_filterManager->m_ActiveFilters[row]->color());
0207         QString str;
0208         if (row == 0)
0209         {
0210             str = QString("%1 *").arg(m_filters[row]);
0211             m_refFilter = 0;
0212         }
0213         else
0214             str = m_filters[row];
0215 
0216         QStandardItem* item0 = new QStandardItem(str);
0217         m_BFOModel.setItem(row, getColumn(BFO_FILTER), item0);
0218 
0219         // Offset
0220         QStandardItem* item1 = new QStandardItem(QString::number(m_filterManager->m_ActiveFilters[row]->offset()));
0221         m_BFOModel.setItem(row, getColumn(BFO_OFFSET), item1);
0222 
0223         // Lock filter
0224         QStandardItem* item2 = new QStandardItem(m_filterManager->m_ActiveFilters[row]->lockedFilter());
0225         m_BFOModel.setItem(row, getColumn(BFO_LOCK), item2);
0226 
0227         // Number of AF runs to perform
0228         QStandardItem* item3 = new QStandardItem(QString::number(5));
0229         m_BFOModel.setItem(row, getColumn(BFO_NUM_FOCUS_RUNS), item3);
0230     }
0231 }
0232 
0233 void BuildFilterOffsets::setBuildFilterOffsetsButtons(const BFOButtonState state)
0234 {
0235     switch (state)
0236     {
0237         case BFO_INIT:
0238             m_runButton->setEnabled(true);
0239             m_stopButton->setEnabled(false);
0240             buildOffsetsButtonBox->button(QDialogButtonBox::Save)->setEnabled(false);
0241             buildOffsetsButtonBox->button(QDialogButtonBox::Close)->setEnabled(true);
0242             break;
0243 
0244         case BFO_RUN:
0245             m_runButton->setEnabled(false);
0246             m_stopButton->setEnabled(true);
0247             buildOffsetsButtonBox->button(QDialogButtonBox::Save)->setEnabled(false);
0248             buildOffsetsButtonBox->button(QDialogButtonBox::Close)->setEnabled(false);
0249             break;
0250 
0251         case BFO_SAVE:
0252             m_runButton->setEnabled(false);
0253             m_stopButton->setEnabled(false);
0254             buildOffsetsButtonBox->button(QDialogButtonBox::Save)->setEnabled(true);
0255             buildOffsetsButtonBox->button(QDialogButtonBox::Close)->setEnabled(true);
0256             break;
0257 
0258         case BFO_STOP:
0259             m_runButton->setEnabled(false);
0260             m_stopButton->setEnabled(false);
0261             buildOffsetsButtonBox->button(QDialogButtonBox::Save)->setEnabled(false);
0262             buildOffsetsButtonBox->button(QDialogButtonBox::Close)->setEnabled(false);
0263             break;
0264 
0265         default:
0266             break;
0267     }
0268 }
0269 
0270 // Loop through all the filters to process, and for each...
0271 // - set Autofocus to use the filter
0272 // - Loop for the number of runs chosen by the user for that filter
0273 //   - Run AF
0274 //   - Get the focus solution
0275 //   - Load the focus solution into table widget in the appropriate cell
0276 // - Calculate the average AF solution for that filter and display it
0277 void BuildFilterOffsets::buildTheOffsets()
0278 {
0279     buildOffsetsQItem qItem;
0280 
0281     // Set the buttons
0282     setBuildFilterOffsetsButtons(BFO_RUN);
0283 
0284     // Make the Number of runs column not editable
0285     // No Edit delegates for Filter, Offset and Lock Filter
0286     QPointer<NotEditableDelegate> noEditDel = new NotEditableDelegate(buildOffsetsTableView);
0287     buildOffsetsTableView->setItemDelegateForColumn(getColumn(BFO_NUM_FOCUS_RUNS), noEditDel);
0288 
0289     // Loop through the work to do and load up Queue and extend tableWidget to record AF answers
0290     int maxAFRuns = 0;
0291     int startRow = -1;
0292     for (int row = 0; row < m_filters.count(); row++ )
0293     {
0294         const int numRuns = m_BFOModel.item(row, getColumn(BFO_NUM_FOCUS_RUNS))->text().toInt();
0295         if (numRuns > 0)
0296         {
0297             if (startRow < 0)
0298                 startRow = row;
0299 
0300             // Set the filter change
0301             qItem.color = m_filters[row];
0302             qItem.changeFilter = true;
0303             m_buildOffsetsQ.enqueue(qItem);
0304 
0305             // Load up the AF runs based on how many the user requested
0306             qItem.changeFilter = false;
0307             maxAFRuns = std::max(maxAFRuns, numRuns);
0308             for (int runNum = 1; runNum <= numRuns; runNum++)
0309             {
0310                 qItem.numAFRun = runNum;
0311                 m_buildOffsetsQ.enqueue(qItem);
0312             }
0313         }
0314     }
0315 
0316     // Add columns to the Model for AF runs and set the headers. Each AF run result is editable
0317     // but the calculated average is not.
0318     int origCols = m_BFOModel.columnCount();
0319     m_BFOModel.setColumnCount(origCols + maxAFRuns + 3);
0320     for (int col = 0; col < maxAFRuns; col++)
0321     {
0322         QStandardItem *newItem = new QStandardItem(i18n("AF Run %1", col + 1));
0323         m_BFOModel.setHorizontalHeaderItem(origCols + col, newItem);
0324         m_BFOModel.setHeaderData(origCols + col, Qt::Horizontal,
0325                                  i18n("AF Run %1. Calculated automatically but can be edited. Set to 0 to exclude from average.", col + 1),
0326                                  Qt::ToolTipRole);
0327         buildOffsetsTableView->setItemDelegateForColumn(origCols + col, noEditDel);
0328     }
0329 
0330     // Add 3 more columns for the average of the AF runs, the offset and whether to save the offset
0331     QStandardItem *averageItem = new QStandardItem(i18n("Average"));
0332     m_BFOModel.setHorizontalHeaderItem(origCols + maxAFRuns, averageItem);
0333     m_BFOModel.setHeaderData(origCols + maxAFRuns, Qt::Horizontal, i18n("AF Average (mean)."), Qt::ToolTipRole);
0334     buildOffsetsTableView->setItemDelegateForColumn(origCols + maxAFRuns, noEditDel);
0335 
0336     QStandardItem *offsetItem = new QStandardItem(i18n("New Offset"));
0337     m_BFOModel.setHorizontalHeaderItem(origCols + maxAFRuns + 1, offsetItem);
0338     m_BFOModel.setHeaderData(origCols + maxAFRuns + 1, Qt::Horizontal,
0339                              i18n("New Offset. Calculated relative to Filter with *. Can be edited."), Qt::ToolTipRole);
0340     buildOffsetsTableView->setItemDelegateForColumn(origCols + maxAFRuns + 1, noEditDel);
0341 
0342     QPointer<ToggleDelegate> saveDelegate = new ToggleDelegate(&m_BFOModel);
0343     QStandardItem *saveItem = new QStandardItem(i18n("Save"));
0344     m_BFOModel.setHorizontalHeaderItem(origCols + maxAFRuns + 2, saveItem);
0345     m_BFOModel.setHeaderData(origCols + maxAFRuns + 2, Qt::Horizontal,
0346                              i18n("Save. Check to save the New Offset for the associated Filter."), Qt::ToolTipRole);
0347     buildOffsetsTableView->setItemDelegateForColumn(origCols + maxAFRuns + 2, saveDelegate);
0348 
0349     // Resize the dialog
0350     buildOffsetsDialogResize();
0351 
0352     // Set the selected cell to the first AF run of the ref filter
0353     m_rowIdx = startRow;
0354     m_colIdx = getColumn(BFO_AF_RUN_1);
0355     QModelIndex index = buildOffsetsTableView->model()->index(m_rowIdx, m_colIdx);
0356     buildOffsetsTableView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect);
0357 
0358     // Initialise the progress bar
0359     buildOffsetsProgressBar->reset();
0360     buildOffsetsProgressBar->setRange(0, m_buildOffsetsQ.count());
0361     buildOffsetsProgressBar->setValue(0);
0362 
0363     // The Q has been loaded with all required actions so lets start processing
0364     m_inBuildOffsets = true;
0365     runBuildOffsets();
0366 }
0367 
0368 // This is signalled when an asynchronous task has been completed, either
0369 // a filter change or an autofocus run
0370 void BuildFilterOffsets::buildTheOffsetsTaskComplete()
0371 {
0372     if (m_stopFlag)
0373     {
0374         // User hit the stop button, so see what they want to do
0375         if (KMessageBox::warningContinueCancel(KStars::Instance(),
0376                                                i18n("Are you sure you want to stop Build Filter Offsets?"), i18n("Stop Build Filter Offsets"),
0377                                                KStandardGuiItem::stop(), KStandardGuiItem::cancel(), "") == KMessageBox::Cancel)
0378         {
0379             // User wants to retry processing
0380             m_stopFlag = false;
0381             setBuildFilterOffsetsButtons(BFO_RUN);
0382 
0383             if (m_abortAFPending)
0384             {
0385                 // If the in-flight task was aborted then retry - don't take the next task off the Q
0386                 m_abortAFPending = false;
0387                 processQItem(m_qItemInProgress);
0388             }
0389             else
0390             {
0391                 // No tasks were aborted so we can just start the next task in the queue
0392                 buildOffsetsProgressBar->setValue(buildOffsetsProgressBar->value() + 1);
0393                 runBuildOffsets();
0394             }
0395         }
0396         else
0397         {
0398             // User wants to abort
0399             m_stopFlag = m_abortAFPending = false;
0400             this->done(QDialog::Rejected);
0401         }
0402     }
0403     else if (m_problemFlag)
0404     {
0405         // The in flight task had a problem so see what the user wants to do
0406         if (KMessageBox::warningContinueCancel(KStars::Instance(),
0407                                                i18n("An unexpected problem occurred.\nStop Build Filter Offsets, or Cancel to retry?"),
0408                                                i18n("Build Filter Offsets Unexpected Problem"),
0409                                                KStandardGuiItem::stop(), KStandardGuiItem::cancel(), "") == KMessageBox::Cancel)
0410         {
0411             // User wants to retry
0412             m_problemFlag = false;
0413             processQItem(m_qItemInProgress);
0414         }
0415         else
0416         {
0417             // User wants to abort
0418             m_problemFlag = false;
0419             this->done(QDialog::Rejected);
0420         }
0421     }
0422     else
0423     {
0424         // All good so update the progress bar and process the next task
0425         buildOffsetsProgressBar->setValue(buildOffsetsProgressBar->value() + 1);
0426         runBuildOffsets();
0427     }
0428 }
0429 
0430 // Resize the dialog to the data
0431 void BuildFilterOffsets::buildOffsetsDialogResize()
0432 {
0433     // Resize the columns to the data
0434     buildOffsetsTableView->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);
0435 
0436     // Resize the dialog to the width and height of the table widget
0437     const int width = buildOffsetsTableView->horizontalHeader()->length() + 40;
0438     const int height = buildOffsetsTableView->verticalHeader()->length() + buildOffsetsButtonBox->height() +
0439                        buildOffsetsProgressBar->height() + buildOffsetsStatusBar->height() + 60;
0440     this->resize(width, height);
0441 }
0442 
0443 void BuildFilterOffsets::runBuildOffsets()
0444 {
0445     if (m_buildOffsetsQ.isEmpty())
0446     {
0447         // All tasks have been actioned so allow the user to edit the results, save them or quit.
0448         setCellsEditable();
0449         setBuildFilterOffsetsButtons(BFO_SAVE);
0450         m_tableInEditMode = true;
0451         buildOffsetsStatusBar->showMessage(i18n("Processing complete."));
0452     }
0453     else
0454     {
0455         // Take the next item off the queue
0456         m_qItemInProgress = m_buildOffsetsQ.dequeue();
0457         processQItem(m_qItemInProgress);
0458     }
0459 }
0460 
0461 void BuildFilterOffsets::processQItem(const buildOffsetsQItem currentItem)
0462 {
0463     if (currentItem.changeFilter)
0464     {
0465         // Need to change filter
0466         buildOffsetsStatusBar->showMessage(i18n("Changing filter to %1...", currentItem.color));
0467 
0468         auto pos = m_filterManager->m_currentFilterLabels.indexOf(currentItem.color) + 1;
0469         if (!m_filterManager->setFilterPosition(pos, m_filterManager->CHANGE_POLICY))
0470         {
0471             // Filter wheel position change failed.
0472             buildOffsetsStatusBar->showMessage(i18n("Problem changing filter to %1...", currentItem.color));
0473             m_problemFlag = true;
0474         }
0475     }
0476     else
0477     {
0478         // Signal an AF run with an arg of "build offsets"
0479         const int run = m_colIdx - getColumn(BFO_AF_RUN_1) + 1;
0480         const int numRuns = m_BFOModel.item(m_rowIdx, getColumn(BFO_NUM_FOCUS_RUNS))->text().toInt();
0481         buildOffsetsStatusBar->showMessage(i18n("Running Autofocus on %1 (%2/%3)...", currentItem.color, run, numRuns));
0482         emit runAutoFocus(m_inBuildOffsets);
0483     }
0484 }
0485 
0486 // This is called at the end of an AF run
0487 void BuildFilterOffsets::autoFocusComplete(FocusState completionState, int position, double temperature, double altitude)
0488 {
0489     if (!m_inBuildOffsets)
0490         return;
0491 
0492     if (completionState != FOCUS_COMPLETE)
0493     {
0494         // The AF run has failed. If the user aborted the run then this is an expected signal
0495         if (!m_abortAFPending)
0496             // In this case the failure is a genuine problem so set a problem flag
0497             m_problemFlag = true;
0498     }
0499     else
0500     {
0501         // AF run was successful so load the solution results
0502         processAFcomplete(position, temperature, altitude);
0503 
0504         // Load the result into the table. The Model update will trigger further updates
0505         loadPosition(buildOffsetsAdaptFocus->isChecked(), m_rowIdx, m_colIdx);
0506 
0507         // Now see what's next, another AF run on this filter or are we moving to the next filter
0508         if (m_colIdx - getColumn(BFO_NUM_FOCUS_RUNS) < getNumRuns(m_rowIdx))
0509             m_colIdx++;
0510         else
0511         {
0512             // Move the active cell to the next AF run in the table
0513             // Usually this will be the next row, but if this row has zero AF runs skip to the next
0514             for (int nextRow = m_rowIdx + 1; nextRow < m_filters.count(); nextRow++)
0515             {
0516                 if (getNumRuns(nextRow) > 0)
0517                 {
0518                     // Found the next filter to process
0519                     m_rowIdx = nextRow;
0520                     m_colIdx = getColumn(BFO_AF_RUN_1);
0521                     break;
0522                 }
0523             }
0524         }
0525         // Highlight the next cell...
0526         const QModelIndex index = buildOffsetsTableView->model()->index(m_rowIdx, m_colIdx);
0527         buildOffsetsTableView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect);
0528     }
0529     // Signal the next processing step
0530     emit ready();
0531 }
0532 
0533 // Called to store the AF position details. The raw AF position is passed in (from Focus)
0534 // The Adapted position (based on temperature and altitude) is calculated
0535 // The sense of the adaptation is to take the current position and adapt it to the
0536 // conditions (temp and alt) of the reference.
0537 void BuildFilterOffsets::processAFcomplete(const int position, const double temperature, const double altitude)
0538 {
0539     AFSolutionDetail solution;
0540 
0541     solution.position = position;
0542     solution.temperature = temperature;
0543     solution.altitude = altitude;
0544     solution.ticksPerTemp = m_filterManager->getFilterTicksPerTemp(m_filters[m_rowIdx]);
0545     solution.ticksPerAlt = m_filterManager->getFilterTicksPerAlt(m_filters[m_rowIdx]);
0546 
0547     if (m_rowIdx == 0 && m_colIdx == getColumn(BFO_AF_RUN_1))
0548     {
0549         // Set the reference temp and alt to be the first AF run
0550         m_refTemperature = temperature;
0551         m_refAltitude = altitude;
0552     }
0553 
0554     // Calculate the temperature adaptation
0555     if (temperature == INVALID_VALUE || m_refTemperature == INVALID_VALUE)
0556     {
0557         solution.deltaTemp = 0.0;
0558         solution.deltaTicksTemperature = 0.0;
0559     }
0560     else
0561     {
0562         solution.deltaTemp = m_refTemperature - temperature;
0563         solution.deltaTicksTemperature = solution.ticksPerTemp * solution.deltaTemp;
0564     }
0565 
0566     // Calculate the altitude adaptation
0567     if (altitude == INVALID_VALUE || m_refAltitude == INVALID_VALUE)
0568     {
0569         solution.deltaAlt = 0.0;
0570         solution.deltaTicksAltitude = 0.0;
0571     }
0572     else
0573     {
0574         solution.deltaAlt = m_refAltitude - altitude;
0575         solution.deltaTicksAltitude = solution.ticksPerAlt * solution.deltaAlt;
0576     }
0577 
0578     // Calculate the total adaptation
0579     solution.deltaTicksTotal = static_cast<int>(round(solution.deltaTicksTemperature + solution.deltaTicksAltitude));
0580 
0581     // Calculate the Adapted position
0582     solution.adaptedPosition = position + solution.deltaTicksTotal;
0583 
0584     m_AFSolutions.push_back(solution);
0585 }
0586 
0587 // Load the focus position depending on the setting of the adaptPos checkbox. The Model update will trigger further updates
0588 void BuildFilterOffsets::loadPosition(const bool checked, const int row, const int col)
0589 {
0590     int idx = 0;
0591     // Work out the array index for m_AFSolutions
0592     for (int i = 0; i < row; i++)
0593         idx += getNumRuns(i);
0594 
0595     idx += col - getColumn(BFO_AF_RUN_1);
0596 
0597     // Check that the passed in row, col has been processed. If not, nothing to do
0598     if (idx < m_AFSolutions.count())
0599     {
0600         // Get the AF position to use based on the setting of 'checked'
0601         int pos = (checked) ? m_AFSolutions[idx].adaptedPosition : m_AFSolutions[idx].position;
0602 
0603         // Present a tooltip explanation of how the original position is changed by adaptation, e.g.
0604         //                    Adapt Focus Explainer
0605         //               Position  Temperature (°C)   Altitude (°Alt)
0606         // Measured Pos: 36704     T: 0.9°C (ΔT=0.7)  Alt: 40.1° (ΔAlt=-4.2)
0607         // Adaptations:      4     T: 7.10 ticks      Alt: -3.20 ticks
0608         // Adapted Pos:  36708
0609         //
0610         const QString temp = QString("%1").arg(m_AFSolutions[idx].temperature, 0, 'f', 1);
0611         const QString deltaTemp = i18n("(ΔT=%1)", QString("%1").arg(m_AFSolutions[idx].deltaTemp, 0, 'f', 1));
0612         const QString ticksTemp = i18n("(%1 ticks)", QString("%1").arg(m_AFSolutions[idx].deltaTicksTemperature, 0, 'f', 1));
0613         const QString alt = QString("%1").arg(m_AFSolutions[idx].altitude, 0, 'f', 1);
0614         const QString deltaAlt = i18n("(ΔAlt=%1)", QString("%1").arg(m_AFSolutions[idx].deltaAlt, 0, 'f', 1));
0615         const QString ticksAlt = i18n("(%1 ticks)", QString("%1").arg(m_AFSolutions[idx].deltaTicksAltitude, 0, 'f', 1));
0616 
0617         QStandardItem *posItem = new QStandardItem(QString::number(pos));
0618         const QString toolTip =
0619             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"
0620                   "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",
0621                   "<head><style>"
0622                   "  th, td, caption {white-space: nowrap; padding-left: 5px; padding-right: 5px;}"
0623                   "  th { text-align: left;}"
0624                   "  td { text-align: right;}"
0625                   "  caption { text-align: center; vertical-align: top; font-weight: bold; margin: 0px; padding-bottom: 5px;}"
0626                   "</head></style>"
0627                   "<body><table>"
0628                   "<caption align=top>Adapt Focus Explainer</caption>"
0629                   "<tr><th></th><th>Position</th><th>Temperature (°C)</th><th>Altitude (°Alt)</th></tr>"
0630                   "<tr><th>Measured Pos</th><td>%1</td><td>%2 %3</td><td>%4 %5</td></tr>"
0631                   "<tr><th>Adaptations</th><td>%6</td><td>%7</td><td>%8</td></tr>"
0632                   "<tr><th>Adapted Pos</th><td>%9</td></tr>"
0633                   "</table></body>",
0634                   m_AFSolutions[idx].position, temp, deltaTemp, alt, deltaAlt,
0635                   m_AFSolutions[idx].deltaTicksTotal, ticksTemp, ticksAlt,
0636                   m_AFSolutions[idx].adaptedPosition);
0637 
0638         posItem->setToolTip(toolTip);
0639         m_BFOModel.setItem(row, col, posItem);
0640     }
0641 }
0642 
0643 // Reload the focus position grid depending on the setting of the adaptPos checkbox.
0644 void BuildFilterOffsets::reloadPositions(const bool checked)
0645 {
0646     for (int row = 0; row <= m_rowIdx; row++)
0647     {
0648         const int numRuns = getNumRuns(row);
0649         const int maxCol = (row < m_rowIdx) ? numRuns : m_colIdx - getColumn(BFO_AF_RUN_1) + 1;
0650         for (int col = 0; col < maxCol; col++)
0651             loadPosition(checked, row, col + getColumn(BFO_AF_RUN_1));
0652     }
0653 }
0654 
0655 // Called when the user wants to persist the calculated offsets
0656 void BuildFilterOffsets::saveTheOffsets()
0657 {
0658     for (int row = 0; row < m_filters.count(); row++)
0659     {
0660         // Check there's an item set for the current row before accessing
0661         if (m_BFOModel.item(row, getColumn(BFO_SAVE_CHECK))->text().toInt())
0662         {
0663             // Save item is set so persist the offset
0664             const int offset = m_BFOModel.item(row, getColumn(BFO_NEW_OFFSET))->text().toInt();
0665             if (!m_filterManager->setFilterOffset(m_filters[row], offset))
0666                 qCDebug(KSTARS) << "Unable to save calculated offset for filter " << m_filters[row];
0667         }
0668     }
0669     // All done so close the dialog
0670     this->done(QDialog::Accepted);
0671 }
0672 
0673 // Processing done so make certain cells editable for the user
0674 void BuildFilterOffsets::setCellsEditable()
0675 {
0676     // Enable an edit delegate on the AF run result columns so the user can adjust as necessary
0677     // The delegates operate at the row or column level so some cells need to be manually disabled
0678     for (int col = getColumn(BFO_AF_RUN_1); col < getColumn(BFO_AF_RUN_1) + getMaxRuns(); col++)
0679     {
0680         IntegerDelegate *AFDel = new IntegerDelegate(buildOffsetsTableView, 0, 1000000, 1);
0681         buildOffsetsTableView->setItemDelegateForColumn(col, AFDel);
0682 
0683         // Disable any cells where for that filter less AF runs were requested
0684         for (int row = 0; row < m_BFOModel.rowCount(); row++)
0685         {
0686             const int numRuns = getNumRuns(row);
0687             if ((numRuns > 0) && (col > numRuns + getColumn(BFO_AF_RUN_1) - 1))
0688             {
0689                 QStandardItem *currentItem = new QStandardItem();
0690                 currentItem->setEditable(false);
0691                 m_BFOModel.setItem(row, col, currentItem);
0692             }
0693         }
0694     }
0695 
0696     // Offset column
0697     IntegerDelegate *offsetDel = new IntegerDelegate(buildOffsetsTableView, -10000, 10000, 1);
0698     buildOffsetsTableView->setItemDelegateForColumn(getColumn(BFO_NEW_OFFSET), offsetDel);
0699 
0700     // Save column
0701     ToggleDelegate *saveDel = new ToggleDelegate(buildOffsetsTableView);
0702     buildOffsetsTableView->setItemDelegateForColumn(getColumn(BFO_SAVE_CHECK), saveDel);
0703 
0704     // Check filters where user requested zero AF runs
0705     for (int row = 0; row < m_filters.count(); row++)
0706     {
0707         if (getNumRuns(row) <= 0)
0708         {
0709             // Uncheck save just in case user activated it
0710             QStandardItem *saveItem = new QStandardItem("");
0711             m_BFOModel.setItem(row, getColumn(BFO_SAVE_CHECK), saveItem);
0712             NotEditableDelegate *newDelegate = new NotEditableDelegate(buildOffsetsTableView);
0713             buildOffsetsTableView->setItemDelegateForRow(row, newDelegate);
0714         }
0715     }
0716 }
0717 
0718 void BuildFilterOffsets::stopProcessing()
0719 {
0720     m_stopFlag = true;
0721     setBuildFilterOffsetsButtons(BFO_STOP);
0722 
0723     if (m_qItemInProgress.changeFilter)
0724     {
0725         // Change filter in progress. Let it run to completion
0726         m_abortAFPending = false;
0727     }
0728     else
0729     {
0730         // AF run is currently in progress so signal an abort
0731         buildOffsetsStatusBar->showMessage(i18n("Aborting Autofocus..."));
0732         m_abortAFPending = true;
0733         emit abortAutoFocus();
0734     }
0735 }
0736 
0737 // Callback when an item in the Model is changed
0738 void BuildFilterOffsets::itemChanged(QStandardItem *item)
0739 {
0740     if (item->column() == getColumn(BFO_NUM_FOCUS_RUNS))
0741     {
0742         if (item->row() == m_refFilter && m_BFOModel.item(item->row(), item->column())->text().toInt() == 0)
0743             // If user is trying to set the num AF runs of the ref filter to 0, set to 1
0744             m_BFOModel.setItem(item->row(), item->column(), new QStandardItem(QString::number(1)));
0745     }
0746     else if ((item->column() >= getColumn(BFO_AF_RUN_1)) && (item->column() < getColumn(BFO_AVERAGE)))
0747     {
0748         // One of the AF runs has changed so recalc the Average and Offset
0749         calculateAFAverage(item->row(), item->column());
0750         calculateOffset(item->row());
0751     }
0752 }
0753 
0754 // Callback when a row in the Model is changed
0755 void BuildFilterOffsets::refChanged(QModelIndex index)
0756 {
0757     if (m_inBuildOffsets)
0758         return;
0759 
0760     const int row = index.row();
0761     const int col = index.column();
0762     if (col == 0 && row >= 0 && row < m_filters.count() && row != m_refFilter)
0763     {
0764         // User double clicked the filter column in a different cell to the current ref filter
0765         QStandardItem* itemSelect = new QStandardItem(QString("%1 *").arg(m_filters[row]));
0766         m_BFOModel.setItem(row, getColumn(BFO_FILTER), itemSelect);
0767 
0768         // The ref filter needs to have at least 1 AF run so that there is an average
0769         // solution to base the offsets of other filters against... so force this condition
0770         if (getNumRuns(row) == 0)
0771             m_BFOModel.setItem(row, getColumn(BFO_NUM_FOCUS_RUNS), new QStandardItem(QString::number(1)));
0772 
0773         // Reset the previous selection
0774         QStandardItem* itemDeselect = new QStandardItem(m_filters[m_refFilter]);
0775         m_BFOModel.setItem(m_refFilter, getColumn(BFO_FILTER), itemDeselect);
0776 
0777         m_refFilter = row;
0778         // Resize the cols and dialog
0779         buildOffsetsDialogResize();
0780     }
0781 }
0782 
0783 // This routine calculates the average of the AF runs. Given that the number of runs is likely to be low
0784 // a simple average is used. The user may manually adjust the values.
0785 void BuildFilterOffsets::calculateAFAverage(const int row, const int col)
0786 {
0787     int numRuns;
0788     if (m_tableInEditMode)
0789         numRuns = getNumRuns(row);
0790     else
0791         numRuns = col - getColumn(BFO_AF_RUN_1) + 1;
0792 
0793     // Firstly, the average of the AF runs
0794     double total = 0;
0795     int useableRuns = numRuns;
0796     for(int i = 0; i < numRuns; i++)
0797     {
0798         int j = m_BFOModel.item(row, getColumn(BFO_AF_RUN_1) + i)->text().toInt();
0799         if (j > 0)
0800             total += j;
0801         else
0802             useableRuns--;
0803     }
0804 
0805     const int average = (useableRuns > 0) ? static_cast<int>(round(total / useableRuns)) : 0;
0806 
0807     // Update the Model with the newly calculated average
0808     QStandardItem *averageItem = new QStandardItem(QString::number(average));
0809     m_BFOModel.setItem(row, getColumn(BFO_AVERAGE), averageItem);
0810 }
0811 
0812 // calculateOffset updates new offsets when AF averages have been calculated. There are 2 posibilities:
0813 // 1. The updated row is the reference filter in the list so update the offset of other filters
0814 // 2. The updated row is another filter in which case just update its offset
0815 void BuildFilterOffsets::calculateOffset(const int row)
0816 {
0817     if (row == m_refFilter)
0818     {
0819         // The first filter has been changed so loop through the other filters and adjust the offsets
0820         if (m_tableInEditMode)
0821         {
0822             for (int i = 0; i < m_filters.count(); i++)
0823             {
0824                 if (i != m_refFilter && getNumRuns(i) > 0)
0825                     calculateOffset(i);
0826             }
0827         }
0828         else
0829         {
0830             // If there are some filters higher in the table than ref filter then these can only be
0831             // proessed now that the ref filter has been processed.
0832             for (int i = 0; i < m_refFilter; i++)
0833             {
0834                 if (getNumRuns(i) > 0)
0835                     calculateOffset(i);
0836             }
0837         }
0838     }
0839 
0840     // If we haven't processed the ref filter yet then skip over
0841     if (m_rowIdx >= m_refFilter)
0842     {
0843         // The ref filter has been processed so we can calculate the offset from it
0844         const int average = m_BFOModel.item(row, getColumn(BFO_AVERAGE))->text().toInt();
0845         const int refFilterAverage = m_BFOModel.item(m_refFilter, getColumn(BFO_AVERAGE))->text().toInt();
0846 
0847         // Calculate the offset and set it in the model
0848         const int offset = average - refFilterAverage;
0849         QStandardItem *offsetItem = new QStandardItem(QString::number(offset));
0850         m_BFOModel.setItem(row, getColumn(BFO_NEW_OFFSET), offsetItem);
0851 
0852         // Set the save checkbox
0853         QStandardItem *saveItem = new QStandardItem(QString::number(1));
0854         m_BFOModel.setItem(row, getColumn(BFO_SAVE_CHECK), saveItem);
0855     }
0856 }
0857 
0858 // Returns the column in the table for the passed in id. The structure is:
0859 // Col 0 -- BFO_FILTER         -- Filter name
0860 // Col 1 -- BFO_OFFSET         -- Current offset value
0861 // Col 2 -- BFO_LOCK           -- Lock filter name
0862 // Col 3 -- BFO_NUM_FOCUS_RUNS -- Number of AF runs
0863 // Col 4 -- BFO_AF_RUN_1       -- 1st AF run
0864 // Col x -- BFO_AVERAGE        -- Average AF run. User selects the number of AF runs at run time
0865 // Col y -- BFO_NEW_OFFSET     -- New offset.
0866 // Col z -- BFO_SAVE_CHECK     -- Save checkbox
0867 int BuildFilterOffsets::getColumn(const BFOColID id)
0868 {
0869     switch (id)
0870     {
0871         case BFO_FILTER:
0872         case BFO_OFFSET:
0873         case BFO_LOCK:
0874         case BFO_NUM_FOCUS_RUNS:
0875         case BFO_AF_RUN_1:
0876             break;
0877 
0878         case BFO_AVERAGE:
0879             return m_BFOModel.columnCount() - 3;
0880             break;
0881 
0882         case BFO_NEW_OFFSET:
0883             return m_BFOModel.columnCount() - 2;
0884             break;
0885 
0886         case BFO_SAVE_CHECK:
0887             return m_BFOModel.columnCount() - 1;
0888             break;
0889 
0890         default:
0891             break;
0892     }
0893     return id;
0894 }
0895 
0896 // Get the number of AF runs for the passed in row
0897 int BuildFilterOffsets::getNumRuns(const int row)
0898 {
0899     return m_BFOModel.item(row, getColumn(BFO_NUM_FOCUS_RUNS))->text().toInt();
0900 }
0901 
0902 // Get the maximum number of AF runs
0903 int BuildFilterOffsets::getMaxRuns()
0904 {
0905     return getColumn(BFO_AVERAGE) - getColumn(BFO_AF_RUN_1);
0906 }
0907 
0908 }