File indexing completed on 2022-12-06 18:58:52

0001 /*
0002     SPDX-FileCopyrightText: 2001 Jason Harris <jharris@30doradus.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "skyobjectuserdata.h"
0008 #ifdef _WIN32
0009 #include <windows.h>
0010 #endif
0011 
0012 #include "skymap.h"
0013 
0014 #include "ksasteroid.h"
0015 #include "kstars_debug.h"
0016 #include "fov.h"
0017 #include "imageviewer.h"
0018 #include "xplanetimageviewer.h"
0019 #include "ksdssdownloader.h"
0020 #include "kspaths.h"
0021 #include "kspopupmenu.h"
0022 #include "kstars.h"
0023 #include "ksutils.h"
0024 #include "Options.h"
0025 #include "skymapcomposite.h"
0026 #ifdef HAVE_OPENGL
0027 #include "skymapgldraw.h"
0028 #endif
0029 #include "skymapqdraw.h"
0030 #include "starhopperdialog.h"
0031 #include "starobject.h"
0032 #include "texturemanager.h"
0033 #include "dialogs/detaildialog.h"
0034 #include "printing/printingwizard.h"
0035 #include "skycomponents/flagcomponent.h"
0036 #include "skyobjects/ksplanetbase.h"
0037 #include "skyobjects/satellite.h"
0038 #include "tools/flagmanager.h"
0039 #include "widgets/infoboxwidget.h"
0040 #include "projections/azimuthalequidistantprojector.h"
0041 #include "projections/equirectangularprojector.h"
0042 #include "projections/lambertprojector.h"
0043 #include "projections/gnomonicprojector.h"
0044 #include "projections/orthographicprojector.h"
0045 #include "projections/stereographicprojector.h"
0046 #include "catalogobject.h"
0047 #include "catalogsdb.h"
0048 #include "catalogscomponent.h"
0049 
0050 #include <KActionCollection>
0051 #include <KToolBar>
0052 
0053 #include <QBitmap>
0054 #include <QToolTip>
0055 #include <QClipboard>
0056 #include <QInputDialog>
0057 #include <QDesktopServices>
0058 
0059 #include <QProcess>
0060 #include <QFileDialog>
0061 
0062 #include <cmath>
0063 
0064 namespace
0065 {
0066 // Draw bitmap for zoom cursor. Width is size of pen to draw with.
0067 QBitmap zoomCursorBitmap(int width)
0068 {
0069     QBitmap b(32, 32);
0070     b.fill(Qt::color0);
0071     int mx = 16, my = 16;
0072     // Begin drawing
0073     QPainter p;
0074     p.begin(&b);
0075     p.setPen(QPen(Qt::color1, width));
0076     p.drawEllipse(mx - 7, my - 7, 14, 14);
0077     p.drawLine(mx + 5, my + 5, mx + 11, my + 11);
0078     p.end();
0079     return b;
0080 }
0081 
0082 // Draw bitmap for default cursor. Width is size of pen to draw with.
0083 QBitmap defaultCursorBitmap(int width)
0084 {
0085     QBitmap b(32, 32);
0086     b.fill(Qt::color0);
0087     int mx = 16, my = 16;
0088     // Begin drawing
0089     QPainter p;
0090     p.begin(&b);
0091     p.setPen(QPen(Qt::color1, width));
0092     // 1. diagonal
0093     p.drawLine(mx - 2, my - 2, mx - 8, mx - 8);
0094     p.drawLine(mx + 2, my + 2, mx + 8, mx + 8);
0095     // 2. diagonal
0096     p.drawLine(mx - 2, my + 2, mx - 8, mx + 8);
0097     p.drawLine(mx + 2, my - 2, mx + 8, mx - 8);
0098     p.end();
0099     return b;
0100 }
0101 
0102 QBitmap circleCursorBitmap(int width)
0103 {
0104     QBitmap b(32, 32);
0105     b.fill(Qt::color0);
0106     int mx = 16, my = 16;
0107     // Begin drawing
0108     QPainter p;
0109     p.begin(&b);
0110     p.setPen(QPen(Qt::color1, width));
0111 
0112     // Circle
0113     p.drawEllipse(mx - 8, my - 8, mx, my);
0114     // 1. diagonal
0115     p.drawLine(mx - 8, my - 8, 0, 0);
0116     p.drawLine(mx + 8, my - 8, 32, 0);
0117     // 2. diagonal
0118     p.drawLine(mx - 8, my + 8, 0, 32);
0119     p.drawLine(mx + 8, my + 8, 32, 32);
0120 
0121     p.end();
0122     return b;
0123 }
0124 
0125 } // namespace
0126 
0127 SkyMap *SkyMap::pinstance = nullptr;
0128 
0129 SkyMap *SkyMap::Create()
0130 {
0131     delete pinstance;
0132     pinstance = new SkyMap();
0133     return pinstance;
0134 }
0135 
0136 SkyMap *SkyMap::Instance()
0137 {
0138     return pinstance;
0139 }
0140 
0141 SkyMap::SkyMap()
0142     : QGraphicsView(KStars::Instance()), computeSkymap(true), rulerMode(false), data(KStarsData::Instance()), pmenu(nullptr),
0143       ClickedObject(nullptr), FocusObject(nullptr), m_proj(nullptr), m_previewLegend(false), m_objPointingMode(false)
0144 {
0145 #if !defined(KSTARS_LITE)
0146     grabGesture(Qt::PinchGesture);
0147     grabGesture(Qt::TapAndHoldGesture);
0148 #endif
0149     m_Scale = 1.0;
0150 
0151     ZoomRect = QRect();
0152 
0153     // set the default cursor
0154     setMouseCursorShape(static_cast<Cursor>(Options::defaultCursor()));
0155 
0156     QPalette p = palette();
0157     p.setColor(QPalette::Window, QColor(data->colorScheme()->colorNamed("SkyColor")));
0158     setPalette(p);
0159 
0160     setFocusPolicy(Qt::StrongFocus);
0161     setMinimumSize(380, 250);
0162     setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding));
0163     setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0164     setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0165     setStyleSheet("QGraphicsView { border-style: none; }");
0166 
0167     setMouseTracking(true); //Generate MouseMove events!
0168     midMouseButtonDown = false;
0169     mouseButtonDown    = false;
0170     slewing            = false;
0171     clockSlewing       = false;
0172 
0173     ClickedObject = nullptr;
0174     FocusObject   = nullptr;
0175 
0176     m_SkyMapDraw = nullptr;
0177 
0178     pmenu = new KSPopupMenu();
0179 
0180     setupProjector();
0181 
0182     //Initialize Transient label stuff
0183     m_HoverTimer.setSingleShot(true); // using this timer as a single shot timer
0184 
0185     connect(&m_HoverTimer, SIGNAL(timeout()), this, SLOT(slotTransientLabel()));
0186     connect(this, SIGNAL(destinationChanged()), this, SLOT(slewFocus()));
0187     connect(KStarsData::Instance(), SIGNAL(skyUpdate(bool)), this, SLOT(slotUpdateSky(bool)));
0188 
0189     // Time infobox
0190     m_timeBox = new InfoBoxWidget(Options::shadeTimeBox(), Options::positionTimeBox(), Options::stickyTimeBox(),
0191                                   QStringList(), this);
0192     m_timeBox->setVisible(Options::showTimeBox());
0193     connect(data->clock(), SIGNAL(timeChanged()), m_timeBox, SLOT(slotTimeChanged()));
0194     connect(data->clock(), SIGNAL(timeAdvanced()), m_timeBox, SLOT(slotTimeChanged()));
0195 
0196     // Geo infobox
0197     m_geoBox = new InfoBoxWidget(Options::shadeGeoBox(), Options::positionGeoBox(), Options::stickyGeoBox(),
0198                                  QStringList(), this);
0199     m_geoBox->setVisible(Options::showGeoBox());
0200     connect(data, SIGNAL(geoChanged()), m_geoBox, SLOT(slotGeoChanged()));
0201 
0202     // Object infobox
0203     m_objBox = new InfoBoxWidget(Options::shadeFocusBox(), Options::positionFocusBox(), Options::stickyFocusBox(),
0204                                  QStringList(), this);
0205     m_objBox->setVisible(Options::showFocusBox());
0206     connect(this, &SkyMap::objectChanged, m_objBox, &InfoBoxWidget::slotObjectChanged);
0207     connect(this, &SkyMap::positionChanged, m_objBox, &InfoBoxWidget::slotPointChanged);
0208 
0209     m_SkyMapDraw = new SkyMapQDraw(this);
0210     m_SkyMapDraw->setMouseTracking(true);
0211 
0212     m_SkyMapDraw->setParent(this->viewport());
0213     m_SkyMapDraw->show();
0214 
0215     m_iboxes = new InfoBoxes(m_SkyMapDraw);
0216 
0217     m_iboxes->setVisible(Options::showInfoBoxes());
0218     m_iboxes->addInfoBox(m_timeBox);
0219     m_iboxes->addInfoBox(m_geoBox);
0220     m_iboxes->addInfoBox(m_objBox);
0221 }
0222 
0223 void SkyMap::slotToggleGeoBox(bool flag)
0224 {
0225     m_geoBox->setVisible(flag);
0226 }
0227 
0228 void SkyMap::slotToggleFocusBox(bool flag)
0229 {
0230     m_objBox->setVisible(flag);
0231 }
0232 
0233 void SkyMap::slotToggleTimeBox(bool flag)
0234 {
0235     m_timeBox->setVisible(flag);
0236 }
0237 
0238 void SkyMap::slotToggleInfoboxes(bool flag)
0239 {
0240     m_iboxes->setVisible(flag);
0241     Options::setShowInfoBoxes(flag);
0242 }
0243 
0244 SkyMap::~SkyMap()
0245 {
0246     /* == Save infoxes status into Options == */
0247     //Options::setShowInfoBoxes(m_iboxes->isVisibleTo(parentWidget()));
0248     // Time box
0249     Options::setPositionTimeBox(m_timeBox->pos());
0250     Options::setShadeTimeBox(m_timeBox->shaded());
0251     Options::setStickyTimeBox(m_timeBox->sticky());
0252     Options::setShowTimeBox(m_timeBox->isVisibleTo(m_iboxes));
0253     // Geo box
0254     Options::setPositionGeoBox(m_geoBox->pos());
0255     Options::setShadeGeoBox(m_geoBox->shaded());
0256     Options::setStickyGeoBox(m_geoBox->sticky());
0257     Options::setShowGeoBox(m_geoBox->isVisibleTo(m_iboxes));
0258     // Obj box
0259     Options::setPositionFocusBox(m_objBox->pos());
0260     Options::setShadeFocusBox(m_objBox->shaded());
0261     Options::setStickyFocusBox(m_objBox->sticky());
0262     Options::setShowFocusBox(m_objBox->isVisibleTo(m_iboxes));
0263 
0264     //store focus values in Options
0265     //If not tracking and using Alt/Az coords, stor the Alt/Az coordinates
0266     if (Options::useAltAz() && !Options::isTracking())
0267     {
0268         Options::setFocusRA(focus()->az().Degrees());
0269         Options::setFocusDec(focus()->alt().Degrees());
0270     }
0271     else
0272     {
0273         Options::setFocusRA(focus()->ra().Hours());
0274         Options::setFocusDec(focus()->dec().Degrees());
0275     }
0276 
0277 #ifdef HAVE_OPENGL
0278     delete m_SkyMapGLDraw;
0279     delete m_SkyMapQDraw;
0280     m_SkyMapDraw = 0; // Just a formality
0281 #else
0282     delete m_SkyMapDraw;
0283 #endif
0284 
0285     delete pmenu;
0286 
0287     delete m_proj;
0288 
0289     pinstance = nullptr;
0290 }
0291 
0292 void SkyMap::showFocusCoords()
0293 {
0294     if (focusObject() && Options::isTracking())
0295         emit objectChanged(focusObject());
0296     else
0297         emit positionChanged(focus());
0298 }
0299 
0300 void SkyMap::updateInfoBoxes()
0301 {
0302     if (focusObject() && Options::isTracking())
0303         m_objBox->slotObjectChanged(focusObject());
0304     else
0305         m_objBox->slotPointChanged(focus());
0306 }
0307 
0308 void SkyMap::slotTransientLabel()
0309 {
0310     //This function is only called if the HoverTimer manages to timeout.
0311     //(HoverTimer is restarted with every mouseMoveEvent; so if it times
0312     //out, that means there was no mouse movement for HOVER_INTERVAL msec.)
0313     if (hasFocus() && !slewing &&
0314             !(Options::useAltAz() && Options::showGround() && m_MousePoint.altRefracted().Degrees() < 0.0))
0315     {
0316         double maxrad = 1000.0 / Options::zoomFactor();
0317         SkyObject *so = data->skyComposite()->objectNearest(&m_MousePoint, maxrad);
0318 
0319         if (so && !isObjectLabeled(so))
0320         {
0321             QString name = so->translatedLongName();
0322             if (!std::isnan(so->mag()))
0323                 name += QString(": %1<sup>m</sup>").arg(QString::number(so->mag(), 'f', 1));
0324             QToolTip::showText(QCursor::pos(), name, this);
0325         }
0326     }
0327 }
0328 
0329 //Slots
0330 
0331 void SkyMap::setClickedObject(SkyObject *o)
0332 {
0333     ClickedObject = o;
0334 }
0335 
0336 void SkyMap::setFocusObject(SkyObject *o)
0337 {
0338     FocusObject = o;
0339     if (FocusObject)
0340         Options::setFocusObject(FocusObject->name());
0341     else
0342         Options::setFocusObject(i18n("nothing"));
0343 }
0344 
0345 void SkyMap::slotCenter()
0346 {
0347     KStars *kstars        = KStars::Instance();
0348     TrailObject *trailObj = dynamic_cast<TrailObject *>(focusObject());
0349 
0350     SkyPoint *foc;
0351     if(ClickedObject != nullptr)
0352         foc = ClickedObject;
0353     else
0354         foc = &ClickedPoint;
0355 
0356     //clear the planet trail of old focusObject, if it was temporary
0357     if (trailObj && data->temporaryTrail)
0358     {
0359         trailObj->clearTrail();
0360         data->temporaryTrail = false;
0361     }
0362 
0363     //If the requested object is below the opaque horizon, issue a warning message
0364     //(unless user is already pointed below the horizon)
0365     if (Options::useAltAz() && Options::showGround() &&
0366             focus()->alt().Degrees() > SkyPoint::altCrit &&
0367             foc->alt().Degrees() <= SkyPoint::altCrit)
0368     {
0369         QString caption = i18n("Requested Position Below Horizon");
0370         QString message = i18n("The requested position is below the horizon.\nWould you like to go there anyway?");
0371         if (KMessageBox::warningYesNo(this, message, caption, KGuiItem(i18n("Go Anyway")),
0372                                       KGuiItem(i18n("Keep Position")), "dag_focus_below_horiz") == KMessageBox::No)
0373         {
0374             setClickedObject(nullptr);
0375             setFocusObject(nullptr);
0376             Options::setIsTracking(false);
0377 
0378             return;
0379         }
0380     }
0381 
0382     //set FocusObject before slewing.  Otherwise, KStarsData::updateTime() can reset
0383     //destination to previous object...
0384     setFocusObject(ClickedObject);
0385     if(ClickedObject == nullptr)
0386         setFocusPoint(&ClickedPoint);
0387 
0388     Options::setIsTracking(true);
0389 
0390     if (kstars)
0391     {
0392         kstars->actionCollection()
0393         ->action("track_object")
0394         ->setIcon(QIcon::fromTheme("document-encrypt"));
0395         kstars->actionCollection()->action("track_object")->setText(i18n("Stop &Tracking"));
0396     }
0397 
0398     //If focusObject is a SS body and doesn't already have a trail, set the temporaryTrail
0399 
0400     if (Options::useAutoTrail() && trailObj && trailObj->hasTrail())
0401     {
0402         trailObj->addToTrail();
0403         data->temporaryTrail = true;
0404     }
0405 
0406     //update the destination to the selected coordinates
0407     if (Options::useAltAz())
0408     {
0409         setDestinationAltAz(foc->alt(), foc->az(), false);
0410     }
0411     else
0412     {
0413         setDestination(*foc);
0414     }
0415 
0416     foc->EquatorialToHorizontal(data->lst(), data->geo()->lat());
0417 
0418     //display coordinates in statusBar
0419     emit mousePointChanged(foc);
0420     showFocusCoords(); //update FocusBox
0421 }
0422 
0423 void SkyMap::slotUpdateSky(bool now)
0424 {
0425     // Code moved from KStarsData::updateTime()
0426     //Update focus
0427     updateFocus();
0428 
0429     if (now)
0430         QTimer::singleShot(
0431             0, this,
0432             SLOT(forceUpdateNow())); // Why is it done this way rather than just calling forceUpdateNow()? -- asimha // --> Opening a neww thread? -- Valentin
0433     else
0434         forceUpdate();
0435 }
0436 
0437 void SkyMap::slotDSS()
0438 {
0439     dms ra(0.0), dec(0.0);
0440     QString urlstring;
0441 
0442     //ra and dec must be the coordinates at J2000.  If we clicked on an object, just use the object's ra0, dec0 coords
0443     //if we clicked on empty sky, we need to precess to J2000.
0444     if (clickedObject())
0445     {
0446         urlstring = KSDssDownloader::getDSSURL(clickedObject());
0447     }
0448     else
0449     {
0450         //SkyPoint deprecessedPoint = clickedPoint()->deprecess(data->updateNum());
0451         SkyPoint deprecessedPoint = clickedPoint()->catalogueCoord(data->updateNum()->julianDay());
0452         ra                        = deprecessedPoint.ra();
0453         dec                       = deprecessedPoint.dec();
0454         urlstring                 = KSDssDownloader::getDSSURL(ra, dec); // Use default size for non-objects
0455     }
0456 
0457     QUrl url(urlstring);
0458 
0459     KStars *kstars = KStars::Instance();
0460     if (kstars)
0461     {
0462         new ImageViewer(
0463             url, i18n("Digitized Sky Survey image provided by the Space Telescope Science Institute [free for non-commercial use]."),
0464             this);
0465         //iv->show();
0466     }
0467 }
0468 
0469 void SkyMap::slotCopyCoordinates()
0470 {
0471     dms J2000RA(0.0), J2000DE(0.0), JNowRA(0.0), JNowDE(0.0), Az, Alt;
0472     if (clickedObject())
0473     {
0474         J2000RA  = clickedObject()->ra0();
0475         J2000DE = clickedObject()->dec0();
0476         JNowRA = clickedObject()->ra();
0477         JNowDE = clickedObject()->dec();
0478         Az = clickedObject()->az();
0479         Alt = clickedObject()->alt();
0480     }
0481     else
0482     {
0483         // Empty point only have valid JNow RA/DE, not J2000 information.
0484         SkyPoint emptyPoint = *clickedPoint();
0485         // Now get J2000 from JNow but de-aberrating, de-nutating, de-preccessing
0486         // This modifies emptyPoint, but the RA/DE are now missing and need
0487         // to be repopulated.
0488         emptyPoint.catalogueCoord(data->updateNum()->julianDay());
0489         emptyPoint.setRA(clickedPoint()->ra());
0490         emptyPoint.setDec(clickedPoint()->dec());
0491         emptyPoint.EquatorialToHorizontal(data->lst(), data->geo()->lat());
0492 
0493         J2000RA = emptyPoint.ra0();
0494         J2000DE = emptyPoint.dec0();
0495         JNowRA = emptyPoint.ra();
0496         JNowDE = emptyPoint.dec();
0497         Az = emptyPoint.az();
0498         Alt = emptyPoint.alt();
0499     }
0500 
0501     QApplication::clipboard()->setText(i18nc("Equatorial & Horizontal Coordinates",
0502                                        "JNow:\t%1\t%2\nJ2000:\t%3\t%4\nAzAlt:\t%5\t%6",
0503                                        JNowRA.toHMSString(),
0504                                        JNowDE.toDMSString(),
0505                                        J2000RA.toHMSString(),
0506                                        J2000DE.toDMSString(),
0507                                        Az.toDMSString(),
0508                                        Alt.toDMSString()));
0509 }
0510 
0511 
0512 void SkyMap::slotCopyTLE()
0513 {
0514 
0515     QString tle = "";
0516     if (clickedObject()->type() == SkyObject::SATELLITE)
0517     {
0518         Satellite *sat = (Satellite *) clickedObject();
0519         tle = sat->tle();
0520     }
0521     else
0522     {
0523         tle = "NO TLE FOR OBJECT";
0524     }
0525 
0526 
0527     QApplication::clipboard()->setText(tle);
0528 }
0529 
0530 void SkyMap::slotSDSS()
0531 {
0532     // TODO: Remove code duplication -- we have the same stuff
0533     // implemented in ObservingList::setCurrentImage() etc. in
0534     // tools/observinglist.cpp; must try to de-duplicate as much as
0535     // possible.
0536     QString URLprefix("http://skyserver.sdss.org/dr16/SkyServerWS/ImgCutout/getjpeg?");
0537     QString URLsuffix("&scale=1.0&width=600&height=600");
0538     dms ra(0.0), dec(0.0);
0539     QString RAString, DecString;
0540 
0541     //ra and dec must be the coordinates at J2000.  If we clicked on an object, just use the object's ra0, dec0 coords
0542     //if we clicked on empty sky, we need to precess to J2000.
0543     if (clickedObject())
0544     {
0545         ra  = clickedObject()->ra0();
0546         dec = clickedObject()->dec0();
0547     }
0548     else
0549     {
0550         //SkyPoint deprecessedPoint = clickedPoint()->deprecess(data->updateNum());
0551         SkyPoint deprecessedPoint = clickedPoint()->catalogueCoord(data->updateNum()->julianDay());
0552         deprecessedPoint.catalogueCoord(data->updateNum()->julianDay());
0553         ra                        = deprecessedPoint.ra();
0554         dec                       = deprecessedPoint.dec();
0555     }
0556 
0557     RAString  = QString::asprintf("ra=%f", ra.Degrees());
0558     DecString = QString::asprintf("&dec=%f", dec.Degrees());
0559 
0560     //concat all the segments into the kview command line:
0561     QUrl url(URLprefix + RAString + DecString + URLsuffix);
0562 
0563     KStars *kstars = KStars::Instance();
0564     if (kstars)
0565     {
0566         new ImageViewer(url,
0567                         i18n("Sloan Digital Sky Survey image provided by the Astrophysical Research Consortium [free "
0568                              "for non-commercial use]."),
0569                         this);
0570         //iv->show();
0571     }
0572 }
0573 
0574 void SkyMap::slotEyepieceView()
0575 {
0576     KStars::Instance()->slotEyepieceView((clickedObject() ? clickedObject() : clickedPoint()));
0577 }
0578 void SkyMap::slotBeginAngularDistance()
0579 {
0580     beginRulerMode(false);
0581 }
0582 
0583 void SkyMap::slotBeginStarHop()
0584 {
0585     beginRulerMode(true);
0586 }
0587 
0588 void SkyMap::beginRulerMode(bool starHopRuler)
0589 {
0590     rulerMode         = true;
0591     starHopDefineMode = starHopRuler;
0592     AngularRuler.clear();
0593 
0594     //If the cursor is near a SkyObject, reset the AngularRuler's
0595     //start point to the position of the SkyObject
0596     double maxrad = 1000.0 / Options::zoomFactor();
0597     SkyObject *so = data->skyComposite()->objectNearest(clickedPoint(), maxrad);
0598     if (so)
0599     {
0600         AngularRuler.append(so);
0601         AngularRuler.append(so);
0602         m_rulerStartPoint = so;
0603     }
0604     else
0605     {
0606         AngularRuler.append(clickedPoint());
0607         AngularRuler.append(clickedPoint());
0608         m_rulerStartPoint = clickedPoint();
0609     }
0610 
0611     AngularRuler.update(data);
0612 }
0613 
0614 void SkyMap::slotEndRulerMode()
0615 {
0616     if (!rulerMode)
0617         return;
0618     if (!starHopDefineMode) // Angular Ruler
0619     {
0620         QString sbMessage;
0621 
0622         //If the cursor is near a SkyObject, reset the AngularRuler's
0623         //end point to the position of the SkyObject
0624         double maxrad = 1000.0 / Options::zoomFactor();
0625         SkyPoint *rulerEndPoint;
0626         SkyObject *so = data->skyComposite()->objectNearest(clickedPoint(), maxrad);
0627         if (so)
0628         {
0629             AngularRuler.setPoint(1, so);
0630             sbMessage     = so->translatedLongName() + "   ";
0631             rulerEndPoint = so;
0632         }
0633         else
0634         {
0635             AngularRuler.setPoint(1, clickedPoint());
0636             rulerEndPoint = clickedPoint();
0637         }
0638 
0639         rulerMode = false;
0640         AngularRuler.update(data);
0641         dms angularDistance = AngularRuler.angularSize();
0642 
0643         sbMessage += i18n("Angular distance: %1", angularDistance.toDMSString());
0644 
0645         const StarObject *p1 = dynamic_cast<const StarObject *>(m_rulerStartPoint);
0646         const StarObject *p2 = dynamic_cast<const StarObject *>(rulerEndPoint);
0647 
0648         qCDebug(KSTARS) << "Starobjects? " << p1 << p2;
0649         if (p1 && p2)
0650             qCDebug(KSTARS) << "Distances: " << p1->distance() << "pc; " << p2->distance() << "pc";
0651         if (p1 && p2 && std::isfinite(p1->distance()) && std::isfinite(p2->distance()) && p1->distance() > 0 &&
0652                 p2->distance() > 0)
0653         {
0654             double dist = sqrt(p1->distance() * p1->distance() + p2->distance() * p2->distance() -
0655                                2 * p1->distance() * p2->distance() * cos(angularDistance.radians()));
0656             qCDebug(KSTARS) << "Could calculate physical distance: " << dist << " pc";
0657             sbMessage += i18n("; Physical distance: %1 pc", QString::number(dist));
0658         }
0659 
0660         AngularRuler.clear();
0661 
0662         // Create unobsructive message box with suicidal tendencies
0663         // to display result.
0664         InfoBoxWidget *box = new InfoBoxWidget(true, mapFromGlobal(QCursor::pos()), 0, QStringList(sbMessage), this);
0665         connect(box, SIGNAL(clicked()), box, SLOT(deleteLater()));
0666         QTimer::singleShot(5000, box, SLOT(deleteLater()));
0667         box->adjust();
0668         box->show();
0669     }
0670     else // Star Hop
0671     {
0672         StarHopperDialog *shd    = new StarHopperDialog(this);
0673         const SkyPoint &startHop = *AngularRuler.point(0);
0674         const SkyPoint &stopHop  = *clickedPoint();
0675         double fov; // Field of view in arcminutes
0676         bool ok;    // true if user did not cancel the operation
0677         if (data->getAvailableFOVs().size() == 1)
0678         {
0679             // Exactly 1 FOV symbol visible, so use that. Also assume a circular FOV of size min{sizeX, sizeY}
0680             FOV *f = data->getAvailableFOVs().first();
0681             fov    = ((f->sizeX() >= f->sizeY() && f->sizeY() != 0) ? f->sizeY() : f->sizeX());
0682             ok     = true;
0683         }
0684         else if (!data->getAvailableFOVs().isEmpty())
0685         {
0686             // Ask the user to choose from a list of available FOVs.
0687             FOV const *f;
0688             QMap<QString, double> nameToFovMap;
0689             foreach (f, data->getAvailableFOVs())
0690             {
0691                 nameToFovMap.insert(f->name(),
0692                                     ((f->sizeX() >= f->sizeY() && f->sizeY() != 0) ? f->sizeY() : f->sizeX()));
0693             }
0694             fov = nameToFovMap[QInputDialog::getItem(this, i18n("Star Hopper: Choose a field-of-view"),
0695                                                            i18n("FOV to use for star hopping:"), nameToFovMap.keys(), 0,
0696                                                            false, &ok)];
0697         }
0698         else
0699         {
0700             // Ask the user to enter a field of view
0701             fov =
0702                 QInputDialog::getDouble(this, i18n("Star Hopper: Enter field-of-view to use"),
0703                                         i18n("FOV to use for star hopping (in arcminutes):"), 60.0, 1.0, 600.0, 1, &ok);
0704         }
0705 
0706         Q_ASSERT(fov > 0.0);
0707 
0708         if (ok)
0709         {
0710             qCDebug(KSTARS) << "fov = " << fov;
0711 
0712             shd->starHop(startHop, stopHop, fov / 60.0, 9.0); //FIXME: Hardcoded maglimit value
0713             shd->show();
0714         }
0715 
0716         rulerMode = false;
0717     }
0718 }
0719 
0720 void SkyMap::slotCancelRulerMode(void)
0721 {
0722     rulerMode = false;
0723     AngularRuler.clear();
0724 }
0725 
0726 void SkyMap::slotAddFlag()
0727 {
0728     KStars *ks = KStars::Instance();
0729 
0730     // popup FlagManager window and update coordinates
0731     ks->slotFlagManager();
0732     ks->flagManager()->clearFields();
0733 
0734     //ra and dec must be the coordinates at J2000.  If we clicked on an object, just use the object's ra0, dec0 coords
0735     //if we clicked on empty sky, we need to precess to J2000.
0736 
0737     dms J2000RA, J2000DE;
0738 
0739     if (clickedObject())
0740     {
0741         J2000RA = clickedObject()->ra0();
0742         J2000DE = clickedObject()->dec0();
0743     }
0744     else
0745     {
0746         //SkyPoint deprecessedPoint = clickedPoint()->deprecess(data->updateNum());
0747         SkyPoint deprecessedPoint = clickedPoint()->catalogueCoord(data->updateNum()->julianDay());
0748         deprecessedPoint.catalogueCoord(data->updateNum()->julianDay());
0749         J2000RA                   = deprecessedPoint.ra();
0750         J2000DE                   = deprecessedPoint.dec();
0751     }
0752 
0753     ks->flagManager()->setRaDec(J2000RA, J2000DE);
0754 }
0755 
0756 void SkyMap::slotEditFlag(int flagIdx)
0757 {
0758     KStars *ks = KStars::Instance();
0759 
0760     // popup FlagManager window and switch to selected flag
0761     ks->slotFlagManager();
0762     ks->flagManager()->showFlag(flagIdx);
0763 }
0764 
0765 void SkyMap::slotDeleteFlag(int flagIdx)
0766 {
0767     KStars *ks = KStars::Instance();
0768 
0769     ks->data()->skyComposite()->flags()->remove(flagIdx);
0770     ks->data()->skyComposite()->flags()->saveToFile();
0771 
0772     // if there is FlagManager created, update its flag model
0773     if (ks->flagManager())
0774     {
0775         ks->flagManager()->deleteFlagItem(flagIdx);
0776     }
0777 }
0778 
0779 void SkyMap::slotImage()
0780 {
0781     const auto *action = qobject_cast<QAction *>(sender());
0782     const auto url     = action->data().toUrl();
0783     const QString message{ action->text().remove('&') };
0784 
0785     if (!url.isEmpty())
0786         new ImageViewer(url, clickedObject()->messageFromTitle(message), this);
0787 }
0788 
0789 void SkyMap::slotInfo()
0790 {
0791     const auto *action = qobject_cast<QAction *>(sender());
0792     const auto url     = action->data().toUrl();
0793 
0794     if (!url.isEmpty())
0795         QDesktopServices::openUrl(url);
0796 }
0797 
0798 bool SkyMap::isObjectLabeled(SkyObject *object)
0799 {
0800     return data->skyComposite()->labelObjects().contains(object);
0801 }
0802 
0803 SkyPoint SkyMap::getCenterPoint()
0804 {
0805     SkyPoint retVal;
0806     // FIXME: subtraction of these 0.00001 is a simple workaround, because wrong
0807     // SkyPoint is returned when _exact_ center of SkyMap is passed to the projector.
0808     retVal = projector()->fromScreen(QPointF((qreal)width() / 2 - 0.00001, (qreal)height() / 2 - 0.00001), data->lst(),
0809                                      data->geo()->lat());
0810     return retVal;
0811 }
0812 
0813 void SkyMap::slotRemoveObjectLabel()
0814 {
0815     data->skyComposite()->removeNameLabel(clickedObject());
0816     forceUpdate();
0817 }
0818 
0819 void SkyMap::slotRemoveCustomObject()
0820 {
0821     auto *object = dynamic_cast<CatalogObject *>(clickedObject());
0822     if (!object)
0823         return;
0824 
0825     const auto &cat = object->getCatalog();
0826     if (!cat.mut)
0827         return;
0828 
0829     CatalogsDB::DBManager manager{ CatalogsDB::dso_db_path() };
0830     manager.remove_object(cat.id, object->getObjectId());
0831 
0832     emit removeSkyObject(object);
0833     data->skyComposite()->removeFromNames(object);
0834     data->skyComposite()->removeFromLists(object);
0835     data->skyComposite()->reloadDeepSky();
0836     KStars::Instance()->updateTime();
0837 }
0838 
0839 void SkyMap::slotAddObjectLabel()
0840 {
0841     data->skyComposite()->addNameLabel(clickedObject());
0842     forceUpdate();
0843 }
0844 
0845 void SkyMap::slotRemovePlanetTrail()
0846 {
0847     TrailObject *tobj = dynamic_cast<TrailObject *>(clickedObject());
0848     if (tobj)
0849     {
0850         tobj->clearTrail();
0851         forceUpdate();
0852     }
0853 }
0854 
0855 void SkyMap::slotAddPlanetTrail()
0856 {
0857     TrailObject *tobj = dynamic_cast<TrailObject *>(clickedObject());
0858     if (tobj)
0859     {
0860         tobj->addToTrail();
0861         forceUpdate();
0862     }
0863 }
0864 
0865 void SkyMap::slotDetail()
0866 {
0867     // check if object is selected
0868     if (!clickedObject())
0869     {
0870         KMessageBox::sorry(this, i18n("No object selected."), i18n("Object Details"));
0871         return;
0872     }
0873     DetailDialog *detail = new DetailDialog(clickedObject(), data->ut(), data->geo(), KStars::Instance());
0874     detail->setAttribute(Qt::WA_DeleteOnClose);
0875     detail->show();
0876 }
0877 
0878 void SkyMap::slotObjectSelected()
0879 {
0880     if (m_objPointingMode && KStars::Instance()->printingWizard())
0881     {
0882         KStars::Instance()->printingWizard()->pointingDone(clickedObject());
0883         m_objPointingMode = false;
0884     }
0885 }
0886 
0887 void SkyMap::slotCancelLegendPreviewMode()
0888 {
0889     m_previewLegend = false;
0890     forceUpdate(true);
0891     KStars::Instance()->showImgExportDialog();
0892 }
0893 
0894 void SkyMap::slotFinishFovCaptureMode()
0895 {
0896     if (m_fovCaptureMode && KStars::Instance()->printingWizard())
0897     {
0898         KStars::Instance()->printingWizard()->fovCaptureDone();
0899         m_fovCaptureMode = false;
0900     }
0901 }
0902 
0903 void SkyMap::slotCaptureFov()
0904 {
0905     if (KStars::Instance()->printingWizard())
0906     {
0907         KStars::Instance()->printingWizard()->captureFov();
0908     }
0909 }
0910 
0911 void SkyMap::slotClockSlewing()
0912 {
0913     //If the current timescale exceeds slewTimeScale, set clockSlewing=true, and stop the clock.
0914     if ((fabs(data->clock()->scale()) > Options::slewTimeScale()) ^ clockSlewing)
0915     {
0916         data->clock()->setManualMode(!clockSlewing);
0917         clockSlewing = !clockSlewing;
0918         // don't change automatically the DST status
0919         KStars *kstars = KStars::Instance();
0920         if (kstars)
0921             kstars->updateTime(false);
0922     }
0923 }
0924 
0925 void SkyMap::setFocus(SkyPoint *p)
0926 {
0927     setFocus(p->ra(), p->dec());
0928 }
0929 
0930 void SkyMap::setFocus(const dms &ra, const dms &dec)
0931 {
0932     Options::setFocusRA(ra.Hours());
0933     Options::setFocusDec(dec.Degrees());
0934 
0935     focus()->set(ra, dec);
0936     focus()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
0937 }
0938 
0939 void SkyMap::setFocusAltAz(const dms &alt, const dms &az)
0940 {
0941     Options::setFocusRA(focus()->ra().Hours());
0942     Options::setFocusDec(focus()->dec().Degrees());
0943     focus()->setAlt(alt);
0944     focus()->setAz(az);
0945     focus()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
0946 
0947     slewing = false;
0948     forceUpdate(); //need a total update, or slewing with the arrow keys doesn't work.
0949 }
0950 
0951 void SkyMap::setDestination(const SkyPoint &p)
0952 {
0953     setDestination(p.ra(), p.dec());
0954 }
0955 
0956 void SkyMap::setDestination(const dms &ra, const dms &dec)
0957 {
0958     destination()->set(ra, dec);
0959     destination()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
0960     emit destinationChanged();
0961 }
0962 
0963 void SkyMap::setDestinationAltAz(const dms &alt, const dms &az, bool altIsRefracted)
0964 {
0965     if (altIsRefracted)
0966     {
0967         // The alt in the SkyPoint is always actual, not apparent
0968         destination()->setAlt(SkyPoint::unrefract(alt));
0969     }
0970     else
0971     {
0972         destination()->setAlt(alt);
0973     }
0974     destination()->setAz(az);
0975     destination()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
0976     emit destinationChanged();
0977 }
0978 
0979 void SkyMap::setClickedPoint(const SkyPoint *f)
0980 {
0981     ClickedPoint = *f;
0982 }
0983 
0984 void SkyMap::updateFocus()
0985 {
0986     if (slewing)
0987         return;
0988 
0989     //Tracking on an object
0990     if (Options::isTracking())
0991     {
0992         if (focusObject())
0993         {
0994             focusObject()->updateCoordsNow(data->updateNum());
0995             setDestination(*focusObject());
0996         }
0997         else
0998             setDestination(*focusPoint());
0999     }
1000     else
1001         focus()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
1002 }
1003 
1004 void SkyMap::slewFocus()
1005 {
1006     //Don't slew if the mouse button is pressed
1007     //Also, no animated slews if the Manual Clock is active
1008     //08/2002: added possibility for one-time skipping of slew with snapNextFocus
1009     if (!mouseButtonDown)
1010     {
1011         bool goSlew = (Options::useAnimatedSlewing() && !data->snapNextFocus()) &&
1012                       !(data->clock()->isManualMode() && data->clock()->isActive());
1013         if (goSlew)
1014         {
1015             double dX, dY;
1016             double maxstep = 10.0;
1017             if (Options::useAltAz())
1018             {
1019                 dX = destination()->az().Degrees() - focus()->az().Degrees();
1020                 dY = destination()->alt().Degrees() - focus()->alt().Degrees();
1021             }
1022             else
1023             {
1024                 dX = destination()->ra().Degrees() - focus()->ra().Degrees();
1025                 dY = destination()->dec().Degrees() - focus()->dec().Degrees();
1026             }
1027 
1028             //switch directions to go the short way around the celestial sphere, if necessary.
1029             dX = KSUtils::reduceAngle(dX, -180.0, 180.0);
1030 
1031             double r0 = sqrt(dX * dX + dY * dY);
1032             if (r0 < 20.0) //smaller slews have smaller maxstep
1033             {
1034                 maxstep *= (10.0 + 0.5 * r0) / 20.0;
1035             }
1036             double step = 0.5;
1037             double r    = r0;
1038             while (r > step)
1039             {
1040                 //DEBUG
1041                 //qDebug() << Q_FUNC_INFO << step << ": " << r << ": " << r0;
1042                 double fX = dX / r;
1043                 double fY = dY / r;
1044 
1045                 if (Options::useAltAz())
1046                 {
1047                     focus()->setAlt(focus()->alt().Degrees() + fY * step);
1048                     focus()->setAz(dms(focus()->az().Degrees() + fX * step).reduce());
1049                     focus()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
1050                 }
1051                 else
1052                 {
1053                     fX = fX / 15.; //convert RA degrees to hours
1054                     SkyPoint newFocus(focus()->ra().Hours() + fX * step, focus()->dec().Degrees() + fY * step);
1055                     setFocus(&newFocus);
1056                     focus()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
1057                 }
1058 
1059                 slewing = true;
1060 
1061                 forceUpdate();
1062                 qApp->processEvents(); //keep up with other stuff
1063 
1064                 if (Options::useAltAz())
1065                 {
1066                     dX = destination()->az().Degrees() - focus()->az().Degrees();
1067                     dY = destination()->alt().Degrees() - focus()->alt().Degrees();
1068                 }
1069                 else
1070                 {
1071                     dX = destination()->ra().Degrees() - focus()->ra().Degrees();
1072                     dY = destination()->dec().Degrees() - focus()->dec().Degrees();
1073                 }
1074 
1075                 //switch directions to go the short way around the celestial sphere, if necessary.
1076                 dX = KSUtils::reduceAngle(dX, -180.0, 180.0);
1077                 r  = sqrt(dX * dX + dY * dY);
1078 
1079                 //Modify step according to a cosine-shaped profile
1080                 //centered on the midpoint of the slew
1081                 //NOTE: don't allow the full range from -PI/2 to PI/2
1082                 //because the slew will never reach the destination as
1083                 //the speed approaches zero at the end!
1084                 double t = dms::PI * (r - 0.5 * r0) / (1.05 * r0);
1085                 step     = cos(t) * maxstep;
1086             }
1087         }
1088 
1089         //Either useAnimatedSlewing==false, or we have slewed, and are within one step of destination
1090         //set focus=destination.
1091         if (Options::useAltAz())
1092         {
1093             setFocusAltAz(destination()->alt(), destination()->az());
1094             focus()->HorizontalToEquatorial(data->lst(), data->geo()->lat());
1095         }
1096         else
1097         {
1098             setFocus(destination());
1099             focus()->EquatorialToHorizontal(data->lst(), data->geo()->lat());
1100         }
1101 
1102         slewing = false;
1103 
1104         //Turn off snapNextFocus, we only want it to happen once
1105         if (data->snapNextFocus())
1106         {
1107             data->setSnapNextFocus(false);
1108         }
1109 
1110         //Start the HoverTimer. if the user leaves the mouse in place after a slew,
1111         //we want to attach a label to the nearest object.
1112         if (Options::useHoverLabel())
1113             m_HoverTimer.start(HOVER_INTERVAL);
1114 
1115         forceUpdate();
1116     }
1117 }
1118 
1119 void SkyMap::slotZoomIn()
1120 {
1121     setZoomFactor(Options::zoomFactor() * DZOOM);
1122 }
1123 
1124 void SkyMap::slotZoomOut()
1125 {
1126     setZoomFactor(Options::zoomFactor() / DZOOM);
1127 }
1128 
1129 void SkyMap::slotZoomDefault()
1130 {
1131     setZoomFactor(DEFAULTZOOM);
1132 }
1133 
1134 void SkyMap::setZoomFactor(double factor)
1135 {
1136     Options::setZoomFactor(KSUtils::clamp(factor, MINZOOM, MAXZOOM));
1137     forceUpdate();
1138     emit zoomChanged();
1139 }
1140 
1141 // force a new calculation of the skymap (used instead of update(), which may skip the redraw)
1142 // if now=true, SkyMap::paintEvent() is run immediately, rather than being added to the event queue
1143 // also, determine new coordinates of mouse cursor.
1144 void SkyMap::forceUpdate(bool now)
1145 {
1146     QPoint mp(mapFromGlobal(QCursor::pos()));
1147     if (!projector()->unusablePoint(mp))
1148     {
1149         //determine RA, Dec of mouse pointer
1150         m_MousePoint = projector()->fromScreen(mp, data->lst(), data->geo()->lat());
1151     }
1152 
1153     computeSkymap = true;
1154 
1155     // Ensure that stars are recomputed
1156     data->incUpdateID();
1157 
1158     if (now)
1159         m_SkyMapDraw->repaint();
1160     else
1161         m_SkyMapDraw->update();
1162 }
1163 
1164 float SkyMap::fov()
1165 {
1166     float diagonalPixels = sqrt(static_cast<double>(width() * width() + height() * height()));
1167     return diagonalPixels / (2 * Options::zoomFactor() * dms::DegToRad);
1168 }
1169 
1170 void SkyMap::setupProjector()
1171 {
1172     //Update View Parameters for projection
1173     ViewParams p;
1174     p.focus         = focus();
1175     p.height        = height();
1176     p.width         = width();
1177     p.useAltAz      = Options::useAltAz();
1178     p.useRefraction = Options::useRefraction();
1179     p.zoomFactor    = Options::zoomFactor();
1180     p.fillGround    = Options::showGround();
1181     //Check if we need a new projector
1182     if (m_proj && Options::projection() == m_proj->type())
1183         m_proj->setViewParams(p);
1184     else
1185     {
1186         delete m_proj;
1187         switch (Options::projection())
1188         {
1189             case Gnomonic:
1190                 m_proj = new GnomonicProjector(p);
1191                 break;
1192             case Stereographic:
1193                 m_proj = new StereographicProjector(p);
1194                 break;
1195             case Orthographic:
1196                 m_proj = new OrthographicProjector(p);
1197                 break;
1198             case AzimuthalEquidistant:
1199                 m_proj = new AzimuthalEquidistantProjector(p);
1200                 break;
1201             case Equirectangular:
1202                 m_proj = new EquirectangularProjector(p);
1203                 break;
1204             case Lambert:
1205             default:
1206                 //TODO: implement other projection classes
1207                 m_proj = new LambertProjector(p);
1208                 break;
1209         }
1210     }
1211 }
1212 
1213 void SkyMap::setZoomMouseCursor()
1214 {
1215     mouseMoveCursor = false; // no mousemove cursor
1216     mouseDragCursor = false;
1217     QBitmap cursor  = zoomCursorBitmap(2);
1218     QBitmap mask    = zoomCursorBitmap(4);
1219     setCursor(QCursor(cursor, mask));
1220 }
1221 
1222 void SkyMap::setMouseCursorShape(Cursor type)
1223 {
1224     // no mousemove cursor
1225     mouseMoveCursor = false;
1226     mouseDragCursor = false;
1227 
1228     switch (type)
1229     {
1230         case Cross:
1231         {
1232             QBitmap cursor  = defaultCursorBitmap(2);
1233             QBitmap mask    = defaultCursorBitmap(3);
1234             setCursor(QCursor(cursor, mask));
1235         }
1236         break;
1237 
1238         case Circle:
1239         {
1240             QBitmap cursor  = circleCursorBitmap(2);
1241             QBitmap mask    = circleCursorBitmap(3);
1242             setCursor(QCursor(cursor, mask));
1243         }
1244         break;
1245 
1246         case NoCursor:
1247             setCursor(Qt::ArrowCursor);
1248             break;
1249     }
1250 }
1251 
1252 void SkyMap::setMouseMoveCursor()
1253 {
1254     if (mouseButtonDown)
1255     {
1256         setCursor(Qt::SizeAllCursor); // cursor shape defined in qt
1257         mouseMoveCursor = true;
1258     }
1259 }
1260 
1261 void SkyMap::setMouseDragCursor()
1262 {
1263     if (mouseButtonDown)
1264     {
1265         setCursor(Qt::OpenHandCursor); // cursor shape defined in qt
1266         mouseDragCursor = true;
1267     }
1268 }
1269 
1270 void SkyMap::updateAngleRuler()
1271 {
1272     if (rulerMode && (!pmenu || !pmenu->isVisible()))
1273         AngularRuler.setPoint(1, &m_MousePoint);
1274     AngularRuler.update(data);
1275 }
1276 
1277 bool SkyMap::isSlewing() const
1278 {
1279     return (slewing || (clockSlewing && data->clock()->isActive()));
1280 }
1281 
1282 void SkyMap::slotStartXplanetViewer()
1283 {
1284     if(clickedObject())
1285         new XPlanetImageViewer(clickedObject()->name(), this);
1286     else
1287         new XPlanetImageViewer(i18n("Saturn"), this);
1288 }
1289 
1290