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 "fitshistogramcommand.h"
12#include "fitsview.h"
13#include "fitsviewer.h"
14#include "ksnotification.h"
15#include "kstars.h"
16#include "Options.h"
17#include "ui_fitsheaderdialog.h"
18#include "ui_statform.h"
19#include "fitsstretchui.h"
20#include "skymap.h"
21#include <KMessageBox>
22#include <QFileDialog>
23#include <QClipboard>
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
65 if (ans == KMessageBox::Continue)
66 saveFile();
67 if (ans == KMessageBox::Cancel)
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);
111 QVBoxLayout *vlayout = new QVBoxLayout();
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
172 QVBoxLayout *recentPanelLayout = new QVBoxLayout();
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
201 setLayout(vlayout);
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 {
221 emit failed(i18nc("File not found: %1", imageURL.toString().toLatin1()));
222 return;
223 }
224
225 if (setupView(mode, filter))
226 {
227
228 // On Success loading image
229 connect(m_View.get(), &FITSView::loaded, this, [&]()
230 {
231 processData();
232 emit loaded();
233 });
234
235 connect(m_View.get(), &FITSView::updated, this, &FITSTab::updated);
236 }
237 else
238 // update tab text
239 modifyFITSState(true, imageURL);
240
241 currentURL = imageURL;
242
243 m_View->setFilter(filter);
244
245 m_View->loadFile(imageURL.toLocalFile());
246}
247
248bool FITSTab::shouldComputeHFR() const
249{
250 if (viewer->isStarsMarked())
251 return true;
252 if (!Options::autoHFR())
253 return false;
254 return ((!m_View.isNull()) && (m_View->getMode() == FITS_NORMAL));
255}
256
257void FITSTab::processData()
258{
259 const QSharedPointer<FITSData> &imageData = m_View->imageData();
260
261 m_HistogramEditor->setImageData(imageData);
262
263 // Only construct histogram if it is actually visible
264 // Otherwise wait until histogram is needed before creating it.
265 // if (fitsSplitter->sizes().at(0) != 0 && !imageData->isHistogramConstructed() &&
266 // !Options::nonLinearHistogram())
267 // {
268 // imageData->constructHistogram();
269 // }
270
271 if (shouldComputeHFR())
272 {
273 m_View->searchStars();
274 qCDebug(KSTARS_FITS) << "FITS HFR:" << imageData->getHFR();
275 }
276
277 displayStats();
278
279 loadFITSHeader();
280
281 // Don't add it to the list if it is already there
282 if (recentImages->findItems(currentURL.toLocalFile(), Qt::MatchExactly).count() == 0)
283 {
284 //Don't add it to the list if it is a preview
285 if(!imageData->filename().startsWith(QDir::tempPath()))
286 {
287 disconnect(recentImages, &QListWidget::currentRowChanged, this,
288 &FITSTab::selectRecentFITS);
289 recentImages->addItem(imageData->filename());
290 recentImages->setCurrentRow(recentImages->count() - 1);
291 connect(recentImages, &QListWidget::currentRowChanged, this,
292 &FITSTab::selectRecentFITS);
293 }
294 }
295
296 // This could both compute the HFRs and setup the graphics, however,
297 // if shouldComputeHFR() above is true, then that will compute the HFRs
298 // and this would notice that and just setup graphics. They are separated
299 // for the case where the graphics is not desired.
300 if (viewer->isStarsMarked())
301 {
302 m_View->toggleStars(true);
303 m_View->updateFrame();
304 }
305
306 // if (Options::nonLinearHistogram())
307 // m_HistogramEditor->createNonLinearHistogram();
308
309 stretchUI->generateHistogram();
310}
311
312bool FITSTab::loadData(const QSharedPointer<FITSData> &data, FITSMode mode, FITSScale filter)
313{
314 setupView(mode, filter);
315
316 // Empty URL
317 currentURL = QUrl();
318
319 if (viewer->isStarsMarked())
320 {
321 m_View->toggleStars(true);
322 //view->updateFrame();
323 }
324
325 m_View->setFilter(filter);
326
327 if (!m_View->loadData(data))
328 {
329 // On Failure to load
330 // connect(view.get(), &FITSView::failed, this, &FITSTab::failed);
331 return false;
332 }
333
334 processData();
335 return true;
336}
337
338void FITSTab::modifyFITSState(bool clean, const QUrl &imageURL)
339{
340 if (clean)
341 {
342 if (undoStack->isClean() == false)
343 undoStack->setClean();
344
345 mDirty = false;
346 }
347 else
348 mDirty = true;
349
350 emit changeStatus(clean, imageURL);
351}
352
353bool FITSTab::saveImage(const QString &filename)
354{
355 return m_View->saveImage(filename);
356}
357
358void FITSTab::copyFITS()
359{
360 QApplication::clipboard()->setImage(m_View->getDisplayImage());
361}
362
363void FITSTab::histoFITS()
364{
365 fitsTools->setCurrentIndex(1);
366 if(m_View->width() > 200)
367 fitsSplitter->setSizes(QList<int>() << 200 << m_View->width() - 200);
368 else
369 fitsSplitter->setSizes(QList<int>() << 50 << 50);
370}
371
372void FITSTab::displayStats(bool roi)
373{
374 const QSharedPointer<FITSData> &imageData = m_View->imageData();
375
376 stat.statsTable->item(STAT_WIDTH, 0)->setText(QString::number(imageData->width(roi)));
377 stat.statsTable->item(STAT_HEIGHT, 0)->setText(QString::number(imageData->height(roi)));
378 stat.statsTable->item(STAT_BITPIX, 0)->setText(QString::number(imageData->bpp()));
379
380 if (!roi)
381 stat.statsTable->item(STAT_HFR, 0)->setText(QString::number(imageData->getHFR(), 'f', 3));
382 else
383 stat.statsTable->item(STAT_HFR, 0)->setText("---");
384
385 if (imageData->channels() == 1)
386 {
387 for (int i = STAT_MIN; i <= STAT_STDDEV; i++)
388 {
389 if (stat.statsTable->columnSpan(i, 0) != 3)
390 stat.statsTable->setSpan(i, 0, 1, 3);
391 }
392
393 stat.statsTable->horizontalHeaderItem(0)->setText(i18n("Value"));
394 stat.statsTable->hideColumn(1);
395 stat.statsTable->hideColumn(2);
396 }
397 else
398 {
399 for (int i = STAT_MIN; i <= STAT_STDDEV; i++)
400 {
401 if (stat.statsTable->columnSpan(i, 0) != 1)
402 stat.statsTable->setSpan(i, 0, 1, 1);
403 }
404
405 stat.statsTable->horizontalHeaderItem(0)->setText(i18nc("Red", "R"));
406 stat.statsTable->showColumn(1);
407 stat.statsTable->showColumn(2);
408 }
409
410 if (!Options::nonLinearHistogram() && !imageData->isHistogramConstructed())
411 imageData->constructHistogram();
412
413 for (int i = 0; i < imageData->channels(); i++)
414 {
415 stat.statsTable->item(STAT_MIN, i)->setText(QString::number(imageData->getMin(i, roi), 'f', 3));
416 stat.statsTable->item(STAT_MAX, i)->setText(QString::number(imageData->getMax(i, roi), 'f', 3));
417 stat.statsTable->item(STAT_MEAN, i)->setText(QString::number(imageData->getMean(i, roi), 'f', 3));
418 stat.statsTable->item(STAT_MEDIAN, i)->setText(QString::number(imageData->getMedian(i, roi), 'f', 3));
419 stat.statsTable->item(STAT_STDDEV, i)->setText(QString::number(imageData->getStdDev(i, roi), 'f', 3));
420 }
421}
422
423void FITSTab::statFITS()
424{
425 fitsTools->setCurrentIndex(0);
426 if(m_View->width() > 200)
427 fitsSplitter->setSizes(QList<int>() << 200 << m_View->width() - 200);
428 else
429 fitsSplitter->setSizes(QList<int>() << 50 << 50);
430 displayStats();
431}
432
433void FITSTab::loadFITSHeader()
434{
435 const QSharedPointer<FITSData> &imageData = m_View->imageData();
436
437 int nkeys = imageData->getRecords().size();
438 int counter = 0;
439 header.tableWidget->setRowCount(nkeys);
440 for (const auto &oneRecord : imageData->getRecords())
441 {
442 QTableWidgetItem *tempItem = new QTableWidgetItem(oneRecord.key);
444 header.tableWidget->setItem(counter, 0, tempItem);
445 tempItem = new QTableWidgetItem(oneRecord.value.toString());
447 header.tableWidget->setItem(counter, 1, tempItem);
448 tempItem = new QTableWidgetItem(oneRecord.comment);
450 header.tableWidget->setItem(counter, 2, tempItem);
451 counter++;
452 }
453
454 header.tableWidget->setColumnWidth(0, 100);
455 header.tableWidget->setColumnWidth(1, 100);
456 header.tableWidget->setColumnWidth(2, 250);
457}
458
459void FITSTab::headerFITS()
460{
461 fitsTools->setCurrentIndex(2);
462 if(m_View->width() > 200)
463 fitsSplitter->setSizes(QList<int>() << 200 << m_View->width() - 200);
464 else
465 fitsSplitter->setSizes(QList<int>() << 50 << 50);
466}
467
468bool FITSTab::saveFile()
469{
470 QUrl backupCurrent = currentURL;
471 QUrl currentDir(Options::fitsDir());
472 currentDir.setScheme("file");
473
474 if (currentURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || currentURL.toLocalFile().contains("/Temp"))
475 currentURL.clear();
476
477 // If no changes made, return.
478 if (mDirty == false && !currentURL.isEmpty())
479 return false;
480
481 if (currentURL.isEmpty())
482 {
483 QString selectedFilter;
484#ifdef Q_OS_MACOS //For some reason, the other code caused KStars to crash on MacOS
485 currentURL =
486 QFileDialog::getSaveFileUrl(KStars::Instance(), i18nc("@title:window", "Save FITS"), currentDir,
487 "Images (*.fits *.fits.gz *.fit *.xisf *.jpg *.jpeg *.png)");
488#else
489 currentURL =
490 QFileDialog::getSaveFileUrl(KStars::Instance(), i18nc("@title:window", "Save FITS"), currentDir,
491 "FITS (*.fits *.fits.gz *.fit);;XISF (*.xisf);;JPEG (*.jpg *.jpeg);;PNG (*.png)", &selectedFilter);
492#endif
493 // if user presses cancel
494 if (currentURL.isEmpty())
495 {
496 currentURL = backupCurrent;
497 return false;
498 }
499
500 // If no extension is selected append one
501 if (currentURL.toLocalFile().contains('.') == 0)
502 {
503 if (selectedFilter.contains("XISF"))
504 currentURL.setPath(currentURL.toLocalFile() + ".xisf");
505 else if (selectedFilter.contains("JPEG"))
506 currentURL.setPath(currentURL.toLocalFile() + ".jpg");
507 else if (selectedFilter.contains("PNG"))
508 currentURL.setPath(currentURL.toLocalFile() + ".png");
509 else
510 currentURL.setPath(currentURL.toLocalFile() + ".fits");
511 }
512 }
513
514 if (currentURL.isValid())
515 {
516 QString localFile = currentURL.toLocalFile();
517 // if (localFile.contains(".fit"))
518 // localFile = "!" + localFile;
519
520 if (!saveImage(localFile))
521 {
522 KSNotification::error(i18n("Image save error: %1", m_View->imageData()->getLastError()), i18n("Image Save"));
523 return false;
524 }
525
526 emit newStatus(i18n("File saved to %1", currentURL.url()), FITS_MESSAGE);
527 modifyFITSState();
528 return true;
529 }
530 else
531 {
532 QString message = i18n("Invalid URL: %1", currentURL.url());
533 KSNotification::sorry(message, i18n("Invalid URL"));
534 return false;
535 }
536}
537
538bool FITSTab::saveFileAs()
539{
540 currentURL.clear();
541 return saveFile();
542}
543
544void FITSTab::ZoomIn()
545{
546 QPoint oldCenter = m_View->getImagePoint(m_View->viewport()->rect().center());
547 m_View->ZoomIn();
548 m_View->cleanUpZoom(oldCenter);
549}
550
551void FITSTab::ZoomOut()
552{
553 QPoint oldCenter = m_View->getImagePoint(m_View->viewport()->rect().center());
554 m_View->ZoomOut();
555 m_View->cleanUpZoom(oldCenter);
556}
557
558void FITSTab::ZoomDefault()
559{
560 QPoint oldCenter = m_View->getImagePoint(m_View->viewport()->rect().center());
561 m_View->ZoomDefault();
562 m_View->cleanUpZoom(oldCenter);
563}
564
565void FITSTab::tabPositionUpdated()
566{
567 undoStack->setActive(true);
568 m_View->emitZoom();
569 emit newStatus(QString("%1x%2").arg(m_View->imageData()->width()).arg(m_View->imageData()->height()),
570 FITS_RESOLUTION);
571}
572
573void FITSTab::setStretchValues(double shadows, double midtones, double highlights)
574{
575 if (stretchUI)
576 stretchUI->setStretchValues(shadows, midtones, highlights);
577}
578
579void FITSTab::setAutoStretch()
580{
581 if (!m_View->getAutoStretch())
582 m_View->setAutoStretchParams();
583}
584
585namespace
586{
587const QList<SSolver::Parameters> getSSolverParametersList(Ekos::ProfileGroup module)
588{
589 QString savedProfiles;
590 switch(module)
591 {
592 case Ekos::AlignProfiles:
593 default:
594 savedProfiles = QDir(KSPaths::writableLocation(
595 QStandardPaths::AppLocalDataLocation)).filePath("SavedAlignProfiles.ini");
596 return QFile(savedProfiles).exists() ?
597 StellarSolver::loadSavedOptionsProfiles(savedProfiles) :
598 Ekos::getDefaultAlignOptionsProfiles();
599 break;
600 case Ekos::FocusProfiles:
601 savedProfiles = QDir(KSPaths::writableLocation(
602 QStandardPaths::AppLocalDataLocation)).filePath("SavedFocusProfiles.ini");
603 return QFile(savedProfiles).exists() ?
604 StellarSolver::loadSavedOptionsProfiles(savedProfiles) :
605 Ekos::getDefaultFocusOptionsProfiles();
606 break;
607 case Ekos::GuideProfiles:
608 savedProfiles = QDir(KSPaths::writableLocation(
609 QStandardPaths::AppLocalDataLocation)).filePath("SavedGuideProfiles.ini");
610 return QFile(savedProfiles).exists() ?
611 StellarSolver::loadSavedOptionsProfiles(savedProfiles) :
612 Ekos::getDefaultGuideOptionsProfiles();
613 break;
614 case Ekos::HFRProfiles:
615 savedProfiles = QDir(KSPaths::writableLocation(
616 QStandardPaths::AppLocalDataLocation)).filePath("SavedHFRProfiles.ini");
617 return QFile(savedProfiles).exists() ?
618 StellarSolver::loadSavedOptionsProfiles(savedProfiles) :
619 Ekos::getDefaultHFROptionsProfiles();
620 break;
621 }
622}
623} // namespace
624
625void FITSTab::setupSolver(bool extractOnly)
626{
627 auto parameters = getSSolverParametersList(static_cast<Ekos::ProfileGroup>(Options::fitsSolverModule())).at(
628 m_PlateSolveUI.kcfg_FitsSolverProfile->currentIndex());
629 parameters.search_radius = m_PlateSolveUI.kcfg_FitsSolverRadius->value();
630 if (extractOnly)
631 {
632 m_Solver.reset(new SolverUtils(parameters, parameters.solverTimeLimit, SSolver::EXTRACT), &QObject::deleteLater);
633 connect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::extractorDone, Qt::UniqueConnection);
634 }
635 else
636 {
637 m_Solver.reset(new SolverUtils(parameters, parameters.solverTimeLimit, SSolver::SOLVE), &QObject::deleteLater);
638 connect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::solverDone, Qt::UniqueConnection);
639 }
640
641 const int imageWidth = m_View->imageData()->width();
642 const int imageHeight = m_View->imageData()->height();
643 if (m_PlateSolveUI.kcfg_FitsSolverUseScale->isChecked() && imageWidth != 0 && imageHeight != 0)
644 {
645 const double scale = m_PlateSolveUI.kcfg_FitsSolverScale->value();
646 double lowScale = scale * 0.8;
647 double highScale = scale * 1.2;
648
649 // solver utils uses arcsecs per pixel only
650 const int units = m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits->currentIndex();
651 if (units == SSolver::DEG_WIDTH)
652 {
653 lowScale = (lowScale * 3600) / std::max(imageWidth, imageHeight);
654 highScale = (highScale * 3600) / std::min(imageWidth, imageHeight);
655 }
656 else if (units == SSolver::ARCMIN_WIDTH)
657 {
658 lowScale = (lowScale * 60) / std::max(imageWidth, imageHeight);
659 highScale = (highScale * 60) / std::min(imageWidth, imageHeight);
660 }
661
662 m_Solver->useScale(m_PlateSolveUI.kcfg_FitsSolverUseScale->isChecked(), lowScale, highScale);
663 }
664 else m_Solver->useScale(false, 0, 0);
665
666 if (m_PlateSolveUI.kcfg_FitsSolverUsePosition->isChecked())
667 {
668 bool ok;
669 const dms ra = m_PlateSolveUI.FitsSolverEstRA->createDms(&ok);
670 bool ok2;
671 const dms dec = m_PlateSolveUI.FitsSolverEstDec->createDms(&ok2);
672 if (ok && ok2)
673 m_Solver->usePosition(true, ra.Degrees(), dec.Degrees());
674 else
675 m_Solver->usePosition(false, 0, 0);
676 }
677 else m_Solver->usePosition(false, 0, 0);
678}
679
680// If it is currently solving an image, then cancel the solve.
681// Otherwise start solving.
682void FITSTab::extractImage()
683{
684 if (m_Solver.get() && m_Solver->isRunning())
685 {
686 m_PlateSolveUI.SolveButton->setText(i18n("Aborting..."));
687 m_Solver->abort();
688 return;
689 }
690 m_PlateSolveUI.SolveButton->setText(i18n("Cancel"));
691
692 setupSolver(true);
693
694 m_PlateSolveUI.FitsSolverAngle->setText("");
695 m_PlateSolveUI.FitsSolverIndexfile->setText("");
696 m_PlateSolveUI.Solution1->setText(i18n("Extracting..."));
697 m_PlateSolveUI.Solution2->setText("");
698
699 m_Solver->runSolver(m_View->imageData());
700}
701
702void FITSTab::solveImage()
703{
704 if (m_Solver.get() && m_Solver->isRunning())
705 {
706 m_PlateSolveUI.SolveButton->setText(i18n("Aborting..."));
707 m_Solver->abort();
708 return;
709 }
710 m_PlateSolveUI.SolveButton->setText(i18n("Cancel"));
711
712 setupSolver(false);
713
714 m_PlateSolveUI.Solution2->setText(i18n("Solving..."));
715
716 m_Solver->runSolver(m_View->imageData());
717}
718
719void FITSTab::extractorDone(bool timedOut, bool success, const FITSImage::Solution &solution, double elapsedSeconds)
720{
721 Q_UNUSED(solution);
722 disconnect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::extractorDone);
723 m_PlateSolveUI.Solution2->setText("");
724
725 if (timedOut)
726 {
727 const QString result = i18n("Extractor timed out: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
728 m_PlateSolveUI.Solution1->setText(result);
729
730 // Can't run the solver. Just reset.
731 m_PlateSolveUI.SolveButton->setText("Solve");
732 return;
733 }
734 else if (!success)
735 {
736 const QString result = i18n("Extractor failed: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
737 m_PlateSolveUI.Solution1->setText(result);
738
739 // Can't run the solver. Just reset.
740 m_PlateSolveUI.SolveButton->setText(i18n("Solve"));
741 return;
742 }
743 else
744 {
745 const QString starStr = i18n("Extracted %1 stars (%2 unfiltered) in %3s",
746 m_Solver->getNumStarsFound(),
747 m_Solver->getBackground().num_stars_detected,
748 QString("%1").arg(elapsedSeconds, 0, 'f', 1));
749 m_PlateSolveUI.Solution1->setText(starStr);
750
751 // Set the stars in the FITSData object so the user can view them.
752 const QList<FITSImage::Star> &starList = m_Solver->getStarList();
753 QList<Edge*> starCenters;
754 starCenters.reserve(starList.size());
755 for (int i = 0; i < starList.size(); i++)
756 {
757 const auto &star = starList[i];
758 Edge *oneEdge = new Edge();
759 oneEdge->x = star.x;
760 oneEdge->y = star.y;
761 oneEdge->val = star.peak;
762 oneEdge->sum = star.flux;
763 oneEdge->HFR = star.HFR;
764 oneEdge->width = star.a;
765 oneEdge->numPixels = star.numPixels;
766 if (star.a > 0)
767 // See page 63 to find the ellipticity equation for SEP.
768 // http://astroa.physics.metu.edu.tr/MANUALS/sextractor/Guide2source_extractor.pdf
769 oneEdge->ellipticity = 1 - star.b / star.a;
770 else
771 oneEdge->ellipticity = 0;
772
773 starCenters.append(oneEdge);
774 }
775 m_View->imageData()->setStarCenters(starCenters);
776 m_View->updateFrame();
777
778 // Now run the solver.
779 solveImage();
780 }
781}
782
783void FITSTab::solverDone(bool timedOut, bool success, const FITSImage::Solution &solution, double elapsedSeconds)
784{
785 disconnect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::solverDone);
786 m_PlateSolveUI.SolveButton->setText("Solve");
787
788 if (m_Solver->isRunning())
789 qCDebug(KSTARS_FITS) << "solverDone called, but it is still running.";
790
791 if (timedOut)
792 {
793 const QString result = i18n("Solver timed out: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
794 m_PlateSolveUI.Solution2->setText(result);
795 }
796 else if (!success)
797 {
798 const QString result = i18n("Solver failed: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
799 m_PlateSolveUI.Solution2->setText(result);
800 }
801 else
802 {
803 const bool eastToTheRight = solution.parity == FITSImage::POSITIVE ? false : true;
804 m_View->imageData()->injectWCS(solution.orientation, solution.ra, solution.dec, solution.pixscale, eastToTheRight);
805 m_View->imageData()->loadWCS();
806
807 const QString result = QString("Solved in %1s").arg(elapsedSeconds, 0, 'f', 1);
808 const double solverPA = KSUtils::rotationToPositionAngle(solution.orientation);
809 m_PlateSolveUI.FitsSolverAngle->setText(QString("%1ยบ").arg(solverPA, 0, 'f', 2));
810
811 int indexUsed = -1, healpixUsed = -1;
812 m_Solver->getSolutionHealpix(&indexUsed, &healpixUsed);
813 if (indexUsed < 0)
814 m_PlateSolveUI.FitsSolverIndexfile->setText("");
815 else
816 m_PlateSolveUI.FitsSolverIndexfile->setText(
817 QString("%1%2")
818 .arg(indexUsed)
819 .arg(healpixUsed >= 0 ? QString("-%1").arg(healpixUsed) : QString("")));;
820
821 // Set the scale widget to the current result
822 const int imageWidth = m_View->imageData()->width();
823 const int units = m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits->currentIndex();
824 if (units == SSolver::DEG_WIDTH)
825 m_PlateSolveUI.kcfg_FitsSolverScale->setValue(solution.pixscale * imageWidth / 3600.0);
826 else if (units == SSolver::ARCMIN_WIDTH)
827 m_PlateSolveUI.kcfg_FitsSolverScale->setValue(solution.pixscale * imageWidth / 60.0);
828 else
829 m_PlateSolveUI.kcfg_FitsSolverScale->setValue(solution.pixscale);
830
831 // Set the ra and dec widgets to the current result
832 m_PlateSolveUI.FitsSolverEstRA->show(dms(solution.ra));
833 m_PlateSolveUI.FitsSolverEstDec->show(dms(solution.dec));
834
835 m_PlateSolveUI.Solution2->setText(result);
836 }
837}
838
839// Each module can default to its own profile index. These two methods retrieves and saves
840// the values in a JSON string using an Options variable.
841int FITSTab::getProfileIndex(int moduleIndex)
842{
843 if (moduleIndex < 0 || moduleIndex >= Ekos::ProfileGroupNames.size())
844 return 0;
845 const QString moduleName = Ekos::ProfileGroupNames[moduleIndex];
846 const QString str = Options::fitsSolverProfileIndeces();
848 if (doc.isNull() || !doc.isObject())
849 return 0;
850 const QJsonObject indeces = doc.object();
851 return indeces[moduleName].toString().toInt();
852}
853
854void FITSTab::setProfileIndex(int moduleIndex, int profileIndex)
855{
856 if (moduleIndex < 0 || moduleIndex >= Ekos::ProfileGroupNames.size())
857 return;
858 QString str = Options::fitsSolverProfileIndeces();
860 if (doc.isNull() || !doc.isObject())
861 {
862 QJsonObject initialIndeces;
863 for (int i = 0; i < Ekos::ProfileGroupNames.size(); i++)
864 {
865 QString name = Ekos::ProfileGroupNames[i];
866 if (name == "Align")
867 initialIndeces[name] = QString::number(Options::solveOptionsProfile());
868 else if (name == "Guide")
869 initialIndeces[name] = QString::number(Options::guideOptionsProfile());
870 else if (name == "HFR")
871 initialIndeces[name] = QString::number(Options::hFROptionsProfile());
872 else // Focus has a weird setting, just default to 0
873 initialIndeces[name] = "0";
874 }
875 doc = QJsonDocument(initialIndeces);
876 }
877
878 QJsonObject indeces = doc.object();
879 indeces[Ekos::ProfileGroupNames[moduleIndex]] = QString::number(profileIndex);
880 doc = QJsonDocument(indeces);
881 Options::setFitsSolverProfileIndeces(QString(doc.toJson()));
882}
883
884void FITSTab::setupProfiles(int moduleIndex)
885{
886 if (moduleIndex < 0 || moduleIndex >= Ekos::ProfileGroupNames.size())
887 return;
888 Ekos::ProfileGroup profileGroup = static_cast<Ekos::ProfileGroup>(moduleIndex);
889 Options::setFitsSolverModule(moduleIndex);
890
891 // Set up the profiles' menu.
892 const QList<SSolver::Parameters> optionsList = getSSolverParametersList(profileGroup);
893 m_PlateSolveUI.kcfg_FitsSolverProfile->clear();
894 for(auto &param : optionsList)
895 m_PlateSolveUI.kcfg_FitsSolverProfile->addItem(param.listName);
896
897 m_ProfileEditor->setProfileGroup(profileGroup, false);
898
899 // Restore the stored options.
900 m_PlateSolveUI.kcfg_FitsSolverProfile->setCurrentIndex(getProfileIndex(Options::fitsSolverModule()));
901
902 m_ProfileEditorPage->setHeader(QString("FITS Viewer Solver %1 Profiles Editor")
903 .arg(Ekos::ProfileGroupNames[moduleIndex]));
904}
905
906void FITSTab::initSolverUI()
907{
908 // Init the modules combo box.
909 m_PlateSolveUI.kcfg_FitsSolverModule->clear();
910 for (int i = 0; i < Ekos::ProfileGroupNames.size(); i++)
911 m_PlateSolveUI.kcfg_FitsSolverModule->addItem(Ekos::ProfileGroupNames[i]);
912 m_PlateSolveUI.kcfg_FitsSolverModule->setCurrentIndex(Options::fitsSolverModule());
913
914 setupProfiles(Options::fitsSolverModule());
915
916 // Change the profiles combo box whenever the modules combo changes
917 connect(m_PlateSolveUI.kcfg_FitsSolverModule, QOverload<int>::of(&QComboBox::activated), this, &FITSTab::setupProfiles);
918
919 m_PlateSolveUI.kcfg_FitsSolverUseScale->setChecked(Options::fitsSolverUseScale());
920 m_PlateSolveUI.kcfg_FitsSolverScale->setValue(Options::fitsSolverScale());
921 m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits->setCurrentIndex(Options::fitsSolverImageScaleUnits());
922
923 m_PlateSolveUI.kcfg_FitsSolverUsePosition->setChecked(Options::fitsSolverUsePosition());
924 m_PlateSolveUI.kcfg_FitsSolverRadius->setValue(Options::fitsSolverRadius());
925
926 m_PlateSolveUI.FitsSolverEstRA->setUnits(dmsBox::HOURS);
927 m_PlateSolveUI.FitsSolverEstDec->setUnits(dmsBox::DEGREES);
928
929 // Save the values of user controls when the user changes them.
930 connect(m_PlateSolveUI.kcfg_FitsSolverProfile, QOverload<int>::of(&QComboBox::activated), [this](int index)
931 {
932 setProfileIndex(m_PlateSolveUI.kcfg_FitsSolverModule->currentIndex(), index);
933 });
934
935 connect(m_PlateSolveUI.kcfg_FitsSolverUseScale, &QCheckBox::stateChanged, this, [](int state)
936 {
937 Options::setFitsSolverUseScale(state);
938 });
939 connect(m_PlateSolveUI.kcfg_FitsSolverScale, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [](double value)
940 {
941 Options::setFitsSolverScale(value);
942 });
943 connect(m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits, QOverload<int>::of(&QComboBox::activated), [](int index)
944 {
945 Options::setFitsSolverImageScaleUnits(index);
946 });
947
948 connect(m_PlateSolveUI.kcfg_FitsSolverUsePosition, &QCheckBox::stateChanged, this, [](int state)
949 {
950 Options::setFitsSolverUsePosition(state);
951 });
952
953 connect(m_PlateSolveUI.kcfg_FitsSolverRadius, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [](double value)
954 {
955 Options::setFitsSolverRadius(value);
956 });
957 connect(m_PlateSolveUI.UpdatePosition, &QPushButton::clicked, this, [&]()
958 {
959 const auto center = SkyMap::Instance()->getCenterPoint();
960 m_PlateSolveUI.FitsSolverEstRA->show(center.ra());
961 m_PlateSolveUI.FitsSolverEstDec->show(center.dec());
962 });
963
964 // Warn if the user is not using the internal StellarSolver solver.
965 const SSolver::SolverType type = static_cast<SSolver::SolverType>(Options::solverType());
966 if(type != SSolver::SOLVER_STELLARSOLVER)
967 {
968 m_PlateSolveUI.Solution2->setText(i18n("Warning! This tool only supports the internal StellarSolver solver."));
969 m_PlateSolveUI.Solution1->setText(i18n("Change to that in the Ekos Align options menu."));
970 }
971}
Primary window to view monochrome and color FITS images.
Definition fitsviewer.h:54
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...)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:83
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
QString name(StandardAction id)
KGuiItem save()
KGuiItem discard()
void clicked(bool checked)
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
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)
void accept()
void ignore()
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
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 append(QList< T > &&value)
void reserve(qsizetype size)
qsizetype size() 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 * data() const const
void setWidget(QWidget *widget)
void setWidgetResizable(bool resizable)
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 toLatin1() const const
QByteArray toUtf8() const const
AlignHCenter
UniqueConnection
ItemIsSelectable
MatchExactly
Horizontal
WA_LayoutUsesWidgetRect
QTextStream & dec(QTextStream &stream)
void setFlags(Qt::ItemFlags flags)
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 toString(FormattingOptions options) 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 Oct 11 2024 12:15:12 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.