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

0001 /*
0002     SPDX-FileCopyrightText: 2016 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "darklibrary.h"
0008 #include "Options.h"
0009 
0010 #include "ekos/manager.h"
0011 #include "ekos/capture/capture.h"
0012 #include "ekos/capture/sequencejob.h"
0013 #include "ekos/auxiliary/opticaltrainmanager.h"
0014 #include "ekos/auxiliary/profilesettings.h"
0015 #include "ekos/auxiliary/opticaltrainsettings.h"
0016 #include "kstars.h"
0017 #include "kspaths.h"
0018 #include "kstarsdata.h"
0019 #include "fitsviewer/fitsdata.h"
0020 #include "fitsviewer/fitsview.h"
0021 
0022 #include <QDesktopServices>
0023 #include <QSqlRecord>
0024 #include <QSqlTableModel>
0025 #include <QStatusBar>
0026 #include <algorithm>
0027 #include <array>
0028 
0029 namespace Ekos
0030 {
0031 DarkLibrary *DarkLibrary::_DarkLibrary = nullptr;
0032 
0033 DarkLibrary *DarkLibrary::Instance()
0034 {
0035     if (_DarkLibrary == nullptr)
0036         _DarkLibrary = new DarkLibrary(Manager::Instance());
0037 
0038     return _DarkLibrary;
0039 }
0040 
0041 DarkLibrary::DarkLibrary(QWidget *parent) : QDialog(parent)
0042 {
0043     setupUi(this);
0044 
0045     m_StatusBar = new QStatusBar(this);
0046     m_StatusLabel = new QLabel(i18n("Idle"), this);
0047     m_FileLabel = new QLabel(this);
0048     m_FileLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
0049 
0050     m_StatusBar->insertPermanentWidget(0, m_StatusLabel);
0051     m_StatusBar->insertPermanentWidget(1, m_FileLabel, 1);
0052     mainLayout->addWidget(m_StatusBar);
0053 
0054     QDir writableDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
0055     writableDir.mkpath("darks");
0056     writableDir.mkpath("defectmaps");
0057 
0058     ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
0059     // Dark Generation Connections
0060     ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
0061     m_CurrentDarkFrame.reset(new FITSData(), &QObject::deleteLater);
0062 
0063     connect(darkTableView,  &QAbstractItemView::doubleClicked, this, [this](QModelIndex index)
0064     {
0065         loadIndexInView(index.row());
0066     });
0067     connect(openDarksFolderB, &QPushButton::clicked, this, &DarkLibrary::openDarksFolder);
0068     connect(clearAllB, &QPushButton::clicked, this, &DarkLibrary::clearAll);
0069     connect(clearRowB, &QPushButton::clicked, this, &DarkLibrary::clearRow);
0070     connect(clearExpiredB, &QPushButton::clicked, this, &DarkLibrary::clearExpired);
0071     connect(refreshB, &QPushButton::clicked, this, &DarkLibrary::reloadDarksFromDatabase);
0072 
0073     connect(&m_DarkFrameFutureWatcher, &QFutureWatcher<bool>::finished, this, [this]()
0074     {
0075         // If loading is successful, then set it in current dark view
0076         if (m_DarkFrameFutureWatcher.result())
0077         {
0078             m_DarkView->loadData(m_CurrentDarkFrame);
0079             loadCurrentMasterDefectMap();
0080             populateMasterMetedata();
0081         }
0082         else
0083             m_FileLabel->setText(i18n("Failed to load %1: %2",  m_MasterDarkFrameFilename, m_CurrentDarkFrame->getLastError()));
0084 
0085     });
0086 
0087     connect(masterDarksCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [this](int index)
0088     {
0089         if (m_Camera)
0090             DarkLibrary::loadCurrentMasterDark(m_Camera->getDeviceName(), index);
0091     });
0092 
0093     connect(minExposureSpin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
0094     connect(maxExposureSpin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
0095     connect(exposureStepSin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
0096 
0097     connect(minTemperatureSpin, &QDoubleSpinBox::editingFinished, this, [this]()
0098     {
0099         maxTemperatureSpin->setMinimum(minTemperatureSpin->value());
0100         countDarkTotalTime();
0101     });
0102     connect(maxTemperatureSpin, &QDoubleSpinBox::editingFinished, this, [this]()
0103     {
0104         minTemperatureSpin->setMaximum(maxTemperatureSpin->value());
0105         countDarkTotalTime();
0106     });
0107     connect(temperatureStepSpin, &QDoubleSpinBox::editingFinished, this, [this]()
0108     {
0109         maxTemperatureSpin->setMinimum(minTemperatureSpin->value());
0110         minTemperatureSpin->setMaximum(maxTemperatureSpin->value());
0111         countDarkTotalTime();
0112     });
0113 
0114     connect(countSpin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
0115 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
0116     connect(binningButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled),
0117             this, [this](int, bool)
0118 #else
0119     connect(binningButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled),
0120             this, [this](int, bool)
0121 #endif
0122     {
0123         countDarkTotalTime();
0124     });
0125 
0126     connect(startB, &QPushButton::clicked, this, &DarkLibrary::start);
0127     connect(stopB, &QPushButton::clicked, this, &DarkLibrary::stop);
0128 
0129     KStarsData::Instance()->userdb()->GetAllDarkFrames(m_DarkFramesDatabaseList);
0130     ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
0131     // Defect Map Connections
0132     ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
0133     connect(darkTabsWidget, &QTabWidget::currentChanged, this, [this](int index)
0134     {
0135         m_DarkView->setDefectMapEnabled(index == 1 && m_CurrentDefectMap);
0136     });
0137     connect(aggresivenessHotSlider, &QSlider::valueChanged, aggresivenessHotSpin, &QSpinBox::setValue);
0138     connect(aggresivenessColdSlider, &QSlider::valueChanged, aggresivenessColdSpin, &QSpinBox::setValue);
0139     connect(hotPixelsEnabled, &QCheckBox::toggled, this, [this](bool toggled)
0140     {
0141         if (m_CurrentDefectMap)
0142             m_CurrentDefectMap->setProperty("HotEnabled", toggled);
0143     });
0144     connect(coldPixelsEnabled, &QCheckBox::toggled, this, [this](bool toggled)
0145     {
0146         if (m_CurrentDefectMap)
0147             m_CurrentDefectMap->setProperty("ColdEnabled", toggled);
0148     });
0149     connect(generateMapB, &QPushButton::clicked, this, [this]()
0150     {
0151         if (m_CurrentDefectMap)
0152         {
0153             m_CurrentDefectMap->setProperty("HotPixelAggressiveness", aggresivenessHotSpin->value());
0154             m_CurrentDefectMap->setProperty("ColdPixelAggressiveness", aggresivenessColdSpin->value());
0155             m_CurrentDefectMap->filterPixels();
0156             emit newFrame(m_DarkView);
0157         }
0158     });
0159     connect(resetMapParametersB, &QPushButton::clicked, this, [this]()
0160     {
0161         if (m_CurrentDefectMap)
0162         {
0163             aggresivenessHotSlider->setValue(75);
0164             aggresivenessColdSlider->setValue(75);
0165             m_CurrentDefectMap->setProperty("HotPixelAggressiveness", 75);
0166             m_CurrentDefectMap->setProperty("ColdPixelAggressiveness", 75);
0167             m_CurrentDefectMap->filterPixels();
0168         }
0169     });
0170     connect(saveMapB, &QPushButton::clicked, this, &DarkLibrary::saveDefectMap);
0171     ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
0172     // Settings & Initialization
0173     ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
0174     m_RememberFITSViewer = Options::useFITSViewer();
0175     m_RememberSummaryView = Options::useSummaryPreview();
0176     initView();
0177 
0178     loadGlobalSettings();
0179 
0180     connectSettings();
0181 
0182     setupOpticalTrainManager();
0183 }
0184 
0185 DarkLibrary::~DarkLibrary()
0186 {
0187 }
0188 
0189 ///////////////////////////////////////////////////////////////////////////////////////
0190 ///
0191 ///////////////////////////////////////////////////////////////////////////////////////
0192 void DarkLibrary::refreshFromDB()
0193 {
0194     KStarsData::Instance()->userdb()->GetAllDarkFrames(m_DarkFramesDatabaseList);
0195 }
0196 
0197 ///////////////////////////////////////////////////////////////////////////////////////
0198 ///
0199 ///////////////////////////////////////////////////////////////////////////////////////
0200 bool DarkLibrary::findDarkFrame(ISD::CameraChip *m_TargetChip, double duration, QSharedPointer<FITSData> &darkData)
0201 {
0202     QVariantMap bestCandidate;
0203     for (auto &map : m_DarkFramesDatabaseList)
0204     {
0205         // First check CCD name matches and check if we are on the correct chip
0206         if (map["ccd"].toString() == m_TargetChip->getCCD()->getDeviceName() &&
0207                 map["chip"].toInt() == static_cast<int>(m_TargetChip->getType()))
0208         {
0209             // Match Gain
0210             int gain = getGain();
0211             if (gain >= 0 && map["gain"].toInt() != gain)
0212                 continue;
0213 
0214             // Match ISO
0215             QString isoValue;
0216             if (m_TargetChip->getISOValue(isoValue) && map["iso"].toString() != isoValue)
0217                 continue;
0218 
0219             // Match binning
0220             int binX = 1, binY = 1;
0221             m_TargetChip->getBinning(&binX, &binY);
0222 
0223             // Then check if binning is the same
0224             if (map["binX"].toInt() != binX || map["binY"].toInt() != binY)
0225                 continue;
0226 
0227             // If camera has an active cooler, then we check temperature against the absolute threshold.
0228             if (m_TargetChip->getCCD()->hasCoolerControl())
0229             {
0230                 double temperature = 0;
0231                 m_TargetChip->getCCD()->getTemperature(&temperature);
0232                 double darkTemperature = map["temperature"].toDouble();
0233                 // If different is above threshold, it is completely rejected.
0234                 if (darkTemperature != INVALID_VALUE && fabs(darkTemperature - temperature) > maxDarkTemperatureDiff->value())
0235                     continue;
0236             }
0237 
0238             if (bestCandidate.isEmpty())
0239             {
0240                 bestCandidate = map;
0241                 continue;
0242             }
0243 
0244             // We try to find the best frame
0245             // Frame closest in exposure duration wins
0246             // Frame with temperature closest to stored temperature wins (if temperature is reported)
0247             uint32_t thisMapScore = 0;
0248             uint32_t bestCandidateScore = 0;
0249 
0250             // Else we check for the closest passive temperature
0251             if (m_TargetChip->getCCD()->hasCooler())
0252             {
0253                 double temperature = 0;
0254                 m_TargetChip->getCCD()->getTemperature(&temperature);
0255                 double diffMap = std::fabs(temperature - map["temperature"].toDouble());
0256                 double diffBest = std::fabs(temperature - bestCandidate["temperature"].toDouble());
0257                 // Prefer temperatures closest to target
0258                 if (diffMap < diffBest)
0259                     thisMapScore++;
0260                 else if (diffBest < diffMap)
0261                     bestCandidateScore++;
0262             }
0263 
0264             // Duration has a higher score priority over temperature
0265             {
0266                 double diffMap = std::fabs(map["duration"].toDouble() - duration);
0267                 double diffBest = std::fabs(bestCandidate["duration"].toDouble() - duration);
0268                 if (diffMap < diffBest)
0269                     thisMapScore += 5;
0270                 else if (diffBest < diffMap)
0271                     bestCandidateScore += 5;
0272             }
0273 
0274             // More recent has a higher score than older.
0275             {
0276                 const QDateTime now = QDateTime::currentDateTime();
0277                 int64_t diffMap  = map["timestamp"].toDateTime().secsTo(now);
0278                 int64_t diffBest = bestCandidate["timestamp"].toDateTime().secsTo(now);
0279                 if (diffMap < diffBest)
0280                     thisMapScore++;
0281                 else if (diffBest < diffMap)
0282                     bestCandidateScore++;
0283             }
0284 
0285             // Find candidate with closest time in case we have multiple defect maps
0286             if (thisMapScore > bestCandidateScore)
0287                 bestCandidate = map;
0288         }
0289     }
0290 
0291     if (bestCandidate.isEmpty())
0292         return false;
0293 
0294     if (fabs(bestCandidate["duration"].toDouble() - duration) > 3)
0295         emit i18n("Using available dark frame with %1 seconds exposure. Please take a dark frame with %1 seconds exposure for more accurate results.",
0296                   QString::number(bestCandidate["duration"].toDouble(), 'f', 1),
0297                   QString::number(duration, 'f', 1));
0298 
0299     QString filename = bestCandidate["filename"].toString();
0300 
0301     // Finally check if the duration is acceptable
0302     QDateTime frameTime = bestCandidate["timestamp"].toDateTime();
0303     if (frameTime.daysTo(QDateTime::currentDateTime()) > Options::darkLibraryDuration())
0304     {
0305         emit i18n("Dark frame %s is expired. Please create new master dark.", filename);
0306         return false;
0307     }
0308 
0309     if (m_CachedDarkFrames.contains(filename))
0310     {
0311         darkData = m_CachedDarkFrames[filename];
0312         return true;
0313     }
0314 
0315     // Before adding to cache, clear the cache if memory drops too low.
0316     auto memoryMB = KSUtils::getAvailableRAM() / 1e6;
0317     if (memoryMB < CACHE_MEMORY_LIMIT)
0318         m_CachedDarkFrames.clear();
0319 
0320     // Finally we made it, let's put it in the hash
0321     if (cacheDarkFrameFromFile(filename))
0322     {
0323         darkData = m_CachedDarkFrames[filename];
0324         return true;
0325     }
0326 
0327     // Remove bad dark frame
0328     emit newLog(i18n("Removing bad dark frame file %1", filename));
0329     m_CachedDarkFrames.remove(filename);
0330     QFile::remove(filename);
0331     KStarsData::Instance()->userdb()->DeleteDarkFrame(filename);
0332     return false;
0333 
0334 }
0335 
0336 ///////////////////////////////////////////////////////////////////////////////////////
0337 ///
0338 ///////////////////////////////////////////////////////////////////////////////////////
0339 bool DarkLibrary::findDefectMap(ISD::CameraChip *m_TargetChip, double duration, QSharedPointer<DefectMap> &defectMap)
0340 {
0341     QVariantMap bestCandidate;
0342     for (auto &map : m_DarkFramesDatabaseList)
0343     {
0344         if (map["defectmap"].toString().isEmpty())
0345             continue;
0346 
0347         // First check CCD name matches and check if we are on the correct chip
0348         if (map["ccd"].toString() == m_TargetChip->getCCD()->getDeviceName() &&
0349                 map["chip"].toInt() == static_cast<int>(m_TargetChip->getType()))
0350         {
0351             int binX, binY;
0352             m_TargetChip->getBinning(&binX, &binY);
0353 
0354             // Then check if binning is the same
0355             if (map["binX"].toInt() == binX && map["binY"].toInt() == binY)
0356             {
0357                 if (bestCandidate.isEmpty())
0358                 {
0359                     bestCandidate = map;
0360                     continue;
0361                 }
0362 
0363                 // We try to find the best frame
0364                 // Frame closest in exposure duration wins
0365                 // Frame with temperature closest to stored temperature wins (if temperature is reported)
0366                 uint32_t thisMapScore = 0;
0367                 uint32_t bestCandidateScore = 0;
0368 
0369                 // Else we check for the closest passive temperature
0370                 if (m_TargetChip->getCCD()->hasCooler())
0371                 {
0372                     double temperature = 0;
0373                     m_TargetChip->getCCD()->getTemperature(&temperature);
0374                     double diffMap = std::fabs(temperature - map["temperature"].toDouble());
0375                     double diffBest = std::fabs(temperature - bestCandidate["temperature"].toDouble());
0376                     // Prefer temperatures closest to target
0377                     if (diffMap < diffBest)
0378                         thisMapScore++;
0379                     else if (diffBest < diffMap)
0380                         bestCandidateScore++;
0381                 }
0382 
0383                 // Duration has a higher score priority over temperature
0384                 double diffMap = std::fabs(map["duration"].toDouble() - duration);
0385                 double diffBest = std::fabs(bestCandidate["duration"].toDouble() - duration);
0386                 if (diffMap < diffBest)
0387                     thisMapScore += 2;
0388                 else if (diffBest < diffMap)
0389                     bestCandidateScore += 2;
0390 
0391                 // Find candidate with closest time in case we have multiple defect maps
0392                 if (thisMapScore > bestCandidateScore)
0393                     bestCandidate = map;
0394             }
0395         }
0396     }
0397 
0398 
0399     if (bestCandidate.isEmpty())
0400         return false;
0401 
0402 
0403     QString darkFilename = bestCandidate["filename"].toString();
0404     QString defectFilename = bestCandidate["defectmap"].toString();
0405 
0406     if (darkFilename.isEmpty() || defectFilename.isEmpty())
0407         return false;
0408 
0409     if (m_CachedDefectMaps.contains(darkFilename))
0410     {
0411         defectMap = m_CachedDefectMaps[darkFilename];
0412         return true;
0413     }
0414 
0415     // Finally we made it, let's put it in the hash
0416     if (cacheDefectMapFromFile(darkFilename, defectFilename))
0417     {
0418         defectMap = m_CachedDefectMaps[darkFilename];
0419         return true;
0420     }
0421     else
0422     {
0423         // Remove bad dark frame
0424         emit newLog(i18n("Failed to load defect map %1", defectFilename));
0425         return false;
0426     }
0427 }
0428 
0429 ///////////////////////////////////////////////////////////////////////////////////////
0430 ///
0431 ///////////////////////////////////////////////////////////////////////////////////////
0432 bool DarkLibrary::cacheDefectMapFromFile(const QString &key, const QString &filename)
0433 {
0434     QSharedPointer<DefectMap> oneMap;
0435     oneMap.reset(new DefectMap());
0436 
0437     if (oneMap->load(filename))
0438     {
0439         oneMap->filterPixels();
0440         m_CachedDefectMaps[key] = oneMap;
0441         return true;
0442     }
0443 
0444     emit newLog(i18n("Failed to load defect map file %1", filename));
0445     return false;
0446 }
0447 
0448 ///////////////////////////////////////////////////////////////////////////////////////
0449 ///
0450 ///////////////////////////////////////////////////////////////////////////////////////
0451 bool DarkLibrary::cacheDarkFrameFromFile(const QString &filename)
0452 {
0453     QSharedPointer<FITSData> data;
0454     data.reset(new FITSData(FITS_CALIBRATE), &QObject::deleteLater);
0455     QFuture<bool> rc = data->loadFromFile(filename);
0456 
0457     rc.waitForFinished();
0458     if (rc.result())
0459     {
0460         m_CachedDarkFrames[filename] = data;
0461     }
0462     else
0463     {
0464         emit newLog(i18n("Failed to load dark frame file %1", filename));
0465     }
0466 
0467     return rc;
0468 }
0469 
0470 ///////////////////////////////////////////////////////////////////////////////////////
0471 ///
0472 ///////////////////////////////////////////////////////////////////////////////////////
0473 void DarkLibrary::processNewImage(SequenceJob *job, const QSharedPointer<FITSData> &data)
0474 {
0475     Q_UNUSED(data)
0476     if (job->getStatus() == JOB_IDLE)
0477         return;
0478 
0479     if (job->getCompleted() == job->getCoreProperty(SequenceJob::SJ_Count).toInt())
0480     {
0481         QJsonObject metadata
0482         {
0483             {"camera", m_Camera->getDeviceName()},
0484             {"chip", m_TargetChip->getType()},
0485             {"binx", job->getCoreProperty(SequenceJob::SJ_Binning).toPoint().x()},
0486             {"biny", job->getCoreProperty(SequenceJob::SJ_Binning).toPoint().y()},
0487             {"duration", job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble()}
0488         };
0489 
0490         // Record temperature
0491         double value = 0;
0492         bool success = m_Camera->getTemperature(&value);
0493         if (success)
0494             metadata["temperature"] = value;
0495 
0496         success = m_Camera->hasGain() && m_Camera->getGain(&value);
0497         if (success)
0498             metadata["gain"] = value;
0499 
0500         QString isoValue;
0501         success = m_TargetChip->getISOValue(isoValue);
0502         if (success)
0503             metadata["iso"] = isoValue;
0504 
0505         metadata["count"] = job->getCoreProperty(SequenceJob::SJ_Count).toInt();
0506         generateMasterFrame(m_CurrentDarkFrame, metadata);
0507         reloadDarksFromDatabase();
0508         populateMasterMetedata();
0509     }
0510 }
0511 
0512 ///////////////////////////////////////////////////////////////////////////////////////
0513 ///
0514 ///////////////////////////////////////////////////////////////////////////////////////
0515 void DarkLibrary::updateProperty(INDI::Property prop)
0516 {
0517     if (prop.getType() != INDI_BLOB)
0518         return;
0519 
0520     auto bp = prop.getBLOB()->at(0);
0521     QByteArray buffer = QByteArray::fromRawData(reinterpret_cast<char *>(bp->getBlob()), bp->getSize());
0522     if (!m_CurrentDarkFrame->loadFromBuffer(buffer, bp->getFormat()))
0523     {
0524         m_FileLabel->setText(i18n("Failed to process dark data."));
0525         return;
0526     }
0527 
0528     if (!m_DarkView->loadData(m_CurrentDarkFrame))
0529     {
0530         m_FileLabel->setText(i18n("Failed to load dark data."));
0531         return;
0532     }
0533 
0534     uint32_t totalElements = m_CurrentDarkFrame->channels() * m_CurrentDarkFrame->samplesPerChannel();
0535     if (totalElements != m_DarkMasterBuffer.size())
0536         m_DarkMasterBuffer.assign(totalElements, 0);
0537 
0538     aggregate(m_CurrentDarkFrame);
0539     darkProgress->setValue(darkProgress->value() + 1);
0540     m_StatusLabel->setText(i18n("Received %1/%2 images.", darkProgress->value(), darkProgress->maximum()));
0541 }
0542 
0543 ///////////////////////////////////////////////////////////////////////////////////////
0544 ///
0545 ///////////////////////////////////////////////////////////////////////////////////////
0546 void DarkLibrary::Release()
0547 {
0548     delete (_DarkLibrary);
0549     _DarkLibrary = nullptr;
0550 
0551     //    m_Cameras.clear();
0552     //    cameraS->clear();
0553     //    m_CurrentCamera = nullptr;
0554 }
0555 
0556 ///////////////////////////////////////////////////////////////////////////////////////
0557 ///
0558 ///////////////////////////////////////////////////////////////////////////////////////
0559 void DarkLibrary::closeEvent(QCloseEvent *ev)
0560 {
0561     Q_UNUSED(ev)
0562     Options::setUseFITSViewer(m_RememberFITSViewer);
0563     Options::setUseSummaryPreview(m_RememberSummaryView);
0564     if (m_JobsGenerated)
0565     {
0566         m_JobsGenerated = false;
0567         m_CaptureModule->clearSequenceQueue();
0568         m_CaptureModule->setPresetSettings(m_PresetSettings);
0569         m_CaptureModule->setFileSettings(m_FileSettings);
0570     }
0571 }
0572 
0573 ///////////////////////////////////////////////////////////////////////////////////////
0574 ///
0575 ///////////////////////////////////////////////////////////////////////////////////////
0576 void DarkLibrary::setCompleted()
0577 {
0578     startB->setEnabled(true);
0579     stopB->setEnabled(false);
0580 
0581     Options::setUseFITSViewer(m_RememberFITSViewer);
0582     Options::setUseSummaryPreview(m_RememberSummaryView);
0583     if (m_JobsGenerated)
0584     {
0585         m_JobsGenerated = false;
0586         m_CaptureModule->clearSequenceQueue();
0587         m_CaptureModule->setPresetSettings(m_PresetSettings);
0588         m_CaptureModule->setFileSettings(m_FileSettings);
0589     }
0590 
0591     m_Camera->disconnect(this);
0592     m_CaptureModule->disconnect(this);
0593 }
0594 
0595 ///////////////////////////////////////////////////////////////////////////////////////
0596 ///
0597 ///////////////////////////////////////////////////////////////////////////////////////
0598 void DarkLibrary::clearExpired()
0599 {
0600     if (darkFramesModel->rowCount() == 0)
0601         return;
0602 
0603     // Anything before this must go
0604     QDateTime expiredDate = QDateTime::currentDateTime().addDays(darkLibraryDuration->value() * -1);
0605 
0606     auto userdb = QSqlDatabase::database(KStarsData::Instance()->userdb()->connectionName());
0607     QSqlTableModel darkframe(nullptr, userdb);
0608     darkframe.setEditStrategy(QSqlTableModel::OnManualSubmit);
0609     darkframe.setTable("darkframe");
0610     // Select all those that already expired.
0611     darkframe.setFilter("ccd LIKE \'" + m_Camera->getDeviceName() + "\' AND timestamp < \'" + expiredDate.toString(
0612                             Qt::ISODate) + "\'");
0613 
0614     darkframe.select();
0615 
0616     // Now remove all the expired files from disk
0617     for (int i = 0; i < darkframe.rowCount(); ++i)
0618     {
0619         QString oneFile = darkframe.record(i).value("filename").toString();
0620         QFile::remove(oneFile);
0621         QString defectMap = darkframe.record(i).value("defectmap").toString();
0622         if (defectMap.isEmpty() == false)
0623             QFile::remove(defectMap);
0624 
0625     }
0626 
0627     // And remove them from the database
0628     darkframe.removeRows(0, darkframe.rowCount());
0629     darkframe.submitAll();
0630 
0631     Ekos::DarkLibrary::Instance()->refreshFromDB();
0632 
0633     reloadDarksFromDatabase();
0634 }
0635 
0636 ///////////////////////////////////////////////////////////////////////////////////////
0637 ///
0638 ///////////////////////////////////////////////////////////////////////////////////////
0639 void DarkLibrary::clearBuffers()
0640 {
0641     m_CurrentDarkFrame.clear();
0642     // Should clear existing view
0643     m_CurrentDarkFrame.reset(new FITSData(), &QObject::deleteLater);
0644     m_DarkView->clearData();
0645     m_CurrentDefectMap.clear();
0646 
0647 }
0648 ///////////////////////////////////////////////////////////////////////////////////////
0649 ///
0650 ///////////////////////////////////////////////////////////////////////////////////////
0651 void DarkLibrary::clearAll()
0652 {
0653     if (darkFramesModel->rowCount() == 0)
0654         return;
0655 
0656     if (KMessageBox::questionYesNo(KStars::Instance(),
0657                                    i18n("Are you sure you want to delete all dark frames images and data?")) ==
0658             KMessageBox::No)
0659         return;
0660 
0661     auto userdb = QSqlDatabase::database(KStarsData::Instance()->userdb()->connectionName());
0662     QSqlTableModel darkframe(nullptr, userdb);
0663     darkFramesModel->setEditStrategy(QSqlTableModel::OnManualSubmit);
0664     darkframe.setTable("darkframe");
0665     darkframe.setFilter("ccd LIKE \'" + m_Camera->getDeviceName() + "\'");
0666     darkFramesModel->select();
0667 
0668     // Now remove all the expired files from disk
0669     for (int i = 0; i < darkframe.rowCount(); ++i)
0670     {
0671         QString oneFile = darkframe.record(i).value("filename").toString();
0672         QFile::remove(oneFile);
0673         QString defectMap = darkframe.record(i).value("defectmap").toString();
0674         if (defectMap.isEmpty() == false)
0675             QFile::remove(defectMap);
0676 
0677     }
0678 
0679     darkFramesModel->removeRows(0, darkFramesModel->rowCount());
0680     darkFramesModel->submitAll();
0681 
0682     Ekos::DarkLibrary::Instance()->refreshFromDB();
0683 
0684     // Refesh db entries for other cameras
0685     reloadDarksFromDatabase();
0686 }
0687 
0688 ///////////////////////////////////////////////////////////////////////////////////////
0689 ///
0690 ///////////////////////////////////////////////////////////////////////////////////////
0691 void DarkLibrary::clearRow(int index)
0692 {
0693     auto userdb = QSqlDatabase::database(KStarsData::Instance()->userdb()->connectionName());
0694     if (index < 0)
0695         index = darkTableView->currentIndex().row();
0696 
0697     QSqlRecord record = darkFramesModel->record(index);
0698     QString filename = record.value("filename").toString();
0699     QString defectMap = record.value("defectmap").toString();
0700     QFile::remove(filename);
0701     if (!defectMap.isEmpty())
0702         QFile::remove(defectMap);
0703 
0704     darkFramesModel->removeRow(index);
0705     darkFramesModel->submitAll();
0706     userdb.close();
0707 
0708     darkTableView->selectionModel()->select(darkFramesModel->index(index - 1, 0), QItemSelectionModel::ClearAndSelect);
0709 
0710     refreshFromDB();
0711     reloadDarksFromDatabase();
0712 }
0713 
0714 ///////////////////////////////////////////////////////////////////////////////////////
0715 ///
0716 ///////////////////////////////////////////////////////////////////////////////////////
0717 void DarkLibrary::openDarksFolder()
0718 {
0719     QString darkFilesPath = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("darks");
0720 
0721     QDesktopServices::openUrl(QUrl::fromLocalFile(darkFilesPath));
0722 }
0723 
0724 ///////////////////////////////////////////////////////////////////////////////////////
0725 ///
0726 ///////////////////////////////////////////////////////////////////////////////////////
0727 void DarkLibrary::refreshDefectMastersList(const QString &camera)
0728 {
0729     if (darkFramesModel->rowCount() == 0)
0730         return;
0731 
0732     masterDarksCombo->blockSignals(true);
0733     masterDarksCombo->clear();
0734 
0735     for (int i = 0; i < darkFramesModel->rowCount(); ++i)
0736     {
0737         QSqlRecord record = darkFramesModel->record(i);
0738 
0739         if (record.value("ccd") != camera)
0740             continue;
0741 
0742         auto binX = record.value("binX").toInt();
0743         auto binY = record.value("binY").toInt();
0744         auto temperature = record.value("temperature").toDouble();
0745         auto duration = record.value("duration").toDouble();
0746         auto gain = record.value("gain").toInt();
0747         auto iso = record.value("iso").toString();
0748         QString ts = record.value("timestamp").toString();
0749 
0750         QString entry = QString("%1 secs %2x%3")
0751                         .arg(QString::number(duration, 'f', 1))
0752                         .arg(QString::number(binX))
0753                         .arg(QString::number(binY));
0754 
0755         if (temperature > INVALID_VALUE)
0756             entry.append(QString(" @ %1°").arg(QString::number(temperature, 'f', 1)));
0757 
0758         if (gain >= 0)
0759             entry.append(QString(" G %1").arg(gain));
0760         if (!iso.isEmpty())
0761             entry.append(QString(" ISO %1").arg(iso));
0762 
0763         masterDarksCombo->addItem(entry);
0764     }
0765 
0766     masterDarksCombo->blockSignals(false);
0767 
0768     //loadDefectMap();
0769 
0770 }
0771 ///////////////////////////////////////////////////////////////////////////////////////
0772 ///
0773 ///////////////////////////////////////////////////////////////////////////////////////
0774 void DarkLibrary::reloadDarksFromDatabase()
0775 {
0776     auto userdb = QSqlDatabase::database(KStarsData::Instance()->userdb()->connectionName());
0777 
0778     const QString camera = m_Camera->getDeviceName();
0779 
0780     delete (darkFramesModel);
0781     delete (sortFilter);
0782 
0783     darkFramesModel = new QSqlTableModel(this, userdb);
0784     darkFramesModel->setTable("darkframe");
0785     darkFramesModel->setFilter(QString("ccd='%1'").arg(camera));
0786     darkFramesModel->select();
0787 
0788     sortFilter = new QSortFilterProxyModel(this);
0789     sortFilter->setSourceModel(darkFramesModel);
0790     sortFilter->sort (0);
0791     darkTableView->setModel (sortFilter);
0792 
0793     //darkTableView->setModel(darkFramesModel);
0794     // Hide ID
0795     darkTableView->hideColumn(0);
0796     // Hide Chip
0797     darkTableView->hideColumn(2);
0798 
0799     if (darkFramesModel->rowCount() == 0 && m_CurrentDarkFrame)
0800     {
0801         clearBuffers();
0802         return;
0803     }
0804 
0805     refreshDefectMastersList(camera);
0806     loadCurrentMasterDark(camera);
0807 }
0808 
0809 ///////////////////////////////////////////////////////////////////////////////////////
0810 ///
0811 ///////////////////////////////////////////////////////////////////////////////////////
0812 void DarkLibrary::loadCurrentMasterDark(const QString &camera, int masterIndex)
0813 {
0814     // Do not process empty models
0815     if (darkFramesModel->rowCount() == 0)
0816         return;
0817 
0818     if (masterIndex == -1)
0819         masterIndex = masterDarksCombo->currentIndex();
0820 
0821     if (masterIndex < 0 || masterIndex >= darkFramesModel->rowCount())
0822         return;
0823 
0824     QSqlRecord record = darkFramesModel->record(masterIndex);
0825     if (record.value("ccd") != camera)
0826         return;
0827     // Get the master dark frame file name
0828     m_MasterDarkFrameFilename = record.value("filename").toString();
0829 
0830     if (m_MasterDarkFrameFilename.isEmpty() || !QFileInfo::exists(m_MasterDarkFrameFilename))
0831         return;
0832 
0833     // Get defect file name as well if available.
0834     m_DefectMapFilename = record.value("defectmap").toString();
0835 
0836     // If current dark frame is different from target filename, then load from file
0837     if (m_CurrentDarkFrame->filename() != m_MasterDarkFrameFilename)
0838         m_DarkFrameFutureWatcher.setFuture(m_CurrentDarkFrame->loadFromFile(m_MasterDarkFrameFilename));
0839     // If current dark frame is the same one loaded, then check if we need to reload defect map
0840     else
0841         loadCurrentMasterDefectMap();
0842 }
0843 
0844 ///////////////////////////////////////////////////////////////////////////////////////
0845 ///
0846 ///////////////////////////////////////////////////////////////////////////////////////
0847 void DarkLibrary::loadCurrentMasterDefectMap()
0848 {
0849     // Find if we have an existing map
0850     if (m_CachedDefectMaps.contains(m_MasterDarkFrameFilename))
0851     {
0852         if (m_CurrentDefectMap != m_CachedDefectMaps.value(m_MasterDarkFrameFilename))
0853         {
0854             m_CurrentDefectMap = m_CachedDefectMaps.value(m_MasterDarkFrameFilename);
0855             m_DarkView->setDefectMap(m_CurrentDefectMap);
0856             m_CurrentDefectMap->setDarkData(m_CurrentDarkFrame);
0857         }
0858     }
0859     // Create new defect map
0860     else
0861     {
0862         m_CurrentDefectMap.reset(new DefectMap());
0863         connect(m_CurrentDefectMap.data(), &DefectMap::pixelsUpdated, this, [this](uint32_t hot, uint32_t cold)
0864         {
0865             hotPixelsCount->setValue(hot);
0866             coldPixelsCount->setValue(cold);
0867             aggresivenessHotSlider->setValue(m_CurrentDefectMap->property("HotPixelAggressiveness").toInt());
0868             aggresivenessColdSlider->setValue(m_CurrentDefectMap->property("ColdPixelAggressiveness").toInt());
0869         });
0870 
0871         if (!m_DefectMapFilename.isEmpty())
0872             cacheDefectMapFromFile(m_MasterDarkFrameFilename, m_DefectMapFilename);
0873 
0874         m_DarkView->setDefectMap(m_CurrentDefectMap);
0875         m_CurrentDefectMap->setDarkData(m_CurrentDarkFrame);
0876     }
0877 }
0878 
0879 ///////////////////////////////////////////////////////////////////////////////////////
0880 ///
0881 ///////////////////////////////////////////////////////////////////////////////////////
0882 void DarkLibrary::populateMasterMetedata()
0883 {
0884     if (m_CurrentDarkFrame.isNull())
0885         return;
0886 
0887     QVariant value;
0888     // TS
0889     if (m_CurrentDarkFrame->getRecordValue("DATE-OBS", value))
0890         masterTime->setText(value.toString());
0891     // Temperature
0892     if (m_CurrentDarkFrame->getRecordValue("CCD-TEMP", value) && value.toDouble() < 100)
0893         masterTemperature->setText(QString::number(value.toDouble(), 'f', 1));
0894     // Exposure
0895     if (m_CurrentDarkFrame->getRecordValue("EXPTIME", value))
0896         masterExposure->setText(value.toString());
0897     // Median
0898     {
0899         double median = m_CurrentDarkFrame->getAverageMedian();
0900         if (median > 0)
0901             masterMedian->setText(QString::number(median, 'f', 1));
0902     }
0903     // Mean
0904     {
0905         double mean = m_CurrentDarkFrame->getAverageMean();
0906         masterMean->setText(QString::number(mean, 'f', 1));
0907     }
0908     // Standard Deviation
0909     {
0910         double stddev = m_CurrentDarkFrame->getAverageStdDev();
0911         masterDeviation->setText(QString::number(stddev, 'f', 1));
0912     }
0913 }
0914 
0915 ///////////////////////////////////////////////////////////////////////////////////////
0916 ///
0917 ///////////////////////////////////////////////////////////////////////////////////////
0918 ///////////////////////////////////////////////////////////////////////////////////////
0919 ///
0920 ///////////////////////////////////////////////////////////////////////////////////////
0921 void DarkLibrary::loadIndexInView(int row)
0922 {
0923     QSqlRecord record = darkFramesModel->record(row);
0924     QString filename = record.value("filename").toString();
0925     // Avoid duplicate loads
0926     if (m_DarkView->imageData().isNull() || m_DarkView->imageData()->filename() != filename)
0927         m_DarkView->loadFile(filename);
0928 }
0929 
0930 ///////////////////////////////////////////////////////////////////////////////////////
0931 ///
0932 ///////////////////////////////////////////////////////////////////////////////////////
0933 bool DarkLibrary::setCamera(ISD::Camera * device)
0934 {
0935     if (m_Camera == device)
0936         return false;
0937 
0938     if (m_Camera)
0939         m_Camera->disconnect(this);
0940 
0941     m_Camera = device;
0942 
0943     if (m_Camera)
0944     {
0945         darkTabsWidget->setEnabled(true);
0946         checkCamera();
0947         reloadDarksFromDatabase();
0948         return true;
0949     }
0950     else
0951     {
0952         darkTabsWidget->setEnabled(false);
0953         return false;
0954     }
0955 }
0956 
0957 ///////////////////////////////////////////////////////////////////////////////////////
0958 ///
0959 ///////////////////////////////////////////////////////////////////////////////////////
0960 void DarkLibrary::removeDevice(const QSharedPointer<ISD::GenericDevice> &device)
0961 {
0962     if (m_Camera && m_Camera->getDeviceName() == device->getDeviceName())
0963     {
0964         m_Camera->disconnect(this);
0965         m_Camera = nullptr;
0966     }
0967 }
0968 
0969 ///////////////////////////////////////////////////////////////////////////////////////
0970 ///
0971 ///////////////////////////////////////////////////////////////////////////////////////
0972 void DarkLibrary::checkCamera()
0973 {
0974     if (!m_Camera)
0975         return;
0976 
0977     auto device = m_Camera->getDeviceName();
0978 
0979     m_TargetChip = nullptr;
0980     // FIXME TODO
0981     // Need to figure guide head
0982     if (device.contains("Guider"))
0983     {
0984         m_UseGuideHead = true;
0985         m_TargetChip   = m_Camera->getChip(ISD::CameraChip::GUIDE_CCD);
0986     }
0987 
0988     if (m_TargetChip == nullptr)
0989     {
0990         m_UseGuideHead = false;
0991         m_TargetChip   = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
0992     }
0993 
0994     // Make sure we have a valid chip and valid base device.
0995     // Make sure we are not in capture process.
0996     if (!m_TargetChip || !m_TargetChip->getCCD() || m_TargetChip->isCapturing())
0997         return;
0998 
0999     if (m_Camera->hasCoolerControl())
1000     {
1001         temperatureLabel->setEnabled(true);
1002         temperatureStepLabel->setEnabled(true);
1003         temperatureToLabel->setEnabled(true);
1004         temperatureStepSpin->setEnabled(true);
1005         minTemperatureSpin->setEnabled(true);
1006         maxTemperatureSpin->setEnabled(true);
1007 
1008         // Get default temperature
1009         double temperature = 0;
1010         // Update if no setting was previously set
1011         if (m_Camera->getTemperature(&temperature))
1012         {
1013             minTemperatureSpin->setValue(temperature);
1014             maxTemperatureSpin->setValue(temperature);
1015         }
1016 
1017     }
1018     else
1019     {
1020         temperatureLabel->setEnabled(false);
1021         temperatureStepLabel->setEnabled(false);
1022         temperatureToLabel->setEnabled(false);
1023         temperatureStepSpin->setEnabled(false);
1024         minTemperatureSpin->setEnabled(false);
1025         maxTemperatureSpin->setEnabled(false);
1026     }
1027 
1028     QStringList isoList = m_TargetChip->getISOList();
1029     captureISOS->blockSignals(true);
1030     captureISOS->clear();
1031 
1032     // No ISO range available
1033     if (isoList.isEmpty())
1034     {
1035         captureISOS->setEnabled(false);
1036     }
1037     else
1038     {
1039         captureISOS->setEnabled(true);
1040         captureISOS->addItems(isoList);
1041         captureISOS->setCurrentIndex(m_TargetChip->getISOIndex());
1042     }
1043     captureISOS->blockSignals(false);
1044 
1045     // Gain Check
1046     if (m_Camera->hasGain())
1047     {
1048         double min, max, step, value, targetCustomGain;
1049         m_Camera->getGainMinMaxStep(&min, &max, &step);
1050 
1051         // Allow the possibility of no gain value at all.
1052         GainSpinSpecialValue = min - step;
1053         captureGainN->setRange(GainSpinSpecialValue, max);
1054         captureGainN->setSpecialValueText(i18n("--"));
1055         captureGainN->setEnabled(true);
1056         captureGainN->setSingleStep(step);
1057         m_Camera->getGain(&value);
1058 
1059         targetCustomGain = getGain();
1060 
1061         // Set the custom gain if we have one
1062         // otherwise it will not have an effect.
1063         if (targetCustomGain > 0)
1064             captureGainN->setValue(targetCustomGain);
1065         else
1066             captureGainN->setValue(GainSpinSpecialValue);
1067 
1068         captureGainN->setReadOnly(m_Camera->getGainPermission() == IP_RO);
1069     }
1070     else
1071         captureGainN->setEnabled(false);
1072 
1073     countDarkTotalTime();
1074 
1075 }
1076 
1077 ///////////////////////////////////////////////////////////////////////////////////////
1078 ///
1079 ///////////////////////////////////////////////////////////////////////////////////////
1080 void DarkLibrary::countDarkTotalTime()
1081 {
1082     double temperatureCount = 1;
1083     if (m_Camera && m_Camera->hasCoolerControl() && std::abs(maxTemperatureSpin->value() - minTemperatureSpin->value()) > 0)
1084         temperatureCount = (std::abs((maxTemperatureSpin->value() - minTemperatureSpin->value())) / temperatureStepSpin->value()) +
1085                            1;
1086     int binnings = 0;
1087     if (bin1Check->isChecked())
1088         binnings++;
1089     if (bin2Check->isChecked())
1090         binnings++;
1091     if (bin4Check->isChecked())
1092         binnings++;
1093 
1094     double darkTime = 0;
1095     int imagesCount = 0;
1096     for (double startExposure = minExposureSpin->value(); startExposure <= maxExposureSpin->value();
1097             startExposure += exposureStepSin->value())
1098     {
1099         darkTime += startExposure * temperatureCount * binnings * countSpin->value();
1100         imagesCount += temperatureCount * binnings * countSpin->value();
1101     }
1102 
1103     totalTime->setText(QString::number(darkTime / 60.0, 'f', 1));
1104     totalImages->setText(QString::number(imagesCount));
1105     darkProgress->setMaximum(imagesCount);
1106 
1107 }
1108 
1109 ///////////////////////////////////////////////////////////////////////////////////////
1110 ///
1111 ///////////////////////////////////////////////////////////////////////////////////////
1112 void DarkLibrary::generateDarkJobs()
1113 {
1114     // Always clear sequence queue before starting
1115     m_CaptureModule->clearSequenceQueue();
1116 
1117     if (m_JobsGenerated == false)
1118     {
1119         m_JobsGenerated = true;
1120         m_PresetSettings = m_CaptureModule->getPresetSettings();
1121         m_FileSettings = m_CaptureModule->getFileSettings();
1122     }
1123 
1124     QList<double> temperatures;
1125     if (m_Camera->hasCoolerControl() && std::fabs(maxTemperatureSpin->value() - minTemperatureSpin->value()) >= 0)
1126     {
1127         for (double oneTemperature = minTemperatureSpin->value(); oneTemperature <= maxTemperatureSpin->value();
1128                 oneTemperature += temperatureStepSpin->value())
1129         {
1130             temperatures << oneTemperature;
1131         }
1132 
1133         // Enforce temperature set
1134         m_CaptureModule->setForceTemperature(true);
1135     }
1136     else
1137     {
1138         // Disable temperature set
1139         m_CaptureModule->setForceTemperature(false);
1140         temperatures << INVALID_VALUE;
1141     }
1142 
1143     QList<uint8_t> bins;
1144     if (bin1Check->isChecked())
1145         bins << 1;
1146     if (bin2Check->isChecked())
1147         bins << 2;
1148     if (bin4Check->isChecked())
1149         bins << 4;
1150 
1151     QList<double> exposures;
1152     for (double oneExposure = minExposureSpin->value(); oneExposure <= maxExposureSpin->value();
1153             oneExposure += exposureStepSin->value())
1154     {
1155         exposures << oneExposure;
1156     }
1157 
1158     QString prefix = QDir::tempPath() + QDir::separator() + QString::number(QDateTime::currentSecsSinceEpoch()) +
1159                      QDir::separator();
1160 
1161 
1162     int sequence = 0;
1163     for (auto &oneTemperature : temperatures)
1164     {
1165         for (auto &oneExposure : exposures)
1166         {
1167             for (auto &oneBin : bins)
1168             {
1169                 sequence++;
1170 
1171                 QJsonObject settings;
1172 
1173                 settings["optical_train"] = opticalTrainCombo->currentText();
1174                 settings["exp"] = oneExposure;
1175                 settings["bin"] = oneBin;
1176                 settings["frameType"] = FRAME_DARK;
1177                 settings["temperature"] = oneTemperature;
1178                 if (captureGainN->isEnabled())
1179                     settings["gain"] = captureGainN->value();
1180                 if (captureISOS->isEnabled())
1181                     settings["iso"] = captureISOS->currentIndex();
1182 
1183                 QString directory = prefix + QString("sequence_%1").arg(sequence);
1184                 QJsonObject fileSettings;
1185 
1186                 fileSettings["directory"] = directory;
1187                 m_CaptureModule->setPresetSettings(settings);
1188                 m_CaptureModule->setFileSettings(fileSettings);
1189                 m_CaptureModule->setCount(countSpin->value());
1190                 m_CaptureModule->createJob();
1191             }
1192         }
1193     }
1194 }
1195 
1196 ///////////////////////////////////////////////////////////////////////////////////////
1197 ///
1198 ///////////////////////////////////////////////////////////////////////////////////////
1199 void DarkLibrary::execute()
1200 {
1201     m_DarkImagesCounter = 0;
1202     darkProgress->setValue(0);
1203     darkProgress->setTextVisible(true);
1204     connect(m_CaptureModule, &Capture::newImage, this, &DarkLibrary::processNewImage, Qt::UniqueConnection);
1205     connect(m_CaptureModule, &Capture::newStatus, this, &DarkLibrary::setCaptureState, Qt::UniqueConnection);
1206     connect(m_Camera, &ISD::Camera::propertyUpdated, this, &DarkLibrary::updateProperty, Qt::UniqueConnection);
1207 
1208     Options::setUseFITSViewer(false);
1209     Options::setUseSummaryPreview(false);
1210     startB->setEnabled(false);
1211     stopB->setEnabled(true);
1212     m_DarkView->reset();
1213     m_StatusLabel->setText(i18n("In progress..."));
1214     m_CaptureModule->start();
1215 
1216 }
1217 
1218 ///////////////////////////////////////////////////////////////////////////////////////
1219 ///
1220 ///////////////////////////////////////////////////////////////////////////////////////
1221 void DarkLibrary::stop()
1222 {
1223     m_CaptureModule->abort();
1224     darkProgress->setValue(0);
1225     m_DarkView->reset();
1226 }
1227 
1228 ///////////////////////////////////////////////////////////////////////////////////////
1229 ///
1230 ///////////////////////////////////////////////////////////////////////////////////////
1231 void DarkLibrary::initView()
1232 {
1233     m_DarkView.reset(new DarkView(darkWidget));
1234     m_DarkView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1235     m_DarkView->setBaseSize(darkWidget->size());
1236     m_DarkView->createFloatingToolBar();
1237     QVBoxLayout *vlayout = new QVBoxLayout();
1238     vlayout->addWidget(m_DarkView.get());
1239     darkWidget->setLayout(vlayout);
1240     connect(m_DarkView.get(), &DarkView::loaded, this, [this]()
1241     {
1242         emit newImage(m_DarkView->imageData());
1243     });
1244 }
1245 
1246 ///////////////////////////////////////////////////////////////////////////////////////
1247 ///
1248 ///////////////////////////////////////////////////////////////////////////////////////
1249 void DarkLibrary::aggregate(const QSharedPointer<FITSData> &data)
1250 {
1251     switch (data->dataType())
1252     {
1253         case TBYTE:
1254             aggregateInternal<uint8_t>(data);
1255             break;
1256 
1257         case TSHORT:
1258             aggregateInternal<int16_t>(data);
1259             break;
1260 
1261         case TUSHORT:
1262             aggregateInternal<uint16_t>(data);
1263             break;
1264 
1265         case TLONG:
1266             aggregateInternal<int32_t>(data);
1267             break;
1268 
1269         case TULONG:
1270             aggregateInternal<uint32_t>(data);
1271             break;
1272 
1273         case TFLOAT:
1274             aggregateInternal<float>(data);
1275             break;
1276 
1277         case TLONGLONG:
1278             aggregateInternal<int64_t>(data);
1279             break;
1280 
1281         case TDOUBLE:
1282             aggregateInternal<double>(data);
1283             break;
1284 
1285         default:
1286             break;
1287     }
1288 }
1289 
1290 ///////////////////////////////////////////////////////////////////////////////////////
1291 ///
1292 ///////////////////////////////////////////////////////////////////////////////////////
1293 template <typename T>
1294 void DarkLibrary::aggregateInternal(const QSharedPointer<FITSData> &data)
1295 {
1296     T const *darkBuffer  = reinterpret_cast<T const*>(data->getImageBuffer());
1297     for (uint32_t i = 0; i < m_DarkMasterBuffer.size(); i++)
1298         m_DarkMasterBuffer[i] += darkBuffer[i];
1299 }
1300 
1301 ///////////////////////////////////////////////////////////////////////////////////////
1302 ///
1303 ///////////////////////////////////////////////////////////////////////////////////////
1304 void DarkLibrary::generateMasterFrame(const QSharedPointer<FITSData> &data, const QJsonObject &metadata)
1305 {
1306     switch (data->dataType())
1307     {
1308         case TBYTE:
1309             generateMasterFrameInternal<uint8_t>(data, metadata);
1310             break;
1311 
1312         case TSHORT:
1313             generateMasterFrameInternal<int16_t>(data, metadata);
1314             break;
1315 
1316         case TUSHORT:
1317             generateMasterFrameInternal<uint16_t>(data, metadata);
1318             break;
1319 
1320         case TLONG:
1321             generateMasterFrameInternal<int32_t>(data, metadata);
1322             break;
1323 
1324         case TULONG:
1325             generateMasterFrameInternal<uint32_t>(data, metadata);
1326             break;
1327 
1328         case TFLOAT:
1329             generateMasterFrameInternal<float>(data, metadata);
1330             break;
1331 
1332         case TLONGLONG:
1333             generateMasterFrameInternal<int64_t>(data, metadata);
1334             break;
1335 
1336         case TDOUBLE:
1337             generateMasterFrameInternal<double>(data, metadata);
1338             break;
1339 
1340         default:
1341             break;
1342     }
1343 
1344     emit newImage(data);
1345     // Reset Master Buffer
1346     m_DarkMasterBuffer.assign(m_DarkMasterBuffer.size(), 0);
1347 
1348 }
1349 
1350 ///////////////////////////////////////////////////////////////////////////////////////
1351 ///
1352 ///////////////////////////////////////////////////////////////////////////////////////
1353 template <typename T>  void DarkLibrary::generateMasterFrameInternal(const QSharedPointer<FITSData> &data,
1354         const QJsonObject &metadata)
1355 {
1356     T *writableBuffer = reinterpret_cast<T *>(data->getWritableImageBuffer());
1357     const uint32_t count = metadata["count"].toInt();
1358     // Average the values
1359     for (uint32_t i = 0; i < m_DarkMasterBuffer.size(); i++)
1360         writableBuffer[i] = m_DarkMasterBuffer[i] / count;
1361 
1362     QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss");
1363     QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("darks/darkframe_" + ts +
1364                    data->extension());
1365 
1366     data->calculateStats(true);
1367     if (!data->saveImage(path))
1368     {
1369         m_FileLabel->setText(i18n("Failed to save master frame: %1", data->getLastError()));
1370         return;
1371     }
1372 
1373     auto memoryMB = KSUtils::getAvailableRAM() / 1e6;
1374     if (memoryMB > CACHE_MEMORY_LIMIT)
1375         cacheDarkFrameFromFile(data->filename());
1376 
1377     QVariantMap map;
1378     map["ccd"]         = metadata["camera"].toString();
1379     map["chip"]        = metadata["chip"].toInt();
1380     map["binX"]        = metadata["binx"].toInt();
1381     map["binY"]        = metadata["biny"].toInt();
1382     map["temperature"] = metadata["temperature"].toDouble(INVALID_VALUE);
1383     map["gain"] = metadata["gain"].toInt(-1);
1384     map["iso"] = metadata["iso"].toString();
1385     map["duration"]    = metadata["duration"].toDouble();
1386     map["filename"]    = path;
1387     map["timestamp"]   = QDateTime::currentDateTime().toString(Qt::ISODate);
1388 
1389     m_DarkFramesDatabaseList.append(map);
1390     m_FileLabel->setText(i18n("Master Dark saved to %1", path));
1391     KStarsData::Instance()->userdb()->AddDarkFrame(map);
1392 }
1393 
1394 ///////////////////////////////////////////////////////////////////////////////////////
1395 ///
1396 ///////////////////////////////////////////////////////////////////////////////////////
1397 void DarkLibrary::setCaptureModule(Capture *instance)
1398 {
1399     m_CaptureModule = instance;
1400 }
1401 
1402 ///////////////////////////////////////////////////////////////////////////////////////
1403 ///
1404 ///////////////////////////////////////////////////////////////////////////////////////
1405 void DarkLibrary::setCaptureState(CaptureState state)
1406 {
1407     switch (state)
1408     {
1409         case CAPTURE_ABORTED:
1410             setCompleted();
1411             m_StatusLabel->setText(i18n("Capture aborted."));
1412             break;
1413         case CAPTURE_COMPLETE:
1414             setCompleted();
1415             m_StatusLabel->setText(i18n("Capture completed."));
1416             break;
1417         default:
1418             break;
1419     }
1420 }
1421 
1422 ///////////////////////////////////////////////////////////////////////////////////////
1423 ///
1424 ///////////////////////////////////////////////////////////////////////////////////////
1425 void DarkLibrary::saveDefectMap()
1426 {
1427     if (!m_CurrentDarkFrame)
1428         return;
1429 
1430     QString filename = m_CurrentDefectMap->filename();
1431     bool newFile = false;
1432     if (filename.isEmpty())
1433     {
1434         QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss");
1435         filename = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("defectmaps/defectmap_" + ts +
1436                    ".json");
1437         newFile = true;
1438     }
1439 
1440     if (m_CurrentDefectMap->save(filename, m_Camera->getDeviceName()))
1441     {
1442         m_FileLabel->setText(i18n("Defect map saved to %1", filename));
1443 
1444         if (newFile)
1445         {
1446             auto currentMap = std::find_if(m_DarkFramesDatabaseList.begin(),
1447                                            m_DarkFramesDatabaseList.end(), [&](const QVariantMap & oneMap)
1448             {
1449                 return oneMap["filename"].toString() == m_CurrentDarkFrame->filename();
1450             });
1451 
1452             if (currentMap != m_DarkFramesDatabaseList.end())
1453             {
1454                 (*currentMap)["defectmap"] = filename;
1455                 (*currentMap)["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate);
1456                 KStarsData::Instance()->userdb()->UpdateDarkFrame(*currentMap);
1457             }
1458         }
1459     }
1460     else
1461     {
1462         m_FileLabel->setText(i18n("Failed to save defect map to %1", filename));
1463     }
1464 }
1465 
1466 ///////////////////////////////////////////////////////////////////////////////////////
1467 ///
1468 ///////////////////////////////////////////////////////////////////////////////////////
1469 void DarkLibrary::start()
1470 {
1471     generateDarkJobs();
1472     execute();
1473 }
1474 
1475 ///////////////////////////////////////////////////////////////////////////////////////
1476 ///
1477 ///////////////////////////////////////////////////////////////////////////////////////
1478 void DarkLibrary::setCameraPresets(const QJsonObject &settings)
1479 {
1480     const auto opticalTrain = settings["optical_train"].toString();
1481     const auto isDarkPrefer = settings["isDarkPrefer"].toBool(preferDarksRadio->isChecked());
1482     const auto isDefectPrefer = settings["isDefectPrefer"].toBool(preferDefectsRadio->isChecked());
1483     opticalTrainCombo->setCurrentText(opticalTrain);
1484     preferDarksRadio->setChecked(isDarkPrefer);
1485     preferDefectsRadio->setChecked(isDefectPrefer);
1486     checkCamera();
1487     reloadDarksFromDatabase();
1488 }
1489 
1490 ///////////////////////////////////////////////////////////////////////////////////////
1491 ///
1492 ///////////////////////////////////////////////////////////////////////////////////////
1493 QJsonObject DarkLibrary::getCameraPresets()
1494 {
1495     QJsonObject cameraSettings =
1496     {
1497         {"optical_train", opticalTrainCombo->currentText()},
1498         {"preferDarksRadio", preferDarksRadio->isChecked()},
1499         {"preferDefectsRadio", preferDefectsRadio->isChecked()},
1500         {"fileName", m_FileLabel->text()}
1501     };
1502     return cameraSettings;
1503 }
1504 
1505 ///////////////////////////////////////////////////////////////////////////////////////
1506 ///
1507 ///////////////////////////////////////////////////////////////////////////////////////
1508 QJsonArray DarkLibrary::getViewMasters()
1509 {
1510     QJsonArray array;
1511 
1512     for(int i = 0; i < darkFramesModel->rowCount(); i++)
1513     {
1514         QSqlRecord record = darkFramesModel->record(i);
1515         auto camera = record.value("ccd").toString();
1516         auto binX = record.value("binX").toInt();
1517         auto binY = record.value("binY").toInt();
1518         auto temperature = record.value("temperature").toDouble();
1519         auto duration = record.value("duration").toDouble();
1520         auto ts = record.value("timestamp").toString();
1521         auto gain = record.value("gain").toInt();
1522         auto iso = record.value("iso").toString();
1523 
1524         QJsonObject filterRows =
1525         {
1526             {"camera", camera},
1527             {"binX", binX},
1528             {"binY", binY},
1529             {"temperature", temperature},
1530             {"duaration", duration},
1531             {"ts", ts}
1532         };
1533 
1534         if (gain >= 0)
1535             filterRows["gain"] = gain;
1536         if (!iso.isEmpty())
1537             filterRows["iso"] = iso;
1538 
1539         array.append(filterRows);
1540     }
1541     return array;
1542 }
1543 
1544 ///////////////////////////////////////////////////////////////////////////////////////
1545 ///
1546 ///////////////////////////////////////////////////////////////////////////////////////
1547 void DarkLibrary::setDefectPixels(const QJsonObject &payload)
1548 {
1549     const auto hotSpin = payload["hotSpin"].toInt();
1550     const auto coldSpin = payload["coldSpin"].toInt();
1551     const auto hotEnabled = payload["hotEnabled"].toBool(hotPixelsEnabled->isChecked());
1552     const auto coldEnabled = payload["coldEnabled"].toBool(coldPixelsEnabled->isChecked());
1553 
1554     hotPixelsEnabled->setChecked(hotEnabled);
1555     coldPixelsEnabled->setChecked(coldEnabled);
1556 
1557     aggresivenessHotSpin->setValue(hotSpin);
1558     aggresivenessColdSpin->setValue(coldSpin);
1559 
1560     m_DarkView->ZoomDefault();
1561 
1562     setDefectMapEnabled(true);
1563     generateMapB->click();
1564 }
1565 
1566 ///////////////////////////////////////////////////////////////////////////////////////
1567 ///
1568 ///////////////////////////////////////////////////////////////////////////////////////
1569 void DarkLibrary::setDefectMapEnabled(bool enabled)
1570 {
1571     m_DarkView->setDefectMapEnabled(enabled);
1572 }
1573 
1574 ///////////////////////////////////////////////////////////////////////////////////////
1575 ///
1576 ///////////////////////////////////////////////////////////////////////////////////////
1577 double DarkLibrary::getGain()
1578 {
1579     // Gain is manifested in two forms
1580     // Property CCD_GAIN and
1581     // Part of CCD_CONTROLS properties.
1582     // Therefore, we have to find what the currently camera supports first.
1583     auto gain = m_Camera->getProperty("CCD_GAIN");
1584     if (gain)
1585         return gain.getNumber()->at(0)->value;
1586 
1587 
1588     auto controls = m_Camera->getProperty("CCD_CONTROLS");
1589     if (controls)
1590     {
1591         auto oneGain = controls.getNumber()->findWidgetByName("Gain");
1592         if (oneGain)
1593             return oneGain->value;
1594     }
1595 
1596     return -1;
1597 }
1598 
1599 ///////////////////////////////////////////////////////////////////////////////////////
1600 ///
1601 ///////////////////////////////////////////////////////////////////////////////////////
1602 void DarkLibrary::setupOpticalTrainManager()
1603 {
1604     connect(OpticalTrainManager::Instance(), &OpticalTrainManager::updated, this, &DarkLibrary::refreshOpticalTrain);
1605     connect(trainB, &QPushButton::clicked, this, [this]()
1606     {
1607         OpticalTrainManager::Instance()->openEditor(opticalTrainCombo->currentText());
1608     });
1609     connect(opticalTrainCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index)
1610     {
1611         ProfileSettings::Instance()->setOneSetting(ProfileSettings::DarkLibraryOpticalTrain,
1612                 OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(index)));
1613         refreshOpticalTrain();
1614         emit trainChanged();
1615     });
1616 }
1617 
1618 ///////////////////////////////////////////////////////////////////////////////////////
1619 ///
1620 ///////////////////////////////////////////////////////////////////////////////////////
1621 void DarkLibrary::refreshOpticalTrain()
1622 {
1623     opticalTrainCombo->blockSignals(true);
1624     opticalTrainCombo->clear();
1625     opticalTrainCombo->addItems(OpticalTrainManager::Instance()->getTrainNames());
1626     trainB->setEnabled(true);
1627 
1628     QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::DarkLibraryOpticalTrain);
1629 
1630     if (trainID.isValid())
1631     {
1632         auto id = trainID.toUInt();
1633 
1634         // If train not found, select the first one available.
1635         if (OpticalTrainManager::Instance()->exists(id) == false)
1636         {
1637             emit newLog(i18n("Optical train doesn't exist for id %1", id));
1638             id = OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(0));
1639         }
1640 
1641         auto name = OpticalTrainManager::Instance()->name(id);
1642 
1643         opticalTrainCombo->setCurrentText(name);
1644 
1645         auto camera = OpticalTrainManager::Instance()->getCamera(name);
1646         if (camera)
1647         {
1648             auto scope = OpticalTrainManager::Instance()->getScope(name);
1649             opticalTrainCombo->setToolTip(QString("%1 @ %2").arg(camera->getDeviceName(), scope["name"].toString()));
1650         }
1651         setCamera(camera);
1652 
1653         // Load train settings
1654         OpticalTrainSettings::Instance()->setOpticalTrainID(id);
1655         auto settings = OpticalTrainSettings::Instance()->getOneSetting(OpticalTrainSettings::DarkLibrary);
1656         if (settings.isValid())
1657             setAllSettings(settings.toJsonObject().toVariantMap());
1658         else
1659             m_Settings = m_GlobalSettings;
1660     }
1661 
1662     opticalTrainCombo->blockSignals(false);
1663 }
1664 
1665 ///////////////////////////////////////////////////////////////////////////////////////
1666 ///
1667 ///////////////////////////////////////////////////////////////////////////////////////
1668 void DarkLibrary::loadGlobalSettings()
1669 {
1670     QString key;
1671     QVariant value;
1672 
1673     QVariantMap settings;
1674     // All Combo Boxes
1675     for (auto &oneWidget : findChildren<QComboBox*>())
1676     {
1677         if (oneWidget->objectName() == "opticalTrainCombo")
1678             continue;
1679 
1680         key = oneWidget->objectName();
1681         value = Options::self()->property(key.toLatin1());
1682         if (value.isValid())
1683         {
1684             oneWidget->setCurrentText(value.toString());
1685             settings[key] = value;
1686         }
1687     }
1688 
1689     // All Double Spin Boxes
1690     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
1691     {
1692         key = oneWidget->objectName();
1693         value = Options::self()->property(key.toLatin1());
1694         if (value.isValid())
1695         {
1696             oneWidget->setValue(value.toDouble());
1697             settings[key] = value;
1698         }
1699     }
1700 
1701     // All Spin Boxes
1702     for (auto &oneWidget : findChildren<QSpinBox*>())
1703     {
1704         key = oneWidget->objectName();
1705         value = Options::self()->property(key.toLatin1());
1706         if (value.isValid())
1707         {
1708             oneWidget->setValue(value.toInt());
1709             settings[key] = value;
1710         }
1711     }
1712 
1713     // All Checkboxes
1714     for (auto &oneWidget : findChildren<QCheckBox*>())
1715     {
1716         key = oneWidget->objectName();
1717         value = Options::self()->property(key.toLatin1());
1718         if (value.isValid())
1719         {
1720             oneWidget->setChecked(value.toBool());
1721             settings[key] = value;
1722         }
1723     }
1724 
1725     m_GlobalSettings = m_Settings = settings;
1726 }
1727 
1728 
1729 ///////////////////////////////////////////////////////////////////////////////////////
1730 ///
1731 ///////////////////////////////////////////////////////////////////////////////////////
1732 void DarkLibrary::connectSettings()
1733 {
1734     // All Combo Boxes
1735     for (auto &oneWidget : findChildren<QComboBox*>())
1736         connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::DarkLibrary::syncSettings);
1737 
1738     // All Double Spin Boxes
1739     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
1740         connect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::DarkLibrary::syncSettings);
1741 
1742     // All Spin Boxes
1743     for (auto &oneWidget : findChildren<QSpinBox*>())
1744         connect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::DarkLibrary::syncSettings);
1745 
1746     // All Checkboxes
1747     for (auto &oneWidget : findChildren<QCheckBox*>())
1748         connect(oneWidget, &QCheckBox::toggled, this, &Ekos::DarkLibrary::syncSettings);
1749 
1750     // All Radio buttons
1751     for (auto &oneWidget : findChildren<QRadioButton*>())
1752         connect(oneWidget, &QCheckBox::toggled, this, &Ekos::DarkLibrary::syncSettings);
1753 
1754     // Train combo box should NOT be synced.
1755     disconnect(opticalTrainCombo, QOverload<int>::of(&QComboBox::activated), this, &Ekos::DarkLibrary::syncSettings);
1756 }
1757 
1758 ///////////////////////////////////////////////////////////////////////////////////////
1759 ///
1760 ///////////////////////////////////////////////////////////////////////////////////////
1761 void DarkLibrary::disconnectSettings()
1762 {
1763     // All Combo Boxes
1764     for (auto &oneWidget : findChildren<QComboBox*>())
1765         disconnect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::DarkLibrary::syncSettings);
1766 
1767     // All Double Spin Boxes
1768     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
1769         disconnect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::DarkLibrary::syncSettings);
1770 
1771     // All Spin Boxes
1772     for (auto &oneWidget : findChildren<QSpinBox*>())
1773         disconnect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::DarkLibrary::syncSettings);
1774 
1775     // All Checkboxes
1776     for (auto &oneWidget : findChildren<QCheckBox*>())
1777         disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::DarkLibrary::syncSettings);
1778 
1779     // All Radio buttons
1780     for (auto &oneWidget : findChildren<QRadioButton*>())
1781         disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::DarkLibrary::syncSettings);
1782 
1783 }
1784 
1785 ///////////////////////////////////////////////////////////////////////////////////////////
1786 ///
1787 ///////////////////////////////////////////////////////////////////////////////////////////
1788 QVariantMap DarkLibrary::getAllSettings() const
1789 {
1790     QVariantMap settings;
1791 
1792     // All Combo Boxes
1793     for (auto &oneWidget : findChildren<QComboBox*>())
1794         settings.insert(oneWidget->objectName(), oneWidget->currentText());
1795 
1796     // All Double Spin Boxes
1797     for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
1798         settings.insert(oneWidget->objectName(), oneWidget->value());
1799 
1800     // All Spin Boxes
1801     for (auto &oneWidget : findChildren<QSpinBox*>())
1802         settings.insert(oneWidget->objectName(), oneWidget->value());
1803 
1804     // All Checkboxes
1805     for (auto &oneWidget : findChildren<QCheckBox*>())
1806         settings.insert(oneWidget->objectName(), oneWidget->isChecked());
1807 
1808     return settings;
1809 }
1810 
1811 ///////////////////////////////////////////////////////////////////////////////////////////
1812 ///
1813 ///////////////////////////////////////////////////////////////////////////////////////////
1814 void DarkLibrary::setAllSettings(const QVariantMap &settings)
1815 {
1816     // Disconnect settings that we don't end up calling syncSettings while
1817     // performing the changes.
1818     disconnectSettings();
1819 
1820     for (auto &name : settings.keys())
1821     {
1822         // Combo
1823         auto comboBox = findChild<QComboBox*>(name);
1824         if (comboBox)
1825         {
1826             syncControl(settings, name, comboBox);
1827             continue;
1828         }
1829 
1830         // Double spinbox
1831         auto doubleSpinBox = findChild<QDoubleSpinBox*>(name);
1832         if (doubleSpinBox)
1833         {
1834             syncControl(settings, name, doubleSpinBox);
1835             continue;
1836         }
1837 
1838         // spinbox
1839         auto spinBox = findChild<QSpinBox*>(name);
1840         if (spinBox)
1841         {
1842             syncControl(settings, name, spinBox);
1843             continue;
1844         }
1845 
1846         // checkbox
1847         auto checkbox = findChild<QCheckBox*>(name);
1848         if (checkbox)
1849         {
1850             syncControl(settings, name, checkbox);
1851             continue;
1852         }
1853 
1854         // Radio button
1855         auto radioButton = findChild<QRadioButton*>(name);
1856         if (radioButton)
1857         {
1858             syncControl(settings, name, radioButton);
1859             continue;
1860         }
1861     }
1862 
1863     // Sync to options
1864     for (auto &key : settings.keys())
1865     {
1866         auto value = settings[key];
1867         // Save immediately
1868         Options::self()->setProperty(key.toLatin1(), value);
1869 
1870         m_Settings[key] = value;
1871         m_GlobalSettings[key] = value;
1872     }
1873 
1874     emit settingsUpdated(getAllSettings());
1875 
1876     // Save to optical train specific settings as well
1877     OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
1878     OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::DarkLibrary, m_Settings);
1879 
1880     // Restablish connections
1881     connectSettings();
1882 }
1883 
1884 ///////////////////////////////////////////////////////////////////////////////////////////
1885 ///
1886 ///////////////////////////////////////////////////////////////////////////////////////////
1887 bool DarkLibrary::syncControl(const QVariantMap &settings, const QString &key, QWidget * widget)
1888 {
1889     QSpinBox *pSB = nullptr;
1890     QDoubleSpinBox *pDSB = nullptr;
1891     QCheckBox *pCB = nullptr;
1892     QComboBox *pComboBox = nullptr;
1893     QRadioButton *pRadioButton = nullptr;
1894     bool ok = false;
1895 
1896     if ((pSB = qobject_cast<QSpinBox *>(widget)))
1897     {
1898         const int value = settings[key].toInt(&ok);
1899         if (ok)
1900         {
1901             pSB->setValue(value);
1902             return true;
1903         }
1904     }
1905     else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget)))
1906     {
1907         const double value = settings[key].toDouble(&ok);
1908         if (ok)
1909         {
1910             pDSB->setValue(value);
1911             return true;
1912         }
1913     }
1914     else if ((pCB = qobject_cast<QCheckBox *>(widget)))
1915     {
1916         const bool value = settings[key].toBool();
1917         pCB->setChecked(value);
1918         return true;
1919     }
1920     // ONLY FOR STRINGS, not INDEX
1921     else if ((pComboBox = qobject_cast<QComboBox *>(widget)))
1922     {
1923         const QString value = settings[key].toString();
1924         pComboBox->setCurrentText(value);
1925         return true;
1926     }
1927     else if ((pRadioButton = qobject_cast<QRadioButton *>(widget)))
1928     {
1929         const bool value = settings[key].toBool();
1930         pRadioButton->setChecked(value);
1931         return true;
1932     }
1933 
1934     return false;
1935 };
1936 
1937 ///////////////////////////////////////////////////////////////////////////////////////
1938 ///
1939 ///////////////////////////////////////////////////////////////////////////////////////
1940 void DarkLibrary::syncSettings()
1941 {
1942     QDoubleSpinBox *dsb = nullptr;
1943     QSpinBox *sb = nullptr;
1944     QCheckBox *cb = nullptr;
1945     QComboBox *cbox = nullptr;
1946     QRadioButton *cradio = nullptr;
1947 
1948     QString key;
1949     QVariant value;
1950 
1951     if ( (dsb = qobject_cast<QDoubleSpinBox*>(sender())))
1952     {
1953         key = dsb->objectName();
1954         value = dsb->value();
1955 
1956     }
1957     else if ( (sb = qobject_cast<QSpinBox*>(sender())))
1958     {
1959         key = sb->objectName();
1960         value = sb->value();
1961     }
1962     else if ( (cb = qobject_cast<QCheckBox*>(sender())))
1963     {
1964         key = cb->objectName();
1965         value = cb->isChecked();
1966     }
1967     else if ( (cbox = qobject_cast<QComboBox*>(sender())))
1968     {
1969         key = cbox->objectName();
1970         value = cbox->currentText();
1971     }
1972     else if ( (cradio = qobject_cast<QRadioButton*>(sender())))
1973     {
1974         key = cradio->objectName();
1975 
1976         // N.B. Only store CHECKED radios, do not store unchecked ones
1977         // as we only have exclusive groups.
1978         if (cradio->isChecked() == false)
1979         {
1980             // Remove from setting if it was added before
1981             if (m_Settings.contains(key))
1982             {
1983                 m_Settings.remove(key);
1984                 emit settingsUpdated(getAllSettings());
1985                 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
1986                 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::DarkLibrary, m_Settings);
1987             }
1988             return;
1989         }
1990 
1991         value = true;
1992     }
1993 
1994     // Save immediately
1995     Options::self()->setProperty(key.toLatin1(), value);
1996     Options::self()->save();
1997 
1998     m_Settings[key] = value;
1999     m_GlobalSettings[key] = value;
2000 
2001     emit settingsUpdated(getAllSettings());
2002 
2003     // Save to optical train specific settings as well
2004     OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
2005     OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::DarkLibrary, m_Settings);
2006 }
2007 
2008 ///////////////////////////////////////////////////////////////////////////////////////
2009 ///
2010 ///////////////////////////////////////////////////////////////////////////////////////
2011 QJsonObject DarkLibrary::getDefectSettings()
2012 {
2013     QStringList darkMasters;
2014     for (int i = 0; i < masterDarksCombo->count(); i++)
2015         darkMasters << masterDarksCombo->itemText(i);
2016 
2017     QJsonObject createDefectMaps =
2018     {
2019         {"masterTime", masterTime->text()},
2020         {"masterDarks", darkMasters.join('|')},
2021         {"masterExposure", masterExposure->text()},
2022         {"masterTempreture", masterTemperature->text()},
2023         {"masterMean", masterMean->text()},
2024         {"masterMedian", masterMedian->text()},
2025         {"masterDeviation", masterDeviation->text()},
2026         {"hotPixelsEnabled", hotPixelsEnabled->isChecked()},
2027         {"coldPixelsEnabled", coldPixelsEnabled->isChecked()},
2028     };
2029     return createDefectMaps;
2030 }
2031 
2032 
2033 
2034 }
2035