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

0001 /*
0002     SPDX-FileCopyrightText: 2014 Akarsh Simha <akarsh.simha@kdemail.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "eyepiecefield.h"
0008 
0009 #include "exporteyepieceview.h"
0010 #include "fov.h"
0011 #include "ksdssdownloader.h"
0012 #include "kstars.h"
0013 #include "ksnotification.h"
0014 #include "Options.h"
0015 #include "skymap.h"
0016 #include "skyqpainter.h"
0017 
0018 #include <QBitmap>
0019 #include <QCheckBox>
0020 #include <QComboBox>
0021 #include <QHBoxLayout>
0022 #include <QLabel>
0023 #include <QPushButton>
0024 #include <QSlider>
0025 #include <QSvgGenerator>
0026 #include <QSvgRenderer>
0027 #include <QVBoxLayout>
0028 
0029 #include <kstars_debug.h>
0030 
0031 EyepieceField::EyepieceField(QWidget *parent) : QDialog(parent)
0032 {
0033 #ifdef Q_OS_OSX
0034     setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
0035 #endif
0036 
0037     setWindowTitle(i18nc("@title:window", "Eyepiece Field View"));
0038 
0039     m_sp         = nullptr;
0040     m_dt         = nullptr;
0041     m_currentFOV = nullptr;
0042     m_fovWidth = m_fovHeight = 0;
0043     m_dler                   = nullptr;
0044 
0045     QWidget *mainWidget     = new QWidget(this);
0046     QVBoxLayout *mainLayout = new QVBoxLayout;
0047     mainLayout->addWidget(mainWidget);
0048     setLayout(mainLayout);
0049 
0050     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
0051     buttonBox->addButton(i18nc("Export image", "Export"), QDialogButtonBox::AcceptRole);
0052     mainLayout->addWidget(buttonBox);
0053     connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
0054     connect(buttonBox, SIGNAL(accepted()), this, SLOT(slotExport()));
0055 
0056     QVBoxLayout *rows = new QVBoxLayout;
0057     mainWidget->setLayout(rows);
0058 
0059     m_skyChartDisplay = new QLabel;
0060     m_skyChartDisplay->setBackgroundRole(QPalette::Base);
0061     m_skyChartDisplay->setScaledContents(false);
0062     m_skyChartDisplay->setMinimumWidth(400);
0063 
0064     m_skyImageDisplay = new QLabel;
0065     m_skyImageDisplay->setBackgroundRole(QPalette::Base);
0066     m_skyImageDisplay->setScaledContents(false);
0067     m_skyImageDisplay->setMinimumWidth(400);
0068     m_skyImageDisplay->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
0069 
0070     QHBoxLayout *imageLayout = new QHBoxLayout;
0071     rows->addLayout(imageLayout);
0072     imageLayout->addWidget(m_skyChartDisplay);
0073     imageLayout->addWidget(m_skyImageDisplay);
0074 
0075     m_invertView   = new QCheckBox(i18n("Invert view"), this);
0076     m_flipView     = new QCheckBox(i18n("Flip view"), this);
0077     m_overlay      = new QCheckBox(i18n("Overlay"), this);
0078     m_invertColors = new QCheckBox(i18n("Invert colors"), this);
0079     m_getDSS       = new QPushButton(i18n("Fetch DSS image"), this);
0080 
0081     m_getDSS->setVisible(false);
0082 
0083     QHBoxLayout *optionsLayout = new QHBoxLayout;
0084     optionsLayout->addWidget(m_invertView);
0085     optionsLayout->addWidget(m_flipView);
0086     optionsLayout->addStretch();
0087     optionsLayout->addWidget(m_overlay);
0088     optionsLayout->addWidget(m_invertColors);
0089     optionsLayout->addWidget(m_getDSS);
0090 
0091     rows->addLayout(optionsLayout);
0092 
0093     m_rotationSlider = new QSlider(Qt::Horizontal, this);
0094     m_rotationSlider->setMaximum(180);
0095     m_rotationSlider->setMinimum(-180);
0096     m_rotationSlider->setTickInterval(30);
0097     m_rotationSlider->setPageStep(30);
0098 
0099     QLabel *sliderLabel = new QLabel(i18n("Rotation:"), this);
0100 
0101     m_presetCombo = new QComboBox(this);
0102     m_presetCombo->addItem(i18n("None"));
0103     m_presetCombo->addItem(i18n("Vanilla"));
0104     m_presetCombo->addItem(i18n("Flipped"));
0105     m_presetCombo->addItem(i18n("Refractor"));
0106     m_presetCombo->addItem(i18n("Dobsonian"));
0107 
0108     QLabel *presetLabel = new QLabel(i18n("Preset:"), this);
0109 
0110     QHBoxLayout *rotationLayout = new QHBoxLayout;
0111     rotationLayout->addWidget(sliderLabel);
0112     rotationLayout->addWidget(m_rotationSlider);
0113     rotationLayout->addWidget(presetLabel);
0114     rotationLayout->addWidget(m_presetCombo);
0115 
0116     rows->addLayout(rotationLayout);
0117 
0118     connect(m_invertView, SIGNAL(stateChanged(int)), this, SLOT(render()));
0119     connect(m_flipView, SIGNAL(stateChanged(int)), this, SLOT(render()));
0120     connect(m_invertColors, SIGNAL(stateChanged(int)), this, SLOT(render()));
0121     connect(m_overlay, SIGNAL(stateChanged(int)), this, SLOT(render()));
0122     connect(m_rotationSlider, SIGNAL(valueChanged(int)), this, SLOT(render()));
0123     connect(m_presetCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotEnforcePreset(int)));
0124     connect(m_presetCombo, SIGNAL(activated(int)), this, SLOT(slotEnforcePreset(int)));
0125     connect(m_getDSS, SIGNAL(clicked()), this, SLOT(slotDownloadDss()));
0126 }
0127 
0128 void EyepieceField::slotEnforcePreset(int index)
0129 {
0130     if (index == -1)
0131         index = m_presetCombo->currentIndex();
0132     if (index == -1)
0133         index = 0;
0134 
0135     if (index == 0)
0136         return; // Preset "None" makes no changes
0137 
0138     double altAzRot = (m_usedAltAz ? 0.0 : findNorthAngle(m_sp, KStarsData::Instance()->geo()->lat()).Degrees());
0139     if (altAzRot > 180.0)
0140         altAzRot -= 360.0;
0141     double dobRot = altAzRot - m_sp->alt().Degrees(); // set rotation to altitude CW
0142     if (dobRot > 180.0)
0143         dobRot -= 360.0;
0144     if (dobRot < -180.0)
0145         dobRot += 360.0;
0146     switch (index)
0147     {
0148         case 1:
0149             // Preset vanilla
0150             m_rotationSlider->setValue(0.0); // reset rotation
0151             m_invertView->setChecked(false); // reset inversion
0152             m_flipView->setChecked(false);   // reset flip
0153             break;
0154         case 2:
0155             // Preset flipped
0156             m_rotationSlider->setValue(0.0); // reset rotation
0157             m_invertView->setChecked(false); // reset inversion
0158             m_flipView->setChecked(true);    // set flip
0159             break;
0160         case 3:
0161             // Preset refractor
0162             m_rotationSlider->setValue(altAzRot);
0163             m_invertView->setChecked(true);
0164             m_flipView->setChecked(false);
0165             break;
0166         case 4:
0167             // Preset Dobsonian
0168             m_rotationSlider->setValue(dobRot); // set rotation for dob
0169             m_invertView->setChecked(true);     // set inversion
0170             m_flipView->setChecked(false);
0171             break;
0172         default:
0173             break;
0174     }
0175 }
0176 
0177 void EyepieceField::showEyepieceField(SkyPoint *sp, FOV const *const fov, const QString &imagePath)
0178 {
0179     double fovWidth, fovHeight;
0180 
0181     Q_ASSERT(sp);
0182 
0183     // See if we were supplied a sky image; if so, load its metadata
0184     // Set up the new sky map FOV and pointing. full map FOV = 4 times the given FOV.
0185     if (fov)
0186     {
0187         fovWidth  = fov->sizeX();
0188         fovHeight = fov->sizeY();
0189     }
0190     else if (QFile::exists(imagePath))
0191     {
0192         fovWidth = fovHeight = -1.0; // figure out from the image.
0193     }
0194     else
0195     {
0196         //Q_ASSERT( false );
0197         // Don't crash the program
0198         KSNotification::error(i18n(("No image found. Please specify the exact FOV.")));
0199         return;
0200     }
0201 
0202     showEyepieceField(sp, fovWidth, fovHeight, imagePath);
0203     m_currentFOV = fov;
0204 }
0205 
0206 void EyepieceField::showEyepieceField(SkyPoint *sp, const double fovWidth, double fovHeight, const QString &imagePath)
0207 {
0208     if (m_skyChart.get() == nullptr)
0209         m_skyChart.reset(new QImage());
0210 
0211     if (QFile::exists(imagePath))
0212     {
0213         qCDebug(KSTARS) << "Image path " << imagePath << " exists";
0214         if (m_skyImage.get() == nullptr)
0215         {
0216             qCDebug(KSTARS) << "Sky image did not exist, creating.";
0217             m_skyImage.reset(new QImage());
0218         }
0219     }
0220     else
0221     {
0222         m_skyImage.reset();
0223     }
0224 
0225     m_usedAltAz = Options::useAltAz();
0226     generateEyepieceView(sp, m_skyChart.get(), m_skyImage.get(), fovWidth, fovHeight, imagePath);
0227 
0228     // Keep a copy for local purposes (computation of field rotation etc.)
0229     if (m_sp != sp)
0230     {
0231         if (m_sp)
0232             delete m_sp;
0233         m_sp = new SkyPoint(*sp);
0234     }
0235 
0236     // Update our date/time
0237     delete m_dt;
0238     m_dt = new KStarsDateTime(KStarsData::Instance()->ut());
0239 
0240     // Enforce preset as per selection, since we have loaded a new eyepiece view
0241     slotEnforcePreset(-1);
0242     // Render the display
0243     render();
0244     m_fovWidth   = fovWidth;
0245     m_fovHeight  = fovHeight;
0246     m_currentFOV = nullptr;
0247 }
0248 
0249 void EyepieceField::generateEyepieceView(SkyPoint *sp, QImage *skyChart, QImage *skyImage, const FOV *fov,
0250         const QString &imagePath)
0251 {
0252     if (fov)
0253     {
0254         generateEyepieceView(sp, skyChart, skyImage, fov->sizeX(), fov->sizeY(), imagePath);
0255     }
0256     else
0257     {
0258         generateEyepieceView(sp, skyChart, skyImage, -1.0, -1.0, imagePath);
0259     }
0260 }
0261 
0262 void EyepieceField::generateEyepieceView(SkyPoint *sp, QImage *skyChart, QImage *skyImage, double fovWidth,
0263         double fovHeight, const QString &imagePath)
0264 {
0265     SkyMap *map = SkyMap::Instance();
0266     KStars *ks  = KStars::Instance();
0267 
0268     Q_ASSERT(sp);
0269     Q_ASSERT(map);
0270     Q_ASSERT(ks);
0271     Q_ASSERT(skyChart);
0272 
0273     if (!skyChart)
0274         return;
0275 
0276     if (!map) // Requires initialization of Sky map.
0277         return;
0278 
0279     if (fovWidth <= 0)
0280     {
0281         if (!QFile::exists(imagePath))
0282             return;
0283         // Otherwise, we will assume that the user wants the FOV of the image and we'll try to guess it from there
0284     }
0285     if (fovHeight <= 0)
0286         fovHeight = fovWidth;
0287 
0288     // Get DSS image width / height
0289     double dssWidth = 0, dssHeight = 0;
0290 
0291     if (QFile::exists(imagePath))
0292     {
0293         KSDssImage dssImage(imagePath);
0294         dssWidth  = dssImage.getMetadata().width;
0295         dssHeight = dssImage.getMetadata().height;
0296         if (!dssImage.getMetadata().isValid() || dssWidth == 0 || dssHeight == 0)
0297         {
0298             // Metadata unavailable, guess based on most common DSS arcsec/pixel
0299             //const double dssArcSecPerPixel = 1.01;
0300             dssWidth  = dssImage.getImage().width() * 1.01 / 60.0;
0301             dssHeight = dssImage.getImage().height() * 1.01 / 60.0;
0302         }
0303         qCDebug(KSTARS) << "DSS width: " << dssWidth << " height: " << dssHeight;
0304     }
0305 
0306     // Set FOV width/height from DSS if necessary
0307     if (fovWidth <= 0)
0308     {
0309         fovWidth  = dssWidth;
0310         fovHeight = dssHeight;
0311     }
0312 
0313     // Grab the sky chart
0314     // Save the current state of the sky map
0315     SkyPoint *oldFocus   = map->focus();
0316     double oldZoomFactor = Options::zoomFactor();
0317 
0318     // Set the right zoom
0319     ks->setApproxFOV(((fovWidth > fovHeight) ? fovWidth : fovHeight) / 15.0);
0320 
0321     //    map->setFocus( sp ); // FIXME: Why does setFocus() need a non-const SkyPoint pointer?
0322     KStarsData *const data = KStarsData::Instance();
0323     sp->updateCoords(data->updateNum(), true, data->geo()->lat(), data->lst(), false);
0324     map->setClickedPoint(sp);
0325     map->slotCenter();
0326     qApp->processEvents();
0327 
0328     // Repeat -- dirty workaround for some problem in KStars
0329     map->setClickedPoint(sp);
0330     map->slotCenter();
0331     qApp->processEvents();
0332 
0333     // determine screen arcminutes per pixel value
0334     const double arcMinToScreen = dms::PI * Options::zoomFactor() / 10800.0;
0335 
0336     // Vector export
0337     QTemporaryFile myTempSvgFile;
0338     myTempSvgFile.open();
0339 
0340     // export as SVG
0341     QSvgGenerator svgGenerator;
0342     svgGenerator.setFileName(myTempSvgFile.fileName());
0343     // svgGenerator.setTitle(i18n(""));
0344     // svgGenerator.setDescription(i18n(""));
0345     svgGenerator.setSize(QSize(map->width(), map->height()));
0346     svgGenerator.setResolution(qMax(map->logicalDpiX(), map->logicalDpiY()));
0347     svgGenerator.setViewBox(QRect(map->width() / 2.0 - arcMinToScreen * fovWidth / 2.0,
0348                                   map->height() / 2.0 - arcMinToScreen * fovHeight / 2.0, arcMinToScreen * fovWidth,
0349                                   arcMinToScreen * fovHeight));
0350 
0351     SkyQPainter painter(KStars::Instance(), &svgGenerator);
0352     painter.begin();
0353 
0354     map->exportSkyImage(&painter);
0355 
0356     painter.end();
0357 
0358     // Render SVG file on raster QImage canvas
0359     QSvgRenderer svgRenderer(myTempSvgFile.fileName());
0360     QImage *mySkyChart = new QImage(arcMinToScreen * fovWidth * 2.0, arcMinToScreen * fovHeight * 2.0,
0361                                     QImage::Format_ARGB32); // 2 times bigger in both dimensions.
0362     QPainter p2(mySkyChart);
0363     svgRenderer.render(&p2);
0364     p2.end();
0365     *skyChart = *mySkyChart;
0366     delete mySkyChart;
0367 
0368     myTempSvgFile.close();
0369 
0370     // Reset the sky-map
0371     map->setZoomFactor(oldZoomFactor);
0372     map->setClickedPoint(oldFocus);
0373     map->slotCenter();
0374     qApp->processEvents();
0375 
0376     // Repeat -- dirty workaround for some problem in KStars
0377     map->setZoomFactor(oldZoomFactor);
0378     map->setClickedPoint(oldFocus);
0379     map->slotCenter();
0380     qApp->processEvents();
0381     map->forceUpdate();
0382 
0383     // Prepare the sky image
0384     if (QFile::exists(imagePath) && skyImage)
0385     {
0386         QImage *mySkyImage = new QImage(int(arcMinToScreen * fovWidth * 2.0), int(arcMinToScreen * fovHeight * 2.0),
0387                                         QImage::Format_ARGB32);
0388 
0389         mySkyImage->fill(Qt::transparent);
0390 
0391         QPainter p(mySkyImage);
0392         QImage rawImg(imagePath);
0393 
0394         if (rawImg.isNull())
0395         {
0396             qWarning() << "Image constructed from " << imagePath
0397                        << "is a null image! Are you sure you supplied an image file? Continuing nevertheless...";
0398         }
0399 
0400         QImage img     = rawImg.scaled(arcMinToScreen * dssWidth * 2.0, arcMinToScreen * dssHeight * 2.0,
0401                                        Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
0402         const auto ksd = KStarsData::Instance();
0403         sp->updateCoordsNow(ksd->updateNum());
0404 
0405         if (Options::useAltAz())
0406         {
0407             // Need to rotate the image so that up is towards zenith rather than north.
0408             sp->EquatorialToHorizontal(ksd->lst(), ksd->geo()->lat());
0409             dms northBearing = findNorthAngle(sp, ksd->geo()->lat());
0410             qCDebug(KSTARS) << "North angle = " << northBearing.toDMSString();
0411 
0412             QTransform transform;
0413 
0414             transform.rotate(northBearing.Degrees());
0415             img = img.transformed(transform, Qt::SmoothTransformation);
0416         }
0417         p.drawImage(
0418             QPointF(mySkyImage->width() / 2.0 - img.width() / 2.0, mySkyImage->height() / 2.0 - img.height() / 2.0),
0419             img);
0420         p.end();
0421 
0422         *skyImage = *mySkyImage;
0423         delete mySkyImage;
0424     }
0425 }
0426 
0427 void EyepieceField::renderEyepieceView(const QImage *skyChart, QPixmap *renderChart, const double rotation,
0428                                        const double scale, const bool flip, const bool invert, const QImage *skyImage,
0429                                        QPixmap *renderImage, const bool overlay, const bool invertColors)
0430 {
0431     QTransform transform;
0432     bool deleteRenderImage = false;
0433     transform.rotate(rotation);
0434     if (flip)
0435         transform.scale(-1, 1);
0436     if (invert)
0437         transform.scale(-1, -1);
0438     transform.scale(scale, scale);
0439 
0440     Q_ASSERT(skyChart && renderChart);
0441     if (!skyChart || !renderChart)
0442         return;
0443 
0444     *renderChart = QPixmap::fromImage(skyChart->transformed(transform, Qt::SmoothTransformation));
0445 
0446     if (skyImage)
0447     {
0448         Q_ASSERT(overlay || renderImage); // in debug mode, check for calls that supply skyImage but not renderImage
0449     }
0450     if (overlay && !renderImage)
0451     {
0452         renderImage       = new QPixmap(); // temporary, used for rendering skymap before overlay is done.
0453         deleteRenderImage = true;          // we created it, so we must delete it.
0454     }
0455 
0456     if (skyImage && renderImage)
0457     {
0458         if (skyImage->isNull())
0459             qWarning() << "Sky image supplied to renderEyepieceView() for rendering is a Null image!";
0460         QImage i;
0461         i = skyImage->transformed(transform, Qt::SmoothTransformation);
0462         if (invertColors)
0463             i.invertPixels();
0464         *renderImage = QPixmap::fromImage(i);
0465     }
0466     if (overlay && skyImage)
0467     {
0468         QColor skyColor = KStarsData::Instance()->colorScheme()->colorNamed("SkyColor");
0469         QBitmap mask    = QBitmap::fromImage(
0470                               skyChart->createMaskFromColor(skyColor.rgb()).transformed(transform, Qt::SmoothTransformation));
0471         renderChart->setMask(mask);
0472         QPainter p(renderImage);
0473         p.drawImage(QPointF(renderImage->width() / 2.0 - renderChart->width() / 2.0,
0474                             renderImage->height() / 2.0 - renderChart->height() / 2.0),
0475                     renderChart->toImage());
0476         QPixmap temp(renderImage->width(), renderImage->height());
0477         temp.fill(skyColor);
0478         QPainter p2(&temp);
0479         p2.drawImage(QPointF(0, 0), renderImage->toImage());
0480         p2.end();
0481         p.end();
0482         *renderChart = *renderImage = temp;
0483     }
0484     if (deleteRenderImage)
0485         delete renderImage;
0486 }
0487 
0488 void EyepieceField::renderEyepieceView(SkyPoint *sp, QPixmap *renderChart, double fovWidth, double fovHeight,
0489                                        const double rotation, const double scale, const bool flip, const bool invert,
0490                                        const QString &imagePath, QPixmap *renderImage, const bool overlay,
0491                                        const bool invertColors)
0492 {
0493     QImage *skyChart, *skyImage = nullptr;
0494     skyChart = new QImage();
0495     if (QFile::exists(imagePath) && (renderImage || overlay))
0496         skyImage = new QImage();
0497     generateEyepieceView(sp, skyChart, skyImage, fovWidth, fovHeight, imagePath);
0498     renderEyepieceView(skyChart, renderChart, rotation, scale, flip, invert, skyImage, renderImage, overlay,
0499                        invertColors);
0500     delete skyChart;
0501     delete skyImage;
0502 }
0503 
0504 void EyepieceField::render()
0505 {
0506     double rotation   = m_rotationSlider->value();
0507     bool flip         = m_flipView->isChecked();
0508     bool invert       = m_invertView->isChecked();
0509     bool invertColors = m_invertColors->isChecked();
0510     bool overlay      = m_overlay->isChecked() && m_skyImage.get();
0511 
0512     Q_ASSERT(m_skyChart.get());
0513 
0514     renderEyepieceView(m_skyChart.get(), &m_renderChart, rotation, 1.0, flip, invert, m_skyImage.get(), &m_renderImage,
0515                        overlay, invertColors);
0516 
0517     m_skyChartDisplay->setVisible(!overlay);
0518     if (m_skyImage.get() != nullptr)
0519     {
0520         m_skyImageDisplay->setVisible(true);
0521         m_overlay->setVisible(true);
0522         m_invertColors->setVisible(true);
0523         m_getDSS->setVisible(false);
0524     }
0525     else
0526     {
0527         m_skyImageDisplay->setVisible(false);
0528         m_overlay->setVisible(false);
0529         m_invertColors->setVisible(false);
0530         m_getDSS->setVisible(true);
0531     }
0532 
0533     if (!overlay)
0534         m_skyChartDisplay->setPixmap(m_renderChart.scaled(m_skyChartDisplay->width(), m_skyChartDisplay->height(),
0535                                      Qt::KeepAspectRatio, Qt::SmoothTransformation));
0536     if (m_skyImage.get() != nullptr)
0537         m_skyImageDisplay->setPixmap(m_renderImage.scaled(m_skyImageDisplay->width(), m_skyImageDisplay->height(),
0538                                      Qt::KeepAspectRatio, Qt::SmoothTransformation));
0539 
0540     update();
0541     show();
0542 }
0543 
0544 void EyepieceField::slotDownloadDss()
0545 {
0546     double fovWidth = 0, fovHeight = 0;
0547     if (m_fovWidth == 0 && m_currentFOV == nullptr)
0548     {
0549         fovWidth = fovHeight = 15.0;
0550     }
0551     else if (m_currentFOV)
0552     {
0553         fovWidth  = m_currentFOV->sizeX();
0554         fovHeight = m_currentFOV->sizeY();
0555     }
0556     if (!m_dler)
0557     {
0558         m_dler = new KSDssDownloader(this);
0559         connect(m_dler, SIGNAL(downloadComplete(bool)), SLOT(slotDssDownloaded(bool)));
0560     }
0561     KSDssImage::Metadata md;
0562     m_tempFile.open();
0563     QUrl srcUrl = QUrl(KSDssDownloader::getDSSURL(m_sp, fovWidth, fovHeight, "all", &md));
0564     m_dler->startSingleDownload(srcUrl, m_tempFile.fileName(), md);
0565     m_tempFile.close();
0566 }
0567 
0568 void EyepieceField::slotDssDownloaded(bool success)
0569 {
0570     if (!success)
0571     {
0572         KSNotification::sorry(i18n("Failed to download DSS/SDSS image."));
0573         return;
0574     }
0575     else
0576         showEyepieceField(m_sp, m_fovWidth, m_fovHeight, m_tempFile.fileName());
0577 }
0578 
0579 void EyepieceField::slotExport()
0580 {
0581     bool overlay = m_overlay->isChecked() && m_skyImage.get();
0582     new ExportEyepieceView(m_sp, *m_dt, ((m_skyImage.get() && !overlay) ? &m_renderImage : nullptr),
0583                            &m_renderChart, this);
0584 }
0585 
0586 dms EyepieceField::findNorthAngle(const SkyPoint *sp, const dms *lat)
0587 {
0588     Q_ASSERT(sp && lat);
0589 
0590     // NOTE: northAngle1 is the correction due to lunisolar precession
0591     // (needs testing and checking). northAngle2 is the correction due
0592     // to going from equatorial to horizontal coordinates.
0593 
0594     // FIXME: The following code is a guess at how to handle
0595     // precession. While it might work in many cases, it might fail in
0596     // some. Careful testing will be needed to ensure that all
0597     // conditions are met, esp. with getting the signs right when
0598     // using arccosine! Nutation and planetary precession corrections
0599     // have not been included. -- asimha
0600     // TODO: Look at the Meeus book and see if it has some formulas -- asimha
0601     const double equinoxPrecessionPerYear =
0602         (50.35 /
0603          3600.0); // Equinox precession in ecliptic longitude per year in degrees (ref: http://star-www.st-and.ac.uk/~fv/webnotes/chapt16.htm)
0604     dms netEquinoxPrecession(((sp->getLastPrecessJD() - J2000) / 365.25) * equinoxPrecessionPerYear);
0605     double cosNorthAngle1 =
0606         (netEquinoxPrecession.cos() - sp->dec0().sin() * sp->dec().sin()) / (sp->dec0().cos() * sp->dec().cos());
0607     double northAngle1 = acos(cosNorthAngle1);
0608     if (sp->getLastPrecessJD() < J2000)
0609         northAngle1 = -northAngle1;
0610     if (sp->dec0().Degrees() < 0)
0611         northAngle1 = -northAngle1;
0612     // We trust that EquatorialToHorizontal has been called on sp, after all, how else can it have an alt/az representation.
0613     // Use spherical cosine rule (the triangle with vertices at sp, zenith and NCP) to compute the angle between direction of increasing altitude and north
0614     double cosNorthAngle2 = (lat->sin() - sp->alt().sin() * sp->dec().sin()) / (sp->alt().cos() * sp->dec().cos());
0615     double northAngle2    = acos(cosNorthAngle2); // arccosine is blind to sign of the angle
0616     if (sp->az().reduce().Degrees() < 180.0)      // if on the eastern hemisphere, flip sign
0617         northAngle2 = -northAngle2;
0618     double northAngle = northAngle1 + northAngle2;
0619     qCDebug(KSTARS) << "Data: alt = " << sp->alt().toDMSString() << "; az = " << sp->az().toDMSString() << "; ra, dec ("
0620                     << sp->getLastPrecessJD() / 365.25 << ") = " << sp->ra().toHMSString() << "," << sp->dec().toDMSString()
0621                     << "; ra0,dec0 (J2000.0) = " << sp->ra0().toHMSString() << "," << sp->dec0().toDMSString();
0622     qCDebug(KSTARS) << "PA corrections: precession cosine = " << cosNorthAngle1 << "; angle = " << northAngle1
0623                     << "; horizontal = " << cosNorthAngle2 << "; angle = " << northAngle2;
0624     return dms(northAngle * 180 / M_PI);
0625 }