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 initialize();
986}
987
988// Sets up the hide/show buttons that minimize/maximize the plot/search/filters/image sections.
989void ImagingPlanner::setupHideButtons(bool(*option)(), void(*setOption)(bool),
990 QPushButton * hideButton, QPushButton * showButton,
991 QFrame * widget, QFrame * hiddenWidget)
992{
993 hiddenWidget->setVisible(option());
994 widget->setVisible(!option());
995
996 connect(hideButton, &QAbstractButton::clicked, this, [this, setOption, hiddenWidget, widget]()
997 {
998 setOption(true);
999 Options::self()->save();
1000 hiddenWidget->setVisible(true);
1001 widget->setVisible(false);
1002 focusOnTable();
1003 adjustWindowSize();
1004 });
1005 connect(showButton, &QAbstractButton::clicked, this, [this, setOption, hiddenWidget, widget]()
1006 {
1007 setOption(false);
1008 Options::self()->save();
1009 hiddenWidget->setVisible(false);
1010 widget->setVisible(true);
1011 focusOnTable();
1012 });
1013}
1014
1015// Gives the keyboard focus to the CatalogView object table.
1016void ImagingPlanner::focusOnTable()
1017{
1018 ui->CatalogView->setFocus();
1019}
1020
1021void ImagingPlanner::adjustWindowSize()
1022{
1023 const int keepWidth = width();
1024 adjustSize();
1025 const int newHeight = height();
1026 resize(keepWidth, newHeight);
1027}
1028
1029// Sets up the galaxy/nebula/... filter buttons.
1030void ImagingPlanner::setupFilterButton(QCheckBox * checkbox, bool(*option)(), void(*setOption)(bool))
1031{
1032 checkbox->setChecked(option());
1033 connect(checkbox, &QCheckBox::toggled, [this, setOption](bool checked)
1034 {
1035 setOption(checked);
1036 Options::self()->save();
1037 m_CatalogSortModel->invalidate();
1038 updateDisplays();
1039 ui->CatalogView->resizeColumnsToContents();
1040 focusOnTable();
1041 });
1042}
1043
1044// Sets up the picked/imaged/ignored/keyword buttons
1045void ImagingPlanner::setupFilter2Buttons(
1046 QCheckBox * yes, QCheckBox * no, QCheckBox * dontCare,
1047 bool(*yesOption)(), bool(*noOption)(), bool(*dontCareOption)(),
1048 void(*setYesOption)(bool), void(*setNoOption)(bool), void(*setDontCareOption)(bool))
1049{
1050
1051 // Use clicked, not toggled to avoid callbacks when the state is changed programatically.
1052 connect(yes, &QCheckBox::clicked, [this, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare](bool checked)
1053 {
1054 setupShowCallback(checked, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare);
1055 updateSortConstraints();
1056 m_CatalogSortModel->invalidate();
1057 ui->CatalogView->resizeColumnsToContents();
1058 updateDisplays();
1059 focusOnTable();
1060 });
1061 connect(no, &QCheckBox::clicked, [this, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare](bool checked)
1062 {
1063 setupShowNotCallback(checked, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare);
1064 updateSortConstraints();
1065 m_CatalogSortModel->invalidate();
1066 ui->CatalogView->resizeColumnsToContents();
1067 updateDisplays();
1068 focusOnTable();
1069 });
1070 connect(dontCare, &QCheckBox::clicked, [this, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare](bool checked)
1071 {
1072 setupDontCareCallback(checked, setYesOption, setNoOption, setDontCareOption, yes, no, dontCare);
1073 updateSortConstraints();
1074 m_CatalogSortModel->invalidate();
1075 ui->CatalogView->resizeColumnsToContents();
1076 updateDisplays();
1077 focusOnTable();
1078 });
1079
1080 yes->setChecked(yesOption());
1081 no->setChecked(noOption());
1082 dontCare->setChecked(dontCareOption());
1083}
1084
1085// Updates the QSortFilterProxyModel with new picked/imaged/ignore settings.
1086void ImagingPlanner::updateSortConstraints()
1087{
1088 m_CatalogSortModel->setPickedConstraints(!ui->dontCarePickedCB->isChecked(),
1089 ui->pickedCB->isChecked());
1090 m_CatalogSortModel->setImagedConstraints(!ui->dontCareImagedCB->isChecked(),
1091 ui->imagedCB->isChecked());
1092 m_CatalogSortModel->setIgnoredConstraints(!ui->dontCareIgnoredCB->isChecked(),
1093 ui->ignoredCB->isChecked());
1094 m_CatalogSortModel->setKeywordConstraints(!ui->dontCareKeywordCB->isChecked(),
1095 ui->keywordCB->isChecked(), ui->keywordEdit->toPlainText().trimmed());
1096}
1097
1098// Called once, at the first viewing of the tool, to initalize all the widgets.
1099void ImagingPlanner::initialize()
1100{
1101 if (KStarsData::Instance() == nullptr)
1102 {
1103 QTimer::singleShot(200, this, &ImagingPlanner::initialize);
1104 return;
1105 }
1106
1107 // Connects the threaded catalog loader to the UI.
1108 connect(this, &ImagingPlanner::popupSorry, this, &ImagingPlanner::sorry);
1109
1110 // Setup the Table Views
1111 m_CatalogModel = new QStandardItemModel(0, LAST_COLUMN);
1112
1113 // Setup the labels and tooltips for the header row of the table.
1114 m_CatalogModel->setHorizontalHeaderLabels(
1115 QStringList() << i18n("Name") << i18n("Hours") << i18n("Type") << i18n("Size") << i18n("Alt") << i18n("Moon") <<
1116 i18n("Const") << i18n("Coord"));
1117 m_CatalogModel->horizontalHeaderItem(NAME_COLUMN)->setToolTip(
1118 i18n("Object Name--click header to sort ascending/descending."));
1119 m_CatalogModel->horizontalHeaderItem(
1120 HOURS_COLUMN)->setToolTip(i18n("Number of hours the object can be imaged--click header to sort ascending/descending."));
1121 m_CatalogModel->horizontalHeaderItem(TYPE_COLUMN)->setToolTip(
1122 i18n("Object Type--click header to sort ascending/descending."));
1123 m_CatalogModel->horizontalHeaderItem(
1124 SIZE_COLUMN)->setToolTip(i18n("Maximum object dimension (arcmin)--click header to sort ascending/descending."));
1125 m_CatalogModel->horizontalHeaderItem(
1126 ALTITUDE_COLUMN)->setToolTip(i18n("Maximum altitude--click header to sort ascending/descending."));
1127 m_CatalogModel->horizontalHeaderItem(
1128 MOON_COLUMN)->setToolTip(i18n("Moon angular separation at midnight--click header to sort ascending/descending."));
1129 m_CatalogModel->horizontalHeaderItem(
1130 CONSTELLATION_COLUMN)->setToolTip(i18n("Constellation--click header to sort ascending/descending."));
1131 m_CatalogModel->horizontalHeaderItem(
1132 COORD_COLUMN)->setToolTip(i18n("RA/DEC coordinates--click header to sort ascending/descending."));
1133
1134 m_CatalogSortModel = new CatalogFilter(this);
1135 m_CatalogSortModel->setSortCaseSensitivity(Qt::CaseInsensitive);
1136 m_CatalogSortModel->setSourceModel(m_CatalogModel.data());
1137 m_CatalogSortModel->setDynamicSortFilter(true);
1138
1139 ui->CatalogView->setModel(m_CatalogSortModel.data());
1140 ui->CatalogView->setSortingEnabled(false); // We explicitly control the clicking on headers.
1141 ui->CatalogView->horizontalHeader()->setStretchLastSection(false);
1142 ui->CatalogView->resizeColumnsToContents();
1143 ui->CatalogView->verticalHeader()->setVisible(false); // Remove the row-number display.
1144 ui->CatalogView->setColumnHidden(FLAGS_COLUMN, true);
1145
1146 connect(ui->CatalogView->selectionModel(), &QItemSelectionModel::selectionChanged,
1147 this, &ImagingPlanner::selectionChanged);
1148
1149 // Initialize the date to KStars' date.
1150 if (getGeo())
1151 {
1152 auto utc = KStarsData::Instance()->clock()->utc();
1153 auto localTime = getGeo()->UTtoLT(utc);
1154 ui->DateEdit->setDate(localTime.date());
1155 updateMoon();
1156 }
1157
1158 setStatus("");
1159
1160 setupHideButtons(&Options::imagingPlannerHideAltitudeGraph, &Options::setImagingPlannerHideAltitudeGraph,
1161 ui->hideAltitudeGraphB, ui->showAltitudeGraphB,
1162 ui->AltitudeGraphFrame, ui->HiddenAltitudeGraphFrame);
1163
1164 // Date buttons
1165 connect(ui->backOneDay, &QPushButton::clicked, this, &ImagingPlanner::moveBackOneDay);
1166 connect(ui->forwardOneDay, &QPushButton::clicked, this, &ImagingPlanner::moveForwardOneDay);
1167 connect(ui->DateEdit, &QDateTimeEdit::dateChanged, this, [this]()
1168 {
1169 QString selection = currentObjectName();
1170 updateMoon();
1171 recompute();
1172 updateDisplays();
1173 scrollToName(selection);
1174 });
1175
1176 // Setup the section with Web search and Astrobin search details.
1177
1178 // Setup Web Search buttons
1179 connect(ui->astrobinButton, &QPushButton::clicked, this, &ImagingPlanner::searchAstrobin);
1180 connect(ui->astrobinButton2, &QPushButton::clicked, this, &ImagingPlanner::searchAstrobin);
1181 connect(ui->searchWikipedia, &QPushButton::clicked, this, &ImagingPlanner::searchWikipedia);
1182 connect(ui->searchWikipedia2, &QPushButton::clicked, this, &ImagingPlanner::searchWikipedia);
1183 connect(ui->searchNGCICImages, &QPushButton::clicked, this, &ImagingPlanner::searchNGCICImages);
1184 connect(ui->searchNGCICImages2, &QPushButton::clicked, this, &ImagingPlanner::searchNGCICImages);
1185 connect(ui->searchSimbad, &QPushButton::clicked, this, &ImagingPlanner::searchSimbad);
1186 connect(ui->searchSimbad2, &QPushButton::clicked, this, &ImagingPlanner::searchSimbad);
1187
1188 // Always start with hiding the details.
1189 Options::setImagingPlannerHideAstrobinDetails(true);
1190 setupHideButtons(&Options::imagingPlannerHideAstrobinDetails, &Options::setImagingPlannerHideAstrobinDetails,
1191 ui->hideAstrobinDetailsButton, ui->showAstrobinDetailsButton,
1192 ui->AstrobinSearchFrame, ui->HiddenAstrobinSearchFrame);
1193 ui->AstrobinAward->setChecked(Options::astrobinAward());
1194 connect(ui->AstrobinAward, &QAbstractButton::clicked, [this](bool checked)
1195 {
1196 Options::setAstrobinAward(checked);
1197 Options::self()->save();
1198 focusOnTable();
1199 });
1200 ui->AstrobinMinRadius->setValue(Options::astrobinMinRadius());
1201 connect(ui->AstrobinMinRadius, &QDoubleSpinBox::editingFinished, [this]()
1202 {
1203 Options::setAstrobinMinRadius(ui->AstrobinMinRadius->value());
1204 Options::self()->save();
1205 focusOnTable();
1206 });
1207 ui->AstrobinMaxRadius->setValue(Options::astrobinMaxRadius());
1208 connect(ui->AstrobinMaxRadius, &QDoubleSpinBox::editingFinished, [this]()
1209 {
1210 Options::setAstrobinMaxRadius(ui->AstrobinMaxRadius->value());
1211 Options::self()->save();
1212 focusOnTable();
1213 });
1214
1215 // Initialize image and catalog section
1216 m_NoImagePixmap =
1217 QPixmap(":/images/noimage.png").scaled(ui->ImagePreview->width(), ui->ImagePreview->height(), Qt::KeepAspectRatio,
1219 setDefaultImage();
1220 connect(ui->LoadCatalogButton, &QPushButton::clicked, this, &ImagingPlanner::loadCatalogViaMenu);
1221 connect(ui->LoadCatalogButton2, &QPushButton::clicked, this, &ImagingPlanner::loadCatalogViaMenu);
1222 setupHideButtons(&Options::imagingPlannerHideImage, &Options::setImagingPlannerHideImage,
1223 ui->hideImageButton, ui->showImageButton,
1224 ui->ImageFrame, ui->HiddenImageFrame);
1225
1226 // Initialize filter section
1227 Options::setImagingPlannerHideFilters(true);
1228 setupHideButtons(&Options::imagingPlannerHideFilters, &Options::setImagingPlannerHideFilters,
1229 ui->hideFilterTypesButton, ui->showFilterTypesButton,
1230 ui->FilterTypesFrame, ui->HiddenFilterTypesFrame);
1231 setupFilterButton(ui->OpenClusterCB, &Options::imagingPlannerAcceptOpenCluster,
1232 &Options::setImagingPlannerAcceptOpenCluster);
1233 setupFilterButton(ui->NebulaCB, &Options::imagingPlannerAcceptNebula, &Options::setImagingPlannerAcceptNebula);
1234 setupFilterButton(ui->GlobularClusterCB, &Options::imagingPlannerAcceptGlobularCluster,
1235 &Options::setImagingPlannerAcceptGlobularCluster);
1236 setupFilterButton(ui->PlanetaryCB, &Options::imagingPlannerAcceptPlanetary, &Options::setImagingPlannerAcceptPlanetary);
1237 setupFilterButton(ui->SupernovaRemnantCB, &Options::imagingPlannerAcceptSupernovaRemnant,
1238 &Options::setImagingPlannerAcceptSupernovaRemnant);
1239 setupFilterButton(ui->GalaxyCB, &Options::imagingPlannerAcceptGalaxy, &Options::setImagingPlannerAcceptGalaxy);
1240 setupFilterButton(ui->GalaxyClusterCB, &Options::imagingPlannerAcceptGalaxyCluster,
1241 &Options::setImagingPlannerAcceptGalaxyCluster);
1242 setupFilterButton(ui->DarkNebulaCB, &Options::imagingPlannerAcceptDarkNebula, &Options::setImagingPlannerAcceptDarkNebula);
1243 setupFilterButton(ui->OtherCB, &Options::imagingPlannerAcceptOther, &Options::setImagingPlannerAcceptOther);
1244
1245 setupFilter2Buttons(ui->pickedCB, ui->notPickedCB, ui->dontCarePickedCB,
1246 &Options::imagingPlannerShowPicked, &Options::imagingPlannerShowNotPicked, &Options::imagingPlannerDontCarePicked,
1247 &Options::setImagingPlannerShowPicked, &Options::setImagingPlannerShowNotPicked, &Options::setImagingPlannerDontCarePicked);
1248
1249 setupFilter2Buttons(ui->imagedCB, ui->notImagedCB, ui->dontCareImagedCB,
1250 &Options::imagingPlannerShowImaged, &Options::imagingPlannerShowNotImaged, &Options::imagingPlannerDontCareImaged,
1251 &Options::setImagingPlannerShowImaged, &Options::setImagingPlannerShowNotImaged, &Options::setImagingPlannerDontCareImaged);
1252
1253 setupFilter2Buttons(ui->ignoredCB, ui->notIgnoredCB, ui->dontCareIgnoredCB,
1254 &Options::imagingPlannerShowIgnored, &Options::imagingPlannerShowNotIgnored, &Options::imagingPlannerDontCareIgnored,
1255 &Options::setImagingPlannerShowIgnored, &Options::setImagingPlannerShowNotIgnored,
1256 &Options::setImagingPlannerDontCareIgnored);
1257
1258 ui->keywordEdit->setText(Options::imagingPlannerKeyword());
1259 ui->keywordEdit->setAcceptRichText(false);
1260 m_Keyword = Options::imagingPlannerKeyword();
1261 setupFilter2Buttons(ui->keywordCB, ui->notKeywordCB, ui->dontCareKeywordCB,
1262 &Options::imagingPlannerShowKeyword, &Options::imagingPlannerShowNotKeyword, &Options::imagingPlannerDontCareKeyword,
1263 &Options::setImagingPlannerShowKeyword, &Options::setImagingPlannerShowNotKeyword,
1264 &Options::setImagingPlannerDontCareKeyword);
1265
1266 ui->keywordEdit->setFocusPolicy(Qt::StrongFocus);
1267
1268 // Initialize the altitude/moon/hours inputs
1269 ui->useArtificialHorizon->setChecked(Options::imagingPlannerUseArtificialHorizon());
1270 m_UseArtificialHorizon = Options::imagingPlannerUseArtificialHorizon();
1271 ui->minMoon->setValue(Options::imagingPlannerMinMoonSeparation());
1272 m_MinMoon = Options::imagingPlannerMinMoonSeparation();
1273 ui->minAltitude->setValue(Options::imagingPlannerMinAltitude());
1274 m_MinAltitude = Options::imagingPlannerMinAltitude();
1275 ui->minHours->setValue(Options::imagingPlannerMinHours());
1276 m_MinHours = Options::imagingPlannerMinHours();
1277 m_CatalogSortModel->setMinHours(Options::imagingPlannerMinHours());
1278 connect(ui->useArtificialHorizon, &QCheckBox::toggled, [this]()
1279 {
1280 if (m_UseArtificialHorizon == ui->useArtificialHorizon->isChecked())
1281 return;
1282 m_UseArtificialHorizon = ui->useArtificialHorizon->isChecked();
1283 Options::setImagingPlannerUseArtificialHorizon(ui->useArtificialHorizon->isChecked());
1284 Options::self()->save();
1285 recompute();
1286 updateDisplays();
1287 });
1288 connect(ui->minMoon, &QDoubleSpinBox::editingFinished, [this]()
1289 {
1290 if (m_MinMoon == ui->minMoon->value())
1291 return;
1292 m_MinMoon = ui->minMoon->value();
1293 Options::setImagingPlannerMinMoonSeparation(ui->minMoon->value());
1294 Options::self()->save();
1295 recompute();
1296 updateDisplays();
1297 });
1298 connect(ui->minAltitude, &QDoubleSpinBox::editingFinished, [this]()
1299 {
1300 if (m_MinAltitude == ui->minAltitude->value())
1301 return;
1302 m_MinAltitude = ui->minAltitude->value();
1303 Options::setImagingPlannerMinAltitude(ui->minAltitude->value());
1304 Options::self()->save();
1305 recompute();
1306 updateDisplays();
1307 });
1308 connect(ui->minHours, &QDoubleSpinBox::editingFinished, [this]()
1309 {
1310 if (m_MinHours == ui->minHours->value())
1311 return;
1312 m_MinHours = ui->minHours->value();
1313 Options::setImagingPlannerMinHours(ui->minHours->value());
1314 Options::self()->save();
1315 m_CatalogSortModel->setMinHours(Options::imagingPlannerMinHours());
1316 m_CatalogSortModel->invalidate();
1317 ui->CatalogView->resizeColumnsToContents();
1318 updateDisplays();
1319 });
1320
1321 updateSortConstraints();
1322
1323 m_CatalogSortModel->setMinHours(ui->minHours->value());
1324
1325 ui->CatalogView->setColumnHidden(NOTES_COLUMN, true);
1326
1327 initUserNotes();
1328
1329 connect(ui->userNotesDoneButton, &QAbstractButton::clicked, this, &ImagingPlanner::userNotesEditFinished);
1330 ui->userNotesEdit->setFocusPolicy(Qt::StrongFocus);
1331
1332 connect(ui->userNotesEditButton, &QAbstractButton::clicked, this, [this]()
1333 {
1334 ui->userNotesLabel->setVisible(true);
1335 ui->userNotesEdit->setText(ui->userNotes->text());
1336 ui->userNotesEdit->setVisible(true);
1337 ui->userNotesEditButton->setVisible(false);
1338 ui->userNotesDoneButton->setVisible(true);
1339 ui->userNotes->setVisible(false);
1340 ui->userNotesLabel->setVisible(true);
1341 ui->userNotesOpenLink->setVisible(false);
1342 ui->userNotesOpenLink2->setVisible(false);
1343 ui->userNotesOpenLink3->setVisible(false);
1344 });
1345
1346 connect(ui->userNotesOpenLink, &QAbstractButton::clicked, this, [this]()
1347 {
1348 focusOnTable();
1349 QString urlString = findUrl(ui->userNotes->text());
1350 if (urlString.isEmpty())
1351 return;
1352 QDesktopServices::openUrl(QUrl(urlString));
1353 });
1354 connect(ui->userNotesOpenLink2, &QAbstractButton::clicked, this, [this]()
1355 {
1356 focusOnTable();
1357 QString urlString = findUrl(ui->userNotes->text(), 2);
1358 if (urlString.isEmpty())
1359 return;
1360 QDesktopServices::openUrl(QUrl(urlString));
1361 });
1362 connect(ui->userNotesOpenLink3, &QAbstractButton::clicked, this, [this]()
1363 {
1364 focusOnTable();
1365 QString urlString = findUrl(ui->userNotes->text(), 3);
1366 if (urlString.isEmpty())
1367 return;
1368 QDesktopServices::openUrl(QUrl(urlString));
1369 });
1370
1371 connect(ui->loadImagedB, &QPushButton::clicked, this, &ImagingPlanner::loadImagedFile);
1372
1373 connect(ui->SearchB, &QPushButton::clicked, this, &ImagingPlanner::searchSlot);
1374
1375 connect(ui->CatalogView->horizontalHeader(), &QHeaderView::sectionPressed, this, [this](int column)
1376 {
1377 m_CatalogSortModel->setSortColumn(column);
1378 m_CatalogSortModel->invalidate();
1379 ui->CatalogView->resizeColumnsToContents();
1380 });
1381
1382 adjustWindowSize();
1383
1384 connect(ui->helpButton, &QPushButton::clicked, this, &ImagingPlanner::getHelp);
1385 connect(ui->optionsButton, &QPushButton::clicked, this, &ImagingPlanner::openOptionsMenu);
1386
1387 // Since we thread the loading of catalogs, need to connect the thread back to UI.
1388 qRegisterMetaType<QList<QStandardItem *>>("QList<QStandardItem *>");
1389 connect(this, &ImagingPlanner::addRow, this, &ImagingPlanner::addRowSlot);
1390
1391 // Needed to fix weird bug on Windows that started with Qt 5.9 that makes the title bar
1392 // not visible and therefore dialog not movable.
1393#ifdef Q_OS_WIN
1394 move(100, 100);
1395#endif
1396
1397 // Install the event filters. Put them at the end of initialize so
1398 // the event filter isn't called until initialize is complete.
1399 installEventFilters();
1400}
1401
1402void ImagingPlanner::installEventFilters()
1403{
1404 // Install the event filters. Put them at the end of initialize so
1405 // the event filter isn't called until initialize is complete.
1406 ui->SearchText->installEventFilter(this);
1407 ui->userNotesEdit->installEventFilter(this);
1408 ui->keywordEdit->installEventFilter(this);
1409 ui->ImagePreviewCreditLink->installEventFilter(this);
1410 ui->ImagePreviewCredit->installEventFilter(this);
1411 ui->ImagePreview->installEventFilter(this);
1412 ui->CatalogView->viewport()->installEventFilter(this);
1413 ui->CatalogView->installEventFilter(this);
1414}
1415
1416void ImagingPlanner::removeEventFilters()
1417{
1418 ui->SearchText->removeEventFilter(this);
1419 ui->userNotesEdit->removeEventFilter(this);
1420 ui->keywordEdit->removeEventFilter(this);
1421 ui->ImagePreviewCreditLink->removeEventFilter(this);
1422 ui->ImagePreviewCredit->removeEventFilter(this);
1423 ui->ImagePreview->removeEventFilter(this);
1424 ui->CatalogView->viewport()->removeEventFilter(this);
1425 ui->CatalogView->removeEventFilter(this);
1426}
1427
1428void ImagingPlanner::openOptionsMenu()
1429{
1430 QSharedPointer<ImagingPlannerOptions> options(new ImagingPlannerOptions(this));
1431 options->exec();
1432 focusOnTable();
1433}
1434
1435// KDE KHelpClient::invokeHelp() doesn't seem to work.
1436void ImagingPlanner::getHelp()
1437{
1438#if 0
1439 // This code can be turned on to check out the targets, but should normally be off.
1440 checkTargets();
1441 return;
1442#endif
1443
1444#if 0
1445 // This code can be turned on to downsame the png images and convert them to jpg
1446 if (downsampleImageFiles("/home/hy/Desktop/SharedFolder/PLANNER_IMAGES/MESSIER", 300))
1447 fprintf(stderr, "downsampling succeeded\n");
1448 else
1449 fprintf(stderr, "downsampling failed\n");
1450
1451 if (downsampleImageFiles("/home/hy/Desktop/SharedFolder/PLANNER_IMAGES/OTHER", 300))
1452 fprintf(stderr, "downsampling succeeded\n");
1453 else
1454 fprintf(stderr, "downsampling failed\n");
1455
1456 if (downsampleImageFiles("/home/hy/Desktop/SharedFolder/PLANNER_IMAGES/CALDWELL", 300))
1457 fprintf(stderr, "downsampling succeeded\n");
1458 else
1459 fprintf(stderr, "downsampling failed\n");
1460
1461 if (downsampleImageFiles("/home/hy/Desktop/SharedFolder/PLANNER_IMAGES/AWARDS", 300))
1462 fprintf(stderr, "downsampling succeeded\n");
1463 else
1464 fprintf(stderr, "downsampling failed\n");
1465
1466 if (downsampleImageFiles("/home/hy/Desktop/SharedFolder/PLANNER_IMAGES/HERSCHEL12", 300))
1467 fprintf(stderr, "downsampling succeeded\n");
1468 else
1469 fprintf(stderr, "downsampling failed\n");
1470#endif
1471 focusOnTable();
1472 const QUrl url("https://docs.kde.org/trunk5/en/kstars/kstars/kstars.pdf#tool-imaging-planner");
1473 if (!url.isEmpty())
1475}
1476
1477KSMoon *ImagingPlanner::getMoon()
1478{
1479 if (KStarsData::Instance() == nullptr)
1480 return nullptr;
1481
1482 KSMoon *moon = dynamic_cast<KSMoon *>(KStarsData::Instance()->skyComposite()->findByName(i18n("Moon")));
1483 if (moon)
1484 {
1485 auto tz = QTimeZone(getGeo()->TZ() * 3600);
1486 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
1487 midnight.setTimeZone(tz);
1488 CachingDms LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(midnight).gst());
1489 KSNumbers numbers(midnight.djd());
1490 moon->updateCoords(&numbers, true, getGeo()->lat(), &LST, true);
1491 }
1492 return moon;
1493}
1494
1495// Setup the moon image.
1496void ImagingPlanner::updateMoon()
1497{
1498 KSMoon *moon = getMoon();
1499 if (!moon)
1500 return;
1501
1502 // You need to know the sun's position in order to get the right phase of the moon.
1503 auto tz = QTimeZone(getGeo()->TZ() * 3600);
1504 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
1505 CachingDms LST = getGeo()->GSTtoLST(getGeo()->LTtoUT(midnight).gst());
1506 KSNumbers numbers(midnight.djd());
1507 KSSun *sun = dynamic_cast<KSSun *>(KStarsData::Instance()->skyComposite()->findByName(i18n("Sun")));
1508 sun->updateCoords(&numbers, true, getGeo()->lat(), &LST, true);
1509 moon->findPhase(sun);
1510
1511 ui->moonImage->setPixmap(QPixmap::fromImage(moon->image().scaled(32, 32, Qt::KeepAspectRatio)));
1512 ui->moonPercentLabel->setText(QString("%1%").arg(moon->illum() * 100.0 + 0.5, 0, 'f', 0));
1513}
1514
1515bool ImagingPlanner::scrollToName(const QString &name)
1516{
1517 if (name.isEmpty())
1518 return false;
1519 QModelIndexList matchList = ui->CatalogView->model()->match(ui->CatalogView->model()->index(0, 0), Qt::EditRole,
1521 if(matchList.count() >= 1)
1522 {
1523 int bestIndex = 0;
1524 for (int i = 0; i < matchList.count(); i++)
1525 {
1526 QString nn = ui->CatalogView->model()->data(matchList[i], Qt::DisplayRole).toString();
1527 if (nn.compare(name, Qt::CaseInsensitive) == 0)
1528 {
1529 bestIndex = i;
1530 break;
1531 }
1532 }
1533 ui->CatalogView->scrollTo(matchList[bestIndex]);
1534 ui->CatalogView->setCurrentIndex(matchList[bestIndex]);
1535 return true;
1536 }
1537 return false;
1538}
1539
1540void ImagingPlanner::searchSlot()
1541{
1542 if (m_loadingCatalog)
1543 return;
1544 QString origName = ui->SearchText->toPlainText().trimmed();
1545 QString name = tweakNames(origName);
1546 ui->SearchText->setPlainText(name);
1547 if (name.isEmpty())
1548 return;
1549
1550 if (!scrollToName(name))
1551 KSNotification::sorry(i18n("No match for \"%1\"", origName));
1552
1553 // Still leaves around some </p> in the html unfortunaltely. Don't know how to remove that.
1554 ui->SearchText->clear();
1555 ui->SearchText->setPlainText("");
1556}
1557
1558void ImagingPlanner::initUserNotes()
1559{
1560 ui->userNotesLabel->setVisible(true);
1561 ui->userNotesEdit->setVisible(false);
1562 ui->userNotesEditButton->setVisible(true);
1563 ui->userNotesDoneButton->setVisible(false);
1564 ui->userNotes->setVisible(true);
1565 ui->userNotesLabel->setVisible(true);
1566 ui->userNotesOpenLink->setVisible(false);
1567 ui->userNotesOpenLink2->setVisible(false);
1568 ui->userNotesOpenLink3->setVisible(false);
1569}
1570
1571void ImagingPlanner::disableUserNotes()
1572{
1573 ui->userNotesEdit->setVisible(false);
1574 ui->userNotesEditButton->setVisible(false);
1575 ui->userNotesDoneButton->setVisible(false);
1576 ui->userNotes->setVisible(false);
1577 ui->userNotesLabel->setVisible(false);
1578 ui->userNotesOpenLink->setVisible(false);
1579 ui->userNotesOpenLink2->setVisible(false);
1580 ui->userNotesOpenLink3->setVisible(false);
1581}
1582
1583void ImagingPlanner::userNotesEditFinished()
1584{
1585 const QString &notes = ui->userNotesEdit->toPlainText().trimmed();
1586 ui->userNotes->setText(notes);
1587 ui->userNotesLabel->setVisible(notes.isEmpty());
1588 ui->userNotesEdit->setVisible(false);
1589 ui->userNotesEditButton->setVisible(true);
1590 ui->userNotesDoneButton->setVisible(false);
1591 ui->userNotes->setVisible(true);
1592 ui->userNotesLabel->setVisible(true);
1593 setCurrentObjectNotes(notes);
1594 setupNotesLinks(notes);
1595 focusOnTable();
1596 auto o = currentCatalogObject();
1597 if (!o) return;
1598 saveToDB(currentObjectName(), currentObjectFlags(), notes);
1599}
1600
1601void ImagingPlanner::updateNotes(const QString &notes)
1602{
1603 ui->userNotes->setMaximumWidth(ui->RightPanel->width() - 125);
1604 initUserNotes();
1605 ui->userNotes->setText(notes);
1606 ui->userNotesLabel->setVisible(notes.isEmpty());
1607 setupNotesLinks(notes);
1608}
1609
1610void ImagingPlanner::setupNotesLinks(const QString &notes)
1611{
1612 QString link = findUrl(notes);
1613 ui->userNotesOpenLink->setVisible(!link.isEmpty());
1614 if (!link.isEmpty())
1615 ui->userNotesOpenLink->setToolTip(i18n("Open a browser with the 1st link in this note: %1", link));
1616
1617 link = findUrl(notes, 2);
1618 ui->userNotesOpenLink2->setVisible(!link.isEmpty());
1619 if (!link.isEmpty())
1620 ui->userNotesOpenLink2->setToolTip(i18n("Open a browser with the 2nd link in this note: %1", link));
1621
1622 link = findUrl(notes, 3);
1623 ui->userNotesOpenLink3->setVisible(!link.isEmpty());
1624 if (!link.isEmpty())
1625 ui->userNotesOpenLink3->setToolTip(i18n("Open a browser with the 3rd link in this note: %1", link));
1626}
1627
1628// Given an object name, return the KStars catalog object.
1629bool ImagingPlanner::getKStarsCatalogObject(const QString &name, CatalogObject * catObject)
1630{
1631 // find_objects_by_name is much faster with exactMatchOnly=true.
1632 // Therefore, since most will match exactly given the string pre-processing,
1633 // first try exact=true, and if that fails, follow up with exact=false.
1634 QString filteredName = FindDialog::processSearchText(name).toUpper();
1635 std::list<CatalogObject> objs =
1636 m_manager.find_objects_by_name(filteredName, 1, true);
1637
1638 // If we didn't find it and it's Sharpless, try sh2 with a space instead of a dash
1639 // and vica versa
1640 if (objs.size() == 0 && filteredName.startsWith("sh2-", Qt::CaseInsensitive))
1641 {
1642 QString name2 = filteredName;
1644 objs = m_manager.find_objects_by_name(name2, 1, true);
1645 }
1646 if (objs.size() == 0 && filteredName.startsWith("sh2 ", Qt::CaseInsensitive))
1647 {
1648 QString name2 = filteredName;
1650 objs = m_manager.find_objects_by_name(name2, 1, true);
1651 }
1652
1653 if (objs.size() == 0)
1654 objs = m_manager.find_objects_by_name(filteredName.toLower(), 20, false);
1655 if (objs.size() == 0)
1656 {
1657 QElapsedTimer timer;
1658 timer.start();
1659 // The resolveName search is touchy about the dash.
1660 if (filteredName.startsWith("sh2", Qt::CaseInsensitive))
1661 filteredName.replace(QRegularExpression("sh2\\s*-?", QRegularExpression::CaseInsensitiveOption), "sh2-");
1662 const auto &cedata = NameResolver::resolveName(filteredName);
1663 if (!cedata.first)
1664 {
1665 return false;
1666 }
1667 else
1668 {
1669 m_manager.add_object(CatalogsDB::user_catalog_id, cedata.second);
1670 const auto &added_object =
1671 m_manager.get_object(cedata.second.getId(), CatalogsDB::user_catalog_id);
1672
1673
1674 if (added_object.first)
1675 {
1676 *catObject = KStarsData::Instance()
1677 ->skyComposite()
1678 ->catalogsComponent()
1679 ->insertStaticObject(added_object.second);
1680 }
1681 DPRINTF(stderr, "***** Found %s using name resolver (%.1fs)\n", name.toLatin1().data(),
1682 timer.elapsed() / 1000.0);
1683 return true;
1684 }
1685 }
1686
1687 if (objs.size() == 0)
1688 return false;
1689
1690 // If there is more than one match, see if there's an exact match in name, name2, or longname.
1691 *catObject = objs.front();
1692 if (objs.size() > 1)
1693 {
1694 QString addSpace = filteredName;
1695 addSpace.append(" ");
1696 for (const auto &obj : objs)
1697 {
1698 if ((filteredName.compare(obj.name(), Qt::CaseInsensitive) == 0) ||
1699 (filteredName.compare(obj.name2(), Qt::CaseInsensitive) == 0) ||
1700 obj.longname().contains(addSpace, Qt::CaseInsensitive) ||
1701 obj.longname().endsWith(filteredName, Qt::CaseInsensitive))
1702 {
1703 *catObject = obj;
1704 break;
1705 }
1706 }
1707 }
1708 return true;
1709}
1710
1711CatalogObject *ImagingPlanner::getObject(const QString &name)
1712{
1713 if (name.isEmpty())
1714 return nullptr;
1715 QString lName = name.toLower();
1716 auto o = m_CatalogHash.find(lName);
1717 if (o == m_CatalogHash.end())
1718 return nullptr;
1719 return &(*o);
1720}
1721
1722void ImagingPlanner::clearObjects()
1723{
1724 // Important to tell SkyMap that our objects are gone.
1725 // We give SkyMap points to these objects in ImagingPlanner::centerOnSkymap()
1726 SkyMap::Instance()->setClickedObject(nullptr);
1727 SkyMap::Instance()->setFocusObject(nullptr);
1728 m_CatalogHash.clear();
1729}
1730
1731CatalogObject *ImagingPlanner::addObject(const QString &name)
1732{
1733 if (name.isEmpty())
1734 return nullptr;
1735 QString lName = name.toLower();
1736 if (getObject(lName) != nullptr)
1737 {
1738 DPRINTF(stderr, "Didn't add \"%s\" because it's already there\n", name.toLatin1().data());
1739 return nullptr;
1740 }
1741
1742 CatalogObject o;
1743 if (!getKStarsCatalogObject(lName, &o))
1744 {
1745 DPRINTF(stderr, "************* Couldn't find \"%s\"\n", lName.toLatin1().data());
1746 return nullptr;
1747 }
1748 m_CatalogHash[lName] = o;
1749 return &(m_CatalogHash[lName]);
1750}
1751
1752// Adds the object to the catalog model, assuming a KStars catalog object can be found
1753// for that name.
1754bool ImagingPlanner::addCatalogItem(const KSAlmanac &ksal, const QString &name, int flags)
1755{
1756 CatalogObject *object = addObject(name);
1757 if (object == nullptr)
1758 return false;
1759
1760 auto getItemWithUserRole = [](const QString & itemText) -> QStandardItem *
1761 {
1762 QStandardItem *ret = new QStandardItem(itemText);
1763 ret->setData(itemText, Qt::UserRole);
1765 return ret;
1766 };
1767
1768 // Build the data. The columns must be the same as the #define columns at the top of this file.
1769 QList<QStandardItem *> itemList;
1770 for (int i = 0; i < LAST_COLUMN; ++i)
1771 {
1772 if (i == NAME_COLUMN)
1773 {
1774 itemList.append(getItemWithUserRole(name));
1775 }
1776 else if (i == HOURS_COLUMN)
1777 {
1778 double runHours = getRunHours(*object, getDate(), *getGeo(), ui->minAltitude->value(), ui->minMoon->value(),
1779 ui->useArtificialHorizon->isChecked());
1780 auto hoursItem = getItemWithUserRole(QString("%1").arg(runHours, 0, 'f', 1));
1781 hoursItem->setData(runHours, HOURS_ROLE);
1782 itemList.append(hoursItem);
1783 }
1784 else if (i == TYPE_COLUMN)
1785 {
1786 auto typeItem = getItemWithUserRole(QString("%1").arg(SkyObject::typeShortName(object->type())));
1787 typeItem->setData(object->type(), TYPE_ROLE);
1788 itemList.append(typeItem);
1789 }
1790 else if (i == SIZE_COLUMN)
1791 {
1792 double size = std::max(object->a(), object->b());
1793 auto sizeItem = getItemWithUserRole(QString("%1'").arg(size, 0, 'f', 1));
1794 sizeItem->setData(size, SIZE_ROLE);
1795 itemList.append(sizeItem);
1796 }
1797 else if (i == ALTITUDE_COLUMN)
1798 {
1799 const auto time = KStarsDateTime(QDateTime(getDate(), QTime(12, 0)));
1800 const double altitude = getMaxAltitude(ksal, getDate(), getGeo(), *object, 0, 0);
1801 auto altItem = getItemWithUserRole(QString("%1º").arg(altitude, 0, 'f', 0));
1802 altItem->setData(altitude, ALTITUDE_ROLE);
1803 itemList.append(altItem);
1804 }
1805 else if (i == MOON_COLUMN)
1806 {
1807 KSMoon *moon = getMoon();
1808 if (moon)
1809 {
1810 SkyPoint o;
1811 o.setRA0(object->ra0());
1812 o.setDec0(object->dec0());
1813 auto tz = QTimeZone(getGeo()->TZ() * 3600);
1814 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
1815 midnight.setTimeZone(tz);
1816 KSNumbers numbers(midnight.djd());
1817 o.updateCoordsNow(&numbers);
1818
1819 double const separation = moon->angularDistanceTo(&o).Degrees();
1820 auto moonItem = getItemWithUserRole(QString("%1º").arg(separation, 0, 'f', 0));
1821 moonItem->setData(separation, MOON_ROLE);
1822 itemList.append(moonItem);
1823 }
1824 else
1825 {
1826 auto moonItem = getItemWithUserRole(QString(""));
1827 moonItem->setData(-1, MOON_ROLE);
1828 }
1829 }
1830 else if (i == CONSTELLATION_COLUMN)
1831 {
1832 QString cname = KStarsData::Instance()
1833 ->skyComposite()
1834 ->constellationBoundary()
1835 ->constellationName(object);
1836 cname = cname.toLower().replace(0, 1, cname[0].toUpper());
1837 auto constellationItem = getItemWithUserRole(cname);
1838 itemList.append(constellationItem);
1839 }
1840 else if (i == COORD_COLUMN)
1841 {
1842 itemList.append(getItemWithUserRole(shortCoordString(object->ra0(), object->dec0())));
1843 }
1844 else if (i == FLAGS_COLUMN)
1845 {
1846 QStandardItem *flag = getItemWithUserRole("flag");
1847 flag->setData(flags, FLAGS_ROLE);
1848 itemList.append(flag);
1849 }
1850 else if (i == NOTES_COLUMN)
1851 {
1852 QStandardItem *notes = getItemWithUserRole("notes");
1853 notes->setData(QString(), NOTES_ROLE);
1854 itemList.append(notes);
1855 }
1856 else
1857 {
1858 DPRINTF(stderr, "Bug in addCatalogItem() !\n");
1859 }
1860 }
1861
1862 // Can't do UI in this thread, must move back to the UI thread.
1863 emit addRow(itemList);
1864 return true;
1865}
1866
1867void ImagingPlanner::addRowSlot(QList<QStandardItem *> itemList)
1868{
1869 m_CatalogModel->appendRow(itemList);
1870 updateCounts();
1871}
1872
1873void ImagingPlanner::recompute()
1874{
1875 setStatus(i18n("Updating tables..."));
1876
1877 // Disconnect the filter from the model, or else we'll re-filter numRows squared times.
1878 m_CatalogSortModel->setSourceModel(nullptr);
1879
1880 QElapsedTimer timer;
1881 timer.start();
1882
1883 auto tz = QTimeZone(getGeo()->TZ() * 3600);
1884 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
1885 KStarsDateTime ut = getGeo()->LTtoUT(KStarsDateTime(midnight));
1886 KSAlmanac ksal(ut, getGeo());
1887
1888 for (int i = 0; i < m_CatalogModel->rowCount(); ++i)
1889 {
1890 const QString &name = m_CatalogModel->item(i, 0)->text();
1891 const CatalogObject *catalogEntry = getObject(name);
1892 if (catalogEntry == nullptr)
1893 {
1894 DPRINTF(stderr, "************* Couldn't find \"%s\"\n", name.toLatin1().data());
1895 return;
1896 }
1897 double runHours = getRunHours(*catalogEntry, getDate(), *getGeo(), ui->minAltitude->value(),
1898 ui->minMoon->value(), ui->useArtificialHorizon->isChecked());
1899 QString hoursText = QString("%1").arg(runHours, 0, 'f', 1);
1900 QStandardItem *hItem = new QStandardItem(hoursText);
1901 hItem->setData(hoursText, Qt::UserRole);
1903 hItem->setData(runHours, HOURS_ROLE);
1904 m_CatalogModel->setItem(i, HOURS_COLUMN, hItem);
1905
1906
1907 const auto time = KStarsDateTime(QDateTime(getDate(), QTime(12, 0)));
1908 const double altitude = getMaxAltitude(ksal, getDate(), getGeo(), *catalogEntry, 0, 0);
1909 QString altText = QString("%1º").arg(altitude, 0, 'f', 0);
1910 auto altItem = new QStandardItem(altText);
1911 altItem->setData(altText, Qt::UserRole);
1912 altItem->setData(altitude, ALTITUDE_ROLE);
1913 m_CatalogModel->setItem(i, ALTITUDE_COLUMN, altItem);
1914
1915 KSMoon *moon = getMoon();
1916 if (moon)
1917 {
1918 SkyPoint o;
1919 o.setRA0(catalogEntry->ra0());
1920 o.setDec0(catalogEntry->dec0());
1921 auto tz = QTimeZone(getGeo()->TZ() * 3600);
1922 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
1923 midnight.setTimeZone(tz);
1924 KSNumbers numbers(midnight.djd());
1925 o.updateCoordsNow(&numbers);
1926
1927 double const separation = moon->angularDistanceTo(&o).Degrees();
1928 QString moonText = QString("%1º").arg(separation, 0, 'f', 0);
1929 auto moonItem = new QStandardItem(moonText);
1930 moonItem->setData(moonText, Qt::UserRole);
1931 moonItem->setData(separation, MOON_ROLE);
1932 m_CatalogModel->setItem(i, MOON_COLUMN, moonItem);
1933 }
1934 else
1935 {
1936 auto moonItem = new QStandardItem("");
1937 moonItem->setData("", Qt::UserRole);
1938 moonItem->setData(-1, MOON_ROLE);
1939 m_CatalogModel->setItem(i, MOON_COLUMN, moonItem);
1940 }
1941
1942 // Don't lose the imaged background highlighting.
1943 const bool imaged = m_CatalogModel->item(i, FLAGS_COLUMN)->data(FLAGS_ROLE).toInt() & IMAGED_BIT;
1944 if (imaged)
1945 highlightImagedObject(m_CatalogModel->index(i, NAME_COLUMN), true);
1946 const bool picked = m_CatalogModel->item(i, FLAGS_COLUMN)->data(FLAGS_ROLE).toInt() & PICKED_BIT;
1947 if (picked)
1948 highlightPickedObject(m_CatalogModel->index(i, NAME_COLUMN), true);
1949 }
1950 // Reconnect the filter to the model.
1951 m_CatalogSortModel->setSourceModel(m_CatalogModel.data());
1952
1953 DPRINTF(stderr, "Recompute took %.1fs\n", timer.elapsed() / 1000.0);
1954 updateStatus();
1955}
1956
1957// Debugging/development method.
1958// Use this to sanitize the list of catalog objects.
1959// enable in header also
1960void ImagingPlanner::checkTargets()
1961{
1962 FlagComponent *flags = KStarsData::Instance()->skyComposite()->flags();
1963
1964 fprintf(stderr, "****************** check objects (%d)***************\n", flags->size());
1965 for (int i = flags->size() - 1; i >= 0; --i) flags->remove(i);
1966 fprintf(stderr, "Removed, now %d\n", flags->size());
1967 QList<QString> targets;
1968 int rows = m_CatalogModel->rowCount();
1969 QVector<bool> accepted(rows);
1970
1971
1972 for (int i = 0; i < rows; ++i)
1973 {
1974 const QString &name = m_CatalogModel->item(i, NAME_COLUMN)->text();
1975 targets.push_back(name);
1976 accepted[i] = getObject(name) != nullptr;
1977
1978 auto object = getObject(name);
1979 if (object)
1980 {
1981 flags->add(SkyPoint(object->ra(), object->dec()), "J2000.0", "", name, Qt::red);
1982 fprintf(stderr, "%d ", i);
1983 }
1984
1985 }
1986 for (int i = 0; i < targets.size(); ++i)
1987 {
1988 if (accepted[i])
1989 {
1990 auto objectName = targets[i];
1991 auto object = getObject(objectName);
1992 object->setRA(object->ra0());
1993 object->setDec(object->dec0());
1994 for (int j = 0; j < targets.size(); ++j)
1995 {
1996 if (i == j) continue;
1997 if (!accepted[j]) continue;
1998 auto name2 = targets[j];
1999 auto object2 = getObject(name2);
2000 object2->setRA(object2->ra0());
2001 object2->setDec(object2->dec0());
2002 const dms dist = object->angularDistanceTo(object2);
2003 const double arcsecDist = dist.Degrees() * 3600.0;
2004 if (arcsecDist < 120)
2005 {
2006 fprintf(stderr, "dist %10s (%s %s) to %10s (%s %s) = %.0f\" %s\n",
2007 objectName.toLatin1().data(),
2008 object->ra().toHMSString().toLatin1().data(),
2009 object->dec().toDMSString().toLatin1().data(),
2010 name2.toLatin1().data(),
2011 object2->ra().toHMSString().toLatin1().data(),
2012 object2->dec().toDMSString().toLatin1().data(),
2013 arcsecDist, object->longname().toLatin1().data());
2014 }
2015 }
2016 }
2017 }
2018
2019 fprintf(stderr, "Done\n");
2020
2021 // Clean up.
2022 ///clearObjects();
2023}
2024
2025// This is the top-level ImagingPlanning catalog directory.
2026QString ImagingPlanner::defaultDirectory() const
2027{
2028 return KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)
2029 + QDir::separator() + "ImagingPlanner";
2030}
2031
2032// The default catalog is one loaded by the "Data -> Download New Data..." menu.
2033// Search the default directory for a ImagingPlanner subdirectory
2034QString ImagingPlanner::findDefaultCatalog() const
2035{
2036 const QFileInfoList subDirs = QDir(defaultDirectory()).entryInfoList(
2038 for (int i = 0; i < subDirs.size(); i++)
2039 {
2040 // Found a possible catalog directory. Will pick the first one we find with .csv files.
2041 const QDir subDir(subDirs[i].absoluteFilePath());
2042 const QStringList csvFilter({"*.csv"});
2043 const QFileInfoList files = subDir.entryInfoList(csvFilter, QDir::NoDotAndDotDot | QDir::Files);
2044 if (files.size() > 0)
2045 {
2046 QString firstFile;
2047 // Look through all the .csv files. Pick all.csv if it exists,
2048 // otherwise one of the other .csv files.
2049 for (const auto &file : files)
2050 {
2051 if (firstFile.isEmpty())
2052 firstFile = file.absoluteFilePath();
2053 if (!file.baseName().compare("all", Qt::CaseInsensitive))
2054 return file.absoluteFilePath();
2055 }
2056 if (!firstFile.isEmpty())
2057 return firstFile;
2058 }
2059 }
2060 return QString();
2061}
2062
2063void ImagingPlanner::loadInitialCatalog()
2064{
2065 QString catalog = Options::imagingPlannerCatalogPath();
2066 if (catalog.isEmpty())
2067 catalog = findDefaultCatalog();
2068 if (catalog.isEmpty())
2069 {
2070 KSNotification::sorry(i18n("You need to load a catalog to start using this tool.\nSee Data -> Download New Data..."));
2071 setStatus(i18n("No Catalog!"));
2072 }
2073 else
2074 loadCatalog(catalog);
2075}
2076
2077void ImagingPlanner::setStatus(const QString &message)
2078{
2079 ui->statusLabel->setText(message);
2080}
2081
2082void ImagingPlanner::catalogLoaded()
2083{
2084 DPRINTF(stderr, "All catalogs loaded: %d of %d have catalog images\n", m_numWithImage, m_numWithImage + m_numMissingImage);
2085 // This cannot go in the threaded loadInitialCatalog()!
2086 loadFromDB();
2087
2088 // TODO: At this point we'd read in various files (picked/imaged/deleted targets ...)
2089 // Can't do this in initialize() as we don't have columns yet.
2090 ui->CatalogView->setColumnHidden(FLAGS_COLUMN, true);
2091 ui->CatalogView->setColumnHidden(NOTES_COLUMN, true);
2092
2093 m_CatalogSortModel->invalidate();
2094 ui->CatalogView->sortByColumn(HOURS_COLUMN, Qt::DescendingOrder);
2095 ui->CatalogView->resizeColumnsToContents();
2096
2097 // Select the first row and give it the keyboard focus (so up/down keyboard keys work).
2098 auto index = ui->CatalogView->model()->index(0, 0);
2099 //ui->CatalogView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select |QItemSelectionModel::Current| QItemSelectionModel::Rows);
2100 ui->CatalogView->selectionModel()->select(index,
2102 ui->CatalogView->setFocus();
2103 updateDisplays();
2104
2105 updateStatus();
2106 adjustWindowSize();
2107}
2108
2109void ImagingPlanner::updateStatus()
2110{
2111 if (currentObjectName().isEmpty())
2112 {
2113 const int numDisplayedObjects = m_CatalogSortModel->rowCount();
2114 const int totalCatalogObjects = m_CatalogModel->rowCount();
2115
2116 if (numDisplayedObjects > 0)
2117 setStatus(i18n("Select an object."));
2118 else if (totalCatalogObjects > 0)
2119 setStatus(i18n("Check Filters to unhide objects."));
2120 else
2121 setStatus(i18n("Load a Catalog."));
2122 }
2123 else
2124 setStatus("");
2125}
2126
2127// This runs when the window gets a show event.
2128void ImagingPlanner::showEvent(QShowEvent *e)
2129{
2130 // ONLY run for first ever show
2131 if (m_initialShow == false)
2132 {
2133 m_initialShow = true;
2134 const int ht = height();
2135 resize(1000, ht);
2137 }
2138}
2139
2140//FIXME: On close, we will need to close any open Details/AVT windows
2141void ImagingPlanner::slotClose()
2142{
2143}
2144
2145// Reverse engineering of the Astrobin search URL (with permission from Salvatore).
2146// See https://github.com/astrobin/astrobin/blob/master/common/encoded_search_viewset.py#L15
2147QUrl ImagingPlanner::getAstrobinUrl(const QString &target, bool requireAwards, bool requireSomeFilters, double minRadius,
2148 double maxRadius)
2149{
2150 QString myQuery = QString("text={\"value\":\"%1\",\"matchType\":\"ALL\"}").arg(target);
2151
2152 // This is a place where the actual date, not the date in the widget, is the right one to find.
2153 auto localTime = getGeo()->UTtoLT(KStarsData::Instance()->clock()->utc());
2154 QDate today = localTime.date();
2155 myQuery.append(QString("&date_acquired={\"min\":\"2018-01-01\",\"max\":\"%1\"}").arg(today.toString("yyyy-MM-dd")));
2156
2157 if (requireAwards)
2158 myQuery.append(QString("&award=[\"iotd\",\"top-pick\",\"top-pick-nomination\"]"));
2159
2160 if (requireSomeFilters)
2161 myQuery.append(QString("&filter_types={\"value\":[\"H_ALPHA\",\"SII\",\"OIII\",\"R\",\"G\",\"B\"],\"matchType\":\"ANY\"}"));
2162
2163 if ((minRadius > 0 || maxRadius > 0) && (maxRadius > minRadius))
2164 myQuery.append(QString("&field_radius={\"min\":%1,\"max\":%2}").arg(minRadius).arg(maxRadius));
2165
2166 QByteArray b(myQuery.toLatin1().data());
2167
2168 // See quick pack implmentation in anonymous namespace above.
2169 QByteArray packed = pack(b);
2170
2171 QByteArray compressed = qCompress(packed).remove(0, 4);
2172
2173 QByteArray b64 = compressed.toBase64();
2174
2175 replaceByteArrayChars(b64, '+', QByteArray("%2B"));
2176 replaceByteArrayChars(b64, '=', QByteArray("%3D"));
2177 replaceByteArrayChars(b, '"', QByteArray("%22"));
2178 replaceByteArrayChars(b, ':', QByteArray("%3A"));
2179 replaceByteArrayChars(b, '[', QByteArray("%5B"));
2180 replaceByteArrayChars(b, ']', QByteArray("%5D"));
2181 replaceByteArrayChars(b, ',', QByteArray("%2C"));
2182 replaceByteArrayChars(b, '\'', QByteArray("%27"));
2183 replaceByteArrayChars(b, '{', QByteArray("%7B"));
2184 replaceByteArrayChars(b, '}', QByteArray("%7D"));
2185
2186 QString url = QString("https://app.astrobin.com/search?p=%1").arg(b64.toStdString().c_str());
2187 return QUrl(url);
2188}
2189
2190void ImagingPlanner::popupAstrobin(const QString &target)
2191{
2192 QString newStr = massageObjectName(target);
2193 if (newStr.isEmpty()) return;
2194
2195 const QUrl url = getAstrobinUrl(newStr, Options::astrobinAward(), false, Options::astrobinMinRadius(),
2196 Options::astrobinMaxRadius());
2197 if (!url.isEmpty())
2199}
2200
2201// Popup a browser on the Professor Segilman website https://cseligman.com
2202void ImagingPlanner::searchNGCICImages()
2203{
2204 focusOnTable();
2205 auto o = currentCatalogObject();
2206 if (!o)
2207 {
2208 fprintf(stderr, "NULL object sent to searchNGCICImages.\n");
2209 return;
2210 }
2211 int num = -1;
2212 if (o->name().startsWith("ngc", Qt::CaseInsensitive))
2213 {
2214 num = o->name().mid(3).toInt();
2215 QString urlString = QString("https://cseligman.com/text/atlas/ngc%1%2.htm#%3").arg(num / 100).arg(
2216 num % 100 < 50 ? "" : "a").arg(num);
2217 QDesktopServices::openUrl(QUrl(urlString));
2218 return;
2219 }
2220 else if (o->name().startsWith("ic", Qt::CaseInsensitive))
2221 {
2222 num = o->name().mid(2).toInt();
2223 QString urlString = QString("https://cseligman.com/text/atlas/ic%1%2.htm#ic%3").arg(num / 100).arg(
2224 num % 100 < 50 ? "" : "a").arg(num);
2225 QDesktopServices::openUrl(QUrl(urlString));
2226 return;
2227 }
2228}
2229
2230void ImagingPlanner::searchSimbad()
2231{
2232 focusOnTable();
2233 QString name = currentObjectName();
2234
2235 if (name.startsWith("sh2"))
2236 name.replace(QRegularExpression("sh2\\s*"), "sh2-");
2237 else if (name.startsWith("hickson", Qt::CaseInsensitive))
2238 name.replace(QRegularExpression("hickson\\s*"), "HCG");
2239 else
2240 name.replace(' ', "");
2241
2242 QString urlStr = QString("https://simbad.cds.unistra.fr/simbad/sim-id?Ident=%1&NbIdent=1"
2243 "&Radius=20&Radius.unit=arcmin&submit=submit+id").arg(name);
2245}
2246
2247
2248// Crude massaging to conform to wikipedia standards
2249void ImagingPlanner::searchWikipedia()
2250{
2251 focusOnTable();
2252 QString wikipediaAddress = "https://en.wikipedia.org";
2253 QString name = currentObjectName();
2254 if (name.isEmpty())
2255 {
2256 fprintf(stderr, "NULL object sent to Wikipedia.\n");
2257 return;
2258 }
2259
2260 QString massagedName = name;
2262 massagedName = QString("Messier_%1").arg(name.mid(2, -1));
2263 else if (name.startsWith("ngc ", Qt::CaseInsensitive))
2264 massagedName = QString("NGC_%1").arg(name.mid(4, -1));
2265 else if (name.startsWith("ic ", Qt::CaseInsensitive))
2266 massagedName = QString("IC_%1").arg(name.mid(3, -1));
2267 else if (name.startsWith("sh2 ", Qt::CaseInsensitive))
2268 massagedName = QString("sh2-%1").arg(name.mid(4, -1));
2269 else if (name.startsWith("Abell ", Qt::CaseInsensitive))
2270 massagedName = QString("Abell_%1").arg(name.mid(6, -1));
2271 else
2272 {
2273 QString backupSearch = QString("%1/w/index.php?search=%2")
2274 .arg(wikipediaAddress).arg(massageObjectName(name));
2275 return;
2276 }
2278 QUrl(QString("%1/wiki/%2").arg(wikipediaAddress).arg(massagedName)));
2279}
2280
2281void ImagingPlanner::searchAstrobin()
2282{
2283 focusOnTable();
2284 QString name = currentObjectName();
2285 if (name.isEmpty())
2286 return;
2287 popupAstrobin(name);
2288}
2289
2290bool ImagingPlanner::eventFilter(QObject * obj, QEvent * event)
2291{
2292 if (m_loadingCatalog)
2293 return false;
2294
2295 if (m_InitialLoad && event->type() == QEvent::Paint)
2296 {
2297 m_InitialLoad = false;
2298 // Load the initial catalog in another thread.
2299 setStatus(i18n("Loading Catalogs..."));
2300 loadInitialCatalog();
2301 return false;
2302 }
2303
2304 // Right click on object in catalog view brings up this menu.
2305 QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
2306 if ((obj == ui->CatalogView->viewport()) &&
2307 // (ui->CatalogView->currentIndex().row() >= 0) &&
2308 (event->type() == QEvent::MouseButtonRelease) &&
2309 (mouseEvent->button() == Qt::RightButton))
2310 {
2311 int numImaged = 0, numNotImaged = 0, numPicked = 0, numNotPicked = 0, numIgnored = 0, numNotIgnored = 0;
2312 QStringList selectedNames;
2313 for (const auto &r : ui->CatalogView->selectionModel()->selectedRows())
2314 {
2315 selectedNames.append(r.siblingAtColumn(0).data().toString());
2316 bool isPicked = getFlag(r, PICKED_BIT, ui->CatalogView->model());
2317 if (isPicked) numPicked++;
2318 else numNotPicked++;
2319 bool isImaged = getFlag(r, IMAGED_BIT, ui->CatalogView->model());
2320 if (isImaged) numImaged++;
2321 else numNotImaged++;
2322 bool isIgnored = getFlag(r, IGNORED_BIT, ui->CatalogView->model());
2323 if (isIgnored) numIgnored++;
2324 else numNotIgnored++;
2325 }
2326
2327 if (selectedNames.size() == 0)
2328 return false;
2329
2330 if (!m_PopupMenu)
2331 m_PopupMenu = new ImagingPlannerPopup;
2332
2333 const bool imaged = numImaged > 0;
2334 const bool picked = numPicked > 0;
2335 const bool ignored = numIgnored > 0;
2336 m_PopupMenu->init(this, selectedNames,
2337 (numImaged > 0 && numNotImaged > 0) ? nullptr : &imaged,
2338 (numPicked > 0 && numNotPicked > 0) ? nullptr : &picked,
2339 (numIgnored > 0 && numNotIgnored > 0) ? nullptr : &ignored);
2340 QPoint pos(mouseEvent->globalX(), mouseEvent->globalY());
2341 m_PopupMenu->popup(pos);
2342 }
2343
2344 else if (obj == ui->userNotesEdit && event->type() == QEvent::FocusOut)
2345 userNotesEditFinished();
2346
2347 else if (obj == ui->keywordEdit && event->type() == QEvent::FocusOut)
2348 keywordEditFinished();
2349
2350 else if (obj == ui->keywordEdit && (event->type() == QEvent::KeyPress))
2351 {
2352 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
2353 auto key = keyEvent->key();
2354 switch(key)
2355 {
2356 case Qt::Key_Enter:
2357 case Qt::Key_Tab:
2358 case Qt::Key_Return:
2359 keywordEditFinished();
2360 ui->keywordEdit->clearFocus();
2361 break;
2362 default:
2363 ;
2364 }
2365 }
2366
2367 else if (obj == ui->SearchText && (event->type() == QEvent::KeyPress))
2368 {
2369 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
2370 auto key = keyEvent->key();
2371 switch(key)
2372 {
2373 case Qt::Key_Enter:
2374 case Qt::Key_Tab:
2375 case Qt::Key_Return:
2376 searchSlot();
2377 break;
2378 default:
2379 ;
2380 }
2381 }
2382
2383 else if ((obj == ui->ImagePreview ||
2384 obj == ui->ImagePreviewCredit ||
2385 obj == ui->ImagePreviewCreditLink) &&
2387 {
2388 if (!ui->ImagePreviewCreditLink->text().isEmpty())
2389 {
2390 QUrl url(ui->ImagePreviewCreditLink->text());
2392 }
2393 }
2394
2395 return false;
2396}
2397
2398void ImagingPlanner::keywordEditFinished()
2399{
2400 QString kwd = ui->keywordEdit->toPlainText().trimmed();
2401 ui->keywordEdit->clear();
2402 ui->keywordEdit->setText(kwd);
2403 if (m_Keyword != kwd)
2404 {
2405 m_Keyword = kwd;
2406 Options::setImagingPlannerKeyword(kwd);
2407 Options::self()->save();
2408 updateSortConstraints();
2409 m_CatalogSortModel->invalidate();
2410 ui->CatalogView->resizeColumnsToContents();
2411 updateDisplays();
2412 }
2413
2414}
2415void ImagingPlanner::setDefaultImage()
2416{
2417 ui->ImagePreview->setPixmap(m_NoImagePixmap);
2418 ui->ImagePreview->update();
2419 ui->ImagePreviewCredit->setText("");
2420 ui->ImagePreviewCreditLink->setText("");
2421}
2422
2423void ImagingPlanner::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
2424{
2425 if (m_loadingCatalog)
2426 return;
2427
2428 Q_UNUSED(deselected);
2429 if (selected.indexes().size() == 0)
2430 {
2431 disableUserNotes();
2432 return;
2433 }
2434
2435 initUserNotes();
2436 updateStatus();
2437 auto selection = selected.indexes()[0];
2438 QString name = selection.data().toString();
2439 CatalogObject *object = getObject(name);
2440 if (object == nullptr)
2441 return;
2442
2443 // This assumes current object and current selection are the same.
2444 // Could pass in "selected" if necessary.
2445 updateDisplays();
2446
2447 ui->ImagePreviewCredit->setText("");
2448 ui->ImagePreviewCreditLink->setText("");
2449 // clear the image too?
2450
2451 CatalogImageInfo catalogImageInfo;
2452 if (findCatalogImageInfo(name, &catalogImageInfo))
2453 {
2454 QString filename = catalogImageInfo.m_Filename;
2455 if (!filename.isEmpty() && !Options::imagingPlannerCatalogPath().isEmpty())
2456 {
2457 QString imageFullPath = filename;
2458 if (QFileInfo(filename).isRelative())
2459 {
2460 QString catDir = QFileInfo(Options::imagingPlannerCatalogPath()).absolutePath();
2461 imageFullPath = QString("%1%2%3").arg(catDir)
2462 .arg(QDir::separator()).arg(filename);
2463 }
2464 if (!QFile(imageFullPath).exists())
2465 DPRINTF(stderr, "Image for \"%s\" -- \"%s\" doesn't exist\n",
2466 name.toLatin1().data(), imageFullPath.toLatin1().data());
2467
2468 ui->ImagePreview->setPixmap(QPixmap::fromImage(QImage(imageFullPath)));
2469 if (!catalogImageInfo.m_Link.isEmpty())
2470 {
2471 ui->ImagePreviewCreditLink->setText(catalogImageInfo.m_Link);
2472 ui->ImagePreview->setToolTip("Click to see original");
2473 ui->ImagePreviewCreditLink->setToolTip("Click to see original");
2474 }
2475 else
2476 {
2477 ui->ImagePreviewCreditLink->setText("");
2478 ui->ImagePreview->setToolTip("");
2479 ui->ImagePreviewCreditLink->setToolTip("");
2480 }
2481
2482 if (!catalogImageInfo.m_Author.isEmpty() && !catalogImageInfo.m_License.isEmpty())
2483 {
2484 ui->ImagePreviewCredit->setText(
2485 QString("Credit: %1 (with license %2)").arg(catalogImageInfo.m_Author)
2486 .arg(creativeCommonsString(catalogImageInfo.m_License)));
2487 ui->ImagePreviewCredit->setToolTip(
2488 QString("Original image license: %1")
2489 .arg(creativeCommonsTooltipString(catalogImageInfo.m_License)));
2490 }
2491 else if (!catalogImageInfo.m_Author.isEmpty())
2492 {
2493 ui->ImagePreviewCredit->setText(
2494 QString("Credit: %1").arg(catalogImageInfo.m_Author));
2495 ui->ImagePreviewCredit->setToolTip("");
2496 }
2497 else if (!catalogImageInfo.m_License.isEmpty())
2498 {
2499 ui->ImagePreviewCredit->setText(
2500 QString("(license %1)").arg(creativeCommonsString(catalogImageInfo.m_License)));
2501 ui->ImagePreviewCredit->setToolTip(
2502 QString("Original image license: %1")
2503 .arg(creativeCommonsTooltipString(catalogImageInfo.m_License)));
2504 }
2505 else
2506 {
2507 ui->ImagePreviewCredit->setText("");
2508 ui->ImagePreviewCredit->setToolTip("");
2509 }
2510 }
2511 }
2512 else
2513 {
2514 object->load_image();
2515 auto image = object->image();
2516 if (!image.first)
2517 {
2518 // As a backup, see if the image is stored elsewhere...
2519 // I've seen many images stored in ~/.local/share/kstars/ZZ/ZZ-name.png,
2520 // e.g. kstars/thumb_ngc/thumb_ngc-m1.png
2521 const QString foundFilename = findObjectImage(name);
2522 if (!name.isEmpty())
2523 {
2524 constexpr int thumbHeight = 300, thumbWidth = 400;
2525 const QImage img = QImage(foundFilename);
2526 const bool scale = img.width() > thumbWidth || img.height() > thumbHeight;
2527 if (scale)
2528 ui->ImagePreview->setPixmap(
2529 QPixmap::fromImage(img.scaled(thumbWidth, thumbHeight, Qt::KeepAspectRatio)));
2530 else
2531 ui->ImagePreview->setPixmap(QPixmap::fromImage(img));
2532 }
2533 else
2534 setDefaultImage();
2535 }
2536 else
2537 ui->ImagePreview->setPixmap(QPixmap::fromImage(image.second));
2538 }
2539
2540}
2541
2542void ImagingPlanner::updateDisplays()
2543{
2544 updateCounts();
2545
2546 // If nothing is selected, then select the first thing.
2547 if (!currentCatalogObject())
2548 {
2549 if (ui->CatalogView->model()->rowCount() > 0)
2550 {
2551 auto index = ui->CatalogView->model()->index(0, 0);
2552 ui->CatalogView->selectionModel()->select(index,
2554 }
2555 }
2556
2557 auto object = currentCatalogObject();
2558 if (object)
2559 {
2560 updateDetails(*object, currentObjectFlags());
2561 updateNotes(currentObjectNotes());
2562 plotAltitudeGraph(getDate(), object->ra0(), object->dec0());
2563 centerOnSkymap();
2564 }
2565 updateStatus();
2566 focusOnTable();
2567}
2568
2569void ImagingPlanner::updateDetails(const CatalogObject &object, int flags)
2570{
2571 ui->infoObjectName->setText(object.name());
2572 ui->infoSize->setText(QString("%1' x %2'").arg(object.a(), 0, 'f', 1).arg(object.b(), 0, 'f', 1));
2573
2574 QPalette palette = ui->infoObjectLongName->palette();
2575 //palette.setColor(ui->infoObjectLongName->backgroundRole(), Qt::darkGray);
2576 palette.setColor(ui->infoObjectLongName->foregroundRole(), Qt::darkGray);
2577 ui->infoObjectLongName->setPalette(palette);
2578 if (object.longname().isEmpty() || (object.longname() == object.name()))
2579 ui->infoObjectLongName->clear();
2580 else
2581 ui->infoObjectLongName->setText(QString("(%1)").arg(object.longname()));
2582
2583 ui->infoObjectType->setText(SkyObject::typeName(object.type()));
2584
2585 auto noon = KStarsDateTime(getDate(), QTime(12, 0, 0));
2586 QTime riseTime = object.riseSetTime(noon, getGeo(), true);
2587 QTime setTime = object.riseSetTime(noon, getGeo(), false);
2588 QTime transitTime = object.transitTime(noon, getGeo());
2589 dms transitAltitude = object.transitAltitude(noon, getGeo());
2590
2591 QString moonString;
2592 KSMoon *moon = getMoon();
2593 if (moon)
2594 {
2595 const double separation = ui->CatalogView->selectionModel()->currentIndex()
2596 .siblingAtColumn(MOON_COLUMN).data(MOON_ROLE).toDouble();
2597 // The unicode character is the angle sign.
2598 if (separation >= 0)
2599 moonString = QString("%1 \u2220 %3º").arg(i18n("Moon")).arg(separation, 0, 'f', 1);
2600 }
2601
2602 QString riseSetString;
2603 if (!riseTime.isValid() && !setTime.isValid() && transitTime.isValid())
2604 riseSetString = QString("%1 %2 @ %3º")
2605 .arg(i18n("Transits"))
2606 .arg(transitTime.toString("h:mm"))
2607 .arg(transitAltitude.Degrees(), 0, 'f', 1);
2608 else if (!riseTime.isValid() && setTime.isValid() && !transitTime.isValid())
2609 riseSetString = QString("%1 %2")
2610 .arg(i18n("Sets at"))
2611 .arg(setTime.toString("h:mm"));
2612 else if (!riseTime.isValid() && setTime.isValid() && transitTime.isValid())
2613 riseSetString = QString("%1 %2 %3 %4 @ %5º")
2614 .arg(i18n("Sets at"))
2615 .arg(setTime.toString("h:mm"))
2616 .arg(i18n("Transit"))
2617 .arg(transitTime.toString("h:mm"))
2618 .arg(transitAltitude.Degrees(), 0, 'f', 1);
2619 else if (riseTime.isValid() && !setTime.isValid() && !transitTime.isValid())
2620 riseSetString = QString("%1 %2")
2621 .arg(i18n("Rises at"))
2622 .arg(riseTime.toString("h:mm"));
2623 else if (riseTime.isValid() && !setTime.isValid() && transitTime.isValid())
2624 riseSetString = QString("%1 %2 %3 %4 @ %5º")
2625 .arg(i18n("Rises at"))
2626 .arg(riseTime.toString("h:mm"))
2627 .arg(i18n("Transit"))
2628 .arg(transitTime.toString("h:mm"))
2629 .arg(transitAltitude.Degrees(), 0, 'f', 1);
2630 else if (riseTime.isValid() && setTime.isValid() && !transitTime.isValid())
2631 riseSetString = QString("%1 %2 %3 %4")
2632 .arg(i18n("Rises"))
2633 .arg(riseTime.toString("h:mm"))
2634 .arg(i18n("Sets"))
2635 .arg(setTime.toString("h:mm"));
2636 else if (riseTime.isValid() && setTime.isValid() && transitTime.isValid())
2637 riseSetString = QString("%1 %2 %3 %4 %5 %6 @ %7º")
2638 .arg(i18n("Rises"))
2639 .arg(riseTime.toString("h:mm"))
2640 .arg(i18n("Sets"))
2641 .arg(setTime.toString("h:mm"))
2642 .arg(i18n("Transit"))
2643 .arg(transitTime.toString("h:mm"))
2644 .arg(transitAltitude.Degrees(), 0, 'f', 1);
2645 if (moonString.size() > 0)
2646 riseSetString.append(QString(", %1").arg(moonString));
2647 ui->infoRiseSet->setText(riseSetString);
2648
2649 palette = ui->infoObjectFlags->palette();
2650 palette.setColor(ui->infoObjectFlags->foregroundRole(), Qt::darkGray);
2651 ui->infoObjectFlags->setPalette(palette);
2652 ui->infoObjectFlags->setText(flagString(flags));
2653}
2654
2655// TODO: This code needs to be shared with the scheduler somehow.
2656// Right now 2 very similar copies at the end of scheduler.cpp and here.
2657//
2658// Clearly below I had timezone issues. The problem was running this code using a timezone
2659// that was not the local timezone of the machine. E.g. setting KStars to australia
2660// when I'm in california.
2661void ImagingPlanner::plotAltitudeGraph(const QDate &date, const dms &ra, const dms &dec)
2662{
2663 auto altitudeGraph = ui->altitudeGraph;
2664 altitudeGraph->setAltitudeAxis(-20.0, 90.0);
2665 //altitudeGraph->axis(KPlotWidget::TopAxis)->setVisible(false);
2666
2667 QVector<QDateTime> jobStartTimes, jobEndTimes;
2668 getRunTimes(date, *getGeo(), ui->minAltitude->value(), ui->minMoon->value(), ra, dec, ui->useArtificialHorizon->isChecked(),
2669 &jobStartTimes, &jobEndTimes);
2670
2671 auto tz = QTimeZone(getGeo()->TZ() * 3600);
2672 KStarsDateTime midnight = KStarsDateTime(date.addDays(1), QTime(0, 1));
2673 midnight.setTimeZone(tz);
2674
2675 KStarsDateTime ut = getGeo()->LTtoUT(KStarsDateTime(midnight));
2676 KSAlmanac ksal(ut, getGeo());
2677 QDateTime dawn = midnight.addSecs(24 * 3600 * ksal.getDawnAstronomicalTwilight());
2678 dawn.setTimeZone(tz);
2679 QDateTime dusk = midnight.addSecs(24 * 3600 * ksal.getDuskAstronomicalTwilight());
2680 dusk.setTimeZone(tz);
2681
2682 Ekos::SchedulerJob job;
2683 setupJob(job, "temp", ui->minAltitude->value(), ui->minMoon->value(), ra, dec, ui->useArtificialHorizon->isChecked());
2684
2685 QVector<double> times, alts;
2686 QDateTime plotStart = dusk;
2687 plotStart.setTimeZone(tz);
2688
2689
2690 // Start the plot 1 hour before dusk and end it an hour after dawn.
2691 plotStart = plotStart.addSecs(-1 * 3600);
2692 auto t = plotStart;
2693 t.setTimeZone(tz);
2694 auto plotEnd = dawn.addSecs(1 * 3600);
2695 plotEnd.setTimeZone(tz);
2696
2697 while (t.secsTo(plotEnd) > 0)
2698 {
2699 SkyPoint coords = job.getTargetCoords();
2700 double alt = getAltitude(getGeo(), coords, t);
2701 alts.push_back(alt);
2702 double hour = midnight.secsTo(t) / 3600.0;
2703 times.push_back(hour);
2704 t = t.addSecs(60 * 10);
2705 }
2706
2707 altitudeGraph->plot(getGeo(), &ksal, times, alts, false);
2708
2709 for (int i = 0; i < jobStartTimes.size(); ++i)
2710 {
2711 auto startTime = jobStartTimes[i];
2712 auto stopTime = jobEndTimes[i];
2713 if (startTime < plotStart) startTime = plotStart;
2714 if (stopTime > plotEnd) stopTime = plotEnd;
2715
2716 startTime.setTimeZone(tz);
2717 stopTime.setTimeZone(tz);
2718
2719 QVector<double> runTimes, runAlts;
2720 auto t = startTime;
2721 t.setTimeZone(tz);
2722 //t.setTimeZone(jobStartTimes[0].timeZone());
2723
2724 while (t.secsTo(stopTime) > 0)
2725 {
2726 SkyPoint coords = job.getTargetCoords();
2727 double alt = getAltitude(getGeo(), coords, t);
2728 runAlts.push_back(alt);
2729 double hour = midnight.secsTo(t) / 3600.0;
2730 runTimes.push_back(hour);
2731 t = t.addSecs(60 * 10);
2732 }
2733 altitudeGraph->plot(getGeo(), &ksal, runTimes, runAlts, true);
2734 }
2735}
2736
2737void ImagingPlanner::updateCounts()
2738{
2739 const int numDisplayedObjects = m_CatalogSortModel->rowCount();
2740 const int totalCatalogObjects = m_CatalogModel->rowCount();
2741 if (numDisplayedObjects == 1)
2742 ui->tableCount->setText(QString("1/%1 %2").arg(totalCatalogObjects).arg(i18n("object")));
2743 else
2744 ui->tableCount->setText(QString("%1/%2 %3").arg(numDisplayedObjects).arg(totalCatalogObjects).arg(i18n("objects")));
2745}
2746
2747void ImagingPlanner::moveBackOneDay()
2748{
2749 // Try to keep the object.
2750 QString selection = currentObjectName();
2751 ui->DateEdit->setDate(ui->DateEdit->date().addDays(-1));
2752 // Don't need to call recompute(), called by dateChanged callback.
2753 updateDisplays();
2754 updateMoon();
2755 scrollToName(selection);
2756}
2757
2758void ImagingPlanner::moveForwardOneDay()
2759{
2760 QString selection = currentObjectName();
2761 ui->DateEdit->setDate(ui->DateEdit->date().addDays(1));
2762 // Don't need to call recompute(), called by dateChanged callback.
2763 updateDisplays();
2764 updateMoon();
2765 scrollToName(selection);
2766}
2767
2768QString ImagingPlanner::currentObjectName() const
2769{
2770 QString name = ui->CatalogView->selectionModel()->currentIndex().siblingAtColumn(NAME_COLUMN).data(
2771 Qt::DisplayRole).toString();
2772 return name;
2773}
2774
2775CatalogObject *ImagingPlanner::currentCatalogObject()
2776{
2777 QString name = currentObjectName();
2778 return getObject(name);
2779}
2780
2781//FIXME: This will open multiple Detail windows for each object;
2782//Should have one window whose target object changes with selection
2783void ImagingPlanner::objectDetails()
2784{
2785 CatalogObject *current = currentCatalogObject();
2786 if (current == nullptr)
2787 return;
2788 auto ut = KStarsData::Instance()->ut();
2789 ut.setDate(getDate());
2791 new DetailDialog(current, ut, getGeo(), KStars::Instance());
2792 dd->exec();
2793 delete dd;
2794}
2795
2796void ImagingPlanner::centerOnSkymap()
2797{
2798 if (!Options::imagingPlannerCenterOnSkyMap())
2799 return;
2800 reallyCenterOnSkymap();
2801}
2802
2803void ImagingPlanner::reallyCenterOnSkymap()
2804{
2805 CatalogObject *current = currentCatalogObject();
2806 if (current == nullptr)
2807 return;
2808
2809 // These shouldn't happen anymore--seemed to happen when I let in null objects.
2810 if (current->ra().Degrees() == 0 && current->dec().Degrees() == 0)
2811 {
2812 DPRINTF(stderr, "found a 0,0 object\n");
2813 return;
2814 }
2815
2816 // Set up the Alt/Az coordinates that SkyMap needs.
2817 KStarsDateTime time = KStarsData::Instance()->clock()->utc();
2818 dms lst = getGeo()->GSTtoLST(time.gst());
2819 current->EquatorialToHorizontal(&lst, getGeo()->lat());
2820
2821
2822 // Doing this to avoid the pop-up warning that an object is below the ground.
2823 bool keepGround = Options::showGround();
2824 bool keepAnimatedSlew = Options::useAnimatedSlewing();
2825 Options::setShowGround(false);
2826 Options::setUseAnimatedSlewing(false);
2827
2828 SkyMap::Instance()->setClickedObject(current);
2829 SkyMap::Instance()->setClickedPoint(current);
2830 SkyMap::Instance()->slotCenter();
2831
2832 Options::setShowGround(keepGround);
2833 Options::setUseAnimatedSlewing(keepAnimatedSlew);
2834}
2835
2836void ImagingPlanner::setSelection(int flag, bool enabled)
2837{
2838 auto rows = ui->CatalogView->selectionModel()->selectedRows();
2839
2840 // We can't use the selection for processing, because it may change on the fly
2841 // as we modify flags (e.g. if the view is set to showing picked objects only
2842 // and we are disabling the picked flag, as a selected object with a picked flag
2843 // gets de-picked, it will also get deselected.
2844 // So, we store a list of the source model indeces, and operate on the source model.
2845
2846 // Find the source model indeces.
2847 QList<QModelIndex> sourceIndeces;
2848 for (int i = 0; i < rows.size(); ++i)
2849 {
2850 auto proxyIndex = rows[i].siblingAtColumn(FLAGS_COLUMN);
2851 auto sourceIndex = m_CatalogSortModel->mapToSource(proxyIndex);
2852 sourceIndeces.append(sourceIndex);
2853 }
2854
2855 for (int i = 0; i < sourceIndeces.size(); ++i)
2856 {
2857 auto &sourceIndex = sourceIndeces[i];
2858
2859 // Set or clear the flags using the source model.
2860 if (enabled)
2861 setFlag(sourceIndex, flag, m_CatalogModel.data());
2862 else
2863 clearFlag(sourceIndex, flag, m_CatalogModel.data());
2864
2865 QString name = m_CatalogModel->data(sourceIndex.siblingAtColumn(NAME_COLUMN)).toString();
2866 int flags = m_CatalogModel->data(sourceIndex.siblingAtColumn(FLAGS_COLUMN), FLAGS_ROLE).toInt();
2867 QString notes = m_CatalogModel->data(sourceIndex.siblingAtColumn(NOTES_COLUMN), NOTES_ROLE).toString();
2868 saveToDB(name, flags, notes);
2869
2870 if (flag == IMAGED_BIT)
2871 highlightImagedObject(sourceIndex, enabled);
2872 if (flag == PICKED_BIT)
2873 highlightPickedObject(sourceIndex, enabled);
2874 }
2875 updateDisplays();
2876}
2877
2878void ImagingPlanner::highlightImagedObject(const QModelIndex &index, bool imaged)
2879{
2880 // TODO: Ugly, for now. Figure out how to use the color schemes the right way.
2881 QColor m_DefaultCellBackground(36, 35, 35);
2882 QColor m_ImagedObjectBackground(10, 65, 10);
2883 QString themeName = KSTheme::Manager::instance()->currentThemeName().toLatin1().data();
2884 if (themeName == "High Key" || themeName == "Default" || themeName == "White Balance")
2885 {
2886 m_DefaultCellBackground = QColor(240, 240, 240);
2887 m_ImagedObjectBackground = QColor(180, 240, 180);
2888 }
2889 for (int col = 0; col < LAST_COLUMN; ++col)
2890 {
2891 auto colIndex = index.siblingAtColumn(col);
2892 m_CatalogModel->setData(colIndex, imaged ? m_ImagedObjectBackground : m_DefaultCellBackground, Qt::BackgroundRole);
2893 }
2894}
2895
2896void ImagingPlanner::highlightPickedObject(const QModelIndex &index, bool picked)
2897{
2898 for (int col = 0; col < LAST_COLUMN; ++col)
2899 {
2900 auto colIndex = index.siblingAtColumn(col);
2901 auto font = m_CatalogModel->data(colIndex, Qt::FontRole);
2902 auto ff = qvariant_cast<QFont>(font);
2903 ff.setBold(picked);
2904 ff.setItalic(picked);
2905 ff.setUnderline(picked);
2906 font = ff;
2907 m_CatalogModel->setData(colIndex, font, Qt::FontRole);
2908 }
2909}
2910
2911void ImagingPlanner::setSelectionPicked()
2912{
2913 setSelection(PICKED_BIT, true);
2914}
2915
2916void ImagingPlanner::setSelectionNotPicked()
2917{
2918 setSelection(PICKED_BIT, false);
2919}
2920
2921void ImagingPlanner::setSelectionImaged()
2922{
2923 setSelection(IMAGED_BIT, true);
2924}
2925
2926void ImagingPlanner::setSelectionNotImaged()
2927{
2928 setSelection(IMAGED_BIT, false);
2929}
2930
2931void ImagingPlanner::setSelectionIgnored()
2932{
2933 setSelection(IGNORED_BIT, true);
2934}
2935
2936void ImagingPlanner::setSelectionNotIgnored()
2937{
2938 setSelection(IGNORED_BIT, false);
2939}
2940
2941int ImagingPlanner::currentObjectFlags()
2942{
2943 auto index = ui->CatalogView->selectionModel()->currentIndex().siblingAtColumn(FLAGS_COLUMN);
2944 const bool hasFlags = ui->CatalogView->model()->data(index, FLAGS_ROLE).canConvert<int>();
2945 if (!hasFlags)
2946 return 0;
2947 return ui->CatalogView->model()->data(index, FLAGS_ROLE).toInt();
2948}
2949
2950QString ImagingPlanner::currentObjectNotes()
2951{
2952 auto index = ui->CatalogView->selectionModel()->currentIndex().siblingAtColumn(NOTES_COLUMN);
2953 const bool hasNotes = ui->CatalogView->model()->data(index, NOTES_ROLE).canConvert<QString>();
2954 if (!hasNotes)
2955 return QString();
2956 return ui->CatalogView->model()->data(index, NOTES_ROLE).toString();
2957}
2958
2959void ImagingPlanner::setCurrentObjectNotes(const QString &notes)
2960{
2961 auto index = ui->CatalogView->selectionModel()->currentIndex();
2962 if (!index.isValid())
2963 return;
2964 auto sibling = index.siblingAtColumn(NOTES_COLUMN);
2965
2966 auto sourceIndex = m_CatalogSortModel->mapToSource(sibling);
2967 QVariant n(notes);
2968 m_CatalogModel->setData(sourceIndex, n, NOTES_ROLE);
2969}
2970
2971ImagingPlannerPopup::ImagingPlannerPopup() : QMenu(nullptr)
2972{
2973}
2974
2975// The bools are pointers to we can have a 3-valued input parameter.
2976// If the pointer is a nullptr, then we say, for example it is neigher imaged, not not imaged.
2977// That is, really, some of the selection are imaged and some not imaged.
2978// If the pointer does point to a bool, then the value of that bool tells you if all the selection
2979// is (e.g.) imaged, or if all of it is not imaged.
2980void ImagingPlannerPopup::init(ImagingPlanner * planner, const QStringList &names,
2981 const bool * imaged, const bool * picked, const bool * ignored)
2982{
2983 clear();
2984 if (names.size() == 0) return;
2985
2986 QString title;
2987 if (names.size() == 1)
2988 title = names[0];
2989 else if (names.size() <= 3)
2990 {
2991 title = names[0];
2992 for (int i = 1; i < names.size(); i++)
2993 title.append(QString(", %1").arg(names[i]));
2994 }
2995 else
2996 title = i18n("%1, %2 and %3 other objects", names[0], names[1], names.size() - 2);
2997
2999
3000 QString word = names.size() == 1 ? names[0] : i18n("objects");
3001
3002 if (imaged == nullptr)
3003 {
3004 addAction(i18n("Mark %1 as NOT imaged", word), planner, &ImagingPlanner::setSelectionNotImaged);
3005 addAction(i18n("Mark %1 as already imaged", word), planner, &ImagingPlanner::setSelectionImaged);
3006 }
3007 else if (*imaged)
3008 addAction(i18n("Mark %1 as NOT imaged", word), planner, &ImagingPlanner::setSelectionNotImaged);
3009 else
3010 addAction(i18n("Mark %1 as already imaged", word), planner, &ImagingPlanner::setSelectionImaged);
3011
3012 if (picked == nullptr)
3013 {
3014 addAction(i18n("Un-pick %1", word), planner, &ImagingPlanner::setSelectionNotPicked);
3015 addAction(i18n("Pick %1", word), planner, &ImagingPlanner::setSelectionPicked);
3016 }
3017 else if (*picked)
3018 addAction(i18n("Un-pick %1", word), planner, &ImagingPlanner::setSelectionNotPicked);
3019 else
3020 addAction(i18n("Pick %1", word), planner, &ImagingPlanner::setSelectionPicked);
3021
3022
3023 if (ignored == nullptr)
3024 {
3025 addAction(i18n("Stop ignoring %1", word), planner, &ImagingPlanner::setSelectionNotIgnored);
3026 addAction(i18n("Ignore %1", word), planner, &ImagingPlanner::setSelectionIgnored);
3027
3028 }
3029 else if (*ignored)
3030 addAction(i18n("Stop ignoring %1", word), planner, &ImagingPlanner::setSelectionNotIgnored);
3031 else
3032 addAction(i18n("Ignore %1", word), planner, &ImagingPlanner::setSelectionIgnored);
3033
3034 addSeparator();
3035 addAction(i18n("Center %1 on SkyMap", names[0]), planner, &ImagingPlanner::reallyCenterOnSkymap);
3036
3037}
3038
3039ImagingPlannerDBEntry::ImagingPlannerDBEntry(const QString &name, bool picked, bool imaged,
3040 bool ignored, const QString &notes) : m_Name(name), m_Notes(notes)
3041{
3042 setFlags(picked, imaged, ignored);
3043}
3044
3045ImagingPlannerDBEntry::ImagingPlannerDBEntry(const QString &name, int flags, const QString &notes)
3046 : m_Name(name), m_Flags(flags), m_Notes(notes)
3047{
3048}
3049
3050void ImagingPlannerDBEntry::getFlags(bool * picked, bool * imaged, bool * ignored)
3051{
3052 *picked = m_Flags & PickedBit;
3053 *imaged = m_Flags & ImagedBit;
3054 *ignored = m_Flags & IgnoredBit;
3055}
3056
3057
3058void ImagingPlannerDBEntry::setFlags(bool picked, bool imaged, bool ignored)
3059{
3060 m_Flags = 0;
3061 if (picked) m_Flags |= PickedBit;
3062 if (imaged) m_Flags |= ImagedBit;
3063 if (ignored) m_Flags |= IgnoredBit;
3064}
3065
3066void ImagingPlanner::saveToDB(const QString &name, bool picked, bool imaged,
3067 bool ignored, const QString &notes)
3068{
3069 ImagingPlannerDBEntry e(name, 0, notes);
3070 e.setFlags(picked, imaged, ignored);
3071 KStarsData::Instance()->userdb()->AddImagingPlannerEntry(e);
3072}
3073
3074void ImagingPlanner::saveToDB(const QString &name, int flags, const QString &notes)
3075{
3076 ImagingPlannerDBEntry e(name, flags, notes);
3077 KStarsData::Instance()->userdb()->AddImagingPlannerEntry(e);
3078}
3079
3080// KSUserDB::GetAllImagingPlannerEntries(QList<ImagingPlannerDBEntry> *entryList)
3081void ImagingPlanner::loadFromDB()
3082{
3083 // Disconnect the filter from the model, or else we'll re-filter numRows squared times.
3084 // Not as big a deal here because we're not touching all rows, just the rows with flags/notes.
3085 // Also see the reconnect below.
3086 m_CatalogSortModel->setSourceModel(nullptr);
3087
3088 auto tz = QTimeZone(getGeo()->TZ() * 3600);
3089 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
3090 KStarsDateTime ut = getGeo()->LTtoUT(KStarsDateTime(midnight));
3091 KSAlmanac ksal(ut, getGeo());
3092
3094 KStarsData::Instance()->userdb()->GetAllImagingPlannerEntries(&list);
3096 QHash<QString, int> dbNotes;
3097 for (const auto &entry : list)
3098 {
3099 dbData[entry.m_Name] = entry;
3100 }
3101
3102 int rows = m_CatalogModel->rowCount();
3103 for (int i = 0; i < rows; ++i)
3104 {
3105 const QString &name = m_CatalogModel->item(i, NAME_COLUMN)->text();
3106 auto entry = dbData.find(name);
3107 if (entry != dbData.end())
3108 {
3109 QVariant f = entry->m_Flags;
3110 m_CatalogModel->item(i, FLAGS_COLUMN)->setData(f, FLAGS_ROLE);
3111 if (entry->m_Flags & IMAGED_BIT)
3112 highlightImagedObject(m_CatalogModel->index(i, NAME_COLUMN), true);
3113 if (entry->m_Flags & PICKED_BIT)
3114 highlightPickedObject(m_CatalogModel->index(i, NAME_COLUMN), true);
3115 QVariant n = entry->m_Notes;
3116 m_CatalogModel->item(i, NOTES_COLUMN)->setData(n, NOTES_ROLE);
3117 }
3118 }
3119 // See above. Reconnect the filter to the model.
3120 m_CatalogSortModel->setSourceModel(m_CatalogModel.data());
3121}
3122
3123void ImagingPlanner::loadImagedFile()
3124{
3125 if (m_loadingCatalog)
3126 return;
3127
3128 focusOnTable();
3129 QString fileName = QFileDialog::getOpenFileName(this,
3130 tr("Open Already-Imaged File"), QDir::homePath(), tr("Any files (*)"));
3131 if (fileName.isEmpty())
3132 return;
3133 QFile inputFile(fileName);
3134 if (inputFile.open(QIODevice::ReadOnly))
3135 {
3136 int numSuccess = 0;
3137 QStringList failedNames;
3138 QTextStream in(&inputFile);
3139 while (!in.atEnd())
3140 {
3141 QString name = in.readLine().trimmed();
3142 if (name.isEmpty() || name.startsWith('#'))
3143 continue;
3144 name = tweakNames(name);
3145 if (getObject(name))
3146 {
3147 numSuccess++;
3148 auto startIndex = m_CatalogModel->index(0, NAME_COLUMN);
3149 QVariant value(name);
3150 auto matches = m_CatalogModel->match(startIndex, Qt::DisplayRole, value, 1, Qt::MatchFixedString);
3151 if (matches.size() > 0)
3152 {
3153 setFlag(matches[0], IMAGED_BIT, m_CatalogModel);
3154 highlightImagedObject(matches[0], true);
3155
3156 // Make sure we save it to the DB.
3157 QString name = m_CatalogModel->data(matches[0].siblingAtColumn(NAME_COLUMN)).toString();
3158 int flags = m_CatalogModel->data(matches[0].siblingAtColumn(FLAGS_COLUMN), FLAGS_ROLE).toInt();
3159 QString notes = m_CatalogModel->data(matches[0].siblingAtColumn(NOTES_COLUMN), NOTES_ROLE).toString();
3160 saveToDB(name, flags, notes);
3161 }
3162 else
3163 {
3164 DPRINTF(stderr, "ooops! internal inconsitency--got an object but match didn't work");
3165 }
3166 }
3167 else
3168 failedNames.append(name);
3169 }
3170 inputFile.close();
3171 if (failedNames.size() == 0)
3172 {
3173 if (numSuccess > 0)
3174 KSNotification::info(i18n("Successfully marked %1 objects as read", numSuccess));
3175 else
3176 KSNotification::sorry(i18n("Empty file"));
3177 }
3178 else
3179 {
3180 int num = std::min((int)failedNames.size(), 10);
3181 QString sample = QString("\"%1\"").arg(failedNames[0]);
3182 for (int i = 1; i < num; ++i)
3183 sample.append(QString(" \"%1\"").arg(failedNames[i]));
3184 if (numSuccess == 0 && failedNames.size() <= 10)
3185 KSNotification::sorry(i18n("Failed marking all of these objects imaged: %1", sample));
3186 else if (numSuccess == 0)
3187 KSNotification::sorry(i18n("Failed marking %1 objects imaged, including: %2", failedNames.size(), sample));
3188 else if (numSuccess > 0 && failedNames.size() <= 10)
3189 KSNotification::sorry(i18n("Succeeded marking %1 objects imaged. Failed with %2: %3",
3190 numSuccess, failedNames.size() == 1 ? "this" : "these", sample));
3191 else
3192 KSNotification::sorry(i18n("Succeeded marking %1 objects imaged. Failed with %2 including these: %3",
3193 numSuccess, failedNames.size(), sample));
3194 }
3195 }
3196 else
3197 {
3198 KSNotification::sorry(i18n("Sorry, couldn't open file: \"%1\"", fileName));
3199 }
3200}
3201
3202void ImagingPlanner::addCatalogImageInfo(const CatalogImageInfo &info)
3203{
3204 m_CatalogImageInfoMap[info.m_Name.toLower()] = info;
3205}
3206
3207bool ImagingPlanner::findCatalogImageInfo(const QString &name, CatalogImageInfo *info)
3208{
3209 auto result = m_CatalogImageInfoMap.find(name.toLower());
3210 if (result == m_CatalogImageInfoMap.end())
3211 return false;
3212 if (result->m_Filename.isEmpty())
3213 return false;
3214 *info = *result;
3215 return true;
3216}
3217
3218void ImagingPlanner::loadCatalogViaMenu()
3219{
3220 QString startDir = Options::imagingPlannerCatalogPath();
3221 if (startDir.isEmpty())
3222 startDir = defaultDirectory();
3223
3224 QString path = QFileDialog::getOpenFileName(this, tr("Open Catalog File"), startDir, tr("Any files (*.csv)"));
3225 if (path.isEmpty())
3226 return;
3227
3228 loadCatalog(path);
3229}
3230
3231void ImagingPlanner::loadCatalog(const QString &path)
3232{
3233#ifdef THREADED_LOAD_CATALOG
3234 // All this below in order to keep the UI active while loading.
3235 m_LoadCatalogs = QtConcurrent::run([this, path]()
3236 {
3237 loadCatalogFromFile(path);
3238 });
3239 m_LoadCatalogsWatcher = new QFutureWatcher<void>(this);
3240 m_LoadCatalogsWatcher->setFuture(m_LoadCatalogs);
3241 connect(m_LoadCatalogsWatcher, &QFutureWatcher<void>::finished,
3242 [this]()
3243 {
3244 catalogLoaded();
3245 disconnect(m_LoadCatalogsWatcher);
3246 });
3247#else
3248 removeEventFilters();
3249 m_loadingCatalog = true;
3250 loadCatalogFromFile(path);
3251 catalogLoaded();
3252 m_loadingCatalog = false;
3253 installEventFilters();
3254#endif
3255}
3256
3257CatalogImageInfo::CatalogImageInfo(const QString &csv)
3258{
3259 QString line = csv.trimmed();
3260 if (line.isEmpty() || line.startsWith('#'))
3261 return;
3262 QStringList columns = line.split(",");
3263 if (columns.size() < 1 || columns[0].isEmpty())
3264 return;
3265 int column = 0;
3266 m_Name = columns[column++];
3267 if (columns.size() <= column) return;
3268 m_Filename = columns[column++];
3269 if (columns.size() <= column) return;
3270 m_Author = columns[column++];
3271 if (columns.size() <= column) return;
3272 m_Link = columns[column++];
3273 if (columns.size() <= column) return;
3274 m_License = columns[column++];
3275}
3276
3277// This does the following:
3278// - Clears the internal catalog
3279// - Initializes the m_CatalogImageInfoMap, which goes from name to image.
3280// - Loads in new objects into the internal catalog.
3281//
3282// CSV File Columns:
3283// 1: ID: M 1
3284// 2: Image Filename: M_1.jpg
3285// 3: Author: Hy Murveit
3286// 4: Link: https://www.astrobin.com/x3utgw/F/
3287// 5: License: ACC (possibilities are ACC,ANCCC,ASACC,ANCCC,ANCSACC)
3288// last one is Attribution Non-Commercial hare-Alike Creative Commons
3289// Currently ID is mandatory, if there is an image filename, then Author,Link,and License
3290// are also required, though could be blank.
3291// Comment lines start with #
3292// Can include another catalog with "LoadCatalog FILENAME"
3293void ImagingPlanner::loadCatalogFromFile(QString path, bool reset)
3294{
3295 QFile inputFile(path);
3296 if (reset)
3297 {
3298 m_numWithImage = 0;
3299 m_numMissingImage = 0;
3300 }
3301 int numMissingImage = 0, numWithImage = 0;
3302 if (!inputFile.exists())
3303 {
3304 emit popupSorry(i18n("Sorry, catalog file doesn't exist: \"%1\"", path));
3305 return;
3306 }
3307 QStringList objectNames;
3308 if (inputFile.open(QIODevice::ReadOnly))
3309 {
3310 auto tz = QTimeZone(getGeo()->TZ() * 3600);
3311 KStarsDateTime midnight = KStarsDateTime(getDate().addDays(1), QTime(0, 1));
3312 KStarsDateTime ut = getGeo()->LTtoUT(KStarsDateTime(midnight));
3313 KSAlmanac ksal(ut, getGeo());
3314
3315 if (reset)
3316 {
3317 Options::setImagingPlannerCatalogPath(path);
3318 Options::self()->save();
3319 if (m_CatalogModel->rowCount() > 0)
3320 m_CatalogModel->removeRows(0, m_CatalogModel->rowCount());
3321 clearObjects();
3322 }
3323 QTextStream in(&inputFile);
3324 while (!in.atEnd())
3325 {
3326 CatalogImageInfo info(in.readLine().trimmed());
3327 if (info.m_Name.isEmpty())
3328 continue;
3329 if (info.m_Name.startsWith("LoadCatalog"))
3330 {
3331 // This line isn't a normal entry, but rather points to another catalog.
3332 // Load that catalog and then skip this line.
3334 auto match = re.match(info.m_Name);
3335 if (match.hasMatch())
3336 {
3337 QString catFilename = match.captured(1);
3338 if (catFilename.isEmpty()) continue;
3339 QFileInfo info(catFilename);
3340
3341 QString catFullPath = catFilename;
3342 if (!info.isAbsolute())
3343 {
3344 QString catDir = QFileInfo(path).absolutePath();
3345 catFullPath = QString("%1%2%3").arg(catDir)
3346 .arg(QDir::separator()).arg(match.captured(1));
3347 }
3348 if (catFullPath != path)
3349 loadCatalogFromFile(catFullPath, false);
3350 }
3351 continue;
3352 }
3353 objectNames.append(info.m_Name);
3354 if (!info.m_Filename.isEmpty())
3355 {
3356 numWithImage++;
3357 QFileInfo fInfo(info.m_Filename);
3358 if (fInfo.isRelative())
3359 info.m_Filename = QString("%1%2%3").arg(QFileInfo(path).absolutePath())
3360 .arg(QDir::separator()).arg(info.m_Filename);
3361 addCatalogImageInfo(info);
3362 }
3363 else
3364 {
3365 numMissingImage++;
3366 DPRINTF(stderr, "No catalog image for %s\n", info.m_Name.toLatin1().data());
3367 }
3368#ifndef THREADED_LOAD_CATALOG
3370#endif
3371 }
3372 inputFile.close();
3373
3374 int num = 0, numBad = 0, iteration = 0;
3375 // Move to threaded thing??
3376 for (const auto &name : objectNames)
3377 {
3378 setStatus(i18n("%1/%2: Adding %3", ++iteration, objectNames.size(), name));
3379 if (addCatalogItem(ksal, name, 0)) num++;
3380 else
3381 {
3382 DPRINTF(stderr, "Couldn't add %s\n", name.toLatin1().data());
3383 numBad++;
3384 }
3385 }
3386 m_numWithImage += numWithImage;
3387 m_numMissingImage += numMissingImage;
3388 DPRINTF(stderr, "Catalog %s: %d of %d have catalog images\n",
3389 path.toLatin1().data(), numWithImage, numWithImage + numMissingImage);
3390
3391 // Clear the old maps? Probably earlier in this method:
3392 // E.g. m_CatalogImageInfoMap?? n_CatalogHash???
3393 // When m_CatalogHash is not cleared, then the add fails currently.
3394 }
3395 else
3396 {
3397 emit popupSorry(i18n("Sorry, couldn't open file: \"%1\"", path));
3398 }
3399}
3400
3401void ImagingPlanner::sorry(const QString &message)
3402{
3403 KSNotification::sorry(message);
3404}
3405
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:1021
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()
void initialize(StandardShortcut id)
std::pair< bool, CatalogObject > resolveName(const QString &name)
Resolve the name of the given DSO and extract data from various sources.
void setChecked(bool)
void clicked(bool checked)
void toggled(bool checked)
virtual QVariant data(const QModelIndex &index, int role) const const=0
virtual 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()
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
void removeEventFilter(QObject *obj)
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 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 Fri Dec 13 2024 11:47:12 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.