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

KDE's Doxygen guidelines are available online.