File indexing completed on 2024-03-24 15:18:09

0001 /*
0002     SPDX-FileCopyrightText: 2004-2020 Jeff Woods <jcwoods@bellsouth.net>
0003     SPDX-FileCopyrightText: 2004-2020 Jason Harris <jharris@30doradus.org>
0004     SPDX-FileCopyrightText: Prakash Mohan <prakash.mohan@kdemail.net>
0005     SPDX-FileCopyrightText: Akarsh Simha <akarsh@kde.org>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "observinglist.h"
0011 
0012 #include "config-kstars.h"
0013 
0014 #include "constellationboundarylines.h"
0015 #include "fov.h"
0016 #include "imageviewer.h"
0017 #include "ksalmanac.h"
0018 #include "ksnotification.h"
0019 #include "ksdssdownloader.h"
0020 #include "kspaths.h"
0021 #include "kstars.h"
0022 #include "kstarsdata.h"
0023 #include "ksutils.h"
0024 #include "obslistpopupmenu.h"
0025 #include "obslistwizard.h"
0026 #include "Options.h"
0027 #include "sessionsortfilterproxymodel.h"
0028 #include "skymap.h"
0029 #include "thumbnailpicker.h"
0030 #include "dialogs/detaildialog.h"
0031 #include "dialogs/finddialog.h"
0032 #include "dialogs/locationdialog.h"
0033 #include "oal/execute.h"
0034 #include "skycomponents/skymapcomposite.h"
0035 #include "skyobjects/skyobject.h"
0036 #include "skyobjects/starobject.h"
0037 #include "tools/altvstime.h"
0038 #include "tools/eyepiecefield.h"
0039 #include "tools/wutdialog.h"
0040 
0041 #ifdef HAVE_INDI
0042 #include <basedevice.h>
0043 #include "indi/indilistener.h"
0044 #include "indi/drivermanager.h"
0045 #include "indi/driverinfo.h"
0046 #include "ekos/manager.h"
0047 #endif
0048 
0049 #include <KPlotting/KPlotAxis>
0050 #include <KPlotting/KPlotObject>
0051 #include <KMessageBox>
0052 #include <QMessageBox>
0053 
0054 #include <kstars_debug.h>
0055 
0056 //
0057 // ObservingListUI
0058 // ---------------------------------
0059 ObservingListUI::ObservingListUI(QWidget *p) : QFrame(p)
0060 {
0061     setupUi(this);
0062 }
0063 
0064 //
0065 // ObservingList
0066 // ---------------------------------
0067 ObservingList::ObservingList()
0068     : QDialog((QWidget *)KStars::Instance()), LogObject(nullptr), m_CurrentObject(nullptr), isModified(false), m_dl(nullptr),
0069       m_manager{ CatalogsDB::dso_db_path() }
0070 {
0071 #ifdef Q_OS_OSX
0072     setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
0073 #endif
0074     ui                      = new ObservingListUI(this);
0075     QVBoxLayout *mainLayout = new QVBoxLayout;
0076     mainLayout->addWidget(ui);
0077     setWindowTitle(i18nc("@title:window", "Observation Planner"));
0078 
0079     setLayout(mainLayout);
0080 
0081     dt = KStarsDateTime::currentDateTime();
0082     setFocusPolicy(Qt::StrongFocus);
0083     geo            = KStarsData::Instance()->geo();
0084     sessionView    = false;
0085     m_listFileName = QString();
0086     pmenu.reset(new ObsListPopupMenu());
0087     //Set up the Table Views
0088     m_WishListModel.reset(new QStandardItemModel(0, 5, this));
0089     m_SessionModel.reset(new QStandardItemModel(0, 5));
0090 
0091     m_WishListModel->setHorizontalHeaderLabels(
0092         QStringList() << i18n("Name") << i18n("Alternate Name") << i18nc("Right Ascension", "RA (J2000)")
0093         << i18nc("Declination", "Dec (J2000)") << i18nc("Magnitude", "Mag") << i18n("Type")
0094         << i18n("Current Altitude"));
0095     m_SessionModel->setHorizontalHeaderLabels(
0096         QStringList() << i18n("Name") << i18n("Alternate Name") << i18nc("Right Ascension", "RA (J2000)")
0097         << i18nc("Declination", "Dec (J2000)") << i18nc("Magnitude", "Mag") << i18n("Type")
0098         << i18nc("Constellation", "Constell.") << i18n("Time") << i18nc("Altitude", "Alt")
0099         << i18nc("Azimuth", "Az"));
0100 
0101     m_WishListSortModel.reset(new QSortFilterProxyModel(this));
0102     m_WishListSortModel->setSourceModel(m_WishListModel.get());
0103     m_WishListSortModel->setDynamicSortFilter(true);
0104     m_WishListSortModel->setSortRole(Qt::UserRole);
0105     ui->WishListView->setModel(m_WishListSortModel.get());
0106     ui->WishListView->horizontalHeader()->setStretchLastSection(true);
0107 
0108     ui->WishListView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
0109     m_SessionSortModel.reset(new SessionSortFilterProxyModel());
0110     m_SessionSortModel->setSourceModel(m_SessionModel.get());
0111     m_SessionSortModel->setDynamicSortFilter(true);
0112     ui->SessionView->setModel(m_SessionSortModel.get());
0113     ui->SessionView->horizontalHeader()->setStretchLastSection(true);
0114     ui->SessionView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
0115     ksal.reset(new KSAlmanac);
0116     ksal->setLocation(geo);
0117     ui->avt->setGeoLocation(geo);
0118     ui->avt->setSunRiseSetTimes(ksal->getSunRise(), ksal->getSunSet());
0119     ui->avt->setLimits(-12.0, 12.0, -90.0, 90.0);
0120     ui->avt->axis(KPlotWidget::BottomAxis)->setTickLabelFormat('t');
0121     ui->avt->axis(KPlotWidget::BottomAxis)->setLabel(i18n("Local Time"));
0122     ui->avt->axis(KPlotWidget::TopAxis)->setTickLabelFormat('t');
0123     ui->avt->axis(KPlotWidget::TopAxis)->setTickLabelsShown(true);
0124     ui->DateEdit->setDate(dt.date());
0125     ui->SetLocation->setText(geo->fullName());
0126     ui->ImagePreview->installEventFilter(this);
0127     ui->WishListView->viewport()->installEventFilter(this);
0128     ui->WishListView->installEventFilter(this);
0129     ui->SessionView->viewport()->installEventFilter(this);
0130     ui->SessionView->installEventFilter(this);
0131     // setDefaultImage();
0132     //Connections
0133     connect(ui->WishListView, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(slotCenterObject()));
0134     connect(ui->WishListView->selectionModel(),
0135             SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this, SLOT(slotNewSelection()));
0136     connect(ui->SessionView->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
0137             this, SLOT(slotNewSelection()));
0138     connect(ui->WUTButton, SIGNAL(clicked()), this, SLOT(slotWUT()));
0139     connect(ui->FindButton, SIGNAL(clicked()), this, SLOT(slotFind()));
0140     connect(ui->OpenButton, SIGNAL(clicked()), this, SLOT(slotOpenList()));
0141     connect(ui->SaveButton, SIGNAL(clicked()), this, SLOT(slotSaveSession()));
0142     connect(ui->SaveAsButton, SIGNAL(clicked()), this, SLOT(slotSaveSessionAs()));
0143     connect(ui->WizardButton, SIGNAL(clicked()), this, SLOT(slotWizard()));
0144     connect(ui->batchAddButton, SIGNAL(clicked()), this, SLOT(slotBatchAdd()));
0145     connect(ui->SetLocation, SIGNAL(clicked()), this, SLOT(slotLocation()));
0146     connect(ui->Update, SIGNAL(clicked()), this, SLOT(slotUpdate()));
0147     connect(ui->DeleteImage, SIGNAL(clicked()), this, SLOT(slotDeleteCurrentImage()));
0148     connect(ui->SearchImage, SIGNAL(clicked()), this, SLOT(slotSearchImage()));
0149     connect(ui->SetTime, SIGNAL(clicked()), this, SLOT(slotSetTime()));
0150     connect(ui->tabWidget, SIGNAL(currentChanged(int)), this, SLOT(slotChangeTab(int)));
0151     connect(ui->saveImages, SIGNAL(clicked()), this, SLOT(slotSaveAllImages()));
0152     connect(ui->DeleteAllImages, SIGNAL(clicked()), this, SLOT(slotDeleteAllImages()));
0153     connect(ui->OALExport, SIGNAL(clicked()), this, SLOT(slotOALExport()));
0154     connect(ui->clearListB, SIGNAL(clicked()), this, SLOT(slotClearList()));
0155     //Add icons to Push Buttons
0156     ui->OpenButton->setIcon(QIcon::fromTheme("document-open"));
0157     ui->OpenButton->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0158     ui->SaveButton->setIcon(QIcon::fromTheme("document-save"));
0159     ui->SaveButton->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0160     ui->SaveAsButton->setIcon(
0161         QIcon::fromTheme("document-save-as"));
0162     ui->SaveAsButton->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0163     ui->WizardButton->setIcon(QIcon::fromTheme("tools-wizard"));
0164     ui->WizardButton->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0165     noSelection = true;
0166     showScope   = false;
0167     ui->NotesEdit->setEnabled(false);
0168     ui->SetTime->setEnabled(false);
0169     ui->TimeEdit->setEnabled(false);
0170     ui->SearchImage->setEnabled(false);
0171     ui->saveImages->setEnabled(false);
0172     ui->DeleteImage->setEnabled(false);
0173     ui->OALExport->setEnabled(false);
0174 
0175     m_NoImagePixmap =
0176         QPixmap(":/images/noimage.png")
0177         .scaled(ui->ImagePreview->width(), ui->ImagePreview->height(), Qt::KeepAspectRatio, Qt::FastTransformation);
0178     m_altCostHelper = [this](const SkyPoint & p) -> QStandardItem *
0179     {
0180         const double inf = std::numeric_limits<double>::infinity();
0181         double altCost   = 0.;
0182         QString itemText;
0183         double maxAlt = p.maxAlt(*(geo->lat()));
0184         if (Options::obsListDemoteHole() && maxAlt > 90. - Options::obsListHoleSize())
0185             maxAlt = 90. - Options::obsListHoleSize();
0186         if (maxAlt <= 0.)
0187         {
0188             altCost  = -inf;
0189             itemText = i18n("Never rises");
0190         }
0191         else
0192         {
0193             altCost = (p.alt().Degrees() / maxAlt) * 100.;
0194             if (altCost < 0)
0195                 itemText = i18nc("Short text to describe that object has not risen yet", "Not risen");
0196             else
0197             {
0198                 if (altCost > 100.)
0199                 {
0200                     altCost  = -inf;
0201                     itemText = i18nc("Object is in the Dobsonian hole", "In hole");
0202                 }
0203                 else
0204                     itemText = QString::number(altCost, 'f', 0) + '%';
0205             }
0206         }
0207 
0208         QStandardItem *altItem = new QStandardItem(itemText);
0209         altItem->setData(altCost, Qt::UserRole);
0210         //        qCDebug(KSTARS) << "Updating altitude for " << p.ra().toHMSString() << " " << p.dec().toDMSString() << " alt = " << p.alt().toDMSString() << " info to " << itemText;
0211         return altItem;
0212     };
0213 
0214     // Needed to fix weird bug on Windows that started with Qt 5.9 that makes the title bar
0215     // not visible and therefore dialog not movable.
0216 #ifdef Q_OS_WIN
0217     move(100, 100);
0218 #endif
0219 }
0220 
0221 void ObservingList::showEvent(QShowEvent *)
0222 {
0223     // ONLY run for first ever load
0224 
0225     if (m_initialWishlistLoad == false)
0226     {
0227         m_initialWishlistLoad = true;
0228 
0229         slotLoadWishList(); //Load the wishlist from disk if present
0230         m_CurrentObject = nullptr;
0231         setSaveImagesButton();
0232 
0233         slotUpdateAltitudes();
0234         m_altitudeUpdater = new QTimer(this);
0235         connect(m_altitudeUpdater, SIGNAL(timeout()), this, SLOT(slotUpdateAltitudes()));
0236         m_altitudeUpdater->start(120000); // update altitudes every 2 minutes
0237     }
0238 }
0239 
0240 //SLOTS
0241 
0242 void ObservingList::slotAddObject(const SkyObject *_obj, bool session, bool update)
0243 {
0244     if (!m_initialWishlistLoad)
0245     {
0246         showEvent(nullptr); // Initialize the observing wishlist
0247     }
0248     bool addToWishList = true;
0249     if (!_obj)
0250         _obj = SkyMap::Instance()->clickedObject(); // Eh? Why? Weird default behavior.
0251 
0252     if (!_obj)
0253     {
0254         qCWarning(KSTARS) << "Trying to add null object to observing list! Ignoring.";
0255         return;
0256     }
0257 
0258     QString finalObjectName = getObjectName(_obj);
0259 
0260     if (finalObjectName.isEmpty())
0261     {
0262         KSNotification::sorry(i18n("Stars and objects whose names KStars does not know are not supported in the observing lists"));
0263         return;
0264     }
0265 
0266     //First, make sure object is not already in the list
0267     QSharedPointer<SkyObject> obj = findObject(_obj);
0268     if (obj)
0269     {
0270         addToWishList = false;
0271         if (!session)
0272         {
0273             KStars::Instance()->statusBar()->showMessage(
0274                 i18n("%1 is already in your wishlist.", finalObjectName),
0275                 0); // FIXME: This message is too inconspicuous if using the Find dialog to add
0276             return;
0277         }
0278     }
0279     else
0280     {
0281         assert(!findObject(_obj, session));
0282         qCDebug(KSTARS) << "Cloned object " << finalObjectName << " to add to observing list.";
0283         obj = QSharedPointer<SkyObject>(
0284                   _obj->clone()); // Use a clone in case the original SkyObject is deleted due to change in catalog configuration.
0285     }
0286 
0287     if (session && sessionList().contains(obj))
0288     {
0289         KStars::Instance()->statusBar()->showMessage(i18n("%1 is already in the session plan.", finalObjectName), 0);
0290         return;
0291     }
0292 
0293     // JM: If we are loading observing list from disk, solar system objects magnitudes are not calculated until later
0294     // Therefore, we manual invoke updateCoords to force computation of magnitude.
0295     if ((obj->type() == SkyObject::COMET || obj->type() == SkyObject::ASTEROID || obj->type() == SkyObject::MOON ||
0296             obj->type() == SkyObject::PLANET) &&
0297             obj->mag() == 0)
0298     {
0299         KSNumbers num(dt.djd());
0300         CachingDms LST = geo->GSTtoLST(dt.gst());
0301         obj->updateCoords(&num, true, geo->lat(), &LST, true);
0302     }
0303 
0304     QString smag = "--";
0305     if (-30.0 < obj->mag() && obj->mag() < 90.0)
0306         smag = QString::number(obj->mag(), 'f', 2); // The lower limit to avoid display of unrealistic comet magnitudes
0307 
0308     SkyPoint p = obj->recomputeHorizontalCoords(dt, geo);
0309 
0310     QList<QStandardItem *> itemList;
0311 
0312     auto getItemWithUserRole = [](const QString & itemText) -> QStandardItem *
0313     {
0314         QStandardItem *ret = new QStandardItem(itemText);
0315         ret->setData(itemText, Qt::UserRole);
0316         return ret;
0317     };
0318 
0319     // Fill itemlist with items that are common to both wishlist additions and session plan additions
0320     auto populateItemList = [&getItemWithUserRole, &itemList, &finalObjectName, obj, &p, &smag]()
0321     {
0322         itemList.clear();
0323         QStandardItem *keyItem = getItemWithUserRole(finalObjectName);
0324         keyItem->setData(QVariant::fromValue<void *>(static_cast<void *>(obj.data())), Qt::UserRole + 1);
0325         itemList
0326                 << keyItem // NOTE: The rest of the methods assume that the SkyObject pointer is available in the first column!
0327                 << getItemWithUserRole(obj->translatedLongName()) << getItemWithUserRole(p.ra0().toHMSString())
0328                 << getItemWithUserRole(p.dec0().toDMSString()) << getItemWithUserRole(smag)
0329                 << getItemWithUserRole(obj->typeName());
0330     };
0331 
0332     //Insert object in the Wish List
0333     if (addToWishList)
0334     {
0335         m_WishList.append(obj);
0336         m_CurrentObject = obj.data();
0337 
0338         //QString ra, dec;
0339         //ra = "";//p.ra().toHMSString();
0340         //dec = p.dec().toDMSString();
0341 
0342         populateItemList();
0343         // FIXME: Instead sort by a "clever" observability score, calculated as follows:
0344         //     - First sort by (max altitude) - (current altitude) rounded off to the nearest
0345         //     - Weight by declination - latitude (in the northern hemisphere, southern objects get higher precedence)
0346         //     - Demote objects in the hole
0347         SkyPoint p = obj->recomputeHorizontalCoords(KStarsDateTime::currentDateTimeUtc(), geo); // Current => now
0348         itemList << m_altCostHelper(p);
0349         m_WishListModel->appendRow(itemList);
0350 
0351         //Note addition in statusbar
0352         KStars::Instance()->statusBar()->showMessage(i18n("Added %1 to observing list.", finalObjectName), 0);
0353         ui->WishListView->resizeColumnsToContents();
0354         if (!update)
0355             slotSaveList();
0356     }
0357     //Insert object in the Session List
0358     if (session)
0359     {
0360         m_SessionList.append(obj);
0361         dt.setTime(TimeHash.value(finalObjectName, obj->transitTime(dt, geo)));
0362         dms lst(geo->GSTtoLST(dt.gst()));
0363         p.EquatorialToHorizontal(&lst, geo->lat());
0364 
0365         QString alt = "--", az = "--";
0366 
0367         QStandardItem *BestTime = new QStandardItem();
0368         /* QString ra, dec;
0369          if(obj->name() == "star" ) {
0370             ra = obj->ra0().toHMSString();
0371             dec = obj->dec0().toDMSString();
0372             BestTime->setData( QString( "--" ), Qt::DisplayRole );
0373         }
0374         else {*/
0375         BestTime->setData(TimeHash.value(finalObjectName, obj->transitTime(dt, geo)), Qt::DisplayRole);
0376         alt = p.alt().toDMSString();
0377         az  = p.az().toDMSString();
0378         //}
0379         // TODO: Change the rest of the parameters to their appropriate datatypes.
0380         populateItemList();
0381         itemList << getItemWithUserRole(KSUtils::constNameToAbbrev(
0382                                             KStarsData::Instance()->skyComposite()->constellationBoundary()->constellationName(obj.data())))
0383                  << BestTime << getItemWithUserRole(alt) << getItemWithUserRole(az);
0384 
0385         m_SessionModel->appendRow(itemList);
0386         //Adding an object should trigger the modified flag
0387         isModified = true;
0388         ui->SessionView->resizeColumnsToContents();
0389         //Note addition in statusbar
0390         KStars::Instance()->statusBar()->showMessage(i18n("Added %1 to session list.", finalObjectName), 0);
0391         SkyMap::Instance()->forceUpdate();
0392     }
0393     setSaveImagesButton();
0394 }
0395 
0396 void ObservingList::slotRemoveObject(const SkyObject *_o, bool session, bool update)
0397 {
0398     if (!update) // EH?!
0399     {
0400         if (!_o)
0401             _o = SkyMap::Instance()->clickedObject();
0402         else if (sessionView) //else if is needed as clickedObject should not be removed from the session list.
0403             session = true;
0404     }
0405 
0406     // Is the pointer supplied in our own lists?
0407     const QList<QSharedPointer<SkyObject>> &list = (session ? sessionList() : obsList());
0408     QStandardItemModel *currentModel             = (session ? m_SessionModel.get() : m_WishListModel.get());
0409 
0410     QSharedPointer<SkyObject> o = findObject(_o, session);
0411     if (!o)
0412     {
0413         qWarning() << "Object (name: " << getObjectName(o.data())
0414                    << ") supplied to ObservingList::slotRemoveObject() was not found in the "
0415                    << QString(session ? "session" : "observing") << " list!";
0416         return;
0417     }
0418 
0419     int k = list.indexOf(o);
0420     assert(k >= 0);
0421 
0422     // Remove from hash
0423     ImagePreviewHash.remove(o.data());
0424 
0425     if (o.data() == LogObject)
0426         saveCurrentUserLog();
0427 
0428     //Remove row from the TableView model
0429     // FIXME: Is there no faster way?
0430     for (int irow = 0; irow < currentModel->rowCount(); ++irow)
0431     {
0432         QString name = currentModel->item(irow, 0)->text();
0433         if (getObjectName(o.data()) == name)
0434         {
0435             currentModel->removeRow(irow);
0436             break;
0437         }
0438     }
0439 
0440     if (!session)
0441     {
0442         obsList().removeAt(k);
0443         ui->avt->removeAllPlotObjects();
0444         ui->WishListView->resizeColumnsToContents();
0445         if (!update)
0446             slotSaveList();
0447     }
0448     else
0449     {
0450         if (!update)
0451             TimeHash.remove(o->name());
0452         sessionList().removeAt(k); //Remove from the session list
0453         isModified = true;         //Removing an object should trigger the modified flag
0454         ui->avt->removeAllPlotObjects();
0455         ui->SessionView->resizeColumnsToContents();
0456         SkyMap::Instance()->forceUpdate();
0457     }
0458 }
0459 
0460 void ObservingList::slotRemoveSelectedObjects()
0461 {
0462     //Find each object by name in the session list, and remove it
0463     //Go backwards so item alignment doesn't get screwed up as rows are removed.
0464     for (int irow = getActiveModel()->rowCount() - 1; irow >= 0; --irow)
0465     {
0466         bool rowSelected;
0467         if (sessionView)
0468             rowSelected = ui->SessionView->selectionModel()->isRowSelected(irow, QModelIndex());
0469         else
0470             rowSelected = ui->WishListView->selectionModel()->isRowSelected(irow, QModelIndex());
0471 
0472         if (rowSelected)
0473         {
0474             QModelIndex sortIndex, index;
0475             sortIndex    = getActiveSortModel()->index(irow, 0);
0476             index        = getActiveSortModel()->mapToSource(sortIndex);
0477             SkyObject *o = static_cast<SkyObject *>(index.data(Qt::UserRole + 1).value<void *>());
0478             Q_ASSERT(o);
0479             slotRemoveObject(o, sessionView);
0480         }
0481     }
0482 
0483     if (sessionView)
0484     {
0485         //we've removed all selected objects, so clear the selection
0486         ui->SessionView->selectionModel()->clear();
0487         //Update the lists in the Execute window as well
0488         KStarsData::Instance()->executeSession()->init();
0489     }
0490 
0491     setSaveImagesButton();
0492     ui->ImagePreview->setCursor(Qt::ArrowCursor);
0493 }
0494 
0495 void ObservingList::slotNewSelection()
0496 {
0497     bool found      = false;
0498     singleSelection = false;
0499     noSelection     = false;
0500     showScope       = false;
0501     //ui->ImagePreview->clearPreview();
0502     //ui->ImagePreview->setPixmap(QPixmap());
0503     ui->ImagePreview->setCursor(Qt::ArrowCursor);
0504     QModelIndexList selectedItems;
0505     QString newName;
0506     QSharedPointer<SkyObject> o;
0507     QString labelText;
0508     ui->DeleteImage->setEnabled(false);
0509 
0510     selectedItems =
0511         getActiveSortModel()->mapSelectionToSource(getActiveView()->selectionModel()->selection()).indexes();
0512 
0513     if (selectedItems.size() == getActiveModel()->columnCount())
0514     {
0515         newName         = selectedItems[0].data().toString();
0516         singleSelection = true;
0517         //Find the selected object in the SessionList,
0518         //then break the loop.  Now SessionList.current()
0519         //points to the new selected object (until now it was the previous object)
0520         for (auto &o_temp : getActiveList())
0521         {
0522             if (getObjectName(o_temp.data()) == newName)
0523             {
0524                 o = o_temp;
0525                 found = true;
0526                 break;
0527             }
0528         }
0529     }
0530 
0531     if (singleSelection)
0532     {
0533         //Enable buttons
0534         ui->ImagePreview->setCursor(Qt::PointingHandCursor);
0535 #ifdef HAVE_INDI
0536         showScope = true;
0537 #endif
0538         if (found)
0539         {
0540             m_CurrentObject = o.data();
0541             //QPoint pos(0,0);
0542             plot(o.data());
0543             //Change the m_currentImageFileName, DSS/SDSS Url to correspond to the new object
0544             setCurrentImage(o.data());
0545             ui->SearchImage->setEnabled(true);
0546             if (currentObject()->hasName())
0547             {
0548                 //Display the current object's user notes in the NotesEdit
0549                 //First, save the last object's user log to disk, if necessary
0550                 saveCurrentUserLog(); //uses LogObject, which is still the previous obj.
0551                 //set LogObject to the new selected object
0552                 LogObject = currentObject();
0553                 ui->NotesEdit->setEnabled(true);
0554 
0555                 const auto &userLog =
0556                     KStarsData::Instance()->getUserData(LogObject->name()).userLog;
0557 
0558                 if (userLog.isEmpty())
0559                 {
0560                     ui->NotesEdit->setPlainText(
0561                         i18n("Record here observation logs and/or data on %1.", getObjectName(LogObject)));
0562                 }
0563                 else
0564                 {
0565                     ui->NotesEdit->setPlainText(userLog);
0566                 }
0567                 if (sessionView)
0568                 {
0569                     ui->TimeEdit->setEnabled(true);
0570                     ui->SetTime->setEnabled(true);
0571                     ui->TimeEdit->setTime(TimeHash.value(o->name(), o->transitTime(dt, geo)));
0572                 }
0573             }
0574             else //selected object is named "star"
0575             {
0576                 //clear the log text box
0577                 saveCurrentUserLog();
0578                 ui->NotesEdit->clear();
0579                 ui->NotesEdit->setEnabled(false);
0580                 ui->SearchImage->setEnabled(false);
0581             }
0582             QString ImagePath = KSPaths::locate(QStandardPaths::AppLocalDataLocation, m_currentImageFileName);
0583             if (!ImagePath.isEmpty())
0584             {
0585                 //If the image is present, show it!
0586                 KSDssImage ksdi(ImagePath);
0587                 KSDssImage::Metadata md = ksdi.getMetadata();
0588                 //ui->ImagePreview->showPreview( QUrl::fromLocalFile( ksdi.getFileName() ) );
0589                 if (ImagePreviewHash.contains(o.data()) == false)
0590                     ImagePreviewHash[o.data()] = QPixmap(ksdi.getFileName()).scaledToHeight(ui->ImagePreview->width());
0591 
0592                 //ui->ImagePreview->setPixmap(QPixmap(ksdi.getFileName()).scaledToHeight(ui->ImagePreview->width()));
0593                 ui->ImagePreview->setPixmap(ImagePreviewHash[o.data()]);
0594                 if (md.isValid())
0595                 {
0596                     ui->dssMetadataLabel->setText(
0597                         i18n("DSS Image metadata: \n Size: %1\' x %2\' \n Photometric band: %3 \n Version: %4",
0598                              QString::number(md.width), QString::number(md.height), QString() + md.band, md.version));
0599                 }
0600                 else
0601                     ui->dssMetadataLabel->setText(i18n("No image info available."));
0602                 ui->ImagePreview->show();
0603                 ui->DeleteImage->setEnabled(true);
0604             }
0605             else
0606             {
0607                 setDefaultImage();
0608                 ui->dssMetadataLabel->setText(
0609                     i18n("No image available. Click on the placeholder image to download one."));
0610             }
0611             QString cname =
0612                 KStarsData::Instance()->skyComposite()->constellationBoundary()->constellationName(o.data());
0613             if (o->type() != SkyObject::CONSTELLATION)
0614             {
0615                 labelText = "<b>";
0616                 if (o->type() == SkyObject::PLANET)
0617                     labelText += o->translatedName();
0618                 else
0619                     labelText += o->name();
0620                 if (std::isfinite(o->mag()) && o->mag() <= 30.)
0621                     labelText += ":</b> " + i18nc("%1 magnitude of object, %2 type of sky object (planet, asteroid "
0622                                                   "etc), %3 name of a constellation",
0623                                                   "%1 mag %2 in %3", o->mag(), o->typeName().toLower(), cname);
0624                 else
0625                     labelText +=
0626                         ":</b> " + i18nc("%1 type of sky object (planet, asteroid etc), %2 name of a constellation",
0627                                          "%1 in %2", o->typeName(), cname);
0628             }
0629         }
0630         else
0631         {
0632             setDefaultImage();
0633             qCWarning(KSTARS) << "Object " << newName << " not found in list.";
0634         }
0635         ui->quickInfoLabel->setText(labelText);
0636     }
0637     else
0638     {
0639         if (selectedItems.isEmpty()) //Nothing selected
0640         {
0641             //Disable buttons
0642             noSelection = true;
0643             ui->NotesEdit->setEnabled(false);
0644             m_CurrentObject = nullptr;
0645             ui->TimeEdit->setEnabled(false);
0646             ui->SetTime->setEnabled(false);
0647             ui->SearchImage->setEnabled(false);
0648             //Clear the user log text box.
0649             saveCurrentUserLog();
0650             ui->NotesEdit->setPlainText("");
0651             //Clear the plot in the AVTPlotwidget
0652             ui->avt->removeAllPlotObjects();
0653         }
0654         else //more than one object selected.
0655         {
0656             ui->NotesEdit->setEnabled(false);
0657             ui->TimeEdit->setEnabled(false);
0658             ui->SetTime->setEnabled(false);
0659             ui->SearchImage->setEnabled(false);
0660             m_CurrentObject = nullptr;
0661             //Clear the plot in the AVTPlotwidget
0662             ui->avt->removeAllPlotObjects();
0663             //Clear the user log text box.
0664             saveCurrentUserLog();
0665             ui->NotesEdit->setPlainText("");
0666             ui->quickInfoLabel->setText(QString());
0667         }
0668     }
0669 }
0670 
0671 void ObservingList::slotCenterObject()
0672 {
0673     if (getSelectedItems().size() == 1)
0674     {
0675         SkyMap::Instance()->setClickedObject(currentObject());
0676         SkyMap::Instance()->setClickedPoint(currentObject());
0677         SkyMap::Instance()->slotCenter();
0678     }
0679 }
0680 
0681 void ObservingList::slotSlewToObject()
0682 {
0683 #ifdef HAVE_INDI
0684 
0685     if (INDIListener::Instance()->size() == 0)
0686     {
0687         KSNotification::sorry(i18n("No connected mounts found."));
0688         return;
0689     }
0690 
0691     for (auto &oneDevice : INDIListener::devices())
0692     {
0693         if (!(oneDevice->getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE))
0694             continue;
0695 
0696         if (oneDevice->isConnected() == false)
0697         {
0698             KSNotification::error(i18n("Mount %1 is offline. Please connect and retry again.", oneDevice->getDeviceName()));
0699             return;
0700         }
0701 
0702         auto mount = oneDevice->getMount();
0703         if (!mount)
0704             continue;
0705         mount->Slew(currentObject());
0706         return;
0707     }
0708 
0709     KSNotification::sorry(i18n("No connected mounts found."));
0710 
0711 #endif
0712 }
0713 
0714 void ObservingList::slotAddToEkosScheduler()
0715 {
0716 #ifdef HAVE_INDI
0717     Ekos::Manager::Instance()->addObjectToScheduler(currentObject());
0718 #endif
0719 }
0720 
0721 //FIXME: This will open multiple Detail windows for each object;
0722 //Should have one window whose target object changes with selection
0723 void ObservingList::slotDetails()
0724 {
0725     if (currentObject())
0726     {
0727         QPointer<DetailDialog> dd =
0728             new DetailDialog(currentObject(), KStarsData::Instance()->ut(), geo, KStars::Instance());
0729         dd->exec();
0730         delete dd;
0731     }
0732 }
0733 
0734 void ObservingList::slotWUT()
0735 {
0736     KStarsDateTime lt = dt;
0737     lt.setTime(QTime(8, 0, 0));
0738     QPointer<WUTDialog> w = new WUTDialog(KStars::Instance(), sessionView, geo, lt);
0739     w->exec();
0740     delete w;
0741 }
0742 
0743 void ObservingList::slotAddToSession()
0744 {
0745     Q_ASSERT(!sessionView);
0746     if (getSelectedItems().size())
0747     {
0748         foreach (const QModelIndex &i, getSelectedItems())
0749         {
0750             foreach (QSharedPointer<SkyObject> o, obsList())
0751                 if (getObjectName(o.data()) == i.data().toString())
0752                     slotAddObject(
0753                         o.data(),
0754                         true); // FIXME: Would be good to have a wrapper that accepts QSharedPointer<SkyObject>
0755         }
0756     }
0757 }
0758 
0759 void ObservingList::slotFind()
0760 {
0761     if (FindDialog::Instance()->exec() == QDialog::Accepted)
0762     {
0763         SkyObject *o = FindDialog::Instance()->targetObject();
0764         if (o != nullptr)
0765         {
0766             slotAddObject(o, sessionView);
0767         }
0768     }
0769 }
0770 
0771 void ObservingList::slotBatchAdd()
0772 {
0773     bool accepted = false;
0774     QString items = QInputDialog::getMultiLineText(this,
0775                     sessionView ? i18n("Batch add to observing session") : i18n("Batch add to observing wishlist"),
0776                     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."),
0777                     QString(),
0778                     &accepted);
0779     bool resolve = Options::resolveNamesOnline();
0780 
0781     if (accepted && !items.isEmpty())
0782     {
0783         QStringList failedObjects;
0784         QStringList objectNames = items.split("\n");
0785         for (QString objectName : objectNames)
0786         {
0787             objectName = FindDialog::processSearchText(objectName);
0788             SkyObject *object = KStarsData::Instance()->objectNamed(objectName);
0789             if (!object && resolve)
0790             {
0791                 object = FindDialog::resolveAndAdd(m_manager, objectName);
0792             }
0793             if (!object)
0794             {
0795                 failedObjects.append(objectName);
0796             }
0797             else
0798             {
0799                 slotAddObject(object, sessionView);
0800             }
0801         }
0802 
0803         if (!failedObjects.isEmpty())
0804         {
0805             QMessageBox msgBox =
0806             {
0807                 QMessageBox::Icon::Warning,
0808                 i18np("Batch add: %1 object not found", "Batch add: %1 objects not found", failedObjects.size()),
0809                 i18np("%1 object could not be found in the database or resolved, and hence could not be added. See the details for more.",
0810                       "%1 objects could not be found in the database or resolved, and hence could not be added. See the details for more.",
0811                       failedObjects.size()),
0812                 QMessageBox::Ok,
0813                 this
0814             };
0815             msgBox.setDetailedText(failedObjects.join("\n"));
0816             msgBox.exec();
0817         }
0818     }
0819     Q_ASSERT(false); // Not implemented
0820 }
0821 
0822 void ObservingList::slotEyepieceView()
0823 {
0824     KStars::Instance()->slotEyepieceView(currentObject(), getCurrentImagePath());
0825 }
0826 
0827 void ObservingList::slotAVT()
0828 {
0829     QModelIndexList selectedItems;
0830     // 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
0831     selectedItems =
0832         (sessionView ?
0833          m_SessionSortModel->mapSelectionToSource(ui->SessionView->selectionModel()->selection()).indexes() :
0834          m_WishListSortModel->mapSelectionToSource(ui->WishListView->selectionModel()->selection()).indexes());
0835 
0836     if (selectedItems.size())
0837     {
0838         QPointer<AltVsTime> avt = new AltVsTime(KStars::Instance());
0839         foreach (const QModelIndex &i, selectedItems)
0840         {
0841             if (i.column() == 0)
0842             {
0843                 SkyObject *o = static_cast<SkyObject *>(i.data(Qt::UserRole + 1).value<void *>());
0844                 Q_ASSERT(o);
0845                 avt->processObject(o);
0846             }
0847         }
0848         avt->exec();
0849         delete avt;
0850     }
0851 }
0852 
0853 //FIXME: On close, we will need to close any open Details/AVT windows
0854 void ObservingList::slotClose()
0855 {
0856     //Save the current User log text
0857     saveCurrentUserLog();
0858     ui->avt->removeAllPlotObjects();
0859     slotNewSelection();
0860     saveCurrentList();
0861     hide();
0862 }
0863 
0864 void ObservingList::saveCurrentUserLog()
0865 {
0866     if (LogObject && !ui->NotesEdit->toPlainText().isEmpty() &&
0867             ui->NotesEdit->toPlainText() !=
0868             i18n("Record here observation logs and/or data on %1.", getObjectName(LogObject)))
0869     {
0870         const auto &success = KStarsData::Instance()->updateUserLog(
0871                                   LogObject->name(), ui->NotesEdit->toPlainText());
0872 
0873         if (!success.first)
0874             KSNotification::sorry(success.second, i18n("Could not update the user log."));
0875 
0876         ui->NotesEdit->clear();
0877         LogObject = nullptr;
0878     }
0879 }
0880 
0881 void ObservingList::slotOpenList()
0882 {
0883     QUrl fileURL = QFileDialog::getOpenFileUrl(KStars::Instance(), i18nc("@title:window", "Open Observing List"), QUrl(),
0884                    "KStars Observing List (*.obslist)");
0885     QFile f;
0886 
0887     if (fileURL.isValid())
0888     {
0889         f.setFileName(fileURL.toLocalFile());
0890         //FIXME do we still need to do this?
0891         /*
0892         if ( ! fileURL.isLocalFile() ) {
0893             //Save remote list to a temporary local file
0894             QTemporaryFile tmpfile;
0895             tmpfile.setAutoRemove(false);
0896             tmpfile.open();
0897             m_listFileName = tmpfile.fileName();
0898             if( KIO::NetAccess::download( fileURL, m_listFileName, this ) )
0899                 f.setFileName( m_listFileName );
0900 
0901         } else {
0902             m_listFileName = fileURL.toLocalFile();
0903             f.setFileName( m_listFileName );
0904         }
0905         */
0906 
0907         if (!f.open(QIODevice::ReadOnly))
0908         {
0909             QString message = i18n("Could not open file %1", f.fileName());
0910             KSNotification::sorry(message, i18n("Could Not Open File"));
0911             return;
0912         }
0913         saveCurrentList(); //See if the current list needs to be saved before opening the new one
0914         ui->tabWidget->setCurrentIndex(1); // FIXME: This is not robust -- asimha
0915         slotChangeTab(1);
0916 
0917         sessionList().clear();
0918         TimeHash.clear();
0919         m_CurrentObject = nullptr;
0920         m_SessionModel->removeRows(0, m_SessionModel->rowCount());
0921         SkyMap::Instance()->forceUpdate();
0922         //First line is the name of the list. The rest of the file is
0923         //object names, one per line. With the TimeHash value if present
0924         QTextStream istream(&f);
0925         QString input;
0926         input = istream.readAll();
0927         OAL::Log logObject;
0928         logObject.readBegin(input);
0929         //Set the New TimeHash
0930         TimeHash = logObject.timeHash();
0931         GeoLocation *geo_new = logObject.geoLocation();
0932         if (!geo_new)
0933         {
0934             // FIXME: This is a very hackish solution -- if we
0935             // encounter an invalid XML file, we know we won't read a
0936             // GeoLocation successfully. It does not detect partially
0937             // corrupt files. -- asimha
0938             KSNotification::sorry(i18n("The specified file is invalid. We expect an XML file based on the OpenAstronomyLog schema."));
0939             f.close();
0940             return;
0941         }
0942         dt = logObject.dateTime();
0943         //foreach (SkyObject *o, *(logObject.targetList()))
0944         for (auto &o : logObject.targetList())
0945             slotAddObject(o.data(), true);
0946         //Update the location and user set times from file
0947         slotUpdate();
0948         //Newly-opened list should not trigger isModified flag
0949         isModified = false;
0950         f.close();
0951     }
0952     else if (!fileURL.toLocalFile().isEmpty())
0953     {
0954         KSNotification::sorry(i18n("The specified file is invalid"));
0955     }
0956 }
0957 
0958 void ObservingList::slotClearList()
0959 {
0960     if ((ui->tabWidget->currentIndex() == 0 && obsList().isEmpty()) ||
0961             (ui->tabWidget->currentIndex() == 1 && sessionList().isEmpty()))
0962         return;
0963 
0964     QString message = i18n("Are you sure you want to clear all objects?");
0965     if (KMessageBox::questionYesNo(this, message, i18n("Clear all?")) == KMessageBox::Yes)
0966     {
0967         // Did I forget anything else to remove?
0968         ui->avt->removeAllPlotObjects();
0969         m_CurrentObject = LogObject = nullptr;
0970 
0971         if (ui->tabWidget->currentIndex() == 0)
0972         {
0973             // IMPORTANT: Is this enough or we will have dangling pointers in memory?
0974             ImagePreviewHash.clear();
0975             obsList().clear();
0976             m_WishListModel->setRowCount(0);
0977         }
0978         else
0979         {
0980             // IMPORTANT: Is this enough or we will have dangling pointers in memory?
0981             sessionList().clear();
0982             TimeHash.clear();
0983             isModified = true; //Removing an object should trigger the modified flag
0984             m_SessionModel->setRowCount(0);
0985             SkyMap::Instance()->forceUpdate();
0986         }
0987     }
0988 }
0989 
0990 void ObservingList::saveCurrentList()
0991 {
0992     //Before loading a new list, do we need to save the current one?
0993     //Assume that if the list is empty, then there's no need to save
0994     if (sessionList().size())
0995     {
0996         if (isModified)
0997         {
0998             QString message = i18n("Do you want to save the current session?");
0999             if (KMessageBox::questionYesNo(this, message, i18n("Save Current session?"), KStandardGuiItem::save(),
1000                                            KStandardGuiItem::discard()) == KMessageBox::Yes)
1001                 slotSaveSession();
1002         }
1003     }
1004 }
1005 
1006 void ObservingList::slotSaveSessionAs(bool nativeSave)
1007 {
1008     if (sessionList().isEmpty())
1009         return;
1010 
1011     QUrl fileURL = QFileDialog::getSaveFileUrl(KStars::Instance(), i18nc("@title:window", "Save Observing List"), QUrl(),
1012                    "KStars Observing List (*.obslist)");
1013     if (fileURL.isValid())
1014     {
1015         m_listFileName = fileURL.toLocalFile();
1016         slotSaveSession(nativeSave);
1017     }
1018 }
1019 
1020 void ObservingList::slotSaveList()
1021 {
1022     QFile f;
1023     // FIXME: Move wishlist into a database.
1024     // TODO: Support multiple wishlists.
1025 
1026     QString fileContents;
1027     QTextStream ostream(
1028         &fileContents); // We first write to a QString to prevent truncating the file in case there is a crash.
1029     foreach (const QSharedPointer<SkyObject> o, obsList())
1030     {
1031         if (!o)
1032         {
1033             qWarning() << "Null entry in observing wishlist! Skipping!";
1034             continue;
1035         }
1036         if (o->name() == "star")
1037         {
1038             //ostream << o->name() << "  " << o->ra0().Hours() << "  " << o->dec0().Degrees() << Qt::endl;
1039             ostream << getObjectName(o.data(), false) << '\n';
1040         }
1041         else if (o->type() == SkyObject::STAR)
1042         {
1043             Q_ASSERT(dynamic_cast<const StarObject *>(o.data()));
1044             const QSharedPointer<StarObject> s = qSharedPointerCast<StarObject>(o);
1045             if (s->name() == s->gname())
1046                 ostream << s->name2() << '\n';
1047             else
1048                 ostream << s->name() << '\n';
1049         }
1050         else
1051         {
1052             ostream << o->name() << '\n';
1053         }
1054     }
1055     f.setFileName(QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("wishlist.obslist"));
1056     if (!f.open(QIODevice::WriteOnly))
1057     {
1058         qWarning() << "Cannot save wish list to file!"; // TODO: This should be presented as a message box to the user
1059         KMessageBox::error(this,
1060                            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.",
1061                                 f.fileName()), i18n("Could not save observing wishlist"));
1062         return;
1063     }
1064     QTextStream writeemall(&f);
1065     writeemall << fileContents;
1066     f.close();
1067 }
1068 
1069 void ObservingList::slotLoadWishList()
1070 {
1071     QFile f;
1072     f.setFileName(QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("wishlist.obslist"));
1073     if (!f.open(QIODevice::ReadOnly))
1074     {
1075         qWarning(KSTARS) << "No WishList Saved yet";
1076         return;
1077     }
1078     QTextStream istream(&f);
1079     QString line;
1080 
1081     QPointer<QProgressDialog> addingObjectsProgress = new QProgressDialog();
1082     addingObjectsProgress->setWindowTitle(i18nc("@title:window", "Observing List Wizard"));
1083     addingObjectsProgress->setLabelText(i18n("Please wait while loading observing wishlist..."));
1084 
1085 
1086     // Read the entire file in one pass so we can show better progress indication
1087     QStringList objects;
1088     while (!istream.atEnd())
1089     {
1090         objects.append(istream.readLine());
1091     }
1092     addingObjectsProgress->setMaximum(objects.size());
1093     addingObjectsProgress->setMinimum(0);
1094     addingObjectsProgress->show();
1095 
1096     QStringList failedObjects;
1097     for (std::size_t idx = 0; idx < objects.size(); ++idx)
1098     {
1099         const auto &objectName = objects[idx];
1100 
1101         if (addingObjectsProgress->wasCanceled())
1102         {
1103             QMessageBox msgBox =
1104             {
1105                 QMessageBox::Icon::Warning,
1106                 i18n("Canceling this will truncate your wishlist"),
1107                 i18n("If you cancel this operation, your wishlist will be truncated and the following objects will be removed from the wishlist when you exit KStars. Are you sure this is okay?"),
1108                 QMessageBox::Yes | QMessageBox::No,
1109                 this
1110             };
1111             msgBox.setDefaultButton(QMessageBox::No);
1112             msgBox.setDetailedText(objects.mid(idx).join("\n") + "\n");
1113             if (msgBox.exec() == QMessageBox::Yes)
1114                 break;
1115             else
1116             {
1117                 addingObjectsProgress->reset();
1118                 addingObjectsProgress->setValue(idx);
1119                 addingObjectsProgress->show();
1120             }
1121 
1122         }
1123 
1124         SkyObject *o = KStarsData::Instance()->objectNamed(objectName);
1125 
1126         //If we haven't identified the object, try interpreting the
1127         //name as a star's genetive name (with ascii letters)
1128         if (!o)
1129             o = KStarsData::Instance()->skyComposite()->findStarByGenetiveName(line);
1130 
1131         if (o)
1132         {
1133             slotAddObject(o, false, true);
1134         }
1135         else
1136         {
1137             failedObjects.append(line);
1138         }
1139 
1140         addingObjectsProgress->setValue(idx + 1);
1141         qApp->processEvents();
1142     }
1143     delete (addingObjectsProgress);
1144     f.close();
1145 
1146     if (!failedObjects.isEmpty())
1147     {
1148         QMessageBox msgBox = {QMessageBox::Icon::Warning,
1149                               i18np("Observing wishlist truncated: %1 object not found", "Observing wishlist truncated: %1 objects not found", failedObjects.size()),
1150                               i18np("%1 object could not be found in the database, and will be removed from the observing wish list. We recommend that you copy its name as a backup so you can add it later.", "%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, whereby you can later use the Batch Add feature in the Observation Planner to add them back using internet search.", failedObjects.size()),
1151                               QMessageBox::Ok,
1152                               this
1153                              };
1154         msgBox.setDetailedText(failedObjects.join("\n") + "\n");
1155         msgBox.exec();
1156     }
1157 }
1158 
1159 void ObservingList::slotSaveSession(bool nativeSave)
1160 {
1161     if (sessionList().isEmpty())
1162     {
1163         KSNotification::error(i18n("Cannot save an empty session list."));
1164         return;
1165     }
1166 
1167     if (m_listFileName.isEmpty())
1168     {
1169         slotSaveSessionAs(nativeSave);
1170         return;
1171     }
1172     QFile f(m_listFileName);
1173     if (!f.open(QIODevice::WriteOnly))
1174     {
1175         QString message = i18n("Could not open file %1.  Try a different filename?", f.fileName());
1176         if (KMessageBox::warningYesNo(nullptr, message, i18n("Could Not Open File"), KGuiItem(i18n("Try Different")),
1177                                       KGuiItem(i18n("Do Not Try"))) == KMessageBox::Yes)
1178         {
1179             m_listFileName.clear();
1180             slotSaveSessionAs(nativeSave);
1181         }
1182         return;
1183     }
1184     QTextStream ostream(&f);
1185     OAL::Log log;
1186     ostream << log.writeLog(nativeSave);
1187     f.close();
1188     isModified = false; //We've saved the session, so reset the modified flag.
1189 }
1190 
1191 void ObservingList::slotWizard()
1192 {
1193     QPointer<ObsListWizard> wizard = new ObsListWizard(KStars::Instance());
1194     if (wizard->exec() == QDialog::Accepted)
1195     {
1196         QPointer<QProgressDialog> addingObjectsProgress = new QProgressDialog();
1197         addingObjectsProgress->setWindowTitle(i18nc("@title:window", "Observing List Wizard"));
1198         addingObjectsProgress->setLabelText(i18n("Please wait while adding objects..."));
1199         addingObjectsProgress->setMaximum(wizard->obsList().size());
1200         addingObjectsProgress->setMinimum(0);
1201         addingObjectsProgress->setValue(0);
1202         addingObjectsProgress->show();
1203         int counter = 1;
1204         foreach (SkyObject *o, wizard->obsList())
1205         {
1206             slotAddObject(o);
1207             addingObjectsProgress->setValue(counter++);
1208             if (addingObjectsProgress->wasCanceled())
1209                 break;
1210             qApp->processEvents();
1211         }
1212         delete addingObjectsProgress;
1213     }
1214 
1215     delete wizard;
1216 }
1217 
1218 void ObservingList::plot(SkyObject *o)
1219 {
1220     if (!o)
1221         return;
1222     float DayOffset = 0;
1223     if (TimeHash.value(o->name(), o->transitTime(dt, geo)).hour() > 12)
1224         DayOffset = 1;
1225 
1226     QDateTime midnight = QDateTime(dt.date(), QTime());
1227     KStarsDateTime ut  = geo->LTtoUT(KStarsDateTime(midnight));
1228     double h1          = geo->GSTtoLST(ut.gst()).Hours();
1229     if (h1 > 12.0)
1230         h1 -= 24.0;
1231 
1232     ui->avt->setSecondaryLimits(h1, h1 + 24.0, -90.0, 90.0);
1233     ksal->setLocation(geo);
1234     ksal->setDate(ut);
1235     ui->avt->setGeoLocation(geo);
1236     ui->avt->setSunRiseSetTimes(ksal->getSunRise(), ksal->getSunSet());
1237     ui->avt->setDawnDuskTimes(ksal->getDawnAstronomicalTwilight(), ksal->getDuskAstronomicalTwilight());
1238     ui->avt->setMinMaxSunAlt(ksal->getSunMinAlt(), ksal->getSunMaxAlt());
1239     ui->avt->setMoonRiseSetTimes(ksal->getMoonRise(), ksal->getMoonSet());
1240     ui->avt->setMoonIllum(ksal->getMoonIllum());
1241     ui->avt->update();
1242     KPlotObject *po = new KPlotObject(Qt::white, KPlotObject::Lines, 2.0);
1243     for (double h = -12.0; h <= 12.0; h += 0.5)
1244     {
1245         po->addPoint(h, findAltitude(o, (h + DayOffset * 24.0)));
1246     }
1247     ui->avt->removeAllPlotObjects();
1248     ui->avt->addPlotObject(po);
1249 }
1250 
1251 double ObservingList::findAltitude(SkyPoint *p, double hour)
1252 {
1253     // Jasem 2015-09-05 Using correct procedure to find altitude
1254     SkyPoint sp                   = *p; // make a copy
1255     QDateTime midnight            = QDateTime(dt.date(), QTime());
1256     KStarsDateTime ut             = geo->LTtoUT(KStarsDateTime(midnight));
1257     KStarsDateTime targetDateTime = ut.addSecs(hour * 3600.0);
1258     dms LST                       = geo->GSTtoLST(targetDateTime.gst());
1259     sp.EquatorialToHorizontal(&LST, geo->lat());
1260     return sp.alt().Degrees();
1261 }
1262 
1263 void ObservingList::slotChangeTab(int index)
1264 {
1265     noSelection = true;
1266     saveCurrentUserLog();
1267     ui->NotesEdit->setEnabled(false);
1268     ui->TimeEdit->setEnabled(false);
1269     ui->SetTime->setEnabled(false);
1270     ui->SearchImage->setEnabled(false);
1271     ui->DeleteImage->setEnabled(false);
1272     m_CurrentObject = nullptr;
1273     sessionView     = index != 0;
1274     setSaveImagesButton();
1275     ui->WizardButton->setEnabled(!sessionView); //wizard adds only to the Wish List
1276     ui->OALExport->setEnabled(sessionView);
1277     //Clear the selection in the Tables
1278     ui->WishListView->clearSelection();
1279     ui->SessionView->clearSelection();
1280     //Clear the user log text box.
1281     saveCurrentUserLog();
1282     ui->NotesEdit->setPlainText("");
1283     ui->avt->removeAllPlotObjects();
1284 }
1285 
1286 void ObservingList::slotLocation()
1287 {
1288     QPointer<LocationDialog> ld = new LocationDialog(this);
1289     if (ld->exec() == QDialog::Accepted)
1290     {
1291         geo = ld->selectedCity();
1292         ui->SetLocation->setText(geo->fullName());
1293     }
1294     delete ld;
1295 }
1296 
1297 void ObservingList::slotUpdate()
1298 {
1299     dt.setDate(ui->DateEdit->date());
1300     ui->avt->removeAllPlotObjects();
1301     //Creating a copy of the lists, we can't use the original lists as they'll keep getting modified as the loop iterates
1302     QList<QSharedPointer<SkyObject>> _obsList = m_WishList, _SessionList = m_SessionList;
1303 
1304     for (QSharedPointer<SkyObject> &o : _obsList)
1305     {
1306         if (o->name() != "star")
1307         {
1308             slotRemoveObject(o.data(), false, true);
1309             slotAddObject(o.data(), false, true);
1310         }
1311     }
1312     for (QSharedPointer<SkyObject> &obj : _SessionList)
1313     {
1314         if (obj->name() != "star")
1315         {
1316             slotRemoveObject(obj.data(), true, true);
1317             slotAddObject(obj.data(), true, true);
1318         }
1319     }
1320     SkyMap::Instance()->forceUpdate();
1321 }
1322 
1323 void ObservingList::slotSetTime()
1324 {
1325     SkyObject *o = currentObject();
1326     slotRemoveObject(o, true);
1327     TimeHash[o->name()] = ui->TimeEdit->time();
1328     slotAddObject(o, true, true);
1329 }
1330 
1331 void ObservingList::slotCustomDSS()
1332 {
1333     ui->SearchImage->setEnabled(false);
1334     //ui->ImagePreview->clearPreview();
1335     ui->ImagePreview->setPixmap(QPixmap());
1336 
1337     KSDssImage::Metadata md;
1338     bool ok = true;
1339 
1340     int width  = QInputDialog::getInt(this, i18n("Customized DSS Download"), i18n("Specify image width (arcminutes): "),
1341                                       15, 15, 75, 1, &ok);
1342     int height = QInputDialog::getInt(this, i18n("Customized DSS Download"),
1343                                       i18n("Specify image height (arcminutes): "), 15, 15, 75, 1, &ok);
1344     QStringList strList = (QStringList() << "poss2ukstu_blue"
1345                            << "poss2ukstu_red"
1346                            << "poss2ukstu_ir"
1347                            << "poss1_blue"
1348                            << "poss1_red"
1349                            << "quickv"
1350                            << "all");
1351     QString version =
1352         QInputDialog::getItem(this, i18n("Customized DSS Download"), i18n("Specify version: "), strList, 0, false, &ok);
1353 
1354     QUrl srcUrl(KSDssDownloader::getDSSURL(currentObject()->ra0(), currentObject()->dec0(), width, height, "gif",
1355                                            version, &md));
1356 
1357     delete m_dl;
1358     m_dl = new KSDssDownloader();
1359     connect(m_dl, SIGNAL(downloadComplete(bool)), SLOT(downloadReady(bool)));
1360     m_dl->startSingleDownload(srcUrl, getCurrentImagePath(), md);
1361 }
1362 
1363 void ObservingList::slotGetImage(bool _dss, const SkyObject *o)
1364 {
1365     dss = _dss;
1366     if (!o)
1367         o = currentObject();
1368     ui->SearchImage->setEnabled(false);
1369     setCurrentImage(o);
1370     QString currentImagePath = getCurrentImagePath();
1371     if (QFile::exists(currentImagePath))
1372         QFile::remove(currentImagePath);
1373     //QUrl url;
1374     dss = true;
1375     std::function<void(bool)> slot = std::bind(&ObservingList::downloadReady, this, std::placeholders::_1);
1376     new KSDssDownloader(o, currentImagePath, slot, this);
1377 }
1378 
1379 void ObservingList::downloadReady(bool success)
1380 {
1381     // set downloadJob to 0, but don't delete it - the job will be deleted automatically
1382     //    downloadJob = 0;
1383 
1384     delete m_dl;
1385     m_dl = nullptr; // required if we came from slotCustomDSS; does nothing otherwise
1386 
1387     if (!success)
1388     {
1389         KSNotification::sorry(i18n("Failed to download DSS/SDSS image."));
1390     }
1391     else
1392     {
1393         /*
1394           if( QFile( QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath(m_currentImageFileName) ).size() > 13000)
1395           //The default image is around 8689 bytes
1396         */
1397         //ui->ImagePreview->showPreview( QUrl::fromLocalFile( getCurrentImagePath() ) );
1398         ui->ImagePreview->setPixmap(QPixmap(getCurrentImagePath()).scaledToHeight(ui->ImagePreview->width()));
1399         saveThumbImage();
1400         ui->ImagePreview->show();
1401         ui->ImagePreview->setCursor(Qt::PointingHandCursor);
1402         ui->DeleteImage->setEnabled(true);
1403     }
1404     /*
1405     // FIXME: Implement a priority order SDSS > DSS in the DSS downloader
1406     else if( ! dss )
1407         slotGetImage( true );
1408     */
1409 }
1410 
1411 void ObservingList::setCurrentImage(const SkyObject *o)
1412 {
1413     QString sanitizedName = o->name().remove(' ').remove('\'').remove('\"').toLower();
1414 
1415     // JM: Always use .png across all platforms. No JPGs at all?
1416     m_currentImageFileName = "image-" + sanitizedName + ".png";
1417 
1418     m_currentThumbImageFileName = "thumb-" + sanitizedName + ".png";
1419 
1420     // Does full image exists in the path?
1421     QString currentImagePath = KSPaths::locate(QStandardPaths::AppLocalDataLocation, m_currentImageFileName);
1422 
1423     // Let's try to fallback to thumb-* images if they exist
1424     if (currentImagePath.isEmpty())
1425     {
1426         currentImagePath = KSPaths::locate(QStandardPaths::AppLocalDataLocation, m_currentThumbImageFileName);
1427 
1428         // If thumb image exists, let's use it
1429         if (currentImagePath.isEmpty() == false)
1430             m_currentImageFileName = m_currentThumbImageFileName;
1431     }
1432 
1433     // 2017-04-14: Unnamed stars already unsupported in observing list
1434     /*
1435     if( o->name() == "star" )
1436     {
1437         QString RAString( o->ra0().toHMSString() );
1438         QString DecString( o->dec0().toDMSString() );
1439         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.
1440         // Unnecessary complication below:
1441         // QChar decsgn = ( (o->dec0().Degrees() < 0.0 ) ? '-' : '+' );
1442         // m_currentImageFileName = m_currentImageFileName.remove('+').remove('-') + decsgn;
1443     }
1444     */
1445 
1446     // 2017-04-14 JM: If we use .png always, let us use it across all platforms.
1447     /*
1448     QString imagePath = getCurrentImagePath();
1449     if ( QFile::exists( imagePath))   // New convention -- append filename extension so file is usable on Windows etc.
1450     {
1451         QFile::rename( imagePath, imagePath + ".png" );
1452     }
1453     m_currentImageFileName += ".png";
1454     */
1455 }
1456 
1457 QString ObservingList::getCurrentImagePath()
1458 {
1459     QString currentImagePath = KSPaths::locate(QStandardPaths::AppLocalDataLocation, m_currentImageFileName);
1460     if (QFile::exists(currentImagePath))
1461     {
1462         return currentImagePath;
1463     }
1464     else
1465         return QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath(m_currentImageFileName);
1466 }
1467 
1468 void ObservingList::slotSaveAllImages()
1469 {
1470     ui->SearchImage->setEnabled(false);
1471     ui->DeleteImage->setEnabled(false);
1472     m_CurrentObject = nullptr;
1473     //Clear the selection in the Tables
1474     ui->WishListView->clearSelection();
1475     ui->SessionView->clearSelection();
1476 
1477     foreach (QSharedPointer<SkyObject> o, getActiveList())
1478     {
1479         if (!o)
1480             continue; // FIXME: Why would we have null objects? But appears that we do.
1481         setCurrentImage(o.data());
1482         QString img(getCurrentImagePath());
1483         //        QUrl url( ( Options::obsListPreferDSS() ) ? DSSUrl : SDSSUrl ); // FIXME: We have removed SDSS support!
1484         QUrl url(KSDssDownloader::getDSSURL(o.data()));
1485         if (!o->isSolarSystem()) //TODO find a way for adding support for solar system images
1486             saveImage(url, img, o.data());
1487     }
1488 }
1489 
1490 void ObservingList::saveImage(QUrl /*url*/, QString /*filename*/, const SkyObject *o)
1491 {
1492     if (!o)
1493         o = currentObject();
1494     Q_ASSERT(o);
1495     if (!QFile::exists(getCurrentImagePath()))
1496     {
1497         // Call the DSS downloader
1498         slotGetImage(true, o);
1499     }
1500 }
1501 
1502 void ObservingList::slotImageViewer()
1503 {
1504     QPointer<ImageViewer> iv;
1505     QString currentImagePath = getCurrentImagePath();
1506     if (QFile::exists(currentImagePath))
1507     {
1508         QUrl url = QUrl::fromLocalFile(currentImagePath);
1509         iv       = new ImageViewer(url);
1510     }
1511 
1512     if (iv)
1513         iv->show();
1514 }
1515 
1516 void ObservingList::slotDeleteAllImages()
1517 {
1518     if (KMessageBox::warningYesNo(nullptr, i18n("This will delete all saved images. Are you sure you want to do this?"),
1519                                   i18n("Delete All Images")) == KMessageBox::No)
1520         return;
1521     ui->ImagePreview->setCursor(Qt::ArrowCursor);
1522     ui->SearchImage->setEnabled(false);
1523     ui->DeleteImage->setEnabled(false);
1524     m_CurrentObject = nullptr;
1525     //Clear the selection in the Tables
1526     ui->WishListView->clearSelection();
1527     ui->SessionView->clearSelection();
1528     //ui->ImagePreview->clearPreview();
1529     ui->ImagePreview->setPixmap(QPixmap());
1530     QDirIterator iterator(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
1531     while (iterator.hasNext())
1532     {
1533         // TODO: Probably, there should be a different directory for cached images in the observing list.
1534         if (iterator.fileName().contains("Image") && (!iterator.fileName().contains("dat")) &&
1535                 (!iterator.fileName().contains("obslist")))
1536         {
1537             QFile file(iterator.filePath());
1538             file.remove();
1539         }
1540         iterator.next();
1541     }
1542 }
1543 
1544 void ObservingList::setSaveImagesButton()
1545 {
1546     ui->saveImages->setEnabled(!getActiveList().isEmpty());
1547 }
1548 
1549 // FIXME: Is there a reason to implement these as an event filter,
1550 // instead of as a signal-slot connection? Shouldn't we just use slots
1551 // to subscribe to various events from the Table / Session view?
1552 //
1553 // NOTE: ui->ImagePreview is a QLabel, which has no clicked() event or
1554 // public mouseReleaseEvent(), so eventFilter makes sense.
1555 bool ObservingList::eventFilter(QObject *obj, QEvent *event)
1556 {
1557     if (obj == ui->ImagePreview)
1558     {
1559         if (event->type() == QEvent::MouseButtonRelease)
1560         {
1561             if (currentObject())
1562             {
1563                 if (!QFile::exists(getCurrentImagePath()))
1564                 {
1565                     if (!currentObject()->isSolarSystem())
1566                         slotGetImage(Options::obsListPreferDSS());
1567                     else
1568                         slotSearchImage();
1569                 }
1570                 else
1571                     slotImageViewer();
1572             }
1573             return true;
1574         }
1575     }
1576     if (obj == ui->WishListView->viewport() || obj == ui->SessionView->viewport())
1577     {
1578         bool sessionViewEvent = (obj == ui->SessionView->viewport());
1579 
1580         if (event->type() == QEvent::MouseButtonRelease) // Mouse button release event
1581         {
1582             QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
1583             QPoint pos(mouseEvent->globalX(), mouseEvent->globalY());
1584 
1585             if (mouseEvent->button() == Qt::RightButton)
1586             {
1587                 if (!noSelection)
1588                 {
1589                     pmenu->initPopupMenu(sessionViewEvent, !singleSelection, showScope);
1590                     pmenu->popup(pos);
1591                 }
1592                 return true;
1593             }
1594         }
1595     }
1596 
1597     if (obj == ui->WishListView || obj == ui->SessionView)
1598     {
1599         if (event->type() == QEvent::KeyPress)
1600         {
1601             QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
1602             if (keyEvent->key() == Qt::Key_Delete)
1603             {
1604                 slotRemoveSelectedObjects();
1605                 return true;
1606             }
1607         }
1608     }
1609 
1610     return false;
1611 }
1612 
1613 void ObservingList::slotSearchImage()
1614 {
1615     QPixmap *pm                  = new QPixmap(":/images/noimage.png");
1616     QPointer<ThumbnailPicker> tp = new ThumbnailPicker(currentObject(), *pm, this, 200, 200, i18n("Image Chooser"));
1617     if (tp->exec() == QDialog::Accepted)
1618     {
1619         QString currentImagePath = getCurrentImagePath();
1620         QFile f(currentImagePath);
1621 
1622         //If a real image was set, save it.
1623         if (tp->imageFound())
1624         {
1625             const auto image = *tp->image();
1626             image.save(f.fileName(), "PNG");
1627             //ui->ImagePreview->showPreview( QUrl::fromLocalFile( f.fileName() ) );
1628             saveThumbImage();
1629             slotNewSelection();
1630             ui->ImagePreview->setPixmap(image.scaledToHeight(ui->ImagePreview->width()));
1631             ui->ImagePreview->repaint();
1632         }
1633     }
1634     delete pm;
1635     delete tp;
1636 }
1637 
1638 void ObservingList::slotDeleteCurrentImage()
1639 {
1640     QFile::remove(getCurrentImagePath());
1641     ImagePreviewHash.remove(m_CurrentObject);
1642     slotNewSelection();
1643 }
1644 
1645 void ObservingList::saveThumbImage()
1646 {
1647     QFileInfo const f(QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath(
1648                           m_currentThumbImageFileName));
1649     if (!f.exists())
1650     {
1651         QImage img(getCurrentImagePath());
1652         img = img.scaled(200, 200, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
1653         img.save(f.filePath());
1654     }
1655 }
1656 
1657 QString ObservingList::getTime(const SkyObject *o) const
1658 {
1659     return TimeHash.value(o->name(), QTime(30, 0, 0)).toString("h:mm:ss AP");
1660 }
1661 
1662 QTime ObservingList::scheduledTime(SkyObject *o) const
1663 {
1664     return TimeHash.value(o->name(), o->transitTime(dt, geo));
1665 }
1666 
1667 void ObservingList::setTime(const SkyObject *o, QTime t)
1668 {
1669     TimeHash.insert(o->name(), t);
1670 }
1671 
1672 void ObservingList::slotOALExport()
1673 {
1674     slotSaveSessionAs(false);
1675 }
1676 
1677 void ObservingList::slotAddVisibleObj()
1678 {
1679     KStarsDateTime lt = dt;
1680     lt.setTime(QTime(8, 0, 0));
1681     QPointer<WUTDialog> w = new WUTDialog(KStars::Instance(), sessionView, geo, lt);
1682     w->init();
1683     QModelIndexList selectedItems;
1684     selectedItems =
1685         m_WishListSortModel->mapSelectionToSource(ui->WishListView->selectionModel()->selection()).indexes();
1686     if (selectedItems.size())
1687     {
1688         foreach (const QModelIndex &i, selectedItems)
1689         {
1690             foreach (QSharedPointer<SkyObject> o, obsList())
1691                 if (getObjectName(o.data()) == i.data().toString() && w->checkVisibility(o.data()))
1692                     slotAddObject(
1693                         o.data(),
1694                         true); // FIXME: Better if there is a QSharedPointer override for this, although the check will ensure that we don't duplicate.
1695         }
1696     }
1697     delete w;
1698 }
1699 
1700 SkyObject *ObservingList::findObjectByName(QString name)
1701 {
1702     foreach (QSharedPointer<SkyObject> o, sessionList())
1703     {
1704         if (getObjectName(o.data(), false) == name)
1705             return o.data();
1706     }
1707     return nullptr;
1708 }
1709 
1710 void ObservingList::selectObject(const SkyObject *o)
1711 {
1712     ui->tabWidget->setCurrentIndex(1);
1713     ui->SessionView->selectionModel()->clear();
1714     for (int irow = m_SessionModel->rowCount() - 1; irow >= 0; --irow)
1715     {
1716         QModelIndex mSortIndex = m_SessionSortModel->index(irow, 0);
1717         QModelIndex mIndex     = m_SessionSortModel->mapToSource(mSortIndex);
1718         int idxrow             = mIndex.row();
1719         if (m_SessionModel->item(idxrow, 0)->text() == getObjectName(o))
1720             ui->SessionView->selectRow(idxrow);
1721         slotNewSelection();
1722     }
1723 }
1724 
1725 void ObservingList::setDefaultImage()
1726 {
1727     ui->ImagePreview->setPixmap(m_NoImagePixmap);
1728     ui->ImagePreview->update();
1729 }
1730 
1731 QString ObservingList::getObjectName(const SkyObject *o, bool translated)
1732 {
1733     QString finalObjectName;
1734     if (o->name() == "star")
1735     {
1736         const StarObject *s = dynamic_cast<const StarObject *>(o);
1737 
1738         // JM: Enable HD Index stars to be added to the observing list.
1739         if (s != nullptr && s->getHDIndex() != 0)
1740             finalObjectName = QString("HD %1").arg(QString::number(s->getHDIndex()));
1741     }
1742     else
1743         finalObjectName = translated ? o->translatedName() : o->name();
1744 
1745     return finalObjectName;
1746 }
1747 
1748 void ObservingList::slotUpdateAltitudes()
1749 {
1750     // FIXME: Update upon gaining visibility, do not update when not visible
1751     KStarsDateTime now = KStarsDateTime::currentDateTimeUtc();
1752     //    qCDebug(KSTARS) << "Updating altitudes in observation planner @ JD - J2000 = " << double( now.djd() - J2000 );
1753     for (int irow = m_WishListModel->rowCount() - 1; irow >= 0; --irow)
1754     {
1755         QModelIndex idx = m_WishListSortModel->mapToSource(m_WishListSortModel->index(irow, 0));
1756         SkyObject *o    = static_cast<SkyObject *>(idx.data(Qt::UserRole + 1).value<void *>());
1757         Q_ASSERT(o);
1758         SkyPoint p = o->recomputeHorizontalCoords(now, geo);
1759         idx =
1760             m_WishListSortModel->mapToSource(m_WishListSortModel->index(irow, m_WishListSortModel->columnCount() - 1));
1761         QStandardItem *replacement = m_altCostHelper(p);
1762         m_WishListModel->setData(idx, replacement->data(Qt::DisplayRole), Qt::DisplayRole);
1763         m_WishListModel->setData(idx, replacement->data(Qt::UserRole), Qt::UserRole);
1764         delete replacement;
1765     }
1766     emit m_WishListModel->dataChanged(
1767         m_WishListModel->index(0, m_WishListModel->columnCount() - 1),
1768         m_WishListModel->index(m_WishListModel->rowCount() - 1, m_WishListModel->columnCount() - 1));
1769 }
1770 
1771 QSharedPointer<SkyObject> ObservingList::findObject(const SkyObject *o, bool session)
1772 {
1773     const QList<QSharedPointer<SkyObject>> &list = (session ? sessionList() : obsList());
1774     const QString &target                        = getObjectName(o);
1775     foreach (QSharedPointer<SkyObject> obj, list)
1776     {
1777         if (getObjectName(obj.data()) == target)
1778             return obj;
1779     }
1780     return QSharedPointer<SkyObject>(); // null pointer
1781 }