Kstars

imagingplanner.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 "imagingplanner.h"
8
9#include <kio_version.h>
10
11#include "artificialhorizoncomponent.h"
12#include "auxiliary/screencapture.h"
13#include "auxiliary/thememanager.h"
14#include "catalogscomponent.h"
15#include "constellationboundarylines.h"
16#include "dialogs/detaildialog.h"
17#include "dialogs/finddialog.h"
18// TODO: replace this. See comment above SchedulerUtils_setupJob().
19//#include "ekos/scheduler/schedulerutils.h"
20#include "ekos/scheduler/schedulerjob.h"
21
22// These are just for the debugging method checkTargets()
23#include "flagmanager.h"
24#include "flagcomponent.h"
25
26#include "nameresolver.h"
27#include "imageoverlaycomponent.h"
28#include "imagingplanneroptions.h"
29#include "kplotwidget.h"
30#include "kplotobject.h"
31#include "kplotaxis.h"
32#include "ksalmanac.h"
33#include "ksmoon.h"
34#include "ksnotification.h"
35#include <kspaths.h>
36#include "kstars.h"
37#include "ksuserdb.h"
38#include "kstarsdata.h"
39#include "fitsviewer/platesolve.h"
40#include "skymap.h"
41#include "skymapcomposite.h"
42
43#include <QDesktopServices>
44#include <QDialog>
45#include <QDir>
46#include <QFileDialog>
47#include <QImage>
48#include <QNetworkReply>
49#include <QPixmap>
50#include <QRegularExpression>
51#include <QSortFilterProxyModel>
52#include <QStandardItemModel>
53#include <QStringList>
54#include <QWidget>
55#include "zlib.h"
56
57#define DPRINTF if (false) fprintf
58
59// Data columns in the model.
60// Must agree with the header string near the start of initialize()
61// and the if/else-if test values in addCatalogItem().
62namespace
63{
64enum ColumnNames
65{
66 NAME_COLUMN = 0,
67 HOURS_COLUMN,
68 TYPE_COLUMN,
69 SIZE_COLUMN,
70 ALTITUDE_COLUMN,
71 MOON_COLUMN,
72 CONSTELLATION_COLUMN,
73 COORD_COLUMN,
74 FLAGS_COLUMN,
75 NOTES_COLUMN,
76 LAST_COLUMN
77};
78}
79
80// These could probably all be Qt::UserRole + 1
81#define TYPE_ROLE (Qt::UserRole + 1)
82#define HOURS_ROLE (Qt::UserRole + 2)
83#define SIZE_ROLE (Qt::UserRole + 3)
84#define ALTITUDE_ROLE (Qt::UserRole + 4)
85#define MOON_ROLE (Qt::UserRole + 5)
86#define FLAGS_ROLE (Qt::UserRole + 6)
87#define NOTES_ROLE (Qt::UserRole + 7)
88
89#define PICKED_BIT ImagingPlannerDBEntry::PickedBit
90#define IMAGED_BIT ImagingPlannerDBEntry::ImagedBit
91#define IGNORED_BIT ImagingPlannerDBEntry::IgnoredBit
92
93/**********************************************************
94TODO/Ideas:
95- Filter by size
96- Log at bottom
97- Imaging time constraint in hours calc
98- Move some or all of the filtering to menus in the column headers
99- Just use UserRole or UserRole+1 for the non-display roles.
100- Altitude graph has some replicated code with the scheduler
101- Weird timezone stuff when setting kstars to a timezone that's not the system's timezone.
102- Add a catalog name, and display it
103***********************************************************/
104
105namespace
106{
107
108QString capitalize(const QString &str)
109{
110 QString temp = str.toLower();
111 temp[0] = str[0].toUpper();
112 return temp;
113}
114
115// Checks the appropriate Options variable to see if the object-type
116// should be displayed.
117bool acceptType(SkyObject::TYPE type)
118{
119 switch (type)
120 {
121 case SkyObject::OPEN_CLUSTER:
122 return Options::imagingPlannerAcceptOpenCluster();
123 case SkyObject::GLOBULAR_CLUSTER:
124 return Options::imagingPlannerAcceptGlobularCluster();
125 case SkyObject::GASEOUS_NEBULA:
126 return Options::imagingPlannerAcceptNebula();
127 case SkyObject::PLANETARY_NEBULA:
128 return Options::imagingPlannerAcceptPlanetary();
129 case SkyObject::SUPERNOVA_REMNANT:
130 return Options::imagingPlannerAcceptSupernovaRemnant();
131 case SkyObject::GALAXY:
132 return Options::imagingPlannerAcceptGalaxy();
133 case SkyObject::GALAXY_CLUSTER:
134 return Options::imagingPlannerAcceptGalaxyCluster();
135 case SkyObject::DARK_NEBULA:
136 return Options::imagingPlannerAcceptDarkNebula();
137 default:
138 return Options::imagingPlannerAcceptOther();
139 }
140}
141
142bool getFlag(const QModelIndex &index, int bit, QAbstractItemModel *model)
143{
144 auto idx = index.siblingAtColumn(FLAGS_COLUMN);
145 const bool hasFlags = model->data(idx, FLAGS_ROLE).canConvert<int>();
146 if (!hasFlags)
147 return false;
148 const bool flag = model->data(idx, FLAGS_ROLE).toInt() & bit;
149 return flag;
150}
151
152void setFlag(const QModelIndex &index, int bit, QAbstractItemModel *model)
153{
154 auto idx = index.siblingAtColumn(FLAGS_COLUMN);
155 const bool hasFlags = model->data(idx, FLAGS_ROLE).canConvert<int>();
156 int currentFlags = 0;
157 if (hasFlags)
158 currentFlags = model->data(idx, FLAGS_ROLE).toInt();
159 QVariant val(currentFlags | bit);
160 model->setData(idx, val, FLAGS_ROLE);
161}
162
163void clearFlag(const QModelIndex &index, int bit, QAbstractItemModel *model)
164{
165 auto idx = index.siblingAtColumn(FLAGS_COLUMN);
166 const bool hasFlags = model->data(idx, FLAGS_ROLE).canConvert<int>();
167 if (!hasFlags)
168 return;
169 const int currentFlags = model->data(idx, FLAGS_ROLE).toInt();
170 QVariant val(currentFlags & ~bit);
171 model->setData(idx, val, FLAGS_ROLE);
172}
173
174QString flagString(int flags)
175{
176 QString str;
177 if (flags & IMAGED_BIT) str.append(i18n("Imaged"));
178 if (flags & PICKED_BIT)
179 {
180 if (str.size() != 0)
181 str.append(", ");
182 str.append(i18n("Picked"));
183 }
184 if (flags & IGNORED_BIT)
185 {
186 if (str.size() != 0)
187 str.append(", ");
188 str.append(i18n("Ignored"));
189 }
190 return str;
191}
192
193// The next 3 methods condense repeated code needed for the filtering checkboxes.
194void setupShowCallback(bool checked,
195 void (*showOption)(bool), void (*showNotOption)(bool),
196 void (*dontCareOption)(bool),
197 QCheckBox *showCheckbox, QCheckBox *showNotCheckbox,
198 QCheckBox *dontCareCheckbox)
199{
200 Q_UNUSED(showCheckbox);
201 if (checked)
202 {
203 showOption(true);
204 showNotOption(false);
205 dontCareOption(false);
206 showNotCheckbox->setChecked(false);
207 dontCareCheckbox->setChecked(false);
208 Options::self()->save();
209 }
210 else
211 {
212 showOption(false);
213 showNotOption(false);
214 dontCareOption(true);
215 showNotCheckbox->setChecked(false);
216 dontCareCheckbox->setChecked(true);
217 Options::self()->save();
218 }
219}
220
221void setupShowNotCallback(bool checked,
222 void (*showOption)(bool), void (*showNotOption)(bool), void (*dontCareOption)(bool),
223 QCheckBox *showCheckbox, QCheckBox *showNotCheckbox, QCheckBox *dontCareCheckbox)
224{
225 Q_UNUSED(showNotCheckbox);
226 if (checked)
227 {
228 showOption(false);
229 showNotOption(true);
230 dontCareOption(false);
231 showCheckbox->setChecked(false);
232 dontCareCheckbox->setChecked(false);
233 Options::self()->save();
234 }
235 else
236 {
237 showOption(false);
238 showNotOption(false);
239 dontCareOption(true);
240 showCheckbox->setChecked(false);
241 dontCareCheckbox->setChecked(true);
242 Options::self()->save();
243 }
244}
245
246void setupDontCareCallback(bool checked,
247 void (*showOption)(bool), void (*showNotOption)(bool), void (*dontCareOption)(bool),
248 QCheckBox *showCheckbox, QCheckBox *showNotCheckbox, QCheckBox *dontCareCheckbox)
249{
250 if (checked)
251 {
252 showOption(false);
253 showNotOption(false);
254 dontCareOption(true);
255 showCheckbox->setChecked(false);
256 showNotCheckbox->setChecked(false);
257 Options::self()->save();
258 }
259 else
260 {
261 // Yes, the user just set this to false, but
262 // there's no obvious way to tell what the user wants.
263 showOption(false);
264 showNotOption(false);
265 dontCareOption(true);
266 showCheckbox->setChecked(false);
267 showNotCheckbox->setChecked(false);
268 dontCareCheckbox->setChecked(true);
269 Options::self()->save();
270 }
271}
272
273// Find the nth url inside a QString. Returns an empty QString if none is found.
274QString findUrl(const QString &input, int nth = 1)
275{
276 // Got the RE by asking Google's AI!
277 QRegularExpression re("https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._"
278 "\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)");
279
280 re.setPatternOptions(QRegularExpression::MultilineOption |
283 auto match = re.match(input);
284 if (!match.hasMatch())
285 return QString();
286 else if (nth == 1)
287 return(match.captured(0));
288
289 QString inp = input;
290 while (--nth >= 1)
291 {
292 inp = inp.mid(match.capturedEnd());
293 match = re.match(inp);
294 if (!match.hasMatch())
295 return QString();
296 else if (nth == 1)
297 return (match.captured(0));
298 }
299 return QString();
300}
301
302// Make some guesses about possible input-name confusions.
303// Used in the table's search box and when reading the file of already-imaged objects.
304QString tweakNames(const QString &input)
305{
306 QString fixed = input;
307 if (fixed.startsWith("sharpless", Qt::CaseInsensitive))
309 if (fixed.startsWith("messier", Qt::CaseInsensitive))
311
312 fixed.replace(QRegularExpression("^(ngc|ic|abell|ldn|lbn|m|sh2|vdb)\\s*(\\d)",
314 if (fixed.startsWith("sh2-", Qt::CaseInsensitive))
315 fixed.replace(QRegularExpression("^sh2-\\s*(\\d)", QRegularExpression::CaseInsensitiveOption), "sh2 \\1");
316 return fixed;
317}
318
319// Return true if left side is less than right side (values are floats)
320// As opposed to the below, we want the non-reversed sort to be 9 -> 0.
321// This is used when sorting the table by floating point columns.
322double floatCompareFcn( const QModelIndex &left, const QModelIndex &right,
323 int column, int role)
324{
325 const double l = left.siblingAtColumn(column).data(role).toDouble();
326 const double r = right.siblingAtColumn(column).data(role).toDouble();
327 return l - r;
328}
329
330// Return true if left side is less than right side
331// Values can be simple strings or object names like "M 31" where the 2nd part is sorted arithmatically.
332// We want the non-reversed sort to be A -> Z and 0 -> 9, which is why all the returns have minus signs.
333// This is used when sorting the table by string columns.
334int stringCompareFcn( const QModelIndex &left, const QModelIndex &right, int column, int role)
335{
336 const QString l = left.siblingAtColumn(column).data(role).toString();
337 const QString r = right.siblingAtColumn(column).data(role).toString();
340
341 if (lList.size() == 0 || rList.size() == 0)
343
344 // Both sides have at least one item. If the first item is not the same,
345 // return the string compare value for those.
346 const int comp = QString::compare(lList[0], rList[0], Qt::CaseInsensitive);
347 if (comp != 0)
348 return -comp;
349
350 // Here we deal with standard object names, like comparing "M 100" and "M 33"
351 // I'm assuming here that our object names have spaces, as is standard in kstars.
352 if (lList.size() >= 2 && rList.size() >= 2)
353 {
354 int lInt = lList[1].toInt();
355 int rInt = rList[1].toInt();
356 // If they're not ints, then toInt returns 0.
357 // Not expecting negative numbers here.
358 if (lInt > 0 && rInt > 0)
359 return -(lInt - rInt);
360 }
361 // Go back to the original string compare
363}
364
365// TODO: This is copied from schedulerutils.h/cpp because for some reason the build failed when
366// including
367void SchedulerUtils_setupJob(Ekos::SchedulerJob &job, const QString &name, bool isLead, const QString &group,
368 const QString &train, const dms &ra, const dms &dec, double djd, double rotation, const QUrl &sequenceUrl,
369 const QUrl &fitsUrl, Ekos::StartupCondition startup, const QDateTime &startupTime, Ekos::CompletionCondition completion,
370 const QDateTime &completionTime, int completionRepeats, double minimumAltitude, double minimumMoonSeparation,
371 double maxMoonAltitude,
372 bool enforceTwilight, bool enforceArtificialHorizon, bool track, bool focus, bool align, bool guide)
373{
374 /* Configure or reconfigure the observation job */
375
376 job.setIsLead(isLead);
377 job.setOpticalTrain(train);
378 job.setPositionAngle(rotation);
379
380 if (isLead)
381 {
382 job.setName(name);
383 job.setGroup(group);
384 job.setLeadJob(nullptr);
385 // djd should be ut.djd
386 job.setTargetCoords(ra, dec, djd);
387 job.setFITSFile(fitsUrl);
388
389 // #1 Startup conditions
390 job.setStartupCondition(startup);
391 if (startup == Ekos::START_AT)
392 {
393 job.setStartupTime(startupTime);
394 }
395 /* Store the original startup condition */
396 job.setFileStartupCondition(job.getStartupCondition());
397 job.setStartAtTime(job.getStartupTime());
398
399 // #2 Constraints
400 job.setMinAltitude(minimumAltitude);
401 job.setMinMoonSeparation(minimumMoonSeparation);
402 job.setMaxMoonAltitude(maxMoonAltitude);
403
404 // Check enforce weather constraints
405 // twilight constraints
406 job.setEnforceTwilight(enforceTwilight);
407 job.setEnforceArtificialHorizon(enforceArtificialHorizon);
408
409 // Job steps
410 job.setStepPipeline(Ekos::SchedulerJob::USE_NONE);
411 if (track)
412 job.setStepPipeline(static_cast<Ekos::SchedulerJob::StepPipeline>(job.getStepPipeline() | Ekos::SchedulerJob::USE_TRACK));
413 if (focus)
414 job.setStepPipeline(static_cast<Ekos::SchedulerJob::StepPipeline>(job.getStepPipeline() | Ekos::SchedulerJob::USE_FOCUS));
415 if (align)
416 job.setStepPipeline(static_cast<Ekos::SchedulerJob::StepPipeline>(job.getStepPipeline() | Ekos::SchedulerJob::USE_ALIGN));
417 if (guide)
418 job.setStepPipeline(static_cast<Ekos::SchedulerJob::StepPipeline>(job.getStepPipeline() | Ekos::SchedulerJob::USE_GUIDE));
419
420 /* Store the original startup condition */
421 job.setFileStartupCondition(job.getStartupCondition());
422 job.setStartAtTime(job.getStartupTime());
423 }
424
425 /* Consider sequence file is new, and clear captured frames map */
426 job.setCapturedFramesMap(Ekos::CapturedFramesMap());
427 job.setSequenceFile(sequenceUrl);
428 job.setCompletionCondition(completion);
429 if (completion == Ekos::FINISH_AT)
430 job.setFinishAtTime(completionTime);
431 else if (completion == Ekos::FINISH_REPEAT)
432 {
433 job.setRepeatsRequired(completionRepeats);
434 job.setRepeatsRemaining(completionRepeats);
435 }
436
437 /* Reset job state to evaluate the changes */
438 job.reset();
439}
440
441// Sets up a SchedulerJob, used by getRunTimes to see when the target can be imaged.
442void setupJob(Ekos::SchedulerJob &job, const QString name, double minAltitude, double minMoonSeparation,
443 double maxMoonAltitude, dms ra, dms dec,
444 bool useArtificialHorizon)
445{
446 double djd = KStars::Instance()->data()->ut().djd();
447 double rotation = 0.0;
448 QString train = "";
449 QUrl sequenceURL; // is this needed?
450
451 // TODO: Hopefully go back to calling SchedulerUtils::setupJob()
452 //Ekos::SchedulerUtils::setupJob(job, name, true, "",
453 SchedulerUtils_setupJob(job, name, true, "",
454 train, ra, dec, djd,
455 rotation, sequenceURL, QUrl(),
456 Ekos::START_ASAP, QDateTime(),
457 Ekos::FINISH_LOOP, QDateTime(), 1,
458 minAltitude, minMoonSeparation, maxMoonAltitude,
459 true, useArtificialHorizon,
460 true, true, true, true);
461}
462
463// Computes the times when the given coordinates can be imaged on the date.
464void getRunTimes(const QDate &date, const GeoLocation &geo, double minAltitude, double minMoonSeparation,
465 double maxMoonAltitude, const dms &ra, const dms &dec, bool useArtificialHorizon, QVector<QDateTime> *jobStartTimes,
466 QVector<QDateTime> *jobEndTimes)
467{
468 jobStartTimes->clear();
469 jobEndTimes->clear();
470 constexpr int SCHEDULE_RESOLUTION_MINUTES = 10;
471 Ekos::SchedulerJob job;
472 setupJob(job, "temp", minAltitude, minMoonSeparation, maxMoonAltitude, ra, dec, useArtificialHorizon);
473
474 auto tz = QTimeZone(geo.TZ() * 3600);
475
476 // Find possible imaging times between noon and the next noon.
477 QDateTime startTime(QDateTime(date, QTime(12, 0, 1)));
478 QDateTime stopTime( QDateTime(date.addDays(1), QTime(12, 0, 1)));
479 startTime.setTimeZone(tz);
480 stopTime.setTimeZone(tz);
481
482 QString constraintReason;
483 int maxIters = 10;
484 while (--maxIters >= 0)
485 {
486 QDateTime s = job.getNextPossibleStartTime(startTime, SCHEDULE_RESOLUTION_MINUTES, false, stopTime);
487 if (!s.isValid())
488 return;
489 s.setTimeZone(tz);
490
491 QDateTime e = job.getNextEndTime(s, SCHEDULE_RESOLUTION_MINUTES, &constraintReason, stopTime);
492 if (!e.isValid())
493 return;
494 e.setTimeZone(tz);
495
496 jobStartTimes->push_back(s);
497 jobEndTimes->push_back(e);
498
499 if (e.secsTo(stopTime) < 600)
500 return;
501
502 startTime = e.addSecs(60);
503 startTime.setTimeZone(tz);
504 }
505}
506
507// Computes the times when the given catalog object can be imaged on the date.
508double getRunHours(const CatalogObject &object, const QDate &date, const GeoLocation &geo, double minAltitude,
509 double minMoonSeparation, double maxMoonAltitude, bool useArtificialHorizon)
510{
511 QVector<QDateTime> jobStartTimes, jobEndTimes;
512 getRunTimes(date, geo, minAltitude, minMoonSeparation, maxMoonAltitude, object.ra0(), object.dec0(), useArtificialHorizon,
513 &jobStartTimes,
514 &jobEndTimes);
515 if (jobStartTimes.size() == 0 || jobEndTimes.size() == 0)
516 return 0;
517 else
518 {
519 double totalHours = 0.0;
520 for (int i = 0; i < jobStartTimes.size(); ++i)
521 totalHours += jobStartTimes[i].secsTo(jobEndTimes[i]) * 1.0 / 3600.0;
522 return totalHours;
523 }
524}
525
526// Pack is needed to generate the Astrobin search URLs.
527// This implementation was inspired by
528// https://github.com/romixlab/qmsgpack/blob/master/src/private/pack_p.cpp
529// Returns the size it would have, or actually did, pack.
530int packString(const QString &input, quint8 *p, bool reallyPack)
531{
532 QByteArray str_data = input.toUtf8();
533 quint32 len = str_data.length();
534 const char *str = str_data.data();
535 constexpr bool compatibilityMode = false;
536 const quint8 *origP = p;
537 if (len <= 31)
538 {
539 if (reallyPack) *p = 0xa0 | len;
540 p++;
541 }
542 else if (len <= std::numeric_limits<quint8>::max() &&
543 compatibilityMode == false)
544 {
545 if (reallyPack) *p = 0xd9;
546 p++;
547 if (reallyPack) *p = len;
548 p++;
549 }
550 else if (len <= std::numeric_limits<quint16>::max())
551 {
552 if (reallyPack) *p = 0xda;
553 p++;
554 if (reallyPack)
555 {
556 quint16 val = len;
557 memcpy(p, &val, 2);
558 }
559 p += 2;
560 }
561 else return 0; // Bailing if the url is longer than 64K--shouldn't happen.
562
563 if (reallyPack) memcpy(p, str, len);
564 return (p - origP) + len;
565}
566
567QByteArray pack(const QString &input)
568{
569 QVector<QByteArray> user_data;
570 // first run, calculate size
571 int size = packString(input, nullptr, false);
572 QByteArray arr;
573 arr.resize(size);
574 // second run, pack it
575 packString(input, reinterpret_cast<quint8*>(arr.data()), true);
576 return arr;
577}
578
579
580// Turn the first space to a dash and remove the rest of the spaces
581QString replaceSpaceWith(const QString &name, const QString &replacement)
582{
583 QString result = name;
584
585 // Replace the first space with a dash
586 QRegularExpression firstSpaceRegex(QStringLiteral(" "));
587 QRegularExpressionMatch firstMatch = firstSpaceRegex.match(result);
588 if (firstMatch.hasMatch())
589 {
590 result.replace(firstMatch.capturedStart(), 1, replacement);
591 // Remove all remaining spaces
592 QRegularExpression remainingSpacesRegex(QStringLiteral(" "));
593 result.replace(remainingSpacesRegex, QStringLiteral(""));
594 }
595 return result;
596}
597
598// This function is just used in catalog development. Output to stderr.
599bool downsampleImageFiles(const QString &baseDir, int maxHeight)
600{
601 QString fn = "Test.txt";
602 QFile file( fn );
603 if ( file.open(QIODevice::ReadWrite) )
604 {
605 QTextStream stream( &file );
606 stream << "hello" << Qt::endl;
607 }
608 file.close();
609
610 const QString subDir = "REDUCED";
611 QDir directory(baseDir);
612 if (!directory.exists())
613 {
614 fprintf(stderr, "downsampleImageFiles: Base directory doesn't exist\n");
615 return false;
616 }
617 QDir outDir = QDir(directory.absolutePath().append(QDir::separator()).append(subDir));
618 if (!outDir.exists())
619 {
620 if (!outDir.mkpath("."))
621 {
622 fprintf(stderr, "downsampleImageFiles: Failed making the output directory\n");
623 return false;
624 }
625 }
626
627 int numSaved = 0;
628 QStringList files = directory.entryList(QStringList() << "*.jpg" << "*.JPG" << "*.png", QDir::Files);
629 foreach (QString filename, files)
630 {
631 QString fullPath = QString("%1%2%3").arg(baseDir).arg(QDir::separator()).arg(filename);
632 QImage img(fullPath);
633 QImage scaledImg;
634 if (img.height() > maxHeight)
635 scaledImg = img.scaledToHeight(maxHeight, Qt::SmoothTransformation);
636 else
637 scaledImg = img;
638
639 QString writeFilename = outDir.absolutePath().append(QDir::separator()).append(filename);
640 QFileInfo info(writeFilename);
641 QString jpgFilename = info.path() + QDir::separator() + info.completeBaseName() + ".jpg";
642
643 if (!scaledImg.save(jpgFilename, "JPG"))
644 fprintf(stderr, "downsampleImageFiles: Failed saving \"%s\"\n", writeFilename.toLatin1().data());
645 else
646 {
647 numSaved++;
648 fprintf(stderr, "downsampleImageFiles: saved \"%s\"\n", writeFilename.toLatin1().data());
649 }
650 }
651 fprintf(stderr, "downsampleImageFiles: Wrote %d files\n", numSaved);
652 return true;
653}
654
655// Seaches for all the occurances of the byte cc in the QByteArray, and replaces each
656// of them with the sequence of bytes in the QByteArray substitute.
657// There's probably a QByteArray method that does this.
658void replaceByteArrayChars(QByteArray &bInput, char cc, const QByteArray &substitute)
659{
660 while (true)
661 {
662 const int len = bInput.size();
663 int pos = -1;
664 for (int i = 0; i < len; ++i)
665 {
666 if (bInput[i] == cc)
667 {
668 pos = i;
669 break;
670 }
671 }
672 if (pos < 0)
673 break;
674 bInput.replace(pos, 1, substitute);
675 }
676}
677
678// Look in the app directories in case a .png or .jpg file exists that ends
679// with the object name.
680QString findObjectImage(const QString &name)
681{
682 // Remove any spaces, but "sh2 " becomes "sh2-"
683 auto massagedName = name;
684 if (massagedName.startsWith("sh2 ", Qt::CaseInsensitive))
685 massagedName = massagedName.replace(0, 4, "sh2-");
686 massagedName = massagedName.replace(' ', "");
687
688 QDir dir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
689 QStringList nameFilter;
690 nameFilter << QString("*%1.png").arg(massagedName) << QString("*%1.jpg").arg(massagedName);
691 QFileInfoList files = dir.entryInfoList(nameFilter, QDir::Files);
692 if (files.size() > 0)
693 return files[0].absoluteFilePath();
694
695 QFileInfoList subDirs = dir.entryInfoList(nameFilter, QDir::NoDotAndDotDot | QDir::AllDirs);
696 for (int i = 0; i < subDirs.size(); i++)
697 {
698 QDir subDir(subDirs[i].absoluteFilePath());
699 QFileInfoList files = subDir.entryInfoList(nameFilter, QDir::NoDotAndDotDot | QDir::Files);
700 if (files.size() > 0)
701 return files[0].absoluteFilePath();
702 }
703 return QString();
704}
705
706QString creativeCommonsString(const QString &astrobinAbbrev)
707{
708 if (astrobinAbbrev == "ACC")
709 return "CC-BY";
710 else if (astrobinAbbrev == "ASACC")
711 return "CC-BY-SA";
712 else if (astrobinAbbrev == "ANCCC")
713 return "CC-BY-NC";
714 else if (astrobinAbbrev == "ANCSACC")
715 return "CC-BY-SA-NC";
716 else return "";
717}
718
719QString creativeCommonsTooltipString(const QString &astrobinAbbrev)
720{
721 if (astrobinAbbrev == "ACC")
722 return "Atribution Creative Commons";
723 else if (astrobinAbbrev == "ASACC")
724 return "Atribution Share-Alike Creative Commons";
725 else if (astrobinAbbrev == "ANCCC")
726 return "Atribution Non-Commercial Creative Commons";
727 else if (astrobinAbbrev == "ANCSACC")
728 return "Atribution Non-Commercial Share-Alike Creative Commons";
729 else return "";
730}
731
732QString shortCoordString(const dms &ra, const dms &dec)
733{
734 return QString("%1h%2' %3%4°%5'").arg(ra.hour()).arg(ra.minute())
735 .arg(dec.Degrees() < 0 ? "-" : "").arg(abs(dec.degree())).arg(abs(dec.arcmin()));
736}
737
738double getAltitude(GeoLocation *geo, SkyPoint &p, const QDateTime &time)
739{
740 auto ut2 = geo->LTtoUT(KStarsDateTime(time));
741 CachingDms LST = geo->GSTtoLST(ut2.gst());
742 p.EquatorialToHorizontal(&LST, geo->lat());
743 return p.alt().Degrees();
744}
745
746double getMaxAltitude(const KSAlmanac &ksal, const QDate &date, GeoLocation *geo, const SkyObject &object,
747 double hoursAfterDusk = 0, double hoursBeforeDawn = 0)
748{
749 auto tz = QTimeZone(geo->TZ() * 3600);
750 KStarsDateTime midnight = KStarsDateTime(date.addDays(1), QTime(0, 1));
751 midnight.setTimeZone(tz);
752
753 QDateTime dawn = midnight.addSecs(24 * 3600 * ksal.getDawnAstronomicalTwilight());
754 dawn.setTimeZone(tz);
755 QDateTime dusk = midnight.addSecs(24 * 3600 * ksal.getDuskAstronomicalTwilight());
756 dusk.setTimeZone(tz);
757
758 QDateTime start = dusk.addSecs(hoursAfterDusk * 3600);
759 start.setTimeZone(tz);
760
761 auto end = dawn.addSecs(-hoursBeforeDawn * 3600);
762 end.setTimeZone(tz);
763
764 SkyPoint coords = object;
765 double maxAlt = -90;
766 auto t = start;
767 t.setTimeZone(tz);
768 QDateTime maxTime = t;
769
770 while (t.secsTo(end) > 0)
771 {
772 double alt = getAltitude(geo, coords, t);
773 if (alt > maxAlt)
774 {
775 maxAlt = alt;
776 maxTime = t;
777 }
778 t = t.addSecs(60 * 20);
779 }
780 return maxAlt;
781}
782
783} // namespace
784
785CatalogFilter::CatalogFilter(QObject* parent) : QSortFilterProxyModel(parent)
786{
787 m_SortColumn = HOURS_COLUMN;
788}
789
790// This method decides whether a row is shown in the object table.
791bool CatalogFilter::filterAcceptsRow(int row, const QModelIndex &parent) const
792{
793 const QModelIndex typeIndex = sourceModel()->index(row, TYPE_COLUMN, parent);
794 const SkyObject::TYPE type = static_cast<SkyObject::TYPE>(sourceModel()->data(typeIndex, TYPE_ROLE).toInt());
795 if (!acceptType(type)) return false;
796
797 const QModelIndex hoursIndex = sourceModel()->index(row, HOURS_COLUMN, parent);
798 const bool hasEnoughHours = sourceModel()->data(hoursIndex, Qt::DisplayRole).toDouble() >= m_MinHours;
799 if (!hasEnoughHours) return false;
800
801 const QModelIndex flagsIndex = sourceModel()->index(row, FLAGS_COLUMN, parent);
802
803 const bool isImaged = sourceModel()->data(flagsIndex, FLAGS_ROLE).toInt() & IMAGED_BIT;
804 const bool passesImagedConstraints = !m_ImagedConstraintsEnabled || (isImaged == m_ImagedRequired);
805 if (!passesImagedConstraints) return false;
806
807 const bool isIgnored = sourceModel()->data(flagsIndex, FLAGS_ROLE).toInt() & IGNORED_BIT;
808 const bool passesIgnoredConstraints = !m_IgnoredConstraintsEnabled || (isIgnored == m_IgnoredRequired);
809 if (!passesIgnoredConstraints) return false;
810
811 const bool isPicked = sourceModel()->data(flagsIndex, FLAGS_ROLE).toInt() & PICKED_BIT;
812 const bool passesPickedConstraints = !m_PickedConstraintsEnabled || (isPicked == m_PickedRequired);
813 if (!passesPickedConstraints) return false;
814
815 // keyword constraint is inactive without a keyword.
816 if (m_Keyword.isEmpty() || !m_KeywordConstraintsEnabled) return true;
817 const QModelIndex notesIndex = sourceModel()->index(row, NOTES_COLUMN, parent);
818 const QString notes = sourceModel()->data(notesIndex, NOTES_ROLE).toString();
819
820 const bool REMatches = m_KeywordRE.match(notes).hasMatch();
821 return (m_KeywordRequired == REMatches);
822}
823
824void CatalogFilter::setMinHours(double hours)
825{
826 m_MinHours = hours;
827}
828
829void CatalogFilter::setImagedConstraints(bool enabled, bool required)
830{
831 m_ImagedConstraintsEnabled = enabled;
832 m_ImagedRequired = required;
833}
834
835void CatalogFilter::setPickedConstraints(bool enabled, bool required)
836{
837 m_PickedConstraintsEnabled = enabled;
838 m_PickedRequired = required;
839}
840
841void CatalogFilter::setIgnoredConstraints(bool enabled, bool required)
842{
843 m_IgnoredConstraintsEnabled = enabled;
844 m_IgnoredRequired = required;
845}
846
847void CatalogFilter::setKeywordConstraints(bool enabled, bool required, const QString &keyword)
848{
849 m_KeywordConstraintsEnabled = enabled;
850 m_KeywordRequired = required;
851 m_Keyword = keyword;
852 m_KeywordRE = QRegularExpression(keyword);
853}
854
855void CatalogFilter::setSortColumn(int column)
856{
857 if (column == m_SortColumn)
858 m_ReverseSort = !m_ReverseSort;
859 m_SortColumn = column;
860}
861
862// The main function used when sorting the table by a column (which is stored in m_SortColumn).
863// The secondary-sort columns are hard-coded below (and commented) for each primary-sort column.
864// When reversing the sort, we only reverse the primary column. The secondary sort column's
865// sort is not reversed.
866bool CatalogFilter::lessThan ( const QModelIndex &left, const QModelIndex &right) const
867{
868 double compareVal = 0;
869 switch(m_SortColumn)
870 {
871 case NAME_COLUMN:
872 // Name. There shouldn't be any ties, so no secondary sort.
873 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
874 if (m_ReverseSort) compareVal = -compareVal;
875 break;
876 case TYPE_COLUMN:
877 // Type then hours then name. There can be plenty of ties in type and hours.
878 compareVal = stringCompareFcn(left, right, TYPE_COLUMN, Qt::DisplayRole);
879 if (m_ReverseSort) compareVal = -compareVal;
880 if (compareVal != 0) break;
881 compareVal = floatCompareFcn(left, right, HOURS_COLUMN, HOURS_ROLE);
882 if (compareVal != 0) break;
883 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
884 break;
885 case SIZE_COLUMN:
886 // Size then hours then name. Size mostly has ties when size is unknown (== 0).
887 compareVal = floatCompareFcn(left, right, SIZE_COLUMN, SIZE_ROLE);
888 if (m_ReverseSort) compareVal = -compareVal;
889 if (compareVal != 0) break;
890 compareVal = floatCompareFcn(left, right, HOURS_COLUMN, HOURS_ROLE);
891 if (compareVal != 0) break;
892 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
893 break;
894 case ALTITUDE_COLUMN:
895 // Altitude then hours then name. Probably altitude rarely ties.
896 compareVal = floatCompareFcn(left, right, ALTITUDE_COLUMN, ALTITUDE_ROLE);
897 if (m_ReverseSort) compareVal = -compareVal;
898 if (compareVal != 0) break;
899 compareVal = floatCompareFcn(left, right, HOURS_COLUMN, HOURS_ROLE);
900 if (compareVal != 0) break;
901 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
902 break;
903 case MOON_COLUMN:
904 // Moon then hours then name. Probably moon rarely ties.
905 compareVal = floatCompareFcn(left, right, MOON_COLUMN, MOON_ROLE);
906 if (m_ReverseSort) compareVal = -compareVal;
907 if (compareVal != 0) break;
908 compareVal = floatCompareFcn(left, right, HOURS_COLUMN, HOURS_ROLE);
909 if (compareVal != 0) break;
910 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
911 break;
912 case CONSTELLATION_COLUMN:
913 // Constellation, then hours, then name.
914 compareVal = stringCompareFcn(left, right, CONSTELLATION_COLUMN, Qt::DisplayRole);
915 if (m_ReverseSort) compareVal = -compareVal;
916 if (compareVal != 0) break;
917 compareVal = floatCompareFcn(left, right, HOURS_COLUMN, HOURS_ROLE);
918 if (compareVal != 0) break;
919 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
920 break;
921 case COORD_COLUMN:
922 // Coordinate string is a weird thing to sort. Anyway, Coord, then hours, then name.
923 compareVal = stringCompareFcn(left, right, COORD_COLUMN, Qt::DisplayRole);
924 if (m_ReverseSort) compareVal = -compareVal;
925 if (compareVal != 0) break;
926 compareVal = floatCompareFcn(left, right, HOURS_COLUMN, HOURS_ROLE);
927 if (compareVal != 0) break;
928 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
929 break;
930 case HOURS_COLUMN:
931 default:
932 // In all other conditions (e.g. HOURS) sort by hours. Secondary sort is name.
933 compareVal = floatCompareFcn(left, right, HOURS_COLUMN, HOURS_ROLE);
934 if (m_ReverseSort) compareVal = -compareVal;
935 if (compareVal != 0) break;
936 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
937 break;
938 }
939 return compareVal < 0;
940}
941
942ImagingPlannerUI::ImagingPlannerUI(QWidget * p) : QFrame(p)
943{
944 setupUi(this);
945 setupIcons();
946}
947
948// Icons can't just be set up in the .ui file for Mac, so explicitly doing it here.
949void ImagingPlannerUI::setupIcons()
950{
951 SearchB->setIcon(QIcon::fromTheme("edit-find"));
952 backOneDay->setIcon(QIcon::fromTheme("arrow-left"));
953 forwardOneDay->setIcon(QIcon::fromTheme("arrow-right"));
954 optionsButton->setIcon(QIcon::fromTheme("open-menu-symbolic"));
955 helpButton->setIcon(QIcon::fromTheme("help-about"));
956 userNotesEditButton->setIcon(QIcon::fromTheme("document-edit"));
957 userNotesDoneButton->setIcon(QIcon::fromTheme("checkmark"));
958 userNotesOpenLink->setIcon(QIcon::fromTheme("link"));
959 userNotesOpenLink2->setIcon(QIcon::fromTheme("link"));
960 userNotesOpenLink3->setIcon(QIcon::fromTheme("link"));
961 hideAltitudeGraphB->setIcon(QIcon::fromTheme("window-minimize"));
962 showAltitudeGraphB->setIcon(QIcon::fromTheme("window-maximize"));
963 hideAstrobinDetailsButton->setIcon(QIcon::fromTheme("window-minimize"));
964 showAstrobinDetailsButton->setIcon(QIcon::fromTheme("window-maximize"));
965 hideFilterTypesButton->setIcon(QIcon::fromTheme("window-minimize"));
966 showFilterTypesButton->setIcon(QIcon::fromTheme("window-maximize"));
967 hideImageButton->setIcon(QIcon::fromTheme("window-minimize"));
968 showImageButton->setIcon(QIcon::fromTheme("window-maximize"));
969}
970
971GeoLocation *ImagingPlanner::getGeo()
972{
973 return KStarsData::Instance()->geo();
974}
975
976QDate ImagingPlanner::getDate() const
977{
978 return ui->DateEdit->date();
979}
980
981ImagingPlanner::ImagingPlanner() : QDialog(nullptr), m_networkManager(this), m_manager{ CatalogsDB::dso_db_path() }
982{
983 ui = new ImagingPlannerUI(this);
984
985 // Seem to need these or when the user stretches the window width, the widgets
986 // don't take advantage of the width.
987 QVBoxLayout *mainLayout = new QVBoxLayout;
988 mainLayout->addWidget(ui);
989 setLayout(mainLayout);
990
991 setWindowTitle(i18nc("@title:window", "Imaging Planner"));
992 setFocusPolicy(Qt::StrongFocus);
993
994 if (Options::imagingPlannerIndependentWindow())
995 {
996 // Removing the Dialog bit (but neet to add back the window bit) allows
997 // the window to go below other windows.
998 setParent(nullptr, (windowFlags() & ~Qt::Dialog) | Qt::Window);
999 }
1000 else
1001 {
1002#ifdef Q_OS_MACOS
1003 setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
1004#endif
1005 }
1006 initialize();
1007}
1008
1009// Sets up the hide/show buttons that minimize/maximize the plot/search/filters/image sections.
1010void ImagingPlanner::setupHideButtons(bool(*option)(), void(*setOption)(bool),
1011 QPushButton * hideButton, QPushButton * showButton,
1012 QFrame * widget, QFrame * hiddenWidget)
1013{
1014 hiddenWidget->setVisible(option());
1015 widget->setVisible(!option());
1016
1017 connect(hideButton, &QAbstractButton::clicked, this, [this, setOption, hiddenWidget, widget]()
1018 {
1019 setOption(true);
1020 Options::self()->save();
1021 hiddenWidget->setVisible(true);
1022 widget->setVisible(false);
1023 focusOnTable();
1024 adjustWindowSize();
1025 });
1026 connect(showButton, &QAbstractButton::clicked, this, [this, setOption, hiddenWidget, widget]()
1027 {
1028 setOption(false);
1029 Options::self()->save();
1030 hiddenWidget->setVisible(false);
1031 widget->setVisible(true);
1032 focusOnTable();
1033 });
1034}
1035
1036// Gives the keyboard focus to the CatalogView object table.
1037void ImagingPlanner::focusOnTable()
1038{
1039 ui->CatalogView->setFocus();
1040}
1041
1042void ImagingPlanner::adjustWindowSize()
1043{
1044 const int keepWidth = width();
1045 adjustSize();
1046 const int newHeight = height();
1047 resize(keepWidth, newHeight);
1048}
1049
1050// Sets up the galaxy/nebula/... filter buttons.
1051void ImagingPlanner::setupFilterButton(QCheckBox * checkbox, bool(*option)(), void(*setOption)(bool))
1052{
1053 checkbox->setChecked(option());
1054 connect(checkbox, &QCheckBox::toggled, [this, setOption](bool checked)
1055 {
1056 setOption(checked);
1057 Options::self()->save();
1058 m_CatalogSortModel->invalidate();
1059 updateDisplays();
1060 ui->CatalogView->resizeColumnsToContents();
1061 focusOnTable();
1062 });
1063}
1064
1065// Sets up the picked/imaged/ignored/keyword buttons
1066void ImagingPlanner::setupFilter2Buttons(
1067 QCheckBox * yes, QCheckBox * no, QCheckBox * dontCare,
1068 bool(*yesOption)(), bool(*noOption)(), bool(*dontCareOption)(),
1069 void(*setYesOption)(bool), void(*setNoOption)(bool), void(*setDontCareOption)(bool))
1070{
1071
1072 // Use clicked, not toggled to avoid callbacks when the state is changed programatically.
1073 connect(yes, &QCheckBox::clicked, [this, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare](bool checked)
1074 {
1075 setupShowCallback(checked, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare);
1076 updateSortConstraints();
1077 m_CatalogSortModel->invalidate();
1078 ui->CatalogView->resizeColumnsToContents();
1079 updateDisplays();
1080 focusOnTable();
1081 });
1082 connect(no, &QCheckBox::clicked, [this, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare](bool checked)
1083 {
1084 setupShowNotCallback(checked, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare);
1085 updateSortConstraints();
1086 m_CatalogSortModel->invalidate();
1087 ui->CatalogView->resizeColumnsToContents();
1088 updateDisplays();
1089 focusOnTable();
1090 });
1091 connect(dontCare, &QCheckBox::clicked, [this, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare](bool checked)
1092 {
1093 setupDontCareCallback(checked, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare);
1094 updateSortConstraints();
1095 m_CatalogSortModel->invalidate();
1096 ui->CatalogView->resizeColumnsToContents();
1097 updateDisplays();
1098 focusOnTable();
1099 });
1100
1101 yes->setChecked(yesOption());
1102 no->setChecked(noOption());
1103 dontCare->setChecked(dontCareOption());
1104}
1105
1106// Updates the QSortFilterProxyModel with new picked/imaged/ignore settings.
1107void ImagingPlanner::updateSortConstraints()
1108{
1109 m_CatalogSortModel->setPickedConstraints(!ui->dontCarePickedCB->isChecked(),
1110 ui->pickedCB->isChecked());
1111 m_CatalogSortModel->setImagedConstraints(!ui->dontCareImagedCB->isChecked(),
1112 ui->imagedCB->isChecked());
1113 m_CatalogSortModel->setIgnoredConstraints(!ui->dontCareIgnoredCB->isChecked(),
1114 ui->ignoredCB->isChecked());
1115 m_CatalogSortModel->setKeywordConstraints(!ui->dontCareKeywordCB->isChecked(),
1116 ui->keywordCB->isChecked(), ui->keywordEdit->toPlainText().trimmed());
1117}
1118
1119// Called once, at the first viewing of the tool, to initalize all the widgets.
1120void ImagingPlanner::initialize()
1121{
1122 if (KStarsData::Instance() == nullptr)
1123 {
1124 QTimer::singleShot(200, this, &ImagingPlanner::initialize);
1125 return;
1126 }
1127
1128 // Connects the threaded catalog loader to the UI.
1129 connect(this, &ImagingPlanner::popupSorry, this, &ImagingPlanner::sorry);
1130
1131 // Setup the Table Views
1132 m_CatalogModel = new QStandardItemModel(0, LAST_COLUMN);
1133
1134 // Setup the labels and tooltips for the header row of the table.
1135 m_CatalogModel->setHorizontalHeaderLabels(
1136 QStringList() << i18n("Name") << i18n("Hours") << i18n("Type") << i18n("Size") << i18n("Alt") << i18n("Moon") <<
1137 i18n("Const") << i18n("Coord"));
1138 m_CatalogModel->horizontalHeaderItem(NAME_COLUMN)->setToolTip(
1139 i18n("Object Name--click header to sort ascending/descending."));
1140 m_CatalogModel->horizontalHeaderItem(
1141 HOURS_COLUMN)->setToolTip(i18n("Number of hours the object can be imaged--click header to sort ascending/descending."));
1142 m_CatalogModel->horizontalHeaderItem(TYPE_COLUMN)->setToolTip(
1143 i18n("Object Type--click header to sort ascending/descending."));
1144 m_CatalogModel->horizontalHeaderItem(
1145 SIZE_COLUMN)->setToolTip(i18n("Maximum object dimension (arcmin)--click header to sort ascending/descending."));
1146 m_CatalogModel->horizontalHeaderItem(
1147 ALTITUDE_COLUMN)->setToolTip(i18n("Maximum altitude--click header to sort ascending/descending."));
1148 m_CatalogModel->horizontalHeaderItem(
1149 MOON_COLUMN)->setToolTip(i18n("Moon angular separation at midnight--click header to sort ascending/descending."));
1150 m_CatalogModel->horizontalHeaderItem(
1151 CONSTELLATION_COLUMN)->setToolTip(i18n("Constellation--click header to sort ascending/descending."));
1152 m_CatalogModel->horizontalHeaderItem(
1153 COORD_COLUMN)->setToolTip(i18n("RA/DEC coordinates--click header to sort ascending/descending."));
1154
1155 m_CatalogSortModel = new CatalogFilter(this);
1156 m_CatalogSortModel->setSortCaseSensitivity(Qt::CaseInsensitive);
1157 m_CatalogSortModel->setSourceModel(m_CatalogModel.data());
1158 m_CatalogSortModel->setDynamicSortFilter(true);
1159
1160 ui->CatalogView->setModel(m_CatalogSortModel.data());
1161 ui->CatalogView->setSortingEnabled(false); // We explicitly control the clicking on headers.
1162 ui->CatalogView->horizontalHeader()->setStretchLastSection(false);
1163 ui->CatalogView->resizeColumnsToContents();
1164 ui->CatalogView->verticalHeader()->setVisible(false); // Remove the row-number display.
1165 ui->CatalogView->setColumnHidden(FLAGS_COLUMN, true);
1166
1167 connect(ui->CatalogView->selectionModel(), &QItemSelectionModel::selectionChanged,
1168 this, &ImagingPlanner::selectionChanged);
1169
1170 // Initialize the date to KStars' date.
1171 if (getGeo())
1172 {
1173 auto utc = KStarsData::Instance()->clock()->utc();
1174 auto localTime = getGeo()->UTtoLT(utc);
1175 ui->DateEdit->setDate(localTime.date());
1176 updateMoon();
1177 }
1178
1179 setStatus("");
1180
1181 setupHideButtons(&Options::imagingPlannerHideAltitudeGraph, &Options::setImagingPlannerHideAltitudeGraph,
1182 ui->hideAltitudeGraphB, ui->showAltitudeGraphB,
1183 ui->AltitudeGraphFrame, ui->HiddenAltitudeGraphFrame);
1184
1185 // Date buttons
1186 connect(ui->backOneDay, &QPushButton::clicked, this, &ImagingPlanner::moveBackOneDay);
1187 connect(ui->forwardOneDay, &QPushButton::clicked, this, &ImagingPlanner::moveForwardOneDay);
1188 connect(ui->DateEdit, &QDateTimeEdit::dateChanged, this, [this]()
1189 {
1190 QString selection = currentObjectName();
1191 updateMoon();
1192 recompute();
1193 updateDisplays();
1194 scrollToName(selection);
1195 });
1196
1197 // Setup the section with Web search and Astrobin search details.
1198
1199 // Setup Web Search buttons
1200 connect(ui->astrobinButton, &QPushButton::clicked, this, &ImagingPlanner::searchAstrobin);
1201 connect(ui->astrobinButton2, &QPushButton::clicked, this, &ImagingPlanner::searchAstrobin);
1202 connect(ui->searchWikipedia, &QPushButton::clicked, this, &ImagingPlanner::searchWikipedia);
1203 connect(ui->searchWikipedia2, &QPushButton::clicked, this, &ImagingPlanner::searchWikipedia);
1204 connect(ui->searchSpecialWebPageImages, &QPushButton::clicked, this, &ImagingPlanner::searchSpecialWebPageImages);
1205 connect(ui->searchSpecialWebPageImages2, &QPushButton::clicked, this, &ImagingPlanner::searchSpecialWebPageImages);
1206 connect(ui->searchSimbad, &QPushButton::clicked, this, &ImagingPlanner::searchSimbad);
1207 connect(ui->searchSimbad2, &QPushButton::clicked, this, &ImagingPlanner::searchSimbad);
1208
1209 // These buttons are in the "hidden" catalog development section
1210 connect(ui->DevelCheckTargetsButton, &QPushButton::clicked, this, &ImagingPlanner::checkTargets);
1211 connect(ui->DevelCheckCatalogButton, &QPushButton::clicked, this, [this]()
1212 {
1213 checkTargets(true);
1214 });
1215 connect(ui->DevelCheckTargetsNextButton, &QPushButton::clicked, this, &ImagingPlanner::checkTargets2);
1216 connect(ui->DevelCheckTargetsPrevButton, &QPushButton::clicked, this, [this]()
1217 {
1218 checkTargets2(true);
1219 });
1220 connect(ui->DevelDownsampleButton, &QPushButton::clicked, this, [this]()
1221 {
1222 QString dir = QFileDialog::getExistingDirectory(this, tr("Downsample Directory"));
1223 if (!dir.isEmpty())
1224 {
1225 if (downsampleImageFiles(dir, 300))
1226 fprintf(stderr, "downsampling succeeded\n");
1227 else
1228 fprintf(stderr, "downsampling failed\n");
1229 }
1230 });
1231
1232 // Always start with hiding the details.
1233 Options::setImagingPlannerHideAstrobinDetails(true);
1234 setupHideButtons(&Options::imagingPlannerHideAstrobinDetails, &Options::setImagingPlannerHideAstrobinDetails,
1235 ui->hideAstrobinDetailsButton, ui->showAstrobinDetailsButton,
1236 ui->AstrobinSearchFrame, ui->HiddenAstrobinSearchFrame);
1237 ui->AstrobinAward->setChecked(Options::astrobinAward());
1238 connect(ui->AstrobinAward, &QAbstractButton::clicked, [this](bool checked)
1239 {
1240 Options::setAstrobinAward(checked);
1241 Options::self()->save();
1242 focusOnTable();
1243 });
1244 ui->AstrobinMinRadius->setValue(Options::astrobinMinRadius());
1245 connect(ui->AstrobinMinRadius, &QDoubleSpinBox::editingFinished, [this]()
1246 {
1247 Options::setAstrobinMinRadius(ui->AstrobinMinRadius->value());
1248 Options::self()->save();
1249 focusOnTable();
1250 });
1251 ui->AstrobinMaxRadius->setValue(Options::astrobinMaxRadius());
1252 connect(ui->AstrobinMaxRadius, &QDoubleSpinBox::editingFinished, [this]()
1253 {
1254 Options::setAstrobinMaxRadius(ui->AstrobinMaxRadius->value());
1255 Options::self()->save();
1256 focusOnTable();
1257 });
1258
1259 // Initialize image and catalog section
1260 m_NoImagePixmap =
1261 QPixmap(":/images/noimage.png").scaled(ui->ImagePreview->width(), ui->ImagePreview->height(), Qt::KeepAspectRatio,
1263 setDefaultImage();
1264 connect(ui->LoadCatalogButton, &QPushButton::clicked, this, &ImagingPlanner::loadCatalogViaMenu);
1265 connect(ui->LoadCatalogButton2, &QPushButton::clicked, this, &ImagingPlanner::loadCatalogViaMenu);
1266 setupHideButtons(&Options::imagingPlannerHideImage, &Options::setImagingPlannerHideImage,
1267 ui->hideImageButton, ui->showImageButton,
1268 ui->ImageFrame, ui->HiddenImageFrame);
1269
1270 // Initialize filter section
1271 Options::setImagingPlannerHideFilters(true);
1272 setupHideButtons(&Options::imagingPlannerHideFilters, &Options::setImagingPlannerHideFilters,
1273 ui->hideFilterTypesButton, ui->showFilterTypesButton,
1274 ui->FilterTypesFrame, ui->HiddenFilterTypesFrame);
1275 setupFilterButton(ui->OpenClusterCB, &Options::imagingPlannerAcceptOpenCluster,
1276 &Options::setImagingPlannerAcceptOpenCluster);
1277 setupFilterButton(ui->NebulaCB, &Options::imagingPlannerAcceptNebula, &Options::setImagingPlannerAcceptNebula);
1278 setupFilterButton(ui->GlobularClusterCB, &Options::imagingPlannerAcceptGlobularCluster,
1279 &Options::setImagingPlannerAcceptGlobularCluster);
1280 setupFilterButton(ui->PlanetaryCB, &Options::imagingPlannerAcceptPlanetary, &Options::setImagingPlannerAcceptPlanetary);
1281 setupFilterButton(ui->SupernovaRemnantCB, &Options::imagingPlannerAcceptSupernovaRemnant,
1282 &Options::setImagingPlannerAcceptSupernovaRemnant);
1283 setupFilterButton(ui->GalaxyCB, &Options::imagingPlannerAcceptGalaxy, &Options::setImagingPlannerAcceptGalaxy);
1284 setupFilterButton(ui->GalaxyClusterCB, &Options::imagingPlannerAcceptGalaxyCluster,
1285 &Options::setImagingPlannerAcceptGalaxyCluster);
1286 setupFilterButton(ui->DarkNebulaCB, &Options::imagingPlannerAcceptDarkNebula, &Options::setImagingPlannerAcceptDarkNebula);
1287 setupFilterButton(ui->OtherCB, &Options::imagingPlannerAcceptOther, &Options::setImagingPlannerAcceptOther);
1288
1289 setupFilter2Buttons(ui->pickedCB, ui->notPickedCB, ui->dontCarePickedCB,
1290 &Options::imagingPlannerShowPicked, &Options::imagingPlannerShowNotPicked, &Options::imagingPlannerDontCarePicked,
1291 &Options::setImagingPlannerShowPicked, &Options::setImagingPlannerShowNotPicked, &Options::setImagingPlannerDontCarePicked);
1292
1293 setupFilter2Buttons(ui->imagedCB, ui->notImagedCB, ui->dontCareImagedCB,
1294 &Options::imagingPlannerShowImaged, &Options::imagingPlannerShowNotImaged, &Options::imagingPlannerDontCareImaged,
1295 &Options::setImagingPlannerShowImaged, &Options::setImagingPlannerShowNotImaged, &Options::setImagingPlannerDontCareImaged);
1296
1297 setupFilter2Buttons(ui->ignoredCB, ui->notIgnoredCB, ui->dontCareIgnoredCB,
1298 &Options::imagingPlannerShowIgnored, &Options::imagingPlannerShowNotIgnored, &Options::imagingPlannerDontCareIgnored,
1299 &Options::setImagingPlannerShowIgnored, &Options::setImagingPlannerShowNotIgnored,
1300 &Options::setImagingPlannerDontCareIgnored);
1301
1302 ui->keywordEdit->setText(Options::imagingPlannerKeyword());
1303 ui->keywordEdit->setAcceptRichText(false);
1304 m_Keyword = Options::imagingPlannerKeyword();
1305 setupFilter2Buttons(ui->keywordCB, ui->notKeywordCB, ui->dontCareKeywordCB,
1306 &Options::imagingPlannerShowKeyword, &Options::imagingPlannerShowNotKeyword, &Options::imagingPlannerDontCareKeyword,
1307 &Options::setImagingPlannerShowKeyword, &Options::setImagingPlannerShowNotKeyword,
1308 &Options::setImagingPlannerDontCareKeyword);
1309
1310 ui->keywordEdit->setFocusPolicy(Qt::StrongFocus);
1311
1312 // Initialize the altitude/moon/hours inputs
1313 ui->useArtificialHorizon->setChecked(Options::imagingPlannerUseArtificialHorizon());
1314 m_UseArtificialHorizon = Options::imagingPlannerUseArtificialHorizon();
1315 ui->minMoon->setValue(Options::imagingPlannerMinMoonSeparation());
1316 m_MinMoon = Options::imagingPlannerMinMoonSeparation();
1317 ui->maxMoonAltitude->setValue(Options::imagingPlannerMaxMoonAltitude());
1318 m_MaxMoonAltitude = Options::imagingPlannerMaxMoonAltitude();
1319 ui->minAltitude->setValue(Options::imagingPlannerMinAltitude());
1320 m_MinAltitude = Options::imagingPlannerMinAltitude();
1321 ui->minHours->setValue(Options::imagingPlannerMinHours());
1322 m_MinHours = Options::imagingPlannerMinHours();
1323 m_CatalogSortModel->setMinHours(Options::imagingPlannerMinHours());
1324 connect(ui->useArtificialHorizon, &QCheckBox::toggled, [this]()
1325 {
1326 if (m_UseArtificialHorizon == ui->useArtificialHorizon->isChecked())
1327 return;
1328 m_UseArtificialHorizon = ui->useArtificialHorizon->isChecked();
1329 Options::setImagingPlannerUseArtificialHorizon(ui->useArtificialHorizon->isChecked());
1330 Options::self()->save();
1331 recompute();
1332 updateDisplays();
1333 });
1334 connect(ui->minMoon, &QDoubleSpinBox::editingFinished, [this]()
1335 {
1336 if (m_MinMoon == ui->minMoon->value())
1337 return;
1338 m_MinMoon = ui->minMoon->value();
1339 Options::setImagingPlannerMinMoonSeparation(ui->minMoon->value());
1340 Options::self()->save();
1341 recompute();
1342 updateDisplays();
1343 });
1344 connect(ui->maxMoonAltitude, &QDoubleSpinBox::editingFinished, [this]()
1345 {
1346 if (m_MaxMoonAltitude == ui->maxMoonAltitude->value())
1347 return;
1348 m_MaxMoonAltitude = ui->maxMoonAltitude->value();
1349 Options::setImagingPlannerMaxMoonAltitude(ui->maxMoonAltitude->value());
1350 Options::self()->save();
1351 recompute();
1352 updateDisplays();
1353 });
1354 connect(ui->minAltitude, &QDoubleSpinBox::editingFinished, [this]()
1355 {
1356 if (m_MinAltitude == ui->minAltitude->value())
1357 return;
1358 m_MinAltitude = ui->minAltitude->value();
1359 Options::setImagingPlannerMinAltitude(ui->minAltitude->value());
1360 Options::self()->save();
1361 recompute();
1362 updateDisplays();
1363 });
1364 connect(ui->minHours, &QDoubleSpinBox::editingFinished, [this]()
1365 {
1366 if (m_MinHours == ui->minHours->value())
1367 return;
1368 m_MinHours = ui->minHours->value();
1369 Options::setImagingPlannerMinHours(ui->minHours->value());
1370 Options::self()->save();
1371 m_CatalogSortModel->setMinHours(Options::imagingPlannerMinHours());
1372 m_CatalogSortModel->invalidate();
1373 ui->CatalogView->resizeColumnsToContents();
1374 updateDisplays();
1375 });
1376
1377 updateSortConstraints();
1378
1379 m_CatalogSortModel->setMinHours(ui->minHours->value());
1380
1381 ui->CatalogView->setColumnHidden(NOTES_COLUMN, true);
1382
1383 initUserNotes();
1384
1385 connect(ui->userNotesDoneButton, &QAbstractButton::clicked, this, &ImagingPlanner::userNotesEditFinished);
1386 ui->userNotesEdit->setFocusPolicy(Qt::StrongFocus);
1387
1388 connect(ui->userNotesEditButton, &QAbstractButton::clicked, this, [this]()
1389 {
1390 ui->userNotesLabel->setVisible(true);
1391 ui->userNotesEdit->setText(ui->userNotes->text());
1392 ui->userNotesEdit->setVisible(true);
1393 ui->userNotesEditButton->setVisible(false);
1394 ui->userNotesDoneButton->setVisible(true);
1395 ui->userNotes->setVisible(false);
1396 ui->userNotesLabel->setVisible(true);
1397 ui->userNotesOpenLink->setVisible(false);
1398 ui->userNotesOpenLink2->setVisible(false);
1399 ui->userNotesOpenLink3->setVisible(false);
1400 });
1401
1402 connect(ui->userNotesOpenLink, &QAbstractButton::clicked, this, [this]()
1403 {
1404 focusOnTable();
1405 QString urlString = findUrl(ui->userNotes->text());
1406 if (urlString.isEmpty())
1407 return;
1408 QDesktopServices::openUrl(QUrl(urlString));
1409 });
1410 connect(ui->userNotesOpenLink2, &QAbstractButton::clicked, this, [this]()
1411 {
1412 focusOnTable();
1413 QString urlString = findUrl(ui->userNotes->text(), 2);
1414 if (urlString.isEmpty())
1415 return;
1416 QDesktopServices::openUrl(QUrl(urlString));
1417 });
1418 connect(ui->userNotesOpenLink3, &QAbstractButton::clicked, this, [this]()
1419 {
1420 focusOnTable();
1421 QString urlString = findUrl(ui->userNotes->text(), 3);
1422 if (urlString.isEmpty())
1423 return;
1424 QDesktopServices::openUrl(QUrl(urlString));
1425 });
1426
1427 connect(ui->loadImagedB, &QPushButton::clicked, this, &ImagingPlanner::loadImagedFile);
1428
1429 connect(ui->SearchB, &QPushButton::clicked, this, &ImagingPlanner::searchSlot);
1430
1431 connect(ui->CatalogView->horizontalHeader(), &QHeaderView::sectionPressed, this, [this](int column)
1432 {
1433 m_CatalogSortModel->setSortColumn(column);
1434 m_CatalogSortModel->invalidate();
1435 ui->CatalogView->resizeColumnsToContents();
1436 });
1437
1438 adjustWindowSize();
1439
1440 connect(ui->optionsButton, &QPushButton::clicked, this, &ImagingPlanner::openOptionsMenu);
1441
1442 // Since we thread the loading of catalogs, need to connect the thread back to UI.
1443 qRegisterMetaType<QList<QStandardItem * >> ("QList<QStandardItem *>");
1444 connect(this, &ImagingPlanner::addRow, this, &ImagingPlanner::addRowSlot);
1445
1446 // Needed to fix weird bug on Windows that started with Qt 5.9 that makes the title bar
1447 // not visible and therefore dialog not movable.
1448#ifdef Q_OS_WIN
1449 move(100, 100);
1450#endif
1451
1452 // Install the event filters. Put them at the end of initialize so
1453 // the event filter isn't called until initialize is complete.
1454 installEventFilters();
1455
1456 m_PlateSolve.reset(new PlateSolve(this));
1457 m_PlateSolve->enableAuxButton("Retake screenshot",
1458 "Retake the screenshot of the object if you're having issues solving.");
1459 connect(m_PlateSolve.get(), &PlateSolve::clicked, this, &ImagingPlanner::extractImage, Qt::UniqueConnection);
1460 connect(m_PlateSolve.get(), &PlateSolve::auxClicked, this, [this]()
1461 {
1462 m_PlateSolve->abort();
1463 takeScreenshot();
1464 });
1465}
1466
1467void ImagingPlanner::installEventFilters()
1468{
1469 // Install the event filters. Put them at the end of initialize so
1470 // the event filter isn't called until initialize is complete.
1471 ui->SearchText->installEventFilter(this);
1472 ui->userNotesEdit->installEventFilter(this);
1473 ui->keywordEdit->installEventFilter(this);
1474 ui->ImagePreviewCreditLink->installEventFilter(this);
1475 ui->ImagePreviewCredit->installEventFilter(this);
1476 ui->ImagePreview->installEventFilter(this);
1477 ui->CatalogView->viewport()->installEventFilter(this);
1478 ui->CatalogView->installEventFilter(this);
1479 ui->helpButton->installEventFilter(this);
1480}
1481
1482void ImagingPlanner::removeEventFilters()
1483{
1484 ui->SearchText->removeEventFilter(this);
1485 ui->userNotesEdit->removeEventFilter(this);
1486 ui->keywordEdit->removeEventFilter(this);
1487 ui->ImagePreviewCreditLink->removeEventFilter(this);
1488 ui->ImagePreviewCredit->removeEventFilter(this);
1489 ui->ImagePreview->removeEventFilter(this);
1490 ui->CatalogView->viewport()->removeEventFilter(this);
1491 ui->helpButton->removeEventFilter(this);
1492}
1493
1494void ImagingPlanner::openOptionsMenu()
1495{
1496 QSharedPointer<ImagingPlannerOptions> options(new ImagingPlannerOptions(this));
1497 options->exec();
1498 focusOnTable();
1499}
1500
1501// KDE KHelpClient::invokeHelp() doesn't seem to work.
1502void ImagingPlanner::getHelp()
1503{
1504 focusOnTable();
1505 const QUrl url("https://kstars-docs.kde.org/en/user_manual/tool-imaging-planner.html");
1506 if (!url.isEmpty())
1508}
1509
1510KSMoon *ImagingPlanner::getMoon()
1511{
1512 if (KStarsData::Instance() == nullptr)
1513 return nullptr;
1514
1515 KSMoon *moon = dynamic_cast<KSMoon *>(KStarsData::Instance()->skyComposite()->findByName(i18n("Moon")));
1516 if (moon)
1517 {
1518 auto tz = QTimeZone(getGeo()->TZ() * 3600);
1519 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
1520 midnight.setTimeZone(tz);
1521 CachingDms LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(midnight).gst());
1522 KSNumbers numbers(midnight.djd());
1523 moon->updateCoords(&numbers, true, getGeo()->lat(), &LST, true);
1524 }
1525 return moon;
1526}
1527
1528// Setup the moon image.
1529void ImagingPlanner::updateMoon()
1530{
1531 KSMoon *moon = getMoon();
1532 if (!moon)
1533 return;
1534
1535 // You need to know the sun's position in order to get the right phase of the moon.
1536 auto tz = QTimeZone(getGeo()->TZ() * 3600);
1537 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
1538 CachingDms LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(midnight).gst());
1539 KSNumbers numbers(midnight.djd());
1540 KSSun *sun = dynamic_cast<KSSun *>(KStarsData::Instance()->skyComposite()->findByName(i18n("Sun")));
1541 sun->updateCoords(&numbers, true, getGeo()->lat(), &LST, true);
1542 moon->findPhase(sun);
1543
1544 ui->moonImage->setPixmap(QPixmap::fromImage(moon->image().scaled(32, 32, Qt::KeepAspectRatio)));
1545 ui->moonPercentLabel->setText(QString("%1%").arg(moon->illum() * 100.0 + 0.5, 0, 'f', 0));
1546}
1547
1548bool ImagingPlanner::scrollToName(const QString &name)
1549{
1550 if (name.isEmpty())
1551 return false;
1552 QModelIndexList matchList = ui->CatalogView->model()->match(ui->CatalogView->model()->index(0, 0), Qt::EditRole,
1554 if(matchList.count() >= 1)
1555 {
1556 int bestIndex = 0;
1557 for (int i = 0; i < matchList.count(); i++)
1558 {
1559 QString nn = ui->CatalogView->model()->data(matchList[i], Qt::DisplayRole).toString();
1560 if (nn.compare(name, Qt::CaseInsensitive) == 0)
1561 {
1562 bestIndex = i;
1563 break;
1564 }
1565 }
1566 ui->CatalogView->scrollTo(matchList[bestIndex]);
1567 ui->CatalogView->setCurrentIndex(matchList[bestIndex]);
1568 return true;
1569 }
1570 return false;
1571}
1572
1573void ImagingPlanner::searchSlot()
1574{
1575 if (m_loadingCatalog)
1576 return;
1577 QString origName = ui->SearchText->toPlainText().trimmed();
1578 QString name = tweakNames(origName);
1579 ui->SearchText->setPlainText(name);
1580 if (name.isEmpty())
1581 return;
1582
1583 if (!scrollToName(name))
1584 KSNotification::sorry(i18n("No match for \"%1\"", origName));
1585
1586 // Still leaves around some </p> in the html unfortunaltely. Don't know how to remove that.
1587 ui->SearchText->clear();
1588 ui->SearchText->setPlainText("");
1589}
1590
1591void ImagingPlanner::initUserNotes()
1592{
1593 ui->userNotesLabel->setVisible(true);
1594 ui->userNotesEdit->setVisible(false);
1595 ui->userNotesEditButton->setVisible(true);
1596 ui->userNotesDoneButton->setVisible(false);
1597 ui->userNotes->setVisible(true);
1598 ui->userNotesLabel->setVisible(true);
1599 ui->userNotesOpenLink->setVisible(false);
1600 ui->userNotesOpenLink2->setVisible(false);
1601 ui->userNotesOpenLink3->setVisible(false);
1602}
1603
1604void ImagingPlanner::disableUserNotes()
1605{
1606 ui->userNotesEdit->setVisible(false);
1607 ui->userNotesEditButton->setVisible(false);
1608 ui->userNotesDoneButton->setVisible(false);
1609 ui->userNotes->setVisible(false);
1610 ui->userNotesLabel->setVisible(false);
1611 ui->userNotesOpenLink->setVisible(false);
1612 ui->userNotesOpenLink2->setVisible(false);
1613 ui->userNotesOpenLink3->setVisible(false);
1614}
1615
1616void ImagingPlanner::userNotesEditFinished()
1617{
1618 const QString &notes = ui->userNotesEdit->toPlainText().trimmed();
1619 ui->userNotes->setText(notes);
1620 ui->userNotesLabel->setVisible(notes.isEmpty());
1621 ui->userNotesEdit->setVisible(false);
1622 ui->userNotesEditButton->setVisible(true);
1623 ui->userNotesDoneButton->setVisible(false);
1624 ui->userNotes->setVisible(true);
1625 ui->userNotesLabel->setVisible(true);
1626 setCurrentObjectNotes(notes);
1627 setupNotesLinks(notes);
1628 focusOnTable();
1629 auto o = currentCatalogObject();
1630 if (!o) return;
1631 saveToDB(currentObjectName(), currentObjectFlags(), notes);
1632}
1633
1634void ImagingPlanner::updateNotes(const QString &notes)
1635{
1636 ui->userNotes->setMaximumWidth(ui->RightPanel->width() - 125);
1637 initUserNotes();
1638 ui->userNotes->setText(notes);
1639 ui->userNotesLabel->setVisible(notes.isEmpty());
1640 setupNotesLinks(notes);
1641}
1642
1643void ImagingPlanner::setupNotesLinks(const QString &notes)
1644{
1645 QString link = findUrl(notes);
1646 ui->userNotesOpenLink->setVisible(!link.isEmpty());
1647 if (!link.isEmpty())
1648 ui->userNotesOpenLink->setToolTip(i18n("Open a browser with the 1st link in this note: %1", link));
1649
1650 link = findUrl(notes, 2);
1651 ui->userNotesOpenLink2->setVisible(!link.isEmpty());
1652 if (!link.isEmpty())
1653 ui->userNotesOpenLink2->setToolTip(i18n("Open a browser with the 2nd link in this note: %1", link));
1654
1655 link = findUrl(notes, 3);
1656 ui->userNotesOpenLink3->setVisible(!link.isEmpty());
1657 if (!link.isEmpty())
1658 ui->userNotesOpenLink3->setToolTip(i18n("Open a browser with the 3rd link in this note: %1", link));
1659}
1660
1661bool ImagingPlanner::internetNameSearch(const QString &name, bool abellPlanetary, int abellNumber,
1662 CatalogObject * catObject)
1663{
1664 DPRINTF(stderr, "***** internetNameSearch(%s)\n", name.toLatin1().data());
1665 QElapsedTimer timer;
1666 timer.start();
1667 QString filteredName = name;
1668 // The resolveName search is touchy about the dash.
1669 if (filteredName.startsWith("sh2", Qt::CaseInsensitive))
1670 filteredName.replace(QRegularExpression("sh2\\s*-?", QRegularExpression::CaseInsensitiveOption), "sh2-");
1671 QString resolverName = filteredName;
1672 if (abellPlanetary)
1673 {
1674 // Use "PN A66 ##" instead of "Abell ##" for name resolver
1675 resolverName = QString("PN A66 %1").arg(abellNumber);
1676 }
1677
1678 const auto &cedata = NameResolver::resolveName(resolverName);
1679 if (!cedata.first)
1680 return false;
1681
1682 CatalogObject object = cedata.second;
1683 if (abellPlanetary)
1684 {
1685 if (object.name() == object.name2())
1686 object.setName2(filteredName);
1687 object.setName(filteredName);
1688 }
1689
1690 m_manager.add_object(CatalogsDB::user_catalog_id, object);
1691 const auto &added_object =
1692 m_manager.get_object(object.getId(), CatalogsDB::user_catalog_id);
1693
1694 if (added_object.first)
1695 {
1696 *catObject = KStarsData::Instance()
1697 ->skyComposite()
1698 ->catalogsComponent()
1699 ->insertStaticObject(added_object.second);
1700 }
1701
1702 DPRINTF(stderr, "***** Found %s using name resolver (%.1fs)\n", name.toLatin1().data(),
1703 timer.elapsed() / 1000.0);
1704 return true;
1705}
1706
1707bool isAbellPlanetary(const QString &name, int *number)
1708{
1709 *number = -1;
1710 if (name.startsWith("Abell", Qt::CaseInsensitive))
1711 {
1712 QRegularExpression abellRE("Abell\\s*(\\d+)\\s*", QRegularExpression::CaseInsensitiveOption);
1713 auto match = abellRE.match(name);
1714 if (match.hasMatch())
1715 {
1716 *number = match.captured(1).toInt();
1717 if (*number <= 86)
1718 return true;
1719 }
1720 }
1721 return false;
1722}
1723
1724// Given an object name, return the KStars catalog object.
1725bool ImagingPlanner::getKStarsCatalogObject(const QString &name, CatalogObject * catObject)
1726{
1727 DPRINTF(stderr, "getKStarsCatalogObject(%s)\n", name.toLatin1().data());
1728 // find_objects_by_name is much faster with exactMatchOnly=true.
1729 // Therefore, since most will match exactly given the string pre-processing,
1730 // first try exact=true, and if that fails, follow up with exact=false.
1731 QString filteredName = FindDialog::processSearchText(name).toUpper();
1732 std::list<CatalogObject> objs =
1733 m_manager.find_objects_by_name(filteredName, 1, true);
1734
1735 // Don't accept objects that are Abell, have number <= 86 and are galaxy clusters.
1736 // Those were almost definitely planetary nebulae confused by Simbad/NameResolver.
1737 int abellNumber = 0;
1738 bool abellPlanetary = isAbellPlanetary(name, &abellNumber);
1739 if (objs.size() > 0 && abellPlanetary && objs.front().type() == SkyObject::GALAXY_CLUSTER)
1740 objs.clear();
1741
1742 if (objs.size() == 0 && filteredName.size() > 0)
1743 {
1744 // Try capitalizing
1745 const QString capitalized = capitalize(filteredName);
1746 objs = m_manager.find_objects_by_name(capitalized, 1, true);
1747 if (objs.size() > 0 && abellPlanetary && objs.front().type() == SkyObject::GALAXY_CLUSTER)
1748 objs.clear();
1749
1750 if (objs.size() == 0)
1751 {
1752 // Try lowercase
1753 const QString lowerCase = filteredName.toLower();
1754 objs = m_manager.find_objects_by_name(lowerCase, 1, true);
1755 if (objs.size() > 0 && abellPlanetary && objs.front().type() == SkyObject::GALAXY_CLUSTER)
1756 objs.clear();
1757 }
1758 }
1759
1760 // If we didn't find it and it's Sharpless, try sh2 with a space instead of a dash
1761 // and vica versa
1762 if (objs.size() == 0 && filteredName.startsWith("sh2-", Qt::CaseInsensitive))
1763 {
1764 QString name2 = filteredName;
1765 name2.replace(QRegularExpression("sh2-", QRegularExpression::CaseInsensitiveOption), "sh2 ");
1766 objs = m_manager.find_objects_by_name(name2, 1, true);
1767 }
1768 if (objs.size() == 0 && filteredName.startsWith("sh2 ", Qt::CaseInsensitive))
1769 {
1770 QString name2 = filteredName;
1771 name2.replace(QRegularExpression("sh2 ", QRegularExpression::CaseInsensitiveOption), "sh2-");
1772 objs = m_manager.find_objects_by_name(name2, 1, true);
1773 }
1774
1775 if (objs.size() == 0 && !abellPlanetary)
1776 objs = m_manager.find_objects_by_name(filteredName.toLower(), 20, false);
1777 if (objs.size() == 0)
1778 return internetNameSearch(filteredName, abellPlanetary, abellNumber, catObject);
1779
1780 if (objs.size() == 0)
1781 return false;
1782
1783 // If there's a match, see if there's an exact match in name, name2, or longname.
1784 *catObject = objs.front();
1785 if (objs.size() >= 1)
1786 {
1787 bool foundIt = false;
1788 QString addSpace = filteredName;
1789 addSpace.append(" ");
1790 QString addComma = filteredName;
1791 addComma.append(",");
1792 QString sh2Fix = filteredName;
1793 sh2Fix.replace(QRegularExpression("sh2 ", QRegularExpression::CaseInsensitiveOption), "sh2-");
1794 for (const auto &obj : objs)
1795 {
1796 if ((filteredName.compare(obj.name(), Qt::CaseInsensitive) == 0) ||
1797 (filteredName.compare(obj.name2(), Qt::CaseInsensitive) == 0) ||
1798 obj.longname().contains(addSpace, Qt::CaseInsensitive) ||
1799 obj.longname().contains(addComma, Qt::CaseInsensitive) ||
1800 obj.longname().endsWith(filteredName, Qt::CaseInsensitive) ||
1801 (sh2Fix.compare(obj.name(), Qt::CaseInsensitive) == 0) ||
1802 (sh2Fix.compare(obj.name2(), Qt::CaseInsensitive) == 0)
1803 )
1804 {
1805 *catObject = obj;
1806 foundIt = true;
1807 break;
1808 }
1809 }
1810 if (!foundIt)
1811 {
1812 if (objs.size() == 1)
1813 DPRINTF(stderr, " ========> \"%s\" had 1 match \"%s\", but not trusting it!!!!\n", name.toLatin1().data(),
1814 objs.front().name().toLatin1().data());
1815
1816 if (internetNameSearch(filteredName, abellPlanetary, abellNumber, catObject))
1817 return true;
1818
1819 DPRINTF(stderr, "Didn't find %s (%s) -- Not using name \"%s\" name2 \"%s\" longname \"%s\"\n",
1820 name.toLatin1().data(), filteredName.toLatin1().data(), catObject->name().toLatin1().data(),
1821 catObject->name2().toLatin1().data(),
1822 catObject->longname().toLatin1().data());
1823 return false;
1824 }
1825 }
1826 return true;
1827}
1828
1829CatalogObject *ImagingPlanner::getObject(const QString &name)
1830{
1831 if (name.isEmpty())
1832 return nullptr;
1833 QString lName = name.toLower();
1834 auto o = m_CatalogHash.find(lName);
1835 if (o == m_CatalogHash.end())
1836 return nullptr;
1837 return &(*o);
1838}
1839
1840void ImagingPlanner::clearObjects()
1841{
1842 // Important to tell SkyMap that our objects are gone.
1843 // We give SkyMap points to these objects in ImagingPlanner::centerOnSkymap()
1844 SkyMap::Instance()->setClickedObject(nullptr);
1845 SkyMap::Instance()->setFocusObject(nullptr);
1846 m_CatalogHash.clear();
1847}
1848
1849CatalogObject *ImagingPlanner::addObject(const QString &name)
1850{
1851 if (name.isEmpty())
1852 return nullptr;
1853 QString lName = name.toLower();
1854 if (getObject(lName) != nullptr)
1855 {
1856 DPRINTF(stderr, "Didn't add \"%s\" because it's already there\n", name.toLatin1().data());
1857 return nullptr;
1858 }
1859
1860 CatalogObject o;
1861 if (!getKStarsCatalogObject(lName, &o))
1862 {
1863 DPRINTF(stderr, "************* Couldn't find \"%s\"\n", lName.toLatin1().data());
1864 return nullptr;
1865 }
1866 m_CatalogHash[lName] = o;
1867 return &(m_CatalogHash[lName]);
1868}
1869
1870// Adds the object to the catalog model, assuming a KStars catalog object can be found
1871// for that name.
1872bool ImagingPlanner::addCatalogItem(const KSAlmanac &ksal, const QString &name, int flags)
1873{
1874 CatalogObject *object = addObject(name);
1875 if (object == nullptr)
1876 return false;
1877
1878 auto getItemWithUserRole = [](const QString & itemText) -> QStandardItem *
1879 {
1880 QStandardItem *ret = new QStandardItem(itemText);
1881 ret->setData(itemText, Qt::UserRole);
1883 return ret;
1884 };
1885
1886 // Build the data. The columns must be the same as the #define columns at the top of this file.
1887 QList<QStandardItem *> itemList;
1888 for (int i = 0; i < LAST_COLUMN; ++i)
1889 {
1890 if (i == NAME_COLUMN)
1891 {
1892 itemList.append(getItemWithUserRole(name));
1893 }
1894 else if (i == HOURS_COLUMN)
1895 {
1896 double runHours = getRunHours(*object, getDate(), *getGeo(), ui->minAltitude->value(), ui->minMoon->value(),
1897 ui->maxMoonAltitude->value(), ui->useArtificialHorizon->isChecked());
1898 auto hoursItem = getItemWithUserRole(QString("%1").arg(runHours, 0, 'f', 1));
1899 hoursItem->setData(runHours, HOURS_ROLE);
1900 itemList.append(hoursItem);
1901 }
1902 else if (i == TYPE_COLUMN)
1903 {
1904 auto typeItem = getItemWithUserRole(QString("%1").arg(SkyObject::typeShortName(object->type())));
1905 typeItem->setData(object->type(), TYPE_ROLE);
1906 itemList.append(typeItem);
1907 }
1908 else if (i == SIZE_COLUMN)
1909 {
1910 double size = std::max(object->a(), object->b());
1911 auto sizeItem = getItemWithUserRole(QString("%1'").arg(size, 0, 'f', 1));
1912 sizeItem->setData(size, SIZE_ROLE);
1913 itemList.append(sizeItem);
1914 }
1915 else if (i == ALTITUDE_COLUMN)
1916 {
1917 const auto time = KStarsDateTime(QDateTime(getDate(), QTime(12, 0)));
1918 const double altitude = getMaxAltitude(ksal, getDate(), getGeo(), *object, 0, 0);
1919 auto altItem = getItemWithUserRole(QString("%1º").arg(altitude, 0, 'f', 0));
1920 altItem->setData(altitude, ALTITUDE_ROLE);
1921 itemList.append(altItem);
1922 }
1923 else if (i == MOON_COLUMN)
1924 {
1925 KSMoon *moon = getMoon();
1926 if (moon)
1927 {
1928 SkyPoint o;
1929 o.setRA0(object->ra0());
1930 o.setDec0(object->dec0());
1931 auto tz = QTimeZone(getGeo()->TZ() * 3600);
1932 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
1933 midnight.setTimeZone(tz);
1934 KSNumbers numbers(midnight.djd());
1935 o.updateCoordsNow(&numbers);
1936
1937 double const separation = moon->angularDistanceTo(&o).Degrees();
1938 auto moonItem = getItemWithUserRole(QString("%1º").arg(separation, 0, 'f', 0));
1939 moonItem->setData(separation, MOON_ROLE);
1940 itemList.append(moonItem);
1941 }
1942 else
1943 {
1944 auto moonItem = getItemWithUserRole(QString(""));
1945 moonItem->setData(-1, MOON_ROLE);
1946 }
1947 }
1948 else if (i == CONSTELLATION_COLUMN)
1949 {
1950 QString cname = KStarsData::Instance()
1951 ->skyComposite()
1952 ->constellationBoundary()
1953 ->constellationName(object);
1954 cname = cname.toLower().replace(0, 1, cname[0].toUpper());
1955 auto constellationItem = getItemWithUserRole(cname);
1956 itemList.append(constellationItem);
1957 }
1958 else if (i == COORD_COLUMN)
1959 {
1960 itemList.append(getItemWithUserRole(shortCoordString(object->ra0(), object->dec0())));
1961 }
1962 else if (i == FLAGS_COLUMN)
1963 {
1964 QStandardItem *flag = getItemWithUserRole("flag");
1965 flag->setData(flags, FLAGS_ROLE);
1966 itemList.append(flag);
1967 }
1968 else if (i == NOTES_COLUMN)
1969 {
1970 QStandardItem *notes = getItemWithUserRole("notes");
1971 notes->setData(QString(), NOTES_ROLE);
1972 itemList.append(notes);
1973 }
1974 else
1975 {
1976 DPRINTF(stderr, "Bug in addCatalogItem() !\n");
1977 }
1978 }
1979
1980 // Can't do UI in this thread, must move back to the UI thread.
1981 emit addRow(itemList);
1982 return true;
1983}
1984
1985void ImagingPlanner::addRowSlot(QList<QStandardItem *> itemList)
1986{
1987 m_CatalogModel->appendRow(itemList);
1988 updateCounts();
1989}
1990
1991void ImagingPlanner::recompute()
1992{
1993 setStatus(i18n("Updating tables..."));
1994
1995 // Disconnect the filter from the model, or else we'll re-filter numRows squared times.
1996 m_CatalogSortModel->setSourceModel(nullptr);
1997
1998 QElapsedTimer timer;
1999 timer.start();
2000
2001 auto tz = QTimeZone(getGeo()->TZ() * 3600);
2002 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
2003 KStarsDateTime ut = getGeo()->LTtoUT(KStarsDateTime(midnight));
2004 KSAlmanac ksal(ut, getGeo());
2005
2006 for (int i = 0; i < m_CatalogModel->rowCount(); ++i)
2007 {
2008 const QString &name = m_CatalogModel->item(i, 0)->text();
2009 const CatalogObject *catalogEntry = getObject(name);
2010 if (catalogEntry == nullptr)
2011 {
2012 DPRINTF(stderr, "************* Couldn't find \"%s\"\n", name.toLatin1().data());
2013 return;
2014 }
2015 double runHours = getRunHours(*catalogEntry, getDate(), *getGeo(), ui->minAltitude->value(),
2016 ui->minMoon->value(), ui->maxMoonAltitude->value(), ui->useArtificialHorizon->isChecked());
2017 QString hoursText = QString("%1").arg(runHours, 0, 'f', 1);
2018 QStandardItem *hItem = new QStandardItem(hoursText);
2019 hItem->setData(hoursText, Qt::UserRole);
2021 hItem->setData(runHours, HOURS_ROLE);
2022 m_CatalogModel->setItem(i, HOURS_COLUMN, hItem);
2023
2024
2025 const auto time = KStarsDateTime(QDateTime(getDate(), QTime(12, 0)));
2026 const double altitude = getMaxAltitude(ksal, getDate(), getGeo(), *catalogEntry, 0, 0);
2027 QString altText = QString("%1º").arg(altitude, 0, 'f', 0);
2028 auto altItem = new QStandardItem(altText);
2029 altItem->setData(altText, Qt::UserRole);
2030 altItem->setData(altitude, ALTITUDE_ROLE);
2031 m_CatalogModel->setItem(i, ALTITUDE_COLUMN, altItem);
2032
2033 KSMoon *moon = getMoon();
2034 if (moon)
2035 {
2036 SkyPoint o;
2037 o.setRA0(catalogEntry->ra0());
2038 o.setDec0(catalogEntry->dec0());
2039 auto tz = QTimeZone(getGeo()->TZ() * 3600);
2040 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
2041 midnight.setTimeZone(tz);
2042 KSNumbers numbers(midnight.djd());
2043 o.updateCoordsNow(&numbers);
2044
2045 double const separation = moon->angularDistanceTo(&o).Degrees();
2046 QString moonText = QString("%1º").arg(separation, 0, 'f', 0);
2047 auto moonItem = new QStandardItem(moonText);
2048 moonItem->setData(moonText, Qt::UserRole);
2049 moonItem->setData(separation, MOON_ROLE);
2050 m_CatalogModel->setItem(i, MOON_COLUMN, moonItem);
2051 }
2052 else
2053 {
2054 auto moonItem = new QStandardItem("");
2055 moonItem->setData("", Qt::UserRole);
2056 moonItem->setData(-1, MOON_ROLE);
2057 m_CatalogModel->setItem(i, MOON_COLUMN, moonItem);
2058 }
2059
2060 // Don't lose the imaged background highlighting.
2061 const bool imaged = m_CatalogModel->item(i, FLAGS_COLUMN)->data(FLAGS_ROLE).toInt() & IMAGED_BIT;
2062 if (imaged)
2063 highlightImagedObject(m_CatalogModel->index(i, NAME_COLUMN), true);
2064 const bool picked = m_CatalogModel->item(i, FLAGS_COLUMN)->data(FLAGS_ROLE).toInt() & PICKED_BIT;
2065 if (picked)
2066 highlightPickedObject(m_CatalogModel->index(i, NAME_COLUMN), true);
2067 }
2068 // Reconnect the filter to the model.
2069 m_CatalogSortModel->setSourceModel(m_CatalogModel.data());
2070
2071 DPRINTF(stderr, "Recompute took %.1fs\n", timer.elapsed() / 1000.0);
2072 updateStatus();
2073}
2074
2075// This section used in catalog development--methods checkTargets() and checkTargets2().
2076// CheckTargets() either reads in a file of target names, one per line, and sees if they
2077// can be found, and also computes distances between these targets and existing catalog
2078// targets, and other new targets. It can also work with just catalog targets.
2079// CheckTargets2() helps with browsing these objects on the SkyMap using flags.
2080namespace
2081{
2082bool ALREADY_CHECKING = false;
2083int ALREADY_CHECKING_INDEX = -1;
2084QList<CatalogObject> addedObjects;
2085struct ObjectNeighbor
2086{
2087 CatalogObject object;
2088 double distance;
2089 QString neighbor;
2090 ObjectNeighbor(CatalogObject o, double d, QString nei) : object(o), distance(d), neighbor(nei) {}
2091};
2092QList<ObjectNeighbor> sortedAddedObjects;
2093} // namespace
2094
2095// CheckTargets2() browses the objects read in in checkTargets().
2096void ImagingPlanner::checkTargets2(bool backwards)
2097{
2098 if (ALREADY_CHECKING)
2099 {
2100 if (backwards)
2101 ALREADY_CHECKING_INDEX--;
2102 else
2103 ALREADY_CHECKING_INDEX++;
2104
2105 if (sortedAddedObjects.size() == 0)
2106 {
2107 fprintf(stderr, "No TARGETS\n");
2108 return;
2109 }
2110 if (ALREADY_CHECKING_INDEX >= sortedAddedObjects.size())
2111 ALREADY_CHECKING_INDEX = 0;
2112 else if (ALREADY_CHECKING_INDEX < 0)
2113 ALREADY_CHECKING_INDEX = sortedAddedObjects.size() - 1;
2114 KStarsDateTime time = KStarsData::Instance()->clock()->utc();
2115 dms lst = getGeo()->GSTtoLST(time.gst());
2116 CatalogObject &o = sortedAddedObjects[ALREADY_CHECKING_INDEX].object;
2117 o.EquatorialToHorizontal(&lst, getGeo()->lat());
2118 fprintf(stderr, "%d: %s\n", ALREADY_CHECKING_INDEX, o.name().toLatin1().data());
2119
2120 // Doing this to avoid the pop-up warning that an object is below the ground.
2121 bool keepGround = Options::showGround();
2122 bool keepAnimatedSlew = Options::useAnimatedSlewing();
2123 Options::setShowGround(false);
2124 Options::setUseAnimatedSlewing(false);
2125 SkyMap::Instance()->setClickedObject(&o);
2126 SkyMap::Instance()->setClickedPoint(&o);
2127 SkyMap::Instance()->slotCenter();
2128 Options::setShowGround(keepGround);
2129 Options::setUseAnimatedSlewing(keepAnimatedSlew);
2130 }
2131}
2132
2133// Puts flags on all existing and proposed catalog targets, computes distances,
2134// and sets up some browsing in the above method.
2135void ImagingPlanner::checkTargets(bool justCheckCurrentCatalog)
2136{
2137 if (ALREADY_CHECKING)
2138 {
2139 checkTargets2(false);
2140 return;
2141 }
2142 ALREADY_CHECKING = true;
2143
2144 // Put flags for all existing targets.
2145 FlagComponent *flags = KStarsData::Instance()->skyComposite()->flags();
2146 for (int i = flags->size() - 1; i >= 0; --i) flags->remove(i);
2147 int rows = m_CatalogModel->rowCount();
2148
2149 int numFlags = 0;
2150 for (int i = 0; i < rows; ++i)
2151 {
2152 const QString &name = m_CatalogModel->item(i, NAME_COLUMN)->text();
2153 auto object = getObject(name);
2154 if (object)
2155 {
2156 numFlags++;
2157 flags->add(SkyPoint(object->ra(), object->dec()), "J2000.0", "", name, Qt::red);
2158 }
2159 }
2160 fprintf(stderr, "Added %d flags\n", numFlags);
2161
2162
2163 // Read a file with a list of proposed new targets.
2164 QList<QString> targets;
2165 QList<QString> newObjects;
2166 if (!justCheckCurrentCatalog)
2167 {
2168 QString fileName = QFileDialog::getOpenFileName(this,
2169 tr("Targets Filename"), QDir::homePath(), tr("Any files (*)"));
2170 if (fileName.isEmpty())
2171 return;
2172 QFile inputFile(fileName);
2173 if (inputFile.open(QIODevice::ReadOnly))
2174 {
2175 QTextStream in(&inputFile);
2176 while (!in.atEnd())
2177 {
2178 const QString line = in.readLine().trimmed();
2179 if (line.size() > 0 && line[0] != '#' && newObjects.indexOf(line) == -1)
2180 newObjects.push_back(line);
2181 }
2182 inputFile.close();
2183 }
2184 if (newObjects.size() == 0)
2185 {
2186 fprintf(stderr, "No New Targets\n");
2187 return;
2188 }
2189 }
2190
2191 QList<CatalogObject> addedObjects;
2192 sortedAddedObjects.clear();
2193
2194 int count = 0, good = 0;
2195 for (int i = 0; i < rows; ++i)
2196 {
2197 const QString &name = m_CatalogModel->item(i, NAME_COLUMN)->text();
2198 count++;
2199 auto o = getObject(name);
2200 if (o != nullptr)
2201 {
2202 good++;
2203 addedObjects.push_back(*o);
2204 }
2205 targets.push_back(name);
2206 }
2207 fprintf(stderr, "********** %d/%d targets found. %d unique test objects\n", good, count, newObjects.size());
2208
2209 // First we add all the new objects that aren't already existing, and that can be found by KStars, to a
2210 // list. This is done so we can find distances to the nearest other one.
2211 if (!justCheckCurrentCatalog)
2212 {
2213 fprintf(stderr, "Adding: ");
2214 for (const auto &name : newObjects)
2215 {
2216 if (getObject(name) != nullptr)
2217 {
2218 fprintf(stderr, "0 %s ** EXISTS!\n", name.toLatin1().data());
2219 continue;
2220 }
2221 CatalogObject object;
2222 if (!getKStarsCatalogObject(name, &object))
2223 {
2224 fprintf(stderr, "0 %s ** COULD NOT FIND IT\n", name.toLatin1().data());
2225 continue;
2226 }
2227 object.setRA(object.ra0());
2228 object.setDec(object.dec0());
2229 if (name.compare(object.name(), Qt::CaseInsensitive) != 0)
2230 {
2231 fprintf(stderr, "%s had primary name %s -- reverting.\n",
2232 name.toLatin1().data(), object.name().toLatin1().data());
2233 object.setName(name);
2234 }
2235 fprintf(stderr, "%s ", name.toLatin1().data());
2236 fflush(stderr);
2237 addedObjects.push_back(object);
2238 }
2239 fprintf(stderr, "\n--------------------------------------------------------\n");
2240 }
2241
2242 // AddedObjects may actually contain the current catalog if justCheckCurrentCatalog is true.
2243 for (int i = 0; i < addedObjects.size(); ++i)
2244 {
2245 auto &object = addedObjects[i];
2246 double closest = 1e9;
2247 QString closestName;
2248 for (int j = 0; j < targets.size(); ++j)
2249 {
2250 if (justCheckCurrentCatalog && i == j)
2251 continue;
2252 auto name2 = targets[j];
2253 auto object2 = getObject(name2);
2254 if (object2 == nullptr)
2255 {
2256 fprintf(stderr, "********************************************************* O2 for targets[%d]: %s null!\n", j,
2257 name2.toLatin1().data());
2258 break;
2259 }
2260 object2->setRA(object2->ra0());
2261 object2->setDec(object2->dec0());
2262 const dms dist = object.angularDistanceTo(object2);
2263 const double arcsecDist = dist.Degrees() * 3600.0;
2264 if (closest > arcsecDist)
2265 {
2266 closest = arcsecDist;
2267 closestName = name2;
2268 }
2269 }
2270
2271 sortedAddedObjects.push_back(ObjectNeighbor(addedObjects[i], closest, closestName));
2272 // Also check the new objects -- not quite right see line below:
2273 // 702.8 c 7 ** OK (closest 703' ic 5148) (closestNew 0' IC 1459)
2274
2275 if (justCheckCurrentCatalog)
2276 {
2277 fprintf(stderr, "%7.1f %-10s closest %s\n", closest / 60.0, object.name().toLatin1().data(),
2278 closestName.toLatin1().data());
2279 }
2280 else
2281 {
2282 double closestNew = 1e9;
2283 QString closestNewName;
2284 for (int j = 0; j < addedObjects.size() - 1; ++j)
2285 {
2286 if (i == j) continue;
2287 auto object2 = addedObjects[j];
2288 object2.setRA(object2.ra0());
2289 object2.setDec(object2.dec0());
2290 const dms dist = object.angularDistanceTo(&object2);
2291 const double arcsecDist = dist.Degrees() * 3600.0;
2292 if (closestNew > arcsecDist)
2293 {
2294 closestNew = arcsecDist;
2295 closestNewName = object2.name();
2296 }
2297 }
2298 fprintf(stderr, "%7.1f %-10s (closest %s) (closestNew %5.0f' %-10s)\n",
2299 closest / 60.0, object.name().toLatin1().data(), closestName.toLatin1().data(),
2300 closestNew / 60, closestNewName.toLatin1().data());
2301 flags->add(SkyPoint(object.ra(), object.dec()), "J2000.0", "", QString("%1").arg(object.name()), Qt::yellow);
2302 }
2303 }
2304 std::sort(sortedAddedObjects.begin(), sortedAddedObjects.end(),
2305 [](const ObjectNeighbor & a, const ObjectNeighbor & b)
2306 {
2307 return a.distance > b.distance;
2308 });
2309 if (justCheckCurrentCatalog)
2310 {
2311 fprintf(stderr, "Sorted: ------------------------------------------\n");
2312 for (const auto &o : sortedAddedObjects)
2313 fprintf(stderr, "%7.1f %-10s closest %s\n",
2314 o.distance / 60.0, o.object.name().toLatin1().data(),
2315 o.neighbor.toLatin1().data());
2316 }
2317 fprintf(stderr, "DONE. ------------------------------------------\n");
2318}
2319
2320// This is the top-level ImagingPlanning catalog directory.
2321QString ImagingPlanner::defaultDirectory() const
2322{
2323 return KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)
2324 + QDir::separator() + "ImagingPlanner";
2325}
2326
2327namespace
2328{
2329
2330bool sortOldest(const QFileInfo &file1, const QFileInfo &file2)
2331{
2332 return file1.birthTime() < file2.birthTime();
2333}
2334
2335QFileInfoList findDefaultDirectories()
2336{
2337 QString kstarsDir = KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
2338 QDir kDir(kstarsDir);
2339 kDir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
2340 QStringList nameFilters;
2341 nameFilters << "ImagingPlanner*";
2342 QFileInfoList dirs1 = kDir.entryInfoList(nameFilters);
2343
2344 QString ipDir = kstarsDir + QDir::separator() + "ImagingPlanner";
2345 QFileInfoList dirs2 = QDir(ipDir).entryInfoList(QStringList(), QDir::NoDotAndDotDot | QDir::AllDirs);
2346
2347 dirs1.append(dirs2);
2348 std::sort(dirs1.begin(), dirs1.end(), sortOldest);
2349 return dirs1;
2350}
2351} // namespace
2352
2353// The default catalog is one loaded by the "Data -> Download New Data..." menu.
2354// Search the default directory for a ImagingPlanner subdirectory
2355QString ImagingPlanner::findDefaultCatalog() const
2356{
2357 QFileInfoList subDirs = findDefaultDirectories();
2358 for( const auto &dd : subDirs)
2359 {
2360 QDir subDir(dd.absoluteFilePath());
2361 const QStringList csvFilter({"*.csv"});
2362 const QFileInfoList files = subDir.entryInfoList(csvFilter, QDir::NoDotAndDotDot | QDir::Files);
2363 if (files.size() > 0)
2364 {
2365 QString firstFile;
2366 // Look through all the .csv files. Pick all.csv if it exists,
2367 // otherwise one of the other .csv files.
2368 for (const auto &file : files)
2369 {
2370 if (firstFile.isEmpty())
2371 firstFile = file.absoluteFilePath();
2372 if (!file.baseName().compare("all", Qt::CaseInsensitive))
2373 return file.absoluteFilePath();
2374 }
2375 if (!firstFile.isEmpty())
2376 return firstFile;
2377 }
2378 }
2379 return QString();
2380}
2381
2382void ImagingPlanner::loadInitialCatalog()
2383{
2384 QString catalog = Options::imagingPlannerCatalogPath();
2385 if (catalog.isEmpty())
2386 catalog = findDefaultCatalog();
2387 if (catalog.isEmpty())
2388 {
2389 KSNotification::sorry(
2390 i18n("You need to load a catalog to start using this tool.\n"
2391 "Use the Load Catalog button if you have one.\n"
2392 "See Data -> Download New Data if not..."));
2393 setStatus(i18n("No Catalog!"));
2394 }
2395 else
2396 loadCatalog(catalog);
2397}
2398
2399void ImagingPlanner::setStatus(const QString &message)
2400{
2401 ui->statusLabel->setText(message);
2402}
2403
2404void ImagingPlanner::catalogLoaded()
2405{
2406 DPRINTF(stderr, "All catalogs loaded: %d of %d have catalog images\n", m_numWithImage, m_numWithImage + m_numMissingImage);
2407 // This cannot go in the threaded loadInitialCatalog()!
2408 loadFromDB();
2409
2410 // TODO: At this point we'd read in various files (picked/imaged/deleted targets ...)
2411 // Can't do this in initialize() as we don't have columns yet.
2412 ui->CatalogView->setColumnHidden(FLAGS_COLUMN, true);
2413 ui->CatalogView->setColumnHidden(NOTES_COLUMN, true);
2414
2415 m_CatalogSortModel->invalidate();
2416 ui->CatalogView->sortByColumn(HOURS_COLUMN, Qt::DescendingOrder);
2417 ui->CatalogView->resizeColumnsToContents();
2418
2419 // Select the first row and give it the keyboard focus (so up/down keyboard keys work).
2420 auto index = ui->CatalogView->model()->index(0, 0);
2421 //ui->CatalogView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select |QItemSelectionModel::Current| QItemSelectionModel::Rows);
2422 ui->CatalogView->selectionModel()->select(index,
2424 ui->CatalogView->setFocus();
2425 updateDisplays();
2426
2427 updateStatus();
2428 adjustWindowSize();
2429}
2430
2431void ImagingPlanner::updateStatus()
2432{
2433 if (currentObjectName().isEmpty())
2434 {
2435 const int numDisplayedObjects = m_CatalogSortModel->rowCount();
2436 const int totalCatalogObjects = m_CatalogModel->rowCount();
2437
2438 if (numDisplayedObjects > 0)
2439 setStatus(i18n("Select an object."));
2440 else if (totalCatalogObjects > 0)
2441 setStatus(i18n("Check Filters to unhide objects."));
2442 else
2443 setStatus(i18n("Load a Catalog."));
2444 }
2445 else
2446 setStatus("");
2447}
2448
2449// This runs when the window gets a show event.
2450void ImagingPlanner::showEvent(QShowEvent * e)
2451{
2452 // ONLY run for first ever show
2453 if (m_initialShow == false)
2454 {
2455 m_initialShow = true;
2456 const int ht = height();
2457 resize(1000, ht);
2459 }
2460}
2461
2462//FIXME: On close, we will need to close any open Details/AVT windows
2463void ImagingPlanner::slotClose()
2464{
2465}
2466
2467// Reverse engineering of the Astrobin search URL (with permission from Salvatore).
2468// See https://github.com/astrobin/astrobin/blob/master/common/encoded_search_viewset.py#L15
2469QUrl ImagingPlanner::getAstrobinUrl(const QString &target, bool requireAwards, bool requireSomeFilters, double minRadius,
2470 double maxRadius)
2471{
2472 QString myQuery = QString("text={\"value\":\"%1\",\"matchType\":\"ALL\"}").arg(target);
2473
2474 // This is a place where the actual date, not the date in the widget, is the right one to find.
2475 auto localTime = getGeo()->UTtoLT(KStarsData::Instance()->clock()->utc());
2476 QDate today = localTime.date();
2477 myQuery.append(QString("&date_acquired={\"min\":\"2018-01-01\",\"max\":\"%1\"}").arg(today.toString("yyyy-MM-dd")));
2478
2479 if (requireAwards)
2480 myQuery.append(QString("&award=[\"iotd\",\"top-pick\",\"top-pick-nomination\"]"));
2481
2482 if (requireSomeFilters)
2483 myQuery.append(QString("&filter_types={\"value\":[\"H_ALPHA\",\"SII\",\"OIII\",\"R\",\"G\",\"B\"],\"matchType\":\"ANY\"}"));
2484
2485 if ((minRadius > 0 || maxRadius > 0) && (maxRadius > minRadius))
2486 myQuery.append(QString("&field_radius={\"min\":%1,\"max\":%2}").arg(minRadius).arg(maxRadius));
2487
2488 QByteArray b(myQuery.toLatin1().data());
2489
2490 // See quick pack implmentation in anonymous namespace above.
2491 QByteArray packed = pack(b);
2492
2493 QByteArray compressed = qCompress(packed).remove(0, 4);
2494
2495 QByteArray b64 = compressed.toBase64();
2496
2497 replaceByteArrayChars(b64, '+', QByteArray("%2B"));
2498 replaceByteArrayChars(b64, '=', QByteArray("%3D"));
2499 replaceByteArrayChars(b, '"', QByteArray("%22"));
2500 replaceByteArrayChars(b, ':', QByteArray("%3A"));
2501 replaceByteArrayChars(b, '[', QByteArray("%5B"));
2502 replaceByteArrayChars(b, ']', QByteArray("%5D"));
2503 replaceByteArrayChars(b, ',', QByteArray("%2C"));
2504 replaceByteArrayChars(b, '\'', QByteArray("%27"));
2505 replaceByteArrayChars(b, '{', QByteArray("%7B"));
2506 replaceByteArrayChars(b, '}', QByteArray("%7D"));
2507
2508 QString url = QString("https://app.astrobin.com/search?p=%1").arg(b64.toStdString().c_str());
2509 return QUrl(url);
2510}
2511
2512void ImagingPlanner::popupAstrobin(const QString &target)
2513{
2514 QString newStr = replaceSpaceWith(target, "-");
2515 if (newStr.isEmpty()) return;
2516
2517 const QUrl url = getAstrobinUrl(newStr, Options::astrobinAward(), false, Options::astrobinMinRadius(),
2518 Options::astrobinMaxRadius());
2519 if (!url.isEmpty())
2521}
2522
2523// Returns true if the url will result in a successful get.
2524// Times out after 3 seconds.
2525// Used for the special search button because some of the object
2526// web pages for vdb don't exist.
2527bool ImagingPlanner::checkIfPageExists(const QString &urlString)
2528{
2529 if (urlString.isEmpty())
2530 return false;
2531
2532 QUrl url(urlString);
2533 QNetworkRequest request(url);
2534 QNetworkReply *reply = m_networkManager.get(request);
2535
2536 QEventLoop loop;
2538
2539 QTimer timer;
2540 timer.setSingleShot(true);
2541 timer.setInterval(3000); // 3 seconds timeout
2542
2543 connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
2544 timer.start();
2545 loop.exec();
2546 timer.stop();
2547
2548 if (reply->error() == QNetworkReply::NoError)
2549 {
2550 reply->deleteLater();
2551 return true;
2552 }
2553 else if (timer.isActive() )
2554 {
2555 reply->deleteLater();
2556 return false;
2557 }
2558 else
2559 {
2560 reply->deleteLater();
2561 return false;
2562 }
2563}
2564
2565// Changes the label and tooltop on the searchSpecialWebPages buttons,
2566// depending on the current object, whose name is passed in.
2567void ImagingPlanner::adjustSpecialWebPageButton(const QString &name)
2568{
2569 QString catalog, toolTip, label;
2571 {
2572 catalog = "ngc";
2573 toolTip = i18n("Search the Professor Seligman online site for NGC images.");
2574 }
2575 else if (name.startsWith("ic", Qt::CaseInsensitive))
2576 {
2577 catalog = "ic";
2578 toolTip = i18n("Search the Professor Seligman online site for information about IC objects..");
2579 }
2580 else if (name.startsWith("sh2", Qt::CaseInsensitive))
2581 {
2582 catalog = "sh2";
2583 label = "Sharpless";
2584 toolTip = i18n("Search the galaxymap.org online site for information about Sharpless2 objects.");
2585 }
2586 else if (name.startsWith("m", Qt::CaseInsensitive))
2587 {
2588 catalog = "m";
2589 label = "Messier";
2590 toolTip = i18n("Search Nasa's online site for information about Messier objects..");
2591 }
2592 else if (name.startsWith("vdb", Qt::CaseInsensitive))
2593 {
2594 catalog = "vdb";
2595 toolTip = i18n("Search Emil Ivanov's online site for information about VDB objects.");
2596 }
2597 if (!catalog.isEmpty())
2598 {
2599 const QString numberPart = name.mid(catalog.size()).trimmed();
2600 if (!label.isEmpty()) catalog = label;
2601 bool ok;
2602 const int num = numberPart.toInt(&ok);
2603 Q_UNUSED(num);
2604 if (ok)
2605 {
2606 ui->searchSpecialWebPageImages->setText(catalog);
2607 ui->searchSpecialWebPageImages2->setText(catalog);
2608 ui->searchSpecialWebPageImages->setEnabled(true);
2609 ui->searchSpecialWebPageImages2->setEnabled(true);
2610 ui->searchSpecialWebPageImages->setToolTip(toolTip);
2611 ui->searchSpecialWebPageImages2->setToolTip(toolTip);
2612 return;
2613 }
2614 }
2615 ui->searchSpecialWebPageImages->setText("");
2616 ui->searchSpecialWebPageImages2->setText("");
2617 ui->searchSpecialWebPageImages->setEnabled(false);
2618 ui->searchSpecialWebPageImages2->setEnabled(false);
2619 ui->searchSpecialWebPageImages->setToolTip("");
2620 ui->searchSpecialWebPageImages2->setToolTip("");
2621
2622}
2623
2624void ImagingPlanner::searchSpecialWebPageImages()
2625{
2626 focusOnTable();
2627 const QString objectName = currentObjectName();
2628 QString urlString;
2629 bool ok;
2630 if (objectName.startsWith("ngc", Qt::CaseInsensitive))
2631 {
2632 const QString numberPart = objectName.mid(3).trimmed();
2633 const int num = numberPart.toInt(&ok);
2634 if (ok)
2635 urlString = QString("https://cseligman.com/text/atlas/ngc%1%2.htm#%3")
2636 .arg(num / 100).arg(num % 100 < 50 ? "" : "a").arg(num);
2637 }
2638 else if (objectName.startsWith("ic", Qt::CaseInsensitive))
2639 {
2640 const QString numberPart = objectName.mid(2).trimmed();
2641 const int num = numberPart.toInt(&ok);
2642 if (ok)
2643 urlString = QString("https://cseligman.com/text/atlas/ic%1%2.htm#ic%3")
2644 .arg(num / 100).arg(num % 100 < 50 ? "" : "a").arg(num);
2645 }
2646 else if (objectName.startsWith("sh2", Qt::CaseInsensitive))
2647 {
2648 const QString numberPart = objectName.mid(3).trimmed();
2649 const int num = numberPart.toInt(&ok);
2650 if (ok)
2651 urlString = QString("http://galaxymap.org/cat/view/sharpless/%1").arg(num);
2652 }
2653 else if (objectName.startsWith("m", Qt::CaseInsensitive))
2654 {
2655 const QString numberPart = objectName.mid(1).trimmed();
2656 const int num = numberPart.toInt(&ok);
2657 if (ok)
2658 urlString = QString("https://science.nasa.gov/mission/hubble/science/"
2659 "explore-the-night-sky/hubble-messier-catalog/messier-%1").arg(num);
2660 }
2661 else if (objectName.startsWith("vdb", Qt::CaseInsensitive))
2662 {
2663 const QString numberPart = objectName.mid(3).trimmed();
2664 const int num = numberPart.toInt(&ok);
2665 if (ok)
2666 {
2667 urlString = QString("https://www.irida-observatory.org/CCD/VdB%1/VdB%1.html").arg(num);
2668 if (!checkIfPageExists(urlString))
2669 urlString = "https://www.emilivanov.com/CCD%20Images/Catalog_VdB.htm";
2670 }
2671
2672 }
2673 if (!urlString.isEmpty())
2674 QDesktopServices::openUrl(QUrl(urlString));
2675}
2676void ImagingPlanner::searchSimbad()
2677{
2678 focusOnTable();
2679 QString name = currentObjectName();
2680
2681 int abellNumber = 0;
2682 bool abellPlanetary = isAbellPlanetary(name, &abellNumber);
2683 if (abellPlanetary)
2684 name = QString("PN A66 %1").arg(abellNumber);
2685 else if (name.startsWith("sh2", Qt::CaseInsensitive))
2686 name.replace(QRegularExpression("sh2\\s*"), "sh2-");
2687 else if (name.startsWith("hickson", Qt::CaseInsensitive))
2688 {
2689 name.replace(0, 7, "HCG");
2690 name.replace(' ', "");
2691 }
2692 else
2693 name.replace(' ', "");
2694
2695 QString urlStr = QString("https://simbad.cds.unistra.fr/simbad/sim-id?Ident=%1&NbIdent=1"
2696 "&Radius=20&Radius.unit=arcmin&submit=submit+id").arg(name);
2697 QDesktopServices::openUrl(QUrl(urlStr));
2698}
2699
2700
2701// Crude massaging to conform to wikipedia standards
2702void ImagingPlanner::searchWikipedia()
2703{
2704 focusOnTable();
2705 QString wikipediaAddress = "https://en.wikipedia.org";
2706 QString name = currentObjectName();
2707 if (name.isEmpty())
2708 {
2709 DPRINTF(stderr, "NULL object sent to Wikipedia.\n");
2710 return;
2711 }
2712
2714 name = QString("Messier_%1").arg(name.mid(2, -1));
2715 QString urlStr = QString("%1/w/index.php?search=%2").arg(wikipediaAddress).arg(replaceSpaceWith(name, "_"));
2716 QDesktopServices::openUrl(QUrl(urlStr));
2717 return;
2718}
2719
2720void ImagingPlanner::searchAstrobin()
2721{
2722 focusOnTable();
2723 QString name = currentObjectName();
2724 if (name.isEmpty())
2725 return;
2726 popupAstrobin(name);
2727}
2728
2729bool ImagingPlanner::eventFilter(QObject * obj, QEvent * event)
2730{
2731 if (m_loadingCatalog)
2732 return false;
2733
2734 // Load the catalog on tool startup.
2735 if (m_InitialLoad && event->type() == QEvent::Paint)
2736 {
2737 m_InitialLoad = false;
2738 setStatus(i18n("Loading Catalogs..."));
2739 QTimer::singleShot(100, this, &ImagingPlanner::loadInitialCatalog);
2740 return false;
2741 }
2742
2743 QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
2744
2745 if ((obj == ui->helpButton) &&
2746 (mouseEvent->button() == Qt::LeftButton) &&
2747 (event->type() == QEvent::MouseButtonRelease) &&
2748 (mouseEvent->modifiers() & Qt::KeyboardModifier::ShiftModifier) &&
2749 (mouseEvent->modifiers() & Qt::KeyboardModifier::ControlModifier) &&
2750 (mouseEvent->modifiers() & Qt::KeyboardModifier::AltModifier))
2751 {
2752 // Development code to help browse new catalog entries.
2753 // "Secret" keyboard modifies hijack the help button to reveal/hide the development buttons.
2754 ui->DevelFrame->setVisible(!ui->DevelFrame->isVisible());
2755 return false;
2756 }
2757 else if (obj == ui->helpButton && (event->type() == QEvent::MouseButtonRelease))
2758 {
2759 // Standard use of the help button.
2760 getHelp();
2761 return false;
2762 }
2763
2764 // Right click on object in catalog view brings up this menu.
2765 else if ((obj == ui->CatalogView->viewport()) &&
2766 (event->type() == QEvent::MouseButtonRelease) &&
2767 (mouseEvent->button() == Qt::RightButton))
2768 {
2769 int numImaged = 0, numNotImaged = 0, numPicked = 0, numNotPicked = 0, numIgnored = 0, numNotIgnored = 0;
2770 QStringList selectedNames;
2771 for (const auto &r : ui->CatalogView->selectionModel()->selectedRows())
2772 {
2773 selectedNames.append(r.siblingAtColumn(0).data().toString());
2774 bool isPicked = getFlag(r, PICKED_BIT, ui->CatalogView->model());
2775 if (isPicked) numPicked++;
2776 else numNotPicked++;
2777 bool isImaged = getFlag(r, IMAGED_BIT, ui->CatalogView->model());
2778 if (isImaged) numImaged++;
2779 else numNotImaged++;
2780 bool isIgnored = getFlag(r, IGNORED_BIT, ui->CatalogView->model());
2781 if (isIgnored) numIgnored++;
2782 else numNotIgnored++;
2783 }
2784
2785 if (selectedNames.size() == 0)
2786 return false;
2787
2788 if (!m_PopupMenu)
2789 m_PopupMenu = new ImagingPlannerPopup;
2790
2791 const bool imaged = numImaged > 0;
2792 const bool picked = numPicked > 0;
2793 const bool ignored = numIgnored > 0;
2794 m_PopupMenu->init(this, selectedNames,
2795 (numImaged > 0 && numNotImaged > 0) ? nullptr : &imaged,
2796 (numPicked > 0 && numNotPicked > 0) ? nullptr : &picked,
2797 (numIgnored > 0 && numNotIgnored > 0) ? nullptr : &ignored);
2798 QPoint pos(mouseEvent->globalX(), mouseEvent->globalY());
2799 m_PopupMenu->popup(pos);
2800 }
2801
2802 else if (obj == ui->userNotesEdit && event->type() == QEvent::FocusOut)
2803 userNotesEditFinished();
2804
2805 else if (obj == ui->keywordEdit && event->type() == QEvent::FocusOut)
2806 keywordEditFinished();
2807
2808 else if (obj == ui->keywordEdit && (event->type() == QEvent::KeyPress))
2809 {
2810 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
2811 auto key = keyEvent->key();
2812 switch(key)
2813 {
2814 case Qt::Key_Enter:
2815 case Qt::Key_Tab:
2816 case Qt::Key_Return:
2817 keywordEditFinished();
2818 ui->keywordEdit->clearFocus();
2819 break;
2820 default:
2821 ;
2822 }
2823 }
2824
2825 else if (obj == ui->SearchText && (event->type() == QEvent::KeyPress))
2826 {
2827 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
2828 auto key = keyEvent->key();
2829 switch(key)
2830 {
2831 case Qt::Key_Enter:
2832 case Qt::Key_Tab:
2833 case Qt::Key_Return:
2834 searchSlot();
2835 break;
2836 default:
2837 ;
2838 }
2839 }
2840
2841 else if ((obj == ui->ImagePreview ||
2842 obj == ui->ImagePreviewCredit ||
2843 obj == ui->ImagePreviewCreditLink) &&
2845 {
2846 if (!ui->ImagePreviewCreditLink->text().isEmpty())
2847 {
2848 QUrl url(ui->ImagePreviewCreditLink->text());
2850 }
2851 }
2852
2853 return false;
2854}
2855
2856void ImagingPlanner::keywordEditFinished()
2857{
2858 QString kwd = ui->keywordEdit->toPlainText().trimmed();
2859 ui->keywordEdit->clear();
2860 ui->keywordEdit->setText(kwd);
2861 if (m_Keyword != kwd)
2862 {
2863 m_Keyword = kwd;
2864 Options::setImagingPlannerKeyword(kwd);
2865 Options::self()->save();
2866 updateSortConstraints();
2867 m_CatalogSortModel->invalidate();
2868 ui->CatalogView->resizeColumnsToContents();
2869 updateDisplays();
2870 }
2871
2872}
2873void ImagingPlanner::setDefaultImage()
2874{
2875 ui->ImagePreview->setPixmap(m_NoImagePixmap);
2876 ui->ImagePreview->update();
2877 ui->ImagePreviewCredit->setText("");
2878 ui->ImagePreviewCreditLink->setText("");
2879}
2880
2881void ImagingPlanner::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
2882{
2883 if (m_loadingCatalog)
2884 return;
2885
2886 Q_UNUSED(deselected);
2887 if (selected.indexes().size() == 0)
2888 {
2889 disableUserNotes();
2890 return;
2891 }
2892
2893 initUserNotes();
2894 updateStatus();
2895 auto selection = selected.indexes()[0];
2896 QString name = selection.data().toString();
2897 CatalogObject *object = getObject(name);
2898 if (object == nullptr)
2899 return;
2900
2901 // This assumes current object and current selection are the same.
2902 // Could pass in "selected" if necessary.
2903 updateDisplays();
2904
2905 ui->ImagePreviewCredit->setText("");
2906 ui->ImagePreviewCreditLink->setText("");
2907 // clear the image too?
2908
2909 CatalogImageInfo catalogImageInfo;
2910 if (findCatalogImageInfo(name, &catalogImageInfo))
2911 {
2912 QString filename = catalogImageInfo.m_Filename;
2913 if (!filename.isEmpty() && !Options::imagingPlannerCatalogPath().isEmpty())
2914 {
2915 QString imageFullPath = filename;
2916 if (QFileInfo(filename).isRelative())
2917 {
2918 QString catDir = QFileInfo(Options::imagingPlannerCatalogPath()).absolutePath();
2919 imageFullPath = QString("%1%2%3").arg(catDir)
2920 .arg(QDir::separator()).arg(filename);
2921 }
2922 if (!QFile(imageFullPath).exists())
2923 DPRINTF(stderr, "Image for \"%s\" -- \"%s\" doesn't exist\n",
2924 name.toLatin1().data(), imageFullPath.toLatin1().data());
2925
2926 ui->ImagePreview->setPixmap(QPixmap::fromImage(QImage(imageFullPath)));
2927 if (!catalogImageInfo.m_Link.isEmpty())
2928 {
2929 ui->ImagePreviewCreditLink->setText(catalogImageInfo.m_Link);
2930 ui->ImagePreview->setToolTip("Click to see original");
2931 ui->ImagePreviewCreditLink->setToolTip("Click to see original");
2932 }
2933 else
2934 {
2935 ui->ImagePreviewCreditLink->setText("");
2936 ui->ImagePreview->setToolTip("");
2937 ui->ImagePreviewCreditLink->setToolTip("");
2938 }
2939
2940 if (!catalogImageInfo.m_Author.isEmpty() && !catalogImageInfo.m_License.isEmpty())
2941 {
2942 ui->ImagePreviewCredit->setText(
2943 QString("Credit: %1 (with license %2)").arg(catalogImageInfo.m_Author)
2944 .arg(creativeCommonsString(catalogImageInfo.m_License)));
2945 ui->ImagePreviewCredit->setToolTip(
2946 QString("Original image license: %1")
2947 .arg(creativeCommonsTooltipString(catalogImageInfo.m_License)));
2948 }
2949 else if (!catalogImageInfo.m_Author.isEmpty())
2950 {
2951 ui->ImagePreviewCredit->setText(
2952 QString("Credit: %1").arg(catalogImageInfo.m_Author));
2953 ui->ImagePreviewCredit->setToolTip("");
2954 }
2955 else if (!catalogImageInfo.m_License.isEmpty())
2956 {
2957 ui->ImagePreviewCredit->setText(
2958 QString("(license %1)").arg(creativeCommonsString(catalogImageInfo.m_License)));
2959 ui->ImagePreviewCredit->setToolTip(
2960 QString("Original image license: %1")
2961 .arg(creativeCommonsTooltipString(catalogImageInfo.m_License)));
2962 }
2963 else
2964 {
2965 ui->ImagePreviewCredit->setText("");
2966 ui->ImagePreviewCredit->setToolTip("");
2967 }
2968 }
2969 }
2970 else
2971 {
2972 object->load_image();
2973 auto image = object->image();
2974 if (!image.first)
2975 {
2976 // As a backup, see if the image is stored elsewhere...
2977 // I've seen many images stored in ~/.local/share/kstars/ZZ/ZZ-name.png,
2978 // e.g. kstars/thumb_ngc/thumb_ngc-m1.png
2979 const QString foundFilename = findObjectImage(name);
2980 if (!name.isEmpty())
2981 {
2982 constexpr int thumbHeight = 300, thumbWidth = 400;
2983 const QImage img = QImage(foundFilename);
2984 const bool scale = img.width() > thumbWidth || img.height() > thumbHeight;
2985 if (scale)
2986 ui->ImagePreview->setPixmap(
2987 QPixmap::fromImage(img.scaled(thumbWidth, thumbHeight, Qt::KeepAspectRatio)));
2988 else
2989 ui->ImagePreview->setPixmap(QPixmap::fromImage(img));
2990 }
2991 else
2992 setDefaultImage();
2993 }
2994 else
2995 ui->ImagePreview->setPixmap(QPixmap::fromImage(image.second));
2996 }
2997 adjustSpecialWebPageButton(currentObjectName());
2998}
2999
3000void ImagingPlanner::updateDisplays()
3001{
3002 updateCounts();
3003
3004 // If nothing is selected, then select the first thing.
3005 if (!currentCatalogObject())
3006 {
3007 if (ui->CatalogView->model()->rowCount() > 0)
3008 {
3009 auto index = ui->CatalogView->model()->index(0, 0);
3010 ui->CatalogView->selectionModel()->select(index,
3012 }
3013 }
3014
3015 auto object = currentCatalogObject();
3016 if (object)
3017 {
3018 updateDetails(*object, currentObjectFlags());
3019 updateNotes(currentObjectNotes());
3020 plotAltitudeGraph(getDate(), object->ra0(), object->dec0());
3021 centerOnSkymap();
3022 }
3023 updateStatus();
3024 focusOnTable();
3025}
3026
3027void ImagingPlanner::updateDetails(const CatalogObject &object, int flags)
3028{
3029 ui->infoObjectName->setText(object.name());
3030 ui->infoSize->setText(QString("%1' x %2'").arg(object.a(), 0, 'f', 1).arg(object.b(), 0, 'f', 1));
3031
3032 QPalette palette = ui->infoObjectLongName->palette();
3033 //palette.setColor(ui->infoObjectLongName->backgroundRole(), Qt::darkGray);
3034 palette.setColor(ui->infoObjectLongName->foregroundRole(), Qt::darkGray);
3035 ui->infoObjectLongName->setPalette(palette);
3036 if (object.longname().isEmpty() || (object.longname() == object.name()))
3037 ui->infoObjectLongName->clear();
3038 else
3039 ui->infoObjectLongName->setText(QString("(%1)").arg(object.longname()));
3040
3041 ui->infoObjectType->setText(SkyObject::typeName(object.type()));
3042
3043 auto noon = KStarsDateTime(getDate(), QTime(12, 0, 0));
3044 QTime riseTime = object.riseSetTime(noon, getGeo(), true);
3045 QTime setTime = object.riseSetTime(noon, getGeo(), false);
3046 QTime transitTime = object.transitTime(noon, getGeo());
3047 dms transitAltitude = object.transitAltitude(noon, getGeo());
3048
3049 QString moonString;
3050 KSMoon *moon = getMoon();
3051 if (moon)
3052 {
3053 const double separation = ui->CatalogView->selectionModel()->currentIndex()
3054 .siblingAtColumn(MOON_COLUMN).data(MOON_ROLE).toDouble();
3055 // The unicode character is the angle sign.
3056 if (separation >= 0)
3057 moonString = QString("%1 \u2220 %3º").arg(i18n("Moon")).arg(separation, 0, 'f', 1);
3058 }
3059
3060 QString riseSetString;
3061 if (!riseTime.isValid() && !setTime.isValid() && transitTime.isValid())
3062 riseSetString = QString("%1 %2 @ %3º")
3063 .arg(i18n("Transits"))
3064 .arg(transitTime.toString("h:mm"))
3065 .arg(transitAltitude.Degrees(), 0, 'f', 1);
3066 else if (!riseTime.isValid() && setTime.isValid() && !transitTime.isValid())
3067 riseSetString = QString("%1 %2")
3068 .arg(i18n("Sets at"))
3069 .arg(setTime.toString("h:mm"));
3070 else if (!riseTime.isValid() && setTime.isValid() && transitTime.isValid())
3071 riseSetString = QString("%1 %2 %3 %4 @ %5º")
3072 .arg(i18n("Sets at"))
3073 .arg(setTime.toString("h:mm"))
3074 .arg(i18n("Transit"))
3075 .arg(transitTime.toString("h:mm"))
3076 .arg(transitAltitude.Degrees(), 0, 'f', 1);
3077 else if (riseTime.isValid() && !setTime.isValid() && !transitTime.isValid())
3078 riseSetString = QString("%1 %2")
3079 .arg(i18n("Rises at"))
3080 .arg(riseTime.toString("h:mm"));
3081 else if (riseTime.isValid() && !setTime.isValid() && transitTime.isValid())
3082 riseSetString = QString("%1 %2 %3 %4 @ %5º")
3083 .arg(i18n("Rises at"))
3084 .arg(riseTime.toString("h:mm"))
3085 .arg(i18n("Transit"))
3086 .arg(transitTime.toString("h:mm"))
3087 .arg(transitAltitude.Degrees(), 0, 'f', 1);
3088 else if (riseTime.isValid() && setTime.isValid() && !transitTime.isValid())
3089 riseSetString = QString("%1 %2 %3 %4")
3090 .arg(i18n("Rises"))
3091 .arg(riseTime.toString("h:mm"))
3092 .arg(i18n("Sets"))
3093 .arg(setTime.toString("h:mm"));
3094 else if (riseTime.isValid() && setTime.isValid() && transitTime.isValid())
3095 riseSetString = QString("%1 %2 %3 %4 %5 %6 @ %7º")
3096 .arg(i18n("Rises"))
3097 .arg(riseTime.toString("h:mm"))
3098 .arg(i18n("Sets"))
3099 .arg(setTime.toString("h:mm"))
3100 .arg(i18n("Transit"))
3101 .arg(transitTime.toString("h:mm"))
3102 .arg(transitAltitude.Degrees(), 0, 'f', 1);
3103 if (moonString.size() > 0)
3104 riseSetString.append(QString(", %1").arg(moonString));
3105 ui->infoRiseSet->setText(riseSetString);
3106
3107 palette = ui->infoObjectFlags->palette();
3108 palette.setColor(ui->infoObjectFlags->foregroundRole(), Qt::darkGray);
3109 ui->infoObjectFlags->setPalette(palette);
3110 ui->infoObjectFlags->setText(flagString(flags));
3111}
3112
3113// TODO: This code needs to be shared with the scheduler somehow.
3114// Right now 2 very similar copies at the end of scheduler.cpp and here.
3115//
3116// Clearly below I had timezone issues. The problem was running this code using a timezone
3117// that was not the local timezone of the machine. E.g. setting KStars to australia
3118// when I'm in california.
3119void ImagingPlanner::plotAltitudeGraph(const QDate &date, const dms &ra, const dms &dec)
3120{
3121 auto altitudeGraph = ui->altitudeGraph;
3122 altitudeGraph->setAltitudeAxis(-20.0, 90.0);
3123 //altitudeGraph->axis(KPlotWidget::TopAxis)->setVisible(false);
3124
3125 QVector<QDateTime> jobStartTimes, jobEndTimes;
3126 getRunTimes(date, *getGeo(), ui->minAltitude->value(), ui->minMoon->value(), ui->maxMoonAltitude->value(), ra, dec,
3127 ui->useArtificialHorizon->isChecked(),
3128 &jobStartTimes, &jobEndTimes);
3129
3130 auto tz = QTimeZone(getGeo()->TZ() * 3600);
3131 KStarsDateTime midnight = KStarsDateTime(date.addDays(1), QTime(0, 1));
3132 midnight.setTimeZone(tz);
3133
3134 KStarsDateTime ut = getGeo()->LTtoUT(KStarsDateTime(midnight));
3135 KSAlmanac ksal(ut, getGeo());
3136 QDateTime dawn = midnight.addSecs(24 * 3600 * ksal.getDawnAstronomicalTwilight());
3137 dawn.setTimeZone(tz);
3138 QDateTime dusk = midnight.addSecs(24 * 3600 * ksal.getDuskAstronomicalTwilight());
3139 dusk.setTimeZone(tz);
3140
3141 Ekos::SchedulerJob job;
3142 setupJob(job, "temp", ui->minAltitude->value(), ui->minMoon->value(), ui->maxMoonAltitude->value(), ra, dec,
3143 ui->useArtificialHorizon->isChecked());
3144
3145 QVector<double> times, alts;
3146 QDateTime plotStart = dusk;
3147 plotStart.setTimeZone(tz);
3148
3149
3150 // Start the plot 1 hour before dusk and end it an hour after dawn.
3151 plotStart = plotStart.addSecs(-1 * 3600);
3152 auto t = plotStart;
3153 t.setTimeZone(tz);
3154 auto plotEnd = dawn.addSecs(1 * 3600);
3155 plotEnd.setTimeZone(tz);
3156
3157 while (t.secsTo(plotEnd) > 0)
3158 {
3159 SkyPoint coords = job.getTargetCoords();
3160 double alt = getAltitude(getGeo(), coords, t);
3161 alts.push_back(alt);
3162 double hour = midnight.secsTo(t) / 3600.0;
3163 times.push_back(hour);
3164 t = t.addSecs(60 * 10);
3165 }
3166
3167 altitudeGraph->plot(getGeo(), &ksal, times, alts);
3168
3169 for (int i = 0; i < jobStartTimes.size(); ++i)
3170 {
3171 auto startTime = jobStartTimes[i];
3172 auto stopTime = jobEndTimes[i];
3173 if (startTime < plotStart) startTime = plotStart;
3174 if (stopTime > plotEnd) stopTime = plotEnd;
3175
3176 startTime.setTimeZone(tz);
3177 stopTime.setTimeZone(tz);
3178
3179 QVector<double> runTimes, runAlts;
3180 auto t = startTime;
3181 t.setTimeZone(tz);
3182 //t.setTimeZone(jobStartTimes[0].timeZone());
3183
3184 while (t.secsTo(stopTime) > 0)
3185 {
3186 SkyPoint coords = job.getTargetCoords();
3187 double alt = getAltitude(getGeo(), coords, t);
3188 runAlts.push_back(alt);
3189 double hour = midnight.secsTo(t) / 3600.0;
3190 runTimes.push_back(hour);
3191 t = t.addSecs(60 * 10);
3192 }
3193 altitudeGraph->plotOverlay(runTimes, runAlts);
3194 }
3195}
3196
3197void ImagingPlanner::updateCounts()
3198{
3199 const int numDisplayedObjects = m_CatalogSortModel->rowCount();
3200 const int totalCatalogObjects = m_CatalogModel->rowCount();
3201 if (numDisplayedObjects == 1)
3202 ui->tableCount->setText(QString("1/%1 %2").arg(totalCatalogObjects).arg(i18n("object")));
3203 else
3204 ui->tableCount->setText(QString("%1/%2 %3").arg(numDisplayedObjects).arg(totalCatalogObjects).arg(i18n("objects")));
3205}
3206
3207void ImagingPlanner::moveBackOneDay()
3208{
3209 // Try to keep the object.
3210 QString selection = currentObjectName();
3211 ui->DateEdit->setDate(ui->DateEdit->date().addDays(-1));
3212 // Don't need to call recompute(), called by dateChanged callback.
3213 updateDisplays();
3214 updateMoon();
3215 scrollToName(selection);
3216}
3217
3218void ImagingPlanner::moveForwardOneDay()
3219{
3220 QString selection = currentObjectName();
3221 ui->DateEdit->setDate(ui->DateEdit->date().addDays(1));
3222 // Don't need to call recompute(), called by dateChanged callback.
3223 updateDisplays();
3224 updateMoon();
3225 scrollToName(selection);
3226}
3227
3228QString ImagingPlanner::currentObjectName() const
3229{
3230 QString name = ui->CatalogView->selectionModel()->currentIndex().siblingAtColumn(NAME_COLUMN).data(
3231 Qt::DisplayRole).toString();
3232 return name;
3233}
3234
3235CatalogObject *ImagingPlanner::currentCatalogObject()
3236{
3237 QString name = currentObjectName();
3238 return getObject(name);
3239}
3240
3241//FIXME: This will open multiple Detail windows for each object;
3242//Should have one window whose target object changes with selection
3243void ImagingPlanner::objectDetails()
3244{
3245 CatalogObject *current = currentCatalogObject();
3246 if (current == nullptr)
3247 return;
3248 auto ut = KStarsData::Instance()->ut();
3249 ut.setDate(getDate());
3250 QPointer<DetailDialog> dd =
3251 new DetailDialog(current, ut, getGeo(), KStars::Instance());
3252 dd->exec();
3253 delete dd;
3254}
3255
3256void ImagingPlanner::centerOnSkymap()
3257{
3258 if (!Options::imagingPlannerCenterOnSkyMap())
3259 return;
3260 reallyCenterOnSkymap();
3261}
3262
3263void ImagingPlanner::reallyCenterOnSkymap()
3264{
3265 CatalogObject *current = currentCatalogObject();
3266 if (current == nullptr)
3267 return;
3268
3269 // These shouldn't happen anymore--seemed to happen when I let in null objects.
3270 if (current->ra().Degrees() == 0 && current->dec().Degrees() == 0)
3271 {
3272 DPRINTF(stderr, "found a 0,0 object\n");
3273 return;
3274 }
3275
3276 // Set up the Alt/Az coordinates that SkyMap needs.
3277 KStarsDateTime time = KStarsData::Instance()->clock()->utc();
3278 dms lst = getGeo()->GSTtoLST(time.gst());
3279 current->EquatorialToHorizontal(&lst, getGeo()->lat());
3280
3281
3282 // Doing this to avoid the pop-up warning that an object is below the ground.
3283 bool keepGround = Options::showGround();
3284 bool keepAnimatedSlew = Options::useAnimatedSlewing();
3285 Options::setShowGround(false);
3286 Options::setUseAnimatedSlewing(false);
3287
3288 SkyMap::Instance()->setClickedObject(current);
3289 SkyMap::Instance()->setClickedPoint(current);
3290 SkyMap::Instance()->slotCenter();
3291
3292 Options::setShowGround(keepGround);
3293 Options::setUseAnimatedSlewing(keepAnimatedSlew);
3294}
3295
3296void ImagingPlanner::setSelection(int flag, bool enabled)
3297{
3298 auto rows = ui->CatalogView->selectionModel()->selectedRows();
3299
3300 // We can't use the selection for processing, because it may change on the fly
3301 // as we modify flags (e.g. if the view is set to showing picked objects only
3302 // and we are disabling the picked flag, as a selected object with a picked flag
3303 // gets de-picked, it will also get deselected.
3304 // So, we store a list of the source model indeces, and operate on the source model.
3305
3306 // Find the source model indeces.
3307 QList<QModelIndex> sourceIndeces;
3308 for (int i = 0; i < rows.size(); ++i)
3309 {
3310 auto proxyIndex = rows[i].siblingAtColumn(FLAGS_COLUMN);
3311 auto sourceIndex = m_CatalogSortModel->mapToSource(proxyIndex);
3312 sourceIndeces.append(sourceIndex);
3313 }
3314
3315 for (int i = 0; i < sourceIndeces.size(); ++i)
3316 {
3317 auto &sourceIndex = sourceIndeces[i];
3318
3319 // Set or clear the flags using the source model.
3320 if (enabled)
3321 setFlag(sourceIndex, flag, m_CatalogModel.data());
3322 else
3323 clearFlag(sourceIndex, flag, m_CatalogModel.data());
3324
3325 QString name = m_CatalogModel->data(sourceIndex.siblingAtColumn(NAME_COLUMN)).toString();
3326 int flags = m_CatalogModel->data(sourceIndex.siblingAtColumn(FLAGS_COLUMN), FLAGS_ROLE).toInt();
3327 QString notes = m_CatalogModel->data(sourceIndex.siblingAtColumn(NOTES_COLUMN), NOTES_ROLE).toString();
3328 saveToDB(name, flags, notes);
3329
3330 if (flag == IMAGED_BIT)
3331 highlightImagedObject(sourceIndex, enabled);
3332 if (flag == PICKED_BIT)
3333 highlightPickedObject(sourceIndex, enabled);
3334 }
3335 updateDisplays();
3336}
3337
3338void ImagingPlanner::highlightImagedObject(const QModelIndex &index, bool imaged)
3339{
3340 // TODO: Ugly, for now. Figure out how to use the color schemes the right way.
3341 QColor m_DefaultCellBackground(36, 35, 35);
3342 QColor m_ImagedObjectBackground(10, 65, 10);
3343 QString themeName = KSTheme::Manager::instance()->currentThemeName().toLatin1().data();
3344 if (themeName == "High Key" || themeName == "Default" || themeName == "White Balance")
3345 {
3346 m_DefaultCellBackground = QColor(240, 240, 240);
3347 m_ImagedObjectBackground = QColor(180, 240, 180);
3348 }
3349 for (int col = 0; col < LAST_COLUMN; ++col)
3350 {
3351 auto colIndex = index.siblingAtColumn(col);
3352 m_CatalogModel->setData(colIndex, imaged ? m_ImagedObjectBackground : m_DefaultCellBackground, Qt::BackgroundRole);
3353 }
3354}
3355
3356void ImagingPlanner::highlightPickedObject(const QModelIndex &index, bool picked)
3357{
3358 for (int col = 0; col < LAST_COLUMN; ++col)
3359 {
3360 auto colIndex = index.siblingAtColumn(col);
3361 auto font = m_CatalogModel->data(colIndex, Qt::FontRole);
3362 auto ff = qvariant_cast<QFont>(font);
3363 ff.setBold(picked);
3364 ff.setItalic(picked);
3365 ff.setUnderline(picked);
3366 font = ff;
3367 m_CatalogModel->setData(colIndex, font, Qt::FontRole);
3368 }
3369}
3370
3371void ImagingPlanner::setSelectionPicked()
3372{
3373 setSelection(PICKED_BIT, true);
3374}
3375
3376void ImagingPlanner::setSelectionNotPicked()
3377{
3378 setSelection(PICKED_BIT, false);
3379}
3380
3381void ImagingPlanner::setSelectionImaged()
3382{
3383 setSelection(IMAGED_BIT, true);
3384}
3385
3386void ImagingPlanner::setSelectionNotImaged()
3387{
3388 setSelection(IMAGED_BIT, false);
3389}
3390
3391void ImagingPlanner::setSelectionIgnored()
3392{
3393 setSelection(IGNORED_BIT, true);
3394}
3395
3396void ImagingPlanner::setSelectionNotIgnored()
3397{
3398 setSelection(IGNORED_BIT, false);
3399}
3400
3401int ImagingPlanner::currentObjectFlags()
3402{
3403 auto index = ui->CatalogView->selectionModel()->currentIndex().siblingAtColumn(FLAGS_COLUMN);
3404 const bool hasFlags = ui->CatalogView->model()->data(index, FLAGS_ROLE).canConvert<int>();
3405 if (!hasFlags)
3406 return 0;
3407 return ui->CatalogView->model()->data(index, FLAGS_ROLE).toInt();
3408}
3409
3410QString ImagingPlanner::currentObjectNotes()
3411{
3412 auto index = ui->CatalogView->selectionModel()->currentIndex().siblingAtColumn(NOTES_COLUMN);
3413 const bool hasNotes = ui->CatalogView->model()->data(index, NOTES_ROLE).canConvert<QString>();
3414 if (!hasNotes)
3415 return QString();
3416 return ui->CatalogView->model()->data(index, NOTES_ROLE).toString();
3417}
3418
3419void ImagingPlanner::setCurrentObjectNotes(const QString &notes)
3420{
3421 auto index = ui->CatalogView->selectionModel()->currentIndex();
3422 if (!index.isValid())
3423 return;
3424 auto sibling = index.siblingAtColumn(NOTES_COLUMN);
3425
3426 auto sourceIndex = m_CatalogSortModel->mapToSource(sibling);
3427 QVariant n(notes);
3428 m_CatalogModel->setData(sourceIndex, n, NOTES_ROLE);
3429}
3430
3431ImagingPlannerPopup::ImagingPlannerPopup() : QMenu(nullptr)
3432{
3433}
3434
3435// The bools are pointers to we can have a 3-valued input parameter.
3436// If the pointer is a nullptr, then we say, for example it is neigher imaged, not not imaged.
3437// That is, really, some of the selection are imaged and some not imaged.
3438// If the pointer does point to a bool, then the value of that bool tells you if all the selection
3439// is (e.g.) imaged, or if all of it is not imaged.
3440void ImagingPlannerPopup::init(ImagingPlanner * planner, const QStringList &names,
3441 const bool * imaged, const bool * picked, const bool * ignored)
3442{
3443 clear();
3444 if (names.size() == 0) return;
3445
3446 QString title;
3447 if (names.size() == 1)
3448 title = names[0];
3449 else if (names.size() <= 3)
3450 {
3451 title = names[0];
3452 for (int i = 1; i < names.size(); i++)
3453 title.append(QString(", %1").arg(names[i]));
3454 }
3455 else
3456 title = i18n("%1, %2 and %3 other objects", names[0], names[1], names.size() - 2);
3457
3459
3460 QString word = names.size() == 1 ? names[0] : i18n("objects");
3461
3462 if (imaged == nullptr)
3463 {
3464 addAction(i18n("Mark %1 as NOT imaged", word), planner, &ImagingPlanner::setSelectionNotImaged);
3465 addAction(i18n("Mark %1 as already imaged", word), planner, &ImagingPlanner::setSelectionImaged);
3466 }
3467 else if (*imaged)
3468 addAction(i18n("Mark %1 as NOT imaged", word), planner, &ImagingPlanner::setSelectionNotImaged);
3469 else
3470 addAction(i18n("Mark %1 as already imaged", word), planner, &ImagingPlanner::setSelectionImaged);
3471
3472 if (picked == nullptr)
3473 {
3474 addAction(i18n("Un-pick %1", word), planner, &ImagingPlanner::setSelectionNotPicked);
3475 addAction(i18n("Pick %1", word), planner, &ImagingPlanner::setSelectionPicked);
3476 }
3477 else if (*picked)
3478 addAction(i18n("Un-pick %1", word), planner, &ImagingPlanner::setSelectionNotPicked);
3479 else
3480 addAction(i18n("Pick %1", word), planner, &ImagingPlanner::setSelectionPicked);
3481
3482
3483 if (ignored == nullptr)
3484 {
3485 addAction(i18n("Stop ignoring %1", word), planner, &ImagingPlanner::setSelectionNotIgnored);
3486 addAction(i18n("Ignore %1", word), planner, &ImagingPlanner::setSelectionIgnored);
3487
3488 }
3489 else if (*ignored)
3490 addAction(i18n("Stop ignoring %1", word), planner, &ImagingPlanner::setSelectionNotIgnored);
3491 else
3492 addAction(i18n("Ignore %1", word), planner, &ImagingPlanner::setSelectionIgnored);
3493
3494 addSeparator();
3495 addAction(i18n("Center %1 on SkyMap", names[0]), planner, &ImagingPlanner::reallyCenterOnSkymap);
3496 addSeparator();
3497 addAction(i18n("Screenshot some image of %1, plate-solve it, and temporarily place it on the SkyMap", names[0]), planner,
3498 &ImagingPlanner::takeScreenshot);
3499
3500}
3501
3502ImagingPlannerDBEntry::ImagingPlannerDBEntry(const QString &name, bool picked, bool imaged,
3503 bool ignored, const QString &notes) : m_Name(name), m_Notes(notes)
3504{
3505 setFlags(picked, imaged, ignored);
3506}
3507
3508ImagingPlannerDBEntry::ImagingPlannerDBEntry(const QString &name, int flags, const QString &notes)
3509 : m_Name(name), m_Flags(flags), m_Notes(notes)
3510{
3511}
3512
3513void ImagingPlannerDBEntry::getFlags(bool * picked, bool * imaged, bool * ignored)
3514{
3515 *picked = m_Flags & PickedBit;
3516 *imaged = m_Flags & ImagedBit;
3517 *ignored = m_Flags & IgnoredBit;
3518}
3519
3520
3521void ImagingPlannerDBEntry::setFlags(bool picked, bool imaged, bool ignored)
3522{
3523 m_Flags = 0;
3524 if (picked) m_Flags |= PickedBit;
3525 if (imaged) m_Flags |= ImagedBit;
3526 if (ignored) m_Flags |= IgnoredBit;
3527}
3528
3529void ImagingPlanner::saveToDB(const QString &name, bool picked, bool imaged,
3530 bool ignored, const QString &notes)
3531{
3532 ImagingPlannerDBEntry e(name, 0, notes);
3533 e.setFlags(picked, imaged, ignored);
3534 KStarsData::Instance()->userdb()->AddImagingPlannerEntry(e);
3535}
3536
3537void ImagingPlanner::saveToDB(const QString &name, int flags, const QString &notes)
3538{
3539 ImagingPlannerDBEntry e(name, flags, notes);
3540 KStarsData::Instance()->userdb()->AddImagingPlannerEntry(e);
3541}
3542
3543// KSUserDB::GetAllImagingPlannerEntries(QList<ImagingPlannerDBEntry> *entryList)
3544void ImagingPlanner::loadFromDB()
3545{
3546 // Disconnect the filter from the model, or else we'll re-filter numRows squared times.
3547 // Not as big a deal here because we're not touching all rows, just the rows with flags/notes.
3548 // Also see the reconnect below.
3549 m_CatalogSortModel->setSourceModel(nullptr);
3550
3551 auto tz = QTimeZone(getGeo()->TZ() * 3600);
3552 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
3553 KStarsDateTime ut = getGeo()->LTtoUT(KStarsDateTime(midnight));
3554 KSAlmanac ksal(ut, getGeo());
3555
3556 QList<ImagingPlannerDBEntry> list;
3557 KStarsData::Instance()->userdb()->GetAllImagingPlannerEntries(&list);
3558 QHash<QString, ImagingPlannerDBEntry> dbData;
3559 QHash<QString, int> dbNotes;
3560 for (const auto &entry : list)
3561 {
3562 dbData[entry.m_Name] = entry;
3563 }
3564
3565 int rows = m_CatalogModel->rowCount();
3566 for (int i = 0; i < rows; ++i)
3567 {
3568 const QString &name = m_CatalogModel->item(i, NAME_COLUMN)->text();
3569 auto entry = dbData.find(name);
3570 if (entry != dbData.end())
3571 {
3572 QVariant f = entry->m_Flags;
3573 m_CatalogModel->item(i, FLAGS_COLUMN)->setData(f, FLAGS_ROLE);
3574 if (entry->m_Flags & IMAGED_BIT)
3575 highlightImagedObject(m_CatalogModel->index(i, NAME_COLUMN), true);
3576 if (entry->m_Flags & PICKED_BIT)
3577 highlightPickedObject(m_CatalogModel->index(i, NAME_COLUMN), true);
3578 QVariant n = entry->m_Notes;
3579 m_CatalogModel->item(i, NOTES_COLUMN)->setData(n, NOTES_ROLE);
3580 }
3581 }
3582 // See above. Reconnect the filter to the model.
3583 m_CatalogSortModel->setSourceModel(m_CatalogModel.data());
3584}
3585
3586void ImagingPlanner::loadImagedFile()
3587{
3588 if (m_loadingCatalog)
3589 return;
3590
3591 focusOnTable();
3592 QString fileName = QFileDialog::getOpenFileName(this,
3593 tr("Open Already-Imaged File"), QDir::homePath(), tr("Any files (*)"));
3594 if (fileName.isEmpty())
3595 return;
3596 QFile inputFile(fileName);
3597 if (inputFile.open(QIODevice::ReadOnly))
3598 {
3599 int numSuccess = 0;
3600 QStringList failedNames;
3601 QTextStream in(&inputFile);
3602 while (!in.atEnd())
3603 {
3604 QString name = in.readLine().trimmed();
3605 if (name.isEmpty() || name.startsWith('#'))
3606 continue;
3607 name = tweakNames(name);
3608 if (getObject(name))
3609 {
3610 numSuccess++;
3611 auto startIndex = m_CatalogModel->index(0, NAME_COLUMN);
3612 QVariant value(name);
3613 auto matches = m_CatalogModel->match(startIndex, Qt::DisplayRole, value, 1, Qt::MatchFixedString);
3614 if (matches.size() > 0)
3615 {
3616 setFlag(matches[0], IMAGED_BIT, m_CatalogModel);
3617 highlightImagedObject(matches[0], true);
3618
3619 // Make sure we save it to the DB.
3620 QString name = m_CatalogModel->data(matches[0].siblingAtColumn(NAME_COLUMN)).toString();
3621 int flags = m_CatalogModel->data(matches[0].siblingAtColumn(FLAGS_COLUMN), FLAGS_ROLE).toInt();
3622 QString notes = m_CatalogModel->data(matches[0].siblingAtColumn(NOTES_COLUMN), NOTES_ROLE).toString();
3623 saveToDB(name, flags, notes);
3624 }
3625 else
3626 {
3627 DPRINTF(stderr, "ooops! internal inconsitency--got an object but match didn't work");
3628 }
3629 }
3630 else
3631 failedNames.append(name);
3632 }
3633 inputFile.close();
3634 if (failedNames.size() == 0)
3635 {
3636 if (numSuccess > 0)
3637 KSNotification::info(i18n("Successfully marked %1 objects as read", numSuccess));
3638 else
3639 KSNotification::sorry(i18n("Empty file"));
3640 }
3641 else
3642 {
3643 int num = std::min((int)failedNames.size(), 10);
3644 QString sample = QString("\"%1\"").arg(failedNames[0]);
3645 for (int i = 1; i < num; ++i)
3646 sample.append(QString(" \"%1\"").arg(failedNames[i]));
3647 if (numSuccess == 0 && failedNames.size() <= 10)
3648 KSNotification::sorry(i18n("Failed marking all of these objects imaged: %1", sample));
3649 else if (numSuccess == 0)
3650 KSNotification::sorry(i18n("Failed marking %1 objects imaged, including: %2", failedNames.size(), sample));
3651 else if (numSuccess > 0 && failedNames.size() <= 10)
3652 KSNotification::sorry(i18n("Succeeded marking %1 objects imaged. Failed with %2: %3",
3653 numSuccess, failedNames.size() == 1 ? "this" : "these", sample));
3654 else
3655 KSNotification::sorry(i18n("Succeeded marking %1 objects imaged. Failed with %2 including these: %3",
3656 numSuccess, failedNames.size(), sample));
3657 }
3658 }
3659 else
3660 {
3661 KSNotification::sorry(i18n("Sorry, couldn't open file: \"%1\"", fileName));
3662 }
3663}
3664
3665void ImagingPlanner::addCatalogImageInfo(const CatalogImageInfo &info)
3666{
3667 m_CatalogImageInfoMap[info.m_Name.toLower()] = info;
3668}
3669
3670bool ImagingPlanner::findCatalogImageInfo(const QString &name, CatalogImageInfo * info)
3671{
3672 auto result = m_CatalogImageInfoMap.find(name.toLower());
3673 if (result == m_CatalogImageInfoMap.end())
3674 return false;
3675 if (result->m_Filename.isEmpty())
3676 return false;
3677 *info = *result;
3678 return true;
3679}
3680
3681void ImagingPlanner::loadCatalogViaMenu()
3682{
3683 QString startDir = Options::imagingPlannerCatalogPath();
3684 if (startDir.isEmpty())
3685 startDir = defaultDirectory();
3686
3687 QString path = QFileDialog::getOpenFileName(this, tr("Open Catalog File"), startDir, tr("Any files (*.csv)"));
3688 if (path.isEmpty())
3689 return;
3690
3691 loadCatalog(path);
3692}
3693
3694void ImagingPlanner::loadCatalog(const QString &path)
3695{
3696 removeEventFilters();
3697
3698 // This tool occassionally crashed when UI interactions happen during catalog loading.
3699 // Don't know why, but disabling that, and re-enabling after load below.
3700 setEnabled(false);
3701 setFixedSize(this->width(), this->height());
3702
3703 m_loadingCatalog = true;
3704 loadCatalogFromFile(path);
3705 catalogLoaded();
3706
3707 // Re-enable UI
3708 setEnabled(true);
3709 setMinimumSize(0, 0);
3711
3712 m_loadingCatalog = false;
3713 installEventFilters();
3714
3715 // Select and display the first row
3716 if (m_CatalogSortModel->rowCount() > 0)
3717 {
3718 auto name = m_CatalogSortModel->index(0, 0).data().toString();
3719 scrollToName(name);
3720 QItemSelection selection, deselection;
3721 selection.select(m_CatalogSortModel->index(0, 0), m_CatalogSortModel->index(0, 0));
3722 selectionChanged(selection, deselection);
3723 }
3724}
3725
3726CatalogImageInfo::CatalogImageInfo(const QString &csv)
3727{
3728 QString line = csv.trimmed();
3729 if (line.isEmpty() || line.startsWith('#'))
3730 return;
3731 QStringList columns = line.split(",");
3732 if (columns.size() < 1 || columns[0].isEmpty())
3733 return;
3734 int column = 0;
3735 m_Name = columns[column++];
3736 if (columns.size() <= column) return;
3737 m_Filename = columns[column++];
3738 if (columns.size() <= column) return;
3739 m_Author = columns[column++];
3740 if (columns.size() <= column) return;
3741 m_Link = columns[column++];
3742 if (columns.size() <= column) return;
3743 m_License = columns[column++];
3744}
3745
3746// This does the following:
3747// - Clears the internal catalog
3748// - Initializes the m_CatalogImageInfoMap, which goes from name to image.
3749// - Loads in new objects into the internal catalog.
3750//
3751// CSV File Columns:
3752// 1: ID: M 1
3753// 2: Image Filename: M_1.jpg
3754// 3: Author: Hy Murveit
3755// 4: Link: https://www.astrobin.com/x3utgw/F/
3756// 5: License: ACC (possibilities are ACC,ANCCC,ASACC,ANCCC,ANCSACC)
3757// last one is Attribution Non-Commercial hare-Alike Creative Commons
3758// Currently ID is mandatory, if there is an image filename, then Author,Link,and License
3759// are also required, though could be blank.
3760// - Comment lines start with #
3761// - Can include another catalog with "LoadCatalog FILENAME"
3762// - Can request loading a provided KStars DSO catalog with "LoadDSOCatalog FILENAME"
3763// - Can request removing a KStars DSO catalog given its ID (presumably an old version).
3764void ImagingPlanner::loadCatalogFromFile(QString path, bool reset)
3765{
3766 QFile inputFile(path);
3767 if (reset)
3768 {
3769 m_numWithImage = 0;
3770 m_numMissingImage = 0;
3771 }
3772 int numMissingImage = 0, numWithImage = 0;
3773 if (!inputFile.exists())
3774 {
3775 emit popupSorry(i18n("Sorry, catalog file doesn't exist: \"%1\"", path));
3776 return;
3777 }
3778 QStringList objectNames;
3779 if (inputFile.open(QIODevice::ReadOnly))
3780 {
3781 const auto tz = QTimeZone(getGeo()->TZ() * 3600);
3782 const KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
3783 const KStarsDateTime ut = getGeo()->LTtoUT(KStarsDateTime(midnight));
3784 const KSAlmanac ksal(ut, getGeo());
3785
3786 if (reset)
3787 {
3788 Options::setImagingPlannerCatalogPath(path);
3789 Options::self()->save();
3790 if (m_CatalogModel->rowCount() > 0)
3791 m_CatalogModel->removeRows(0, m_CatalogModel->rowCount());
3792 clearObjects();
3793 }
3794 QTextStream in(&inputFile);
3795 while (!in.atEnd())
3796 {
3797 CatalogImageInfo info(in.readLine().trimmed());
3798 const QString name = info.m_Name;
3799 if (name.isEmpty())
3800 continue;
3801 if (name.startsWith("LoadCatalog"))
3802 {
3803 // This line isn't a normal entry, but rather points to another catalog.
3804 // Load that catalog and then skip this line.
3805 QRegularExpression re("^LoadCatalog\\s+(\\S+)", QRegularExpression::CaseInsensitiveOption);
3806 const auto match = re.match(name);
3807 if (match.hasMatch())
3808 {
3809 const QString catFilename = match.captured(1);
3810 if (catFilename.isEmpty()) continue;
3811 const QFileInfo fInfo(catFilename);
3812
3813 QString catFullPath = catFilename;
3814 if (!fInfo.isAbsolute())
3815 {
3816 const QString catDir = QFileInfo(path).absolutePath();
3817 catFullPath = QString("%1%2%3").arg(catDir)
3818 .arg(QDir::separator()).arg(match.captured(1));
3819 }
3820 if (catFullPath != path)
3821 loadCatalogFromFile(catFullPath, false);
3822 }
3823 continue;
3824 }
3825 if (name.startsWith("LoadDSOCatalog"))
3826 {
3827 // This line isn't a normal entry, but rather points to a DSO catalog
3828 // (that is, a standard KStars sky-object catalog)
3829 // which may be helpful to avoid a lot fetching of coordinates from online sources.
3830 QRegularExpression re("^LoadDSOCatalog\\s+(\\S+)", QRegularExpression::CaseInsensitiveOption);
3831 const auto match = re.match(name);
3832 if (match.hasMatch())
3833 {
3834 const QString catFilename = match.captured(1);
3835 if (catFilename.isEmpty()) continue;
3836 const QFileInfo fInfo(catFilename);
3837
3838 QString catFullPath = catFilename;
3839 if (!fInfo.isAbsolute())
3840 {
3841 const QString catDir = QFileInfo(path).absolutePath();
3842 catFullPath = QString("%1%2%3").arg(catDir)
3843 .arg(QDir::separator()).arg(match.captured(1));
3844 }
3845 std::pair<bool, QString> out = m_manager.import_catalog(catFullPath, false);
3846 DPRINTF(stderr, "Load of KStars catalog %s %s%s\n", catFullPath.toLatin1().data(),
3847 out.first ? "succeeded." : "failed: ", out.second.toLatin1().data());
3848 }
3849 continue;
3850 }
3851 if (name.startsWith("RemoveDSOCatalog"))
3852 {
3853 // This line isn't a normal entry, but rather points to an ID of an old DSO catalog
3854 // which presumably a current DSO Catalog replaces.
3855 QRegularExpression re("^RemoveDSOCatalog\\s+(\\S+)", QRegularExpression::CaseInsensitiveOption);
3856 const auto match = re.match(name);
3857 if (match.hasMatch())
3858 {
3859 const QString catIDstr = match.captured(1);
3860 if (catIDstr.isEmpty()) continue;
3861
3862 bool ok;
3863 const int catID = catIDstr.toInt(&ok);
3864 if (ok && m_manager.catalog_exists(catID))
3865 {
3866 const std::pair<bool, QString> out = m_manager.remove_catalog(catID);
3867 DPRINTF(stderr, "Removal of out-of-date catalog %d %s%s\n", catID,
3868 out.first ? "succeeded." : "failed: ", out.second.toLatin1().data());
3869 }
3870 }
3871 continue;
3872 }
3873 objectNames.append(name);
3874 if (!info.m_Filename.isEmpty())
3875 {
3876 numWithImage++;
3877 QFileInfo fInfo(info.m_Filename);
3878 if (fInfo.isRelative())
3879 info.m_Filename = QString("%1%2%3").arg(QFileInfo(path).absolutePath())
3880 .arg(QDir::separator()).arg(info.m_Filename);
3881 addCatalogImageInfo(info);
3882 }
3883 else
3884 {
3885 numMissingImage++;
3886 DPRINTF(stderr, "No catalog image for %s\n", name.toLatin1().data());
3887 }
3889 }
3890 inputFile.close();
3891
3892 int num = 0, numBad = 0, iteration = 0;
3893 // Move to threaded thing??
3894 for (const auto &name : objectNames)
3895 {
3896 setStatus(i18n("%1/%2: Adding %3", ++iteration, objectNames.size(), name));
3897 if (addCatalogItem(ksal, name, 0)) num++;
3898 else
3899 {
3900 DPRINTF(stderr, "Couldn't add %s\n", name.toLatin1().data());
3901 numBad++;
3902 }
3903 }
3904 m_numWithImage += numWithImage;
3905 m_numMissingImage += numMissingImage;
3906 DPRINTF(stderr, "Catalog %s: %d of %d have catalog images\n",
3907 path.toLatin1().data(), numWithImage, numWithImage + numMissingImage);
3908 }
3909 else
3910 {
3911 emit popupSorry(i18n("Sorry, couldn't open file: \"%1\"", path));
3912 }
3913}
3914
3915void ImagingPlanner::sorry(const QString &message)
3916{
3917 KSNotification::sorry(message);
3918}
3919
3920void ImagingPlanner::captureRegion(const QImage &screenshot)
3921{
3922 if (m_PlateSolve.get()) disconnect(m_PlateSolve.get());
3923
3924 // This code to convert the screenshot to a FITSData is, of course, convoluted.
3925 // TODO: improve it.
3926 m_ScreenShotImage = screenshot;
3927 QString temporaryPath = QDir(KSPaths::writableLocation(QStandardPaths::TempLocation) + QDir::separator() +
3928 qAppName()).path();
3929 QString tempQImage = QDir(temporaryPath).filePath("screenshot.png");
3930 m_ScreenShotImage.save(tempQImage);
3931 FITSData::ImageToFITS(tempQImage, "png", m_ScreenShotFilename);
3932 this->raise();
3933 this->activateWindow();
3934
3935 if (!m_PlateSolve.get())
3936 m_PlateSolve.reset(new PlateSolve(this));
3937
3938 m_PlateSolve->setImageDisplay(m_ScreenShotImage);
3939 if (currentCatalogObject())
3940 {
3941 m_PlateSolve->setPosition(*currentCatalogObject());
3942 m_PlateSolve->setUsePosition(true);
3943 m_PlateSolve->setUseScale(false);
3944 m_PlateSolve->setLinear(false);
3945 reallyCenterOnSkymap();
3946 }
3947
3948 m_PlateSolve->setWindowTitle(QString("Plate Solve for %1").arg(currentObjectName()));
3949 m_PlateSolve->show();
3950 if (Options::imagingPlannerStartSolvingImmediately())
3951 extractImage();
3952}
3953
3954void ImagingPlanner::takeScreenshot()
3955{
3956 if (!currentCatalogObject())
3957 return;
3958
3959 const QString messageID = "ImagingPlannerScreenShotInfo";
3960 const QString screenshotInfo =
3961 QString("<p><b>Taking a screenshot of %1 for the SkyMap</b></p>"
3962 "<p>This allows you to screenshot/copy a good example image of %1 from another application, "
3963 "such as a browser viewing %1 on Astrobin. It then plate-solves that screenshot and overlays "
3964 "it temporarily on the SkyMap.</p>"
3965 "<p>You can use this to help you frame your future %1 capture. "
3966 "The SkyMap overlay will only be visible in the current KStars session.</p>"
3967 "<p>In order to do this, you should make the image you wish to copy visible "
3968 "on your screen now, before clicking OK. After you click OK you will see the mouse pointer change "
3969 "to the screenshot pointer. You then drag your mouse over the part of the %1 image "
3970 "you wish to copy. If you check do-not-ask-again, then you must make sure that your desired image "
3971 "is already visible before you run this.</p>"
3972 "<p>After you take your screenshot, the system will bring up a menu to help plate-solve the image. "
3973 "Click SOLVE on that menu to start the process, unless it is automatically started. "
3974 "Once successfully plate-solved, your image will be overlayed onto the SkyMap.").arg(currentObjectName());
3975#if KIO_VERSION >= QT_VERSION_CHECK(5, 100, 0)
3977 screenshotInfo,
3978 "ScreenShot",
3979 KGuiItem(i18nc("@action:button", "OK")),
3980 KGuiItem(i18nc("@action:button", "Cancel")),
3981 messageID
3982 )
3984#else
3985 const int result = KMessageBox::questionYesNo(this, screenshotInfo, "ScreenShot",
3986 KGuiItem("OK"), KGuiItem("Cancel"), messageID);
3987 if (result != KMessageBox::Yes)
3988#endif
3989 {
3990 // Don't remember the cancel response.
3991 KMessageBox::enableMessage(messageID);
3992 disconnect(m_CaptureWidget.get());
3993 m_CaptureWidget.reset();
3994 this->raise();
3995 this->activateWindow();
3996 return;
3997 }
3998
3999 if (m_CaptureWidget.get()) disconnect(m_CaptureWidget.get());
4000 m_CaptureWidget.reset(new ScreenCapture());
4001 QObject::connect(m_CaptureWidget.get(), &ScreenCapture::areaSelected,
4002 this, &ImagingPlanner::captureRegion, Qt::UniqueConnection);
4003 disconnect(m_CaptureWidget.get(), &ScreenCapture::aborted, nullptr, nullptr);
4004 QObject::connect(m_CaptureWidget.get(), &ScreenCapture::aborted, this, [this]()
4005 {
4006 disconnect(m_CaptureWidget.get());
4007 m_CaptureWidget.reset();
4008 this->raise();
4009 this->activateWindow();
4010 });
4011 m_CaptureWidget->show();
4012}
4013
4014void ImagingPlanner::extractImage()
4015{
4016 disconnect(m_PlateSolve.get(), &PlateSolve::solverFailed, nullptr, nullptr);
4017 connect(m_PlateSolve.get(), &PlateSolve::solverFailed, this, [this]()
4018 {
4019 disconnect(m_PlateSolve.get());
4020 });
4021 disconnect(m_PlateSolve.get(), &PlateSolve::solverSuccess, nullptr, nullptr);
4022 connect(m_PlateSolve.get(), &PlateSolve::solverSuccess, this, [this]()
4023 {
4024 disconnect(m_PlateSolve.get());
4025 const FITSImage::Solution &solution = m_PlateSolve->solution();
4026 ImageOverlay overlay;
4027 overlay.m_Orientation = solution.orientation;
4028 overlay.m_RA = solution.ra;
4029 overlay.m_DEC = solution.dec;
4030 overlay.m_ArcsecPerPixel = solution.pixscale;
4031 overlay.m_EastToTheRight = solution.parity;
4032 overlay.m_Status = ImageOverlay::AVAILABLE;
4033
4034 const bool mirror = !solution.parity;
4035 const int scaleWidth = std::min(m_ScreenShotImage.width(), Options::imageOverlayMaxDimension());
4036 QImage *processedImg = new QImage;
4037 if (mirror)
4038 *processedImg = m_ScreenShotImage.mirrored(true, false).scaledToWidth(scaleWidth); // It's reflected horizontally.
4039 else
4040 *processedImg = m_ScreenShotImage.scaledToWidth(scaleWidth);
4041 overlay.m_Img.reset(processedImg);
4042 overlay.m_Width = processedImg->width();
4043 overlay.m_Height = processedImg->height();
4044 KStarsData::Instance()->skyComposite()->imageOverlay()->show();
4045 KStarsData::Instance()->skyComposite()->imageOverlay()->addTemporaryImageOverlay(overlay);
4046 centerOnSkymap();
4047 KStars::Instance()->activateWindow();
4048 KStars::Instance()->raise();
4049 m_PlateSolve->close();
4050 });
4051 m_PlateSolve->solveImage(m_ScreenShotFilename);
4052}
a dms subclass that caches its sine and cosine values every time the angle is changed.
Definition cachingdms.h:19
A simple container object to hold the minimum information for a Deep Sky Object to be drawn on the sk...
float a() const
float b() const
CatalogObject & insertStaticObject(const CatalogObject &obj)
Insert an object obj into m_static_objects and return a reference to the newly inserted object.
static QString processSearchText(QString searchText)
Do some post processing on the search text to interpret what the user meant This could include replac...
int size()
Return the numbers of flags.
void remove(int index)
Remove a flag.
void add(const SkyPoint &flagPoint, QString epoch, QString image, QString label, QColor labelColor)
Add a flag.
Contains all relevant information for specifying a location on Earth: City Name, State/Province name,...
Definition geolocation.h:28
A class that implements methods to find sun rise, sun set, twilight begin / end times,...
Definition ksalmanac.h:27
Provides necessary information about the Moon.
Definition ksmoon.h:26
double illum() const
Definition ksmoon.h:49
void findPhase(const KSSun *Sun=nullptr)
Determine the phase angle of the moon, and assign the appropriate moon image.
Definition ksmoon.cpp:268
const QImage & image() const
void updateCoords(const KSNumbers *num, bool includePlanets=true, const CachingDms *lat=nullptr, const CachingDms *LST=nullptr, bool forceRecompute=false) override
Update position of the planet (reimplemented from SkyPoint)
bool AddImagingPlannerEntry(const ImagingPlannerDBEntry &entry)
Adds a new Imaging Planner row into the database.
bool GetAllImagingPlannerEntries(QList< ImagingPlannerDBEntry > *entryList)
Gets all the Imaging Planner rows from the database.
KSUserDB * userdb()
Definition kstarsdata.h:223
const KStarsDateTime & ut() const
Definition kstarsdata.h:159
Q_INVOKABLE SimClock * clock()
Definition kstarsdata.h:226
GeoLocation * geo()
Definition kstarsdata.h:238
SkyMapComposite * skyComposite()
Definition kstarsdata.h:174
Extension of QDateTime for KStars KStarsDateTime can represent the date/time as a Julian Day,...
KStarsDateTime addSecs(double s) const
void setDate(const QDate &d)
Assign the Date according to a QDate object.
long double djd() const
static KStars * Instance()
Definition kstars.h:122
KStarsData * data() const
Definition kstars.h:134
const KStarsDateTime & utc() const
Definition simclock.h:35
SkyObject * findByName(const QString &name, bool exact=true) override
Search the children of this SkyMapComposite for a SkyObject whose name matches the argument.
void setClickedPoint(const SkyPoint *f)
Set the ClickedPoint to the skypoint given as an argument.
Definition skymap.cpp:1052
void setClickedObject(SkyObject *o)
Set the ClickedObject pointer to the argument.
Definition skymap.cpp:399
void setFocusObject(SkyObject *o)
Set the FocusObject pointer to the argument.
Definition skymap.cpp:404
void slotCenter()
Center the display at the point ClickedPoint.
Definition skymap.cpp:413
Provides all necessary information about an object in the sky: its coordinates, name(s),...
Definition skyobject.h:50
virtual QString name(void) const
Definition skyobject.h:154
virtual QString longname(void) const
Definition skyobject.h:182
QString name2(void) const
Definition skyobject.h:168
int type(void) const
Definition skyobject.h:212
QString typeName() const
TYPE
The type classification of the SkyObject.
Definition skyobject.h:120
The sky coordinates of a point in the sky.
Definition skypoint.h:45
const CachingDms & dec() const
Definition skypoint.h:269
const CachingDms & ra0() const
Definition skypoint.h:251
virtual void updateCoordsNow(const KSNumbers *num)
updateCoordsNow Shortcut for updateCoords( const KSNumbers *num, false, nullptr, nullptr,...
Definition skypoint.h:410
void setRA(dms &r)
Sets RA, the current Right Ascension.
Definition skypoint.h:144
const CachingDms & ra() const
Definition skypoint.h:263
dms angularDistanceTo(const SkyPoint *sp, double *const positionAngle=nullptr) const
Computes the angular distance between two SkyObjects.
Definition skypoint.cpp:919
void EquatorialToHorizontal(const CachingDms *LST, const CachingDms *lat)
Determine the (Altitude, Azimuth) coordinates of the SkyPoint from its (RA, Dec) coordinates,...
Definition skypoint.cpp:77
void setRA0(dms r)
Sets RA0, the catalog Right Ascension.
Definition skypoint.h:94
const dms & alt() const
Definition skypoint.h:281
const CachingDms & dec0() const
Definition skypoint.h:257
void setDec0(dms d)
Sets Dec0, the catalog Declination.
Definition skypoint.h:119
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
int minute() const
Definition dms.cpp:221
int hour() const
Definition dms.h:147
const double & Degrees() const
Definition dms.h:141
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
Type type(const QSqlDatabase &db)
StartupCondition
Conditions under which a SchedulerJob may start.
QMap< QString, uint16_t > CapturedFramesMap
mapping signature --> frames count
CompletionCondition
Conditions under which a SchedulerJob may complete.
KCRASH_EXPORT void setFlags(KCrash::CrashFlags flags)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
KIOCORE_EXPORT CopyJob * move(const QList< QUrl > &src, const QUrl &dest, JobFlags flags=DefaultFlags)
KIOCORE_EXPORT QString number(KIO::filesize_t size)
KIOCORE_EXPORT CopyJob * link(const QList< QUrl > &src, const QUrl &destDir, JobFlags flags=DefaultFlags)
GeoCoordinates geo(const QVariant &location)
QString path(const QString &relativePath)
ButtonCode questionTwoActions(QWidget *parent, const QString &text, const QString &title, const KGuiItem &primaryAction, const KGuiItem &secondaryAction, const QString &dontAskAgainName=QString(), Options options=Notify)
void enableMessage(const QString &dontShowAgainName)
KIOCORE_EXPORT QString dir(const QString &fileClass)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
QString name(StandardAction id)
QString label(StandardShortcut id)
const QList< QKeySequence > & end()
void initialize(StandardShortcut id)
std::pair< bool, CatalogObject > resolveName(const QString &name)
Resolve the name of the given DSO and extract data from various sources.
void setChecked(bool)
void clicked(bool checked)
void toggled(bool checked)
virtual QVariant data(const QModelIndex &index, int role) const const=0
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const=0
virtual QModelIndex parent(const QModelIndex &index) const const=0
virtual bool setData(const QModelIndex &index, const QVariant &value, int role)
void editingFinished()
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
char * data()
qsizetype length() const const
QByteArray & replace(QByteArrayView before, QByteArrayView after)
void resize(qsizetype newSize, char c)
qsizetype size() const const
QByteArray toBase64(Base64Options options) const const
std::string toStdString() const const
void processEvents(QEventLoop::ProcessEventsFlags flags)
QDate addDays(qint64 ndays) const const
QString toString(QStringView format, QCalendar cal) const const
QDateTime addSecs(qint64 s) const const
bool isValid() const const
qint64 secsTo(const QDateTime &other) const const
void setTimeZone(const QTimeZone &toZone)
void dateChanged(QDate date)
bool openUrl(const QUrl &url)
int result() const const
QString absolutePath() const const
QFileInfoList entryInfoList(Filters filters, SortFlags sort) const const
bool exists() const const
QString homePath()
bool mkpath(const QString &dirPath) const const
QChar separator()
qint64 elapsed() const const
int exec(ProcessEventsFlags flags)
void quit()
QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, Options options)
QDateTime birthTime() const const
iterator end()
iterator find(const Key &key)
void sectionPressed(int logicalIndex)
QIcon fromTheme(const QString &name)
int height() const const
bool save(QIODevice *device, const char *format, int quality) const const
QImage scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
QImage scaledToHeight(int height, Qt::TransformationMode mode) const const
int width() const const
Qt::KeyboardModifiers modifiers() const const
QModelIndexList indexes() const const
void select(const QModelIndex &topLeft, const QModelIndex &bottomRight)
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
void append(QList< T > &&value)
void clear()
qsizetype indexOf(const AT &value, qsizetype from) const const
void push_back(parameter_type value)
qsizetype size() const const
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
QAction * addSection(const QIcon &icon, const QString &text)
QAction * addSeparator()
void clear()
bool isValid() const const
const QAbstractItemModel * model() const const
QModelIndex siblingAtColumn(int column) const const
int globalX() const const
int globalY() const const
NetworkError error() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
bool disconnect(const QMetaObject::Connection &connection)
QString tr(const char *sourceText, const char *disambiguation, int n)
QPixmap fromImage(QImage &&image, Qt::ImageConversionFlags flags)
QPixmap scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformMode) const const
qsizetype capturedStart(QStringView name) const const
bool hasMatch() const const
Qt::MouseButton button() const const
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
virtual QVariant data(int role) const const
virtual void setData(const QVariant &value, int role)
void setTextAlignment(Qt::Alignment alignment)
QString & append(QChar ch)
QString arg(Args &&... args) const const
void clear()
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
QChar * data()
bool isEmpty() const const
QString mid(qsizetype position, qsizetype n) const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
qsizetype size() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
int toInt(bool *ok, int base) const const
QByteArray toLatin1() const const
QString toLower() const const
QString toUpper() const const
QByteArray toUtf8() const const
QString trimmed() const const
AlignHCenter
KeepAspectRatio
CaseInsensitive
UniqueConnection
StrongFocus
DisplayRole
Key_Enter
typedef MatchFlags
LeftButton
DescendingOrder
SkipEmptyParts
SmoothTransformation
QTextStream & dec(QTextStream &stream)
QTextStream & endl(QTextStream &stream)
QTextStream & fixed(QTextStream &stream)
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
void keyEvent(KeyAction action, QWidget *widget, Qt::Key key, Qt::KeyboardModifiers modifier, int delay)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
bool isValid(int h, int m, int s, int ms)
QString toString(QStringView format) const const
void setInterval(int msec)
bool isActive() const const
void start()
void stop()
void timeout()
bool isEmpty() const const
bool canConvert() const const
int toInt(bool *ok) const const
QString toString() const const
QWIDGETSIZE_MAXQWIDGETSIZE_MAX
void activateWindow()
void adjustSize()
virtual bool event(QEvent *event) override
void setMaximumSize(const QSize &)
void setMinimumSize(const QSize &)
void raise()
void setFixedSize(const QSize &s)
virtual void showEvent(QShowEvent *event)
void resize(const QSize &)
virtual void setVisible(bool visible)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 25 2025 11:58:40 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.