/***************************************************************************
                eyepiecefield.cpp  -  K Desktop Planetarium
                             -------------------
    begin                : Fri 30 May 2014 15:07:46 CDT
    copyright            : (c) 2014 by Akarsh Simha
    email                : akarsh.simha@kdemail.net
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/


/* Project Includes */
#include "eyepiecefield.h"
#include "exporteyepieceview.h"
#include "fov.h"
#include "skypoint.h"
#include "skymap.h"
#include "skyqpainter.h"
#include "kstars.h"
#include "Options.h"
#include "ksdssimage.h"
#include "kstarsdatetime.h"
#include "ksdssdownloader.h"
/* KDE Includes */

/* Qt Includes */
#include <QString>
#include <QLabel>
#include <QSlider>
#include <QComboBox>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QCheckBox>
#include <QImage>
#include <QPixmap>
#include <QBitmap>
#include <QTemporaryFile>
#include <QSvgRenderer>
#include <QSvgGenerator>

EyepieceField::EyepieceField( QWidget *parent ) : QDialog( parent ) {

#ifdef Q_OS_OSX
        setWindowFlags(Qt::Tool| Qt::WindowStaysOnTopHint);
#endif

    setWindowTitle( i18n( "Eyepiece Field View" ) );

    m_sp = 0;
    m_dt = 0;
    m_lat = 0;
    m_currentFOV = 0;
    m_fovWidth = m_fovHeight = 0;
    m_dler = 0;

    QWidget *mainWidget = new QWidget( this );
    QVBoxLayout *mainLayout = new QVBoxLayout;
    mainLayout->addWidget(mainWidget);
    setLayout(mainLayout);

    QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Close );
    buttonBox->addButton( i18nc("Export image", "Export"), QDialogButtonBox::AcceptRole );
    mainLayout->addWidget(buttonBox);
    connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
    connect(buttonBox, SIGNAL(accepted()), this, SLOT(slotExport()));

    QVBoxLayout *rows = new QVBoxLayout;
    mainWidget->setLayout( rows );

    m_skyChartDisplay = new QLabel;
    m_skyChartDisplay->setBackgroundRole( QPalette::Base );
    m_skyChartDisplay->setScaledContents( false );
    m_skyChartDisplay->setMinimumWidth( 400 );

    m_skyImageDisplay = new QLabel;
    m_skyImageDisplay->setBackgroundRole( QPalette::Base );
    m_skyImageDisplay->setScaledContents( false );
    m_skyImageDisplay->setMinimumWidth( 400 );
    m_skyImageDisplay->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Expanding );

    QHBoxLayout *imageLayout = new QHBoxLayout;
    rows->addLayout( imageLayout );
    imageLayout->addWidget( m_skyChartDisplay );
    imageLayout->addWidget( m_skyImageDisplay );

    m_invertView = new QCheckBox( i18n( "Invert view" ), this );
    m_flipView = new QCheckBox( i18n( "Flip view" ), this );
    m_overlay = new QCheckBox( i18n( "Overlay" ), this );
    m_invertColors = new QCheckBox( i18n( "Invert colors" ), this );
    m_getDSS = new QPushButton( i18n( "Fetch DSS image" ), this );

    m_getDSS->setVisible( false );

    QHBoxLayout *optionsLayout = new QHBoxLayout;
    optionsLayout->addWidget( m_invertView );
    optionsLayout->addWidget( m_flipView );
    optionsLayout->addStretch();
    optionsLayout->addWidget( m_overlay );
    optionsLayout->addWidget( m_invertColors );
    optionsLayout->addWidget( m_getDSS );

    rows->addLayout( optionsLayout );

    m_rotationSlider = new QSlider( Qt::Horizontal, this );
    m_rotationSlider->setMaximum( 180 );
    m_rotationSlider->setMinimum( -180 );
    m_rotationSlider->setTickInterval( 30 );
    m_rotationSlider->setPageStep( 30 );

    QLabel *sliderLabel = new QLabel( i18n( "Rotation: " ), this );

    m_presetCombo = new QComboBox( this );
    m_presetCombo->addItem( i18n( "None" ) );
    m_presetCombo->addItem( i18n( "Vanilla" ) );
    m_presetCombo->addItem( i18n( "Flipped" ) );
    m_presetCombo->addItem( i18n( "Refractor" ) );
    m_presetCombo->addItem( i18n( "Dobsonian" ) );

    QLabel *presetLabel = new QLabel( i18n( "Preset: " ), this );

    QHBoxLayout *rotationLayout = new QHBoxLayout;
    rotationLayout->addWidget( sliderLabel );
    rotationLayout->addWidget( m_rotationSlider );
    rotationLayout->addWidget( presetLabel );
    rotationLayout->addWidget( m_presetCombo );

    rows->addLayout( rotationLayout );

    connect( m_invertView, SIGNAL( stateChanged( int ) ), this, SLOT( render() ) );
    connect( m_flipView, SIGNAL( stateChanged( int ) ), this, SLOT( render() ) );
    connect( m_invertColors, SIGNAL( stateChanged( int ) ), this, SLOT( render() ) );
    connect( m_overlay, SIGNAL( stateChanged( int ) ), this, SLOT( render() ) );
    connect( m_rotationSlider, SIGNAL( valueChanged( int ) ), this, SLOT( render() ) );
    connect( m_presetCombo, SIGNAL( currentIndexChanged( int ) ), this, SLOT( slotEnforcePreset( int ) ) );
    connect( m_presetCombo, SIGNAL( activated( int ) ), this, SLOT( slotEnforcePreset( int ) ) );
    connect( m_getDSS, SIGNAL( clicked() ), this, SLOT( slotDownloadDss() ) );

    m_skyChart = 0;
    m_skyImage = 0;

}

void EyepieceField::slotEnforcePreset( int index ) {
    if( index == -1 )
        index = m_presetCombo->currentIndex();
    if( index == -1 )
        index = 0;

    if( index == 0 )
        return; // Preset "None" makes no changes

    double altAzRot = ( m_usedAltAz ? 0.0 : findNorthAngle( m_sp, KStarsData::Instance()->geo()->lat() ).Degrees() );
    if( altAzRot > 180.0 )
        altAzRot -= 360.0;
    double dobRot = altAzRot - m_sp->alt().Degrees();  // set rotation to altitude CW
    if( dobRot > 180.0 )
        dobRot -= 360.0;
    if( dobRot < -180.0 )
        dobRot += 360.0;
    switch( index ) {
    case 1:
        // Preset vanilla
        m_rotationSlider->setValue( 0.0 ); // reset rotation
        m_invertView->setChecked( false ); // reset inversion
        m_flipView->setChecked( false ); // reset flip
        break;
    case 2:
        // Preset flipped
        m_rotationSlider->setValue( 0.0 ); // reset rotation
        m_invertView->setChecked( false ); // reset inversion
        m_flipView->setChecked( true ); // set flip
        break;
    case 3:
        // Preset refractor
        m_rotationSlider->setValue( altAzRot );
        m_invertView->setChecked( true );
        m_flipView->setChecked( false );
        break;
    case 4:
        // Preset Dobsonian
        m_rotationSlider->setValue( dobRot ); // set rotation for dob
        m_invertView->setChecked( true ); // set inversion
        m_flipView->setChecked( false );
        break;
    default:
        break;
    }
}

void EyepieceField::showEyepieceField( SkyPoint *sp, FOV const * const fov, const QString &imagePath ) {

    double fovWidth, fovHeight;

    Q_ASSERT( sp );

    // See if we were supplied a sky image; if so, load its metadata
    // Set up the new sky map FOV and pointing. full map FOV = 4 times the given FOV.
    if( fov ) {
        fovWidth = fov->sizeX();
        fovHeight = fov->sizeY();
    }
    else if( QFile::exists( imagePath ) ) {
        fovWidth = fovHeight = -1.0; // figure out from the image.
    }
    else {
        //Q_ASSERT( false );
        // Don't crash the program
        KMessageBox::error(0, i18n(("No image found. Please specify the exact FOV.")));
        return;
    }

    showEyepieceField( sp, fovWidth, fovHeight, imagePath );
    m_currentFOV = fov;

}

void EyepieceField::showEyepieceField( SkyPoint *sp, const double fovWidth, double fovHeight, const QString &imagePath ) {

    if( !m_skyChart )
        m_skyChart = new QImage();

    if( QFile::exists( imagePath ) ) {
        qDebug() << "Image path " << imagePath << " exists";
        if( !m_skyImage ) {
            qDebug() << "Sky image did not exist, creating.";
            m_skyImage = new QImage();
        }
    }
    else {
        delete m_skyImage;
        m_skyImage = 0;
    }

    m_usedAltAz = Options::useAltAz();
    generateEyepieceView( sp, m_skyChart, m_skyImage, fovWidth, fovHeight, imagePath);
    m_lat = KStarsData::Instance()->geo()->lat()->radians();

    // Keep a copy for local purposes (computation of field rotation etc.)
    if( m_sp != sp ) {
        if( m_sp )
            delete m_sp;
        m_sp = new SkyPoint( *sp );
    }

    // Update our date/time
    delete m_dt;
    m_dt = new KStarsDateTime( KStarsData::Instance()->ut() );

    // Enforce preset as per selection, since we have loaded a new eyepiece view
    slotEnforcePreset( -1 );
    // Render the display
    render();
    m_fovWidth = fovWidth;
    m_fovHeight = fovHeight;
    m_currentFOV = 0;

}

void EyepieceField::generateEyepieceView( SkyPoint *sp, QImage *skyChart, QImage *skyImage, const FOV *fov, const QString &imagePath ) {
    if( fov ) {
        generateEyepieceView( sp, skyChart, skyImage, fov->sizeX(), fov->sizeY(), imagePath );
    }
    else {
        generateEyepieceView( sp, skyChart, skyImage, -1.0, -1.0, imagePath );
    }
}

void EyepieceField::generateEyepieceView( SkyPoint *sp, QImage *skyChart, QImage *skyImage, double fovWidth, double fovHeight,
                                          const QString &imagePath ) {

    SkyMap *map = SkyMap::Instance();
    KStars *ks = KStars::Instance();

    Q_ASSERT( sp );
    Q_ASSERT( map );
    Q_ASSERT( ks );
    Q_ASSERT( skyChart );

    if( !skyChart )
        return;

    if( !map ) // Requires initialization of Sky map.
        return;

    if( fovWidth <= 0 ) {
        if( !QFile::exists( imagePath ) )
            return;
        // Otherwise, we will assume that the user wants the FOV of the image and we'll try to guess it from there
    }
    if( fovHeight <= 0 )
        fovHeight = fovWidth;

    // Get DSS image width / height
    double dssWidth, dssHeight;
    if( QFile::exists( imagePath ) ) {
        KSDssImage dssImage( imagePath );
        dssWidth = dssImage.getMetadata().width;
        dssHeight = dssImage.getMetadata().height;
        if( !dssImage.getMetadata().isValid() || dssWidth == 0 || dssHeight == 0 ) {
            // Metadata unavailable, guess based on most common DSS arcsec/pixel
            //const double dssArcSecPerPixel = 1.01;
            dssWidth = dssImage.getImage().width()*1.01/60.0;
            dssHeight = dssImage.getImage().height()*1.01/60.0;
        }
        qDebug() << "DSS width: " << dssWidth << " height: " << dssHeight;
    }


    // Set FOV width/height from DSS if necessary
    if( fovWidth <= 0 ) {
        fovWidth = dssWidth;
        fovHeight = dssHeight;
    }

    // Grab the sky chart
    // Save the current state of the sky map
    SkyPoint *oldFocus = map->focus();
    double oldZoomFactor = Options::zoomFactor();

    // Set the right zoom
    ks->setApproxFOV( ( ( fovWidth > fovHeight ) ? fovWidth : fovHeight ) / 15.0 );

    //    map->setFocus( sp ); // FIXME: Why does setFocus() need a non-const SkyPoint pointer?
    KStarsData * const data = KStarsData::Instance();
    sp->updateCoords( data->updateNum(), true, data->geo()->lat(), data->lst(), false );
    map->setClickedPoint(sp);
    map->slotCenter();
    qApp->processEvents();

    // Repeat -- dirty workaround for some problem in KStars
    map->setClickedPoint(sp);
    map->slotCenter();
    qApp->processEvents();


    // determine screen arcminutes per pixel value
    const double arcMinToScreen = dms::PI * Options::zoomFactor() / 10800.0;

    // Vector export
    QTemporaryFile myTempSvgFile;
    myTempSvgFile.open();

    // export as SVG
    QSvgGenerator svgGenerator;
    svgGenerator.setFileName( myTempSvgFile.fileName() );
    // svgGenerator.setTitle(i18n(""));
    // svgGenerator.setDescription(i18n(""));
    svgGenerator.setSize( QSize(map->width(), map->height()) );
    svgGenerator.setResolution(qMax(map->logicalDpiX(), map->logicalDpiY()));
    svgGenerator.setViewBox( QRect( map->width()/2.0 - arcMinToScreen*fovWidth/2.0, map->height()/2.0 - arcMinToScreen*fovHeight/2.0,
                                    arcMinToScreen*fovWidth, arcMinToScreen*fovHeight ) );

    SkyQPainter painter(KStars::Instance(), &svgGenerator);
    painter.begin();

    map->exportSkyImage(&painter);

    painter.end();

    // Render SVG file on raster QImage canvas
    QSvgRenderer svgRenderer( myTempSvgFile.fileName() );
    QImage *mySkyChart = new QImage( arcMinToScreen * fovWidth * 2.0, arcMinToScreen * fovHeight * 2.0, QImage::Format_ARGB32 ); // 2 times bigger in both dimensions.
    QPainter p2( mySkyChart );
    svgRenderer.render( &p2 );
    p2.end();
    *skyChart = *mySkyChart;
    delete mySkyChart;

    myTempSvgFile.close();


    // Reset the sky-map
    map->setZoomFactor( oldZoomFactor );
    map->setClickedPoint( oldFocus );
    map->slotCenter();
    qApp->processEvents();

    // Repeat -- dirty workaround for some problem in KStars
    map->setZoomFactor( oldZoomFactor );
    map->setClickedPoint( oldFocus );
    map->slotCenter();
    qApp->processEvents();
    map->forceUpdate();

    // Prepare the sky image
    if( QFile::exists( imagePath ) && skyImage ) {

        QImage *mySkyImage = 0;
        mySkyImage = new QImage( int(arcMinToScreen * fovWidth * 2.0), int(arcMinToScreen * fovHeight * 2.0), QImage::Format_ARGB32 );
        mySkyImage->fill( Qt::transparent );
        QPainter p( mySkyImage );
        QImage rawImg( imagePath );
        if( rawImg.isNull() ) {
            qWarning() << "Image constructed from " << imagePath << "is a null image! Are you sure you supplied an image file? Continuing nevertheless...";
        }

        QImage img = rawImg.scaled( arcMinToScreen * dssWidth * 2.0, arcMinToScreen * dssHeight * 2.0, Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
        const auto ksd = KStarsData::Instance();
        sp->updateCoordsNow( ksd->updateNum() );

        if( Options::useAltAz() ) {
            // Need to rotate the image so that up is towards zenith rather than north.
            sp->EquatorialToHorizontal( ksd->lst(), ksd->geo()->lat() );
            dms northBearing = findNorthAngle( sp, ksd->geo()->lat() );
            qDebug() << "North angle = " << northBearing.toDMSString();

            QTransform transform;
            transform.rotate( northBearing.Degrees() );
            img = img.transformed( transform, Qt::SmoothTransformation );
        }
        p.drawImage( QPointF( mySkyImage->width()/2.0 - img.width()/2.0, mySkyImage->height()/2.0 - img.height()/2.0 ), img );
        p.end();

        *skyImage = *mySkyImage;
        delete mySkyImage;
    }
}

void EyepieceField::renderEyepieceView( const QImage *skyChart, QPixmap *renderChart, const double rotation, const double scale, const bool flip, const bool invert,
                                        const QImage *skyImage, QPixmap *renderImage, const bool overlay, const bool invertColors ) {
    QTransform transform;
    bool deleteRenderImage = false;
    transform.rotate( rotation );
    if( flip )
        transform.scale( -1, 1 );
    if( invert )
        transform.scale( -1, -1 );
    transform.scale( scale, scale );

    Q_ASSERT( skyChart && renderChart );
    if( !skyChart || !renderChart )
        return;

    *renderChart = QPixmap::fromImage( skyChart->transformed( transform, Qt::SmoothTransformation ) );

    if( skyImage ) {
        Q_ASSERT( overlay || renderImage ); // in debug mode, check for calls that supply skyImage but not renderImage
    }
    if( overlay && !renderImage ) {
        renderImage = new QPixmap(); // temporary, used for rendering skymap before overlay is done.
        deleteRenderImage = true; // we created it, so we must delete it.
    }

    if( skyImage && renderImage ) {
        if( skyImage->isNull() )
            qWarning() << "Sky image supplied to renderEyepieceView() for rendering is a Null image!";
        QImage i;
        i = skyImage->transformed( transform, Qt::SmoothTransformation );
        if( invertColors )
            i.invertPixels();
        *renderImage = QPixmap::fromImage( i );
    }
    if( overlay && skyImage ) {
        QColor skyColor = KStarsData::Instance()->colorScheme()->colorNamed( "SkyColor" );
        QBitmap mask = QBitmap::fromImage( skyChart->createMaskFromColor( skyColor.rgb() ).transformed( transform, Qt::SmoothTransformation ) );
        renderChart->setMask( mask );
        QPainter p( renderImage );
        p.drawImage( QPointF( renderImage->width()/2.0 - renderChart->width()/2.0, renderImage->height()/2.0 - renderChart->height()/2.0 ),
                     renderChart->toImage() );
        QPixmap temp( renderImage->width(), renderImage->height() );
        temp.fill( skyColor );
        QPainter p2( &temp );
        p2.drawImage( QPointF(0,0), renderImage->toImage() );
        p2.end();
        p.end();
        *renderChart = *renderImage = temp;
    }
    if( deleteRenderImage )
        delete renderImage;

}

void EyepieceField::renderEyepieceView( SkyPoint *sp, QPixmap *renderChart, double fovWidth, double fovHeight, const double rotation, const double scale,
                                        const bool flip, const bool invert, const QString &imagePath, QPixmap *renderImage, const bool overlay, const bool invertColors) {
    QImage *skyChart, *skyImage = 0;
    skyChart = new QImage();
    if( QFile::exists( imagePath ) && ( renderImage || overlay ) )
        skyImage = new QImage();
    generateEyepieceView( sp, skyChart, skyImage, fovWidth, fovHeight, imagePath );
    renderEyepieceView( skyChart, renderChart, rotation, scale, flip, invert,
                        skyImage, renderImage, overlay, invertColors );
    delete skyChart;
    delete skyImage;
}


void EyepieceField::render() {

    double rotation = m_rotationSlider->value();
    bool flip = m_flipView->isChecked();
    bool invert = m_invertView->isChecked();
    bool invertColors = m_invertColors->isChecked();
    bool overlay = m_overlay->isChecked() && m_skyImage;

    Q_ASSERT( m_skyChart );

    renderEyepieceView( m_skyChart, &m_renderChart, rotation, 1.0, flip, invert,
                        m_skyImage, &m_renderImage, overlay, invertColors );

    m_skyChartDisplay->setVisible( !overlay );
    if( m_skyImage ) {
        m_skyImageDisplay->setVisible( true );
        m_overlay->setVisible( true );
        m_invertColors->setVisible( true );
        m_getDSS->setVisible( false );
    }
    else {
        m_skyImageDisplay->setVisible( false );
        m_overlay->setVisible( false );
        m_invertColors->setVisible( false );
        m_getDSS->setVisible( true );
    }


    if( !overlay )
        m_skyChartDisplay->setPixmap( m_renderChart.scaled( m_skyChartDisplay->width(), m_skyChartDisplay->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation ) );
    if( m_skyImage )
        m_skyImageDisplay->setPixmap( m_renderImage.scaled( m_skyImageDisplay->width(), m_skyImageDisplay->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation ) );

    update();
    show();
}

void EyepieceField::slotDownloadDss() {
    double fovWidth, fovHeight;
    if( m_fovWidth == 0 && m_currentFOV == 0 ) {
        fovWidth = fovHeight = 15.0;
    }
    else if( m_currentFOV ) {
        fovWidth = m_currentFOV->sizeX();
        fovHeight = m_currentFOV->sizeY();
    }
    if( !m_dler ) {
        m_dler = new KSDssDownloader( this );
        connect( m_dler, SIGNAL ( downloadComplete( bool ) ), SLOT ( slotDssDownloaded( bool ) ) );
    }
    KSDssImage::Metadata md;
    m_tempFile.open();
    QUrl srcUrl = QUrl( KSDssDownloader::getDSSURL( m_sp, fovWidth, fovHeight, "all", &md ) );
    m_dler->startSingleDownload( srcUrl, m_tempFile.fileName(), md );
    m_tempFile.close();
}

void EyepieceField::slotDssDownloaded( bool success ) {
    if( !success ) {
        KMessageBox::sorry(0, i18n( "Failed to download DSS/SDSS image!" ) );
        return;
    }
    else
        showEyepieceField( m_sp, m_fovWidth, m_fovHeight, m_tempFile.fileName() );
}

EyepieceField::~EyepieceField() {
    // Empty
    delete m_skyChart;
    delete m_skyImage;
}

void EyepieceField::slotExport() {
    bool overlay = m_overlay->isChecked() && m_skyImage;
    new ExportEyepieceView( m_sp, *m_dt, ((m_skyImage && !overlay) ? &m_renderImage : 0), &m_renderChart, this );
}

dms EyepieceField::findNorthAngle( const SkyPoint *sp, const dms *lat ) {
    Q_ASSERT( sp && lat );

    // NOTE: northAngle1 is the correction due to lunisolar precession
    // (needs testing and checking). northAngle2 is the correction due
    // to going from equatorial to horizontal coordinates.

    // FIXME: The following code is a guess at how to handle
    // precession. While it might work in many cases, it might fail in
    // some. Careful testing will be needed to ensure that all
    // conditions are met, esp. with getting the signs right when
    // using arccosine! Nutation and planetary precession corrections
    // have not been included. -- asimha
    // TODO: Look at the Meeus book and see if it has some formulas -- asimha
    const double equinoxPrecessionPerYear = (50.35 / 3600.0); // Equinox precession in ecliptic longitude per year in degrees (ref: http://star-www.st-and.ac.uk/~fv/webnotes/chapt16.htm)
    dms netEquinoxPrecession( ( (sp->getLastPrecessJD() - J2000)/365.25 ) * equinoxPrecessionPerYear );
    double cosNorthAngle1 = ( netEquinoxPrecession.cos() - sp->dec0().sin() * sp->dec().sin() ) / ( sp->dec0().cos() * sp->dec().cos() );
    double northAngle1 = acos( cosNorthAngle1 );
    if( sp->getLastPrecessJD() < J2000 )
        northAngle1 = -northAngle1;
    if( sp->dec0().Degrees() < 0 )
        northAngle1 = -northAngle1;
    // We trust that EquatorialToHorizontal has been called on sp, after all, how else can it have an alt/az representation.
    // Use spherical cosine rule (the triangle with vertices at sp, zenith and NCP) to compute the angle between direction of increasing altitude and north
    double cosNorthAngle2 = ( lat->sin() - sp->alt().sin() * sp->dec().sin() )/( sp->alt().cos() * sp->dec().cos() );
    double northAngle2 = acos( cosNorthAngle2 ); // arccosine is blind to sign of the angle
    if( sp->az().reduce().Degrees() < 180.0 ) // if on the eastern hemisphere, flip sign
        northAngle2 = -northAngle2;
    double northAngle = northAngle1 + northAngle2;
    qDebug() << "Data: alt = " << sp->alt().toDMSString() << "; az = " << sp->az().toDMSString() << "; ra, dec (" << sp->getLastPrecessJD()/365.25 << ") = " << sp->ra().toHMSString() <<
        "," << sp->dec().toDMSString() << "; ra0,dec0 (J2000.0) = " << sp->ra0().toHMSString() << "," << sp->dec0().toDMSString();
    qDebug() << "PA corrections: precession cosine = " << cosNorthAngle1 << "; angle = " << northAngle1 << "; horizontal = " << cosNorthAngle2 << "; angle = " << northAngle2;
    return dms( northAngle * 180/M_PI );
}
