1 /*
2     SPDX-FileCopyrightText: 2016 Jasem Mutlaq <mutlaqja@ikarustech.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "darklibrary.h"
8 #include "auxiliary/ksmessagebox.h"
9 #include "auxiliary/ksnotification.h"
10 
11 #include "Options.h"
12 
13 #include "ekos/manager.h"
14 #include "ekos/capture/capture.h"
15 #include "ekos/capture/sequencejob.h"
16 #include "kstars.h"
17 #include "kspaths.h"
18 #include "kstarsdata.h"
19 #include "fitsviewer/fitsdata.h"
20 #include "fitsviewer/fitsview.h"
21 #include "fitsviewer/fitshistogramview.h"
22 
23 #include "ekos_debug.h"
24 
25 #include <QDesktopServices>
26 #include <QSqlRecord>
27 #include <QSqlTableModel>
28 #include <QStatusBar>
29 #include <QtConcurrent>
30 #include <algorithm>
31 #include <array>
32 
33 namespace Ekos
34 {
35 DarkLibrary *DarkLibrary::_DarkLibrary = nullptr;
36 
Instance()37 DarkLibrary *DarkLibrary::Instance()
38 {
39     if (_DarkLibrary == nullptr)
40         _DarkLibrary = new DarkLibrary(Manager::Instance());
41 
42     return _DarkLibrary;
43 }
44 
DarkLibrary(QWidget * parent)45 DarkLibrary::DarkLibrary(QWidget *parent) : QDialog(parent)
46 {
47     setupUi(this);
48 
49     m_StatusBar = new QStatusBar(this);
50     m_StatusLabel = new QLabel(i18n("Idle"), this);
51     m_FileLabel = new QLabel(this);
52     m_FileLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
53 
54     m_StatusBar->insertPermanentWidget(0, m_StatusLabel);
55     m_StatusBar->insertPermanentWidget(1, m_FileLabel, 1);
56     mainLayout->addWidget(m_StatusBar);
57 
58     histogramView->setProperty("axesLabelEnabled", false);
59     //histogramView->setProperty("linear", true);
60 
61     QDir writableDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation));
62     writableDir.mkpath("darks");
63     writableDir.mkpath("defectmaps");
64 
65     ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
66     // Dark Generation Connections
67     ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
68     m_CurrentDarkFrame.reset(new FITSData(), &QObject::deleteLater);
69 
70     m_DarkCameras = Options::darkCameras();
71     m_DefectCameras = Options::defectCameras();
72 
73     connect(darkHandlingButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), [this]()
74     {
75         const QString device = m_CurrentCamera->getDeviceName();
76         if (preferDarksRadio->isChecked())
77         {
78             m_DefectCameras.removeOne(device);
79             if (!m_DarkCameras.contains(device))
80                 m_DarkCameras.append(device);
81         }
82         else
83         {
84             m_DarkCameras.removeOne(device);
85             if (!m_DefectCameras.contains(device))
86                 m_DefectCameras.append(device);
87         }
88 
89         Options::setDarkCameras(m_DarkCameras);
90         Options::setDefectCameras(m_DefectCameras);
91     });
92 
93     connect(darkTableView,  &QAbstractItemView::doubleClicked, this, &DarkLibrary::loadDarkFITS);
94     connect(openDarksFolderB, &QPushButton::clicked, this, &DarkLibrary::openDarksFolder);
95     connect(clearAllB, &QPushButton::clicked, this, &DarkLibrary::clearAll);
96     connect(clearRowB, &QPushButton::clicked, this, &DarkLibrary::clearRow);
97     connect(clearExpiredB, &QPushButton::clicked, this, &DarkLibrary::clearExpired);
98     connect(refreshB, &QPushButton::clicked, this, &DarkLibrary::reloadDarksFromDatabase);
99 
100     connect(cameraS, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), [this]()
101     {
102         checkCamera();
103         reloadDarksFromDatabase();
104     });
105 
106     connect(&m_DarkFrameFutureWatcher, &QFutureWatcher<bool>::finished, [this]()
107     {
108         // If loading is successful, then set it in current dark view
109         if (m_DarkFrameFutureWatcher.result())
110         {
111             m_DarkView->loadData(m_CurrentDarkFrame);
112             loadCurrentMasterDefectMap();
113             histogramView->setImageData(m_CurrentDarkFrame);
114             if (!Options::nonLinearHistogram() && !m_CurrentDarkFrame->isHistogramConstructed())
115                 m_CurrentDarkFrame->constructHistogram();
116             populateMasterMetedata();
117         }
118         else
119             m_FileLabel->setText(i18n("Failed to load %1: %2",  m_MasterDarkFrameFilename, m_CurrentDarkFrame->getLastError()));
120 
121     });
122     connect(m_CurrentDarkFrame.data(), &FITSData::histogramReady, [this]()
123     {
124         histogramView->setEnabled(true);
125         histogramView->reset();
126         histogramView->syncGUI();
127     });
128 
129     connect(masterDarksCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [this](int index)
130     {
131         DarkLibrary::loadCurrentMasterDark(cameraS->currentText(), index);
132     });
133 
134 
135     connect(minExposureSpin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
136     connect(maxExposureSpin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
137     connect(exposureStepSin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
138 
139     connect(minTemperatureSpin, &QDoubleSpinBox::editingFinished, [this]()
140     {
141         maxTemperatureSpin->setMinimum(minTemperatureSpin->value());
142         countDarkTotalTime();
143     });
144     connect(maxTemperatureSpin, &QDoubleSpinBox::editingFinished, [this]()
145     {
146         minTemperatureSpin->setMaximum(maxTemperatureSpin->value());
147         countDarkTotalTime();
148     });
149     connect(temperatureStepSpin, &QDoubleSpinBox::editingFinished, [this]()
150     {
151         maxTemperatureSpin->setMinimum(minTemperatureSpin->value());
152         minTemperatureSpin->setMaximum(maxTemperatureSpin->value());
153         countDarkTotalTime();
154     });
155 
156     connect(countSpin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
157 
158     connect(binningButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled), this, [this](int,
159             bool)
160     {
161         countDarkTotalTime();
162     });
163 
164     connect(startB, &QPushButton::clicked, [this]()
165     {
166         generateDarkJobs();
167         executeDarkJobs();
168     });
169 
170     connect(stopB, &QPushButton::clicked, this, &DarkLibrary::stopDarkJobs);
171 
172     ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
173     // Master Darks Database Connections
174     ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
175     kcfg_DarkLibraryDuration->setValue(Options::darkLibraryDuration());
176     connect(kcfg_DarkLibraryDuration, &QDoubleSpinBox::editingFinished, [this]()
177     {
178         Options::setDarkLibraryDuration(kcfg_DarkLibraryDuration->value());
179     });
180 
181     kcfg_MaxDarkTemperatureDiff->setValue(Options::maxDarkTemperatureDiff());
182     connect(kcfg_MaxDarkTemperatureDiff, &QDoubleSpinBox::editingFinished, [this]()
183     {
184         Options::setMaxDarkTemperatureDiff(kcfg_MaxDarkTemperatureDiff->value());
185     });
186 
187     KStarsData::Instance()->userdb()->GetAllDarkFrames(m_DarkFramesDatabaseList);
188     ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
189     // Defect Map Connections
190     ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
191     connect(darkTabsWidget, &QTabWidget::currentChanged, [this](int index)
192     {
193         m_DarkView->setDefectMapEnabled(index == 1 && m_CurrentDefectMap);
194     });
195     connect(aggresivenessHotSlider, &QSlider::valueChanged, aggresivenessHotSpin, &QSpinBox::setValue);
196     connect(aggresivenessColdSlider, &QSlider::valueChanged, aggresivenessColdSpin, &QSpinBox::setValue);
197     connect(hotPixelsEnabled, &QCheckBox::toggled, [this](bool toggled)
198     {
199         if (m_CurrentDefectMap)
200             m_CurrentDefectMap->setProperty("HotEnabled", toggled);
201     });
202     connect(coldPixelsEnabled, &QCheckBox::toggled, [this](bool toggled)
203     {
204         if (m_CurrentDefectMap)
205             m_CurrentDefectMap->setProperty("ColdEnabled", toggled);
206     });
207     connect(generateMapB, &QPushButton::clicked, [this]()
208     {
209         if (m_CurrentDefectMap)
210         {
211             m_CurrentDefectMap->setProperty("HotPixelAggressiveness", aggresivenessHotSpin->value());
212             m_CurrentDefectMap->setProperty("ColdPixelAggressiveness", aggresivenessColdSpin->value());
213             m_CurrentDefectMap->filterPixels();
214         }
215     });
216     connect(resetMapParametersB, &QPushButton::clicked, [this]()
217     {
218         if (m_CurrentDefectMap)
219         {
220             aggresivenessHotSlider->setValue(75);
221             aggresivenessColdSlider->setValue(75);
222             m_CurrentDefectMap->setProperty("HotPixelAggressiveness", 75);
223             m_CurrentDefectMap->setProperty("ColdPixelAggressiveness", 75);
224             m_CurrentDefectMap->filterPixels();
225         }
226     });
227     connect(saveMapB, &QPushButton::clicked, this, &DarkLibrary::saveDefectMap);
228     ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
229     // Settings & Initialization
230     ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
231     m_RememberFITSViewer = Options::useFITSViewer();
232     m_RememberSummaryView = Options::useSummaryPreview();
233     initView();
234 }
235 
~DarkLibrary()236 DarkLibrary::~DarkLibrary()
237 {
238 }
239 
240 ///////////////////////////////////////////////////////////////////////////////////////
241 ///
242 ///////////////////////////////////////////////////////////////////////////////////////
refreshFromDB()243 void DarkLibrary::refreshFromDB()
244 {
245     KStarsData::Instance()->userdb()->GetAllDarkFrames(m_DarkFramesDatabaseList);
246 }
247 
248 ///////////////////////////////////////////////////////////////////////////////////////
249 ///
250 ///////////////////////////////////////////////////////////////////////////////////////
findDarkFrame(ISD::CCDChip * m_TargetChip,double duration,QSharedPointer<FITSData> & darkData)251 bool DarkLibrary::findDarkFrame(ISD::CCDChip *m_TargetChip, double duration, QSharedPointer<FITSData> &darkData)
252 {
253     QVariantMap bestCandidate;
254     for (auto &map : m_DarkFramesDatabaseList)
255     {
256         // First check CCD name matches and check if we are on the correct chip
257         if (map["ccd"].toString() == m_TargetChip->getCCD()->getDeviceName() &&
258                 map["chip"].toInt() == static_cast<int>(m_TargetChip->getType()))
259         {
260             int binX, binY;
261             m_TargetChip->getBinning(&binX, &binY);
262 
263             // Then check if binning is the same
264             if (map["binX"].toInt() == binX && map["binY"].toInt() == binY)
265             {
266                 // If camera has an active cooler, then we check temperature against the absolute threshold.
267                 if (m_TargetChip->getCCD()->hasCoolerControl())
268                 {
269                     double temperature = 0;
270                     m_TargetChip->getCCD()->getTemperature(&temperature);
271                     double darkTemperature = map["temperature"].toDouble();
272                     // If different is above threshold, it is completely rejected.
273                     if (darkTemperature != INVALID_VALUE && fabs(darkTemperature - temperature) > Options::maxDarkTemperatureDiff())
274                         continue;
275                 }
276 
277                 if (bestCandidate.isEmpty())
278                 {
279                     bestCandidate = map;
280                     continue;
281                 }
282 
283                 // We try to find the best frame
284                 // Frame closest in exposure duration wins
285                 // Frame with temperature closest to stored temperature wins (if temperature is reported)
286                 uint32_t thisMapScore = 0;
287                 uint32_t bestCandidateScore = 0;
288 
289                 // Else we check for the closest passive temperature
290                 if (m_TargetChip->getCCD()->hasCooler())
291                 {
292                     double temperature = 0;
293                     m_TargetChip->getCCD()->getTemperature(&temperature);
294                     double diffMap = std::fabs(temperature - map["temperature"].toDouble());
295                     double diffBest = std::fabs(temperature - bestCandidate["temperature"].toDouble());
296                     // Prefer temperatures closest to target
297                     if (diffMap < diffBest)
298                         thisMapScore++;
299                     else if (diffBest < diffMap)
300                         bestCandidateScore++;
301                 }
302 
303                 // Duration has a higher score priority over temperature
304                 {
305                     double diffMap = std::fabs(map["duration"].toDouble() - duration);
306                     double diffBest = std::fabs(bestCandidate["duration"].toDouble() - duration);
307                     if (diffMap < diffBest)
308                         thisMapScore += 2;
309                     else if (diffBest < diffMap)
310                         bestCandidateScore += 2;
311                 }
312 
313                 // More recent has a higher score than older.
314                 {
315                     const QDateTime now = QDateTime::currentDateTime();
316                     int64_t diffMap  = map["timestamp"].toDateTime().secsTo(now);
317                     int64_t diffBest = bestCandidate["timestamp"].toDateTime().secsTo(now);
318                     if (diffMap < diffBest)
319                         thisMapScore += 2;
320                     else if (diffBest < diffMap)
321                         bestCandidateScore += 2;
322                 }
323 
324                 // Find candidate with closest time in case we have multiple defect maps
325                 if (thisMapScore > bestCandidateScore)
326                     bestCandidate = map;
327             }
328         }
329     }
330 
331     if (bestCandidate.isEmpty())
332         return false;
333 
334     if (fabs(bestCandidate["duration"].toDouble() - duration) > 3)
335         emit i18n("Using available dark frame with %1 seconds exposure. Please take a dark frame with %1 seconds exposure for more accurate results.",
336                   QString::number(bestCandidate["duration"].toDouble(), 'f', 1),
337                   QString::number(duration, 'f', 1));
338 
339     QString filename = bestCandidate["filename"].toString();
340 
341     // Finally check if the duration is acceptable
342     QDateTime frameTime = bestCandidate["timestamp"].toDateTime();
343     if (frameTime.daysTo(QDateTime::currentDateTime()) > Options::darkLibraryDuration())
344     {
345         emit i18n("Dark frame %s is expired. Please create new master dark.", filename);
346         return false;
347     }
348 
349     if (m_CachedDarkFrames.contains(filename))
350     {
351         darkData = m_CachedDarkFrames[filename];
352         return true;
353     }
354 
355     // Finally we made it, let's put it in the hash
356     if (cacheDarkFrameFromFile(filename))
357     {
358         darkData = m_CachedDarkFrames[filename];
359         return true;
360     }
361 
362     // Remove bad dark frame
363     emit newLog(i18n("Removing bad dark frame file %1", filename));
364     m_CachedDarkFrames.remove(filename);
365     QFile::remove(filename);
366     KStarsData::Instance()->userdb()->DeleteDarkFrame(filename);
367     return false;
368 
369 }
370 
371 ///////////////////////////////////////////////////////////////////////////////////////
372 ///
373 ///////////////////////////////////////////////////////////////////////////////////////
findDefectMap(ISD::CCDChip * m_TargetChip,double duration,QSharedPointer<DefectMap> & defectMap)374 bool DarkLibrary::findDefectMap(ISD::CCDChip *m_TargetChip, double duration, QSharedPointer<DefectMap> &defectMap)
375 {
376     QVariantMap bestCandidate;
377     for (auto &map : m_DarkFramesDatabaseList)
378     {
379         if (map["defectmap"].toString().isEmpty())
380             continue;
381 
382         // First check CCD name matches and check if we are on the correct chip
383         if (map["ccd"].toString() == m_TargetChip->getCCD()->getDeviceName() &&
384                 map["chip"].toInt() == static_cast<int>(m_TargetChip->getType()))
385         {
386             int binX, binY;
387             m_TargetChip->getBinning(&binX, &binY);
388 
389             // Then check if binning is the same
390             if (map["binX"].toInt() == binX && map["binY"].toInt() == binY)
391             {
392                 if (bestCandidate.isEmpty())
393                 {
394                     bestCandidate = map;
395                     continue;
396                 }
397 
398                 // We try to find the best frame
399                 // Frame closest in exposure duration wins
400                 // Frame with temperature closest to stored temperature wins (if temperature is reported)
401                 uint32_t thisMapScore = 0;
402                 uint32_t bestCandidateScore = 0;
403 
404                 // Else we check for the closest passive temperature
405                 if (m_TargetChip->getCCD()->hasCooler())
406                 {
407                     double temperature = 0;
408                     m_TargetChip->getCCD()->getTemperature(&temperature);
409                     double diffMap = std::fabs(temperature - map["temperature"].toDouble());
410                     double diffBest = std::fabs(temperature - bestCandidate["temperature"].toDouble());
411                     // Prefer temperatures closest to target
412                     if (diffMap < diffBest)
413                         thisMapScore++;
414                     else if (diffBest < diffMap)
415                         bestCandidateScore++;
416                 }
417 
418                 // Duration has a higher score priority over temperature
419                 double diffMap = std::fabs(map["duration"].toDouble() - duration);
420                 double diffBest = std::fabs(bestCandidate["duration"].toDouble() - duration);
421                 if (diffMap < diffBest)
422                     thisMapScore += 2;
423                 else if (diffBest < diffMap)
424                     bestCandidateScore += 2;
425 
426                 // Find candidate with closest time in case we have multiple defect maps
427                 if (thisMapScore > bestCandidateScore)
428                     bestCandidate = map;
429             }
430         }
431     }
432 
433 
434     if (bestCandidate.isEmpty())
435         return false;
436 
437     QString darkFilename = bestCandidate["filename"].toString();
438     QString defectFilename = bestCandidate["defectmap"].toString();
439 
440     if (darkFilename.isEmpty() || defectFilename.isEmpty())
441         return false;
442 
443     if (m_CachedDefectMaps.contains(darkFilename))
444     {
445         defectMap = m_CachedDefectMaps[darkFilename];
446         return true;
447     }
448 
449     // Finally we made it, let's put it in the hash
450     if (cacheDefectMapFromFile(darkFilename, defectFilename))
451     {
452         defectMap = m_CachedDefectMaps[darkFilename];
453         return true;
454     }
455     else
456     {
457         // Remove bad dark frame
458         emit newLog(i18n("Failed to load defect map %1", defectFilename));
459         return false;
460     }
461 }
462 
463 ///////////////////////////////////////////////////////////////////////////////////////
464 ///
465 ///////////////////////////////////////////////////////////////////////////////////////
cacheDefectMapFromFile(const QString & key,const QString & filename)466 bool DarkLibrary::cacheDefectMapFromFile(const QString &key, const QString &filename)
467 {
468     QSharedPointer<DefectMap> oneMap;
469     oneMap.reset(new DefectMap());
470 
471     if (oneMap->load(filename))
472     {
473         oneMap->filterPixels();
474         m_CachedDefectMaps[key] = oneMap;
475         return true;
476     }
477 
478     emit newLog(i18n("Failed to load defect map file %1", filename));
479     return false;
480 }
481 
482 ///////////////////////////////////////////////////////////////////////////////////////
483 ///
484 ///////////////////////////////////////////////////////////////////////////////////////
cacheDarkFrameFromFile(const QString & filename)485 bool DarkLibrary::cacheDarkFrameFromFile(const QString &filename)
486 {
487     QFuture<bool> rc = m_CurrentDarkFrame->loadFromFile(filename);
488 
489     rc.waitForFinished();
490     if (rc.result())
491         m_CachedDarkFrames[filename] = m_CurrentDarkFrame;
492     else
493     {
494         emit newLog(i18n("Failed to load dark frame file %1", filename));
495     }
496 
497     return rc;
498 }
499 
500 ///////////////////////////////////////////////////////////////////////////////////////
501 ///
502 ///////////////////////////////////////////////////////////////////////////////////////
processNewImage(SequenceJob * job,const QSharedPointer<FITSData> & data)503 void DarkLibrary::processNewImage(SequenceJob *job, const QSharedPointer<FITSData> &data)
504 {
505     Q_UNUSED(data)
506     if (job->getStatus() == SequenceJob::JOB_IDLE)
507         return;
508 
509     if (job->getCompleted() == job->getCount())
510     {
511         QJsonObject metadata
512         {
513             {"camera", m_CurrentCamera->getDeviceName()},
514             {"chip", m_TargetChip->getType()},
515             {"binx", job->getXBin()},
516             {"biny", job->getYBin()},
517             {"duration", job->getExposure()}
518         };
519 
520         // Record temperature
521         if (m_CurrentCamera->hasCooler())
522             metadata["temperature"] = job->getCurrentTemperature();
523 
524         metadata["count"] = job->getCount();
525         generateMasterFrame(m_CurrentDarkFrame, metadata);
526         reloadDarksFromDatabase();
527         populateMasterMetedata();
528         histogramView->setImageData(m_CurrentDarkFrame);
529         if (!Options::nonLinearHistogram() && !m_CurrentDarkFrame->isHistogramConstructed())
530             m_CurrentDarkFrame->constructHistogram();
531     }
532 }
533 
534 ///////////////////////////////////////////////////////////////////////////////////////
535 ///
536 ///////////////////////////////////////////////////////////////////////////////////////
processNewBLOB(IBLOB * bp)537 void DarkLibrary::processNewBLOB(IBLOB *bp)
538 {
539     QByteArray buffer = QByteArray::fromRawData(reinterpret_cast<char *>(bp->blob), bp->size);
540     if (!m_CurrentDarkFrame->loadFromBuffer(buffer, "fits"))
541     {
542         m_FileLabel->setText(i18n("Failed to process dark data."));
543         return;
544     }
545 
546     if (!m_DarkView->loadData(m_CurrentDarkFrame))
547     {
548         m_FileLabel->setText(i18n("Failed to load dark data."));
549         return;
550     }
551 
552     uint32_t totalElements = m_CurrentDarkFrame->channels() * m_CurrentDarkFrame->samplesPerChannel();
553     if (totalElements != m_DarkMasterBuffer.size())
554         m_DarkMasterBuffer.assign(totalElements, 0);
555 
556     aggregate(m_CurrentDarkFrame);
557     darkProgress->setValue(darkProgress->value() + 1);
558     m_StatusLabel->setText(i18n("Received %1/%2 images.", darkProgress->value(), darkProgress->maximum()));
559 }
560 
561 ///////////////////////////////////////////////////////////////////////////////////////
562 ///
563 ///////////////////////////////////////////////////////////////////////////////////////
Release()564 void DarkLibrary::Release()
565 {
566     delete (_DarkLibrary);
567     _DarkLibrary = nullptr;
568 
569     //    m_Cameras.clear();
570     //    cameraS->clear();
571     //    m_CurrentCamera = nullptr;
572 }
573 
574 ///////////////////////////////////////////////////////////////////////////////////////
575 ///
576 ///////////////////////////////////////////////////////////////////////////////////////
closeEvent(QCloseEvent * ev)577 void DarkLibrary::closeEvent(QCloseEvent *ev)
578 {
579     Q_UNUSED(ev)
580     Options::setUseFITSViewer(m_RememberFITSViewer);
581     Options::setUseFITSViewer(m_RememberSummaryView);
582     if (!m_RememberFITSDirectory.isEmpty())
583         m_CaptureModule->fileDirectoryT->setText(m_RememberFITSDirectory);
584     if (m_JobsGenerated)
585     {
586         m_JobsGenerated = false;
587         m_CaptureModule->clearSequenceQueue();
588         m_CaptureModule->setPresetSettings(m_PresetSettings);
589     }
590 }
591 
592 ///////////////////////////////////////////////////////////////////////////////////////
593 ///
594 ///////////////////////////////////////////////////////////////////////////////////////
setCompleted()595 void DarkLibrary::setCompleted()
596 {
597     startB->setEnabled(true);
598     stopB->setEnabled(false);
599 
600     Options::setUseFITSViewer(m_RememberFITSViewer);
601     Options::setUseFITSViewer(m_RememberSummaryView);
602     if (!m_RememberFITSDirectory.isEmpty())
603         m_CaptureModule->fileDirectoryT->setText(m_RememberFITSDirectory);
604     if (m_JobsGenerated)
605     {
606         m_JobsGenerated = false;
607         m_CaptureModule->clearSequenceQueue();
608         m_CaptureModule->setPresetSettings(m_PresetSettings);
609     }
610 
611     m_CurrentCamera->disconnect(this);
612     m_CaptureModule->disconnect(this);
613 }
614 
615 ///////////////////////////////////////////////////////////////////////////////////////
616 ///
617 ///////////////////////////////////////////////////////////////////////////////////////
clearExpired()618 void DarkLibrary::clearExpired()
619 {
620     if (darkFramesModel->rowCount() == 0)
621         return;
622 
623     // Anything before this must go
624     QDateTime expiredDate = QDateTime::currentDateTime().addDays(kcfg_DarkLibraryDuration->value() * -1);
625 
626     QSqlDatabase userdb = QSqlDatabase::database("userdb");
627     userdb.open();
628     QSqlTableModel darkframe(nullptr, userdb);
629     darkframe.setEditStrategy(QSqlTableModel::OnManualSubmit);
630     darkframe.setTable("darkframe");
631     // Select all those that already expired.
632     darkframe.setFilter("ccd LIKE \'" + m_CurrentCamera->getDeviceName() + "\' AND timestamp < \'" + expiredDate.toString(
633                             Qt::ISODate) + "\'");
634 
635     darkframe.select();
636 
637     // Now remove all the expired files from disk
638     for (int i = 0; i < darkframe.rowCount(); ++i)
639     {
640         QString oneFile = darkframe.record(i).value("filename").toString();
641         QFile::remove(oneFile);
642         QString defectMap = darkframe.record(i).value("defectmap").toString();
643         if (defectMap.isEmpty() == false)
644             QFile::remove(defectMap);
645 
646     }
647 
648     // And remove them from the database
649     darkframe.removeRows(0, darkframe.rowCount());
650     darkframe.submitAll();
651     userdb.close();
652 
653     Ekos::DarkLibrary::Instance()->refreshFromDB();
654 
655     reloadDarksFromDatabase();
656 }
657 
658 ///////////////////////////////////////////////////////////////////////////////////////
659 ///
660 ///////////////////////////////////////////////////////////////////////////////////////
clearBuffers()661 void DarkLibrary::clearBuffers()
662 {
663     m_CurrentDarkFrame.clear();
664     // Should clear existing view
665     m_CurrentDarkFrame.reset(new FITSData(), &QObject::deleteLater);
666     connect(m_CurrentDarkFrame.data(), &FITSData::histogramReady, [this]()
667     {
668         histogramView->setEnabled(true);
669         histogramView->reset();
670         histogramView->syncGUI();
671     });
672     m_DarkView->clearData();
673     m_CurrentDefectMap.clear();
674 
675 }
676 ///////////////////////////////////////////////////////////////////////////////////////
677 ///
678 ///////////////////////////////////////////////////////////////////////////////////////
clearAll()679 void DarkLibrary::clearAll()
680 {
681     if (darkFramesModel->rowCount() == 0)
682         return;
683 
684     if (KMessageBox::questionYesNo(KStars::Instance(),
685                                    i18n("Are you sure you want to delete all dark frames images and data?")) ==
686             KMessageBox::No)
687         return;
688 
689     QSqlDatabase userdb = QSqlDatabase::database("userdb");
690     userdb.open();
691     QSqlTableModel darkframe(nullptr, userdb);
692     darkFramesModel->setEditStrategy(QSqlTableModel::OnManualSubmit);
693     darkframe.setTable("darkframe");
694     darkframe.setFilter("ccd LIKE \'" + m_CurrentCamera->getDeviceName() + "\'");
695     darkFramesModel->select();
696 
697     // Now remove all the expired files from disk
698     for (int i = 0; i < darkframe.rowCount(); ++i)
699     {
700         QString oneFile = darkframe.record(i).value("filename").toString();
701         QFile::remove(oneFile);
702         QString defectMap = darkframe.record(i).value("defectmap").toString();
703         if (defectMap.isEmpty() == false)
704             QFile::remove(defectMap);
705 
706     }
707 
708     darkFramesModel->removeRows(0, darkFramesModel->rowCount());
709     darkFramesModel->submitAll();
710     userdb.close();
711 
712     Ekos::DarkLibrary::Instance()->refreshFromDB();
713 
714     // Refesh db entries for other cameras
715     reloadDarksFromDatabase();
716 }
717 
718 ///////////////////////////////////////////////////////////////////////////////////////
719 ///
720 ///////////////////////////////////////////////////////////////////////////////////////
clearRow()721 void DarkLibrary::clearRow()
722 {
723     QSqlDatabase userdb = QSqlDatabase::database("userdb");
724     int row = darkTableView->currentIndex().row();
725 
726     QSqlRecord record = darkFramesModel->record(row);
727     QString filename = record.value("filename").toString();
728     QString defectMap = record.value("defectmap").toString();
729     QFile::remove(filename);
730     if (!defectMap.isEmpty())
731         QFile::remove(defectMap);
732 
733     userdb.open();
734 
735     darkFramesModel->removeRow(row);
736     darkFramesModel->submitAll();
737     userdb.close();
738 
739     darkTableView->selectionModel()->select(darkFramesModel->index(row - 1, 0), QItemSelectionModel::ClearAndSelect);
740 
741     refreshFromDB();
742     reloadDarksFromDatabase();
743 }
744 
745 ///////////////////////////////////////////////////////////////////////////////////////
746 ///
747 ///////////////////////////////////////////////////////////////////////////////////////
openDarksFolder()748 void DarkLibrary::openDarksFolder()
749 {
750     QString darkFilesPath = QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("darks");
751 
752     QDesktopServices::openUrl(QUrl::fromLocalFile(darkFilesPath));
753 }
754 
755 ///////////////////////////////////////////////////////////////////////////////////////
756 ///
757 ///////////////////////////////////////////////////////////////////////////////////////
refreshDefectMastersList(const QString & camera)758 void DarkLibrary::refreshDefectMastersList(const QString &camera)
759 {
760     if (darkFramesModel->rowCount() == 0)
761         return;
762 
763     masterDarksCombo->blockSignals(true);
764     masterDarksCombo->clear();
765 
766     for (int i = 0; i < darkFramesModel->rowCount(); ++i)
767     {
768         QSqlRecord record = darkFramesModel->record(i);
769 
770         if (record.value("ccd") != camera)
771             continue;
772 
773         int binX = record.value("binX").toInt();
774         int binY = record.value("binY").toInt();
775         double temperature = record.value("temperature").toDouble();
776         double duration = record.value("duration").toDouble();
777         QString ts = record.value("timestamp").toString();
778 
779         QString entry = QString("%1 secs %2x%3")
780                         .arg(QString::number(duration, 'f', 1))
781                         .arg(QString::number(binX))
782                         .arg(QString::number(binY));
783 
784         if (temperature > INVALID_VALUE)
785             entry.append(QString(" @ %1°").arg(QString::number(temperature, 'f', 1)));
786 
787         masterDarksCombo->addItem(entry);
788     }
789 
790     masterDarksCombo->blockSignals(false);
791 
792     //loadDefectMap();
793 
794 }
795 ///////////////////////////////////////////////////////////////////////////////////////
796 ///
797 ///////////////////////////////////////////////////////////////////////////////////////
reloadDarksFromDatabase()798 void DarkLibrary::reloadDarksFromDatabase()
799 {
800     QSqlDatabase userdb = QSqlDatabase::database("userdb");
801     userdb.open();
802 
803     const QString camera = m_CurrentCamera->getDeviceName();
804 
805     delete (darkFramesModel);
806     delete (sortFilter);
807 
808     darkFramesModel = new QSqlTableModel(this, userdb);
809     darkFramesModel->setTable("darkframe");
810     darkFramesModel->setFilter(QString("ccd='%1'").arg(camera));
811     darkFramesModel->select();
812 
813     sortFilter = new QSortFilterProxyModel(this);
814     sortFilter->setSourceModel(darkFramesModel);
815     sortFilter->sort (0);
816     darkTableView->setModel (sortFilter);
817 
818     //darkTableView->setModel(darkFramesModel);
819     // Hide ID
820     darkTableView->hideColumn(0);
821     // Hide Chip
822     darkTableView->hideColumn(2);
823 
824     userdb.close();
825 
826     if (darkFramesModel->rowCount() == 0 && m_CurrentDarkFrame)
827     {
828         clearBuffers();
829         return;
830     }
831 
832     refreshDefectMastersList(camera);
833     loadCurrentMasterDark(camera);
834 }
835 
836 ///////////////////////////////////////////////////////////////////////////////////////
837 ///
838 ///////////////////////////////////////////////////////////////////////////////////////
loadCurrentMasterDark(const QString & camera,int masterIndex)839 void DarkLibrary::loadCurrentMasterDark(const QString &camera, int masterIndex)
840 {
841     // Do not process empty models
842     if (darkFramesModel->rowCount() == 0)
843         return;
844 
845     if (masterIndex == -1)
846         masterIndex = masterDarksCombo->currentIndex();
847 
848     if (masterIndex < 0 || masterIndex >= darkFramesModel->rowCount())
849         return;
850 
851     QSqlRecord record = darkFramesModel->record(masterIndex);
852     if (record.value("ccd") != camera)
853         return;
854     // Get the master dark frame file name
855     m_MasterDarkFrameFilename = record.value("filename").toString();
856 
857     if (m_MasterDarkFrameFilename.isEmpty())
858         return;
859 
860     // Get defect file name as well if available.
861     m_DefectMapFilename = record.value("defectmap").toString();
862 
863     // If current dark frame is different from target filename, then load from file
864     if (m_CurrentDarkFrame->filename() != m_MasterDarkFrameFilename)
865         m_DarkFrameFutureWatcher.setFuture(m_CurrentDarkFrame->loadFromFile(m_MasterDarkFrameFilename));
866     // If current dark frame is the same one loaded, then check if we need to reload defect map
867     else
868         loadCurrentMasterDefectMap();
869 }
870 
871 ///////////////////////////////////////////////////////////////////////////////////////
872 ///
873 ///////////////////////////////////////////////////////////////////////////////////////
loadCurrentMasterDefectMap()874 void DarkLibrary::loadCurrentMasterDefectMap()
875 {
876     // Find if we have an existing map
877     if (m_CachedDefectMaps.contains(m_MasterDarkFrameFilename))
878     {
879         if (m_CurrentDefectMap != m_CachedDefectMaps.value(m_MasterDarkFrameFilename))
880         {
881             m_CurrentDefectMap = m_CachedDefectMaps.value(m_MasterDarkFrameFilename);
882             m_DarkView->setDefectMap(m_CurrentDefectMap);
883             m_CurrentDefectMap->setDarkData(m_CurrentDarkFrame);
884         }
885     }
886     // Create new defect map
887     else
888     {
889         m_CurrentDefectMap.reset(new DefectMap());
890         connect(m_CurrentDefectMap.data(), &DefectMap::pixelsUpdated, [this](uint32_t hot, uint32_t cold)
891         {
892             hotPixelsCount->setValue(hot);
893             coldPixelsCount->setValue(cold);
894             aggresivenessHotSlider->setValue(m_CurrentDefectMap->property("HotPixelAggressiveness").toInt());
895             aggresivenessColdSlider->setValue(m_CurrentDefectMap->property("ColdPixelAggressiveness").toInt());
896         });
897 
898         if (!m_DefectMapFilename.isEmpty())
899             cacheDefectMapFromFile(m_MasterDarkFrameFilename, m_DefectMapFilename);
900 
901         m_DarkView->setDefectMap(m_CurrentDefectMap);
902         m_CurrentDefectMap->setDarkData(m_CurrentDarkFrame);
903     }
904 }
905 
906 ///////////////////////////////////////////////////////////////////////////////////////
907 ///
908 ///////////////////////////////////////////////////////////////////////////////////////
populateMasterMetedata()909 void DarkLibrary::populateMasterMetedata()
910 {
911     if (m_CurrentDarkFrame.isNull())
912         return;
913 
914     QVariant value;
915     // TS
916     if (m_CurrentDarkFrame->getRecordValue("DATE-OBS", value))
917         masterTime->setText(value.toString());
918     // Temperature
919     if (m_CurrentDarkFrame->getRecordValue("CCD-TEMP", value) && value.toDouble() < 100)
920         masterTemperature->setText(QString::number(value.toDouble(), 'f', 1));
921     // Exposure
922     if (m_CurrentDarkFrame->getRecordValue("EXPTIME", value))
923         masterExposure->setText(value.toString());
924     // Median
925     {
926         double median = m_CurrentDarkFrame->getAverageMedian();
927         if (median > 0)
928             masterMedian->setText(QString::number(median, 'f', 1));
929     }
930     // Mean
931     {
932         double mean = m_CurrentDarkFrame->getAverageMean();
933         masterMean->setText(QString::number(mean, 'f', 1));
934     }
935     // Standard Deviation
936     {
937         double stddev = m_CurrentDarkFrame->getAverageStdDev();
938         masterDeviation->setText(QString::number(stddev, 'f', 1));
939     }
940 }
941 
942 ///////////////////////////////////////////////////////////////////////////////////////
943 ///
944 ///////////////////////////////////////////////////////////////////////////////////////
loadDarkFITS(QModelIndex index)945 void DarkLibrary::loadDarkFITS(QModelIndex index)
946 {
947     QSqlRecord record = darkFramesModel->record(index.row());
948 
949     QString filename = record.value("filename").toString();
950 
951     if (filename.isEmpty() == false)
952         m_DarkView->loadFile(filename);
953 }
954 
955 ///////////////////////////////////////////////////////////////////////////////////////
956 ///
957 ///////////////////////////////////////////////////////////////////////////////////////
addCamera(ISD::GDInterface * newCCD)958 void DarkLibrary::addCamera(ISD::GDInterface * newCCD)
959 {
960     m_Cameras.append(static_cast<ISD::CCD*>(newCCD));
961     cameraS->addItem(newCCD->getDeviceName());
962 
963     checkCamera();
964 
965     reloadDarksFromDatabase();
966 }
967 
968 ///////////////////////////////////////////////////////////////////////////////////////
969 ///
970 ///////////////////////////////////////////////////////////////////////////////////////
removeCamera(ISD::GDInterface * newCCD)971 void DarkLibrary::removeCamera(ISD::GDInterface * newCCD)
972 {
973     m_Cameras.removeOne(static_cast<ISD::CCD*>(newCCD));
974     cameraS->removeItem(cameraS->findText(newCCD->getDeviceName()));
975 }
976 
977 ///////////////////////////////////////////////////////////////////////////////////////
978 ///
979 ///////////////////////////////////////////////////////////////////////////////////////
checkCamera(int ccdNum)980 void DarkLibrary::checkCamera(int ccdNum)
981 {
982     if (ccdNum == -1)
983     {
984         ccdNum = cameraS->currentIndex();
985 
986         if (ccdNum == -1)
987             return;
988     }
989 
990     if (ccdNum < m_Cameras.count())
991     {
992         // Check whether main camera or guide head only
993         m_CurrentCamera = m_Cameras.at(ccdNum);
994 
995         const QString device = m_CurrentCamera->getDeviceName();
996         if (m_DarkCameras.contains(device))
997             preferDarksRadio->setChecked(true);
998         else if (m_DefectCameras.contains(device))
999             preferDefectsRadio->setChecked(true);
1000 
1001         m_TargetChip = nullptr;
1002         if (cameraS->itemText(ccdNum).right(6) == QString("Guider"))
1003         {
1004             m_UseGuideHead = true;
1005             m_TargetChip   = m_CurrentCamera->getChip(ISD::CCDChip::GUIDE_CCD);
1006         }
1007 
1008         if (m_TargetChip == nullptr)
1009         {
1010             m_UseGuideHead = false;
1011             m_TargetChip   = m_CurrentCamera->getChip(ISD::CCDChip::PRIMARY_CCD);
1012         }
1013 
1014         // Make sure we have a valid chip and valid base device.
1015         // Make sure we are not in capture process.
1016         if (!m_TargetChip || !m_TargetChip->getCCD() || !m_TargetChip->getCCD()->getBaseDevice() ||
1017                 m_TargetChip->isCapturing())
1018             return;
1019 
1020         //        for (auto &ccd : m_Cameras)
1021         //        {
1022         //            disconnect(ccd, &ISD::CCD::numberUpdated, this, &Ekos::Capture::processCCDNumber);
1023         //            disconnect(ccd, &ISD::CCD::newTemperatureValue, this, &Ekos::Capture::updateCCDTemperature);
1024         //            disconnect(ccd, &ISD::CCD::coolerToggled, this, &Ekos::Capture::setCoolerToggled);
1025         //            disconnect(ccd, &ISD::CCD::newRemoteFile, this, &Ekos::Capture::setNewRemoteFile);
1026         //            disconnect(ccd, &ISD::CCD::videoStreamToggled, this, &Ekos::Capture::setVideoStreamEnabled);
1027         //            disconnect(ccd, &ISD::CCD::ready, this, &Ekos::Capture::ready);
1028         //        }
1029 
1030         if (m_CurrentCamera->hasCoolerControl())
1031         {
1032             temperatureLabel->setEnabled(true);
1033             temperatureStepLabel->setEnabled(true);
1034             temperatureToLabel->setEnabled(true);
1035             temperatureStepSpin->setEnabled(true);
1036             minTemperatureSpin->setEnabled(true);
1037             maxTemperatureSpin->setEnabled(true);
1038 
1039         }
1040         else
1041         {
1042             temperatureLabel->setEnabled(false);
1043             temperatureStepLabel->setEnabled(false);
1044             temperatureToLabel->setEnabled(false);
1045             temperatureStepSpin->setEnabled(false);
1046             minTemperatureSpin->setEnabled(false);
1047             maxTemperatureSpin->setEnabled(false);
1048         }
1049 
1050         countDarkTotalTime();
1051     }
1052 }
1053 
1054 ///////////////////////////////////////////////////////////////////////////////////////
1055 ///
1056 ///////////////////////////////////////////////////////////////////////////////////////
countDarkTotalTime()1057 void DarkLibrary::countDarkTotalTime()
1058 {
1059 
1060     //double exposureCount = (maxExposureSpin->value() - minExposureSpin->value()) / exposureStepSin->value();
1061     double temperatureCount = 1;
1062     if (m_CurrentCamera->hasCoolerControl() && std::fabs(maxTemperatureSpin->value() - minTemperatureSpin->value()) > 0)
1063         temperatureCount = (std::fabs((maxTemperatureSpin->value() - minTemperatureSpin->value())) / temperatureStepSpin->value()) +
1064                            1;
1065     int binnings = 0;
1066     if (bin1Check->isChecked())
1067         binnings++;
1068     if (bin2Check->isChecked())
1069         binnings++;
1070     if (bin4Check->isChecked())
1071         binnings++;
1072 
1073     double darkTime = 0;
1074     int imagesCount = 0;
1075     for (double startExposure = minExposureSpin->value(); startExposure <= maxExposureSpin->value();
1076             startExposure += exposureStepSin->value())
1077     {
1078         darkTime += startExposure * temperatureCount * binnings * countSpin->value();
1079         imagesCount += temperatureCount * binnings * countSpin->value();
1080     }
1081 
1082     totalTime->setText(QString::number(darkTime / 60.0, 'f', 1));
1083     totalImages->setText(QString::number(imagesCount));
1084     darkProgress->setMaximum(imagesCount);
1085 
1086 }
1087 
1088 ///////////////////////////////////////////////////////////////////////////////////////
1089 ///
1090 ///////////////////////////////////////////////////////////////////////////////////////
generateDarkJobs()1091 void DarkLibrary::generateDarkJobs()
1092 {
1093     // Always clear sequence queue before starting
1094     m_CaptureModule->clearSequenceQueue();
1095 
1096     if (m_JobsGenerated == false)
1097     {
1098         m_JobsGenerated = true;
1099         m_PresetSettings = m_CaptureModule->getPresetSettings();
1100     }
1101 
1102     QList<double> temperatures;
1103     if (m_CurrentCamera->hasCoolerControl() && std::fabs(maxTemperatureSpin->value() - minTemperatureSpin->value()) >= 0)
1104     {
1105         for (double oneTemperature = minTemperatureSpin->value(); oneTemperature <= maxTemperatureSpin->value();
1106                 oneTemperature += temperatureStepSpin->value())
1107         {
1108             temperatures << oneTemperature;
1109         }
1110 
1111         // Enforce temperature set
1112         m_CaptureModule->setForceTemperature(true);
1113     }
1114     else
1115     {
1116         // Disable temperature set
1117         m_CaptureModule->setForceTemperature(false);
1118         temperatures << INVALID_VALUE;
1119     }
1120 
1121     QList<uint8_t> bins;
1122     if (bin1Check->isChecked())
1123         bins << 1;
1124     if (bin2Check->isChecked())
1125         bins << 2;
1126     if (bin4Check->isChecked())
1127         bins << 4;
1128 
1129     QList<double> exposures;
1130     for (double oneExposure = minExposureSpin->value(); oneExposure <= maxExposureSpin->value();
1131             oneExposure += exposureStepSin->value())
1132     {
1133         exposures << oneExposure;
1134     }
1135 
1136     QString prefix = QDir::tempPath() + QDir::separator() + QString::number(QDateTime::currentSecsSinceEpoch()) +
1137                      QDir::separator();
1138 
1139 
1140     int sequence = 0;
1141     for (const auto oneTemperature : qAsConst(temperatures))
1142     {
1143         for (const auto &oneExposure : qAsConst(exposures))
1144         {
1145             for (const auto &oneBin : qAsConst(bins))
1146             {
1147                 sequence++;
1148 
1149                 QJsonObject settings;
1150 
1151                 settings["camera"] = cameraS->currentText();
1152                 settings["exp"] = oneExposure;
1153                 settings["bin"] = oneBin;
1154                 settings["frameType"] = FRAME_DARK;
1155                 settings["temperature"] = oneTemperature;
1156                 settings["format"] = 0;
1157 
1158                 QString directory = prefix + QString("sequence_%1").arg(sequence);
1159                 QJsonObject fileSettings;
1160 
1161                 fileSettings["directory"] = directory;
1162                 m_CaptureModule->setPresetSettings(settings);
1163                 m_CaptureModule->setFileSettings(fileSettings);
1164                 m_CaptureModule->setCount(countSpin->value());
1165                 m_CaptureModule->addJob();
1166             }
1167         }
1168     }
1169 }
1170 
1171 ///////////////////////////////////////////////////////////////////////////////////////
1172 ///
1173 ///////////////////////////////////////////////////////////////////////////////////////
executeDarkJobs()1174 void DarkLibrary::executeDarkJobs()
1175 {
1176     m_DarkImagesCounter = 0;
1177     darkProgress->setValue(0);
1178     darkProgress->setTextVisible(true);
1179     connect(m_CaptureModule, &Capture::newImage, this, &DarkLibrary::processNewImage, Qt::UniqueConnection);
1180     connect(m_CaptureModule, &Capture::newStatus, this, &DarkLibrary::setCaptureState, Qt::UniqueConnection);
1181     connect(m_CurrentCamera, &ISD::CCD::BLOBUpdated, this, &DarkLibrary::processNewBLOB, Qt::UniqueConnection);
1182 
1183     Options::setUseFITSViewer(false);
1184     Options::setUseSummaryPreview(false);
1185     startB->setEnabled(false);
1186     stopB->setEnabled(true);
1187     m_DarkView->reset();
1188     m_StatusLabel->setText(i18n("In progress..."));
1189     m_CaptureModule->start();
1190 
1191 }
1192 
1193 ///////////////////////////////////////////////////////////////////////////////////////
1194 ///
1195 ///////////////////////////////////////////////////////////////////////////////////////
stopDarkJobs()1196 void DarkLibrary::stopDarkJobs()
1197 {
1198     m_CaptureModule->abort();
1199     darkProgress->setValue(0);
1200     m_DarkView->reset();
1201 }
1202 
1203 ///////////////////////////////////////////////////////////////////////////////////////
1204 ///
1205 ///////////////////////////////////////////////////////////////////////////////////////
initView()1206 void DarkLibrary::initView()
1207 {
1208     m_DarkView = new DarkView(darkWidget);
1209     m_DarkView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1210     m_DarkView->setBaseSize(darkWidget->size());
1211     m_DarkView->createFloatingToolBar();
1212     QVBoxLayout *vlayout = new QVBoxLayout();
1213     vlayout->addWidget(m_DarkView);
1214     darkWidget->setLayout(vlayout);
1215     //connect(m_DarkView, &FITSView::loaded, this, &DarkLibrary::loadCurrentMasterDefectMap);
1216 }
1217 
1218 ///////////////////////////////////////////////////////////////////////////////////////
1219 ///
1220 ///////////////////////////////////////////////////////////////////////////////////////
aggregate(const QSharedPointer<FITSData> & data)1221 void DarkLibrary::aggregate(const QSharedPointer<FITSData> &data)
1222 {
1223     switch (data->dataType())
1224     {
1225         case TBYTE:
1226             aggregateInternal<uint8_t>(data);
1227             break;
1228 
1229         case TSHORT:
1230             aggregateInternal<int16_t>(data);
1231             break;
1232 
1233         case TUSHORT:
1234             aggregateInternal<uint16_t>(data);
1235             break;
1236 
1237         case TLONG:
1238             aggregateInternal<int32_t>(data);
1239             break;
1240 
1241         case TULONG:
1242             aggregateInternal<uint32_t>(data);
1243             break;
1244 
1245         case TFLOAT:
1246             aggregateInternal<float>(data);
1247             break;
1248 
1249         case TLONGLONG:
1250             aggregateInternal<int64_t>(data);
1251             break;
1252 
1253         case TDOUBLE:
1254             aggregateInternal<double>(data);
1255             break;
1256 
1257         default:
1258             break;
1259     }
1260 }
1261 
1262 ///////////////////////////////////////////////////////////////////////////////////////
1263 ///
1264 ///////////////////////////////////////////////////////////////////////////////////////
1265 template <typename T>
aggregateInternal(const QSharedPointer<FITSData> & data)1266 void DarkLibrary::aggregateInternal(const QSharedPointer<FITSData> &data)
1267 {
1268     T const *darkBuffer  = reinterpret_cast<T const*>(data->getImageBuffer());
1269     for (uint32_t i = 0; i < m_DarkMasterBuffer.size(); i++)
1270         m_DarkMasterBuffer[i] += darkBuffer[i];
1271 }
1272 
1273 ///////////////////////////////////////////////////////////////////////////////////////
1274 ///
1275 ///////////////////////////////////////////////////////////////////////////////////////
generateMasterFrame(const QSharedPointer<FITSData> & data,const QJsonObject & metadata)1276 void DarkLibrary::generateMasterFrame(const QSharedPointer<FITSData> &data, const QJsonObject &metadata)
1277 {
1278     switch (data->dataType())
1279     {
1280         case TBYTE:
1281             generateMasterFrameInternal<uint8_t>(data, metadata);
1282             break;
1283 
1284         case TSHORT:
1285             generateMasterFrameInternal<int16_t>(data, metadata);
1286             break;
1287 
1288         case TUSHORT:
1289             generateMasterFrameInternal<uint16_t>(data, metadata);
1290             break;
1291 
1292         case TLONG:
1293             generateMasterFrameInternal<int32_t>(data, metadata);
1294             break;
1295 
1296         case TULONG:
1297             generateMasterFrameInternal<uint32_t>(data, metadata);
1298             break;
1299 
1300         case TFLOAT:
1301             generateMasterFrameInternal<float>(data, metadata);
1302             break;
1303 
1304         case TLONGLONG:
1305             generateMasterFrameInternal<int64_t>(data, metadata);
1306             break;
1307 
1308         case TDOUBLE:
1309             generateMasterFrameInternal<double>(data, metadata);
1310             break;
1311 
1312         default:
1313             break;
1314     }
1315 
1316     // Reset Master Buffer
1317     m_DarkMasterBuffer.assign(m_DarkMasterBuffer.size(), 0);
1318 }
1319 
1320 ///////////////////////////////////////////////////////////////////////////////////////
1321 ///
1322 ///////////////////////////////////////////////////////////////////////////////////////
generateMasterFrameInternal(const QSharedPointer<FITSData> & data,const QJsonObject & metadata)1323 template <typename T>  void DarkLibrary::generateMasterFrameInternal(const QSharedPointer<FITSData> &data,
1324         const QJsonObject &metadata)
1325 {
1326     T *writableBuffer = reinterpret_cast<T *>(data->getWritableImageBuffer());
1327     const uint32_t count = metadata["count"].toInt();
1328     // Average the values
1329     for (uint32_t i = 0; i < m_DarkMasterBuffer.size(); i++)
1330         writableBuffer[i] = m_DarkMasterBuffer[i] / count;
1331 
1332 
1333     QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss");
1334     QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("darks/darkframe_" + ts + ".fits");
1335 
1336     data->calculateStats(true);
1337     if (!data->saveImage(path))
1338     {
1339         m_FileLabel->setText(i18n("Failed to save master frame: %1", data->getLastError()));
1340         return;
1341     }
1342 
1343     m_CachedDarkFrames[path] = data;
1344 
1345     QVariantMap map;
1346     map["ccd"]         = metadata["camera"].toString();
1347     map["chip"]        = metadata["chip"].toInt();
1348     map["binX"]        = metadata["binx"].toInt();
1349     map["binY"]        = metadata["biny"].toInt();
1350     map["temperature"] = metadata["temperature"].toDouble(INVALID_VALUE);
1351     map["duration"]    = metadata["duration"].toDouble();
1352     map["filename"]    = path;
1353 
1354     m_DarkFramesDatabaseList.append(map);
1355     m_FileLabel->setText(i18n("Master Dark saved to %1", path));
1356     KStarsData::Instance()->userdb()->AddDarkFrame(map);
1357 }
1358 
1359 ///////////////////////////////////////////////////////////////////////////////////////
1360 ///
1361 ///////////////////////////////////////////////////////////////////////////////////////
setCaptureModule(Capture * instance)1362 void DarkLibrary::setCaptureModule(Capture *instance)
1363 {
1364     m_CaptureModule = instance;
1365     m_RememberFITSDirectory = m_CaptureModule->fileDirectoryT->text();
1366 }
1367 
1368 ///////////////////////////////////////////////////////////////////////////////////////
1369 ///
1370 ///////////////////////////////////////////////////////////////////////////////////////
setCaptureState(CaptureState state)1371 void DarkLibrary::setCaptureState(CaptureState state)
1372 {
1373     switch (state)
1374     {
1375         case CAPTURE_ABORTED:
1376             setCompleted();
1377             m_StatusLabel->setText(i18n("Capture aborted."));
1378             break;
1379         case CAPTURE_COMPLETE:
1380             setCompleted();
1381             m_StatusLabel->setText(i18n("Capture completed."));
1382             break;
1383         default:
1384             break;
1385     }
1386 }
1387 
1388 ///////////////////////////////////////////////////////////////////////////////////////
1389 ///
1390 ///////////////////////////////////////////////////////////////////////////////////////
saveDefectMap()1391 void DarkLibrary::saveDefectMap()
1392 {
1393     if (!m_CurrentDarkFrame)
1394         return;
1395 
1396     QString filename = m_CurrentDefectMap->filename();
1397     bool newFile = false;
1398     if (filename.isEmpty())
1399     {
1400         QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss");
1401         filename = QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("defectmaps/defectmap_" + ts +
1402                    ".json");
1403         newFile = true;
1404     }
1405 
1406     if (m_CurrentDefectMap->save(filename, m_CurrentCamera->getDeviceName()))
1407     {
1408         m_FileLabel->setText(i18n("Defect map saved to %1", filename));
1409 
1410         if (newFile)
1411         {
1412             auto currentMap = std::find_if(m_DarkFramesDatabaseList.begin(),
1413                                            m_DarkFramesDatabaseList.end(), [&](const QVariantMap & oneMap)
1414             {
1415                 return oneMap["filename"].toString() == m_CurrentDarkFrame->filename();
1416             });
1417 
1418             if (currentMap != m_DarkFramesDatabaseList.end())
1419             {
1420                 (*currentMap)["defectmap"] = filename;
1421                 KStarsData::Instance()->userdb()->UpdateDarkFrame(*currentMap);
1422             }
1423         }
1424     }
1425     else
1426     {
1427         m_FileLabel->setText(i18n("Failed to save defect map to %1", filename));
1428     }
1429 }
1430 
1431 }
1432 
1433