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

KDE's Doxygen guidelines are available online.