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 m_CurrentDarkFrame->setExtension(QString(bp->getFormat()));
536 QByteArray buffer = QByteArray::fromRawData(reinterpret_cast<char *>(bp->getBlob()), bp->getSize());
537 if (!m_CurrentDarkFrame->loadFromBuffer(buffer))
538 {
539 m_FileLabel->setText(i18n("Failed to process dark data."));
540 return;
541 }
542
543 if (!m_DarkView->loadData(m_CurrentDarkFrame))
544 {
545 m_FileLabel->setText(i18n("Failed to load dark data."));
546 return;
547 }
548
549 uint32_t totalElements = m_CurrentDarkFrame->channels() * m_CurrentDarkFrame->samplesPerChannel();
550 if (totalElements != m_DarkMasterBuffer.size())
551 m_DarkMasterBuffer.assign(totalElements, 0);
552
553 aggregate(m_CurrentDarkFrame);
554 darkProgress->setValue(darkProgress->value() + 1);
555 m_StatusLabel->setText(i18n("Received %1/%2 images.", darkProgress->value(), darkProgress->maximum()));
556}
557
558///////////////////////////////////////////////////////////////////////////////////////
559///
560///////////////////////////////////////////////////////////////////////////////////////
561void DarkLibrary::Release()
562{
563 delete (_DarkLibrary);
564 _DarkLibrary = nullptr;
565
566 // m_Cameras.clear();
567 // cameraS->clear();
568 // m_CurrentCamera = nullptr;
569}
570
571///////////////////////////////////////////////////////////////////////////////////////
572///
573///////////////////////////////////////////////////////////////////////////////////////
574void DarkLibrary::closeEvent(QCloseEvent *ev)
575{
576 Q_UNUSED(ev)
577 Options::setUseFITSViewer(m_RememberFITSViewer);
578 Options::setUseSummaryPreview(m_RememberSummaryView);
579 if (m_JobsGenerated)
580 {
581 m_JobsGenerated = false;
582 m_CaptureModule->clearSequenceQueue();
583 m_CaptureModule->cameraUI->setAllSettings(m_CaptureModuleSettings);
584 }
585}
586
587///////////////////////////////////////////////////////////////////////////////////////
588///
589///////////////////////////////////////////////////////////////////////////////////////
590void DarkLibrary::setCompleted()
591{
592 startB->setEnabled(true);
593 stopB->setEnabled(false);
594
595 Options::setUseFITSViewer(m_RememberFITSViewer);
596 Options::setUseSummaryPreview(m_RememberSummaryView);
597 if (m_JobsGenerated)
598 {
599 m_JobsGenerated = false;
600 m_CaptureModule->clearSequenceQueue();
601 m_CaptureModule->cameraUI->setAllSettings(m_CaptureModuleSettings);
602 }
603
604 m_Camera->disconnect(this);
605 m_CaptureModule->disconnect(this);
606}
607
608///////////////////////////////////////////////////////////////////////////////////////
609///
610///////////////////////////////////////////////////////////////////////////////////////
611void DarkLibrary::clearExpired()
612{
613 if (darkFramesModel->rowCount() == 0)
614 return;
615
616 // Anything before this must go
618
619 auto userdb = QSqlDatabase::database(KStarsData::Instance()->userdb()->connectionName());
620 QSqlTableModel darkframe(nullptr, userdb);
622 darkframe.setTable("darkframe");
623 // Select all those that already expired.
624 darkframe.setFilter("ccd LIKE \'" + m_Camera->getDeviceName() + "\' AND timestamp < \'" + expiredDate.toString(
625 Qt::ISODate) + "\'");
626
627 darkframe.select();
628
629 // Now remove all the expired files from disk
630 for (int i = 0; i < darkframe.rowCount(); ++i)
631 {
632 QString oneFile = darkframe.record(i).value("filename").toString();
634 QString defectMap = darkframe.record(i).value("defectmap").toString();
635 if (defectMap.isEmpty() == false)
637
638 }
639
640 // And remove them from the database
641 darkframe.removeRows(0, darkframe.rowCount());
642 darkframe.submitAll();
643
644 Ekos::DarkLibrary::Instance()->refreshFromDB();
645
646 reloadDarksFromDatabase();
647}
648
649///////////////////////////////////////////////////////////////////////////////////////
650///
651///////////////////////////////////////////////////////////////////////////////////////
652void DarkLibrary::clearBuffers()
653{
654 m_CurrentDarkFrame.clear();
655 // Should clear existing view
656 m_CurrentDarkFrame.reset(new FITSData(), &QObject::deleteLater);
657 m_DarkView->clearData();
658 m_CurrentDefectMap.clear();
659
660}
661///////////////////////////////////////////////////////////////////////////////////////
662///
663///////////////////////////////////////////////////////////////////////////////////////
664void DarkLibrary::clearAll()
665{
666 if (darkFramesModel->rowCount() == 0)
667 return;
668
669 if (KMessageBox::questionYesNo(KStars::Instance(),
670 i18n("Are you sure you want to delete all dark frames images and data?")) ==
671 KMessageBox::No)
672 return;
673
674 // Now remove all the expired files from disk
675 for (int i = 0; i < darkFramesModel->rowCount(); ++i)
676 {
677 QString oneFile = darkFramesModel->record(i).value("filename").toString();
679 QString defectMap = darkFramesModel->record(i).value("defectmap").toString();
680 if (defectMap.isEmpty() == false)
682 darkFramesModel->removeRow(i);
683
684 }
685
686 darkFramesModel->submitAll();
687
688 // Refesh db entries for other cameras
689 refreshFromDB();
690 reloadDarksFromDatabase();
691}
692
693///////////////////////////////////////////////////////////////////////////////////////
694///
695///////////////////////////////////////////////////////////////////////////////////////
696void DarkLibrary::clearRow(int index)
697{
698 if (index < 0)
699 return;
700
701 QSqlRecord record = darkFramesModel->record(index);
702 QString filename = record.value("filename").toString();
703 QString defectMap = record.value("defectmap").toString();
704 QFile::remove(filename);
705 if (!defectMap.isEmpty())
707
708 KStarsData::Instance()->userdb()->DeleteDarkFrame(filename);
709 refreshFromDB();
710 reloadDarksFromDatabase();
711}
712
713///////////////////////////////////////////////////////////////////////////////////////
714///
715///////////////////////////////////////////////////////////////////////////////////////
716void DarkLibrary::openDarksFolder()
717{
718 QString darkFilesPath = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("darks");
719
721}
722
723///////////////////////////////////////////////////////////////////////////////////////
724///
725///////////////////////////////////////////////////////////////////////////////////////
726void DarkLibrary::refreshDefectMastersList(const QString &camera)
727{
728 if (darkFramesModel->rowCount() == 0)
729 return;
730
731 masterDarksCombo->blockSignals(true);
732 masterDarksCombo->clear();
733
734 for (int i = 0; i < darkFramesModel->rowCount(); ++i)
735 {
736 QSqlRecord record = darkFramesModel->record(i);
737
738 if (record.value("ccd") != camera)
739 continue;
740
741 auto binX = record.value("binX").toInt();
742 auto binY = record.value("binY").toInt();
743 auto temperature = record.value("temperature").toDouble();
744 auto duration = record.value("duration").toDouble();
745 auto gain = record.value("gain").toInt();
746 auto iso = record.value("iso").toString();
747 QString ts = record.value("timestamp").toString();
748
749 QString entry = QString("%1 secs %2x%3")
750 .arg(QString::number(duration, 'f', 1))
753
754 if (temperature > INVALID_VALUE)
755 entry.append(QString(" @ %1°").arg(QString::number(temperature, 'f', 1)));
756
757 if (gain >= 0)
758 entry.append(QString(" G %1").arg(gain));
759 if (!iso.isEmpty())
760 entry.append(QString(" ISO %1").arg(iso));
761
762 masterDarksCombo->addItem(entry);
763 }
764
765 masterDarksCombo->blockSignals(false);
766
767 //loadDefectMap();
768
769}
770///////////////////////////////////////////////////////////////////////////////////////
771///
772///////////////////////////////////////////////////////////////////////////////////////
773void DarkLibrary::reloadDarksFromDatabase()
774{
775 if (!m_Camera) return;
776 auto userdb = QSqlDatabase::database(KStarsData::Instance()->userdb()->connectionName());
777
778 const QString camera = m_Camera->getDeviceName();
779
780 delete (darkFramesModel);
781 delete (sortFilter);
782
783 darkFramesModel = new QSqlTableModel(this, userdb);
784 darkFramesModel->setTable("darkframe");
785 darkFramesModel->setFilter(QString("ccd='%1'").arg(camera));
786 darkFramesModel->select();
787
788 sortFilter = new QSortFilterProxyModel(this);
789 sortFilter->setSourceModel(darkFramesModel);
790 sortFilter->sort (0);
791 darkTableView->setModel (sortFilter);
792
793 //darkTableView->setModel(darkFramesModel);
794 // Hide ID
795 darkTableView->hideColumn(0);
796 // Hide Chip
797 darkTableView->hideColumn(2);
798
799 if (darkFramesModel->rowCount() == 0 && m_CurrentDarkFrame)
800 {
801 clearBuffers();
802 return;
803 }
804
805 refreshDefectMastersList(camera);
806 loadCurrentMasterDark(camera);
807}
808
809///////////////////////////////////////////////////////////////////////////////////////
810///
811///////////////////////////////////////////////////////////////////////////////////////
812void DarkLibrary::loadCurrentMasterDark(const QString &camera, int masterIndex)
813{
814 // Do not process empty models
815 if (darkFramesModel->rowCount() == 0)
816 return;
817
818 if (masterIndex == -1)
819 masterIndex = masterDarksCombo->currentIndex();
820
821 if (masterIndex < 0 || masterIndex >= darkFramesModel->rowCount())
822 return;
823
824 QSqlRecord record = darkFramesModel->record(masterIndex);
825 if (record.value("ccd") != camera)
826 return;
827 // Get the master dark frame file name
828 m_MasterDarkFrameFilename = record.value("filename").toString();
829
830 if (m_MasterDarkFrameFilename.isEmpty() || !QFileInfo::exists(m_MasterDarkFrameFilename))
831 return;
832
833 // Get defect file name as well if available.
834 m_DefectMapFilename = record.value("defectmap").toString();
835
836 // If current dark frame is different from target filename, then load from file
837 if (m_CurrentDarkFrame->filename() != m_MasterDarkFrameFilename)
838 m_DarkFrameFutureWatcher.setFuture(m_CurrentDarkFrame->loadFromFile(m_MasterDarkFrameFilename));
839 // If current dark frame is the same one loaded, then check if we need to reload defect map
840 else
841 loadCurrentMasterDefectMap();
842}
843
844///////////////////////////////////////////////////////////////////////////////////////
845///
846///////////////////////////////////////////////////////////////////////////////////////
847void DarkLibrary::loadCurrentMasterDefectMap()
848{
849 // Find if we have an existing map
850 if (m_CachedDefectMaps.contains(m_MasterDarkFrameFilename))
851 {
852 if (m_CurrentDefectMap != m_CachedDefectMaps.value(m_MasterDarkFrameFilename))
853 {
854 m_CurrentDefectMap = m_CachedDefectMaps.value(m_MasterDarkFrameFilename);
855 m_DarkView->setDefectMap(m_CurrentDefectMap);
856 m_CurrentDefectMap->setDarkData(m_CurrentDarkFrame);
857 }
858 }
859 // Create new defect map
860 else
861 {
862 m_CurrentDefectMap.reset(new DefectMap());
863 connect(m_CurrentDefectMap.data(), &DefectMap::pixelsUpdated, this, [this](uint32_t hot, uint32_t cold)
864 {
865 hotPixelsCount->setValue(hot);
866 coldPixelsCount->setValue(cold);
867 aggresivenessHotSlider->setValue(m_CurrentDefectMap->property("HotPixelAggressiveness").toInt());
868 aggresivenessColdSlider->setValue(m_CurrentDefectMap->property("ColdPixelAggressiveness").toInt());
869 });
870
871 if (!m_DefectMapFilename.isEmpty())
872 cacheDefectMapFromFile(m_MasterDarkFrameFilename, m_DefectMapFilename);
873
874 m_DarkView->setDefectMap(m_CurrentDefectMap);
875 m_CurrentDefectMap->setDarkData(m_CurrentDarkFrame);
876 }
877}
878
879///////////////////////////////////////////////////////////////////////////////////////
880///
881///////////////////////////////////////////////////////////////////////////////////////
882void DarkLibrary::populateMasterMetedata()
883{
884 if (m_CurrentDarkFrame.isNull())
885 return;
886
887 QVariant value;
888 // TS
889 if (m_CurrentDarkFrame->getRecordValue("DATE-OBS", value))
890 masterTime->setText(value.toString());
891 // Temperature
892 if (m_CurrentDarkFrame->getRecordValue("CCD-TEMP", value) && value.toDouble() < 100)
893 masterTemperature->setText(QString::number(value.toDouble(), 'f', 1));
894 // Exposure
895 if (m_CurrentDarkFrame->getRecordValue("EXPTIME", value))
896 masterExposure->setText(value.toString());
897 // Median
898 {
899 double median = m_CurrentDarkFrame->getAverageMedian();
900 if (median > 0)
901 masterMedian->setText(QString::number(median, 'f', 1));
902 }
903 // Mean
904 {
905 double mean = m_CurrentDarkFrame->getAverageMean();
906 masterMean->setText(QString::number(mean, 'f', 1));
907 }
908 // Standard Deviation
909 {
910 double stddev = m_CurrentDarkFrame->getAverageStdDev();
911 masterDeviation->setText(QString::number(stddev, 'f', 1));
912 }
913}
914
915///////////////////////////////////////////////////////////////////////////////////////
916///
917///////////////////////////////////////////////////////////////////////////////////////
918///////////////////////////////////////////////////////////////////////////////////////
919///
920///////////////////////////////////////////////////////////////////////////////////////
921void DarkLibrary::loadIndexInView(int row)
922{
923 QSqlRecord record = darkFramesModel->record(row);
924 QString filename = record.value("filename").toString();
925 // Avoid duplicate loads
926 if (m_DarkView->imageData().isNull() || m_DarkView->imageData()->filename() != filename)
927 m_DarkView->loadFile(filename);
928}
929
930///////////////////////////////////////////////////////////////////////////////////////
931///
932///////////////////////////////////////////////////////////////////////////////////////
933bool DarkLibrary::setCamera(ISD::Camera * device)
934{
935 if (m_Camera == device)
936 return false;
937
938 if (m_Camera)
939 m_Camera->disconnect(this);
940
941 m_Camera = device;
942
943 if (m_Camera)
944 {
945 darkTabsWidget->setEnabled(true);
946 checkCamera();
947 // JM 2024.03.09: Add a bandaid for a mysteroius crash that sometimes happen
948 // when loading dark frame on Ekos startup. The crash occurs in cfitsio
949 // Hopefully this delay might fix it
950 QTimer::singleShot(1000, this, &DarkLibrary::reloadDarksFromDatabase);
951 return true;
952 }
953 else
954 {
955 darkTabsWidget->setEnabled(false);
956 return false;
957 }
958}
959
960///////////////////////////////////////////////////////////////////////////////////////
961///
962///////////////////////////////////////////////////////////////////////////////////////
963void DarkLibrary::removeDevice(const QSharedPointer<ISD::GenericDevice> &device)
964{
965 if (m_Camera && m_Camera->getDeviceName() == device->getDeviceName())
966 {
967 m_Camera->disconnect(this);
968 m_Camera = nullptr;
969 }
970}
971
972///////////////////////////////////////////////////////////////////////////////////////
973///
974///////////////////////////////////////////////////////////////////////////////////////
975void DarkLibrary::checkCamera()
976{
977 if (!m_Camera)
978 return;
979
980 auto device = m_Camera->getDeviceName();
981
982 m_TargetChip = nullptr;
983 // FIXME TODO
984 // Need to figure guide head
985 if (device.contains("Guider"))
986 {
987 m_UseGuideHead = true;
988 m_TargetChip = m_Camera->getChip(ISD::CameraChip::GUIDE_CCD);
989 }
990
991 if (m_TargetChip == nullptr)
992 {
993 m_UseGuideHead = false;
994 m_TargetChip = m_Camera->getChip(ISD::CameraChip::PRIMARY_CCD);
995 }
996
997 // Make sure we have a valid chip and valid base device.
998 // Make sure we are not in capture process.
999 if (!m_TargetChip || !m_TargetChip->getCCD() || m_TargetChip->isCapturing())
1000 return;
1001
1002 if (m_Camera->hasCoolerControl())
1003 {
1004 temperatureLabel->setEnabled(true);
1005 temperatureStepLabel->setEnabled(true);
1006 temperatureToLabel->setEnabled(true);
1007 temperatureStepSpin->setEnabled(true);
1008 minTemperatureSpin->setEnabled(true);
1009 maxTemperatureSpin->setEnabled(true);
1010
1011 // Get default temperature
1012 double temperature = 0;
1013 // Update if no setting was previously set
1014 if (m_Camera->getTemperature(&temperature))
1015 {
1016 minTemperatureSpin->setValue(temperature);
1017 maxTemperatureSpin->setValue(temperature);
1018 }
1019
1020 }
1021 else
1022 {
1023 temperatureLabel->setEnabled(false);
1024 temperatureStepLabel->setEnabled(false);
1025 temperatureToLabel->setEnabled(false);
1026 temperatureStepSpin->setEnabled(false);
1027 minTemperatureSpin->setEnabled(false);
1028 maxTemperatureSpin->setEnabled(false);
1029 }
1030
1031 QStringList isoList = m_TargetChip->getISOList();
1032 captureISOS->blockSignals(true);
1033 captureISOS->clear();
1034
1035 // No ISO range available
1036 if (isoList.isEmpty())
1037 {
1038 captureISOS->setEnabled(false);
1039 }
1040 else
1041 {
1042 captureISOS->setEnabled(true);
1043 captureISOS->addItems(isoList);
1044 captureISOS->setCurrentIndex(m_TargetChip->getISOIndex());
1045 }
1046 captureISOS->blockSignals(false);
1047
1048 // Gain Check
1049 if (m_Camera->hasGain())
1050 {
1051 double min, max, step, value, targetCustomGain;
1052 m_Camera->getGainMinMaxStep(&min, &max, &step);
1053
1054 // Allow the possibility of no gain value at all.
1055 GainSpinSpecialValue = min - step;
1056 captureGainN->setRange(GainSpinSpecialValue, max);
1057 captureGainN->setSpecialValueText(i18n("--"));
1058 captureGainN->setEnabled(true);
1059 captureGainN->setSingleStep(step);
1060 m_Camera->getGain(&value);
1061
1062 targetCustomGain = getGain();
1063
1064 // Set the custom gain if we have one
1065 // otherwise it will not have an effect.
1066 if (targetCustomGain > 0)
1067 captureGainN->setValue(targetCustomGain);
1068 else
1069 captureGainN->setValue(GainSpinSpecialValue);
1070
1071 captureGainN->setReadOnly(m_Camera->getGainPermission() == IP_RO);
1072 }
1073 else
1074 captureGainN->setEnabled(false);
1075
1076 countDarkTotalTime();
1077
1078}
1079
1080///////////////////////////////////////////////////////////////////////////////////////
1081///
1082///////////////////////////////////////////////////////////////////////////////////////
1083void DarkLibrary::countDarkTotalTime()
1084{
1085 double temperatureCount = 1;
1086 if (m_Camera && m_Camera->hasCoolerControl() && std::abs(maxTemperatureSpin->value() - minTemperatureSpin->value()) > 0)
1087 temperatureCount = (std::abs((maxTemperatureSpin->value() - minTemperatureSpin->value())) / temperatureStepSpin->value()) +
1088 1;
1089 int binnings = 0;
1090 if (bin1Check->isChecked())
1091 binnings++;
1092 if (bin2Check->isChecked())
1093 binnings++;
1094 if (bin4Check->isChecked())
1095 binnings++;
1096
1097 double darkTime = 0;
1098 int imagesCount = 0;
1099 for (double startExposure = minExposureSpin->value(); startExposure <= maxExposureSpin->value();
1100 startExposure += exposureStepSin->value())
1101 {
1104 }
1105
1106 totalTime->setText(QString::number(darkTime / 60.0, 'f', 1));
1108 darkProgress->setMaximum(imagesCount);
1109
1110}
1111
1112///////////////////////////////////////////////////////////////////////////////////////
1113///
1114///////////////////////////////////////////////////////////////////////////////////////
1115void DarkLibrary::generateDarkJobs()
1116{
1117 // Always clear sequence queue before starting
1118 m_CaptureModule->clearSequenceQueue();
1119
1120 if (m_JobsGenerated == false)
1121 {
1122 m_JobsGenerated = true;
1123 m_CaptureModuleSettings = m_CaptureModule->cameraUI->getAllSettings();
1124 }
1125
1127 if (m_Camera->hasCoolerControl() && std::fabs(maxTemperatureSpin->value() - minTemperatureSpin->value()) >= 0)
1128 {
1129 for (double oneTemperature = minTemperatureSpin->value(); oneTemperature <= maxTemperatureSpin->value();
1131 {
1133 }
1134
1135 // Enforce temperature set
1136 m_CaptureModule->cameraUI->setForceTemperature(true);
1137 }
1138 else
1139 {
1140 // Disable temperature set
1141 m_CaptureModule->cameraUI->setForceTemperature(false);
1142 temperatures << INVALID_VALUE;
1143 }
1144
1146 if (bin1Check->isChecked())
1147 bins << 1;
1148 if (bin2Check->isChecked())
1149 bins << 2;
1150 if (bin4Check->isChecked())
1151 bins << 4;
1152
1154 for (double oneExposure = minExposureSpin->value(); oneExposure <= maxExposureSpin->value();
1155 oneExposure += exposureStepSin->value())
1156 {
1158 }
1159
1162
1163
1164 int sequence = 0;
1165 for (auto &oneTemperature : temperatures)
1166 {
1167 for (auto &oneExposure : exposures)
1168 {
1169 for (auto &oneBin : bins)
1170 {
1171 sequence++;
1172 QVariantMap settings;
1173
1174 settings["opticalTrainCombo"] = opticalTrainCombo->currentText();
1175 settings["captureExposureN"] = oneExposure;
1176 settings["captureBinHN"] = oneBin;
1177 settings["captureFormatS"] = "Dark";
1178 settings["cameraTemperatureN"] = oneTemperature;
1179 if (captureGainN->isEnabled())
1180 settings["captureGainN"] = captureGainN->value();
1181 if (captureISOS->isEnabled())
1182 settings["captureISOS"] = captureISOS->currentText();
1183
1184 settings["fileDirectoryT"] = QString(prefix + QString("sequence_%1").arg(sequence));
1185 settings["captureCountN"] = countSpin->value();
1186
1187 m_CaptureModule->cameraUI->setAllSettings(settings);
1188 m_CaptureModule->cameraUI->createJob();
1189 }
1190 }
1191 }
1192}
1193
1194///////////////////////////////////////////////////////////////////////////////////////
1195///
1196///////////////////////////////////////////////////////////////////////////////////////
1197void DarkLibrary::execute()
1198{
1199 m_DarkImagesCounter = 0;
1200 darkProgress->setValue(0);
1201 darkProgress->setTextVisible(true);
1202 connect(m_CaptureModule, &Capture::newImage, this, &DarkLibrary::processNewImage, Qt::UniqueConnection);
1203 connect(m_CaptureModule, &Capture::newStatus, this, &DarkLibrary::setCaptureState, Qt::UniqueConnection);
1204 connect(m_Camera, &ISD::Camera::propertyUpdated, this, &DarkLibrary::updateProperty, Qt::UniqueConnection);
1205
1206 Options::setUseFITSViewer(false);
1207 Options::setUseSummaryPreview(false);
1208 startB->setEnabled(false);
1209 stopB->setEnabled(true);
1210 m_DarkView->reset();
1211 m_StatusLabel->setText(i18n("In progress..."));
1212 m_CaptureModule->start();
1213
1214}
1215
1216///////////////////////////////////////////////////////////////////////////////////////
1217///
1218///////////////////////////////////////////////////////////////////////////////////////
1219void DarkLibrary::stop()
1220{
1221 m_CaptureModule->abort();
1222 darkProgress->setValue(0);
1223 m_DarkView->reset();
1224}
1225
1226///////////////////////////////////////////////////////////////////////////////////////
1227///
1228///////////////////////////////////////////////////////////////////////////////////////
1229void DarkLibrary::initView()
1230{
1231 m_DarkView.reset(new DarkView(darkWidget));
1232 m_DarkView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1233 m_DarkView->setBaseSize(darkWidget->size());
1234 m_DarkView->createFloatingToolBar();
1236 vlayout->addWidget(m_DarkView.get());
1237 darkWidget->setLayout(vlayout);
1238 connect(m_DarkView.get(), &DarkView::loaded, this, [this]()
1239 {
1240 emit newImage(m_DarkView->imageData());
1241 });
1242}
1243
1244///////////////////////////////////////////////////////////////////////////////////////
1245///
1246///////////////////////////////////////////////////////////////////////////////////////
1247void DarkLibrary::aggregate(const QSharedPointer<FITSData> &data)
1248{
1249 switch (data->dataType())
1250 {
1251 case TBYTE:
1253 break;
1254
1255 case TSHORT:
1257 break;
1258
1259 case TUSHORT:
1261 break;
1262
1263 case TLONG:
1265 break;
1266
1267 case TULONG:
1269 break;
1270
1271 case TFLOAT:
1273 break;
1274
1275 case TLONGLONG:
1277 break;
1278
1279 case TDOUBLE:
1281 break;
1282
1283 default:
1284 break;
1285 }
1286}
1287
1288///////////////////////////////////////////////////////////////////////////////////////
1289///
1290///////////////////////////////////////////////////////////////////////////////////////
1291template <typename T>
1292void DarkLibrary::aggregateInternal(const QSharedPointer<FITSData> &data)
1293{
1294 T const *darkBuffer = reinterpret_cast<T const*>(data->getImageBuffer());
1295 for (uint32_t i = 0; i < m_DarkMasterBuffer.size(); i++)
1296 m_DarkMasterBuffer[i] += darkBuffer[i];
1297}
1298
1299///////////////////////////////////////////////////////////////////////////////////////
1300///
1301///////////////////////////////////////////////////////////////////////////////////////
1302void DarkLibrary::generateMasterFrame(const QSharedPointer<FITSData> &data, const QJsonObject &metadata)
1303{
1304 switch (data->dataType())
1305 {
1306 case TBYTE:
1308 break;
1309
1310 case TSHORT:
1312 break;
1313
1314 case TUSHORT:
1316 break;
1317
1318 case TLONG:
1320 break;
1321
1322 case TULONG:
1324 break;
1325
1326 case TFLOAT:
1327 generateMasterFrameInternal<float>(data, metadata);
1328 break;
1329
1330 case TLONGLONG:
1332 break;
1333
1334 case TDOUBLE:
1336 break;
1337
1338 default:
1339 break;
1340 }
1341
1342 emit newImage(data);
1343 // Reset Master Buffer
1344 m_DarkMasterBuffer.assign(m_DarkMasterBuffer.size(), 0);
1345
1346}
1347
1348///////////////////////////////////////////////////////////////////////////////////////
1349///
1350///////////////////////////////////////////////////////////////////////////////////////
1351template <typename T> void DarkLibrary::generateMasterFrameInternal(const QSharedPointer<FITSData> &data,
1352 const QJsonObject &metadata)
1353{
1354 T *writableBuffer = reinterpret_cast<T *>(data->getWritableImageBuffer());
1355 const uint32_t count = metadata["count"].toInt();
1356 // Average the values
1357 for (uint32_t i = 0; i < m_DarkMasterBuffer.size(); i++)
1358 writableBuffer[i] = m_DarkMasterBuffer[i] / count;
1359
1360 QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss");
1361 QString path = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("darks/darkframe_" + ts +
1362 data->extension());
1363
1364 data->calculateStats(true);
1365 if (!data->saveImage(path))
1366 {
1367 m_FileLabel->setText(i18n("Failed to save master frame: %1", data->getLastError()));
1368 return;
1369 }
1370
1371 auto memoryMB = KSUtils::getAvailableRAM() / 1e6;
1372 if (memoryMB > CACHE_MEMORY_LIMIT)
1373 cacheDarkFrameFromFile(data->filename());
1374
1375 QVariantMap map;
1376 map["ccd"] = metadata["camera"].toString();
1377 map["chip"] = metadata["chip"].toInt();
1378 map["binX"] = metadata["binx"].toInt();
1379 map["binY"] = metadata["biny"].toInt();
1380 map["temperature"] = metadata["temperature"].toDouble(INVALID_VALUE);
1381 map["gain"] = metadata["gain"].toInt(-1);
1382 map["iso"] = metadata["iso"].toString();
1383 map["duration"] = metadata["duration"].toDouble();
1384 map["filename"] = path;
1386
1387 m_DarkFramesDatabaseList.append(map);
1388 m_FileLabel->setText(i18n("Master Dark saved to %1", path));
1389 KStarsData::Instance()->userdb()->AddDarkFrame(map);
1390}
1391
1392///////////////////////////////////////////////////////////////////////////////////////
1393///
1394///////////////////////////////////////////////////////////////////////////////////////
1395void DarkLibrary::setCaptureModule(Capture *instance)
1396{
1397 m_CaptureModule = instance;
1398}
1399
1400///////////////////////////////////////////////////////////////////////////////////////
1401///
1402///////////////////////////////////////////////////////////////////////////////////////
1403void DarkLibrary::setCaptureState(CaptureState state)
1404{
1405 switch (state)
1406 {
1407 case CAPTURE_ABORTED:
1408 setCompleted();
1409 m_StatusLabel->setText(i18n("Capture aborted."));
1410 break;
1411 case CAPTURE_COMPLETE:
1412 setCompleted();
1413 m_StatusLabel->setText(i18n("Capture completed."));
1414 break;
1415 default:
1416 break;
1417 }
1418}
1419
1420///////////////////////////////////////////////////////////////////////////////////////
1421///
1422///////////////////////////////////////////////////////////////////////////////////////
1423void DarkLibrary::saveDefectMap()
1424{
1425 if (!m_CurrentDarkFrame)
1426 return;
1427
1428 QString filename = m_CurrentDefectMap->filename();
1429 bool newFile = false;
1430 if (filename.isEmpty())
1431 {
1432 QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss");
1433 filename = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("defectmaps/defectmap_" + ts +
1434 ".json");
1435 newFile = true;
1436 }
1437
1438 if (m_CurrentDefectMap->save(filename, m_Camera->getDeviceName()))
1439 {
1440 m_FileLabel->setText(i18n("Defect map saved to %1", filename));
1441
1442 if (newFile)
1443 {
1444 auto currentMap = std::find_if(m_DarkFramesDatabaseList.begin(),
1445 m_DarkFramesDatabaseList.end(), [&](const QVariantMap & oneMap)
1446 {
1447 return oneMap["filename"].toString() == m_CurrentDarkFrame->filename();
1448 });
1449
1450 if (currentMap != m_DarkFramesDatabaseList.end())
1451 {
1452 (*currentMap)["defectmap"] = filename;
1453 (*currentMap)["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate);
1454 KStarsData::Instance()->userdb()->UpdateDarkFrame(*currentMap);
1455 }
1456 }
1457 }
1458 else
1459 {
1460 m_FileLabel->setText(i18n("Failed to save defect map to %1", filename));
1461 }
1462}
1463
1464///////////////////////////////////////////////////////////////////////////////////////
1465///
1466///////////////////////////////////////////////////////////////////////////////////////
1467void DarkLibrary::start()
1468{
1469 generateDarkJobs();
1470 execute();
1471}
1472
1473///////////////////////////////////////////////////////////////////////////////////////
1474///
1475///////////////////////////////////////////////////////////////////////////////////////
1476void DarkLibrary::setCameraPresets(const QJsonObject &settings)
1477{
1478 const auto opticalTrain = settings["optical_train"].toString();
1479 const auto isDarkPrefer = settings["isDarkPrefer"].toBool(preferDarksRadio->isChecked());
1480 const auto isDefectPrefer = settings["isDefectPrefer"].toBool(preferDefectsRadio->isChecked());
1481 opticalTrainCombo->setCurrentText(opticalTrain);
1482 preferDarksRadio->setChecked(isDarkPrefer);
1484 checkCamera();
1485 reloadDarksFromDatabase();
1486}
1487
1488///////////////////////////////////////////////////////////////////////////////////////
1489///
1490///////////////////////////////////////////////////////////////////////////////////////
1491QJsonObject DarkLibrary::getCameraPresets()
1492{
1494 {
1495 {"optical_train", opticalTrainCombo->currentText()},
1496 {"preferDarksRadio", preferDarksRadio->isChecked()},
1497 {"preferDefectsRadio", preferDefectsRadio->isChecked()},
1498 {"fileName", m_FileLabel->text()}
1499 };
1500 return cameraSettings;
1501}
1502
1503///////////////////////////////////////////////////////////////////////////////////////
1504///
1505///////////////////////////////////////////////////////////////////////////////////////
1506QJsonArray DarkLibrary::getViewMasters()
1507{
1508 QJsonArray array;
1509
1510 for(int i = 0; i < darkFramesModel->rowCount(); i++)
1511 {
1512 QSqlRecord record = darkFramesModel->record(i);
1513 auto camera = record.value("ccd").toString();
1514 auto binX = record.value("binX").toInt();
1515 auto binY = record.value("binY").toInt();
1516 auto temperature = record.value("temperature").toDouble();
1517 auto duration = record.value("duration").toDouble();
1518 auto ts = record.value("timestamp").toString();
1519 auto gain = record.value("gain").toInt();
1520 auto iso = record.value("iso").toString();
1521
1523 {
1524 {"camera", camera},
1525 {"binX", binX},
1526 {"binY", binY},
1527 {"temperature", temperature},
1528 {"duaration", duration},
1529 {"ts", ts}
1530 };
1531
1532 if (gain >= 0)
1533 filterRows["gain"] = gain;
1534 if (!iso.isEmpty())
1535 filterRows["iso"] = iso;
1536
1537 array.append(filterRows);
1538 }
1539 return array;
1540}
1541
1542///////////////////////////////////////////////////////////////////////////////////////
1543///
1544///////////////////////////////////////////////////////////////////////////////////////
1545void DarkLibrary::setDefectPixels(const QJsonObject &payload)
1546{
1547 const auto hotSpin = payload["hotSpin"].toInt();
1548 const auto coldSpin = payload["coldSpin"].toInt();
1549 const auto hotEnabled = payload["hotEnabled"].toBool(hotPixelsEnabled->isChecked());
1550 const auto coldEnabled = payload["coldEnabled"].toBool(coldPixelsEnabled->isChecked());
1551
1552 hotPixelsEnabled->setChecked(hotEnabled);
1553 coldPixelsEnabled->setChecked(coldEnabled);
1554
1555 aggresivenessHotSpin->setValue(hotSpin);
1557
1558 m_DarkView->ZoomDefault();
1559
1560 setDefectMapEnabled(true);
1561 generateMapB->click();
1562}
1563
1564///////////////////////////////////////////////////////////////////////////////////////
1565///
1566///////////////////////////////////////////////////////////////////////////////////////
1567void DarkLibrary::setDefectMapEnabled(bool enabled)
1568{
1569 m_DarkView->setDefectMapEnabled(enabled);
1570}
1571
1572///////////////////////////////////////////////////////////////////////////////////////
1573///
1574///////////////////////////////////////////////////////////////////////////////////////
1575double DarkLibrary::getGain()
1576{
1577 // Gain is manifested in two forms
1578 // Property CCD_GAIN and
1579 // Part of CCD_CONTROLS properties.
1580 // Therefore, we have to find what the currently camera supports first.
1581 auto gain = m_Camera->getProperty("CCD_GAIN");
1582 if (gain)
1583 return gain.getNumber()->at(0)->value;
1584
1585
1586 auto controls = m_Camera->getProperty("CCD_CONTROLS");
1587 if (controls)
1588 {
1589 auto oneGain = controls.getNumber()->findWidgetByName("Gain");
1590 if (oneGain)
1591 return oneGain->value;
1592 }
1593
1594 return -1;
1595}
1596
1597///////////////////////////////////////////////////////////////////////////////////////
1598///
1599///////////////////////////////////////////////////////////////////////////////////////
1600void DarkLibrary::setupOpticalTrainManager()
1601{
1602 connect(OpticalTrainManager::Instance(), &OpticalTrainManager::updated, this, &DarkLibrary::refreshOpticalTrain);
1603 connect(trainB, &QPushButton::clicked, this, [this]()
1604 {
1605 OpticalTrainManager::Instance()->openEditor(opticalTrainCombo->currentText());
1606 });
1608 {
1609 ProfileSettings::Instance()->setOneSetting(ProfileSettings::DarkLibraryOpticalTrain,
1610 OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(index)));
1611 refreshOpticalTrain();
1612 emit trainChanged();
1613 });
1614}
1615
1616///////////////////////////////////////////////////////////////////////////////////////
1617///
1618///////////////////////////////////////////////////////////////////////////////////////
1619void DarkLibrary::refreshOpticalTrain()
1620{
1621 opticalTrainCombo->blockSignals(true);
1622 opticalTrainCombo->clear();
1623 opticalTrainCombo->addItems(OpticalTrainManager::Instance()->getTrainNames());
1624 trainB->setEnabled(true);
1625
1626 QVariant trainID = ProfileSettings::Instance()->getOneSetting(ProfileSettings::DarkLibraryOpticalTrain);
1627
1628 if (trainID.isValid())
1629 {
1630 auto id = trainID.toUInt();
1631
1632 // If train not found, select the first one available.
1633 if (OpticalTrainManager::Instance()->exists(id) == false)
1634 {
1635 emit newLog(i18n("Optical train doesn't exist for id %1", id));
1636 id = OpticalTrainManager::Instance()->id(opticalTrainCombo->itemText(0));
1637 }
1638
1639 auto name = OpticalTrainManager::Instance()->name(id);
1640
1641 opticalTrainCombo->setCurrentText(name);
1642
1643 auto camera = OpticalTrainManager::Instance()->getCamera(name);
1644 if (camera)
1645 {
1646 auto scope = OpticalTrainManager::Instance()->getScope(name);
1647 opticalTrainCombo->setToolTip(QString("%1 @ %2").arg(camera->getDeviceName(), scope["name"].toString()));
1648 }
1649 setCamera(camera);
1650
1651 // Load train settings
1652 OpticalTrainSettings::Instance()->setOpticalTrainID(id);
1653 auto settings = OpticalTrainSettings::Instance()->getOneSetting(OpticalTrainSettings::DarkLibrary);
1654 if (settings.isValid())
1655 {
1656 auto map = settings.toJsonObject().toVariantMap();
1657 if (map != m_Settings)
1658 {
1659 m_Settings.clear();
1660 setAllSettings(map);
1661 }
1662 }
1663 else
1664 m_Settings = m_GlobalSettings;
1665 }
1666
1667 opticalTrainCombo->blockSignals(false);
1668}
1669
1670///////////////////////////////////////////////////////////////////////////////////////
1671///
1672///////////////////////////////////////////////////////////////////////////////////////
1673void DarkLibrary::loadGlobalSettings()
1674{
1675 QString key;
1676 QVariant value;
1677
1678 QVariantMap settings;
1679 // All Combo Boxes
1680 for (auto &oneWidget : findChildren<QComboBox*>())
1681 {
1682 if (oneWidget->objectName() == "opticalTrainCombo")
1683 continue;
1684
1685 key = oneWidget->objectName();
1686 value = Options::self()->property(key.toLatin1());
1687 if (value.isValid())
1688 {
1689 oneWidget->setCurrentText(value.toString());
1690 settings[key] = value;
1691 }
1692 }
1693
1694 // All Double Spin Boxes
1695 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
1696 {
1697 key = oneWidget->objectName();
1698 value = Options::self()->property(key.toLatin1());
1699 if (value.isValid())
1700 {
1701 oneWidget->setValue(value.toDouble());
1702 settings[key] = value;
1703 }
1704 }
1705
1706 // All Spin Boxes
1707 for (auto &oneWidget : findChildren<QSpinBox*>())
1708 {
1709 key = oneWidget->objectName();
1710 value = Options::self()->property(key.toLatin1());
1711 if (value.isValid())
1712 {
1713 oneWidget->setValue(value.toInt());
1714 settings[key] = value;
1715 }
1716 }
1717
1718 // All Checkboxes
1719 for (auto &oneWidget : findChildren<QCheckBox*>())
1720 {
1721 key = oneWidget->objectName();
1722 value = Options::self()->property(key.toLatin1());
1723 if (value.isValid())
1724 {
1725 oneWidget->setChecked(value.toBool());
1726 settings[key] = value;
1727 }
1728 }
1729
1730 m_GlobalSettings = m_Settings = settings;
1731}
1732
1733
1734///////////////////////////////////////////////////////////////////////////////////////
1735///
1736///////////////////////////////////////////////////////////////////////////////////////
1737void DarkLibrary::connectSettings()
1738{
1739 // All Combo Boxes
1740 for (auto &oneWidget : findChildren<QComboBox*>())
1741 connect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::DarkLibrary::syncSettings);
1742
1743 // All Double Spin Boxes
1744 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
1745 connect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::DarkLibrary::syncSettings);
1746
1747 // All Spin Boxes
1748 for (auto &oneWidget : findChildren<QSpinBox*>())
1749 connect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::DarkLibrary::syncSettings);
1750
1751 // All Checkboxes
1752 for (auto &oneWidget : findChildren<QCheckBox*>())
1753 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::DarkLibrary::syncSettings);
1754
1755 // All Radio buttons
1756 for (auto &oneWidget : findChildren<QRadioButton*>())
1757 connect(oneWidget, &QCheckBox::toggled, this, &Ekos::DarkLibrary::syncSettings);
1758
1759 // Train combo box should NOT be synced.
1760 disconnect(opticalTrainCombo, QOverload<int>::of(&QComboBox::activated), this, &Ekos::DarkLibrary::syncSettings);
1761}
1762
1763///////////////////////////////////////////////////////////////////////////////////////
1764///
1765///////////////////////////////////////////////////////////////////////////////////////
1766void DarkLibrary::disconnectSettings()
1767{
1768 // All Combo Boxes
1769 for (auto &oneWidget : findChildren<QComboBox*>())
1770 disconnect(oneWidget, QOverload<int>::of(&QComboBox::activated), this, &Ekos::DarkLibrary::syncSettings);
1771
1772 // All Double Spin Boxes
1773 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
1774 disconnect(oneWidget, &QDoubleSpinBox::editingFinished, this, &Ekos::DarkLibrary::syncSettings);
1775
1776 // All Spin Boxes
1777 for (auto &oneWidget : findChildren<QSpinBox*>())
1778 disconnect(oneWidget, &QSpinBox::editingFinished, this, &Ekos::DarkLibrary::syncSettings);
1779
1780 // All Checkboxes
1781 for (auto &oneWidget : findChildren<QCheckBox*>())
1782 disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::DarkLibrary::syncSettings);
1783
1784 // All Radio buttons
1785 for (auto &oneWidget : findChildren<QRadioButton*>())
1786 disconnect(oneWidget, &QCheckBox::toggled, this, &Ekos::DarkLibrary::syncSettings);
1787
1788}
1789
1790///////////////////////////////////////////////////////////////////////////////////////////
1791///
1792///////////////////////////////////////////////////////////////////////////////////////////
1793QVariantMap DarkLibrary::getAllSettings() const
1794{
1795 QVariantMap settings;
1796
1797 // All Combo Boxes
1798 for (auto &oneWidget : findChildren<QComboBox*>())
1799 settings.insert(oneWidget->objectName(), oneWidget->currentText());
1800
1801 // All Double Spin Boxes
1802 for (auto &oneWidget : findChildren<QDoubleSpinBox*>())
1803 settings.insert(oneWidget->objectName(), oneWidget->value());
1804
1805 // All Spin Boxes
1806 for (auto &oneWidget : findChildren<QSpinBox*>())
1807 settings.insert(oneWidget->objectName(), oneWidget->value());
1808
1809 // All Checkboxes
1810 for (auto &oneWidget : findChildren<QCheckBox*>())
1811 settings.insert(oneWidget->objectName(), oneWidget->isChecked());
1812
1813 return settings;
1814}
1815
1816///////////////////////////////////////////////////////////////////////////////////////////
1817///
1818///////////////////////////////////////////////////////////////////////////////////////////
1819void DarkLibrary::setAllSettings(const QVariantMap &settings)
1820{
1821 // Disconnect settings that we don't end up calling syncSettings while
1822 // performing the changes.
1823 disconnectSettings();
1824
1825 for (auto &name : settings.keys())
1826 {
1827 // Combo
1828 auto comboBox = findChild<QComboBox*>(name);
1829 if (comboBox)
1830 {
1831 syncControl(settings, name, comboBox);
1832 continue;
1833 }
1834
1835 // Double spinbox
1836 auto doubleSpinBox = findChild<QDoubleSpinBox*>(name);
1837 if (doubleSpinBox)
1838 {
1839 syncControl(settings, name, doubleSpinBox);
1840 continue;
1841 }
1842
1843 // spinbox
1844 auto spinBox = findChild<QSpinBox*>(name);
1845 if (spinBox)
1846 {
1847 syncControl(settings, name, spinBox);
1848 continue;
1849 }
1850
1851 // checkbox
1852 auto checkbox = findChild<QCheckBox*>(name);
1853 if (checkbox)
1854 {
1855 syncControl(settings, name, checkbox);
1856 continue;
1857 }
1858
1859 // Radio button
1861 if (radioButton)
1862 {
1863 syncControl(settings, name, radioButton);
1864 continue;
1865 }
1866 }
1867
1868 // Sync to options
1869 for (auto &key : settings.keys())
1870 {
1871 auto value = settings[key];
1872 // Save immediately
1873 Options::self()->setProperty(key.toLatin1(), value);
1874
1875 m_Settings[key] = value;
1876 m_GlobalSettings[key] = value;
1877 }
1878
1879 emit settingsUpdated(getAllSettings());
1880
1881 // Save to optical train specific settings as well
1882 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
1883 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::DarkLibrary, m_Settings);
1884
1885 // Restablish connections
1886 connectSettings();
1887}
1888
1889///////////////////////////////////////////////////////////////////////////////////////////
1890///
1891///////////////////////////////////////////////////////////////////////////////////////////
1892bool DarkLibrary::syncControl(const QVariantMap &settings, const QString &key, QWidget * widget)
1893{
1894 QSpinBox *pSB = nullptr;
1895 QDoubleSpinBox *pDSB = nullptr;
1896 QCheckBox *pCB = nullptr;
1897 QComboBox *pComboBox = nullptr;
1898 QRadioButton *pRadioButton = nullptr;
1899 bool ok = false;
1900
1901 if ((pSB = qobject_cast<QSpinBox *>(widget)))
1902 {
1903 const int value = settings[key].toInt(&ok);
1904 if (ok)
1905 {
1906 pSB->setValue(value);
1907 return true;
1908 }
1909 }
1910 else if ((pDSB = qobject_cast<QDoubleSpinBox *>(widget)))
1911 {
1912 const double value = settings[key].toDouble(&ok);
1913 if (ok)
1914 {
1915 pDSB->setValue(value);
1916 return true;
1917 }
1918 }
1919 else if ((pCB = qobject_cast<QCheckBox *>(widget)))
1920 {
1921 const bool value = settings[key].toBool();
1922 if (value != pCB->isChecked())
1923 pCB->click();
1924 return true;
1925 }
1926 // ONLY FOR STRINGS, not INDEX
1927 else if ((pComboBox = qobject_cast<QComboBox *>(widget)))
1928 {
1929 const QString value = settings[key].toString();
1930 pComboBox->setCurrentText(value);
1931 return true;
1932 }
1933 else if ((pRadioButton = qobject_cast<QRadioButton *>(widget)))
1934 {
1935 const bool value = settings[key].toBool();
1936 if (value)
1937 pRadioButton->click();
1938 return true;
1939 }
1940
1941 return false;
1942};
1943
1944///////////////////////////////////////////////////////////////////////////////////////
1945///
1946///////////////////////////////////////////////////////////////////////////////////////
1947void DarkLibrary::syncSettings()
1948{
1949 QDoubleSpinBox *dsb = nullptr;
1950 QSpinBox *sb = nullptr;
1951 QCheckBox *cb = nullptr;
1952 QComboBox *cbox = nullptr;
1953 QRadioButton *cradio = nullptr;
1954
1955 QString key;
1956 QVariant value;
1957
1958 if ( (dsb = qobject_cast<QDoubleSpinBox*>(sender())))
1959 {
1960 key = dsb->objectName();
1961 value = dsb->value();
1962
1963 }
1964 else if ( (sb = qobject_cast<QSpinBox*>(sender())))
1965 {
1966 key = sb->objectName();
1967 value = sb->value();
1968 }
1969 else if ( (cb = qobject_cast<QCheckBox*>(sender())))
1970 {
1971 key = cb->objectName();
1972 value = cb->isChecked();
1973 }
1974 else if ( (cbox = qobject_cast<QComboBox*>(sender())))
1975 {
1976 key = cbox->objectName();
1977 value = cbox->currentText();
1978 }
1979 else if ( (cradio = qobject_cast<QRadioButton*>(sender())))
1980 {
1981 key = cradio->objectName();
1982 // Discard false requests
1983 if (cradio->isChecked() == false)
1984 {
1985 m_Settings.remove(key);
1986 return;
1987 }
1988 value = true;
1989 }
1990
1991 // Save immediately
1992 Options::self()->setProperty(key.toLatin1(), value);
1993 m_Settings[key] = value;
1994 m_GlobalSettings[key] = value;
1995
1996 m_DebounceTimer.start();
1997}
1998
1999///////////////////////////////////////////////////////////////////////////////////////////
2000///
2001///////////////////////////////////////////////////////////////////////////////////////////
2002void DarkLibrary::settleSettings()
2003{
2004 emit settingsUpdated(getAllSettings());
2005 Options::self()->save();
2006 // Save to optical train specific settings as well
2007 OpticalTrainSettings::Instance()->setOpticalTrainID(OpticalTrainManager::Instance()->id(opticalTrainCombo->currentText()));
2008 OpticalTrainSettings::Instance()->setOneSetting(OpticalTrainSettings::DarkLibrary, m_Settings);
2009}
2010
2011///////////////////////////////////////////////////////////////////////////////////////
2012///
2013///////////////////////////////////////////////////////////////////////////////////////
2014QJsonObject DarkLibrary::getDefectSettings()
2015{
2017 for (int i = 0; i < masterDarksCombo->count(); i++)
2018 darkMasters << masterDarksCombo->itemText(i);
2019
2021 {
2022 {"masterTime", masterTime->text()},
2023 {"masterDarks", darkMasters.join('|')},
2024 {"masterExposure", masterExposure->text()},
2025 {"masterTempreture", masterTemperature->text()},
2026 {"masterMean", masterMean->text()},
2027 {"masterMedian", masterMedian->text()},
2028 {"masterDeviation", masterDeviation->text()},
2029 {"hotPixelsEnabled", hotPixelsEnabled->isChecked()},
2030 {"coldPixelsEnabled", coldPixelsEnabled->isChecked()},
2031 };
2032 return createDefectMaps;
2033}
2034
2035
2036
2037}
2038
CameraChip class controls a particular chip in camera.
Camera class controls an INDI Camera device.
Definition indicamera.h:44
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...)
bool insert(Part *part, qint64 *insertId=nullptr)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:79
@ CAPTURE_ABORTED
Definition ekos.h:99
@ CAPTURE_COMPLETE
Definition ekos.h:112
QString path(const QString &relativePath)
QString name(StandardAction 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 Fri May 17 2024 11:48:25 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.