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