Kstars

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

KDE's Doxygen guidelines are available online.