1 /*
2     SPDX-FileCopyrightText: 2004-2020 Jeff Woods <jcwoods@bellsouth.net>
3     SPDX-FileCopyrightText: 2004-2020 Jason Harris <jharris@30doradus.org>
4     SPDX-FileCopyrightText: Prakash Mohan <prakash.mohan@kdemail.net>
5     SPDX-FileCopyrightText: Akarsh Simha <akarsh@kde.org>
6 
7     SPDX-License-Identifier: GPL-2.0-or-later
8 */
9 
10 #include "observinglist.h"
11 
12 #include "config-kstars.h"
13 
14 #include "constellationboundarylines.h"
15 #include "fov.h"
16 #include "imageviewer.h"
17 #include "ksalmanac.h"
18 #include "ksnotification.h"
19 #include "ksdssdownloader.h"
20 #include "kspaths.h"
21 #include "kstars.h"
22 #include "kstarsdata.h"
23 #include "ksutils.h"
24 #include "obslistpopupmenu.h"
25 #include "obslistwizard.h"
26 #include "Options.h"
27 #include "sessionsortfilterproxymodel.h"
28 #include "skymap.h"
29 #include "thumbnailpicker.h"
30 #include "dialogs/detaildialog.h"
31 #include "dialogs/finddialog.h"
32 #include "dialogs/locationdialog.h"
33 #include "oal/execute.h"
34 #include "skycomponents/skymapcomposite.h"
35 #include "skyobjects/skyobject.h"
36 #include "skyobjects/starobject.h"
37 #include "tools/altvstime.h"
38 #include "tools/eyepiecefield.h"
39 #include "tools/wutdialog.h"
40 
41 #ifdef HAVE_INDI
42 #include <basedevice.h>
43 #include "indi/indilistener.h"
44 #include "indi/drivermanager.h"
45 #include "indi/driverinfo.h"
46 #include "ekos/manager.h"
47 #endif
48 
49 #include <KPlotting/KPlotAxis>
50 #include <KPlotting/KPlotObject>
51 #include <KMessageBox>
52 #include <QMessageBox>
53 
54 #include <kstars_debug.h>
55 
56 //
57 // ObservingListUI
58 // ---------------------------------
ObservingListUI(QWidget * p)59 ObservingListUI::ObservingListUI(QWidget *p) : QFrame(p)
60 {
61     setupUi(this);
62 }
63 
64 //
65 // ObservingList
66 // ---------------------------------
ObservingList()67 ObservingList::ObservingList()
68     : QDialog((QWidget *)KStars::Instance()), LogObject(nullptr), m_CurrentObject(nullptr), isModified(false), m_dl(nullptr),
69       m_manager{ CatalogsDB::dso_db_path() }
70 {
71 #ifdef Q_OS_OSX
72     setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
73 #endif
74     ui                      = new ObservingListUI(this);
75     QVBoxLayout *mainLayout = new QVBoxLayout;
76     mainLayout->addWidget(ui);
77     setWindowTitle(i18nc("@title:window", "Observation Planner"));
78 
79     setLayout(mainLayout);
80 
81     dt = KStarsDateTime::currentDateTime();
82     setFocusPolicy(Qt::StrongFocus);
83     geo            = KStarsData::Instance()->geo();
84     sessionView    = false;
85     m_listFileName = QString();
86     pmenu.reset(new ObsListPopupMenu());
87     //Set up the Table Views
88     m_WishListModel.reset(new QStandardItemModel(0, 5, this));
89     m_SessionModel.reset(new QStandardItemModel(0, 5));
90 
91     m_WishListModel->setHorizontalHeaderLabels(
92         QStringList() << i18n("Name") << i18n("Alternate Name") << i18nc("Right Ascension", "RA (J2000)")
93         << i18nc("Declination", "Dec (J2000)") << i18nc("Magnitude", "Mag") << i18n("Type")
94         << i18n("Current Altitude"));
95     m_SessionModel->setHorizontalHeaderLabels(
96         QStringList() << i18n("Name") << i18n("Alternate Name") << i18nc("Right Ascension", "RA (J2000)")
97         << i18nc("Declination", "Dec (J2000)") << i18nc("Magnitude", "Mag") << i18n("Type")
98         << i18nc("Constellation", "Constell.") << i18n("Time") << i18nc("Altitude", "Alt")
99         << i18nc("Azimuth", "Az"));
100 
101     m_WishListSortModel.reset(new QSortFilterProxyModel(this));
102     m_WishListSortModel->setSourceModel(m_WishListModel.get());
103     m_WishListSortModel->setDynamicSortFilter(true);
104     m_WishListSortModel->setSortRole(Qt::UserRole);
105     ui->WishListView->setModel(m_WishListSortModel.get());
106     ui->WishListView->horizontalHeader()->setStretchLastSection(true);
107 
108     ui->WishListView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
109     m_SessionSortModel.reset(new SessionSortFilterProxyModel());
110     m_SessionSortModel->setSourceModel(m_SessionModel.get());
111     m_SessionSortModel->setDynamicSortFilter(true);
112     ui->SessionView->setModel(m_SessionSortModel.get());
113     ui->SessionView->horizontalHeader()->setStretchLastSection(true);
114     ui->SessionView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
115     ksal.reset(new KSAlmanac);
116     ksal->setLocation(geo);
117     ui->avt->setGeoLocation(geo);
118     ui->avt->setSunRiseSetTimes(ksal->getSunRise(), ksal->getSunSet());
119     ui->avt->setLimits(-12.0, 12.0, -90.0, 90.0);
120     ui->avt->axis(KPlotWidget::BottomAxis)->setTickLabelFormat('t');
121     ui->avt->axis(KPlotWidget::BottomAxis)->setLabel(i18n("Local Time"));
122     ui->avt->axis(KPlotWidget::TopAxis)->setTickLabelFormat('t');
123     ui->avt->axis(KPlotWidget::TopAxis)->setTickLabelsShown(true);
124     ui->DateEdit->setDate(dt.date());
125     ui->SetLocation->setText(geo->fullName());
126     ui->ImagePreview->installEventFilter(this);
127     ui->WishListView->viewport()->installEventFilter(this);
128     ui->WishListView->installEventFilter(this);
129     ui->SessionView->viewport()->installEventFilter(this);
130     ui->SessionView->installEventFilter(this);
131     // setDefaultImage();
132     //Connections
133     connect(ui->WishListView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(slotCenterObject()));
134     connect(ui->WishListView->selectionModel(),
135             SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(slotNewSelection()));
136     connect(ui->SessionView->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
137             this, SLOT(slotNewSelection()));
138     connect(ui->WUTButton, SIGNAL(clicked()), this, SLOT(slotWUT()));
139     connect(ui->FindButton, SIGNAL(clicked()), this, SLOT(slotFind()));
140     connect(ui->OpenButton, SIGNAL(clicked()), this, SLOT(slotOpenList()));
141     connect(ui->SaveButton, SIGNAL(clicked()), this, SLOT(slotSaveSession()));
142     connect(ui->SaveAsButton, SIGNAL(clicked()), this, SLOT(slotSaveSessionAs()));
143     connect(ui->WizardButton, SIGNAL(clicked()), this, SLOT(slotWizard()));
144     connect(ui->batchAddButton, SIGNAL(clicked()), this, SLOT(slotBatchAdd()));
145     connect(ui->SetLocation, SIGNAL(clicked()), this, SLOT(slotLocation()));
146     connect(ui->Update, SIGNAL(clicked()), this, SLOT(slotUpdate()));
147     connect(ui->DeleteImage, SIGNAL(clicked()), this, SLOT(slotDeleteCurrentImage()));
148     connect(ui->SearchImage, SIGNAL(clicked()), this, SLOT(slotSearchImage()));
149     connect(ui->SetTime, SIGNAL(clicked()), this, SLOT(slotSetTime()));
150     connect(ui->tabWidget, SIGNAL(currentChanged(int)), this, SLOT(slotChangeTab(int)));
151     connect(ui->saveImages, SIGNAL(clicked()), this, SLOT(slotSaveAllImages()));
152     connect(ui->DeleteAllImages, SIGNAL(clicked()), this, SLOT(slotDeleteAllImages()));
153     connect(ui->OALExport, SIGNAL(clicked()), this, SLOT(slotOALExport()));
154     connect(ui->clearListB, SIGNAL(clicked()), this, SLOT(slotClearList()));
155     //Add icons to Push Buttons
156     ui->OpenButton->setIcon(QIcon::fromTheme("document-open"));
157     ui->OpenButton->setAttribute(Qt::WA_LayoutUsesWidgetRect);
158     ui->SaveButton->setIcon(QIcon::fromTheme("document-save"));
159     ui->SaveButton->setAttribute(Qt::WA_LayoutUsesWidgetRect);
160     ui->SaveAsButton->setIcon(
161         QIcon::fromTheme("document-save-as"));
162     ui->SaveAsButton->setAttribute(Qt::WA_LayoutUsesWidgetRect);
163     ui->WizardButton->setIcon(QIcon::fromTheme("tools-wizard"));
164     ui->WizardButton->setAttribute(Qt::WA_LayoutUsesWidgetRect);
165     noSelection = true;
166     showScope   = false;
167     ui->NotesEdit->setEnabled(false);
168     ui->SetTime->setEnabled(false);
169     ui->TimeEdit->setEnabled(false);
170     ui->SearchImage->setEnabled(false);
171     ui->saveImages->setEnabled(false);
172     ui->DeleteImage->setEnabled(false);
173     ui->OALExport->setEnabled(false);
174 
175     m_NoImagePixmap =
176         QPixmap(":/images/noimage.png")
177         .scaled(ui->ImagePreview->width(), ui->ImagePreview->height(), Qt::KeepAspectRatio, Qt::FastTransformation);
178     m_altCostHelper = [this](const SkyPoint & p) -> QStandardItem *
__anon587aeb9a0102(const SkyPoint & p) 179     {
180         const double inf = std::numeric_limits<double>::infinity();
181         double altCost   = 0.;
182         QString itemText;
183         double maxAlt = p.maxAlt(*(geo->lat()));
184         if (Options::obsListDemoteHole() && maxAlt > 90. - Options::obsListHoleSize())
185             maxAlt = 90. - Options::obsListHoleSize();
186         if (maxAlt <= 0.)
187         {
188             altCost  = -inf;
189             itemText = i18n("Never rises");
190         }
191         else
192         {
193             altCost = (p.alt().Degrees() / maxAlt) * 100.;
194             if (altCost < 0)
195                 itemText = i18nc("Short text to describe that object has not risen yet", "Not risen");
196             else
197             {
198                 if (altCost > 100.)
199                 {
200                     altCost  = -inf;
201                     itemText = i18nc("Object is in the Dobsonian hole", "In hole");
202                 }
203                 else
204                     itemText = QString::number(altCost, 'f', 0) + '%';
205             }
206         }
207 
208         QStandardItem *altItem = new QStandardItem(itemText);
209         altItem->setData(altCost, Qt::UserRole);
210         //        qCDebug(KSTARS) << "Updating altitude for " << p.ra().toHMSString() << " " << p.dec().toDMSString() << " alt = " << p.alt().toDMSString() << " info to " << itemText;
211         return altItem;
212     };
213 
214     // Needed to fix weird bug on Windows that started with Qt 5.9 that makes the title bar
215     // not visible and therefore dialog not movable.
216 #ifdef Q_OS_WIN
217     move(100, 100);
218 #endif
219 }
220 
showEvent(QShowEvent *)221 void ObservingList::showEvent(QShowEvent *)
222 {
223     // ONLY run for first ever load
224 
225     if (m_initialWishlistLoad == false)
226     {
227         m_initialWishlistLoad = true;
228 
229         slotLoadWishList(); //Load the wishlist from disk if present
230         m_CurrentObject = nullptr;
231         setSaveImagesButton();
232 
233         slotUpdateAltitudes();
234         m_altitudeUpdater = new QTimer(this);
235         connect(m_altitudeUpdater, SIGNAL(timeout()), this, SLOT(slotUpdateAltitudes()));
236         m_altitudeUpdater->start(120000); // update altitudes every 2 minutes
237     }
238 }
239 
240 //SLOTS
241 
slotAddObject(const SkyObject * _obj,bool session,bool update)242 void ObservingList::slotAddObject(const SkyObject *_obj, bool session, bool update)
243 {
244     bool addToWishList = true;
245     if (!_obj)
246         _obj = SkyMap::Instance()->clickedObject(); // Eh? Why? Weird default behavior.
247 
248     if (!_obj)
249     {
250         qCWarning(KSTARS) << "Trying to add null object to observing list! Ignoring.";
251         return;
252     }
253 
254     QString finalObjectName = getObjectName(_obj);
255 
256     if (finalObjectName.isEmpty())
257     {
258         KSNotification::sorry(i18n("Unnamed stars are not supported in the observing lists"));
259         return;
260     }
261 
262     //First, make sure object is not already in the list
263     QSharedPointer<SkyObject> obj = findObject(_obj);
264     if (obj)
265     {
266         addToWishList = false;
267         if (!session)
268         {
269             KStars::Instance()->statusBar()->showMessage(
270                 i18n("%1 is already in your wishlist.", finalObjectName),
271                 0); // FIXME: This message is too inconspicuous if using the Find dialog to add
272             return;
273         }
274     }
275     else
276     {
277         assert(!findObject(_obj, session));
278         qCDebug(KSTARS) << "Cloned object " << finalObjectName << " to add to observing list.";
279         obj = QSharedPointer<SkyObject>(
280                   _obj->clone()); // Use a clone in case the original SkyObject is deleted due to change in catalog configuration.
281     }
282 
283     if (session && sessionList().contains(obj))
284     {
285         KStars::Instance()->statusBar()->showMessage(i18n("%1 is already in the session plan.", finalObjectName), 0);
286         return;
287     }
288 
289     // JM: If we are loading observing list from disk, solar system objects magnitudes are not calculated until later
290     // Therefore, we manual invoke updateCoords to force computation of magnitude.
291     if ((obj->type() == SkyObject::COMET || obj->type() == SkyObject::ASTEROID || obj->type() == SkyObject::MOON ||
292             obj->type() == SkyObject::PLANET) &&
293             obj->mag() == 0)
294     {
295         KSNumbers num(dt.djd());
296         CachingDms LST = geo->GSTtoLST(dt.gst());
297         obj->updateCoords(&num, true, geo->lat(), &LST, true);
298     }
299 
300     QString smag = "--";
301     if (-30.0 < obj->mag() && obj->mag() < 90.0)
302         smag = QString::number(obj->mag(), 'f', 2); // The lower limit to avoid display of unrealistic comet magnitudes
303 
304     SkyPoint p = obj->recomputeHorizontalCoords(dt, geo);
305 
306     QList<QStandardItem *> itemList;
307 
308     auto getItemWithUserRole = [](const QString & itemText) -> QStandardItem *
309     {
310         QStandardItem *ret = new QStandardItem(itemText);
311         ret->setData(itemText, Qt::UserRole);
312         return ret;
313     };
314 
315     // Fill itemlist with items that are common to both wishlist additions and session plan additions
316     auto populateItemList = [&getItemWithUserRole, &itemList, &finalObjectName, obj, &p, &smag]()
317     {
318         itemList.clear();
319         QStandardItem *keyItem = getItemWithUserRole(finalObjectName);
320         keyItem->setData(QVariant::fromValue<void *>(static_cast<void *>(obj.data())), Qt::UserRole + 1);
321         itemList
322                 << keyItem // NOTE: The rest of the methods assume that the SkyObject pointer is available in the first column!
323                 << getItemWithUserRole(obj->translatedLongName()) << getItemWithUserRole(p.ra0().toHMSString())
324                 << getItemWithUserRole(p.dec0().toDMSString()) << getItemWithUserRole(smag)
325                 << getItemWithUserRole(obj->typeName());
326     };
327 
328     //Insert object in the Wish List
329     if (addToWishList)
330     {
331         m_WishList.append(obj);
332         m_CurrentObject = obj.data();
333 
334         //QString ra, dec;
335         //ra = "";//p.ra().toHMSString();
336         //dec = p.dec().toDMSString();
337 
338         populateItemList();
339         // FIXME: Instead sort by a "clever" observability score, calculated as follows:
340         //     - First sort by (max altitude) - (current altitude) rounded off to the nearest
341         //     - Weight by declination - latitude (in the northern hemisphere, southern objects get higher precedence)
342         //     - Demote objects in the hole
343         SkyPoint p = obj->recomputeHorizontalCoords(KStarsDateTime::currentDateTimeUtc(), geo); // Current => now
344         itemList << m_altCostHelper(p);
345         m_WishListModel->appendRow(itemList);
346 
347         //Note addition in statusbar
348         KStars::Instance()->statusBar()->showMessage(i18n("Added %1 to observing list.", finalObjectName), 0);
349         ui->WishListView->resizeColumnsToContents();
350         if (!update)
351             slotSaveList();
352     }
353     //Insert object in the Session List
354     if (session)
355     {
356         m_SessionList.append(obj);
357         dt.setTime(TimeHash.value(finalObjectName, obj->transitTime(dt, geo)));
358         dms lst(geo->GSTtoLST(dt.gst()));
359         p.EquatorialToHorizontal(&lst, geo->lat());
360 
361         QString alt = "--", az = "--";
362 
363         QStandardItem *BestTime = new QStandardItem();
364         /* QString ra, dec;
365          if(obj->name() == "star" ) {
366             ra = obj->ra0().toHMSString();
367             dec = obj->dec0().toDMSString();
368             BestTime->setData( QString( "--" ), Qt::DisplayRole );
369         }
370         else {*/
371         BestTime->setData(TimeHash.value(finalObjectName, obj->transitTime(dt, geo)), Qt::DisplayRole);
372         alt = p.alt().toDMSString();
373         az  = p.az().toDMSString();
374         //}
375         // TODO: Change the rest of the parameters to their appropriate datatypes.
376         populateItemList();
377         itemList << getItemWithUserRole(KSUtils::constNameToAbbrev(
378                                             KStarsData::Instance()->skyComposite()->constellationBoundary()->constellationName(obj.data())))
379                  << BestTime << getItemWithUserRole(alt) << getItemWithUserRole(az);
380 
381         m_SessionModel->appendRow(itemList);
382         //Adding an object should trigger the modified flag
383         isModified = true;
384         ui->SessionView->resizeColumnsToContents();
385         //Note addition in statusbar
386         KStars::Instance()->statusBar()->showMessage(i18n("Added %1 to session list.", finalObjectName), 0);
387         SkyMap::Instance()->forceUpdate();
388     }
389     setSaveImagesButton();
390 }
391 
slotRemoveObject(const SkyObject * _o,bool session,bool update)392 void ObservingList::slotRemoveObject(const SkyObject *_o, bool session, bool update)
393 {
394     if (!update) // EH?!
395     {
396         if (!_o)
397             _o = SkyMap::Instance()->clickedObject();
398         else if (sessionView) //else if is needed as clickedObject should not be removed from the session list.
399             session = true;
400     }
401 
402     // Is the pointer supplied in our own lists?
403     const QList<QSharedPointer<SkyObject>> &list = (session ? sessionList() : obsList());
404     QStandardItemModel *currentModel             = (session ? m_SessionModel.get() : m_WishListModel.get());
405 
406     QSharedPointer<SkyObject> o = findObject(_o, session);
407     if (!o)
408     {
409         qWarning() << "Object (name: " << getObjectName(o.data())
410                    << ") supplied to ObservingList::slotRemoveObject() was not found in the "
411                    << QString(session ? "session" : "observing") << " list!";
412         return;
413     }
414 
415     int k = list.indexOf(o);
416     assert(k >= 0);
417 
418     // Remove from hash
419     ImagePreviewHash.remove(o.data());
420 
421     if (o.data() == LogObject)
422         saveCurrentUserLog();
423 
424     //Remove row from the TableView model
425     // FIXME: Is there no faster way?
426     for (int irow = 0; irow < currentModel->rowCount(); ++irow)
427     {
428         QString name = currentModel->item(irow, 0)->text();
429         if (getObjectName(o.data()) == name)
430         {
431             currentModel->removeRow(irow);
432             break;
433         }
434     }
435 
436     if (!session)
437     {
438         obsList().removeAt(k);
439         ui->avt->removeAllPlotObjects();
440         ui->WishListView->resizeColumnsToContents();
441         if (!update)
442             slotSaveList();
443     }
444     else
445     {
446         if (!update)
447             TimeHash.remove(o->name());
448         sessionList().removeAt(k); //Remove from the session list
449         isModified = true;         //Removing an object should trigger the modified flag
450         ui->avt->removeAllPlotObjects();
451         ui->SessionView->resizeColumnsToContents();
452         SkyMap::Instance()->forceUpdate();
453     }
454 }
455 
slotRemoveSelectedObjects()456 void ObservingList::slotRemoveSelectedObjects()
457 {
458     //Find each object by name in the session list, and remove it
459     //Go backwards so item alignment doesn't get screwed up as rows are removed.
460     for (int irow = getActiveModel()->rowCount() - 1; irow >= 0; --irow)
461     {
462         bool rowSelected;
463         if (sessionView)
464             rowSelected = ui->SessionView->selectionModel()->isRowSelected(irow, QModelIndex());
465         else
466             rowSelected = ui->WishListView->selectionModel()->isRowSelected(irow, QModelIndex());
467 
468         if (rowSelected)
469         {
470             QModelIndex sortIndex, index;
471             sortIndex    = getActiveSortModel()->index(irow, 0);
472             index        = getActiveSortModel()->mapToSource(sortIndex);
473             SkyObject *o = static_cast<SkyObject *>(index.data(Qt::UserRole + 1).value<void *>());
474             Q_ASSERT(o);
475             slotRemoveObject(o, sessionView);
476         }
477     }
478 
479     if (sessionView)
480     {
481         //we've removed all selected objects, so clear the selection
482         ui->SessionView->selectionModel()->clear();
483         //Update the lists in the Execute window as well
484         KStarsData::Instance()->executeSession()->init();
485     }
486 
487     setSaveImagesButton();
488     ui->ImagePreview->setCursor(Qt::ArrowCursor);
489 }
490 
slotNewSelection()491 void ObservingList::slotNewSelection()
492 {
493     bool found      = false;
494     singleSelection = false;
495     noSelection     = false;
496     showScope       = false;
497     //ui->ImagePreview->clearPreview();
498     //ui->ImagePreview->setPixmap(QPixmap());
499     ui->ImagePreview->setCursor(Qt::ArrowCursor);
500     QModelIndexList selectedItems;
501     QString newName;
502     QSharedPointer<SkyObject> o;
503     QString labelText;
504     ui->DeleteImage->setEnabled(false);
505 
506     selectedItems =
507         getActiveSortModel()->mapSelectionToSource(getActiveView()->selectionModel()->selection()).indexes();
508 
509     if (selectedItems.size() == getActiveModel()->columnCount())
510     {
511         newName         = selectedItems[0].data().toString();
512         singleSelection = true;
513         //Find the selected object in the SessionList,
514         //then break the loop.  Now SessionList.current()
515         //points to the new selected object (until now it was the previous object)
516         for (auto &o_temp : getActiveList())
517         {
518             if (getObjectName(o_temp.data()) == newName)
519             {
520                 o = o_temp;
521                 found = true;
522                 break;
523             }
524         }
525     }
526 
527     if (singleSelection)
528     {
529         //Enable buttons
530         ui->ImagePreview->setCursor(Qt::PointingHandCursor);
531 #ifdef HAVE_INDI
532         showScope = true;
533 #endif
534         if (found)
535         {
536             m_CurrentObject = o.data();
537             //QPoint pos(0,0);
538             plot(o.data());
539             //Change the m_currentImageFileName, DSS/SDSS Url to correspond to the new object
540             setCurrentImage(o.data());
541             ui->SearchImage->setEnabled(true);
542             if (newName != i18n("star"))
543             {
544                 //Display the current object's user notes in the NotesEdit
545                 //First, save the last object's user log to disk, if necessary
546                 saveCurrentUserLog(); //uses LogObject, which is still the previous obj.
547                 //set LogObject to the new selected object
548                 LogObject = currentObject();
549                 ui->NotesEdit->setEnabled(true);
550 
551                 const auto &userLog =
552                   KStarsData::Instance()->getUserData(LogObject->name()).userLog;
553 
554                 if (userLog.isEmpty())
555                 {
556                     ui->NotesEdit->setPlainText(
557                         i18n("Record here observation logs and/or data on %1.", getObjectName(LogObject)));
558                 }
559                 else
560                 {
561                     ui->NotesEdit->setPlainText(userLog);
562                 }
563                 if (sessionView)
564                 {
565                     ui->TimeEdit->setEnabled(true);
566                     ui->SetTime->setEnabled(true);
567                     ui->TimeEdit->setTime(TimeHash.value(o->name(), o->transitTime(dt, geo)));
568                 }
569             }
570             else //selected object is named "star"
571             {
572                 //clear the log text box
573                 saveCurrentUserLog();
574                 ui->NotesEdit->clear();
575                 ui->NotesEdit->setEnabled(false);
576                 ui->SearchImage->setEnabled(false);
577             }
578             QString ImagePath = KSPaths::locate(QStandardPaths::AppDataLocation, m_currentImageFileName);
579             if (!ImagePath.isEmpty())
580             {
581                 //If the image is present, show it!
582                 KSDssImage ksdi(ImagePath);
583                 KSDssImage::Metadata md = ksdi.getMetadata();
584                 //ui->ImagePreview->showPreview( QUrl::fromLocalFile( ksdi.getFileName() ) );
585                 if (ImagePreviewHash.contains(o.data()) == false)
586                     ImagePreviewHash[o.data()] = QPixmap(ksdi.getFileName()).scaledToHeight(ui->ImagePreview->width());
587 
588                 //ui->ImagePreview->setPixmap(QPixmap(ksdi.getFileName()).scaledToHeight(ui->ImagePreview->width()));
589                 ui->ImagePreview->setPixmap(ImagePreviewHash[o.data()]);
590                 if (md.isValid())
591                 {
592                     ui->dssMetadataLabel->setText(
593                         i18n("DSS Image metadata: \n Size: %1\' x %2\' \n Photometric band: %3 \n Version: %4",
594                              QString::number(md.width), QString::number(md.height), QString() + md.band, md.version));
595                 }
596                 else
597                     ui->dssMetadataLabel->setText(i18n("No image info available."));
598                 ui->ImagePreview->show();
599                 ui->DeleteImage->setEnabled(true);
600             }
601             else
602             {
603                 setDefaultImage();
604                 ui->dssMetadataLabel->setText(
605                     i18n("No image available. Click on the placeholder image to download one."));
606             }
607             QString cname =
608                 KStarsData::Instance()->skyComposite()->constellationBoundary()->constellationName(o.data());
609             if (o->type() != SkyObject::CONSTELLATION)
610             {
611                 labelText = "<b>";
612                 if (o->type() == SkyObject::PLANET)
613                     labelText += o->translatedName();
614                 else
615                     labelText += o->name();
616                 if (std::isfinite(o->mag()) && o->mag() <= 30.)
617                     labelText += ":</b> " + i18nc("%1 magnitude of object, %2 type of sky object (planet, asteroid "
618                                                   "etc), %3 name of a constellation",
619                                                   "%1 mag %2 in %3", o->mag(), o->typeName().toLower(), cname);
620                 else
621                     labelText +=
622                         ":</b> " + i18nc("%1 type of sky object (planet, asteroid etc), %2 name of a constellation",
623                                          "%1 in %2", o->typeName(), cname);
624             }
625         }
626         else
627         {
628             setDefaultImage();
629             qCWarning(KSTARS) << "Object " << newName << " not found in list.";
630         }
631         ui->quickInfoLabel->setText(labelText);
632     }
633     else
634     {
635         if (selectedItems.isEmpty()) //Nothing selected
636         {
637             //Disable buttons
638             noSelection = true;
639             ui->NotesEdit->setEnabled(false);
640             m_CurrentObject = nullptr;
641             ui->TimeEdit->setEnabled(false);
642             ui->SetTime->setEnabled(false);
643             ui->SearchImage->setEnabled(false);
644             //Clear the user log text box.
645             saveCurrentUserLog();
646             ui->NotesEdit->setPlainText("");
647             //Clear the plot in the AVTPlotwidget
648             ui->avt->removeAllPlotObjects();
649         }
650         else //more than one object selected.
651         {
652             ui->NotesEdit->setEnabled(false);
653             ui->TimeEdit->setEnabled(false);
654             ui->SetTime->setEnabled(false);
655             ui->SearchImage->setEnabled(false);
656             m_CurrentObject = nullptr;
657             //Clear the plot in the AVTPlotwidget
658             ui->avt->removeAllPlotObjects();
659             //Clear the user log text box.
660             saveCurrentUserLog();
661             ui->NotesEdit->setPlainText("");
662             ui->quickInfoLabel->setText(QString());
663         }
664     }
665 }
666 
slotCenterObject()667 void ObservingList::slotCenterObject()
668 {
669     if (getSelectedItems().size() == 1)
670     {
671         SkyMap::Instance()->setClickedObject(currentObject());
672         SkyMap::Instance()->setClickedPoint(currentObject());
673         SkyMap::Instance()->slotCenter();
674     }
675 }
676 
slotSlewToObject()677 void ObservingList::slotSlewToObject()
678 {
679 #ifdef HAVE_INDI
680 
681     if (INDIListener::Instance()->size() == 0)
682     {
683         KSNotification::sorry(i18n("KStars did not find any active telescopes."));
684         return;
685     }
686 
687     foreach (ISD::GDInterface *gd, INDIListener::Instance()->getDevices())
688     {
689         INDI::BaseDevice *bd = gd->getBaseDevice();
690 
691         if (gd->getType() != KSTARS_TELESCOPE)
692             continue;
693 
694         if (bd == nullptr)
695             continue;
696 
697         if (bd->isConnected() == false)
698         {
699             KSNotification::error(
700                 i18n("Telescope %1 is offline. Please connect and retry again.", gd->getDeviceName()));
701             return;
702         }
703 
704         ISD::GDSetCommand SlewCMD(INDI_SWITCH, "ON_COORD_SET", "TRACK", ISS_ON, this);
705 
706         gd->setProperty(&SlewCMD);
707         gd->runCommand(INDI_SEND_COORDS, currentObject());
708 
709         return;
710     }
711 
712     KSNotification::sorry(i18n("KStars did not find any active telescopes."));
713 
714 #endif
715 }
716 
slotAddToEkosScheduler()717 void ObservingList::slotAddToEkosScheduler()
718 {
719 #ifdef HAVE_INDI
720     Ekos::Manager::Instance()->addObjectToScheduler(currentObject());
721 #endif
722 }
723 
724 //FIXME: This will open multiple Detail windows for each object;
725 //Should have one window whose target object changes with selection
slotDetails()726 void ObservingList::slotDetails()
727 {
728     if (currentObject())
729     {
730         QPointer<DetailDialog> dd =
731             new DetailDialog(currentObject(), KStarsData::Instance()->ut(), geo, KStars::Instance());
732         dd->exec();
733         delete dd;
734     }
735 }
736 
slotWUT()737 void ObservingList::slotWUT()
738 {
739     KStarsDateTime lt = dt;
740     lt.setTime(QTime(8, 0, 0));
741     QPointer<WUTDialog> w = new WUTDialog(KStars::Instance(), sessionView, geo, lt);
742     w->exec();
743     delete w;
744 }
745 
slotAddToSession()746 void ObservingList::slotAddToSession()
747 {
748     Q_ASSERT(!sessionView);
749     if (getSelectedItems().size())
750     {
751         foreach (const QModelIndex &i, getSelectedItems())
752         {
753             foreach (QSharedPointer<SkyObject> o, obsList())
754                 if (getObjectName(o.data()) == i.data().toString())
755                     slotAddObject(
756                         o.data(),
757                         true); // FIXME: Would be good to have a wrapper that accepts QSharedPointer<SkyObject>
758         }
759     }
760 }
761 
slotFind()762 void ObservingList::slotFind()
763 {
764     if (FindDialog::Instance()->exec() == QDialog::Accepted)
765     {
766         SkyObject *o = FindDialog::Instance()->targetObject();
767         if (o != nullptr)
768         {
769             slotAddObject(o, sessionView);
770         }
771     }
772 }
773 
slotBatchAdd()774 void ObservingList::slotBatchAdd()
775 {
776     bool accepted = false;
777     QString items = QInputDialog::getMultiLineText(this,
778                     sessionView ? i18n("Batch add to observing session") : i18n("Batch add to observing wishlist"),
779                     i18n("Specify a list of objects with one object on each line to add. The names must be understood to KStars, or if the internet resolver is enabled in settings, to the CDS Sesame resolver. Objects that are internet resolved will be added to the database."),
780                     QString(),
781                     &accepted);
782     bool resolve = Options::resolveNamesOnline();
783 
784     if (accepted && !items.isEmpty())
785     {
786         QStringList failedObjects;
787         QStringList objectNames = items.split("\n");
788         for (QString objectName : objectNames)
789         {
790             objectName = FindDialog::processSearchText(objectName);
791             SkyObject *object = KStarsData::Instance()->objectNamed(objectName);
792             if (!object && resolve)
793             {
794                 object = FindDialog::resolveAndAdd(m_manager, objectName);
795             }
796             if (!object)
797             {
798                 failedObjects.append(objectName);
799             }
800             else
801             {
802                 slotAddObject(object, sessionView);
803             }
804         }
805 
806         if (!failedObjects.isEmpty())
807         {
808             QMessageBox msgBox =
809             {
810                 QMessageBox::Icon::Warning,
811                 i18np("Batch add: %1 object not found", "Batch add: %1 objects not found", failedObjects.size()),
812                 i18np("%1 object could not be found in the database or resolved, and hence could not be added. See the details for more.",
813                       "%1 objects could not be found in the database or resolved, and hence could not be added. See the details for more.",
814                       failedObjects.size()),
815                 QMessageBox::Ok,
816                 this
817             };
818             msgBox.setDetailedText(failedObjects.join("\n"));
819             msgBox.exec();
820         }
821     }
822     Q_ASSERT(false); // Not implemented
823 }
824 
slotEyepieceView()825 void ObservingList::slotEyepieceView()
826 {
827     KStars::Instance()->slotEyepieceView(currentObject(), getCurrentImagePath());
828 }
829 
slotAVT()830 void ObservingList::slotAVT()
831 {
832     QModelIndexList selectedItems;
833     // TODO: Think and see if there's a more efficient way to do this. I can't seem to think of any, but this code looks like it could be improved. - Akarsh
834     selectedItems =
835         (sessionView ?
836          m_SessionSortModel->mapSelectionToSource(ui->SessionView->selectionModel()->selection()).indexes() :
837          m_WishListSortModel->mapSelectionToSource(ui->WishListView->selectionModel()->selection()).indexes());
838 
839     if (selectedItems.size())
840     {
841         QPointer<AltVsTime> avt = new AltVsTime(KStars::Instance());
842         foreach (const QModelIndex &i, selectedItems)
843         {
844             if (i.column() == 0)
845             {
846                 SkyObject *o = static_cast<SkyObject *>(i.data(Qt::UserRole + 1).value<void *>());
847                 Q_ASSERT(o);
848                 avt->processObject(o);
849             }
850         }
851         avt->exec();
852         delete avt;
853     }
854 }
855 
856 //FIXME: On close, we will need to close any open Details/AVT windows
slotClose()857 void ObservingList::slotClose()
858 {
859     //Save the current User log text
860     saveCurrentUserLog();
861     ui->avt->removeAllPlotObjects();
862     slotNewSelection();
863     saveCurrentList();
864     hide();
865 }
866 
saveCurrentUserLog()867 void ObservingList::saveCurrentUserLog()
868 {
869     if (LogObject && !ui->NotesEdit->toPlainText().isEmpty() &&
870             ui->NotesEdit->toPlainText() !=
871             i18n("Record here observation logs and/or data on %1.", getObjectName(LogObject)))
872     {
873         const auto &success = KStarsData::Instance()->updateUserLog(
874             LogObject->name(), ui->NotesEdit->toPlainText());
875 
876         if (!success.first)
877             KSNotification::sorry(success.second, i18n("Could not update the user log."));
878 
879         ui->NotesEdit->clear();
880         LogObject = nullptr;
881     }
882 }
883 
slotOpenList()884 void ObservingList::slotOpenList()
885 {
886     QUrl fileURL = QFileDialog::getOpenFileUrl(KStars::Instance(), i18nc("@title:window", "Open Observing List"), QUrl(),
887                    "KStars Observing List (*.obslist)");
888     QFile f;
889 
890     if (fileURL.isValid())
891     {
892         f.setFileName(fileURL.toLocalFile());
893         //FIXME do we still need to do this?
894         /*
895         if ( ! fileURL.isLocalFile() ) {
896             //Save remote list to a temporary local file
897             QTemporaryFile tmpfile;
898             tmpfile.setAutoRemove(false);
899             tmpfile.open();
900             m_listFileName = tmpfile.fileName();
901             if( KIO::NetAccess::download( fileURL, m_listFileName, this ) )
902                 f.setFileName( m_listFileName );
903 
904         } else {
905             m_listFileName = fileURL.toLocalFile();
906             f.setFileName( m_listFileName );
907         }
908         */
909 
910         if (!f.open(QIODevice::ReadOnly))
911         {
912             QString message = i18n("Could not open file %1", f.fileName());
913             KSNotification::sorry(message, i18n("Could Not Open File"));
914             return;
915         }
916         saveCurrentList(); //See if the current list needs to be saved before opening the new one
917         ui->tabWidget->setCurrentIndex(1); // FIXME: This is not robust -- asimha
918         slotChangeTab(1);
919 
920         sessionList().clear();
921         TimeHash.clear();
922         m_CurrentObject = nullptr;
923         m_SessionModel->removeRows(0, m_SessionModel->rowCount());
924         SkyMap::Instance()->forceUpdate();
925         //First line is the name of the list. The rest of the file is
926         //object names, one per line. With the TimeHash value if present
927         QTextStream istream(&f);
928         QString input;
929         input = istream.readAll();
930         OAL::Log logObject;
931         logObject.readBegin(input);
932         //Set the New TimeHash
933         TimeHash = logObject.timeHash();
934         GeoLocation *geo_new = logObject.geoLocation();
935         if (!geo_new)
936         {
937             // FIXME: This is a very hackish solution -- if we
938             // encounter an invalid XML file, we know we won't read a
939             // GeoLocation successfully. It does not detect partially
940             // corrupt files. -- asimha
941             KSNotification::sorry(i18n("The specified file is invalid. We expect an XML file based on the OpenAstronomyLog schema."));
942             f.close();
943             return;
944         }
945         dt = logObject.dateTime();
946         //foreach (SkyObject *o, *(logObject.targetList()))
947         for (auto &o : logObject.targetList())
948             slotAddObject(o.data(), true);
949         //Update the location and user set times from file
950         slotUpdate();
951         //Newly-opened list should not trigger isModified flag
952         isModified = false;
953         f.close();
954     }
955     else if (!fileURL.toLocalFile().isEmpty())
956     {
957         KSNotification::sorry(i18n("The specified file is invalid"));
958     }
959 }
960 
slotClearList()961 void ObservingList::slotClearList()
962 {
963     if ((ui->tabWidget->currentIndex() == 0 && obsList().isEmpty()) ||
964             (ui->tabWidget->currentIndex() == 1 && sessionList().isEmpty()))
965         return;
966 
967     QString message = i18n("Are you sure you want to clear all objects?");
968     if (KMessageBox::questionYesNo(this, message, i18n("Clear all?")) == KMessageBox::Yes)
969     {
970         // Did I forget anything else to remove?
971         ui->avt->removeAllPlotObjects();
972         m_CurrentObject = LogObject = nullptr;
973 
974         if (ui->tabWidget->currentIndex() == 0)
975         {
976             // IMPORTANT: Is this enough or we will have dangling pointers in memory?
977             ImagePreviewHash.clear();
978             obsList().clear();
979             m_WishListModel->setRowCount(0);
980         }
981         else
982         {
983             // IMPORTANT: Is this enough or we will have dangling pointers in memory?
984             sessionList().clear();
985             TimeHash.clear();
986             isModified = true; //Removing an object should trigger the modified flag
987             m_SessionModel->setRowCount(0);
988             SkyMap::Instance()->forceUpdate();
989         }
990     }
991 }
992 
saveCurrentList()993 void ObservingList::saveCurrentList()
994 {
995     //Before loading a new list, do we need to save the current one?
996     //Assume that if the list is empty, then there's no need to save
997     if (sessionList().size())
998     {
999         if (isModified)
1000         {
1001             QString message = i18n("Do you want to save the current session?");
1002             if (KMessageBox::questionYesNo(this, message, i18n("Save Current session?"), KStandardGuiItem::save(),
1003                                            KStandardGuiItem::discard()) == KMessageBox::Yes)
1004                 slotSaveSession();
1005         }
1006     }
1007 }
1008 
slotSaveSessionAs(bool nativeSave)1009 void ObservingList::slotSaveSessionAs(bool nativeSave)
1010 {
1011     if (sessionList().isEmpty())
1012         return;
1013 
1014     QUrl fileURL = QFileDialog::getSaveFileUrl(KStars::Instance(), i18nc("@title:window", "Save Observing List"), QUrl(),
1015                    "KStars Observing List (*.obslist)");
1016     if (fileURL.isValid())
1017     {
1018         m_listFileName = fileURL.toLocalFile();
1019         slotSaveSession(nativeSave);
1020     }
1021 }
1022 
slotSaveList()1023 void ObservingList::slotSaveList()
1024 {
1025     QFile f;
1026     // FIXME: Move wishlist into a database.
1027     // TODO: Support multiple wishlists.
1028 
1029     QString fileContents;
1030     QTextStream ostream(
1031         &fileContents); // We first write to a QString to prevent truncating the file in case there is a crash.
1032     foreach (const QSharedPointer<SkyObject> o, obsList())
1033     {
1034         if (!o)
1035         {
1036             qWarning() << "Null entry in observing wishlist! Skipping!";
1037             continue;
1038         }
1039         if (o->name() == "star")
1040         {
1041             //ostream << o->name() << "  " << o->ra0().Hours() << "  " << o->dec0().Degrees() << endl;
1042             ostream << getObjectName(o.data(), false) << '\n';
1043         }
1044         else if (o->type() == SkyObject::STAR)
1045         {
1046             Q_ASSERT(dynamic_cast<const StarObject *>(o.data()));
1047             const QSharedPointer<StarObject> s = qSharedPointerCast<StarObject>(o);
1048             if (s->name() == s->gname())
1049                 ostream << s->name2() << '\n';
1050             else
1051                 ostream << s->name() << '\n';
1052         }
1053         else
1054         {
1055             ostream << o->name() << '\n';
1056         }
1057     }
1058     f.setFileName(QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("wishlist.obslist"));
1059     if (!f.open(QIODevice::WriteOnly))
1060     {
1061         qWarning() << "Cannot save wish list to file!"; // TODO: This should be presented as a message box to the user
1062         KMessageBox::error(this, i18n("Could not open the observing wishlist file %1 for writing. Your wishlist changes will not be saved. Check if the location is writable and not full.", f.fileName()), i18n("Could not save observing wishlist"));
1063         return;
1064     }
1065     QTextStream writeemall(&f);
1066     writeemall << fileContents;
1067     f.close();
1068 }
1069 
slotLoadWishList()1070 void ObservingList::slotLoadWishList()
1071 {
1072     QFile f;
1073     f.setFileName(QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath("wishlist.obslist"));
1074     if (!f.open(QIODevice::ReadOnly))
1075     {
1076         qWarning(KSTARS) << "No WishList Saved yet";
1077         return;
1078     }
1079     QTextStream istream(&f);
1080     QString line;
1081 
1082     QPointer<QProgressDialog> addingObjectsProgress = new QProgressDialog();
1083     addingObjectsProgress->setWindowTitle(i18nc("@title:window", "Observing List Wizard"));
1084     addingObjectsProgress->setLabelText(i18n("Please wait while loading objects..."));
1085     addingObjectsProgress->setMaximum(0);
1086     addingObjectsProgress->setMinimum(0);
1087     addingObjectsProgress->show();
1088 
1089     QStringList failedObjects;
1090 
1091     while (!istream.atEnd())
1092     {
1093         line = istream.readLine();
1094         //If the object is named "star", add it by coordinates
1095         SkyObject *o;
1096         /*if ( line.startsWith( QLatin1String( "star" ) ) ) {
1097             QStringList fields = line.split( ' ', QString::SkipEmptyParts );
1098             dms ra = dms::fromString( fields[1], false ); //false = hours
1099             dms dc = dms::fromString( fields[2], true );  //true  = degrees
1100             SkyPoint p( ra, dc );
1101             double maxrad = 1000.0/Options::zoomFactor();
1102             o = ks->data()->skyComposite()->starNearest( &p, maxrad );
1103         }
1104         else {*/
1105         o = KStarsData::Instance()->objectNamed(line);
1106         //}
1107         //If we haven't identified the object, try interpreting the
1108         //name as a star's genetive name (with ascii letters)
1109         if (!o)
1110             o = KStarsData::Instance()->skyComposite()->findStarByGenetiveName(line);
1111         if (o)
1112             slotAddObject(o, false, true);
1113         else
1114             failedObjects.append(line);
1115 
1116         if (addingObjectsProgress->wasCanceled())
1117             break;
1118         qApp->processEvents();
1119     }
1120     delete (addingObjectsProgress);
1121     f.close();
1122 
1123     if (!failedObjects.isEmpty())
1124     {
1125         QMessageBox msgBox = {QMessageBox::Icon::Warning,
1126                               i18np("Observing wishlist truncated: %1 object not found", "Observing wishlist truncated: %1 objects not found", failedObjects.size()),
1127                               i18np("%1 object could not be found in the database, and will be removed from the observing wish list. We recommend that you copy the detailed list as a backup.", "%1 objects could not be found in the database, and will be removed from the observing wish list. We recommend that you copy the detailed list as a backup.", failedObjects.size()),
1128                               QMessageBox::Ok,
1129                               this
1130                              };
1131         msgBox.setDetailedText(failedObjects.join("\n"));
1132         msgBox.exec();
1133     }
1134 }
1135 
slotSaveSession(bool nativeSave)1136 void ObservingList::slotSaveSession(bool nativeSave)
1137 {
1138     if (sessionList().isEmpty())
1139     {
1140         KSNotification::error(i18n("Cannot save an empty session list."));
1141         return;
1142     }
1143 
1144     if (m_listFileName.isEmpty())
1145     {
1146         slotSaveSessionAs(nativeSave);
1147         return;
1148     }
1149     QFile f(m_listFileName);
1150     if (!f.open(QIODevice::WriteOnly))
1151     {
1152         QString message = i18n("Could not open file %1.  Try a different filename?", f.fileName());
1153         if (KMessageBox::warningYesNo(nullptr, message, i18n("Could Not Open File"), KGuiItem(i18n("Try Different")),
1154                                       KGuiItem(i18n("Do Not Try"))) == KMessageBox::Yes)
1155         {
1156             m_listFileName.clear();
1157             slotSaveSessionAs(nativeSave);
1158         }
1159         return;
1160     }
1161     QTextStream ostream(&f);
1162     OAL::Log log;
1163     ostream << log.writeLog(nativeSave);
1164     f.close();
1165     isModified = false; //We've saved the session, so reset the modified flag.
1166 }
1167 
slotWizard()1168 void ObservingList::slotWizard()
1169 {
1170     QPointer<ObsListWizard> wizard = new ObsListWizard(KStars::Instance());
1171     if (wizard->exec() == QDialog::Accepted)
1172     {
1173         QPointer<QProgressDialog> addingObjectsProgress = new QProgressDialog();
1174         addingObjectsProgress->setWindowTitle(i18nc("@title:window", "Observing List Wizard"));
1175         addingObjectsProgress->setLabelText(i18n("Please wait while adding objects..."));
1176         addingObjectsProgress->setMaximum(wizard->obsList().size());
1177         addingObjectsProgress->setMinimum(0);
1178         addingObjectsProgress->setValue(0);
1179         addingObjectsProgress->show();
1180         int counter = 1;
1181         foreach (SkyObject *o, wizard->obsList())
1182         {
1183             slotAddObject(o);
1184             addingObjectsProgress->setValue(counter++);
1185             if (addingObjectsProgress->wasCanceled())
1186                 break;
1187             qApp->processEvents();
1188         }
1189         delete addingObjectsProgress;
1190     }
1191 
1192     delete wizard;
1193 }
1194 
plot(SkyObject * o)1195 void ObservingList::plot(SkyObject *o)
1196 {
1197     if (!o)
1198         return;
1199     float DayOffset = 0;
1200     if (TimeHash.value(o->name(), o->transitTime(dt, geo)).hour() > 12)
1201         DayOffset = 1;
1202 
1203     QDateTime midnight = QDateTime(dt.date(), QTime());
1204     KStarsDateTime ut  = geo->LTtoUT(KStarsDateTime(midnight));
1205     double h1          = geo->GSTtoLST(ut.gst()).Hours();
1206     if (h1 > 12.0)
1207         h1 -= 24.0;
1208 
1209     ui->avt->setSecondaryLimits(h1, h1 + 24.0, -90.0, 90.0);
1210     ksal->setLocation(geo);
1211     ksal->setDate(ut);
1212     ui->avt->setGeoLocation(geo);
1213     ui->avt->setSunRiseSetTimes(ksal->getSunRise(), ksal->getSunSet());
1214     ui->avt->setDawnDuskTimes(ksal->getDawnAstronomicalTwilight(), ksal->getDuskAstronomicalTwilight());
1215     ui->avt->setMinMaxSunAlt(ksal->getSunMinAlt(), ksal->getSunMaxAlt());
1216     ui->avt->setMoonRiseSetTimes(ksal->getMoonRise(), ksal->getMoonSet());
1217     ui->avt->setMoonIllum(ksal->getMoonIllum());
1218     ui->avt->update();
1219     KPlotObject *po = new KPlotObject(Qt::white, KPlotObject::Lines, 2.0);
1220     for (double h = -12.0; h <= 12.0; h += 0.5)
1221     {
1222         po->addPoint(h, findAltitude(o, (h + DayOffset * 24.0)));
1223     }
1224     ui->avt->removeAllPlotObjects();
1225     ui->avt->addPlotObject(po);
1226 }
1227 
findAltitude(SkyPoint * p,double hour)1228 double ObservingList::findAltitude(SkyPoint *p, double hour)
1229 {
1230     // Jasem 2015-09-05 Using correct procedure to find altitude
1231     SkyPoint sp                   = *p; // make a copy
1232     QDateTime midnight            = QDateTime(dt.date(), QTime());
1233     KStarsDateTime ut             = geo->LTtoUT(KStarsDateTime(midnight));
1234     KStarsDateTime targetDateTime = ut.addSecs(hour * 3600.0);
1235     dms LST                       = geo->GSTtoLST(targetDateTime.gst());
1236     sp.EquatorialToHorizontal(&LST, geo->lat());
1237     return sp.alt().Degrees();
1238 }
1239 
slotChangeTab(int index)1240 void ObservingList::slotChangeTab(int index)
1241 {
1242     noSelection = true;
1243     saveCurrentUserLog();
1244     ui->NotesEdit->setEnabled(false);
1245     ui->TimeEdit->setEnabled(false);
1246     ui->SetTime->setEnabled(false);
1247     ui->SearchImage->setEnabled(false);
1248     ui->DeleteImage->setEnabled(false);
1249     m_CurrentObject = nullptr;
1250     sessionView     = index != 0;
1251     setSaveImagesButton();
1252     ui->WizardButton->setEnabled(!sessionView); //wizard adds only to the Wish List
1253     ui->OALExport->setEnabled(sessionView);
1254     //Clear the selection in the Tables
1255     ui->WishListView->clearSelection();
1256     ui->SessionView->clearSelection();
1257     //Clear the user log text box.
1258     saveCurrentUserLog();
1259     ui->NotesEdit->setPlainText("");
1260     ui->avt->removeAllPlotObjects();
1261 }
1262 
slotLocation()1263 void ObservingList::slotLocation()
1264 {
1265     QPointer<LocationDialog> ld = new LocationDialog(this);
1266     if (ld->exec() == QDialog::Accepted)
1267     {
1268         geo = ld->selectedCity();
1269         ui->SetLocation->setText(geo->fullName());
1270     }
1271     delete ld;
1272 }
1273 
slotUpdate()1274 void ObservingList::slotUpdate()
1275 {
1276     dt.setDate(ui->DateEdit->date());
1277     ui->avt->removeAllPlotObjects();
1278     //Creating a copy of the lists, we can't use the original lists as they'll keep getting modified as the loop iterates
1279     QList<QSharedPointer<SkyObject>> _obsList = m_WishList, _SessionList = m_SessionList;
1280 
1281     for (QSharedPointer<SkyObject> &o : _obsList)
1282     {
1283         if (o->name() != "star")
1284         {
1285             slotRemoveObject(o.data(), false, true);
1286             slotAddObject(o.data(), false, true);
1287         }
1288     }
1289     for (QSharedPointer<SkyObject> &obj : _SessionList)
1290     {
1291         if (obj->name() != "star")
1292         {
1293             slotRemoveObject(obj.data(), true, true);
1294             slotAddObject(obj.data(), true, true);
1295         }
1296     }
1297     SkyMap::Instance()->forceUpdate();
1298 }
1299 
slotSetTime()1300 void ObservingList::slotSetTime()
1301 {
1302     SkyObject *o = currentObject();
1303     slotRemoveObject(o, true);
1304     TimeHash[o->name()] = ui->TimeEdit->time();
1305     slotAddObject(o, true, true);
1306 }
1307 
slotCustomDSS()1308 void ObservingList::slotCustomDSS()
1309 {
1310     ui->SearchImage->setEnabled(false);
1311     //ui->ImagePreview->clearPreview();
1312     ui->ImagePreview->setPixmap(QPixmap());
1313 
1314     KSDssImage::Metadata md;
1315     bool ok = true;
1316 
1317     int width  = QInputDialog::getInt(this, i18n("Customized DSS Download"), i18n("Specify image width (arcminutes): "),
1318                                       15, 15, 75, 1, &ok);
1319     int height = QInputDialog::getInt(this, i18n("Customized DSS Download"),
1320                                       i18n("Specify image height (arcminutes): "), 15, 15, 75, 1, &ok);
1321     QStringList strList = (QStringList() << "poss2ukstu_blue"
1322                            << "poss2ukstu_red"
1323                            << "poss2ukstu_ir"
1324                            << "poss1_blue"
1325                            << "poss1_red"
1326                            << "quickv"
1327                            << "all");
1328     QString version =
1329         QInputDialog::getItem(this, i18n("Customized DSS Download"), i18n("Specify version: "), strList, 0, false, &ok);
1330 
1331     QUrl srcUrl(KSDssDownloader::getDSSURL(currentObject()->ra0(), currentObject()->dec0(), width, height, "gif",
1332                                            version, &md));
1333 
1334     delete m_dl;
1335     m_dl = new KSDssDownloader();
1336     connect(m_dl, SIGNAL(downloadComplete(bool)), SLOT(downloadReady(bool)));
1337     m_dl->startSingleDownload(srcUrl, getCurrentImagePath(), md);
1338 }
1339 
slotGetImage(bool _dss,const SkyObject * o)1340 void ObservingList::slotGetImage(bool _dss, const SkyObject *o)
1341 {
1342     dss = _dss;
1343     Q_ASSERT(
1344         !o ||
1345         o == currentObject()); // FIXME: Meaningless to operate on m_currentImageFileName unless o == currentObject()!
1346     if (!o)
1347         o = currentObject();
1348     ui->SearchImage->setEnabled(false);
1349     //ui->ImagePreview->clearPreview();
1350     //ui->ImagePreview->setPixmap(QPixmap());
1351     setCurrentImage(o);
1352     QString currentImagePath = getCurrentImagePath();
1353     if (QFile::exists(currentImagePath))
1354         QFile::remove(currentImagePath);
1355     //QUrl url;
1356     dss = true;
1357     qWarning() << "FIXME: Removed support for SDSS. Until reintroduction, we will supply a DSS image";
1358     std::function<void(bool)> slot = std::bind(&ObservingList::downloadReady, this, std::placeholders::_1);
1359     new KSDssDownloader(o, currentImagePath, slot, this);
1360 }
1361 
downloadReady(bool success)1362 void ObservingList::downloadReady(bool success)
1363 {
1364     // set downloadJob to 0, but don't delete it - the job will be deleted automatically
1365     //    downloadJob = 0;
1366 
1367     delete m_dl;
1368     m_dl = nullptr; // required if we came from slotCustomDSS; does nothing otherwise
1369 
1370     if (!success)
1371     {
1372         KSNotification::sorry(i18n("Failed to download DSS/SDSS image."));
1373     }
1374     else
1375     {
1376         /*
1377           if( QFile( QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath(m_currentImageFileName) ).size() > 13000)
1378           //The default image is around 8689 bytes
1379         */
1380         //ui->ImagePreview->showPreview( QUrl::fromLocalFile( getCurrentImagePath() ) );
1381         ui->ImagePreview->setPixmap(QPixmap(getCurrentImagePath()).scaledToHeight(ui->ImagePreview->width()));
1382         saveThumbImage();
1383         ui->ImagePreview->show();
1384         ui->ImagePreview->setCursor(Qt::PointingHandCursor);
1385         ui->DeleteImage->setEnabled(true);
1386     }
1387     /*
1388     // FIXME: Implement a priority order SDSS > DSS in the DSS downloader
1389     else if( ! dss )
1390         slotGetImage( true );
1391     */
1392 }
1393 
setCurrentImage(const SkyObject * o)1394 void ObservingList::setCurrentImage(const SkyObject *o)
1395 {
1396     QString sanitizedName = o->name().remove(' ').remove('\'').remove('\"').toLower();
1397 
1398     // JM: Always use .png across all platforms. No JPGs at all?
1399     m_currentImageFileName = "image-" + sanitizedName + ".png";
1400 
1401     m_currentThumbImageFileName = "thumb-" + sanitizedName + ".png";
1402 
1403     // Does full image exists in the path?
1404     QString currentImagePath = KSPaths::locate(QStandardPaths::AppDataLocation, m_currentImageFileName);
1405 
1406     // Let's try to fallback to thumb-* images if they exist
1407     if (currentImagePath.isEmpty())
1408     {
1409         currentImagePath = KSPaths::locate(QStandardPaths::AppDataLocation, m_currentThumbImageFileName);
1410 
1411         // If thumb image exists, let's use it
1412         if (currentImagePath.isEmpty() == false)
1413             m_currentImageFileName = m_currentThumbImageFileName;
1414     }
1415 
1416     // 2017-04-14: Unnamed stars already unsupported in observing list
1417     /*
1418     if( o->name() == "star" )
1419     {
1420         QString RAString( o->ra0().toHMSString() );
1421         QString DecString( o->dec0().toDMSString() );
1422         m_currentImageFileName = "Image_J" + RAString.remove(' ').remove( ':' ) + DecString.remove(' ').remove( ':' ); // Note: Changed naming convention to standard 2016-08-25 asimha; old images shall have to be re-downloaded.
1423         // Unnecessary complication below:
1424         // QChar decsgn = ( (o->dec0().Degrees() < 0.0 ) ? '-' : '+' );
1425         // m_currentImageFileName = m_currentImageFileName.remove('+').remove('-') + decsgn;
1426     }
1427     */
1428 
1429     // 2017-04-14 JM: If we use .png always, let us use it across all platforms.
1430     /*
1431     QString imagePath = getCurrentImagePath();
1432     if ( QFile::exists( imagePath))   // New convention -- append filename extension so file is usable on Windows etc.
1433     {
1434         QFile::rename( imagePath, imagePath + ".png" );
1435     }
1436     m_currentImageFileName += ".png";
1437     */
1438 }
1439 
getCurrentImagePath()1440 QString ObservingList::getCurrentImagePath()
1441 {
1442     QString currentImagePath = KSPaths::locate(QStandardPaths::AppDataLocation, m_currentImageFileName);
1443     if (QFile::exists(currentImagePath))
1444     {
1445         return currentImagePath;
1446     }
1447     else
1448         return QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath(m_currentImageFileName);
1449 }
1450 
slotSaveAllImages()1451 void ObservingList::slotSaveAllImages()
1452 {
1453     ui->SearchImage->setEnabled(false);
1454     ui->DeleteImage->setEnabled(false);
1455     m_CurrentObject = nullptr;
1456     //Clear the selection in the Tables
1457     ui->WishListView->clearSelection();
1458     ui->SessionView->clearSelection();
1459 
1460     foreach (QSharedPointer<SkyObject> o, getActiveList())
1461     {
1462         if (!o)
1463             continue; // FIXME: Why would we have null objects? But appears that we do.
1464         setCurrentImage(o.data());
1465         QString img(getCurrentImagePath());
1466         //        QUrl url( ( Options::obsListPreferDSS() ) ? DSSUrl : SDSSUrl ); // FIXME: We have removed SDSS support!
1467         QUrl url(KSDssDownloader::getDSSURL(o.data()));
1468         if (!o->isSolarSystem()) //TODO find a way for adding support for solar system images
1469             saveImage(url, img, o.data());
1470     }
1471 }
1472 
saveImage(QUrl,QString,const SkyObject * o)1473 void ObservingList::saveImage(QUrl /*url*/, QString /*filename*/, const SkyObject *o)
1474 {
1475     if (!o)
1476         o = currentObject();
1477     Q_ASSERT(o);
1478     if (!QFile::exists(getCurrentImagePath()))
1479     {
1480         // Call the DSS downloader
1481         slotGetImage(true, o);
1482     }
1483 }
1484 
slotImageViewer()1485 void ObservingList::slotImageViewer()
1486 {
1487     QPointer<ImageViewer> iv;
1488     QString currentImagePath = getCurrentImagePath();
1489     if (QFile::exists(currentImagePath))
1490     {
1491         QUrl url = QUrl::fromLocalFile(currentImagePath);
1492         iv       = new ImageViewer(url);
1493     }
1494 
1495     if (iv)
1496         iv->show();
1497 }
1498 
slotDeleteAllImages()1499 void ObservingList::slotDeleteAllImages()
1500 {
1501     if (KMessageBox::warningYesNo(nullptr, i18n("This will delete all saved images. Are you sure you want to do this?"),
1502                                   i18n("Delete All Images")) == KMessageBox::No)
1503         return;
1504     ui->ImagePreview->setCursor(Qt::ArrowCursor);
1505     ui->SearchImage->setEnabled(false);
1506     ui->DeleteImage->setEnabled(false);
1507     m_CurrentObject = nullptr;
1508     //Clear the selection in the Tables
1509     ui->WishListView->clearSelection();
1510     ui->SessionView->clearSelection();
1511     //ui->ImagePreview->clearPreview();
1512     ui->ImagePreview->setPixmap(QPixmap());
1513     QDirIterator iterator(KSPaths::writableLocation(QStandardPaths::AppDataLocation));
1514     while (iterator.hasNext())
1515     {
1516         // TODO: Probably, there should be a different directory for cached images in the observing list.
1517         if (iterator.fileName().contains("Image") && (!iterator.fileName().contains("dat")) &&
1518                 (!iterator.fileName().contains("obslist")))
1519         {
1520             QFile file(iterator.filePath());
1521             file.remove();
1522         }
1523         iterator.next();
1524     }
1525 }
1526 
setSaveImagesButton()1527 void ObservingList::setSaveImagesButton()
1528 {
1529     ui->saveImages->setEnabled(!getActiveList().isEmpty());
1530 }
1531 
1532 // FIXME: Is there a reason to implement these as an event filter,
1533 // instead of as a signal-slot connection? Shouldn't we just use slots
1534 // to subscribe to various events from the Table / Session view?
1535 //
1536 // NOTE: ui->ImagePreview is a QLabel, which has no clicked() event or
1537 // public mouseReleaseEvent(), so eventFilter makes sense.
eventFilter(QObject * obj,QEvent * event)1538 bool ObservingList::eventFilter(QObject *obj, QEvent *event)
1539 {
1540     if (obj == ui->ImagePreview)
1541     {
1542         if (event->type() == QEvent::MouseButtonRelease)
1543         {
1544             if (currentObject())
1545             {
1546                 if (!QFile::exists(getCurrentImagePath()))
1547                 {
1548                     if (!currentObject()->isSolarSystem())
1549                         slotGetImage(Options::obsListPreferDSS());
1550                     else
1551                         slotSearchImage();
1552                 }
1553                 else
1554                     slotImageViewer();
1555             }
1556             return true;
1557         }
1558     }
1559     if (obj == ui->WishListView->viewport() || obj == ui->SessionView->viewport())
1560     {
1561         bool sessionViewEvent = (obj == ui->SessionView->viewport());
1562 
1563         if (event->type() == QEvent::MouseButtonRelease) // Mouse button release event
1564         {
1565             QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
1566             QPoint pos(mouseEvent->globalX(), mouseEvent->globalY());
1567 
1568             if (mouseEvent->button() == Qt::RightButton)
1569             {
1570                 if (!noSelection)
1571                 {
1572                     pmenu->initPopupMenu(sessionViewEvent, !singleSelection, showScope);
1573                     pmenu->popup(pos);
1574                 }
1575                 return true;
1576             }
1577         }
1578     }
1579 
1580     if (obj == ui->WishListView || obj == ui->SessionView)
1581     {
1582         if (event->type() == QEvent::KeyPress)
1583         {
1584             QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
1585             if (keyEvent->key() == Qt::Key_Delete)
1586             {
1587                 slotRemoveSelectedObjects();
1588                 return true;
1589             }
1590         }
1591     }
1592 
1593     return false;
1594 }
1595 
slotSearchImage()1596 void ObservingList::slotSearchImage()
1597 {
1598     QPixmap *pm                  = new QPixmap(":/images/noimage.png");
1599     QPointer<ThumbnailPicker> tp = new ThumbnailPicker(currentObject(), *pm, this, 200, 200, i18n("Image Chooser"));
1600     if (tp->exec() == QDialog::Accepted)
1601     {
1602         QString currentImagePath = getCurrentImagePath();
1603         QFile f(currentImagePath);
1604 
1605         //If a real image was set, save it.
1606         if (tp->imageFound())
1607         {
1608             const auto image = *tp->image();
1609             image.save(f.fileName(), "PNG");
1610             //ui->ImagePreview->showPreview( QUrl::fromLocalFile( f.fileName() ) );
1611             saveThumbImage();
1612             slotNewSelection();
1613             ui->ImagePreview->setPixmap(image.scaledToHeight(ui->ImagePreview->width()));
1614             ui->ImagePreview->repaint();
1615         }
1616     }
1617     delete pm;
1618     delete tp;
1619 }
1620 
slotDeleteCurrentImage()1621 void ObservingList::slotDeleteCurrentImage()
1622 {
1623     QFile::remove(getCurrentImagePath());
1624     ImagePreviewHash.remove(m_CurrentObject);
1625     slotNewSelection();
1626 }
1627 
saveThumbImage()1628 void ObservingList::saveThumbImage()
1629 {
1630     QFileInfo const f(QDir(KSPaths::writableLocation(QStandardPaths::AppDataLocation)).filePath(m_currentThumbImageFileName));
1631     if (!f.exists())
1632     {
1633         QImage img(getCurrentImagePath());
1634         img = img.scaled(200, 200, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
1635         img.save(f.filePath());
1636     }
1637 }
1638 
getTime(const SkyObject * o) const1639 QString ObservingList::getTime(const SkyObject *o) const
1640 {
1641     return TimeHash.value(o->name(), QTime(30, 0, 0)).toString("h:mm:ss AP");
1642 }
1643 
scheduledTime(SkyObject * o) const1644 QTime ObservingList::scheduledTime(SkyObject *o) const
1645 {
1646     return TimeHash.value(o->name(), o->transitTime(dt, geo));
1647 }
1648 
setTime(const SkyObject * o,QTime t)1649 void ObservingList::setTime(const SkyObject *o, QTime t)
1650 {
1651     TimeHash.insert(o->name(), t);
1652 }
1653 
slotOALExport()1654 void ObservingList::slotOALExport()
1655 {
1656     slotSaveSessionAs(false);
1657 }
1658 
slotAddVisibleObj()1659 void ObservingList::slotAddVisibleObj()
1660 {
1661     KStarsDateTime lt = dt;
1662     lt.setTime(QTime(8, 0, 0));
1663     QPointer<WUTDialog> w = new WUTDialog(KStars::Instance(), sessionView, geo, lt);
1664     w->init();
1665     QModelIndexList selectedItems;
1666     selectedItems =
1667         m_WishListSortModel->mapSelectionToSource(ui->WishListView->selectionModel()->selection()).indexes();
1668     if (selectedItems.size())
1669     {
1670         foreach (const QModelIndex &i, selectedItems)
1671         {
1672             foreach (QSharedPointer<SkyObject> o, obsList())
1673                 if (getObjectName(o.data()) == i.data().toString() && w->checkVisibility(o.data()))
1674                     slotAddObject(
1675                         o.data(),
1676                         true); // FIXME: Better if there is a QSharedPointer override for this, although the check will ensure that we don't duplicate.
1677         }
1678     }
1679     delete w;
1680 }
1681 
findObjectByName(QString name)1682 SkyObject *ObservingList::findObjectByName(QString name)
1683 {
1684     foreach (QSharedPointer<SkyObject> o, sessionList())
1685     {
1686         if (getObjectName(o.data(), false) == name)
1687             return o.data();
1688     }
1689     return nullptr;
1690 }
1691 
selectObject(const SkyObject * o)1692 void ObservingList::selectObject(const SkyObject *o)
1693 {
1694     ui->tabWidget->setCurrentIndex(1);
1695     ui->SessionView->selectionModel()->clear();
1696     for (int irow = m_SessionModel->rowCount() - 1; irow >= 0; --irow)
1697     {
1698         QModelIndex mSortIndex = m_SessionSortModel->index(irow, 0);
1699         QModelIndex mIndex     = m_SessionSortModel->mapToSource(mSortIndex);
1700         int idxrow             = mIndex.row();
1701         if (m_SessionModel->item(idxrow, 0)->text() == getObjectName(o))
1702             ui->SessionView->selectRow(idxrow);
1703         slotNewSelection();
1704     }
1705 }
1706 
setDefaultImage()1707 void ObservingList::setDefaultImage()
1708 {
1709     ui->ImagePreview->setPixmap(m_NoImagePixmap);
1710     ui->ImagePreview->update();
1711 }
1712 
getObjectName(const SkyObject * o,bool translated)1713 QString ObservingList::getObjectName(const SkyObject *o, bool translated)
1714 {
1715     QString finalObjectName;
1716     if (o->name() == "star")
1717     {
1718         const StarObject *s = dynamic_cast<const StarObject *>(o);
1719 
1720         // JM: Enable HD Index stars to be added to the observing list.
1721         if (s != nullptr && s->getHDIndex() != 0)
1722             finalObjectName = QString("HD %1").arg(QString::number(s->getHDIndex()));
1723     }
1724     else
1725         finalObjectName = translated ? o->translatedName() : o->name();
1726 
1727     return finalObjectName;
1728 }
1729 
slotUpdateAltitudes()1730 void ObservingList::slotUpdateAltitudes()
1731 {
1732     // FIXME: Update upon gaining visibility, do not update when not visible
1733     KStarsDateTime now = KStarsDateTime::currentDateTimeUtc();
1734     //    qCDebug(KSTARS) << "Updating altitudes in observation planner @ JD - J2000 = " << double( now.djd() - J2000 );
1735     for (int irow = m_WishListModel->rowCount() - 1; irow >= 0; --irow)
1736     {
1737         QModelIndex idx = m_WishListSortModel->mapToSource(m_WishListSortModel->index(irow, 0));
1738         SkyObject *o    = static_cast<SkyObject *>(idx.data(Qt::UserRole + 1).value<void *>());
1739         Q_ASSERT(o);
1740         SkyPoint p = o->recomputeHorizontalCoords(now, geo);
1741         idx =
1742             m_WishListSortModel->mapToSource(m_WishListSortModel->index(irow, m_WishListSortModel->columnCount() - 1));
1743         QStandardItem *replacement = m_altCostHelper(p);
1744         m_WishListModel->setData(idx, replacement->data(Qt::DisplayRole), Qt::DisplayRole);
1745         m_WishListModel->setData(idx, replacement->data(Qt::UserRole), Qt::UserRole);
1746         delete replacement;
1747     }
1748     emit m_WishListModel->dataChanged(
1749         m_WishListModel->index(0, m_WishListModel->columnCount() - 1),
1750         m_WishListModel->index(m_WishListModel->rowCount() - 1, m_WishListModel->columnCount() - 1));
1751 }
1752 
findObject(const SkyObject * o,bool session)1753 QSharedPointer<SkyObject> ObservingList::findObject(const SkyObject *o, bool session)
1754 {
1755     const QList<QSharedPointer<SkyObject>> &list = (session ? sessionList() : obsList());
1756     const QString &target                        = getObjectName(o);
1757     foreach (QSharedPointer<SkyObject> obj, list)
1758     {
1759         if (getObjectName(obj.data()) == target)
1760             return obj;
1761     }
1762     return QSharedPointer<SkyObject>(); // null pointer
1763 }
1764