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

KDE's Doxygen guidelines are available online.