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 if (m_Solver.get())
130 {
131 disconnect(m_Solver.get());
132 m_Solver->abort();
133 }
134}
135
136void PlateSolve::setupSolver(const QSharedPointer<FITSData> &imageData, bool extractOnly)
137{
138 auto parameters = getSSolverParametersList(static_cast<Ekos::ProfileGroup>(Options::fitsSolverModule())).at(
139 kcfg_FitsSolverProfile->currentIndex());
140 parameters.search_radius = kcfg_FitsSolverRadius->value();
141 if (extractOnly)
142 {
143 if (!kcfg_FitsSolverLinear->isChecked())
144 {
145 // If image is non-linear seed the threshold offset with the background using median pixel value. Note
146 // that there is a bug in the median calculation due to an issue compiling on Mac that means that not
147 // all datatypes are supported by the median calculation. If median is zero use the mean instead.
148 double offset = imageData->getAverageMedian();
149 if (offset <= 0.0)
150 offset = imageData->getAverageMean();
151 parameters.threshold_offset = offset;
152 }
153
154 m_Solver.reset(new SolverUtils(parameters, parameters.solverTimeLimit, SSolver::EXTRACT), &QObject::deleteLater);
155 connect(m_Solver.get(), &SolverUtils::done, this, &PlateSolve::extractorDone, Qt::UniqueConnection);
156 }
157 else
158 {
159 // If image is non-linear then set the offset to the average background in the image
160 // which was found in the first solver (extract only) run.
161 if (m_Solver && !kcfg_FitsSolverLinear->isChecked())
162 parameters.threshold_offset = m_Solver->getBackground().global;
163
164 m_Solver.reset(new SolverUtils(parameters, parameters.solverTimeLimit, SSolver::SOLVE), &QObject::deleteLater);
165 connect(m_Solver.get(), &SolverUtils::done, this, &PlateSolve::solverDone, Qt::UniqueConnection);
166 }
167
168 const int imageWidth = imageData->width();
169 const int imageHeight = imageData->height();
170 if (kcfg_FitsSolverUseScale->isChecked() && imageWidth != 0 && imageHeight != 0)
171 {
172 const double scale = kcfg_FitsSolverScale->value();
173 double lowScale = scale * 0.8;
174 double highScale = scale * 1.2;
175
176 // solver utils uses arcsecs per pixel only
177 const int units = kcfg_FitsSolverImageScaleUnits->currentIndex();
178 if (units == SSolver::DEG_WIDTH)
179 {
180 lowScale = (lowScale * 3600) / std::max(imageWidth, imageHeight);
181 highScale = (highScale * 3600) / std::min(imageWidth, imageHeight);
182 }
183 else if (units == SSolver::ARCMIN_WIDTH)
184 {
185 lowScale = (lowScale * 60) / std::max(imageWidth, imageHeight);
186 highScale = (highScale * 60) / std::min(imageWidth, imageHeight);
187 }
188
189 m_Solver->useScale(kcfg_FitsSolverUseScale->isChecked(), lowScale, highScale);
190 }
191 else m_Solver->useScale(false, 0, 0);
192
193 if (kcfg_FitsSolverUsePosition->isChecked())
194 {
195 bool ok;
196 const dms ra = FitsSolverEstRA->createDms(&ok);
197 bool ok2;
198 const dms dec = FitsSolverEstDec->createDms(&ok2);
199 if (ok && ok2)
200 m_Solver->usePosition(true, ra.Degrees(), dec.Degrees());
201 else
202 m_Solver->usePosition(false, 0, 0);
203 }
204 else m_Solver->usePosition(false, 0, 0);
205}
206
207// If it is currently solving an image, then cancel the solve.
208// Otherwise start solving.
209void PlateSolve::extractImage(const QSharedPointer<FITSData> &imageData)
210{
211 m_imageData = imageData;
212 if (m_Solver.get() && m_Solver->isRunning())
213 {
214 SolveButton->setText(i18n("Aborting..."));
215 m_Solver->abort();
216 return;
217 }
218 SolveButton->setText(i18n("Cancel"));
219
220 setupSolver(imageData, true);
221
222 FitsSolverAngle->setText("");
223 FitsSolverIndexfile->setText("");
224 Solution1->setText(i18n("Extracting..."));
225 Solution2->setText("");
226
227 m_Solver->runSolver(imageData);
228}
229
230void PlateSolve::solveImage(const QString &filename)
231{
232 connect(&m_Watcher, &QFutureWatcher<bool>::finished, this, &PlateSolve::loadFileDone, Qt::UniqueConnection);
233
234 m_imageData.reset(new FITSData(), &QObject::deleteLater);
235 QFuture<bool> response = m_imageData->loadFromFile(filename);
236 m_Watcher.setFuture(response);
237}
238
239void PlateSolve::loadFileDone()
240{
241 solveImage(m_imageData);
242 disconnect(&m_Watcher);
243}
244
245void PlateSolve::solveImage(const QSharedPointer<FITSData> &imageData)
246{
247 m_imageData = imageData;
248 if (m_Solver.get() && m_Solver->isRunning())
249 {
250 SolveButton->setText(i18n("Aborting..."));
251 m_Solver->abort();
252 return;
253 }
254 SolveButton->setText(i18n("Cancel"));
255
256 setupSolver(imageData, false);
257
258 Solution2->setText(i18n("Solving..."));
259
260 m_Solver->runSolver(imageData);
261}
262
263void PlateSolve::extractorDone(bool timedOut, bool success, const FITSImage::Solution &solution, double elapsedSeconds)
264{
265 Q_UNUSED(solution);
266 disconnect(m_Solver.get(), &SolverUtils::done, this, &PlateSolve::extractorDone);
267 Solution2->setText("");
268
269 if (timedOut)
270 {
271 const QString result = i18n("Extractor timed out: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
272 Solution1->setText(result);
273
274 // Can't run the solver. Just reset.
275 SolveButton->setText("Solve");
276 emit extractorFailed();
277 return;
278 }
279 else if (!success)
280 {
281 const QString result = i18n("Extractor failed: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
282 Solution1->setText(result);
283
284 // Can't run the solver. Just reset.
285 SolveButton->setText(i18n("Solve"));
286 emit extractorFailed();
287 return;
288 }
289 else
290 {
291 const QString starStr = i18n("Extracted %1 stars (%2 unfiltered) in %3s",
292 m_Solver->getNumStarsFound(),
293 m_Solver->getBackground().num_stars_detected,
294 QString("%1").arg(elapsedSeconds, 0, 'f', 1));
295 Solution1->setText(starStr);
296
297 // Set the stars in the FITSData object so the user can view them.
298 const QList<FITSImage::Star> &starList = m_Solver->getStarList();
299 QList<Edge*> starCenters;
300 starCenters.reserve(starList.size());
301 for (int i = 0; i < starList.size(); i++)
302 {
303 const auto &star = starList[i];
304 Edge *oneEdge = new Edge();
305 oneEdge->x = star.x;
306 oneEdge->y = star.y;
307 oneEdge->val = star.peak;
308 oneEdge->sum = star.flux;
309 oneEdge->HFR = star.HFR;
310 oneEdge->width = star.a;
311 oneEdge->numPixels = star.numPixels;
312 if (star.a > 0)
313 // See page 63 to find the ellipticity equation for SEP.
314 // http://astroa.physics.metu.edu.tr/MANUALS/sextractor/Guide2source_extractor.pdf
315 oneEdge->ellipticity = 1 - star.b / star.a;
316 else
317 oneEdge->ellipticity = 0;
318
319 starCenters.append(oneEdge);
320 }
321 m_imageData->setStarCenters(starCenters);
322 emit extractorSuccess();
323 }
324}
325
326void PlateSolve::solverDone(bool timedOut, bool success, const FITSImage::Solution &solution, double elapsedSeconds)
327{
328 m_Solution = FITSImage::Solution();
329 disconnect(m_Solver.get(), &SolverUtils::done, this, &PlateSolve::solverDone);
330 SolveButton->setText("Solve");
331
332 if (m_Solver->isRunning())
333 qCDebug(KSTARS_FITS) << "solverDone called, but it is still running.";
334
335 if (timedOut)
336 {
337 const QString result = i18n("Solver timed out: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
338 Solution2->setText(result);
339 emit solverFailed();
340 }
341 else if (!success)
342 {
343 const QString result = i18n("Solver failed: %1s", QString("%L1").arg(elapsedSeconds, 0, 'f', 1));
344 Solution2->setText(result);
345 emit solverFailed();
346 }
347 else
348 {
349 m_Solution = solution;
350 const bool eastToTheRight = solution.parity == FITSImage::POSITIVE ? false : true;
351 m_imageData->injectWCS(solution.orientation, solution.ra, solution.dec, solution.pixscale, eastToTheRight);
352 m_imageData->loadWCS();
353
354 const QString result = QString("Solved in %1s").arg(elapsedSeconds, 0, 'f', 1);
355 const double solverPA = KSUtils::rotationToPositionAngle(solution.orientation);
356 FitsSolverAngle->setText(QString("%1ยบ").arg(solverPA, 0, 'f', 2));
357
358 int indexUsed = -1, healpixUsed = -1;
359 m_Solver->getSolutionHealpix(&indexUsed, &healpixUsed);
360 if (indexUsed < 0)
361 FitsSolverIndexfile->setText("");
362 else
363 FitsSolverIndexfile->setText(
364 QString("%1%2")
365 .arg(indexUsed)
366 .arg(healpixUsed >= 0 ? QString("-%1").arg(healpixUsed) : QString("")));;
367
368 // Set the scale widget to the current result
369 const int imageWidth = m_imageData->width();
370 const int units = kcfg_FitsSolverImageScaleUnits->currentIndex();
371 if (units == SSolver::DEG_WIDTH)
372 kcfg_FitsSolverScale->setValue(solution.pixscale * imageWidth / 3600.0);
373 else if (units == SSolver::ARCMIN_WIDTH)
374 kcfg_FitsSolverScale->setValue(solution.pixscale * imageWidth / 60.0);
375 else
376 kcfg_FitsSolverScale->setValue(solution.pixscale);
377
378 // Set the ra and dec widgets to the current result
379 FitsSolverEstRA->show(dms(solution.ra));
380 FitsSolverEstDec->show(dms(solution.dec));
381
382 Solution2->setText(result);
383 emit solverSuccess();
384 }
385}
386
387// Each module can default to its own profile index. These two methods retrieves and saves
388// the values in a JSON string using an Options variable.
389int PlateSolve::getProfileIndex(int moduleIndex)
390{
391 if (moduleIndex < 0 || moduleIndex >= Ekos::ProfileGroupNames.size())
392 return 0;
393 const QString moduleName = Ekos::ProfileGroupNames[moduleIndex].toString();
394 const QString str = Options::fitsSolverProfileIndeces();
395 const QJsonDocument doc = QJsonDocument::fromJson(str.toUtf8());
396 if (doc.isNull() || !doc.isObject())
397 return 0;
398 const QJsonObject indeces = doc.object();
399 return indeces[moduleName].toString().toInt();
400}
401
402void PlateSolve::setProfileIndex(int moduleIndex, int profileIndex)
403{
404 if (moduleIndex < 0 || moduleIndex >= Ekos::ProfileGroupNames.size())
405 return;
406 QString str = Options::fitsSolverProfileIndeces();
407 QJsonDocument doc = QJsonDocument::fromJson(str.toUtf8());
408 if (doc.isNull() || !doc.isObject())
409 {
410 QJsonObject initialIndeces;
411 for (int i = 0; i < Ekos::ProfileGroupNames.size(); i++)
412 {
413 QString name = Ekos::ProfileGroupNames[i].toString();
414 if (name == "Align")
415 initialIndeces[name] = QString::number(Options::solveOptionsProfile());
416 else if (name == "Guide")
417 initialIndeces[name] = QString::number(Options::guideOptionsProfile());
418 else if (name == "HFR")
419 initialIndeces[name] = QString::number(Options::hFROptionsProfile());
420 else // Focus has a weird setting, just default to 0
421 initialIndeces[name] = "0";
422 }
423 doc = QJsonDocument(initialIndeces);
424 }
425
426 QJsonObject indeces = doc.object();
427 indeces[Ekos::ProfileGroupNames[moduleIndex].toString()] = QString::number(profileIndex);
428 doc = QJsonDocument(indeces);
429 Options::setFitsSolverProfileIndeces(QString(doc.toJson()));
430}
431
432void PlateSolve::setupProfiles(int moduleIndex)
433{
434 if (moduleIndex < 0 || moduleIndex >= Ekos::ProfileGroupNames.size())
435 return;
436 Ekos::ProfileGroup profileGroup = static_cast<Ekos::ProfileGroup>(moduleIndex);
437 Options::setFitsSolverModule(moduleIndex);
438
439 // Set up the profiles' menu.
440 const QList<SSolver::Parameters> optionsList = getSSolverParametersList(profileGroup);
441 kcfg_FitsSolverProfile->clear();
442 for(auto &param : optionsList)
443 kcfg_FitsSolverProfile->addItem(param.listName);
444
445 m_ProfileEditor->setProfileGroup(profileGroup, false);
446
447 // Restore the stored options.
448 kcfg_FitsSolverProfile->setCurrentIndex(getProfileIndex(Options::fitsSolverModule()));
449
450 m_ProfileEditorPage->setHeader(QString("FITS Viewer Solver %1 Profiles Editor")
451 .arg(Ekos::ProfileGroupNames[moduleIndex].toString()));
452}
453
454void PlateSolve::setPosition(const SkyPoint &p)
455{
456 FitsSolverEstRA->show(p.ra());
457 FitsSolverEstDec->show(p.dec());
458}
459void PlateSolve::setUsePosition(bool yesNo)
460{
461 kcfg_FitsSolverUsePosition->setChecked(yesNo);
462}
463void PlateSolve::setScale(double scale)
464{
465 kcfg_FitsSolverScale->setValue(scale);
466}
467void PlateSolve::setScaleUnits(int units)
468{
469 kcfg_FitsSolverImageScaleUnits->setCurrentIndex(units);
470}
471void PlateSolve::setUseScale(bool yesNo)
472{
473 kcfg_FitsSolverUseScale->setChecked(yesNo);
474}
475void PlateSolve::setLinear(bool yesNo)
476{
477 kcfg_FitsSolverLinear->setChecked(yesNo);
478}
479
480void PlateSolve::initSolverUI()
481{
482 // Init the modules combo box.
483 kcfg_FitsSolverModule->clear();
484 for (int i = 0; i < Ekos::ProfileGroupNames.size(); i++)
485 kcfg_FitsSolverModule->addItem(Ekos::ProfileGroupNames[i].toString());
486 kcfg_FitsSolverModule->setCurrentIndex(Options::fitsSolverModule());
487
488 setupProfiles(Options::fitsSolverModule());
489
490 // Change the profiles combo box whenever the modules combo changes
491 connect(kcfg_FitsSolverModule, QOverload<int>::of(&QComboBox::activated), this, &PlateSolve::setupProfiles,
493
494 kcfg_FitsSolverUseScale->setChecked(Options::fitsSolverUseScale());
495 kcfg_FitsSolverScale->setValue(Options::fitsSolverScale());
496 kcfg_FitsSolverImageScaleUnits->setCurrentIndex(Options::fitsSolverImageScaleUnits());
497
498 kcfg_FitsSolverUsePosition->setChecked(Options::fitsSolverUsePosition());
499 kcfg_FitsSolverRadius->setValue(Options::fitsSolverRadius());
500
501 FitsSolverEstRA->setUnits(dmsBox::HOURS);
502 FitsSolverEstDec->setUnits(dmsBox::DEGREES);
503
504 // Save the values of user controls when the user changes them.
505 connect(kcfg_FitsSolverProfile, QOverload<int>::of(&QComboBox::activated), [this](int index)
506 {
507 setProfileIndex(kcfg_FitsSolverModule->currentIndex(), index);
508 });
509
510 connect(kcfg_FitsSolverUseScale, &QCheckBox::stateChanged, this, [](int state)
511 {
512 Options::setFitsSolverUseScale(state);
513 });
514 connect(kcfg_FitsSolverScale, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [](double value)
515 {
516 Options::setFitsSolverScale(value);
517 });
518 connect(kcfg_FitsSolverImageScaleUnits, QOverload<int>::of(&QComboBox::activated), [](int index)
519 {
520 Options::setFitsSolverImageScaleUnits(index);
521 });
522
523 connect(kcfg_FitsSolverUsePosition, &QCheckBox::stateChanged, this, [](int state)
524 {
525 Options::setFitsSolverUsePosition(state);
526 });
527
528 connect(kcfg_FitsSolverRadius, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [](double value)
529 {
530 Options::setFitsSolverRadius(value);
531 });
532 connect(UpdatePosition, &QPushButton::clicked, this, [&]()
533 {
534 const auto center = SkyMap::Instance()->getCenterPoint();
535 FitsSolverEstRA->show(center.ra());
536 FitsSolverEstDec->show(center.dec());
537 });
538
539 // Warn if the user is not using the internal StellarSolver solver.
540 const SSolver::SolverType type = static_cast<SSolver::SolverType>(Options::solverType());
541 if(type != SSolver::SOLVER_STELLARSOLVER)
542 {
543 Solution2->setText(i18n("Warning! This tool only supports the internal StellarSolver solver."));
544 Solution1->setText(i18n("Change to that in the Ekos Align options menu."));
545 }
546}
547
548void PlateSolve::setImageDisplay(const QImage &image)
549{
550 QImage scaled = image.scaledToHeight(300);
551 plateSolveImage->setVisible(true);
552 plateSolveImage->setPixmap(QPixmap::fromImage(scaled));
553}
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 25 2025 11:58:36 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.