Kstars

platesolve.cpp
1/*
2 SPDX-FileCopyrightText: 2024 Hy Murveit <hy@murveit.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "platesolve.h"
8#include "ui_platesolve.h"
9
10#include "auxiliary/kspaths.h"
11#include "Options.h"
12#include <KConfigDialog>
13#include "fitsdata.h"
14#include "skymap.h"
15#include <fits_debug.h>
16
17QPointer<Ekos::StellarSolverProfileEditor> PlateSolve::m_ProfileEditor;
18QPointer<KConfigDialog> PlateSolve::m_EditorDialog;
19QPointer<KPageWidgetItem> PlateSolve::m_ProfileEditorPage;
20
21namespace
22{
23const QList<SSolver::Parameters> getSSolverParametersList(Ekos::ProfileGroup module)
24{
25 QString savedProfiles;
26 switch(module)
27 {
28 case Ekos::AlignProfiles:
29 default:
30 savedProfiles = QDir(KSPaths::writableLocation(
31 QStandardPaths::AppLocalDataLocation)).filePath("SavedAlignProfiles.ini");
32 return QFile(savedProfiles).exists() ?
33 StellarSolver::loadSavedOptionsProfiles(savedProfiles) :
34 Ekos::getDefaultAlignOptionsProfiles();
35 break;
36 case Ekos::FocusProfiles:
37 savedProfiles = QDir(KSPaths::writableLocation(
38 QStandardPaths::AppLocalDataLocation)).filePath("SavedFocusProfiles.ini");
39 return QFile(savedProfiles).exists() ?
40 StellarSolver::loadSavedOptionsProfiles(savedProfiles) :
41 Ekos::getDefaultFocusOptionsProfiles();
42 break;
43 case Ekos::GuideProfiles:
44 savedProfiles = QDir(KSPaths::writableLocation(
45 QStandardPaths::AppLocalDataLocation)).filePath("SavedGuideProfiles.ini");
46 return QFile(savedProfiles).exists() ?
47 StellarSolver::loadSavedOptionsProfiles(savedProfiles) :
48 Ekos::getDefaultGuideOptionsProfiles();
49 break;
50 case Ekos::HFRProfiles:
51 savedProfiles = QDir(KSPaths::writableLocation(
52 QStandardPaths::AppLocalDataLocation)).filePath("SavedHFRProfiles.ini");
53 return QFile(savedProfiles).exists() ?
54 StellarSolver::loadSavedOptionsProfiles(savedProfiles) :
55 Ekos::getDefaultHFROptionsProfiles();
56 break;
57 }
58}
59} // namespace
60
61
62PlateSolve::PlateSolve(QWidget * parent) : QDialog(parent)
63{
64 setup();
65}
66
67void PlateSolve::setup()
68{
69 setupUi(this);
70 editProfile->setIcon(QIcon::fromTheme("document-edit"));
71 editProfile->setAttribute(Qt::WA_LayoutUsesWidgetRect);
72
73 const QString EditorID = "FITSSolverProfileEditor";
74 if (!m_EditorDialog)
75 {
76 // These are static, shared by all FITS Viewer tabs.
77 m_EditorDialog = new KConfigDialog(nullptr, EditorID, Options::self());
78 m_ProfileEditor = new Ekos::StellarSolverProfileEditor(nullptr, Ekos::AlignProfiles, m_EditorDialog.data());
79 m_ProfileEditorPage = m_EditorDialog->addPage(m_ProfileEditor.data(),
80 i18n("FITS Viewer Solver Profiles Editor"));
81 }
82
83 connect(editProfile, &QAbstractButton::clicked, this, [this, EditorID]
84 {
85 m_ProfileEditor->loadProfile(kcfg_FitsSolverProfile->currentText());
86 KConfigDialog * d = KConfigDialog::exists(EditorID);
87 if(d)
88 {
89 d->setCurrentPage(m_ProfileEditorPage);
90 d->show();
91 }
92 });
93 connect(SolveButton, &QPushButton::clicked, this, [this]()
94 {
95 if (m_Solver.get() && m_Solver->isRunning())
96 {
97 SolveButton->setText(i18n("Aborting..."));
98 m_Solver->abort();
99 emit solverFailed();
100 return;
101 }
102
103 emit clicked();
104 });
105 initSolverUI();
106}
107
108void PlateSolve::enableAuxButton(const QString &label, const QString &toolTip)
109{
110 auxButton->setText(label);
111 auxButton->setToolTip(toolTip);
112 auxButton->setVisible(true);
113 disconnect(auxButton);
114 connect(auxButton, &QPushButton::clicked, this, [this]()
115 {
116 emit auxClicked();
117 });
118}
119
120void PlateSolve::disableAuxButton()
121{
122 auxButton->setVisible(false);
123 disconnect(auxButton);
124}
125
126void PlateSolve::abort()
127{
128 disconnect(&m_Watcher);
129 disconnect(m_Solver.get());
130 m_Solver->abort();
131}
132
133void PlateSolve::setupSolver(const QSharedPointer<FITSData> &imageData, bool extractOnly)
134{
135 auto parameters = getSSolverParametersList(static_cast<Ekos::ProfileGroup>(Options::fitsSolverModule())).at(
136 kcfg_FitsSolverProfile->currentIndex());
137 parameters.search_radius = kcfg_FitsSolverRadius->value();
138 if (extractOnly)
139 {
140 if (!kcfg_FitsSolverLinear->isChecked())
141 {
142 // If image is non-linear seed the threshold offset with the background using median pixel value. Note
143 // that there is a bug in the median calculation due to an issue compiling on Mac that means that not
144 // all datatypes are supported by the median calculation. If median is zero use the mean instead.
145 double offset = imageData->getAverageMedian();
146 if (offset <= 0.0)
147 offset = imageData->getAverageMean();
148 parameters.threshold_offset = offset;
149 }
150
151 m_Solver.reset(new SolverUtils(parameters, parameters.solverTimeLimit, SSolver::EXTRACT), &QObject::deleteLater);
152 connect(m_Solver.get(), &SolverUtils::done, this, &PlateSolve::extractorDone, Qt::UniqueConnection);
153 }
154 else
155 {
156 // If image is non-linear then set the offset to the average background in the image
157 // which was found in the first solver (extract only) run.
158 if (m_Solver && !kcfg_FitsSolverLinear->isChecked())
159 parameters.threshold_offset = m_Solver->getBackground().global;
160
161 m_Solver.reset(new SolverUtils(parameters, parameters.solverTimeLimit, SSolver::SOLVE), &QObject::deleteLater);
162 connect(m_Solver.get(), &SolverUtils::done, this, &PlateSolve::solverDone, Qt::UniqueConnection);
163 }
164
165 const int imageWidth = imageData->width();
166 const int imageHeight = imageData->height();
167 if (kcfg_FitsSolverUseScale->isChecked() && imageWidth != 0 && imageHeight != 0)
168 {
169 const double scale = kcfg_FitsSolverScale->value();
170 double lowScale = scale * 0.8;
171 double highScale = scale * 1.2;
172
173 // solver utils uses arcsecs per pixel only
174 const int units = kcfg_FitsSolverImageScaleUnits->currentIndex();
175 if (units == SSolver::DEG_WIDTH)
176 {
177 lowScale = (lowScale * 3600) / std::max(imageWidth, imageHeight);
178 highScale = (highScale * 3600) / std::min(imageWidth, imageHeight);
179 }
180 else if (units == SSolver::ARCMIN_WIDTH)
181 {
182 lowScale = (lowScale * 60) / std::max(imageWidth, imageHeight);
183 highScale = (highScale * 60) / std::min(imageWidth, imageHeight);
184 }
185
186 m_Solver->useScale(kcfg_FitsSolverUseScale->isChecked(), lowScale, highScale);
187 }
188 else m_Solver->useScale(false, 0, 0);
189
190 if (kcfg_FitsSolverUsePosition->isChecked())
191 {
192 bool ok;
193 const dms ra = FitsSolverEstRA->createDms(&ok);
194 bool ok2;
195 const dms dec = FitsSolverEstDec->createDms(&ok2);
196 if (ok && ok2)
197 m_Solver->usePosition(true, ra.Degrees(), dec.Degrees());
198 else
199 m_Solver->usePosition(false, 0, 0);
200 }
201 else m_Solver->usePosition(false, 0, 0);
202}
203
204// If it is currently solving an image, then cancel the solve.
205// Otherwise start solving.
206void PlateSolve::extractImage(const QSharedPointer<FITSData> &imageData)
207{
208 m_imageData = imageData;
209 if (m_Solver.get() && m_Solver->isRunning())
210 {
211 SolveButton->setText(i18n("Aborting..."));
212 m_Solver->abort();
213 return;
214 }
215 SolveButton->setText(i18n("Cancel"));
216
217 setupSolver(imageData, true);
218
219 FitsSolverAngle->setText("");
220 FitsSolverIndexfile->setText("");
221 Solution1->setText(i18n("Extracting..."));
222 Solution2->setText("");
223
224 m_Solver->runSolver(imageData);
225}
226
227void PlateSolve::solveImage(const QString &filename)
228{
229 connect(&m_Watcher, &QFutureWatcher<bool>::finished, this, &PlateSolve::loadFileDone, Qt::UniqueConnection);
230
231 m_imageData.reset(new FITSData(), &QObject::deleteLater);
232 QFuture<bool> response = m_imageData->loadFromFile(filename);
233 m_Watcher.setFuture(response);
234}
235
236void PlateSolve::loadFileDone()
237{
238 solveImage(m_imageData);
239 disconnect(&m_Watcher);
240}
241
242void PlateSolve::solveImage(const QSharedPointer<FITSData> &imageData)
243{
244 m_imageData = imageData;
245 if (m_Solver.get() && m_Solver->isRunning())
246 {
247 SolveButton->setText(i18n("Aborting..."));
248 m_Solver->abort();
249 return;
250 }
251 SolveButton->setText(i18n("Cancel"));
252
253 setupSolver(imageData, false);
254
255 Solution2->setText(i18n("Solving..."));
256
257 m_Solver->runSolver(imageData);
258}
259
260void PlateSolve::extractorDone(bool timedOut, bool success, const FITSImage::Solution &solution, double elapsedSeconds)
261{
262 Q_UNUSED(solution);
263 disconnect(m_Solver.get(), &SolverUtils::done, this, &PlateSolve::extractorDone);
264 Solution2->setText("");
265
266 if (timedOut)
267 {
268 const QString result = i18n("Extractor timed out: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
269 Solution1->setText(result);
270
271 // Can't run the solver. Just reset.
272 SolveButton->setText("Solve");
273 emit extractorFailed();
274 return;
275 }
276 else if (!success)
277 {
278 const QString result = i18n("Extractor failed: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
279 Solution1->setText(result);
280
281 // Can't run the solver. Just reset.
282 SolveButton->setText(i18n("Solve"));
283 emit extractorFailed();
284 return;
285 }
286 else
287 {
288 const QString starStr = i18n("Extracted %1 stars (%2 unfiltered) in %3s",
289 m_Solver->getNumStarsFound(),
290 m_Solver->getBackground().num_stars_detected,
291 QString("%1").arg(elapsedSeconds, 0, 'f', 1));
292 Solution1->setText(starStr);
293
294 // Set the stars in the FITSData object so the user can view them.
295 const QList<FITSImage::Star> &starList = m_Solver->getStarList();
296 QList<Edge*> starCenters;
297 starCenters.reserve(starList.size());
298 for (int i = 0; i < starList.size(); i++)
299 {
300 const auto &star = starList[i];
301 Edge *oneEdge = new Edge();
302 oneEdge->x = star.x;
303 oneEdge->y = star.y;
304 oneEdge->val = star.peak;
305 oneEdge->sum = star.flux;
306 oneEdge->HFR = star.HFR;
307 oneEdge->width = star.a;
308 oneEdge->numPixels = star.numPixels;
309 if (star.a > 0)
310 // See page 63 to find the ellipticity equation for SEP.
311 // http://astroa.physics.metu.edu.tr/MANUALS/sextractor/Guide2source_extractor.pdf
312 oneEdge->ellipticity = 1 - star.b / star.a;
313 else
314 oneEdge->ellipticity = 0;
315
316 starCenters.append(oneEdge);
317 }
318 m_imageData->setStarCenters(starCenters);
319 emit extractorSuccess();
320 }
321}
322
323void PlateSolve::solverDone(bool timedOut, bool success, const FITSImage::Solution &solution, double elapsedSeconds)
324{
325 m_Solution = FITSImage::Solution();
326 disconnect(m_Solver.get(), &SolverUtils::done, this, &PlateSolve::solverDone);
327 SolveButton->setText("Solve");
328
329 if (m_Solver->isRunning())
330 qCDebug(KSTARS_FITS) << "solverDone called, but it is still running.";
331
332 if (timedOut)
333 {
334 const QString result = i18n("Solver timed out: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
335 Solution2->setText(result);
336 emit solverFailed();
337 }
338 else if (!success)
339 {
340 const QString result = i18n("Solver failed: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
341 Solution2->setText(result);
342 emit solverFailed();
343 }
344 else
345 {
346 m_Solution = solution;
347 const bool eastToTheRight = solution.parity == FITSImage::POSITIVE ? false : true;
348 m_imageData->injectWCS(solution.orientation, solution.ra, solution.dec, solution.pixscale, eastToTheRight);
349 m_imageData->loadWCS();
350
351 const QString result = QString("Solved in %1s").arg(elapsedSeconds, 0, 'f', 1);
352 const double solverPA = KSUtils::rotationToPositionAngle(solution.orientation);
353 FitsSolverAngle->setText(QString("%1ยบ").arg(solverPA, 0, 'f', 2));
354
355 int indexUsed = -1, healpixUsed = -1;
356 m_Solver->getSolutionHealpix(&indexUsed, &healpixUsed);
357 if (indexUsed < 0)
358 FitsSolverIndexfile->setText("");
359 else
360 FitsSolverIndexfile->setText(
361 QString("%1%2")
362 .arg(indexUsed)
363 .arg(healpixUsed >= 0 ? QString("-%1").arg(healpixUsed) : QString("")));;
364
365 // Set the scale widget to the current result
366 const int imageWidth = m_imageData->width();
367 const int units = kcfg_FitsSolverImageScaleUnits->currentIndex();
368 if (units == SSolver::DEG_WIDTH)
369 kcfg_FitsSolverScale->setValue(solution.pixscale * imageWidth / 3600.0);
370 else if (units == SSolver::ARCMIN_WIDTH)
371 kcfg_FitsSolverScale->setValue(solution.pixscale * imageWidth / 60.0);
372 else
373 kcfg_FitsSolverScale->setValue(solution.pixscale);
374
375 // Set the ra and dec widgets to the current result
376 FitsSolverEstRA->show(dms(solution.ra));
377 FitsSolverEstDec->show(dms(solution.dec));
378
379 Solution2->setText(result);
380 emit solverSuccess();
381 }
382}
383
384// Each module can default to its own profile index. These two methods retrieves and saves
385// the values in a JSON string using an Options variable.
386int PlateSolve::getProfileIndex(int moduleIndex)
387{
388 if (moduleIndex < 0 || moduleIndex >= Ekos::ProfileGroupNames.size())
389 return 0;
390 const QString moduleName = Ekos::ProfileGroupNames[moduleIndex].toString();
391 const QString str = Options::fitsSolverProfileIndeces();
392 const QJsonDocument doc = QJsonDocument::fromJson(str.toUtf8());
393 if (doc.isNull() || !doc.isObject())
394 return 0;
395 const QJsonObject indeces = doc.object();
396 return indeces[moduleName].toString().toInt();
397}
398
399void PlateSolve::setProfileIndex(int moduleIndex, int profileIndex)
400{
401 if (moduleIndex < 0 || moduleIndex >= Ekos::ProfileGroupNames.size())
402 return;
403 QString str = Options::fitsSolverProfileIndeces();
404 QJsonDocument doc = QJsonDocument::fromJson(str.toUtf8());
405 if (doc.isNull() || !doc.isObject())
406 {
407 QJsonObject initialIndeces;
408 for (int i = 0; i < Ekos::ProfileGroupNames.size(); i++)
409 {
410 QString name = Ekos::ProfileGroupNames[i].toString();
411 if (name == "Align")
412 initialIndeces[name] = QString::number(Options::solveOptionsProfile());
413 else if (name == "Guide")
414 initialIndeces[name] = QString::number(Options::guideOptionsProfile());
415 else if (name == "HFR")
416 initialIndeces[name] = QString::number(Options::hFROptionsProfile());
417 else // Focus has a weird setting, just default to 0
418 initialIndeces[name] = "0";
419 }
420 doc = QJsonDocument(initialIndeces);
421 }
422
423 QJsonObject indeces = doc.object();
424 indeces[Ekos::ProfileGroupNames[moduleIndex].toString()] = QString::number(profileIndex);
425 doc = QJsonDocument(indeces);
426 Options::setFitsSolverProfileIndeces(QString(doc.toJson()));
427}
428
429void PlateSolve::setupProfiles(int moduleIndex)
430{
431 if (moduleIndex < 0 || moduleIndex >= Ekos::ProfileGroupNames.size())
432 return;
433 Ekos::ProfileGroup profileGroup = static_cast<Ekos::ProfileGroup>(moduleIndex);
434 Options::setFitsSolverModule(moduleIndex);
435
436 // Set up the profiles' menu.
437 const QList<SSolver::Parameters> optionsList = getSSolverParametersList(profileGroup);
438 kcfg_FitsSolverProfile->clear();
439 for(auto &param : optionsList)
440 kcfg_FitsSolverProfile->addItem(param.listName);
441
442 m_ProfileEditor->setProfileGroup(profileGroup, false);
443
444 // Restore the stored options.
445 kcfg_FitsSolverProfile->setCurrentIndex(getProfileIndex(Options::fitsSolverModule()));
446
447 m_ProfileEditorPage->setHeader(QString("FITS Viewer Solver %1 Profiles Editor")
448 .arg(Ekos::ProfileGroupNames[moduleIndex].toString()));
449}
450
451void PlateSolve::setPosition(const SkyPoint &p)
452{
453 FitsSolverEstRA->show(p.ra());
454 FitsSolverEstDec->show(p.dec());
455}
456void PlateSolve::setUsePosition(bool yesNo)
457{
458 kcfg_FitsSolverUsePosition->setChecked(yesNo);
459}
460void PlateSolve::setScale(double scale)
461{
462 kcfg_FitsSolverScale->setValue(scale);
463}
464void PlateSolve::setScaleUnits(int units)
465{
466 kcfg_FitsSolverImageScaleUnits->setCurrentIndex(units);
467}
468void PlateSolve::setUseScale(bool yesNo)
469{
470 kcfg_FitsSolverUseScale->setChecked(yesNo);
471}
472void PlateSolve::setLinear(bool yesNo)
473{
474 kcfg_FitsSolverLinear->setChecked(yesNo);
475}
476
477void PlateSolve::initSolverUI()
478{
479 // Init the modules combo box.
480 kcfg_FitsSolverModule->clear();
481 for (int i = 0; i < Ekos::ProfileGroupNames.size(); i++)
482 kcfg_FitsSolverModule->addItem(Ekos::ProfileGroupNames[i].toString());
483 kcfg_FitsSolverModule->setCurrentIndex(Options::fitsSolverModule());
484
485 setupProfiles(Options::fitsSolverModule());
486
487 // Change the profiles combo box whenever the modules combo changes
488 connect(kcfg_FitsSolverModule, QOverload<int>::of(&QComboBox::activated), this, &PlateSolve::setupProfiles,
490
491 kcfg_FitsSolverUseScale->setChecked(Options::fitsSolverUseScale());
492 kcfg_FitsSolverScale->setValue(Options::fitsSolverScale());
493 kcfg_FitsSolverImageScaleUnits->setCurrentIndex(Options::fitsSolverImageScaleUnits());
494
495 kcfg_FitsSolverUsePosition->setChecked(Options::fitsSolverUsePosition());
496 kcfg_FitsSolverRadius->setValue(Options::fitsSolverRadius());
497
498 FitsSolverEstRA->setUnits(dmsBox::HOURS);
499 FitsSolverEstDec->setUnits(dmsBox::DEGREES);
500
501 // Save the values of user controls when the user changes them.
502 connect(kcfg_FitsSolverProfile, QOverload<int>::of(&QComboBox::activated), [this](int index)
503 {
504 setProfileIndex(kcfg_FitsSolverModule->currentIndex(), index);
505 });
506
507 connect(kcfg_FitsSolverUseScale, &QCheckBox::stateChanged, this, [](int state)
508 {
509 Options::setFitsSolverUseScale(state);
510 });
511 connect(kcfg_FitsSolverScale, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [](double value)
512 {
513 Options::setFitsSolverScale(value);
514 });
515 connect(kcfg_FitsSolverImageScaleUnits, QOverload<int>::of(&QComboBox::activated), [](int index)
516 {
517 Options::setFitsSolverImageScaleUnits(index);
518 });
519
520 connect(kcfg_FitsSolverUsePosition, &QCheckBox::stateChanged, this, [](int state)
521 {
522 Options::setFitsSolverUsePosition(state);
523 });
524
525 connect(kcfg_FitsSolverRadius, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [](double value)
526 {
527 Options::setFitsSolverRadius(value);
528 });
529 connect(UpdatePosition, &QPushButton::clicked, this, [&]()
530 {
531 const auto center = SkyMap::Instance()->getCenterPoint();
532 FitsSolverEstRA->show(center.ra());
533 FitsSolverEstDec->show(center.dec());
534 });
535
536 // Warn if the user is not using the internal StellarSolver solver.
537 const SSolver::SolverType type = static_cast<SSolver::SolverType>(Options::solverType());
538 if(type != SSolver::SOLVER_STELLARSOLVER)
539 {
540 Solution2->setText(i18n("Warning! This tool only supports the internal StellarSolver solver."));
541 Solution1->setText(i18n("Change to that in the Ekos Align options menu."));
542 }
543}
544
545void PlateSolve::setImageDisplay(const QImage &image)
546{
547 QImage scaled = image.scaledToHeight(300);
548 plateSolveImage->setVisible(true);
549 plateSolveImage->setPixmap(QPixmap::fromImage(scaled));
550}
static KConfigDialog * exists(const QString &name)
The sky coordinates of a point in the sky.
Definition skypoint.h:45
const CachingDms & dec() const
Definition skypoint.h:269
const CachingDms & ra() const
Definition skypoint.h:263
const double & Degrees() const
Definition dms.h:141
QString i18n(const char *text, const TYPE &arg...)
Type type(const QSqlDatabase &db)
char * toString(const EngineQuery &query)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:83
QString name(StandardAction id)
void clicked(bool checked)
void stateChanged(int state)
void activated(int index)
int result() const const
QString filePath(const QString &fileName) const const
void valueChanged(double d)
bool exists(const QString &fileName)
QIcon fromTheme(const QString &name)
QImage scaledToHeight(int height, Qt::TransformationMode mode) const const
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
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
bool disconnect(const QMetaObject::Connection &connection)
QPixmap fromImage(QImage &&image, Qt::ImageConversionFlags flags)
QString number(double n, char format, int precision)
QByteArray toUtf8() const const
UniqueConnection
WA_LayoutUsesWidgetRect
QTextStream & center(QTextStream &stream)
QTextStream & dec(QTextStream &stream)
void setupUi(QWidget *widget)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 18 2025 12:01:33 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.