Kstars

fitstab.cpp
1/*
2 SPDX-FileCopyrightText: 2012 Jasem Mutlaq <mutlaqja@ikarustech.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "fitstab.h"
8
9#include "auxiliary/kspaths.h"
10#include "fitsdata.h"
11#include "fitshistogrameditor.h"
12#include "fitshistogramcommand.h"
13#include "fitsview.h"
14#include "fitsviewer.h"
15#include "ksnotification.h"
16#include "kstars.h"
17#include "Options.h"
18#include "ui_fitsheaderdialog.h"
19#include "ui_statform.h"
20#include "fitsstretchui.h"
21#include "skymap.h"
22#include <KMessageBox>
23#include <QtConcurrent>
24#include <QIcon>
25#include "ekos/auxiliary/stellarsolverprofile.h"
26#include "ekos/auxiliary/stellarsolverprofileeditor.h"
27
28#include <fits_debug.h>
29
30QPointer<Ekos::StellarSolverProfileEditor> FITSTab::m_ProfileEditor;
31QPointer<KConfigDialog> FITSTab::m_EditorDialog;
32QPointer<KPageWidgetItem> FITSTab::m_ProfileEditorPage;
33
34FITSTab::FITSTab(FITSViewer *parent) : QWidget(parent)
35{
36 viewer = parent;
37 undoStack = new QUndoStack(this);
38 undoStack->setUndoLimit(10);
39 undoStack->clear();
40 connect(undoStack, SIGNAL(cleanChanged(bool)), this, SLOT(modifyFITSState(bool)));
41
42 m_PlateSolveWidget = new QDialog(this);
43 statWidget = new QDialog(this);
44 fitsHeaderDialog = new QDialog(this);
45 m_HistogramEditor = new FITSHistogramEditor(this);
46 connect(m_HistogramEditor, &FITSHistogramEditor::newHistogramCommand, this, [this](FITSHistogramCommand * command)
47 {
48 undoStack->push(command);
49 });
50}
51
52FITSTab::~FITSTab()
53{
54}
55
56void FITSTab::saveUnsaved()
57{
58 if (undoStack->isClean() || m_View->getMode() != FITS_NORMAL)
59 return;
60
61 QString caption = i18n("Save Changes to FITS?");
62 QString message = i18n("The current FITS file has unsaved changes. Would you like to save before closing it?");
63
64 int ans = KMessageBox::warningYesNoCancel(nullptr, message, caption, KStandardGuiItem::save(), KStandardGuiItem::discard());
65 if (ans == KMessageBox::Yes)
66 saveFile();
67 if (ans == KMessageBox::No)
68 {
69 undoStack->clear();
70 modifyFITSState();
71 }
72}
73
74void FITSTab::closeEvent(QCloseEvent *ev)
75{
76 saveUnsaved();
77
78 if (undoStack->isClean())
79 ev->accept();
80 else
81 ev->ignore();
82}
83QString FITSTab::getPreviewText() const
84{
85 return previewText;
86}
87
88void FITSTab::setPreviewText(const QString &value)
89{
90 previewText = value;
91}
92
93void FITSTab::selectRecentFITS(int i)
94{
95 loadFile(QUrl::fromLocalFile(recentImages->item(i)->text()));
96}
97
98void FITSTab::clearRecentFITS()
99{
100 disconnect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS);
101 recentImages->clear();
102 connect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS);
103}
104
105bool FITSTab::setupView(FITSMode mode, FITSScale filter)
106{
107 if (m_View.isNull())
108 {
109 m_View.reset(new FITSView(this, mode, filter));
110 m_View->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
112
113 connect(m_View.get(), &FITSView::rectangleUpdated, this, [this](QRect roi)
114 {
115 displayStats(roi.isValid());
116 });
117 fitsSplitter = new QSplitter(Qt::Horizontal, this);
118 fitsTools = new QToolBox();
119
120 stat.setupUi(statWidget);
121 m_PlateSolveUI.setupUi(m_PlateSolveWidget);
122
123 m_PlateSolveUI.editProfile->setIcon(QIcon::fromTheme("document-edit"));
124 m_PlateSolveUI.editProfile->setAttribute(Qt::WA_LayoutUsesWidgetRect);
125
126 const QString EditorID = "FITSSolverProfileEditor";
127 if (!m_EditorDialog)
128 {
129 // These are static, shared by all FITS Viewer tabs.
130 m_EditorDialog = new KConfigDialog(nullptr, EditorID, Options::self());
131 m_ProfileEditor = new Ekos::StellarSolverProfileEditor(nullptr, Ekos::AlignProfiles, m_EditorDialog.data());
132 m_ProfileEditorPage = m_EditorDialog->addPage(m_ProfileEditor.data(),
133 i18n("FITS Viewer Solver Profiles Editor"));
134 }
135
136 connect(m_PlateSolveUI.editProfile, &QAbstractButton::clicked, this, [this, EditorID]
137 {
138 m_ProfileEditor->loadProfile(m_PlateSolveUI.kcfg_FitsSolverProfile->currentText());
139 KConfigDialog * d = KConfigDialog::exists(EditorID);
140 if(d)
141 {
142 d->setCurrentPage(m_ProfileEditorPage);
143 d->show();
144 }
145 });
146
147 connect(m_PlateSolveUI.SolveButton, &QPushButton::clicked, this, &FITSTab::extractImage);
148
149 for (int i = 0; i <= STAT_STDDEV; i++)
150 {
151 for (int j = 0; j < 3; j++)
152 {
153 stat.statsTable->setItem(i, j, new QTableWidgetItem());
154 stat.statsTable->item(i, j)->setTextAlignment(Qt::AlignHCenter);
155 }
156
157 // Set col span for items up to HFR
158 if (i <= STAT_HFR)
159 stat.statsTable->setSpan(i, 0, 1, 3);
160 }
161
162 fitsTools->addItem(statWidget, i18n("Statistics"));
163
164 fitsTools->addItem(m_PlateSolveWidget, i18n("Plate Solving"));
165 initSolverUI();
166
167 fitsTools->addItem(m_HistogramEditor, i18n("Histogram"));
168
169 header.setupUi(fitsHeaderDialog);
170 fitsTools->addItem(fitsHeaderDialog, i18n("FITS Header"));
171
173 QWidget *recentPanel = new QWidget(fitsSplitter);
174 recentPanel->setLayout(recentPanelLayout);
175 fitsTools->addItem(recentPanel, i18n("Recent Images"));
176 recentImages = new QListWidget(recentPanel);
177 recentPanelLayout->addWidget(recentImages);
178 QPushButton *clearRecent = new QPushButton(i18n("Clear"));
179 recentPanelLayout->addWidget(clearRecent);
180 connect(clearRecent, &QPushButton::pressed, this, &FITSTab::clearRecentFITS);
181 connect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS);
182
183 QScrollArea *scrollFitsPanel = new QScrollArea(fitsSplitter);
184 scrollFitsPanel->setWidgetResizable(true);
185 scrollFitsPanel->setWidget(fitsTools);
186
187 fitsSplitter->addWidget(scrollFitsPanel);
188 fitsSplitter->addWidget(m_View.get());
189
190
191 //This code allows the fitsTools to start in a closed state
192 fitsSplitter->setSizes(QList<int>() << 0 << m_View->width() );
193
194 vlayout->addWidget(fitsSplitter);
195
196 stretchUI.reset(new FITSStretchUI(m_View, nullptr));
197 vlayout->addWidget(stretchUI.get());
198
199 connect(fitsSplitter, &QSplitter::splitterMoved, m_HistogramEditor, &FITSHistogramEditor::resizePlot);
200
202 connect(m_View.get(), &FITSView::newStatus, this, &FITSTab::newStatus);
203 connect(m_View.get(), &FITSView::debayerToggled, this, &FITSTab::debayerToggled);
204 connect(m_View.get(), &FITSView::updated, this, &FITSTab::updated);
205
206 // On Failure to load
207 connect(m_View.get(), &FITSView::failed, this, &FITSTab::failed);
208
209 return true;
210 }
211
212 // returns false if no setup needed.
213 return false;
214}
215
216void FITSTab::loadFile(const QUrl &imageURL, FITSMode mode, FITSScale filter)
217{
218 // check if the address points to an appropriate address
219 if (imageURL.isEmpty() || !imageURL.isValid() || !QFileInfo::exists(imageURL.toLocalFile()))
220 return;
221
222 if (setupView(mode, filter))
223 {
224
225 // On Success loading image
226 connect(m_View.get(), &FITSView::loaded, this, [&]()
227 {
228 processData();
229 emit loaded();
230 });
231
232 connect(m_View.get(), &FITSView::updated, this, &FITSTab::updated);
233 }
234 else
235 // update tab text
236 modifyFITSState(true, imageURL);
237
238 currentURL = imageURL;
239
240 m_View->setFilter(filter);
241
242 m_View->loadFile(imageURL.toLocalFile());
243}
244
245bool FITSTab::shouldComputeHFR() const
246{
247 if (viewer->isStarsMarked())
248 return true;
249 if (!Options::autoHFR())
250 return false;
251 return ((!m_View.isNull()) && (m_View->getMode() == FITS_NORMAL));
252}
253
254void FITSTab::processData()
255{
256 const QSharedPointer<FITSData> &imageData = m_View->imageData();
257
258 m_HistogramEditor->setImageData(imageData);
259
260 // Only construct histogram if it is actually visible
261 // Otherwise wait until histogram is needed before creating it.
262 // if (fitsSplitter->sizes().at(0) != 0 && !imageData->isHistogramConstructed() &&
263 // !Options::nonLinearHistogram())
264 // {
265 // imageData->constructHistogram();
266 // }
267
268 if (shouldComputeHFR())
269 {
270 m_View->searchStars();
271 qCDebug(KSTARS_FITS) << "FITS HFR:" << imageData->getHFR();
272 }
273
274 displayStats();
275
276 loadFITSHeader();
277
278 // Don't add it to the list if it is already there
279 if (recentImages->findItems(currentURL.toLocalFile(), Qt::MatchExactly).count() == 0)
280 {
281 //Don't add it to the list if it is a preview
282 if(!imageData->filename().startsWith(QDir::tempPath()))
283 {
284 disconnect(recentImages, &QListWidget::currentRowChanged, this,
285 &FITSTab::selectRecentFITS);
286 recentImages->addItem(imageData->filename());
287 recentImages->setCurrentRow(recentImages->count() - 1);
288 connect(recentImages, &QListWidget::currentRowChanged, this,
289 &FITSTab::selectRecentFITS);
290 }
291 }
292
293 // This could both compute the HFRs and setup the graphics, however,
294 // if shouldComputeHFR() above is true, then that will compute the HFRs
295 // and this would notice that and just setup graphics. They are separated
296 // for the case where the graphics is not desired.
297 if (viewer->isStarsMarked())
298 {
299 m_View->toggleStars(true);
300 m_View->updateFrame();
301 }
302
303 // if (Options::nonLinearHistogram())
304 // m_HistogramEditor->createNonLinearHistogram();
305
306 stretchUI->generateHistogram();
307}
308
309bool FITSTab::loadData(const QSharedPointer<FITSData> &data, FITSMode mode, FITSScale filter)
310{
311 setupView(mode, filter);
312
313 // Empty URL
314 currentURL = QUrl();
315
316 if (viewer->isStarsMarked())
317 {
318 m_View->toggleStars(true);
319 //view->updateFrame();
320 }
321
322 m_View->setFilter(filter);
323
324 if (!m_View->loadData(data))
325 {
326 // On Failure to load
327 // connect(view.get(), &FITSView::failed, this, &FITSTab::failed);
328 return false;
329 }
330
331 processData();
332 return true;
333}
334
335void FITSTab::modifyFITSState(bool clean, const QUrl &imageURL)
336{
337 if (clean)
338 {
339 if (undoStack->isClean() == false)
340 undoStack->setClean();
341
342 mDirty = false;
343 }
344 else
345 mDirty = true;
346
347 emit changeStatus(clean, imageURL);
348}
349
350bool FITSTab::saveImage(const QString &filename)
351{
352 return m_View->saveImage(filename);
353}
354
355void FITSTab::copyFITS()
356{
357 QApplication::clipboard()->setImage(m_View->getDisplayImage());
358}
359
360void FITSTab::histoFITS()
361{
362 fitsTools->setCurrentIndex(1);
363 if(m_View->width() > 200)
364 fitsSplitter->setSizes(QList<int>() << 200 << m_View->width() - 200);
365 else
366 fitsSplitter->setSizes(QList<int>() << 50 << 50);
367}
368
369void FITSTab::displayStats(bool roi)
370{
371 const QSharedPointer<FITSData> &imageData = m_View->imageData();
372
373 stat.statsTable->item(STAT_WIDTH, 0)->setText(QString::number(imageData->width(roi)));
374 stat.statsTable->item(STAT_HEIGHT, 0)->setText(QString::number(imageData->height(roi)));
375 stat.statsTable->item(STAT_BITPIX, 0)->setText(QString::number(imageData->bpp()));
376
377 if (!roi)
378 stat.statsTable->item(STAT_HFR, 0)->setText(QString::number(imageData->getHFR(), 'f', 3));
379 else
380 stat.statsTable->item(STAT_HFR, 0)->setText("---");
381
382 if (imageData->channels() == 1)
383 {
384 for (int i = STAT_MIN; i <= STAT_STDDEV; i++)
385 {
386 if (stat.statsTable->columnSpan(i, 0) != 3)
387 stat.statsTable->setSpan(i, 0, 1, 3);
388 }
389
390 stat.statsTable->horizontalHeaderItem(0)->setText(i18n("Value"));
391 stat.statsTable->hideColumn(1);
392 stat.statsTable->hideColumn(2);
393 }
394 else
395 {
396 for (int i = STAT_MIN; i <= STAT_STDDEV; i++)
397 {
398 if (stat.statsTable->columnSpan(i, 0) != 1)
399 stat.statsTable->setSpan(i, 0, 1, 1);
400 }
401
402 stat.statsTable->horizontalHeaderItem(0)->setText(i18nc("Red", "R"));
403 stat.statsTable->showColumn(1);
404 stat.statsTable->showColumn(2);
405 }
406
407 if (!Options::nonLinearHistogram() && !imageData->isHistogramConstructed())
408 imageData->constructHistogram();
409
410 for (int i = 0; i < imageData->channels(); i++)
411 {
412 stat.statsTable->item(STAT_MIN, i)->setText(QString::number(imageData->getMin(i, roi), 'f', 3));
413 stat.statsTable->item(STAT_MAX, i)->setText(QString::number(imageData->getMax(i, roi), 'f', 3));
414 stat.statsTable->item(STAT_MEAN, i)->setText(QString::number(imageData->getMean(i, roi), 'f', 3));
415 stat.statsTable->item(STAT_MEDIAN, i)->setText(QString::number(imageData->getMedian(i, roi), 'f', 3));
416 stat.statsTable->item(STAT_STDDEV, i)->setText(QString::number(imageData->getStdDev(i, roi), 'f', 3));
417 }
418}
419
420void FITSTab::statFITS()
421{
422 fitsTools->setCurrentIndex(0);
423 if(m_View->width() > 200)
424 fitsSplitter->setSizes(QList<int>() << 200 << m_View->width() - 200);
425 else
426 fitsSplitter->setSizes(QList<int>() << 50 << 50);
427 displayStats();
428}
429
430void FITSTab::loadFITSHeader()
431{
432 const QSharedPointer<FITSData> &imageData = m_View->imageData();
433
434 int nkeys = imageData->getRecords().size();
435 int counter = 0;
436 header.tableWidget->setRowCount(nkeys);
437 for (const auto &oneRecord : imageData->getRecords())
438 {
441 header.tableWidget->setItem(counter, 0, tempItem);
442 tempItem = new QTableWidgetItem(oneRecord.value.toString());
444 header.tableWidget->setItem(counter, 1, tempItem);
445 tempItem = new QTableWidgetItem(oneRecord.comment);
447 header.tableWidget->setItem(counter, 2, tempItem);
448 counter++;
449 }
450
451 header.tableWidget->setColumnWidth(0, 100);
452 header.tableWidget->setColumnWidth(1, 100);
453 header.tableWidget->setColumnWidth(2, 250);
454}
455
456void FITSTab::headerFITS()
457{
458 fitsTools->setCurrentIndex(2);
459 if(m_View->width() > 200)
460 fitsSplitter->setSizes(QList<int>() << 200 << m_View->width() - 200);
461 else
462 fitsSplitter->setSizes(QList<int>() << 50 << 50);
463}
464
465bool FITSTab::saveFile()
466{
467 QUrl backupCurrent = currentURL;
468 QUrl currentDir(Options::fitsDir());
469 currentDir.setScheme("file");
470
471 if (currentURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || currentURL.toLocalFile().contains("/Temp"))
472 currentURL.clear();
473
474 // If no changes made, return.
475 if (mDirty == false && !currentURL.isEmpty())
476 return false;
477
478 if (currentURL.isEmpty())
479 {
481#ifdef Q_OS_OSX //For some reason, the other code caused KStars to crash on MacOS
482 currentURL =
483 QFileDialog::getSaveFileUrl(KStars::Instance(), i18nc("@title:window", "Save FITS"), currentDir,
484 "Images (*.fits *.fits.gz *.fit *.xisf *.jpg *.jpeg *.png)");
485#else
486 currentURL =
487 QFileDialog::getSaveFileUrl(KStars::Instance(), i18nc("@title:window", "Save FITS"), currentDir,
488 "FITS (*.fits *.fits.gz *.fit);;XISF (*.xisf);;JPEG (*.jpg *.jpeg);;PNG (*.png)", &selectedFilter);
489#endif
490 // if user presses cancel
491 if (currentURL.isEmpty())
492 {
493 currentURL = backupCurrent;
494 return false;
495 }
496
497 // If no extension is selected append one
498 if (currentURL.toLocalFile().contains('.') == 0)
499 {
500 if (selectedFilter.contains("XISF"))
501 currentURL.setPath(currentURL.toLocalFile() + ".xisf");
502 else if (selectedFilter.contains("JPEG"))
503 currentURL.setPath(currentURL.toLocalFile() + ".jpg");
504 else if (selectedFilter.contains("PNG"))
505 currentURL.setPath(currentURL.toLocalFile() + ".png");
506 else
507 currentURL.setPath(currentURL.toLocalFile() + ".fits");
508 }
509 }
510
511 if (currentURL.isValid())
512 {
513 QString localFile = currentURL.toLocalFile();
514 // if (localFile.contains(".fit"))
515 // localFile = "!" + localFile;
516
517 if (!saveImage(localFile))
518 {
519 KSNotification::error(i18n("Image save error: %1", m_View->imageData()->getLastError()), i18n("Image Save"));
520 return false;
521 }
522
523 emit newStatus(i18n("File saved to %1", currentURL.url()), FITS_MESSAGE);
524 modifyFITSState();
525 return true;
526 }
527 else
528 {
529 QString message = i18n("Invalid URL: %1", currentURL.url());
530 KSNotification::sorry(message, i18n("Invalid URL"));
531 return false;
532 }
533}
534
535bool FITSTab::saveFileAs()
536{
537 currentURL.clear();
538 return saveFile();
539}
540
541void FITSTab::ZoomIn()
542{
543 QPoint oldCenter = m_View->getImagePoint(m_View->viewport()->rect().center());
544 m_View->ZoomIn();
545 m_View->cleanUpZoom(oldCenter);
546}
547
548void FITSTab::ZoomOut()
549{
550 QPoint oldCenter = m_View->getImagePoint(m_View->viewport()->rect().center());
551 m_View->ZoomOut();
552 m_View->cleanUpZoom(oldCenter);
553}
554
555void FITSTab::ZoomDefault()
556{
557 QPoint oldCenter = m_View->getImagePoint(m_View->viewport()->rect().center());
558 m_View->ZoomDefault();
559 m_View->cleanUpZoom(oldCenter);
560}
561
562void FITSTab::tabPositionUpdated()
563{
564 undoStack->setActive(true);
565 m_View->emitZoom();
566 emit newStatus(QString("%1x%2").arg(m_View->imageData()->width()).arg(m_View->imageData()->height()),
567 FITS_RESOLUTION);
568}
569
570void FITSTab::setStretchValues(double shadows, double midtones, double highlights)
571{
572 if (stretchUI)
573 stretchUI->setStretchValues(shadows, midtones, highlights);
574}
575
576void FITSTab::setAutoStretch()
577{
578 if (!m_View->getAutoStretch())
579 m_View->setAutoStretchParams();
580}
581
582namespace
583{
585{
587 switch(module)
588 {
589 case Ekos::AlignProfiles:
590 default:
591 savedProfiles = QDir(KSPaths::writableLocation(
592 QStandardPaths::AppLocalDataLocation)).filePath("SavedAlignProfiles.ini");
593 return QFile(savedProfiles).exists() ?
595 Ekos::getDefaultAlignOptionsProfiles();
596 break;
597 case Ekos::FocusProfiles:
598 savedProfiles = QDir(KSPaths::writableLocation(
599 QStandardPaths::AppLocalDataLocation)).filePath("SavedFocusProfiles.ini");
600 return QFile(savedProfiles).exists() ?
602 Ekos::getDefaultFocusOptionsProfiles();
603 break;
604 case Ekos::GuideProfiles:
605 savedProfiles = QDir(KSPaths::writableLocation(
606 QStandardPaths::AppLocalDataLocation)).filePath("SavedGuideProfiles.ini");
607 return QFile(savedProfiles).exists() ?
609 Ekos::getDefaultGuideOptionsProfiles();
610 break;
611 case Ekos::HFRProfiles:
612 savedProfiles = QDir(KSPaths::writableLocation(
613 QStandardPaths::AppLocalDataLocation)).filePath("SavedHFRProfiles.ini");
614 return QFile(savedProfiles).exists() ?
616 Ekos::getDefaultHFROptionsProfiles();
617 break;
618 }
619}
620} // namespace
621
622void FITSTab::setupSolver(bool extractOnly)
623{
624 auto parameters = getSSolverParametersList(static_cast<Ekos::ProfileGroup>(Options::fitsSolverModule())).at(
625 m_PlateSolveUI.kcfg_FitsSolverProfile->currentIndex());
626 parameters.search_radius = m_PlateSolveUI.kcfg_FitsSolverRadius->value();
627 if (extractOnly)
628 {
629 m_Solver.reset(new SolverUtils(parameters, parameters.solverTimeLimit, SSolver::EXTRACT), &QObject::deleteLater);
630 connect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::extractorDone, Qt::UniqueConnection);
631 }
632 else
633 {
634 m_Solver.reset(new SolverUtils(parameters, parameters.solverTimeLimit, SSolver::SOLVE), &QObject::deleteLater);
635 connect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::solverDone, Qt::UniqueConnection);
636 }
637
638 const int imageWidth = m_View->imageData()->width();
639 const int imageHeight = m_View->imageData()->height();
640 if (m_PlateSolveUI.kcfg_FitsSolverUseScale->isChecked() && imageWidth != 0 && imageHeight != 0)
641 {
642 const double scale = m_PlateSolveUI.kcfg_FitsSolverScale->value();
643 double lowScale = scale * 0.8;
644 double highScale = scale * 1.2;
645
646 // solver utils uses arcsecs per pixel only
647 const int units = m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits->currentIndex();
648 if (units == SSolver::DEG_WIDTH)
649 {
650 lowScale = (lowScale * 3600) / std::max(imageWidth, imageHeight);
651 highScale = (highScale * 3600) / std::min(imageWidth, imageHeight);
652 }
653 else if (units == SSolver::ARCMIN_WIDTH)
654 {
655 lowScale = (lowScale * 60) / std::max(imageWidth, imageHeight);
656 highScale = (highScale * 60) / std::min(imageWidth, imageHeight);
657 }
658
659 m_Solver->useScale(m_PlateSolveUI.kcfg_FitsSolverUseScale->isChecked(), lowScale, highScale);
660 }
661 else m_Solver->useScale(false, 0, 0);
662
663 if (m_PlateSolveUI.kcfg_FitsSolverUsePosition->isChecked())
664 {
665 bool ok;
666 const dms ra = m_PlateSolveUI.FitsSolverEstRA->createDms(&ok);
667 bool ok2;
668 const dms dec = m_PlateSolveUI.FitsSolverEstDec->createDms(&ok2);
669 if (ok && ok2)
670 m_Solver->usePosition(true, ra.Degrees(), dec.Degrees());
671 else
672 m_Solver->usePosition(false, 0, 0);
673 }
674 else m_Solver->usePosition(false, 0, 0);
675}
676
677// If it is currently solving an image, then cancel the solve.
678// Otherwise start solving.
679void FITSTab::extractImage()
680{
681 if (m_Solver.get() && m_Solver->isRunning())
682 {
683 m_PlateSolveUI.SolveButton->setText(i18n("Aborting..."));
684 m_Solver->abort();
685 return;
686 }
687 m_PlateSolveUI.SolveButton->setText(i18n("Cancel"));
688
689 setupSolver(true);
690
691 m_PlateSolveUI.FitsSolverAngle->setText("");
692 m_PlateSolveUI.Solution1->setText(i18n("Extracting..."));
693 m_PlateSolveUI.Solution2->setText("");
694
695 m_Solver->runSolver(m_View->imageData());
696}
697
698void FITSTab::solveImage()
699{
700 if (m_Solver.get() && m_Solver->isRunning())
701 {
702 m_PlateSolveUI.SolveButton->setText(i18n("Aborting..."));
703 m_Solver->abort();
704 return;
705 }
706 m_PlateSolveUI.SolveButton->setText(i18n("Cancel"));
707
708 setupSolver(false);
709
710 m_PlateSolveUI.Solution2->setText(i18n("Solving..."));
711
712 m_Solver->runSolver(m_View->imageData());
713}
714
715void FITSTab::extractorDone(bool timedOut, bool success, const FITSImage::Solution &solution, double elapsedSeconds)
716{
717 Q_UNUSED(solution);
718 disconnect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::extractorDone);
719 m_PlateSolveUI.Solution2->setText("");
720
721 if (timedOut)
722 {
723 const QString result = i18n("Extractor timed out: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
724 m_PlateSolveUI.Solution1->setText(result);
725
726 // Can't run the solver. Just reset.
727 m_PlateSolveUI.SolveButton->setText("Solve");
728 return;
729 }
730 else if (!success)
731 {
732 const QString result = i18n("Extractor failed: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
733 m_PlateSolveUI.Solution1->setText(result);
734
735 // Can't run the solver. Just reset.
736 m_PlateSolveUI.SolveButton->setText(i18n("Solve"));
737 return;
738 }
739 else
740 {
741 const QString starStr = i18n("Extracted %1 stars (%2 unfiltered) in %3s",
742 m_Solver->getNumStarsFound(),
743 m_Solver->getBackground().num_stars_detected,
744 QString("%1").arg(elapsedSeconds, 0, 'f', 1));
745 m_PlateSolveUI.Solution1->setText(starStr);
746
747 // Set the stars in the FITSData object so the user can view them.
748 const QList<FITSImage::Star> &starList = m_Solver->getStarList();
749 QList<Edge*> starCenters;
750 starCenters.reserve(starList.size());
751 for (int i = 0; i < starList.size(); i++)
752 {
753 const auto &star = starList[i];
754 Edge *oneEdge = new Edge();
755 oneEdge->x = star.x;
756 oneEdge->y = star.y;
757 oneEdge->val = star.peak;
758 oneEdge->sum = star.flux;
759 oneEdge->HFR = star.HFR;
760 oneEdge->width = star.a;
761 oneEdge->numPixels = star.numPixels;
762 if (star.a > 0)
763 // See page 63 to find the ellipticity equation for SEP.
764 // http://astroa.physics.metu.edu.tr/MANUALS/sextractor/Guide2source_extractor.pdf
765 oneEdge->ellipticity = 1 - star.b / star.a;
766 else
767 oneEdge->ellipticity = 0;
768
769 starCenters.append(oneEdge);
770 }
771 m_View->imageData()->setStarCenters(starCenters);
772 m_View->updateFrame();
773
774 // Now run the solver.
775 solveImage();
776 }
777}
778
779void FITSTab::solverDone(bool timedOut, bool success, const FITSImage::Solution &solution, double elapsedSeconds)
780{
781 disconnect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::solverDone);
782 m_PlateSolveUI.SolveButton->setText("Solve");
783
784 if (m_Solver->isRunning())
785 qCDebug(KSTARS_FITS) << "solverDone called, but it is still running.";
786
787 if (timedOut)
788 {
789 const QString result = i18n("Solver timed out: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
790 m_PlateSolveUI.Solution2->setText(result);
791 }
792 else if (!success)
793 {
794 const QString result = i18n("Solver failed: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
795 m_PlateSolveUI.Solution2->setText(result);
796 }
797 else
798 {
799 const bool eastToTheRight = solution.parity == FITSImage::POSITIVE ? false : true;
800 m_View->imageData()->injectWCS(solution.orientation, solution.ra, solution.dec, solution.pixscale, eastToTheRight);
801 m_View->imageData()->loadWCS();
802
803 const QString result = QString("Solved in %1s").arg(elapsedSeconds, 0, 'f', 1);
804 const double solverPA = KSUtils::rotationToPositionAngle(solution.orientation);
805 m_PlateSolveUI.FitsSolverAngle->setText(QString("%1ยบ").arg(solverPA, 0, 'f', 2));
806
807 // Set the scale widget to the current result
808 const int imageWidth = m_View->imageData()->width();
809 const int units = m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits->currentIndex();
810 if (units == SSolver::DEG_WIDTH)
811 m_PlateSolveUI.kcfg_FitsSolverScale->setValue(solution.pixscale * imageWidth / 3600.0);
812 else if (units == SSolver::ARCMIN_WIDTH)
813 m_PlateSolveUI.kcfg_FitsSolverScale->setValue(solution.pixscale * imageWidth / 60.0);
814 else
815 m_PlateSolveUI.kcfg_FitsSolverScale->setValue(solution.pixscale);
816
817 // Set the ra and dec widgets to the current result
818 m_PlateSolveUI.FitsSolverEstRA->show(dms(solution.ra));
819 m_PlateSolveUI.FitsSolverEstDec->show(dms(solution.dec));
820
821 m_PlateSolveUI.Solution2->setText(result);
822 }
823}
824
825// Each module can default to its own profile index. These two methods retrieves and saves
826// the values in a JSON string using an Options variable.
827int FITSTab::getProfileIndex(int moduleIndex)
828{
829 if (moduleIndex < 0 || moduleIndex >= Ekos::ProfileGroupNames.size())
830 return 0;
831 const QString moduleName = Ekos::ProfileGroupNames[moduleIndex];
832 const QString str = Options::fitsSolverProfileIndeces();
834 if (doc.isNull() || !doc.isObject())
835 return 0;
836 const QJsonObject indeces = doc.object();
837 return indeces[moduleName].toString().toInt();
838}
839
840void FITSTab::setProfileIndex(int moduleIndex, int profileIndex)
841{
842 if (moduleIndex < 0 || moduleIndex >= Ekos::ProfileGroupNames.size())
843 return;
844 QString str = Options::fitsSolverProfileIndeces();
846 if (doc.isNull() || !doc.isObject())
847 {
849 for (int i = 0; i < Ekos::ProfileGroupNames.size(); i++)
850 {
851 QString name = Ekos::ProfileGroupNames[i];
852 if (name == "Align")
853 initialIndeces[name] = QString::number(Options::solveOptionsProfile());
854 else if (name == "Guide")
855 initialIndeces[name] = QString::number(Options::guideOptionsProfile());
856 else if (name == "HFR")
857 initialIndeces[name] = QString::number(Options::hFROptionsProfile());
858 else // Focus has a weird setting, just default to 0
859 initialIndeces[name] = "0";
860 }
862 }
863
864 QJsonObject indeces = doc.object();
865 indeces[Ekos::ProfileGroupNames[moduleIndex]] = QString::number(profileIndex);
866 doc = QJsonDocument(indeces);
867 Options::setFitsSolverProfileIndeces(QString(doc.toJson()));
868}
869
870void FITSTab::setupProfiles(int moduleIndex)
871{
872 if (moduleIndex < 0 || moduleIndex >= Ekos::ProfileGroupNames.size())
873 return;
874 Ekos::ProfileGroup profileGroup = static_cast<Ekos::ProfileGroup>(moduleIndex);
875 Options::setFitsSolverModule(moduleIndex);
876
877 // Set up the profiles' menu.
879 m_PlateSolveUI.kcfg_FitsSolverProfile->clear();
880 for(auto &param : optionsList)
881 m_PlateSolveUI.kcfg_FitsSolverProfile->addItem(param.listName);
882
883 m_ProfileEditor->setProfileGroup(profileGroup, false);
884
885 // Restore the stored options.
886 m_PlateSolveUI.kcfg_FitsSolverProfile->setCurrentIndex(getProfileIndex(Options::fitsSolverModule()));
887
888 m_ProfileEditorPage->setHeader(QString("FITS Viewer Solver %1 Profiles Editor")
889 .arg(Ekos::ProfileGroupNames[moduleIndex]));
890}
891
892void FITSTab::initSolverUI()
893{
894 // Init the modules combo box.
895 m_PlateSolveUI.kcfg_FitsSolverModule->clear();
896 for (int i = 0; i < Ekos::ProfileGroupNames.size(); i++)
897 m_PlateSolveUI.kcfg_FitsSolverModule->addItem(Ekos::ProfileGroupNames[i]);
898 m_PlateSolveUI.kcfg_FitsSolverModule->setCurrentIndex(Options::fitsSolverModule());
899
900 setupProfiles(Options::fitsSolverModule());
901
902 // Change the profiles combo box whenever the modules combo changes
903 connect(m_PlateSolveUI.kcfg_FitsSolverModule, QOverload<int>::of(&QComboBox::activated), this, &FITSTab::setupProfiles);
904
905 m_PlateSolveUI.kcfg_FitsSolverUseScale->setChecked(Options::fitsSolverUseScale());
906 m_PlateSolveUI.kcfg_FitsSolverScale->setValue(Options::fitsSolverScale());
907 m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits->setCurrentIndex(Options::fitsSolverImageScaleUnits());
908
909 m_PlateSolveUI.kcfg_FitsSolverUsePosition->setChecked(Options::fitsSolverUsePosition());
910 m_PlateSolveUI.kcfg_FitsSolverRadius->setValue(Options::fitsSolverRadius());
911
912 m_PlateSolveUI.FitsSolverEstRA->setUnits(dmsBox::HOURS);
913 m_PlateSolveUI.FitsSolverEstDec->setUnits(dmsBox::DEGREES);
914
915 // Save the values of user controls when the user changes them.
916 connect(m_PlateSolveUI.kcfg_FitsSolverProfile, QOverload<int>::of(&QComboBox::activated), [this](int index)
917 {
918 setProfileIndex(m_PlateSolveUI.kcfg_FitsSolverModule->currentIndex(), index);
919 });
920
921 connect(m_PlateSolveUI.kcfg_FitsSolverUseScale, &QCheckBox::stateChanged, this, [](int state)
922 {
923 Options::setFitsSolverUseScale(state);
924 });
925 connect(m_PlateSolveUI.kcfg_FitsSolverScale, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [](double value)
926 {
927 Options::setFitsSolverScale(value);
928 });
929 connect(m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits, QOverload<int>::of(&QComboBox::activated), [](int index)
930 {
931 Options::setFitsSolverImageScaleUnits(index);
932 });
933
934 connect(m_PlateSolveUI.kcfg_FitsSolverUsePosition, &QCheckBox::stateChanged, this, [](int state)
935 {
936 Options::setFitsSolverUsePosition(state);
937 });
938
939 connect(m_PlateSolveUI.kcfg_FitsSolverRadius, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [](double value)
940 {
941 Options::setFitsSolverRadius(value);
942 });
943 connect(m_PlateSolveUI.UpdatePosition, &QPushButton::clicked, this, [&]()
944 {
945 const auto center = SkyMap::Instance()->getCenterPoint();
946 m_PlateSolveUI.FitsSolverEstRA->show(center.ra());
947 m_PlateSolveUI.FitsSolverEstDec->show(center.dec());
948 });
949
950 // Warn if the user is not using the internal StellarSolver solver.
951 const SSolver::SolverType type = static_cast<SSolver::SolverType>(Options::solverType());
952 if(type != SSolver::SOLVER_STELLARSOLVER)
953 {
954 m_PlateSolveUI.Solution2->setText(i18n("Warning! This tool only supports the internal StellarSolver solver."));
955 m_PlateSolveUI.Solution1->setText(i18n("Change to that in the Ekos Align options menu."));
956 }
957}
Primary window to view monochrome and color FITS images.
Definition fitsviewer.h:50
static KStars * Instance()
Definition kstars.h:123
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
const double & Degrees() const
Definition dms.h:141
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
Type type(const QSqlDatabase &db)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:79
QString name(StandardAction id)
KGuiItem save()
KGuiItem discard()
void clicked(bool checked)
void stateChanged(int state)
void setImage(const QImage &image, Mode mode)
void activated(int index)
QString filePath(const QString &fileName) const const
QString tempPath()
void valueChanged(double d)
bool exists(const QString &fileName)
QUrl getSaveFileUrl(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, Options options, const QStringList &supportedSchemes)
bool exists() const const
Int toInt() const const
QClipboard * clipboard()
QIcon fromTheme(const QString &name)
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
bool isNull() const const
bool isObject() const const
QJsonObject object() const const
QByteArray toJson(JsonFormat format) const const
void currentRowChanged(int currentRow)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
bool disconnect(const QMetaObject::Connection &connection)
T * get() const const
bool isNull() const const
void splitterMoved(int pos, int index)
QString arg(Args &&... args) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString number(double n, char format, int precision)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toUtf8() const const
AlignHCenter
UniqueConnection
ItemIsSelectable
MatchExactly
Horizontal
WA_LayoutUsesWidgetRect
QTextStream & dec(QTextStream &stream)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void setActive(bool active)
void clear()
bool isClean() const const
void setClean()
void clear()
QUrl fromLocalFile(const QString &localFile)
bool isEmpty() const const
bool isValid() const const
void setPath(const QString &path, ParsingMode mode)
QString toLocalFile() const const
QString url(FormattingOptions options) const const
QWidget(QWidget *parent, Qt::WindowFlags f)
void setLayout(QLayout *layout)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 17 2024 11:48:26 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.