Kstars

darklibrary.cpp
1 /*
2  SPDX-FileCopyrightText: 2016 Jasem Mutlaq <[email protected]>
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 
22 #include "ekos_debug.h"
23 
24 #include <QDesktopServices>
25 #include <QSqlRecord>
26 #include <QSqlTableModel>
27 #include <QStatusBar>
28 #include <algorithm>
29 #include <array>
30 
31 namespace Ekos
32 {
33 DarkLibrary *DarkLibrary::_DarkLibrary = nullptr;
34 
35 DarkLibrary *DarkLibrary::Instance()
36 {
37  if (_DarkLibrary == nullptr)
38  _DarkLibrary = new DarkLibrary(Manager::Instance());
39 
40  return _DarkLibrary;
41 }
42 
43 DarkLibrary::DarkLibrary(QWidget *parent) : QDialog(parent)
44 {
45  setupUi(this);
46 
47  m_StatusBar = new QStatusBar(this);
48  m_StatusLabel = new QLabel(i18n("Idle"), this);
49  m_FileLabel = new QLabel(this);
50  m_FileLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
51 
52  m_StatusBar->insertPermanentWidget(0, m_StatusLabel);
53  m_StatusBar->insertPermanentWidget(1, m_FileLabel, 1);
54  mainLayout->addWidget(m_StatusBar);
55 
56  QDir writableDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
57  writableDir.mkpath("darks");
58  writableDir.mkpath("defectmaps");
59 
60  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
61  // Dark Generation Connections
62  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
63  m_CurrentDarkFrame.reset(new FITSData(), &QObject::deleteLater);
64 
65  m_DarkCameras = Options::darkCameras();
66  m_DefectCameras = Options::defectCameras();
67 
68 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
69  connect(darkHandlingButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled),
70  this, [this]()
71 #else
72  connect(darkHandlingButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled),
73  this, [this]()
74 #endif
75  {
76  const QString device = m_CurrentCamera->getDeviceName();
77  if (preferDarksRadio->isChecked())
78  {
79  m_DefectCameras.removeOne(device);
80  if (!m_DarkCameras.contains(device))
81  m_DarkCameras.append(device);
82  }
83  else
84  {
85  m_DarkCameras.removeOne(device);
86  if (!m_DefectCameras.contains(device))
87  m_DefectCameras.append(device);
88  }
89 
90  Options::setDarkCameras(m_DarkCameras);
91  Options::setDefectCameras(m_DefectCameras);
92  });
93 
94  connect(darkTableView, &QAbstractItemView::doubleClicked, this, [this](QModelIndex index)
95  {
96  loadIndexInView(index.row());
97  });
98  connect(openDarksFolderB, &QPushButton::clicked, this, &DarkLibrary::openDarksFolder);
99  connect(clearAllB, &QPushButton::clicked, this, &DarkLibrary::clearAll);
100  connect(clearRowB, &QPushButton::clicked, this, &DarkLibrary::clearRow);
101  connect(clearExpiredB, &QPushButton::clicked, this, &DarkLibrary::clearExpired);
102  connect(refreshB, &QPushButton::clicked, this, &DarkLibrary::reloadDarksFromDatabase);
103 
104  connect(cameraS, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, [this]()
105  {
106  checkCamera();
107  reloadDarksFromDatabase();
108  });
109 
110  connect(&m_DarkFrameFutureWatcher, &QFutureWatcher<bool>::finished, this, [this]()
111  {
112  // If loading is successful, then set it in current dark view
113  if (m_DarkFrameFutureWatcher.result())
114  {
115  m_DarkView->loadData(m_CurrentDarkFrame);
116  loadCurrentMasterDefectMap();
117  populateMasterMetedata();
118  }
119  else
120  m_FileLabel->setText(i18n("Failed to load %1: %2", m_MasterDarkFrameFilename, m_CurrentDarkFrame->getLastError()));
121 
122  });
123 
124  connect(masterDarksCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [this](int index)
125  {
126  DarkLibrary::loadCurrentMasterDark(cameraS->currentText(), index);
127  });
128 
129 
130  connect(minExposureSpin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
131  connect(maxExposureSpin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
132  connect(exposureStepSin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
133 
134  connect(minTemperatureSpin, &QDoubleSpinBox::editingFinished, this, [this]()
135  {
136  maxTemperatureSpin->setMinimum(minTemperatureSpin->value());
137  countDarkTotalTime();
138  });
139  connect(maxTemperatureSpin, &QDoubleSpinBox::editingFinished, this, [this]()
140  {
141  minTemperatureSpin->setMaximum(maxTemperatureSpin->value());
142  countDarkTotalTime();
143  });
144  connect(temperatureStepSpin, &QDoubleSpinBox::editingFinished, this, [this]()
145  {
146  maxTemperatureSpin->setMinimum(minTemperatureSpin->value());
147  minTemperatureSpin->setMaximum(maxTemperatureSpin->value());
148  countDarkTotalTime();
149  });
150 
151  connect(countSpin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
152 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
153  connect(binningButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled),
154  this, [this](int, bool)
155 #else
156  connect(binningButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled),
157  this, [this](int, bool)
158 #endif
159  {
160  countDarkTotalTime();
161  });
162 
163  connect(startB, &QPushButton::clicked, this, &DarkLibrary::start);
164  connect(stopB, &QPushButton::clicked, this, &DarkLibrary::stop);
165 
166  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
167  // Master Darks Database Connections
168  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
169  kcfg_DarkLibraryDuration->setValue(Options::darkLibraryDuration());
170  connect(kcfg_DarkLibraryDuration, &QDoubleSpinBox::editingFinished, [this]()
171  {
172  Options::setDarkLibraryDuration(kcfg_DarkLibraryDuration->value());
173  });
174 
175  kcfg_MaxDarkTemperatureDiff->setValue(Options::maxDarkTemperatureDiff());
176  connect(kcfg_MaxDarkTemperatureDiff, &QDoubleSpinBox::editingFinished, [this]()
177  {
178  Options::setMaxDarkTemperatureDiff(kcfg_MaxDarkTemperatureDiff->value());
179  });
180 
181  KStarsData::Instance()->userdb()->GetAllDarkFrames(m_DarkFramesDatabaseList);
182  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
183  // Defect Map Connections
184  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
185  connect(darkTabsWidget, &QTabWidget::currentChanged, this, [this](int index)
186  {
187  m_DarkView->setDefectMapEnabled(index == 1 && m_CurrentDefectMap);
188  });
189  connect(aggresivenessHotSlider, &QSlider::valueChanged, aggresivenessHotSpin, &QSpinBox::setValue);
190  connect(aggresivenessColdSlider, &QSlider::valueChanged, aggresivenessColdSpin, &QSpinBox::setValue);
191  connect(hotPixelsEnabled, &QCheckBox::toggled, this, [this](bool toggled)
192  {
193  if (m_CurrentDefectMap)
194  m_CurrentDefectMap->setProperty("HotEnabled", toggled);
195  });
196  connect(coldPixelsEnabled, &QCheckBox::toggled, this, [this](bool toggled)
197  {
198  if (m_CurrentDefectMap)
199  m_CurrentDefectMap->setProperty("ColdEnabled", toggled);
200  });
201  connect(generateMapB, &QPushButton::clicked, this, [this]()
202  {
203  if (m_CurrentDefectMap)
204  {
205  m_CurrentDefectMap->setProperty("HotPixelAggressiveness", aggresivenessHotSpin->value());
206  m_CurrentDefectMap->setProperty("ColdPixelAggressiveness", aggresivenessColdSpin->value());
207  m_CurrentDefectMap->filterPixels();
208  emit newFrame(m_DarkView);
209  }
210  });
211  connect(resetMapParametersB, &QPushButton::clicked, this, [this]()
212  {
213  if (m_CurrentDefectMap)
214  {
215  aggresivenessHotSlider->setValue(75);
216  aggresivenessColdSlider->setValue(75);
217  m_CurrentDefectMap->setProperty("HotPixelAggressiveness", 75);
218  m_CurrentDefectMap->setProperty("ColdPixelAggressiveness", 75);
219  m_CurrentDefectMap->filterPixels();
220  }
221  });
222  connect(saveMapB, &QPushButton::clicked, this, &DarkLibrary::saveDefectMap);
223  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
224  // Settings & Initialization
225  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
226  m_RememberFITSViewer = Options::useFITSViewer();
227  m_RememberSummaryView = Options::useSummaryPreview();
228  initView();
229 }
230 
231 DarkLibrary::~DarkLibrary()
232 {
233 }
234 
235 ///////////////////////////////////////////////////////////////////////////////////////
236 ///
237 ///////////////////////////////////////////////////////////////////////////////////////
238 void DarkLibrary::refreshFromDB()
239 {
240  KStarsData::Instance()->userdb()->GetAllDarkFrames(m_DarkFramesDatabaseList);
241 }
242 
243 ///////////////////////////////////////////////////////////////////////////////////////
244 ///
245 ///////////////////////////////////////////////////////////////////////////////////////
246 bool DarkLibrary::findDarkFrame(ISD::CameraChip *m_TargetChip, double duration, QSharedPointer<FITSData> &darkData)
247 {
248  QVariantMap bestCandidate;
249  for (auto &map : m_DarkFramesDatabaseList)
250  {
251  // First check CCD name matches and check if we are on the correct chip
252  if (map["ccd"].toString() == m_TargetChip->getCCD()->getDeviceName() &&
253  map["chip"].toInt() == static_cast<int>(m_TargetChip->getType()))
254  {
255  // Match Gain
256  int gain = getGain();
257  if (gain >= 0 && map["gain"].toInt() != gain)
258  continue;
259 
260  // Match ISO
261  QString isoValue;
262  if (m_TargetChip->getISOValue(isoValue) && map["iso"].toString() != isoValue)
263  continue;
264 
265  // Match binning
266  int binX = 1, binY = 1;
267  m_TargetChip->getBinning(&binX, &binY);
268 
269  // Then check if binning is the same
270  if (map["binX"].toInt() != binX || map["binY"].toInt() != binY)
271  continue;
272 
273  // If camera has an active cooler, then we check temperature against the absolute threshold.
274  if (m_TargetChip->getCCD()->hasCoolerControl())
275  {
276  double temperature = 0;
277  m_TargetChip->getCCD()->getTemperature(&temperature);
278  double darkTemperature = map["temperature"].toDouble();
279  // If different is above threshold, it is completely rejected.
280  if (darkTemperature != INVALID_VALUE && fabs(darkTemperature - temperature) > Options::maxDarkTemperatureDiff())
281  continue;
282  }
283 
284  if (bestCandidate.isEmpty())
285  {
286  bestCandidate = map;
287  continue;
288  }
289 
290  // We try to find the best frame
291  // Frame closest in exposure duration wins
292  // Frame with temperature closest to stored temperature wins (if temperature is reported)
293  uint32_t thisMapScore = 0;
294  uint32_t bestCandidateScore = 0;
295 
296  // Else we check for the closest passive temperature
297  if (m_TargetChip->getCCD()->hasCooler())
298  {
299  double temperature = 0;
300  m_TargetChip->getCCD()->getTemperature(&temperature);
301  double diffMap = std::fabs(temperature - map["temperature"].toDouble());
302  double diffBest = std::fabs(temperature - bestCandidate["temperature"].toDouble());
303  // Prefer temperatures closest to target
304  if (diffMap < diffBest)
305  thisMapScore++;
306  else if (diffBest < diffMap)
307  bestCandidateScore++;
308  }
309 
310  // Duration has a higher score priority over temperature
311  {
312  double diffMap = std::fabs(map["duration"].toDouble() - duration);
313  double diffBest = std::fabs(bestCandidate["duration"].toDouble() - duration);
314  if (diffMap < diffBest)
315  thisMapScore += 2;
316  else if (diffBest < diffMap)
317  bestCandidateScore += 2;
318  }
319 
320  // More recent has a higher score than older.
321  {
323  int64_t diffMap = map["timestamp"].toDateTime().secsTo(now);
324  int64_t diffBest = bestCandidate["timestamp"].toDateTime().secsTo(now);
325  if (diffMap < diffBest)
326  thisMapScore += 2;
327  else if (diffBest < diffMap)
328  bestCandidateScore += 2;
329  }
330 
331  // Find candidate with closest time in case we have multiple defect maps
332  if (thisMapScore > bestCandidateScore)
333  bestCandidate = map;
334  }
335  }
336 
337  if (bestCandidate.isEmpty())
338  return false;
339 
340  if (fabs(bestCandidate["duration"].toDouble() - duration) > 3)
341  emit i18n("Using available dark frame with %1 seconds exposure. Please take a dark frame with %1 seconds exposure for more accurate results.",
342  QString::number(bestCandidate["duration"].toDouble(), 'f', 1),
343  QString::number(duration, 'f', 1));
344 
345  QString filename = bestCandidate["filename"].toString();
346 
347  // Finally check if the duration is acceptable
348  QDateTime frameTime = bestCandidate["timestamp"].toDateTime();
349  if (frameTime.daysTo(QDateTime::currentDateTime()) > Options::darkLibraryDuration())
350  {
351  emit i18n("Dark frame %s is expired. Please create new master dark.", filename);
352  return false;
353  }
354 
355  if (m_CachedDarkFrames.contains(filename))
356  {
357  darkData = m_CachedDarkFrames[filename];
358  return true;
359  }
360 
361  // Before adding to cache, clear the cache if memory drops too low.
362  auto memoryMB = KSUtils::getAvailableRAM() / 1e6;
363  if (memoryMB < CACHE_MEMORY_LIMIT)
364  m_CachedDarkFrames.clear();
365 
366  // Finally we made it, let's put it in the hash
367  if (cacheDarkFrameFromFile(filename))
368  {
369  darkData = m_CachedDarkFrames[filename];
370  return true;
371  }
372 
373  // Remove bad dark frame
374  emit newLog(i18n("Removing bad dark frame file %1", filename));
375  m_CachedDarkFrames.remove(filename);
376  QFile::remove(filename);
377  KStarsData::Instance()->userdb()->DeleteDarkFrame(filename);
378  return false;
379 
380 }
381 
382 ///////////////////////////////////////////////////////////////////////////////////////
383 ///
384 ///////////////////////////////////////////////////////////////////////////////////////
385 bool DarkLibrary::findDefectMap(ISD::CameraChip *m_TargetChip, double duration, QSharedPointer<DefectMap> &defectMap)
386 {
387  QVariantMap bestCandidate;
388  for (auto &map : m_DarkFramesDatabaseList)
389  {
390  if (map["defectmap"].toString().isEmpty())
391  continue;
392 
393  // First check CCD name matches and check if we are on the correct chip
394  if (map["ccd"].toString() == m_TargetChip->getCCD()->getDeviceName() &&
395  map["chip"].toInt() == static_cast<int>(m_TargetChip->getType()))
396  {
397  int binX, binY;
398  m_TargetChip->getBinning(&binX, &binY);
399 
400  // Then check if binning is the same
401  if (map["binX"].toInt() == binX && map["binY"].toInt() == binY)
402  {
403  if (bestCandidate.isEmpty())
404  {
405  bestCandidate = map;
406  continue;
407  }
408 
409  // We try to find the best frame
410  // Frame closest in exposure duration wins
411  // Frame with temperature closest to stored temperature wins (if temperature is reported)
412  uint32_t thisMapScore = 0;
413  uint32_t bestCandidateScore = 0;
414 
415  // Else we check for the closest passive temperature
416  if (m_TargetChip->getCCD()->hasCooler())
417  {
418  double temperature = 0;
419  m_TargetChip->getCCD()->getTemperature(&temperature);
420  double diffMap = std::fabs(temperature - map["temperature"].toDouble());
421  double diffBest = std::fabs(temperature - bestCandidate["temperature"].toDouble());
422  // Prefer temperatures closest to target
423  if (diffMap < diffBest)
424  thisMapScore++;
425  else if (diffBest < diffMap)
426  bestCandidateScore++;
427  }
428 
429  // Duration has a higher score priority over temperature
430  double diffMap = std::fabs(map["duration"].toDouble() - duration);
431  double diffBest = std::fabs(bestCandidate["duration"].toDouble() - duration);
432  if (diffMap < diffBest)
433  thisMapScore += 2;
434  else if (diffBest < diffMap)
435  bestCandidateScore += 2;
436 
437  // Find candidate with closest time in case we have multiple defect maps
438  if (thisMapScore > bestCandidateScore)
439  bestCandidate = map;
440  }
441  }
442  }
443 
444 
445  if (bestCandidate.isEmpty())
446  return false;
447 
448 
449  QString darkFilename = bestCandidate["filename"].toString();
450  QString defectFilename = bestCandidate["defectmap"].toString();
451 
452  if (darkFilename.isEmpty() || defectFilename.isEmpty())
453  return false;
454 
455  if (m_CachedDefectMaps.contains(darkFilename))
456  {
457  defectMap = m_CachedDefectMaps[darkFilename];
458  return true;
459  }
460 
461  // Finally we made it, let's put it in the hash
462  if (cacheDefectMapFromFile(darkFilename, defectFilename))
463  {
464  defectMap = m_CachedDefectMaps[darkFilename];
465  return true;
466  }
467  else
468  {
469  // Remove bad dark frame
470  emit newLog(i18n("Failed to load defect map %1", defectFilename));
471  return false;
472  }
473 }
474 
475 ///////////////////////////////////////////////////////////////////////////////////////
476 ///
477 ///////////////////////////////////////////////////////////////////////////////////////
478 bool DarkLibrary::cacheDefectMapFromFile(const QString &key, const QString &filename)
479 {
481  oneMap.reset(new DefectMap());
482 
483  if (oneMap->load(filename))
484  {
485  oneMap->filterPixels();
486  m_CachedDefectMaps[key] = oneMap;
487  return true;
488  }
489 
490  emit newLog(i18n("Failed to load defect map file %1", filename));
491  return false;
492 }
493 
494 ///////////////////////////////////////////////////////////////////////////////////////
495 ///
496 ///////////////////////////////////////////////////////////////////////////////////////
497 bool DarkLibrary::cacheDarkFrameFromFile(const QString &filename)
498 {
499  QFuture<bool> rc = m_CurrentDarkFrame->loadFromFile(filename);
500 
501  rc.waitForFinished();
502  if (rc.result())
503  m_CachedDarkFrames[filename] = m_CurrentDarkFrame;
504  else
505  {
506  emit newLog(i18n("Failed to load dark frame file %1", filename));
507  }
508 
509  return rc;
510 }
511 
512 ///////////////////////////////////////////////////////////////////////////////////////
513 ///
514 ///////////////////////////////////////////////////////////////////////////////////////
515 void DarkLibrary::processNewImage(SequenceJob *job, const QSharedPointer<FITSData> &data)
516 {
517  Q_UNUSED(data)
518  if (job->getStatus() == JOB_IDLE)
519  return;
520 
521  if (job->getCompleted() == job->getCoreProperty(SequenceJob::SJ_Count).toInt())
522  {
523  QJsonObject metadata
524  {
525  {"camera", m_CurrentCamera->getDeviceName()},
526  {"chip", m_TargetChip->getType()},
527  {"binx", job->getCoreProperty(SequenceJob::SJ_Binning).toPoint().x()},
528  {"biny", job->getCoreProperty(SequenceJob::SJ_Binning).toPoint().y()},
529  {"duration", job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble()}
530  };
531 
532  // Record temperature
533  double value = 0;
534  bool success = m_CurrentCamera->getTemperature(&value);
535  if (success)
536  metadata["temperature"] = value;
537 
538  success = m_CurrentCamera->hasGain() && m_CurrentCamera->getGain(&value);
539  if (success)
540  metadata["gain"] = value;
541 
542  QString isoValue;
543  success = m_TargetChip->getISOValue(isoValue);
544  if (success)
545  metadata["iso"] = isoValue;
546 
547  metadata["count"] = job->getCoreProperty(SequenceJob::SJ_Count).toInt();
548  generateMasterFrame(m_CurrentDarkFrame, metadata);
549  reloadDarksFromDatabase();
550  populateMasterMetedata();
551  }
552 }
553 
554 ///////////////////////////////////////////////////////////////////////////////////////
555 ///
556 ///////////////////////////////////////////////////////////////////////////////////////
557 void DarkLibrary::processNewBLOB(IBLOB *bp)
558 {
559  QByteArray buffer = QByteArray::fromRawData(reinterpret_cast<char *>(bp->blob), bp->size);
560  if (!m_CurrentDarkFrame->loadFromBuffer(buffer, "fits"))
561  {
562  m_FileLabel->setText(i18n("Failed to process dark data."));
563  return;
564  }
565 
566  if (!m_DarkView->loadData(m_CurrentDarkFrame))
567  {
568  m_FileLabel->setText(i18n("Failed to load dark data."));
569  return;
570  }
571 
572  uint32_t totalElements = m_CurrentDarkFrame->channels() * m_CurrentDarkFrame->samplesPerChannel();
573  if (totalElements != m_DarkMasterBuffer.size())
574  m_DarkMasterBuffer.assign(totalElements, 0);
575 
576  aggregate(m_CurrentDarkFrame);
577  darkProgress->setValue(darkProgress->value() + 1);
578  m_StatusLabel->setText(i18n("Received %1/%2 images.", darkProgress->value(), darkProgress->maximum()));
579 }
580 
581 ///////////////////////////////////////////////////////////////////////////////////////
582 ///
583 ///////////////////////////////////////////////////////////////////////////////////////
584 void DarkLibrary::Release()
585 {
586  delete (_DarkLibrary);
587  _DarkLibrary = nullptr;
588 
589  // m_Cameras.clear();
590  // cameraS->clear();
591  // m_CurrentCamera = nullptr;
592 }
593 
594 ///////////////////////////////////////////////////////////////////////////////////////
595 ///
596 ///////////////////////////////////////////////////////////////////////////////////////
597 void DarkLibrary::closeEvent(QCloseEvent *ev)
598 {
599  Q_UNUSED(ev)
600  Options::setUseFITSViewer(m_RememberFITSViewer);
601  Options::setUseSummaryPreview(m_RememberSummaryView);
602  if (m_JobsGenerated)
603  {
604  m_JobsGenerated = false;
605  m_CaptureModule->clearSequenceQueue();
606  m_CaptureModule->setPresetSettings(m_PresetSettings);
607  m_CaptureModule->setFileSettings(m_FileSettings);
608  }
609 }
610 
611 ///////////////////////////////////////////////////////////////////////////////////////
612 ///
613 ///////////////////////////////////////////////////////////////////////////////////////
614 void DarkLibrary::setCompleted()
615 {
616  startB->setEnabled(true);
617  stopB->setEnabled(false);
618 
619  Options::setUseFITSViewer(m_RememberFITSViewer);
620  Options::setUseSummaryPreview(m_RememberSummaryView);
621  if (m_JobsGenerated)
622  {
623  m_JobsGenerated = false;
624  m_CaptureModule->clearSequenceQueue();
625  m_CaptureModule->setPresetSettings(m_PresetSettings);
626  m_CaptureModule->setFileSettings(m_FileSettings);
627  }
628 
629  m_CurrentCamera->disconnect(this);
630  m_CaptureModule->disconnect(this);
631 }
632 
633 ///////////////////////////////////////////////////////////////////////////////////////
634 ///
635 ///////////////////////////////////////////////////////////////////////////////////////
636 void DarkLibrary::clearExpired()
637 {
638  if (darkFramesModel->rowCount() == 0)
639  return;
640 
641  // Anything before this must go
642  QDateTime expiredDate = QDateTime::currentDateTime().addDays(kcfg_DarkLibraryDuration->value() * -1);
643 
644  QSqlDatabase userdb = QSqlDatabase::database("userdb");
645  userdb.open();
646  QSqlTableModel darkframe(nullptr, userdb);
647  darkframe.setEditStrategy(QSqlTableModel::OnManualSubmit);
648  darkframe.setTable("darkframe");
649  // Select all those that already expired.
650  darkframe.setFilter("ccd LIKE \'" + m_CurrentCamera->getDeviceName() + "\' AND timestamp < \'" + expiredDate.toString(
651  Qt::ISODate) + "\'");
652 
653  darkframe.select();
654 
655  // Now remove all the expired files from disk
656  for (int i = 0; i < darkframe.rowCount(); ++i)
657  {
658  QString oneFile = darkframe.record(i).value("filename").toString();
659  QFile::remove(oneFile);
660  QString defectMap = darkframe.record(i).value("defectmap").toString();
661  if (defectMap.isEmpty() == false)
662  QFile::remove(defectMap);
663 
664  }
665 
666  // And remove them from the database
667  darkframe.removeRows(0, darkframe.rowCount());
668  darkframe.submitAll();
669  userdb.close();
670 
671  Ekos::DarkLibrary::Instance()->refreshFromDB();
672 
673  reloadDarksFromDatabase();
674 }
675 
676 ///////////////////////////////////////////////////////////////////////////////////////
677 ///
678 ///////////////////////////////////////////////////////////////////////////////////////
679 void DarkLibrary::clearBuffers()
680 {
681  m_CurrentDarkFrame.clear();
682  // Should clear existing view
683  m_CurrentDarkFrame.reset(new FITSData(), &QObject::deleteLater);
684  m_DarkView->clearData();
685  m_CurrentDefectMap.clear();
686 
687 }
688 ///////////////////////////////////////////////////////////////////////////////////////
689 ///
690 ///////////////////////////////////////////////////////////////////////////////////////
691 void DarkLibrary::clearAll()
692 {
693  if (darkFramesModel->rowCount() == 0)
694  return;
695 
697  i18n("Are you sure you want to delete all dark frames images and data?")) ==
698  KMessageBox::No)
699  return;
700 
701  QSqlDatabase userdb = QSqlDatabase::database("userdb");
702  userdb.open();
703  QSqlTableModel darkframe(nullptr, userdb);
704  darkFramesModel->setEditStrategy(QSqlTableModel::OnManualSubmit);
705  darkframe.setTable("darkframe");
706  darkframe.setFilter("ccd LIKE \'" + m_CurrentCamera->getDeviceName() + "\'");
707  darkFramesModel->select();
708 
709  // Now remove all the expired files from disk
710  for (int i = 0; i < darkframe.rowCount(); ++i)
711  {
712  QString oneFile = darkframe.record(i).value("filename").toString();
713  QFile::remove(oneFile);
714  QString defectMap = darkframe.record(i).value("defectmap").toString();
715  if (defectMap.isEmpty() == false)
716  QFile::remove(defectMap);
717 
718  }
719 
720  darkFramesModel->removeRows(0, darkFramesModel->rowCount());
721  darkFramesModel->submitAll();
722  userdb.close();
723 
724  Ekos::DarkLibrary::Instance()->refreshFromDB();
725 
726  // Refesh db entries for other cameras
727  reloadDarksFromDatabase();
728 }
729 
730 ///////////////////////////////////////////////////////////////////////////////////////
731 ///
732 ///////////////////////////////////////////////////////////////////////////////////////
733 void DarkLibrary::clearRow(int index)
734 {
735  QSqlDatabase userdb = QSqlDatabase::database("userdb");
736  if (index < 0)
737  index = darkTableView->currentIndex().row();
738 
739  QSqlRecord record = darkFramesModel->record(index);
740  QString filename = record.value("filename").toString();
741  QString defectMap = record.value("defectmap").toString();
742  QFile::remove(filename);
743  if (!defectMap.isEmpty())
744  QFile::remove(defectMap);
745 
746  userdb.open();
747 
748  darkFramesModel->removeRow(index);
749  darkFramesModel->submitAll();
750  userdb.close();
751 
752  darkTableView->selectionModel()->select(darkFramesModel->index(index - 1, 0), QItemSelectionModel::ClearAndSelect);
753 
754  refreshFromDB();
755  reloadDarksFromDatabase();
756 }
757 
758 ///////////////////////////////////////////////////////////////////////////////////////
759 ///
760 ///////////////////////////////////////////////////////////////////////////////////////
761 void DarkLibrary::openDarksFolder()
762 {
763  QString darkFilesPath = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("darks");
764 
766 }
767 
768 ///////////////////////////////////////////////////////////////////////////////////////
769 ///
770 ///////////////////////////////////////////////////////////////////////////////////////
771 void DarkLibrary::refreshDefectMastersList(const QString &camera)
772 {
773  if (darkFramesModel->rowCount() == 0)
774  return;
775 
776  masterDarksCombo->blockSignals(true);
777  masterDarksCombo->clear();
778 
779  for (int i = 0; i < darkFramesModel->rowCount(); ++i)
780  {
781  QSqlRecord record = darkFramesModel->record(i);
782 
783  if (record.value("ccd") != camera)
784  continue;
785 
786  auto binX = record.value("binX").toInt();
787  auto binY = record.value("binY").toInt();
788  auto temperature = record.value("temperature").toDouble();
789  auto duration = record.value("duration").toDouble();
790  auto gain = record.value("gain").toInt();
791  auto iso = record.value("iso").toString();
792  QString ts = record.value("timestamp").toString();
793 
794  QString entry = QString("%1 secs %2x%3")
795  .arg(QString::number(duration, 'f', 1))
796  .arg(QString::number(binX))
797  .arg(QString::number(binY));
798 
799  if (temperature > INVALID_VALUE)
800  entry.append(QString(" @ %1°").arg(QString::number(temperature, 'f', 1)));
801 
802  if (gain >= 0)
803  entry.append(QString(" G %1").arg(gain));
804  if (!iso.isEmpty())
805  entry.append(QString(" ISO %1").arg(iso));
806 
807  masterDarksCombo->addItem(entry);
808  }
809 
810  masterDarksCombo->blockSignals(false);
811 
812  //loadDefectMap();
813 
814 }
815 ///////////////////////////////////////////////////////////////////////////////////////
816 ///
817 ///////////////////////////////////////////////////////////////////////////////////////
818 void DarkLibrary::reloadDarksFromDatabase()
819 {
820  QSqlDatabase userdb = QSqlDatabase::database("userdb");
821  userdb.open();
822 
823  const QString camera = m_CurrentCamera->getDeviceName();
824 
825  delete (darkFramesModel);
826  delete (sortFilter);
827 
828  darkFramesModel = new QSqlTableModel(this, userdb);
829  darkFramesModel->setTable("darkframe");
830  darkFramesModel->setFilter(QString("ccd='%1'").arg(camera));
831  darkFramesModel->select();
832 
833  sortFilter = new QSortFilterProxyModel(this);
834  sortFilter->setSourceModel(darkFramesModel);
835  sortFilter->sort (0);
836  darkTableView->setModel (sortFilter);
837 
838  //darkTableView->setModel(darkFramesModel);
839  // Hide ID
840  darkTableView->hideColumn(0);
841  // Hide Chip
842  darkTableView->hideColumn(2);
843 
844  userdb.close();
845 
846  if (darkFramesModel->rowCount() == 0 && m_CurrentDarkFrame)
847  {
848  clearBuffers();
849  return;
850  }
851 
852  refreshDefectMastersList(camera);
853  loadCurrentMasterDark(camera);
854 }
855 
856 ///////////////////////////////////////////////////////////////////////////////////////
857 ///
858 ///////////////////////////////////////////////////////////////////////////////////////
859 void DarkLibrary::loadCurrentMasterDark(const QString &camera, int masterIndex)
860 {
861  // Do not process empty models
862  if (darkFramesModel->rowCount() == 0)
863  return;
864 
865  if (masterIndex == -1)
866  masterIndex = masterDarksCombo->currentIndex();
867 
868  if (masterIndex < 0 || masterIndex >= darkFramesModel->rowCount())
869  return;
870 
871  QSqlRecord record = darkFramesModel->record(masterIndex);
872  if (record.value("ccd") != camera)
873  return;
874  // Get the master dark frame file name
875  m_MasterDarkFrameFilename = record.value("filename").toString();
876 
877  if (m_MasterDarkFrameFilename.isEmpty())
878  return;
879 
880  // Get defect file name as well if available.
881  m_DefectMapFilename = record.value("defectmap").toString();
882 
883  // If current dark frame is different from target filename, then load from file
884  if (m_CurrentDarkFrame->filename() != m_MasterDarkFrameFilename)
885  m_DarkFrameFutureWatcher.setFuture(m_CurrentDarkFrame->loadFromFile(m_MasterDarkFrameFilename));
886  // If current dark frame is the same one loaded, then check if we need to reload defect map
887  else
888  loadCurrentMasterDefectMap();
889 }
890 
891 ///////////////////////////////////////////////////////////////////////////////////////
892 ///
893 ///////////////////////////////////////////////////////////////////////////////////////
894 void DarkLibrary::loadCurrentMasterDefectMap()
895 {
896  // Find if we have an existing map
897  if (m_CachedDefectMaps.contains(m_MasterDarkFrameFilename))
898  {
899  if (m_CurrentDefectMap != m_CachedDefectMaps.value(m_MasterDarkFrameFilename))
900  {
901  m_CurrentDefectMap = m_CachedDefectMaps.value(m_MasterDarkFrameFilename);
902  m_DarkView->setDefectMap(m_CurrentDefectMap);
903  m_CurrentDefectMap->setDarkData(m_CurrentDarkFrame);
904  }
905  }
906  // Create new defect map
907  else
908  {
909  m_CurrentDefectMap.reset(new DefectMap());
910  connect(m_CurrentDefectMap.data(), &DefectMap::pixelsUpdated, this, [this](uint32_t hot, uint32_t cold)
911  {
912  hotPixelsCount->setValue(hot);
913  coldPixelsCount->setValue(cold);
914  aggresivenessHotSlider->setValue(m_CurrentDefectMap->property("HotPixelAggressiveness").toInt());
915  aggresivenessColdSlider->setValue(m_CurrentDefectMap->property("ColdPixelAggressiveness").toInt());
916  });
917 
918  if (!m_DefectMapFilename.isEmpty())
919  cacheDefectMapFromFile(m_MasterDarkFrameFilename, m_DefectMapFilename);
920 
921  m_DarkView->setDefectMap(m_CurrentDefectMap);
922  m_CurrentDefectMap->setDarkData(m_CurrentDarkFrame);
923  }
924 }
925 
926 ///////////////////////////////////////////////////////////////////////////////////////
927 ///
928 ///////////////////////////////////////////////////////////////////////////////////////
929 void DarkLibrary::populateMasterMetedata()
930 {
931  if (m_CurrentDarkFrame.isNull())
932  return;
933 
934  QVariant value;
935  // TS
936  if (m_CurrentDarkFrame->getRecordValue("DATE-OBS", value))
937  masterTime->setText(value.toString());
938  // Temperature
939  if (m_CurrentDarkFrame->getRecordValue("CCD-TEMP", value) && value.toDouble() < 100)
940  masterTemperature->setText(QString::number(value.toDouble(), 'f', 1));
941  // Exposure
942  if (m_CurrentDarkFrame->getRecordValue("EXPTIME", value))
943  masterExposure->setText(value.toString());
944  // Median
945  {
946  double median = m_CurrentDarkFrame->getAverageMedian();
947  if (median > 0)
948  masterMedian->setText(QString::number(median, 'f', 1));
949  }
950  // Mean
951  {
952  double mean = m_CurrentDarkFrame->getAverageMean();
953  masterMean->setText(QString::number(mean, 'f', 1));
954  }
955  // Standard Deviation
956  {
957  double stddev = m_CurrentDarkFrame->getAverageStdDev();
958  masterDeviation->setText(QString::number(stddev, 'f', 1));
959  }
960 }
961 
962 ///////////////////////////////////////////////////////////////////////////////////////
963 ///
964 ///////////////////////////////////////////////////////////////////////////////////////
965 ///////////////////////////////////////////////////////////////////////////////////////
966 ///
967 ///////////////////////////////////////////////////////////////////////////////////////
968 void DarkLibrary::loadIndexInView(int row)
969 {
970  QSqlRecord record = darkFramesModel->record(row);
971  QString filename = record.value("filename").toString();
972  // Avoid duplicate loads
973  if (m_DarkView->imageData().isNull() || m_DarkView->imageData()->filename() != filename)
974  m_DarkView->loadFile(filename);
975 }
976 
977 ///////////////////////////////////////////////////////////////////////////////////////
978 ///
979 ///////////////////////////////////////////////////////////////////////////////////////
980 void DarkLibrary::addCamera(ISD::Camera * camera)
981 {
982  m_Cameras.append(camera);
983  cameraS->addItem(camera->getDeviceName());
984 
985  checkCamera();
986 
987  reloadDarksFromDatabase();
988 }
989 
990 ///////////////////////////////////////////////////////////////////////////////////////
991 ///
992 ///////////////////////////////////////////////////////////////////////////////////////
993 void DarkLibrary::removeDevice(ISD::GenericDevice * device)
994 {
995  for (auto &oneCamera : m_Cameras)
996  {
997  if (oneCamera->getDeviceName() == device->getDeviceName())
998  {
999  m_Cameras.removeOne(oneCamera);
1000  cameraS->removeItem(cameraS->findText(oneCamera->getDeviceName()));
1001  }
1002  }
1003 }
1004 
1005 ///////////////////////////////////////////////////////////////////////////////////////
1006 ///
1007 ///////////////////////////////////////////////////////////////////////////////////////
1008 void DarkLibrary::checkCamera(int ccdNum)
1009 {
1010  if (ccdNum == -1)
1011  {
1012  ccdNum = cameraS->currentIndex();
1013 
1014  if (ccdNum == -1)
1015  return;
1016  }
1017 
1018  if (ccdNum < m_Cameras.count())
1019  {
1020  // Check whether main camera or guide head only
1021  m_CurrentCamera = m_Cameras.at(ccdNum);
1022 
1023  const QString device = m_CurrentCamera->getDeviceName();
1024  if (m_DarkCameras.contains(device))
1025  preferDarksRadio->setChecked(true);
1026  else if (m_DefectCameras.contains(device))
1027  preferDefectsRadio->setChecked(true);
1028 
1029  m_TargetChip = nullptr;
1030  if (cameraS->itemText(ccdNum).right(6) == QString("Guider"))
1031  {
1032  m_UseGuideHead = true;
1033  m_TargetChip = m_CurrentCamera->getChip(ISD::CameraChip::GUIDE_CCD);
1034  }
1035 
1036  if (m_TargetChip == nullptr)
1037  {
1038  m_UseGuideHead = false;
1039  m_TargetChip = m_CurrentCamera->getChip(ISD::CameraChip::PRIMARY_CCD);
1040  }
1041 
1042  // Make sure we have a valid chip and valid base device.
1043  // Make sure we are not in capture process.
1044  if (!m_TargetChip || !m_TargetChip->getCCD() || m_TargetChip->isCapturing())
1045  return;
1046 
1047  if (m_CurrentCamera->hasCoolerControl())
1048  {
1049  temperatureLabel->setEnabled(true);
1050  temperatureStepLabel->setEnabled(true);
1051  temperatureToLabel->setEnabled(true);
1052  temperatureStepSpin->setEnabled(true);
1053  minTemperatureSpin->setEnabled(true);
1054  maxTemperatureSpin->setEnabled(true);
1055 
1056  }
1057  else
1058  {
1059  temperatureLabel->setEnabled(false);
1060  temperatureStepLabel->setEnabled(false);
1061  temperatureToLabel->setEnabled(false);
1062  temperatureStepSpin->setEnabled(false);
1063  minTemperatureSpin->setEnabled(false);
1064  maxTemperatureSpin->setEnabled(false);
1065  }
1066 
1067  QStringList isoList = m_TargetChip->getISOList();
1068  captureISOS->blockSignals(true);
1069  captureISOS->clear();
1070 
1071  // No ISO range available
1072  if (isoList.isEmpty())
1073  {
1074  captureISOS->setEnabled(false);
1075  }
1076  else
1077  {
1078  captureISOS->setEnabled(true);
1079  captureISOS->addItems(isoList);
1080  captureISOS->setCurrentIndex(m_TargetChip->getISOIndex());
1081  }
1082  captureISOS->blockSignals(false);
1083 
1084  // Gain Check
1085  if (m_CurrentCamera->hasGain())
1086  {
1087  double min, max, step, value, targetCustomGain;
1088  m_CurrentCamera->getGainMinMaxStep(&min, &max, &step);
1089 
1090  // Allow the possibility of no gain value at all.
1091  GainSpinSpecialValue = min - step;
1092  captureGainN->setRange(GainSpinSpecialValue, max);
1093  captureGainN->setSpecialValueText(i18n("--"));
1094  captureGainN->setEnabled(true);
1095  captureGainN->setSingleStep(step);
1096  m_CurrentCamera->getGain(&value);
1097 
1098  targetCustomGain = getGain();
1099 
1100  // Set the custom gain if we have one
1101  // otherwise it will not have an effect.
1102  if (targetCustomGain > 0)
1103  captureGainN->setValue(targetCustomGain);
1104  else
1105  captureGainN->setValue(GainSpinSpecialValue);
1106 
1107  captureGainN->setReadOnly(m_CurrentCamera->getGainPermission() == IP_RO);
1108  }
1109  else
1110  captureGainN->setEnabled(false);
1111 
1112  countDarkTotalTime();
1113  }
1114 }
1115 
1116 ///////////////////////////////////////////////////////////////////////////////////////
1117 ///
1118 ///////////////////////////////////////////////////////////////////////////////////////
1119 void DarkLibrary::countDarkTotalTime()
1120 {
1121 
1122  //double exposureCount = (maxExposureSpin->value() - minExposureSpin->value()) / exposureStepSin->value();
1123  double temperatureCount = 1;
1124  if (m_CurrentCamera->hasCoolerControl() && std::abs(maxTemperatureSpin->value() - minTemperatureSpin->value()) > 0)
1125  temperatureCount = (std::abs((maxTemperatureSpin->value() - minTemperatureSpin->value())) / temperatureStepSpin->value()) +
1126  1;
1127  int binnings = 0;
1128  if (bin1Check->isChecked())
1129  binnings++;
1130  if (bin2Check->isChecked())
1131  binnings++;
1132  if (bin4Check->isChecked())
1133  binnings++;
1134 
1135  double darkTime = 0;
1136  int imagesCount = 0;
1137  for (double startExposure = minExposureSpin->value(); startExposure <= maxExposureSpin->value();
1138  startExposure += exposureStepSin->value())
1139  {
1140  darkTime += startExposure * temperatureCount * binnings * countSpin->value();
1141  imagesCount += temperatureCount * binnings * countSpin->value();
1142  }
1143 
1144  totalTime->setText(QString::number(darkTime / 60.0, 'f', 1));
1145  totalImages->setText(QString::number(imagesCount));
1146  darkProgress->setMaximum(imagesCount);
1147 
1148 }
1149 
1150 ///////////////////////////////////////////////////////////////////////////////////////
1151 ///
1152 ///////////////////////////////////////////////////////////////////////////////////////
1153 void DarkLibrary::generateDarkJobs()
1154 {
1155  // Always clear sequence queue before starting
1156  m_CaptureModule->clearSequenceQueue();
1157 
1158  if (m_JobsGenerated == false)
1159  {
1160  m_JobsGenerated = true;
1161  m_PresetSettings = m_CaptureModule->getPresetSettings();
1162  m_FileSettings = m_CaptureModule->getFileSettings();
1163  }
1164 
1165  QList<double> temperatures;
1166  if (m_CurrentCamera->hasCoolerControl() && std::fabs(maxTemperatureSpin->value() - minTemperatureSpin->value()) >= 0)
1167  {
1168  for (double oneTemperature = minTemperatureSpin->value(); oneTemperature <= maxTemperatureSpin->value();
1169  oneTemperature += temperatureStepSpin->value())
1170  {
1171  temperatures << oneTemperature;
1172  }
1173 
1174  // Enforce temperature set
1175  m_CaptureModule->setForceTemperature(true);
1176  }
1177  else
1178  {
1179  // Disable temperature set
1180  m_CaptureModule->setForceTemperature(false);
1181  temperatures << INVALID_VALUE;
1182  }
1183 
1184  QList<uint8_t> bins;
1185  if (bin1Check->isChecked())
1186  bins << 1;
1187  if (bin2Check->isChecked())
1188  bins << 2;
1189  if (bin4Check->isChecked())
1190  bins << 4;
1191 
1192  QList<double> exposures;
1193  for (double oneExposure = minExposureSpin->value(); oneExposure <= maxExposureSpin->value();
1194  oneExposure += exposureStepSin->value())
1195  {
1196  exposures << oneExposure;
1197  }
1198 
1200  QDir::separator();
1201 
1202 
1203  int sequence = 0;
1204  for (const auto oneTemperature : qAsConst(temperatures))
1205  {
1206  for (const auto &oneExposure : qAsConst(exposures))
1207  {
1208  for (const auto &oneBin : qAsConst(bins))
1209  {
1210  sequence++;
1211 
1212  QJsonObject settings;
1213 
1214  settings["camera"] = cameraS->currentText();
1215  settings["exp"] = oneExposure;
1216  settings["bin"] = oneBin;
1217  settings["frameType"] = FRAME_DARK;
1218  settings["temperature"] = oneTemperature;
1219  if (captureGainN->isEnabled())
1220  settings["gain"] = captureGainN->value();
1221  if (captureISOS->isEnabled())
1222  settings["iso"] = captureISOS->currentIndex();
1223  settings["transferFormat"] = 0;
1224 
1225  QString directory = prefix + QString("sequence_%1").arg(sequence);
1226  QJsonObject fileSettings;
1227 
1228  fileSettings["directory"] = directory;
1229  m_CaptureModule->setPresetSettings(settings);
1230  m_CaptureModule->setFileSettings(fileSettings);
1231  m_CaptureModule->setCount(countSpin->value());
1232  m_CaptureModule->addJob();
1233  }
1234  }
1235  }
1236 }
1237 
1238 ///////////////////////////////////////////////////////////////////////////////////////
1239 ///
1240 ///////////////////////////////////////////////////////////////////////////////////////
1241 void DarkLibrary::execute()
1242 {
1243  m_DarkImagesCounter = 0;
1244  darkProgress->setValue(0);
1245  darkProgress->setTextVisible(true);
1246  connect(m_CaptureModule, &Capture::newImage, this, &DarkLibrary::processNewImage, Qt::UniqueConnection);
1247  connect(m_CaptureModule, &Capture::newStatus, this, &DarkLibrary::setCaptureState, Qt::UniqueConnection);
1248  connect(m_CurrentCamera, &ISD::Camera::BLOBUpdated, this, &DarkLibrary::processNewBLOB, Qt::UniqueConnection);
1249 
1250  Options::setUseFITSViewer(false);
1251  Options::setUseSummaryPreview(false);
1252  startB->setEnabled(false);
1253  stopB->setEnabled(true);
1254  m_DarkView->reset();
1255  m_StatusLabel->setText(i18n("In progress..."));
1256  m_CaptureModule->start();
1257 
1258 }
1259 
1260 ///////////////////////////////////////////////////////////////////////////////////////
1261 ///
1262 ///////////////////////////////////////////////////////////////////////////////////////
1263 void DarkLibrary::stop()
1264 {
1265  m_CaptureModule->abort();
1266  darkProgress->setValue(0);
1267  m_DarkView->reset();
1268 }
1269 
1270 ///////////////////////////////////////////////////////////////////////////////////////
1271 ///
1272 ///////////////////////////////////////////////////////////////////////////////////////
1273 void DarkLibrary::initView()
1274 {
1275  m_DarkView.reset(new DarkView(darkWidget));
1276  m_DarkView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1277  m_DarkView->setBaseSize(darkWidget->size());
1278  m_DarkView->createFloatingToolBar();
1279  QVBoxLayout *vlayout = new QVBoxLayout();
1280  vlayout->addWidget(m_DarkView.get());
1281  darkWidget->setLayout(vlayout);
1282  connect(m_DarkView.get(), &DarkView::loaded, this, [this]()
1283  {
1284  emit newImage(m_DarkView->imageData());
1285  });
1286 }
1287 
1288 ///////////////////////////////////////////////////////////////////////////////////////
1289 ///
1290 ///////////////////////////////////////////////////////////////////////////////////////
1291 void DarkLibrary::aggregate(const QSharedPointer<FITSData> &data)
1292 {
1293  switch (data->dataType())
1294  {
1295  case TBYTE:
1296  aggregateInternal<uint8_t>(data);
1297  break;
1298 
1299  case TSHORT:
1300  aggregateInternal<int16_t>(data);
1301  break;
1302 
1303  case TUSHORT:
1304  aggregateInternal<uint16_t>(data);
1305  break;
1306 
1307  case TLONG:
1308  aggregateInternal<int32_t>(data);
1309  break;
1310 
1311  case TULONG:
1312  aggregateInternal<uint32_t>(data);
1313  break;
1314 
1315  case TFLOAT:
1316  aggregateInternal<float>(data);
1317  break;
1318 
1319  case TLONGLONG:
1320  aggregateInternal<int64_t>(data);
1321  break;
1322 
1323  case TDOUBLE:
1324  aggregateInternal<double>(data);
1325  break;
1326 
1327  default:
1328  break;
1329  }
1330 }
1331 
1332 ///////////////////////////////////////////////////////////////////////////////////////
1333 ///
1334 ///////////////////////////////////////////////////////////////////////////////////////
1335 template <typename T>
1336 void DarkLibrary::aggregateInternal(const QSharedPointer<FITSData> &data)
1337 {
1338  T const *darkBuffer = reinterpret_cast<T const*>(data->getImageBuffer());
1339  for (uint32_t i = 0; i < m_DarkMasterBuffer.size(); i++)
1340  m_DarkMasterBuffer[i] += darkBuffer[i];
1341 }
1342 
1343 ///////////////////////////////////////////////////////////////////////////////////////
1344 ///
1345 ///////////////////////////////////////////////////////////////////////////////////////
1346 void DarkLibrary::generateMasterFrame(const QSharedPointer<FITSData> &data, const QJsonObject &metadata)
1347 {
1348  switch (data->dataType())
1349  {
1350  case TBYTE:
1351  generateMasterFrameInternal<uint8_t>(data, metadata);
1352  break;
1353 
1354  case TSHORT:
1355  generateMasterFrameInternal<int16_t>(data, metadata);
1356  break;
1357 
1358  case TUSHORT:
1359  generateMasterFrameInternal<uint16_t>(data, metadata);
1360  break;
1361 
1362  case TLONG:
1363  generateMasterFrameInternal<int32_t>(data, metadata);
1364  break;
1365 
1366  case TULONG:
1367  generateMasterFrameInternal<uint32_t>(data, metadata);
1368  break;
1369 
1370  case TFLOAT:
1371  generateMasterFrameInternal<float>(data, metadata);
1372  break;
1373 
1374  case TLONGLONG:
1375  generateMasterFrameInternal<int64_t>(data, metadata);
1376  break;
1377 
1378  case TDOUBLE:
1379  generateMasterFrameInternal<double>(data, metadata);
1380  break;
1381 
1382  default:
1383  break;
1384  }
1385 
1386  emit newImage(data);
1387  // Reset Master Buffer
1388  m_DarkMasterBuffer.assign(m_DarkMasterBuffer.size(), 0);
1389 
1390 }
1391 
1392 ///////////////////////////////////////////////////////////////////////////////////////
1393 ///
1394 ///////////////////////////////////////////////////////////////////////////////////////
1395 template <typename T> void DarkLibrary::generateMasterFrameInternal(const QSharedPointer<FITSData> &data,
1396  const QJsonObject &metadata)
1397 {
1398  T *writableBuffer = reinterpret_cast<T *>(data->getWritableImageBuffer());
1399  const uint32_t count = metadata["count"].toInt();
1400  // Average the values
1401  for (uint32_t i = 0; i < m_DarkMasterBuffer.size(); i++)
1402  writableBuffer[i] = m_DarkMasterBuffer[i] / count;
1403 
1404  QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss");
1405  QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("darks/darkframe_" + ts +
1406  ".fits");
1407 
1408  data->calculateStats(true);
1409  if (!data->saveImage(path))
1410  {
1411  m_FileLabel->setText(i18n("Failed to save master frame: %1", data->getLastError()));
1412  return;
1413  }
1414 
1415  auto memoryMB = KSUtils::getAvailableRAM() / 1e6;
1416  if (memoryMB > CACHE_MEMORY_LIMIT)
1417  m_CachedDarkFrames[path] = data;
1418 
1419  QVariantMap map;
1420  map["ccd"] = metadata["camera"].toString();
1421  map["chip"] = metadata["chip"].toInt();
1422  map["binX"] = metadata["binx"].toInt();
1423  map["binY"] = metadata["biny"].toInt();
1424  map["temperature"] = metadata["temperature"].toDouble(INVALID_VALUE);
1425  map["gain"] = metadata["gain"].toInt(-1);
1426  map["iso"] = metadata["iso"].toString();
1427  map["duration"] = metadata["duration"].toDouble();
1428  map["filename"] = path;
1430 
1431  m_DarkFramesDatabaseList.append(map);
1432  m_FileLabel->setText(i18n("Master Dark saved to %1", path));
1433  KStarsData::Instance()->userdb()->AddDarkFrame(map);
1434 }
1435 
1436 ///////////////////////////////////////////////////////////////////////////////////////
1437 ///
1438 ///////////////////////////////////////////////////////////////////////////////////////
1439 void DarkLibrary::setCaptureModule(Capture *instance)
1440 {
1441  m_CaptureModule = instance;
1442 }
1443 
1444 ///////////////////////////////////////////////////////////////////////////////////////
1445 ///
1446 ///////////////////////////////////////////////////////////////////////////////////////
1447 void DarkLibrary::setCaptureState(CaptureState state)
1448 {
1449  switch (state)
1450  {
1451  case CAPTURE_ABORTED:
1452  setCompleted();
1453  m_StatusLabel->setText(i18n("Capture aborted."));
1454  break;
1455  case CAPTURE_COMPLETE:
1456  setCompleted();
1457  m_StatusLabel->setText(i18n("Capture completed."));
1458  break;
1459  default:
1460  break;
1461  }
1462 }
1463 
1464 ///////////////////////////////////////////////////////////////////////////////////////
1465 ///
1466 ///////////////////////////////////////////////////////////////////////////////////////
1467 void DarkLibrary::saveDefectMap()
1468 {
1469  if (!m_CurrentDarkFrame)
1470  return;
1471 
1472  QString filename = m_CurrentDefectMap->filename();
1473  bool newFile = false;
1474  if (filename.isEmpty())
1475  {
1476  QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss");
1477  filename = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("defectmaps/defectmap_" + ts +
1478  ".json");
1479  newFile = true;
1480  }
1481 
1482  if (m_CurrentDefectMap->save(filename, m_CurrentCamera->getDeviceName()))
1483  {
1484  m_FileLabel->setText(i18n("Defect map saved to %1", filename));
1485 
1486  if (newFile)
1487  {
1488  auto currentMap = std::find_if(m_DarkFramesDatabaseList.begin(),
1489  m_DarkFramesDatabaseList.end(), [&](const QVariantMap & oneMap)
1490  {
1491  return oneMap["filename"].toString() == m_CurrentDarkFrame->filename();
1492  });
1493 
1494  if (currentMap != m_DarkFramesDatabaseList.end())
1495  {
1496  (*currentMap)["defectmap"] = filename;
1497  (*currentMap)["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate);
1498  KStarsData::Instance()->userdb()->UpdateDarkFrame(*currentMap);
1499  }
1500  }
1501  }
1502  else
1503  {
1504  m_FileLabel->setText(i18n("Failed to save defect map to %1", filename));
1505  }
1506 }
1507 
1508 ///////////////////////////////////////////////////////////////////////////////////////
1509 ///
1510 ///////////////////////////////////////////////////////////////////////////////////////
1511 void DarkLibrary::start()
1512 {
1513  generateDarkJobs();
1514  execute();
1515 }
1516 
1517 void DarkLibrary::setDarkSettings(const QJsonObject &settings)
1518 {
1519  const auto camera = settings["camera"].toString();
1520  const auto minExposure = settings["minExposure"].toDouble(minExposureSpin->value());
1521  const auto maxExposure = settings["maxExposure"].toDouble(maxExposureSpin->value());
1522  const auto minTemperature = settings["minTemperature"].toDouble(minTemperatureSpin->value());
1523  const auto maxTemperature = settings["maxTemperature"].toDouble(maxTemperatureSpin->value());
1524  const auto binOneCheck = settings["BinOne"].toBool(bin1Check->isChecked());
1525  const auto binTwoCheck = settings["BinTwo"].toBool(bin2Check->isChecked());
1526  const auto binFourCheck = settings["BinFour"].toBool(bin4Check->isChecked());
1527  const auto count = settings["count"].toInt(countSpin->value());
1528  const auto gain = settings["gain"].toInt(-1);
1529  const auto iso = settings["iso"].toString();
1530 
1531  cameraS->setCurrentText(camera);
1532  bin1Check->setChecked(binOneCheck);
1533  bin2Check->setChecked(binTwoCheck);
1534  bin4Check->setChecked(binFourCheck);
1535 
1536  minExposureSpin->setValue(minExposure);
1537  maxExposureSpin->setValue(maxExposure);
1538 
1539  if (captureGainN->isEnabled() && gain >= 0)
1540  captureGainN->setValue(gain);
1541  if (captureISOS->isEnabled() && !iso.isEmpty())
1542  captureISOS->setCurrentText(iso);
1543 
1544  if (minTemperatureSpin->isEnabled())
1545  minTemperatureSpin->setValue(minTemperature);
1546  if (maxTemperatureSpin->isEnabled())
1547  maxTemperatureSpin->setValue(maxTemperature);
1548  countSpin->setValue(count);
1549 
1550 }
1551 
1552 QJsonObject DarkLibrary::getDarkSettings()
1553 {
1554  QJsonObject createDarks =
1555  {
1556  {"camera", cameraS->currentText()},
1557  {"minExposureValue", minExposureSpin->value()},
1558  {"maxExposureValue", maxExposureSpin->value()},
1559  {"minTemperatureValue", minTemperatureSpin->value()},
1560  {"maxTemperatureValue", maxTemperatureSpin->value()},
1561  {"temperatureEnabled", minTemperatureSpin->isEnabled()},
1562  {"bin1Check", bin1Check->isChecked()},
1563  {"bin2Check", bin2Check->isChecked()},
1564  {"bin4Check", bin4Check->isChecked()},
1565  {"countSpin", countSpin->value()},
1566  {"totalImages", totalImages->text()},
1567  {"totalTime", totalTime->text()},
1568  {"darkProgress", darkProgress->value()},
1569  {"gain", captureGainN->value()},
1570  {"iso", captureISOS->currentText()}
1571  };
1572 
1573  if (captureGainN->isEnabled())
1574  createDarks["gain"] = captureGainN->value();
1575  if (captureISOS->isEnabled())
1576  createDarks["iso"] = captureISOS->currentText();
1577 
1578  return createDarks;
1579 }
1580 
1581 void DarkLibrary::setCameraPresets(const QJsonObject &settings)
1582 {
1583  const auto camera = settings["camera"].toString();
1584  const auto isDarkPrefer = settings["isDarkPrefer"].toBool(preferDarksRadio->isChecked());
1585  const auto isDefectPrefer = settings["isDefectPrefer"].toBool(preferDefectsRadio->isChecked());
1586  cameraS->setCurrentText(camera);
1587  preferDarksRadio->setChecked(isDarkPrefer);
1588  preferDefectsRadio->setChecked(isDefectPrefer);
1589  checkCamera();
1590  reloadDarksFromDatabase();
1591 }
1592 
1593 QJsonObject DarkLibrary::getCameraPresets()
1594 {
1595  QJsonObject cameraSettings =
1596  {
1597  {"camera", cameraS->currentText()},
1598  {"preferDarksRadio", preferDarksRadio->isChecked()},
1599  {"preferDefectsRadio", preferDefectsRadio->isChecked()},
1600  {"fileName", m_FileLabel->text()}
1601  };
1602  return cameraSettings;
1603 }
1604 
1605 QJsonObject DarkLibrary::getDefectSettings()
1606 {
1607  QJsonObject createDefectMaps =
1608  {
1609  {"masterTime", masterTime->text()},
1610  {"masterExposure", masterExposure->text()},
1611  {"masterTempreture", masterTemperature->text()},
1612  {"masterMean", masterMean->text()},
1613  {"masterMedian", masterMedian->text()},
1614  {"masterDeviation", masterDeviation->text()},
1615  {"hotPixelsCount", hotPixelsCount->value()},
1616  {"coldPixelsCount", coldPixelsCount->value()},
1617  {"aggresivenessHotSpin", aggresivenessHotSpin->value()},
1618  {"aggresivenessColdSpin", aggresivenessColdSpin->value()},
1619  {"hotPixelsEnabled", hotPixelsEnabled->isChecked()},
1620  {"coldPixelsEnabled", coldPixelsEnabled->isChecked()},
1621  };
1622  return createDefectMaps;
1623 }
1624 
1625 QJsonArray DarkLibrary::getViewMasters()
1626 {
1627  QJsonArray array;
1628 
1629  for(int i = 0; i < darkFramesModel->rowCount(); i++)
1630  {
1631  QSqlRecord record = darkFramesModel->record(i);
1632  auto camera = record.value("ccd").toString();
1633  auto binX = record.value("binX").toInt();
1634  auto binY = record.value("binY").toInt();
1635  auto temperature = record.value("temperature").toDouble();
1636  auto duration = record.value("duration").toDouble();
1637  auto ts = record.value("timestamp").toString();
1638  auto gain = record.value("gain").toInt();
1639  auto iso = record.value("iso").toString();
1640 
1641  QJsonObject filterRows =
1642  {
1643  {"camera", camera},
1644  {"binX", binX},
1645  {"binY", binY},
1646  {"temperature", temperature},
1647  {"duaration", duration},
1648  {"ts", ts}
1649  };
1650 
1651  if (gain >= 0)
1652  filterRows["gain"] = gain;
1653  if (!iso.isEmpty())
1654  filterRows["iso"] = iso;
1655 
1656  array.append(filterRows);
1657  }
1658  return array;
1659 }
1660 
1661 void DarkLibrary::setDefectSettings(const QJsonObject payload)
1662 {
1663  const auto index = payload["rowIndex"].toInt(0);
1664  masterDarksCombo->setCurrentIndex(index);
1665 }
1666 
1667 void DarkLibrary::setDefectPixels(const QJsonObject &payload)
1668 {
1669  const auto hotSpin = payload["hotSpin"].toInt();
1670  const auto coldSpin = payload["coldSpin"].toInt();
1671  const auto hotEnabled = payload["hotEnabled"].toBool(hotPixelsEnabled->isChecked());
1672  const auto coldEnabled = payload["coldEnabled"].toBool(coldPixelsEnabled->isChecked());
1673 
1674  hotPixelsEnabled->setChecked(hotEnabled);
1675  coldPixelsEnabled->setChecked(coldEnabled);
1676 
1677  aggresivenessHotSpin->setValue(hotSpin);
1678  aggresivenessColdSpin->setValue(coldSpin);
1679 
1680  m_DarkView->ZoomDefault();
1681 
1682  setDefectMapEnabled(true);
1683  generateMapB->click();
1684 }
1685 
1686 void DarkLibrary::setDefectMapEnabled(bool enabled)
1687 {
1688  m_DarkView->setDefectMapEnabled(enabled);
1689 }
1690 
1691 double DarkLibrary::getGain()
1692 {
1693  // Gain is manifested in two forms
1694  // Property CCD_GAIN and
1695  // Part of CCD_CONTROLS properties.
1696  // Therefore, we have to find what the currently camera supports first.
1697  auto gain = m_CurrentCamera->getProperty("CCD_GAIN");
1698  if (gain)
1699  return gain->getNumber()->at(0)->value;
1700 
1701 
1702  auto controls = m_CurrentCamera->getProperty("CCD_CONTROLS");
1703  if (controls)
1704  {
1705  auto oneGain = controls->getNumber()->findWidgetByName("Gain");
1706  if (oneGain)
1707  return oneGain->value;
1708  }
1709 
1710  return -1;
1711 }
1712 
1713 }
1714 
@ CAPTURE_COMPLETE
Definition: ekos.h:112
AlignRight
void doubleClicked(const QModelIndex &index)
QString number(int n, int base)
QByteArray fromRawData(const char *data, int size)
bool remove()
Ekos is an advanced Astrophotography tool for Linux. It is based on a modular extensible framework to...
Definition: align.cpp:70
QDateTime currentDateTime()
QChar separator()
QDateTime addDays(qint64 ndays) const const
void clicked(bool checked)
KSUserDB * userdb()
Definition: kstarsdata.h:214
void DeleteDarkFrame(const QString &filename)
KSUserDB::DeleteDarkFrame Delete from database a dark frame record that matches the filename field.
Definition: ksuserdb.cpp:616
bool openUrl(const QUrl &url)
QVariant value(int index) const const
qint64 currentSecsSinceEpoch()
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void toggled(bool checked)
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
static KStars * Instance()
Definition: kstars.h:125
QString tempPath()
void deleteLater()
Sequence Job is a container for the details required to capture a series of images.
Definition: sequencejob.h:18
double toDouble(bool *ok) const const
QString i18n(const char *text, const TYPE &arg...)
bool isEmpty() const const
QUrl fromLocalFile(const QString &localFile)
void currentChanged(int index)
int toInt(bool *ok) const const
UniqueConnection
bool isEmpty() const const
QJsonValue value(const QString &key) const const
void UpdateDarkFrame(const QVariantMap &oneFrame)
KSUserDB::UpdateDarkFrame Updates an existing dark frame record in the data, replace all values match...
Definition: ksuserdb.cpp:595
void setupUi(QWidget *widget)
void waitForFinished()
@ CAPTURE_ABORTED
Definition: ekos.h:99
int row() const const
qint64 daysTo(const QDateTime &other) const const
void buttonToggled(QAbstractButton *button, bool checked)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
void editingFinished()
void setValue(int val)
QString filePath(const QString &fileName) const const
void currentIndexChanged(int index)
void AddDarkFrame(const QVariantMap &oneFrame)
KSUserDB::AddDarkFrame Saves a new dark frame data to the database.
Definition: ksuserdb.cpp:571
void clear()
void valueChanged(int value)
void idToggled(int id, bool checked)
void append(const QJsonValue &value)
QString toString(Qt::DateFormat format) const const
QSqlDatabase database(const QString &connectionName, bool open)
ButtonCode questionYesNo(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonYes=KStandardGuiItem::yes(), const KGuiItem &buttonNo=KStandardGuiItem::no(), const QString &dontAskAgainName=QString(), Options options=Notify)
QFuture< void > map(Sequence &sequence, MapFunctor function)
void activated(int index)
QString & append(QChar ch)
T result() const const
QString toString() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Fri Aug 12 2022 04:00:53 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.