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 "QtNetwork/qnetworkreply.h"
10#include "auxiliary/kspaths.h"
11#include "fitsdata.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 <QFileDialog>
24#include <QClipboard>
25#include <QIcon>
26#include "ekos/auxiliary/stellarsolverprofile.h"
27#include "ekos/auxiliary/stellarsolverprofileeditor.h"
28
29#include <fits_debug.h>
30#include "fitscommon.h"
31#include <QDesktopServices>
32#include <QUrl>
33#include <QDialog>
34
35QPointer<Ekos::StellarSolverProfileEditor> FITSTab::m_ProfileEditor;
36QPointer<KConfigDialog> FITSTab::m_EditorDialog;
37QPointer<KPageWidgetItem> FITSTab::m_ProfileEditorPage;
38
39constexpr int CAT_OBJ_SORT_ROLE = Qt::UserRole + 1;
40
41FITSTab::FITSTab(FITSViewer *parent) : QWidget(parent)
42{
43 viewer = parent;
44 undoStack = new QUndoStack(this);
45 undoStack->setUndoLimit(10);
46 undoStack->clear();
47 connect(undoStack, SIGNAL(cleanChanged(bool)), this, SLOT(modifyFITSState(bool)));
48
49 m_PlateSolveWidget = new QDialog(this);
50 m_CatalogObjectWidget = new QDialog(this);
51 statWidget = new QDialog(this);
52 fitsHeaderDialog = new QDialog(this);
53 m_HistogramEditor = new FITSHistogramEditor(this);
54 connect(m_HistogramEditor, &FITSHistogramEditor::newHistogramCommand, this, [this](FITSHistogramCommand * command)
55 {
56 undoStack->push(command);
57 });
58}
59
60FITSTab::~FITSTab()
61{
62}
63
64void FITSTab::saveUnsaved()
65{
66 if (undoStack->isClean() || m_View->getMode() != FITS_NORMAL)
67 return;
68
69 QString caption = i18n("Save Changes to FITS?");
70 QString message = i18n("The current FITS file has unsaved changes. Would you like to save before closing it?");
71
72 int ans = KMessageBox::warningContinueCancel(nullptr, message, caption, KStandardGuiItem::save(),
74 if (ans == KMessageBox::Continue)
75 saveFile();
76 if (ans == KMessageBox::Cancel)
77 {
78 undoStack->clear();
79 modifyFITSState();
80 }
81}
82
83void FITSTab::closeEvent(QCloseEvent *ev)
84{
85 saveUnsaved();
86
87 if (undoStack->isClean())
88 ev->accept();
89 else
90 ev->ignore();
91}
92QString FITSTab::getPreviewText() const
93{
94 return previewText;
95}
96
97void FITSTab::setPreviewText(const QString &value)
98{
99 previewText = value;
100}
101
102void FITSTab::selectRecentFITS(int i)
103{
104 loadFile(QUrl::fromLocalFile(recentImages->item(i)->text()));
105}
106
107void FITSTab::clearRecentFITS()
108{
109 disconnect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS);
110 recentImages->clear();
111 connect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS);
112}
113
114bool FITSTab::setupView(FITSMode mode, FITSScale filter)
115{
116 if (m_View.isNull())
117 {
118 m_View.reset(new FITSView(this, mode, filter));
119 m_View->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
120 QVBoxLayout *vlayout = new QVBoxLayout();
121
122 connect(m_View.get(), &FITSView::rectangleUpdated, this, [this](QRect roi)
123 {
124 displayStats(roi.isValid());
125 });
126 fitsSplitter = new QSplitter(Qt::Horizontal, this);
127 fitsTools = new QToolBox();
128
129 stat.setupUi(statWidget);
130 m_PlateSolveUI.setupUi(m_PlateSolveWidget);
131
132 m_PlateSolveUI.editProfile->setIcon(QIcon::fromTheme("document-edit"));
133 m_PlateSolveUI.editProfile->setAttribute(Qt::WA_LayoutUsesWidgetRect);
134
135 const QString EditorID = "FITSSolverProfileEditor";
136 if (!m_EditorDialog)
137 {
138 // These are static, shared by all FITS Viewer tabs.
139 m_EditorDialog = new KConfigDialog(nullptr, EditorID, Options::self());
140 m_ProfileEditor = new Ekos::StellarSolverProfileEditor(nullptr, Ekos::AlignProfiles, m_EditorDialog.data());
141 m_ProfileEditorPage = m_EditorDialog->addPage(m_ProfileEditor.data(),
142 i18n("FITS Viewer Solver Profiles Editor"));
143 }
144
145 connect(m_PlateSolveUI.editProfile, &QAbstractButton::clicked, this, [this, EditorID]
146 {
147 m_ProfileEditor->loadProfile(m_PlateSolveUI.kcfg_FitsSolverProfile->currentText());
148 KConfigDialog * d = KConfigDialog::exists(EditorID);
149 if(d)
150 {
151 d->setCurrentPage(m_ProfileEditorPage);
152 d->show();
153 }
154 });
155
156 connect(m_PlateSolveUI.SolveButton, &QPushButton::clicked, this, &FITSTab::extractImage);
157
158 for (int i = 0; i <= STAT_STDDEV; i++)
159 {
160 for (int j = 0; j < 3; j++)
161 {
162 stat.statsTable->setItem(i, j, new QTableWidgetItem());
163 stat.statsTable->item(i, j)->setTextAlignment(Qt::AlignHCenter);
164 }
165
166 // Set col span for items up to HFR
167 if (i <= STAT_HFR)
168 stat.statsTable->setSpan(i, 0, 1, 3);
169 }
170
171 fitsTools->addItem(statWidget, i18n("Statistics"));
172
173 fitsTools->addItem(m_PlateSolveWidget, i18n("Plate Solving"));
174 initSolverUI();
175
176 // Setup the Catalog Object page
177 m_CatalogObjectUI.setupUi(m_CatalogObjectWidget);
178 m_CatalogObjectItem = fitsTools->addItem(m_CatalogObjectWidget, i18n("Catalog Objects"));
179 initCatalogObject();
180
181 fitsTools->addItem(m_HistogramEditor, i18n("Histogram"));
182
183 header.setupUi(fitsHeaderDialog);
184 fitsTools->addItem(fitsHeaderDialog, i18n("FITS Header"));
185 connect(m_View.get(), &FITSView::headerChanged, this, &FITSTab::loadFITSHeader);
186
187 QVBoxLayout *recentPanelLayout = new QVBoxLayout();
188 QWidget *recentPanel = new QWidget(fitsSplitter);
189 recentPanel->setLayout(recentPanelLayout);
190 fitsTools->addItem(recentPanel, i18n("Recent Images"));
191 recentImages = new QListWidget(recentPanel);
192 recentPanelLayout->addWidget(recentImages);
193 QPushButton *clearRecent = new QPushButton(i18n("Clear"));
194 recentPanelLayout->addWidget(clearRecent);
195 connect(clearRecent, &QPushButton::pressed, this, &FITSTab::clearRecentFITS);
196 connect(recentImages, &QListWidget::currentRowChanged, this, &FITSTab::selectRecentFITS);
197
198 QScrollArea *scrollFitsPanel = new QScrollArea(fitsSplitter);
199 scrollFitsPanel->setWidgetResizable(true);
200 scrollFitsPanel->setWidget(fitsTools);
201
202 fitsSplitter->addWidget(scrollFitsPanel);
203 fitsSplitter->addWidget(m_View.get());
204
205
206 //This code allows the fitsTools to start in a closed state
207 fitsSplitter->setSizes(QList<int>() << 0 << m_View->width() );
208
209 vlayout->addWidget(fitsSplitter);
210
211 stretchUI.reset(new FITSStretchUI(m_View, nullptr));
212 vlayout->addWidget(stretchUI.get());
213
214 connect(fitsSplitter, &QSplitter::splitterMoved, m_HistogramEditor, &FITSHistogramEditor::resizePlot);
215
216 setLayout(vlayout);
217 connect(m_View.get(), &FITSView::newStatus, this, &FITSTab::newStatus);
218 connect(m_View.get(), &FITSView::debayerToggled, this, &FITSTab::debayerToggled);
219 connect(m_View.get(), &FITSView::updated, this, &FITSTab::updated);
220
221 // On Failure to load
222 connect(m_View.get(), &FITSView::failed, this, &FITSTab::failed);
223
224 return true;
225 }
226
227 // returns false if no setup needed.
228 return false;
229}
230
231void FITSTab::loadFile(const QUrl &imageURL, FITSMode mode, FITSScale filter)
232{
233 // check if the address points to an appropriate address
234 if (imageURL.isEmpty() || !imageURL.isValid() || !QFileInfo::exists(imageURL.toLocalFile()))
235 {
236 emit failed(i18nc("File not found: %1", imageURL.toString().toLatin1()));
237 return;
238 }
239
240 if (setupView(mode, filter))
241 {
242
243 // On Success loading image
244 connect(m_View.get(), &FITSView::loaded, this, [&]()
245 {
246 processData();
247 emit loaded();
248 });
249
250 connect(m_View.get(), &FITSView::updated, this, &FITSTab::updated);
251 }
252 else
253 // update tab text
254 modifyFITSState(true, imageURL);
255
256 currentURL = imageURL;
257
258 m_View->setFilter(filter);
259
260 m_View->loadFile(imageURL.toLocalFile());
261}
262
263bool FITSTab::shouldComputeHFR() const
264{
265 if (viewer->isStarsMarked())
266 return true;
267 if (!Options::autoHFR())
268 return false;
269 return ((!m_View.isNull()) && (m_View->getMode() == FITS_NORMAL));
270}
271
272void FITSTab::processData()
273{
274 const QSharedPointer<FITSData> &imageData = m_View->imageData();
275
276 m_HistogramEditor->setImageData(imageData);
277
278 // Only construct histogram if it is actually visible
279 // Otherwise wait until histogram is needed before creating it.
280 // if (fitsSplitter->sizes().at(0) != 0 && !imageData->isHistogramConstructed() &&
281 // !Options::nonLinearHistogram())
282 // {
283 // imageData->constructHistogram();
284 // }
285
286 if (shouldComputeHFR())
287 {
288 m_View->searchStars();
289 qCDebug(KSTARS_FITS) << "FITS HFR:" << imageData->getHFR();
290 }
291
292 displayStats();
293
294 loadFITSHeader();
295
296 // Don't add it to the list if it is already there
297 if (recentImages->findItems(currentURL.toLocalFile(), Qt::MatchExactly).count() == 0)
298 {
299 //Don't add it to the list if it is a preview
300 if(!imageData->filename().startsWith(QDir::tempPath()))
301 {
302 disconnect(recentImages, &QListWidget::currentRowChanged, this,
303 &FITSTab::selectRecentFITS);
304 recentImages->addItem(imageData->filename());
305 recentImages->setCurrentRow(recentImages->count() - 1);
306 connect(recentImages, &QListWidget::currentRowChanged, this,
307 &FITSTab::selectRecentFITS);
308 }
309 }
310
311 // This could both compute the HFRs and setup the graphics, however,
312 // if shouldComputeHFR() above is true, then that will compute the HFRs
313 // and this would notice that and just setup graphics. They are separated
314 // for the case where the graphics is not desired.
315 if (viewer->isStarsMarked())
316 {
317 m_View->toggleStars(true);
318 m_View->updateFrame();
319 }
320
321 // if (Options::nonLinearHistogram())
322 // m_HistogramEditor->createNonLinearHistogram();
323
324 stretchUI->generateHistogram();
325}
326
327bool FITSTab::loadData(const QSharedPointer<FITSData> &data, FITSMode mode, FITSScale filter)
328{
329 setupView(mode, filter);
330
331 // Empty URL
332 currentURL = QUrl();
333
334 if (viewer->isStarsMarked())
335 {
336 m_View->toggleStars(true);
337 //view->updateFrame();
338 }
339
340 m_View->setFilter(filter);
341
342 if (!m_View->loadData(data))
343 {
344 // On Failure to load
345 // connect(view.get(), &FITSView::failed, this, &FITSTab::failed);
346 return false;
347 }
348
349 processData();
350 return true;
351}
352
353void FITSTab::modifyFITSState(bool clean, const QUrl &imageURL)
354{
355 if (clean)
356 {
357 if (undoStack->isClean() == false)
358 undoStack->setClean();
359
360 mDirty = false;
361 }
362 else
363 mDirty = true;
364
365 emit changeStatus(clean, imageURL);
366}
367
368bool FITSTab::saveImage(const QString &filename)
369{
370 return m_View->saveImage(filename);
371}
372
373void FITSTab::copyFITS()
374{
375 QApplication::clipboard()->setImage(m_View->getDisplayImage());
376}
377
378void FITSTab::histoFITS()
379{
380 fitsTools->setCurrentIndex(1);
381 if(m_View->width() > 200)
382 fitsSplitter->setSizes(QList<int>() << 200 << m_View->width() - 200);
383 else
384 fitsSplitter->setSizes(QList<int>() << 50 << 50);
385}
386
387void FITSTab::displayStats(bool roi)
388{
389 const QSharedPointer<FITSData> &imageData = m_View->imageData();
390
391 stat.statsTable->item(STAT_WIDTH, 0)->setText(QString::number(imageData->width(roi)));
392 stat.statsTable->item(STAT_HEIGHT, 0)->setText(QString::number(imageData->height(roi)));
393 stat.statsTable->item(STAT_BITPIX, 0)->setText(QString::number(imageData->bpp()));
394
395 if (!roi)
396 stat.statsTable->item(STAT_HFR, 0)->setText(QString::number(imageData->getHFR(), 'f', 3));
397 else
398 stat.statsTable->item(STAT_HFR, 0)->setText("---");
399
400 if (imageData->channels() == 1)
401 {
402 for (int i = STAT_MIN; i <= STAT_STDDEV; i++)
403 {
404 if (stat.statsTable->columnSpan(i, 0) != 3)
405 stat.statsTable->setSpan(i, 0, 1, 3);
406 }
407
408 stat.statsTable->horizontalHeaderItem(0)->setText(i18n("Value"));
409 stat.statsTable->hideColumn(1);
410 stat.statsTable->hideColumn(2);
411 }
412 else
413 {
414 for (int i = STAT_MIN; i <= STAT_STDDEV; i++)
415 {
416 if (stat.statsTable->columnSpan(i, 0) != 1)
417 stat.statsTable->setSpan(i, 0, 1, 1);
418 }
419
420 stat.statsTable->horizontalHeaderItem(0)->setText(i18nc("Red", "R"));
421 stat.statsTable->showColumn(1);
422 stat.statsTable->showColumn(2);
423 }
424
425 if (!Options::nonLinearHistogram() && !imageData->isHistogramConstructed())
426 imageData->constructHistogram();
427
428 for (int i = 0; i < imageData->channels(); i++)
429 {
430 stat.statsTable->item(STAT_MIN, i)->setText(QString::number(imageData->getMin(i, roi), 'f', 3));
431 stat.statsTable->item(STAT_MAX, i)->setText(QString::number(imageData->getMax(i, roi), 'f', 3));
432 stat.statsTable->item(STAT_MEAN, i)->setText(QString::number(imageData->getMean(i, roi), 'f', 3));
433 stat.statsTable->item(STAT_MEDIAN, i)->setText(QString::number(imageData->getMedian(i, roi), 'f', 3));
434 stat.statsTable->item(STAT_STDDEV, i)->setText(QString::number(imageData->getStdDev(i, roi), 'f', 3));
435 }
436}
437
438void FITSTab::statFITS()
439{
440 fitsTools->setCurrentIndex(0);
441 if(m_View->width() > 200)
442 fitsSplitter->setSizes(QList<int>() << 200 << m_View->width() - 200);
443 else
444 fitsSplitter->setSizes(QList<int>() << 50 << 50);
445 displayStats();
446}
447
448void FITSTab::loadFITSHeader()
449{
450 const QSharedPointer<FITSData> &imageData = m_View->imageData();
451 if (!imageData)
452 return;
453
454 int nkeys = imageData->getRecords().size();
455 int counter = 0;
456 header.tableWidget->setRowCount(nkeys);
457 for (const auto &oneRecord : imageData->getRecords())
458 {
459 QTableWidgetItem *tempItem = new QTableWidgetItem(oneRecord.key);
461 header.tableWidget->setItem(counter, 0, tempItem);
462 tempItem = new QTableWidgetItem(oneRecord.value.toString());
464 header.tableWidget->setItem(counter, 1, tempItem);
465 tempItem = new QTableWidgetItem(oneRecord.comment);
467 header.tableWidget->setItem(counter, 2, tempItem);
468 counter++;
469 }
470
471 header.tableWidget->setColumnWidth(0, 100);
472 header.tableWidget->setColumnWidth(1, 100);
473 header.tableWidget->setColumnWidth(2, 250);
474}
475
476void FITSTab::loadCatalogObjects()
477{
478 // Check pointers are OK
479 if (!m_View)
480 return;
481 const QSharedPointer<FITSData> &imageData = m_View->imageData();
482 if (!imageData)
483 return;
484 QList<CatObject> catObjects = imageData->getCatObjects();
485
486 // Disable sorting whilst building the table
487 m_CatalogObjectUI.tableView->setSortingEnabled(false);
488 // Remove all rows
489 m_CatObjModel.removeRows(0, m_CatObjModel.rowCount());
490
491 int counter = 0, total = 0, highlightRow = -1;
492 QPixmap cdsPortalPixmap = QPixmap(":/icons/cdsportal.svg").scaled(50, 50, Qt::KeepAspectRatio, Qt::SmoothTransformation);
493 QPixmap simbadPixmap = QPixmap(":/icons/simbad.svg").scaled(50, 50, Qt::KeepAspectRatio, Qt::SmoothTransformation);
494 QPixmap nedPixmap = QPixmap(":/icons/NED.png").scaled(50, 50, Qt::KeepAspectRatio, Qt::SmoothTransformation);
495 for (const CatObject &catObject : catObjects)
496 {
497 total++;
498 if (!catObject.show)
499 continue;
500 m_CatObjModel.setRowCount(counter + 1);
501
502 // Num
503 QStandardItem* tempItem = new QStandardItem(QString::number(catObject.num));
504 tempItem->setData(catObject.num, CAT_OBJ_SORT_ROLE);
506 m_CatObjModel.setItem(counter, CAT_NUM, tempItem);
507
508 // CDS Portal - no sorting
509 tempItem = new QStandardItem();
510 tempItem->setData(cdsPortalPixmap, Qt::DecorationRole);
511 m_CatObjModel.setItem(counter, CAT_CDSPORTAL, tempItem);
512
513 // Simbad - no sorting
514 tempItem = new QStandardItem();
515 tempItem->setData(simbadPixmap, Qt::DecorationRole);
516 m_CatObjModel.setItem(counter, CAT_SIMBAD, tempItem);
517
518 // NED - no sorting
519 tempItem = new QStandardItem();
520 tempItem->setData(nedPixmap, Qt::DecorationRole);
521 m_CatObjModel.setItem(counter, CAT_NED, tempItem);
522
523 // Object
524 tempItem = new QStandardItem(catObject.name);
525 tempItem->setData(catObject.name, CAT_OBJ_SORT_ROLE);
526 m_CatObjModel.setItem(counter, CAT_OBJECT, tempItem);
527
528 // Type code
529 tempItem = new QStandardItem(catObject.typeCode);
530 tempItem->setData(catObject.typeCode, CAT_OBJ_SORT_ROLE);
531 m_CatObjModel.setItem(counter, CAT_TYPECODE, tempItem);
532
533 // Type label
534 tempItem = new QStandardItem(catObject.typeLabel);
535 tempItem->setData(catObject.typeLabel, CAT_OBJ_SORT_ROLE);
536 m_CatObjModel.setItem(counter, CAT_TYPELABEL, tempItem);
537
538 // Coordinates
539 QString coordStr = QString("%1 %2").arg(catObject.r.toHMSString(false, true))
540 .arg(catObject.d.toDMSString(true, false, true));
541 tempItem = new QStandardItem(coordStr);
542 tempItem->setData(coordStr, CAT_OBJ_SORT_ROLE);
543 m_CatObjModel.setItem(counter, CAT_COORDS, tempItem);
544
545 // Distance
546 tempItem = new QStandardItem(QString::number(catObject.dist));
547 tempItem->setData(catObject.dist, CAT_OBJ_SORT_ROLE);
549 m_CatObjModel.setItem(counter, CAT_DISTANCE, tempItem);
550
551 // Magnitude
552 QString magStr = (catObject.magnitude <= 0.0) ? "" : QString("%1").arg(catObject.magnitude, 0, 'f', 1);
553 tempItem = new QStandardItem(magStr);
554 tempItem->setData(catObject.magnitude, CAT_OBJ_SORT_ROLE);
556 m_CatObjModel.setItem(counter, CAT_MAGNITUDE, tempItem);
557
558 // Size
559 QString sizeStr = (catObject.size <= 0.0) ? "" : QString("%1").arg(catObject.size, 0, 'f', 0);
560 tempItem = new QStandardItem(sizeStr);
561 tempItem->setData(catObject.size, CAT_OBJ_SORT_ROLE);
563 m_CatObjModel.setItem(counter, CAT_SIZE, tempItem);
564
565 if (catObject.highlight)
566 highlightRow = counter;
567
568 counter++;
569 }
570
571 // Resize the columns to the data
572 m_CatalogObjectUI.tableView->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);
573
574 if (highlightRow >= 0)
575 catHighlightRow(highlightRow);
576
577 // Enable sorting
578 m_CatObjModel.setSortRole(CAT_OBJ_SORT_ROLE);
579 m_CatalogObjectUI.tableView->setSortingEnabled(true);
580
581 // Update the status widget unless the query is still in progress
582 if (!imageData->getCatQueryInProgress())
583 m_CatalogObjectUI.status->setText(i18n("%1 / %2 Simbad objects loaded", counter, total));
584}
585
586void FITSTab::queriedCatalogObjects()
587{
588 // Show the Catalog Objects item (unless already shown)
589 if (fitsTools->currentIndex() != m_CatalogObjectItem)
590 {
591 fitsTools->setCurrentIndex(m_CatalogObjectItem);
592 if(m_View->width() > 200)
593 fitsSplitter->setSizes(QList<int>() << 200 << m_View->width() - 200);
594 else
595 fitsSplitter->setSizes(QList<int>() << 50 << 50);
596 }
597
598 m_CatalogObjectUI.status->setText(i18n("Simbad query in progress..."));
599}
600
601void FITSTab::catQueryFailed(const QString text)
602{
603 m_CatalogObjectUI.status->setText(text);
604}
605
606void FITSTab::catReset()
607{
608 m_CatalogObjectUI.status->setText("");
609 // Remove all rows from the table
610 m_CatObjModel.removeRows(0, m_CatObjModel.rowCount());
611}
612
613void FITSTab::catHighlightRow(const int row)
614{
615 m_CatalogObjectUI.tableView->selectRow(row);
616 QModelIndex index = m_CatalogObjectUI.tableView->indexAt(QPoint(row, CAT_NUM));
617 if (index.isValid())
618 m_CatalogObjectUI.tableView->scrollTo(index, QAbstractItemView::EnsureVisible);
619}
620
621void FITSTab::catHighlightChanged(const int highlight)
622{
623 if (!m_View)
624 return;
625 const QSharedPointer<FITSData> &imageData = m_View->imageData();
626 if (!imageData)
627 return;
628 QList<CatObject> catObjects = imageData->getCatObjects();
629
630 if (highlight < 0 || highlight >= catObjects.size())
631 return;
632
633 int num = catObjects[highlight].num;
634 for (int i = 0; i < m_CatObjModel.rowCount(); i++)
635 {
636 bool ok;
637 QStandardItem *itm = m_CatObjModel.item(i, CAT_NUM);
638 if (itm->text().toInt(&ok) == num)
639 {
640 int itmRow = itm->row();
641 QModelIndex currentIndex = m_CatalogObjectUI.tableView->currentIndex();
642 if (currentIndex.isValid())
643 {
644 int currentRow = m_CatObjModel.itemFromIndex(currentIndex)->row();
645 if (currentRow == itmRow)
646 // Row to highlight is already highlighted - so nothing to do
647 break;
648 }
649 // Set the new highlight to the new row
650 catHighlightRow(itmRow);
651 }
652 }
653}
654
655void FITSTab::catRowChanged(const QModelIndex &current, const QModelIndex &previous)
656{
657 if (!m_View)
658 return;
659 const QSharedPointer<FITSData> &imageData = m_View->imageData();
660 if (!imageData)
661 return;
662
663 // Get the "Num" of the selected (current) row
664 int selNum = -1, deselNum = -1;
665 bool ok;
666 QStandardItem * selItem = m_CatObjModel.itemFromIndex(current);
667 if (selItem)
668 selNum = m_CatObjModel.item(selItem->row(), CAT_NUM)->text().toInt(&ok);
669
670 // Get the "Num" of the deselected (previous) row
671 QStandardItem * deselItem = m_CatObjModel.itemFromIndex(previous);
672 if (deselItem)
673 deselNum = m_CatObjModel.item(deselItem->row(), CAT_NUM)->text().toInt(&ok);
674
675 if (selNum >= 0)
676 {
677 imageData->highlightCatObject(selNum - 1, deselNum - 1);
678 m_View->updateFrame();
679 }
680}
681
682void FITSTab::catCellDoubleClicked(const QModelIndex &index)
683{
684 QStandardItem * item = m_CatObjModel.itemFromIndex(index);
685 int row = item->row();
686 int col = item->column();
687
688 QString catObjectName = m_CatObjModel.item(row, CAT_OBJECT)->text();
689
690 if (col == CAT_CDSPORTAL)
691 launchCDS(catObjectName);
692 else if (col == CAT_SIMBAD)
693 launchSimbad(catObjectName);
694 else if (col == CAT_NED)
695 launchNED(catObjectName);
696 else if (col == CAT_TYPECODE || col == CAT_TYPELABEL)
697 launchCatTypeFilterDialog();
698}
699
700void FITSTab::launchCatTypeFilterDialog()
701{
702 m_CatObjTypeFilterDialog->show();
703 m_CatObjTypeFilterDialog->raise();
704}
705
706void FITSTab::showCatObjNames(bool enabled)
707{
708 if (!m_View)
709 return;
710 const QSharedPointer<FITSData> &imageData = m_View->imageData();
711 if (!imageData)
712 return;
713
714 Options::setFitsCatObjShowNames(enabled);
715 m_View->updateFrame();
716}
717
718void FITSTab::launchSimbad(QString name)
719{
720 QUrl url = QUrl("https://simbad.u-strasbg.fr/simbad/sim-id");
721 QString ident = QString("Ident=%1").arg(name);
722 ident.replace("+", "%2B");
723 ident.remove(QRegularExpression("[\\[\\]]+"));
724 url.setQuery(ident);
725
727 qCDebug(KSTARS_FITS) << "Unable to open Simbad url:" << url;
728}
729
730void FITSTab::launchCDS(QString name)
731{
732 QUrl url = QUrl("https://cdsportal.u-strasbg.fr/");
733 QString ident = QString("target=%1").arg(name);
734 ident.replace("+", "%2B");
735 ident.remove(QRegularExpression("[\\[\\]]+"));
736 url.setQuery(ident);
737
739 qCDebug(KSTARS_FITS) << "Unable to open CDS Portal url:" << url;
740}
741
742void FITSTab::launchNED(QString name)
743{
744 QUrl url = QUrl("https://ned.ipac.caltech.edu/cgi-bin/objsearch");
745 QString query = QString("objname=%1").arg(name);
746 query.replace("+", "%2B");
747 query.remove(QRegularExpression("[\\[\\]]+"));
748 url.setQuery(query);
749
751 qCDebug(KSTARS_FITS) << "Unable to open NED url" << url;
752}
753
754void FITSTab::initCatalogObject()
755{
756 // Setup MVC
757 m_CatalogObjectUI.tableView->setModel(&m_CatObjModel);
758
759 // Set the column headers
760 QStringList Headers { i18n("Num"),
761 i18n("CDS Portal"),
762 i18n("Simbad"),
763 i18n("NED"),
764 i18n("Object"),
765 i18n("Type Code"),
766 i18n("Type Label"),
767 i18n("Coordinates"),
768 i18n("Distance"),
769 i18n("Magnitude"),
770 i18n("Size") };
771 m_CatObjModel.setColumnCount(CAT_MAX_COLS);
772 m_CatObjModel.setHorizontalHeaderLabels(Headers);
773
774 // Setup tooltips on column headers
775 m_CatObjModel.setHeaderData(CAT_NUM, Qt::Horizontal, i18n("Item reference number"), Qt::ToolTipRole);
776 m_CatObjModel.setHeaderData(CAT_CDSPORTAL, Qt::Horizontal, i18n("Double click to launch CDS Portal browser"),
778 m_CatObjModel.setHeaderData(CAT_SIMBAD, Qt::Horizontal, i18n("Double click to launch Simbad browser"), Qt::ToolTipRole);
779 m_CatObjModel.setHeaderData(CAT_NED, Qt::Horizontal, i18n("Double click to launch NED browser"), Qt::ToolTipRole);
780 m_CatObjModel.setHeaderData(CAT_OBJECT, Qt::Horizontal, i18n("Catalog Object"), Qt::ToolTipRole);
781 m_CatObjModel.setHeaderData(CAT_TYPECODE, Qt::Horizontal,
782 i18n("Object Type Code. Double click to launch Object Type Filter.\n\nSee https://simbad.cds.unistra.fr/guide/otypes.htx for more details"),
784 m_CatObjModel.setHeaderData(CAT_TYPELABEL, Qt::Horizontal,
785 i18n("Object Type Label. Double click to launch Object Type Filter.\n\nSee https://simbad.cds.unistra.fr/guide/otypes.htx for more details"),
787 m_CatObjModel.setHeaderData(CAT_COORDS, Qt::Horizontal, i18n("Object coordinates"), Qt::ToolTipRole);
788 m_CatObjModel.setHeaderData(CAT_DISTANCE, Qt::Horizontal, i18n("Object distance in arcmins from the search center"),
790 m_CatObjModel.setHeaderData(CAT_MAGNITUDE, Qt::Horizontal, i18n("Object V magnitude"), Qt::ToolTipRole);
791 m_CatObjModel.setHeaderData(CAT_SIZE, Qt::Horizontal, i18n("Object major coordinate size in arcsmins"), Qt::ToolTipRole);
792
793 // Setup delegates for each column
794 QStyledItemDelegate *delegate = new QStyledItemDelegate(m_CatalogObjectUI.tableView);
795 m_CatalogObjectUI.tableView->setItemDelegateForColumn(CAT_NUM, delegate);
796 m_CatalogObjectUI.tableView->setItemDelegateForColumn(CAT_CDSPORTAL, delegate);
797 m_CatalogObjectUI.tableView->setItemDelegateForColumn(CAT_SIMBAD, delegate);
798 m_CatalogObjectUI.tableView->setItemDelegateForColumn(CAT_NED, delegate);
799 m_CatalogObjectUI.tableView->setItemDelegateForColumn(CAT_OBJECT, delegate);
800 m_CatalogObjectUI.tableView->setItemDelegateForColumn(CAT_TYPECODE, delegate);
801 m_CatalogObjectUI.tableView->setItemDelegateForColumn(CAT_TYPELABEL, delegate);
802 m_CatalogObjectUI.tableView->setItemDelegateForColumn(CAT_COORDS, delegate);
803 m_CatalogObjectUI.tableView->setItemDelegateForColumn(CAT_DISTANCE, delegate);
804 m_CatalogObjectUI.tableView->setItemDelegateForColumn(CAT_MAGNITUDE, delegate);
805 m_CatalogObjectUI.tableView->setItemDelegateForColumn(CAT_SIZE, delegate);
806
807 m_CatalogObjectUI.tableView->setAutoScroll(true);
808
809 connect(m_View.get(), &FITSView::catLoaded, this, &FITSTab::loadCatalogObjects);
810 connect(m_View.get(), &FITSView::catQueried, this, &FITSTab::queriedCatalogObjects);
811 connect(m_View.get(), &FITSView::catQueryFailed, this, &FITSTab::catQueryFailed);
812 connect(m_View.get(), &FITSView::catReset, this, &FITSTab::catReset);
813 connect(m_View.get(), &FITSView::catHighlightChanged, this, &FITSTab::catHighlightChanged);
814 connect(m_CatalogObjectUI.tableView->selectionModel(), &QItemSelectionModel::currentRowChanged, this,
815 &FITSTab::catRowChanged);
816 connect(m_CatalogObjectUI.tableView, &QAbstractItemView::doubleClicked, this, &FITSTab::catCellDoubleClicked);
817 connect(m_CatalogObjectUI.filterPB, &QPushButton::clicked, this, &FITSTab::launchCatTypeFilterDialog);
818
819 // Setup the Object Type filter popup
820 setupCatObjTypeFilter();
821
822 // Set the Show Names checkbox from Options
823 m_CatalogObjectUI.kcfg_FitsCatObjShowNames->setChecked(Options::fitsCatObjShowNames());
824 connect(m_CatalogObjectUI.kcfg_FitsCatObjShowNames, &QCheckBox::toggled, this, &FITSTab::showCatObjNames);
825}
826
827void FITSTab::setupCatObjTypeFilter()
828{
829 // Setup the dialog box
830 m_CatObjTypeFilterDialog = new QDialog(this);
831 m_CatObjTypeFilterUI.setupUi(m_CatObjTypeFilterDialog);
832#ifdef Q_OS_MACOS
833 m_CatObjTypeFilterDialog->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
834#endif
835
836 // Setup the buttons
837 QPushButton *applyButton = m_CatObjTypeFilterUI.buttonBox->button(QDialogButtonBox::Apply);
838 connect(applyButton, &QPushButton::clicked, this, &FITSTab::applyTypeFilter);
839 m_CheckAllButton = m_CatObjTypeFilterUI.buttonBox->addButton("Check All", QDialogButtonBox::ActionRole);
840 connect(m_CheckAllButton, &QPushButton::clicked, this, &FITSTab::checkAllTypeFilter);
841 m_UncheckAllButton = m_CatObjTypeFilterUI.buttonBox->addButton("Uncheck All", QDialogButtonBox::ActionRole);
842 connect(m_UncheckAllButton, &QPushButton::clicked, this, &FITSTab::uncheckAllTypeFilter);
843
844 // Setup the tree
845 QTreeWidgetItem * treeItem[CAT_OBJ_MAX_DEPTH];
846 for (int i = 0; i < MAX_CAT_OBJ_TYPES; i++)
847 {
848 const int depth = catObjTypes[i].depth;
849 if (depth < 0 || depth >= MAX_CAT_OBJ_TYPES)
850 continue;
851
852 if (depth == 0)
853 // Top level node
854 treeItem[depth] = new QTreeWidgetItem(m_CatObjTypeFilterUI.tree);
855 else
856 // Child node
857 treeItem[depth] = new QTreeWidgetItem(treeItem[depth - 1]);
858
859 treeItem[depth]->setCheckState(CATTYPE_CODE, Qt::Unchecked);
860 treeItem[depth]->setText(CATTYPE_CODE, catObjTypes[i].code);
861 treeItem[depth]->setText(CATTYPE_CANDCODE, catObjTypes[i].candidateCode);
862 treeItem[depth]->setText(CATTYPE_LABEL, catObjTypes[i].label);
863 treeItem[depth]->setText(CATTYPE_DESCRIPTION, catObjTypes[i].description);
864 treeItem[depth]->setText(CATTYPE_COMMENTS, catObjTypes[i].comments);
865 }
866 m_CatObjTypeFilterUI.tree->expandAll();
867 for (int i = 0; i < CAT_OBJ_MAX_DEPTH; i++)
868 {
869 m_CatObjTypeFilterUI.tree->resizeColumnToContents(i);
870 }
871
872 connect(m_CatObjTypeFilterUI.tree, &QTreeWidget::itemChanged, this, &FITSTab::typeFilterItemChanged);
873}
874
875void FITSTab::applyTypeFilter()
876{
877 if (!m_View)
878 return;
879 const QSharedPointer<FITSData> &imageData = m_View->imageData();
880 if (!imageData)
881 return;
882
883 QStringList filters;
884 QList<QTreeWidgetItem *> items = m_CatObjTypeFilterUI.tree->findItems("*",
886 for (auto item : items)
887 {
888 if (item->checkState(0) == Qt::Checked)
889 {
890 if (!item->text(0).isEmpty())
891 filters.push_back(item->text(0));
892 if (!item->text(1).isEmpty())
893 filters.push_back(item->text(1));
894 }
895 }
896
897 // Store the new filter paradigm
898 m_View->imageData()->setCatObjectsFilters(filters);
899 // Process the data according to the new filter paradigm
900 m_View->imageData()->filterCatObjects();
901 m_View->updateFrame();
902 // Reload FITSTab
903 loadCatalogObjects();
904}
905
906void FITSTab::checkAllTypeFilter()
907{
908 QList<QTreeWidgetItem *> items = m_CatObjTypeFilterUI.tree->findItems("*",
910 for (auto item : items)
911 {
912 item->setCheckState(0, Qt::Checked);
913 }
914}
915
916void FITSTab::uncheckAllTypeFilter()
917{
918 QList<QTreeWidgetItem *> items = m_CatObjTypeFilterUI.tree->findItems("*",
920 for (auto item : items)
921 {
923 }
924}
925
926void FITSTab::typeFilterItemChanged(QTreeWidgetItem *item, int column)
927{
928 if (column != 0)
929 return;
930
931 Qt::CheckState check = item->checkState(column);
932 for (int i = 0; i < item->childCount(); i++)
933 {
934 QTreeWidgetItem *child = item->child(i);
935 child->setCheckState(column, check);
936 }
937}
938
939void FITSTab::headerFITS()
940{
941 fitsTools->setCurrentIndex(2);
942 if(m_View->width() > 200)
943 fitsSplitter->setSizes(QList<int>() << 200 << m_View->width() - 200);
944 else
945 fitsSplitter->setSizes(QList<int>() << 50 << 50);
946}
947
948bool FITSTab::saveFile()
949{
950 QUrl backupCurrent = currentURL;
951 QUrl currentDir(Options::fitsDir());
952 currentDir.setScheme("file");
953
954 if (currentURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || currentURL.toLocalFile().contains("/Temp"))
955 currentURL.clear();
956
957 // If no changes made, return.
958 if (mDirty == false && !currentURL.isEmpty())
959 return false;
960
961 if (currentURL.isEmpty())
962 {
963 QString selectedFilter;
964#ifdef Q_OS_MACOS //For some reason, the other code caused KStars to crash on MacOS
965 currentURL =
966 QFileDialog::getSaveFileUrl(KStars::Instance(), i18nc("@title:window", "Save FITS"), currentDir,
967 "Images (*.fits *.fits.gz *.fit *.xisf *.jpg *.jpeg *.png)");
968#else
969 currentURL =
970 QFileDialog::getSaveFileUrl(KStars::Instance(), i18nc("@title:window", "Save FITS"), currentDir,
971 "FITS (*.fits *.fits.gz *.fit);;XISF (*.xisf);;JPEG (*.jpg *.jpeg);;PNG (*.png)", &selectedFilter);
972#endif
973 // if user presses cancel
974 if (currentURL.isEmpty())
975 {
976 currentURL = backupCurrent;
977 return false;
978 }
979
980 // If no extension is selected append one
981 if (currentURL.toLocalFile().contains('.') == 0)
982 {
983 if (selectedFilter.contains("XISF"))
984 currentURL.setPath(currentURL.toLocalFile() + ".xisf");
985 else if (selectedFilter.contains("JPEG"))
986 currentURL.setPath(currentURL.toLocalFile() + ".jpg");
987 else if (selectedFilter.contains("PNG"))
988 currentURL.setPath(currentURL.toLocalFile() + ".png");
989 else
990 currentURL.setPath(currentURL.toLocalFile() + ".fits");
991 }
992 }
993
994 if (currentURL.isValid())
995 {
996 QString localFile = currentURL.toLocalFile();
997 // if (localFile.contains(".fit"))
998 // localFile = "!" + localFile;
999
1000 if (!saveImage(localFile))
1001 {
1002 KSNotification::error(i18n("Image save error: %1", m_View->imageData()->getLastError()), i18n("Image Save"));
1003 return false;
1004 }
1005
1006 emit newStatus(i18n("File saved to %1", currentURL.url()), FITS_MESSAGE);
1007 modifyFITSState();
1008 return true;
1009 }
1010 else
1011 {
1012 QString message = i18n("Invalid URL: %1", currentURL.url());
1013 KSNotification::sorry(message, i18n("Invalid URL"));
1014 return false;
1015 }
1016}
1017
1018bool FITSTab::saveFileAs()
1019{
1020 currentURL.clear();
1021 return saveFile();
1022}
1023
1024void FITSTab::ZoomIn()
1025{
1026 QPoint oldCenter = m_View->getImagePoint(m_View->viewport()->rect().center());
1027 m_View->ZoomIn();
1028 m_View->cleanUpZoom(oldCenter);
1029}
1030
1031void FITSTab::ZoomOut()
1032{
1033 QPoint oldCenter = m_View->getImagePoint(m_View->viewport()->rect().center());
1034 m_View->ZoomOut();
1035 m_View->cleanUpZoom(oldCenter);
1036}
1037
1038void FITSTab::ZoomDefault()
1039{
1040 QPoint oldCenter = m_View->getImagePoint(m_View->viewport()->rect().center());
1041 m_View->ZoomDefault();
1042 m_View->cleanUpZoom(oldCenter);
1043}
1044
1045void FITSTab::tabPositionUpdated()
1046{
1047 undoStack->setActive(true);
1048 m_View->emitZoom();
1049 emit newStatus(QString("%1x%2").arg(m_View->imageData()->width()).arg(m_View->imageData()->height()),
1050 FITS_RESOLUTION);
1051}
1052
1053void FITSTab::setStretchValues(double shadows, double midtones, double highlights)
1054{
1055 if (stretchUI)
1056 stretchUI->setStretchValues(shadows, midtones, highlights);
1057}
1058
1059void FITSTab::setAutoStretch()
1060{
1061 if (!m_View->getAutoStretch())
1062 m_View->setAutoStretchParams();
1063}
1064
1065namespace
1066{
1067const QList<SSolver::Parameters> getSSolverParametersList(Ekos::ProfileGroup module)
1068{
1069 QString savedProfiles;
1070 switch(module)
1071 {
1072 case Ekos::AlignProfiles:
1073 default:
1074 savedProfiles = QDir(KSPaths::writableLocation(
1075 QStandardPaths::AppLocalDataLocation)).filePath("SavedAlignProfiles.ini");
1076 return QFile(savedProfiles).exists() ?
1077 StellarSolver::loadSavedOptionsProfiles(savedProfiles) :
1078 Ekos::getDefaultAlignOptionsProfiles();
1079 break;
1080 case Ekos::FocusProfiles:
1081 savedProfiles = QDir(KSPaths::writableLocation(
1082 QStandardPaths::AppLocalDataLocation)).filePath("SavedFocusProfiles.ini");
1083 return QFile(savedProfiles).exists() ?
1084 StellarSolver::loadSavedOptionsProfiles(savedProfiles) :
1085 Ekos::getDefaultFocusOptionsProfiles();
1086 break;
1087 case Ekos::GuideProfiles:
1088 savedProfiles = QDir(KSPaths::writableLocation(
1089 QStandardPaths::AppLocalDataLocation)).filePath("SavedGuideProfiles.ini");
1090 return QFile(savedProfiles).exists() ?
1091 StellarSolver::loadSavedOptionsProfiles(savedProfiles) :
1092 Ekos::getDefaultGuideOptionsProfiles();
1093 break;
1094 case Ekos::HFRProfiles:
1095 savedProfiles = QDir(KSPaths::writableLocation(
1096 QStandardPaths::AppLocalDataLocation)).filePath("SavedHFRProfiles.ini");
1097 return QFile(savedProfiles).exists() ?
1098 StellarSolver::loadSavedOptionsProfiles(savedProfiles) :
1099 Ekos::getDefaultHFROptionsProfiles();
1100 break;
1101 }
1102}
1103} // namespace
1104
1105void FITSTab::setupSolver(bool extractOnly)
1106{
1107 auto parameters = getSSolverParametersList(static_cast<Ekos::ProfileGroup>(Options::fitsSolverModule())).at(
1108 m_PlateSolveUI.kcfg_FitsSolverProfile->currentIndex());
1109 parameters.search_radius = m_PlateSolveUI.kcfg_FitsSolverRadius->value();
1110 if (extractOnly)
1111 {
1112 if (!m_PlateSolveUI.kcfg_FitsSolverLinear->isChecked())
1113 {
1114 // If image is non-linear seed the threshold offset with the background using median pixel value. Note
1115 // that there is a bug in the median calculation due to an issue compiling on Mac that means that not
1116 // all datatypes are supported by the median calculation. If median is zero use the mean instead.
1117 double offset = m_View->imageData()->getAverageMedian();
1118 if (offset <= 0.0)
1119 offset = m_View->imageData()->getAverageMean();
1120 parameters.threshold_offset = offset;
1121 }
1122
1123 m_Solver.reset(new SolverUtils(parameters, parameters.solverTimeLimit, SSolver::EXTRACT), &QObject::deleteLater);
1124 connect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::extractorDone, Qt::UniqueConnection);
1125 }
1126 else
1127 {
1128 // If image is non-linear then set the offset to the average background in the image
1129 // which was found in the first solver (extract only) run.
1130 if (m_Solver && !m_PlateSolveUI.kcfg_FitsSolverLinear->isChecked())
1131 parameters.threshold_offset = m_Solver->getBackground().global;
1132
1133 m_Solver.reset(new SolverUtils(parameters, parameters.solverTimeLimit, SSolver::SOLVE), &QObject::deleteLater);
1134 connect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::solverDone, Qt::UniqueConnection);
1135 }
1136
1137 const int imageWidth = m_View->imageData()->width();
1138 const int imageHeight = m_View->imageData()->height();
1139 if (m_PlateSolveUI.kcfg_FitsSolverUseScale->isChecked() && imageWidth != 0 && imageHeight != 0)
1140 {
1141 const double scale = m_PlateSolveUI.kcfg_FitsSolverScale->value();
1142 double lowScale = scale * 0.8;
1143 double highScale = scale * 1.2;
1144
1145 // solver utils uses arcsecs per pixel only
1146 const int units = m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits->currentIndex();
1147 if (units == SSolver::DEG_WIDTH)
1148 {
1149 lowScale = (lowScale * 3600) / std::max(imageWidth, imageHeight);
1150 highScale = (highScale * 3600) / std::min(imageWidth, imageHeight);
1151 }
1152 else if (units == SSolver::ARCMIN_WIDTH)
1153 {
1154 lowScale = (lowScale * 60) / std::max(imageWidth, imageHeight);
1155 highScale = (highScale * 60) / std::min(imageWidth, imageHeight);
1156 }
1157
1158 m_Solver->useScale(m_PlateSolveUI.kcfg_FitsSolverUseScale->isChecked(), lowScale, highScale);
1159 }
1160 else m_Solver->useScale(false, 0, 0);
1161
1162 if (m_PlateSolveUI.kcfg_FitsSolverUsePosition->isChecked())
1163 {
1164 bool ok;
1165 const dms ra = m_PlateSolveUI.FitsSolverEstRA->createDms(&ok);
1166 bool ok2;
1167 const dms dec = m_PlateSolveUI.FitsSolverEstDec->createDms(&ok2);
1168 if (ok && ok2)
1169 m_Solver->usePosition(true, ra.Degrees(), dec.Degrees());
1170 else
1171 m_Solver->usePosition(false, 0, 0);
1172 }
1173 else m_Solver->usePosition(false, 0, 0);
1174}
1175
1176// If it is currently solving an image, then cancel the solve.
1177// Otherwise start solving.
1178void FITSTab::extractImage()
1179{
1180 if (m_Solver.get() && m_Solver->isRunning())
1181 {
1182 m_PlateSolveUI.SolveButton->setText(i18n("Aborting..."));
1183 m_Solver->abort();
1184 return;
1185 }
1186 m_PlateSolveUI.SolveButton->setText(i18n("Cancel"));
1187
1188 setupSolver(true);
1189
1190 m_PlateSolveUI.FitsSolverAngle->setText("");
1191 m_PlateSolveUI.FitsSolverIndexfile->setText("");
1192 m_PlateSolveUI.Solution1->setText(i18n("Extracting..."));
1193 m_PlateSolveUI.Solution2->setText("");
1194
1195 m_Solver->runSolver(m_View->imageData());
1196}
1197
1198void FITSTab::solveImage()
1199{
1200 if (m_Solver.get() && m_Solver->isRunning())
1201 {
1202 m_PlateSolveUI.SolveButton->setText(i18n("Aborting..."));
1203 m_Solver->abort();
1204 return;
1205 }
1206 m_PlateSolveUI.SolveButton->setText(i18n("Cancel"));
1207
1208 setupSolver(false);
1209
1210 m_PlateSolveUI.Solution2->setText(i18n("Solving..."));
1211
1212 m_Solver->runSolver(m_View->imageData());
1213}
1214
1215void FITSTab::extractorDone(bool timedOut, bool success, const FITSImage::Solution &solution, double elapsedSeconds)
1216{
1217 Q_UNUSED(solution);
1218 disconnect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::extractorDone);
1219 m_PlateSolveUI.Solution2->setText("");
1220
1221 if (timedOut)
1222 {
1223 const QString result = i18n("Extractor timed out: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
1224 m_PlateSolveUI.Solution1->setText(result);
1225
1226 // Can't run the solver. Just reset.
1227 m_PlateSolveUI.SolveButton->setText("Solve");
1228 return;
1229 }
1230 else if (!success)
1231 {
1232 const QString result = i18n("Extractor failed: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
1233 m_PlateSolveUI.Solution1->setText(result);
1234
1235 // Can't run the solver. Just reset.
1236 m_PlateSolveUI.SolveButton->setText(i18n("Solve"));
1237 return;
1238 }
1239 else
1240 {
1241 const QString starStr = i18n("Extracted %1 stars (%2 unfiltered) in %3s",
1242 m_Solver->getNumStarsFound(),
1243 m_Solver->getBackground().num_stars_detected,
1244 QString("%1").arg(elapsedSeconds, 0, 'f', 1));
1245 m_PlateSolveUI.Solution1->setText(starStr);
1246
1247 // Set the stars in the FITSData object so the user can view them.
1248 const QList<FITSImage::Star> &starList = m_Solver->getStarList();
1249 QList<Edge*> starCenters;
1250 starCenters.reserve(starList.size());
1251 for (int i = 0; i < starList.size(); i++)
1252 {
1253 const auto &star = starList[i];
1254 Edge *oneEdge = new Edge();
1255 oneEdge->x = star.x;
1256 oneEdge->y = star.y;
1257 oneEdge->val = star.peak;
1258 oneEdge->sum = star.flux;
1259 oneEdge->HFR = star.HFR;
1260 oneEdge->width = star.a;
1261 oneEdge->numPixels = star.numPixels;
1262 if (star.a > 0)
1263 // See page 63 to find the ellipticity equation for SEP.
1264 // http://astroa.physics.metu.edu.tr/MANUALS/sextractor/Guide2source_extractor.pdf
1265 oneEdge->ellipticity = 1 - star.b / star.a;
1266 else
1267 oneEdge->ellipticity = 0;
1268
1269 starCenters.append(oneEdge);
1270 }
1271 m_View->imageData()->setStarCenters(starCenters);
1272 m_View->updateFrame();
1273
1274 // Now run the solver.
1275 solveImage();
1276 }
1277}
1278
1279void FITSTab::solverDone(bool timedOut, bool success, const FITSImage::Solution &solution, double elapsedSeconds)
1280{
1281 disconnect(m_Solver.get(), &SolverUtils::done, this, &FITSTab::solverDone);
1282 m_PlateSolveUI.SolveButton->setText("Solve");
1283
1284 if (m_Solver->isRunning())
1285 qCDebug(KSTARS_FITS) << "solverDone called, but it is still running.";
1286
1287 if (timedOut)
1288 {
1289 const QString result = i18n("Solver timed out: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
1290 m_PlateSolveUI.Solution2->setText(result);
1291 }
1292 else if (!success)
1293 {
1294 const QString result = i18n("Solver failed: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
1295 m_PlateSolveUI.Solution2->setText(result);
1296 }
1297 else
1298 {
1299 const bool eastToTheRight = solution.parity == FITSImage::POSITIVE ? false : true;
1300 m_View->imageData()->injectWCS(solution.orientation, solution.ra, solution.dec, solution.pixscale, eastToTheRight);
1301 m_View->imageData()->loadWCS();
1302 m_View->syncWCSState();
1303 if (m_View->areObjectsShown())
1304 // Requery Objects based on new plate solved solution
1305 m_View->imageData()->searchObjects();
1306
1307 const QString result = QString("Solved in %1s").arg(elapsedSeconds, 0, 'f', 1);
1308 const double solverPA = KSUtils::rotationToPositionAngle(solution.orientation);
1309 m_PlateSolveUI.FitsSolverAngle->setText(QString("%1º").arg(solverPA, 0, 'f', 2));
1310
1311 int indexUsed = -1, healpixUsed = -1;
1312 m_Solver->getSolutionHealpix(&indexUsed, &healpixUsed);
1313 if (indexUsed < 0)
1314 m_PlateSolveUI.FitsSolverIndexfile->setText("");
1315 else
1316 m_PlateSolveUI.FitsSolverIndexfile->setText(
1317 QString("%1%2")
1318 .arg(indexUsed)
1319 .arg(healpixUsed >= 0 ? QString("-%1").arg(healpixUsed) : QString("")));;
1320
1321 // Set the scale widget to the current result
1322 const int imageWidth = m_View->imageData()->width();
1323 const int units = m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits->currentIndex();
1324 if (units == SSolver::DEG_WIDTH)
1325 m_PlateSolveUI.kcfg_FitsSolverScale->setValue(solution.pixscale * imageWidth / 3600.0);
1326 else if (units == SSolver::ARCMIN_WIDTH)
1327 m_PlateSolveUI.kcfg_FitsSolverScale->setValue(solution.pixscale * imageWidth / 60.0);
1328 else
1329 m_PlateSolveUI.kcfg_FitsSolverScale->setValue(solution.pixscale);
1330
1331 // Set the ra and dec widgets to the current result
1332 m_PlateSolveUI.FitsSolverEstRA->show(dms(solution.ra));
1333 m_PlateSolveUI.FitsSolverEstDec->show(dms(solution.dec));
1334
1335 m_PlateSolveUI.Solution2->setText(result);
1336 }
1337}
1338
1339// Each module can default to its own profile index. These two methods retrieves and saves
1340// the values in a JSON string using an Options variable.
1341int FITSTab::getProfileIndex(int moduleIndex)
1342{
1343 if (moduleIndex < 0 || moduleIndex >= Ekos::ProfileGroupNames.size())
1344 return 0;
1345 const QString moduleName = Ekos::ProfileGroupNames[moduleIndex].toString();
1346 const QString str = Options::fitsSolverProfileIndeces();
1347 const QJsonDocument doc = QJsonDocument::fromJson(str.toUtf8());
1348 if (doc.isNull() || !doc.isObject())
1349 return 0;
1350 const QJsonObject indeces = doc.object();
1351 return indeces[moduleName].toString().toInt();
1352}
1353
1354void FITSTab::setProfileIndex(int moduleIndex, int profileIndex)
1355{
1356 if (moduleIndex < 0 || moduleIndex >= Ekos::ProfileGroupNames.size())
1357 return;
1358 QString str = Options::fitsSolverProfileIndeces();
1360 if (doc.isNull() || !doc.isObject())
1361 {
1362 QJsonObject initialIndeces;
1363 for (int i = 0; i < Ekos::ProfileGroupNames.size(); i++)
1364 {
1365 QString name = Ekos::ProfileGroupNames[i].toString();
1366 if (name == "Align")
1367 initialIndeces[name] = QString::number(Options::solveOptionsProfile());
1368 else if (name == "Guide")
1369 initialIndeces[name] = QString::number(Options::guideOptionsProfile());
1370 else if (name == "HFR")
1371 initialIndeces[name] = QString::number(Options::hFROptionsProfile());
1372 else // Focus has a weird setting, just default to 0
1373 initialIndeces[name] = "0";
1374 }
1375 doc = QJsonDocument(initialIndeces);
1376 }
1377
1378 QJsonObject indeces = doc.object();
1379 indeces[Ekos::ProfileGroupNames[moduleIndex].toString()] = QString::number(profileIndex);
1380 doc = QJsonDocument(indeces);
1381 Options::setFitsSolverProfileIndeces(QString(doc.toJson()));
1382}
1383
1384void FITSTab::setupProfiles(int moduleIndex)
1385{
1386 if (moduleIndex < 0 || moduleIndex >= Ekos::ProfileGroupNames.size())
1387 return;
1388 Ekos::ProfileGroup profileGroup = static_cast<Ekos::ProfileGroup>(moduleIndex);
1389 Options::setFitsSolverModule(moduleIndex);
1390
1391 // Set up the profiles' menu.
1392 const QList<SSolver::Parameters> optionsList = getSSolverParametersList(profileGroup);
1393 m_PlateSolveUI.kcfg_FitsSolverProfile->clear();
1394 for(auto &param : optionsList)
1395 m_PlateSolveUI.kcfg_FitsSolverProfile->addItem(param.listName);
1396
1397 m_ProfileEditor->setProfileGroup(profileGroup, false);
1398
1399 // Restore the stored options.
1400 m_PlateSolveUI.kcfg_FitsSolverProfile->setCurrentIndex(getProfileIndex(Options::fitsSolverModule()));
1401
1402 m_ProfileEditorPage->setHeader(QString("FITS Viewer Solver %1 Profiles Editor")
1403 .arg(Ekos::ProfileGroupNames[moduleIndex].toString()));
1404}
1405
1406void FITSTab::initSolverUI()
1407{
1408 // Init the modules combo box.
1409 m_PlateSolveUI.kcfg_FitsSolverModule->clear();
1410 for (int i = 0; i < Ekos::ProfileGroupNames.size(); i++)
1411 m_PlateSolveUI.kcfg_FitsSolverModule->addItem(Ekos::ProfileGroupNames[i].toString());
1412 m_PlateSolveUI.kcfg_FitsSolverModule->setCurrentIndex(Options::fitsSolverModule());
1413
1414 setupProfiles(Options::fitsSolverModule());
1415
1416 // Change the profiles combo box whenever the modules combo changes
1417 connect(m_PlateSolveUI.kcfg_FitsSolverModule, QOverload<int>::of(&QComboBox::activated), this, &FITSTab::setupProfiles);
1418
1419 m_PlateSolveUI.kcfg_FitsSolverUseScale->setChecked(Options::fitsSolverUseScale());
1420 m_PlateSolveUI.kcfg_FitsSolverScale->setValue(Options::fitsSolverScale());
1421 m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits->setCurrentIndex(Options::fitsSolverImageScaleUnits());
1422
1423 m_PlateSolveUI.kcfg_FitsSolverUsePosition->setChecked(Options::fitsSolverUsePosition());
1424 m_PlateSolveUI.kcfg_FitsSolverRadius->setValue(Options::fitsSolverRadius());
1425
1426 m_PlateSolveUI.FitsSolverEstRA->setUnits(dmsBox::HOURS);
1427 m_PlateSolveUI.FitsSolverEstDec->setUnits(dmsBox::DEGREES);
1428
1429 // Save the values of user controls when the user changes them.
1430 connect(m_PlateSolveUI.kcfg_FitsSolverProfile, QOverload<int>::of(&QComboBox::activated), [this](int index)
1431 {
1432 setProfileIndex(m_PlateSolveUI.kcfg_FitsSolverModule->currentIndex(), index);
1433 });
1434
1435 connect(m_PlateSolveUI.kcfg_FitsSolverUseScale, &QCheckBox::stateChanged, this, [](int state)
1436 {
1437 Options::setFitsSolverUseScale(state);
1438 });
1439 connect(m_PlateSolveUI.kcfg_FitsSolverScale, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [](double value)
1440 {
1441 Options::setFitsSolverScale(value);
1442 });
1443 connect(m_PlateSolveUI.kcfg_FitsSolverImageScaleUnits, QOverload<int>::of(&QComboBox::activated), [](int index)
1444 {
1445 Options::setFitsSolverImageScaleUnits(index);
1446 });
1447
1448 connect(m_PlateSolveUI.kcfg_FitsSolverUsePosition, &QCheckBox::stateChanged, this, [](int state)
1449 {
1450 Options::setFitsSolverUsePosition(state);
1451 });
1452
1453 connect(m_PlateSolveUI.kcfg_FitsSolverRadius, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [](double value)
1454 {
1455 Options::setFitsSolverRadius(value);
1456 });
1457 connect(m_PlateSolveUI.UpdatePosition, &QPushButton::clicked, this, [&]()
1458 {
1459 const auto center = SkyMap::Instance()->getCenterPoint();
1460 m_PlateSolveUI.FitsSolverEstRA->show(center.ra());
1461 m_PlateSolveUI.FitsSolverEstDec->show(center.dec());
1462 });
1463
1464 // Warn if the user is not using the internal StellarSolver solver.
1465 const SSolver::SolverType type = static_cast<SSolver::SolverType>(Options::solverType());
1466 if(type != SSolver::SOLVER_STELLARSOLVER)
1467 {
1468 m_PlateSolveUI.Solution2->setText(i18n("Warning! This tool only supports the internal StellarSolver solver."));
1469 m_PlateSolveUI.Solution1->setText(i18n("Change to that in the Ekos Align options menu."));
1470 }
1471}
Primary window to view monochrome and color FITS images.
Definition fitsviewer.h:54
static KStars * Instance()
Definition kstars.h:121
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)
std::optional< QSqlQuery > query(const QString &queryStatement)
char * toString(const EngineQuery &query)
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)
QString name(StandardAction id)
KGuiItem save()
KGuiItem discard()
void clicked(bool checked)
void toggled(bool checked)
void doubleClicked(const QModelIndex &index)
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
void stateChanged(int state)
void setImage(const QImage &image, Mode mode)
void activated(int index)
bool openUrl(const QUrl &url)
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)
void currentRowChanged(const QModelIndex &current, const QModelIndex &previous)
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 push_back(parameter_type value)
void reserve(qsizetype size)
qsizetype size() const const
void currentRowChanged(int currentRow)
bool isValid() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
bool disconnect(const QMetaObject::Connection &connection)
int depth() const const
QPixmap scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
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)
Qt::CheckState checkState() const const
int column() const const
int row() const const
void setCheckState(Qt::CheckState state)
virtual void setData(const QVariant &value, int role)
void setTextAlignment(Qt::Alignment alignment)
QString text() const const
QStandardItem * item(int row, int column) const const
QStandardItem * itemFromIndex(const QModelIndex &index) const const
virtual bool removeRows(int row, int count, const QModelIndex &parent) override
virtual int rowCount(const QModelIndex &parent) const const override
void setColumnCount(int columns)
virtual bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) override
void setHorizontalHeaderLabels(const QStringList &labels)
void setItem(int row, QStandardItem *item)
void setRowCount(int rows)
void setSortRole(int role)
QString arg(Args &&... args) const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
int toInt(bool *ok, int base) const const
QByteArray toLatin1() const const
QByteArray toUtf8() const const
AlignHCenter
KeepAspectRatio
Unchecked
UniqueConnection
UserRole
ItemIsSelectable
MatchExactly
Horizontal
SmoothTransformation
WA_LayoutUsesWidgetRect
QTextStream & dec(QTextStream &stream)
void setFlags(Qt::ItemFlags flags)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void itemChanged(QTreeWidgetItem *item, int column)
Qt::CheckState checkState(int column) const const
QTreeWidgetItem * child(int index) const const
int childCount() const const
void setCheckState(int column, Qt::CheckState state)
void setText(int column, const QString &text)
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)
void setQuery(const QString &query, 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 Sat Dec 21 2024 17:04:46 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.