1 /*
2     SPDX-FileCopyrightText: 2001 Jason Harris <jharris@30doradus.org>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "skyobjectuserdata.h"
8 #ifdef _WIN32
9 #include <windows.h>
10 #endif
11 
12 #include "skymap.h"
13 
14 #include "ksasteroid.h"
15 #include "kstars_debug.h"
16 #include "fov.h"
17 #include "imageviewer.h"
18 #include "xplanetimageviewer.h"
19 #include "ksdssdownloader.h"
20 #include "kspaths.h"
21 #include "kspopupmenu.h"
22 #include "kstars.h"
23 #include "ksutils.h"
24 #include "Options.h"
25 #include "skymapcomposite.h"
26 #ifdef HAVE_OPENGL
27 #include "skymapgldraw.h"
28 #endif
29 #include "skymapqdraw.h"
30 #include "starhopperdialog.h"
31 #include "starobject.h"
32 #include "texturemanager.h"
33 #include "dialogs/detaildialog.h"
34 #include "printing/printingwizard.h"
35 #include "skycomponents/flagcomponent.h"
36 #include "skyobjects/ksplanetbase.h"
37 #include "skyobjects/satellite.h"
38 #include "tools/flagmanager.h"
39 #include "widgets/infoboxwidget.h"
40 #include "projections/azimuthalequidistantprojector.h"
41 #include "projections/equirectangularprojector.h"
42 #include "projections/lambertprojector.h"
43 #include "projections/gnomonicprojector.h"
44 #include "projections/orthographicprojector.h"
45 #include "projections/stereographicprojector.h"
46 #include "catalogobject.h"
47 #include "catalogsdb.h"
48 #include "catalogscomponent.h"
49 
50 #include <KActionCollection>
51 #include <KToolBar>
52 
53 #include <QBitmap>
54 #include <QToolTip>
55 #include <QClipboard>
56 #include <QInputDialog>
57 #include <QDesktopServices>
58 
59 #include <QProcess>
60 #include <QFileDialog>
61 
62 #include <cmath>
63 
64 namespace
65 {
66 // Draw bitmap for zoom cursor. Width is size of pen to draw with.
zoomCursorBitmap(int width)67 QBitmap zoomCursorBitmap(int width)
68 {
69     QBitmap b(32, 32);
70     b.fill(Qt::color0);
71     int mx = 16, my = 16;
72     // Begin drawing
73     QPainter p;
74     p.begin(&b);
75     p.setPen(QPen(Qt::color1, width));
76     p.drawEllipse(mx - 7, my - 7, 14, 14);
77     p.drawLine(mx + 5, my + 5, mx + 11, my + 11);
78     p.end();
79     return b;
80 }
81 
82 // Draw bitmap for default cursor. Width is size of pen to draw with.
defaultCursorBitmap(int width)83 QBitmap defaultCursorBitmap(int width)
84 {
85     QBitmap b(32, 32);
86     b.fill(Qt::color0);
87     int mx = 16, my = 16;
88     // Begin drawing
89     QPainter p;
90     p.begin(&b);
91     p.setPen(QPen(Qt::color1, width));
92     // 1. diagonal
93     p.drawLine(mx - 2, my - 2, mx - 8, mx - 8);
94     p.drawLine(mx + 2, my + 2, mx + 8, mx + 8);
95     // 2. diagonal
96     p.drawLine(mx - 2, my + 2, mx - 8, mx + 8);
97     p.drawLine(mx + 2, my - 2, mx + 8, mx - 8);
98     p.end();
99     return b;
100 }
101 
circleCursorBitmap(int width)102 QBitmap circleCursorBitmap(int width)
103 {
104     QBitmap b(32, 32);
105     b.fill(Qt::color0);
106     int mx = 16, my = 16;
107     // Begin drawing
108     QPainter p;
109     p.begin(&b);
110     p.setPen(QPen(Qt::color1, width));
111 
112     // Circle
113     p.drawEllipse(mx - 8, my - 8, mx, my);
114     // 1. diagonal
115     p.drawLine(mx - 8, my - 8, 0, 0);
116     p.drawLine(mx + 8, my - 8, 32, 0);
117     // 2. diagonal
118     p.drawLine(mx - 8, my + 8, 0, 32);
119     p.drawLine(mx + 8, my + 8, 32, 32);
120 
121     p.end();
122     return b;
123 }
124 
125 } // namespace
126 
127 SkyMap *SkyMap::pinstance = nullptr;
128 
Create()129 SkyMap *SkyMap::Create()
130 {
131     delete pinstance;
132     pinstance = new SkyMap();
133     return pinstance;
134 }
135 
Instance()136 SkyMap *SkyMap::Instance()
137 {
138     return pinstance;
139 }
140 
SkyMap()141 SkyMap::SkyMap()
142     : QGraphicsView(KStars::Instance()), computeSkymap(true), rulerMode(false), data(KStarsData::Instance()), pmenu(nullptr),
143       ClickedObject(nullptr), FocusObject(nullptr), m_proj(nullptr), m_previewLegend(false), m_objPointingMode(false)
144 {
145 #if !defined(KSTARS_LITE)
146     grabGesture(Qt::PinchGesture);
147     grabGesture(Qt::TapAndHoldGesture);
148 #endif
149     m_Scale = 1.0;
150 
151     ZoomRect = QRect();
152 
153     // set the default cursor
154     setMouseCursorShape(static_cast<Cursor>(Options::defaultCursor()));
155 
156     QPalette p = palette();
157     p.setColor(QPalette::Window, QColor(data->colorScheme()->colorNamed("SkyColor")));
158     setPalette(p);
159 
160     setFocusPolicy(Qt::StrongFocus);
161     setMinimumSize(380, 250);
162     setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding));
163     setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
164     setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
165     setStyleSheet("QGraphicsView { border-style: none; }");
166 
167     setMouseTracking(true); //Generate MouseMove events!
168     midMouseButtonDown = false;
169     mouseButtonDown    = false;
170     slewing            = false;
171     clockSlewing       = false;
172 
173     ClickedObject = nullptr;
174     FocusObject   = nullptr;
175 
176     m_SkyMapDraw = nullptr;
177 
178     pmenu = new KSPopupMenu();
179 
180     setupProjector();
181 
182     //Initialize Transient label stuff
183     m_HoverTimer.setSingleShot(true); // using this timer as a single shot timer
184 
185     connect(&m_HoverTimer, SIGNAL(timeout()), this, SLOT(slotTransientLabel()));
186     connect(this, SIGNAL(destinationChanged()), this, SLOT(slewFocus()));
187     connect(KStarsData::Instance(), SIGNAL(skyUpdate(bool)), this, SLOT(slotUpdateSky(bool)));
188 
189     // Time infobox
190     m_timeBox = new InfoBoxWidget(Options::shadeTimeBox(), Options::positionTimeBox(), Options::stickyTimeBox(),
191                                   QStringList(), this);
192     m_timeBox->setVisible(Options::showTimeBox());
193     connect(data->clock(), SIGNAL(timeChanged()), m_timeBox, SLOT(slotTimeChanged()));
194     connect(data->clock(), SIGNAL(timeAdvanced()), m_timeBox, SLOT(slotTimeChanged()));
195 
196     // Geo infobox
197     m_geoBox = new InfoBoxWidget(Options::shadeGeoBox(), Options::positionGeoBox(), Options::stickyGeoBox(),
198                                  QStringList(), this);
199     m_geoBox->setVisible(Options::showGeoBox());
200     connect(data, SIGNAL(geoChanged()), m_geoBox, SLOT(slotGeoChanged()));
201 
202     // Object infobox
203     m_objBox = new InfoBoxWidget(Options::shadeFocusBox(), Options::positionFocusBox(), Options::stickyFocusBox(),
204                                  QStringList(), this);
205     m_objBox->setVisible(Options::showFocusBox());
206     connect(this, SIGNAL(objectChanged(SkyObject*)), m_objBox, SLOT(slotObjectChanged(SkyObject*)));
207     connect(this, SIGNAL(positionChanged(SkyPoint*)), m_objBox, SLOT(slotPointChanged(SkyPoint*)));
208 
209     m_SkyMapDraw = new SkyMapQDraw(this);
210     m_SkyMapDraw->setMouseTracking(true);
211 
212     m_SkyMapDraw->setParent(this->viewport());
213     m_SkyMapDraw->show();
214 
215     m_iboxes = new InfoBoxes(m_SkyMapDraw);
216 
217     m_iboxes->setVisible(Options::showInfoBoxes());
218     m_iboxes->addInfoBox(m_timeBox);
219     m_iboxes->addInfoBox(m_geoBox);
220     m_iboxes->addInfoBox(m_objBox);
221 }
222 
slotToggleGeoBox(bool flag)223 void SkyMap::slotToggleGeoBox(bool flag)
224 {
225     m_geoBox->setVisible(flag);
226 }
227 
slotToggleFocusBox(bool flag)228 void SkyMap::slotToggleFocusBox(bool flag)
229 {
230     m_objBox->setVisible(flag);
231 }
232 
slotToggleTimeBox(bool flag)233 void SkyMap::slotToggleTimeBox(bool flag)
234 {
235     m_timeBox->setVisible(flag);
236 }
237 
slotToggleInfoboxes(bool flag)238 void SkyMap::slotToggleInfoboxes(bool flag)
239 {
240     m_iboxes->setVisible(flag);
241     Options::setShowInfoBoxes(flag);
242 }
243 
~SkyMap()244 SkyMap::~SkyMap()
245 {
246     /* == Save infoxes status into Options == */
247     //Options::setShowInfoBoxes(m_iboxes->isVisibleTo(parentWidget()));
248     // Time box
249     Options::setPositionTimeBox(m_timeBox->pos());
250     Options::setShadeTimeBox(m_timeBox->shaded());
251     Options::setStickyTimeBox(m_timeBox->sticky());
252     Options::setShowTimeBox(m_timeBox->isVisibleTo(m_iboxes));
253     // Geo box
254     Options::setPositionGeoBox(m_geoBox->pos());
255     Options::setShadeGeoBox(m_geoBox->shaded());
256     Options::setStickyGeoBox(m_geoBox->sticky());
257     Options::setShowGeoBox(m_geoBox->isVisibleTo(m_iboxes));
258     // Obj box
259     Options::setPositionFocusBox(m_objBox->pos());
260     Options::setShadeFocusBox(m_objBox->shaded());
261     Options::setStickyFocusBox(m_objBox->sticky());
262     Options::setShowFocusBox(m_objBox->isVisibleTo(m_iboxes));
263 
264     //store focus values in Options
265     //If not tracking and using Alt/Az coords, stor the Alt/Az coordinates
266     if (Options::useAltAz() && !Options::isTracking())
267     {
268         Options::setFocusRA(focus()->az().Degrees());
269         Options::setFocusDec(focus()->alt().Degrees());
270     }
271     else
272     {
273         Options::setFocusRA(focus()->ra().Hours());
274         Options::setFocusDec(focus()->dec().Degrees());
275     }
276 
277 #ifdef HAVE_OPENGL
278     delete m_SkyMapGLDraw;
279     delete m_SkyMapQDraw;
280     m_SkyMapDraw = 0; // Just a formality
281 #else
282     delete m_SkyMapDraw;
283 #endif
284 
285     delete pmenu;
286 
287     delete m_proj;
288 
289     pinstance = nullptr;
290 }
291 
showFocusCoords()292 void SkyMap::showFocusCoords()
293 {
294     if (focusObject() && Options::isTracking())
295         emit objectChanged(focusObject());
296     else
297         emit positionChanged(focus());
298 }
299 
slotTransientLabel()300 void SkyMap::slotTransientLabel()
301 {
302     //This function is only called if the HoverTimer manages to timeout.
303     //(HoverTimer is restarted with every mouseMoveEvent; so if it times
304     //out, that means there was no mouse movement for HOVER_INTERVAL msec.)
305     if (hasFocus() && !slewing &&
306             !(Options::useAltAz() && Options::showGround() && m_MousePoint.altRefracted().Degrees() < 0.0))
307     {
308         double maxrad = 1000.0 / Options::zoomFactor();
309         SkyObject *so = data->skyComposite()->objectNearest(&m_MousePoint, maxrad);
310 
311         if (so && !isObjectLabeled(so))
312         {
313             QString name = so->translatedLongName();
314             if (!std::isnan(so->mag()))
315                 name += QString(": %1<sup>m</sup>").arg(QString::number(so->mag(), 'f', 1));
316             QToolTip::showText(QCursor::pos(), name, this);
317         }
318     }
319 }
320 
321 //Slots
322 
setClickedObject(SkyObject * o)323 void SkyMap::setClickedObject(SkyObject *o)
324 {
325     ClickedObject = o;
326 }
327 
setFocusObject(SkyObject * o)328 void SkyMap::setFocusObject(SkyObject *o)
329 {
330     FocusObject = o;
331     if (FocusObject)
332         Options::setFocusObject(FocusObject->name());
333     else
334         Options::setFocusObject(i18n("nothing"));
335 }
336 
slotCenter()337 void SkyMap::slotCenter()
338 {
339     KStars *kstars        = KStars::Instance();
340     TrailObject *trailObj = dynamic_cast<TrailObject *>(focusObject());
341 
342     SkyPoint *foc;
343     if(ClickedObject != nullptr)
344         foc = ClickedObject;
345     else
346         foc = &ClickedPoint;
347 
348     if (Options::useAltAz())
349     {
350         // JM 2016-09-12: Following call has problems when ra0/dec0 of an object are not valid for example
351         // because they're solar system bodies. So it creates a lot of issues. It is disabled and centering
352         // works correctly for all different body types as I tested.
353         //DeepSkyObject *dso = dynamic_cast<DeepSkyObject *>(focusObject());
354         //if (dso)
355         //    foc->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false);
356 
357         // JM 2018-05-06: No need to do the above
358         foc->EquatorialToHorizontal(data->lst(), data->geo()->lat());
359     }
360     else
361         foc->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false);
362 
363     qCDebug(KSTARS) << "Centering on " << foc->ra().toHMSString() << foc->dec().toDMSString();
364 
365     //clear the planet trail of old focusObject, if it was temporary
366     if (trailObj && data->temporaryTrail)
367     {
368         trailObj->clearTrail();
369         data->temporaryTrail = false;
370     }
371 
372     //If the requested object is below the opaque horizon, issue a warning message
373     //(unless user is already pointed below the horizon)
374     if (Options::useAltAz() && Options::showGround() &&
375             focus()->alt().Degrees() > SkyPoint::altCrit &&
376             foc->alt().Degrees() <= SkyPoint::altCrit)
377     {
378         QString caption = i18n("Requested Position Below Horizon");
379         QString message = i18n("The requested position is below the horizon.\nWould you like to go there anyway?");
380         if (KMessageBox::warningYesNo(this, message, caption, KGuiItem(i18n("Go Anyway")),
381                                       KGuiItem(i18n("Keep Position")), "dag_focus_below_horiz") == KMessageBox::No)
382         {
383             setClickedObject(nullptr);
384             setFocusObject(nullptr);
385             Options::setIsTracking(false);
386 
387             return;
388         }
389     }
390 
391     //set FocusObject before slewing.  Otherwise, KStarsData::updateTime() can reset
392     //destination to previous object...
393     setFocusObject(ClickedObject);
394     if(ClickedObject == nullptr)
395         setFocusPoint(&ClickedPoint);
396 
397     Options::setIsTracking(true);
398 
399     if (kstars)
400     {
401         kstars->actionCollection()
402         ->action("track_object")
403         ->setIcon(QIcon::fromTheme("document-encrypt"));
404         kstars->actionCollection()->action("track_object")->setText(i18n("Stop &Tracking"));
405     }
406 
407     //If focusObject is a SS body and doesn't already have a trail, set the temporaryTrail
408 
409     if (Options::useAutoTrail() && trailObj && trailObj->hasTrail())
410     {
411         trailObj->addToTrail();
412         data->temporaryTrail = true;
413     }
414 
415     //update the destination to the selected coordinates
416     if (Options::useAltAz())
417     {
418         setDestinationAltAz(foc->alt(), foc->az(), false);
419     }
420     else
421     {
422         setDestination(*foc);
423     }
424 
425     foc->EquatorialToHorizontal(data->lst(), data->geo()->lat());
426 
427     //display coordinates in statusBar
428     emit mousePointChanged(foc);
429     showFocusCoords(); //update FocusBox
430 }
431 
slotUpdateSky(bool now)432 void SkyMap::slotUpdateSky(bool now)
433 {
434     // Code moved from KStarsData::updateTime()
435     //Update focus
436     updateFocus();
437 
438     if (now)
439         QTimer::singleShot(
440             0, this,
441             SLOT(forceUpdateNow())); // Why is it done this way rather than just calling forceUpdateNow()? -- asimha // --> Opening a neww thread? -- Valentin
442     else
443         forceUpdate();
444 }
445 
slotDSS()446 void SkyMap::slotDSS()
447 {
448     dms ra(0.0), dec(0.0);
449     QString urlstring;
450 
451     //ra and dec must be the coordinates at J2000.  If we clicked on an object, just use the object's ra0, dec0 coords
452     //if we clicked on empty sky, we need to precess to J2000.
453     if (clickedObject())
454     {
455         urlstring = KSDssDownloader::getDSSURL(clickedObject());
456     }
457     else
458     {
459         //SkyPoint deprecessedPoint = clickedPoint()->deprecess(data->updateNum());
460         SkyPoint deprecessedPoint = clickedPoint()->catalogueCoord(data->updateNum()->julianDay());
461         ra                        = deprecessedPoint.ra();
462         dec                       = deprecessedPoint.dec();
463         urlstring                 = KSDssDownloader::getDSSURL(ra, dec); // Use default size for non-objects
464     }
465 
466     QUrl url(urlstring);
467 
468     KStars *kstars = KStars::Instance();
469     if (kstars)
470     {
471         new ImageViewer(
472             url, i18n("Digitized Sky Survey image provided by the Space Telescope Science Institute [free for non-commercial use]."),
473             this);
474         //iv->show();
475     }
476 }
477 
slotCopyCoordinates()478 void SkyMap::slotCopyCoordinates()
479 {
480     dms J2000RA(0.0), J2000DE(0.0), JNowRA(0.0), JNowDE(0.0), Az, Alt;
481     if (clickedObject())
482     {
483         J2000RA  = clickedObject()->ra0();
484         J2000DE = clickedObject()->dec0();
485         JNowRA = clickedObject()->ra();
486         JNowDE = clickedObject()->dec();
487         Az = clickedObject()->az();
488         Alt = clickedObject()->alt();
489     }
490     else
491     {
492         // Empty point only have valid JNow RA/DE, not J2000 information.
493         SkyPoint emptyPoint = *clickedPoint();
494         // Now get J2000 from JNow but de-aberrating, de-nutating, de-preccessing
495         // This modifies emptyPoint, but the RA/DE are now missing and need
496         // to be repopulated.
497         emptyPoint.catalogueCoord(data->updateNum()->julianDay());
498         emptyPoint.setRA(clickedPoint()->ra());
499         emptyPoint.setDec(clickedPoint()->dec());
500         emptyPoint.EquatorialToHorizontal(data->lst(), data->geo()->lat());
501 
502         J2000RA = emptyPoint.ra0();
503         J2000DE = emptyPoint.dec0();
504         JNowRA = emptyPoint.ra();
505         JNowDE = emptyPoint.dec();
506         Az = emptyPoint.az();
507         Alt = emptyPoint.alt();
508     }
509 
510     QApplication::clipboard()->setText(i18nc("Equatorial & Horizontal Coordinates",
511                                        "JNow:\t%1\t%2\nJ2000:\t%3\t%4\nAzAlt:\t%5\t%6",
512                                        JNowRA.toHMSString(),
513                                        JNowDE.toDMSString(),
514                                        J2000RA.toHMSString(),
515                                        J2000DE.toDMSString(),
516                                        Az.toDMSString(),
517                                        Alt.toDMSString()));
518 }
519 
520 
slotCopyTLE()521 void SkyMap::slotCopyTLE()
522 {
523 
524     QString tle = "";
525     if (clickedObject()->type() == SkyObject::SATELLITE)
526     {
527         Satellite *sat = (Satellite *) clickedObject();
528         tle = sat->tle();
529     }
530     else
531     {
532         tle = "NO TLE FOR OBJECT";
533     }
534 
535 
536     QApplication::clipboard()->setText(tle);
537 }
538 
slotSDSS()539 void SkyMap::slotSDSS()
540 {
541     // TODO: Remove code duplication -- we have the same stuff
542     // implemented in ObservingList::setCurrentImage() etc. in
543     // tools/observinglist.cpp; must try to de-duplicate as much as
544     // possible.
545     QString URLprefix("http://skyserver.sdss.org/dr16/SkyServerWS/ImgCutout/getjpeg?");
546     QString URLsuffix("&scale=1.0&width=600&height=600");
547     dms ra(0.0), dec(0.0);
548     QString RAString, DecString;
549 
550     //ra and dec must be the coordinates at J2000.  If we clicked on an object, just use the object's ra0, dec0 coords
551     //if we clicked on empty sky, we need to precess to J2000.
552     if (clickedObject())
553     {
554         ra  = clickedObject()->ra0();
555         dec = clickedObject()->dec0();
556     }
557     else
558     {
559         //SkyPoint deprecessedPoint = clickedPoint()->deprecess(data->updateNum());
560         SkyPoint deprecessedPoint = clickedPoint()->catalogueCoord(data->updateNum()->julianDay());
561         deprecessedPoint.catalogueCoord(data->updateNum()->julianDay());
562         ra                        = deprecessedPoint.ra();
563         dec                       = deprecessedPoint.dec();
564     }
565 
566     RAString  = QString::asprintf("ra=%f", ra.Degrees());
567     DecString = QString::asprintf("&dec=%f", dec.Degrees());
568 
569     //concat all the segments into the kview command line:
570     QUrl url(URLprefix + RAString + DecString + URLsuffix);
571 
572     KStars *kstars = KStars::Instance();
573     if (kstars)
574     {
575         new ImageViewer(url,
576                         i18n("Sloan Digital Sky Survey image provided by the Astrophysical Research Consortium [free "
577                              "for non-commercial use]."),
578                         this);
579         //iv->show();
580     }
581 }
582 
slotEyepieceView()583 void SkyMap::slotEyepieceView()
584 {
585     KStars::Instance()->slotEyepieceView((clickedObject() ? clickedObject() : clickedPoint()));
586 }
slotBeginAngularDistance()587 void SkyMap::slotBeginAngularDistance()
588 {
589     beginRulerMode(false);
590 }
591 
slotBeginStarHop()592 void SkyMap::slotBeginStarHop()
593 {
594     beginRulerMode(true);
595 }
596 
beginRulerMode(bool starHopRuler)597 void SkyMap::beginRulerMode(bool starHopRuler)
598 {
599     rulerMode         = true;
600     starHopDefineMode = starHopRuler;
601     AngularRuler.clear();
602 
603     //If the cursor is near a SkyObject, reset the AngularRuler's
604     //start point to the position of the SkyObject
605     double maxrad = 1000.0 / Options::zoomFactor();
606     SkyObject *so = data->skyComposite()->objectNearest(clickedPoint(), maxrad);
607     if (so)
608     {
609         AngularRuler.append(so);
610         AngularRuler.append(so);
611         m_rulerStartPoint = so;
612     }
613     else
614     {
615         AngularRuler.append(clickedPoint());
616         AngularRuler.append(clickedPoint());
617         m_rulerStartPoint = clickedPoint();
618     }
619 
620     AngularRuler.update(data);
621 }
622 
slotEndRulerMode()623 void SkyMap::slotEndRulerMode()
624 {
625     if (!rulerMode)
626         return;
627     if (!starHopDefineMode) // Angular Ruler
628     {
629         QString sbMessage;
630 
631         //If the cursor is near a SkyObject, reset the AngularRuler's
632         //end point to the position of the SkyObject
633         double maxrad = 1000.0 / Options::zoomFactor();
634         SkyPoint *rulerEndPoint;
635         SkyObject *so = data->skyComposite()->objectNearest(clickedPoint(), maxrad);
636         if (so)
637         {
638             AngularRuler.setPoint(1, so);
639             sbMessage     = so->translatedLongName() + "   ";
640             rulerEndPoint = so;
641         }
642         else
643         {
644             AngularRuler.setPoint(1, clickedPoint());
645             rulerEndPoint = clickedPoint();
646         }
647 
648         rulerMode = false;
649         AngularRuler.update(data);
650         dms angularDistance = AngularRuler.angularSize();
651 
652         sbMessage += i18n("Angular distance: %1", angularDistance.toDMSString());
653 
654         const StarObject *p1 = dynamic_cast<const StarObject *>(m_rulerStartPoint);
655         const StarObject *p2 = dynamic_cast<const StarObject *>(rulerEndPoint);
656 
657         qCDebug(KSTARS) << "Starobjects? " << p1 << p2;
658         if (p1 && p2)
659             qCDebug(KSTARS) << "Distances: " << p1->distance() << "pc; " << p2->distance() << "pc";
660         if (p1 && p2 && std::isfinite(p1->distance()) && std::isfinite(p2->distance()) && p1->distance() > 0 &&
661                 p2->distance() > 0)
662         {
663             double dist = sqrt(p1->distance() * p1->distance() + p2->distance() * p2->distance() -
664                                2 * p1->distance() * p2->distance() * cos(angularDistance.radians()));
665             qCDebug(KSTARS) << "Could calculate physical distance: " << dist << " pc";
666             sbMessage += i18n("; Physical distance: %1 pc", QString::number(dist));
667         }
668 
669         AngularRuler.clear();
670 
671         // Create unobsructive message box with suicidal tendencies
672         // to display result.
673         InfoBoxWidget *box = new InfoBoxWidget(true, mapFromGlobal(QCursor::pos()), 0, QStringList(sbMessage), this);
674         connect(box, SIGNAL(clicked()), box, SLOT(deleteLater()));
675         QTimer::singleShot(5000, box, SLOT(deleteLater()));
676         box->adjust();
677         box->show();
678     }
679     else // Star Hop
680     {
681         StarHopperDialog *shd    = new StarHopperDialog(this);
682         const SkyPoint &startHop = *AngularRuler.point(0);
683         const SkyPoint &stopHop  = *clickedPoint();
684         double fov; // Field of view in arcminutes
685         bool ok;    // true if user did not cancel the operation
686         if (data->getAvailableFOVs().size() == 1)
687         {
688             // Exactly 1 FOV symbol visible, so use that. Also assume a circular FOV of size min{sizeX, sizeY}
689             FOV *f = data->getAvailableFOVs().first();
690             fov    = ((f->sizeX() >= f->sizeY() && f->sizeY() != 0) ? f->sizeY() : f->sizeX());
691             ok     = true;
692         }
693         else if (!data->getAvailableFOVs().isEmpty())
694         {
695             // Ask the user to choose from a list of available FOVs.
696             FOV const *f;
697             QMap<QString, double> nameToFovMap;
698             foreach (f, data->getAvailableFOVs())
699             {
700                 nameToFovMap.insert(f->name(),
701                                     ((f->sizeX() >= f->sizeY() && f->sizeY() != 0) ? f->sizeY() : f->sizeX()));
702             }
703             fov = nameToFovMap[QInputDialog::getItem(this, i18n("Star Hopper: Choose a field-of-view"),
704                                                            i18n("FOV to use for star hopping:"), nameToFovMap.uniqueKeys(), 0,
705                                                            false, &ok)];
706         }
707         else
708         {
709             // Ask the user to enter a field of view
710             fov =
711                 QInputDialog::getDouble(this, i18n("Star Hopper: Enter field-of-view to use"),
712                                         i18n("FOV to use for star hopping (in arcminutes):"), 60.0, 1.0, 600.0, 1, &ok);
713         }
714 
715         Q_ASSERT(fov > 0.0);
716 
717         if (ok)
718         {
719             qCDebug(KSTARS) << "fov = " << fov;
720 
721             shd->starHop(startHop, stopHop, fov / 60.0, 9.0); //FIXME: Hardcoded maglimit value
722             shd->show();
723         }
724 
725         rulerMode = false;
726     }
727 }
728 
slotCancelRulerMode(void)729 void SkyMap::slotCancelRulerMode(void)
730 {
731     rulerMode = false;
732     AngularRuler.clear();
733 }
734 
slotAddFlag()735 void SkyMap::slotAddFlag()
736 {
737     KStars *ks = KStars::Instance();
738 
739     // popup FlagManager window and update coordinates
740     ks->slotFlagManager();
741     ks->flagManager()->clearFields();
742 
743     //ra and dec must be the coordinates at J2000.  If we clicked on an object, just use the object's ra0, dec0 coords
744     //if we clicked on empty sky, we need to precess to J2000.
745 
746     dms J2000RA, J2000DE;
747 
748     if (clickedObject())
749     {
750         J2000RA = clickedObject()->ra0();
751         J2000DE = clickedObject()->dec0();
752     }
753     else
754     {
755         //SkyPoint deprecessedPoint = clickedPoint()->deprecess(data->updateNum());
756         SkyPoint deprecessedPoint = clickedPoint()->catalogueCoord(data->updateNum()->julianDay());
757         deprecessedPoint.catalogueCoord(data->updateNum()->julianDay());
758         J2000RA                   = deprecessedPoint.ra();
759         J2000DE                   = deprecessedPoint.dec();
760     }
761 
762     ks->flagManager()->setRaDec(J2000RA, J2000DE);
763 }
764 
slotEditFlag(int flagIdx)765 void SkyMap::slotEditFlag(int flagIdx)
766 {
767     KStars *ks = KStars::Instance();
768 
769     // popup FlagManager window and switch to selected flag
770     ks->slotFlagManager();
771     ks->flagManager()->showFlag(flagIdx);
772 }
773 
slotDeleteFlag(int flagIdx)774 void SkyMap::slotDeleteFlag(int flagIdx)
775 {
776     KStars *ks = KStars::Instance();
777 
778     ks->data()->skyComposite()->flags()->remove(flagIdx);
779     ks->data()->skyComposite()->flags()->saveToFile();
780 
781     // if there is FlagManager created, update its flag model
782     if (ks->flagManager())
783     {
784         ks->flagManager()->deleteFlagItem(flagIdx);
785     }
786 }
787 
slotImage()788 void SkyMap::slotImage()
789 {
790     const auto *action = qobject_cast<QAction *>(sender());
791     const auto url     = action->data().toUrl();
792     const QString message{ action->text().remove('&') };
793 
794     if (!url.isEmpty())
795         new ImageViewer(url, clickedObject()->messageFromTitle(message), this);
796 }
797 
slotInfo()798 void SkyMap::slotInfo()
799 {
800     const auto *action = qobject_cast<QAction *>(sender());
801     const auto url     = action->data().toUrl();
802 
803     if (!url.isEmpty())
804         QDesktopServices::openUrl(url);
805 }
806 
isObjectLabeled(SkyObject * object)807 bool SkyMap::isObjectLabeled(SkyObject *object)
808 {
809     return data->skyComposite()->labelObjects().contains(object);
810 }
811 
getCenterPoint()812 SkyPoint SkyMap::getCenterPoint()
813 {
814     SkyPoint retVal;
815     // FIXME: subtraction of these 0.00001 is a simple workaround, because wrong
816     // SkyPoint is returned when _exact_ center of SkyMap is passed to the projector.
817     retVal = projector()->fromScreen(QPointF((qreal)width() / 2 - 0.00001, (qreal)height() / 2 - 0.00001), data->lst(),
818                                      data->geo()->lat());
819     return retVal;
820 }
821 
slotRemoveObjectLabel()822 void SkyMap::slotRemoveObjectLabel()
823 {
824     data->skyComposite()->removeNameLabel(clickedObject());
825     forceUpdate();
826 }
827 
slotRemoveCustomObject()828 void SkyMap::slotRemoveCustomObject()
829 {
830     auto *object = dynamic_cast<CatalogObject *>(clickedObject());
831     if (!object)
832         return;
833 
834     const auto &cat = object->getCatalog();
835     if (!cat.mut)
836         return;
837 
838     CatalogsDB::DBManager manager{ CatalogsDB::dso_db_path() };
839     manager.remove_object(cat.id, object->getObjectId());
840 
841     emit removeSkyObject(object);
842     data->skyComposite()->removeFromNames(object);
843     data->skyComposite()->removeFromLists(object);
844     data->skyComposite()->reloadDeepSky();
845     KStars::Instance()->updateTime();
846 }
847 
slotAddObjectLabel()848 void SkyMap::slotAddObjectLabel()
849 {
850     data->skyComposite()->addNameLabel(clickedObject());
851     forceUpdate();
852 }
853 
slotRemovePlanetTrail()854 void SkyMap::slotRemovePlanetTrail()
855 {
856     TrailObject *tobj = dynamic_cast<TrailObject *>(clickedObject());
857     if (tobj)
858     {
859         tobj->clearTrail();
860         forceUpdate();
861     }
862 }
863 
slotAddPlanetTrail()864 void SkyMap::slotAddPlanetTrail()
865 {
866     TrailObject *tobj = dynamic_cast<TrailObject *>(clickedObject());
867     if (tobj)
868     {
869         tobj->addToTrail();
870         forceUpdate();
871     }
872 }
873 
slotDetail()874 void SkyMap::slotDetail()
875 {
876     // check if object is selected
877     if (!clickedObject())
878     {
879         KMessageBox::sorry(this, i18n("No object selected."), i18n("Object Details"));
880         return;
881     }
882     DetailDialog *detail = new DetailDialog(clickedObject(), data->ut(), data->geo(), KStars::Instance());
883     detail->setAttribute(Qt::WA_DeleteOnClose);
884     detail->show();
885 }
886 
slotObjectSelected()887 void SkyMap::slotObjectSelected()
888 {
889     if (m_objPointingMode && KStars::Instance()->printingWizard())
890     {
891         KStars::Instance()->printingWizard()->pointingDone(clickedObject());
892         m_objPointingMode = false;
893     }
894 }
895 
slotCancelLegendPreviewMode()896 void SkyMap::slotCancelLegendPreviewMode()
897 {
898     m_previewLegend = false;
899     forceUpdate(true);
900     KStars::Instance()->showImgExportDialog();
901 }
902 
slotFinishFovCaptureMode()903 void SkyMap::slotFinishFovCaptureMode()
904 {
905     if (m_fovCaptureMode && KStars::Instance()->printingWizard())
906     {
907         KStars::Instance()->printingWizard()->fovCaptureDone();
908         m_fovCaptureMode = false;
909     }
910 }
911 
slotCaptureFov()912 void SkyMap::slotCaptureFov()
913 {
914     if (KStars::Instance()->printingWizard())
915     {
916         KStars::Instance()->printingWizard()->captureFov();
917     }
918 }
919 
slotClockSlewing()920 void SkyMap::slotClockSlewing()
921 {
922     //If the current timescale exceeds slewTimeScale, set clockSlewing=true, and stop the clock.
923     if ((fabs(data->clock()->scale()) > Options::slewTimeScale()) ^ clockSlewing)
924     {
925         data->clock()->setManualMode(!clockSlewing);
926         clockSlewing = !clockSlewing;
927         // don't change automatically the DST status
928         KStars *kstars = KStars::Instance();
929         if (kstars)
930             kstars->updateTime(false);
931     }
932 }
933 
setFocus(SkyPoint * p)934 void SkyMap::setFocus(SkyPoint *p)
935 {
936     setFocus(p->ra(), p->dec());
937 }
938 
setFocus(const dms & ra,const dms & dec)939 void SkyMap::setFocus(const dms &ra, const dms &dec)
940 {
941     Options::setFocusRA(ra.Hours());
942     Options::setFocusDec(dec.Degrees());
943 
944     focus()->set(ra, dec);
945     focus()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
946 }
947 
setFocusAltAz(const dms & alt,const dms & az)948 void SkyMap::setFocusAltAz(const dms &alt, const dms &az)
949 {
950     Options::setFocusRA(focus()->ra().Hours());
951     Options::setFocusDec(focus()->dec().Degrees());
952     focus()->setAlt(alt);
953     focus()->setAz(az);
954     focus()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
955 
956     slewing = false;
957     forceUpdate(); //need a total update, or slewing with the arrow keys doesn't work.
958 }
959 
setDestination(const SkyPoint & p)960 void SkyMap::setDestination(const SkyPoint &p)
961 {
962     setDestination(p.ra(), p.dec());
963 }
964 
setDestination(const dms & ra,const dms & dec)965 void SkyMap::setDestination(const dms &ra, const dms &dec)
966 {
967     destination()->set(ra, dec);
968     destination()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
969     emit destinationChanged();
970 }
971 
setDestinationAltAz(const dms & alt,const dms & az,bool altIsRefracted)972 void SkyMap::setDestinationAltAz(const dms &alt, const dms &az, bool altIsRefracted)
973 {
974     if (altIsRefracted)
975     {
976         // The alt in the SkyPoint is always actual, not apparent
977         destination()->setAlt(SkyPoint::unrefract(alt));
978     }
979     else
980     {
981         destination()->setAlt(alt);
982     }
983     destination()->setAz(az);
984     destination()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
985     emit destinationChanged();
986 }
987 
setClickedPoint(const SkyPoint * f)988 void SkyMap::setClickedPoint(const SkyPoint *f)
989 {
990     ClickedPoint = *f;
991 }
992 
updateFocus()993 void SkyMap::updateFocus()
994 {
995     if (slewing)
996         return;
997 
998     //Tracking on an object
999     if (Options::isTracking() && focusObject() != nullptr)
1000     {
1001         if (Options::useAltAz())
1002         {
1003             //Tracking any object in Alt/Az mode requires focus updates
1004             focusObject()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
1005             setFocusAltAz(focusObject()->alt(), focusObject()->az());
1006             focus()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
1007             setDestination(*focus());
1008         }
1009         else
1010         {
1011             //Tracking in equatorial coords
1012             setFocus(focusObject());
1013             focus()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
1014             setDestination(*focus());
1015         }
1016 
1017         //Tracking on empty sky
1018     }
1019     else if (Options::isTracking() && focusPoint() != nullptr)
1020     {
1021         if (Options::useAltAz())
1022         {
1023             //Tracking on empty sky in Alt/Az mode
1024             setFocus(focusPoint());
1025             focus()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
1026             setDestination(*focus());
1027         }
1028 
1029         // Not tracking and not slewing, let sky drift by
1030         // This means that horizontal coordinates are constant.
1031     }
1032     else
1033     {
1034         focus()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
1035     }
1036 }
1037 
slewFocus()1038 void SkyMap::slewFocus()
1039 {
1040     //Don't slew if the mouse button is pressed
1041     //Also, no animated slews if the Manual Clock is active
1042     //08/2002: added possibility for one-time skipping of slew with snapNextFocus
1043     if (!mouseButtonDown)
1044     {
1045         bool goSlew = (Options::useAnimatedSlewing() && !data->snapNextFocus()) &&
1046                       !(data->clock()->isManualMode() && data->clock()->isActive());
1047         if (goSlew)
1048         {
1049             double dX, dY;
1050             double maxstep = 10.0;
1051             if (Options::useAltAz())
1052             {
1053                 dX = destination()->az().Degrees() - focus()->az().Degrees();
1054                 dY = destination()->alt().Degrees() - focus()->alt().Degrees();
1055             }
1056             else
1057             {
1058                 dX = destination()->ra().Degrees() - focus()->ra().Degrees();
1059                 dY = destination()->dec().Degrees() - focus()->dec().Degrees();
1060             }
1061 
1062             //switch directions to go the short way around the celestial sphere, if necessary.
1063             dX = KSUtils::reduceAngle(dX, -180.0, 180.0);
1064 
1065             double r0 = sqrt(dX * dX + dY * dY);
1066             if (r0 < 20.0) //smaller slews have smaller maxstep
1067             {
1068                 maxstep *= (10.0 + 0.5 * r0) / 20.0;
1069             }
1070             double step = 0.5;
1071             double r    = r0;
1072             while (r > step)
1073             {
1074                 //DEBUG
1075                 //qDebug() << step << ": " << r << ": " << r0;
1076                 double fX = dX / r;
1077                 double fY = dY / r;
1078 
1079                 if (Options::useAltAz())
1080                 {
1081                     focus()->setAlt(focus()->alt().Degrees() + fY * step);
1082                     focus()->setAz(dms(focus()->az().Degrees() + fX * step).reduce());
1083                     focus()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
1084                 }
1085                 else
1086                 {
1087                     fX = fX / 15.; //convert RA degrees to hours
1088                     SkyPoint newFocus(focus()->ra().Hours() + fX * step, focus()->dec().Degrees() + fY * step);
1089                     setFocus(&newFocus);
1090                     focus()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
1091                 }
1092 
1093                 slewing = true;
1094 
1095                 forceUpdate();
1096                 qApp->processEvents(); //keep up with other stuff
1097 
1098                 if (Options::useAltAz())
1099                 {
1100                     dX = destination()->az().Degrees() - focus()->az().Degrees();
1101                     dY = destination()->alt().Degrees() - focus()->alt().Degrees();
1102                 }
1103                 else
1104                 {
1105                     dX = destination()->ra().Degrees() - focus()->ra().Degrees();
1106                     dY = destination()->dec().Degrees() - focus()->dec().Degrees();
1107                 }
1108 
1109                 //switch directions to go the short way around the celestial sphere, if necessary.
1110                 dX = KSUtils::reduceAngle(dX, -180.0, 180.0);
1111                 r  = sqrt(dX * dX + dY * dY);
1112 
1113                 //Modify step according to a cosine-shaped profile
1114                 //centered on the midpoint of the slew
1115                 //NOTE: don't allow the full range from -PI/2 to PI/2
1116                 //because the slew will never reach the destination as
1117                 //the speed approaches zero at the end!
1118                 double t = dms::PI * (r - 0.5 * r0) / (1.05 * r0);
1119                 step     = cos(t) * maxstep;
1120             }
1121         }
1122 
1123         //Either useAnimatedSlewing==false, or we have slewed, and are within one step of destination
1124         //set focus=destination.
1125         if (Options::useAltAz())
1126         {
1127             setFocusAltAz(destination()->alt(), destination()->az());
1128             focus()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
1129         }
1130         else
1131         {
1132             setFocus(destination());
1133             focus()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
1134         }
1135 
1136         slewing = false;
1137 
1138         //Turn off snapNextFocus, we only want it to happen once
1139         if (data->snapNextFocus())
1140         {
1141             data->setSnapNextFocus(false);
1142         }
1143 
1144         //Start the HoverTimer. if the user leaves the mouse in place after a slew,
1145         //we want to attach a label to the nearest object.
1146         if (Options::useHoverLabel())
1147             m_HoverTimer.start(HOVER_INTERVAL);
1148 
1149         forceUpdate();
1150     }
1151 }
1152 
slotZoomIn()1153 void SkyMap::slotZoomIn()
1154 {
1155     setZoomFactor(Options::zoomFactor() * DZOOM);
1156 }
1157 
slotZoomOut()1158 void SkyMap::slotZoomOut()
1159 {
1160     setZoomFactor(Options::zoomFactor() / DZOOM);
1161 }
1162 
slotZoomDefault()1163 void SkyMap::slotZoomDefault()
1164 {
1165     setZoomFactor(DEFAULTZOOM);
1166 }
1167 
setZoomFactor(double factor)1168 void SkyMap::setZoomFactor(double factor)
1169 {
1170     Options::setZoomFactor(KSUtils::clamp(factor, MINZOOM, MAXZOOM));
1171     forceUpdate();
1172     emit zoomChanged();
1173 }
1174 
1175 // force a new calculation of the skymap (used instead of update(), which may skip the redraw)
1176 // if now=true, SkyMap::paintEvent() is run immediately, rather than being added to the event queue
1177 // also, determine new coordinates of mouse cursor.
forceUpdate(bool now)1178 void SkyMap::forceUpdate(bool now)
1179 {
1180     QPoint mp(mapFromGlobal(QCursor::pos()));
1181     if (!projector()->unusablePoint(mp))
1182     {
1183         //determine RA, Dec of mouse pointer
1184         m_MousePoint = projector()->fromScreen(mp, data->lst(), data->geo()->lat());
1185     }
1186 
1187     computeSkymap = true;
1188 
1189     // Ensure that stars are recomputed
1190     data->incUpdateID();
1191 
1192     if (now)
1193         m_SkyMapDraw->repaint();
1194     else
1195         m_SkyMapDraw->update();
1196 }
1197 
fov()1198 float SkyMap::fov()
1199 {
1200     float diagonalPixels = sqrt(static_cast<double>(width() * width() + height() * height()));
1201     return diagonalPixels / (2 * Options::zoomFactor() * dms::DegToRad);
1202 }
1203 
setupProjector()1204 void SkyMap::setupProjector()
1205 {
1206     //Update View Parameters for projection
1207     ViewParams p;
1208     p.focus         = focus();
1209     p.height        = height();
1210     p.width         = width();
1211     p.useAltAz      = Options::useAltAz();
1212     p.useRefraction = Options::useRefraction();
1213     p.zoomFactor    = Options::zoomFactor();
1214     p.fillGround    = Options::showGround();
1215     //Check if we need a new projector
1216     if (m_proj && Options::projection() == m_proj->type())
1217         m_proj->setViewParams(p);
1218     else
1219     {
1220         delete m_proj;
1221         switch (Options::projection())
1222         {
1223             case Gnomonic:
1224                 m_proj = new GnomonicProjector(p);
1225                 break;
1226             case Stereographic:
1227                 m_proj = new StereographicProjector(p);
1228                 break;
1229             case Orthographic:
1230                 m_proj = new OrthographicProjector(p);
1231                 break;
1232             case AzimuthalEquidistant:
1233                 m_proj = new AzimuthalEquidistantProjector(p);
1234                 break;
1235             case Equirectangular:
1236                 m_proj = new EquirectangularProjector(p);
1237                 break;
1238             case Lambert:
1239             default:
1240                 //TODO: implement other projection classes
1241                 m_proj = new LambertProjector(p);
1242                 break;
1243         }
1244     }
1245 }
1246 
setZoomMouseCursor()1247 void SkyMap::setZoomMouseCursor()
1248 {
1249     mouseMoveCursor = false; // no mousemove cursor
1250     QBitmap cursor  = zoomCursorBitmap(2);
1251     QBitmap mask    = zoomCursorBitmap(4);
1252     setCursor(QCursor(cursor, mask));
1253 }
1254 
setMouseCursorShape(Cursor type)1255 void SkyMap::setMouseCursorShape(Cursor type)
1256 {
1257     // no mousemove cursor
1258     mouseMoveCursor = false;
1259 
1260     switch (type)
1261     {
1262         case Cross:
1263         {
1264             QBitmap cursor  = defaultCursorBitmap(2);
1265             QBitmap mask    = defaultCursorBitmap(3);
1266             setCursor(QCursor(cursor, mask));
1267         }
1268         break;
1269 
1270         case Circle:
1271         {
1272             QBitmap cursor  = circleCursorBitmap(2);
1273             QBitmap mask    = circleCursorBitmap(3);
1274             setCursor(QCursor(cursor, mask));
1275         }
1276         break;
1277 
1278         case NoCursor:
1279             setCursor(Qt::ArrowCursor);
1280             break;
1281     }
1282 }
1283 
setMouseMoveCursor()1284 void SkyMap::setMouseMoveCursor()
1285 {
1286     if (mouseButtonDown)
1287     {
1288         setCursor(Qt::SizeAllCursor); // cursor shape defined in qt
1289         mouseMoveCursor = true;
1290     }
1291 }
1292 
updateAngleRuler()1293 void SkyMap::updateAngleRuler()
1294 {
1295     if (rulerMode && (!pmenu || !pmenu->isVisible()))
1296         AngularRuler.setPoint(1, &m_MousePoint);
1297     AngularRuler.update(data);
1298 }
1299 
isSlewing() const1300 bool SkyMap::isSlewing() const
1301 {
1302     return (slewing || (clockSlewing && data->clock()->isActive()));
1303 }
1304 
slotStartXplanetViewer()1305 void SkyMap::slotStartXplanetViewer()
1306 {
1307     if(clickedObject())
1308         new XPlanetImageViewer(clickedObject()->name(), this);
1309     else
1310         new XPlanetImageViewer(i18n("Saturn"), this);
1311 }
1312 
1313 
1314