Kstars

imageoverlaycomponent.cpp
1/*
2 SPDX-FileCopyrightText: 2023 Hy Murveit <hy@murveit.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "imageoverlaycomponent.h"
8
9#include "kstars.h"
10#include "Options.h"
11#include "skypainter.h"
12#include "skymap.h"
13#ifdef HAVE_CFITSIO
14#include "fitsviewer/fitsdata.h"
15#endif
16#include "auxiliary/kspaths.h"
17#include "ekos/auxiliary/solverutils.h"
18#include "ekos/auxiliary/stellarsolverprofile.h"
19
20#include <QTableWidget>
21#include <QImageReader>
22#include <QCheckBox>
23#include <QComboBox>
24#include <QtConcurrent>
25#include <QRegularExpression>
26
27namespace
28{
29
30enum ColumnIndex
31{
32 FILENAME_COL = 0,
33 //ENABLED_COL,
34 //NICKNAME_COL,
36 RA_COL,
37 DEC_COL,
44};
45
46// These needs to be syncronized with enum Status and initializeGui::StatusNames().
47constexpr int UNPROCESSED_INDEX = 0;
48constexpr int OK_INDEX = 4;
49
50// Helper to create the image overlay table.
51// Start the table, displaying the heading and timing information, common to all sessions.
52void setupTable(QTableWidget *table)
53{
54 table->clear();
55 table->setRowCount(0);
57 table->setShowGrid(false);
58 table->setWordWrap(true);
59
61 {
62 i18n("Filename"),
63 // "", "Nickname",
64 i18n("Status"), i18n("RA"), i18n("DEC"), i18n("A-S/px"), i18n("Angle"),
65 i18n("Width"), i18n("Height"), i18n("EastRight")
66 };
68}
69
70// This initializes an item in the table widget.
71void setupTableItem(QTableWidget *table, int row, int column, const QString &text, bool editable = true)
72{
73 if (table->rowCount() < row + 1)
74 table->setRowCount(row + 1);
75 if (column >= NUM_COLUMNS)
76 return;
79 item->setText(text);
80 if (!editable)
81 item->setFlags(item->flags() ^ Qt::ItemIsEditable);
82 table->setItem(row, column, item);
83}
84
85// Helper for sorting the overlays alphabetically (case-insensitive).
86bool overlaySorter(const ImageOverlay &o1, const ImageOverlay &o2)
87{
88 return o1.m_Filename.toLower() < o2.m_Filename.toLower();
89}
90
91// The dms method for initializing from text requires knowing if the input is degrees or hours.
92// This is a crude way to detect HMS input, and may not internationalize well.
93bool isHMS(const QString &input)
94{
96 // Just 14h
97 QRegularExpression re1("^(\\d+)\\s*h$");
98 // 14h 2m
99 QRegularExpression re2("^(\\d+)\\s*h\\s(\\d+)\\s*(?:[m\']?)$");
100 // 14h 2m 3.2s
101 QRegularExpression re3("^(\\d+)\\s*h\\s(\\d+)\\s*[m\'\\s]\\s*(\\d+\\.*\\d*)\\s*([s\"]?)$");
102
103 return re1.match(trimmedInput).hasMatch() ||
104 re2.match(trimmedInput).hasMatch() ||
105 re3.match(trimmedInput).hasMatch();
106}
107
108// Even if an image is already solved, the user may have changed the status in the UI.
109bool shouldSolveAnyway(QTableWidget *table, int row)
110{
111 QComboBox *item = dynamic_cast<QComboBox*>(table->cellWidget(row, STATUS_COL));
112 if (!item) return false;
113 return (item->currentIndex() != OK_INDEX);
114}
115
116QString toDecString(const dms &dec)
117{
118 // Sadly DMS::setFromString doesn't parse dms::toDMSString()
119 // return dec.toDMSString();
120 return QString("%1 %2' %3\"").arg(dec.degree()).arg(dec.arcmin()).arg(dec.arcsec());
121}
122
123QString toDecString(double dec)
124{
125 return toDecString(dms(dec));
126}
127} // namespace
128
129ImageOverlayComponent::ImageOverlayComponent(SkyComposite *parent) : SkyComponent(parent)
130{
131 QDir dir = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/imageOverlays");
132 dir.mkpath(".");
133 m_Directory = dir.absolutePath();
134 connect(&m_TryAgainTimer, &QTimer::timeout, this, &ImageOverlayComponent::tryAgain, Qt::UniqueConnection);
135 connect(this, &ImageOverlayComponent::updateLog, this, &ImageOverlayComponent::updateStatusDisplay, Qt::UniqueConnection);
136
137 // Get the latest from the User DB
138 loadFromUserDB();
139
140 // The rest of the initialization happens when we get the setWidgets() call.
141}
142
143// Validate the user inputs, and if invalid, replace with the previous values.
144void ImageOverlayComponent::cellChanged(int row, int col)
145{
146 if (!m_Initialized || col < 0 || col >= NUM_COLUMNS || row < 0 || row >= m_ImageOverlayTable->rowCount()) return;
147 // Note there are a couple columns with widgets instead of normal text items.
148 // This method shouldn't get called for those, but...
149 if (col == STATUS_COL || col == EAST_TO_RIGHT_COL) return;
150
151 QTableWidgetItem *item = m_ImageOverlayTable->item(row, col);
152 if (!item) return;
153
154 disconnect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged);
155 QString itemString = item->text();
156 auto overlay = m_Overlays[row];
157 if (col == RA_COL)
158 {
159 dms raDMS;
160 const bool useHMS = isHMS(itemString);
161 const bool raOK = raDMS.setFromString(itemString, !useHMS);
162 if (!raOK)
163 {
164 item->setText(dms(overlay.m_RA).toHMSString());
165 QString msg = i18n("Bad RA string entered for %1. Reset to original value.", overlay.m_Filename);
166 emit updateLog(msg);
167 }
168 else
169 // Re-format the user-entered value.
170 item->setText(raDMS.toHMSString());
171 }
172 else if (col == DEC_COL)
173 {
174 dms decDMS;
175 const bool decOK = decDMS.setFromString(itemString);
176 if (!decOK)
177 {
178 item->setText(toDecString(overlay.m_DEC));
179 QString msg = i18n("Bad DEC string entered for %1. Reset to original value.", overlay.m_Filename);
180 emit updateLog(msg);
181 }
182 else
183 item->setText(toDecString(decDMS));
184 }
185 else if (col == ORIENTATION_COL)
186 {
187 bool angleOK = false;
188 double angle = itemString.toDouble(&angleOK);
189 if (!angleOK || angle > 360 || angle < -360)
190 {
191 item->setText(QString("%1").arg(overlay.m_Orientation, 0, 'f', 2));
192 QString msg = i18n("Bad orientation angle string entered for %1. Reset to original value.", overlay.m_Filename);
193 emit updateLog(msg);
194 }
195 }
196 else if (col == ARCSEC_PER_PIXEL_COL)
197 {
198 bool scaleOK = false;
199 double scale = itemString.toDouble(&scaleOK);
200 if (!scaleOK || scale < 0 || scale > 1000)
201 {
202 item->setText(QString("%1").arg(overlay.m_ArcsecPerPixel, 0, 'f', 2));
203 QString msg = i18n("Bad scale angle string entered for %1. Reset to original value.", overlay.m_Filename);
204 emit updateLog(msg);
205 }
206 }
207 connect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged, Qt::UniqueConnection);
208}
209
210// Like cellChanged() but for the status column which contains QComboxBox widgets.
211void ImageOverlayComponent::statusCellChanged(int row)
212{
213 if (row < 0 || row >= m_ImageOverlayTable->rowCount()) return;
214
215 auto overlay = m_Overlays[row];
216 disconnect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged);
217
218 // If the user changed the status of a column to OK,
219 // then we check to make sure the required columns are filled out.
220 // If so, then we also save it to the DB.
221 // If the required columns are not filled out, the QComboBox value is reverted to UNPROCESSED.
222 QComboBox *statusItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(row, STATUS_COL));
223 bool failed = false;
224 if (statusItem->currentIndex() == OK_INDEX)
225 {
226 dms raDMS;
227 QTableWidgetItem *raItem = m_ImageOverlayTable->item(row, RA_COL);
228 if (!raItem) return;
229 const bool useHMS = isHMS(raItem->text());
230 const bool raOK = raDMS.setFromString(raItem->text(), !useHMS);
231 if (!raOK || raDMS.Degrees() == 0)
232 {
233 QString msg = i18n("Cannot set status to OK. Legal non-0 RA value required.");
234 emit updateLog(msg);
235 failed = true;
236 }
237
238 dms decDMS;
239 QTableWidgetItem *decItem = m_ImageOverlayTable->item(row, DEC_COL);
240 if (!decItem) return;
241 const bool decOK = decDMS.setFromString(decItem->text());
242 if (!decOK)
243 {
244 QString msg = i18n("Cannot set status to OK. Legal non-0 DEC value required.");
245 emit updateLog(msg);
246 failed = true;
247 }
248
249 bool angleOK = false;
250 QTableWidgetItem *angleItem = m_ImageOverlayTable->item(row, ORIENTATION_COL);
251 if (!angleItem) return;
252 const double angle = angleItem->text().toDouble(&angleOK);
253 if (!angleOK || angle > 360 || angle < -360)
254 {
255 QString msg = i18n("Cannot set status to OK. Legal orientation value required.");
256 emit updateLog(msg);
257 failed = true;
258 }
259
260 bool scaleOK = false;
261 QTableWidgetItem *scaleItem = m_ImageOverlayTable->item(row, ARCSEC_PER_PIXEL_COL);
262 if (!scaleItem) return;
263 const double scale = scaleItem->text().toDouble(&scaleOK);
264 if (!scaleOK || scale < 0 || scale > 1000)
265 {
266 QString msg = i18n("Cannot set status to OK. Legal non-0 a-s/px value required.");
267 emit updateLog(msg);
268 failed = true;
269 }
270
271 if (failed)
272 {
273 QComboBox *statusItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(row, STATUS_COL));
274 statusItem->setCurrentIndex(UNPROCESSED_INDEX);
275 }
276 else
277 {
278 m_Overlays[row].m_Status = ImageOverlay::AVAILABLE;
279 m_Overlays[row].m_RA = raDMS.Degrees();
280 m_Overlays[row].m_DEC = decDMS.Degrees();
281 m_Overlays[row].m_ArcsecPerPixel = scale;
282 m_Overlays[row].m_Orientation = angle;
283 const QComboBox *ewItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(row, EAST_TO_RIGHT_COL));
284 m_Overlays[row].m_EastToTheRight = ewItem->currentIndex();
285
286 if (m_Overlays[row].m_Img.get() == nullptr)
287 {
288 // Load the image.
289 const QString fullFilename = QString("%1/%2").arg(m_Directory).arg(m_Overlays[row].m_Filename);
290 QImage *img = loadImageFile(fullFilename, !m_Overlays[row].m_EastToTheRight);
291 m_Overlays[row].m_Width = img->width();
292 m_Overlays[row].m_Height = img->height();
293 m_Overlays[row].m_Img.reset(img);
294 }
295 saveToUserDB();
296 QString msg = i18n("Stored OK status for %1.", m_Overlays[row].m_Filename);
297 emit updateLog(msg);
298 }
299 }
300 connect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged, Qt::UniqueConnection);
301}
302
303ImageOverlayComponent::~ImageOverlayComponent()
304{
305 if (m_LoadImagesFuture.isRunning())
306 {
307 m_LoadImagesFuture.cancel();
308 m_LoadImagesFuture.waitForFinished();
309 }
310}
311
312void ImageOverlayComponent::selectionChanged()
313{
314 if (m_Initialized && Options::showSelectedImageOverlay())
315 show();
316}
317
319{
320 return Options::showImageOverlays();
321}
322
324{
325#if !defined(KSTARS_LITE)
326 if (m_Initialized)
327 skyp->drawImageOverlay(&m_Overlays);
328#else
329 Q_UNUSED(skyp);
330#endif
331}
332
333void ImageOverlayComponent::setWidgets(QTableWidget *table, QPlainTextEdit *statusDisplay,
335 QComboBox *solverProfile)
336{
337 m_ImageOverlayTable = table;
338 // Temporarily make the table uneditable.
339 m_EditTriggers = m_ImageOverlayTable->editTriggers();
341
342 m_SolveButton = solveButton;
343 m_TableGroupBox = tableGroupBox;
344 m_SolverProfile = solverProfile;
345 solveButton->setText(i18n("Solve"));
346
347 m_StatusDisplay = statusDisplay;
348 setupTable(table);
349 updateTable();
350 connect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged, Qt::UniqueConnection);
351 connect(m_ImageOverlayTable, &QTableWidget::itemSelectionChanged, this, &ImageOverlayComponent::selectionChanged,
353
354 initSolverProfiles();
355 loadAllImageFiles();
356}
357
358void ImageOverlayComponent::initSolverProfiles()
359{
360 QString savedOptionsProfiles = QDir(KSPaths::writableLocation(
361 QStandardPaths::AppLocalDataLocation)).filePath("SavedAlignProfiles.ini");
362
363 QList<SSolver::Parameters> optionsList;
364 if(QFile(savedOptionsProfiles).exists())
365 optionsList = StellarSolver::loadSavedOptionsProfiles(savedOptionsProfiles);
366 else
367 optionsList = Ekos::getDefaultAlignOptionsProfiles();
368
369 m_SolverProfile->clear();
370 for(auto &param : optionsList)
371 m_SolverProfile->addItem(param.listName);
372 m_SolverProfile->setCurrentIndex(Options::solveOptionsProfile());
373}
374
375void ImageOverlayComponent::updateStatusDisplay(const QString &message)
376{
377 if (!m_StatusDisplay)
378 return;
379 m_LogText.insert(0, message);
380 m_StatusDisplay->setPlainText(m_LogText.join("\n"));
381}
382
383// Find all the files in the directory, see if they are in the m_Overlays.
384// If no, append to the end of m_Overlays, and set status as unprocessed.
385void ImageOverlayComponent::updateTable()
386{
387#ifdef HAVE_CFITSIO
388 // Get the list of files from the image overlay directory.
389 QDir directory(m_Directory);
390 emit updateLog(i18n("Updating from directory: %1", m_Directory));
391 QStringList images = directory.entryList(QStringList() << "*", QDir::Files);
393 foreach(QString filename, images)
394 {
395 if (!FITSData::readableFilename(filename))
396 continue;
397 imageFiles.insert(filename);
398 }
399
400 // Sort the files alphabetically.
402 for (const auto &fn : imageFiles)
403 sortedImageFiles.push_back(fn);
404 std::sort(sortedImageFiles.begin(), sortedImageFiles.end(), overlaySorter);
405
406 // Remove any overlays that aren't in the directory.
409 int numDeleted = 0;
410 for (int i = 0; i < m_Overlays.size(); ++i)
411 {
412 auto &fname = m_Overlays[i].m_Filename;
413 if (sortedImageFiles.indexOf(fname) >= 0)
414 {
415 tempOverlays.append(m_Overlays[i]);
416 tempMap[fname] = tempOverlays.size() - 1;
417 }
418 else
419 numDeleted++;
420 }
421 m_Overlays = tempOverlays;
422 m_Filenames = tempMap;
423
424 // Add the new files into the overlay list.
425 int numNew = 0;
426 for (const auto &filename : sortedImageFiles)
427 {
428 auto item = m_Filenames.find(filename);
429 if (item == m_Filenames.end())
430 {
431 // If it doesn't already exist in our database:
432 ImageOverlay overlay(filename);
433 const int size = m_Filenames.size(); // place before push_back().
434 m_Overlays.push_back(overlay);
435 m_Filenames[filename] = size;
436 numNew++;
437 }
438 }
439 emit updateLog(i18n("%1 overlays (%2 new, %3 deleted) %4 solved", m_Overlays.size(), numNew, numDeleted,
440 numAvailable()));
441 m_TableGroupBox->setTitle(i18n("Image Overlays. %1 images, %2 available.", m_Overlays.size(), numAvailable()));
442
443 initializeGui();
444 saveToUserDB();
445#endif
446}
447
448void ImageOverlayComponent::loadAllImageFiles()
449{
450 m_LoadImagesFuture = QtConcurrent::run(this, &ImageOverlayComponent::loadImageFileLoop);
451}
452
453void ImageOverlayComponent::loadImageFileLoop()
454{
455 emit updateLog(i18n("Loading image files..."));
456 while (loadImageFile());
457 int num = 0;
458 for (const auto &o : m_Overlays)
459 if (o.m_Img.get() != nullptr)
460 num++;
461 emit updateLog(i18n("%1 image files loaded.", num));
462 // Restore editing for the table.
463 m_ImageOverlayTable->setEditTriggers(m_EditTriggers);
464 m_Initialized = true;
465}
466
467QImage *ImageOverlayComponent::loadImageFile (const QString &fullFilename, bool mirror)
468{
470 if (tempImage.get() == nullptr) return nullptr;
471 int scaleWidth = std::min(tempImage->width(), Options::imageOverlayMaxDimension());
473 if (mirror)
474 *processedImg = tempImage->mirrored(true, false).scaledToWidth(scaleWidth); // It's reflected horizontally.
475 else
476 *processedImg = tempImage->scaledToWidth(scaleWidth);
477
478 return processedImg;
479}
480
481bool ImageOverlayComponent::loadImageFile()
482{
483 bool updatedSomething = false;
484
485 for (auto &o : m_Overlays)
486 {
487 if (o.m_Status == o.ImageOverlay::AVAILABLE && o.m_Img.get() == nullptr)
488 {
489 QString fullFilename = QString("%1%2%3").arg(m_Directory).arg(QDir::separator()).arg(o.m_Filename);
490 QImage *img = loadImageFile(fullFilename, !o.m_EastToTheRight);
491 o.m_Img.reset(img);
492 updatedSomething = true;
493
494 // Note: The original width and height in o.m_Width/m_Height is kept even
495 // though the image was rescaled. This is to get the rendering right
496 // with the original scale.
497 }
498 }
499 return updatedSomething;
500}
501
502
503// Copies the info in m_Overlays into m_ImageOverlayTable UI.
504void ImageOverlayComponent::initializeGui()
505{
506 if (!m_ImageOverlayTable) return;
507
508 // Don't call callback on programmatic changes.
509 disconnect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged);
510
511 // This clears the table.
512 setupTable(m_ImageOverlayTable);
513
514 int row = 0;
515 for (int i = 0; i < m_Overlays.size(); ++i)
516 {
517 const ImageOverlay &overlay = m_Overlays[row];
518 // False: The user can't edit filename.
519 setupTableItem(m_ImageOverlayTable, row, FILENAME_COL, overlay.m_Filename, false);
520
522 {
523 i18n("Unprocessed"), i18n("Bad File"), i18n("Solve Failed"), i18n("Error"), i18n("OK")
524 };
526 for (int i = 0; i < ImageOverlay::NUM_STATUS; ++i)
527 statusBox->addItem(StatusNames[i]);
529 {
531 statusCellChanged(row);
532 });
533 statusBox->setCurrentIndex(static_cast<int>(overlay.m_Status));
534 m_ImageOverlayTable->setCellWidget(row, STATUS_COL, statusBox);
535
536 setupTableItem(m_ImageOverlayTable, row, ORIENTATION_COL, QString("%1").arg(overlay.m_Orientation, 0, 'f', 2));
537 setupTableItem(m_ImageOverlayTable, row, RA_COL, dms(overlay.m_RA).toHMSString());
538 setupTableItem(m_ImageOverlayTable, row, DEC_COL, toDecString(overlay.m_DEC));
539 setupTableItem(m_ImageOverlayTable, row, ARCSEC_PER_PIXEL_COL, QString("%1").arg(overlay.m_ArcsecPerPixel, 0, 'f', 2));
540
541 // The user can't edit width & height--taken from image.
542 setupTableItem(m_ImageOverlayTable, row, WIDTH_COL, QString("%1").arg(overlay.m_Width), false);
543 setupTableItem(m_ImageOverlayTable, row, HEIGHT_COL, QString("%1").arg(overlay.m_Height), false);
544
546 mirroredBox->addItem(i18n("West-Right"));
547 mirroredBox->addItem(i18n("East-Right"));
549 {
550 Q_UNUSED(row);
552 // Don't need to do anything. Will get picked up on change of status to OK.
553 });
554 mirroredBox->setCurrentIndex(overlay.m_EastToTheRight ? 1 : 0);
555 m_ImageOverlayTable->setCellWidget(row, EAST_TO_RIGHT_COL, mirroredBox);
556
557 row++;
558 }
559 m_ImageOverlayTable->resizeColumnsToContents();
560 m_TableGroupBox->setTitle(i18n("Image Overlays. %1 images, %2 available.", m_Overlays.size(), numAvailable()));
561 connect(m_ImageOverlayTable, &QTableWidget::cellChanged, this, &ImageOverlayComponent::cellChanged, Qt::UniqueConnection);
562
563}
564
565void ImageOverlayComponent::loadFromUserDB()
566{
568 KStarsData::Instance()->userdb()->GetAllImageOverlays(&m_Overlays);
569 // Alphabetize.
570 std::sort(m_Overlays.begin(), m_Overlays.end(), overlaySorter);
571 m_Filenames.clear();
572 int index = 0;
573 for (const auto &o : m_Overlays)
574 {
575 m_Filenames[o.m_Filename] = index;
576 index++;
577 }
578}
579
580void ImageOverlayComponent::saveToUserDB()
581{
582 KStarsData::Instance()->userdb()->DeleteAllImageOverlays();
583 for (const ImageOverlay &metadata : m_Overlays)
584 KStarsData::Instance()->userdb()->AddImageOverlay(metadata);
585}
586
587void ImageOverlayComponent::solveImage(const QString &filename)
588{
589 if (!m_Initialized) return;
590 m_SolveButton->setText(i18n("Abort"));
591 const int solverTimeout = Options::imageOverlayTimeout();
592 auto profiles = Ekos::getDefaultAlignOptionsProfiles();
593 auto parameters = profiles.at(m_SolverProfile->currentIndex());
594 // Double search radius
595 parameters.search_radius = parameters.search_radius * 2;
596
597 m_Solver.reset(new SolverUtils(parameters, solverTimeout), &QObject::deleteLater);
598 connect(m_Solver.get(), &SolverUtils::done, this, &ImageOverlayComponent::solverDone, Qt::UniqueConnection);
599
600 if (m_RowsToSolve.size() > 1)
601 emit updateLog(i18n("Solving: %1. %2 in queue.", filename, m_RowsToSolve.size()));
602 else
603 emit updateLog(i18n("Solving: %1.", filename));
604
605 // If the user added some RA/DEC/Scale values to the table, they will be used in the solve
606 // (but aren't remembered in the DB unless the solve is successful).
607 int row = m_RowsToSolve[0];
608 QString raString = m_ImageOverlayTable->item(row, RA_COL)->text().toLatin1().data();
609 QString decString = m_ImageOverlayTable->item(row, DEC_COL)->text().toLatin1().data();
610 QString scaleString = m_ImageOverlayTable->item(row, ARCSEC_PER_PIXEL_COL)->text().toLatin1().data();
611
613 const bool useHMS = isHMS(raString);
614 const bool raOK = raDMS.setFromString(raString, !useHMS) && (raDMS.Degrees() != 0.00);
615 const bool decOK = decDMS.setFromString(decString) && (decDMS.Degrees() != 0.00);
616 bool scaleOK = false;
617 double scale = scaleString.toDouble(&scaleOK);
618 scaleOK = scaleOK && scale != 0.00;
619
620 // Use the default scale if it is > 0 and scale was not specified in the UI table.
621 if (!scaleOK && Options::imageOverlayDefaultScale() > 0.0001)
622 {
623 scale = Options::imageOverlayDefaultScale();
624 scaleOK = true;
625 }
626
627 if (scaleOK)
628 {
629 auto lowScale = scale * 0.75;
630 auto highScale = scale * 1.25;
631 m_Solver->useScale(true, lowScale, highScale);
632 }
633 if (raOK && decOK)
634 m_Solver->usePosition(true, raDMS.Degrees(), decDMS.Degrees());
635
636 m_Solver->runSolver(filename);
637}
638
639void ImageOverlayComponent::tryAgain()
640{
641 m_TryAgainTimer.stop();
642 if (!m_Initialized) return;
643 if (m_RowsToSolve.size() > 0)
644 startSolving();
645}
646
647int ImageOverlayComponent::numAvailable()
648{
649 int num = 0;
650 for (const auto &o : m_Overlays)
651 if (o.m_Status == ImageOverlay::AVAILABLE)
652 num++;
653 return num;
654}
655
656void ImageOverlayComponent::show()
657{
658 if (!m_Initialized || !m_ImageOverlayTable) return;
659 auto selections = m_ImageOverlayTable->selectionModel();
660 if (selections->hasSelection())
661 {
662 auto selectedIndexes = selections->selectedIndexes();
663 const int row = selectedIndexes.at(0).row();
664 if (m_Overlays.size() > row && row >= 0)
665 {
666 if (m_Overlays[row].m_Status != ImageOverlay::AVAILABLE)
667 {
668 emit updateLog(i18n("Can't show %1. Not plate solved.", m_Overlays[row].m_Filename));
669 return;
670 }
671 if (m_Overlays[row].m_Img.get() == nullptr)
672 {
673 emit updateLog(i18n("Can't show %1. Image not loaded.", m_Overlays[row].m_Filename));
674 return;
675 }
676 const double ra = m_Overlays[row].m_RA;
677 const double dec = m_Overlays[row].m_DEC;
678
679 // Convert the RA/DEC from j2000 to jNow.
680 auto localTime = KStarsData::Instance()->geo()->UTtoLT(KStarsData::Instance()->clock()->utc());
681 const dms raDms(ra), decDms(dec);
683 coord.apparentCoord(static_cast<long double>(J2000), KStars::Instance()->data()->ut().djd());
684
685 // Is this the right way to move to the SkyMap?
686 Options::setIsTracking(false);
687 SkyMap::Instance()->setFocusObject(nullptr);
688 SkyMap::Instance()->setFocusPoint(nullptr);
689 SkyMap::Instance()->setFocus(dms(coord.ra()), dms(coord.dec()));
690
691 // Zoom factor is in pixels per radian.
692 double zoomFactor = (400 * 60.0 * 10800.0) / (m_Overlays[row].m_Width * m_Overlays[row].m_ArcsecPerPixel * dms::PI);
693 SkyMap::Instance()->setZoomFactor(zoomFactor);
694
695 SkyMap::Instance()->forceUpdate(true);
696 }
697 }
698}
699
700void ImageOverlayComponent::abortSolving()
701{
702 if (!m_Initialized) return;
703 m_RowsToSolve.clear();
704 if (m_Solver)
705 m_Solver->abort();
706 emit updateLog(i18n("Solving aborted."));
707 m_SolveButton->setText(i18n("Solve"));
708}
709
710void ImageOverlayComponent::startSolving()
711{
712 if (!m_Initialized) return;
713 if (m_SolveButton->text() == i18n("Abort"))
714 {
715 abortSolving();
716 return;
717 }
718 if (m_Solver && m_Solver->isRunning())
719 {
720 m_Solver->abort();
721 if (m_RowsToSolve.size() > 0)
722 m_TryAgainTimer.start(2000);
723 return;
724 }
725
726 if (m_RowsToSolve.size() == 0)
727 {
728 QSet<int> selectedRows;
729 auto selections = m_ImageOverlayTable->selectionModel();
730 if (selections->hasSelection())
731 {
732 // Need to de-dup, as selecting the whole row will select all the columns.
733 auto selectedIndexes = selections->selectedIndexes();
734 for (int i = 0; i < selectedIndexes.count(); ++i)
735 {
736 // Don't insert a row that's already solved.
737 const int row = selectedIndexes.at(i).row();
738 if ((m_Overlays[row].m_Status == ImageOverlay::AVAILABLE) &&
739 !shouldSolveAnyway(m_ImageOverlayTable, row))
740 {
741 emit updateLog(i18n("Skipping already solved: %1.", m_Overlays[row].m_Filename));
742 continue;
743 }
744 selectedRows.insert(row);
745 }
746 }
747 m_RowsToSolve.clear();
748 for (int row : selectedRows)
749 m_RowsToSolve.push_back(row);
750 }
751
752 if (m_RowsToSolve.size() > 0)
753 {
754 const int row = m_RowsToSolve[0];
755 const QString filename =
756 QString("%1/%2").arg(m_Directory).arg(m_Overlays[row].m_Filename);
757 if ((m_Overlays[row].m_Status == ImageOverlay::AVAILABLE) &&
758 !shouldSolveAnyway(m_ImageOverlayTable, row))
759 {
760 emit updateLog(i18n("%1 already solved. Skipping.", filename));
761 m_RowsToSolve.removeFirst();
762 if (m_RowsToSolve.size() > 0)
763 startSolving();
764 return;
765 }
766
767 auto img = new QImage(filename);
768 m_Overlays[row].m_Width = img->width();
769 m_Overlays[row].m_Height = img->height();
770 solveImage(filename);
771 }
772}
773
774void ImageOverlayComponent::reload()
775{
776 if (!m_Initialized) return;
777 m_Initialized = false;
778 emit updateLog(i18n("Reloading. Image overlays temporarily disabled."));
779 updateTable();
780 loadAllImageFiles();
781}
782
783void ImageOverlayComponent::solverDone(bool timedOut, bool success, const FITSImage::Solution &solution,
784 double elapsedSeconds)
785{
786 disconnect(m_Solver.get(), &SolverUtils::done, this, &ImageOverlayComponent::solverDone);
787 m_SolveButton->setText(i18n("Solve"));
788 if (m_RowsToSolve.size() == 0)
789 return;
790
791 const int solverRow = m_RowsToSolve[0];
792 m_RowsToSolve.removeFirst();
793
794 QComboBox *statusItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(solverRow, STATUS_COL));
795 if (timedOut)
796 {
797 emit updateLog(i18n("Solver timed out in %1s", QString::number(elapsedSeconds, 'f', 1)));
798 m_Overlays[solverRow].m_Status = ImageOverlay::PLATE_SOLVE_FAILURE;
799 statusItem->setCurrentIndex(static_cast<int>(m_Overlays[solverRow].m_Status));
800 }
801 else if (!success)
802 {
803 emit updateLog(i18n("Solver failed in %1s", QString::number(elapsedSeconds, 'f', 1)));
804 m_Overlays[solverRow].m_Status = ImageOverlay::PLATE_SOLVE_FAILURE;
805 statusItem->setCurrentIndex(static_cast<int>(m_Overlays[solverRow].m_Status));
806 }
807 else
808 {
809 m_Overlays[solverRow].m_Orientation = solution.orientation;
810 m_Overlays[solverRow].m_RA = solution.ra;
811 m_Overlays[solverRow].m_DEC = solution.dec;
812 m_Overlays[solverRow].m_ArcsecPerPixel = solution.pixscale;
813 m_Overlays[solverRow].m_EastToTheRight = solution.parity;
814 m_Overlays[solverRow].m_Status = ImageOverlay::AVAILABLE;
815
816 QString msg = i18n("Solver success in %1s: RA %2 DEC %3 Scale %4 Angle %5",
818 QString::number(solution.ra, 'f', 2),
819 QString::number(solution.dec, 'f', 2),
820 QString::number(solution.pixscale, 'f', 2),
821 QString::number(solution.orientation, 'f', 2));
822 emit updateLog(msg);
823
824 // Store the new values in the table.
825 auto overlay = m_Overlays[solverRow];
826 m_ImageOverlayTable->item(solverRow, RA_COL)->setText(dms(overlay.m_RA).toHMSString());
827 m_ImageOverlayTable->item(solverRow, DEC_COL)->setText(toDecString(overlay.m_DEC));
828 m_ImageOverlayTable->item(solverRow, ARCSEC_PER_PIXEL_COL)->setText(
829 QString("%1").arg(overlay.m_ArcsecPerPixel, 0, 'f', 2));
830 m_ImageOverlayTable->item(solverRow, ORIENTATION_COL)->setText(QString("%1").arg(overlay.m_Orientation, 0, 'f', 2));
831 QComboBox *ewItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(solverRow, EAST_TO_RIGHT_COL));
832 ewItem->setCurrentIndex(overlay.m_EastToTheRight ? 1 : 0);
833
834 QComboBox *statusItem = dynamic_cast<QComboBox*>(m_ImageOverlayTable->cellWidget(solverRow, STATUS_COL));
835 statusItem->setCurrentIndex(static_cast<int>(overlay.m_Status));
836
837 // Load the image.
838 QString fullFilename = QString("%1/%2").arg(m_Directory).arg(m_Overlays[solverRow].m_Filename);
839 QImage *img = loadImageFile(fullFilename, !m_Overlays[solverRow].m_EastToTheRight);
840 m_Overlays[solverRow].m_Img.reset(img);
841 }
842 saveToUserDB();
843
844 if (m_RowsToSolve.size() > 0)
845 startSolving();
846 else
847 {
848 emit updateLog(i18n("Done solving. %1 available.", numAvailable()));
849 m_TableGroupBox->setTitle(i18n("Image Overlays. %1 images, %2 available.", m_Overlays.size(), numAvailable()));
850 }
851}
void draw(SkyPainter *skyp) override
Draw the object on the SkyMap skyp a pointer to the SkyPainter to use.
KStarsData is the backbone of KStars.
Definition kstarsdata.h:72
static KStars * Instance()
Definition kstars.h:123
SkyComponent represents an object on the sky map.
SkyComposite is a kind of container class for SkyComponent objects.
Draws things on the sky, without regard to backend.
Definition skypainter.h:40
The sky coordinates of a point in the sky.
Definition skypoint.h:45
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
static constexpr double PI
PI is a const static member; it's public so that it can be used anywhere, as long as dms....
Definition dms.h:385
const QString toHMSString(const bool machineReadable=false, const bool highPrecision=false) const
Definition dms.cpp:378
QString i18n(const char *text, const TYPE &arg...)
KIOCORE_EXPORT TransferJob * get(const QUrl &url, LoadType reload=NoReload, JobFlags flags=DefaultFlags)
KIOCORE_EXPORT QString dir(const QString &fileClass)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
void setText(const QString &text)
QItemSelectionModel * selectionModel() const const
char * data()
void activated(int index)
void clear()
QString filePath(const QString &fileName) const const
QChar separator()
void cancel()
bool isRunning() const const
void waitForFinished()
void setTitle(const QString &title)
iterator begin()
void clear()
iterator end()
iterator insert(const_iterator before, parameter_type value)
void push_back(parameter_type value)
void removeFirst()
qsizetype size() const const
void clear()
iterator end()
iterator find(const Key &key)
size_type size() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
bool disconnect(const QMetaObject::Connection &connection)
void setPlainText(const QString &text)
T * get() const const
QString arg(Args &&... args) const const
QString number(double n, char format, int precision)
QByteArray toLatin1() const const
QString trimmed() const const
QString join(QChar separator) const const
AlignLeft
UniqueConnection
ItemIsEditable
QTextStream & dec(QTextStream &stream)
void resizeColumnsToContents()
void setShowGrid(bool show)
void setWordWrap(bool on)
void cellChanged(int row, int column)
QWidget * cellWidget(int row, int column) const const
QTableWidgetItem * item(int row, int column) const const
void itemSelectionChanged()
void setCellWidget(int row, int column, QWidget *widget)
void setColumnCount(int columns)
void setHorizontalHeaderLabels(const QStringList &labels)
void setItem(int row, int column, QTableWidgetItem *item)
void setRowCount(int rows)
Qt::ItemFlags flags() const const
void setFlags(Qt::ItemFlags flags)
void setText(const QString &text)
void setTextAlignment(Qt::Alignment alignment)
QString text() const const
QFuture< T > run(Function function,...)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void start()
void stop()
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Apr 27 2024 22:13:28 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.