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 }