Kstars

darklibrary.cpp
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 "Options.h"
9
10#include "ekos/manager.h"
11#include "ekos/capture/capture.h"
12#include "ekos/capture/sequencejob.h"
13#include "ekos/auxiliary/opticaltrainmanager.h"
14#include "ekos/auxiliary/profilesettings.h"
15#include "ekos/auxiliary/opticaltrainsettings.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 <QDesktopServices>
23#include <QSqlRecord>
24#include <QSqlTableModel>
25#include <QStatusBar>
26#include <algorithm>
27#include <array>
28
29namespace Ekos
30{
31DarkLibrary *DarkLibrary::_DarkLibrary = nullptr;
32
33DarkLibrary *DarkLibrary::Instance()
34{
35 if (_DarkLibrary == nullptr)
36 _DarkLibrary = new DarkLibrary(Manager::Instance());
37
38 return _DarkLibrary;
39}
40
41DarkLibrary::DarkLibrary(QWidget *parent) : QDialog(parent)
42{
43 setupUi(this);
44
45 m_StatusBar = new QStatusBar(this);
46 m_StatusLabel = new QLabel(i18n("Idle"), this);
47 m_FileLabel = new QLabel(this);
48 m_FileLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
49
50 m_StatusBar->insertPermanentWidget(0, m_StatusLabel);
51 m_StatusBar->insertPermanentWidget(1, m_FileLabel, 1);
52 mainLayout->addWidget(m_StatusBar);
53
54 QDir writableDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
55 writableDir.mkpath("darks");
56 writableDir.mkpath("defectmaps");
57
58 // Setup Debounce timer to limit over-activation of settings changes
59 m_DebounceTimer.setInterval(500);
60 m_DebounceTimer.setSingleShot(true);
61 connect(&m_DebounceTimer, &QTimer::timeout, this, &DarkLibrary::settleSettings);
62
63 ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
64 // Dark Generation Connections
65 ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
66 m_CurrentDarkFrame.reset(new FITSData(), &QObject::deleteLater);
67
69 {
70 loadIndexInView(index.row());
71 });
72 connect(openDarksFolderB, &QPushButton::clicked, this, &DarkLibrary::openDarksFolder);
73 connect(clearAllB, &QPushButton::clicked, this, &DarkLibrary::clearAll);
74 connect(clearRowB, &QPushButton::clicked, this, [this]()
75 {
76 auto selectionModel = darkTableView->selectionModel();
77 if (selectionModel->hasSelection())
78 {
79 auto index = selectionModel->currentIndex().row();
80 clearRow(index);
81 }
82 });
83
84 connect(clearExpiredB, &QPushButton::clicked, this, &DarkLibrary::clearExpired);
85 connect(refreshB, &QPushButton::clicked, this, &DarkLibrary::reloadDarksFromDatabase);
86
87 connect(&m_DarkFrameFutureWatcher, &QFutureWatcher<bool>::finished, this, [this]()
88 {
89 // If loading is successful, then set it in current dark view
90 if (m_DarkFrameFutureWatcher.result())
91 {
92 m_DarkView->loadData(m_CurrentDarkFrame);
93 loadCurrentMasterDefectMap();
94 populateMasterMetedata();
95 }
96 else
97 m_FileLabel->setText(i18n("Failed to load %1: %2", m_MasterDarkFrameFilename, m_CurrentDarkFrame->getLastError()));
98
99 });
100
101 connect(masterDarksCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [this](int index)
102 {
103 if (m_Camera)
104 DarkLibrary::loadCurrentMasterDark(m_Camera->getDeviceName(), index);
105 });
106
107 connect(minExposureSpin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
108 connect(maxExposureSpin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
109 connect(exposureStepSin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
110
112 {
113 maxTemperatureSpin->setMinimum(minTemperatureSpin->value());
114 countDarkTotalTime();
115 });
117 {
118 minTemperatureSpin->setMaximum(maxTemperatureSpin->value());
119 countDarkTotalTime();
120 });
122 {
123 maxTemperatureSpin->setMinimum(minTemperatureSpin->value());
124 minTemperatureSpin->setMaximum(maxTemperatureSpin->value());
125 countDarkTotalTime();
126 });
127
128 connect(countSpin, &QDoubleSpinBox::editingFinished, this, &DarkLibrary::countDarkTotalTime);
129#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
130 connect(binningButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::buttonToggled),
131 this, [this](int, bool)
132#else
133 connect(binningButtonGroup, static_cast<void (QButtonGroup::*)(int, bool)>(&QButtonGroup::idToggled),
134 this, [this](int, bool)
135#endif
136 {
137 countDarkTotalTime();
138 });
139
140 connect(startB, &QPushButton::clicked, this, &DarkLibrary::start);
141 connect(stopB, &QPushButton::clicked, this, &DarkLibrary::stop);
142
143 KStarsData::Instance()->userdb()->GetAllDarkFrames(m_DarkFramesDatabaseList);
144 ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
145 // Defect Map Connections
146 ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
147 connect(darkTabsWidget, &QTabWidget::currentChanged, this, [this](int index)
148 {
149 m_DarkView->setDefectMapEnabled(index == 1 && m_CurrentDefectMap);
150 });
153 connect(hotPixelsEnabled, &QCheckBox::toggled, this, [this](bool toggled)
154 {
155 if (m_CurrentDefectMap)
156 m_CurrentDefectMap->setProperty("HotEnabled", toggled);
157 });
158 connect(coldPixelsEnabled, &QCheckBox::toggled, this, [this](bool toggled)
159 {
160 if (m_CurrentDefectMap)
161 m_CurrentDefectMap->setProperty("ColdEnabled", toggled);
162 });
164 {
165 if (m_CurrentDefectMap)
166 {
167 m_CurrentDefectMap->setProperty("HotPixelAggressiveness", aggresivenessHotSpin->value());
168 m_CurrentDefectMap->setProperty("ColdPixelAggressiveness", aggresivenessColdSpin->value());
169 m_CurrentDefectMap->filterPixels();
170 emit newFrame(m_DarkView);
171 }
172 });
174 {
175 if (m_CurrentDefectMap)
176 {
177 aggresivenessHotSlider->setValue(75);
178 aggresivenessColdSlider->setValue(75);
179 m_CurrentDefectMap->setProperty("HotPixelAggressiveness", 75);
180 m_CurrentDefectMap->setProperty("ColdPixelAggressiveness", 75);
181 m_CurrentDefectMap->filterPixels();
182 }
183 });
184 connect(saveMapB, &QPushButton::clicked, this, &DarkLibrary::saveDefectMap);
185 ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
186 // Settings & Initialization
187 ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
188 m_RememberFITSViewer = Options::useFITSViewer();
189 m_RememberSummaryView = Options::useSummaryPreview();
190 initView();
191
192 loadGlobalSettings();
193
194 connectSettings();
195
196 setupOpticalTrainManager();
197}
198
199DarkLibrary::~DarkLibrary()
200{
201}
202
203///////////////////////////////////////////////////////////////////////////////////////
204///
205///////////////////////////////////////////////////////////////////////////////////////
206void DarkLibrary::refreshFromDB()
207{
208 KStarsData::Instance()->userdb()->GetAllDarkFrames(m_DarkFramesDatabaseList);
209}
210
211///////////////////////////////////////////////////////////////////////////////////////
212///
213///////////////////////////////////////////////////////////////////////////////////////
214bool DarkLibrary::findDarkFrame(ISD::CameraChip *m_TargetChip, double duration, QSharedPointer<FITSData> &darkData)
215{
216 QVariantMap bestCandidate;
217 for (auto &map : m_DarkFramesDatabaseList)
218 {
219 // First check CCD name matches and check if we are on the correct chip
220 if (map["ccd"].toString() == m_TargetChip->getCCD()->getDeviceName() &&
221 map["chip"].toInt() == static_cast<int>(m_TargetChip->getType()))
222 {
223 // Match Gain
224 int gain = getGain();
225 if (gain >= 0 && map["gain"].toInt() != gain)
226 continue;
227
228 // Match ISO
230 if (m_TargetChip->getISOValue(isoValue) && map["iso"].toString() != isoValue)
231 continue;
232
233 // Match binning
234 int binX = 1, binY = 1;
235 m_TargetChip->getBinning(&binX, &binY);
236
237 // Then check if binning is the same
238 if (map["binX"].toInt() != binX || map["binY"].toInt() != binY)
239 continue;
240
241 // If camera has an active cooler, then we check temperature against the absolute threshold.
242 if (m_TargetChip->getCCD()->hasCoolerControl())
243 {
244 double temperature = 0;
245 m_TargetChip->getCCD()->getTemperature(&temperature);
246 double darkTemperature = map["temperature"].toDouble();
247 // If different is above threshold, it is completely rejected.
248 if (darkTemperature != INVALID_VALUE && fabs(darkTemperature - temperature) > maxDarkTemperatureDiff->value())
249 continue;
250 }
251
252 if (bestCandidate.isEmpty())
253 {
254 bestCandidate = map;
255 continue;
256 }
257
258 // We try to find the best frame
259 // Frame closest in exposure duration wins
260 // Frame with temperature closest to stored temperature wins (if temperature is reported)
261 uint32_t thisMapScore = 0;
262 uint32_t bestCandidateScore = 0;
263
264 // Else we check for the closest passive temperature
265 if (m_TargetChip->getCCD()->hasCooler())
266 {
267 double temperature = 0;
268 m_TargetChip->getCCD()->getTemperature(&temperature);
269 double diffMap = std::fabs(temperature - map["temperature"].toDouble());
270 double diffBest = std::fabs(temperature - bestCandidate["temperature"].toDouble());
271 // Prefer temperatures closest to target
272 if (diffMap < diffBest)
273 thisMapScore++;
274 else if (diffBest < diffMap)
276 }
277
278 // Duration has a higher score priority over temperature
279 {
280 double diffMap = std::fabs(map["duration"].toDouble() - duration);
281 double diffBest = std::fabs(bestCandidate["duration"].toDouble() - duration);
282 if (diffMap < diffBest)
283 thisMapScore += 5;
284 else if (diffBest < diffMap)
286 }
287
288 // More recent has a higher score than older.
289 {
291 int64_t diffMap = map["timestamp"].toDateTime().secsTo(now);
292 int64_t diffBest = bestCandidate["timestamp"].toDateTime().secsTo(now);
293 if (diffMap < diffBest)
294 thisMapScore++;
295 else if (diffBest < diffMap)
297 }
298
299 // Find candidate with closest time in case we have multiple defect maps
301 bestCandidate = map;
302 }
303 }
304
305 if (bestCandidate.isEmpty())
306 return false;
307
308 if (fabs(bestCandidate["duration"].toDouble() - duration) > 3)
309 emit i18n("Using available dark frame with %1 seconds exposure. Please take a dark frame with %1 seconds exposure for more accurate results.",
310 QString::number(bestCandidate["duration"].toDouble(), 'f', 1),
311 QString::number(duration, 'f', 1));
312
313 QString filename = bestCandidate["filename"].toString();
314
315 // Finally check if the duration is acceptable
316 QDateTime frameTime = bestCandidate["timestamp"].toDateTime();
317 if (frameTime.daysTo(QDateTime::currentDateTime()) > Options::darkLibraryDuration())
318 {
319 emit i18n("Dark frame %s is expired. Please create new master dark.", filename);
320 return false;
321 }
322
323 if (m_CachedDarkFrames.contains(filename))
324 {
325 darkData = m_CachedDarkFrames[filename];
326 return true;
327 }
328
329 // Before adding to cache, clear the cache if memory drops too low.
330 auto memoryMB = KSUtils::getAvailableRAM() / 1e6;
331 if (memoryMB < CACHE_MEMORY_LIMIT)
332 m_CachedDarkFrames.clear();
333
334 // Finally we made it, let's put it in the hash
335 if (cacheDarkFrameFromFile(filename))
336 {
337 darkData = m_CachedDarkFrames[filename];
338 return true;
339 }
340
341 // Remove bad dark frame
342 emit newLog(i18n("Removing bad dark frame file %1", filename));
343 m_CachedDarkFrames.remove(filename);
344 QFile::remove(filename);
345 KStarsData::Instance()->userdb()->DeleteDarkFrame(filename);
346 return false;
347
348}
349
350///////////////////////////////////////////////////////////////////////////////////////
351///
352///////////////////////////////////////////////////////////////////////////////////////
353bool DarkLibrary::findDefectMap(ISD::CameraChip *m_TargetChip, double duration, QSharedPointer<DefectMap> &defectMap)
354{
355 QVariantMap bestCandidate;
356 for (auto &map : m_DarkFramesDatabaseList)
357 {
358 if (map["defectmap"].toString().isEmpty())
359 continue;
360
361 // First check CCD name matches and check if we are on the correct chip
362 if (map["ccd"].toString() == m_TargetChip->getCCD()->getDeviceName() &&
363 map["chip"].toInt() == static_cast<int>(m_TargetChip->getType()))
364 {
365 int binX, binY;
366 m_TargetChip->getBinning(&binX, &binY);
367
368 // Then check if binning is the same
369 if (map["binX"].toInt() == binX && map["binY"].toInt() == binY)
370 {
371 if (bestCandidate.isEmpty())
372 {
373 bestCandidate = map;
374 continue;
375 }
376
377 // We try to find the best frame
378 // Frame closest in exposure duration wins
379 // Frame with temperature closest to stored temperature wins (if temperature is reported)
380 uint32_t thisMapScore = 0;
381 uint32_t bestCandidateScore = 0;
382
383 // Else we check for the closest passive temperature
384 if (m_TargetChip->getCCD()->hasCooler())
385 {
386 double temperature = 0;
387 m_TargetChip->getCCD()->getTemperature(&temperature);
388 double diffMap = std::fabs(temperature - map["temperature"].toDouble());
389 double diffBest = std::fabs(temperature - bestCandidate["temperature"].toDouble());
390 // Prefer temperatures closest to target
391 if (diffMap < diffBest)
392 thisMapScore++;
393 else if (diffBest < diffMap)
395 }
396
397 // Duration has a higher score priority over temperature
398 double diffMap = std::fabs(map["duration"].toDouble() - duration);
399 double diffBest = std::fabs(bestCandidate["duration"].toDouble() - duration);
400 if (diffMap < diffBest)
401 thisMapScore += 2;
402 else if (diffBest < diffMap)
404
405 // Find candidate with closest time in case we have multiple defect maps
407 bestCandidate = map;
408 }
409 }
410 }
411
412
413 if (bestCandidate.isEmpty())
414 return false;
415
416
417 QString darkFilename = bestCandidate["filename"].toString();
418 QString defectFilename = bestCandidate["defectmap"].toString();
419
420 if (darkFilename.isEmpty() || defectFilename.isEmpty())
421 return false;
422
423 if (m_CachedDefectMaps.contains(darkFilename))
424 {
425 defectMap = m_CachedDefectMaps[darkFilename];
426 return true;
427 }
428
429 // Finally we made it, let's put it in the hash
430 if (cacheDefectMapFromFile(darkFilename, defectFilename))
431 {
432 defectMap = m_CachedDefectMaps[darkFilename];
433 return true;
434 }
435 else
436 {
437 // Remove bad dark frame
438 emit newLog(i18n("Failed to load defect map %1", defectFilename));
439 return false;
440 }
441}
442
443///////////////////////////////////////////////////////////////////////////////////////
444///
445///////////////////////////////////////////////////////////////////////////////////////
446bool DarkLibrary::cacheDefectMapFromFile(const QString &key, const QString &filename)
447{
449 oneMap.reset(new DefectMap());
450
451 if (oneMap->load(filename))
452 {
453 oneMap->filterPixels();
454 m_CachedDefectMaps[key] = oneMap;
455 return true;
456 }
457
458 emit newLog(i18n("Failed to load defect map file %1", filename));
459 return false;
460}
461
462///////////////////////////////////////////////////////////////////////////////////////
463///
464///////////////////////////////////////////////////////////////////////////////////////
465bool DarkLibrary::cacheDarkFrameFromFile(const QString &filename)
466{
468 data.reset(new FITSData(FITS_CALIBRATE), &QObject::deleteLater);
469 QFuture<bool> rc = data->loadFromFile(filename);
470
471 rc.waitForFinished();
472 if (rc.result())
473 {
474 m_CachedDarkFrames[filename] = data;
475 }
476 else
477 {
478 emit newLog(i18n("Failed to load dark frame file %1", filename));
479 }
480
481 return rc;
482}
483
484///////////////////////////////////////////////////////////////////////////////////////
485///
486///////////////////////////////////////////////////////////////////////////////////////
487void DarkLibrary::processNewImage(SequenceJob *job, const QSharedPointer<FITSData> &data)
488{
489 Q_UNUSED(data)
490 if (job->getStatus() == JOB_IDLE)
491 return;
492
493 if (job->getCompleted() == job->getCoreProperty(SequenceJob::SJ_Count).toInt())
494 {
495 QJsonObject metadata
496 {
497 {"camera", m_Camera->getDeviceName()},
498 {"chip", m_TargetChip->getType()},
499 {"binx", job->getCoreProperty(SequenceJob::SJ_Binning).toPoint().x()},
500 {"biny", job->getCoreProperty(SequenceJob::SJ_Binning).toPoint().y()},
501 {"duration", job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble()}
502 };
503
504 // Record temperature
505 double value = 0;
506 bool success = m_Camera->getTemperature(&value);
507 if (success)
508 metadata["temperature"] = value;
509
510 success = m_Camera->hasGain() && m_Camera->getGain(&value);
511 if (success)
512 metadata["gain"] = value;
513
515 success = m_TargetChip->getISOValue(isoValue);
516 if (success)
517 metadata["iso"] = isoValue;
518
519 metadata["count"] = job->getCoreProperty(SequenceJob::SJ_Count).toInt();
520 generateMasterFrame(m_CurrentDarkFrame, metadata);
521 reloadDarksFromDatabase();
522 populateMasterMetedata();
523 }
524}
525
526///////////////////////////////////////////////////////////////////////////////////////
527///
528///////////////////////////////////////////////////////////////////////////////////////
529void DarkLibrary::updateProperty(INDI::Property prop)
530{
531 if (prop.getType() != INDI_BLOB)
532 return;
533
534 auto bp = prop.getBLOB()->at(0);
535 QByteArray buffer = QByteArray::fromRawData(reinterpret_cast<char *>(bp->getBlob()), bp->getSize());
536 if (!m_CurrentDarkFrame->loadFromBuffer(buffer, bp->getFormat()))
537 {
538 m_FileLabel->setText(i18n("Failed to process dark data."));
539 return;
540 }
541
542 if (!m_DarkView->loadData(m_CurrentDarkFrame))
543 {
544 m_FileLabel->setText(i18n("Failed to load dark data."));
545 return;
546 }
547
548 uint32_t totalElements = m_CurrentDarkFrame->channels() * m_CurrentDarkFrame->samplesPerChannel();
549 if (totalElements != m_DarkMasterBuffer.size())
550 m_DarkMasterBuffer.assign(totalElements, 0);
551
552 aggregate(m_CurrentDarkFrame);
553 darkProgress->setValue(darkProgress->value() + 1);
554 m_StatusLabel->setText(i18n("Received %1/%2 images.", darkProgress->value(), darkProgress->maximum()));
555}
556
557///////////////////////////////////////////////////////////////////////////////////////
558///
559///////////////////////////////////////////////////////////////////////////////////////
560void DarkLibrary::Release()
561{
562 delete (_DarkLibrary);
563 _DarkLibrary = nullptr;
564
565 // m_Cameras.clear();
566 // cameraS->clear();
567 // m_CurrentCamera = nullptr;
568}
569
570///////////////////////////////////////////////////////////////////////////////////////
571///
572///////////////////////////////////////////////////////////////////////////////////////
573void DarkLibrary::closeEvent(QCloseEvent *ev)
574{
575 Q_UNUSED(ev)
576 Options::setUseFITSViewer(m_RememberFITSViewer);
577 Options::setUseSummaryPreview(m_RememberSummaryView);
578 if (m_JobsGenerated)
579 {
580 m_JobsGenerated = false;
581 m_CaptureModule->clearSequenceQueue();
582 m_CaptureModule->setAllSettings(m_CaptureModuleSettings);
583 }
584}
585
586///////////////////////////////////////////////////////////////////////////////////////
587///
588///////////////////////////////////////////////////////////////////////////////////////
589void DarkLibrary::setCompleted()
590{
591 startB->setEnabled(true);
592 stopB->setEnabled(false);
593
594 Options::setUseFITSViewer(m_RememberFITSViewer);
595 Options::setUseSummaryPreview(m_RememberSummaryView);
596 if (m_JobsGenerated)
597 {
598 m_JobsGenerated = false;
599 m_CaptureModule->clearSequenceQueue();
600 m_CaptureModule->setAllSettings(m_CaptureModuleSettings);
601 }
602
603 m_Camera->disconnect(this);
604 m_CaptureModule->disconnect(this);
605}
606
607///////////////////////////////////////////////////////////////////////////////////////
608///
609///////////////////////////////////////////////////////////////////////////////////////
610void DarkLibrary::clearExpired()
611{
612 if (darkFramesModel->rowCount() == 0)
613 return;
614
615 // Anything before this must go
617
618 auto userdb = QSqlDatabase::database(KStarsData::Instance()->userdb()->connectionName());
619 QSqlTableModel darkframe(nullptr, userdb);
621 darkframe.setTable("darkframe");
622 // Select all those that already expired.
623 darkframe.setFilter("ccd LIKE \'" + m_Camera->getDeviceName() + "\' AND timestamp < \'" + expiredDate.toString(
624 Qt::ISODate) + "\'");
625
626 darkframe.select();
627
628 // Now remove all the expired files from disk
629 for (int i = 0; i < darkframe.rowCount(); ++i)
630 {
631 QString oneFile = darkframe.record(i).value("filename").toString();
633 QString defectMap = darkframe.record(i).value("defectmap").toString();
634 if (defectMap.isEmpty() == false)
636
637 }
638
639 // And remove them from the database
640 darkframe.removeRows(0, darkframe.rowCount());
641 darkframe.submitAll();
642
643 Ekos::DarkLibrary::Instance()->refreshFromDB();
644
645 reloadDarksFromDatabase();
646}
647
648///////////////////////////////////////////////////////////////////////////////////////
649///
650///////////////////////////////////////////////////////////////////////////////////////
651void DarkLibrary::clearBuffers()
652{
653 m_CurrentDarkFrame.clear();
654 // Should clear existing view
655 m_CurrentDarkFrame.reset(new FITSData(), &QObject::deleteLater);
656 m_DarkView->clearData();
657 m_CurrentDefectMap.clear();
658
659}
660///////////////////////////////////////////////////////////////////////////////////////
661///
662///////////////////////////////////////////////////////////////////////////////////////
663void DarkLibrary::clearAll()
664{
665 if (darkFramesModel->rowCount() == 0)
666 return;
667
668 if (KMessageBox::questionYesNo(KStars::Instance(),
669 i18n("Are you sure you want to delete all dark frames images and data?")) ==
670 KMessageBox::No)
671 return;
672
673 // Now remove all the expired files from disk
674 for (int i = 0; i < darkFramesModel->rowCount(); ++i)
675 {
676 QString oneFile = darkFramesModel->record(i).value("filename").toString();
678 QString defectMap = darkFramesModel->record(i).value("defectmap").toString();
679 if (defectMap.isEmpty() == false)
681 darkFramesModel->removeRow(i);
682
683 }
684
685 darkFramesModel->submitAll();
686
687 // Refesh db entries for other cameras
688 refreshFromDB();
689 reloadDarksFromDatabase();
690}
691
692///////////////////////////////////////////////////////////////////////////////////////
693///
694///////////////////////////////////////////////////////////////////////////////////////
695void DarkLibrary::clearRow(int index)
696{
697 if (index < 0)
698 return;
699
700 QSqlRecord record = darkFramesModel->record(index);
701 QString filename = record.value("filename").toString();
702 QString defectMap = record.value("defectmap").toString();
703 QFile::remove(filename);
704 if (!defectMap.isEmpty())
706
707 KStarsData::Instance()->userdb()->DeleteDarkFrame(filename);
708 refreshFromDB();
709 reloadDarksFromDatabase();
710}
711
712///////////////////////////////////////////////////////////////////////////////////////
713///
714///////////////////////////////////////////////////////////////////////////////////////
715void DarkLibrary::openDarksFolder()
716{
717 QString darkFilesPath = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("darks");
718
720}
721
722///////////////////////////////////////////////////////////////////////////////////////
723///
724///////////////////////////////////////////////////////////////////////////////////////
725void DarkLibrary::refreshDefectMastersList(const QString &camera)
726{
727 if (darkFramesModel->rowCount() == 0)
728 return;
729
730 masterDarksCombo->blockSignals(true);
731 masterDarksCombo->clear();
732
733 for (int i = 0; i < darkFramesModel->rowCount(); ++i)
734 {
735 QSqlRecord record = darkFramesModel->record(i);
736
737 if (record.value("ccd") != camera)
738 continue;
739
740 auto binX = record.value("binX").toInt();
741 auto binY = record.value("binY").toInt();
742 auto temperature = record.value("temperature").toDouble();
743 auto duration = record.value("duration").toDouble();
744 auto gain = record.value("gain").toInt();
745 auto iso = record.value("iso").toString();
746 QString ts = record.value("timestamp").toString();
747
748 QString entry = QString("%1 secs %2x%3")
749 .arg(QString::number(duration, 'f', 1))
752
753 if (temperature > INVALID_VALUE)
754 entry.append(QString(" @ %1°").arg(QString::number(temperature, 'f', 1)));
755
756 if (gain >= 0)
757 entry.append(QString(" G %1").arg(gain));
758 if (!iso.isEmpty())
759 entry.append(QString(" ISO %1").arg(iso));
760
761 masterDarksCombo->addItem(entry);
762 }
763
764 masterDarksCombo->blockSignals(false);
765
766 //loadDefectMap();
767
768}
769///////////////////////////////////////////////////////////////////////////////////////
770///
771///////////////////////////////////////////////////////////////////////////////////////
772void DarkLibrary::reloadDarksFromDatabase()
773{
774 auto userdb = QSqlDatabase::database(KStarsData::Instance()->userdb()->connectionName());
775
776 const QString camera = m_Camera->getDeviceName();
777
778 delete (darkFramesModel);
779 delete (sortFilter);
780
781 darkFramesModel = new QSqlTableModel(this, userdb);
782 darkFramesModel->setTable("darkframe");
783 darkFramesModel->setFilter(QString("ccd='%1'").arg(camera));
784 darkFramesModel->select();
785
786 sortFilter = new QSortFilterProxyModel(this);
787 sortFilter->setSourceModel(darkFramesModel);
788 sortFilter->sort (0);
789 darkTableView->setModel (sortFilter);
790
791 //darkTableView->setModel(darkFramesModel);
792 // Hide ID
793 darkTableView->hideColumn(0);
794 // Hide Chip
795 darkTableView->hideColumn(2);
796
797 if (darkFramesModel->rowCount() == 0 && m_CurrentDarkFrame)
798 {
799 clearBuffers();
800 return;
801 }
802
803 refreshDefectMastersList(camera);
804 loadCurrentMasterDark(camera);
805}
806
807///////////////////////////////////////////////////////////////////////////////////////
808///
809///////////////////////////////////////////////////////////////////////////////////////
810void DarkLibrary::loadCurrentMasterDark(const QString &camera, int masterIndex)
811{
812 // Do not process empty models
813 if (darkFramesModel->rowCount() == 0)
814 return;
815
816 if (masterIndex == -1)
817 masterIndex = masterDarksCombo->currentIndex();
818
819 if (masterIndex < 0 || masterIndex >= darkFramesModel->rowCount())
820 return;
821
822 QSqlRecord record = darkFramesModel->record(masterIndex);
823 if (record.value("ccd") != camera)
824 return;
825 // Get the master dark frame file name
826 m_MasterDarkFrameFilename = record.value("filename").toString();
827
828 if (m_MasterDarkFrameFilename.isEmpty() || !QFileInfo::exists(m_MasterDarkFrameFilename))
829 return;
830
831 // Get defect file name as well if available.
832 m_DefectMapFilename = record.value("defectmap").toString();
833
834 // If current dark frame is different from target filename, then load from file
835 if (m_CurrentDarkFrame->filename() != m_MasterDarkFrameFilename)
836 m_DarkFrameFutureWatcher.setFuture(m_CurrentDarkFrame->loadFromFile(m_MasterDarkFrameFilename));
837 // If current dark frame is the same one loaded, then check if we need to reload defect map
838 else
839 loadCurrentMasterDefectMap();
840}
841
842///////////////////////////////////////////////////////////////////////////////////////
843///
844///////////////////////////////////////////////////////////////////////////////////////
845void DarkLibrary::loadCurrentMasterDefectMap()
846{
847 // Find if we have an existing map
848 if (m_CachedDefectMaps.contains(m_MasterDarkFrameFilename))
849 {
850 if (m_CurrentDefectMap != m_CachedDefectMaps.value(m_MasterDarkFrameFilename))
851 {
852 m_CurrentDefectMap = m_CachedDefectMaps.value(m_MasterDarkFrameFilename);
853 m_DarkView->setDefectMap(m_CurrentDefectMap);
854 m_CurrentDefectMap->setDarkData(m_CurrentDarkFrame);
855 }
856 }
857 // Create new defect map
858 else
859 {
860 m_CurrentDefectMap.reset(new DefectMap());
861 connect(m_CurrentDefectMap.data(), &DefectMap::pixelsUpdated, this, [this](uint32_t hot, uint32_t cold)
862 {
863 hotPixelsCount->setValue(hot);
864 coldPixelsCount->setValue(cold);
865 aggresivenessHotSlider->setValue(m_CurrentDefectMap->property("HotPixelAggressiveness").toInt());
866 aggresivenessColdSlider->setValue(m_CurrentDefectMap->property("ColdPixelAggressiveness").toInt());
867 });
868
869 if (!m_DefectMapFilename.isEmpty())
870 cacheDefectMapFromFile(m_MasterDarkFrameFilename, m_DefectMapFilename);
871
872 m_DarkView->setDefectMap(m_CurrentDefectMap);
873 m_CurrentDefectMap->setDarkData(m_CurrentDarkFrame);
874 }
875}
876
877///////////////////////////////////////////////////////////////////////////////////////
878///
879///////////////////////////////////////////////////////////////////////////////////////
880void DarkLibrary::populateMasterMetedata()
881{
882 if (m_CurrentDarkFrame.isNull())
883 return;
884
885 QVariant value;
886 // TS
887 if (m_CurrentDarkFrame->getRecordValue("DATE-OBS", value))
888 masterTime->setText(value.toString());
889 // Temperature
890 if (m_CurrentDarkFrame->getRecordValue("CCD-TEMP", value) && value.toDouble() < 100)
891 masterTemperature->setText(QString::number(value.toDouble(), 'f', 1));
892 // Exposure
893 if (m_CurrentDarkFrame->getRecordValue("EXPTIME", value))
894 masterExposure->setText(value.toString());
895 // Median
896 {
897 double median = m_CurrentDarkFrame->getAverageMedian();
898 if (median > 0)
899 masterMedian->setText(QString::number(median, 'f', 1));
900 }
901 // Mean
902 {
903 double mean = m_CurrentDarkFrame->getAverageMean();
904 masterMean->setText(QString::number(mean, 'f', 1));
905 }
906 // Standard Deviation
907 {
908 double stddev = m_CurrentDarkFrame->getAverageStdDev();
909 masterDeviation->setText(QString::number(stddev, 'f', 1));
910 }
911}
912
913///////////////////////////////////////////////////////////////////////////////////////
914///
915///////////////////////////////////////////////////////////////////////////////////////
916///////////////////////////////////////////////////////////////////////////////////////
917///
918///////////////////////////////////////////////////////////////////////////////////////
919void DarkLibrary::loadIndexInView(int row)
920{
921 QSqlRecord record = darkFramesModel->record(row);
922 QString filename = record.value("filename").toString();
923 // Avoid duplicate loads
924 if (m_DarkView->imageData().isNull() || m_DarkView->imageData()->filename() != filename)
925 m_DarkView->loadFile(filename);
926}
927
928///////////////////////////////////////////////////////////////////////////////////////
929///
930///////////////////////////////////////////////////////////////////////////////////////
931bool DarkLibrary::setCamera(ISD::Camera * device)
932{
933 if (m_Camera == device)
934 return false;
935
936 if (m_Camera)
937 m_Camera->disconnect(this);
938
939 m_Camera = device;
940
941 if (m_Camera)
942 {
943 darkTabsWidget->setEnabled(true);
944 checkCamera();
945 // JM 2024.03.09: Add a bandaid for a mysteroius crash that sometimes happen
946 // when loading dark frame on Ekos startup. The crash occurs in cfitsio
947 // Hopefully this delay might fix it
948 QTimer::singleShot(1000, this, &DarkLibrary::reloadDarksFromDatabase);
949 return true;
950 }
951 else
952 {
953 darkTabsWidget->setEnabled(false);
954 return false;
955 }
956}
957
958///////////////////////////////////////////////////////////////////////////////////////
959///
960///////////////////////////////////////////////////////////////////////////////////////
961void DarkLibrary::removeDevice(const QSharedPointer<ISD::GenericDevice> &device)
962{
963 if (m_Camera && m_Camera->getDeviceName() == device->getDeviceName())
964 {
965 m_Camera->disconnect(this);
966 m_Camera = nullptr;
967 }
968}
969
970///////////////////////////////////////////////////////////////////////////////////////
971///
972///////////////////////////////////////////////////////////////////////////////////////
973void DarkLibrary::checkCamera()
974{
975 if (!m_Camera)
976 return;
977
978 auto device = m_Camera->getDeviceName();
979
980 m_TargetChip = nullptr;
981 // FIXME TODO
982 // Need to figure guide head
983 if (device.contains("Guider"))
984 {
985 m_UseGuideHead = true;
986 m_TargetChip = m_Camera->getChip(ISD::CameraChip::GUIDE_CCD);
987 }
988
989 if (m_TargetChip == nullptr)
990 {
991 m_UseGuideHead = false;
992 m_TargetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
993 }
994
995 // Make sure we have a valid chip and valid base device.
996 // Make sure we are not in capture process.
997 if (!m_TargetChip || !m_TargetChip->getCCD() || m_TargetChip->isCapturing())
998 return;
999
1000 if (m_Camera->hasCoolerControl())
1001 {
1002 temperatureLabel->setEnabled(true);
1003 temperatureStepLabel->setEnabled(true);
1004 temperatureToLabel->setEnabled(true);
1005 temperatureStepSpin->setEnabled(true);
1006 minTemperatureSpin->setEnabled(true);
1007 maxTemperatureSpin->setEnabled(true);
1008
1009 // Get default temperature
1010 double temperature = 0;
1011 // Update if no setting was previously set
1012 if (m_Camera->getTemperature(&temperature))
1013 {
1014 minTemperatureSpin->setValue(temperature);
1015 maxTemperatureSpin->setValue(temperature);
1016 }
1017
1018 }
1019 else
1020 {
1021 temperatureLabel->setEnabled(false);
1022 temperatureStepLabel->setEnabled(false);
1023 temperatureToLabel->setEnabled(false);
1024 temperatureStepSpin->setEnabled(false);
1025 minTemperatureSpin->setEnabled(false);
1026 maxTemperatureSpin->setEnabled(false);
1027 }
1028
1029 QStringList isoList = m_TargetChip->getISOList();
1030 captureISOS->blockSignals(true);
1031 captureISOS->clear();
1032
1033 // No ISO range available
1034 if (isoList.isEmpty())
1035 {
1036 captureISOS->setEnabled(false);
1037 }
1038 else
1039 {
1040 captureISOS->setEnabled(true);
1041 captureISOS->addItems(isoList);
1042 captureISOS->setCurrentIndex(m_TargetChip->getISOIndex());
1043 }
1044 captureISOS->blockSignals(false);
1045
1046 // Gain Check
1047 if (m_Camera->hasGain())
1048 {
1049 double min, max, step, value, targetCustomGain;
1050 m_Camera->getGainMinMaxStep(&min, &max, &step);
1051
1052 // Allow the possibility of no gain value at all.
1053 GainSpinSpecialValue = min - step;
1054 captureGainN->setRange(GainSpinSpecialValue, max);
1055 captureGainN->setSpecialValueText(i18n("--"));
1056 captureGainN->setEnabled(true);
1057 captureGainN->setSingleStep(step);
1058 m_Camera->getGain(&value);
1059
1060 targetCustomGain = getGain();
1061
1062 // Set the custom gain if we have one
1063 // otherwise it will not have an effect.
1064 if (targetCustomGain > 0)
1065 captureGainN->setValue(targetCustomGain);
1066 else
1067 captureGainN->setValue(GainSpinSpecialValue);
1068
1069 captureGainN->setReadOnly(m_Camera->getGainPermission() == IP_RO);
1070 }
1071 else
1072 captureGainN->setEnabled(false);
1073
1074 countDarkTotalTime();
1075
1076}
1077
1078///////////////////////////////////////////////////////////////////////////////////////
1079///
1080///////////////////////////////////////////////////////////////////////////////////////
1081void DarkLibrary::countDarkTotalTime()
1082{
1083 double temperatureCount = 1;
1084 if (m_Camera && m_Camera->hasCoolerControl() && std::abs(maxTemperatureSpin->value() - minTemperatureSpin->value()) > 0)
1085 temperatureCount = (std::abs((maxTemperatureSpin->value() - minTemperatureSpin->value())) / temperatureStepSpin->value()) +
1086 1;
1087 int binnings = 0;
1088 if (bin1Check->isChecked())
1089 binnings++;
1090 if (bin2Check->isChecked())
1091 binnings++;
1092 if (bin4Check->isChecked())
1093 binnings++;
1094
1095 double darkTime = 0;
1096 int imagesCount = 0;
1097 for (double startExposure = minExposureSpin->value(); startExposure <= maxExposureSpin->value();
1098 startExposure += exposureStepSin->value())
1099 {
1102 }
1103
1104 totalTime->setText(QString::number(darkTime / 60.0, 'f', 1));
1106 darkProgress->setMaximum(imagesCount);
1107
1108}
1109
1110///////////////////////////////////////////////////////////////////////////////////////
1111///
1112///////////////////////////////////////////////////////////////////////////////////////
1113void DarkLibrary::generateDarkJobs()
1114{
1115 // Always clear sequence queue before starting
1116 m_CaptureModule->clearSequenceQueue();
1117
1118 if (m_JobsGenerated == false)
1119 {
1120 m_JobsGenerated = true;
1121 m_CaptureModuleSettings = m_CaptureModule->getAllSettings();
1122 }
1123
1125 if (m_Camera->hasCoolerControl() && std::fabs(maxTemperatureSpin->value() - minTemperatureSpin->value()) >= 0)
1126 {
1127 for (double oneTemperature = minTemperatureSpin->value(); oneTemperature <= maxTemperatureSpin->value();
1129 {
1131 }
1132
1133 // Enforce temperature set
1134 m_CaptureModule->setForceTemperature(true);
1135 }
1136 else
1137 {
1138 // Disable temperature set
1139 m_CaptureModule->setForceTemperature(false);
1140 temperatures << INVALID_VALUE;
1141 }
1142
1144 if (bin1Check->isChecked())
1145 bins << 1;
1146 if (bin2Check->isChecked())
1147 bins << 2;
1148 if (bin4Check->isChecked())
1149 bins << 4;
1150
1152 for (double oneExposure = minExposureSpin->value(); oneExposure <= maxExposureSpin->value();
1153 oneExposure += exposureStepSin->value())
1154 {
1156 }
1157
1160
1161
1162 int sequence = 0;
1163 for (auto &oneTemperature : temperatures)
1164 {
1165 for (auto &oneExposure : exposures)
1166 {
1167 for (auto &oneBin : bins)
1168 {
1169 sequence++;
1170 QVariantMap settings;
1171
1172 settings["opticalTrainCombo"] = opticalTrainCombo->currentText();
1173 settings["captureExposureN"] = oneExposure;
1174 settings["captureBinHN"] = oneBin;
1175 settings["captureFormatS"] = "Dark";
1176 settings["cameraTemperatureN"] = oneTemperature;
1177 if (captureGainN->isEnabled())
1178 settings["captureGainN"] = captureGainN->value();
1179 if (captureISOS->isEnabled())
1180 settings["captureISOS"] = captureISOS->currentText();
1181
1182 settings["fileDirectoryT"] = QString(prefix + QString("sequence_%1").arg(sequence));
1183 settings["captureCountN"] = countSpin->value();
1184
1185 m_CaptureModule->setAllSettings(settings);
1186 m_CaptureModule->createJob();
1187 }
1188 }
1189 }
1190}
1191
1192///////////////////////////////////////////////////////////////////////////////////////
1193///
1194///////////////////////////////////////////////////////////////////////////////////////
1195void DarkLibrary::execute()
1196{
1197 m_DarkImagesCounter = 0;
1198 darkProgress->setValue(0);
1199 darkProgress->setTextVisible(true);
1200 connect(m_CaptureModule, &Capture::newImage, this, &DarkLibrary::processNewImage, Qt::UniqueConnection);
1201 connect(m_CaptureModule, &Capture::newStatus, this, &DarkLibrary::setCaptureState, Qt::UniqueConnection);
1202 connect(m_Camera, &ISD::Camera::propertyUpdated, this, &DarkLibrary::updateProperty, Qt::UniqueConnection);
1203
1204 Options::setUseFITSViewer(false);
1205 Options::setUseSummaryPreview(false);
1206 startB->setEnabled(false);
1207 stopB->setEnabled(true);
1208 m_DarkView->reset();
1209 m_StatusLabel->setText(i18n("In progress..."));
1210 m_CaptureModule->start();
1211
1212}
1213
1214///////////////////////////////////////////////////////////////////////////////////////
1215///
1216///////////////////////////////////////////////////////////////////////////////////////
1217void DarkLibrary::stop()
1218{
1219 m_CaptureModule->abort();
1220 darkProgress->setValue(0);
1221 m_DarkView->reset();
1222}
1223
1224///////////////////////////////////////////////////////////////////////////////////////
1225///
1226///////////////////////////////////////////////////////////////////////////////////////
1227void DarkLibrary::initView()
1228{
1229 m_DarkView.reset(new DarkView(darkWidget));
1230 m_DarkView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1231 m_DarkView->setBaseSize(darkWidget->size());
1232 m_DarkView->createFloatingToolBar();
1234 vlayout->addWidget(m_DarkView.get());
1235 darkWidget->setLayout(vlayout);
1236 connect(m_DarkView.get(), &DarkView::loaded, this, [this]()
1237 {
1238 emit newImage(m_DarkView->imageData());
1239 });
1240}
1241
1242///////////////////////////////////////////////////////////////////////////////////////
1243///
1244///////////////////////////////////////////////////////////////////////////////////////
1245void DarkLibrary::aggregate(const QSharedPointer<FITSData> &data)
1246{
1247 switch (data->dataType())
1248 {
1249 case TBYTE:
1251 break;
1252
1253 case TSHORT:
1255 break;
1256
1257 case TUSHORT:
1259 break;
1260
1261 case TLONG:
1263 break;
1264
1265 case TULONG:
1267 break;
1268
1269 case TFLOAT:
1271 break;
1272
1273 case TLONGLONG:
1275 break;
1276
1277 case TDOUBLE:
1279 break;
1280
1281 default:
1282 break;
1283 }
1284}
1285
1286///////////////////////////////////////////////////////////////////////////////////////
1287///
1288///////////////////////////////////////////////////////////////////////////////////////
1289template <typename T>
1290void DarkLibrary::aggregateInternal(const QSharedPointer<FITSData> &data)
1291{
1292 T const *darkBuffer = reinterpret_cast<T const*>(data->getImageBuffer());
1293 for (uint32_t i = 0; i < m_DarkMasterBuffer.size(); i++)
1294 m_DarkMasterBuffer[i] += darkBuffer[i];
1295}
1296
1297///////////////////////////////////////////////////////////////////////////////////////
1298///
1299///////////////////////////////////////////////////////////////////////////////////////
1300void DarkLibrary::generateMasterFrame(const QSharedPointer<FITSData> &data, const QJsonObject &metadata)
1301{
1302 switch (data->dataType())
1303 {
1304 case TBYTE:
1306 break;
1307
1308 case TSHORT:
1310 break;
1311
1312 case TUSHORT:
1314 break;
1315
1316 case TLONG:
1318 break;
1319
1320 case TULONG:
1322 break;
1323
1324 case TFLOAT:
1325 generateMasterFrameInternal<float>(data, metadata);
1326 break;
1327
1328 case TLONGLONG:
1330 break;
1331
1332 case TDOUBLE:
1334 break;
1335
1336 default:
1337 break;
1338 }
1339
1340 emit newImage(data);
1341 // Reset Master Buffer
1342 m_DarkMasterBuffer.assign(m_DarkMasterBuffer.size(), 0);
1343
1344}
1345
1346///////////////////////////////////////////////////////////////////////////////////////
1347///
1348///////////////////////////////////////////////////////////////////////////////////////
1349template <typename T> void DarkLibrary::generateMasterFrameInternal(const QSharedPointer<FITSData> &data,
1350 const QJsonObject &metadata)
1351{
1352 T *writableBuffer = reinterpret_cast<T *>(data->getWritableImageBuffer());
1353 const uint32_t count = metadata["count"].toInt();
1354 // Average the values
1355 for (uint32_t i = 0; i < m_DarkMasterBuffer.size(); i++)
1356 writableBuffer[i] = m_DarkMasterBuffer[i] / count;
1357
1358 QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss");
1359 QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("darks/darkframe_" + ts +
1360 data->extension());
1361
1362 data->calculateStats(true);
1363 if (!data->saveImage(path))
1364 {
1365 m_FileLabel->setText(i18n("Failed to save master frame: %1", data->getLastError()));
1366 return;
1367 }
1368
1369 auto memoryMB = KSUtils::getAvailableRAM() / 1e6;
1370 if (memoryMB > CACHE_MEMORY_LIMIT)
1371 cacheDarkFrameFromFile(data->filename());
1372
1373 QVariantMap map;
1374 map["ccd"] = metadata["camera"].toString();
1375 map["chip"] = metadata["chip"].toInt();
1376 map["binX"] = metadata["binx"].toInt();
1377 map["binY"] = metadata["biny"].toInt();
1378 map["temperature"] = metadata["temperature"].toDouble(INVALID_VALUE);
1379 map["gain"] = metadata["gain"].toInt(-1);
1380 map["iso"] = metadata["iso"].toString();
1381 map["duration"] = metadata["duration"].toDouble();
1382 map["filename"] = path;
1384
1385 m_DarkFramesDatabaseList.append(map);
1386 m_FileLabel->setText(i18n("Master Dark saved to %1", path));
1387 KStarsData::Instance()->userdb()->AddDarkFrame(map);
1388}
1389
1390///////////////////////////////////////////////////////////////////////////////////////
1391///
1392///////////////////////////////////////////////////////////////////////////////////////
1393void DarkLibrary::setCaptureModule(Capture *instance)
1394{
1395 m_CaptureModule = instance;
1396}
1397
1398///////////////////////////////////////////////////////////////////////////////////////
1399///
1400///////////////////////////////////////////////////////////////////////////////////////
1401void DarkLibrary::setCaptureState(CaptureState state)
1402{
1403 switch (state)
1404 {
1405 case CAPTURE_ABORTED:
1406 setCompleted();
1407 m_StatusLabel->setText(i18n("Capture aborted."));
1408 break;
1409 case CAPTURE_COMPLETE:
1410 setCompleted();
1411 m_StatusLabel->setText(i18n("Capture completed."));
1412 break;
1413 default:
1414 break;
1415 }
1416}
1417
1418///////////////////////////////////////////////////////////////////////////////////////
1419///
1420///////////////////////////////////////////////////////////////////////////////////////
1421void DarkLibrary::saveDefectMap()
1422{
1423 if (!m_CurrentDarkFrame)
1424 return;
1425
1426 QString filename = m_CurrentDefectMap->filename();
1427 bool newFile = false;
1428 if (filename.isEmpty())
1429 {
1430 QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss");
1431 filename = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("defectmaps/defectmap_" + ts +
1432 ".json");
1433 newFile = true;
1434 }
1435
1436 if (m_CurrentDefectMap->save(filename, m_Camera->getDeviceName()))
1437 {
1438 m_FileLabel->setText(i18n("Defect map saved to %1", filename));
1439
1440 if (newFile)
1441 {
1442 auto currentMap = std::find_if(m_DarkFramesDatabaseList.begin(),
1443 m_DarkFramesDatabaseList.end(), [&](const QVariantMap & oneMap)
1444 {
1445 return oneMap["filename"].toString() == m_CurrentDarkFrame->filename();
1446 });
1447
1448 if (currentMap != m_DarkFramesDatabaseList.end())
1449 {
1450 (*currentMap)["defectmap"] = filename;
1451 (*currentMap)["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate);
1452 KStarsData::Instance()->userdb()->UpdateDarkFrame(*currentMap);
1453 }
1454 }
1455 }
1456 else
1457 {
1458 m_FileLabel->setText(i18n("Failed to save defect map to %1", filename));
1459 }
1460}
1461
1462///////////////////////////////////////////////////////////////////////////////////////
1463///
1464///////////////////////////////////////////////////////////////////////////////////////
1465void DarkLibrary::start()
1466{
1467 generateDarkJobs();
1468 execute();
1469}
1470
1471///////////////////////////////////////////////////////////////////////////////////////
1472///
1473///////////////////////////////////////////////////////////////////////////////////////
1474void DarkLibrary::setCameraPresets(const QJsonObject &settings)
1475{
1476 const auto opticalTrain = settings["optical_train"].toString();
1477 const auto isDarkPrefer = settings["isDarkPrefer"].toBool(preferDarksRadio->isChecked());
1478 const auto isDefectPrefer = settings["isDefectPrefer"].toBool(preferDefectsRadio->isChecked());
1479 opticalTrainCombo->setCurrentText(opticalTrain);
1480 preferDarksRadio->setChecked(isDarkPrefer);
1482 checkCamera();
1483 reloadDarksFromDatabase();
1484}
1485
1486///////////////////////////////////////////////////////////////////////////////////////
1487///
1488///////////////////////////////////////////////////////////////////////////////////////
1489QJsonObject DarkLibrary::getCameraPresets()
1490{
1492 {
1493 {"optical_train", opticalTrainCombo->currentText()},
1494 {"preferDarksRadio", preferDarksRadio->isChecked()},
1495 {"preferDefectsRadio", preferDefectsRadio->isChecked()},
1496 {"fileName", m_FileLabel->text()}
1497 };
1498 return cameraSettings;
1499}
1500
1501///////////////////////////////////////////////////////////////////////////////////////
1502///
1503///////////////////////////////////////////////////////////////////////////////////////
1504QJsonArray DarkLibrary::getViewMasters()
1505{
1506 QJsonArray array;
1507
1508 for(int i = 0; i < darkFramesModel->rowCount(); i++)
1509 {
1510 QSqlRecord record = darkFramesModel->record(i);
1511 auto camera = record.value("ccd").toString();
1512 auto binX = record.value("binX").toInt();
1513 auto binY = record.value("binY").toInt();
1514 auto temperature = record.value("temperature").toDouble();
1515 auto duration = record.value("duration").toDouble();
1516 auto ts = record.value("timestamp").toString();
1517 auto gain = record.value("gain").toInt();
1518 auto iso = record.value("iso").toString();
1519
1521 {
1522 {"camera", camera},
1523 {"binX", binX},
1524 {"binY", binY},
1525 {"temperature", temperature},
1526 {"duaration", duration},
1527 {"ts", ts}
1528 };
1529
1530 if (gain >= 0)
1531 filterRows["gain"] = gain;
1532 if (!iso.isEmpty())
1533 filterRows["iso"] = iso;
1534
1535 array.append(filterRows);
1536 }
1537 return array;
1538}
1539
1540///////////////////////////////////////////////////////////////////////////////////////
1541///
1542///////////////////////////////////////////////////////////////////////////////////////
1543void DarkLibrary::setDefectPixels(const QJsonObject &payload)
1544{
1545 const auto hotSpin = payload["hotSpin"].toInt();
1546 const auto coldSpin = payload["coldSpin"].toInt();
1547 const auto hotEnabled = payload["hotEnabled"].toBool(hotPixelsEnabled->isChecked());
1548 const auto coldEnabled = payload["coldEnabled"].toBool(coldPixelsEnabled->isChecked());
1549
1550 hotPixelsEnabled->setChecked(hotEnabled);
1551 coldPixelsEnabled->setChecked(coldEnabled);
1552
1553 aggresivenessHotSpin->setValue(hotSpin);
1555
1556 m_DarkView->ZoomDefault();
1557
1558 setDefectMapEnabled(true);
1559 generateMapB->click();
1560}
1561
1562///////////////////////////////////////////////////////////////////////////////////////
1563///
1564///////////////////////////////////////////////////////////////////////////////////////
1565void DarkLibrary::setDefectMapEnabled(bool enabled)
1566{
1567 m_DarkView->setDefectMapEnabled(enabled);
1568}
1569
1570///////////////////////////////////////////////////////////////////////////////////////
1571///
1572///////////////////////////////////////////////////////////////////////////////////////
1573double DarkLibrary::getGain()
1574{
1575 // Gain is manifested in two forms
1576 // Property CCD_GAIN and
1577 // Part of CCD_CONTROLS properties.
1578 // Therefore, we have to find what the currently camera supports first.
1579 auto gain = m_Camera->getProperty("CCD_GAIN");
1580 if (gain)
1581 return gain.getNumber()->at(0)->value;
1582
1583
1584 auto controls = m_Camera->getProperty("CCD_CONTROLS");
1585 if (controls)
1586 {
1587 auto oneGain = controls.getNumber()->findWidgetByName("Gain");
1588 if (oneGain)
1589 return oneGain->value;
1590 }
1591
1592 return -1;
1593}
1594
1595///////////////////////////////////////////////////////////////////////////////////////
1596///
1597///////////////////////////////////////////////////////////////////////////////////////
1598void DarkLibrary::setupOpticalTrainManager()
1599{
1600 connect(OpticalTrainManager::Instance(), &OpticalTrainManager::updated, this, &DarkLibrary::refreshOpticalTrain);
1601 connect(trainB, &QPushButton::clicked, this, [this]()
1602 {
1603 OpticalTrainManager::Instance()->openEditor(opticalTrainCombo->currentText());
1604 });
1606 {
1607 ProfileSettings::Instance()->setOneSetting(ProfileSettings::DarkLibraryOpticalTrain,
1608 OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(index)));
1609 refreshOpticalTrain();
1610 emit trainChanged();
1611 });
1612}
1613
1614///////////////////////////////////////////////////////////////////////////////////////
1615///
1616///////////////////////////////////////////////////////////////////////////////////////
1617void DarkLibrary::refreshOpticalTrain()
1618{
1619 opticalTrainCombo->blockSignals(true);
1620 opticalTrainCombo->clear();
1621 opticalTrainCombo->addItems(OpticalTrainManager::Instance()->getTrainNames());
1622 trainB->setEnabled(true);
1623
1624 QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::DarkLibraryOpticalTrain);
1625
1626 if (trainID.isValid())
1627 {
1628 auto id = trainID.toUInt();
1629
1630 // If train not found, select the first one available.
1631 if (OpticalTrainManager::Instance()->exists(id) == false)
1632 {
1633 emit newLog(i18n("Optical train doesn't exist for id %1", id));
1634 id = OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(0));
1635 }
1636
1637 auto name = OpticalTrainManager::Instance()->name(id);
1638
1639 opticalTrainCombo->setCurrentText(name);
1640
1641 auto camera = OpticalTrainManager::Instance()->getCamera(name);
1642 if (camera)
1643 {
1644 auto scope = OpticalTrainManager::Instance()->getScope(name);
1645 opticalTrainCombo->setToolTip(QString("%1 @ %2").arg(camera->getDeviceName(), scope["name"].toString()));
1646 }
1647 setCamera(camera);
1648
1649 // Load train settings
1650 OpticalTrainSettings::Instance()->setOpticalTrainID(id);
1651 auto settings = OpticalTrainSettings::Instance()->getOneSetting(OpticalTrainSettings::DarkLibrary);
1652 if (settings.isValid())
1653 {
1654 auto map = settings.toJsonObject().toVariantMap();
1655 if (map != m_Settings)
1656 setAllSettings(map);
1657 }
1658 else
1659 m_Settings = m_GlobalSettings;
1660 }
1661
1662 opticalTrainCombo->blockSignals(false);
1663}
1664
1665///////////////////////////////////////////////////////////////////////////////////////
1666///
1667///////////////////////////////////////////////////////////////////////////////////////
1668void DarkLibrary::loadGlobalSettings()
1669{
1670 QString key;
1671 QVariant value;
1672
1673 QVariantMap settings;
1674 // All Combo Boxes
1675 for (auto &oneWidget : findChildren<QComboBox*>())
1676 {
1677 if (oneWidget->objectName() == "opticalTrainCombo")
1678 continue;
1679
1680 key = oneWidget->objectName();
1681 value = Options::self()->property(key.toLatin1());
1682 if (value.isValid())
1683 {
1684 oneWidget->setCurrentText(value.toString());
1685 settings[key] = value;
1686 }
1687 }
1688
1689 // All Double Spin Boxes
1690 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
1691 {
1692 key = oneWidget->objectName();
1693 value = Options::self()->property(key.toLatin1());
1694 if (value.isValid())
1695 {
1696 oneWidget->setValue(value.toDouble());
1697 settings[key] = value;
1698 }
1699 }
1700
1701 // All Spin Boxes
1702 for (auto &oneWidget : findChildren<QSpinBox*>())
1703 {
1704 key = oneWidget->objectName();
1705 value = Options::self()->property(key.toLatin1());
1706 if (value.isValid())
1707 {
1708 oneWidget->setValue(value.toInt());
1709 settings[key] = value;
1710 }
1711 }
1712
1713 // All Checkboxes
1714 for (auto &oneWidget : findChildren<QCheckBox*>())
1715 {
1716 key = oneWidget->objectName();
1717 value = Options::self()->property(key.toLatin1());
1718 if (value.isValid())
1719 {
1720 oneWidget->setChecked(value.toBool());
1721 settings[key] = value;
1722 }
1723 }
1724
1725 m_GlobalSettings = m_Settings = settings;
1726}
1727
1728
1729///////////////////////////////////////////////////////////////////////////////////////
1730///
1731///////////////////////////////////////////////////////////////////////////////////////
1732void DarkLibrary::connectSettings()
1733{
1734 // All Combo Boxes
1735 for (auto &oneWidget : findChildren<QComboBox*>())
1736 connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::DarkLibrary::syncSettings);
1737
1738 // All Double Spin Boxes
1739 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
1740 connect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::DarkLibrary::syncSettings);
1741
1742 // All Spin Boxes
1743 for (auto &oneWidget : findChildren<QSpinBox*>())
1744 connect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::DarkLibrary::syncSettings);
1745
1746 // All Checkboxes
1747 for (auto &oneWidget : findChildren<QCheckBox*>())
1748 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::DarkLibrary::syncSettings);
1749
1750 // All Radio buttons
1751 for (auto &oneWidget : findChildren<QRadioButton*>())
1752 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::DarkLibrary::syncSettings);
1753
1754 // Train combo box should NOT be synced.
1755 disconnect(opticalTrainCombo, QOverload<int>::of(&QComboBox::activated), this, &Ekos::DarkLibrary::syncSettings);
1756}
1757
1758///////////////////////////////////////////////////////////////////////////////////////
1759///
1760///////////////////////////////////////////////////////////////////////////////////////
1761void DarkLibrary::disconnectSettings()
1762{
1763 // All Combo Boxes
1764 for (auto &oneWidget : findChildren<QComboBox*>())
1765 disconnect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::DarkLibrary::syncSettings);
1766
1767 // All Double Spin Boxes
1768 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
1769 disconnect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::DarkLibrary::syncSettings);
1770
1771 // All Spin Boxes
1772 for (auto &oneWidget : findChildren<QSpinBox*>())
1773 disconnect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::DarkLibrary::syncSettings);
1774
1775 // All Checkboxes
1776 for (auto &oneWidget : findChildren<QCheckBox*>())
1777 disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::DarkLibrary::syncSettings);
1778
1779 // All Radio buttons
1780 for (auto &oneWidget : findChildren<QRadioButton*>())
1781 disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::DarkLibrary::syncSettings);
1782
1783}
1784
1785///////////////////////////////////////////////////////////////////////////////////////////
1786///
1787///////////////////////////////////////////////////////////////////////////////////////////
1788QVariantMap DarkLibrary::getAllSettings() const
1789{
1790 QVariantMap settings;
1791
1792 // All Combo Boxes
1793 for (auto &oneWidget : findChildren<QComboBox*>())
1794 settings.insert(oneWidget->objectName(), oneWidget->currentText());
1795
1796 // All Double Spin Boxes
1797 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
1798 settings.insert(oneWidget->objectName(), oneWidget->value());
1799
1800 // All Spin Boxes
1801 for (auto &oneWidget : findChildren<QSpinBox*>())
1802 settings.insert(oneWidget->objectName(), oneWidget->value());
1803
1804 // All Checkboxes
1805 for (auto &oneWidget : findChildren<QCheckBox*>())
1806 settings.insert(oneWidget->objectName(), oneWidget->isChecked());
1807
1808 return settings;
1809}
1810
1811///////////////////////////////////////////////////////////////////////////////////////////
1812///
1813///////////////////////////////////////////////////////////////////////////////////////////
1814void DarkLibrary::setAllSettings(const QVariantMap &settings)
1815{
1816 // Disconnect settings that we don't end up calling syncSettings while
1817 // performing the changes.
1818 disconnectSettings();
1819
1820 for (auto &name : settings.keys())
1821 {
1822 // Combo
1823 auto comboBox = findChild<QComboBox*>(name);
1824 if (comboBox)
1825 {
1826 syncControl(settings, name, comboBox);
1827 continue;
1828 }
1829
1830 // Double spinbox
1831 auto doubleSpinBox = findChild<QDoubleSpinBox*>(name);
1832 if (doubleSpinBox)
1833 {
1834 syncControl(settings, name, doubleSpinBox);
1835 continue;
1836 }
1837
1838 // spinbox
1839 auto spinBox = findChild<QSpinBox*>(name);
1840 if (spinBox)
1841 {
1842 syncControl(settings, name, spinBox);
1843 continue;
1844 }
1845
1846 // checkbox
1847 auto checkbox = findChild<QCheckBox*>(name);
1848 if (checkbox)
1849 {
1850 syncControl(settings, name, checkbox);
1851 continue;
1852 }
1853
1854 // Radio button
1856 if (radioButton)
1857 {
1858 syncControl(settings, name, radioButton);
1859 continue;
1860 }
1861 }
1862
1863 // Sync to options
1864 for (auto &key : settings.keys())
1865 {
1866 auto value = settings[key];
1867 // Save immediately
1868 Options::self()->setProperty(key.toLatin1(), value);
1869
1870 m_Settings[key] = value;
1871 m_GlobalSettings[key] = value;
1872 }
1873
1874 emit settingsUpdated(getAllSettings());
1875
1876 // Save to optical train specific settings as well
1877 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
1878 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::DarkLibrary, m_Settings);
1879
1880 // Restablish connections
1881 connectSettings();
1882}
1883
1884///////////////////////////////////////////////////////////////////////////////////////////
1885///
1886///////////////////////////////////////////////////////////////////////////////////////////
1887bool DarkLibrary::syncControl(const QVariantMap &settings, const QString &key, QWidget * widget)
1888{
1889 QSpinBox *pSB = nullptr;
1890 QDoubleSpinBox *pDSB = nullptr;
1891 QCheckBox *pCB = nullptr;
1892 QComboBox *pComboBox = nullptr;
1893 QRadioButton *pRadioButton = nullptr;
1894 bool ok = false;
1895
1896 if ((pSB = qobject_cast<QSpinBox *>(widget)))
1897 {
1898 const int value = settings[key].toInt(&ok);
1899 if (ok)
1900 {
1901 pSB->setValue(value);
1902 return true;
1903 }
1904 }
1905 else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget)))
1906 {
1907 const double value = settings[key].toDouble(&ok);
1908 if (ok)
1909 {
1910 pDSB->setValue(value);
1911 return true;
1912 }
1913 }
1914 else if ((pCB = qobject_cast<QCheckBox *>(widget)))
1915 {
1916 const bool value = settings[key].toBool();
1917 if (value != pCB->isChecked())
1918 pCB->click();
1919 return true;
1920 }
1921 // ONLY FOR STRINGS, not INDEX
1922 else if ((pComboBox = qobject_cast<QComboBox *>(widget)))
1923 {
1924 const QString value = settings[key].toString();
1925 pComboBox->setCurrentText(value);
1926 return true;
1927 }
1928 else if ((pRadioButton = qobject_cast<QRadioButton *>(widget)))
1929 {
1930 const bool value = settings[key].toBool();
1931 if (value)
1932 pRadioButton->click();
1933 return true;
1934 }
1935
1936 return false;
1937};
1938
1939///////////////////////////////////////////////////////////////////////////////////////
1940///
1941///////////////////////////////////////////////////////////////////////////////////////
1942void DarkLibrary::syncSettings()
1943{
1944 QDoubleSpinBox *dsb = nullptr;
1945 QSpinBox *sb = nullptr;
1946 QCheckBox *cb = nullptr;
1947 QComboBox *cbox = nullptr;
1948 QRadioButton *cradio = nullptr;
1949
1950 QString key;
1951 QVariant value;
1952
1953 if ( (dsb = qobject_cast<QDoubleSpinBox*>(sender())))
1954 {
1955 key = dsb->objectName();
1956 value = dsb->value();
1957
1958 }
1959 else if ( (sb = qobject_cast<QSpinBox*>(sender())))
1960 {
1961 key = sb->objectName();
1962 value = sb->value();
1963 }
1964 else if ( (cb = qobject_cast<QCheckBox*>(sender())))
1965 {
1966 key = cb->objectName();
1967 value = cb->isChecked();
1968 }
1969 else if ( (cbox = qobject_cast<QComboBox*>(sender())))
1970 {
1971 key = cbox->objectName();
1972 value = cbox->currentText();
1973 }
1974 else if ( (cradio = qobject_cast<QRadioButton*>(sender())))
1975 {
1976 key = cradio->objectName();
1977 value = true;
1978 }
1979
1980 // Save immediately
1981 Options::self()->setProperty(key.toLatin1(), value);
1982 m_Settings[key] = value;
1983 m_GlobalSettings[key] = value;
1984
1985 m_DebounceTimer.start();
1986}
1987
1988///////////////////////////////////////////////////////////////////////////////////////////
1989///
1990///////////////////////////////////////////////////////////////////////////////////////////
1991void DarkLibrary::settleSettings()
1992{
1993 emit settingsUpdated(getAllSettings());
1994 Options::self()->save();
1995 // Save to optical train specific settings as well
1996 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
1997 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::DarkLibrary, m_Settings);
1998}
1999
2000///////////////////////////////////////////////////////////////////////////////////////
2001///
2002///////////////////////////////////////////////////////////////////////////////////////
2003QJsonObject DarkLibrary::getDefectSettings()
2004{
2006 for (int i = 0; i < masterDarksCombo->count(); i++)
2007 darkMasters << masterDarksCombo->itemText(i);
2008
2010 {
2011 {"masterTime", masterTime->text()},
2012 {"masterDarks", darkMasters.join('|')},
2013 {"masterExposure", masterExposure->text()},
2014 {"masterTempreture", masterTemperature->text()},
2015 {"masterMean", masterMean->text()},
2016 {"masterMedian", masterMedian->text()},
2017 {"masterDeviation", masterDeviation->text()},
2018 {"hotPixelsEnabled", hotPixelsEnabled->isChecked()},
2019 {"coldPixelsEnabled", coldPixelsEnabled->isChecked()},
2020 };
2021 return createDefectMaps;
2022}
2023
2024
2025
2026}
2027
CameraChip class controls a particular chip in camera.
Camera class controls an INDI Camera device.
Definition indicamera.h:47
static KStars * Instance()
Definition kstars.h:123
Sequence Job is a container for the details required to capture a series of images.
QString i18n(const char *text, const TYPE &arg...)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:78
@ CAPTURE_ABORTED
Definition ekos.h:99
@ CAPTURE_COMPLETE
Definition ekos.h:112
QString path(const QString &relativePath)
KGuiItem insert()
QString name(StandardShortcut id)
bool isChecked() const const
void clicked(bool checked)
void toggled(bool checked)
void doubleClicked(const QModelIndex &index)
void valueChanged(int value)
void editingFinished()
void buttonToggled(QAbstractButton *button, bool checked)
void idToggled(int id, bool checked)
QByteArray fromRawData(const char *data, qsizetype size)
void activated(int index)
void currentIndexChanged(int index)
QDateTime addDays(qint64 ndays) const const
QDateTime currentDateTime()
qint64 currentSecsSinceEpoch()
QString toString(QStringView format, QCalendar cal) const const
bool openUrl(const QUrl &url)
QString filePath(const QString &fileName) const const
QChar separator()
QString tempPath()
bool remove()
bool exists() const const
void append(const QJsonValue &value)
QVariantMap toVariantMap() const const
int row() const const
void deleteLater()
void setValue(int val)
QSqlDatabase database(const QString &connectionName, bool open)
QVariant value(const QString &name) const const
QString & append(QChar ch)
QString arg(Args &&... args) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
QByteArray toLatin1() const const
AlignRight
UniqueConnection
void currentChanged(int index)
QFuture< void > map(Iterator begin, Iterator end, MapFunctor &&function)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
QUrl fromLocalFile(const QString &localFile)
bool isValid() const const
bool toBool() const const
double toDouble(bool *ok) const const
int toInt(bool *ok) const const
QString toString() const const
uint toUInt(bool *ok) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:19:02 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.