File indexing completed on 2024-04-28 03:43:05
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 }