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 "artificialhorizoncomponent.h"
10#include "auxiliary/thememanager.h"
11#include "catalogscomponent.h"
12#include "constellationboundarylines.h"
13#include "dialogs/detaildialog.h"
14#include "dialogs/finddialog.h"
15// TODO: replace this. See comment above SchedulerUtils_setupJob().
16//#include "ekos/scheduler/schedulerutils.h"
17#include "ekos/scheduler/schedulerjob.h"
18
19// These are just for the debugging method checkTargets()
20#include "flagmanager.h"
21#include "flagcomponent.h"
22
23#include "nameresolver.h"
24#include "imagingplanneroptions.h"
25#include "kplotwidget.h"
26#include "kplotobject.h"
27#include "kplotaxis.h"
28#include "ksalmanac.h"
29#include "ksmoon.h"
30#include "ksnotification.h"
31#include <kspaths.h>
32#include "kstars.h"
33#include "ksuserdb.h"
34#include "kstarsdata.h"
35#include "skymap.h"
36#include "skymapcomposite.h"
37
38#include <QDesktopServices>
39#include <QDialog>
40#include <QDir>
41#include <QFileDialog>
42#include <QImage>
43#include <QRegularExpression>
44#include <QSortFilterProxyModel>
45#include <QStandardItemModel>
46#include <QStringList>
47#include <QWidget>
48#include "zlib.h"
49
50#define DPRINTF if (false) fprintf
51
52// For now, skip the threading. It is more stable this way.
53// #define THREADED_LOAD_CATALOG
54
55// Data columns in the model.
56// Must agree with the header string near the start of initialize()
57// and the if/else-if test values in addCatalogItem().
58namespace
59{
60enum ColumnNames
61{
62 NAME_COLUMN = 0,
63 HOURS_COLUMN,
64 TYPE_COLUMN,
65 SIZE_COLUMN,
66 ALTITUDE_COLUMN,
67 MOON_COLUMN,
68 CONSTELLATION_COLUMN,
69 COORD_COLUMN,
70 FLAGS_COLUMN,
71 NOTES_COLUMN,
72 LAST_COLUMN
73};
74}
75
76// These could probably all be Qt::UserRole + 1
77#define TYPE_ROLE (Qt::UserRole + 1)
78#define HOURS_ROLE (Qt::UserRole + 2)
79#define SIZE_ROLE (Qt::UserRole + 3)
80#define ALTITUDE_ROLE (Qt::UserRole + 4)
81#define MOON_ROLE (Qt::UserRole + 5)
82#define FLAGS_ROLE (Qt::UserRole + 6)
83#define NOTES_ROLE (Qt::UserRole + 7)
84
85#define PICKED_BIT ImagingPlannerDBEntry::PickedBit
86#define IMAGED_BIT ImagingPlannerDBEntry::ImagedBit
87#define IGNORED_BIT ImagingPlannerDBEntry::IgnoredBit
88
89/**********************************************************
90TODO/Ideas:
91
92Log at bottom?
93Imaging time constraint in hours calc
94Maybe options with slew to center checkbox
95Think about moving some or all of the filtering to menus in the column headers
96Norder download link, sort of:
97 https://indilib.org/forum/general/11766-dss-offline-hips.html?start=0
98See if I can just use UserRole or UserRole+1 for the non-display roles.
99Altitude graph has some replicated code with the scheduler
100Weird timezone stuff when setting kstars to a timezone that's not the system's timezone.
101Add a catalog name, and display it?
102***********************************************************/
103
104namespace
105{
106
107// Checks the appropriate Options variable to see if the object-type
108// should be displayed.
109bool acceptType(SkyObject::TYPE type)
110{
111 switch (type)
112 {
113 case SkyObject::OPEN_CLUSTER:
114 return Options::imagingPlannerAcceptOpenCluster();
115 case SkyObject::GLOBULAR_CLUSTER:
116 return Options::imagingPlannerAcceptGlobularCluster();
117 case SkyObject::GASEOUS_NEBULA:
118 return Options::imagingPlannerAcceptNebula();
119 case SkyObject::PLANETARY_NEBULA:
120 return Options::imagingPlannerAcceptPlanetary();
121 case SkyObject::SUPERNOVA_REMNANT:
122 return Options::imagingPlannerAcceptSupernovaRemnant();
123 case SkyObject::GALAXY:
124 return Options::imagingPlannerAcceptGalaxy();
125 case SkyObject::GALAXY_CLUSTER:
126 return Options::imagingPlannerAcceptGalaxyCluster();
127 case SkyObject::DARK_NEBULA:
128 return Options::imagingPlannerAcceptDarkNebula();
129 default:
130 return Options::imagingPlannerAcceptOther();
131 }
132}
133
134bool getFlag(const QModelIndex &index, int bit, QAbstractItemModel *model)
135{
136 auto idx = index.siblingAtColumn(FLAGS_COLUMN);
137 const bool hasFlags = model->data(idx, FLAGS_ROLE).canConvert<int>();
138 if (!hasFlags)
139 return false;
140 const bool flag = model->data(idx, FLAGS_ROLE).toInt() & bit;
141 return flag;
142}
143
144void setFlag(const QModelIndex &index, int bit, QAbstractItemModel *model)
145{
146 auto idx = index.siblingAtColumn(FLAGS_COLUMN);
147 const bool hasFlags = model->data(idx, FLAGS_ROLE).canConvert<int>();
148 int currentFlags = 0;
149 if (hasFlags)
150 currentFlags = model->data(idx, FLAGS_ROLE).toInt();
151 QVariant val(currentFlags | bit);
152 model->setData(idx, val, FLAGS_ROLE);
153}
154
155void clearFlag(const QModelIndex &index, int bit, QAbstractItemModel *model)
156{
157 auto idx = index.siblingAtColumn(FLAGS_COLUMN);
158 const bool hasFlags = model->data(idx, FLAGS_ROLE).canConvert<int>();
159 if (!hasFlags)
160 return;
161 const int currentFlags = model->data(idx, FLAGS_ROLE).toInt();
162 QVariant val(currentFlags & ~bit);
163 model->setData(idx, val, FLAGS_ROLE);
164}
165
166QString flagString(int flags)
167{
168 QString str;
169 if (flags & IMAGED_BIT) str.append(i18n("Imaged"));
170 if (flags & PICKED_BIT)
171 {
172 if (str.size() != 0)
173 str.append(", ");
174 str.append(i18n("Picked"));
175 }
176 if (flags & IGNORED_BIT)
177 {
178 if (str.size() != 0)
179 str.append(", ");
180 str.append(i18n("Ignored"));
181 }
182 return str;
183}
184
185// The next 3 methods condense repeated code needed for the filtering checkboxes.
186void setupShowCallback(bool checked,
187 void (*showOption)(bool), void (*showNotOption)(bool),
188 void (*dontCareOption)(bool),
189 QCheckBox *showCheckbox, QCheckBox *showNotCheckbox,
190 QCheckBox *dontCareCheckbox)
191{
192 Q_UNUSED(showCheckbox);
193 if (checked)
194 {
195 showOption(true);
196 showNotOption(false);
197 dontCareOption(false);
198 showNotCheckbox->setChecked(false);
199 dontCareCheckbox->setChecked(false);
200 Options::self()->save();
201 }
202 else
203 {
204 showOption(false);
205 showNotOption(false);
206 dontCareOption(true);
207 showNotCheckbox->setChecked(false);
208 dontCareCheckbox->setChecked(true);
209 Options::self()->save();
210 }
211}
212
213void setupShowNotCallback(bool checked,
214 void (*showOption)(bool), void (*showNotOption)(bool), void (*dontCareOption)(bool),
215 QCheckBox *showCheckbox, QCheckBox *showNotCheckbox, QCheckBox *dontCareCheckbox)
216{
217 Q_UNUSED(showNotCheckbox);
218 if (checked)
219 {
220 showOption(false);
221 showNotOption(true);
222 dontCareOption(false);
223 showCheckbox->setChecked(false);
224 dontCareCheckbox->setChecked(false);
225 Options::self()->save();
226 }
227 else
228 {
229 showOption(false);
230 showNotOption(false);
231 dontCareOption(true);
232 showCheckbox->setChecked(false);
233 dontCareCheckbox->setChecked(true);
234 Options::self()->save();
235 }
236}
237
238void setupDontCareCallback(bool checked,
239 void (*showOption)(bool), void (*showNotOption)(bool), void (*dontCareOption)(bool),
240 QCheckBox *showCheckbox, QCheckBox *showNotCheckbox, QCheckBox *dontCareCheckbox)
241{
242 if (checked)
243 {
244 showOption(false);
245 showNotOption(false);
246 dontCareOption(true);
247 showCheckbox->setChecked(false);
248 showNotCheckbox->setChecked(false);
249 Options::self()->save();
250 }
251 else
252 {
253 // Yes, the user just set this to false, but
254 // there's no obvious way to tell what the user wants.
255 showOption(false);
256 showNotOption(false);
257 dontCareOption(true);
258 showCheckbox->setChecked(false);
259 showNotCheckbox->setChecked(false);
260 dontCareCheckbox->setChecked(true);
261 Options::self()->save();
262 }
263}
264
265// Find the nth url inside a QString. Returns an empty QString if none is found.
266QString findUrl(const QString &input, int nth = 1)
267{
268 // Got the RE by asking Google's AI!
269 QRegularExpression re("https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._"
270 "\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)");
271
272 re.setPatternOptions(QRegularExpression::MultilineOption |
275 auto match = re.match(input);
276 if (!match.hasMatch())
277 return QString();
278 else if (nth == 1)
279 return(match.captured(0));
280
281 QString inp = input;
282 while (--nth >= 1)
283 {
284 inp = inp.mid(match.capturedEnd());
285 match = re.match(inp);
286 if (!match.hasMatch())
287 return QString();
288 else if (nth == 1)
289 return (match.captured(0));
290 }
291 return QString();
292}
293
294// Make some guesses about possible input-name confusions.
295// Used in the table's search box and when reading the file of already-imaged objects.
296QString tweakNames(const QString &input)
297{
298 QString fixed = input;
299 if (fixed.startsWith("sharpless", Qt::CaseInsensitive))
301 if (fixed.startsWith("messier", Qt::CaseInsensitive))
303
304 fixed.replace(QRegularExpression("^(ngc|ic|abell|ldn|lbn|m|sh2|vdb)\\s*(\\d)",
306 if (fixed.startsWith("sh2-", Qt::CaseInsensitive))
307 fixed.replace(QRegularExpression("^sh2-\\s*(\\d)", QRegularExpression::CaseInsensitiveOption), "sh2 \\1");
308 return fixed;
309}
310
311// Return true if left side is less than right side (values are floats)
312// As opposed to the below, we want the non-reversed sort to be 9 -> 0.
313// This is used when sorting the table by floating point columns.
314double floatCompareFcn( const QModelIndex &left, const QModelIndex &right,
315 int column, int role)
316{
317 const double l = left.siblingAtColumn(column).data(role).toDouble();
318 const double r = right.siblingAtColumn(column).data(role).toDouble();
319 return l - r;
320}
321
322// Return true if left side is less than right side
323// Values can be simple strings or object names like "M 31" where the 2nd part is sorted arithmatically.
324// We want the non-reversed sort to be A -> Z and 0 -> 9, which is why all the returns have minus signs.
325// This is used when sorting the table by string columns.
326int stringCompareFcn( const QModelIndex &left, const QModelIndex &right, int column, int role)
327{
328 const QString l = left.siblingAtColumn(column).data(role).toString();
329 const QString r = right.siblingAtColumn(column).data(role).toString();
332
333 if (lList.size() == 0 || rList.size() == 0)
335
336 // Both sides have at least one item. If the first item is not the same,
337 // return the string compare value for those.
338 const int comp = QString::compare(lList[0], rList[0], Qt::CaseInsensitive);
339 if (comp != 0)
340 return -comp;
341
342 // Here we deal with standard object names, like comparing "M 100" and "M 33"
343 // I'm assuming here that our object names have spaces, as is standard in kstars.
344 if (lList.size() >= 2 && rList.size() >= 2)
345 {
346 int lInt = lList[1].toInt();
347 int rInt = rList[1].toInt();
348 // If they're not ints, then toInt returns 0.
349 // Not expecting negative numbers here.
350 if (lInt > 0 && rInt > 0)
351 return -(lInt - rInt);
352 }
353 // Go back to the original string compare
355}
356
357// TODO: This is copied from schedulerutils.h/cpp because for some reason the build failed when
358// including
359#include "ekos/scheduler/schedulerjob.h"
360void SchedulerUtils_setupJob(Ekos::SchedulerJob &job, const QString &name, bool isLead, const QString &group,
361 const QString &train, const dms &ra, const dms &dec, double djd, double rotation, const QUrl &sequenceUrl,
362 const QUrl &fitsUrl, Ekos::StartupCondition startup, const QDateTime &startupTime, Ekos::CompletionCondition completion,
363 const QDateTime &completionTime, int completionRepeats, double minimumAltitude, double minimumMoonSeparation,
364 bool enforceWeather, bool enforceTwilight, bool enforceArtificialHorizon, bool track, bool focus, bool align, bool guide)
365{
366 /* Configure or reconfigure the observation job */
367
368 job.setIsLead(isLead);
369 job.setOpticalTrain(train);
370 job.setPositionAngle(rotation);
371
372 if (isLead)
373 {
374 job.setName(name);
375 job.setGroup(group);
376 job.setLeadJob(nullptr);
377 // djd should be ut.djd
378 job.setTargetCoords(ra, dec, djd);
379 job.setFITSFile(fitsUrl);
380
381 // #1 Startup conditions
382 job.setStartupCondition(startup);
383 if (startup == Ekos::START_AT)
384 {
385 job.setStartupTime(startupTime);
386 }
387 /* Store the original startup condition */
388 job.setFileStartupCondition(job.getStartupCondition());
389 job.setStartAtTime(job.getStartupTime());
390
391 // #2 Constraints
392
393 job.setMinAltitude(minimumAltitude);
394 job.setMinMoonSeparation(minimumMoonSeparation);
395
396 // Check enforce weather constraints
397 job.setEnforceWeather(enforceWeather);
398 // twilight constraints
399 job.setEnforceTwilight(enforceTwilight);
400 job.setEnforceArtificialHorizon(enforceArtificialHorizon);
401
402 // Job steps
403 job.setStepPipeline(Ekos::SchedulerJob::USE_NONE);
404 if (track)
405 job.setStepPipeline(static_cast<Ekos::SchedulerJob::StepPipeline>(job.getStepPipeline() | Ekos::SchedulerJob::USE_TRACK));
406 if (focus)
407 job.setStepPipeline(static_cast<Ekos::SchedulerJob::StepPipeline>(job.getStepPipeline() | Ekos::SchedulerJob::USE_FOCUS));
408 if (align)
409 job.setStepPipeline(static_cast<Ekos::SchedulerJob::StepPipeline>(job.getStepPipeline() | Ekos::SchedulerJob::USE_ALIGN));
410 if (guide)
411 job.setStepPipeline(static_cast<Ekos::SchedulerJob::StepPipeline>(job.getStepPipeline() | Ekos::SchedulerJob::USE_GUIDE));
412
413 /* Store the original startup condition */
414 job.setFileStartupCondition(job.getStartupCondition());
415 job.setStartAtTime(job.getStartupTime());
416 }
417
418 /* Consider sequence file is new, and clear captured frames map */
419 job.setCapturedFramesMap(Ekos::CapturedFramesMap());
420 job.setSequenceFile(sequenceUrl);
421 job.setCompletionCondition(completion);
422 if (completion == Ekos::FINISH_AT)
423 job.setFinishAtTime(completionTime);
424 else if (completion == Ekos::FINISH_REPEAT)
425 {
426 job.setRepeatsRequired(completionRepeats);
427 job.setRepeatsRemaining(completionRepeats);
428 }
429
430 /* Reset job state to evaluate the changes */
431 job.reset();
432}
433
434// Sets up a SchedulerJob, used by getRunTimes to see when the target can be imaged.
435void setupJob(Ekos::SchedulerJob &job, const QString name, double minAltitude, double minMoonSeparation, dms ra, dms dec,
436 bool useArtificialHorizon)
437{
438 double djd = KStars::Instance()->data()->ut().djd();
439 double rotation = 0.0;
440 QString train = "";
441 QUrl sequenceURL; // is this needed?
442
443 // TODO: Hopefully go back to calling SchedulerUtils::setupJob()
444 //Ekos::SchedulerUtils::setupJob(job, name, true, "",
445 SchedulerUtils_setupJob(job, name, true, "",
446 train, ra, dec, djd,
447 rotation, sequenceURL, QUrl(),
448 Ekos::START_ASAP, QDateTime(),
449 Ekos::FINISH_LOOP, QDateTime(), 1,
450 minAltitude, minMoonSeparation,
451 false, true, useArtificialHorizon,
452 true, true, true, true);
453}
454
455// Computes the times when the given coordinates can be imaged on the date.
456void getRunTimes(const QDate &date, const GeoLocation &geo, double minAltitude, double minMoonSeparation,
457 const dms &ra, const dms &dec, bool useArtificialHorizon, QVector<QDateTime> *jobStartTimes,
458 QVector<QDateTime> *jobEndTimes)
459{
460 jobStartTimes->clear();
461 jobEndTimes->clear();
462 constexpr int SCHEDULE_RESOLUTION_MINUTES = 10;
463 Ekos::SchedulerJob job;
464 setupJob(job, "temp", minAltitude, minMoonSeparation, ra, dec, useArtificialHorizon);
465
466 auto tz = QTimeZone(geo.TZ() * 3600);
467
468 // Find possible imaging times between noon and the next noon.
469 QDateTime startTime(QDateTime(date, QTime(12, 0, 1)));
470 QDateTime stopTime( QDateTime(date.addDays(1), QTime(12, 0, 1)));
471 startTime.setTimeZone(tz);
472 stopTime.setTimeZone(tz);
473
474 QString constraintReason;
475 int maxIters = 10;
476 while (--maxIters >= 0)
477 {
478 QDateTime s = job.getNextPossibleStartTime(startTime, SCHEDULE_RESOLUTION_MINUTES, false, stopTime);
479 if (!s.isValid())
480 return;
481 s.setTimeZone(tz);
482
483 QDateTime e = job.getNextEndTime(s, SCHEDULE_RESOLUTION_MINUTES, &constraintReason, stopTime);
484 if (!e.isValid())
485 return;
486 e.setTimeZone(tz);
487
488 jobStartTimes->push_back(s);
489 jobEndTimes->push_back(e);
490
491 if (e.secsTo(stopTime) < 600)
492 return;
493
494 startTime = e.addSecs(60);
495 startTime.setTimeZone(tz);
496 }
497}
498
499// Computes the times when the given catalog object can be imaged on the date.
500double getRunHours(const CatalogObject &object, const QDate &date, const GeoLocation &geo, double minAltitude,
501 double minMoonSeparation, bool useArtificialHorizon)
502{
503 QVector<QDateTime> jobStartTimes, jobEndTimes;
504 getRunTimes(date, geo, minAltitude, minMoonSeparation, object.ra0(), object.dec0(), useArtificialHorizon, &jobStartTimes,
505 &jobEndTimes);
506 if (jobStartTimes.size() == 0 || jobEndTimes.size() == 0)
507 return 0;
508 else
509 {
510 double totalHours = 0.0;
511 for (int i = 0; i < jobStartTimes.size(); ++i)
512 totalHours += jobStartTimes[i].secsTo(jobEndTimes[i]) * 1.0 / 3600.0;
513 return totalHours;
514 }
515}
516
517// Pack is needed to generate the Astrobin search URLs.
518// This implementation was inspired by
519// https://github.com/romixlab/qmsgpack/blob/master/src/private/pack_p.cpp
520// Returns the size it would have, or actually did, pack.
521int packString(const QString &input, quint8 *p, bool reallyPack)
522{
523 QByteArray str_data = input.toUtf8();
524 quint32 len = str_data.length();
525 const char *str = str_data.data();
526 constexpr bool compatibilityMode = false;
527 const quint8 *origP = p;
528 if (len <= 31)
529 {
530 if (reallyPack) *p = 0xa0 | len;
531 p++;
532 }
533 else if (len <= std::numeric_limits<quint8>::max() &&
534 compatibilityMode == false)
535 {
536 if (reallyPack) *p = 0xd9;
537 p++;
538 if (reallyPack) *p = len;
539 p++;
540 }
541 else if (len <= std::numeric_limits<quint16>::max())
542 {
543 if (reallyPack) *p = 0xda;
544 p++;
545 if (reallyPack)
546 {
547 quint16 val = len;
548 memcpy(p, &val, 2);
549 }
550 p += 2;
551 }
552 else return 0; // Bailing if the url is longer than 64K--shouldn't happen.
553
554 if (reallyPack) memcpy(p, str, len);
555 return (p - origP) + len;
556}
557
558QByteArray pack(const QString &input)
559{
560 QVector<QByteArray> user_data;
561 // first run, calculate size
562 int size = packString(input, nullptr, false);
563 QByteArray arr;
564 arr.resize(size);
565 // second run, pack it
566 packString(input, reinterpret_cast<quint8*>(arr.data()), true);
567 return arr;
568}
569
570QString massageObjectName(const QString &name)
571{
572 // Remove any spaces, but "sh2 " becomes "sh2-".
573 // TODO: Is there a more general way to do this?
574 auto newStr = name;
575 if (newStr.startsWith("sh2 ", Qt::CaseInsensitive))
576 newStr = newStr.replace(0, 4, "sh2-");
577 newStr = newStr.replace(' ', "");
578 return newStr;
579}
580
581bool downsampleImageFiles(const QString &baseDir, int maxHeight)
582{
583 QString fn = "Test.txt";
584 QFile file( fn );
585 if ( file.open(QIODevice::ReadWrite) )
586 {
587 QTextStream stream( &file );
588 stream << "hello" << Qt::endl;
589 }
590 file.close();
591
592 const QString subDir = "REDUCED";
593 QDir directory(baseDir);
594 if (!directory.exists())
595 {
596 fprintf(stderr, "downsampleImageFiles: Base directory doesn't exist\n");
597 return false;
598 }
599 QDir outDir = QDir(directory.absolutePath().append(QDir::separator()).append(subDir));
600 if (!outDir.exists())
601 {
602 if (!outDir.mkpath("."))
603 {
604 fprintf(stderr, "downsampleImageFiles: Failed making the output directory\n");
605 return false;
606 }
607 }
608
609 int numSaved = 0;
610 QStringList files = directory.entryList(QStringList() << "*.jpg" << "*.JPG" << "*.png", QDir::Files);
611 foreach (QString filename, files)
612 {
613 QString fullPath = QString("%1%2%3").arg(baseDir).arg(QDir::separator()).arg(filename);
614 QImage img(fullPath);
615 QImage scaledImg;
616 if (img.height() > maxHeight)
617 scaledImg = img.scaledToHeight(maxHeight, Qt::SmoothTransformation);
618 else
619 scaledImg = img;
620
621 QString writeFilename = outDir.absolutePath().append(QDir::separator()).append(filename);
622 QFileInfo info(writeFilename);
623 QString jpgFilename = info.path() + QDir::separator() + info.completeBaseName() + ".jpg";
624
625 if (!scaledImg.save(jpgFilename, "JPG"))
626 fprintf(stderr, "downsampleImageFiles: Failed saving \"%s\"\n", writeFilename.toLatin1().data());
627 else
628 {
629 numSaved++;
630 fprintf(stderr, "downsampleImageFiles: saved \"%s\"\n", writeFilename.toLatin1().data());
631 }
632 }
633 fprintf(stderr, "downsampleImageFiles: Wrote %d files\n", numSaved);
634 return true;
635}
636
637// Seaches for all the occurances of the byte cc in the QByteArray, and replaces each
638// of them with the sequence of bytes in the QByteArray substitute.
639// There's probably a QByteArray method that does this.
640void replaceByteArrayChars(QByteArray &bInput, char cc, const QByteArray &substitute)
641{
642 while (true)
643 {
644 const int len = bInput.size();
645 int pos = -1;
646 for (int i = 0; i < len; ++i)
647 {
648 if (bInput[i] == cc)
649 {
650 pos = i;
651 break;
652 }
653 }
654 if (pos < 0)
655 break;
656 bInput.replace(pos, 1, substitute);
657 }
658}
659
660// Look in the app directories in case a .png or .jpg file exists that ends
661// with the object name.
662QString findObjectImage(const QString &name)
663{
664 QString massagedName = massageObjectName(name);
665 QDir dir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
666 QStringList nameFilter;
667 nameFilter << QString("*%1.png").arg(massagedName) << QString("*%1.jpg").arg(massagedName);
668 QFileInfoList files = dir.entryInfoList(nameFilter, QDir::Files);
669 if (files.size() > 0)
670 return files[0].absoluteFilePath();
671
672 QFileInfoList subDirs = dir.entryInfoList(nameFilter, QDir::NoDotAndDotDot | QDir::AllDirs);
673 for (int i = 0; i < subDirs.size(); i++)
674 {
675 QDir subDir(subDirs[i].absoluteFilePath());
676 QFileInfoList files = subDir.entryInfoList(nameFilter, QDir::NoDotAndDotDot | QDir::Files);
677 if (files.size() > 0)
678 return files[0].absoluteFilePath();
679 }
680 return QString();
681}
682
683QString creativeCommonsString(const QString &astrobinAbbrev)
684{
685 if (astrobinAbbrev == "ACC")
686 return "CC-BY";
687 else if (astrobinAbbrev == "ASACC")
688 return "CC-BY-SA";
689 else if (astrobinAbbrev == "ANCCC")
690 return "CC-BY-NC";
691 else if (astrobinAbbrev == "ANCSACC")
692 return "CC-BY-SA-NC";
693 else return "";
694}
695
696QString creativeCommonsTooltipString(const QString &astrobinAbbrev)
697{
698 if (astrobinAbbrev == "ACC")
699 return "Atribution Creative Commons";
700 else if (astrobinAbbrev == "ASACC")
701 return "Atribution Share-Alike Creative Commons";
702 else if (astrobinAbbrev == "ANCCC")
703 return "Atribution Non-Commercial Creative Commons";
704 else if (astrobinAbbrev == "ANCSACC")
705 return "Atribution Non-Commercial Share-Alike Creative Commons";
706 else return "";
707}
708
709QString shortCoordString(const dms &ra, const dms &dec)
710{
711 return QString("%1h%2' %3%4°%5'").arg(ra.hour()).arg(ra.minute())
712 .arg(dec.Degrees() < 0 ? "-" : "").arg(abs(dec.degree())).arg(abs(dec.arcmin()));
713}
714
715double getAltitude(GeoLocation *geo, SkyPoint &p, const QDateTime &time)
716{
717 auto ut2 = geo->LTtoUT(KStarsDateTime(time));
718 CachingDms LST = geo->GSTtoLST(ut2.gst());
719 p.EquatorialToHorizontal(&LST, geo->lat());
720 return p.alt().Degrees();
721}
722
723double getMaxAltitude(const KSAlmanac &ksal, const QDate &date, GeoLocation *geo, const SkyObject &object,
724 double hoursAfterDusk = 0, double hoursBeforeDawn = 0)
725{
726 auto tz = QTimeZone(geo->TZ() * 3600);
727 KStarsDateTime midnight = KStarsDateTime(date.addDays(1), QTime(0, 1));
728 midnight.setTimeZone(tz);
729
730 QDateTime dawn = midnight.addSecs(24 * 3600 * ksal.getDawnAstronomicalTwilight());
731 dawn.setTimeZone(tz);
732 QDateTime dusk = midnight.addSecs(24 * 3600 * ksal.getDuskAstronomicalTwilight());
733 dusk.setTimeZone(tz);
734
735 QDateTime start = dusk.addSecs(hoursAfterDusk * 3600);
736 start.setTimeZone(tz);
737
738 auto end = dawn.addSecs(-hoursBeforeDawn * 3600);
739 end.setTimeZone(tz);
740
741 SkyPoint coords = object;
742 double maxAlt = -90;
743 auto t = start;
744 t.setTimeZone(tz);
745 QDateTime maxTime = t;
746
747 // 1.8 here
748
749 while (t.secsTo(end) > 0)
750 {
751 double alt = getAltitude(geo, coords, t);
752 if (alt > maxAlt)
753 {
754 maxAlt = alt;
755 maxTime = t;
756 }
757 t = t.addSecs(60 * 20);
758 }
759 return maxAlt;
760}
761
762} // namespace
763
764CatalogFilter::CatalogFilter(QObject* parent) : QSortFilterProxyModel(parent)
765{
766 m_SortColumn = HOURS_COLUMN;
767}
768
769// This method decides whether a row is shown in the object table.
770bool CatalogFilter::filterAcceptsRow(int row, const QModelIndex &parent) const
771{
772 const QModelIndex typeIndex = sourceModel()->index(row, TYPE_COLUMN, parent);
773 const SkyObject::TYPE type = static_cast<SkyObject::TYPE>(sourceModel()->data(typeIndex, TYPE_ROLE).toInt());
774 if (!acceptType(type)) return false;
775
776 const QModelIndex hoursIndex = sourceModel()->index(row, HOURS_COLUMN, parent);
777 const bool hasEnoughHours = sourceModel()->data(hoursIndex, Qt::DisplayRole).toDouble() >= m_MinHours;
778 if (!hasEnoughHours) return false;
779
780 const QModelIndex flagsIndex = sourceModel()->index(row, FLAGS_COLUMN, parent);
781
782 const bool isImaged = sourceModel()->data(flagsIndex, FLAGS_ROLE).toInt() & IMAGED_BIT;
783 const bool passesImagedConstraints = !m_ImagedConstraintsEnabled || (isImaged == m_ImagedRequired);
784 if (!passesImagedConstraints) return false;
785
786 const bool isIgnored = sourceModel()->data(flagsIndex, FLAGS_ROLE).toInt() & IGNORED_BIT;
787 const bool passesIgnoredConstraints = !m_IgnoredConstraintsEnabled || (isIgnored == m_IgnoredRequired);
788 if (!passesIgnoredConstraints) return false;
789
790 const bool isPicked = sourceModel()->data(flagsIndex, FLAGS_ROLE).toInt() & PICKED_BIT;
791 const bool passesPickedConstraints = !m_PickedConstraintsEnabled || (isPicked == m_PickedRequired);
792 if (!passesPickedConstraints) return false;
793
794 // keyword constraint is inactive without a keyword.
795 if (m_Keyword.isEmpty() || !m_KeywordConstraintsEnabled) return true;
796 const QModelIndex notesIndex = sourceModel()->index(row, NOTES_COLUMN, parent);
797 const QString notes = sourceModel()->data(notesIndex, NOTES_ROLE).toString();
798
799 const bool REMatches = m_KeywordRE.match(notes).hasMatch();
800 return (m_KeywordRequired == REMatches);
801}
802
803void CatalogFilter::setMinHours(double hours)
804{
805 m_MinHours = hours;
806}
807
808void CatalogFilter::setImagedConstraints(bool enabled, bool required)
809{
810 m_ImagedConstraintsEnabled = enabled;
811 m_ImagedRequired = required;
812}
813
814void CatalogFilter::setPickedConstraints(bool enabled, bool required)
815{
816 m_PickedConstraintsEnabled = enabled;
817 m_PickedRequired = required;
818}
819
820void CatalogFilter::setIgnoredConstraints(bool enabled, bool required)
821{
822 m_IgnoredConstraintsEnabled = enabled;
823 m_IgnoredRequired = required;
824}
825
826void CatalogFilter::setKeywordConstraints(bool enabled, bool required, const QString &keyword)
827{
828 m_KeywordConstraintsEnabled = enabled;
829 m_KeywordRequired = required;
830 m_Keyword = keyword;
831 m_KeywordRE = QRegularExpression(keyword);
832}
833
834void CatalogFilter::setSortColumn(int column)
835{
836 if (column == m_SortColumn)
837 m_ReverseSort = !m_ReverseSort;
838 m_SortColumn = column;
839}
840
841// The main function used when sorting the table by a column (which is stored in m_SortColumn).
842// The secondary-sort columns are hard-coded below (and commented) for each primary-sort column.
843// When reversing the sort, we only reverse the primary column. The secondary sort column's
844// sort is not reversed.
845bool CatalogFilter::lessThan ( const QModelIndex &left, const QModelIndex &right) const
846{
847 double compareVal = 0;
848 switch(m_SortColumn)
849 {
850 case NAME_COLUMN:
851 // Name. There shouldn't be any ties, so no secondary sort.
852 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
853 if (m_ReverseSort) compareVal = -compareVal;
854 break;
855 case TYPE_COLUMN:
856 // Type then hours then name. There can be plenty of ties in type and hours.
857 compareVal = stringCompareFcn(left, right, TYPE_COLUMN, Qt::DisplayRole);
858 if (m_ReverseSort) compareVal = -compareVal;
859 if (compareVal != 0) break;
860 compareVal = floatCompareFcn(left, right, HOURS_COLUMN, HOURS_ROLE);
861 if (compareVal != 0) break;
862 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
863 break;
864 case SIZE_COLUMN:
865 // Size then hours then name. Size mostly has ties when size is unknown (== 0).
866 compareVal = floatCompareFcn(left, right, SIZE_COLUMN, SIZE_ROLE);
867 if (m_ReverseSort) compareVal = -compareVal;
868 if (compareVal != 0) break;
869 compareVal = floatCompareFcn(left, right, HOURS_COLUMN, HOURS_ROLE);
870 if (compareVal != 0) break;
871 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
872 break;
873 case ALTITUDE_COLUMN:
874 // Altitude then hours then name. Probably altitude rarely ties.
875 compareVal = floatCompareFcn(left, right, ALTITUDE_COLUMN, ALTITUDE_ROLE);
876 if (m_ReverseSort) compareVal = -compareVal;
877 if (compareVal != 0) break;
878 compareVal = floatCompareFcn(left, right, HOURS_COLUMN, HOURS_ROLE);
879 if (compareVal != 0) break;
880 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
881 break;
882 case MOON_COLUMN:
883 // Moon then hours then name. Probably moon rarely ties.
884 compareVal = floatCompareFcn(left, right, MOON_COLUMN, MOON_ROLE);
885 if (m_ReverseSort) compareVal = -compareVal;
886 if (compareVal != 0) break;
887 compareVal = floatCompareFcn(left, right, HOURS_COLUMN, HOURS_ROLE);
888 if (compareVal != 0) break;
889 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
890 break;
891 case CONSTELLATION_COLUMN:
892 // Constellation, then hours, then name.
893 compareVal = stringCompareFcn(left, right, CONSTELLATION_COLUMN, Qt::DisplayRole);
894 if (m_ReverseSort) compareVal = -compareVal;
895 if (compareVal != 0) break;
896 compareVal = floatCompareFcn(left, right, HOURS_COLUMN, HOURS_ROLE);
897 if (compareVal != 0) break;
898 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
899 break;
900 case COORD_COLUMN:
901 // Coordinate string is a weird thing to sort. Anyway, Coord, then hours, then name.
902 compareVal = stringCompareFcn(left, right, COORD_COLUMN, Qt::DisplayRole);
903 if (m_ReverseSort) compareVal = -compareVal;
904 if (compareVal != 0) break;
905 compareVal = floatCompareFcn(left, right, HOURS_COLUMN, HOURS_ROLE);
906 if (compareVal != 0) break;
907 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
908 break;
909 case HOURS_COLUMN:
910 default:
911 // In all other conditions (e.g. HOURS) sort by hours. Secondary sort is name.
912 compareVal = floatCompareFcn(left, right, HOURS_COLUMN, HOURS_ROLE);
913 if (m_ReverseSort) compareVal = -compareVal;
914 if (compareVal != 0) break;
915 compareVal = stringCompareFcn(left, right, NAME_COLUMN, Qt::DisplayRole);
916 break;
917 }
918 return compareVal < 0;
919}
920
921ImagingPlannerUI::ImagingPlannerUI(QWidget * p) : QFrame(p)
922{
923 setupUi(this);
924 setupIcons();
925}
926
927// Icons can't just be set up in the .ui file for Mac, so explicitly doing it here.
928void ImagingPlannerUI::setupIcons()
929{
930 SearchB->setIcon(QIcon::fromTheme("edit-find"));
931 backOneDay->setIcon(QIcon::fromTheme("arrow-left"));
932 forwardOneDay->setIcon(QIcon::fromTheme("arrow-right"));
933 optionsButton->setIcon(QIcon::fromTheme("open-menu-symbolic"));
934 helpButton->setIcon(QIcon::fromTheme("help-about"));
935 userNotesEditButton->setIcon(QIcon::fromTheme("document-edit"));
936 userNotesDoneButton->setIcon(QIcon::fromTheme("checkmark"));
937 userNotesOpenLink->setIcon(QIcon::fromTheme("link"));
938 userNotesOpenLink2->setIcon(QIcon::fromTheme("link"));
939 userNotesOpenLink3->setIcon(QIcon::fromTheme("link"));
940 hideAltitudeGraphB->setIcon(QIcon::fromTheme("window-minimize"));
941 showAltitudeGraphB->setIcon(QIcon::fromTheme("window-maximize"));
942 hideAstrobinDetailsButton->setIcon(QIcon::fromTheme("window-minimize"));
943 showAstrobinDetailsButton->setIcon(QIcon::fromTheme("window-maximize"));
944 hideFilterTypesButton->setIcon(QIcon::fromTheme("window-minimize"));
945 showFilterTypesButton->setIcon(QIcon::fromTheme("window-maximize"));
946 hideImageButton->setIcon(QIcon::fromTheme("window-minimize"));
947 showImageButton->setIcon(QIcon::fromTheme("window-maximize"));
948}
949
950GeoLocation *ImagingPlanner::getGeo()
951{
952 return KStarsData::Instance()->geo();
953}
954
955QDate ImagingPlanner::getDate() const
956{
957 return ui->DateEdit->date();
958}
959
960ImagingPlanner::ImagingPlanner() : QDialog(nullptr), m_manager{ CatalogsDB::dso_db_path() }
961{
962 ui = new ImagingPlannerUI(this);
963
964 // Seem to need these or when the user stretches the window width, the widgets
965 // don't take advantage of the width.
966 QVBoxLayout *mainLayout = new QVBoxLayout;
967 mainLayout->addWidget(ui);
968 setLayout(mainLayout);
969
970 setWindowTitle(i18nc("@title:window", "Imaging Planner"));
971 setFocusPolicy(Qt::StrongFocus);
972
973 if (Options::imagingPlannerIndependentWindow())
974 {
975 // Removing the Dialog bit (but neet to add back the window bit) allows
976 // the window to go below other windows.
977 setParent(nullptr, (windowFlags() & ~Qt::Dialog) | Qt::Window);
978 }
979 else
980 {
981#ifdef Q_OS_MACOS
982 setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
983#endif
984 }
985}
986
987// Sets up the hide/show buttons that minimize/maximize the plot/search/filters/image sections.
988void ImagingPlanner::setupHideButtons(bool(*option)(), void(*setOption)(bool),
989 QPushButton * hideButton, QPushButton * showButton,
990 QFrame * widget, QFrame * hiddenWidget)
991{
992 hiddenWidget->setVisible(option());
993 widget->setVisible(!option());
994
995 connect(hideButton, &QAbstractButton::clicked, this, [this, setOption, hiddenWidget, widget]()
996 {
997 setOption(true);
998 Options::self()->save();
999 hiddenWidget->setVisible(true);
1000 widget->setVisible(false);
1001 focusOnTable();
1002 adjustWindowSize();
1003 });
1004 connect(showButton, &QAbstractButton::clicked, this, [this, setOption, hiddenWidget, widget]()
1005 {
1006 setOption(false);
1007 Options::self()->save();
1008 hiddenWidget->setVisible(false);
1009 widget->setVisible(true);
1010 focusOnTable();
1011 });
1012}
1013
1014// Gives the keyboard focus to the CatalogView object table.
1015void ImagingPlanner::focusOnTable()
1016{
1017 ui->CatalogView->setFocus();
1018}
1019
1020void ImagingPlanner::adjustWindowSize()
1021{
1022 const int keepWidth = width();
1023 adjustSize();
1024 const int newHeight = height();
1025 resize(keepWidth, newHeight);
1026}
1027
1028// Sets up the galaxy/nebula/... filter buttons.
1029void ImagingPlanner::setupFilterButton(QCheckBox * checkbox, bool(*option)(), void(*setOption)(bool))
1030{
1031 checkbox->setChecked(option());
1032 connect(checkbox, &QCheckBox::toggled, [this, setOption](bool checked)
1033 {
1034 setOption(checked);
1035 Options::self()->save();
1036 m_CatalogSortModel->invalidate();
1037 updateDisplays();
1038 ui->CatalogView->resizeColumnsToContents();
1039 focusOnTable();
1040 });
1041}
1042
1043// Sets up the picked/imaged/ignored/keyword buttons
1044void ImagingPlanner::setupFilter2Buttons(
1045 QCheckBox * yes, QCheckBox * no, QCheckBox * dontCare,
1046 bool(*yesOption)(), bool(*noOption)(), bool(*dontCareOption)(),
1047 void(*setYesOption)(bool), void(*setNoOption)(bool), void(*setDontCareOption)(bool))
1048{
1049
1050 // Use clicked, not toggled to avoid callbacks when the state is changed programatically.
1051 connect(yes, &QCheckBox::clicked, [this, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare](bool checked)
1052 {
1053 setupShowCallback(checked, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare);
1054 updateSortConstraints();
1055 m_CatalogSortModel->invalidate();
1056 ui->CatalogView->resizeColumnsToContents();
1057 updateDisplays();
1058 focusOnTable();
1059 });
1060 connect(no, &QCheckBox::clicked, [this, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare](bool checked)
1061 {
1062 setupShowNotCallback(checked, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare);
1063 updateSortConstraints();
1064 m_CatalogSortModel->invalidate();
1065 ui->CatalogView->resizeColumnsToContents();
1066 updateDisplays();
1067 focusOnTable();
1068 });
1069 connect(dontCare, &QCheckBox::clicked, [this, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare](bool checked)
1070 {
1071 setupDontCareCallback(checked, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare);
1072 updateSortConstraints();
1073 m_CatalogSortModel->invalidate();
1074 ui->CatalogView->resizeColumnsToContents();
1075 updateDisplays();
1076 focusOnTable();
1077 });
1078
1079 yes->setChecked(yesOption());
1080 no->setChecked(noOption());
1081 dontCare->setChecked(dontCareOption());
1082}
1083
1084// Updates the QSortFilterProxyModel with new picked/imaged/ignore settings.
1085void ImagingPlanner::updateSortConstraints()
1086{
1087 m_CatalogSortModel->setPickedConstraints(!ui->dontCarePickedCB->isChecked(),
1088 ui->pickedCB->isChecked());
1089 m_CatalogSortModel->setImagedConstraints(!ui->dontCareImagedCB->isChecked(),
1090 ui->imagedCB->isChecked());
1091 m_CatalogSortModel->setIgnoredConstraints(!ui->dontCareIgnoredCB->isChecked(),
1092 ui->ignoredCB->isChecked());
1093 m_CatalogSortModel->setKeywordConstraints(!ui->dontCareKeywordCB->isChecked(),
1094 ui->keywordCB->isChecked(), ui->keywordEdit->toPlainText().trimmed());
1095}
1096
1097// Called once, at the first viewing of the tool, to initalize all the widgets.
1098void ImagingPlanner::initialize()
1099{
1100 if (KStarsData::Instance() == nullptr)
1101 {
1102 QTimer::singleShot(200, this, &ImagingPlanner::initialize);
1103 return;
1104 }
1105
1106 // Connects the threaded catalog loader to the UI.
1107 connect(this, &ImagingPlanner::popupSorry, this, &ImagingPlanner::sorry);
1108
1109 // Setup the Table Views
1110 m_CatalogModel = new QStandardItemModel(0, LAST_COLUMN);
1111
1112 // Setup the labels and tooltips for the header row of the table.
1113 m_CatalogModel->setHorizontalHeaderLabels(
1114 QStringList() << i18n("Name") << i18n("Hours") << i18n("Type") << i18n("Size") << i18n("Alt") << i18n("Moon") <<
1115 i18n("Const") << i18n("Coord"));
1116 m_CatalogModel->horizontalHeaderItem(NAME_COLUMN)->setToolTip(
1117 i18n("Object Name--click header to sort ascending/descending."));
1118 m_CatalogModel->horizontalHeaderItem(
1119 HOURS_COLUMN)->setToolTip(i18n("Number of hours the object can be imaged--click header to sort ascending/descending."));
1120 m_CatalogModel->horizontalHeaderItem(TYPE_COLUMN)->setToolTip(
1121 i18n("Object Type--click header to sort ascending/descending."));
1122 m_CatalogModel->horizontalHeaderItem(
1123 SIZE_COLUMN)->setToolTip(i18n("Maximum object dimension (arcmin)--click header to sort ascending/descending."));
1124 m_CatalogModel->horizontalHeaderItem(
1125 ALTITUDE_COLUMN)->setToolTip(i18n("Maximum altitude--click header to sort ascending/descending."));
1126 m_CatalogModel->horizontalHeaderItem(
1127 MOON_COLUMN)->setToolTip(i18n("Moon angular separation at midnight--click header to sort ascending/descending."));
1128 m_CatalogModel->horizontalHeaderItem(
1129 CONSTELLATION_COLUMN)->setToolTip(i18n("Constellation--click header to sort ascending/descending."));
1130 m_CatalogModel->horizontalHeaderItem(
1131 COORD_COLUMN)->setToolTip(i18n("RA/DEC coordinates--click header to sort ascending/descending."));
1132
1133 m_CatalogSortModel = new CatalogFilter(this);
1134 m_CatalogSortModel->setSortCaseSensitivity(Qt::CaseInsensitive);
1135 m_CatalogSortModel->setSourceModel(m_CatalogModel.data());
1136 m_CatalogSortModel->setDynamicSortFilter(true);
1137
1138 ui->CatalogView->setModel(m_CatalogSortModel.data());
1139 ui->CatalogView->setSortingEnabled(false); // We explicitly control the clicking on headers.
1140 ui->CatalogView->horizontalHeader()->setStretchLastSection(false);
1141 ui->CatalogView->resizeColumnsToContents();
1142 ui->CatalogView->viewport()->installEventFilter(this);
1143 ui->CatalogView->installEventFilter(this);
1144 ui->CatalogView->verticalHeader()->setVisible(false); // Remove the row-number display.
1145 ui->CatalogView->setColumnHidden(FLAGS_COLUMN, true);
1146
1147 connect(ui->CatalogView->selectionModel(), &QItemSelectionModel::selectionChanged,
1148 this, &ImagingPlanner::selectionChanged);
1149
1150 // Initialize the date to KStars' date.
1151 if (getGeo())
1152 {
1153 auto utc = KStarsData::Instance()->clock()->utc();
1154 auto localTime = getGeo()->UTtoLT(utc);
1155 ui->DateEdit->setDate(localTime.date());
1156 updateMoon();
1157 }
1158
1159 setStatus("");
1160
1161 setupHideButtons(&Options::imagingPlannerHideAltitudeGraph, &Options::setImagingPlannerHideAltitudeGraph,
1162 ui->hideAltitudeGraphB, ui->showAltitudeGraphB,
1163 ui->AltitudeGraphFrame, ui->HiddenAltitudeGraphFrame);
1164
1165 // Date buttons
1166 connect(ui->backOneDay, &QPushButton::clicked, this, &ImagingPlanner::moveBackOneDay);
1167 connect(ui->forwardOneDay, &QPushButton::clicked, this, &ImagingPlanner::moveForwardOneDay);
1168 connect(ui->DateEdit, &QDateTimeEdit::dateChanged, this, [this]()
1169 {
1170 QString selection = currentObjectName();
1171 updateMoon();
1172 recompute();
1173 updateDisplays();
1174 scrollToName(selection);
1175 });
1176
1177 // Setup the section with Web search and Astrobin search details.
1178
1179 // Setup Web Search buttons
1180 connect(ui->astrobinButton, &QPushButton::clicked, this, &ImagingPlanner::searchAstrobin);
1181 connect(ui->astrobinButton2, &QPushButton::clicked, this, &ImagingPlanner::searchAstrobin);
1182 connect(ui->searchWikipedia, &QPushButton::clicked, this, &ImagingPlanner::searchWikipedia);
1183 connect(ui->searchWikipedia2, &QPushButton::clicked, this, &ImagingPlanner::searchWikipedia);
1184 connect(ui->searchNGCICImages, &QPushButton::clicked, this, &ImagingPlanner::searchNGCICImages);
1185 connect(ui->searchNGCICImages2, &QPushButton::clicked, this, &ImagingPlanner::searchNGCICImages);
1186 connect(ui->searchSimbad, &QPushButton::clicked, this, &ImagingPlanner::searchSimbad);
1187 connect(ui->searchSimbad2, &QPushButton::clicked, this, &ImagingPlanner::searchSimbad);
1188
1189 // Always start with hiding the details.
1190 Options::setImagingPlannerHideAstrobinDetails(true);
1191 setupHideButtons(&Options::imagingPlannerHideAstrobinDetails, &Options::setImagingPlannerHideAstrobinDetails,
1192 ui->hideAstrobinDetailsButton, ui->showAstrobinDetailsButton,
1193 ui->AstrobinSearchFrame, ui->HiddenAstrobinSearchFrame);
1194 ui->AstrobinAward->setChecked(Options::astrobinAward());
1195 connect(ui->AstrobinAward, &QAbstractButton::clicked, [this](bool checked)
1196 {
1197 Options::setAstrobinAward(checked);
1198 Options::self()->save();
1199 focusOnTable();
1200 });
1201 ui->AstrobinMinRadius->setValue(Options::astrobinMinRadius());
1202 connect(ui->AstrobinMinRadius, &QDoubleSpinBox::editingFinished, [this]()
1203 {
1204 Options::setAstrobinMinRadius(ui->AstrobinMinRadius->value());
1205 Options::self()->save();
1206 focusOnTable();
1207 });
1208 ui->AstrobinMaxRadius->setValue(Options::astrobinMaxRadius());
1209 connect(ui->AstrobinMaxRadius, &QDoubleSpinBox::editingFinished, [this]()
1210 {
1211 Options::setAstrobinMaxRadius(ui->AstrobinMaxRadius->value());
1212 Options::self()->save();
1213 focusOnTable();
1214 });
1215
1216 // Initialize image and catalog section
1217 m_NoImagePixmap =
1218 QPixmap(":/images/noimage.png").scaled(ui->ImagePreview->width(), ui->ImagePreview->height(), Qt::KeepAspectRatio,
1220 ui->ImagePreviewCreditLink->installEventFilter(this);
1221 ui->ImagePreviewCredit->installEventFilter(this);
1222 ui->ImagePreview->installEventFilter(this);
1223 setDefaultImage();
1224 connect(ui->LoadCatalogButton, &QPushButton::clicked, this, &ImagingPlanner::loadCatalogViaMenu);
1225 connect(ui->LoadCatalogButton2, &QPushButton::clicked, this, &ImagingPlanner::loadCatalogViaMenu);
1226 setupHideButtons(&Options::imagingPlannerHideImage, &Options::setImagingPlannerHideImage,
1227 ui->hideImageButton, ui->showImageButton,
1228 ui->ImageFrame, ui->HiddenImageFrame);
1229
1230 // Initialize filter section
1231 Options::setImagingPlannerHideFilters(true);
1232 setupHideButtons(&Options::imagingPlannerHideFilters, &Options::setImagingPlannerHideFilters,
1233 ui->hideFilterTypesButton, ui->showFilterTypesButton,
1234 ui->FilterTypesFrame, ui->HiddenFilterTypesFrame);
1235 setupFilterButton(ui->OpenClusterCB, &Options::imagingPlannerAcceptOpenCluster,
1236 &Options::setImagingPlannerAcceptOpenCluster);
1237 setupFilterButton(ui->NebulaCB, &Options::imagingPlannerAcceptNebula, &Options::setImagingPlannerAcceptNebula);
1238 setupFilterButton(ui->GlobularClusterCB, &Options::imagingPlannerAcceptGlobularCluster,
1239 &Options::setImagingPlannerAcceptGlobularCluster);
1240 setupFilterButton(ui->PlanetaryCB, &Options::imagingPlannerAcceptPlanetary, &Options::setImagingPlannerAcceptPlanetary);
1241 setupFilterButton(ui->SupernovaRemnantCB, &Options::imagingPlannerAcceptSupernovaRemnant,
1242 &Options::setImagingPlannerAcceptSupernovaRemnant);
1243 setupFilterButton(ui->GalaxyCB, &Options::imagingPlannerAcceptGalaxy, &Options::setImagingPlannerAcceptGalaxy);
1244 setupFilterButton(ui->GalaxyClusterCB, &Options::imagingPlannerAcceptGalaxyCluster,
1245 &Options::setImagingPlannerAcceptGalaxyCluster);
1246 setupFilterButton(ui->DarkNebulaCB, &Options::imagingPlannerAcceptDarkNebula, &Options::setImagingPlannerAcceptDarkNebula);
1247 setupFilterButton(ui->OtherCB, &Options::imagingPlannerAcceptOther, &Options::setImagingPlannerAcceptOther);
1248
1249 setupFilter2Buttons(ui->pickedCB, ui->notPickedCB, ui->dontCarePickedCB,
1250 &Options::imagingPlannerShowPicked, &Options::imagingPlannerShowNotPicked, &Options::imagingPlannerDontCarePicked,
1251 &Options::setImagingPlannerShowPicked, &Options::setImagingPlannerShowNotPicked, &Options::setImagingPlannerDontCarePicked);
1252
1253 setupFilter2Buttons(ui->imagedCB, ui->notImagedCB, ui->dontCareImagedCB,
1254 &Options::imagingPlannerShowImaged, &Options::imagingPlannerShowNotImaged, &Options::imagingPlannerDontCareImaged,
1255 &Options::setImagingPlannerShowImaged, &Options::setImagingPlannerShowNotImaged, &Options::setImagingPlannerDontCareImaged);
1256
1257 setupFilter2Buttons(ui->ignoredCB, ui->notIgnoredCB, ui->dontCareIgnoredCB,
1258 &Options::imagingPlannerShowIgnored, &Options::imagingPlannerShowNotIgnored, &Options::imagingPlannerDontCareIgnored,
1259 &Options::setImagingPlannerShowIgnored, &Options::setImagingPlannerShowNotIgnored,
1260 &Options::setImagingPlannerDontCareIgnored);
1261
1262 ui->keywordEdit->setText(Options::imagingPlannerKeyword());
1263 ui->keywordEdit->setAcceptRichText(false);
1264 m_Keyword = Options::imagingPlannerKeyword();
1265 setupFilter2Buttons(ui->keywordCB, ui->notKeywordCB, ui->dontCareKeywordCB,
1266 &Options::imagingPlannerShowKeyword, &Options::imagingPlannerShowNotKeyword, &Options::imagingPlannerDontCareKeyword,
1267 &Options::setImagingPlannerShowKeyword, &Options::setImagingPlannerShowNotKeyword,
1268 &Options::setImagingPlannerDontCareKeyword);
1269
1270 ui->keywordEdit->setFocusPolicy(Qt::StrongFocus);
1271 ui->keywordEdit->installEventFilter(this);
1272
1273 // Initialize the altitude/moon/hours inputs
1274 ui->useArtificialHorizon->setChecked(Options::imagingPlannerUseArtificialHorizon());
1275 m_UseArtificialHorizon = Options::imagingPlannerUseArtificialHorizon();
1276 ui->minMoon->setValue(Options::imagingPlannerMinMoonSeparation());
1277 m_MinMoon = Options::imagingPlannerMinMoonSeparation();
1278 ui->minAltitude->setValue(Options::imagingPlannerMinAltitude());
1279 m_MinAltitude = Options::imagingPlannerMinAltitude();
1280 ui->minHours->setValue(Options::imagingPlannerMinHours());
1281 m_MinHours = Options::imagingPlannerMinHours();
1282 m_CatalogSortModel->setMinHours(Options::imagingPlannerMinHours());
1283 connect(ui->useArtificialHorizon, &QCheckBox::toggled, [this]()
1284 {
1285 if (m_UseArtificialHorizon == ui->useArtificialHorizon->isChecked())
1286 return;
1287 m_UseArtificialHorizon = ui->useArtificialHorizon->isChecked();
1288 Options::setImagingPlannerUseArtificialHorizon(ui->useArtificialHorizon->isChecked());
1289 Options::self()->save();
1290 recompute();
1291 updateDisplays();
1292 });
1293 connect(ui->minMoon, &QDoubleSpinBox::editingFinished, [this]()
1294 {
1295 if (m_MinMoon == ui->minMoon->value())
1296 return;
1297 m_MinMoon = ui->minMoon->value();
1298 Options::setImagingPlannerMinMoonSeparation(ui->minMoon->value());
1299 Options::self()->save();
1300 recompute();
1301 updateDisplays();
1302 });
1303 connect(ui->minAltitude, &QDoubleSpinBox::editingFinished, [this]()
1304 {
1305 if (m_MinAltitude == ui->minAltitude->value())
1306 return;
1307 m_MinAltitude = ui->minAltitude->value();
1308 Options::setImagingPlannerMinAltitude(ui->minAltitude->value());
1309 Options::self()->save();
1310 recompute();
1311 updateDisplays();
1312 });
1313 connect(ui->minHours, &QDoubleSpinBox::editingFinished, [this]()
1314 {
1315 if (m_MinHours == ui->minHours->value())
1316 return;
1317 m_MinHours = ui->minHours->value();
1318 Options::setImagingPlannerMinHours(ui->minHours->value());
1319 Options::self()->save();
1320 m_CatalogSortModel->setMinHours(Options::imagingPlannerMinHours());
1321 m_CatalogSortModel->invalidate();
1322 ui->CatalogView->resizeColumnsToContents();
1323 updateDisplays();
1324 });
1325
1326 updateSortConstraints();
1327
1328 m_CatalogSortModel->setMinHours(ui->minHours->value());
1329
1330 ui->CatalogView->setColumnHidden(NOTES_COLUMN, true);
1331
1332 initUserNotes();
1333
1334 connect(ui->userNotesDoneButton, &QAbstractButton::clicked, this, &ImagingPlanner::userNotesEditFinished);
1335 ui->userNotesEdit->setFocusPolicy(Qt::StrongFocus);
1336 ui->userNotesEdit->installEventFilter(this);
1337
1338 connect(ui->userNotesEditButton, &QAbstractButton::clicked, this, [this]()
1339 {
1340 ui->userNotesLabel->setVisible(true);
1341 ui->userNotesEdit->setText(ui->userNotes->text());
1342 ui->userNotesEdit->setVisible(true);
1343 ui->userNotesEditButton->setVisible(false);
1344 ui->userNotesDoneButton->setVisible(true);
1345 ui->userNotes->setVisible(false);
1346 ui->userNotesLabel->setVisible(true);
1347 ui->userNotesOpenLink->setVisible(false);
1348 ui->userNotesOpenLink2->setVisible(false);
1349 ui->userNotesOpenLink3->setVisible(false);
1350 });
1351
1352 connect(ui->userNotesOpenLink, &QAbstractButton::clicked, this, [this]()
1353 {
1354 focusOnTable();
1355 QString urlString = findUrl(ui->userNotes->text());
1356 if (urlString.isEmpty())
1357 return;
1358 QDesktopServices::openUrl(QUrl(urlString));
1359 });
1360 connect(ui->userNotesOpenLink2, &QAbstractButton::clicked, this, [this]()
1361 {
1362 focusOnTable();
1363 QString urlString = findUrl(ui->userNotes->text(), 2);
1364 if (urlString.isEmpty())
1365 return;
1366 QDesktopServices::openUrl(QUrl(urlString));
1367 });
1368 connect(ui->userNotesOpenLink3, &QAbstractButton::clicked, this, [this]()
1369 {
1370 focusOnTable();
1371 QString urlString = findUrl(ui->userNotes->text(), 3);
1372 if (urlString.isEmpty())
1373 return;
1374 QDesktopServices::openUrl(QUrl(urlString));
1375 });
1376
1377 connect(ui->loadImagedB, &QPushButton::clicked, this, &ImagingPlanner::loadImagedFile);
1378
1379 ui->SearchText->installEventFilter(this);
1380 connect(ui->SearchB, &QPushButton::clicked, this, &ImagingPlanner::searchSlot);
1381
1382 connect(ui->CatalogView->horizontalHeader(), &QHeaderView::sectionPressed, this, [this](int column)
1383 {
1384 m_CatalogSortModel->setSortColumn(column);
1385 m_CatalogSortModel->invalidate();
1386 ui->CatalogView->resizeColumnsToContents();
1387 });
1388
1389 adjustWindowSize();
1390
1391 connect(ui->helpButton, &QPushButton::clicked, this, &ImagingPlanner::getHelp);
1392 connect(ui->optionsButton, &QPushButton::clicked, this, &ImagingPlanner::openOptionsMenu);
1393
1394 // Since we thread the loading of catalogs, need to connect the thread back to UI.
1395 qRegisterMetaType<QList<QStandardItem *>>("QList<QStandardItem *>");
1396 connect(this, &ImagingPlanner::addRow, this, &ImagingPlanner::addRowSlot);
1397
1398 // Needed to fix weird bug on Windows that started with Qt 5.9 that makes the title bar
1399 // not visible and therefore dialog not movable.
1400#ifdef Q_OS_WIN
1401 move(100, 100);
1402#endif
1403}
1404
1405void ImagingPlanner::openOptionsMenu()
1406{
1407 QSharedPointer<ImagingPlannerOptions> options(new ImagingPlannerOptions(this));
1408 options->exec();
1409 focusOnTable();
1410}
1411
1412// KDE KHelpClient::invokeHelp() doesn't seem to work.
1413void ImagingPlanner::getHelp()
1414{
1415#if 0
1416 // This code can be turned on to check out the targets, but should normally be off.
1417 checkTargets();
1418 return;
1419#endif
1420
1421#if 0
1422 // This code can be turned on to downsame the png images and convert them to jpg
1423 if (downsampleImageFiles("/home/hy/Desktop/SharedFolder/PLANNER_IMAGES/MESSIER", 300))
1424 fprintf(stderr, "downsampling succeeded\n");
1425 else
1426 fprintf(stderr, "downsampling failed\n");
1427
1428 if (downsampleImageFiles("/home/hy/Desktop/SharedFolder/PLANNER_IMAGES/OTHER", 300))
1429 fprintf(stderr, "downsampling succeeded\n");
1430 else
1431 fprintf(stderr, "downsampling failed\n");
1432
1433 if (downsampleImageFiles("/home/hy/Desktop/SharedFolder/PLANNER_IMAGES/CALDWELL", 300))
1434 fprintf(stderr, "downsampling succeeded\n");
1435 else
1436 fprintf(stderr, "downsampling failed\n");
1437
1438 if (downsampleImageFiles("/home/hy/Desktop/SharedFolder/PLANNER_IMAGES/AWARDS", 300))
1439 fprintf(stderr, "downsampling succeeded\n");
1440 else
1441 fprintf(stderr, "downsampling failed\n");
1442
1443 if (downsampleImageFiles("/home/hy/Desktop/SharedFolder/PLANNER_IMAGES/HERSCHEL12", 300))
1444 fprintf(stderr, "downsampling succeeded\n");
1445 else
1446 fprintf(stderr, "downsampling failed\n");
1447#endif
1448 focusOnTable();
1449 const QUrl url("https://docs.kde.org/trunk5/en/kstars/kstars/kstars.pdf#tool-imaging-planner");
1450 if (!url.isEmpty())
1452}
1453
1454KSMoon *ImagingPlanner::getMoon()
1455{
1456 if (KStarsData::Instance() == nullptr)
1457 return nullptr;
1458
1459 KSMoon *moon = dynamic_cast<KSMoon *>(KStarsData::Instance()->skyComposite()->findByName(i18n("Moon")));
1460 if (moon)
1461 {
1462 auto tz = QTimeZone(getGeo()->TZ() * 3600);
1463 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
1464 midnight.setTimeZone(tz);
1465 CachingDms LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(midnight).gst());
1466 KSNumbers numbers(midnight.djd());
1467 moon->updateCoords(&numbers, true, getGeo()->lat(), &LST, true);
1468 }
1469 return moon;
1470}
1471
1472// Setup the moon image.
1473void ImagingPlanner::updateMoon()
1474{
1475 KSMoon *moon = getMoon();
1476 if (!moon)
1477 return;
1478
1479 // You need to know the sun's position in order to get the right phase of the moon.
1480 auto tz = QTimeZone(getGeo()->TZ() * 3600);
1481 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
1482 CachingDms LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(midnight).gst());
1483 KSNumbers numbers(midnight.djd());
1484 KSSun *sun = dynamic_cast<KSSun *>(KStarsData::Instance()->skyComposite()->findByName(i18n("Sun")));
1485 sun->updateCoords(&numbers, true, getGeo()->lat(), &LST, true);
1486 moon->findPhase(sun);
1487
1488 ui->moonImage->setPixmap(QPixmap::fromImage(moon->image().scaled(32, 32, Qt::KeepAspectRatio)));
1489 ui->moonPercentLabel->setText(QString("%1%").arg(moon->illum() * 100.0 + 0.5, 0, 'f', 0));
1490}
1491
1492bool ImagingPlanner::scrollToName(const QString &name)
1493{
1494 if (name.isEmpty())
1495 return false;
1496 QModelIndexList matchList = ui->CatalogView->model()->match(ui->CatalogView->model()->index(0, 0), Qt::EditRole,
1498 if(matchList.count() >= 1)
1499 {
1500 int bestIndex = 0;
1501 for (int i = 0; i < matchList.count(); i++)
1502 {
1503 QString nn = ui->CatalogView->model()->data(matchList[i], Qt::DisplayRole).toString();
1504 if (nn.compare(name, Qt::CaseInsensitive) == 0)
1505 {
1506 bestIndex = i;
1507 break;
1508 }
1509 }
1510 ui->CatalogView->scrollTo(matchList[bestIndex]);
1511 ui->CatalogView->setCurrentIndex(matchList[bestIndex]);
1512 return true;
1513 }
1514 return false;
1515}
1516
1517void ImagingPlanner::searchSlot()
1518{
1519 QString origName = ui->SearchText->toPlainText().trimmed();
1520 QString name = tweakNames(origName);
1521 ui->SearchText->setPlainText(name);
1522 if (name.isEmpty())
1523 return;
1524
1525 if (!scrollToName(name))
1526 KSNotification::sorry(i18n("No match for \"%1\"", origName));
1527
1528 // Still leaves around some </p> in the html unfortunaltely. Don't know how to remove that.
1529 ui->SearchText->clear();
1530 ui->SearchText->setPlainText("");
1531}
1532
1533void ImagingPlanner::initUserNotes()
1534{
1535 ui->userNotesLabel->setVisible(true);
1536 ui->userNotesEdit->setVisible(false);
1537 ui->userNotesEditButton->setVisible(true);
1538 ui->userNotesDoneButton->setVisible(false);
1539 ui->userNotes->setVisible(true);
1540 ui->userNotesLabel->setVisible(true);
1541 ui->userNotesOpenLink->setVisible(false);
1542 ui->userNotesOpenLink2->setVisible(false);
1543 ui->userNotesOpenLink3->setVisible(false);
1544}
1545
1546void ImagingPlanner::disableUserNotes()
1547{
1548 ui->userNotesEdit->setVisible(false);
1549 ui->userNotesEditButton->setVisible(false);
1550 ui->userNotesDoneButton->setVisible(false);
1551 ui->userNotes->setVisible(false);
1552 ui->userNotesLabel->setVisible(false);
1553 ui->userNotesOpenLink->setVisible(false);
1554 ui->userNotesOpenLink2->setVisible(false);
1555 ui->userNotesOpenLink3->setVisible(false);
1556}
1557
1558void ImagingPlanner::userNotesEditFinished()
1559{
1560 const QString &notes = ui->userNotesEdit->toPlainText().trimmed();
1561 ui->userNotes->setText(notes);
1562 ui->userNotesLabel->setVisible(notes.isEmpty());
1563 ui->userNotesEdit->setVisible(false);
1564 ui->userNotesEditButton->setVisible(true);
1565 ui->userNotesDoneButton->setVisible(false);
1566 ui->userNotes->setVisible(true);
1567 ui->userNotesLabel->setVisible(true);
1568 setCurrentObjectNotes(notes);
1569 setupNotesLinks(notes);
1570 focusOnTable();
1571 auto o = currentCatalogObject();
1572 if (!o) return;
1573 saveToDB(currentObjectName(), currentObjectFlags(), notes);
1574}
1575
1576void ImagingPlanner::updateNotes(const QString &notes)
1577{
1578 ui->userNotes->setMaximumWidth(ui->RightPanel->width() - 125);
1579 initUserNotes();
1580 ui->userNotes->setText(notes);
1581 ui->userNotesLabel->setVisible(notes.isEmpty());
1582 setupNotesLinks(notes);
1583}
1584
1585void ImagingPlanner::setupNotesLinks(const QString &notes)
1586{
1587 QString link = findUrl(notes);
1588 ui->userNotesOpenLink->setVisible(!link.isEmpty());
1589 if (!link.isEmpty())
1590 ui->userNotesOpenLink->setToolTip(i18n("Open a browser with the 1st link in this note: %1", link));
1591
1592 link = findUrl(notes, 2);
1593 ui->userNotesOpenLink2->setVisible(!link.isEmpty());
1594 if (!link.isEmpty())
1595 ui->userNotesOpenLink2->setToolTip(i18n("Open a browser with the 2nd link in this note: %1", link));
1596
1597 link = findUrl(notes, 3);
1598 ui->userNotesOpenLink3->setVisible(!link.isEmpty());
1599 if (!link.isEmpty())
1600 ui->userNotesOpenLink3->setToolTip(i18n("Open a browser with the 3rd link in this note: %1", link));
1601}
1602
1603// Given an object name, return the KStars catalog object.
1604bool ImagingPlanner::getKStarsCatalogObject(const QString &name, CatalogObject * catObject)
1605{
1606 // find_objects_by_name is much faster with exactMatchOnly=true.
1607 // Therefore, since most will match exactly given the string pre-processing,
1608 // first try exact=true, and if that fails, follow up with exact=false.
1609 QString filteredName = FindDialog::processSearchText(name).toUpper();
1610 std::list<CatalogObject> objs =
1611 m_manager.find_objects_by_name(filteredName, 1, true);
1612
1613 // If we didn't find it and it's Sharpless, try sh2 with a space instead of a dash
1614 // and vica versa
1615 if (objs.size() == 0 && filteredName.startsWith("sh2-", Qt::CaseInsensitive))
1616 {
1617 QString name2 = filteredName;
1619 objs = m_manager.find_objects_by_name(name2, 1, true);
1620 }
1621 if (objs.size() == 0 && filteredName.startsWith("sh2 ", Qt::CaseInsensitive))
1622 {
1623 QString name2 = filteredName;
1625 objs = m_manager.find_objects_by_name(name2, 1, true);
1626 }
1627
1628 if (objs.size() == 0)
1629 objs = m_manager.find_objects_by_name(filteredName.toLower(), 20, false);
1630 if (objs.size() == 0)
1631 {
1632 QElapsedTimer timer;
1633 timer.start();
1634 // The resolveName search is touchy about the dash.
1635 if (filteredName.startsWith("sh2", Qt::CaseInsensitive))
1636 filteredName.replace(QRegularExpression("sh2\\s*-?", QRegularExpression::CaseInsensitiveOption), "sh2-");
1637 const auto &cedata = NameResolver::resolveName(filteredName);
1638 if (!cedata.first)
1639 {
1640 return false;
1641 }
1642 else
1643 {
1644 m_manager.add_object(CatalogsDB::user_catalog_id, cedata.second);
1645 const auto &added_object =
1646 m_manager.get_object(cedata.second.getId(), CatalogsDB::user_catalog_id);
1647
1648
1649 if (added_object.first)
1650 {
1651 *catObject = KStarsData::Instance()
1652 ->skyComposite()
1653 ->catalogsComponent()
1654 ->insertStaticObject(added_object.second);
1655 }
1656 DPRINTF(stderr, "***** Found %s using name resolver (%.1fs)\n", name.toLatin1().data(),
1657 timer.elapsed() / 1000.0);
1658 return true;
1659 }
1660 }
1661
1662 if (objs.size() == 0)
1663 return false;
1664
1665 // If there is more than one match, see if there's an exact match in name, name2, or longname.
1666 *catObject = objs.front();
1667 if (objs.size() > 1)
1668 {
1669 QString addSpace = filteredName;
1670 addSpace.append(" ");
1671 for (const auto &obj : objs)
1672 {
1673 if ((filteredName.compare(obj.name(), Qt::CaseInsensitive) == 0) ||
1674 (filteredName.compare(obj.name2(), Qt::CaseInsensitive) == 0) ||
1675 obj.longname().contains(addSpace, Qt::CaseInsensitive) ||
1676 obj.longname().endsWith(filteredName, Qt::CaseInsensitive))
1677 {
1678 *catObject = obj;
1679 break;
1680 }
1681 }
1682 }
1683 return true;
1684}
1685
1686CatalogObject *ImagingPlanner::getObject(const QString &name)
1687{
1688 if (name.isEmpty())
1689 return nullptr;
1690 QString lName = name.toLower();
1691 auto o = m_CatalogHash.find(lName);
1692 if (o == m_CatalogHash.end())
1693 return nullptr;
1694 return &(*o);
1695}
1696
1697void ImagingPlanner::clearObjects()
1698{
1699 // Important to tell SkyMap that our objects are gone.
1700 // We give SkyMap points to these objects in ImagingPlanner::centerOnSkymap()
1701 SkyMap::Instance()->setClickedObject(nullptr);
1702 SkyMap::Instance()->setFocusObject(nullptr);
1703 m_CatalogHash.clear();
1704}
1705
1706CatalogObject *ImagingPlanner::addObject(const QString &name)
1707{
1708 if (name.isEmpty())
1709 return nullptr;
1710 QString lName = name.toLower();
1711 if (getObject(lName) != nullptr)
1712 {
1713 DPRINTF(stderr, "Didn't add \"%s\" because it's already there\n", name.toLatin1().data());
1714 return nullptr;
1715 }
1716
1717 CatalogObject o;
1718 if (!getKStarsCatalogObject(lName, &o))
1719 {
1720 DPRINTF(stderr, "************* Couldn't find \"%s\"\n", lName.toLatin1().data());
1721 return nullptr;
1722 }
1723 m_CatalogHash[lName] = o;
1724 return &(m_CatalogHash[lName]);
1725}
1726
1727// Adds the object to the catalog model, assuming a KStars catalog object can be found
1728// for that name.
1729bool ImagingPlanner::addCatalogItem(const KSAlmanac &ksal, const QString &name, int flags)
1730{
1731 CatalogObject *object = addObject(name);
1732 if (object == nullptr)
1733 return false;
1734
1735 auto getItemWithUserRole = [](const QString & itemText) -> QStandardItem *
1736 {
1737 QStandardItem *ret = new QStandardItem(itemText);
1738 ret->setData(itemText, Qt::UserRole);
1740 return ret;
1741 };
1742
1743 // Build the data. The columns must be the same as the #define columns at the top of this file.
1744 QList<QStandardItem *> itemList;
1745 for (int i = 0; i < LAST_COLUMN; ++i)
1746 {
1747 if (i == NAME_COLUMN)
1748 {
1749 itemList.append(getItemWithUserRole(name));
1750 }
1751 else if (i == HOURS_COLUMN)
1752 {
1753 double runHours = getRunHours(*object, getDate(), *getGeo(), ui->minAltitude->value(), ui->minMoon->value(),
1754 ui->useArtificialHorizon->isChecked());
1755 auto hoursItem = getItemWithUserRole(QString("%1").arg(runHours, 0, 'f', 1));
1756 hoursItem->setData(runHours, HOURS_ROLE);
1757 itemList.append(hoursItem);
1758 }
1759 else if (i == TYPE_COLUMN)
1760 {
1761 auto typeItem = getItemWithUserRole(QString("%1").arg(SkyObject::typeShortName(object->type())));
1762 typeItem->setData(object->type(), TYPE_ROLE);
1763 itemList.append(typeItem);
1764 }
1765 else if (i == SIZE_COLUMN)
1766 {
1767 double size = std::max(object->a(), object->b());
1768 auto sizeItem = getItemWithUserRole(QString("%1'").arg(size, 0, 'f', 1));
1769 sizeItem->setData(size, SIZE_ROLE);
1770 itemList.append(sizeItem);
1771 }
1772 else if (i == ALTITUDE_COLUMN)
1773 {
1774 const auto time = KStarsDateTime(QDateTime(getDate(), QTime(12, 0)));
1775 const double altitude = getMaxAltitude(ksal, getDate(), getGeo(), *object, 0, 0);
1776 auto altItem = getItemWithUserRole(QString("%1º").arg(altitude, 0, 'f', 0));
1777 altItem->setData(altitude, ALTITUDE_ROLE);
1778 itemList.append(altItem);
1779 }
1780 else if (i == MOON_COLUMN)
1781 {
1782 KSMoon *moon = getMoon();
1783 if (moon)
1784 {
1785 SkyPoint o;
1786 o.setRA0(object->ra0());
1787 o.setDec0(object->dec0());
1788 auto tz = QTimeZone(getGeo()->TZ() * 3600);
1789 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
1790 midnight.setTimeZone(tz);
1791 KSNumbers numbers(midnight.djd());
1792 o.updateCoordsNow(&numbers);
1793
1794 double const separation = moon->angularDistanceTo(&o).Degrees();
1795 auto moonItem = getItemWithUserRole(QString("%1º").arg(separation, 0, 'f', 0));
1796 moonItem->setData(separation, MOON_ROLE);
1797 itemList.append(moonItem);
1798 }
1799 else
1800 {
1801 auto moonItem = getItemWithUserRole(QString(""));
1802 moonItem->setData(-1, MOON_ROLE);
1803 }
1804 }
1805 else if (i == CONSTELLATION_COLUMN)
1806 {
1807 QString cname = KStarsData::Instance()
1808 ->skyComposite()
1809 ->constellationBoundary()
1810 ->constellationName(object);
1811 cname = cname.toLower().replace(0, 1, cname[0].toUpper());
1812 auto constellationItem = getItemWithUserRole(cname);
1813 itemList.append(constellationItem);
1814 }
1815 else if (i == COORD_COLUMN)
1816 {
1817 itemList.append(getItemWithUserRole(shortCoordString(object->ra0(), object->dec0())));
1818 }
1819 else if (i == FLAGS_COLUMN)
1820 {
1821 QStandardItem *flag = getItemWithUserRole("flag");
1822 flag->setData(flags, FLAGS_ROLE);
1823 itemList.append(flag);
1824 }
1825 else if (i == NOTES_COLUMN)
1826 {
1827 QStandardItem *notes = getItemWithUserRole("notes");
1828 notes->setData(QString(), NOTES_ROLE);
1829 itemList.append(notes);
1830 }
1831 else
1832 {
1833 DPRINTF(stderr, "Bug in addCatalogItem() !\n");
1834 }
1835 }
1836
1837 // Can't do UI in this thread, must move back to the UI thread.
1838 emit addRow(itemList);
1839 return true;
1840}
1841
1842void ImagingPlanner::addRowSlot(QList<QStandardItem *> itemList)
1843{
1844 m_CatalogModel->appendRow(itemList);
1845 updateCounts();
1846}
1847
1848void ImagingPlanner::recompute()
1849{
1850 setStatus(i18n("Updating tables..."));
1851
1852 // Disconnect the filter from the model, or else we'll re-filter numRows squared times.
1853 m_CatalogSortModel->setSourceModel(nullptr);
1854
1855 QElapsedTimer timer;
1856 timer.start();
1857
1858 auto tz = QTimeZone(getGeo()->TZ() * 3600);
1859 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
1860 KStarsDateTime ut = getGeo()->LTtoUT(KStarsDateTime(midnight));
1861 KSAlmanac ksal(ut, getGeo());
1862
1863 for (int i = 0; i < m_CatalogModel->rowCount(); ++i)
1864 {
1865 const QString &name = m_CatalogModel->item(i, 0)->text();
1866 const CatalogObject *catalogEntry = getObject(name);
1867 if (catalogEntry == nullptr)
1868 {
1869 DPRINTF(stderr, "************* Couldn't find \"%s\"\n", name.toLatin1().data());
1870 return;
1871 }
1872 double runHours = getRunHours(*catalogEntry, getDate(), *getGeo(), ui->minAltitude->value(),
1873 ui->minMoon->value(), ui->useArtificialHorizon->isChecked());
1874 QString hoursText = QString("%1").arg(runHours, 0, 'f', 1);
1875 QStandardItem *hItem = new QStandardItem(hoursText);
1876 hItem->setData(hoursText, Qt::UserRole);
1878 hItem->setData(runHours, HOURS_ROLE);
1879 m_CatalogModel->setItem(i, HOURS_COLUMN, hItem);
1880
1881
1882 const auto time = KStarsDateTime(QDateTime(getDate(), QTime(12, 0)));
1883 const double altitude = getMaxAltitude(ksal, getDate(), getGeo(), *catalogEntry, 0, 0);
1884 QString altText = QString("%1º").arg(altitude, 0, 'f', 0);
1885 auto altItem = new QStandardItem(altText);
1886 altItem->setData(altText, Qt::UserRole);
1887 altItem->setData(altitude, ALTITUDE_ROLE);
1888 m_CatalogModel->setItem(i, ALTITUDE_COLUMN, altItem);
1889
1890 KSMoon *moon = getMoon();
1891 if (moon)
1892 {
1893 SkyPoint o;
1894 o.setRA0(catalogEntry->ra0());
1895 o.setDec0(catalogEntry->dec0());
1896 auto tz = QTimeZone(getGeo()->TZ() * 3600);
1897 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
1898 midnight.setTimeZone(tz);
1899 KSNumbers numbers(midnight.djd());
1900 o.updateCoordsNow(&numbers);
1901
1902 double const separation = moon->angularDistanceTo(&o).Degrees();
1903 QString moonText = QString("%1º").arg(separation, 0, 'f', 0);
1904 auto moonItem = new QStandardItem(moonText);
1905 moonItem->setData(moonText, Qt::UserRole);
1906 moonItem->setData(separation, MOON_ROLE);
1907 m_CatalogModel->setItem(i, MOON_COLUMN, moonItem);
1908 }
1909 else
1910 {
1911 auto moonItem = new QStandardItem("");
1912 moonItem->setData("", Qt::UserRole);
1913 moonItem->setData(-1, MOON_ROLE);
1914 m_CatalogModel->setItem(i, MOON_COLUMN, moonItem);
1915 }
1916
1917 // Don't lose the imaged background highlighting.
1918 const bool imaged = m_CatalogModel->item(i, FLAGS_COLUMN)->data(FLAGS_ROLE).toInt() & IMAGED_BIT;
1919 if (imaged)
1920 highlightImagedObject(m_CatalogModel->index(i, NAME_COLUMN), true);
1921 const bool picked = m_CatalogModel->item(i, FLAGS_COLUMN)->data(FLAGS_ROLE).toInt() & PICKED_BIT;
1922 if (picked)
1923 highlightPickedObject(m_CatalogModel->index(i, NAME_COLUMN), true);
1924 }
1925 // Reconnect the filter to the model.
1926 m_CatalogSortModel->setSourceModel(m_CatalogModel.data());
1927
1928 DPRINTF(stderr, "Recompute took %.1fs\n", timer.elapsed() / 1000.0);
1929 updateStatus();
1930}
1931
1932// Debugging/development method.
1933// Use this to sanitize the list of catalog objects.
1934// enable in header also
1935void ImagingPlanner::checkTargets()
1936{
1937 FlagComponent *flags = KStarsData::Instance()->skyComposite()->flags();
1938
1939 fprintf(stderr, "****************** check objects (%d)***************\n", flags->size());
1940 for (int i = flags->size() - 1; i >= 0; --i) flags->remove(i);
1941 fprintf(stderr, "Removed, now %d\n", flags->size());
1942 QList<QString> targets;
1943 int rows = m_CatalogModel->rowCount();
1944 QVector<bool> accepted(rows);
1945
1946
1947 for (int i = 0; i < rows; ++i)
1948 {
1949 const QString &name = m_CatalogModel->item(i, NAME_COLUMN)->text();
1950 targets.push_back(name);
1951 accepted[i] = getObject(name) != nullptr;
1952
1953 auto object = getObject(name);
1954 if (object)
1955 {
1956 flags->add(SkyPoint(object->ra(), object->dec()), "J2000.0", "", name, Qt::red);
1957 fprintf(stderr, "%d ", i);
1958 }
1959
1960 }
1961 for (int i = 0; i < targets.size(); ++i)
1962 {
1963 if (accepted[i])
1964 {
1965 auto objectName = targets[i];
1966 auto object = getObject(objectName);
1967 object->setRA(object->ra0());
1968 object->setDec(object->dec0());
1969 for (int j = 0; j < targets.size(); ++j)
1970 {
1971 if (i == j) continue;
1972 if (!accepted[j]) continue;
1973 auto name2 = targets[j];
1974 auto object2 = getObject(name2);
1975 object2->setRA(object2->ra0());
1976 object2->setDec(object2->dec0());
1977 const dms dist = object->angularDistanceTo(object2);
1978 const double arcsecDist = dist.Degrees() * 3600.0;
1979 if (arcsecDist < 120)
1980 {
1981 fprintf(stderr, "dist %10s (%s %s) to %10s (%s %s) = %.0f\" %s\n",
1982 objectName.toLatin1().data(),
1983 object->ra().toHMSString().toLatin1().data(),
1984 object->dec().toDMSString().toLatin1().data(),
1985 name2.toLatin1().data(),
1986 object2->ra().toHMSString().toLatin1().data(),
1987 object2->dec().toDMSString().toLatin1().data(),
1988 arcsecDist, object->longname().toLatin1().data());
1989 }
1990 }
1991 }
1992 }
1993
1994 fprintf(stderr, "Done\n");
1995
1996 // Clean up.
1997 ///clearObjects();
1998}
1999
2000// This is the top-level ImagingPlanning catalog directory.
2001QString ImagingPlanner::defaultDirectory() const
2002{
2003 return KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)
2004 + QDir::separator() + "ImagingPlanner";
2005}
2006
2007// The default catalog is one loaded by the "Data -> Download New Data..." menu.
2008// Search the default directory for a ImagingPlanner subdirectory
2009QString ImagingPlanner::findDefaultCatalog() const
2010{
2011 const QFileInfoList subDirs = QDir(defaultDirectory()).entryInfoList(
2013 for (int i = 0; i < subDirs.size(); i++)
2014 {
2015 // Found a possible catalog directory. Will pick the first one we find with .csv files.
2016 const QDir subDir(subDirs[i].absoluteFilePath());
2017 const QStringList csvFilter({"*.csv"});
2018 const QFileInfoList files = subDir.entryInfoList(csvFilter, QDir::NoDotAndDotDot | QDir::Files);
2019 if (files.size() > 0)
2020 {
2021 QString firstFile;
2022 // Look through all the .csv files. Pick all.csv if it exists,
2023 // otherwise one of the other .csv files.
2024 for (const auto &file : files)
2025 {
2026 if (firstFile.isEmpty())
2027 firstFile = file.absoluteFilePath();
2028 if (!file.baseName().compare("all", Qt::CaseInsensitive))
2029 return file.absoluteFilePath();
2030 }
2031 if (!firstFile.isEmpty())
2032 return firstFile;
2033 }
2034 }
2035 return QString();
2036}
2037
2038void ImagingPlanner::loadInitialCatalog()
2039{
2040 QString catalog = Options::imagingPlannerCatalogPath();
2041 if (catalog.isEmpty())
2042 catalog = findDefaultCatalog();
2043 if (catalog.isEmpty())
2044 {
2045 KSNotification::sorry(i18n("You need to load a catalog to start using this tool.\nSee Data -> Download New Data..."));
2046 setStatus(i18n("No Catalog!"));
2047 }
2048 else
2049 loadCatalog(catalog);
2050}
2051
2052void ImagingPlanner::setStatus(const QString &message)
2053{
2054 ui->statusLabel->setText(message);
2055 ui->statusLabel->repaint();
2056}
2057
2058void ImagingPlanner::catalogLoaded()
2059{
2060 DPRINTF(stderr, "All catalogs loaded: %d of %d have catalog images\n", m_numWithImage, m_numWithImage + m_numMissingImage);
2061 // This cannot go in the threaded loadInitialCatalog()!
2062 loadFromDB();
2063
2064 // TODO: At this point we'd read in various files (picked/imaged/deleted targets ...)
2065 // Can't do this in initialize() as we don't have columns yet.
2066 ui->CatalogView->setColumnHidden(FLAGS_COLUMN, true);
2067 ui->CatalogView->setColumnHidden(NOTES_COLUMN, true);
2068
2069 m_CatalogSortModel->invalidate();
2070 ui->CatalogView->sortByColumn(HOURS_COLUMN, Qt::DescendingOrder);
2071 ui->CatalogView->resizeColumnsToContents();
2072
2073 // Select the first row and give it the keyboard focus (so up/down keyboard keys work).
2074 auto index = ui->CatalogView->model()->index(0, 0);
2075 //ui->CatalogView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select |QItemSelectionModel::Current| QItemSelectionModel::Rows);
2076 ui->CatalogView->selectionModel()->select(index,
2078 ui->CatalogView->setFocus();
2079 updateDisplays();
2080
2081 updateStatus();
2082 adjustWindowSize();
2083}
2084
2085void ImagingPlanner::updateStatus()
2086{
2087 if (currentObjectName().isEmpty())
2088 {
2089 const int numDisplayedObjects = m_CatalogSortModel->rowCount();
2090 const int totalCatalogObjects = m_CatalogModel->rowCount();
2091
2092 if (numDisplayedObjects > 0)
2093 setStatus(i18n("Select an object."));
2094 else if (totalCatalogObjects > 0)
2095 setStatus(i18n("Check Filters to unhide objects."));
2096 else
2097 setStatus(i18n("Load a Catalog."));
2098 }
2099 else
2100 setStatus("");
2101}
2102
2103// This runs when the window gets a show event. Mostly used on first show to load the catalogs.
2104void ImagingPlanner::showEvent(QShowEvent *e)
2105{
2106 // ONLY run for first ever load
2107 if (m_initialResultsLoad == false)
2108 {
2109 initialize();
2110 m_initialResultsLoad = true;
2112 }
2113}
2114
2115void ImagingPlanner::resizeEvent(QResizeEvent *e)
2116{
2118}
2119
2120//FIXME: On close, we will need to close any open Details/AVT windows
2121void ImagingPlanner::slotClose()
2122{
2123}
2124
2125// Reverse engineering of the Astrobin search URL (with permission from Salvatore).
2126// See https://github.com/astrobin/astrobin/blob/master/common/encoded_search_viewset.py#L15
2127QUrl ImagingPlanner::getAstrobinUrl(const QString &target, bool requireAwards, bool requireSomeFilters, double minRadius,
2128 double maxRadius)
2129{
2130 QString myQuery = QString("text={\"value\":\"%1\",\"matchType\":\"ALL\"}").arg(target);
2131
2132 // This is a place where the actual date, not the date in the widget, is the right one to find.
2133 auto localTime = getGeo()->UTtoLT(KStarsData::Instance()->clock()->utc());
2134 QDate today = localTime.date();
2135 myQuery.append(QString("&date_acquired={\"min\":\"2018-01-01\",\"max\":\"%1\"}").arg(today.toString("yyyy-MM-dd")));
2136
2137 if (requireAwards)
2138 myQuery.append(QString("&award=[\"iotd\",\"top-pick\",\"top-pick-nomination\"]"));
2139
2140 if (requireSomeFilters)
2141 myQuery.append(QString("&filter_types={\"value\":[\"H_ALPHA\",\"SII\",\"OIII\",\"R\",\"G\",\"B\"],\"matchType\":\"ANY\"}"));
2142
2143 if ((minRadius > 0 || maxRadius > 0) && (maxRadius > minRadius))
2144 myQuery.append(QString("&field_radius={\"min\":%1,\"max\":%2}").arg(minRadius).arg(maxRadius));
2145
2146 QByteArray b(myQuery.toLatin1().data());
2147
2148 // See quick pack implmentation in anonymous namespace above.
2149 QByteArray packed = pack(b);
2150
2151 QByteArray compressed = qCompress(packed).remove(0, 4);
2152
2153 QByteArray b64 = compressed.toBase64();
2154
2155 replaceByteArrayChars(b64, '+', QByteArray("%2B"));
2156 replaceByteArrayChars(b64, '=', QByteArray("%3D"));
2157 replaceByteArrayChars(b, '"', QByteArray("%22"));
2158 replaceByteArrayChars(b, ':', QByteArray("%3A"));
2159 replaceByteArrayChars(b, '[', QByteArray("%5B"));
2160 replaceByteArrayChars(b, ']', QByteArray("%5D"));
2161 replaceByteArrayChars(b, ',', QByteArray("%2C"));
2162 replaceByteArrayChars(b, '\'', QByteArray("%27"));
2163 replaceByteArrayChars(b, '{', QByteArray("%7B"));
2164 replaceByteArrayChars(b, '}', QByteArray("%7D"));
2165
2166 QString url = QString("https://app.astrobin.com/search?p=%1").arg(b64.toStdString().c_str());
2167 return QUrl(url);
2168}
2169
2170void ImagingPlanner::popupAstrobin(const QString &target)
2171{
2172 QString newStr = massageObjectName(target);
2173 if (newStr.isEmpty()) return;
2174
2175 const QUrl url = getAstrobinUrl(newStr, Options::astrobinAward(), false, Options::astrobinMinRadius(),
2176 Options::astrobinMaxRadius());
2177 if (!url.isEmpty())
2179}
2180
2181// Popup a browser on the Professor Segilman website https://cseligman.com
2182void ImagingPlanner::searchNGCICImages()
2183{
2184 focusOnTable();
2185 auto o = currentCatalogObject();
2186 if (!o)
2187 {
2188 fprintf(stderr, "NULL object sent to searchNGCICImages.\n");
2189 return;
2190 }
2191 int num = -1;
2192 if (o->name().startsWith("ngc", Qt::CaseInsensitive))
2193 {
2194 num = o->name().mid(3).toInt();
2195 QString urlString = QString("https://cseligman.com/text/atlas/ngc%1%2.htm#%3").arg(num / 100).arg(
2196 num % 100 < 50 ? "" : "a").arg(num);
2197 QDesktopServices::openUrl(QUrl(urlString));
2198 return;
2199 }
2200 else if (o->name().startsWith("ic", Qt::CaseInsensitive))
2201 {
2202 num = o->name().mid(2).toInt();
2203 QString urlString = QString("https://cseligman.com/text/atlas/ic%1%2.htm#ic%3").arg(num / 100).arg(
2204 num % 100 < 50 ? "" : "a").arg(num);
2205 QDesktopServices::openUrl(QUrl(urlString));
2206 return;
2207 }
2208}
2209
2210void ImagingPlanner::searchSimbad()
2211{
2212 focusOnTable();
2213 QString name = currentObjectName();
2214
2215 if (name.startsWith("sh2"))
2216 name.replace(QRegularExpression("sh2\\s*"), "sh2-");
2217 else if (name.startsWith("hickson", Qt::CaseInsensitive))
2218 name.replace(QRegularExpression("hickson\\s*"), "HCG");
2219 else
2220 name.replace(' ', "");
2221
2222 QString urlStr = QString("https://simbad.cds.unistra.fr/simbad/sim-id?Ident=%1&NbIdent=1"
2223 "&Radius=20&Radius.unit=arcmin&submit=submit+id").arg(name);
2225}
2226
2227
2228// Crude massaging to conform to wikipedia standards
2229void ImagingPlanner::searchWikipedia()
2230{
2231 focusOnTable();
2232 QString wikipediaAddress = "https://en.wikipedia.org";
2233 QString name = currentObjectName();
2234 if (name.isEmpty())
2235 {
2236 fprintf(stderr, "NULL object sent to Wikipedia.\n");
2237 return;
2238 }
2239
2240 QString massagedName = name;
2242 massagedName = QString("Messier_%1").arg(name.mid(2, -1));
2243 else if (name.startsWith("ngc ", Qt::CaseInsensitive))
2244 massagedName = QString("NGC_%1").arg(name.mid(4, -1));
2245 else if (name.startsWith("ic ", Qt::CaseInsensitive))
2246 massagedName = QString("IC_%1").arg(name.mid(3, -1));
2247 else if (name.startsWith("sh2 ", Qt::CaseInsensitive))
2248 massagedName = QString("sh2-%1").arg(name.mid(4, -1));
2249 else if (name.startsWith("Abell ", Qt::CaseInsensitive))
2250 massagedName = QString("Abell_%1").arg(name.mid(6, -1));
2251 else
2252 {
2253 QString backupSearch = QString("%1/w/index.php?search=%2")
2254 .arg(wikipediaAddress).arg(massageObjectName(name));
2255 return;
2256 }
2258 QUrl(QString("%1/wiki/%2").arg(wikipediaAddress).arg(massagedName)));
2259}
2260
2261void ImagingPlanner::searchAstrobin()
2262{
2263 focusOnTable();
2264 QString name = currentObjectName();
2265 if (name.isEmpty())
2266 return;
2267 popupAstrobin(name);
2268}
2269
2270bool ImagingPlanner::eventFilter(QObject * obj, QEvent * event)
2271{
2272 if (m_InitialLoad && event->type() == QEvent::Paint)
2273 {
2274 m_InitialLoad = false;
2275 // Load the initial catalog in another thread.
2276 setStatus(i18n("Loading Catalogs..."));
2277 loadInitialCatalog();
2278 }
2279
2280 // Right click on object in catalog view brings up this menu.
2281 QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
2282 if ((obj == ui->CatalogView->viewport()) &&
2283 // (ui->CatalogView->currentIndex().row() >= 0) &&
2284 (event->type() == QEvent::MouseButtonRelease) &&
2285 (mouseEvent->button() == Qt::RightButton))
2286 {
2287 int numImaged = 0, numNotImaged = 0, numPicked = 0, numNotPicked = 0, numIgnored = 0, numNotIgnored = 0;
2288 QStringList selectedNames;
2289 for (const auto &r : ui->CatalogView->selectionModel()->selectedRows())
2290 {
2291 selectedNames.append(r.siblingAtColumn(0).data().toString());
2292 bool isPicked = getFlag(r, PICKED_BIT, ui->CatalogView->model());
2293 if (isPicked) numPicked++;
2294 else numNotPicked++;
2295 bool isImaged = getFlag(r, IMAGED_BIT, ui->CatalogView->model());
2296 if (isImaged) numImaged++;
2297 else numNotImaged++;
2298 bool isIgnored = getFlag(r, IGNORED_BIT, ui->CatalogView->model());
2299 if (isIgnored) numIgnored++;
2300 else numNotIgnored++;
2301 }
2302
2303 if (selectedNames.size() == 0)
2304 return false;
2305
2306 if (!m_PopupMenu)
2307 m_PopupMenu = new ImagingPlannerPopup;
2308
2309 const bool imaged = numImaged > 0;
2310 const bool picked = numPicked > 0;
2311 const bool ignored = numIgnored > 0;
2312 m_PopupMenu->init(this, selectedNames,
2313 (numImaged > 0 && numNotImaged > 0) ? nullptr : &imaged,
2314 (numPicked > 0 && numNotPicked > 0) ? nullptr : &picked,
2315 (numIgnored > 0 && numNotIgnored > 0) ? nullptr : &ignored);
2316 QPoint pos(mouseEvent->globalX(), mouseEvent->globalY());
2317 m_PopupMenu->popup(pos);
2318 }
2319
2320 else if (obj == ui->userNotesEdit && event->type() == QEvent::FocusOut)
2321 userNotesEditFinished();
2322
2323 else if (obj == ui->keywordEdit && event->type() == QEvent::FocusOut)
2324 keywordEditFinished();
2325
2326 else if (obj == ui->keywordEdit && (event->type() == QEvent::KeyPress))
2327 {
2328 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
2329 auto key = keyEvent->key();
2330 switch(key)
2331 {
2332 case Qt::Key_Enter:
2333 case Qt::Key_Tab:
2334 case Qt::Key_Return:
2335 keywordEditFinished();
2336 ui->keywordEdit->clearFocus();
2337 break;
2338 default:
2339 ;
2340 }
2341 }
2342
2343 else if (obj == ui->SearchText && (event->type() == QEvent::KeyPress))
2344 {
2345 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
2346 auto key = keyEvent->key();
2347 switch(key)
2348 {
2349 case Qt::Key_Enter:
2350 case Qt::Key_Tab:
2351 case Qt::Key_Return:
2352 searchSlot();
2353 break;
2354 default:
2355 ;
2356 }
2357 }
2358
2359 else if ((obj == ui->ImagePreview ||
2360 obj == ui->ImagePreviewCredit ||
2361 obj == ui->ImagePreviewCreditLink) &&
2363 {
2364 if (!ui->ImagePreviewCreditLink->text().isEmpty())
2365 {
2366 QUrl url(ui->ImagePreviewCreditLink->text());
2368 }
2369 }
2370
2371 return false;
2372}
2373
2374void ImagingPlanner::keywordEditFinished()
2375{
2376 QString kwd = ui->keywordEdit->toPlainText().trimmed();
2377 ui->keywordEdit->clear();
2378 ui->keywordEdit->setText(kwd);
2379 if (m_Keyword != kwd)
2380 {
2381 m_Keyword = kwd;
2382 Options::setImagingPlannerKeyword(kwd);
2383 Options::self()->save();
2384 updateSortConstraints();
2385 m_CatalogSortModel->invalidate();
2386 ui->CatalogView->resizeColumnsToContents();
2387 updateDisplays();
2388 }
2389
2390}
2391void ImagingPlanner::setDefaultImage()
2392{
2393 ui->ImagePreview->setPixmap(m_NoImagePixmap);
2394 ui->ImagePreview->update();
2395 ui->ImagePreviewCredit->setText("");
2396 ui->ImagePreviewCreditLink->setText("");
2397}
2398
2399void ImagingPlanner::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
2400{
2401 Q_UNUSED(deselected);
2402 if (selected.indexes().size() == 0)
2403 {
2404 disableUserNotes();
2405 return;
2406 }
2407
2408 initUserNotes();
2409 updateStatus();
2410 auto selection = selected.indexes()[0];
2411 QString name = selection.data().toString();
2412 CatalogObject *object = getObject(name);
2413 if (object == nullptr)
2414 return;
2415
2416 // This assumes current object and current selection are the same.
2417 // Could pass in "selected" if necessary.
2418 updateDisplays();
2419
2420 ui->ImagePreviewCredit->setText("");
2421 ui->ImagePreviewCreditLink->setText("");
2422 // clear the image too?
2423
2424 CatalogImageInfo catalogImageInfo;
2425 if (findCatalogImageInfo(name, &catalogImageInfo))
2426 {
2427 QString filename = catalogImageInfo.m_Filename;
2428 if (!filename.isEmpty() && !Options::imagingPlannerCatalogPath().isEmpty())
2429 {
2430 QString imageFullPath = filename;
2431 if (QFileInfo(filename).isRelative())
2432 {
2433 QString catDir = QFileInfo(Options::imagingPlannerCatalogPath()).absolutePath();
2434 imageFullPath = QString("%1%2%3").arg(catDir)
2435 .arg(QDir::separator()).arg(filename);
2436 }
2437 if (!QFile(imageFullPath).exists())
2438 DPRINTF(stderr, "Image for \"%s\" -- \"%s\" doesn't exist\n",
2439 name.toLatin1().data(), imageFullPath.toLatin1().data());
2440
2441 ui->ImagePreview->setPixmap(QPixmap::fromImage(QImage(imageFullPath)));
2442 if (!catalogImageInfo.m_Link.isEmpty())
2443 {
2444 ui->ImagePreviewCreditLink->setText(catalogImageInfo.m_Link);
2445 ui->ImagePreview->setToolTip("Click to see original");
2446 ui->ImagePreviewCreditLink->setToolTip("Click to see original");
2447 }
2448 else
2449 {
2450 ui->ImagePreviewCreditLink->setText("");
2451 ui->ImagePreview->setToolTip("");
2452 ui->ImagePreviewCreditLink->setToolTip("");
2453 }
2454
2455 if (!catalogImageInfo.m_Author.isEmpty() && !catalogImageInfo.m_License.isEmpty())
2456 {
2457 ui->ImagePreviewCredit->setText(
2458 QString("Credit: %1 (with license %2)").arg(catalogImageInfo.m_Author)
2459 .arg(creativeCommonsString(catalogImageInfo.m_License)));
2460 ui->ImagePreviewCredit->setToolTip(
2461 QString("Original image license: %1")
2462 .arg(creativeCommonsTooltipString(catalogImageInfo.m_License)));
2463 }
2464 else if (!catalogImageInfo.m_Author.isEmpty())
2465 {
2466 ui->ImagePreviewCredit->setText(
2467 QString("Credit: %1").arg(catalogImageInfo.m_Author));
2468 ui->ImagePreviewCredit->setToolTip("");
2469 }
2470 else if (!catalogImageInfo.m_License.isEmpty())
2471 {
2472 ui->ImagePreviewCredit->setText(
2473 QString("(license %1)").arg(creativeCommonsString(catalogImageInfo.m_License)));
2474 ui->ImagePreviewCredit->setToolTip(
2475 QString("Original image license: %1")
2476 .arg(creativeCommonsTooltipString(catalogImageInfo.m_License)));
2477 }
2478 else
2479 {
2480 ui->ImagePreviewCredit->setText("");
2481 ui->ImagePreviewCredit->setToolTip("");
2482 }
2483 }
2484 }
2485 else
2486 {
2487 object->load_image();
2488 auto image = object->image();
2489 if (!image.first)
2490 {
2491 // As a backup, see if the image is stored elsewhere...
2492 // I've seen many images stored in ~/.local/share/kstars/ZZ/ZZ-name.png,
2493 // e.g. kstars/thumb_ngc/thumb_ngc-m1.png
2494 const QString foundFilename = findObjectImage(name);
2495 if (!name.isEmpty())
2496 {
2497 constexpr int thumbHeight = 300, thumbWidth = 400;
2498 const QImage img = QImage(foundFilename);
2499 const bool scale = img.width() > thumbWidth || img.height() > thumbHeight;
2500 if (scale)
2501 ui->ImagePreview->setPixmap(
2502 QPixmap::fromImage(img.scaled(thumbWidth, thumbHeight, Qt::KeepAspectRatio)));
2503 else
2504 ui->ImagePreview->setPixmap(QPixmap::fromImage(img));
2505 }
2506 else
2507 setDefaultImage();
2508 }
2509 else
2510 ui->ImagePreview->setPixmap(QPixmap::fromImage(image.second));
2511 }
2512
2513}
2514
2515void ImagingPlanner::updateDisplays()
2516{
2517 updateCounts();
2518
2519 // If nothing is selected, then select the first thing.
2520 if (!currentCatalogObject())
2521 {
2522 if (ui->CatalogView->model()->rowCount() > 0)
2523 {
2524 auto index = ui->CatalogView->model()->index(0, 0);
2525 ui->CatalogView->selectionModel()->select(index,
2527 }
2528 }
2529
2530 auto object = currentCatalogObject();
2531 if (object)
2532 {
2533 updateDetails(*object, currentObjectFlags());
2534 updateNotes(currentObjectNotes());
2535 plotAltitudeGraph(getDate(), object->ra0(), object->dec0());
2536 centerOnSkymap();
2537 }
2538 updateStatus();
2539 focusOnTable();
2540}
2541
2542void ImagingPlanner::updateDetails(const CatalogObject &object, int flags)
2543{
2544 ui->infoObjectName->setText(object.name());
2545 ui->infoSize->setText(QString("%1' x %2'").arg(object.a(), 0, 'f', 1).arg(object.b(), 0, 'f', 1));
2546
2547 QPalette palette = ui->infoObjectLongName->palette();
2548 //palette.setColor(ui->infoObjectLongName->backgroundRole(), Qt::darkGray);
2549 palette.setColor(ui->infoObjectLongName->foregroundRole(), Qt::darkGray);
2550 ui->infoObjectLongName->setPalette(palette);
2551 if (object.longname().isEmpty() || (object.longname() == object.name()))
2552 ui->infoObjectLongName->clear();
2553 else
2554 ui->infoObjectLongName->setText(QString("(%1)").arg(object.longname()));
2555
2556 ui->infoObjectType->setText(SkyObject::typeName(object.type()));
2557
2558 auto noon = KStarsDateTime(getDate(), QTime(12, 0, 0));
2559 QTime riseTime = object.riseSetTime(noon, getGeo(), true);
2560 QTime setTime = object.riseSetTime(noon, getGeo(), false);
2561 QTime transitTime = object.transitTime(noon, getGeo());
2562 dms transitAltitude = object.transitAltitude(noon, getGeo());
2563
2564 QString moonString;
2565 KSMoon *moon = getMoon();
2566 if (moon)
2567 {
2568 const double separation = ui->CatalogView->selectionModel()->currentIndex()
2569 .siblingAtColumn(MOON_COLUMN).data(MOON_ROLE).toDouble();
2570 // The unicode character is the angle sign.
2571 if (separation >= 0)
2572 moonString = QString("%1 \u2220 %3º").arg(i18n("Moon")).arg(separation, 0, 'f', 1);
2573 }
2574
2575 QString riseSetString;
2576 if (!riseTime.isValid() && !setTime.isValid() && transitTime.isValid())
2577 riseSetString = QString("%1 %2 @ %3º")
2578 .arg(i18n("Transits"))
2579 .arg(transitTime.toString("h:mm"))
2580 .arg(transitAltitude.Degrees(), 0, 'f', 1);
2581 else if (!riseTime.isValid() && setTime.isValid() && !transitTime.isValid())
2582 riseSetString = QString("%1 %2")
2583 .arg(i18n("Sets at"))
2584 .arg(setTime.toString("h:mm"));
2585 else if (!riseTime.isValid() && setTime.isValid() && transitTime.isValid())
2586 riseSetString = QString("%1 %2 %3 %4 @ %5º")
2587 .arg(i18n("Sets at"))
2588 .arg(setTime.toString("h:mm"))
2589 .arg(i18n("Transit"))
2590 .arg(transitTime.toString("h:mm"))
2591 .arg(transitAltitude.Degrees(), 0, 'f', 1);
2592 else if (riseTime.isValid() && !setTime.isValid() && !transitTime.isValid())
2593 riseSetString = QString("%1 %2")
2594 .arg(i18n("Rises at"))
2595 .arg(riseTime.toString("h:mm"));
2596 else if (riseTime.isValid() && !setTime.isValid() && transitTime.isValid())
2597 riseSetString = QString("%1 %2 %3 %4 @ %5º")
2598 .arg(i18n("Rises at"))
2599 .arg(riseTime.toString("h:mm"))
2600 .arg(i18n("Transit"))
2601 .arg(transitTime.toString("h:mm"))
2602 .arg(transitAltitude.Degrees(), 0, 'f', 1);
2603 else if (riseTime.isValid() && setTime.isValid() && !transitTime.isValid())
2604 riseSetString = QString("%1 %2 %3 %4")
2605 .arg(i18n("Rises"))
2606 .arg(riseTime.toString("h:mm"))
2607 .arg(i18n("Sets"))
2608 .arg(setTime.toString("h:mm"));
2609 else if (riseTime.isValid() && setTime.isValid() && transitTime.isValid())
2610 riseSetString = QString("%1 %2 %3 %4 %5 %6 @ %7º")
2611 .arg(i18n("Rises"))
2612 .arg(riseTime.toString("h:mm"))
2613 .arg(i18n("Sets"))
2614 .arg(setTime.toString("h:mm"))
2615 .arg(i18n("Transit"))
2616 .arg(transitTime.toString("h:mm"))
2617 .arg(transitAltitude.Degrees(), 0, 'f', 1);
2618 if (moonString.size() > 0)
2619 riseSetString.append(QString(", %1").arg(moonString));
2620 ui->infoRiseSet->setText(riseSetString);
2621
2622 palette = ui->infoObjectFlags->palette();
2623 palette.setColor(ui->infoObjectFlags->foregroundRole(), Qt::darkGray);
2624 ui->infoObjectFlags->setPalette(palette);
2625 ui->infoObjectFlags->setText(flagString(flags));
2626}
2627
2628// TODO: This code needs to be shared with the scheduler somehow.
2629// Right now 2 very similar copies at the end of scheduler.cpp and here.
2630//
2631// Clearly below I had timezone issues. The problem was running this code using a timezone
2632// that was not the local timezone of the machine. E.g. setting KStars to australia
2633// when I'm in california.
2634void ImagingPlanner::plotAltitudeGraph(const QDate &date, const dms &ra, const dms &dec)
2635{
2636 auto altitudeGraph = ui->altitudeGraph;
2637 altitudeGraph->setAltitudeAxis(-20.0, 90.0);
2638 //altitudeGraph->axis(KPlotWidget::TopAxis)->setVisible(false);
2639
2640 QVector<QDateTime> jobStartTimes, jobEndTimes;
2641 getRunTimes(date, *getGeo(), ui->minAltitude->value(), ui->minMoon->value(), ra, dec, ui->useArtificialHorizon->isChecked(),
2642 &jobStartTimes, &jobEndTimes);
2643
2644 auto tz = QTimeZone(getGeo()->TZ() * 3600);
2645 KStarsDateTime midnight = KStarsDateTime(date.addDays(1), QTime(0, 1));
2646 midnight.setTimeZone(tz);
2647
2648 KStarsDateTime ut = getGeo()->LTtoUT(KStarsDateTime(midnight));
2649 KSAlmanac ksal(ut, getGeo());
2650 QDateTime dawn = midnight.addSecs(24 * 3600 * ksal.getDawnAstronomicalTwilight());
2651 dawn.setTimeZone(tz);
2652 QDateTime dusk = midnight.addSecs(24 * 3600 * ksal.getDuskAstronomicalTwilight());
2653 dusk.setTimeZone(tz);
2654
2655 Ekos::SchedulerJob job;
2656 setupJob(job, "temp", ui->minAltitude->value(), ui->minMoon->value(), ra, dec, ui->useArtificialHorizon->isChecked());
2657
2658 QVector<double> times, alts;
2659 QDateTime plotStart = dusk;
2660 plotStart.setTimeZone(tz);
2661
2662
2663 // Start the plot 1 hour before dusk and end it an hour after dawn.
2664 plotStart = plotStart.addSecs(-1 * 3600);
2665 auto t = plotStart;
2666 t.setTimeZone(tz);
2667 auto plotEnd = dawn.addSecs(1 * 3600);
2668 plotEnd.setTimeZone(tz);
2669
2670 while (t.secsTo(plotEnd) > 0)
2671 {
2672 SkyPoint coords = job.getTargetCoords();
2673 double alt = getAltitude(getGeo(), coords, t);
2674 alts.push_back(alt);
2675 double hour = midnight.secsTo(t) / 3600.0;
2676 times.push_back(hour);
2677 t = t.addSecs(60 * 10);
2678 }
2679
2680 altitudeGraph->plot(getGeo(), &ksal, times, alts, false);
2681
2682 for (int i = 0; i < jobStartTimes.size(); ++i)
2683 {
2684 auto startTime = jobStartTimes[i];
2685 auto stopTime = jobEndTimes[i];
2686 if (startTime < plotStart) startTime = plotStart;
2687 if (stopTime > plotEnd) stopTime = plotEnd;
2688
2689 startTime.setTimeZone(tz);
2690 stopTime.setTimeZone(tz);
2691
2692 QVector<double> runTimes, runAlts;
2693 auto t = startTime;
2694 t.setTimeZone(tz);
2695 //t.setTimeZone(jobStartTimes[0].timeZone());
2696
2697 while (t.secsTo(stopTime) > 0)
2698 {
2699 SkyPoint coords = job.getTargetCoords();
2700 double alt = getAltitude(getGeo(), coords, t);
2701 runAlts.push_back(alt);
2702 double hour = midnight.secsTo(t) / 3600.0;
2703 runTimes.push_back(hour);
2704 t = t.addSecs(60 * 10);
2705 }
2706 altitudeGraph->plot(getGeo(), &ksal, runTimes, runAlts, true);
2707 }
2708}
2709
2710void ImagingPlanner::updateCounts()
2711{
2712 const int numDisplayedObjects = m_CatalogSortModel->rowCount();
2713 const int totalCatalogObjects = m_CatalogModel->rowCount();
2714 if (numDisplayedObjects == 1)
2715 ui->tableCount->setText(QString("1/%1 %2").arg(totalCatalogObjects).arg(i18n("object")));
2716 else
2717 ui->tableCount->setText(QString("%1/%2 %3").arg(numDisplayedObjects).arg(totalCatalogObjects).arg(i18n("objects")));
2718}
2719
2720void ImagingPlanner::moveBackOneDay()
2721{
2722 // Try to keep the object.
2723 QString selection = currentObjectName();
2724 ui->DateEdit->setDate(ui->DateEdit->date().addDays(-1));
2725 // Don't need to call recompute(), called by dateChanged callback.
2726 updateDisplays();
2727 updateMoon();
2728 scrollToName(selection);
2729}
2730
2731void ImagingPlanner::moveForwardOneDay()
2732{
2733 QString selection = currentObjectName();
2734 ui->DateEdit->setDate(ui->DateEdit->date().addDays(1));
2735 // Don't need to call recompute(), called by dateChanged callback.
2736 updateDisplays();
2737 updateMoon();
2738 scrollToName(selection);
2739}
2740
2741QString ImagingPlanner::currentObjectName() const
2742{
2743 QString name = ui->CatalogView->selectionModel()->currentIndex().siblingAtColumn(NAME_COLUMN).data(
2744 Qt::DisplayRole).toString();
2745 return name;
2746}
2747
2748CatalogObject *ImagingPlanner::currentCatalogObject()
2749{
2750 QString name = currentObjectName();
2751 return getObject(name);
2752}
2753
2754//FIXME: This will open multiple Detail windows for each object;
2755//Should have one window whose target object changes with selection
2756void ImagingPlanner::objectDetails()
2757{
2758 CatalogObject *current = currentCatalogObject();
2759 if (current == nullptr)
2760 return;
2761 auto ut = KStarsData::Instance()->ut();
2762 ut.setDate(getDate());
2764 new DetailDialog(current, ut, getGeo(), KStars::Instance());
2765 dd->exec();
2766 delete dd;
2767}
2768
2769void ImagingPlanner::centerOnSkymap()
2770{
2771 if (!Options::imagingPlannerCenterOnSkyMap())
2772 return;
2773 reallyCenterOnSkymap();
2774}
2775
2776void ImagingPlanner::reallyCenterOnSkymap()
2777{
2778 CatalogObject *current = currentCatalogObject();
2779 if (current == nullptr)
2780 return;
2781
2782 // These shouldn't happen anymore--seemed to happen when I let in null objects.
2783 if (current->ra().Degrees() == 0 && current->dec().Degrees() == 0)
2784 {
2785 DPRINTF(stderr, "found a 0,0 object\n");
2786 return;
2787 }
2788
2789 // Set up the Alt/Az coordinates that SkyMap needs.
2790 KStarsDateTime time = KStarsData::Instance()->clock()->utc();
2791 dms lst = getGeo()->GSTtoLST(time.gst());
2792 current->EquatorialToHorizontal(&lst, getGeo()->lat());
2793
2794
2795 // Doing this to avoid the pop-up warning that an object is below the ground.
2796 bool keepGround = Options::showGround();
2797 bool keepAnimatedSlew = Options::useAnimatedSlewing();
2798 Options::setShowGround(false);
2799 Options::setUseAnimatedSlewing(false);
2800
2801 SkyMap::Instance()->setClickedObject(current);
2802 SkyMap::Instance()->setClickedPoint(current);
2803 SkyMap::Instance()->slotCenter();
2804
2805 Options::setShowGround(keepGround);
2806 Options::setUseAnimatedSlewing(keepAnimatedSlew);
2807}
2808
2809void ImagingPlanner::setSelection(int flag, bool enabled)
2810{
2811 auto rows = ui->CatalogView->selectionModel()->selectedRows();
2812
2813 // We can't use the selection for processing, because it may change on the fly
2814 // as we modify flags (e.g. if the view is set to showing picked objects only
2815 // and we are disabling the picked flag, as a selected object with a picked flag
2816 // gets de-picked, it will also get deselected.
2817 // So, we store a list of the source model indeces, and operate on the source model.
2818
2819 // Find the source model indeces.
2820 QList<QModelIndex> sourceIndeces;
2821 for (int i = 0; i < rows.size(); ++i)
2822 {
2823 auto proxyIndex = rows[i].siblingAtColumn(FLAGS_COLUMN);
2824 auto sourceIndex = m_CatalogSortModel->mapToSource(proxyIndex);
2825 sourceIndeces.append(sourceIndex);
2826 }
2827
2828 for (int i = 0; i < sourceIndeces.size(); ++i)
2829 {
2830 auto &sourceIndex = sourceIndeces[i];
2831
2832 // Set or clear the flags using the source model.
2833 if (enabled)
2834 setFlag(sourceIndex, flag, m_CatalogModel.data());
2835 else
2836 clearFlag(sourceIndex, flag, m_CatalogModel.data());
2837
2838 QString name = m_CatalogModel->data(sourceIndex.siblingAtColumn(NAME_COLUMN)).toString();
2839 int flags = m_CatalogModel->data(sourceIndex.siblingAtColumn(FLAGS_COLUMN), FLAGS_ROLE).toInt();
2840 QString notes = m_CatalogModel->data(sourceIndex.siblingAtColumn(NOTES_COLUMN), NOTES_ROLE).toString();
2841 saveToDB(name, flags, notes);
2842
2843 if (flag == IMAGED_BIT)
2844 highlightImagedObject(sourceIndex, enabled);
2845 if (flag == PICKED_BIT)
2846 highlightPickedObject(sourceIndex, enabled);
2847 }
2848 updateDisplays();
2849}
2850
2851void ImagingPlanner::highlightImagedObject(const QModelIndex &index, bool imaged)
2852{
2853 // TODO: Ugly, for now. Figure out how to use the color schemes the right way.
2854 QColor m_DefaultCellBackground(36, 35, 35);
2855 QColor m_ImagedObjectBackground(10, 65, 10);
2856 QString themeName = KSTheme::Manager::instance()->currentThemeName().toLatin1().data();
2857 if (themeName == "High Key" || themeName == "Default" || themeName == "White Balance")
2858 {
2859 m_DefaultCellBackground = QColor(240, 240, 240);
2860 m_ImagedObjectBackground = QColor(180, 240, 180);
2861 }
2862 for (int col = 0; col < LAST_COLUMN; ++col)
2863 {
2864 auto colIndex = index.siblingAtColumn(col);
2865 m_CatalogModel->setData(colIndex, imaged ? m_ImagedObjectBackground : m_DefaultCellBackground, Qt::BackgroundRole);
2866 }
2867}
2868
2869void ImagingPlanner::highlightPickedObject(const QModelIndex &index, bool picked)
2870{
2871 for (int col = 0; col < LAST_COLUMN; ++col)
2872 {
2873 auto colIndex = index.siblingAtColumn(col);
2874 auto font = m_CatalogModel->data(colIndex, Qt::FontRole);
2875 auto ff = qvariant_cast<QFont>(font);
2876 ff.setBold(picked);
2877 ff.setItalic(picked);
2878 ff.setUnderline(picked);
2879 font = ff;
2880 m_CatalogModel->setData(colIndex, font, Qt::FontRole);
2881 }
2882}
2883
2884void ImagingPlanner::setSelectionPicked()
2885{
2886 setSelection(PICKED_BIT, true);
2887}
2888
2889void ImagingPlanner::setSelectionNotPicked()
2890{
2891 setSelection(PICKED_BIT, false);
2892}
2893
2894void ImagingPlanner::setSelectionImaged()
2895{
2896 setSelection(IMAGED_BIT, true);
2897}
2898
2899void ImagingPlanner::setSelectionNotImaged()
2900{
2901 setSelection(IMAGED_BIT, false);
2902}
2903
2904void ImagingPlanner::setSelectionIgnored()
2905{
2906 setSelection(IGNORED_BIT, true);
2907}
2908
2909void ImagingPlanner::setSelectionNotIgnored()
2910{
2911 setSelection(IGNORED_BIT, false);
2912}
2913
2914int ImagingPlanner::currentObjectFlags()
2915{
2916 auto index = ui->CatalogView->selectionModel()->currentIndex().siblingAtColumn(FLAGS_COLUMN);
2917 const bool hasFlags = ui->CatalogView->model()->data(index, FLAGS_ROLE).canConvert<int>();
2918 if (!hasFlags)
2919 return 0;
2920 return ui->CatalogView->model()->data(index, FLAGS_ROLE).toInt();
2921}
2922
2923QString ImagingPlanner::currentObjectNotes()
2924{
2925 auto index = ui->CatalogView->selectionModel()->currentIndex().siblingAtColumn(NOTES_COLUMN);
2926 const bool hasNotes = ui->CatalogView->model()->data(index, NOTES_ROLE).canConvert<QString>();
2927 if (!hasNotes)
2928 return QString();
2929 return ui->CatalogView->model()->data(index, NOTES_ROLE).toString();
2930}
2931
2932void ImagingPlanner::setCurrentObjectNotes(const QString &notes)
2933{
2934 auto index = ui->CatalogView->selectionModel()->currentIndex();
2935 if (!index.isValid())
2936 return;
2937 auto sibling = index.siblingAtColumn(NOTES_COLUMN);
2938
2939 auto sourceIndex = m_CatalogSortModel->mapToSource(sibling);
2940 QVariant n(notes);
2941 m_CatalogModel->setData(sourceIndex, n, NOTES_ROLE);
2942}
2943
2944ImagingPlannerPopup::ImagingPlannerPopup() : QMenu(nullptr)
2945{
2946}
2947
2948// The bools are pointers to we can have a 3-valued input parameter.
2949// If the pointer is a nullptr, then we say, for example it is neigher imaged, not not imaged.
2950// That is, really, some of the selection are imaged and some not imaged.
2951// If the pointer does point to a bool, then the value of that bool tells you if all the selection
2952// is (e.g.) imaged, or if all of it is not imaged.
2953void ImagingPlannerPopup::init(ImagingPlanner * planner, const QStringList &names,
2954 const bool * imaged, const bool * picked, const bool * ignored)
2955{
2956 clear();
2957 if (names.size() == 0) return;
2958
2959 QString title;
2960 if (names.size() == 1)
2961 title = names[0];
2962 else if (names.size() <= 3)
2963 {
2964 title = names[0];
2965 for (int i = 1; i < names.size(); i++)
2966 title.append(QString(", %1").arg(names[i]));
2967 }
2968 else
2969 title = i18n("%1, %2 and %3 other objects", names[0], names[1], names.size() - 2);
2970
2972
2973 QString word = names.size() == 1 ? names[0] : i18n("objects");
2974
2975 if (imaged == nullptr)
2976 {
2977 addAction(i18n("Mark %1 as NOT imaged", word), planner, &ImagingPlanner::setSelectionNotImaged);
2978 addAction(i18n("Mark %1 as already imaged", word), planner, &ImagingPlanner::setSelectionImaged);
2979 }
2980 else if (*imaged)
2981 addAction(i18n("Mark %1 as NOT imaged", word), planner, &ImagingPlanner::setSelectionNotImaged);
2982 else
2983 addAction(i18n("Mark %1 as already imaged", word), planner, &ImagingPlanner::setSelectionImaged);
2984
2985 if (picked == nullptr)
2986 {
2987 addAction(i18n("Un-pick %1", word), planner, &ImagingPlanner::setSelectionNotPicked);
2988 addAction(i18n("Pick %1", word), planner, &ImagingPlanner::setSelectionPicked);
2989 }
2990 else if (*picked)
2991 addAction(i18n("Un-pick %1", word), planner, &ImagingPlanner::setSelectionNotPicked);
2992 else
2993 addAction(i18n("Pick %1", word), planner, &ImagingPlanner::setSelectionPicked);
2994
2995
2996 if (ignored == nullptr)
2997 {
2998 addAction(i18n("Stop ignoring %1", word), planner, &ImagingPlanner::setSelectionNotIgnored);
2999 addAction(i18n("Ignore %1", word), planner, &ImagingPlanner::setSelectionIgnored);
3000
3001 }
3002 else if (*ignored)
3003 addAction(i18n("Stop ignoring %1", word), planner, &ImagingPlanner::setSelectionNotIgnored);
3004 else
3005 addAction(i18n("Ignore %1", word), planner, &ImagingPlanner::setSelectionIgnored);
3006
3007 addSeparator();
3008 addAction(i18n("Center %1 on SkyMap", names[0]), planner, &ImagingPlanner::reallyCenterOnSkymap);
3009
3010}
3011
3012ImagingPlannerDBEntry::ImagingPlannerDBEntry(const QString &name, bool picked, bool imaged,
3013 bool ignored, const QString &notes) : m_Name(name), m_Notes(notes)
3014{
3015 setFlags(picked, imaged, ignored);
3016}
3017
3018ImagingPlannerDBEntry::ImagingPlannerDBEntry(const QString &name, int flags, const QString &notes)
3019 : m_Name(name), m_Flags(flags), m_Notes(notes)
3020{
3021}
3022
3023void ImagingPlannerDBEntry::getFlags(bool * picked, bool * imaged, bool * ignored)
3024{
3025 *picked = m_Flags & PickedBit;
3026 *imaged = m_Flags & ImagedBit;
3027 *ignored = m_Flags & IgnoredBit;
3028}
3029
3030
3031void ImagingPlannerDBEntry::setFlags(bool picked, bool imaged, bool ignored)
3032{
3033 m_Flags = 0;
3034 if (picked) m_Flags |= PickedBit;
3035 if (imaged) m_Flags |= ImagedBit;
3036 if (ignored) m_Flags |= IgnoredBit;
3037}
3038
3039void ImagingPlanner::saveToDB(const QString &name, bool picked, bool imaged,
3040 bool ignored, const QString &notes)
3041{
3042 ImagingPlannerDBEntry e(name, 0, notes);
3043 e.setFlags(picked, imaged, ignored);
3044 KStarsData::Instance()->userdb()->AddImagingPlannerEntry(e);
3045}
3046
3047void ImagingPlanner::saveToDB(const QString &name, int flags, const QString &notes)
3048{
3049 ImagingPlannerDBEntry e(name, flags, notes);
3050 KStarsData::Instance()->userdb()->AddImagingPlannerEntry(e);
3051}
3052
3053// KSUserDB::GetAllImagingPlannerEntries(QList<ImagingPlannerDBEntry> *entryList)
3054void ImagingPlanner::loadFromDB()
3055{
3056 // Disconnect the filter from the model, or else we'll re-filter numRows squared times.
3057 // Not as big a deal here because we're not touching all rows, just the rows with flags/notes.
3058 // Also see the reconnect below.
3059 m_CatalogSortModel->setSourceModel(nullptr);
3060
3061 auto tz = QTimeZone(getGeo()->TZ() * 3600);
3062 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
3063 KStarsDateTime ut = getGeo()->LTtoUT(KStarsDateTime(midnight));
3064 KSAlmanac ksal(ut, getGeo());
3065
3067 KStarsData::Instance()->userdb()->GetAllImagingPlannerEntries(&list);
3069 QHash<QString, int> dbNotes;
3070 for (const auto &entry : list)
3071 {
3072 dbData[entry.m_Name] = entry;
3073 }
3074
3075 int rows = m_CatalogModel->rowCount();
3076 for (int i = 0; i < rows; ++i)
3077 {
3078 const QString &name = m_CatalogModel->item(i, NAME_COLUMN)->text();
3079 auto entry = dbData.find(name);
3080 if (entry != dbData.end())
3081 {
3082 QVariant f = entry->m_Flags;
3083 m_CatalogModel->item(i, FLAGS_COLUMN)->setData(f, FLAGS_ROLE);
3084 if (entry->m_Flags & IMAGED_BIT)
3085 highlightImagedObject(m_CatalogModel->index(i, NAME_COLUMN), true);
3086 if (entry->m_Flags & PICKED_BIT)
3087 highlightPickedObject(m_CatalogModel->index(i, NAME_COLUMN), true);
3088 QVariant n = entry->m_Notes;
3089 m_CatalogModel->item(i, NOTES_COLUMN)->setData(n, NOTES_ROLE);
3090 }
3091 }
3092 // See above. Reconnect the filter to the model.
3093 m_CatalogSortModel->setSourceModel(m_CatalogModel.data());
3094}
3095
3096void ImagingPlanner::loadImagedFile()
3097{
3098 focusOnTable();
3099 QString fileName = QFileDialog::getOpenFileName(this,
3100 tr("Open Already-Imaged File"), QDir::homePath(), tr("Any files (*)"));
3101 if (fileName.isEmpty())
3102 return;
3103 QFile inputFile(fileName);
3104 if (inputFile.open(QIODevice::ReadOnly))
3105 {
3106 int numSuccess = 0;
3107 QStringList failedNames;
3108 QTextStream in(&inputFile);
3109 while (!in.atEnd())
3110 {
3111 QString name = in.readLine().trimmed();
3112 if (name.isEmpty() || name.startsWith('#'))
3113 continue;
3114 name = tweakNames(name);
3115 if (getObject(name))
3116 {
3117 numSuccess++;
3118 auto startIndex = m_CatalogModel->index(0, NAME_COLUMN);
3119 QVariant value(name);
3120 auto matches = m_CatalogModel->match(startIndex, Qt::DisplayRole, value, 1, Qt::MatchFixedString);
3121 if (matches.size() > 0)
3122 {
3123 setFlag(matches[0], IMAGED_BIT, m_CatalogModel);
3124 highlightImagedObject(matches[0], true);
3125
3126 // Make sure we save it to the DB.
3127 QString name = m_CatalogModel->data(matches[0].siblingAtColumn(NAME_COLUMN)).toString();
3128 int flags = m_CatalogModel->data(matches[0].siblingAtColumn(FLAGS_COLUMN), FLAGS_ROLE).toInt();
3129 QString notes = m_CatalogModel->data(matches[0].siblingAtColumn(NOTES_COLUMN), NOTES_ROLE).toString();
3130 saveToDB(name, flags, notes);
3131 }
3132 else
3133 {
3134 DPRINTF(stderr, "ooops! internal inconsitency--got an object but match didn't work");
3135 }
3136 }
3137 else
3138 failedNames.append(name);
3139 }
3140 inputFile.close();
3141 if (failedNames.size() == 0)
3142 {
3143 if (numSuccess > 0)
3144 KSNotification::info(i18n("Successfully marked %1 objects as read", numSuccess));
3145 else
3146 KSNotification::sorry(i18n("Empty file"));
3147 }
3148 else
3149 {
3150 int num = std::min((int)failedNames.size(), 10);
3151 QString sample = QString("\"%1\"").arg(failedNames[0]);
3152 for (int i = 1; i < num; ++i)
3153 sample.append(QString(" \"%1\"").arg(failedNames[i]));
3154 if (numSuccess == 0 && failedNames.size() <= 10)
3155 KSNotification::sorry(i18n("Failed marking all of these objects imaged: %1", sample));
3156 else if (numSuccess == 0)
3157 KSNotification::sorry(i18n("Failed marking %1 objects imaged, including: %2", failedNames.size(), sample));
3158 else if (numSuccess > 0 && failedNames.size() <= 10)
3159 KSNotification::sorry(i18n("Succeeded marking %1 objects imaged. Failed with %2: %3",
3160 numSuccess, failedNames.size() == 1 ? "this" : "these", sample));
3161 else
3162 KSNotification::sorry(i18n("Succeeded marking %1 objects imaged. Failed with %2 including these: %3",
3163 numSuccess, failedNames.size(), sample));
3164 }
3165 }
3166 else
3167 {
3168 KSNotification::sorry(i18n("Sorry, couldn't open file: \"%1\"", fileName));
3169 }
3170}
3171
3172void ImagingPlanner::addCatalogImageInfo(const CatalogImageInfo &info)
3173{
3174 m_CatalogImageInfoMap[info.m_Name.toLower()] = info;
3175}
3176
3177bool ImagingPlanner::findCatalogImageInfo(const QString &name, CatalogImageInfo *info)
3178{
3179 auto result = m_CatalogImageInfoMap.find(name.toLower());
3180 if (result == m_CatalogImageInfoMap.end())
3181 return false;
3182 if (result->m_Filename.isEmpty())
3183 return false;
3184 *info = *result;
3185 return true;
3186}
3187
3188void ImagingPlanner::loadCatalogViaMenu()
3189{
3190 QString startDir = Options::imagingPlannerCatalogPath();
3191 if (startDir.isEmpty())
3192 startDir = defaultDirectory();
3193
3194 QString path = QFileDialog::getOpenFileName(this, tr("Open Catalog File"), startDir, tr("Any files (*.csv)"));
3195 if (path.isEmpty())
3196 return;
3197
3198 loadCatalog(path);
3199}
3200
3201void ImagingPlanner::loadCatalog(const QString &path)
3202{
3203#ifdef THREADED_LOAD_CATALOG
3204 // All this below in order to keep the UI active while loading.
3205 m_LoadCatalogs = QtConcurrent::run([this, path]()
3206 {
3207 loadCatalogFromFile(path);
3208 });
3209 m_LoadCatalogsWatcher = new QFutureWatcher<void>(this);
3210 m_LoadCatalogsWatcher->setFuture(m_LoadCatalogs);
3211 connect(m_LoadCatalogsWatcher, &QFutureWatcher<void>::finished,
3212 [this]()
3213 {
3214 catalogLoaded();
3215 disconnect(m_LoadCatalogsWatcher);
3216 });
3217#else
3218 loadCatalogFromFile(path);
3219 catalogLoaded();
3220#endif
3221}
3222
3223CatalogImageInfo::CatalogImageInfo(const QString &csv)
3224{
3225 QString line = csv.trimmed();
3226 if (line.isEmpty() || line.startsWith('#'))
3227 return;
3228 QStringList columns = line.split(",");
3229 if (columns.size() < 1 || columns[0].isEmpty())
3230 return;
3231 int column = 0;
3232 m_Name = columns[column++];
3233 if (columns.size() <= column) return;
3234 m_Filename = columns[column++];
3235 if (columns.size() <= column) return;
3236 m_Author = columns[column++];
3237 if (columns.size() <= column) return;
3238 m_Link = columns[column++];
3239 if (columns.size() <= column) return;
3240 m_License = columns[column++];
3241}
3242
3243// This does the following:
3244// - Clears the internal catalog
3245// - Initializes the m_CatalogImageInfoMap, which goes from name to image.
3246// - Loads in new objects into the internal catalog.
3247//
3248// CSV File Columns:
3249// 1: ID: M 1
3250// 2: Image Filename: M_1.jpg
3251// 3: Author: Hy Murveit
3252// 4: Link: https://www.astrobin.com/x3utgw/F/
3253// 5: License: ACC (possibilities are ACC,ANCCC,ASACC,ANCCC,ANCSACC)
3254// last one is Attribution Non-Commercial hare-Alike Creative Commons
3255// Currently ID is mandatory, if there is an image filename, then Author,Link,and License
3256// are also required, though could be blank.
3257// Comment lines start with #
3258// Can include another catalog with "LoadCatalog FILENAME"
3259void ImagingPlanner::loadCatalogFromFile(QString path, bool reset)
3260{
3261 QFile inputFile(path);
3262 if (reset)
3263 {
3264 m_numWithImage = 0;
3265 m_numMissingImage = 0;
3266 }
3267 int numMissingImage = 0, numWithImage = 0;
3268 if (!inputFile.exists())
3269 {
3270 emit popupSorry(i18n("Sorry, catalog file doesn't exist: \"%1\"", path));
3271 return;
3272 }
3273 QStringList objectNames;
3274 if (inputFile.open(QIODevice::ReadOnly))
3275 {
3276 auto tz = QTimeZone(getGeo()->TZ() * 3600);
3277 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
3278 KStarsDateTime ut = getGeo()->LTtoUT(KStarsDateTime(midnight));
3279 KSAlmanac ksal(ut, getGeo());
3280
3281 if (reset)
3282 {
3283 Options::setImagingPlannerCatalogPath(path);
3284 Options::self()->save();
3285 if (m_CatalogModel->rowCount() > 0)
3286 m_CatalogModel->removeRows(0, m_CatalogModel->rowCount());
3287 clearObjects();
3288 }
3289 QTextStream in(&inputFile);
3290 while (!in.atEnd())
3291 {
3292 CatalogImageInfo info(in.readLine().trimmed());
3293 if (info.m_Name.isEmpty())
3294 continue;
3295 if (info.m_Name.startsWith("LoadCatalog"))
3296 {
3297 // This line isn't a normal entry, but rather points to another catalog.
3298 // Load that catalog and then skip this line.
3300 auto match = re.match(info.m_Name);
3301 if (match.hasMatch())
3302 {
3303 QString catFilename = match.captured(1);
3304 if (catFilename.isEmpty()) continue;
3305 QFileInfo info(catFilename);
3306
3307 QString catFullPath = catFilename;
3308 if (!info.isAbsolute())
3309 {
3310 QString catDir = QFileInfo(path).absolutePath();
3311 catFullPath = QString("%1%2%3").arg(catDir)
3312 .arg(QDir::separator()).arg(match.captured(1));
3313 }
3314 if (catFullPath != path)
3315 loadCatalogFromFile(catFullPath, false);
3316 }
3317 continue;
3318 }
3319 objectNames.append(info.m_Name);
3320 if (!info.m_Filename.isEmpty())
3321 {
3322 numWithImage++;
3323 QFileInfo fInfo(info.m_Filename);
3324 if (fInfo.isRelative())
3325 info.m_Filename = QString("%1%2%3").arg(QFileInfo(path).absolutePath())
3326 .arg(QDir::separator()).arg(info.m_Filename);
3327 addCatalogImageInfo(info);
3328 }
3329 else
3330 {
3331 numMissingImage++;
3332 DPRINTF(stderr, "No catalog image for %s\n", info.m_Name.toLatin1().data());
3333 }
3334#ifndef THREADED_LOAD_CATALOG
3336#endif
3337 }
3338 inputFile.close();
3339
3340 int num = 0, numBad = 0, iteration = 0;
3341 // Move to threaded thing??
3342 for (const auto &name : objectNames)
3343 {
3344 setStatus(i18n("%1/%2: Adding %3", ++iteration, objectNames.size(), name));
3345 if (addCatalogItem(ksal, name, 0)) num++;
3346 else
3347 {
3348 DPRINTF(stderr, "Couldn't add %s\n", name.toLatin1().data());
3349 numBad++;
3350 }
3351 }
3352 m_numWithImage += numWithImage;
3353 m_numMissingImage += numMissingImage;
3354 DPRINTF(stderr, "Catalog %s: %d of %d have catalog images\n",
3355 path.toLatin1().data(), numWithImage, numWithImage + numMissingImage);
3356
3357 // Clear the old maps? Probably earlier in this method:
3358 // E.g. m_CatalogImageInfoMap?? n_CatalogHash???
3359 // When m_CatalogHash is not cleared, then the add fails currently.
3360 }
3361 else
3362 {
3363 emit popupSorry(i18n("Sorry, couldn't open file: \"%1\"", path));
3364 }
3365}
3366
3367void ImagingPlanner::sorry(const QString &message)
3368{
3369 KSNotification::sorry(message);
3370}
3371
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.
std::pair< bool, QString > add_object(const int catalog_id, const SkyObject::TYPE t, const CachingDms &r, const CachingDms &d, const QString &n, const float m=NaN::f, const QString &lname=QString(), const QString &catalog_identifier=QString(), const float a=0.0, const float b=0.0, const double pa=0.0, const float flux=0)
Add a CatalogObject to a table with `catalog_id`.
CatalogObjectList find_objects_by_name(const QString &name, const int limit=-1, const bool exactMatchOnly=false)
Find an objects by name.
std::pair< bool, CatalogObject > get_object(const CatalogObject::oid &oid)
Get an object by `oid`.
DetailDialog is a window showing detailed information for a selected object.
static QString processSearchText(QString searchText)
Do some post processing on the search text to interpret what the user meant This could include replac...
Represents a flag on the sky map.
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
There are several time-dependent values used in position calculations, that are not specific to an ob...
Definition ksnumbers.h:43
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)
Child class of KSPlanetBase; encapsulates information about the Sun.
Definition kssun.h:24
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:217
const KStarsDateTime & ut() const
Definition kstarsdata.h:159
Q_INVOKABLE SimClock * clock()
Definition kstarsdata.h:220
GeoLocation * geo()
Definition kstarsdata.h:232
SkyMapComposite * skyComposite()
Definition kstarsdata.h:168
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:121
KStarsData * data() const
Definition kstars.h:133
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:1008
void setClickedObject(SkyObject *o)
Set the ClickedObject pointer to the argument.
Definition skymap.cpp:366
void setFocusObject(SkyObject *o)
Set the FocusObject pointer to the argument.
Definition skymap.cpp:371
void slotCenter()
Center the display at the point ClickedPoint.
Definition skymap.cpp:380
Provides all necessary information about an object in the sky: its coordinates, name(s),...
Definition skyobject.h:42
virtual QString longname(void) const
Definition skyobject.h:165
int type(void) const
Definition skyobject.h:189
QString typeName() const
TYPE
The type classification of the SkyObject.
Definition skyobject.h:112
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:391
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:899
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.
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 * link(const QList< QUrl > &src, const QUrl &destDir, JobFlags flags=DefaultFlags)
GeoCoordinates geo(const QVariant &location)
QString path(const QString &relativePath)
KIOCORE_EXPORT QString dir(const QString &fileClass)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
QString name(StandardAction id)
const QList< QKeySequence > & end()
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 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 & remove(qsizetype pos, qsizetype len)
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)
void accepted()
virtual void resizeEvent(QResizeEvent *) override
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()
QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, Options options)
QString absolutePath() const const
void clear()
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
QModelIndexList indexes() const const
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
void append(QList< T > &&value)
void clear()
void push_back(parameter_type value)
qsizetype size() const const
iterator end()
iterator find(const Key &key)
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()
QVariant data(int role) const const
bool isValid() const const
QModelIndex siblingAtColumn(int column) const const
int globalX() const const
int globalY() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
void installEventFilter(QObject *filterObj)
QObject * parent() const const
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
T * data() const const
QRegularExpressionMatch match(QStringView subjectView, qsizetype offset, MatchType matchType, MatchOptions matchOptions) const const
bool hasMatch() const const
Qt::MouseButton button() 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
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
QByteArray toLatin1() const const
QString toLower() const const
QString toUpper() const const
QByteArray toUtf8() const const
QString trimmed() const const
AlignHCenter
KeepAspectRatio
CaseInsensitive
StrongFocus
DisplayRole
Key_Enter
typedef MatchFlags
RightButton
DescendingOrder
SkipEmptyParts
SmoothTransformation
QTextStream & dec(QTextStream &stream)
QTextStream & endl(QTextStream &stream)
QTextStream & fixed(QTextStream &stream)
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
QFuture< T > run(Function function,...)
void keyEvent(KeyAction action, QWidget *widget, Qt::Key key, Qt::KeyboardModifiers modifier, int delay)
bool isValid(int h, int m, int s, int ms)
QString toString(QStringView format) const const
bool isEmpty() const const
bool canConvert() const const
double toDouble(bool *ok) const const
int toInt(bool *ok) const const
void adjustSize()
void clearFocus()
virtual bool event(QEvent *event) override
void setFocusPolicy(Qt::FocusPolicy policy)
QPalette::ColorRole foregroundRole() const const
void setMaximumWidth(int maxw)
void move(const QPoint &)
void repaint()
void setFocus()
virtual void showEvent(QShowEvent *event)
void resize(const QSize &)
void setToolTip(const QString &)
void update()
virtual void setVisible(bool visible)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:16:42 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.