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

KDE's Doxygen guidelines are available online.